diff --git a/docs/commands.md b/docs/commands.md index fbfd5c7f..4cfb64ea 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -475,22 +475,23 @@ Start local development server with hot-reload. agentcore dev agentcore dev --agent MyAgent --port 3000 agentcore dev --logs # Non-interactive -agentcore dev --invoke "Hello" --stream # Direct invoke +agentcore dev "Hello" --stream # Invoke running dev server +agentcore dev "Hello" --agent MyAgent # Invoke specific agent # MCP protocol dev commands -agentcore dev --invoke list-tools -agentcore dev --invoke call-tool --tool myTool --input '{"arg": "value"}' +agentcore dev list-tools +agentcore dev call-tool --tool myTool --input '{"arg": "value"}' ``` -| Flag | Description | -| ----------------------- | ------------------------------------------------------ | -| `-p, --port ` | Port (default: 8080; MCP uses 8000, A2A uses 9000) | -| `-a, --agent ` | Agent to run (required if multiple agents) | -| `-i, --invoke ` | Invoke running server | -| `-s, --stream` | Stream response (with --invoke) | -| `-l, --logs` | Non-interactive stdout logging | -| `--tool ` | MCP tool name (with `--invoke call-tool`) | -| `--input ` | MCP tool arguments as JSON (with `--invoke call-tool`) | +| Flag / Argument | Description | +| -------------------- | ---------------------------------------------------- | +| `[prompt]` | Send a prompt to a running dev server | +| `-p, --port ` | Port (default: 8080; MCP uses 8000, A2A uses 9000) | +| `-a, --agent ` | Agent to run or invoke (required if multiple agents) | +| `-s, --stream` | Stream response when invoking | +| `-l, --logs` | Non-interactive stdout logging | +| `--tool ` | MCP tool name (with `call-tool` prompt) | +| `--input ` | MCP tool arguments as JSON (with `--tool`) | ### invoke diff --git a/docs/local-development.md b/docs/local-development.md index 284bfd6a..1c2400ad 100644 --- a/docs/local-development.md +++ b/docs/local-development.md @@ -20,20 +20,16 @@ agentcore dev --logs ## Invoking Local Agents -With the dev server running, open another terminal: +Start the dev server in one terminal, then send prompts from another: ```bash -# Interactive chat -agentcore invoke - -# Single prompt -agentcore invoke "What can you do?" - -# With streaming -agentcore invoke "Tell me a story" --stream +# Terminal 1: start the dev server +agentcore dev --logs -# Direct invoke to running server -agentcore dev --invoke "Hello" --stream +# Terminal 2: send prompts +agentcore dev "What can you do?" +agentcore dev "Tell me a story" --stream +agentcore dev "Hello" --agent MyAgent ``` ## Environment Setup diff --git a/src/cli/commands/dev/__tests__/dev.test.ts b/src/cli/commands/dev/__tests__/dev.test.ts index 61c3d867..d370bcb3 100644 --- a/src/cli/commands/dev/__tests__/dev.test.ts +++ b/src/cli/commands/dev/__tests__/dev.test.ts @@ -7,13 +7,20 @@ describe('dev command', () => { const result = await runCLI(['dev', '--help'], process.cwd()); expect(result.exitCode).toBe(0); + expect(result.stdout.includes('[prompt]'), 'Should show [prompt] positional argument').toBeTruthy(); expect(result.stdout.includes('--port'), 'Should show --port option').toBeTruthy(); expect(result.stdout.includes('--agent'), 'Should show --agent option').toBeTruthy(); - expect(result.stdout.includes('--invoke'), 'Should show --invoke option').toBeTruthy(); expect(result.stdout.includes('--stream'), 'Should show --stream option').toBeTruthy(); expect(result.stdout.includes('--logs'), 'Should show --logs option').toBeTruthy(); expect(result.stdout.includes('8080'), 'Should show default port').toBeTruthy(); }); + + it('does not show --invoke flag', async () => { + const result = await runCLI(['dev', '--help'], process.cwd()); + + expect(result.exitCode).toBe(0); + expect(result.stdout.includes('--invoke'), 'Should not show removed --invoke option').toBeFalsy(); + }); }); describe('requires project context', () => { @@ -28,6 +35,33 @@ describe('dev command', () => { }); }); + describe('positional prompt invoke', () => { + it('exits with helpful error when no server running and no project found', async () => { + // With no dev server running, invoke path shows connection error + const result = await runCLI(['dev', 'Hello agent'], process.cwd()); + + expect(result.exitCode).toBe(1); + const output = result.stderr.toLowerCase(); + expect( + output.includes('dev server not running'), + `Should mention dev server not running, got: ${result.stderr}` + ).toBeTruthy(); + }); + + it('does not go through requireProject guard when invoking', async () => { + // Invoke path uses loadProjectConfig (soft check), not requireProject() + // The error should mention the dev server, not the generic project guard message + const result = await runCLI(['dev', 'test prompt'], process.cwd()); + + expect(result.exitCode).toBe(1); + const output = result.stderr.toLowerCase(); + expect( + !output.includes('run agentcore create'), + `Should not show the requireProject guard message, got: ${result.stderr}` + ).toBeTruthy(); + }); + }); + describe('flag validation', () => { it('rejects invalid port number', async () => { const result = await runCLI(['dev', '--port', 'abc'], process.cwd()); @@ -46,7 +80,6 @@ describe('dev command', () => { expect(result.exitCode).toBe(0); expect(result.stdout.includes('--stream'), 'Should show --stream option').toBeTruthy(); - expect(result.stdout.includes('--invoke'), 'Should show --invoke option').toBeTruthy(); }); }); }); diff --git a/src/cli/commands/dev/command.tsx b/src/cli/commands/dev/command.tsx index afd352ca..20b8acd2 100644 --- a/src/cli/commands/dev/command.tsx +++ b/src/cli/commands/dev/command.tsx @@ -48,9 +48,9 @@ async function invokeDevServer( console.log(response); } } catch (err) { - if (err instanceof Error && err.message.includes('ECONNREFUSED')) { + if (isConnectionRefused(err)) { console.error(`Error: Dev server not running on port ${port}`); - console.error('Start it with: agentcore dev'); + console.error('Start it with: agentcore dev --logs'); } else { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); } @@ -65,9 +65,9 @@ async function invokeA2ADevServer(port: number, prompt: string, headers?: Record } process.stdout.write('\n'); } catch (err) { - if (err instanceof Error && err.message.includes('ECONNREFUSED')) { + if (isConnectionRefused(err)) { console.error(`Error: Dev server not running on port ${port}`); - console.error('Start it with: agentcore dev'); + console.error('Start it with: agentcore dev --logs'); } else { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); } @@ -75,6 +75,14 @@ async function invokeA2ADevServer(port: number, prompt: string, headers?: Record } } +function isConnectionRefused(err: unknown): boolean { + if (!(err instanceof Error)) return false; + // ConnectionError from invoke.ts wraps fetch failures after retries + if (err.name === 'ConnectionError') return true; + const msg = err.message + (err.cause instanceof Error ? err.cause.message : ''); + return msg.includes('ECONNREFUSED') || msg.includes('fetch failed'); +} + async function handleMcpInvoke( port: number, invokeValue: string, @@ -96,8 +104,8 @@ async function handleMcpInvoke( } } else if (invokeValue === 'call-tool') { if (!toolName) { - console.error('Error: --tool is required with --invoke call-tool'); - console.error('Usage: agentcore dev --invoke call-tool --tool --input \'{"arg": "value"}\''); + console.error('Error: --tool is required with call-tool'); + console.error('Usage: agentcore dev call-tool --tool --input \'{"arg": "value"}\''); process.exit(1); } // Initialize session first, then call tool with the session ID @@ -117,14 +125,14 @@ async function handleMcpInvoke( } else { console.error(`Error: Unknown MCP invoke command "${invokeValue}"`); console.error('Usage:'); - console.error(' agentcore dev --invoke list-tools'); - console.error(' agentcore dev --invoke call-tool --tool --input \'{"arg": "value"}\''); + console.error(' agentcore dev list-tools'); + console.error(' agentcore dev call-tool --tool --input \'{"arg": "value"}\''); process.exit(1); } } catch (err) { - if (err instanceof Error && err.message.includes('ECONNREFUSED')) { + if (isConnectionRefused(err)) { console.error(`Error: Dev server not running on port ${port}`); - console.error('Start it with: agentcore dev'); + console.error('Start it with: agentcore dev --logs'); } else { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); } @@ -137,20 +145,20 @@ export const registerDev = (program: Command) => { .command('dev') .alias('d') .description(COMMAND_DESCRIPTIONS.dev) + .argument('[prompt]', 'Send a prompt to a running dev server [non-interactive]') .option('-p, --port ', 'Port for development server', '8080') .option('-a, --agent ', 'Agent to run or invoke (required if multiple agents)') - .option('-i, --invoke ', 'Invoke running dev server (use --agent if multiple) [non-interactive]') - .option('-s, --stream', 'Stream response when using --invoke [non-interactive]') + .option('-s, --stream', 'Stream response when invoking [non-interactive]') .option('-l, --logs', 'Run dev server with logs to stdout [non-interactive]') - .option('--tool ', 'MCP tool name (used with --invoke call-tool) [non-interactive]') - .option('--input ', 'MCP tool arguments as JSON (used with --invoke call-tool) [non-interactive]') + .option('--tool ', 'MCP tool name (used with "call-tool" prompt) [non-interactive]') + .option('--input ', 'MCP tool arguments as JSON (used with --tool) [non-interactive]') .option( '-H, --header
', 'Custom header to forward to the agent (format: "Name: Value", repeatable) [non-interactive]', (val: string, prev: string[]) => [...prev, val], [] as string[] ) - .action(async opts => { + .action(async (positionalPrompt: string | undefined, opts) => { try { const port = parseInt(opts.port, 10); @@ -160,9 +168,11 @@ export const registerDev = (program: Command) => { headers = parseHeaderFlags(opts.header); } - // If --invoke provided, call the dev server and exit - if (opts.invoke) { - const invokeProject = await loadProjectConfig(getWorkingDirectory()); + // If a prompt is provided, invoke a running dev server + const invokePrompt = positionalPrompt; + if (invokePrompt !== undefined) { + const workingDir = getWorkingDirectory(); + const invokeProject = await loadProjectConfig(workingDir); // Determine which agent/port to invoke let invokePort = port; @@ -190,11 +200,11 @@ export const registerDev = (program: Command) => { // Protocol-aware dispatch if (protocol === 'MCP') { - await handleMcpInvoke(invokePort, opts.invoke, opts.tool, opts.input, headers); + await handleMcpInvoke(invokePort, invokePrompt, opts.tool, opts.input, headers); } else if (protocol === 'A2A') { - await invokeA2ADevServer(invokePort, opts.invoke, headers); + await invokeA2ADevServer(invokePort, invokePrompt, headers); } else { - await invokeDevServer(invokePort, opts.invoke, opts.stream ?? false, headers); + await invokeDevServer(invokePort, invokePrompt, opts.stream ?? false, headers); } return; } diff --git a/src/cli/operations/dev/dev-server.ts b/src/cli/operations/dev/dev-server.ts index a0d3c19d..10424c0d 100644 --- a/src/cli/operations/dev/dev-server.ts +++ b/src/cli/operations/dev/dev-server.ts @@ -82,9 +82,10 @@ export abstract class DevServer { kill(): void { if (!this.child || this.child.killed) return; this.child.kill('SIGTERM'); - setTimeout(() => { + const killTimer = setTimeout(() => { if (this.child && !this.child.killed) this.child.kill('SIGKILL'); }, 2000); + killTimer.unref(); } /** Mode-specific setup (e.g., venv creation, container image build). Returns false to abort. */ diff --git a/src/cli/operations/dev/index.ts b/src/cli/operations/dev/index.ts index ae3c8edd..5c11b692 100644 --- a/src/cli/operations/dev/index.ts +++ b/src/cli/operations/dev/index.ts @@ -1,6 +1,7 @@ export { findAvailablePort, waitForPort, + waitForServerReady, createDevServer, DevServer, type LogLevel, diff --git a/src/cli/operations/dev/server.ts b/src/cli/operations/dev/server.ts index 1f3b3751..050e1bbb 100644 --- a/src/cli/operations/dev/server.ts +++ b/src/cli/operations/dev/server.ts @@ -7,7 +7,7 @@ import type { DevServer, DevServerOptions } from './dev-server'; * Dev server barrel module. * Re-exports types, utilities, and the factory function. */ -export { findAvailablePort, waitForPort } from './utils'; +export { findAvailablePort, waitForPort, waitForServerReady } from './utils'; export { DevServer, type LogLevel, type DevServerCallbacks, type DevServerOptions } from './dev-server'; export { CodeZipDevServer } from './codezip-dev-server'; export { ContainerDevServer } from './container-dev-server'; diff --git a/src/cli/tui/copy.ts b/src/cli/tui/copy.ts index 7e0ddf78..f1cce21a 100644 --- a/src/cli/tui/copy.ts +++ b/src/cli/tui/copy.ts @@ -32,7 +32,7 @@ export const COMMAND_DESCRIPTIONS = { add: 'Add resources (agent, evaluator, online-eval, memory, credential, target)', create: 'Create a new AgentCore project', deploy: 'Deploy project infrastructure to AWS via CDK.', - dev: 'Launch local development server with hot-reload.', + dev: 'Launch local dev server, or invoke an agent locally.', invoke: 'Invoke a deployed agent endpoint.', logs: 'Stream or search agent runtime logs.', package: 'Package agent artifacts without deploying.',