Skip to content
Draft
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
27 changes: 26 additions & 1 deletion src/cli/commands/invoke/__tests__/command.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
});
});
9 changes: 9 additions & 0 deletions src/cli/commands/invoke/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export async function loadInvokeConfig(configIO: ConfigIO = new ConfigIO()): Pro
export async function handleInvoke(context: InvokeContext, options: InvokeOptions = {}): Promise<InvokeResult> {
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);
Expand Down Expand Up @@ -669,6 +674,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption
targetName: selectedTargetName,
response,
sessionId: aguiResult.sessionId,
hasMemory,
logFilePath: logger.logFilePath,
};
}
Expand All @@ -679,6 +685,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption
targetName: selectedTargetName,
response,
sessionId: aguiResult.sessionId,
hasMemory,
logFilePath: logger.logFilePath,
};
} catch (err) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -764,6 +772,7 @@ export async function handleInvoke(context: InvokeContext, options: InvokeOption
targetName: selectedTargetName,
response: response.content,
sessionId: response.sessionId,
hasMemory,
logFilePath: logger.logFilePath,
};
}
Expand Down
27 changes: 17 additions & 10 deletions src/cli/commands/invoke/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,28 @@
// See https://stackoverflow.com/a/74181595 (why JWTs start with `eyJ`).
.replace(/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[REDACTED]')
.replace(/(client[_-]?secret["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, '$1[REDACTED]')
.replace(/((?:access[_-]?)?token["']?\s*[:=]\s*["']?)([^"',\s}]+)/gi, '$1[REDACTED]')

Check warning on line 92 in src/cli/commands/invoke/command.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe Regular Expression
);
}

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}`);
}
Expand All @@ -113,10 +120,7 @@
} 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}`);
}
Expand All @@ -141,7 +145,10 @@
.option('--gateway <name>', 'Invoke through a gateway [non-interactive]')
.option('--gateway-target-name <name>', 'HTTP runtime target on the gateway [non-interactive]')
.option('--target <name>', 'Select deployment target [non-interactive]')
.option('--session-id <id>', 'Use specific session ID for conversation continuity')
.option(
'--session-id <id>',
'Use a specific session ID (groups turns; continuity requires memory — see `agentcore add memory`)'
)
.option('--user-id <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]')
Expand Down
6 changes: 6 additions & 0 deletions src/cli/commands/invoke/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
4 changes: 2 additions & 2 deletions src/cli/tui/screens/generate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
6 changes: 4 additions & 2 deletions src/cli/tui/screens/invoke/InvokeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 13 additions & 1 deletion src/cli/tui/screens/invoke/useInvokeFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
Loading