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