diff --git a/src/cli/operations/dev/__tests__/container-dev-server.test.ts b/src/cli/operations/dev/__tests__/container-dev-server.test.ts index 68e8264b..3b96bd0a 100644 --- a/src/cli/operations/dev/__tests__/container-dev-server.test.ts +++ b/src/cli/operations/dev/__tests__/container-dev-server.test.ts @@ -40,6 +40,24 @@ function createMockChildProcess() { return proc; } +/** Create a mock child process that auto-closes with the given exit code (for build). */ +function createMockBuildProcess(exitCode = 0, stdoutData?: string) { + const proc = createMockChildProcess(); + // Emit 'close' after the listener is attached to guarantee correct ordering + const origOn = proc.on.bind(proc); + proc.on = function (event: string, fn: (...args: any[]) => void) { + origOn(event, fn); + if (event === 'close') { + process.nextTick(() => { + if (stdoutData) proc.stdout.emit('data', Buffer.from(stdoutData)); + proc.emit('close', exitCode); + }); + } + return proc; + }; + return proc; +} + function mockSuccessfulPrepare() { // Runtime detected mockDetectContainerRuntime.mockResolvedValue({ @@ -48,11 +66,12 @@ function mockSuccessfulPrepare() { }); // Dockerfile exists (first call), ~/.aws exists (second call in getSpawnConfig) mockExistsSync.mockReturnValue(true); - // rm, base build, dev build all succeed + // rm succeeds (spawnSync) mockSpawnSync.mockReturnValue({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }); - // spawn for the actual server + // spawn: first call = build (auto-closes with 0), second call = server run + const mockBuild = createMockBuildProcess(0); const mockChild = createMockChildProcess(); - mockSpawn.mockReturnValue(mockChild); + mockSpawn.mockReturnValueOnce(mockBuild).mockReturnValueOnce(mockChild); return mockChild; } @@ -141,16 +160,16 @@ describe('ContainerDevServer', () => { expect(rmCall![1]).toEqual(['rm', '-f', 'agentcore-dev-testagent']); }); - it('returns null when base image build fails', async () => { + it('returns null when image build fails', async () => { mockDetectContainerRuntime.mockResolvedValue({ runtime: { runtime: 'docker', binary: 'docker', version: 'Docker 24.0' }, notReadyRuntimes: [], }); mockExistsSync.mockReturnValue(true); - // rm succeeds, base build fails - mockSpawnSync - .mockReturnValueOnce({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }) // rm - .mockReturnValueOnce({ status: 1, stdout: Buffer.from(''), stderr: Buffer.from('build error') }); // base build + // rm succeeds (spawnSync) + mockSpawnSync.mockReturnValue({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }); + // build fails (spawn, auto-closes with exit code 1) + mockSpawn.mockReturnValue(createMockBuildProcess(1)); const server = new ContainerDevServer(defaultConfig, defaultOptions); const result = await server.start(); @@ -159,26 +178,7 @@ describe('ContainerDevServer', () => { expect(mockCallbacks.onLog).toHaveBeenCalledWith('error', expect.stringContaining('Container build failed')); }); - it('returns null when dev layer build fails', async () => { - mockDetectContainerRuntime.mockResolvedValue({ - runtime: { runtime: 'docker', binary: 'docker', version: 'Docker 24.0' }, - notReadyRuntimes: [], - }); - mockExistsSync.mockReturnValue(true); - // rm succeeds, base build succeeds, dev build fails - mockSpawnSync - .mockReturnValueOnce({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }) // rm - .mockReturnValueOnce({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }) // base build - .mockReturnValueOnce({ status: 1, stdout: Buffer.from(''), stderr: Buffer.from('dev error') }); // dev build - - const server = new ContainerDevServer(defaultConfig, defaultOptions); - const result = await server.start(); - - expect(result).toBeNull(); - expect(mockCallbacks.onLog).toHaveBeenCalledWith('error', expect.stringContaining('Dev layer build failed')); - }); - - it('succeeds when both builds pass and logs success message', async () => { + it('succeeds when build passes and logs success message', async () => { mockSuccessfulPrepare(); const server = new ContainerDevServer(defaultConfig, defaultOptions); @@ -188,67 +188,49 @@ describe('ContainerDevServer', () => { expect(mockCallbacks.onLog).toHaveBeenCalledWith('system', 'Container image built successfully.'); }); - it('dev layer prefers uv when available, falls back to pip', async () => { - mockSuccessfulPrepare(); - - const server = new ContainerDevServer(defaultConfig, defaultOptions); - await server.start(); - - // The dev build is the 3rd spawnSync call (rm, base build, dev build) - const devBuildCall = mockSpawnSync.mock.calls[2]!; - expect(devBuildCall).toBeDefined(); - const input = devBuildCall[2]?.input as string; - // uv path tried first with --system flag - expect(input).toContain('uv pip install --system -q uvicorn'); - expect(input).toContain('uv pip install --system /app'); - // pip fallback - expect(input).toContain('pip install -q uvicorn'); - expect(input).toContain('pip install -q /app'); - // No requirements.txt fallback — pip install /app reads pyproject.toml - expect(input).not.toContain('requirements.txt'); - }); - - it('dev layer FROM references the base image name', async () => { + it('logs system-level start message and triggers TUI readiness after container is spawned', async () => { mockSuccessfulPrepare(); const server = new ContainerDevServer(defaultConfig, defaultOptions); await server.start(); - const devBuildCall = mockSpawnSync.mock.calls[2]!; - const input = devBuildCall[2]?.input as string; - expect(input).toContain('FROM agentcore-dev-testagent-base'); + expect(mockCallbacks.onLog).toHaveBeenCalledWith( + 'system', + 'Container agentcore-dev-testagent started on port 9000.' + ); + // Emits readiness trigger for TUI detection + expect(mockCallbacks.onLog).toHaveBeenCalledWith('info', 'Application startup complete'); }); - it('dev layer does not set USER (runs as root for dev)', async () => { + it('builds image directly without a dev layer', async () => { mockSuccessfulPrepare(); const server = new ContainerDevServer(defaultConfig, defaultOptions); await server.start(); - const devBuildCall = mockSpawnSync.mock.calls[2]!; - const input = devBuildCall[2]?.input as string; - // Should have USER root but not USER bedrock_agentcore - expect(input).toContain('USER root'); - expect(input).not.toContain('USER bedrock_agentcore'); + // spawnSync only called once for rm (build uses async spawn) + expect(mockSpawnSync).toHaveBeenCalledTimes(1); + // First spawn call is the build + const buildCall = mockSpawn.mock.calls[0]!; + const buildArgs = buildCall[1] as string[]; + // Image is built directly as agentcore-dev-testagent (no -base suffix) + expect(buildArgs).toContain('-t'); + const tagIdx = buildArgs.indexOf('-t'); + expect(buildArgs[tagIdx + 1]).toBe('agentcore-dev-testagent'); }); - it('logs non-empty build output lines at system level', async () => { + it('streams build output lines at system level in real-time', async () => { mockDetectContainerRuntime.mockResolvedValue({ runtime: { runtime: 'docker', binary: 'docker', version: 'Docker 24.0' }, notReadyRuntimes: [], }); mockExistsSync.mockReturnValue(true); - mockSpawnSync - .mockReturnValueOnce({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }) // rm - .mockReturnValueOnce({ - status: 0, - stdout: Buffer.from('Step 1/3: FROM python\nStep 2/3: COPY . .\n'), - stderr: Buffer.from(''), - }) // base build - .mockReturnValueOnce({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }); // dev build + mockSpawnSync.mockReturnValue({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }); // rm - const mockChild = createMockChildProcess(); - mockSpawn.mockReturnValue(mockChild); + // Build process that emits stdout lines then closes + const buildProc = createMockBuildProcess(0, 'Step 1/3: FROM python\nStep 2/3: COPY . .\n'); + const serverProc = createMockChildProcess(); + mockSpawn.mockReturnValueOnce(buildProc).mockReturnValueOnce(serverProc); const server = new ContainerDevServer(defaultConfig, defaultOptions); await server.start(); @@ -258,9 +240,9 @@ describe('ContainerDevServer', () => { }); }); - /** Extract the args array from the first mockSpawn call. */ + /** Extract the args array from the server run spawn call (second spawn — first is the build). */ function getSpawnArgs(): string[] { - return mockSpawn.mock.calls[0]![1] as string[]; + return mockSpawn.mock.calls[1]![1] as string[]; } describe('getSpawnConfig() — verified via spawn args', () => { @@ -288,39 +270,36 @@ describe('ContainerDevServer', () => { expect(spawnArgs[nameIdx + 1]).toBe('agentcore-dev-testagent'); }); - it('overrides entrypoint to python', async () => { + it('does not override entrypoint — uses Dockerfile CMD/ENTRYPOINT', async () => { mockSuccessfulPrepare(); const server = new ContainerDevServer(defaultConfig, defaultOptions); await server.start(); const spawnArgs = getSpawnArgs(); - const entrypointIdx = spawnArgs.indexOf('--entrypoint'); - expect(entrypointIdx).toBeGreaterThan(-1); - expect(spawnArgs[entrypointIdx + 1]).toBe('python'); + expect(spawnArgs).not.toContain('--entrypoint'); }); - it('runs as root to ensure system site-packages are accessible', async () => { + it('does not override user', async () => { mockSuccessfulPrepare(); const server = new ContainerDevServer(defaultConfig, defaultOptions); await server.start(); const spawnArgs = getSpawnArgs(); - const userIdx = spawnArgs.indexOf('--user'); - expect(userIdx).toBeGreaterThan(-1); - expect(spawnArgs[userIdx + 1]).toBe('root'); + expect(spawnArgs).not.toContain('--user'); }); - it('mounts source directory as /app volume', async () => { + it('does not mount source directory as volume', async () => { mockSuccessfulPrepare(); const server = new ContainerDevServer(defaultConfig, defaultOptions); await server.start(); const spawnArgs = getSpawnArgs(); - expect(spawnArgs).toContain('-v'); - expect(spawnArgs).toContain('/project/app:/app'); + // No source:/app volume mount (only ~/.aws mount should be present) + const volumeArgs = spawnArgs.filter((arg: string) => arg.includes(':/app')); + expect(volumeArgs).toHaveLength(0); }); it('maps host port to container internal port', async () => { @@ -355,6 +334,16 @@ describe('ContainerDevServer', () => { expect(spawnArgs).toContain(`PORT=${CONTAINER_INTERNAL_PORT}`); }); + it('disables OpenTelemetry SDK to avoid missing-collector errors', async () => { + mockSuccessfulPrepare(); + + const server = new ContainerDevServer(defaultConfig, defaultOptions); + await server.start(); + + const spawnArgs = getSpawnArgs(); + expect(spawnArgs).toContain('OTEL_SDK_DISABLED=true'); + }); + it('forwards AWS env vars when present in process.env', async () => { process.env.AWS_ACCESS_KEY_ID = 'AKIAIOSFODNN7EXAMPLE'; process.env.AWS_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'; @@ -391,11 +380,12 @@ describe('ContainerDevServer', () => { await server.start(); const spawnArgs = getSpawnArgs(); - const awsArgs = spawnArgs.filter((arg: string) => arg.startsWith('AWS_')); + // Filter only forwarded AWS cred env vars, not AWS_CONFIG_FILE/CREDENTIALS_FILE + const awsArgs = spawnArgs.filter((arg: string) => arg.startsWith('AWS_') && !arg.includes('_FILE=')); expect(awsArgs).toHaveLength(0); }); - it('mounts ~/.aws to /root/.aws when exists', async () => { + it('mounts ~/.aws to /aws-config for any container user', async () => { mockSuccessfulPrepare(); // existsSync returns true for all calls (Dockerfile and ~/.aws) @@ -403,9 +393,19 @@ describe('ContainerDevServer', () => { await server.start(); const spawnArgs = getSpawnArgs(); - expect(spawnArgs).toContain('/home/testuser/.aws:/root/.aws:ro'); + expect(spawnArgs).toContain('/home/testuser/.aws:/aws-config:ro'); }); + it('sets AWS_CONFIG_FILE and AWS_SHARED_CREDENTIALS_FILE when ~/.aws exists', async () => { + mockSuccessfulPrepare(); + + const server = new ContainerDevServer(defaultConfig, defaultOptions); + await server.start(); + + const spawnArgs = getSpawnArgs(); + expect(spawnArgs).toContain('AWS_CONFIG_FILE=/aws-config/config'); + expect(spawnArgs).toContain('AWS_SHARED_CREDENTIALS_FILE=/aws-config/credentials'); + }); it('skips ~/.aws mount when directory does not exist', async () => { mockDetectContainerRuntime.mockResolvedValue({ runtime: { runtime: 'docker', binary: 'docker', version: 'Docker 24.0' }, @@ -417,8 +417,9 @@ describe('ContainerDevServer', () => { return true; // Dockerfile exists }); mockSpawnSync.mockReturnValue({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }); + const mockBuild = createMockBuildProcess(0); const mockChild = createMockChildProcess(); - mockSpawn.mockReturnValue(mockChild); + mockSpawn.mockReturnValueOnce(mockBuild).mockReturnValueOnce(mockChild); const server = new ContainerDevServer(defaultConfig, defaultOptions); await server.start(); @@ -428,28 +429,16 @@ describe('ContainerDevServer', () => { expect(awsMountArg).toBeUndefined(); }); - it('uses uvicorn with --reload and --reload-dir /app', async () => { - mockSuccessfulPrepare(); - - const server = new ContainerDevServer(defaultConfig, defaultOptions); - await server.start(); - - const spawnArgs = getSpawnArgs(); - expect(spawnArgs).toContain('-m'); - expect(spawnArgs).toContain('uvicorn'); - expect(spawnArgs).toContain('--reload'); - expect(spawnArgs).toContain('--reload-dir'); - expect(spawnArgs).toContain('/app'); - }); - - it('converts entrypoint via convertEntrypointToModule (main.py -> main:app)', async () => { + it('does not include uvicorn or reload args', async () => { mockSuccessfulPrepare(); const server = new ContainerDevServer(defaultConfig, defaultOptions); await server.start(); const spawnArgs = getSpawnArgs(); - expect(spawnArgs).toContain('main:app'); + expect(spawnArgs).not.toContain('uvicorn'); + expect(spawnArgs).not.toContain('--reload'); + expect(spawnArgs).not.toContain('-m'); }); }); @@ -461,20 +450,22 @@ describe('ContainerDevServer', () => { const child = await server.start(); // Clear mocks to isolate the kill call - mockSpawnSync.mockClear(); + mockSpawn.mockClear(); server.kill(); - expect(mockSpawnSync).toHaveBeenCalledWith('docker', ['stop', 'agentcore-dev-testagent'], { stdio: 'ignore' }); + // Container stop is async (spawn not spawnSync) so UI can render "Stopping..." message + expect(mockSpawn).toHaveBeenCalledWith('docker', ['stop', 'agentcore-dev-testagent'], { stdio: 'ignore' }); expect(child!.kill).toHaveBeenCalledWith('SIGTERM'); // eslint-disable-line @typescript-eslint/unbound-method }); it('does not call container stop when runtimeBinary is empty (prepare not called)', () => { const server = new ContainerDevServer(defaultConfig, defaultOptions); + mockSpawn.mockClear(); server.kill(); - expect(mockSpawnSync).not.toHaveBeenCalled(); + expect(mockSpawn).not.toHaveBeenCalled(); }); }); }); diff --git a/src/cli/operations/dev/container-dev-server.ts b/src/cli/operations/dev/container-dev-server.ts index a2652302..2240496b 100644 --- a/src/cli/operations/dev/container-dev-server.ts +++ b/src/cli/operations/dev/container-dev-server.ts @@ -2,13 +2,12 @@ import { CONTAINER_INTERNAL_PORT, DOCKERFILE_NAME } from '../../../lib'; import { getUvBuildArgs } from '../../../lib/packaging/build-args'; import { detectContainerRuntime, getStartHint } from '../../external-requirements/detect'; import { DevServer, type LogLevel, type SpawnConfig } from './dev-server'; -import { convertEntrypointToModule } from './utils'; -import { spawnSync } from 'child_process'; +import { type ChildProcess, spawn, spawnSync } from 'child_process'; import { existsSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; -/** Dev server for Container agents. Builds and runs a Docker container with volume mount for hot-reload. */ +/** Dev server for Container agents. Builds and runs a Docker container using the user's Dockerfile. */ export class ContainerDevServer extends DevServer { private runtimeBinary = ''; @@ -22,10 +21,26 @@ export class ContainerDevServer extends DevServer { return this.imageName; } - /** Override kill to stop the container properly, cleaning up the port proxy. */ + /** Override start to log when the container is launched and trigger TUI readiness detection. + * DevLogger filters 'info', so without a 'system'-level message the log would be empty after build. + * Include "Application startup complete" so the TUI detects container readiness. */ + override async start(): Promise { + const child = await super.start(); + if (child) { + const { onLog } = this.options.callbacks; + onLog('system', `Container ${this.containerName} started on port ${this.options.port}.`); + // Trigger TUI readiness detection (useDevServer looks for this exact string) + onLog('info', 'Application startup complete'); + } + return child; + } + + /** Override kill to stop the container properly, cleaning up the port proxy. + * Uses async spawn so the UI can render "Stopping..." while container stops. */ override kill(): void { if (this.runtimeBinary) { - spawnSync(this.runtimeBinary, ['stop', this.containerName], { stdio: 'ignore' }); + // Fire-and-forget: stop container asynchronously so UI remains responsive + spawn(this.runtimeBinary, ['stop', this.containerName], { stdio: 'ignore' }); } super.kill(); } @@ -58,48 +73,15 @@ export class ContainerDevServer extends DevServer { // 3. Remove any stale container from a previous run (prevents "proxy already running" errors) spawnSync(this.runtimeBinary, ['rm', '-f', this.containerName], { stdio: 'ignore' }); - // 4. Build the base container image - const baseImageName = `${this.imageName}-base`; + // 4. Build the container image, streaming output in real-time onLog('system', `Building container image: ${this.imageName}...`); - const buildResult = spawnSync( - this.runtimeBinary, - ['build', '-t', baseImageName, '-f', dockerfilePath, ...getUvBuildArgs(), this.config.directory], - { stdio: 'pipe' } + const exitCode = await this.streamBuild( + ['-t', this.imageName, '-f', dockerfilePath, ...getUvBuildArgs(), this.config.directory], + onLog ); - // Log build output for debugging - this.logBuildOutput(buildResult.stdout, buildResult.stderr, onLog); - - if (buildResult.status !== 0) { - onLog('error', `Container build failed (exit code ${buildResult.status})`); - return false; - } - - // 5. Build dev layer on top with uvicorn and project deps installed to system Python. - // At runtime, `-v source:/app` hides any .venv created during the base build, - // so we need all packages in system site-packages where the volume mount can't hide them. - // Prefers uv when available (template images ship it), falls back to pip for BYO images. - onLog('system', 'Preparing dev environment...'); - const devDockerfile = [ - `FROM ${baseImageName}`, - 'USER root', - 'RUN (uv pip install --system -q uvicorn && uv pip install --system /app)' + - ' || (pip install -q uvicorn && pip install -q /app)', - ].join('\n'); - - const devBuild = spawnSync( - this.runtimeBinary, - ['build', '-t', this.imageName, '-f', '-', ...getUvBuildArgs(), this.config.directory], - { - input: devDockerfile, - stdio: ['pipe', 'pipe', 'pipe'], - } - ); - - this.logBuildOutput(devBuild.stdout, devBuild.stderr, onLog); - - if (devBuild.status !== 0) { - onLog('error', `Dev layer build failed (exit code ${devBuild.status})`); + if (exitCode !== 0) { + onLog('error', `Container build failed (exit code ${exitCode})`); return false; } @@ -107,26 +89,40 @@ export class ContainerDevServer extends DevServer { return true; } - /** Log build stdout/stderr through the onLog callback at 'system' level so they persist to log files. */ - private logBuildOutput( - stdout: Buffer | null, - stderr: Buffer | null, - onLog: (level: LogLevel, message: string) => void - ): void { - for (const line of (stdout?.toString() ?? '').split('\n')) { - if (line.trim()) onLog('system', line); - } - for (const line of (stderr?.toString() ?? '').split('\n')) { - if (line.trim()) onLog('system', line); - } + /** Run a container build and stream stdout/stderr lines to onLog in real-time. */ + private streamBuild(args: string[], onLog: (level: LogLevel, message: string) => void): Promise { + return new Promise(resolve => { + const child = spawn(this.runtimeBinary, ['build', ...args], { stdio: 'pipe' }); + + const streamLines = (stream: NodeJS.ReadableStream) => { + let buffer = ''; + stream.on('data', (chunk: Buffer) => { + buffer += chunk.toString(); + const lines = buffer.split('\n'); + buffer = lines.pop()!; + for (const line of lines) { + if (line.trim()) onLog('system', line); + } + }); + stream.on('end', () => { + if (buffer.trim()) onLog('system', buffer); + }); + }; + + if (child.stdout) streamLines(child.stdout); + if (child.stderr) streamLines(child.stderr); + + child.on('error', err => { + onLog('error', `Build process error: ${err.message}`); + resolve(1); + }); + child.on('close', code => resolve(code)); + }); } protected getSpawnConfig(): SpawnConfig { - const { directory, module: entrypoint } = this.config; const { port, envVars = {} } = this.options; - const uvicornModule = convertEntrypointToModule(entrypoint); - // Forward AWS credentials from host environment into the container const awsEnvKeys = [ 'AWS_ACCESS_KEY_ID', @@ -143,18 +139,31 @@ export class ContainerDevServer extends DevServer { } } - // Environment variables: AWS creds + user env + container-specific overrides + // Mount ~/.aws to a neutral path accessible by any container user, and set + // AWS SDK env vars to point to it. This supports SSO, profiles, and credential files + // regardless of what USER the Dockerfile specifies. + const awsDir = join(homedir(), '.aws'); + const awsContainerPath = '/aws-config'; + const awsMountArgs = existsSync(awsDir) ? ['-v', `${awsDir}:${awsContainerPath}:ro`] : []; + const awsConfigEnv = existsSync(awsDir) + ? { + AWS_CONFIG_FILE: `${awsContainerPath}/config`, + AWS_SHARED_CREDENTIALS_FILE: `${awsContainerPath}/credentials`, + } + : {}; + + // Environment variables: AWS creds + config paths + user env + container-specific overrides. + // Disable OpenTelemetry SDK — no collector is running locally, and the OTEL + // exporter connection errors would crash or pollute the dev server output. const envArgs = Object.entries({ ...awsEnvVars, + ...awsConfigEnv, ...envVars, LOCAL_DEV: '1', PORT: String(CONTAINER_INTERNAL_PORT), + OTEL_SDK_DISABLED: 'true', }).flatMap(([k, v]) => ['-e', `${k}=${v}`]); - // Mount ~/.aws for credential file / SSO / profile support - const awsDir = join(homedir(), '.aws'); - const awsMountArgs = existsSync(awsDir) ? ['-v', `${awsDir}:/root/.aws:ro`] : []; - return { cmd: this.runtimeBinary, args: [ @@ -162,27 +171,11 @@ export class ContainerDevServer extends DevServer { '--rm', '--name', this.containerName, - '--entrypoint', - 'python', - '--user', - 'root', - '-v', - `${directory}:/app`, ...awsMountArgs, '-p', `${port}:${CONTAINER_INTERNAL_PORT}`, ...envArgs, this.imageName, - '-m', - 'uvicorn', - uvicornModule, - '--reload', - '--reload-dir', - '/app', - '--host', - '0.0.0.0', - '--port', - String(CONTAINER_INTERNAL_PORT), ], env: { ...process.env }, }; diff --git a/src/cli/operations/dev/invoke.ts b/src/cli/operations/dev/invoke.ts index 8d45e7ea..0c31cf8f 100644 --- a/src/cli/operations/dev/invoke.ts +++ b/src/cli/operations/dev/invoke.ts @@ -36,8 +36,14 @@ function parseSSELine(line: string): { content: string | null; error: string | n const parsed: unknown = JSON.parse(content); if (typeof parsed === 'string') { return { content: parsed, error: null }; - } else if (parsed && typeof parsed === 'object' && 'error' in parsed) { - return { content: null, error: String((parsed as { error: unknown }).error) }; + } else if (parsed && typeof parsed === 'object') { + if ('error' in parsed) { + return { content: null, error: String((parsed as { error: unknown }).error) }; + } + // Handle {"text": "..."} format from bedrock-agentcore runtime + if ('text' in parsed) { + return { content: String((parsed as { text: unknown }).text), error: null }; + } } } catch { return { content, error: null }; @@ -115,7 +121,11 @@ export async function* invokeAgentStreaming( try { const res = await fetch(`http://localhost:${port}/invocations`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + 'x-amzn-bedrock-agentcore-runtime-session-id': 'local-dev-session', + }, body: JSON.stringify({ prompt: msg }), }); @@ -249,7 +259,11 @@ export async function invokeAgent(portOrOptions: number | InvokeOptions, message try { const res = await fetch(`http://localhost:${port}/invocations`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + 'x-amzn-bedrock-agentcore-runtime-session-id': 'local-dev-session', + }, body: JSON.stringify({ prompt: msg }), }); diff --git a/src/cli/tui/screens/dev/DevScreen.tsx b/src/cli/tui/screens/dev/DevScreen.tsx index 619b0ca5..6ceec4af 100644 --- a/src/cli/tui/screens/dev/DevScreen.tsx +++ b/src/cli/tui/screens/dev/DevScreen.tsx @@ -435,7 +435,9 @@ export function DevScreen(props: DevScreenProps) { ))} {isExiting && ( - Stopping server... + + {config?.buildType === 'Container' ? 'Stopping container...' : 'Stopping server...'} + )} {logFilePath && }