Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

```
Expand Down Expand Up @@ -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.

Expand All @@ -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
```

---
Expand Down
20 changes: 20 additions & 0 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -255,6 +258,23 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/runtime/endpoint/list/:runtimeId"
element={<RuntimeListEndpointsScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/memory" element={<MemoryScreen ctx={ctx} core={core} />} />
<Route
path="agentcore/memory/get"
element={<Navigate to="/agentcore/memory/list" replace />}
/>
<Route
path="agentcore/memory/list"
element={<MemoryListScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/memory/get/:memoryId"
element={<MemoryGetScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/memory/get/:memoryId/json"
element={<MemoryGetJsonScreen ctx={ctx} core={core} />}
/>
<Route path="*" element={<HelpScreen ctx={ctx} core={core} />} />
</Routes>
</MemoryRouter>
Expand Down
103 changes: 103 additions & 0 deletions src/handlers/memory/get/screen.tsx
Original file line number Diff line number Diff line change
@@ -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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love to see us using so many common components!

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: do we want to allow the user to refresh even if there isn't an error? (ex. maybe something changed and they want to check).

This looks consistent with https://github.com/aws/agentcore-cli/blob/refactor/src/handlers/runtime/get/screen.tsx#L48 so just curious what behavior we want.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch I think we would want that right? maybe I created a new resource while the TUI screen was open and I just want to refresh it to see if it shows up.

Let me add that quickly in a follow up in both places

void detail.refetch();
return;
}
if (detail.isError || !memory) return;
if (key.return && memoryId) {
navigate(`/agentcore/memory/get/${encodeURIComponent(memoryId)}/json`);
}
});

return (
<Layout
breadcrumb={["agentcore", "memory", "get", memoryId ?? ""]}
keyHints={[
...(!detail.isPending && !detail.isError ? [{ key: "enter", label: "open detail" }] : []),
...(detail.isError ? [{ key: "r", label: "retry" }] : []),
{ key: "esc", label: "back" },
{ key: "ctl+c", label: "quit" },
]}
>
{detail.isPending ? (
<Spinner label="Loading Memory…" />
) : detail.isError ? (
<Text color="red">Error: {(detail.error as Error).message}</Text>
) : (
<Box flexDirection="column">
<Box flexDirection="column" paddingLeft={1}>
<KeyValueTable
items={{
name: memory?.name ?? "",
id: memory?.id ?? "",
status: memory?.status ?? "",
eventExpiryDays: memory?.eventExpiryDuration?.toString() ?? "-",
strategies: memory?.strategies?.length.toString() ?? "0",
updatedAt: memory?.updatedAt?.toISOString() ?? "-",
...(memory?.failureReason ? { failureReason: memory.failureReason } : {}),
arn: memory?.arn ?? "",
}}
/>
</Box>

<Divider />

<Box paddingLeft={1}>
<Text color={darkTheme.colors.focus}>❯ </Text>
<Text bold color={darkTheme.colors.focus}>
{"detail".padEnd(9)}
</Text>
<Text color={darkTheme.colors.muted}>show the full JSON definition</Text>
</Box>
</Box>
)}
</Layout>
);
}

export function MemoryGetJsonScreen(props: ScreenProps) {
const { memoryId } = useParams();
const detail = useMemoryDetail(props, memoryId);

return (
<JsonDetail
breadcrumb={["agentcore", "memory", "get", memoryId ?? "", "json"]}
isPending={detail.isPending}
error={detail.isError ? (detail.error as Error) : null}
data={detail.data?.memory}
loadingLabel="Loading Memory…"
onRetry={() => void detail.refetch()}
/>
);
}
6 changes: 4 additions & 2 deletions src/handlers/memory/index.tsx
Original file line number Diff line number Diff line change
@@ -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));
}
60 changes: 60 additions & 0 deletions src/handlers/memory/list/screen.tsx
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
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<MemoryRow>[];

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 (
<PaginatedTablePicker
breadcrumb={["agentcore", "memory", "list"]}
queryKey={["memories", opts.region]}
loadPage={async (token, pageSize) => {
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."
/>
);
}
Loading
Loading