From 459af3d64f31a76df123e9a98acdcc5949e604ae Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Thu, 25 Jun 2026 06:14:35 +0000 Subject: [PATCH] fix(unit-only): make invoke continuity messaging memory-aware (#1591) Suppress the 'To resume --session-id' hint and reword the --session-id help text and MEMORY_OPTIONS descriptions for memory-less agents, where session continuity is genuinely inert. Threads hasMemory through the invoke action and TUI flow so the resume hint only renders when the resolved agent has memory configured. Refs aws/agentcore-cli#1591 --- .../commands/invoke/__tests__/command.test.ts | 27 ++++++++++++++++++- src/cli/commands/invoke/action.ts | 9 +++++++ src/cli/commands/invoke/command.tsx | 27 ++++++++++++------- src/cli/commands/invoke/types.ts | 6 +++++ src/cli/tui/screens/generate/types.ts | 4 +-- src/cli/tui/screens/invoke/InvokeScreen.tsx | 6 +++-- src/cli/tui/screens/invoke/useInvokeFlow.ts | 14 +++++++++- 7 files changed, 77 insertions(+), 16 deletions(-) diff --git a/src/cli/commands/invoke/__tests__/command.test.ts b/src/cli/commands/invoke/__tests__/command.test.ts index d24ec0978..7ce7a032e 100644 --- a/src/cli/commands/invoke/__tests__/command.test.ts +++ b/src/cli/commands/invoke/__tests__/command.test.ts @@ -1,6 +1,6 @@ // Tests for invoke CLI mode — exitCode propagation and flag validation import { handleInvoke } from '../action.js'; -import { registerInvoke } from '../command.js'; +import { printInvokeResult, registerInvoke } from '../command.js'; import { resolvePrompt } from '../resolve-prompt.js'; import { Command } from '@commander-js/extra-typings'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -146,3 +146,28 @@ describe('invoke CLI mode — exitCode propagation', () => { expect(erred.join('')).not.toContain('--additional-params'); }); }); + +describe('printInvokeResult resume hint', () => { + let erred: string[]; + + beforeEach(() => { + erred = []; + vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => erred.push(args.join(' '))); + vi.spyOn(console, 'log').mockImplementation(() => undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('suppresses the "To resume" hint when the target has no memory', () => { + printInvokeResult({ success: true, response: 'hi', sessionId: 'abc', hasMemory: false }, {}); + expect(erred.join('\n')).toContain('Session: abc'); + expect(erred.join('\n')).not.toContain('To resume'); + }); + + it('shows the "To resume" hint when the target has memory', () => { + printInvokeResult({ success: true, response: 'hi', sessionId: 'abc', hasMemory: true }, {}); + expect(erred.join('\n')).toContain('To resume: agentcore invoke --session-id abc'); + }); +}); diff --git a/src/cli/commands/invoke/action.ts b/src/cli/commands/invoke/action.ts index 7840b50b0..f6b9f9831 100644 --- a/src/cli/commands/invoke/action.ts +++ b/src/cli/commands/invoke/action.ts @@ -48,6 +48,11 @@ export async function loadInvokeConfig(configIO: ConfigIO = new ConfigIO()): Pro export async function handleInvoke(context: InvokeContext, options: InvokeOptions = {}): Promise { const { project, deployedState, awsTargets } = context; + // Session continuity only exists when memory is configured: the no-memory + // template is stateless per turn, so re-invoking with the same --session-id + // resumes nothing. Drives whether the "To resume" hint is shown downstream. + const hasMemory = (project.memories?.length ?? 0) > 0; + // Gateway invoke: route through a deployed gateway if (options.gateway) { const targetNames = Object.keys(deployedState.targets); @@ -669,6 +674,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption targetName: selectedTargetName, response, sessionId: aguiResult.sessionId, + hasMemory, logFilePath: logger.logFilePath, }; } @@ -679,6 +685,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption targetName: selectedTargetName, response, sessionId: aguiResult.sessionId, + hasMemory, logFilePath: logger.logFilePath, }; } catch (err) { @@ -733,6 +740,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption targetName: selectedTargetName, response: fullResponse, sessionId: result.sessionId, + hasMemory, logFilePath: logger.logFilePath, }; } catch (err) { @@ -764,6 +772,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption targetName: selectedTargetName, response: response.content, sessionId: response.sessionId, + hasMemory, logFilePath: logger.logFilePath, }; } diff --git a/src/cli/commands/invoke/command.tsx b/src/cli/commands/invoke/command.tsx index 574aa1357..7a12b2724 100644 --- a/src/cli/commands/invoke/command.tsx +++ b/src/cli/commands/invoke/command.tsx @@ -93,17 +93,24 @@ export function redactSensitiveText(value: string): string { ); } -function printInvokeResult(result: InvokeResult, options: InvokeOptions): void { +export function printInvokeResult(result: InvokeResult, options: InvokeOptions): void { + // Resume continuity only works when the target has memory; without it the + // agent is stateless per turn, so promising "To resume" would be misleading. + const printSession = (): void => { + if (!result.sessionId) return; + console.error(`\nSession: ${result.sessionId}`); + if (result.hasMemory) { + console.error(`To resume: agentcore invoke --session-id ${result.sessionId}`); + } + }; + if (options.json) { const serialized = serializeResult(result); if (typeof serialized.response === 'string') serialized.response = redactSensitiveText(serialized.response); if (typeof serialized.error === 'string') serialized.error = redactSensitiveText(serialized.error); console.log(JSON.stringify(serialized)); } else if (options.stream) { - if (result.sessionId) { - console.error(`\nSession: ${result.sessionId}`); - console.error(`To resume: agentcore invoke --session-id ${result.sessionId}`); - } + printSession(); if (result.logFilePath) { console.error(`Log: ${result.logFilePath}`); } @@ -113,10 +120,7 @@ function printInvokeResult(result: InvokeResult, options: InvokeOptions): void { } else if (!result.success && result.error) { console.error(redactSensitiveText(result.error.message)); } - if (result.sessionId) { - console.error(`\nSession: ${result.sessionId}`); - console.error(`To resume: agentcore invoke --session-id ${result.sessionId}`); - } + printSession(); if (result.logFilePath) { console.error(`Log: ${result.logFilePath}`); } @@ -141,7 +145,10 @@ export const registerInvoke = (program: Command) => { .option('--gateway ', 'Invoke through a gateway [non-interactive]') .option('--gateway-target-name ', 'HTTP runtime target on the gateway [non-interactive]') .option('--target ', 'Select deployment target [non-interactive]') - .option('--session-id ', 'Use specific session ID for conversation continuity') + .option( + '--session-id ', + 'Use a specific session ID (groups turns; continuity requires memory — see `agentcore add memory`)' + ) .option('--user-id ', 'User ID for runtime invocation (default: "default-user")') .option('--json', 'Output as JSON [non-interactive]') .option('--stream', 'Stream response in real-time (TUI streams by default) [non-interactive]') diff --git a/src/cli/commands/invoke/types.ts b/src/cli/commands/invoke/types.ts index 66367c019..0ba4db951 100644 --- a/src/cli/commands/invoke/types.ts +++ b/src/cli/commands/invoke/types.ts @@ -81,4 +81,10 @@ export type InvokeResult = Result & { response?: string; sessionId?: string; exitCode?: number; + /** + * True when the resolved target has memory configured. Without memory the + * agent is stateless per turn, so re-invoking with the same --session-id + * resumes nothing — the "To resume" hint is suppressed in that case. + */ + hasMemory?: boolean; }; diff --git a/src/cli/tui/screens/generate/types.ts b/src/cli/tui/screens/generate/types.ts index 30bbaf066..d6585b3a2 100644 --- a/src/cli/tui/screens/generate/types.ts +++ b/src/cli/tui/screens/generate/types.ts @@ -250,7 +250,7 @@ export function validateDockerfileInput(value: string): true | string { } export const MEMORY_OPTIONS = [ - { id: 'none', title: 'None', description: 'No memory' }, - { id: 'shortTerm', title: 'Short-term memory', description: 'Context within a session' }, + { id: 'none', title: 'None', description: 'No memory - each turn is a fresh, stateless session' }, + { id: 'shortTerm', title: 'Short-term memory', description: 'Remembers prior turns within a session' }, { id: 'longAndShortTerm', title: 'Long-term and short-term', description: 'Persists across sessions' }, ] as const; diff --git a/src/cli/tui/screens/invoke/InvokeScreen.tsx b/src/cli/tui/screens/invoke/InvokeScreen.tsx index 092076a1e..38808d997 100644 --- a/src/cli/tui/screens/invoke/InvokeScreen.tsx +++ b/src/cli/tui/screens/invoke/InvokeScreen.tsx @@ -208,12 +208,14 @@ export function InvokeScreen({ const mcpFetchTriggeredRef = useRef(false); useEffect(() => { - if (sessionId && messages.length > 0) { + // Only promise resume when memory is configured — without it the agent is + // stateless per turn and re-invoking with the same session ID resumes nothing. + if (sessionId && messages.length > 0 && config?.hasMemory) { const cyan = '\x1b[36m'; const reset = '\x1b[0m'; setExitMessage(`To resume this session, run: ${cyan}agentcore invoke --session-id ${sessionId}${reset}`); } - }, [sessionId, messages.length]); + }, [sessionId, messages.length, config?.hasMemory]); // Compute auth type early so hooks can reference it const totalInvokables = (config?.runtimes.length ?? 0) + (config?.harnesses.length ?? 0); diff --git a/src/cli/tui/screens/invoke/useInvokeFlow.ts b/src/cli/tui/screens/invoke/useInvokeFlow.ts index fd01237b6..8a98c19dd 100644 --- a/src/cli/tui/screens/invoke/useInvokeFlow.ts +++ b/src/cli/tui/screens/invoke/useInvokeFlow.ts @@ -64,6 +64,11 @@ export interface InvokeConfig { target: AwsDeploymentTarget; targetName: string; projectName: string; + /** + * True when the project has memory configured. Without it the agent is + * stateless per turn, so the "To resume" exit hint is suppressed. + */ + hasMemory: boolean; } export interface InvokeFlowOptions { @@ -263,7 +268,14 @@ export function useInvokeFlow(options: InvokeFlowOptions = {}): InvokeFlowState }; } - setConfig({ runtimes, harnesses, target: targetConfig, targetName, projectName: project.name }); + setConfig({ + runtimes, + harnesses, + target: targetConfig, + targetName, + projectName: project.name, + hasMemory: (project.memories?.length ?? 0) > 0, + }); if (initialHarnessName) { const harnessIdx = harnesses.findIndex(h => h.name === initialHarnessName);