Skip to content

Commit b58fd6e

Browse files
authored
Merge branch 'main' into feat/linear-processor-feedback
2 parents be93ef3 + 3b0fc19 commit b58fd6e

8 files changed

Lines changed: 526 additions & 130 deletions

File tree

.github/workflows/build.yml

Lines changed: 109 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.gitignore

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@aws-cdk/aws-bedrock-agentcore-alpha": "2.238.0-alpha.0",
1717
"@aws-cdk/aws-bedrock-alpha": "2.238.0-alpha.0",
1818
"@aws-cdk/mixins-preview": "2.238.0-alpha.0",
19-
"@aws-sdk/client-bedrock-agentcore": "^3.1021.0",
19+
"@aws-sdk/client-bedrock-agentcore": "^3.1046.0",
2020
"@aws-sdk/client-bedrock-runtime": "^3.1021.0",
2121
"@aws-sdk/client-ecs": "^3.1021.0",
2222
"@aws-sdk/client-dynamodb": "^3.1021.0",

cdk/src/handlers/shared/memory.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ function processMemoryRecords(
116116
records: MemoryRecordSummary[],
117117
out: string[],
118118
repo: string,
119-
namespace: string,
119+
namespacePath: string,
120120
recordType: string,
121121
): void {
122122
for (const record of records) {
@@ -129,7 +129,7 @@ function processMemoryRecords(
129129
// CloudWatch alarms can detect spikes (genuine tampering or write bugs).
130130
logger.warn('Memory record hash mismatch (expected for extracted records)', {
131131
repo,
132-
namespace,
132+
namespace_path: namespacePath,
133133
record_type: recordType,
134134
expected_hash: record.metadata?.content_sha256?.stringValue ?? '(none)',
135135
actual_hash: hashContent(sanitized),
@@ -164,7 +164,10 @@ function getClient(): BedrockAgentCoreClient {
164164
*
165165
* Namespaces match the templates configured on the extraction strategies:
166166
* - Semantic: `/{actorId}/knowledge/` (actorId = repo)
167-
* - Episodic: `/{actorId}/episodes/` (prefix matches all sessions)
167+
* - Episodic: `/{actorId}/episodes/` (covers all sessions and reflections)
168+
*
169+
* Both calls use `namespacePath` for hierarchical retrieval — episodic per-task
170+
* records live at `/{actorId}/episodes/{sessionId}/`, which is below the read path.
168171
*
169172
* Results are trimmed to a 2000-token budget (knowledge is prioritized before episodes;
170173
* entries beyond the budget are dropped).
@@ -183,14 +186,11 @@ export async function loadMemoryContext(
183186
try {
184187
const client = getClient();
185188

186-
// Namespaces derived from the strategy templates configured in agent-memory.ts:
187-
// Semantic: /{actorId}/knowledge/
188-
// Episodic: /{actorId}/episodes/{sessionId}/
189-
// Events are written with actorId = repo (e.g. "krokoko/agent-plugins"),
190-
// so extracted records land at /{repo}/knowledge/ and /{repo}/episodes/{taskId}/.
191-
// Reads use these paths as namespace prefixes.
192-
const semanticNamespace = `/${repo}/knowledge/`;
193-
const episodicNamespace = `/${repo}/episodes/`;
189+
// Namespace paths derived from the strategy templates configured in agent-memory.ts.
190+
// Events are written with actorId = repo, so extracted records land at
191+
// /{repo}/knowledge/ and /{repo}/episodes/{taskId}/.
192+
const semanticNamespacePath = `/${repo}/knowledge/`;
193+
const episodicNamespacePath = `/${repo}/episodes/`;
194194

195195
// Run semantic and episodic searches in parallel
196196
// eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism
@@ -199,7 +199,7 @@ export async function loadMemoryContext(
199199
taskDescription
200200
? client.send(new RetrieveMemoryRecordsCommand({
201201
memoryId,
202-
namespace: semanticNamespace,
202+
namespacePath: semanticNamespacePath,
203203
searchCriteria: {
204204
searchQuery: taskDescription,
205205
topK: 5,
@@ -212,7 +212,7 @@ export async function loadMemoryContext(
212212
// Episodic search — recent task episodes (prefix matches all sessions)
213213
client.send(new RetrieveMemoryRecordsCommand({
214214
memoryId,
215-
namespace: episodicNamespace,
215+
namespacePath: episodicNamespacePath,
216216
searchCriteria: {
217217
searchQuery: 'recent task episodes',
218218
topK: 3,
@@ -227,11 +227,11 @@ export async function loadMemoryContext(
227227
const pastEpisodes: string[] = [];
228228

229229
if (semanticResult?.memoryRecordSummaries) {
230-
processMemoryRecords(semanticResult.memoryRecordSummaries, repoKnowledge, repo, semanticNamespace, 'repo_knowledge');
230+
processMemoryRecords(semanticResult.memoryRecordSummaries, repoKnowledge, repo, semanticNamespacePath, 'repo_knowledge');
231231
}
232232

233233
if (episodicResult?.memoryRecordSummaries) {
234-
processMemoryRecords(episodicResult.memoryRecordSummaries, pastEpisodes, repo, episodicNamespace, 'past_episode');
234+
processMemoryRecords(episodicResult.memoryRecordSummaries, pastEpisodes, repo, episodicNamespacePath, 'past_episode');
235235
}
236236

237237
if (repoKnowledge.length === 0 && pastEpisodes.length === 0) {

cdk/test/handlers/shared/memory.test.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,32 +69,42 @@ describe('loadMemoryContext', () => {
6969
expect(result!.repo_knowledge[0]).toContain('Jest');
7070
});
7171

72-
test('uses repo-based namespaces for queries', async () => {
72+
test('uses namespacePath (hierarchical retrieval) for both queries', async () => {
7373
const { RetrieveMemoryRecordsCommand } = jest.requireMock('@aws-sdk/client-bedrock-agentcore');
7474
mockAgentCoreSend
7575
.mockResolvedValueOnce({ memoryRecordSummaries: [] })
7676
.mockResolvedValueOnce({ memoryRecordSummaries: [] });
7777

7878
await loadMemoryContext('mem-123', 'owner/repo', 'Fix the build');
7979

80-
// Semantic search uses /{repo}/knowledge/ namespace
80+
// Semantic search uses /{repo}/knowledge/ as namespacePath. The legacy
81+
// `namespace` field switched from prefix-match to exact-match in the
82+
// AgentCore Memory API; namespacePath preserves the hierarchical (prefix)
83+
// semantics this code depends on for episodic per-task records nested
84+
// under /{repo}/episodes/{sessionId}/.
8185
expect(RetrieveMemoryRecordsCommand).toHaveBeenCalledWith(
8286
expect.objectContaining({
83-
namespace: '/owner/repo/knowledge/',
87+
namespacePath: '/owner/repo/knowledge/',
8488
searchCriteria: expect.objectContaining({
8589
searchQuery: 'Fix the build',
8690
}),
8791
}),
8892
);
89-
// Episodic search uses /{repo}/episodes/ namespace prefix
93+
// Episodic search uses /{repo}/episodes/ namespacePath to scoop up records
94+
// under all task sessions plus the cross-task reflection records.
9095
expect(RetrieveMemoryRecordsCommand).toHaveBeenCalledWith(
9196
expect.objectContaining({
92-
namespace: '/owner/repo/episodes/',
97+
namespacePath: '/owner/repo/episodes/',
9398
searchCriteria: expect.objectContaining({
9499
searchQuery: 'recent task episodes',
95100
}),
96101
}),
97102
);
103+
// Confirm the legacy `namespace` field is NOT being passed — we don't
104+
// want to send both fields (the API rejects that) or the wrong one.
105+
expect(RetrieveMemoryRecordsCommand).not.toHaveBeenCalledWith(
106+
expect.objectContaining({ namespace: expect.anything() }),
107+
);
98108
});
99109

100110
test('returns undefined when no results are found', async () => {
@@ -203,7 +213,7 @@ describe('loadMemoryContext', () => {
203213
expect.stringContaining('hash mismatch'),
204214
expect.objectContaining({
205215
repo: 'owner/repo',
206-
namespace: '/owner/repo/knowledge/',
216+
namespace_path: '/owner/repo/knowledge/',
207217
record_type: 'repo_knowledge',
208218
expected_hash: wrongHash,
209219
source_type: 'agent_learning',
@@ -235,7 +245,7 @@ describe('loadMemoryContext', () => {
235245
expect.stringContaining('hash mismatch'),
236246
expect.objectContaining({
237247
repo: 'owner/repo',
238-
namespace: '/owner/repo/episodes/',
248+
namespace_path: '/owner/repo/episodes/',
239249
record_type: 'past_episode',
240250
expected_hash: wrongHash,
241251
source_type: 'agent_episode',

docs/design/MEMORY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Per-user preferences extracted from task descriptions (explicit) and review patt
117117
| Component | Strategy | Namespace | Read | Write |
118118
|---|---|---|---|---|
119119
| Repo knowledge | Semantic (`SemanticKnowledge`) | `/{actorId}/knowledge/` | Task start | Task end |
120-
| Task episodes | Episodic (`TaskEpisodes`) | `/{actorId}/episodes/{sessionId}/` | Task start (prefix match) | Task end |
120+
| Task episodes | Episodic (`TaskEpisodes`) | `/{actorId}/episodes/{sessionId}/` | Task start | Task end |
121121
| Review feedback | Custom (planned) | `/{actorId}/review-rules/` | Task start | PR review webhook |
122122
| User preferences | User preference (planned) | `users/{username}` | Task start | Extracted from patterns |
123123
| Self-feedback | Semantic (`SemanticKnowledge`) | `/{actorId}/knowledge/` | Task start | Task end |
@@ -126,6 +126,7 @@ Namespace conventions:
126126
- `{actorId}` and `{sessionId}` are the only valid AgentCore template variables. Templates are set on extraction strategies at resource creation.
127127
- `actorId = "owner/repo"` for all writes. `sessionId = taskId` for episodic partitioning.
128128
- Changing namespace templates requires recreating the Memory resource (breaking infrastructure change).
129+
- Reads use the `namespacePath` field on [`RetrieveMemoryRecords`](https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_RetrieveMemoryRecords.html) for hierarchical retrieval — episodic records live one level below the parent path so a hierarchical query is required to surface them.
129130

130131
## Memory consolidation
131132

docs/src/content/docs/architecture/Memory.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Per-user preferences extracted from task descriptions (explicit) and review patt
121121
| Component | Strategy | Namespace | Read | Write |
122122
|---|---|---|---|---|
123123
| Repo knowledge | Semantic (`SemanticKnowledge`) | `/{actorId}/knowledge/` | Task start | Task end |
124-
| Task episodes | Episodic (`TaskEpisodes`) | `/{actorId}/episodes/{sessionId}/` | Task start (prefix match) | Task end |
124+
| Task episodes | Episodic (`TaskEpisodes`) | `/{actorId}/episodes/{sessionId}/` | Task start | Task end |
125125
| Review feedback | Custom (planned) | `/{actorId}/review-rules/` | Task start | PR review webhook |
126126
| User preferences | User preference (planned) | `users/{username}` | Task start | Extracted from patterns |
127127
| Self-feedback | Semantic (`SemanticKnowledge`) | `/{actorId}/knowledge/` | Task start | Task end |
@@ -130,6 +130,7 @@ Namespace conventions:
130130
- `{actorId}` and `{sessionId}` are the only valid AgentCore template variables. Templates are set on extraction strategies at resource creation.
131131
- `actorId = "owner/repo"` for all writes. `sessionId = taskId` for episodic partitioning.
132132
- Changing namespace templates requires recreating the Memory resource (breaking infrastructure change).
133+
- Reads use the `namespacePath` field on [`RetrieveMemoryRecords`](https://docs.aws.amazon.com/bedrock-agentcore/latest/APIReference/API_RetrieveMemoryRecords.html) for hierarchical retrieval — episodic records live one level below the parent path so a hierarchical query is required to surface them.
133134

134135
## Memory consolidation
135136

0 commit comments

Comments
 (0)