1import * as AgentCore from "@/AWS/BedrockAgentCore";
2import * as Lambda from "@/AWS/Lambda";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Stream from "effect/Stream";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import path from "pathe";
10
11const main = path.resolve(import.meta.dirname, "handler.ts");
12
13export class AgentCoreTestFunction extends Lambda.Function<Lambda.Function>()(
14 "AgentCoreTestFunction",
15) {}
16
17export default AgentCoreTestFunction.make(
18 {
19 main,
20 functionUrl: true,
21 // code interpreter / browser session start + execution can take >3s
22 timeout: Duration.seconds(60),
23 },
24 Effect.gen(function* () {
25 const memory = yield* AgentCore.Memory("TestMemory", {
26 eventExpiryDuration: "7 days",
27 memoryStrategies: [
28 {
29 semanticMemoryStrategy: {
30 name: "facts",
31 namespaces: ["facts/{actorId}"],
32 },
33 },
34 ],
35 });
36 const codeInterpreter = yield* AgentCore.CodeInterpreter(
37 "TestInterpreter",
38 {},
39 );
40 const browser = yield* AgentCore.BrowserCustom("TestBrowser", {});
41
42 // memory: events
43 const createEvent = yield* AgentCore.CreateEvent(memory);
44 const getEvent = yield* AgentCore.GetEvent(memory);
45 const deleteEvent = yield* AgentCore.DeleteEvent(memory);
46 const listEvents = yield* AgentCore.ListEvents(memory);
47 const listSessions = yield* AgentCore.ListSessions(memory);
48 const listActors = yield* AgentCore.ListActors(memory);
49 // memory: long-term records
50 const listMemoryRecords = yield* AgentCore.ListMemoryRecords(memory);
51 const retrieveMemoryRecords =
52 yield* AgentCore.RetrieveMemoryRecords(memory);
53 const getMemoryRecord = yield* AgentCore.GetMemoryRecord(memory);
54 const deleteMemoryRecord = yield* AgentCore.DeleteMemoryRecord(memory);
55 const batchCreateMemoryRecords =
56 yield* AgentCore.BatchCreateMemoryRecords(memory);
57 const batchUpdateMemoryRecords =
58 yield* AgentCore.BatchUpdateMemoryRecords(memory);
59 const batchDeleteMemoryRecords =
60 yield* AgentCore.BatchDeleteMemoryRecords(memory);
61 // memory: extraction jobs
62 const startMemoryExtractionJob =
63 yield* AgentCore.StartMemoryExtractionJob(memory);
64 const listMemoryExtractionJobs =
65 yield* AgentCore.ListMemoryExtractionJobs(memory);
66 // code interpreter sessions
67 const startSession =
68 yield* AgentCore.StartCodeInterpreterSession(codeInterpreter);
69 const invokeCodeInterpreter =
70 yield* AgentCore.InvokeCodeInterpreter(codeInterpreter);
71 const getCodeInterpreterSession =
72 yield* AgentCore.GetCodeInterpreterSession(codeInterpreter);
73 const listCodeInterpreterSessions =
74 yield* AgentCore.ListCodeInterpreterSessions(codeInterpreter);
75 const stopSession =
76 yield* AgentCore.StopCodeInterpreterSession(codeInterpreter);
77 // browser sessions
78 const startBrowserSession = yield* AgentCore.StartBrowserSession(browser);
79 const getBrowserSession = yield* AgentCore.GetBrowserSession(browser);
80 const listBrowserSessions = yield* AgentCore.ListBrowserSessions(browser);
81 const invokeBrowser = yield* AgentCore.InvokeBrowser(browser);
82 const updateBrowserStream = yield* AgentCore.UpdateBrowserStream(browser);
83 const saveBrowserSessionProfile =
84 yield* AgentCore.SaveBrowserSessionProfile(browser);
85 const stopBrowserSession = yield* AgentCore.StopBrowserSession(browser);
86
87 // batchCreate -> get -> batchUpdate -> delete + batchDelete records.
88 const recordsRoundtrip = Effect.gen(function* () {
89 const namespace = "facts/batch-actor";
90 const created = yield* batchCreateMemoryRecords({
91 records: [
92 {
93 requestIdentifier: "rec-1",
94 namespaces: [namespace],
95 content: { text: "The user's favorite color is teal." },
96 timestamp: new Date(),
97 },
98 {
99 requestIdentifier: "rec-2",
100 namespaces: [namespace],
101 content: { text: "The user prefers window seats." },
102 timestamp: new Date(),
103 },
104 ],
105 });
106 const [first, second] = created.successfulRecords;
107 const fetched = yield* getMemoryRecord({
108 memoryRecordId: first.memoryRecordId,
109 });
110 const updated = yield* batchUpdateMemoryRecords({
111 records: [
112 {
113 memoryRecordId: first.memoryRecordId,
114 timestamp: new Date(),
115 content: { text: "The user's favorite color is green." },
116 namespaces: [namespace],
117 },
118 ],
119 });
120 yield* deleteMemoryRecord({
121 memoryRecordId: first.memoryRecordId,
122 });
123 const batchDeleted = yield* batchDeleteMemoryRecords({
124 records: [{ memoryRecordId: second.memoryRecordId }],
125 });
126 return yield* HttpServerResponse.json({
127 created: created.successfulRecords.length,
128 fetchedRecordId: fetched.memoryRecord.memoryRecordId,
129 updated: updated.successfulRecords.length,
130 batchDeleted: batchDeleted.successfulRecords.length,
131 });
132 });
133
134 return {
135 fetch: Effect.gen(function* () {
136 const request = yield* HttpServerRequest;
137 const url = new URL(request.originalUrl);
138 const pathname = url.pathname;
139
140 if (request.method === "GET" && pathname === "/health") {
141 return yield* HttpServerResponse.json({ ok: true });
142 }
143
144 if (request.method === "POST" && pathname === "/events") {
145 const body = (yield* request.json) as unknown as {
146 actorId: string;
147 sessionId: string;
148 text: string;
149 };
150 const result = yield* createEvent({
151 actorId: body.actorId,
152 sessionId: body.sessionId,
153 eventTimestamp: new Date(),
154 payload: [
155 {
156 conversational: {
157 role: "USER",
158 content: { text: body.text },
159 },
160 },
161 ],
162 });
163 return yield* HttpServerResponse.json({
164 eventId: result.event.eventId,
165 sessionId: result.event.sessionId,
166 });
167 }
168
169 if (request.method === "GET" && pathname === "/events") {
170 const result = yield* listEvents({
171 actorId: url.searchParams.get("actorId")!,
172 sessionId: url.searchParams.get("sessionId")!,
173 });
174 return yield* HttpServerResponse.json({
175 count: result.events.length,
176 });
177 }
178
179 // create -> get -> delete a single event, end to end.
180 if (request.method === "POST" && pathname === "/events/roundtrip") {
181 const actorId = "roundtrip-actor";
182 const sessionId = "roundtrip-session";
183 const created = yield* createEvent({
184 actorId,
185 sessionId,
186 eventTimestamp: new Date(),
187 payload: [
188 {
189 conversational: {
190 role: "USER",
191 content: { text: "roundtrip event" },
192 },
193 },
194 ],
195 });
196 const eventId = created.event.eventId;
197 const fetched = yield* getEvent({ actorId, sessionId, eventId });
198 yield* deleteEvent({ actorId, sessionId, eventId });
199 return yield* HttpServerResponse.json({
200 eventId,
201 fetchedEventId: fetched.event.eventId,
202 deleted: true,
203 });
204 }
205
206 if (request.method === "GET" && pathname === "/sessions") {
207 const result = yield* listSessions({
208 actorId: url.searchParams.get("actorId")!,
209 });
210 return yield* HttpServerResponse.json({
211 count: result.sessionSummaries.length,
212 });
213 }
214
215 if (request.method === "GET" && pathname === "/actors") {
216 const result = yield* listActors({});
217 return yield* HttpServerResponse.json({
218 count: result.actorSummaries.length,
219 });
220 }
221
222 if (request.method === "GET" && pathname === "/records") {
223 const result = yield* listMemoryRecords({
224 namespace: `facts/${url.searchParams.get("actorId")}`,
225 });
226 return yield* HttpServerResponse.json({
227 count: result.memoryRecordSummaries.length,
228 });
229 }
230
231 if (request.method === "GET" && pathname === "/retrieve") {
232 const result = yield* retrieveMemoryRecords({
233 namespace: `facts/${url.searchParams.get("actorId")}`,
234 searchCriteria: {
235 searchQuery: url.searchParams.get("query") ?? "anything",
236 topK: 3,
237 },
238 });
239 return yield* HttpServerResponse.json({
240 count: result.memoryRecordSummaries.length,
241 });
242 }
243
244 // Failures surface as a 500 with the typed tag/message in the body
245 // so the suite's transient-retry logging shows the real cause.
246 if (request.method === "POST" && pathname === "/records/roundtrip") {
247 return yield* recordsRoundtrip.pipe(
248 Effect.catch((error) =>
249 HttpServerResponse.json(
250 {
251 error: error._tag,
252 message: String(
253 (error as { message?: unknown }).message ?? error,
254 ),
255 },
256 { status: 500 },
257 ),
258 ),
259 );
260 }
261
262 if (request.method === "GET" && pathname === "/extraction/jobs") {
263 const result = yield* listMemoryExtractionJobs({});
264 return yield* HttpServerResponse.json({
265 count: result.jobs.length,
266 });
267 }
268
269 // Not driven by tests (re-runs an extraction job by id); exists so
270 // the binding's runtime callable is wired end-to-end.
271 if (request.method === "POST" && pathname === "/extraction/start") {
272 const result = yield* startMemoryExtractionJob({
273 extractionJob: { jobId: url.searchParams.get("jobId")! },
274 });
275 return yield* HttpServerResponse.json({ jobId: result.jobId });
276 }
277
278 if (request.method === "POST" && pathname === "/code/run") {
279 const session = yield* startSession({
280 sessionTimeout: "5 minutes",
281 });
282 const result = yield* invokeCodeInterpreter({
283 sessionId: session.sessionId,
284 name: "executeCode",
285 arguments: { language: "python", code: "print(21 * 2)" },
286 });
287 const chunks = yield* Stream.runCollect(result.stream);
288 const observed = yield* getCodeInterpreterSession({
289 sessionId: session.sessionId,
290 });
291 const sessions = yield* listCodeInterpreterSessions({});
292 yield* stopSession({ sessionId: session.sessionId });
293 return yield* HttpServerResponse.json({
294 sessionId: session.sessionId,
295 sessionStatus: observed.status,
296 sessionCount: sessions.items?.length ?? 0,
297 chunks: Array.from(chunks),
298 });
299 }
300
301 // start -> get -> list -> screenshot -> stop a browser session.
302 if (request.method === "POST" && pathname === "/browser/run") {
303 const session = yield* startBrowserSession({
304 sessionTimeout: "5 minutes",
305 });
306 const observed = yield* getBrowserSession({
307 sessionId: session.sessionId,
308 });
309 const sessions = yield* listBrowserSessions({});
310 const screenshot = yield* invokeBrowser({
311 sessionId: session.sessionId,
312 action: { screenshot: {} },
313 });
314 yield* stopBrowserSession({ sessionId: session.sessionId });
315 return yield* HttpServerResponse.json({
316 sessionId: session.sessionId,
317 sessionStatus: observed.status,
318 sessionCount: sessions.items?.length ?? 0,
319 screenshotTaken: screenshot.result !== undefined,
320 });
321 }
322
323 // Not driven by tests (needs a live automation stream / a browser
324 // profile); exist so the bindings' runtime callables are wired.
325 if (request.method === "POST" && pathname === "/browser/stream") {
326 yield* updateBrowserStream({
327 sessionId: url.searchParams.get("sessionId")!,
328 streamUpdate: {
329 automationStreamUpdate: { streamStatus: "ENABLED" },
330 },
331 });
332 return yield* HttpServerResponse.json({ updated: true });
333 }
334 if (request.method === "POST" && pathname === "/browser/profile") {
335 yield* saveBrowserSessionProfile({
336 sessionId: url.searchParams.get("sessionId")!,
337 profileIdentifier: url.searchParams.get("profileIdentifier")!,
338 });
339 return yield* HttpServerResponse.json({ saved: true });
340 }
341
342 return yield* HttpServerResponse.json(
343 { error: "Not found" },
344 { status: 404 },
345 );
346 }).pipe(Effect.orDie),
347 };
348 }).pipe(
349 Effect.provide(
350 Layer.mergeAll(
351 AgentCore.CreateEventHttp,
352 AgentCore.GetEventHttp,
353 AgentCore.DeleteEventHttp,
354 AgentCore.ListEventsHttp,
355 AgentCore.ListSessionsHttp,
356 AgentCore.ListActorsHttp,
357 AgentCore.ListMemoryRecordsHttp,
358 AgentCore.RetrieveMemoryRecordsHttp,
359 AgentCore.GetMemoryRecordHttp,
360 AgentCore.DeleteMemoryRecordHttp,
361 AgentCore.BatchCreateMemoryRecordsHttp,
362 AgentCore.BatchUpdateMemoryRecordsHttp,
363 AgentCore.BatchDeleteMemoryRecordsHttp,
364 AgentCore.StartMemoryExtractionJobHttp,
365 AgentCore.ListMemoryExtractionJobsHttp,
366 AgentCore.StartCodeInterpreterSessionHttp,
367 AgentCore.InvokeCodeInterpreterHttp,
368 AgentCore.GetCodeInterpreterSessionHttp,
369 AgentCore.ListCodeInterpreterSessionsHttp,
370 AgentCore.StopCodeInterpreterSessionHttp,
371 AgentCore.StartBrowserSessionHttp,
372 AgentCore.GetBrowserSessionHttp,
373 AgentCore.ListBrowserSessionsHttp,
374 AgentCore.InvokeBrowserHttp,
375 AgentCore.UpdateBrowserStreamHttp,
376 AgentCore.SaveBrowserSessionProfileHttp,
377 AgentCore.StopBrowserSessionHttp,
378 ),
379 ),
380 ),
381);