diff --git a/README.md b/README.md
index abfb0d46a..dea73bad2 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ It gives you two ways to work, from the same binary:
- **A scriptable CLI** — every operation is a flag-driven subcommand that emits
JSON (`--json`), so it can be used by codeing agents and can drop cleanly into
scripts, CI, and automation.
-- **An interactive TUI** — bare Harness and Runtime branches and leaves open
+- **An interactive TUI** — bare Harness, Runtime, and Memory branches and leaves open
their corresponding menus and selection flows.
```bash
@@ -26,7 +26,7 @@ responses. `agentcore` wraps all of that behind one ergonomic tool.
## Command surface
-Commands with operation flags run headlessly. Bare Harness and Runtime branches
+Commands with operation flags run headlessly. Bare Harness, Runtime, and Memory branches
and leaves open their interactive flows.
```
@@ -242,7 +242,7 @@ accept ARNs, `--version`, `--interactive`, cross-account targets, or custom
request paths. All requests use the Runtime `/invocations` route, including MCP
Runtimes.
-Bare Runtime branches and leaves require a TTY on stdin and stdout. Supplying
+Bare Runtime and Memory branches and leaves require a TTY on stdin and stdout. Supplying
operation flags runs the command headlessly, and `--json` always suppresses TUI
rendering.
@@ -252,6 +252,9 @@ agentcore runtime list
agentcore runtime get
agentcore runtime version list
agentcore runtime endpoint list
+agentcore memory
+agentcore memory list
+agentcore memory get
```
---
diff --git a/src/components/Root.tsx b/src/components/Root.tsx
index 5d59130e0..5899233ac 100644
--- a/src/components/Root.tsx
+++ b/src/components/Root.tsx
@@ -28,6 +28,9 @@ import { RuntimeListEndpointsScreen } from "../handlers/runtime/endpoint/list/sc
import { RuntimeVersionScreen } from "../handlers/runtime/version/screen.tsx";
import { RuntimeGetVersionScreen } from "../handlers/runtime/version/get/screen.tsx";
import { RuntimeListVersionsScreen } from "../handlers/runtime/version/list/screen.tsx";
+import { MemoryScreen } from "../handlers/memory/screen.tsx";
+import { MemoryGetJsonScreen, MemoryGetScreen } from "../handlers/memory/get/screen.tsx";
+import { MemoryListScreen } from "../handlers/memory/list/screen.tsx";
import { RootScreen, HelpScreen } from "../handlers/screen.tsx";
import type { Context } from "../router";
@@ -255,6 +258,23 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/list/:runtimeId"
element={}
/>
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
} />
diff --git a/src/handlers/memory/get/screen.tsx b/src/handlers/memory/get/screen.tsx
new file mode 100644
index 000000000..643ffbd3e
--- /dev/null
+++ b/src/handlers/memory/get/screen.tsx
@@ -0,0 +1,103 @@
+import { useQuery } from "@tanstack/react-query";
+import { Box, Text, useInput } from "ink";
+import { useNavigate, useParams } from "react-router";
+import { JsonDetail } from "../../../components/JsonDetail";
+import { KeyValueTable } from "../../../components/KeyValueTable.js";
+import { Layout } from "../../../components/Layout";
+import { darkTheme } from "../../../components/ui/_core.js";
+import { Divider } from "../../../components/ui/divider/Divider.js";
+import { Spinner } from "../../../components/ui/spinner";
+import type { ScreenProps } from "../../types";
+import { coreOptsFromCtx } from "../../utils";
+
+function useMemoryDetail({ ctx, core }: ScreenProps, memoryId: string | undefined) {
+ const opts = coreOptsFromCtx(ctx);
+ return useQuery({
+ queryKey: ["memory", opts.region, memoryId, "full"],
+ queryFn: () => core.memory.getMemory(memoryId!, "full", opts),
+ enabled: memoryId !== undefined,
+ });
+}
+
+export function MemoryGetScreen(props: ScreenProps) {
+ const navigate = useNavigate();
+ const { memoryId } = useParams();
+ const detail = useMemoryDetail(props, memoryId);
+ const memory = detail.data?.memory;
+
+ useInput((input, key) => {
+ if (key.escape) {
+ navigate(-1);
+ return;
+ }
+ if (input === "r" && detail.isError) {
+ void detail.refetch();
+ return;
+ }
+ if (detail.isError || !memory) return;
+ if (key.return && memoryId) {
+ navigate(`/agentcore/memory/get/${encodeURIComponent(memoryId)}/json`);
+ }
+ });
+
+ return (
+
+ {detail.isPending ? (
+
+ ) : detail.isError ? (
+ Error: {(detail.error as Error).message}
+ ) : (
+
+
+
+
+
+
+
+
+ ❯
+
+ {"detail".padEnd(9)}
+
+ show the full JSON definition
+
+
+ )}
+
+ );
+}
+
+export function MemoryGetJsonScreen(props: ScreenProps) {
+ const { memoryId } = useParams();
+ const detail = useMemoryDetail(props, memoryId);
+
+ return (
+ void detail.refetch()}
+ />
+ );
+}
diff --git a/src/handlers/memory/index.tsx b/src/handlers/memory/index.tsx
index 8f11ca8df..53ae0a23c 100644
--- a/src/handlers/memory/index.tsx
+++ b/src/handlers/memory/index.tsx
@@ -1,13 +1,15 @@
+import { withTuiOnEmptyFlagsAndArgs } from "../../middleware";
import { Router } from "../../router";
+import { renderTui } from "../../tui";
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))
+ .use(withTuiOnEmptyFlagsAndArgs(core, io))
+ .default(renderTui(core, io))
.handler(createGetMemoryHandler(core))
.handler(createListMemoriesHandler(core));
}
diff --git a/src/handlers/memory/list/screen.tsx b/src/handlers/memory/list/screen.tsx
new file mode 100644
index 000000000..7e8c8104d
--- /dev/null
+++ b/src/handlers/memory/list/screen.tsx
@@ -0,0 +1,60 @@
+import type { MemorySummary } from "@aws-sdk/client-bedrock-agentcore-control";
+import { useNavigate } from "react-router";
+import { formatTimestamp } from "../../../components/formatTimestamp";
+import { PaginatedTablePicker } from "../../../components/PaginatedTablePicker";
+import type { DataTableColumn } from "../../../components/ui/data-table";
+import type { ScreenProps } from "../../types";
+import { coreOptsFromCtx } from "../../utils";
+
+interface MemoryRow extends Record {
+ memoryId: string;
+ status: string;
+ updatedAt: string;
+}
+
+export const memoryColumns = [
+ { key: "memoryId", header: "id", flex: true },
+ { key: "status", header: "status", width: 13 },
+ {
+ key: "updatedAt",
+ header: "updated UTC",
+ width: 16,
+ render: formatTimestamp,
+ },
+] satisfies DataTableColumn[];
+
+function toRow(memory: MemorySummary): MemoryRow {
+ return {
+ memoryId: memory.id ?? "",
+ status: memory.status ?? "-",
+ updatedAt: memory.updatedAt?.toISOString() ?? "-",
+ };
+}
+
+export function MemoryListScreen({ ctx, core }: ScreenProps) {
+ const opts = coreOptsFromCtx(ctx);
+ const navigate = useNavigate();
+
+ return (
+ {
+ const response = await core.memory.listMemories(token, pageSize, opts);
+ return {
+ items: response.memories ?? [],
+ nextToken: response.nextToken,
+ };
+ }}
+ toRow={toRow}
+ columns={memoryColumns}
+ getValue={(row) => row.memoryId}
+ onSelect={(memoryId) => navigate(`/agentcore/memory/get/${encodeURIComponent(memoryId)}`)}
+ onBack={() => navigate("/agentcore/memory")}
+ loadingMessage="Loading Memories…"
+ errorMessage={(error) => `Error: ${error.message}`}
+ emptyMessage="No Memories found in this Region."
+ emptyPageMessage="No Memories on this page."
+ />
+ );
+}
diff --git a/src/handlers/memory/memory.screen.test.tsx b/src/handlers/memory/memory.screen.test.tsx
new file mode 100644
index 000000000..273d8bcd8
--- /dev/null
+++ b/src/handlers/memory/memory.screen.test.tsx
@@ -0,0 +1,263 @@
+import { afterEach, describe, expect, test } from "bun:test";
+import type {
+ GetMemoryOutput,
+ Memory,
+ MemorySummary,
+} from "@aws-sdk/client-bedrock-agentcore-control";
+import { QueryClient } from "@tanstack/react-query";
+import stringWidth from "string-width";
+import {
+ cleanupScreens,
+ renderScreen,
+ TestCoreClient,
+ tick,
+ waitFor,
+ waitForText,
+} from "../../testing";
+
+afterEach(cleanupScreens);
+
+const memoryEndpointUrl = "https://memory.test";
+
+function memorySummary(overrides: Partial = {}): MemorySummary {
+ return {
+ arn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/memory-1",
+ id: "memory-1",
+ status: "ACTIVE",
+ createdAt: new Date("2026-07-19T01:02:03.000Z"),
+ updatedAt: new Date("2026-07-20T12:34:56.000Z"),
+ ...overrides,
+ };
+}
+
+function memory(overrides: Partial = {}): Memory {
+ return {
+ arn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/memory-1",
+ id: "memory-1",
+ name: "orders-memory",
+ description: "Memory for the orders agent",
+ memoryExecutionRoleArn: "arn:aws:iam::123456789012:role/memory-role",
+ eventExpiryDuration: 30,
+ status: "ACTIVE",
+ createdAt: new Date("2026-07-19T01:02:03.000Z"),
+ updatedAt: new Date("2026-07-20T12:34:56.000Z"),
+ strategies: [
+ {
+ strategyId: "summary-1",
+ name: "summary",
+ type: "SUMMARIZATION",
+ namespaces: [],
+ namespaceTemplates: ["/strategies/{memoryStrategyId}/actors/{actorId}"],
+ status: "ACTIVE",
+ },
+ ],
+ ...overrides,
+ };
+}
+
+function getMemoryOutput(overrides: Partial = {}): GetMemoryOutput {
+ return { memory: memory(overrides) };
+}
+
+function coreWithMemories(memories: MemorySummary[]): TestCoreClient {
+ const core = new TestCoreClient();
+ core.memory.setListResponse({ memories });
+ return core;
+}
+
+describe("Memory picker", () => {
+ test("renders Memory identity, status, and update time", async () => {
+ const core = coreWithMemories([
+ memorySummary({
+ id: "memory-visible-id",
+ status: "FAILED",
+ updatedAt: new Date("2026-07-21T02:03:04.000Z"),
+ }),
+ ]);
+ const screen = renderScreen("/agentcore/memory/list", { core });
+
+ await waitForText(screen.lastFrame, "memory-visible-id");
+ const frame = screen.lastFrame()!;
+ expect(frame).toContain("id");
+ expect(frame).toContain("status");
+ expect(frame).toContain("updated UTC");
+ expect(frame).toContain("FAILED");
+ expect(frame).toContain("2026-07-21 02:03");
+ });
+
+ test("keeps long Memory IDs separate from adjacent columns", async () => {
+ const memoryId = `memory-${"x".repeat(70)}`;
+ const core = coreWithMemories([memorySummary({ id: memoryId })]);
+ const screen = renderScreen("/agentcore/memory/list", { core });
+
+ await waitForText(screen.lastFrame, "memory-");
+ await screen.resize(80, 24);
+ const row = screen
+ .lastFrame()!
+ .split("\n")
+ .find((line) => line.includes("memory-"));
+ expect(row).toBeDefined();
+ expect(row).toContain("ACTIVE");
+ expect(row).not.toContain(memoryId);
+ expect(stringWidth(row!)).toBeLessThanOrEqual(80);
+ });
+
+ test("calls listMemories with exact Core options", async () => {
+ const core = coreWithMemories([memorySummary()]);
+ renderScreen("/agentcore/memory/list", { core, endpointUrl: memoryEndpointUrl });
+
+ await waitFor(() => core.memory.calls.some((call) => call.method === "listMemories"));
+ expect(core.memory.calls.filter((call) => call.method === "listMemories")).toEqual([
+ {
+ method: "listMemories",
+ args: [
+ undefined,
+ expect.any(Number),
+ {
+ region: "us-east-1",
+ endpointUrl: memoryEndpointUrl,
+ },
+ ],
+ },
+ ]);
+ });
+
+ test("shows first-page and later-page empty states", async () => {
+ const empty = renderScreen("/agentcore/memory/list");
+ await waitForText(empty.lastFrame, "No Memories found in this Region.");
+ empty.unmount();
+
+ const core = new TestCoreClient();
+ core.memory.setListResponse({
+ memories: [memorySummary({ id: "page-one" })],
+ nextToken: "page-2",
+ });
+ core.memory.setListResponse({ memories: [] }, "page-2");
+ const paged = renderScreen("/agentcore/memory/list", { core });
+
+ await waitForText(paged.lastFrame, "page 1 · more →");
+ await paged.write("l");
+ await waitForText(paged.lastFrame, "No Memories on this page.");
+ expect(paged.lastFrame()).not.toContain("No Memories found in this Region.");
+ });
+
+ test("bare Memory get redirects to the picker", async () => {
+ const core = coreWithMemories([memorySummary({ id: "redirected-memory" })]);
+ const screen = renderScreen("/agentcore/memory/get", { core });
+
+ await waitForText(screen.lastFrame, "redirected-memory");
+ expect(core.memory.calls[0]?.method).toBe("listMemories");
+ });
+
+ test("selection opens the matching Memory detail", async () => {
+ const memoryId = "memory blue";
+ const core = coreWithMemories([memorySummary({ id: memoryId })]);
+ core.memory.setGetResponse(getMemoryOutput({ id: memoryId }));
+ const screen = renderScreen("/agentcore/memory/list", { core });
+
+ await waitForText(screen.lastFrame, memoryId);
+ await screen.press("return");
+ await waitForText(screen.lastFrame, `agentcore → memory → get → ${memoryId}`);
+ await waitFor(() =>
+ core.memory.calls.some((call) => call.method === "getMemory" && call.args[0] === memoryId),
+ );
+ });
+});
+
+describe("Memory detail", () => {
+ test("loads the full view and renders a resource summary", async () => {
+ const core = new TestCoreClient();
+ core.memory.setGetResponse(getMemoryOutput());
+ const screen = renderScreen("/agentcore/memory/get/memory-1", {
+ core,
+ endpointUrl: memoryEndpointUrl,
+ });
+
+ await waitForText(screen.lastFrame, "show the full JSON definition");
+ const frame = screen.lastFrame()!;
+ expect(frame).toContain("orders-memory");
+ expect(frame).toMatch(/eventExpiryDays\s+30/);
+ expect(frame).toMatch(/strategies\s+1/);
+ expect(frame).toContain("arn:aws:bedrock-agentcore");
+ expect(core.memory.calls.find((call) => call.method === "getMemory")).toEqual({
+ method: "getMemory",
+ args: [
+ "memory-1",
+ "full",
+ {
+ region: "us-east-1",
+ endpointUrl: memoryEndpointUrl,
+ },
+ ],
+ });
+ });
+
+ test("shows a failure reason only when the service provides one", async () => {
+ const healthyCore = new TestCoreClient();
+ healthyCore.memory.setGetResponse(getMemoryOutput());
+ const healthy = renderScreen("/agentcore/memory/get/memory-1", { core: healthyCore });
+
+ await waitForText(healthy.lastFrame, "show the full JSON definition");
+ expect(healthy.lastFrame()).not.toContain("failureReason");
+ healthy.unmount();
+
+ const failedCore = new TestCoreClient();
+ failedCore.memory.setGetResponse(
+ getMemoryOutput({ status: "FAILED", failureReason: "Strategy setup failed" }),
+ );
+ const failed = renderScreen("/agentcore/memory/get/memory-1", { core: failedCore });
+
+ await waitForText(failed.lastFrame, "Strategy setup failed");
+ expect(failed.lastFrame()).toContain("failureReason");
+ });
+
+ test("opens the complete Memory JSON", async () => {
+ const core = new TestCoreClient();
+ core.memory.setGetResponse(getMemoryOutput());
+ const screen = renderScreen("/agentcore/memory/get/memory-1", { core });
+
+ await waitForText(screen.lastFrame, "show the full JSON definition");
+ await screen.press("return");
+ await waitForText(screen.lastFrame, "agentcore → memory → get → memory-1 → json");
+ const frame = screen.lastFrame()!;
+ expect(frame).toContain('"memoryExecutionRoleArn"');
+ expect(frame).toContain('"strategies"');
+ });
+
+ test("retries a failed detail query", async () => {
+ const core = new TestCoreClient();
+ core.memory.setError(new Error("memory unavailable"));
+ const screen = renderScreen("/agentcore/memory/get/memory-1", { core });
+
+ await waitForText(screen.lastFrame, "memory unavailable");
+ expect(screen.lastFrame()).toContain("[r] retry");
+
+ core.memory.setError(undefined);
+ core.memory.setGetResponse(getMemoryOutput());
+ await screen.write("r");
+ await waitForText(screen.lastFrame, "show the full JSON definition");
+ });
+
+ test("does not open cached detail after a background refresh fails", async () => {
+ const core = new TestCoreClient();
+ core.memory.setGetResponse(getMemoryOutput());
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, gcTime: Infinity, staleTime: 0 },
+ },
+ });
+ const screen = renderScreen("/agentcore/memory/get/memory-1", { core, queryClient });
+
+ await waitForText(screen.lastFrame, "show the full JSON definition");
+ core.memory.setError(new Error("background refresh failed"));
+ await queryClient.invalidateQueries({
+ queryKey: ["memory", "us-east-1", "memory-1", "full"],
+ });
+ await waitForText(screen.lastFrame, "background refresh failed");
+
+ await screen.press("return");
+ await tick();
+ expect(screen.lastFrame()).toContain("agentcore → memory → get → memory-1");
+ expect(screen.lastFrame()).not.toContain("→ json");
+ });
+});
diff --git a/src/handlers/memory/memory.test.tsx b/src/handlers/memory/memory.test.tsx
index edf086dc2..5253c8ad2 100644
--- a/src/handlers/memory/memory.test.tsx
+++ b/src/handlers/memory/memory.test.tsx
@@ -5,6 +5,7 @@ import {
createSilentLogger,
fixtureFactories,
matchGolden,
+ TestCoreClient,
TestGlobalConfigAccessor,
testIO,
} from "../../testing";
@@ -32,6 +33,21 @@ function createFixtureCore(): CoreClient {
});
}
+function testMemoryCommand() {
+ const core = new TestCoreClient();
+ const io = testIO();
+ const root = createRootHandler(core, {
+ io: io.io,
+ logger: createSilentLogger(),
+ globalConfigAccessor: new TestGlobalConfigAccessor(),
+ });
+
+ return {
+ core,
+ route: (args: string[]) => root.route(["node", "agentcore", ...args, "--region", REGION]),
+ };
+}
+
async function run(args: string[]): Promise {
const io = testIO();
const root = createRootHandler(createFixtureCore(), {
@@ -64,14 +80,28 @@ describe("memory command hierarchy", () => {
expect(view?.schema.parse(undefined)).toBeUndefined();
});
- test("prints help for bare `memory` without an SDK call", async () => {
- const stdout = await run(["memory"]);
+ test("prints help for `memory --json` without an SDK call", async () => {
+ const stdout = await run(["memory", "--json"]);
expect(stdout).toContain("Usage: agentcore memory");
expect(stdout).toContain("Commands:");
});
});
+describe("memory TUI dispatch", () => {
+ test.each([
+ ["get", ["memory", "get"]],
+ ["list", ["memory", "list"]],
+ ] as const)("opens the TUI for a bare Memory %s leaf", async (_label, args) => {
+ const { core, route } = testMemoryCommand();
+
+ await expect(route([...args])).rejects.toThrow(
+ "interactive mode requires a TTY on stdin and stdout",
+ );
+ expect(core.memory.calls).toEqual([]);
+ });
+});
+
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]);
diff --git a/src/handlers/memory/screen.tsx b/src/handlers/memory/screen.tsx
new file mode 100644
index 000000000..c811be7b9
--- /dev/null
+++ b/src/handlers/memory/screen.tsx
@@ -0,0 +1,6 @@
+import { RouterScreen } from "../../components/RouterScreen";
+import type { ScreenProps } from "../types";
+
+export function MemoryScreen(props: ScreenProps) {
+ return ;
+}