diff --git a/src/cli/commands/dev/command.tsx b/src/cli/commands/dev/command.tsx index a501a256..1f2ca8af 100644 --- a/src/cli/commands/dev/command.tsx +++ b/src/cli/commands/dev/command.tsx @@ -17,6 +17,7 @@ import { loadProjectConfig, } from '../../operations/dev'; import { getGatewayEnvVars } from '../../operations/dev/gateway-env.js'; +import { getMemoryEnvVars } from '../../operations/dev/memory-env.js'; import { FatalError } from '../../tui/components'; import { LayoutProvider } from '../../tui/context'; import { COMMAND_DESCRIPTIONS } from '../../tui/copy'; @@ -292,8 +293,9 @@ export const registerDev = (program: Command) => { const configRoot = findConfigRoot(workingDir); const envVars = configRoot ? await readEnvFile(configRoot) : {}; const gatewayEnvVars = await getGatewayEnvVars(); - // Gateway env vars go first, .env.local overrides take precedence - const mergedEnvVars = { ...gatewayEnvVars, ...envVars }; + const memoryEnvVars = await getMemoryEnvVars(); + // Deployed-state env vars go first, .env.local overrides take precedence + const mergedEnvVars = { ...gatewayEnvVars, ...memoryEnvVars, ...envVars }; const config = getDevConfig(workingDir, project, configRoot ?? undefined, agentName); if (!config) { diff --git a/src/cli/operations/dev/__tests__/memory-env.test.ts b/src/cli/operations/dev/__tests__/memory-env.test.ts new file mode 100644 index 00000000..9802d32a --- /dev/null +++ b/src/cli/operations/dev/__tests__/memory-env.test.ts @@ -0,0 +1,84 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const { mockReadDeployedState } = vi.hoisted(() => ({ + mockReadDeployedState: vi.fn(), +})); + +vi.mock('../../../../lib/index.js', () => ({ + ConfigIO: class { + readDeployedState = mockReadDeployedState; + }, +})); + +const { getMemoryEnvVars } = await import('../memory-env.js'); + +describe('getMemoryEnvVars', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('returns empty when no deployed state', async () => { + mockReadDeployedState.mockRejectedValue(new Error('not found')); + const result = await getMemoryEnvVars(); + expect(result).toEqual({}); + }); + + it('returns empty when no memories deployed', async () => { + mockReadDeployedState.mockResolvedValue({ targets: {} }); + const result = await getMemoryEnvVars(); + expect(result).toEqual({}); + }); + + it('generates MEMORY_*_ID env vars for deployed memories', async () => { + mockReadDeployedState.mockResolvedValue({ + targets: { + default: { + resources: { + memories: { + MyAgentMemory: { memoryId: 'mem-abc123', memoryArn: 'arn:aws:bedrock:us-east-1:123:memory/mem-abc123' }, + }, + }, + }, + }, + }); + + const result = await getMemoryEnvVars(); + expect(result).toEqual({ + MEMORY_MYAGENTMEMORY_ID: 'mem-abc123', + }); + }); + + it('handles multiple memories across targets', async () => { + mockReadDeployedState.mockResolvedValue({ + targets: { + default: { + resources: { + memories: { + MyAgentMemory: { memoryId: 'mem-111', memoryArn: 'arn:1' }, + 'other-memory': { memoryId: 'mem-222', memoryArn: 'arn:2' }, + }, + }, + }, + }, + }); + + const result = await getMemoryEnvVars(); + expect(result).toEqual({ + MEMORY_MYAGENTMEMORY_ID: 'mem-111', + MEMORY_OTHER_MEMORY_ID: 'mem-222', + }); + }); + + it('skips memories without memoryId', async () => { + mockReadDeployedState.mockResolvedValue({ + targets: { + default: { + resources: { memories: { broken: {} } }, + }, + }, + }); + + const result = await getMemoryEnvVars(); + expect(result).toEqual({}); + }); +}); diff --git a/src/cli/operations/dev/memory-env.ts b/src/cli/operations/dev/memory-env.ts new file mode 100644 index 00000000..866e462b --- /dev/null +++ b/src/cli/operations/dev/memory-env.ts @@ -0,0 +1,25 @@ +import { ConfigIO } from '../../../lib/index.js'; + +export async function getMemoryEnvVars(): Promise> { + const configIO = new ConfigIO(); + const envVars: Record = {}; + + try { + const deployedState = await configIO.readDeployedState(); + + // Iterate all targets (not just 'default') + for (const target of Object.values(deployedState?.targets ?? {})) { + const memories = target?.resources?.memories ?? {}; + + for (const [name, memory] of Object.entries(memories)) { + if (!memory.memoryId) continue; + const sanitized = name.toUpperCase().replace(/-/g, '_'); + envVars[`MEMORY_${sanitized}_ID`] = memory.memoryId; + } + } + } catch { + // No deployed state — skip memory env vars + } + + return envVars; +} diff --git a/src/cli/tui/hooks/useDevServer.ts b/src/cli/tui/hooks/useDevServer.ts index b466e816..f7fbb92c 100644 --- a/src/cli/tui/hooks/useDevServer.ts +++ b/src/cli/tui/hooks/useDevServer.ts @@ -23,6 +23,7 @@ import { waitForPort, } from '../../operations/dev'; import { getGatewayEnvVars } from '../../operations/dev/gateway-env.js'; +import { getMemoryEnvVars } from '../../operations/dev/memory-env.js'; import { formatMcpToolList } from '../../operations/dev/utils'; import { spawn } from 'child_process'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -60,6 +61,7 @@ export function useDevServer(options: { const [configRoot, setConfigRoot] = useState(undefined); const [envVars, setEnvVars] = useState>({}); const [configLoaded, setConfigLoaded] = useState(false); + const [hasUndeployedMemory, setHasUndeployedMemory] = useState(false); const [targetPort] = useState(options.port); const [actualPort, setActualPort] = useState(targetPort); const actualPortRef = useRef(targetPort); @@ -105,9 +107,17 @@ export function useDevServer(options: { if (root) { const vars = await readEnvFile(root); const gatewayEnvVars = await getGatewayEnvVars(); - // Gateway env vars go first, .env.local overrides take precedence - const mergedEnvVars = { ...gatewayEnvVars, ...vars }; + const memoryEnvVars = await getMemoryEnvVars(); + // Deployed-state env vars go first, .env.local overrides take precedence + const mergedEnvVars = { ...gatewayEnvVars, ...memoryEnvVars, ...vars }; setEnvVars(mergedEnvVars); + + // Show warning only when some configured memories aren't deployed yet + const configuredMemories = cfg?.memories ?? []; + if (configuredMemories.length > 0) { + const deployedCount = Object.keys(memoryEnvVars).length; + setHasUndeployedMemory(deployedCount < configuredMemories.length); + } } setConfigLoaded(true); @@ -529,7 +539,7 @@ export function useDevServer(options: { restart, stop, logFilePath: loggerRef.current?.getRelativeLogPath(), - hasMemory: (project?.memories?.length ?? 0) > 0, + hasUndeployedMemory, hasVpc: project?.runtimes.find(a => a.name === config?.agentName)?.networkMode === 'VPC', protocol, mcpTools, diff --git a/src/cli/tui/screens/dev/DevScreen.tsx b/src/cli/tui/screens/dev/DevScreen.tsx index fc08fd4d..f636e20e 100644 --- a/src/cli/tui/screens/dev/DevScreen.tsx +++ b/src/cli/tui/screens/dev/DevScreen.tsx @@ -191,7 +191,7 @@ export function DevScreen(props: DevScreenProps) { restart, stop, logFilePath, - hasMemory, + hasUndeployedMemory, hasVpc, protocol, mcpTools, @@ -527,9 +527,10 @@ export function DevScreen(props: DevScreenProps) { )} {logFilePath && } - {protocol !== 'MCP' && hasMemory && ( + {protocol !== 'MCP' && hasUndeployedMemory && ( - AgentCore memory is not available when running locally. To test memory, deploy and use invoke. + AgentCore memory must be deployed before it can be used. To test memory, run `agentcore deploy` and restart + dev. )} {hasVpc && (