Skip to content

Commit 5c0f736

Browse files
committed
fix: add web-UI API types and additional testing
1 parent faa3d6f commit 5c0f736

11 files changed

Lines changed: 412 additions & 20 deletions

File tree

src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1871,7 +1871,7 @@ dependencies = [
18711871
{{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
18721872
{{/if}}"a2a-sdk[all] >= 0.2.0, < 1.0.0",
18731873
"aws-opentelemetry-distro",
1874-
"bedrock-agentcore[a2a] >= 1.0.3",
1874+
"bedrock-agentcore[a2a] >= 1.9.1",
18751875
"botocore[crt] >= 1.35.0",
18761876
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
18771877
{{/if}}{{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
@@ -2708,7 +2708,7 @@ dependencies = [
27082708
{{/if}}"ag-ui-strands >= 0.1.7",
27092709
"ag-ui-protocol >= 0.1.10",
27102710
"aws-opentelemetry-distro",
2711-
"bedrock-agentcore >= 1.0.3",
2711+
"bedrock-agentcore >= 1.9.1",
27122712
"botocore[crt] >= 1.35.0",
27132713
"fastapi >= 0.115.12",
27142714
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
@@ -5089,7 +5089,7 @@ requires-python = ">=3.10"
50895089
dependencies = [
50905090
{{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
50915091
{{/if}}"aws-opentelemetry-distro",
5092-
"bedrock-agentcore >= 1.0.3",
5092+
"bedrock-agentcore >= 1.9.1",
50935093
"botocore[crt] >= 1.35.0",
50945094
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
50955095
{{/if}}"mcp >= 1.19.0",

src/assets/python/a2a/strands/base/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dependencies = [
1212
{{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
1313
{{/if}}"a2a-sdk[all] >= 0.2.0, < 1.0.0",
1414
"aws-opentelemetry-distro",
15-
"bedrock-agentcore[a2a] >= 1.0.3",
15+
"bedrock-agentcore[a2a] >= 1.9.1",
1616
"botocore[crt] >= 1.35.0",
1717
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
1818
{{/if}}{{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",

src/assets/python/agui/strands/base/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ dependencies = [
1313
{{/if}}"ag-ui-strands >= 0.1.7",
1414
"ag-ui-protocol >= 0.1.10",
1515
"aws-opentelemetry-distro",
16-
"bedrock-agentcore >= 1.0.3",
16+
"bedrock-agentcore >= 1.9.1",
1717
"botocore[crt] >= 1.35.0",
1818
"fastapi >= 0.115.12",
1919
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",

src/assets/python/http/strands/base/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ requires-python = ">=3.10"
1111
dependencies = [
1212
{{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
1313
{{/if}}"aws-opentelemetry-distro",
14-
"bedrock-agentcore >= 1.0.3",
14+
"bedrock-agentcore >= 1.9.1",
1515
"botocore[crt] >= 1.35.0",
1616
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
1717
{{/if}}"mcp >= 1.19.0",

src/cli/operations/agent/import/pyproject-generator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { ImportedFeatures } from './base-translator';
66

77
const BASE_DEPS = [
88
'aws-opentelemetry-distro',
9-
'bedrock-agentcore >= 1.0.3',
9+
'bedrock-agentcore >= 1.9.1',
1010
'botocore[crt] >= 1.35.0',
1111
'boto3>=1.38.0',
1212
];
@@ -22,7 +22,7 @@ const LANGGRAPH_DEPS = [
2222
'tiktoken==0.11.0',
2323
];
2424

25-
const MEMORY_DEPS = ['bedrock-agentcore[memory] >= 1.0.3'];
25+
const MEMORY_DEPS = ['bedrock-agentcore[memory] >= 1.9.1'];
2626

2727
export function generatePyprojectToml(agentName: string, framework: SDKFramework, features: ImportedFeatures): string {
2828
const deps = [...BASE_DEPS];

src/cli/operations/dev/web-ui/README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,13 @@ Response:
209209
{ "success": true, "spans": [...] }
210210
```
211211

212-
### `GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]`
212+
### `GET /api/memory?memoryName=xxx&(namespace=yyy|namespacePath=yyy)[&strategyId=zzz]`
213213

214-
Lists memory records for a given memory and namespace. Requires a deployed memory with `onListMemoryRecords` handler.
214+
Lists memory records for a given memory, filtered by either an exact namespace or a namespace path prefix. Requires a
215+
deployed memory with `onListMemoryRecords` handler.
216+
217+
Exactly one of `namespace` (exact match) or `namespacePath` (hierarchical path prefix) must be provided. Supplying both
218+
returns HTTP 400.
215219

216220
Response:
217221

@@ -223,10 +227,18 @@ Response:
223227

224228
Performs semantic search across memory records. Requires a deployed memory with `onRetrieveMemoryRecords` handler.
225229

230+
Exactly one of `namespace` or `namespacePath` must be provided in the request body.
231+
226232
Request:
227233

228234
```json
229-
{ "memoryName": "MyMemory", "namespace": "/users/123/facts", "searchQuery": "preferences", "strategyId": "optional" }
235+
{ "memoryName": "MyMemory", "namespacePath": "/users/123/", "searchQuery": "preferences", "strategyId": "optional" }
236+
```
237+
238+
Or with exact-match semantics:
239+
240+
```json
241+
{ "memoryName": "MyMemory", "namespace": "/users/123/facts", "searchQuery": "preferences" }
230242
```
231243

232244
Response:

src/cli/operations/dev/web-ui/__tests__/memory.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,9 @@ describe('handleRetrieveMemoryRecords', () => {
188188
await handleRetrieveMemoryRecords(ctx, req, res);
189189

190190
expect(res._status).toBe(400);
191-
expect(JSON.parse(res._body).error).toContain('mutually exclusive');
191+
const err = JSON.parse(res._body).error;
192+
expect(err).toContain('mutually exclusive');
193+
expect(err).toContain('request fields');
192194
expect(onRetrieveMemoryRecords).not.toHaveBeenCalled();
193195
});
194196

@@ -201,7 +203,9 @@ describe('handleRetrieveMemoryRecords', () => {
201203
await handleRetrieveMemoryRecords(ctx, req, res);
202204

203205
expect(res._status).toBe(400);
204-
expect(JSON.parse(res._body).error).toContain("either 'namespace' or 'namespacePath'");
206+
const err = JSON.parse(res._body).error;
207+
expect(err).toContain("either 'namespace' or 'namespacePath'");
208+
expect(err).toContain('request field');
205209
expect(onRetrieveMemoryRecords).not.toHaveBeenCalled();
206210
});
207211

src/cli/operations/dev/web-ui/api-types.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,18 @@ export interface GetCloudWatchTraceResponse {
314314
export type { CloudWatchTraceRecord, CloudWatchSpanRecord } from '../../traces/types';
315315

316316
// ---------------------------------------------------------------------------
317-
// GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz]
317+
// GET /api/memory?memoryName=xxx&(namespace=yyy|namespacePath=yyy)[&strategyId=zzz]
318318
// ---------------------------------------------------------------------------
319319

320+
/**
321+
* Query parameters for GET /api/memory. Exactly one of `namespace` (exact match) or
322+
* `namespacePath` (hierarchical path prefix) must be provided.
323+
*/
324+
export type ListMemoryRecordsQuery = {
325+
memoryName: string;
326+
strategyId?: string;
327+
} & ({ namespace: string; namespacePath?: never } | { namespace?: never; namespacePath: string });
328+
320329
/** Response shape for GET /api/memory */
321330
export interface ListMemoryRecordsResponse {
322331
success: boolean;
@@ -340,13 +349,15 @@ export interface MemoryRecordResponse {
340349
// POST /api/memory/search
341350
// ---------------------------------------------------------------------------
342351

343-
/** Request body for POST /api/memory/search */
344-
export interface RetrieveMemoryRecordsRequest {
352+
/**
353+
* Request body for POST /api/memory/search. Exactly one of `namespace` (exact match) or
354+
* `namespacePath` (hierarchical path prefix) must be provided.
355+
*/
356+
export type RetrieveMemoryRecordsRequest = {
345357
memoryName: string;
346-
namespace: string;
347358
searchQuery: string;
348359
strategyId?: string;
349-
}
360+
} & ({ namespace: string; namespacePath?: never } | { namespace?: never; namespacePath: string });
350361

351362
/** Response shape for POST /api/memory/search */
352363
export interface RetrieveMemoryRecordsResponse {

src/cli/operations/dev/web-ui/handlers/memory.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,24 @@ export async function handleRetrieveMemoryRecords(
130130
if (namespace && namespacePath) {
131131
ctx.setCorsHeaders(res, origin);
132132
res.writeHead(400, { 'Content-Type': 'application/json' });
133-
res.end(JSON.stringify({ success: false, error: "'namespace' and 'namespacePath' are mutually exclusive" }));
133+
res.end(
134+
JSON.stringify({
135+
success: false,
136+
error: "'namespace' and 'namespacePath' request fields are mutually exclusive",
137+
})
138+
);
134139
return;
135140
}
136141

137142
if (!namespace && !namespacePath) {
138143
ctx.setCorsHeaders(res, origin);
139144
res.writeHead(400, { 'Content-Type': 'application/json' });
140-
res.end(JSON.stringify({ success: false, error: "either 'namespace' or 'namespacePath' is required" }));
145+
res.end(
146+
JSON.stringify({
147+
success: false,
148+
error: "either 'namespace' or 'namespacePath' request field is required",
149+
})
150+
);
141151
return;
142152
}
143153

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { type ListMemoryRecordsOptions, listMemoryRecords } from '../list-memory-records';
2+
import { afterEach, describe, expect, it, vi } from 'vitest';
3+
4+
const { mockSend, capturedInput } = vi.hoisted(() => {
5+
const captured: { value: unknown } = { value: null };
6+
return {
7+
mockSend: vi.fn(),
8+
capturedInput: captured,
9+
};
10+
});
11+
12+
vi.mock('@aws-sdk/client-bedrock-agentcore', () => ({
13+
BedrockAgentCoreClient: class {
14+
send = mockSend;
15+
},
16+
ListMemoryRecordsCommand: class {
17+
constructor(public input: unknown) {
18+
capturedInput.value = input;
19+
}
20+
},
21+
}));
22+
23+
vi.mock('../../../aws', () => ({
24+
getCredentialProvider: vi.fn().mockReturnValue({}),
25+
}));
26+
27+
describe('listMemoryRecords', () => {
28+
afterEach(() => {
29+
vi.clearAllMocks();
30+
capturedInput.value = null;
31+
});
32+
33+
it('rejects when both namespace and namespacePath are provided', async () => {
34+
// Bypassing the discriminated union to simulate a caller from JS or the web UI.
35+
const options = {
36+
region: 'us-west-2',
37+
memoryId: 'mem-1',
38+
namespace: '/a/',
39+
namespacePath: '/b/',
40+
} as unknown as ListMemoryRecordsOptions;
41+
42+
const result = await listMemoryRecords(options);
43+
44+
expect(result.success).toBe(false);
45+
expect((result as { error: Error }).error.message).toContain('mutually exclusive');
46+
expect(mockSend).not.toHaveBeenCalled();
47+
});
48+
49+
it('rejects when neither namespace nor namespacePath is provided', async () => {
50+
const options = { region: 'us-west-2', memoryId: 'mem-1' } as unknown as ListMemoryRecordsOptions;
51+
52+
const result = await listMemoryRecords(options);
53+
54+
expect(result.success).toBe(false);
55+
expect((result as { error: Error }).error.message).toContain('Either');
56+
expect(mockSend).not.toHaveBeenCalled();
57+
});
58+
59+
it('rejects when namespace is an empty string (treated as not provided)', async () => {
60+
const options = { region: 'us-west-2', memoryId: 'mem-1', namespace: '' } as unknown as ListMemoryRecordsOptions;
61+
62+
const result = await listMemoryRecords(options);
63+
64+
expect(result.success).toBe(false);
65+
expect((result as { error: Error }).error.message).toContain('Either');
66+
expect(mockSend).not.toHaveBeenCalled();
67+
});
68+
69+
it('forwards namespace to the SDK when only namespace is provided', async () => {
70+
mockSend.mockResolvedValueOnce({ memoryRecordSummaries: [], nextToken: undefined });
71+
72+
const result = await listMemoryRecords({
73+
region: 'us-west-2',
74+
memoryId: 'mem-1',
75+
namespace: '/users/42/facts',
76+
});
77+
78+
expect(result.success).toBe(true);
79+
expect(capturedInput.value).toMatchObject({
80+
memoryId: 'mem-1',
81+
namespace: '/users/42/facts',
82+
maxResults: 50,
83+
});
84+
expect((capturedInput.value as { namespacePath?: string }).namespacePath).toBeUndefined();
85+
});
86+
87+
it('forwards namespacePath to the SDK when only namespacePath is provided', async () => {
88+
mockSend.mockResolvedValueOnce({ memoryRecordSummaries: [], nextToken: undefined });
89+
90+
const result = await listMemoryRecords({
91+
region: 'us-west-2',
92+
memoryId: 'mem-1',
93+
namespacePath: '/users/42/',
94+
});
95+
96+
expect(result.success).toBe(true);
97+
expect(capturedInput.value).toMatchObject({
98+
memoryId: 'mem-1',
99+
namespacePath: '/users/42/',
100+
});
101+
expect((capturedInput.value as { namespace?: string }).namespace).toBeUndefined();
102+
});
103+
104+
it('parses memory record summaries into the result shape', async () => {
105+
const createdAt = new Date('2026-05-13T00:00:00Z');
106+
mockSend.mockResolvedValueOnce({
107+
memoryRecordSummaries: [
108+
{
109+
memoryRecordId: 'rec-1',
110+
content: { text: 'hello' },
111+
memoryStrategyId: 'strat-1',
112+
namespaces: ['/users/42/facts'],
113+
createdAt,
114+
score: 0.87,
115+
metadata: { source: { stringValue: 'chat' } },
116+
},
117+
],
118+
nextToken: 'next',
119+
});
120+
121+
const result = await listMemoryRecords({
122+
region: 'us-west-2',
123+
memoryId: 'mem-1',
124+
namespace: '/users/42/facts',
125+
});
126+
127+
expect(result.success).toBe(true);
128+
if (!result.success) return;
129+
expect(result.nextToken).toBe('next');
130+
expect(result.records).toEqual([
131+
{
132+
memoryRecordId: 'rec-1',
133+
content: 'hello',
134+
memoryStrategyId: 'strat-1',
135+
namespaces: ['/users/42/facts'],
136+
createdAt: createdAt.toISOString(),
137+
score: 0.87,
138+
metadata: { source: 'chat' },
139+
},
140+
]);
141+
});
142+
143+
it('maps ResourceNotFoundException to a user-friendly error', async () => {
144+
const err = new Error('not found');
145+
err.name = 'ResourceNotFoundException';
146+
mockSend.mockRejectedValueOnce(err);
147+
148+
const result = await listMemoryRecords({
149+
region: 'us-west-2',
150+
memoryId: 'missing',
151+
namespace: '/a/',
152+
});
153+
154+
expect(result.success).toBe(false);
155+
expect((result as { error: Error }).error.message).toContain("Memory 'missing' not found");
156+
});
157+
158+
it('returns the SDK error message for other failures', async () => {
159+
mockSend.mockRejectedValueOnce(new Error('network down'));
160+
161+
const result = await listMemoryRecords({
162+
region: 'us-west-2',
163+
memoryId: 'mem-1',
164+
namespace: '/a/',
165+
});
166+
167+
expect(result.success).toBe(false);
168+
expect((result as { error: Error }).error.message).toBe('network down');
169+
});
170+
});

0 commit comments

Comments
 (0)