Skip to content

Commit 04c3785

Browse files
authored
feat: enable memory in agentcore dev (#801)
1 parent bbe5452 commit 04c3785

5 files changed

Lines changed: 130 additions & 8 deletions

File tree

src/cli/commands/dev/command.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
loadProjectConfig,
1818
} from '../../operations/dev';
1919
import { getGatewayEnvVars } from '../../operations/dev/gateway-env.js';
20+
import { getMemoryEnvVars } from '../../operations/dev/memory-env.js';
2021
import { FatalError } from '../../tui/components';
2122
import { LayoutProvider } from '../../tui/context';
2223
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
@@ -292,8 +293,9 @@ export const registerDev = (program: Command) => {
292293
const configRoot = findConfigRoot(workingDir);
293294
const envVars = configRoot ? await readEnvFile(configRoot) : {};
294295
const gatewayEnvVars = await getGatewayEnvVars();
295-
// Gateway env vars go first, .env.local overrides take precedence
296-
const mergedEnvVars = { ...gatewayEnvVars, ...envVars };
296+
const memoryEnvVars = await getMemoryEnvVars();
297+
// Deployed-state env vars go first, .env.local overrides take precedence
298+
const mergedEnvVars = { ...gatewayEnvVars, ...memoryEnvVars, ...envVars };
297299
const config = getDevConfig(workingDir, project, configRoot ?? undefined, agentName);
298300

299301
if (!config) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
const { mockReadDeployedState } = vi.hoisted(() => ({
4+
mockReadDeployedState: vi.fn(),
5+
}));
6+
7+
vi.mock('../../../../lib/index.js', () => ({
8+
ConfigIO: class {
9+
readDeployedState = mockReadDeployedState;
10+
},
11+
}));
12+
13+
const { getMemoryEnvVars } = await import('../memory-env.js');
14+
15+
describe('getMemoryEnvVars', () => {
16+
afterEach(() => {
17+
vi.restoreAllMocks();
18+
});
19+
20+
it('returns empty when no deployed state', async () => {
21+
mockReadDeployedState.mockRejectedValue(new Error('not found'));
22+
const result = await getMemoryEnvVars();
23+
expect(result).toEqual({});
24+
});
25+
26+
it('returns empty when no memories deployed', async () => {
27+
mockReadDeployedState.mockResolvedValue({ targets: {} });
28+
const result = await getMemoryEnvVars();
29+
expect(result).toEqual({});
30+
});
31+
32+
it('generates MEMORY_*_ID env vars for deployed memories', async () => {
33+
mockReadDeployedState.mockResolvedValue({
34+
targets: {
35+
default: {
36+
resources: {
37+
memories: {
38+
MyAgentMemory: { memoryId: 'mem-abc123', memoryArn: 'arn:aws:bedrock:us-east-1:123:memory/mem-abc123' },
39+
},
40+
},
41+
},
42+
},
43+
});
44+
45+
const result = await getMemoryEnvVars();
46+
expect(result).toEqual({
47+
MEMORY_MYAGENTMEMORY_ID: 'mem-abc123',
48+
});
49+
});
50+
51+
it('handles multiple memories across targets', async () => {
52+
mockReadDeployedState.mockResolvedValue({
53+
targets: {
54+
default: {
55+
resources: {
56+
memories: {
57+
MyAgentMemory: { memoryId: 'mem-111', memoryArn: 'arn:1' },
58+
'other-memory': { memoryId: 'mem-222', memoryArn: 'arn:2' },
59+
},
60+
},
61+
},
62+
},
63+
});
64+
65+
const result = await getMemoryEnvVars();
66+
expect(result).toEqual({
67+
MEMORY_MYAGENTMEMORY_ID: 'mem-111',
68+
MEMORY_OTHER_MEMORY_ID: 'mem-222',
69+
});
70+
});
71+
72+
it('skips memories without memoryId', async () => {
73+
mockReadDeployedState.mockResolvedValue({
74+
targets: {
75+
default: {
76+
resources: { memories: { broken: {} } },
77+
},
78+
},
79+
});
80+
81+
const result = await getMemoryEnvVars();
82+
expect(result).toEqual({});
83+
});
84+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ConfigIO } from '../../../lib/index.js';
2+
3+
export async function getMemoryEnvVars(): Promise<Record<string, string>> {
4+
const configIO = new ConfigIO();
5+
const envVars: Record<string, string> = {};
6+
7+
try {
8+
const deployedState = await configIO.readDeployedState();
9+
10+
// Iterate all targets (not just 'default')
11+
for (const target of Object.values(deployedState?.targets ?? {})) {
12+
const memories = target?.resources?.memories ?? {};
13+
14+
for (const [name, memory] of Object.entries(memories)) {
15+
if (!memory.memoryId) continue;
16+
const sanitized = name.toUpperCase().replace(/-/g, '_');
17+
envVars[`MEMORY_${sanitized}_ID`] = memory.memoryId;
18+
}
19+
}
20+
} catch {
21+
// No deployed state — skip memory env vars
22+
}
23+
24+
return envVars;
25+
}

src/cli/tui/hooks/useDevServer.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
waitForPort,
2424
} from '../../operations/dev';
2525
import { getGatewayEnvVars } from '../../operations/dev/gateway-env.js';
26+
import { getMemoryEnvVars } from '../../operations/dev/memory-env.js';
2627
import { formatMcpToolList } from '../../operations/dev/utils';
2728
import { spawn } from 'child_process';
2829
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -60,6 +61,7 @@ export function useDevServer(options: {
6061
const [configRoot, setConfigRoot] = useState<string | undefined>(undefined);
6162
const [envVars, setEnvVars] = useState<Record<string, string>>({});
6263
const [configLoaded, setConfigLoaded] = useState(false);
64+
const [hasUndeployedMemory, setHasUndeployedMemory] = useState(false);
6365
const [targetPort] = useState(options.port);
6466
const [actualPort, setActualPort] = useState(targetPort);
6567
const actualPortRef = useRef(targetPort);
@@ -105,9 +107,17 @@ export function useDevServer(options: {
105107
if (root) {
106108
const vars = await readEnvFile(root);
107109
const gatewayEnvVars = await getGatewayEnvVars();
108-
// Gateway env vars go first, .env.local overrides take precedence
109-
const mergedEnvVars = { ...gatewayEnvVars, ...vars };
110+
const memoryEnvVars = await getMemoryEnvVars();
111+
// Deployed-state env vars go first, .env.local overrides take precedence
112+
const mergedEnvVars = { ...gatewayEnvVars, ...memoryEnvVars, ...vars };
110113
setEnvVars(mergedEnvVars);
114+
115+
// Show warning only when some configured memories aren't deployed yet
116+
const configuredMemories = cfg?.memories ?? [];
117+
if (configuredMemories.length > 0) {
118+
const deployedCount = Object.keys(memoryEnvVars).length;
119+
setHasUndeployedMemory(deployedCount < configuredMemories.length);
120+
}
111121
}
112122

113123
setConfigLoaded(true);
@@ -529,7 +539,7 @@ export function useDevServer(options: {
529539
restart,
530540
stop,
531541
logFilePath: loggerRef.current?.getRelativeLogPath(),
532-
hasMemory: (project?.memories?.length ?? 0) > 0,
542+
hasUndeployedMemory,
533543
hasVpc: project?.runtimes.find(a => a.name === config?.agentName)?.networkMode === 'VPC',
534544
protocol,
535545
mcpTools,

src/cli/tui/screens/dev/DevScreen.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export function DevScreen(props: DevScreenProps) {
191191
restart,
192192
stop,
193193
logFilePath,
194-
hasMemory,
194+
hasUndeployedMemory,
195195
hasVpc,
196196
protocol,
197197
mcpTools,
@@ -527,9 +527,10 @@ export function DevScreen(props: DevScreenProps) {
527527
</Box>
528528
)}
529529
{logFilePath && <LogLink filePath={logFilePath} />}
530-
{protocol !== 'MCP' && hasMemory && (
530+
{protocol !== 'MCP' && hasUndeployedMemory && (
531531
<Text color="yellow">
532-
AgentCore memory is not available when running locally. To test memory, deploy and use invoke.
532+
AgentCore memory must be deployed before it can be used. To test memory, run `agentcore deploy` and restart
533+
dev.
533534
</Text>
534535
)}
535536
{hasVpc && (

0 commit comments

Comments
 (0)