diff --git a/README.md b/README.md index 89c15367a..abfb0d46a 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ agentcore # interactive TUI │ └── endpoint │ ├── get # get a Runtime endpoint by qualifier │ └── list # list a Runtime's endpoints +├── memory # inspect AgentCore Memories +│ ├── get # fetch a Memory by id +│ └── list # list Memories (server-side paginated) ├── eval # evaluate and optimize AgentCore agents │ └── evaluator # manage AgentCore evaluators │ ├── llm-as-a-judge # LLM-as-a-Judge evaluators @@ -120,6 +123,11 @@ agentcore runtime version list --id --max-results 20 agentcore runtime endpoint get --id --qualifier DEFAULT agentcore runtime endpoint list --id --max-results 20 +# Inspect AgentCore Memories without project configuration or deployment +agentcore memory get --id +agentcore memory get --id --view without_decryption +agentcore memory list --max-results 20 + # Manage API key credential providers agentcore identity api-key-credential-provider create --name my-provider --api-key agentcore identity api-key-credential-provider get --name my-provider @@ -655,8 +663,8 @@ A Husky pre-commit hook runs Prettier (via lint-staged) on staged files automati - **Cover more AgentCore resources.** The harness surface (CRUD, versions, endpoints, invoke, exec) is fully implemented in both the CLI and the TUI; - the same patterns extend naturally to gateways, memory, browser profiles, - and the other control-plane resources. + the same patterns extend naturally to gateways, Memory mutations and + data-plane operations, browser profiles, and the other AgentCore resources. - **Implement `config`.** The `config` command is currently a stub — it should read/write real global settings (telemetry, log level, ...) through an injected config accessor. diff --git a/src/components/RouterScreen.test.tsx b/src/components/RouterScreen.test.tsx index 0fe9c235c..9db945dd8 100644 --- a/src/components/RouterScreen.test.tsx +++ b/src/components/RouterScreen.test.tsx @@ -17,6 +17,8 @@ describe("menu rendering", () => { expect(frame).toContain("manage agentcore harnesses"); expect(frame).toContain("runtime"); expect(frame).toContain("inspect AgentCore Runtimes"); + expect(frame).toContain("memory"); + expect(frame).toContain("manage AgentCore Memories"); expect(frame).toContain("config"); expect(frame).toContain("read/write global config values"); r.unmount(); diff --git a/src/core/core.test.ts b/src/core/core.test.ts index c4424552b..a8d54649c 100644 --- a/src/core/core.test.ts +++ b/src/core/core.test.ts @@ -182,7 +182,7 @@ test("data() caches independently of control()", () => { expect(dataBuilt).toBe(1); }); -test("exposes a harness sub-client", () => { +test("exposes feature sub-clients", () => { const core = new CoreClient({ createControlClient: fakeControl, createDataClient: fakeData, @@ -190,6 +190,7 @@ test("exposes a harness sub-client", () => { logger: createSilentLogger(), }); expect(core.harness).toBeDefined(); + expect(core.memory).toBeDefined(); }); test("getRuntime sends the abort signal to the control client", async () => { diff --git a/src/core/index.tsx b/src/core/index.tsx index b6cb02a3a..77c617c17 100644 --- a/src/core/index.tsx +++ b/src/core/index.tsx @@ -4,6 +4,7 @@ import { IAMClient } from "@aws-sdk/client-iam"; import { EvalClient } from "./eval"; import { HarnessClient } from "./harness"; import { IdentityClient } from "./identity"; +import { MemoryClient } from "./memory"; import { RuntimeClient } from "./runtime"; import type { AwsClients, @@ -51,6 +52,7 @@ export class CoreClient implements AwsClients { // Feature-scoped sub-clients. Access as e.g. `coreClient.harness.getHarness(...)`. readonly harness: HarnessClient = new HarnessClient(this); readonly identity: IdentityClient = new IdentityClient(this); + readonly memory: MemoryClient = new MemoryClient(this); readonly runtime: RuntimeClient; readonly eval: EvalClient = new EvalClient(this); diff --git a/src/core/memory.tsx b/src/core/memory.tsx new file mode 100644 index 000000000..29f1ed2d9 --- /dev/null +++ b/src/core/memory.tsx @@ -0,0 +1,30 @@ +import { + GetMemoryCommand, + ListMemoriesCommand, + type GetMemoryOutput, + type ListMemoriesOutput, + type MemoryView, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import type { CoreMemoryClient } from "../handlers/memory/types"; +import type { AwsClients, CoreOptions } from "./types"; +import { toClientConfig } from "./utils"; + +export class MemoryClient implements CoreMemoryClient { + constructor(private readonly clients: AwsClients) {} + + async getMemory(id: string, view: MemoryView, options: CoreOptions): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new GetMemoryCommand({ memoryId: id, view })); + } + + async listMemories( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + return this.clients + .control(toClientConfig(options)) + .send(new ListMemoriesCommand({ nextToken, maxResults })); + } +} diff --git a/src/handlers/index.tsx b/src/handlers/index.tsx index d2aa1666a..4946607b3 100644 --- a/src/handlers/index.tsx +++ b/src/handlers/index.tsx @@ -2,6 +2,7 @@ import { Router } from "../router"; import { createEvalHandler } from "./eval/index.tsx"; import { createHarnessHandler } from "./harness/index.tsx"; import { createIdentityHandler } from "./identity/index.tsx"; +import { createMemoryHandler } from "./memory/index.tsx"; import { createRuntimeHandler } from "./runtime/index.tsx"; import { DebugKey, EndpointKey, JsonKey, RegionKey } from "./keys.tsx"; import { createConfigHandler } from "./config/"; @@ -44,6 +45,7 @@ export function createRootHandler(core: Core, config: RootHandlerConfig): Router root.handler(createHarnessHandler(core, io)); root.handler(createIdentityHandler(core, io)); root.handler(createRuntimeHandler(core, io)); + root.handler(createMemoryHandler(core, io)); root.handler(createEvalHandler(core, io)); root.handler(createConfigHandler()); root.handler(createProjectHandler({ projectManager: core.projectManager })); diff --git a/src/handlers/memory/__fixtures__/GetMemoryCommand.4f3ba5bd8d8e60e7.json b/src/handlers/memory/__fixtures__/GetMemoryCommand.4f3ba5bd8d8e60e7.json new file mode 100644 index 000000000..d5490138b --- /dev/null +++ b/src/handlers/memory/__fixtures__/GetMemoryCommand.4f3ba5bd8d8e60e7.json @@ -0,0 +1,17 @@ +{ + "memory": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:memory/agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "id": "agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "name": "agentcore_cli_memory_read_only_fixture", + "eventExpiryDuration": 30, + "status": "ACTIVE", + "createdAt": { + "$date": "2026-07-29T19:10:47.336Z" + }, + "updatedAt": { + "$date": "2026-07-29T19:10:52.159Z" + }, + "description": "Stable fixture for AgentCore CLI Memory read-only tests", + "strategies": [] + } +} \ No newline at end of file diff --git a/src/handlers/memory/__fixtures__/GetMemoryCommand.8709459f816da117.json b/src/handlers/memory/__fixtures__/GetMemoryCommand.8709459f816da117.json new file mode 100644 index 000000000..d5490138b --- /dev/null +++ b/src/handlers/memory/__fixtures__/GetMemoryCommand.8709459f816da117.json @@ -0,0 +1,17 @@ +{ + "memory": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:memory/agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "id": "agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "name": "agentcore_cli_memory_read_only_fixture", + "eventExpiryDuration": 30, + "status": "ACTIVE", + "createdAt": { + "$date": "2026-07-29T19:10:47.336Z" + }, + "updatedAt": { + "$date": "2026-07-29T19:10:52.159Z" + }, + "description": "Stable fixture for AgentCore CLI Memory read-only tests", + "strategies": [] + } +} \ No newline at end of file diff --git a/src/handlers/memory/__fixtures__/GetMemoryCommand.ed4e49fb82fa7aa2.json b/src/handlers/memory/__fixtures__/GetMemoryCommand.ed4e49fb82fa7aa2.json new file mode 100644 index 000000000..315d208ad --- /dev/null +++ b/src/handlers/memory/__fixtures__/GetMemoryCommand.ed4e49fb82fa7aa2.json @@ -0,0 +1,6 @@ +{ + "$error": { + "name": "ResourceNotFoundException", + "message": "Resource not found during GetMemory: Failed to retrieve memory item for ID: missing_memory-0000000000" + } +} \ No newline at end of file diff --git a/src/handlers/memory/__fixtures__/ListMemoriesCommand.202662ec9345611.json b/src/handlers/memory/__fixtures__/ListMemoriesCommand.202662ec9345611.json new file mode 100644 index 000000000..7b701b2f3 --- /dev/null +++ b/src/handlers/memory/__fixtures__/ListMemoriesCommand.202662ec9345611.json @@ -0,0 +1,15 @@ +{ + "memories": [ + { + "createdAt": { + "$date": "2026-07-29T19:10:49.223Z" + }, + "updatedAt": { + "$date": "2026-07-29T19:10:53.987Z" + }, + "arn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:memory/agentcore_cli_memory_read_only_fixture_second-yYHO1d5zJb", + "id": "agentcore_cli_memory_read_only_fixture_second-yYHO1d5zJb", + "status": "ACTIVE" + } + ] +} \ No newline at end of file diff --git a/src/handlers/memory/__fixtures__/ListMemoriesCommand.7d2e22c637f6b633.json b/src/handlers/memory/__fixtures__/ListMemoriesCommand.7d2e22c637f6b633.json new file mode 100644 index 000000000..8cc71c30b --- /dev/null +++ b/src/handlers/memory/__fixtures__/ListMemoriesCommand.7d2e22c637f6b633.json @@ -0,0 +1,16 @@ +{ + "memories": [ + { + "createdAt": { + "$date": "2026-07-29T19:10:47.336Z" + }, + "updatedAt": { + "$date": "2026-07-29T19:10:52.159Z" + }, + "arn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:memory/agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "id": "agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "status": "ACTIVE" + } + ], + "nextToken": "AQICAHiUtSe0BW1y/mGNCEkbzUvDO1/kt+MrXlU4kyNsk96TRAFZNu68ZoZRVpTi4J3mdZa1AAAA6zCB6AYJKoZIhvcNAQcGoIHaMIHXAgEAMIHRBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDE5I2hVBPh23XBhEhwIBEICBoyaroKFGBAkZQJ22PGY4hxH+M04e0wtFiBQUCvxcmW0/zbfP9DUozeosJuF6BAY+cNtUzg5w57FGk9+xLt3GotCE9mGKQYsVbzbLKfOpGjQBPgPQ1erPzB/rSH9RL+CII4aCgNo6RQQ39pYscVrjZVT0ZKXUqWSZbV0pGFJtq8OM5ACWTubrr87ShhGFrYzbQO9xhqsgLovhl8/7wgs9OD/bq8g=" +} \ No newline at end of file diff --git a/src/handlers/memory/__fixtures__/get.golden.json b/src/handlers/memory/__fixtures__/get.golden.json new file mode 100644 index 000000000..00ce4f242 --- /dev/null +++ b/src/handlers/memory/__fixtures__/get.golden.json @@ -0,0 +1,13 @@ +{ + "memory": { + "arn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:memory/agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "id": "agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "name": "agentcore_cli_memory_read_only_fixture", + "eventExpiryDuration": 30, + "status": "ACTIVE", + "createdAt": "2026-07-29T19:10:47.336Z", + "updatedAt": "2026-07-29T19:10:52.159Z", + "description": "Stable fixture for AgentCore CLI Memory read-only tests", + "strategies": [] + } +} \ No newline at end of file diff --git a/src/handlers/memory/__fixtures__/list-page-1.golden.json b/src/handlers/memory/__fixtures__/list-page-1.golden.json new file mode 100644 index 000000000..934ddce90 --- /dev/null +++ b/src/handlers/memory/__fixtures__/list-page-1.golden.json @@ -0,0 +1,12 @@ +{ + "memories": [ + { + "createdAt": "2026-07-29T19:10:47.336Z", + "updatedAt": "2026-07-29T19:10:52.159Z", + "arn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:memory/agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "id": "agentcore_cli_memory_read_only_fixture-QZMh466aPK", + "status": "ACTIVE" + } + ], + "nextToken": "AQICAHiUtSe0BW1y/mGNCEkbzUvDO1/kt+MrXlU4kyNsk96TRAFZNu68ZoZRVpTi4J3mdZa1AAAA6zCB6AYJKoZIhvcNAQcGoIHaMIHXAgEAMIHRBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDE5I2hVBPh23XBhEhwIBEICBoyaroKFGBAkZQJ22PGY4hxH+M04e0wtFiBQUCvxcmW0/zbfP9DUozeosJuF6BAY+cNtUzg5w57FGk9+xLt3GotCE9mGKQYsVbzbLKfOpGjQBPgPQ1erPzB/rSH9RL+CII4aCgNo6RQQ39pYscVrjZVT0ZKXUqWSZbV0pGFJtq8OM5ACWTubrr87ShhGFrYzbQO9xhqsgLovhl8/7wgs9OD/bq8g=" +} \ No newline at end of file diff --git a/src/handlers/memory/__fixtures__/list-page-2.golden.json b/src/handlers/memory/__fixtures__/list-page-2.golden.json new file mode 100644 index 000000000..34c5d80a8 --- /dev/null +++ b/src/handlers/memory/__fixtures__/list-page-2.golden.json @@ -0,0 +1,11 @@ +{ + "memories": [ + { + "createdAt": "2026-07-29T19:10:49.223Z", + "updatedAt": "2026-07-29T19:10:53.987Z", + "arn": "arn:aws:bedrock-agentcore:us-west-2:685197708687:memory/agentcore_cli_memory_read_only_fixture_second-yYHO1d5zJb", + "id": "agentcore_cli_memory_read_only_fixture_second-yYHO1d5zJb", + "status": "ACTIVE" + } + ] +} \ No newline at end of file diff --git a/src/handlers/memory/get/index.tsx b/src/handlers/memory/get/index.tsx new file mode 100644 index 000000000..bce0109ee --- /dev/null +++ b/src/handlers/memory/get/index.tsx @@ -0,0 +1,29 @@ +import z from "zod"; +import { InputValidationError } from "../../../errors"; +import { createHandler, flag } from "../../../router"; +import { JsonRendererKey } from "../../../tui"; +import type { Core } from "../../types"; +import { coreOptsFromCtx } from "../../utils"; + +const MEMORY_VIEWS = ["full", "without_decryption"] as const; + +export const createGetMemoryHandler = (core: Core) => + createHandler({ + name: "get", + description: "get an AgentCore Memory", + flags: [ + flag("id", "the ID of the Memory", z.string().optional()), + flag("view", "response view", z.enum(MEMORY_VIEWS).optional()), + ], + handle: async (ctx, flags) => { + if (!flags.id) { + throw new InputValidationError("required option '--id ' not specified"); + } + + ctx + .require(JsonRendererKey) + .renderJson( + await core.memory.getMemory(flags.id, flags.view ?? "full", coreOptsFromCtx(ctx)), + ); + }, + }); diff --git a/src/handlers/memory/index.tsx b/src/handlers/memory/index.tsx new file mode 100644 index 000000000..8f11ca8df --- /dev/null +++ b/src/handlers/memory/index.tsx @@ -0,0 +1,13 @@ +import { Router } from "../../router"; +import type { AppIO } from "../../io"; +import type { Core } from "../types"; +import { createHelpDefault } from "../help"; +import { createGetMemoryHandler } from "./get"; +import { createListMemoriesHandler } from "./list"; + +export function createMemoryHandler(core: Core, io: AppIO): Router { + return new Router("memory", "manage AgentCore Memories") + .default(createHelpDefault(io)) + .handler(createGetMemoryHandler(core)) + .handler(createListMemoriesHandler(core)); +} diff --git a/src/handlers/memory/list/index.tsx b/src/handlers/memory/list/index.tsx new file mode 100644 index 000000000..987306467 --- /dev/null +++ b/src/handlers/memory/list/index.tsx @@ -0,0 +1,26 @@ +import z from "zod"; +import { createHandler, flag } from "../../../router"; +import { JsonRendererKey } from "../../../tui"; +import type { Core } from "../../types"; +import { coreOptsFromCtx } from "../../utils"; + +export const createListMemoriesHandler = (core: Core) => + createHandler({ + name: "list", + description: "list AgentCore Memories", + flags: [ + flag("next-token", "pagination token returned by a previous request", z.string().optional()), + flag("max-results", "maximum number of items to return", z.number().optional()), + ], + handle: async (ctx, flags) => { + ctx + .require(JsonRendererKey) + .renderJson( + await core.memory.listMemories( + flags["next-token"], + flags["max-results"], + coreOptsFromCtx(ctx), + ), + ); + }, + }); diff --git a/src/handlers/memory/memory.test.tsx b/src/handlers/memory/memory.test.tsx new file mode 100644 index 000000000..edf086dc2 --- /dev/null +++ b/src/handlers/memory/memory.test.tsx @@ -0,0 +1,131 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; +import { CoreClient } from "../../core"; +import { + createSilentLogger, + fixtureFactories, + matchGolden, + TestGlobalConfigAccessor, + testIO, +} from "../../testing"; +import { createRootHandler } from "../index"; +import { createGetMemoryHandler } from "./get"; + +const REGION = "us-west-2"; +const FIXTURES = join(import.meta.dir, "__fixtures__"); + +// The e2e-test account holds two persistent fixture Memories: +// agentcore_cli_memory_read_only_fixture (get) and +// agentcore_cli_memory_read_only_fixture_second (needed for list pagination, >=2). +// Record with AWS_PROFILE=e2e-test RECORD=1 bun test src/handlers/memory/memory.test.tsx. +const FIXTURE_MEMORY_ID = "agentcore_cli_memory_read_only_fixture-QZMh466aPK"; +const MISSING_MEMORY_ID = "missing_memory-0000000000"; + +function createFixtureCore(): CoreClient { + const { createControlClient, createDataClient, createIamClient } = fixtureFactories(FIXTURES); + + return new CoreClient({ + createControlClient, + createDataClient, + createIamClient, + logger: createSilentLogger(), + }); +} + +async function run(args: string[]): Promise { + const io = testIO(); + const root = createRootHandler(createFixtureCore(), { + io: io.io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + + await root.route(["node", "agentcore", ...args, "--region", REGION]); + return io.stdout(); +} + +describe("memory command hierarchy", () => { + test("registers the Memory read-only command hierarchy", () => { + const root = createRootHandler(createFixtureCore(), { + io: testIO().io, + logger: createSilentLogger(), + globalConfigAccessor: new TestGlobalConfigAccessor(), + }); + const memory = root.children().find((child) => child.name() === "memory"); + + expect(memory?.flags().map((flag) => flag.name)).not.toContain("interactive"); + expect(memory?.children().map((child) => child.name())).toEqual(["get", "list"]); + }); + + test("keeps an omitted get view undefined for empty-flag routing", () => { + const get = createGetMemoryHandler(createFixtureCore()); + const view = get.flags().find((flag) => flag.name === "view"); + + expect(view?.schema.parse(undefined)).toBeUndefined(); + }); + + test("prints help for bare `memory` without an SDK call", async () => { + const stdout = await run(["memory"]); + + expect(stdout).toContain("Usage: agentcore memory"); + expect(stdout).toContain("Commands:"); + }); +}); + +describe("memory read-only commands", () => { + test("gets a Memory using the full view by default", async () => { + const stdout = await run(["memory", "get", "--id", FIXTURE_MEMORY_ID]); + + matchGolden(FIXTURES, "get.golden.json", stdout); + expect(JSON.parse(stdout).memory.id).toBe(FIXTURE_MEMORY_ID); + }); + + test("accepts the without_decryption view", async () => { + const stdout = await run([ + "memory", + "get", + "--id", + FIXTURE_MEMORY_ID, + "--view", + "without_decryption", + ]); + + matchGolden(FIXTURES, "get.golden.json", stdout); + }); + + test("paginates Memory list with --max-results and --next-token", async () => { + const firstPage = await run(["memory", "list", "--max-results", "1"]); + matchGolden(FIXTURES, "list-page-1.golden.json", firstPage); + + const first = JSON.parse(firstPage); + expect(first.memories).toHaveLength(1); + expect(first.nextToken).toBeString(); + + const secondPage = await run([ + "memory", + "list", + "--max-results", + "1", + "--next-token", + first.nextToken, + ]); + matchGolden(FIXTURES, "list-page-2.golden.json", secondPage); + expect(JSON.parse(secondPage).memories).toHaveLength(1); + }); + + test("rejects a missing Memory selector for headless get", async () => { + await expect(run(["memory", "get", "--json"])).rejects.toThrow(/--id/); + }); + + test("rejects an unsupported response view", async () => { + await expect( + run(["memory", "get", "--id", FIXTURE_MEMORY_ID, "--view", "summary"]), + ).rejects.toThrow(/Invalid value for option '--view'/); + }); + + test("propagates ResourceNotFoundException from Memory get", async () => { + await expect(run(["memory", "get", "--id", MISSING_MEMORY_ID])).rejects.toMatchObject({ + name: "ResourceNotFoundException", + }); + }); +}); diff --git a/src/handlers/memory/types.tsx b/src/handlers/memory/types.tsx new file mode 100644 index 000000000..9fe75386d --- /dev/null +++ b/src/handlers/memory/types.tsx @@ -0,0 +1,15 @@ +import type { + GetMemoryOutput, + ListMemoriesOutput, + MemoryView, +} from "@aws-sdk/client-bedrock-agentcore-control"; +import type { CoreOptions } from "../../core/types"; + +export interface CoreMemoryClient { + getMemory(id: string, view: MemoryView, options: CoreOptions): Promise; + listMemories( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise; +} diff --git a/src/handlers/root.test.tsx b/src/handlers/root.test.tsx index 745e81e94..34b517d00 100644 --- a/src/handlers/root.test.tsx +++ b/src/handlers/root.test.tsx @@ -14,6 +14,7 @@ describe("createRootHandler", () => { "harness", "identity", "runtime", + "memory", "eval", "config", "project", diff --git a/src/handlers/types.tsx b/src/handlers/types.tsx index d33f27145..d41f66cc0 100644 --- a/src/handlers/types.tsx +++ b/src/handlers/types.tsx @@ -1,6 +1,7 @@ import type { CoreEvalClient } from "./eval/types.tsx"; import type { CoreHarnessClient } from "./harness/types.tsx"; import type { CoreIdentityClient } from "./identity/types.tsx"; +import type { CoreMemoryClient } from "./memory/types.tsx"; import type { CoreRuntimeClient } from "./runtime/types.tsx"; import type { Context } from "../router"; import type { ProjectManager } from "./project/types.ts"; @@ -8,6 +9,7 @@ import type { ProjectManager } from "./project/types.ts"; export interface Core { harness: CoreHarnessClient; identity: CoreIdentityClient; + memory: CoreMemoryClient; runtime: CoreRuntimeClient; eval: CoreEvalClient; projectManager: ProjectManager; diff --git a/src/testing/TestCoreClient.tsx b/src/testing/TestCoreClient.tsx index 00d3edf95..54ef05cdb 100644 --- a/src/testing/TestCoreClient.tsx +++ b/src/testing/TestCoreClient.tsx @@ -13,10 +13,12 @@ import type { GetHarnessEndpointResponse, GetAgentRuntimeEndpointResponse, GetAgentRuntimeResponse, + GetMemoryOutput, ListApiKeyCredentialProvidersResponse, ListAgentRuntimeEndpointsResponse, ListAgentRuntimesResponse, ListAgentRuntimeVersionsResponse, + ListMemoriesOutput, ListHarnessesResponse, ListHarnessEndpointsResponse, ListHarnessVersionsResponse, @@ -25,6 +27,7 @@ import type { DeleteEvaluatorResponse, GetEvaluatorResponse, ListEvaluatorsResponse, + MemoryView, UpdateEvaluatorResponse, UpdateApiKeyCredentialProviderResponse, UpdateHarnessEndpointRequest, @@ -47,6 +50,7 @@ import type { CreateApiKeyCredentialProviderInput, UpdateApiKeyCredentialProviderInput, } from "../handlers/identity/types"; +import type { CoreMemoryClient } from "../handlers/memory/types"; import type { CoreRuntimeClient, RuntimeInvokeRequest, @@ -103,6 +107,8 @@ const DEFAULT_LIST_API_KEYS_RESPONSE: ListApiKeyCredentialProvidersResponse = { }; const DEFAULT_UPDATE_API_KEY_RESPONSE = {} as UpdateApiKeyCredentialProviderResponse; const DEFAULT_DELETE_API_KEY_RESPONSE = {} as DeleteApiKeyCredentialProviderResponse; +const DEFAULT_GET_MEMORY_RESPONSE = {} as GetMemoryOutput; +const DEFAULT_LIST_MEMORIES_RESPONSE: ListMemoriesOutput = { memories: [] }; const DEFAULT_GET_RUNTIME_RESPONSE = {} as GetAgentRuntimeResponse; const DEFAULT_GET_RUNTIME_ENDPOINT_RESPONSE = {} as GetAgentRuntimeEndpointResponse; const DEFAULT_LIST_RUNTIMES_RESPONSE: ListAgentRuntimesResponse = { agentRuntimes: [] }; @@ -587,6 +593,50 @@ export class TestRuntimeClient implements CoreRuntimeClient { ); } } + +export class TestMemoryClient implements CoreMemoryClient { + readonly calls: RecordedCall[] = []; + + private getResponse: GetMemoryOutput = DEFAULT_GET_MEMORY_RESPONSE; + private listResponses = new Map(); + private error?: Error; + + setGetResponse(response: GetMemoryOutput): this { + this.getResponse = response; + return this; + } + + setListResponse(response: ListMemoriesOutput, forNextToken?: string): this { + this.listResponses.set(forNextToken, response); + return this; + } + + setError(error: Error | undefined): this { + this.error = error; + return this; + } + + async getMemory(id: string, view: MemoryView, options: CoreOptions): Promise { + this.calls.push({ method: "getMemory", args: [id, view, options] }); + if (this.error) throw this.error; + return this.getResponse; + } + + async listMemories( + nextToken: string | undefined, + maxResults: number | undefined, + options: CoreOptions, + ): Promise { + this.calls.push({ method: "listMemories", args: [nextToken, maxResults, options] }); + if (this.error) throw this.error; + return ( + this.listResponses.get(nextToken) ?? + this.listResponses.get(undefined) ?? + DEFAULT_LIST_MEMORIES_RESPONSE + ); + } +} + type TestCoreClientOptions = { logger?: Logger; }; @@ -743,6 +793,7 @@ export class TestEvalClient implements CoreEvalClient { export class TestCoreClient implements Core { readonly harness = new TestHarnessClient(); readonly identity = new TestIdentityClient(); + readonly memory = new TestMemoryClient(); readonly runtime = new TestRuntimeClient(); readonly eval = new TestEvalClient(); readonly projectManager: ProjectManager; diff --git a/src/testing/index.tsx b/src/testing/index.tsx index ccee19063..7ea3ce891 100644 --- a/src/testing/index.tsx +++ b/src/testing/index.tsx @@ -5,6 +5,7 @@ export { tick, waitFor } from "./timing"; export { TestCoreClient, TestHarnessClient, + TestMemoryClient, TestRuntimeClient, TestEvalClient, type RecordedCall,