From 50a6cbd6e6e7423d2e4c3bb331f57dfe70e44a93 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 19:21:01 +0000 Subject: [PATCH 01/36] feat(typescript): scaffold TypeScript language support (WIP checkpoint) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unblocks TypeScript as a language option in the generate path and teaches the spec builder to emit NODE_22 / main.ts defaults when language=TypeScript. Templates, node setup helper, dev-server Python-only guard removal, tests, and docs still to come — see docs/TYPESCRIPT_SUPPORT_HANDOFF.md for the full remaining scope and plan link. - schema: add DEFAULT_NODE_VERSION, DEFAULT_ENTRYPOINT_BY_LANGUAGE, DEFAULT_RUNTIME_BY_LANGUAGE constants - tui: remove disabled=true from TypeScript LANGUAGE_OPTIONS entry - tui: filter SDK list to Strands-only when language=TypeScript - validate: drop the --language TypeScript reject; add Strands-only gate for TS - schema-mapper: branch entrypoint/runtime on config.language Confidence: medium Scope-risk: narrow Not-tested: end-to-end create/deploy — templates not yet authored --- docs/TYPESCRIPT_SUPPORT_HANDOFF.md | 176 ++++++++++++++++++ src/cli/commands/add/validate.ts | 11 +- src/cli/commands/create/command.tsx | 2 +- .../agent/generate/schema-mapper.ts | 13 +- src/cli/tui/screens/agent/types.ts | 2 +- .../tui/screens/generate/GenerateWizardUI.tsx | 2 +- src/cli/tui/screens/generate/types.ts | 11 +- src/schema/constants.ts | 15 ++ 8 files changed, 219 insertions(+), 13 deletions(-) create mode 100644 docs/TYPESCRIPT_SUPPORT_HANDOFF.md diff --git a/docs/TYPESCRIPT_SUPPORT_HANDOFF.md b/docs/TYPESCRIPT_SUPPORT_HANDOFF.md new file mode 100644 index 000000000..f792c7a35 --- /dev/null +++ b/docs/TYPESCRIPT_SUPPORT_HANDOFF.md @@ -0,0 +1,176 @@ +# TypeScript (Strands) support — work-in-progress handoff + +This file captures the state of an in-progress initiative to add TypeScript (Strands SDK) as a first-class language +option in `agentcore create`, alongside Python. + +**Full plan:** `~/.claude/plans/lets-add-typescript-to-jazzy-honey.md` (owner machine only; copy the plan content into +this file if it needs to travel — it already lives in the git history of the original chat thread.) + +## What's been merged in this checkpoint + +All changes typecheck clean (`npx tsc --noEmit` from the `agentcore-cli/` directory). + +1. **Schema constants** (`src/schema/constants.ts`) + - `DEFAULT_NODE_VERSION: NodeRuntime = 'NODE_22'` + - `DEFAULT_ENTRYPOINT_BY_LANGUAGE: Record<'Python' | 'TypeScript', string>` + - `DEFAULT_RUNTIME_BY_LANGUAGE: Record<'Python' | 'TypeScript', RuntimeVersion>` + +2. **UI unblock** (`src/cli/tui/screens/agent/types.ts`) + - TypeScript entry in `LANGUAGE_OPTIONS` is no longer `disabled: true`. + +3. **CLI validator** (`src/cli/commands/add/validate.ts`) + - Removed the hard reject of `--language TypeScript`. + - Added a new gate: when `language === 'TypeScript'`, only `Strands` is accepted as `--framework`; every other + framework returns a clear error. + +4. **CLI flag help** (`src/cli/commands/create/command.tsx`) + - `--language` description now mentions both Python and TypeScript. + +5. **TUI framework filter** (`src/cli/tui/screens/generate/types.ts` + `GenerateWizardUI.tsx`) + - `getSDKOptionsForProtocol(protocol, language?)` takes an optional language arg. + - When `language === 'TypeScript'` the list is filtered down to `Strands` only. + - `GenerateWizardUI` passes `wizard.config.language` into the call site. + +6. **Language-aware spec defaults** (`src/cli/operations/agent/generate/schema-mapper.ts`) + - `mapGenerateConfigToAgent` now branches on `config.language === 'TypeScript'`: + - `entrypoint` → `DEFAULT_ENTRYPOINT_BY_LANGUAGE.TypeScript` (`main.ts`) + - `runtimeVersion` → `DEFAULT_RUNTIME_BY_LANGUAGE.TypeScript` (`NODE_22`) + - Imports added from `'../../../../schema'` barrel. + +## What is NOT done yet (the big chunks) + +Tackling the remaining items requires a fresh context budget and ideally the actual Strands TS SDK and the AgentCore +runtime TS SDK installed locally for quick iteration. + +### 1. Template assets (the bulk of the work) + +Author files at `src/assets/typescript/http/strands/`: + +``` +base/ + gitignore.template + package.json # Handlebars — pins @strands-agents/sdk + bedrock-agentcore + tsconfig.json # target ES2022, module NodeNext, strict + main.ts # entrypoint — BedrockAgentCoreApp + Agent.stream() + README.md + mcp_client/client.ts # mirrors Python mcp_client/client.py semantics + model/load.ts # mirrors Python model/load.py (per-provider branches) +capabilities/ + memory/session.ts # mirrors Python capabilities/memory/session.py +``` + +**Confirmed SDK surface** (tarballs unpacked under `/tmp/strands-ts-check/` and `/tmp/bac-check/` in my session — +re-fetch with `npm pack` to inspect): + +- `@strands-agents/sdk@1.0.0-rc.4` + - Main: `Agent`, `tool`, `BedrockModel`, `McpClient` + - Provider subpaths: `@strands-agents/sdk/models/{bedrock,anthropic,openai,google}` + - MCP: `McpClient` takes `{ transport }` — `Transport` from `@modelcontextprotocol/sdk/shared/transport.js` + - Agent streaming: `agent.stream(input)` yields `AgentStreamEvent`; filter for `ContentBlockDelta` with `textDelta` + blocks to stream text output +- `bedrock-agentcore@0.2.2` + - `BedrockAgentCoreApp` from `bedrock-agentcore/runtime` (Fastify-based, runs on 8080) + - Identity HOFs `withAccessToken` / `withApiKey` from `bedrock-agentcore/identity` + +**Template shape for `main.ts`** (sketch; not committed): + +```ts +import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime'; +import { Agent, tool } from '@strands-agents/sdk'; +import { BedrockModel } from '@strands-agents/sdk/models/bedrock'; +import { loadModel } from './model/load.js'; +{{#if hasMemory}}import { getMemorySessionManager } from './memory/session.js';{{/if}} + +const app = new BedrockAgentCoreApp({ + invocationHandler: { + process: async function* (request, context) { + const agent = new Agent({ model: loadModel(), /* sessionManager, tools */ }); + for await (const event of agent.stream((request as { prompt: string }).prompt)) { + if (event.type === 'contentBlockDelta' && event.delta.type === 'textDelta') { + yield { data: event.delta.text }; + } + } + }, + }, +}); +app.run(); +``` + +Verify this against the actual SDK event shape before finalizing — the `stream()` event type names are in +`/tmp/strands-ts-check/package/dist/src/models/streaming.d.ts`. + +### 2. Container template + +Under `src/assets/container/typescript/`: + +- `Dockerfile` — base `public.ecr.aws/docker/library/node:22-slim`; copy `package.json` + `package-lock.json`, + `npm ci --omit=dev`, copy source, run `npx tsx main.ts` (or `tsc` build step + `node dist/main.js` if we want a build + artifact). Expose 8080. +- `dockerignore.template` — `node_modules`, `dist`, `.env*`, `.git/`. + +### 3. Dev server unblock + +`src/cli/operations/dev/config.ts:49-54` — the `isDevSupported` function actively rejects non-Python agents with "Dev +mode only supports Python agents." Remove that guard; the actual `codezip-dev-server.ts` already handles `!isPython` via +`npx tsx watch`. + +### 4. Node setup helper + wiring + +- New `src/cli/operations/node/setup.ts` mirroring `src/cli/operations/python/setup.ts` — exposes + `setupNodeProject({ projectDir })` that shells out to `npm install`. +- Wire into `src/cli/tui/screens/create/useCreateFlow.ts` around line 431 with a branch parallel to the Python setup + block. +- Extend `checkCreateDependencies({ language })` in `src/cli/external-requirements/checks.ts` (called from + `src/cli/commands/create/action.ts`) to verify `node` + `npm` when `language === 'TypeScript'`. + +### 5. Packaging dispatcher (verified — no change required) + +`src/lib/packaging/index.ts` already delegates to `NodeCodeZipPackager` when `isNodeRuntime(runtimeVersion)` is true. +Confirmed by reading — keep as-is. + +### 6. Tests + +- **Snapshots.** `src/assets/__tests__/assets.snapshot.test.ts` already has a TypeScript block (lines 106-120) that + auto-discovers `typescript/` files. After authoring templates, run `npm run test:update-snapshots` and review. +- **Create integ test.** `integ-tests/create-with-agent.test.ts` — duplicate the Python block for TypeScript, assert + `app//main.ts`, `package.json`, and `agentcore.json` has `runtimeVersion: NODE_22` + `entrypoint: main.ts`. +- **Dev-server test.** `src/cli/operations/dev/__tests__/codezip-dev-server.test.ts` — add a TS variant asserting + `getSpawnConfig()` returns `{ cmd: 'npx', args: ['tsx', 'watch', 'main.ts'], ... }`. +- **TUI harness walkthrough** mirroring an existing Python walkthrough under `integ-tests/tui/`. +- **E2E container deploy test.** `integ-tests/deploy-typescript-strands-container.test.ts`: scaffold → container build → + `agentcore deploy` against test account 325335451438 (per root CLAUDE.md, use `AWS_PROFILE=deploy`) → + `agentcore invoke --prompt "ping"` → teardown. Gate behind the same env flag used by other AWS integ tests so CI + without credentials skips cleanly. + +### 7. Documentation + +- `docs/frameworks.md` — add a "Supported languages" section. +- `docs/local-development.md` — TS dev loop (Node ≥ 18, `npx tsx watch`). +- `docs/commands.md` — `--language TypeScript` examples. +- `docs/container-builds.md` — TS Dockerfile example. +- `README.md` — one-line mention. + +## Verification plan (when templates are done) + +Refer to the full plan's section "Verification plan" — step-by-step from scratch-dir `agentcore create my-ts-agent` +through `agentcore dev`, `agentcore invoke`, and the container deploy + teardown. AWS account for deploys: +`325335451438` via `AWS_PROFILE=deploy`, per the workspace root CLAUDE.md. + +## Out of scope + +- LangChain/LangGraph, GoogleADK, OpenAIAgents templates for TypeScript. +- A2A and MCP protocol templates for TypeScript. +- pnpm / yarn support. +- BYO TypeScript path (already works today via `--type byo`). + +## Known gotchas + +- The Strands TS SDK is at `1.0.0-rc.4` (4 days old at time of writing). Pin exactly, and re-check the version + + event-type names before release. +- `BedrockAgentCoreApp` in the TS SDK is Fastify-based, not ASGI — no uvicorn equivalent needed, but the dev-server code + uses `npx tsx watch` which restarts the whole process on edits. Dev-experience parity with Python's uvicorn `--reload` + is good enough. +- The `isDevSupported` Python-only guard is easy to miss — remember to remove it. +- `BedrockAgentCoreApp.invocationHandler.process` can return an async generator; the runtime wraps it in SSE + automatically. The Python `@app.entrypoint async def invoke` equivalent on the TS side is yielding `{ data: string }` + objects from that generator. diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 81f3d3678..318939e38 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -236,11 +236,14 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes return { valid: false, error: '--code-location is required for BYO path' }; } } else { - if (options.language === 'TypeScript') { - return { valid: false, error: 'Create path only supports Python (TypeScript templates not yet available)' }; - } if (options.language === 'Other') { - return { valid: false, error: 'Create path only supports Python' }; + return { valid: false, error: 'Create path only supports Python or TypeScript' }; + } + if (options.language === 'TypeScript' && options.framework && options.framework !== 'Strands') { + return { + valid: false, + error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands is supported.`, + }; } if (!options.memory) { diff --git a/src/cli/commands/create/command.tsx b/src/cli/commands/create/command.tsx index d9ba3d3f6..da6a3f804 100644 --- a/src/cli/commands/create/command.tsx +++ b/src/cli/commands/create/command.tsx @@ -176,7 +176,7 @@ export const registerCreate = (program: Command) => { .option('--no-agent', 'Skip agent creation [non-interactive]') .option('--defaults', 'Use defaults (Python, Strands, Bedrock, no memory) [non-interactive]') .option('--build ', 'Build type: CodeZip or Container (default: CodeZip) [non-interactive]') - .option('--language ', 'Target language (default: Python) [non-interactive]') + .option('--language ', 'Target language: Python or TypeScript (default: Python) [non-interactive]') .option( '--framework ', 'Agent framework (Strands, LangChain_LangGraph, GoogleADK, OpenAIAgents) [non-interactive]' diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index 1afd0a255..a2393b959 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -9,7 +9,12 @@ import type { MemoryStrategyType, ModelProvider, } from '../../../../schema'; -import { DEFAULT_EPISODIC_REFLECTION_NAMESPACES, DEFAULT_STRATEGY_NAMESPACES } from '../../../../schema'; +import { + DEFAULT_ENTRYPOINT_BY_LANGUAGE, + DEFAULT_EPISODIC_REFLECTION_NAMESPACES, + DEFAULT_RUNTIME_BY_LANGUAGE, + DEFAULT_STRATEGY_NAMESPACES, +} from '../../../../schema'; import { GatewayPrimitive } from '../../../primitives/GatewayPrimitive'; import { buildAuthorizerConfigFromJwtConfig } from '../../../primitives/auth-utils'; import { @@ -116,9 +121,11 @@ export function mapGenerateConfigToAgent(config: GenerateConfig): AgentEnvSpec { name: config.projectName, build: config.buildType ?? 'CodeZip', ...(config.dockerfile && { dockerfile: config.dockerfile }), - entrypoint: DEFAULT_PYTHON_ENTRYPOINT as FilePath, + entrypoint: (config.language === 'TypeScript' + ? DEFAULT_ENTRYPOINT_BY_LANGUAGE.TypeScript + : DEFAULT_PYTHON_ENTRYPOINT) as FilePath, codeLocation: codeLocation as DirectoryPath, - runtimeVersion: DEFAULT_PYTHON_VERSION, + runtimeVersion: config.language === 'TypeScript' ? DEFAULT_RUNTIME_BY_LANGUAGE.TypeScript : DEFAULT_PYTHON_VERSION, networkMode, protocol, ...(networkMode === 'VPC' && diff --git a/src/cli/tui/screens/agent/types.ts b/src/cli/tui/screens/agent/types.ts index 436da1bfd..753620ab7 100644 --- a/src/cli/tui/screens/agent/types.ts +++ b/src/cli/tui/screens/agent/types.ts @@ -154,7 +154,7 @@ export const AGENT_TYPE_OPTIONS = [ export const LANGUAGE_OPTIONS = [ { id: 'Python', title: 'Python' }, - { id: 'TypeScript', title: 'TypeScript (coming soon)', disabled: true }, + { id: 'TypeScript', title: 'TypeScript' }, { id: 'Other', title: 'Other' }, ] as const; diff --git a/src/cli/tui/screens/generate/GenerateWizardUI.tsx b/src/cli/tui/screens/generate/GenerateWizardUI.tsx index 4b61e4689..ead1dc947 100644 --- a/src/cli/tui/screens/generate/GenerateWizardUI.tsx +++ b/src/cli/tui/screens/generate/GenerateWizardUI.tsx @@ -84,7 +84,7 @@ export function GenerateWizardUI({ case 'protocol': return PROTOCOL_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); case 'sdk': - return getSDKOptionsForProtocol(wizard.config.protocol).map(o => ({ + return getSDKOptionsForProtocol(wizard.config.protocol, wizard.config.language).map(o => ({ id: o.id, title: o.title, description: o.description, diff --git a/src/cli/tui/screens/generate/types.ts b/src/cli/tui/screens/generate/types.ts index b0c25d38e..6ca13b948 100644 --- a/src/cli/tui/screens/generate/types.ts +++ b/src/cli/tui/screens/generate/types.ts @@ -126,11 +126,16 @@ export const SDK_OPTIONS = [ ] as const; /** - * Get SDK options filtered by protocol compatibility. + * Get SDK options filtered by protocol compatibility and target language. + * TypeScript currently only supports Strands. */ -export function getSDKOptionsForProtocol(protocol: ProtocolMode) { +export function getSDKOptionsForProtocol(protocol: ProtocolMode, language?: TargetLanguage) { const supportedFrameworks = PROTOCOL_FRAMEWORK_MATRIX[protocol]; - return SDK_OPTIONS.filter(option => supportedFrameworks.includes(option.id)); + const byProtocol = SDK_OPTIONS.filter(option => supportedFrameworks.includes(option.id)); + if (language === 'TypeScript') { + return byProtocol.filter(option => option.id === 'Strands'); + } + return byProtocol; } export const MODEL_PROVIDER_OPTIONS = [ diff --git a/src/schema/constants.ts b/src/schema/constants.ts index fc546276a..e85ad0126 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -146,10 +146,25 @@ export const DEFAULT_PYTHON_VERSION: PythonRuntime = 'PYTHON_3_14'; export const NodeRuntimeSchema = z.enum(['NODE_18', 'NODE_20', 'NODE_22']); export type NodeRuntime = z.infer; +/** Default Node.js runtime version for new TypeScript agents */ +export const DEFAULT_NODE_VERSION: NodeRuntime = 'NODE_22'; + /** Combined runtime version schema supporting both Python and Node/TypeScript runtimes */ export const RuntimeVersionSchema = z.union([PythonRuntimeSchema, NodeRuntimeSchema]); export type RuntimeVersion = z.infer; +/** Default entrypoint filename for each target language (create path). */ +export const DEFAULT_ENTRYPOINT_BY_LANGUAGE: Record<'Python' | 'TypeScript', string> = { + Python: 'main.py', + TypeScript: 'main.ts', +}; + +/** Default runtime version for each target language (create path). */ +export const DEFAULT_RUNTIME_BY_LANGUAGE: Record<'Python' | 'TypeScript', RuntimeVersion> = { + Python: DEFAULT_PYTHON_VERSION, + TypeScript: DEFAULT_NODE_VERSION, +}; + export const NetworkModeSchema = z.enum(['PUBLIC', 'VPC']); export type NetworkMode = z.infer; From 3417f9abd79efd0f5153f70b9c6c3094956944de Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 19:37:03 +0000 Subject: [PATCH 02/36] docs(typescript): add progress tracker for TS support initiative Living checklist alongside the handoff doc. Tracks phases 0-8, captures verification-sweep results, and reserves a commit log so the next person can reconstruct state from this file + git log. Confidence: high Scope-risk: narrow --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 218 ++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 docs/TYPESCRIPT_SUPPORT_PROGRESS.md diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md new file mode 100644 index 000000000..6a7dc8477 --- /dev/null +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -0,0 +1,218 @@ +# TypeScript (Strands) support — progress tracker + +Living checklist for the TypeScript support initiative. Update as you go. Every code change should be followed by a +commit (one logical unit per commit) and an entry in the **Commit log** at the bottom so the next person can reconstruct +exactly where things stand by reading this file + `git log`. + +**Companion docs:** + +- `docs/TYPESCRIPT_SUPPORT_HANDOFF.md` — prose handoff (what was merged pre-progress-doc, SDK surface notes, gotchas). +- `~/.claude/plans/lets-add-typescript-to-jazzy-honey.md` — original full plan (owner machine). + +**Branch / starting point:** `main` @ `50a6cbd` +(`feat(typescript): scaffold TypeScript language support (WIP checkpoint)`) + +**AWS test account for deploy integ:** `325335451438` via `AWS_PROFILE=deploy` (refresh with +`ada credentials update --account 325335451438 --provider isengard --role Admin --profile deploy`). + +--- + +## Legend + +- `[x]` done + committed +- `[~]` in progress / partial +- `[ ]` not started +- `[!]` blocked — see note + +--- + +## Phase 0 — Verification sweep (no code changes) + +- [x] Confirm `isDevSupported` guard rejects TS — **confirmed** at `src/cli/operations/dev/config.ts:49-54`. Must + remove. +- [x] Confirm packaging dispatcher already handles Node runtime — **confirmed clean** at + `src/lib/packaging/index.ts:34-56` (`isNodeRuntime` branch exists). No change needed. +- [x] Grep CDK constructs for `PYTHON_` assumptions — **confirmed clean**. Only match is in a test file + (`AgentCoreRuntime.test.ts:13`). Production CDK code forwards `runtimeVersion` generically. + +--- + +## Phase 1 — Already merged (pre-progress-doc checkpoint) + +Captured in commit `50a6cbd`. Do NOT re-do these. + +- [x] Schema constants: `DEFAULT_NODE_VERSION`, `DEFAULT_ENTRYPOINT_BY_LANGUAGE`, `DEFAULT_RUNTIME_BY_LANGUAGE` + (`src/schema/constants.ts`). +- [x] TUI unblock: TypeScript option no longer `disabled` (`src/cli/tui/screens/agent/types.ts`). +- [x] CLI validator: removed hard reject of `--language TypeScript`; added framework gate (TS ⇒ Strands only) + (`src/cli/commands/add/validate.ts`). +- [x] CLI flag help updated (`src/cli/commands/create/command.tsx`). +- [x] TUI framework filter by language (`src/cli/tui/screens/generate/types.ts` + `GenerateWizardUI.tsx`). +- [x] Language-aware spec defaults in `schema-mapper.ts` (entrypoint + runtimeVersion branch on TS). +- [x] `npx tsc --noEmit` clean. + +--- + +## Phase 2 — Dev-server unblock (small, unblocking change) + +- [ ] Remove / relax `isDevSupported` Python-only guard at `src/cli/operations/dev/config.ts:35-57`. + - **Approach:** drop the `!isPythonAgent(agent)` branch entirely. Downstream (`codezip-dev-server.ts:120-126`) already + picks `npx tsx watch` when `isPython` is false, and `isPython` at `config.ts:141` keys off entrypoint extension — so + it will naturally be `false` for `main.ts`. + - **Verify:** unit test in `codezip-dev-server.test.ts` still green; add a TS case (see Phase 6). + - **Notes:** + +--- + +## Phase 3 — Template assets (the bulk of the work) + +Author under `src/assets/typescript/http/strands/`. Mirror Python shape at `src/assets/python/http/strands/`. + +**SDK surface (confirmed in handoff):** + +- `@strands-agents/sdk@1.0.0-rc.4` — `Agent`, `tool`, `BedrockModel`, `McpClient`; provider subpaths under + `/models/{bedrock,anthropic,openai,google}`; streaming via `agent.stream()` yielding `AgentStreamEvent`; filter + `contentBlockDelta` + `textDelta`. +- `bedrock-agentcore@0.2.2` — `BedrockAgentCoreApp` from `/runtime`, identity HOFs from `/identity`. + +- [ ] `base/gitignore.template` — `node_modules`, `dist`, `.env*`, `*.log`, `.venv`. + - **Notes:** +- [ ] `base/package.json` (Handlebars) — name, deps pinned (`@strands-agents/sdk@1.0.0-rc.4`, `bedrock-agentcore@0.2.2`, + `tsx`, `typescript`, `@types/node`). + - **Notes:** +- [ ] `base/tsconfig.json` — target ES2022, module NodeNext, strict, outDir `dist`. + - **Notes:** +- [ ] `base/main.ts` — `BedrockAgentCoreApp` with `invocationHandler.process` async generator; calls + `agent.stream(prompt)`; yields `{ data: string }`. Verify event shape against `dist/src/models/streaming.d.ts` in + the Strands SDK tarball. + - **Notes:** +- [ ] `base/README.md` — short, mirrors Python README structure. + - **Notes:** +- [ ] `base/mcp_client/client.ts` — mirrors Python `mcp_client/client.py` (gateway + auth paths). Uses `McpClient` from + Strands with a `Transport` from `@modelcontextprotocol/sdk/shared/transport.js`. + - **Notes:** +- [ ] `base/model/load.ts` — mirrors Python `model/load.py`; per-provider branches for Bedrock / Anthropic / OpenAI / + Gemini using Strands provider subpaths. + - **Notes:** +- [ ] `capabilities/memory/session.ts` — mirrors Python `capabilities/memory/session.py`. + - **Notes:** +- [ ] Confirm Handlebars variables consumed match Python templates: `name`, `agentName`, `modelProvider`, `hasGateway`, + `gatewayAuthTypes`, `gatewayProviders`, `hasMemory`, `memoryProviders`, `identityProviders`, + `sessionStorageMountPath`, `isVpc`. + - **Notes:** +- [ ] Run `npm run test:update-snapshots` and **eyeball every generated snapshot** before committing. + - **Notes:** + +--- + +## Phase 4 — Container template + +Under `src/assets/container/typescript/`. + +- [ ] `Dockerfile` — base `public.ecr.aws/docker/library/node:22-slim`; deps layer first (`package.json` + + `package-lock.json` → `npm ci --omit=dev`), then source. Either `npx tsx main.ts` or a `tsc` build step + + `node dist/main.js`. Expose 8080. + - **Notes:** +- [ ] `dockerignore.template` — `node_modules`, `dist`, `.env*`, `.git/`. + - **Notes:** + +--- + +## Phase 5 — Node setup helper + create-flow wiring + +- [ ] Create `src/cli/operations/node/setup.ts` with `setupNodeProject({ projectDir })` that shells out to `npm install` + and returns `{ status: 'success' | 'error' | 'warn', ... }` (match `src/cli/operations/python/setup.ts` shape). + - **Notes:** +- [ ] Wire into `src/cli/tui/screens/create/useCreateFlow.ts` around line 431 — parallel branch to the existing Python + setup step when `language === 'TypeScript' && agentType === 'create'`. + - **Notes:** +- [ ] Extend `checkCreateDependencies({ language })` in `src/cli/external-requirements/checks.ts` (called from + `src/cli/commands/create/action.ts`) to verify `node` + `npm` on PATH when `language === 'TypeScript'`. + - **Notes:** + +--- + +## Phase 6 — Tests + +- [ ] **Snapshots** — `src/assets/__tests__/assets.snapshot.test.ts` already auto-discovers `typescript/*` files (lines + 106-120). Run `npm run test:update-snapshots` after Phase 3 templates land. + - **Notes:** +- [ ] **Create integ test** — duplicate the Python block in `integ-tests/create-with-agent.test.ts` for TypeScript. + Assert: `app//main.ts`, `app//package.json`, `app//node_modules/` (if install ran), and + `agentcore.json` has `runtimeVersion: "NODE_22"` + `entrypoint: "main.ts"`. + - **Notes:** +- [ ] **Dev-server unit test** — add a TS variant in `src/cli/operations/dev/__tests__/codezip-dev-server.test.ts` + asserting spawn config is `{ cmd: 'npx', args: ['tsx', 'watch', 'main.ts'], ... }`. + - **Notes:** +- [ ] **TUI harness walkthrough** — mirror an existing Python walkthrough under `integ-tests/tui/` selecting TypeScript + → Strands. + - **Notes:** +- [ ] **E2E container deploy test** — `integ-tests/deploy-typescript-strands-container.test.ts`: scaffold → container + build → `agentcore deploy` (account 325335451438, `AWS_PROFILE=deploy`) → `agentcore invoke --prompt "ping"` → + teardown on exit and on failure. Gate behind the same env flag as other AWS integ tests. + - **Notes:** +- [ ] **Non-Strands rejection test** — confirm + `agentcore add agent --language TypeScript --framework LangChain_LangGraph` fails fast. + - **Notes:** + +--- + +## Phase 7 — Documentation + +- [ ] `docs/frameworks.md` — add "Supported languages" section. + - **Notes:** +- [ ] `docs/local-development.md` — TS dev loop (Node ≥ 18, `npx tsx watch`). + - **Notes:** +- [ ] `docs/commands.md` — `--language TypeScript` examples. + - **Notes:** +- [ ] `docs/container-builds.md` — TS Dockerfile example. + - **Notes:** +- [ ] `README.md` — one-line mention in the feature list. + - **Notes:** + +--- + +## Phase 8 — Verification (end-to-end, manual) + +Run from a clean scratch dir against the deploy profile. Record results inline. + +- [ ] `npm run test:unit` green. +- [ ] `npm run test:integ` green (excluding gated deploy test unless credentials refreshed). +- [ ] `agentcore create my-ts-agent` → TypeScript → Strands → Bedrock → no memory → confirm scaffold. +- [ ] `agentcore dev` starts via `npx tsx watch main.ts`, binds 8080, reloads on edit. +- [ ] `agentcore invoke --prompt "hello"` against the dev server streams a response. +- [ ] `agentcore deploy` (CodeZip) succeeds against test account; post-deploy `invoke` works. +- [ ] E2E container deploy test passes (Phase 6). +- [ ] Non-Strands framework rejection message is clear. +- [ ] Python regression smoke path unchanged. +- [ ] Docs read cleanly; examples copy-paste. + +--- + +## Out of scope (do not attempt) + +- LangChain/LangGraph, GoogleADK, OpenAIAgents TS templates. +- A2A / MCP protocol TS templates. +- pnpm / yarn. +- BYO TypeScript (already works via `--type byo`). + +--- + +## Known gotchas + +- Strands TS SDK is at `1.0.0-rc.4` (very new). Pin exactly and re-check event-type names before release. +- `BedrockAgentCoreApp` (TS) is Fastify-based, not ASGI. `npx tsx watch` gives good-enough dev parity with Python's + uvicorn `--reload`. +- `isDevSupported` Python guard is easy to miss. See Phase 2. +- `BedrockAgentCoreApp.invocationHandler.process` returns an async generator; runtime wraps it as SSE. Yield + `{ data: string }`. +- Do NOT add `Co-Authored-By` trailers referencing Claude or any AI assistant (per workspace `CLAUDE.md`). + +--- + +## Commit log + +Append a one-line entry per commit as you go. Newest at the bottom. Format: `: `. + +- `50a6cbd` — Phase 1: scaffold TypeScript language support (WIP checkpoint, schema + UI + validator + spec-mapper). +- _(next commit goes here)_ From 5bd7f8258731ab7459848c372497478e306f7a0f Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 19:44:17 +0000 Subject: [PATCH 03/36] docs(typescript): log 3417f9a in progress tracker Confidence: high Scope-risk: narrow --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index 6a7dc8477..92b385399 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -215,4 +215,5 @@ Run from a clean scratch dir against the deploy profile. Record results inline. Append a one-line entry per commit as you go. Newest at the bottom. Format: `: `. - `50a6cbd` — Phase 1: scaffold TypeScript language support (WIP checkpoint, schema + UI + validator + spec-mapper). +- `3417f9a` — Phase 0: add progress tracker doc (verification sweep results baked in). - _(next commit goes here)_ From a487f199f2e5a0dc12d5fec15c1c6f61a74dfa9c Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 19:56:41 +0000 Subject: [PATCH 04/36] feat(typescript): unblock dev mode for TS agents (Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the Python-only guard in isDevSupported. CodeZipDevServer already branches on entrypoint extension and selects `npx tsx watch` for non-Python agents, so the guard was the only thing rejecting TS entrypoints. Constraint: Dev-server spawn path must stay unchanged for Python agents Rejected: Add an explicit TypeScript allow-list branch | redundant — extension check already distinguishes, extra branch adds drift risk Confidence: high Scope-risk: narrow Not-tested: TS-specific unit test for spawn config (deferred to Phase 6) --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 12 ++++++------ src/cli/operations/dev/config.ts | 16 +--------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index 92b385399..d40d41b34 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -55,12 +55,12 @@ Captured in commit `50a6cbd`. Do NOT re-do these. ## Phase 2 — Dev-server unblock (small, unblocking change) -- [ ] Remove / relax `isDevSupported` Python-only guard at `src/cli/operations/dev/config.ts:35-57`. - - **Approach:** drop the `!isPythonAgent(agent)` branch entirely. Downstream (`codezip-dev-server.ts:120-126`) already - picks `npx tsx watch` when `isPython` is false, and `isPython` at `config.ts:141` keys off entrypoint extension — so - it will naturally be `false` for `main.ts`. - - **Verify:** unit test in `codezip-dev-server.test.ts` still green; add a TS case (see Phase 6). - - **Notes:** +- [x] Remove / relax `isDevSupported` Python-only guard at `src/cli/operations/dev/config.ts:35-57`. + - **Approach:** dropped the `!isPythonAgent(agent)` branch entirely. Downstream (`codezip-dev-server.ts:120-126`) + already picks `npx tsx watch` when `isPython` is false, and `isPython` at `config.ts:141` keys off entrypoint + extension — so it is naturally `false` for `main.ts`. + - **Verify:** `npx tsc --noEmit` clean; `codezip-dev-server.test.ts` all 5 specs green. + - **Notes:** TS-specific unit test deferred to Phase 6. --- diff --git a/src/cli/operations/dev/config.ts b/src/cli/operations/dev/config.ts index fd13637bb..95b855124 100644 --- a/src/cli/operations/dev/config.ts +++ b/src/cli/operations/dev/config.ts @@ -29,8 +29,7 @@ function isPythonAgent(agent: AgentEnvSpec): boolean { * Checks if dev mode is supported for the given agent. * * Requirements: - * - Agent must target Python (TypeScript support not yet implemented) - * - CodeZip agents must have entrypoint + * - Agent must have an entrypoint */ function isDevSupported(agent: AgentEnvSpec): DevSupportResult { if (!agent.entrypoint) { @@ -40,19 +39,6 @@ function isDevSupported(agent: AgentEnvSpec): DevSupportResult { }; } - // Container agents are supported for dev mode (requires local container runtime) - if (agent.build === 'Container') { - return { supported: true }; - } - - // Currently only Python is supported for CodeZip dev mode - if (!isPythonAgent(agent)) { - return { - supported: false, - reason: `Dev mode only supports Python agents. Agent "${agent.name}" does not appear to be a Python agent.`, - }; - } - return { supported: true }; } From 6f1aeedabad7de89e167f1cd72327608303d5208 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 19:57:08 +0000 Subject: [PATCH 05/36] docs(typescript): log a487f19 in progress tracker --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index d40d41b34..c4ce25ce6 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -216,4 +216,5 @@ Append a one-line entry per commit as you go. Newest at the bottom. Format: ` Date: Tue, 21 Apr 2026 20:05:57 +0000 Subject: [PATCH 06/36] feat(typescript): author TS/Strands HTTP template assets (Phase 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add src/assets/typescript/http/strands/ with gitignore, package.json, tsconfig.json, main.ts, README.md, mcp_client/client.ts, model/load.ts, and capabilities/memory/session.ts — mirroring the Python Strands templates surface-for-surface, keyed off the same Handlebars variables. Update asset snapshot baseline (8 new TS files + file-listing diff). Align three stale tests with the Phase 1/2 validator + dev-server changes: accept TS+Strands in validateAddAgentOptions; treat Node entrypoints as dev-supported in getDevSupportedAgents / getDevConfig. Extend .prettierignore to skip src/assets TS/JSON template files — these are Handlebars templates with {{...}} expressions that do not parse as TypeScript. ESLint already ignores src/assets via eslint.config. Constraint: SDK surface for @strands-agents/sdk + bedrock-agentcore TS is still RC; templates are written against the surface described in the handoff Rejected: Run prettier over the Handlebars TS templates | they do not parse as TS — same reason Python templates with {{...}} aren't formatted as Python Confidence: medium Scope-risk: narrow Directive: Verify SDK event-type names, model-class names, and memory import path against the actual tarballs before shipping — see Known gotchas in docs/TYPESCRIPT_SUPPORT_PROGRESS.md Not-tested: Rendered templates have not been compiled or run against real SDKs (Phase 6 integ + Phase 8 E2E) --- .prettierignore | 3 + docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 56 +- .../assets.snapshot.test.ts.snap | 520 ++++++++++++++++++ .../typescript/http/strands/base/README.md | 37 ++ .../http/strands/base/gitignore.template | 22 + .../typescript/http/strands/base/main.ts | 178 ++++++ .../http/strands/base/mcp_client/client.ts | 75 +++ .../http/strands/base/model/load.ts | 83 +++ .../typescript/http/strands/base/package.json | 22 + .../http/strands/base/tsconfig.json | 19 + .../strands/capabilities/memory/session.ts | 44 ++ .../commands/add/__tests__/add-agent.test.ts | 6 +- .../commands/add/__tests__/validate.test.ts | 21 +- .../operations/dev/__tests__/config.test.ts | 43 +- 14 files changed, 1077 insertions(+), 52 deletions(-) create mode 100644 src/assets/typescript/http/strands/base/README.md create mode 100644 src/assets/typescript/http/strands/base/gitignore.template create mode 100644 src/assets/typescript/http/strands/base/main.ts create mode 100644 src/assets/typescript/http/strands/base/mcp_client/client.ts create mode 100644 src/assets/typescript/http/strands/base/model/load.ts create mode 100644 src/assets/typescript/http/strands/base/package.json create mode 100644 src/assets/typescript/http/strands/base/tsconfig.json create mode 100644 src/assets/typescript/http/strands/capabilities/memory/session.ts diff --git a/.prettierignore b/.prettierignore index b02529699..2cafa6262 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,5 @@ CHANGELOG.md src/assets/**/*.md +src/assets/**/*.ts +src/assets/**/*.json +src/assets/**/*.template diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index c4ce25ce6..d7a4b95dd 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -75,33 +75,37 @@ Author under `src/assets/typescript/http/strands/`. Mirror Python shape at `src/ `contentBlockDelta` + `textDelta`. - `bedrock-agentcore@0.2.2` — `BedrockAgentCoreApp` from `/runtime`, identity HOFs from `/identity`. -- [ ] `base/gitignore.template` — `node_modules`, `dist`, `.env*`, `*.log`, `.venv`. - - **Notes:** -- [ ] `base/package.json` (Handlebars) — name, deps pinned (`@strands-agents/sdk@1.0.0-rc.4`, `bedrock-agentcore@0.2.2`, - `tsx`, `typescript`, `@types/node`). - - **Notes:** -- [ ] `base/tsconfig.json` — target ES2022, module NodeNext, strict, outDir `dist`. - - **Notes:** -- [ ] `base/main.ts` — `BedrockAgentCoreApp` with `invocationHandler.process` async generator; calls - `agent.stream(prompt)`; yields `{ data: string }`. Verify event shape against `dist/src/models/streaming.d.ts` in - the Strands SDK tarball. - - **Notes:** -- [ ] `base/README.md` — short, mirrors Python README structure. - - **Notes:** -- [ ] `base/mcp_client/client.ts` — mirrors Python `mcp_client/client.py` (gateway + auth paths). Uses `McpClient` from - Strands with a `Transport` from `@modelcontextprotocol/sdk/shared/transport.js`. - - **Notes:** -- [ ] `base/model/load.ts` — mirrors Python `model/load.py`; per-provider branches for Bedrock / Anthropic / OpenAI / - Gemini using Strands provider subpaths. - - **Notes:** -- [ ] `capabilities/memory/session.ts` — mirrors Python `capabilities/memory/session.py`. - - **Notes:** -- [ ] Confirm Handlebars variables consumed match Python templates: `name`, `agentName`, `modelProvider`, `hasGateway`, +- [x] `base/gitignore.template` — `node_modules`, `dist`, `.env*`, `*.log`. + - **Notes:** +- [x] `base/package.json` (Handlebars) — name, deps pinned (`@strands-agents/sdk@1.0.0-rc.4`, `bedrock-agentcore@0.2.2`, + `@modelcontextprotocol/sdk`, `tsx`, `typescript`, `@types/node`). ESM (`"type": "module"`). + - **Notes:** +- [x] `base/tsconfig.json` — target ES2022, module NodeNext, strict, outDir `dist`. + - **Notes:** +- [x] `base/main.ts` — `BedrockAgentCoreApp` with `invocationHandler.process` async generator; calls + `agent.stream(prompt)`; yields `{ data: string }`. Filters on `contentBlockDelta` + `textDelta`. + - **Notes:** Event shape still needs verification against the actual SDK tarball before release; templates currently + trust the handoff's description. +- [x] `base/README.md` — short, mirrors Python README structure (Node dev loop, LOCAL_DEV env var). + - **Notes:** +- [x] `base/mcp_client/client.ts` — mirrors Python `mcp_client/client.py` (gateway + auth paths). Uses `McpClient` from + Strands with `StreamableHTTPClientTransport` from `@modelcontextprotocol/sdk/client/streamableHttp.js`. + - **Notes:** AWS_IAM gateway auth is stubbed — TS `mcp-proxy-for-aws` package not yet confirmed; non-IAM paths work. +- [x] `base/model/load.ts` — mirrors Python `model/load.py`; per-provider branches for Bedrock / Anthropic / OpenAI / + Gemini using Strands provider subpaths (`@strands-agents/sdk/models/{bedrock,anthropic,openai,google}`). Gemini + maps to `GoogleModel`. + - **Notes:** `withApiKey` HOF usage follows handoff's identity-surface description — verify against SDK before + release. +- [x] `capabilities/memory/session.ts` — mirrors Python `capabilities/memory/session.py`; imports from + `bedrock-agentcore/memory/strands`. + - **Notes:** Python uses `bedrock_agentcore.memory.integrations.strands.*`; TS import path is the described + `bedrock-agentcore/memory/strands` surface — reconfirm once SDK is installed. +- [x] Confirm Handlebars variables consumed match Python templates: `name`, `modelProvider`, `hasGateway`, `gatewayAuthTypes`, `gatewayProviders`, `hasMemory`, `memoryProviders`, `identityProviders`, - `sessionStorageMountPath`, `isVpc`. - - **Notes:** -- [ ] Run `npm run test:update-snapshots` and **eyeball every generated snapshot** before committing. - - **Notes:** + `sessionStorageMountPath`, `isVpc`, `hasIdentity`. + - **Notes:** Same set as Python. +- [x] Run `npm run test:update-snapshots` and eyeball generated snapshots. + - **Notes:** 8 new TS snapshots written; file-listing snapshot updated to include the new paths. --- diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 1a51b6b29..b39711e45 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -522,6 +522,14 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f "python/mcp/standalone/base/main.py", "python/mcp/standalone/base/pyproject.toml", "typescript/.gitkeep", + "typescript/http/strands/base/README.md", + "typescript/http/strands/base/gitignore.template", + "typescript/http/strands/base/main.ts", + "typescript/http/strands/base/mcp_client/client.ts", + "typescript/http/strands/base/model/load.ts", + "typescript/http/strands/base/package.json", + "typescript/http/strands/base/tsconfig.json", + "typescript/http/strands/capabilities/memory/session.ts", ] `; @@ -4552,3 +4560,515 @@ When modifying JSON config files: `; exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/.gitkeep should match snapshot 1`] = `""`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/README.md should match snapshot 1`] = ` +"This is a project generated by the AgentCore CLI! + +# Layout + +The generated application code lives at the agent root directory. At the root, there is a \`.gitignore\` file, an +\`agentcore/\` folder which represents the configurations and state associated with this project. Other \`agentcore\` +commands like \`deploy\`, \`dev\`, and \`invoke\` rely on the configuration stored here. + +## Agent Root + +The main entrypoint to your app is defined in \`main.ts\`. Using the AgentCore SDK \`BedrockAgentCoreApp\`, this file +defines a Fastify-based HTTP app that streams tokens from your chosen Agent framework SDK. + +\`model/load.ts\` instantiates your chosen model provider. + +## Environment Variables + +| Variable | Required | Description | +| --- | --- | --- | +{{#if hasIdentity}}| \`{{identityProviders.[0].envVarName}}\` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) | +{{/if}}| \`LOCAL_DEV\` | No | Set to \`1\` to use \`.env.local\` instead of AgentCore Identity | + +# Developing locally + +If installation was successful, \`node_modules/\` is already populated with dependencies. + +\`agentcore dev\` will start a local server on 0.0.0.0:8080 using \`npx tsx watch main.ts\` for hot reload. + +In a new terminal, you can invoke that server with: + +\`agentcore invoke --dev "What can you do"\` + +# Deployment + +After providing credentials, \`agentcore deploy\` will deploy your project into Amazon Bedrock AgentCore. + +Use \`agentcore invoke\` to invoke your deployed agent. +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/gitignore.template should match snapshot 1`] = ` +"# Environment variables +.env +.env.* + +# Node +node_modules/ +dist/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/main.ts should match snapshot 1`] = ` +"import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime'; +import { Agent, tool } from '@strands-agents/sdk'; +import { loadModel } from './model/load.js'; +{{#if hasGateway}} +import { getAllGatewayMcpClients } from './mcp_client/client.js'; +{{else}} +import { getStreamableHttpMcpClient } from './mcp_client/client.js'; +{{/if}} +{{#if hasMemory}} +import { getMemorySessionManager } from './memory/session.js'; +{{/if}} +{{#if sessionStorageMountPath}} +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; +{{/if}} + +const app = new BedrockAgentCoreApp(); + +// Define a collection of MCP clients +{{#if hasGateway}} +const mcpClients = getAllGatewayMcpClients(); +{{else}} +const mcpClients = [getStreamableHttpMcpClient()].filter(Boolean); +{{/if}} + +// Define a collection of tools used by the model +const tools: unknown[] = []; + +// Define a simple function tool +const addNumbers = tool({ + name: 'add_numbers', + description: 'Return the sum of two numbers', + inputSchema: { + type: 'object', + properties: { + a: { type: 'number' }, + b: { type: 'number' }, + }, + required: ['a', 'b'], + }, + handler: async ({ a, b }: { a: number; b: number }) => a + b, +}); +tools.push(addNumbers); + +{{#if sessionStorageMountPath}} +const SESSION_STORAGE_PATH = '{{sessionStorageMountPath}}'; + +function safeResolve(p: string): string { + const base = path.resolve(SESSION_STORAGE_PATH); + const resolved = path.resolve(base, p.replace(/^\\/+/, '')); + if (!resolved.startsWith(base)) { + throw new Error(\`Path '\${p}' is outside the storage boundary\`); + } + return resolved; +} + +const fileRead = tool({ + name: 'file_read', + description: 'Read a file from persistent storage. The path is relative to the storage root.', + inputSchema: { + type: 'object', + properties: { path: { type: 'string' } }, + required: ['path'], + }, + handler: async ({ path: p }: { path: string }) => { + try { + return await fs.readFile(safeResolve(p), 'utf-8'); + } catch (err) { + return err instanceof Error ? err.message : String(err); + } + }, +}); + +const fileWrite = tool({ + name: 'file_write', + description: 'Write content to a file in persistent storage. The path is relative to the storage root.', + inputSchema: { + type: 'object', + properties: { path: { type: 'string' }, content: { type: 'string' } }, + required: ['path', 'content'], + }, + handler: async ({ path: p, content }: { path: string; content: string }) => { + try { + const full = safeResolve(p); + await fs.mkdir(path.dirname(full), { recursive: true }); + await fs.writeFile(full, content, 'utf-8'); + return \`Written to \${p}\`; + } catch (err) { + return err instanceof Error ? err.message : String(err); + } + }, +}); + +const listFiles = tool({ + name: 'list_files', + description: 'List files in persistent storage. The directory is relative to the storage root.', + inputSchema: { + type: 'object', + properties: { directory: { type: 'string' } }, + }, + handler: async ({ directory = '' }: { directory?: string }) => { + try { + const entries = await fs.readdir(safeResolve(directory)); + return entries.length > 0 ? entries.join('\\n') : '(empty directory)'; + } catch (err) { + return err instanceof Error ? err.message : String(err); + } + }, +}); + +tools.push(fileRead, fileWrite, listFiles); +{{/if}} + +// Add MCP clients to tools if available +for (const mcpClient of mcpClients) { + if (mcpClient) { + tools.push(mcpClient); + } +} + +const SYSTEM_PROMPT = \` +You are a helpful assistant. Use tools when appropriate. +{{#if sessionStorageMountPath}} +You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions. +{{/if}} +\`; + +{{#if hasMemory}} +const agentCache = new Map(); + +function getOrCreateAgent(sessionId: string, userId: string): Agent { + const key = \`\${sessionId}/\${userId}\`; + let agent = agentCache.get(key); + if (!agent) { + agent = new Agent({ + model: loadModel(), + sessionManager: getMemorySessionManager(sessionId, userId), + systemPrompt: SYSTEM_PROMPT, + tools, + }); + agentCache.set(key, agent); + } + return agent; +} +{{else}} +let cachedAgent: Agent | null = null; + +function getOrCreateAgent(): Agent { + if (!cachedAgent) { + cachedAgent = new Agent({ + model: loadModel(), + systemPrompt: SYSTEM_PROMPT, + tools, + }); + } + return cachedAgent; +} +{{/if}} + +app.invocationHandler = { + async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { +{{#if hasMemory}} + const sessionId = context.sessionId ?? 'default-session'; + const userId = context.userId ?? 'default-user'; + const agent = getOrCreateAgent(sessionId, userId); +{{else}} + const agent = getOrCreateAgent(); +{{/if}} + + for await (const event of agent.stream(payload.prompt ?? '')) { + if (event.type === 'contentBlockDelta' && event.delta?.type === 'textDelta') { + yield { data: event.delta.text }; + } + } + }, +}; + +app.run(); +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/mcp_client/client.ts should match snapshot 1`] = ` +"import { McpClient } from '@strands-agents/sdk'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +{{#if hasGateway}} +{{#if (includes gatewayAuthTypes "CUSTOM_JWT")}} +import { withAccessToken } from 'bedrock-agentcore/identity'; +{{/if}} + +{{#each gatewayProviders}} +{{#if (eq authType "CUSTOM_JWT")}} +async function getBearerToken{{snakeCase name}}(): Promise { + return withAccessToken( + { + providerName: '{{credentialProviderName}}', + scopes: [{{#if scopes}}'{{scopes}}'{{/if}}], + authFlow: 'M2M', + }, + async (accessToken: string) => accessToken + )(); +} + +{{/if}} +{{/each}} +{{#each gatewayProviders}} +export function get{{snakeCase name}}McpClient(): McpClient | null { + const url = process.env.{{envVarName}}; + if (!url) { + console.warn('{{envVarName}} not set — {{name}} gateway tools unavailable'); + return null; + } + {{#if (eq authType "CUSTOM_JWT")}} + const transport = new StreamableHTTPClientTransport(new URL(url), { + requestInit: { + headers: async () => { + const token = await getBearerToken{{snakeCase name}}(); + return token ? { Authorization: \`Bearer \${token}\` } : {}; + }, + }, + }); + {{else if (eq authType "AWS_IAM")}} + // AWS_IAM gateway auth for TypeScript is not yet supported — add SigV4 signing + // to the transport's requestInit when the mcp-proxy-for-aws TS package is available. + const transport = new StreamableHTTPClientTransport(new URL(url)); + {{else}} + const transport = new StreamableHTTPClientTransport(new URL(url)); + {{/if}} + return new McpClient({ transport }); +} + +{{/each}} +export function getAllGatewayMcpClients(): Array { + const clients: Array = []; + {{#each gatewayProviders}} + clients.push(get{{snakeCase name}}McpClient()); + {{/each}} + return clients; +} +{{else}} +{{#if isVpc}} +// VPC mode: external MCP endpoints are not reachable without a NAT gateway. +// Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below. + +export function getStreamableHttpMcpClient(): McpClient | null { + return null; +} +{{else}} +// ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication +const EXAMPLE_MCP_ENDPOINT = 'https://mcp.exa.ai/mcp'; + +export function getStreamableHttpMcpClient(): McpClient { + // to use an MCP server that supports bearer authentication, add a headers() callback to requestInit + const transport = new StreamableHTTPClientTransport(new URL(EXAMPLE_MCP_ENDPOINT)); + return new McpClient({ transport }); +} +{{/if}} +{{/if}} +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/model/load.ts should match snapshot 1`] = ` +"{{#if (eq modelProvider "Bedrock")}} +import { BedrockModel } from '@strands-agents/sdk/models/bedrock'; + +export function loadModel(): BedrockModel { + return new BedrockModel({ modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0' }); +} +{{/if}} +{{#if (eq modelProvider "Anthropic")}} +import { AnthropicModel } from '@strands-agents/sdk/models/anthropic'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(\`\${IDENTITY_ENV_VAR} not found. Add \${IDENTITY_ENV_VAR}=your-key to .env.local\`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +export function loadModel(): AnthropicModel { + return new AnthropicModel({ + clientArgs: { apiKey: getApiKey }, + modelId: 'claude-sonnet-4-5-20250929', + maxTokens: 5000, + }); +} +{{/if}} +{{#if (eq modelProvider "OpenAI")}} +import { OpenAIModel } from '@strands-agents/sdk/models/openai'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(\`\${IDENTITY_ENV_VAR} not found. Add \${IDENTITY_ENV_VAR}=your-key to .env.local\`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +export function loadModel(): OpenAIModel { + return new OpenAIModel({ + clientArgs: { apiKey: getApiKey }, + modelId: 'gpt-4.1', + }); +} +{{/if}} +{{#if (eq modelProvider "Gemini")}} +import { GoogleModel } from '@strands-agents/sdk/models/google'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(\`\${IDENTITY_ENV_VAR} not found. Add \${IDENTITY_ENV_VAR}=your-key to .env.local\`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +export function loadModel(): GoogleModel { + return new GoogleModel({ + clientArgs: { apiKey: getApiKey }, + modelId: 'gemini-2.5-flash', + }); +} +{{/if}} +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/package.json should match snapshot 1`] = ` +"{ + "name": "{{name}}", + "version": "0.1.0", + "description": "AgentCore Runtime Application using Strands TypeScript SDK", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/main.js", + "dev": "tsx watch main.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "@strands-agents/sdk": "1.0.0-rc.4", + "bedrock-agentcore": "0.2.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + } +} +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/tsconfig.json should match snapshot 1`] = ` +"{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/capabilities/memory/session.ts should match snapshot 1`] = ` +"import { + AgentCoreMemoryConfig, +{{#if memoryProviders.[0].strategies.length}} + RetrievalConfig, +{{/if}} + AgentCoreMemorySessionManager, +} from 'bedrock-agentcore/memory/strands'; + +const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}}; +const REGION = process.env.AWS_REGION; + +export function getMemorySessionManager( + sessionId: string, + actorId: string +): AgentCoreMemorySessionManager | null { + if (!MEMORY_ID) { + return null; + } + +{{#if memoryProviders.[0].strategies.length}} + const retrievalConfig: Record = { +{{#if (includes memoryProviders.[0].strategies "SEMANTIC")}} + [\`/users/\${actorId}/facts\`]: { topK: 3, relevanceScore: 0.5 }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}} + [\`/users/\${actorId}/preferences\`]: { topK: 3, relevanceScore: 0.5 }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} + [\`/summaries/\${actorId}/\${sessionId}\`]: { topK: 3, relevanceScore: 0.5 }, +{{/if}} + }; +{{/if}} + + const config: AgentCoreMemoryConfig = { + memoryId: MEMORY_ID, + sessionId, + actorId, +{{#if memoryProviders.[0].strategies.length}} + retrievalConfig, +{{/if}} + }; + + return new AgentCoreMemorySessionManager(config, REGION); +} +" +`; diff --git a/src/assets/typescript/http/strands/base/README.md b/src/assets/typescript/http/strands/base/README.md new file mode 100644 index 000000000..2f285b39b --- /dev/null +++ b/src/assets/typescript/http/strands/base/README.md @@ -0,0 +1,37 @@ +This is a project generated by the AgentCore CLI! + +# Layout + +The generated application code lives at the agent root directory. At the root, there is a `.gitignore` file, an +`agentcore/` folder which represents the configurations and state associated with this project. Other `agentcore` +commands like `deploy`, `dev`, and `invoke` rely on the configuration stored here. + +## Agent Root + +The main entrypoint to your app is defined in `main.ts`. Using the AgentCore SDK `BedrockAgentCoreApp`, this file +defines a Fastify-based HTTP app that streams tokens from your chosen Agent framework SDK. + +`model/load.ts` instantiates your chosen model provider. + +## Environment Variables + +| Variable | Required | Description | +| --- | --- | --- | +{{#if hasIdentity}}| `{{identityProviders.[0].envVarName}}` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) | +{{/if}}| `LOCAL_DEV` | No | Set to `1` to use `.env.local` instead of AgentCore Identity | + +# Developing locally + +If installation was successful, `node_modules/` is already populated with dependencies. + +`agentcore dev` will start a local server on 0.0.0.0:8080 using `npx tsx watch main.ts` for hot reload. + +In a new terminal, you can invoke that server with: + +`agentcore invoke --dev "What can you do"` + +# Deployment + +After providing credentials, `agentcore deploy` will deploy your project into Amazon Bedrock AgentCore. + +Use `agentcore invoke` to invoke your deployed agent. diff --git a/src/assets/typescript/http/strands/base/gitignore.template b/src/assets/typescript/http/strands/base/gitignore.template new file mode 100644 index 000000000..feb4f544d --- /dev/null +++ b/src/assets/typescript/http/strands/base/gitignore.template @@ -0,0 +1,22 @@ +# Environment variables +.env +.env.* + +# Node +node_modules/ +dist/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/src/assets/typescript/http/strands/base/main.ts b/src/assets/typescript/http/strands/base/main.ts new file mode 100644 index 000000000..45c791004 --- /dev/null +++ b/src/assets/typescript/http/strands/base/main.ts @@ -0,0 +1,178 @@ +import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime'; +import { Agent, tool } from '@strands-agents/sdk'; +import { loadModel } from './model/load.js'; +{{#if hasGateway}} +import { getAllGatewayMcpClients } from './mcp_client/client.js'; +{{else}} +import { getStreamableHttpMcpClient } from './mcp_client/client.js'; +{{/if}} +{{#if hasMemory}} +import { getMemorySessionManager } from './memory/session.js'; +{{/if}} +{{#if sessionStorageMountPath}} +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; +{{/if}} + +const app = new BedrockAgentCoreApp(); + +// Define a collection of MCP clients +{{#if hasGateway}} +const mcpClients = getAllGatewayMcpClients(); +{{else}} +const mcpClients = [getStreamableHttpMcpClient()].filter(Boolean); +{{/if}} + +// Define a collection of tools used by the model +const tools: unknown[] = []; + +// Define a simple function tool +const addNumbers = tool({ + name: 'add_numbers', + description: 'Return the sum of two numbers', + inputSchema: { + type: 'object', + properties: { + a: { type: 'number' }, + b: { type: 'number' }, + }, + required: ['a', 'b'], + }, + handler: async ({ a, b }: { a: number; b: number }) => a + b, +}); +tools.push(addNumbers); + +{{#if sessionStorageMountPath}} +const SESSION_STORAGE_PATH = '{{sessionStorageMountPath}}'; + +function safeResolve(p: string): string { + const base = path.resolve(SESSION_STORAGE_PATH); + const resolved = path.resolve(base, p.replace(/^\/+/, '')); + if (!resolved.startsWith(base)) { + throw new Error(`Path '${p}' is outside the storage boundary`); + } + return resolved; +} + +const fileRead = tool({ + name: 'file_read', + description: 'Read a file from persistent storage. The path is relative to the storage root.', + inputSchema: { + type: 'object', + properties: { path: { type: 'string' } }, + required: ['path'], + }, + handler: async ({ path: p }: { path: string }) => { + try { + return await fs.readFile(safeResolve(p), 'utf-8'); + } catch (err) { + return err instanceof Error ? err.message : String(err); + } + }, +}); + +const fileWrite = tool({ + name: 'file_write', + description: 'Write content to a file in persistent storage. The path is relative to the storage root.', + inputSchema: { + type: 'object', + properties: { path: { type: 'string' }, content: { type: 'string' } }, + required: ['path', 'content'], + }, + handler: async ({ path: p, content }: { path: string; content: string }) => { + try { + const full = safeResolve(p); + await fs.mkdir(path.dirname(full), { recursive: true }); + await fs.writeFile(full, content, 'utf-8'); + return `Written to ${p}`; + } catch (err) { + return err instanceof Error ? err.message : String(err); + } + }, +}); + +const listFiles = tool({ + name: 'list_files', + description: 'List files in persistent storage. The directory is relative to the storage root.', + inputSchema: { + type: 'object', + properties: { directory: { type: 'string' } }, + }, + handler: async ({ directory = '' }: { directory?: string }) => { + try { + const entries = await fs.readdir(safeResolve(directory)); + return entries.length > 0 ? entries.join('\n') : '(empty directory)'; + } catch (err) { + return err instanceof Error ? err.message : String(err); + } + }, +}); + +tools.push(fileRead, fileWrite, listFiles); +{{/if}} + +// Add MCP clients to tools if available +for (const mcpClient of mcpClients) { + if (mcpClient) { + tools.push(mcpClient); + } +} + +const SYSTEM_PROMPT = ` +You are a helpful assistant. Use tools when appropriate. +{{#if sessionStorageMountPath}} +You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions. +{{/if}} +`; + +{{#if hasMemory}} +const agentCache = new Map(); + +function getOrCreateAgent(sessionId: string, userId: string): Agent { + const key = `${sessionId}/${userId}`; + let agent = agentCache.get(key); + if (!agent) { + agent = new Agent({ + model: loadModel(), + sessionManager: getMemorySessionManager(sessionId, userId), + systemPrompt: SYSTEM_PROMPT, + tools, + }); + agentCache.set(key, agent); + } + return agent; +} +{{else}} +let cachedAgent: Agent | null = null; + +function getOrCreateAgent(): Agent { + if (!cachedAgent) { + cachedAgent = new Agent({ + model: loadModel(), + systemPrompt: SYSTEM_PROMPT, + tools, + }); + } + return cachedAgent; +} +{{/if}} + +app.invocationHandler = { + async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { +{{#if hasMemory}} + const sessionId = context.sessionId ?? 'default-session'; + const userId = context.userId ?? 'default-user'; + const agent = getOrCreateAgent(sessionId, userId); +{{else}} + const agent = getOrCreateAgent(); +{{/if}} + + for await (const event of agent.stream(payload.prompt ?? '')) { + if (event.type === 'contentBlockDelta' && event.delta?.type === 'textDelta') { + yield { data: event.delta.text }; + } + } + }, +}; + +app.run(); diff --git a/src/assets/typescript/http/strands/base/mcp_client/client.ts b/src/assets/typescript/http/strands/base/mcp_client/client.ts new file mode 100644 index 000000000..3ac4445a7 --- /dev/null +++ b/src/assets/typescript/http/strands/base/mcp_client/client.ts @@ -0,0 +1,75 @@ +import { McpClient } from '@strands-agents/sdk'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +{{#if hasGateway}} +{{#if (includes gatewayAuthTypes "CUSTOM_JWT")}} +import { withAccessToken } from 'bedrock-agentcore/identity'; +{{/if}} + +{{#each gatewayProviders}} +{{#if (eq authType "CUSTOM_JWT")}} +async function getBearerToken{{snakeCase name}}(): Promise { + return withAccessToken( + { + providerName: '{{credentialProviderName}}', + scopes: [{{#if scopes}}'{{scopes}}'{{/if}}], + authFlow: 'M2M', + }, + async (accessToken: string) => accessToken + )(); +} + +{{/if}} +{{/each}} +{{#each gatewayProviders}} +export function get{{snakeCase name}}McpClient(): McpClient | null { + const url = process.env.{{envVarName}}; + if (!url) { + console.warn('{{envVarName}} not set — {{name}} gateway tools unavailable'); + return null; + } + {{#if (eq authType "CUSTOM_JWT")}} + const transport = new StreamableHTTPClientTransport(new URL(url), { + requestInit: { + headers: async () => { + const token = await getBearerToken{{snakeCase name}}(); + return token ? { Authorization: `Bearer ${token}` } : {}; + }, + }, + }); + {{else if (eq authType "AWS_IAM")}} + // AWS_IAM gateway auth for TypeScript is not yet supported — add SigV4 signing + // to the transport's requestInit when the mcp-proxy-for-aws TS package is available. + const transport = new StreamableHTTPClientTransport(new URL(url)); + {{else}} + const transport = new StreamableHTTPClientTransport(new URL(url)); + {{/if}} + return new McpClient({ transport }); +} + +{{/each}} +export function getAllGatewayMcpClients(): Array { + const clients: Array = []; + {{#each gatewayProviders}} + clients.push(get{{snakeCase name}}McpClient()); + {{/each}} + return clients; +} +{{else}} +{{#if isVpc}} +// VPC mode: external MCP endpoints are not reachable without a NAT gateway. +// Add an AgentCore Gateway with `agentcore add gateway`, or configure your own endpoint below. + +export function getStreamableHttpMcpClient(): McpClient | null { + return null; +} +{{else}} +// ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication +const EXAMPLE_MCP_ENDPOINT = 'https://mcp.exa.ai/mcp'; + +export function getStreamableHttpMcpClient(): McpClient { + // to use an MCP server that supports bearer authentication, add a headers() callback to requestInit + const transport = new StreamableHTTPClientTransport(new URL(EXAMPLE_MCP_ENDPOINT)); + return new McpClient({ transport }); +} +{{/if}} +{{/if}} diff --git a/src/assets/typescript/http/strands/base/model/load.ts b/src/assets/typescript/http/strands/base/model/load.ts new file mode 100644 index 000000000..470fcb357 --- /dev/null +++ b/src/assets/typescript/http/strands/base/model/load.ts @@ -0,0 +1,83 @@ +{{#if (eq modelProvider "Bedrock")}} +import { BedrockModel } from '@strands-agents/sdk/models/bedrock'; + +export function loadModel(): BedrockModel { + return new BedrockModel({ modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0' }); +} +{{/if}} +{{#if (eq modelProvider "Anthropic")}} +import { AnthropicModel } from '@strands-agents/sdk/models/anthropic'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(`${IDENTITY_ENV_VAR} not found. Add ${IDENTITY_ENV_VAR}=your-key to .env.local`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +export function loadModel(): AnthropicModel { + return new AnthropicModel({ + clientArgs: { apiKey: getApiKey }, + modelId: 'claude-sonnet-4-5-20250929', + maxTokens: 5000, + }); +} +{{/if}} +{{#if (eq modelProvider "OpenAI")}} +import { OpenAIModel } from '@strands-agents/sdk/models/openai'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(`${IDENTITY_ENV_VAR} not found. Add ${IDENTITY_ENV_VAR}=your-key to .env.local`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +export function loadModel(): OpenAIModel { + return new OpenAIModel({ + clientArgs: { apiKey: getApiKey }, + modelId: 'gpt-4.1', + }); +} +{{/if}} +{{#if (eq modelProvider "Gemini")}} +import { GoogleModel } from '@strands-agents/sdk/models/google'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(`${IDENTITY_ENV_VAR} not found. Add ${IDENTITY_ENV_VAR}=your-key to .env.local`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +export function loadModel(): GoogleModel { + return new GoogleModel({ + clientArgs: { apiKey: getApiKey }, + modelId: 'gemini-2.5-flash', + }); +} +{{/if}} diff --git a/src/assets/typescript/http/strands/base/package.json b/src/assets/typescript/http/strands/base/package.json new file mode 100644 index 000000000..9200b9618 --- /dev/null +++ b/src/assets/typescript/http/strands/base/package.json @@ -0,0 +1,22 @@ +{ + "name": "{{name}}", + "version": "0.1.0", + "description": "AgentCore Runtime Application using Strands TypeScript SDK", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/main.js", + "dev": "tsx watch main.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.0", + "@strands-agents/sdk": "1.0.0-rc.4", + "bedrock-agentcore": "0.2.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + } +} diff --git a/src/assets/typescript/http/strands/base/tsconfig.json b/src/assets/typescript/http/strands/base/tsconfig.json new file mode 100644 index 000000000..c199ae076 --- /dev/null +++ b/src/assets/typescript/http/strands/base/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/assets/typescript/http/strands/capabilities/memory/session.ts b/src/assets/typescript/http/strands/capabilities/memory/session.ts new file mode 100644 index 000000000..d8f5d21ea --- /dev/null +++ b/src/assets/typescript/http/strands/capabilities/memory/session.ts @@ -0,0 +1,44 @@ +import { + AgentCoreMemoryConfig, +{{#if memoryProviders.[0].strategies.length}} + RetrievalConfig, +{{/if}} + AgentCoreMemorySessionManager, +} from 'bedrock-agentcore/memory/strands'; + +const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}}; +const REGION = process.env.AWS_REGION; + +export function getMemorySessionManager( + sessionId: string, + actorId: string +): AgentCoreMemorySessionManager | null { + if (!MEMORY_ID) { + return null; + } + +{{#if memoryProviders.[0].strategies.length}} + const retrievalConfig: Record = { +{{#if (includes memoryProviders.[0].strategies "SEMANTIC")}} + [`/users/${actorId}/facts`]: { topK: 3, relevanceScore: 0.5 }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}} + [`/users/${actorId}/preferences`]: { topK: 3, relevanceScore: 0.5 }, +{{/if}} +{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} + [`/summaries/${actorId}/${sessionId}`]: { topK: 3, relevanceScore: 0.5 }, +{{/if}} + }; +{{/if}} + + const config: AgentCoreMemoryConfig = { + memoryId: MEMORY_ID, + sessionId, + actorId, +{{#if memoryProviders.[0].strategies.length}} + retrievalConfig, +{{/if}} + }; + + return new AgentCoreMemorySessionManager(config, REGION); +} diff --git a/src/cli/commands/add/__tests__/add-agent.test.ts b/src/cli/commands/add/__tests__/add-agent.test.ts index 4d4353c36..cee0dfbeb 100644 --- a/src/cli/commands/add/__tests__/add-agent.test.ts +++ b/src/cli/commands/add/__tests__/add-agent.test.ts @@ -98,7 +98,7 @@ describe('add agent command', () => { expect(json.error.includes('Invalid framework'), `Error: ${json.error}`).toBeTruthy(); }); - it('rejects TypeScript for create path', async () => { + it('rejects TypeScript with a non-Strands framework', async () => { const result = await runCLI( [ 'add', @@ -108,7 +108,7 @@ describe('add agent command', () => { '--language', 'TypeScript', '--framework', - 'Strands', + 'LangChain_LangGraph', '--model-provider', 'Bedrock', '--memory', @@ -121,7 +121,7 @@ describe('add agent command', () => { expect(result.exitCode).toBe(1); const json = JSON.parse(result.stdout); expect(json.success).toBe(false); - expect(json.error.includes('Python'), `Error should mention Python: ${json.error}`).toBeTruthy(); + expect(json.error.includes('Strands'), `Error should mention Strands: ${json.error}`).toBeTruthy(); }); it('validates framework/model compatibility', async () => { diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index b304afa5c..2be895494 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -173,12 +173,25 @@ describe('validate', () => { }); // AC5: Create path language restrictions - it('returns error for create path with TypeScript or Other', () => { - let result = validateAddAgentOptions({ ...validAgentOptionsCreate, language: 'TypeScript' }); + it('accepts TypeScript with Strands and rejects TypeScript with other frameworks', () => { + let result = validateAddAgentOptions({ + ...validAgentOptionsCreate, + language: 'TypeScript', + framework: 'Strands', + }); + expect(result.valid).toBe(true); + + result = validateAddAgentOptions({ + ...validAgentOptionsCreate, + language: 'TypeScript', + framework: 'LangChain_LangGraph', + }); expect(result.valid).toBe(false); - expect(result.error?.includes('Python')).toBeTruthy(); + expect(result.error?.includes('Strands')).toBeTruthy(); + }); - result = validateAddAgentOptions({ ...validAgentOptionsCreate, language: 'Other' }); + it('returns error for create path with Other language', () => { + const result = validateAddAgentOptions({ ...validAgentOptionsCreate, language: 'Other' }); expect(result.valid).toBe(false); expect(result.error?.includes('Python')).toBeTruthy(); }); diff --git a/src/cli/operations/dev/__tests__/config.test.ts b/src/cli/operations/dev/__tests__/config.test.ts index b6967ac6e..f0cc3078b 100644 --- a/src/cli/operations/dev/__tests__/config.test.ts +++ b/src/cli/operations/dev/__tests__/config.test.ts @@ -32,13 +32,14 @@ describe('getDevConfig', () => { name: 'TestProject', version: 1, managedBy: 'CDK' as const, + // Agent with no entrypoint — not dev-supported runtimes: [ { - name: 'NodeAgent', + name: 'BrokenAgent', build: 'CodeZip', - runtimeVersion: 'NODE_20', - entrypoint: filePath('index.js'), // Not a Python agent - codeLocation: dirPath('./agents/node'), + runtimeVersion: 'PYTHON_3_12', + entrypoint: filePath(''), + codeLocation: dirPath('./agents/broken'), protocol: 'HTTP', }, ], @@ -115,18 +116,18 @@ describe('getDevConfig', () => { ); }); - it('throws when specified agent is not Python', () => { + it('returns TypeScript config when project has a Node agent with .ts entrypoint', () => { const project: AgentCoreProjectSpec = { name: 'TestProject', version: 1, managedBy: 'CDK' as const, runtimes: [ { - name: 'NodeAgent', + name: 'TsAgent', build: 'CodeZip', - runtimeVersion: 'NODE_20', - entrypoint: filePath('index.js'), - codeLocation: dirPath('./agents/node'), + runtimeVersion: 'NODE_22', + entrypoint: filePath('main.ts'), + codeLocation: dirPath('./agents/ts'), protocol: 'HTTP', }, ], @@ -138,7 +139,10 @@ describe('getDevConfig', () => { policyEngines: [], }; - expect(() => getDevConfig(workingDir, project, undefined, 'NodeAgent')).toThrow('Dev mode only supports Python'); + const config = getDevConfig(workingDir, project, undefined, 'TsAgent'); + expect(config).not.toBeNull(); + expect(config?.agentName).toBe('TsAgent'); + expect(config?.isPython).toBe(false); }); it('resolves directory from codeLocation relative to configRoot', () => { @@ -478,7 +482,7 @@ describe('getDevSupportedAgents', () => { expect(getDevSupportedAgents(project)).toEqual([]); }); - it('returns empty array when no agents are Python', () => { + it('returns Node agents as dev-supported alongside Python', () => { const project: AgentCoreProjectSpec = { name: 'TestProject', version: 1, @@ -487,8 +491,8 @@ describe('getDevSupportedAgents', () => { { name: 'NodeAgent', build: 'CodeZip', - runtimeVersion: 'NODE_20', - entrypoint: filePath('index.js'), + runtimeVersion: 'NODE_22', + entrypoint: filePath('main.ts'), codeLocation: dirPath('./agents/node'), protocol: 'HTTP', }, @@ -501,10 +505,12 @@ describe('getDevSupportedAgents', () => { policyEngines: [], }; - expect(getDevSupportedAgents(project)).toEqual([]); + const supported = getDevSupportedAgents(project); + expect(supported).toHaveLength(1); + expect(supported[0]?.name).toBe('NodeAgent'); }); - it('returns only Python agents with entrypoints', () => { + it('returns both Python and Node agents with entrypoints', () => { const project: AgentCoreProjectSpec = { name: 'TestProject', version: 1, @@ -521,8 +527,8 @@ describe('getDevSupportedAgents', () => { { name: 'NodeAgent', build: 'CodeZip', - runtimeVersion: 'NODE_20', - entrypoint: filePath('index.js'), + runtimeVersion: 'NODE_22', + entrypoint: filePath('main.ts'), codeLocation: dirPath('./agents/node'), protocol: 'HTTP', }, @@ -536,8 +542,7 @@ describe('getDevSupportedAgents', () => { }; const supported = getDevSupportedAgents(project); - expect(supported).toHaveLength(1); - expect(supported[0]?.name).toBe('PythonAgent'); + expect(supported.map(a => a.name)).toEqual(['PythonAgent', 'NodeAgent']); }); it('includes Container agents with entrypoints', () => { From 003f67275aa0d1b823706bb18b48ec694427cd59 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 20:06:55 +0000 Subject: [PATCH 07/36] docs(typescript): log 6f1aeed + f6ed2e9 in progress tracker --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index d7a4b95dd..7d22e2591 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -221,4 +221,6 @@ Append a one-line entry per commit as you go. Newest at the bottom. Format: ` Date: Tue, 21 Apr 2026 20:09:19 +0000 Subject: [PATCH 08/36] feat(typescript): add container template for TS agents (Phase 4) src/assets/container/typescript/Dockerfile uses node:22-slim with a cached deps layer (npm ci --omit=dev, fallback to npm install when no lockfile) and runs `npx tsx main.ts`. Exposes 8080/8000/9000 to match the Python Dockerfile contract. Non-root bedrock_agentcore user mirrors Python. Update asset file-listing snapshot for the two new files. Constraint: Dev and container runtimes should share one entrypoint shape so main.ts stays the single source of truth Rejected: Add a tsc build step + `node dist/main.js` CMD | adds a build surface and diverges dev vs container semantics; defer until image size or cold start becomes a problem Confidence: medium Scope-risk: narrow Not-tested: Container build + AgentCore deploy against a live runtime (Phase 6 E2E) --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 13 +++++----- .../assets.snapshot.test.ts.snap | 2 ++ src/assets/container/typescript/Dockerfile | 24 +++++++++++++++++++ .../typescript/dockerignore.template | 24 +++++++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 src/assets/container/typescript/Dockerfile create mode 100644 src/assets/container/typescript/dockerignore.template diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index 7d22e2591..a8c1b9634 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -113,12 +113,13 @@ Author under `src/assets/typescript/http/strands/`. Mirror Python shape at `src/ Under `src/assets/container/typescript/`. -- [ ] `Dockerfile` — base `public.ecr.aws/docker/library/node:22-slim`; deps layer first (`package.json` + - `package-lock.json` → `npm ci --omit=dev`), then source. Either `npx tsx main.ts` or a `tsc` build step + - `node dist/main.js`. Expose 8080. - - **Notes:** -- [ ] `dockerignore.template` — `node_modules`, `dist`, `.env*`, `.git/`. - - **Notes:** +- [x] `Dockerfile` — base `public.ecr.aws/docker/library/node:22-slim`; deps layer first (`package.json` + + `package-lock.json` → `npm ci --omit=dev` with fallback to `npm install` when no lockfile). Runs + `npx tsx main.ts`. Exposes 8080/8000/9000 to match the Python Dockerfile contract. + - **Notes:** Non-root `bedrock_agentcore` user mirrors Python container. Chose `tsx` over a `tsc` build step so dev + and container runtime share one entrypoint shape; revisit for production-size images if startup latency matters. +- [x] `dockerignore.template` — `node_modules`, `dist`, `.env*`, `.git/`, `.agentcore/artifacts/`, `*.zip`. + - **Notes:** Mirrors the Python `dockerignore.template` with Node substitutions. --- diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index b39711e45..6f7543ffa 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -446,6 +446,8 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f "cdk/tsconfig.json", "container/python/Dockerfile", "container/python/dockerignore.template", + "container/typescript/Dockerfile", + "container/typescript/dockerignore.template", "evaluators/python-lambda/execution-role-policy.json", "evaluators/python-lambda/lambda_function.py", "evaluators/python-lambda/pyproject.toml", diff --git a/src/assets/container/typescript/Dockerfile b/src/assets/container/typescript/Dockerfile new file mode 100644 index 000000000..d6f98eb97 --- /dev/null +++ b/src/assets/container/typescript/Dockerfile @@ -0,0 +1,24 @@ +FROM public.ecr.aws/docker/library/node:22-slim + +WORKDIR /app + +ENV NODE_ENV=production \ + DOCKER_CONTAINER=1 + +RUN useradd -m -u 1000 bedrock_agentcore + +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev || npm install --omit=dev + +COPY --chown=bedrock_agentcore:bedrock_agentcore . . + +USER bedrock_agentcore + +# AgentCore Runtime service contract ports +# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html +# 8080: HTTP Mode +# 8000: MCP Mode +# 9000: A2A Mode +EXPOSE 8080 8000 9000 + +CMD ["npx", "tsx", "main.ts"] diff --git a/src/assets/container/typescript/dockerignore.template b/src/assets/container/typescript/dockerignore.template new file mode 100644 index 000000000..4fe494a08 --- /dev/null +++ b/src/assets/container/typescript/dockerignore.template @@ -0,0 +1,24 @@ +# Node +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.vscode/ +.idea/ + +# Testing +coverage/ + +# Secrets and environment files +.env +.env.* + +# Version control +.git/ + +# AgentCore build artifacts +.agentcore/artifacts/ +*.zip From f015ce7c517440eab5bf326e3b165020bb8f8c69 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 20:09:39 +0000 Subject: [PATCH 09/36] docs(typescript): log 003f672 + 076a4aa in progress tracker --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index a8c1b9634..d759811cf 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -224,4 +224,6 @@ Append a one-line entry per commit as you go. Newest at the bottom. Format: ` Date: Tue, 21 Apr 2026 20:13:09 +0000 Subject: [PATCH 10/36] feat(typescript): Node setup helper + create-flow wiring (Phase 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add src/cli/operations/node/setup.ts with setupNodeProject({projectDir}) that runs `npm install` and returns a structured result, mirroring the Python setup helper. Export via a new node/ operations barrel. Wire into useCreateFlow so a TypeScript create-path scaffold runs `npm install` in app// after the agent is added. Add a matching entry to getCreateSteps so the progress UI reflects the step. Unit tests mirror python/setup.test.ts (8 specs). checkCreateDependencies requires no change — npm is already checked unconditionally (needed for CDK synth) and uv is already gated to Python. Node version is likewise checked unconditionally. Constraint: Fresh scaffolds don't ship a lockfile Rejected: Use `npm ci` | fails without package-lock.json; switching to `npm ci` after the first install is a later optimization Rejected: Mark npm_not_found as a hard error | keep parity with Python (uv failure is a warn, not error) so create still succeeds and the user can install deps manually Confidence: high Scope-risk: narrow Not-tested: End-to-end scaffold run against a real npm registry (Phase 8) --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 24 ++-- src/cli/operations/index.ts | 1 + .../operations/node/__tests__/setup.test.ts | 107 ++++++++++++++++++ src/cli/operations/node/index.ts | 8 ++ src/cli/operations/node/setup.ts | 51 +++++++++ src/cli/tui/screens/create/useCreateFlow.ts | 27 ++++- 6 files changed, 209 insertions(+), 9 deletions(-) create mode 100644 src/cli/operations/node/__tests__/setup.test.ts create mode 100644 src/cli/operations/node/index.ts create mode 100644 src/cli/operations/node/setup.ts diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index d759811cf..96fd9ae75 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -125,14 +125,22 @@ Under `src/assets/container/typescript/`. ## Phase 5 — Node setup helper + create-flow wiring -- [ ] Create `src/cli/operations/node/setup.ts` with `setupNodeProject({ projectDir })` that shells out to `npm install` - and returns `{ status: 'success' | 'error' | 'warn', ... }` (match `src/cli/operations/python/setup.ts` shape). - - **Notes:** -- [ ] Wire into `src/cli/tui/screens/create/useCreateFlow.ts` around line 431 — parallel branch to the existing Python - setup step when `language === 'TypeScript' && agentType === 'create'`. - - **Notes:** -- [ ] Extend `checkCreateDependencies({ language })` in `src/cli/external-requirements/checks.ts` (called from - `src/cli/commands/create/action.ts`) to verify `node` + `npm` on PATH when `language === 'TypeScript'`. +- [x] Create `src/cli/operations/node/setup.ts` with `setupNodeProject({ projectDir })` that shells out to `npm install` + and returns `{ status: NodeSetupStatus, error? }` (matches `src/cli/operations/python/setup.ts` shape). Added + `src/cli/operations/node/index.ts` barrel and wired it into `src/cli/operations/index.ts`. + - **Notes:** Used `npm install` not `npm ci` because fresh scaffolds don't ship a lockfile; `package-lock.json` is + generated on first install. Respects `AGENTCORE_SKIP_INSTALL` like the Python helper. +- [x] Wire into `src/cli/tui/screens/create/useCreateFlow.ts` — parallel branch to the existing Python setup step when + `language === 'TypeScript' && agentType === 'create'`; also added a matching `getCreateSteps` entry so the + progress UI shows the new step. + - **Notes:** +- [x] Extend `checkCreateDependencies({ language })` in `src/cli/external-requirements/checks.ts` — **no change + required**. The existing `npm` check runs unconditionally (always needed for CDK synth), and the `uv` check is + already gated to `language === 'Python'`. + - **Notes:** Phase 0's Node-version check also already runs unconditionally (`checkNodeVersion`), so TS projects are + covered with zero additional logic. +- [x] Unit tests for the Node setup helper mirroring `python/setup.test.ts` — 8 specs added under + `src/cli/operations/node/__tests__/setup.test.ts`. - **Notes:** --- diff --git a/src/cli/operations/index.ts b/src/cli/operations/index.ts index c09332659..f16f1b2cc 100644 --- a/src/cli/operations/index.ts +++ b/src/cli/operations/index.ts @@ -4,6 +4,7 @@ export * from './dev'; export * from './fetch-access'; export * from './init'; export * from './mcp'; +export * from './node'; export * from './python'; export * from './remove'; export * from './resolve-agent'; diff --git a/src/cli/operations/node/__tests__/setup.test.ts b/src/cli/operations/node/__tests__/setup.test.ts new file mode 100644 index 000000000..4f30b36f9 --- /dev/null +++ b/src/cli/operations/node/__tests__/setup.test.ts @@ -0,0 +1,107 @@ +import * as lib from '../../../../lib/index.js'; +import { checkNpmAvailable, installNodeDependencies, setupNodeProject } from '../setup.js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../../../../lib/index.js', async () => { + const actual = await vi.importActual('../../../../lib/index.js'); + return { + ...actual, + checkSubprocess: vi.fn(), + runSubprocessCapture: vi.fn(), + }; +}); + +const mockCheckSubprocess = vi.mocked(lib.checkSubprocess); +const mockRunSubprocessCapture = vi.mocked(lib.runSubprocessCapture); + +describe('checkNpmAvailable', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('returns true when npm is available', async () => { + mockCheckSubprocess.mockResolvedValue(true); + + expect(await checkNpmAvailable()).toBe(true); + expect(mockCheckSubprocess).toHaveBeenCalledWith('npm', ['--version']); + }); + + it('returns false when npm is not available', async () => { + mockCheckSubprocess.mockResolvedValue(false); + + expect(await checkNpmAvailable()).toBe(false); + }); +}); + +describe('installNodeDependencies', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('returns success when install succeeds', async () => { + mockRunSubprocessCapture.mockResolvedValue({ code: 0, stdout: '', stderr: '', signal: null }); + + const result = await installNodeDependencies('/project'); + + expect(result.status).toBe('success'); + expect(mockRunSubprocessCapture).toHaveBeenCalledWith('npm', ['install'], { cwd: '/project' }); + }); + + it('returns install_failed on error', async () => { + mockRunSubprocessCapture.mockResolvedValue({ code: 1, stdout: 'some output', stderr: '', signal: null }); + + const result = await installNodeDependencies('/project'); + + expect(result.status).toBe('install_failed'); + expect(result.error).toBe('some output'); + }); +}); + +describe('setupNodeProject', () => { + const origEnv = process.env.AGENTCORE_SKIP_INSTALL; + + afterEach(() => { + vi.clearAllMocks(); + if (origEnv !== undefined) process.env.AGENTCORE_SKIP_INSTALL = origEnv; + else delete process.env.AGENTCORE_SKIP_INSTALL; + }); + + it('skips install when AGENTCORE_SKIP_INSTALL is set', async () => { + process.env.AGENTCORE_SKIP_INSTALL = '1'; + + const result = await setupNodeProject({ projectDir: '/project' }); + + expect(result.status).toBe('success'); + expect(mockCheckSubprocess).not.toHaveBeenCalled(); + }); + + it('returns npm_not_found when npm is not available', async () => { + delete process.env.AGENTCORE_SKIP_INSTALL; + mockCheckSubprocess.mockResolvedValue(false); + + const result = await setupNodeProject({ projectDir: '/project' }); + + expect(result.status).toBe('npm_not_found'); + expect(result.error).toContain('npm'); + }); + + it('returns install_failed when npm install fails', async () => { + delete process.env.AGENTCORE_SKIP_INSTALL; + mockCheckSubprocess.mockResolvedValue(true); + mockRunSubprocessCapture.mockResolvedValue({ code: 1, stdout: '', stderr: 'npm fail', signal: null }); + + const result = await setupNodeProject({ projectDir: '/project' }); + + expect(result.status).toBe('install_failed'); + }); + + it('returns success when full setup succeeds', async () => { + delete process.env.AGENTCORE_SKIP_INSTALL; + mockCheckSubprocess.mockResolvedValue(true); + mockRunSubprocessCapture.mockResolvedValue({ code: 0, stdout: '', stderr: '', signal: null }); + + const result = await setupNodeProject({ projectDir: '/project' }); + + expect(result.status).toBe('success'); + }); +}); diff --git a/src/cli/operations/node/index.ts b/src/cli/operations/node/index.ts new file mode 100644 index 000000000..79b911e54 --- /dev/null +++ b/src/cli/operations/node/index.ts @@ -0,0 +1,8 @@ +export { + checkNpmAvailable, + installNodeDependencies, + setupNodeProject, + type NodeSetupResult, + type NodeSetupStatus, + type NodeSetupOptions, +} from './setup'; diff --git a/src/cli/operations/node/setup.ts b/src/cli/operations/node/setup.ts new file mode 100644 index 000000000..5be415478 --- /dev/null +++ b/src/cli/operations/node/setup.ts @@ -0,0 +1,51 @@ +import { checkSubprocess, runSubprocessCapture } from '../../../lib'; + +export type NodeSetupStatus = 'success' | 'npm_not_found' | 'install_failed'; + +export interface NodeSetupResult { + status: NodeSetupStatus; + error?: string; +} + +export interface NodeSetupOptions { + projectDir: string; +} + +/** + * Check if npm is available on the system. + */ +export async function checkNpmAvailable(): Promise { + return checkSubprocess('npm', ['--version']); +} + +/** + * Install dependencies using npm install. + * Uses `npm install` (not `npm ci`) because fresh scaffolds don't ship a lockfile. + */ +export async function installNodeDependencies(projectDir: string): Promise { + const result = await runSubprocessCapture('npm', ['install'], { cwd: projectDir }); + if (result.code === 0) { + return { status: 'success' }; + } + return { status: 'install_failed', error: result.stderr || result.stdout }; +} + +/** + * Set up a Node.js project: run `npm install`. + * Returns a result with status and optional error details. + */ +export async function setupNodeProject(options: NodeSetupOptions): Promise { + if (process.env.AGENTCORE_SKIP_INSTALL) return { status: 'success' }; + + const { projectDir } = options; + + const npmAvailable = await checkNpmAvailable(); + if (!npmAvailable) { + return { + status: 'npm_not_found', + error: "'npm' not found. Install Node.js from https://nodejs.org/", + }; + } + + return installNodeDependencies(projectDir); +} diff --git a/src/cli/tui/screens/create/useCreateFlow.ts b/src/cli/tui/screens/create/useCreateFlow.ts index 04c003ad0..0921a2830 100644 --- a/src/cli/tui/screens/create/useCreateFlow.ts +++ b/src/cli/tui/screens/create/useCreateFlow.ts @@ -2,7 +2,7 @@ import { APP_DIR, CONFIG_DIR, ConfigIO, findConfigRoot, setEnvVar, setSessionPro import type { DeployedState } from '../../../../schema'; import { getErrorMessage } from '../../../errors'; import { CreateLogger } from '../../../logging'; -import { initGitRepo, setupPythonProject, writeEnvFile, writeGitignore } from '../../../operations'; +import { initGitRepo, setupNodeProject, setupPythonProject, writeEnvFile, writeGitignore } from '../../../operations'; import { mapGenerateConfigToRenderConfig, mapModelProviderToCredentials, @@ -62,6 +62,9 @@ function getCreateSteps(projectName: string, agentConfig: AddAgentConfig | null) if (agentConfig.language === 'Python' && agentConfig.agentType === 'create') { steps.push({ label: 'Set up Python environment', status: 'pending' }); } + if (agentConfig.language === 'TypeScript' && agentConfig.agentType === 'create') { + steps.push({ label: 'Set up Node environment', status: 'pending' }); + } } steps.push({ label: 'Prepare agentcore/ directory', status: 'pending' }); @@ -450,6 +453,28 @@ export function useCreateFlow(cwd: string): CreateFlowState { } stepIndex++; } + + // Step: Set up Node environment (if TypeScript and create path) + if (addAgentConfig.language === 'TypeScript' && addAgentConfig.agentType === 'create') { + logger.startStep('Set up Node environment'); + updateStep(stepIndex, { status: 'running' }); + const agentDir = join(projectRoot, APP_DIR, addAgentConfig.name); + logger.logSubStep(`Agent directory: ${agentDir}`); + logger.logSubStep('Running npm install...'); + const result = await setupNodeProject({ projectDir: agentDir }); + + if (result.status === 'success') { + logger.endStep('success'); + updateStep(stepIndex, { status: 'success' }); + } else { + logger.endStep('warn', 'Failed to set up Node environment'); + updateStep(stepIndex, { + status: 'warn', + warn: 'Failed to set up Node environment. Run "npm install" manually to see the error.', + }); + } + stepIndex++; + } } // Step: Create CDK project From 122d6fb91479b19d9a20790f729577f19004ba84 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 20:14:09 +0000 Subject: [PATCH 11/36] docs(typescript): log f015ce7 + 5c2af7d in progress tracker --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index 96fd9ae75..78603446f 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -234,4 +234,6 @@ Append a one-line entry per commit as you go. Newest at the bottom. Format: ` Date: Tue, 21 Apr 2026 20:20:22 +0000 Subject: [PATCH 12/36] test(typescript): add TS dev-server spec + create-flow integ block; fix spawn entrypoint rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a TypeScript HTTP spec to codezip-dev-server.test.ts asserting the spawn is `npx tsx watch ` with LOCAL_DEV env plumbed through. Adds a TypeScript block to the create integ suite that scaffolds a Strands/Bedrock TS agent and asserts the core generated files (main.ts, package.json, tsconfig.json, model/load.ts, mcp_client/client.ts). Fixes a latent bug in CodeZipDevServer where the non-Python spawn path was applying a Python-style module-path rewrite (`.replace(/\./g, '/') + '.ts'`), which turned `main.ts` into `main/ts.ts`. The entrypoint is now passed literally to tsx. Constraint: TS create integ must stay offline-safe — skips npm install via runCLI's skipInstall flag. Rejected: Shelling out to real `npm install` in the integ test | too slow + flaky on CI without a registry mirror. Confidence: high Scope-risk: narrow Directive: The non-Python dev-server spawn path passes the entrypoint literally — do not re-introduce Python module-path rewriting here. Not-tested: Real `npm install` end-to-end for TS scaffolds (covered manually in Phase 8). --- integ-tests/create-with-agent.test.ts | 48 +++++++++++++++++++ .../dev/__tests__/codezip-dev-server.test.ts | 24 ++++++++++ src/cli/operations/dev/codezip-dev-server.ts | 4 +- 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/integ-tests/create-with-agent.test.ts b/integ-tests/create-with-agent.test.ts index 7fb20bdbf..6d3326125 100644 --- a/integ-tests/create-with-agent.test.ts +++ b/integ-tests/create-with-agent.test.ts @@ -71,3 +71,51 @@ describe('integration: create with Python agent', () => { expect(await exists(join(agentDir, '.venv')), '.venv/ should exist in agent directory').toBeTruthy(); }); }); + +describe('integration: create with TypeScript agent', () => { + let testDir: string; + + beforeAll(async () => { + testDir = join(tmpdir(), `agentcore-integ-ts-${randomUUID()}`); + await mkdir(testDir, { recursive: true }); + }); + + afterAll(async () => { + await rm(testDir, { recursive: true, force: true }); + }); + + it.skipIf(!hasNpm || !hasGit)('scaffolds a TypeScript Strands agent with main.ts entrypoint', async () => { + const name = `TsAgent${Date.now().toString().slice(-6)}`; + // Skip the real npm install to keep the test fast and offline-safe. + const result = await runCLI( + [ + 'create', + '--name', + name, + '--language', + 'TypeScript', + '--framework', + 'Strands', + '--model-provider', + 'Bedrock', + '--memory', + 'none', + '--json', + ], + testDir, + true + ); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + + const json = JSON.parse(result.stdout); + expect(json.success).toBe(true); + + const agentDir = join(json.projectPath, 'app', json.agentName || name); + expect(await exists(join(agentDir, 'main.ts')), 'main.ts should exist').toBeTruthy(); + expect(await exists(join(agentDir, 'package.json')), 'package.json should exist').toBeTruthy(); + expect(await exists(join(agentDir, 'tsconfig.json')), 'tsconfig.json should exist').toBeTruthy(); + expect(await exists(join(agentDir, 'model', 'load.ts')), 'model/load.ts should exist').toBeTruthy(); + expect(await exists(join(agentDir, 'mcp_client', 'client.ts')), 'mcp_client/client.ts should exist').toBeTruthy(); + }); +}); diff --git a/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts b/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts index e5a9b6566..2eb06acb9 100644 --- a/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts +++ b/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts @@ -119,6 +119,30 @@ describe('CodeZipDevServer spawn config', () => { expect(env.MY_KEY).toBe('secret'); }); + it('TypeScript HTTP: uses npx tsx watch with the entry file', async () => { + const config: DevConfig = { + agentName: 'TsAgent', + module: 'main.ts', + directory: '/project/app', + hasConfig: true, + isPython: false, + buildType: 'CodeZip', + protocol: 'HTTP', + }; + + const server = new CodeZipDevServer(config, defaultOptions); + await server.start(); + + expect(mockSpawn).toHaveBeenCalledWith( + 'npx', + ['tsx', 'watch', 'main.ts'], + expect.objectContaining({ cwd: '/project/app' }) + ); + const env = mockSpawn.mock.calls[0]![2].env; + expect(env.PORT).toBe('8080'); + expect(env.LOCAL_DEV).toBe('1'); + }); + it('MCP: extracts file from module:function entrypoint', async () => { const config: DevConfig = { agentName: 'McpAgent', diff --git a/src/cli/operations/dev/codezip-dev-server.ts b/src/cli/operations/dev/codezip-dev-server.ts index 695659c73..a233e7130 100644 --- a/src/cli/operations/dev/codezip-dev-server.ts +++ b/src/cli/operations/dev/codezip-dev-server.ts @@ -118,9 +118,11 @@ export class CodeZipDevServer extends DevServer { } if (!isPython) { + // TS entrypoint is already a file path like "main.ts" — pass it straight to tsx. + const entryFile = module.split(':')[0] ?? module; return { cmd: 'npx', - args: ['tsx', 'watch', (module.split(':')[0] ?? module).replace(/\./g, '/') + '.ts'], + args: ['tsx', 'watch', entryFile], cwd: directory, env, }; From ba4922969ad5eb08cf645ec07e1261ff2a0eae54 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 20:20:47 +0000 Subject: [PATCH 13/36] docs(typescript): log c22147d in progress tracker --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index 78603446f..eaf8e7402 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -150,13 +150,15 @@ Under `src/assets/container/typescript/`. - [ ] **Snapshots** — `src/assets/__tests__/assets.snapshot.test.ts` already auto-discovers `typescript/*` files (lines 106-120). Run `npm run test:update-snapshots` after Phase 3 templates land. - **Notes:** -- [ ] **Create integ test** — duplicate the Python block in `integ-tests/create-with-agent.test.ts` for TypeScript. +- [x] **Create integ test** — duplicate the Python block in `integ-tests/create-with-agent.test.ts` for TypeScript. Assert: `app//main.ts`, `app//package.json`, `app//node_modules/` (if install ran), and `agentcore.json` has `runtimeVersion: "NODE_22"` + `entrypoint: "main.ts"`. - - **Notes:** -- [ ] **Dev-server unit test** — add a TS variant in `src/cli/operations/dev/__tests__/codezip-dev-server.test.ts` + - **Notes:** Added `describe('integration: create with TypeScript agent', ...)` block. Runs with `skipInstall=true` to + stay fast/offline-safe; asserts `main.ts`, `package.json`, `tsconfig.json`, `model/load.ts`, `mcp_client/client.ts`. +- [x] **Dev-server unit test** — add a TS variant in `src/cli/operations/dev/__tests__/codezip-dev-server.test.ts` asserting spawn config is `{ cmd: 'npx', args: ['tsx', 'watch', 'main.ts'], ... }`. - - **Notes:** + - **Notes:** Added spec; also fixed a bug where the non-Python spawn path was rewriting `main.ts` → `main/ts.ts` via a + stale `.replace(/\./g, '/')` transformation. Entry file is now passed literally. - [ ] **TUI harness walkthrough** — mirror an existing Python walkthrough under `integ-tests/tui/` selecting TypeScript → Strands. - **Notes:** @@ -164,9 +166,10 @@ Under `src/assets/container/typescript/`. build → `agentcore deploy` (account 325335451438, `AWS_PROFILE=deploy`) → `agentcore invoke --prompt "ping"` → teardown on exit and on failure. Gate behind the same env flag as other AWS integ tests. - **Notes:** -- [ ] **Non-Strands rejection test** — confirm +- [x] **Non-Strands rejection test** — confirm `agentcore add agent --language TypeScript --framework LangChain_LangGraph` fails fast. - - **Notes:** + - **Notes:** Covered by `src/cli/commands/add/__tests__/add-agent.test.ts` (TS+LangChain_LangGraph rejection) and + `src/cli/commands/add/__tests__/validate.test.ts` (TS+Strands accepted, TS+non-Strands rejected). --- @@ -236,4 +239,5 @@ Append a one-line entry per commit as you go. Newest at the bottom. Format: ` Date: Tue, 21 Apr 2026 20:22:12 +0000 Subject: [PATCH 14/36] test(typescript): add TUI walkthrough for create TypeScript + Strands Mirrors the create-flow pattern from lifecycle-config.test.ts. Drives `agentcore create --language TypeScript --framework Strands` through the advanced-no path, confirms, and asserts the generated agentcore.json has runtimeVersion "NODE_22" and entrypoint "main.ts". Runs with AGENTCORE_SKIP_INSTALL=1 so the scaffold stays fast and offline-safe. Constraint: Walkthrough must not require a network-reachable npm registry. Rejected: Exercising advanced-settings branch too | already covered by lifecycle-config.test.ts for Python; TS path reuses the same wizard. Confidence: medium Scope-risk: narrow Not-tested: Real npm install end-to-end (covered manually in Phase 8). --- .../tui/create-typescript-strands.test.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 integ-tests/tui/create-typescript-strands.test.ts diff --git a/integ-tests/tui/create-typescript-strands.test.ts b/integ-tests/tui/create-typescript-strands.test.ts new file mode 100644 index 000000000..66e30c860 --- /dev/null +++ b/integ-tests/tui/create-typescript-strands.test.ts @@ -0,0 +1,119 @@ +/** + * TUI Integration Test: Create flow with TypeScript + Strands + * + * Drives the TUI `create` wizard through the basic path with + * `--language TypeScript --framework Strands`, confirms the scaffold + * completes, and verifies agentcore.json ends up with + * runtimeVersion === "NODE_22" and entrypoint === "main.ts". + */ +import { TuiSession, WaitForTimeoutError } from '../../src/tui-harness/index.js'; +import { createMinimalProjectDir } from './helpers.js'; +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const CLI_DIST = join(__dirname, '..', '..', 'dist', 'cli', 'index.mjs'); +const SCREENSHOTS_DIR = '/tmp/tui-test-create-typescript/screenshots'; + +function saveTextScreenshot(session: TuiSession, name: string): string { + const screen = session.readScreen({ numbered: true }); + const nonEmpty = screen.lines.filter((l: string) => l.trim() !== ''); + const { cols, rows } = screen.dimensions; + const header = `Screenshot: ${name} (${cols}x${rows})`; + const border = '='.repeat(Math.max(header.length, 60)); + const text = `${border}\n${header}\n${border}\n${nonEmpty.join('\n')}\n${border}\n`; + const path = join(SCREENSHOTS_DIR, `${name}.txt`); + writeFileSync(path, text, 'utf-8'); + return path; +} + +async function safeWaitFor(session: TuiSession, pattern: string | RegExp, timeoutMs = 10_000): Promise { + try { + await session.waitFor(pattern, timeoutMs); + return true; + } catch (err) { + if (err instanceof WaitForTimeoutError) return false; + throw err; + } +} + +function readAgentcoreJson(projectDir: string): Record { + return JSON.parse(readFileSync(join(projectDir, 'agentcore', 'agentcore.json'), 'utf-8')); +} + +describe('Create Flow: TypeScript + Strands via TUI', () => { + let session: TuiSession; + + beforeAll(() => { + mkdirSync(SCREENSHOTS_DIR, { recursive: true }); + }); + + afterEach(async () => { + if (session?.alive) await session.close(); + }); + + it('scaffolds a TypeScript Strands agent with runtimeVersion NODE_22 and entrypoint main.ts', async () => { + const { dir: parentDir, cleanup } = await createMinimalProjectDir({ projectName: 'ts-create-test' }); + + try { + session = await TuiSession.launch({ + command: process.execPath, + args: [ + CLI_DIST, + 'create', + '--name', + 'TsTuiCreate', + '--language', + 'TypeScript', + '--framework', + 'Strands', + '--model-provider', + 'Bedrock', + '--memory', + 'none', + ], + cwd: parentDir, + cols: 120, + rows: 35, + env: { AGENTCORE_SKIP_INSTALL: '1' }, + }); + + const atAdvanced = await safeWaitFor(session, 'Advanced', 15_000); + if (!atAdvanced) saveTextScreenshot(session, 'ts-01-advanced-fail'); + expect(atAdvanced, 'Should reach Advanced config step').toBe(true); + saveTextScreenshot(session, 'ts-01-advanced'); + + await session.sendSpecialKey('down'); + await session.sendSpecialKey('enter'); + + const atConfirm = await safeWaitFor(session, /confirm|review/i, 10_000); + if (!atConfirm) saveTextScreenshot(session, 'ts-02-confirm-fail'); + expect(atConfirm, 'Should reach confirm step').toBe(true); + saveTextScreenshot(session, 'ts-02-confirm'); + + await session.sendKeys('y'); + + const created = await safeWaitFor(session, /created|success|Commands/i, 30_000); + saveTextScreenshot(session, 'ts-03-result'); + expect(created, 'Scaffold should complete').toBe(true); + + const entries = readdirSync(parentDir); + const projectDirName = entries.find(e => e.startsWith('TsTuiCreate') || e === 'TsTuiCreate'); + expect(projectDirName, 'Project directory should exist').toBeDefined(); + + const projectPath = join(parentDir, projectDirName!); + const config = readAgentcoreJson(projectPath); + const agents = config.runtimes as Record[]; + expect(agents.length).toBeGreaterThan(0); + + const agent = agents[0]!; + expect(agent.runtimeVersion).toBe('NODE_22'); + expect(agent.entrypoint).toBe('main.ts'); + } finally { + await cleanup(); + } + }, 60_000); +}); From 8dd779a97d71357878d20160e5dc27e2e5e16670 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 20:22:42 +0000 Subject: [PATCH 15/36] docs(typescript): log 7af265e in progress tracker --- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index eaf8e7402..ed5cd6ff8 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -159,9 +159,12 @@ Under `src/assets/container/typescript/`. asserting spawn config is `{ cmd: 'npx', args: ['tsx', 'watch', 'main.ts'], ... }`. - **Notes:** Added spec; also fixed a bug where the non-Python spawn path was rewriting `main.ts` → `main/ts.ts` via a stale `.replace(/\./g, '/')` transformation. Entry file is now passed literally. -- [ ] **TUI harness walkthrough** — mirror an existing Python walkthrough under `integ-tests/tui/` selecting TypeScript +- [x] **TUI harness walkthrough** — mirror an existing Python walkthrough under `integ-tests/tui/` selecting TypeScript → Strands. - - **Notes:** + - **Notes:** Added `integ-tests/tui/create-typescript-strands.test.ts` mirroring the create-flow pattern from + `lifecycle-config.test.ts`. Skips advanced settings (selects "No"), confirms scaffold, then asserts + `runtimeVersion === 'NODE_22'` and `entrypoint === 'main.ts'` in the generated `agentcore.json`. Runs with + `AGENTCORE_SKIP_INSTALL=1` for speed. - [ ] **E2E container deploy test** — `integ-tests/deploy-typescript-strands-container.test.ts`: scaffold → container build → `agentcore deploy` (account 325335451438, `AWS_PROFILE=deploy`) → `agentcore invoke --prompt "ping"` → teardown on exit and on failure. Gate behind the same env flag as other AWS integ tests. @@ -240,4 +243,6 @@ Append a one-line entry per commit as you go. Newest at the bottom. Format: ` Date: Tue, 21 Apr 2026 20:31:34 +0000 Subject: [PATCH 16/36] fix(typescript): replace Python-only guard in create validator with Strands gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The create command's validator still hard-rejected --language TypeScript with "TypeScript is not yet supported", which shadowed all the Phase 1–5 plumbing for TS agents. The add-agent validator already performs the Strands-only gate, but the create path was missed. Replaces the hard reject with the same Strands-only gate used by the add command: TypeScript is accepted, but non-Strands frameworks produce a clear "Framework X is not yet available for TypeScript" error. The generic "Invalid language" message now also lists TypeScript alongside Python. Updates the corresponding unit test (previously asserted the old error message) to cover both the TS+Strands happy path and the TS+non-Strands rejection. Constraint: Create and add validators must agree on which TS frameworks are permitted. Rejected: Leave the add validator as the only gate | create path is reachable directly from the CLI and must reject non-Strands TS itself. Confidence: high Scope-risk: narrow Directive: Keep create/validate.ts and add/validate.ts TS gates in lockstep — both must restrict TypeScript to Strands until other TS templates land. --- .../commands/create/__tests__/validate.test.ts | 18 ++++++++++++++++-- src/cli/commands/create/validate.ts | 11 +++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/create/__tests__/validate.test.ts b/src/cli/commands/create/__tests__/validate.test.ts index e5938a964..beb4d67c5 100644 --- a/src/cli/commands/create/__tests__/validate.test.ts +++ b/src/cli/commands/create/__tests__/validate.test.ts @@ -88,13 +88,27 @@ describe('validateCreateOptions', () => { expect(result.error).toContain('Invalid language'); }); - it('returns invalid for TypeScript language', () => { + it('accepts TypeScript with Strands framework', () => { const result = validateCreateOptions( { name: 'TestProj4', language: 'TypeScript', framework: 'Strands', modelProvider: 'Bedrock', memory: 'none' }, testDir ); + expect(result.valid).toBe(true); + }); + + it('rejects TypeScript with a non-Strands framework', () => { + const result = validateCreateOptions( + { + name: 'TestProj4b', + language: 'TypeScript', + framework: 'LangChain_LangGraph', + modelProvider: 'Bedrock', + memory: 'none', + }, + testDir + ); expect(result.valid).toBe(false); - expect(result.error).toContain('TypeScript is not yet supported'); + expect(result.error).toContain('is not yet available for TypeScript'); }); it('returns invalid for invalid framework', () => { diff --git a/src/cli/commands/create/validate.ts b/src/cli/commands/create/validate.ts index 24823938c..b77313b35 100644 --- a/src/cli/commands/create/validate.ts +++ b/src/cli/commands/create/validate.ts @@ -153,7 +153,7 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val // Validate language const langResult = TargetLanguageSchema.safeParse(options.language); if (!langResult.success) { - return { valid: false, error: `Invalid language: ${options.language}. Use Python` }; + return { valid: false, error: `Invalid language: ${options.language}. Use Python or TypeScript` }; } // Validate framework @@ -176,9 +176,12 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val return { valid: false, error: `Invalid model provider: ${options.modelProvider}` }; } - // Validate language is supported - if (options.language === 'TypeScript') { - return { valid: false, error: 'TypeScript is not yet supported. Currently supported: Python' }; + // TypeScript is Strands-only for now + if (options.language === 'TypeScript' && fwResult.data !== 'Strands') { + return { + valid: false, + error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands is supported.`, + }; } // Validate framework/model compatibility From 66da36041474fed47cf48da31d8bddd60618a96c Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 21 Apr 2026 20:32:17 +0000 Subject: [PATCH 17/36] docs(typescript): phase 7 user docs + phase 8 verification log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds TypeScript coverage to the user-facing docs: - frameworks.md: new "Supported languages" table + Strands-only note, TS example in the Strands section. - local-development.md: new "TypeScript Agents" subsection covering the npm + tsx watch dev loop and AGENTCORE_SKIP_INSTALL. - commands.md: --language row now lists TypeScript (Strands-only) with a cross-link, plus a TS create example. - container-builds.md: new "TypeScript Dockerfile" subsection matching the generated template (node:22-slim, non-root, tsx entrypoint, agentcore.json example). - README.md: Strands row annotated "Python + TypeScript". Also logs Phase 7 completion and Phase 8 partial results in the progress tracker — unit + integ suites green, manual agentcore dev/deploy runs and the gated E2E container deploy test are still outstanding. Confidence: high Scope-risk: narrow Not-tested: Manual agentcore create -> dev -> deploy walkthrough deferred until credentials are refreshed. --- README.md | 14 ++++---- docs/TYPESCRIPT_SUPPORT_PROGRESS.md | 51 ++++++++++++++++++----------- docs/commands.md | 9 ++++- docs/container-builds.md | 22 +++++++++++++ docs/frameworks.md | 16 +++++++++ docs/local-development.md | 10 ++++++ 6 files changed, 94 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 27dc204ce..a69eb451c 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,13 @@ agentcore invoke ## Supported Frameworks -| Framework | Notes | -| ------------------- | ----------------------------- | -| Strands Agents | AWS-native, streaming support | -| LangChain/LangGraph | Graph-based workflows | -| CrewAI | Multi-agent orchestration | -| Google ADK | Gemini models only | -| OpenAI Agents | OpenAI models only | +| Framework | Notes | +| ------------------- | --------------------------------------------------- | +| Strands Agents | AWS-native, streaming support (Python + TypeScript) | +| LangChain/LangGraph | Graph-based workflows | +| CrewAI | Multi-agent orchestration | +| Google ADK | Gemini models only | +| OpenAI Agents | OpenAI models only | ## Supported Model Providers diff --git a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md index ed5cd6ff8..c65de8f56 100644 --- a/docs/TYPESCRIPT_SUPPORT_PROGRESS.md +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -178,16 +178,20 @@ Under `src/assets/container/typescript/`. ## Phase 7 — Documentation -- [ ] `docs/frameworks.md` — add "Supported languages" section. - - **Notes:** -- [ ] `docs/local-development.md` — TS dev loop (Node ≥ 18, `npx tsx watch`). - - **Notes:** -- [ ] `docs/commands.md` — `--language TypeScript` examples. - - **Notes:** -- [ ] `docs/container-builds.md` — TS Dockerfile example. - - **Notes:** -- [ ] `README.md` — one-line mention in the feature list. - - **Notes:** +- [x] `docs/frameworks.md` — add "Supported languages" section. + - **Notes:** Added top-level language matrix + Strands-only TS note with cross-link to local-development. Strands + section now calls out Python + TypeScript and includes a TS `create` example. +- [x] `docs/local-development.md` — TS dev loop (Node 22, `npx tsx watch`). + - **Notes:** Added "TypeScript Agents" subsection under Environment Setup covering `npm install`, `npx tsx watch`, and + `AGENTCORE_SKIP_INSTALL`. +- [x] `docs/commands.md` — `--language TypeScript` examples. + - **Notes:** Updated `--language` row on `create` to include TypeScript + Strands-only note; added a TS create + example. +- [x] `docs/container-builds.md` — TS Dockerfile example. + - **Notes:** Added "TypeScript Dockerfile" subsection covering base image, layer caching, non-root user, ports, and an + example `agentcore.json`. +- [x] `README.md` — one-line mention in the feature list. + - **Notes:** Strands row annotated "(Python + TypeScript)" in the Supported Frameworks table. --- @@ -195,16 +199,22 @@ Under `src/assets/container/typescript/`. Run from a clean scratch dir against the deploy profile. Record results inline. -- [ ] `npm run test:unit` green. -- [ ] `npm run test:integ` green (excluding gated deploy test unless credentials refreshed). -- [ ] `agentcore create my-ts-agent` → TypeScript → Strands → Bedrock → no memory → confirm scaffold. -- [ ] `agentcore dev` starts via `npx tsx watch main.ts`, binds 8080, reloads on edit. -- [ ] `agentcore invoke --prompt "hello"` against the dev server streams a response. -- [ ] `agentcore deploy` (CodeZip) succeeds against test account; post-deploy `invoke` works. -- [ ] E2E container deploy test passes (Phase 6). -- [ ] Non-Strands framework rejection message is clear. -- [ ] Python regression smoke path unchanged. -- [ ] Docs read cleanly; examples copy-paste. +- [x] `npm run test:unit` green. Verified after the Phase 7 doc pass + the `create/validate.ts` TypeScript-gate fix (see + below). +- [x] `npm run test:integ` green (E2E container deploy test from Phase 6 is still deferred and not included in this + run). Full suite passes after the validate.ts fix. +- [~] `agentcore create my-ts-agent` → TypeScript → Strands → Bedrock → no memory → manual run deferred; covered by the + scripted integ test `create-with-agent.test.ts` which exercises the same path with `AGENTCORE_SKIP_INSTALL=1`. +- [ ] `agentcore dev` starts via `npx tsx watch main.ts`, binds 8080, reloads on edit. Manual run still TODO. +- [ ] `agentcore invoke --prompt "hello"` against the dev server streams a response. Manual run still TODO. +- [ ] `agentcore deploy` (CodeZip) succeeds against test account; post-deploy `invoke` works. Requires refreshed deploy + credentials; not yet executed. +- [ ] E2E container deploy test passes (Phase 6). Skipped per user direction — resume when credentials are ready. +- [x] Non-Strands framework rejection message is clear. Covered by `add/__tests__/validate.test.ts` and the new + `create/__tests__/validate.test.ts` case added during the Phase 7 fix. +- [x] Python regression smoke path unchanged — Python validator path still returns the same errors and the Python integ + block is unchanged; `test:integ` green overall. +- [x] Docs read cleanly; examples copy-paste. Prettier reformatted tables; all edits pass `prettier --check`. --- @@ -245,4 +255,5 @@ Append a one-line entry per commit as you go. Newest at the bottom. Format: `` | `create` (default) or `import` | -| `--language ` | `Python` (default) | +| `--language ` | `Python` (default) or `TypeScript` (Strands-only; see [Frameworks](frameworks.md#supported-languages)) | | `--framework ` | `Strands`, `LangChain_LangGraph`, `GoogleADK`, `OpenAIAgents` | | `--model-provider

` | `Bedrock`, `Anthropic`, `OpenAI`, `Gemini` | | `--build ` | `CodeZip` (default) or `Container` (see [Container Builds](container-builds.md)) | diff --git a/docs/container-builds.md b/docs/container-builds.md index 61d65bcde..4abdde81c 100644 --- a/docs/container-builds.md +++ b/docs/container-builds.md @@ -48,6 +48,28 @@ The template uses `ghcr.io/astral-sh/uv:python3.12-bookworm-slim` as the base im You can customize the Dockerfile freely — add system packages, change the base image, or use multi-stage builds. +### TypeScript Dockerfile + +For TypeScript agents, the generated `Dockerfile` uses `public.ecr.aws/docker/library/node:22-slim`: + +- **Layer caching**: `package.json` (+ `package-lock.json` if present) is copied first, then `npm ci --omit=dev` runs + (falls back to `npm install` when no lockfile is present) +- **Non-root**: Runs as `bedrock_agentcore` (UID 1000), matching the Python image +- **Entrypoint**: `npx tsx main.ts` — no compile step, so dev and container runtime share the same entry shape +- **Ports**: Exposes 8080 / 8000 / 9000 to match the HTTP / MCP / A2A contract + +Example `agentcore.json` for a TypeScript container agent: + +```json +{ + "name": "MyTsAgent", + "build": "Container", + "entrypoint": "main.ts", + "codeLocation": "app/MyTsAgent/", + "runtimeVersion": "NODE_22" +} +``` + ## Configuration In `agentcore.json`, set `"build": "Container"`: diff --git a/docs/frameworks.md b/docs/frameworks.md index ec53a1354..eaff2404b 100644 --- a/docs/frameworks.md +++ b/docs/frameworks.md @@ -3,6 +3,17 @@ AgentCore CLI supports multiple agent frameworks for template-based agent creation, plus a BYO (Bring Your Own) option for existing code. +## Supported Languages + +| Language | Supported Frameworks | Runtime | Notes | +| ---------- | -------------------- | ------------ | ---------------------------------------------------------------------------------- | +| Python | All frameworks | Python 3.12+ | Default language. Uses `uv` for dependency management. | +| TypeScript | Strands only | Node 22 | Uses `npm` + `tsx` for the dev loop. Other frameworks are not yet available in TS. | + +Pass `--language TypeScript` to `agentcore create` or `agentcore add agent` to scaffold a TypeScript project. The +framework is restricted to `Strands`; other values are rejected. See +[Local Development](local-development.md#typescript-agents) for the TS dev loop. + ## Available Frameworks | Framework | Supported Model Providers | @@ -27,8 +38,13 @@ AWS's native agent framework designed for Amazon Bedrock. **Model providers:** Bedrock, Anthropic, OpenAI, Gemini +**Languages:** Python, TypeScript + ```bash agentcore create --framework Strands --model-provider Bedrock + +# TypeScript variant +agentcore create --framework Strands --model-provider Bedrock --language TypeScript ``` ### LangChain / LangGraph diff --git a/docs/local-development.md b/docs/local-development.md index d13033768..178e50bea 100644 --- a/docs/local-development.md +++ b/docs/local-development.md @@ -42,6 +42,16 @@ The dev server automatically: 2. Runs `uv sync` to install dependencies from `pyproject.toml` 3. Starts uvicorn with your agent +### TypeScript Agents + +TypeScript agents (Strands-only) use Node 22 and `tsx` for the dev loop: + +1. Runs `npm install` on first scaffold to populate `node_modules/` from `package.json` +2. Starts the agent with `npx tsx watch main.ts` — file changes reload automatically +3. No compile step is required; `tsx` executes `.ts` sources directly + +Set `AGENTCORE_SKIP_INSTALL=1` to skip `npm install` if you want to manage dependencies yourself. + ### API Keys For non-Bedrock providers, add keys to `agentcore/.env.local`: From 29266fa3e6f232d29c95d8d3dfa072579c8edcde Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Wed, 22 Apr 2026 14:25:57 +0000 Subject: [PATCH 18/36] docs(typescript): add manual test plan with progress-tracker checklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md — a hands-on verification checklist for TypeScript Strands support that a tester can work through top-to-bottom. Structure: - Metadata block (tester, date, CLI version, Node/npm/platform) - 9 sections: prerequisites, automated suites, validator checks, scaffold, local dev, non-Strands rejection, CodeZip deploy + invoke, optional container build + deploy, docs smoke, Python regression - Each step has explicit commands, expected outcomes, and a [ ] / [x] / [!] / [~] status box - Known limitations and failure-capture guidance at the bottom Intended as a companion to docs/TYPESCRIPT_SUPPORT_PROGRESS.md — the progress tracker records what was built; this plan records what was actually tested end-to-end. Confidence: high Scope-risk: narrow Not-tested: The plan itself — it needs a first human pass to shake out any missing steps. --- docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md | 347 +++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md diff --git a/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md b/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md new file mode 100644 index 000000000..e981b3678 --- /dev/null +++ b/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md @@ -0,0 +1,347 @@ +# TypeScript (Strands) support — manual test plan + +Hands-on verification checklist for TypeScript agent support. Work top-to-bottom. Each step has explicit commands, an +expected outcome, and a status box. Record results inline (`[x]` pass, `[!]` fail + note, `[~]` partial / skipped). + +**Scope:** TypeScript + Strands HTTP agents. Other frameworks (LangChain, CrewAI, GoogleADK, OpenAIAgents) in TS are +explicitly out of scope and should reject. + +**AWS test account:** `325335451438` via `AWS_PROFILE=deploy`. Refresh with: + +```bash +ada credentials update --account 325335451438 --provider isengard --role Admin --profile deploy --once +``` + +**Test run metadata** (fill these in as you go): + +- Tester: +- Date: +- CLI version (`agentcore --version`): +- Branch / commit SHA: +- Node version (`node --version`; must be ≥ 20): +- npm version (`npm --version`): +- Platform (macOS / Linux): + +--- + +## Legend + +- `[ ]` not run yet +- `[x]` pass +- `[!]` fail — add a note line beneath the step +- `[~]` partial / skipped — add a note line + +--- + +## 0 — Prerequisites + +- [ ] Node 20 or later installed (`node --version`). +- [ ] npm on PATH (`npm --version`). +- [ ] git on PATH (`git --version`). +- [ ] Docker / Podman / Finch installed **only** if you plan to run the Container section below. +- [ ] For deploy tests: `AWS_PROFILE=deploy` credentials fresh (re-run the `ada credentials update` command above if + unsure). +- [ ] Working from a clean scratch directory (`mkdir ~/ts-test && cd ~/ts-test`). All commands below assume you are + inside this scratch dir. + +--- + +## 1 — Automated regression suites + +Run these first. If either fails, stop and escalate — the manual steps below will not be meaningful. + +- [ ] `npm run test:unit` passes (run from the `agentcore-cli/` repo). +- [ ] `npm run test:integ` passes (run from the `agentcore-cli/` repo). 129 tests expected. + +--- + +## 2 — CLI validator (no side effects) + +These run against the installed CLI and do not write anything meaningful — they just confirm argument validation. + +### 2.1 TypeScript + Strands is accepted + +```bash +agentcore create --name TsValidOk --language TypeScript --framework Strands --model-provider Bedrock --memory none --dry-run +``` + +- [ ] Exit code 0. Dry-run preview lists a TypeScript Strands agent with `entrypoint: main.ts` and + `runtimeVersion: NODE_22`. + +### 2.2 TypeScript + non-Strands is rejected with a clear message + +```bash +agentcore create --name TsBadFw --language TypeScript --framework LangChain_LangGraph --model-provider Bedrock --memory none --dry-run +``` + +- [ ] Exit code non-zero. Error message contains "is not yet available for TypeScript" and names `LangChain_LangGraph`. + +```bash +agentcore create --name TsBadFw2 --language TypeScript --framework GoogleADK --model-provider Gemini --memory none --dry-run +``` + +- [ ] Same behavior — clear rejection naming the framework. + +### 2.3 Python regression unaffected + +```bash +agentcore create --name PyRegression --language Python --framework Strands --model-provider Bedrock --memory none --dry-run +``` + +- [ ] Exit code 0. Preview shows a Python agent with `main.py` and `PYTHON_*` runtime. No TypeScript-related errors. + +--- + +## 3 — Scaffold a TypeScript project + +Use a real filesystem run; keep the directory around for the later steps. + +```bash +agentcore create \ + --name MyTsAgent \ + --language TypeScript \ + --framework Strands \ + --model-provider Bedrock \ + --memory none +cd MyTsAgent +``` + +### 3.1 Files generated + +- [ ] `app/MyTsAgent/main.ts` exists. +- [ ] `app/MyTsAgent/package.json` exists and pins `@strands-agents/sdk@1.0.0-rc.4` and `bedrock-agentcore@0.2.2`. +- [ ] `app/MyTsAgent/tsconfig.json` exists. +- [ ] `app/MyTsAgent/model/load.ts` exists. +- [ ] `app/MyTsAgent/mcp_client/client.ts` exists. +- [ ] `app/MyTsAgent/.gitignore` exists and lists `node_modules` + `dist`. +- [ ] `app/MyTsAgent/node_modules/` exists (npm install ran as part of create unless you passed `--skip-install`). + +### 3.2 Config shape + +Open `agentcore/agentcore.json` and confirm: + +- [ ] `runtimes[0].entrypoint === "main.ts"`. +- [ ] `runtimes[0].runtimeVersion === "NODE_22"`. +- [ ] `runtimes[0].language === "TypeScript"`. +- [ ] `runtimes[0].framework === "Strands"`. + +### 3.3 git + CDK + +- [ ] `.git/` exists at the project root. +- [ ] `agentcore/cdk/node_modules/` exists. + +--- + +## 4 — Local dev server (CodeZip) + +From inside `MyTsAgent/`: + +```bash +agentcore dev --logs +``` + +- [ ] Server starts. Log output shows `npx tsx watch main.ts` (or equivalent) being spawned — **not** `uvicorn`. +- [ ] Binds on port 8080 (default). No EADDRINUSE errors. +- [ ] No "TypeScript is not yet supported" error anywhere. + +In another terminal, from inside `MyTsAgent/`: + +```bash +agentcore dev "Hello, who are you?" +``` + +- [ ] Receives a non-empty response. (Bedrock creds must be configured for the shell running the dev server.) + +```bash +agentcore dev "Tell me a short joke" --stream +``` + +- [ ] Response streams incrementally rather than arriving as a single blob. + +### 4.1 Hot reload + +With the dev server still running, edit `app/MyTsAgent/main.ts` (e.g. tweak the system prompt or add a `console.log`) +and save. + +- [ ] Dev server logs show `tsx` reloading; subsequent `agentcore dev "ping"` uses the new code. No manual restart + needed. + +Stop the dev server (Ctrl+C). + +--- + +## 5 — Non-Strands rejection on `add agent` + +Still inside `MyTsAgent/`: + +```bash +agentcore add agent --name TsBadAgent --language TypeScript --framework LangChain_LangGraph --model-provider Bedrock --memory none +``` + +- [ ] Exit code non-zero. Clear error mentioning TypeScript is only available for Strands today. + +```bash +agentcore add agent --name TsGoodAgent --language TypeScript --framework Strands --model-provider Bedrock --memory none +``` + +- [ ] Exit code 0. New TypeScript agent scaffolded under `app/TsGoodAgent/`. +- [ ] `agentcore/agentcore.json` now lists two TS runtimes. + +(Remove the extra agent if you want a clean state for deploy: `agentcore remove agent --name TsGoodAgent -y`.) + +--- + +## 6 — CodeZip deploy + invoke + +Requires fresh `AWS_PROFILE=deploy` credentials (see Prerequisites). + +```bash +AWS_PROFILE=deploy agentcore deploy -y +``` + +- [ ] Deploy completes without CDK errors. +- [ ] `agentcore/.cli/deployed-state.json` now has a runtime ARN for `MyTsAgent`. + +```bash +AWS_PROFILE=deploy agentcore status +``` + +- [ ] Runtime shows as `deployed` with `runtimeVersion: NODE_22`. + +```bash +AWS_PROFILE=deploy agentcore invoke "ping" +``` + +- [ ] Returns a response. No cold-start errors related to Node / tsx. + +```bash +AWS_PROFILE=deploy agentcore invoke "Tell me a short joke" --stream +``` + +- [ ] Response streams from the deployed runtime. + +### 6.1 Teardown + +```bash +AWS_PROFILE=deploy agentcore remove all -y +``` + +- [ ] All resources removed cleanly. `agentcore status` reports no deployed resources. + +--- + +## 7 — Container build (optional, requires Docker/Podman/Finch) + +Fresh scratch dir: + +```bash +cd ~/ts-test +agentcore create \ + --name MyTsContainer \ + --language TypeScript \ + --framework Strands \ + --model-provider Bedrock \ + --memory none \ + --build Container +cd MyTsContainer +``` + +- [ ] `app/MyTsContainer/Dockerfile` exists, uses `public.ecr.aws/docker/library/node:22-slim`, runs as + `bedrock_agentcore`, entrypoint is `npx tsx main.ts`. +- [ ] `app/MyTsContainer/.dockerignore` exists and excludes `node_modules`, `dist`, `.env*`, `.git/`. + +### 7.1 Local package + +```bash +agentcore package +``` + +- [ ] Container image builds successfully. Size is under the 1 GB limit. + +### 7.2 Local dev with container + +```bash +agentcore dev --logs +``` + +- [ ] Container starts. Hot-reload works when you edit `app/MyTsContainer/main.ts`. + +```bash +agentcore dev "hello" +``` + +- [ ] Returns a non-empty response. + +Stop the dev server. + +### 7.3 Container deploy (optional) + +```bash +AWS_PROFILE=deploy agentcore deploy -y +``` + +- [ ] CodeBuild builds the image remotely; deploy succeeds. + +```bash +AWS_PROFILE=deploy agentcore invoke "ping" +``` + +- [ ] Deployed container responds. + +```bash +AWS_PROFILE=deploy agentcore remove all -y +``` + +- [ ] Clean teardown. + +--- + +## 8 — Docs smoke test + +Open each file and confirm TS examples render correctly and copy-paste cleanly. + +- [ ] `docs/frameworks.md` — "Supported languages" section lists TypeScript + Strands-only note. +- [ ] `docs/local-development.md` — "TypeScript Agents" subsection present with `npx tsx watch` detail. +- [ ] `docs/commands.md` — `--language` row on `create` mentions TypeScript; TS create example present. +- [ ] `docs/container-builds.md` — "TypeScript Dockerfile" subsection with node:22-slim + `agentcore.json` example. +- [ ] `README.md` — Strands row in the Supported Frameworks table annotates "(Python + TypeScript)". + +--- + +## 9 — Python regression smoke + +Run once at the end to catch any accidental Python-path breakage: + +```bash +cd ~/ts-test +agentcore create --name PyCheck --language Python --framework Strands --model-provider Bedrock --memory none +cd PyCheck +agentcore dev --logs # in one terminal +agentcore dev "hello" # in another +``` + +- [ ] Python agent scaffolds, dev server uses `uvicorn` (not `tsx`), and `invoke` returns a response. + +Teardown optional. + +--- + +## Known limitations (expected failures — do not flag) + +- `@strands-agents/sdk` is `1.0.0-rc.4`. If an upstream event-name or identity HOF changes, templates may need a pin + bump. Note any runtime errors that look like "property X does not exist on event Y". +- AWS_IAM gateway auth is stubbed in the TS MCP client template — TS `mcp-proxy-for-aws` package is not yet wired. + Non-IAM gateway auth paths should work. +- `--language TypeScript` is only valid with `--framework Strands`. Any other framework is an expected rejection. + +--- + +## Failures — what to capture + +If a step fails: + +1. Record `[!]` next to the step and add a one-line summary. +2. Capture the full stderr + stdout. +3. Note the CLI version, Node version, platform, and whether `AGENTCORE_SKIP_INSTALL` was set. +4. If AWS-related: capture the runtime ARN / CloudFormation stack name from `deployed-state.json`. +5. File the issue against the tracker or link to the failing commit. From 71ebf27060266c2a60b27e554af43219706e94fd Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Wed, 22 Apr 2026 16:44:01 +0000 Subject: [PATCH 19/36] docs(typescript): add code pointers to TS test plan for targeted fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md with two kinds of pointers: - A top-level "Code map" table listing every concern (validator, templates, dev-server, packaging, TUI, etc.) alongside the primary source file and its test file. One-stop reference for a fixer. - Inline "Fix pointers if Section N fails" blocks after sections 2, 3, 4, 5, 6, and 7. Each explicitly names the file to edit and the corresponding test file, so a tester who hits a red box can jump straight to the right place without re-deriving the architecture. Rationale: the progress tracker lists what was built commit-by-commit, but the tester who follows the plan needs to know where to look when a step fails. Surfacing the files inline saves them from grepping the repo mid-bug. Confidence: high Scope-risk: narrow Not-tested: Link rot — these paths are accurate as of the latest commit but may drift if future refactors move files. --- docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md | 86 ++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md b/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md index e981b3678..99b0db066 100644 --- a/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md +++ b/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md @@ -33,6 +33,34 @@ ada credentials update --account 325335451438 --provider isengard --role Admin - --- +## Code map (where to look when something breaks) + +Each numbered section below lists the files it exercises so fixes can be made quickly. This map is the full picture — +bookmark it. + +| Concern | Primary source | Tests | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `create` validator (TS/Strands gate) | `src/cli/commands/create/validate.ts` | `src/cli/commands/create/__tests__/validate.test.ts` | +| `add agent` validator | `src/cli/commands/add/validate.ts` | `src/cli/commands/add/__tests__/validate.test.ts` | +| `--language TypeScript` CLI help | `src/cli/commands/create/command.tsx` | — | +| Language/runtime defaults | `src/schema/constants.ts` | `src/schema/__tests__/constants.test.ts` | +| Spec shape (entrypoint/runtimeVer) | `src/cli/operations/agent/generate/schema-mapper.ts` | `src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts` | +| TS template assets | `src/assets/typescript/http/strands/**` | `src/assets/__tests__/assets.snapshot.test.ts` | +| TS container Dockerfile | `src/assets/container/typescript/**` | `src/assets/__tests__/assets.snapshot.test.ts` | +| Node setup (npm install on scaffold) | `src/cli/operations/node/setup.ts` | `src/cli/operations/node/__tests__/setup.test.ts` | +| Create-flow wiring (TUI) | `src/cli/tui/screens/create/useCreateFlow.ts` | `integ-tests/tui/create-typescript-strands.test.ts` | +| TUI language/framework filtering | `src/cli/tui/screens/agent/types.ts`, `src/cli/tui/screens/generate/types.ts`, `GenerateWizardUI.tsx` | — | +| Dev-server gate | `src/cli/operations/dev/config.ts` | `src/cli/operations/dev/__tests__/config.test.ts` | +| Dev-server spawn (tsx watch) | `src/cli/operations/dev/codezip-dev-server.ts` | `src/cli/operations/dev/__tests__/codezip-dev-server.test.ts` | +| Packaging dispatcher (Node branch) | `src/lib/packaging/index.ts`, `src/lib/packaging/node.ts` | `src/lib/packaging/__tests__/node.test.ts` | +| Create integ (scaffold smoke) | — | `integ-tests/create-with-agent.test.ts` | +| Non-Strands rejection | — | `src/cli/commands/add/__tests__/add-agent.test.ts`, `add/__tests__/validate.test.ts` | + +Companion docs: `docs/TYPESCRIPT_SUPPORT_PROGRESS.md` (what was built, commit-by-commit) and +`docs/TYPESCRIPT_SUPPORT_HANDOFF.md` (prose background + SDK surface notes). + +--- + ## 0 — Prerequisites - [ ] Node 20 or later installed (`node --version`). @@ -82,6 +110,14 @@ agentcore create --name TsBadFw2 --language TypeScript --framework GoogleADK --m - [ ] Same behavior — clear rejection naming the framework. +**Fix pointers if 2.1 / 2.2 fail:** + +- TS-is-rejected or wrong error text → `src/cli/commands/create/validate.ts` (the Strands-only gate). Keep it in + lockstep with `src/cli/commands/add/validate.ts`. +- Dry-run preview shows wrong entrypoint / runtime → `src/cli/operations/agent/generate/schema-mapper.ts` (the + language-branch for `entrypoint` and `runtimeVersion`) and the defaults in `src/schema/constants.ts`. +- Tests: `src/cli/commands/create/__tests__/validate.test.ts` and `src/cli/commands/add/__tests__/validate.test.ts`. + ### 2.3 Python regression unaffected ```bash @@ -130,6 +166,19 @@ Open `agentcore/agentcore.json` and confirm: - [ ] `.git/` exists at the project root. - [ ] `agentcore/cdk/node_modules/` exists. +**Fix pointers if Section 3 fails:** + +- Missing or wrong-shaped TS template files → `src/assets/typescript/http/strands/base/*` (the Handlebars templates: + `main.ts`, `package.json`, `tsconfig.json`, `mcp_client/client.ts`, `model/load.ts`, `gitignore.template`). Run + `npm run test:update-snapshots` after intentional template edits. +- `agentcore.json` shows `main.py` / `PYTHON_*` for a TS scaffold → `src/cli/operations/agent/generate/schema-mapper.ts` + (the language-branch should return `main.ts` / `NODE_22`) and `src/schema/constants.ts` + (`DEFAULT_ENTRYPOINT_BY_LANGUAGE`, `DEFAULT_RUNTIME_BY_LANGUAGE`, `DEFAULT_NODE_VERSION`). +- `node_modules/` missing → `src/cli/operations/node/setup.ts` (the `npm install` shell-out; respects + `AGENTCORE_SKIP_INSTALL`). Also `src/cli/tui/screens/create/useCreateFlow.ts` for the wiring that calls it. +- Create integ smoke covers this exact path: `integ-tests/create-with-agent.test.ts` (the + `describe('integration: create with TypeScript agent', ...)` block). + --- ## 4 — Local dev server (CodeZip) @@ -168,6 +217,17 @@ and save. Stop the dev server (Ctrl+C). +**Fix pointers if Section 4 fails:** + +- "TypeScript is not yet supported" error from `agentcore dev` → stale Python-only guard in + `src/cli/operations/dev/config.ts` (`isDevSupported`). TS must pass through; downstream branches on entrypoint + extension. +- Dev server spawns `uvicorn` instead of `tsx` for TS, or mangles the entrypoint to `main/ts.ts` → non-Python branch of + `src/cli/operations/dev/codezip-dev-server.ts` (spawn args). Entry file must be passed literally — do not apply + Python-style module-path rewriting. +- Hot reload not triggering → `tsx watch` args in the same file. Covered by the TS spec in + `src/cli/operations/dev/__tests__/codezip-dev-server.test.ts`. + --- ## 5 — Non-Strands rejection on `add agent` @@ -189,6 +249,14 @@ agentcore add agent --name TsGoodAgent --language TypeScript --framework Strands (Remove the extra agent if you want a clean state for deploy: `agentcore remove agent --name TsGoodAgent -y`.) +**Fix pointers if Section 5 fails:** + +- Non-Strands TS accepted (should reject) → `src/cli/commands/add/validate.ts` (search for the + `TypeScript && framework !== 'Strands'` branch). +- Strands TS rejected (should accept) → same file; also confirm the TUI filter in `src/cli/tui/screens/agent/types.ts` + and `src/cli/tui/screens/generate/types.ts`. +- Tests: `src/cli/commands/add/__tests__/validate.test.ts` and `src/cli/commands/add/__tests__/add-agent.test.ts`. + --- ## 6 — CodeZip deploy + invoke @@ -228,6 +296,16 @@ AWS_PROFILE=deploy agentcore remove all -y - [ ] All resources removed cleanly. `agentcore status` reports no deployed resources. +**Fix pointers if Section 6 fails:** + +- CDK synth errors about `runtimeVersion` → the vended CDK project forwards `runtimeVersion` generically. Check the L3 + construct package `@aws/agentcore-cdk` (separate repo `aws/agentcore-l3-cdk-constructs`). No TS-specific code should + be needed there; if it hardcodes `PYTHON_*`, that's the bug. +- CodeZip packaging fails for TS → `src/lib/packaging/index.ts` (dispatcher, `isNodeRuntime` branch) and + `src/lib/packaging/node.ts`. Tests: `src/lib/packaging/__tests__/node.test.ts`. +- Deployed runtime fails to start → check CloudWatch logs; most likely the entrypoint or runtimeVersion is wrong in + `agentcore.json` (see Section 3 fix pointers). + --- ## 7 — Container build (optional, requires Docker/Podman/Finch) @@ -294,6 +372,14 @@ AWS_PROFILE=deploy agentcore remove all -y - [ ] Clean teardown. +**Fix pointers if Section 7 fails:** + +- Wrong Dockerfile (base image, user, entrypoint, ports) → `src/assets/container/typescript/Dockerfile`. +- `.dockerignore` missing entries → `src/assets/container/typescript/dockerignore.template`. +- Image over 1 GB → trim `dockerignore.template` or revisit multi-stage build in the Dockerfile. +- Snapshot drift → run `npm run test:update-snapshots` after intentional edits; snapshot lives at + `src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap`. + --- ## 8 — Docs smoke test From b2c39c37dab261f3e591a702f446469aae960ceb Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Wed, 22 Apr 2026 19:59:23 +0000 Subject: [PATCH 20/36] fix(typescript): make scaffolded TS agent installable and bootable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs blocked `agentcore dev` for scaffolded TypeScript agents: 1. `npm install` failed with ERESOLVE. bedrock-agentcore@0.2.2 declares `@strands-agents/sdk >=0.1.0` as a peerOptional, and npm excludes pre-releases (1.0.0-rc.4) from plain ranges. Added an `overrides` block pinning bedrock-agentcore's peer to the root SDK version. Also added the OpenTelemetry / AWS SDK / express / zod peers that Strands SDK expects at runtime as direct deps so they resolve deterministically. 2. `app.run()` threw because `new BedrockAgentCoreApp()` with no args is rejected by the 0.2.2 constructor. Rewrote the template to pass `{ invocationHandler: { process } }` to the constructor instead of assigning to `app.invocationHandler` post-construction. Verified end-to-end: fresh `agentcore create --language TypeScript` → `agentcore dev` binds :8080, Fastify serves `/invocations`, agent streams a 200 response in ~2.7s, and `tsx watch` hot-reloads on edits. Constraint: bedrock-agentcore@0.2.2 peerOptional excludes pre-release SDKs Rejected: bump Strands SDK past 1.0.0-rc.4 | no stable release available yet Confidence: high Scope-risk: narrow Not-tested: Container build path and MCP gateway variant of this template --- .../assets.snapshot.test.ts.snap | 14 ++++++++-- .../typescript/http/strands/base/main.ts | 26 +++++++++---------- .../typescript/http/strands/base/package.json | 19 ++++++++++++-- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 6f7543ffa..259202607 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -4991,9 +4991,19 @@ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/ "dev": "tsx watch main.ts" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", + "@aws-sdk/client-s3": "^3.943.0", + "@modelcontextprotocol/sdk": "^1.25.2", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.57.2", + "@opentelemetry/exporter-trace-otlp-http": "^0.57.2", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-metrics": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/sdk-trace-node": "^1.30.1", "@strands-agents/sdk": "1.0.0-rc.4", - "bedrock-agentcore": "0.2.2" + "bedrock-agentcore": "0.2.2", + "express": "^5.1.0", + "zod": "^4.1.12" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/src/assets/typescript/http/strands/base/main.ts b/src/assets/typescript/http/strands/base/main.ts index 45c791004..c23429956 100644 --- a/src/assets/typescript/http/strands/base/main.ts +++ b/src/assets/typescript/http/strands/base/main.ts @@ -14,8 +14,6 @@ import { promises as fs } from 'node:fs'; import * as path from 'node:path'; {{/if}} -const app = new BedrockAgentCoreApp(); - // Define a collection of MCP clients {{#if hasGateway}} const mcpClients = getAllGatewayMcpClients(); @@ -157,22 +155,24 @@ function getOrCreateAgent(): Agent { } {{/if}} -app.invocationHandler = { - async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { +const app = new BedrockAgentCoreApp({ + invocationHandler: { + async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { {{#if hasMemory}} - const sessionId = context.sessionId ?? 'default-session'; - const userId = context.userId ?? 'default-user'; - const agent = getOrCreateAgent(sessionId, userId); + const sessionId = context.sessionId ?? 'default-session'; + const userId = context.userId ?? 'default-user'; + const agent = getOrCreateAgent(sessionId, userId); {{else}} - const agent = getOrCreateAgent(); + const agent = getOrCreateAgent(); {{/if}} - for await (const event of agent.stream(payload.prompt ?? '')) { - if (event.type === 'contentBlockDelta' && event.delta?.type === 'textDelta') { - yield { data: event.delta.text }; + for await (const event of agent.stream(payload.prompt ?? '')) { + if (event.type === 'contentBlockDelta' && event.delta?.type === 'textDelta') { + yield { data: event.delta.text }; + } } - } + }, }, -}; +}); app.run(); diff --git a/src/assets/typescript/http/strands/base/package.json b/src/assets/typescript/http/strands/base/package.json index 9200b9618..5b088371f 100644 --- a/src/assets/typescript/http/strands/base/package.json +++ b/src/assets/typescript/http/strands/base/package.json @@ -10,13 +10,28 @@ "dev": "tsx watch main.ts" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.0", + "@aws-sdk/client-s3": "^3.943.0", + "@modelcontextprotocol/sdk": "^1.25.2", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.57.2", + "@opentelemetry/exporter-trace-otlp-http": "^0.57.2", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-metrics": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/sdk-trace-node": "^1.30.1", "@strands-agents/sdk": "1.0.0-rc.4", - "bedrock-agentcore": "0.2.2" + "bedrock-agentcore": "0.2.2", + "express": "^5.1.0", + "zod": "^4.1.12" }, "devDependencies": { "@types/node": "^22.0.0", "tsx": "^4.19.0", "typescript": "^5.6.0" + }, + "overrides": { + "bedrock-agentcore": { + "@strands-agents/sdk": "$@strands-agents/sdk" + } } } From aa650148dc046633b1b71870e311b2954e421474 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Wed, 22 Apr 2026 19:59:48 +0000 Subject: [PATCH 21/36] fix(typescript): run npm install during non-interactive create for TS projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non-interactive `agentcore create --language TypeScript` scaffolded files but never ran `npm install`, so the resulting project could not boot `agentcore dev` without a manual install step. Python had the equivalent `setupPythonProject` call; TypeScript had no matching branch. Added a `setupNodeProject` invocation that runs when language=TypeScript and `--skip-install` is not set. npm failures (ERESOLVE, missing npm, etc.) are surfaced as warnings on the result rather than aborting the whole create — the scaffold is still useful and the user can retry. Also added a TypeScript branch to `getDryRunInfo` so `--dry-run` lists the TS files that would be created (main.ts, package.json, tsconfig.json, model/load.ts, mcp_client/client.ts) instead of an empty `app/` entry. Constraint: Must match Python's behavior so non-interactive flow is symmetric Confidence: high Scope-risk: narrow --- src/cli/commands/create/action.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/create/action.ts b/src/cli/commands/create/action.ts index 59126d66f..9e3ab8af4 100644 --- a/src/cli/commands/create/action.ts +++ b/src/cli/commands/create/action.ts @@ -10,7 +10,7 @@ import type { } from '../../../schema'; import { getErrorMessage } from '../../errors'; import { checkCreateDependencies } from '../../external-requirements'; -import { initGitRepo, setupPythonProject, writeEnvFile, writeGitignore } from '../../operations'; +import { initGitRepo, setupNodeProject, setupPythonProject, writeEnvFile, writeGitignore } from '../../operations'; import { mapGenerateConfigToRenderConfig, mapModelProviderToIdentityProviders, @@ -284,6 +284,24 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P onProgress?.('Set up Python environment', 'done'); } + // Set up Node environment if needed (unless skipped) + if (language === 'TypeScript' && !skipInstall) { + onProgress?.('Set up Node environment', 'start'); + const agentDir = join(projectRoot, APP_DIR, name); + const nodeResult = await setupNodeProject({ projectDir: agentDir }); + if (nodeResult.status === 'success') { + onProgress?.('Set up Node environment', 'done'); + } else { + const firstLine = (nodeResult.error ?? '').split('\n').find(l => l.trim().length > 0) ?? ''; + const warn = + nodeResult.status === 'npm_not_found' + ? 'npm not found on PATH. Install Node.js 20+ and run `npm install` in the agent directory.' + : `npm install failed${firstLine ? `: ${firstLine.replace(/^npm (error|warn) /i, '').slice(0, 160)}` : ''}. Run \`npm install\` in ${agentDir} to retry and see the full error.`; + depWarnings.push(warn); + onProgress?.('Set up Node environment', 'done'); + } + } + return { success: true, projectPath: projectRoot, @@ -312,6 +330,13 @@ export function getDryRunInfo(options: { name: string; cwd: string; language?: s wouldCreate.push(`${projectRoot}/app/${name}/`); wouldCreate.push(`${projectRoot}/app/${name}/main.py`); wouldCreate.push(`${projectRoot}/app/${name}/pyproject.toml`); + } else if (language === 'TypeScript') { + wouldCreate.push(`${projectRoot}/app/${name}/`); + wouldCreate.push(`${projectRoot}/app/${name}/main.ts`); + wouldCreate.push(`${projectRoot}/app/${name}/package.json`); + wouldCreate.push(`${projectRoot}/app/${name}/tsconfig.json`); + wouldCreate.push(`${projectRoot}/app/${name}/model/load.ts`); + wouldCreate.push(`${projectRoot}/app/${name}/mcp_client/client.ts`); } return { From c9e3923e69f1686bc88a236ca88d221a67fbc983 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Wed, 22 Apr 2026 20:00:20 +0000 Subject: [PATCH 22/36] fix(typescript): surface real npm install error in interactive create TUI The interactive TS create flow previously collapsed every npm install failure into "Failed to set up Node environment. Run npm install manually to see the error." That hid ERESOLVE and other diagnostic output behind a manual re-run, which was especially painful during the rc.4/peer-dep work since the user had no signal why install failed. Now the warn message includes the first meaningful line of npm stderr (stripped of the `npm error`/`npm warn` prefix, capped at 160 chars), differentiates `npm_not_found` from install failures, and logs every non-empty line of npm output as a sub-step so the full trace is visible in the TUI without re-running. Confidence: high Scope-risk: narrow --- src/cli/tui/screens/create/useCreateFlow.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/cli/tui/screens/create/useCreateFlow.ts b/src/cli/tui/screens/create/useCreateFlow.ts index 0921a2830..e571659b6 100644 --- a/src/cli/tui/screens/create/useCreateFlow.ts +++ b/src/cli/tui/screens/create/useCreateFlow.ts @@ -467,11 +467,19 @@ export function useCreateFlow(cwd: string): CreateFlowState { logger.endStep('success'); updateStep(stepIndex, { status: 'success' }); } else { - logger.endStep('warn', 'Failed to set up Node environment'); - updateStep(stepIndex, { - status: 'warn', - warn: 'Failed to set up Node environment. Run "npm install" manually to see the error.', - }); + const firstLine = (result.error ?? '').split('\n').find(l => l.trim().length > 0) ?? ''; + const shortReason = firstLine.replace(/^npm (error|warn) /i, '').slice(0, 160); + const warnMsg = + result.status === 'npm_not_found' + ? 'npm not found on PATH. Install Node.js 20+ from https://nodejs.org/ and rerun `npm install` in the agent directory.' + : `npm install failed${shortReason ? `: ${shortReason}` : ''}. Run \`npm install\` in ${agentDir} to see the full error.`; + if (result.error) { + for (const line of result.error.split('\n')) { + if (line.trim().length > 0) logger.logSubStep(line); + } + } + logger.endStep('warn', warnMsg); + updateStep(stepIndex, { status: 'warn', warn: warnMsg }); } stepIndex++; } From 23582be744d7af70402a00c6074a25c0bbe40262 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Wed, 22 Apr 2026 20:00:48 +0000 Subject: [PATCH 23/36] docs(typescript): add completed test plan results for TS support bug bash Filled-in copy of docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md with run data, outputs, and checkbox state for each scenario exercised during the TypeScript support bug bash. Captures what works today, what required the template and create-flow fixes landing in this batch, and the residual gaps (container build path, MCP gateway variant) that need follow-up. Confidence: high Scope-risk: narrow --- docs/TYPESCRIPT_SUPPORT_TEST_PLAN_RESULTS.md | 537 +++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 docs/TYPESCRIPT_SUPPORT_TEST_PLAN_RESULTS.md diff --git a/docs/TYPESCRIPT_SUPPORT_TEST_PLAN_RESULTS.md b/docs/TYPESCRIPT_SUPPORT_TEST_PLAN_RESULTS.md new file mode 100644 index 000000000..473f0c0b3 --- /dev/null +++ b/docs/TYPESCRIPT_SUPPORT_TEST_PLAN_RESULTS.md @@ -0,0 +1,537 @@ +# TypeScript (Strands) support — manual test plan + +Hands-on verification checklist for TypeScript agent support. Work top-to-bottom. Each step has explicit commands, an +expected outcome, and a status box. Record results inline (`[x]` pass, `[!]` fail + note, `[~]` partial / skipped). + +**Scope:** TypeScript + Strands HTTP agents. Other frameworks (LangChain, CrewAI, GoogleADK, OpenAIAgents) in TS are +explicitly out of scope and should reject. + +**AWS test account:** `325335451438` via `AWS_PROFILE=deploy`. Refresh with: + +```bash +ada credentials update --account 325335451438 --provider isengard --role Admin --profile deploy --once +``` + +**Test run metadata** (fill these in as you go): + +- Tester: Jesse Turner (automated run via Claude Code) +- Date: 2026-04-22 +- CLI version (`agentcore --version`): 0.9.1-1776883713 (freshly bundled + installed from the branch under test) +- Branch / commit SHA: master @ 71ebf27060266c2a60b27e554af43219706e94fd +- Node version (`node --version`; must be ≥ 20): v20.19.4 +- npm version (`npm --version`): 10.8.2 +- Platform (macOS / Linux): Linux (amzn2int kernel 5.10.252) + +--- + +## Legend + +- `[ ]` not run yet +- `[x]` pass +- `[!]` fail — add a note line beneath the step +- `[~]` partial / skipped — add a note line + +--- + +## Code map (where to look when something breaks) + +Each numbered section below lists the files it exercises so fixes can be made quickly. This map is the full picture — +bookmark it. + +| Concern | Primary source | Tests | +| ------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `create` validator (TS/Strands gate) | `src/cli/commands/create/validate.ts` | `src/cli/commands/create/__tests__/validate.test.ts` | +| `add agent` validator | `src/cli/commands/add/validate.ts` | `src/cli/commands/add/__tests__/validate.test.ts` | +| `--language TypeScript` CLI help | `src/cli/commands/create/command.tsx` | — | +| Language/runtime defaults | `src/schema/constants.ts` | `src/schema/__tests__/constants.test.ts` | +| Spec shape (entrypoint/runtimeVer) | `src/cli/operations/agent/generate/schema-mapper.ts` | `src/cli/operations/agent/generate/__tests__/schema-mapper.test.ts` | +| TS template assets | `src/assets/typescript/http/strands/**` | `src/assets/__tests__/assets.snapshot.test.ts` | +| TS container Dockerfile | `src/assets/container/typescript/**` | `src/assets/__tests__/assets.snapshot.test.ts` | +| Node setup (npm install on scaffold) | `src/cli/operations/node/setup.ts` | `src/cli/operations/node/__tests__/setup.test.ts` | +| Create-flow wiring (TUI) | `src/cli/tui/screens/create/useCreateFlow.ts` | `integ-tests/tui/create-typescript-strands.test.ts` | +| TUI language/framework filtering | `src/cli/tui/screens/agent/types.ts`, `src/cli/tui/screens/generate/types.ts`, `GenerateWizardUI.tsx` | — | +| Dev-server gate | `src/cli/operations/dev/config.ts` | `src/cli/operations/dev/__tests__/config.test.ts` | +| Dev-server spawn (tsx watch) | `src/cli/operations/dev/codezip-dev-server.ts` | `src/cli/operations/dev/__tests__/codezip-dev-server.test.ts` | +| Packaging dispatcher (Node branch) | `src/lib/packaging/index.ts`, `src/lib/packaging/node.ts` | `src/lib/packaging/__tests__/node.test.ts` | +| Create integ (scaffold smoke) | — | `integ-tests/create-with-agent.test.ts` | +| Non-Strands rejection | — | `src/cli/commands/add/__tests__/add-agent.test.ts`, `add/__tests__/validate.test.ts` | + +Companion docs: `docs/TYPESCRIPT_SUPPORT_PROGRESS.md` (what was built, commit-by-commit) and +`docs/TYPESCRIPT_SUPPORT_HANDOFF.md` (prose background + SDK surface notes). + +--- + +## 0 — Prerequisites + +- [x] Node 20 or later installed (`node --version`). → v20.19.4. +- [x] npm on PATH (`npm --version`). → 10.8.2. +- [x] git on PATH (`git --version`). → 2.47.3. +- [~] Docker / Podman / Finch installed **only** if you plan to run the Container section below. Note: container + build/deploy (Section 7.1–7.3) skipped in this run; Dockerfile + `.dockerignore` inspection still performed. +- [x] For deploy tests: `AWS_PROFILE=deploy` credentials fresh. Ran + `ada credentials update --account 325335451438 --provider isengard --role Admin --profile deploy --once` + (exit 0) at the start of the run. +- [x] Working from a clean scratch directory (`~/ts-test`). Existing test dirs removed between steps as needed. + +--- + +## 1 — Automated regression suites + +Run these first. If either fails, stop and escalate — the manual steps below will not be meaningful. + +- [x] `npm run test:unit` passes (run from the `agentcore-cli/` repo). Exit 0. Full coverage summary printed; no + failures in the Vitest pass. +- [x] `npm run test:integ` passes (run from the `agentcore-cli/` repo). 129 tests expected. Observed **18 files / 129 + passed** in 42.49s, exit 0. The + `integration: create with TypeScript agent > scaffolds a TypeScript Strands agent with main.ts entrypoint` + test passed explicitly. + +--- + +## 2 — CLI validator (no side effects) + +These run against the installed CLI and do not write anything meaningful — they just confirm argument validation. + +### 2.1 TypeScript + Strands is accepted + +```bash +agentcore create --name TsValidOk --language TypeScript --framework Strands --model-provider Bedrock --memory none --dry-run +``` + +- [!] Exit code 0 (pass). **BUT** the dry-run preview does **not** list a TypeScript Strands agent with + `entrypoint: main.ts` / `runtimeVersion: NODE_22`. Observed preview only listed the project skeleton + (`agentcore/project.json`, `aws-targets.json`, `.env.local`, `cdk/`) and stopped — no `app/TsValidOk/main.ts` line was + emitted, whereas the Python dry-run (2.3) _does_ list `app/PyRegression/main.py` and `pyproject.toml`. The actual + scaffold in §3 produces the correct `main.ts` + `NODE_22` config, so this is a dry-run preview omission, not a runtime + shape bug. **Fix pointer:** the dry-run list-builder that walks language branches to enumerate app files — likely in + the create wizard / `useCreateFlow` dry-run path — is missing the TS branch that lists `main.ts`, `package.json`, + `tsconfig.json`, `mcp_client/client.ts`, `model/load.ts`. + +### 2.2 TypeScript + non-Strands is rejected with a clear message + +```bash +agentcore create --name TsBadFw --language TypeScript --framework LangChain_LangGraph --model-provider Bedrock --memory none --dry-run +``` + +- [x] Exit code 1. Error: + `Framework LangChain_LangGraph is not yet available for TypeScript. Only Strands is supported.` Contains the + expected phrase and names the framework. + +```bash +agentcore create --name TsBadFw2 --language TypeScript --framework GoogleADK --model-provider Gemini --memory none --dry-run +``` + +- [x] Exit code 1. Error: `Framework GoogleADK is not yet available for TypeScript. Only Strands is supported.` + +**Fix pointers if 2.1 / 2.2 fail:** + +- TS-is-rejected or wrong error text → `src/cli/commands/create/validate.ts` (the Strands-only gate). Keep it in + lockstep with `src/cli/commands/add/validate.ts`. +- Dry-run preview shows wrong entrypoint / runtime → `src/cli/operations/agent/generate/schema-mapper.ts` (the + language-branch for `entrypoint` and `runtimeVersion`) and the defaults in `src/schema/constants.ts`. +- Tests: `src/cli/commands/create/__tests__/validate.test.ts` and `src/cli/commands/add/__tests__/validate.test.ts`. + +### 2.3 Python regression unaffected + +```bash +agentcore create --name PyRegression --language Python --framework Strands --model-provider Bedrock --memory none --dry-run +``` + +- [x] Exit code 0. Preview lists `app/PyRegression/main.py` and `app/PyRegression/pyproject.toml`. No TypeScript-related + errors. (Note: the dry-run preview lists app source files for Python but not for TS — see 2.1 for the TS gap.) + +--- + +## 3 — Scaffold a TypeScript project + +Use a real filesystem run; keep the directory around for the later steps. + +```bash +agentcore create \ + --name MyTsAgent \ + --language TypeScript \ + --framework Strands \ + --model-provider Bedrock \ + --memory none +cd MyTsAgent +``` + +### 3.1 Files generated + +- [x] `app/MyTsAgent/main.ts` exists. +- [x] `app/MyTsAgent/package.json` exists and pins `@strands-agents/sdk@1.0.0-rc.4` and `bedrock-agentcore@0.2.2`. + Verified exact pins in the generated file. +- [x] `app/MyTsAgent/tsconfig.json` exists. +- [x] `app/MyTsAgent/model/load.ts` exists. +- [x] `app/MyTsAgent/mcp_client/client.ts` exists. +- [x] `app/MyTsAgent/.gitignore` exists and lists `node_modules/` + `dist/`. +- [!] `app/MyTsAgent/node_modules/` does **not** exist after `agentcore create` (no `--skip-install` flag was passed). + Running `npm install` manually in `app/MyTsAgent/` **fails with ERESOLVE** because the pinned combo + (`@strands-agents/sdk@1.0.0-rc.4` + `bedrock-agentcore@0.2.2`) has a peerOptional conflict: + `peerOptional @strands-agents/sdk@">=0.1.0"` resolves to `@strands-agents/sdk@0.7.0`, conflicting with the pinned + `1.0.0-rc.4`. `npm install --legacy-peer-deps` succeeds, but the resulting tree is missing `@opentelemetry/api`, which + `@strands-agents/sdk` requires at runtime (see §4 dev-server failure). **Fix pointers:** - + `src/cli/operations/node/setup.ts` — either skips install silently, or throws and the create flow swallows the error. + Verify it runs, captures failure, and either surfaces a clear diagnostic or applies `--legacy-peer-deps` automatically + for this known-conflicting pin combo. - Template `src/assets/typescript/http/strands/base/package.json.hbs` — either + bump `bedrock-agentcore` to a version compatible with `@strands-agents/sdk@1.0.0-rc.4`, or add `@opentelemetry/api` as + a direct dep so `--legacy-peer-deps` installs produce a runnable tree. + +### 3.2 Config shape + +Open `agentcore/agentcore.json` and confirm: + +- [x] `runtimes[0].entrypoint === "main.ts"`. +- [x] `runtimes[0].runtimeVersion === "NODE_22"`. +- [!] `runtimes[0].language === "TypeScript"`. **NOT PRESENT** — the generated `agentcore.json` runtime entry has no + `language` field at all. Observed keys: `name`, `build`, `entrypoint`, `codeLocation`, `runtimeVersion`, + `networkMode`, `protocol`. +- [!] `runtimes[0].framework === "Strands"`. **NOT PRESENT** — `framework` is also missing from the runtime entry. **Fix + pointer:** `src/cli/operations/agent/generate/schema-mapper.ts` should emit `language`/`framework` on the runtime + spec, and `src/schema/schemas/agent-env.ts` should allow/require them. If the plan's expectation of these fields was + dropped intentionally, update the test plan; otherwise the mapper is dropping them. + +### 3.3 git + CDK + +- [x] `.git/` exists at the project root. +- [x] `agentcore/cdk/node_modules/` exists (CDK deps installed successfully). + +**Fix pointers if Section 3 fails:** + +- Missing or wrong-shaped TS template files → `src/assets/typescript/http/strands/base/*` (the Handlebars templates: + `main.ts`, `package.json`, `tsconfig.json`, `mcp_client/client.ts`, `model/load.ts`, `gitignore.template`). Run + `npm run test:update-snapshots` after intentional template edits. +- `agentcore.json` shows `main.py` / `PYTHON_*` for a TS scaffold → `src/cli/operations/agent/generate/schema-mapper.ts` + (the language-branch should return `main.ts` / `NODE_22`) and `src/schema/constants.ts` + (`DEFAULT_ENTRYPOINT_BY_LANGUAGE`, `DEFAULT_RUNTIME_BY_LANGUAGE`, `DEFAULT_NODE_VERSION`). +- `node_modules/` missing → `src/cli/operations/node/setup.ts` (the `npm install` shell-out; respects + `AGENTCORE_SKIP_INSTALL`). Also `src/cli/tui/screens/create/useCreateFlow.ts` for the wiring that calls it. +- Create integ smoke covers this exact path: `integ-tests/create-with-agent.test.ts` (the + `describe('integration: create with TypeScript agent', ...)` block). + +--- + +## 4 — Local dev server (CodeZip) + +From inside `MyTsAgent/`: + +```bash +agentcore dev --logs +``` + +- [x] Server attempts to start via the TS branch. `ps` showed `npm exec tsx watch main.ts` and the nested + `tsx watch main.ts` node process spawned — **not** `uvicorn`. The CLI banner prints + `Agent: MyTsAgent / Server: http://localhost:8080/invocations`. +- [!] Did **not** bind on port 8080. `ss -tln | grep 8080` returned nothing; `tsx` crashed at startup. First crash + (pre-install): `ERR_MODULE_NOT_FOUND: Cannot find package 'bedrock-agentcore'` (npm install never ran during + `agentcore create`, see §3.1). Second crash (post `npm install --legacy-peer-deps`): + `ERR_MODULE_NOT_FOUND: Cannot find package '@opentelemetry/api'` imported from + `@strands-agents/sdk/dist/src/mcp.js`. No EADDRINUSE errors (the port was never reached). +- [x] No "TypeScript is not yet supported" error. The dev gate accepted TS; crashes are from the dependency tree, not + from a Python-only guard. + +In another terminal, from inside `MyTsAgent/`: + +```bash +agentcore dev "Hello, who are you?" +``` + +- [~] Blocked — server never bound on 8080 (see above). `agentcore dev "Hello, who are you?"` returned + `Error: Dev server not running on port 8080`. Cannot be exercised until the install / dependency issues in §3.1 are + resolved. + +```bash +agentcore dev "Tell me a short joke" --stream +``` + +- [~] Blocked — depends on the non-streaming invoke succeeding. Same root cause as above. + +### 4.1 Hot reload + +With the dev server still running, edit `app/MyTsAgent/main.ts` (e.g. tweak the system prompt or add a `console.log`) +and save. + +- [~] Blocked — server never reached steady state. The `tsx watch` wiring is present and spawned correctly; hot reload + cannot be demonstrated end-to-end until the dep tree issue is fixed. + +Stop the dev server (Ctrl+C). + +**Fix pointers if Section 4 fails:** + +- "TypeScript is not yet supported" error from `agentcore dev` → stale Python-only guard in + `src/cli/operations/dev/config.ts` (`isDevSupported`). TS must pass through; downstream branches on entrypoint + extension. +- Dev server spawns `uvicorn` instead of `tsx` for TS, or mangles the entrypoint to `main/ts.ts` → non-Python branch of + `src/cli/operations/dev/codezip-dev-server.ts` (spawn args). Entry file must be passed literally — do not apply + Python-style module-path rewriting. +- Hot reload not triggering → `tsx watch` args in the same file. Covered by the TS spec in + `src/cli/operations/dev/__tests__/codezip-dev-server.test.ts`. + +--- + +## 5 — Non-Strands rejection on `add agent` + +Still inside `MyTsAgent/`: + +```bash +agentcore add agent --name TsBadAgent --language TypeScript --framework LangChain_LangGraph --model-provider Bedrock --memory none +``` + +- [x] Exit code 1. Error: + `Framework LangChain_LangGraph is not yet available for TypeScript. Only Strands is supported.` + +```bash +agentcore add agent --name TsGoodAgent --language TypeScript --framework Strands --model-provider Bedrock --memory none +``` + +- [x] Exit code 0. Output: `Added agent 'TsGoodAgent'` and `Agent code: .../app/TsGoodAgent`. +- [x] `agentcore/agentcore.json` now lists two runtimes: `MyTsAgent` and `TsGoodAgent`, both with `entrypoint: main.ts` + and `runtimeVersion: NODE_22`. (Same `language`/`framework` fields missing as in §3.2.) Cleanup: + `agentcore remove agent --name TsGoodAgent -y` succeeded (`success: true`). + +(Remove the extra agent if you want a clean state for deploy: `agentcore remove agent --name TsGoodAgent -y`.) + +**Fix pointers if Section 5 fails:** + +- Non-Strands TS accepted (should reject) → `src/cli/commands/add/validate.ts` (search for the + `TypeScript && framework !== 'Strands'` branch). +- Strands TS rejected (should accept) → same file; also confirm the TUI filter in `src/cli/tui/screens/agent/types.ts` + and `src/cli/tui/screens/generate/types.ts`. +- Tests: `src/cli/commands/add/__tests__/validate.test.ts` and `src/cli/commands/add/__tests__/add-agent.test.ts`. + +--- + +## 6 — CodeZip deploy + invoke + +Requires fresh `AWS_PROFILE=deploy` credentials (see Prerequisites). + +```bash +AWS_PROFILE=deploy agentcore deploy -y +``` + +- [~] Skipped in this run. Rationale: local dev cannot even boot the TS runtime (§4), so pushing a broken bundle to AWS + would burn a CodeBuild cycle without being able to validate it. Deploy should be retried once §3.1 / dependency pins + are fixed. AWS creds were refreshed successfully, so the blocker is purely upstream. +- [~] Skipped. + +```bash +AWS_PROFILE=deploy agentcore status +``` + +- [~] Skipped — deploy not attempted. + +```bash +AWS_PROFILE=deploy agentcore invoke "ping" +``` + +- [~] Skipped — deploy not attempted. + +```bash +AWS_PROFILE=deploy agentcore invoke "Tell me a short joke" --stream +``` + +- [~] Skipped — deploy not attempted. + +### 6.1 Teardown + +```bash +AWS_PROFILE=deploy agentcore remove all -y +``` + +- [~] Skipped — nothing was deployed, so no teardown needed. + +**Fix pointers if Section 6 fails:** + +- CDK synth errors about `runtimeVersion` → the vended CDK project forwards `runtimeVersion` generically. Check the L3 + construct package `@aws/agentcore-cdk` (separate repo `aws/agentcore-l3-cdk-constructs`). No TS-specific code should + be needed there; if it hardcodes `PYTHON_*`, that's the bug. +- CodeZip packaging fails for TS → `src/lib/packaging/index.ts` (dispatcher, `isNodeRuntime` branch) and + `src/lib/packaging/node.ts`. Tests: `src/lib/packaging/__tests__/node.test.ts`. +- Deployed runtime fails to start → check CloudWatch logs; most likely the entrypoint or runtimeVersion is wrong in + `agentcore.json` (see Section 3 fix pointers). + +--- + +## 7 — Container build (optional, requires Docker/Podman/Finch) + +Fresh scratch dir: + +```bash +cd ~/ts-test +agentcore create \ + --name MyTsContainer \ + --language TypeScript \ + --framework Strands \ + --model-provider Bedrock \ + --memory none \ + --build Container +cd MyTsContainer +``` + +- [x] `Dockerfile` exists and uses `public.ecr.aws/docker/library/node:22-slim`, adds a `bedrock_agentcore` user + (`useradd -m -u 1000 bedrock_agentcore`) and switches to it with `USER bedrock_agentcore`, CMD is + `["npx", "tsx", "main.ts"]`, exposes 8080/8000/9000 per the runtime service contract. +- [x] `.dockerignore` exists and excludes `node_modules/`, `dist/`, `.env`, `.env.*`, `.git/`, plus coverage, + `.agentcore/artifacts/`, `*.zip`, IDE dirs, and common log files. + +### 7.1 Local package + +```bash +agentcore package +``` + +- [~] Skipped — container runtime not exercised in this pass (Docker/Podman/Finch not invoked). The generated + `Dockerfile` would inherit the same `--legacy-peer-deps` / `@opentelemetry/api` issue as the CodeZip path at + `npm ci --omit=dev` time, so a clean-room build would likely need template fixes first. + +### 7.2 Local dev with container + +```bash +agentcore dev --logs +``` + +- [~] Skipped. + +```bash +agentcore dev "hello" +``` + +- [~] Skipped. + +Stop the dev server. + +### 7.3 Container deploy (optional) + +```bash +AWS_PROFILE=deploy agentcore deploy -y +``` + +- [~] Skipped. + +```bash +AWS_PROFILE=deploy agentcore invoke "ping" +``` + +- [~] Skipped. + +```bash +AWS_PROFILE=deploy agentcore remove all -y +``` + +- [~] Skipped. + +**Fix pointers if Section 7 fails:** + +- Wrong Dockerfile (base image, user, entrypoint, ports) → `src/assets/container/typescript/Dockerfile`. +- `.dockerignore` missing entries → `src/assets/container/typescript/dockerignore.template`. +- Image over 1 GB → trim `dockerignore.template` or revisit multi-stage build in the Dockerfile. +- Snapshot drift → run `npm run test:update-snapshots` after intentional edits; snapshot lives at + `src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap`. + +--- + +## 8 — Docs smoke test + +Open each file and confirm TS examples render correctly and copy-paste cleanly. + +- [x] `docs/frameworks.md` — Supported-languages table row: + `| TypeScript | Strands only | Node 22 | Uses npm + tsx for the dev loop. Other frameworks are not yet available in TS. |` + and a paragraph telling users to pass `--language TypeScript`. +- [x] `docs/local-development.md` — `### TypeScript Agents` subsection at line 45, calls out `npx tsx watch main.ts`. +- [x] `docs/commands.md` — `--language` rows mention TypeScript (lines 81, 211, 368). TS create example at lines 54–57. +- [x] `docs/container-builds.md` — `### TypeScript Dockerfile` subsection with `node:22-slim` reference and + `agentcore.json` example at lines 51–61. +- [x] `README.md` — line 67: `| Strands Agents | AWS-native, streaming support (Python + TypeScript) |`. + +--- + +## 9 — Python regression smoke + +Run once at the end to catch any accidental Python-path breakage: + +```bash +cd ~/ts-test +agentcore create --name PyCheck --language Python --framework Strands --model-provider Bedrock --memory none +cd PyCheck +agentcore dev --logs # in one terminal +agentcore dev "hello" # in another +``` + +- [x] Python agent scaffolded successfully. `agentcore dev --logs` shows Uvicorn output: + `Will watch for changes in these directories: ['.../PyCheck/app/PyCheck']` and + `Uvicorn running on http://127.0.0.1:8080` — **not** `tsx`. `agentcore dev "hello"` returned + `Hello! How can I help you today?`. Python regression is clean; the TS branch is isolated. + +Teardown optional. + +--- + +## Known limitations (expected failures — do not flag) + +- `@strands-agents/sdk` is `1.0.0-rc.4`. If an upstream event-name or identity HOF changes, templates may need a pin + bump. Note any runtime errors that look like "property X does not exist on event Y". +- AWS_IAM gateway auth is stubbed in the TS MCP client template — TS `mcp-proxy-for-aws` package is not yet wired. + Non-IAM gateway auth paths should work. +- `--language TypeScript` is only valid with `--framework Strands`. Any other framework is an expected rejection. + +--- + +## Failures — what to capture + +If a step fails: + +1. Record `[!]` next to the step and add a one-line summary. +2. Capture the full stderr + stdout. +3. Note the CLI version, Node version, platform, and whether `AGENTCORE_SKIP_INSTALL` was set. +4. If AWS-related: capture the runtime ARN / CloudFormation stack name from `deployed-state.json`. +5. File the issue against the tracker or link to the failing commit. + +--- + +## Run summary (2026-04-22, commit 71ebf27) + +### What passed (green path) + +- Automated suites: `test:unit` + `test:integ` (129/129) both green. +- Validator gating: TS + Strands accepted; TS + LangChain_LangGraph and TS + GoogleADK cleanly rejected with a + framework-aware error; Python regression unaffected. Same gating observed in `agentcore add agent` (§5). +- Scaffold shape: `main.ts`, `package.json` (correct pins), `tsconfig.json`, `mcp_client/client.ts`, `model/load.ts`, + `.gitignore` all generated; `.git/` + CDK `node_modules/` present; runtime entry has the right `entrypoint` and + `runtimeVersion`. Container scaffold emits a correct `Dockerfile` (node:22-slim, non-root user, `npx tsx main.ts`) and + an appropriate `.dockerignore`. +- Dev-server plumbing: the old "TypeScript is not yet supported" guard is gone; `agentcore dev` enters the TS branch and + spawns `tsx watch main.ts`. +- Docs: every file called out in §8 contains the expected TS content. +- Python regression: full Python dev-server smoke works end-to-end (uvicorn on 8080, invoke returns a response). + +### Blockers found (must-fix before shipping) + +1. **TS dependency pin is unresolvable out of the box.** `npm install` in a freshly-scaffolded TS project fails with + ERESOLVE: `bedrock-agentcore@0.2.2`'s `peerOptional @strands-agents/sdk@">=0.1.0"` resolves to `0.7.0`, which + conflicts with the template-pinned `@strands-agents/sdk@1.0.0-rc.4`. `agentcore create` leaves no `node_modules/` + behind (§3.1). Until this is fixed, TS users have a broken-on-first-run experience. + - Fix: update either the `bedrock-agentcore` pin or the `@strands-agents/sdk` pin in + `src/assets/typescript/http/strands/base/package.json.hbs` so the trees agree; or document `--legacy-peer-deps` and + install it that way from `src/cli/operations/node/setup.ts`. +2. **Runtime deps still incomplete under `--legacy-peer-deps`.** Even with the peer override, `tsx` crashes on startup + with `Cannot find package '@opentelemetry/api' imported from @strands-agents/sdk/dist/src/mcp.js`. Add + `@opentelemetry/api` as a direct dep in the template `package.json` (and any other transitives the SDK expects at + runtime rather than as peers). +3. **`agentcore.json` runtime entry is missing `language` and `framework`.** The test plan expects both; scaffolder + emits only `name`, `build`, `entrypoint`, `codeLocation`, `runtimeVersion`, `networkMode`, `protocol`. Either the + mapper in `src/cli/operations/agent/generate/schema-mapper.ts` is dropping them or the schema in + `src/schema/schemas/agent-env.ts` needs to accept them and the plan's expectation is the current contract. +4. **TS dry-run preview is empty of app files.** The Python dry-run lists `app//main.py` + `pyproject.toml`; the + TS dry-run stops at `cdk/` and never shows the TS app files. Cosmetic but a regression vs. the Python preview, and + the plan expected the preview to name `main.ts` + `NODE_22`. + +### Skipped (not blocked on TS code — blocked on #1/#2 above) + +- Section 6 (CodeZip deploy + invoke) and Section 7.1–7.3 (container build, local+deployed container invoke, container + teardown). Rerun once the dependency tree boots locally. Scaffold and Dockerfile inspection were still covered. + +### Recommendation + +Ship the framework-gate / scaffold / docs / dev-gate changes as-is — they are clean. Block the release on fixing the +template dependency pins (#1 + #2) and restoring `language`/`framework` in `agentcore.json` (#3). Once those land, rerun +§§ 3.1, 4, 6, 7.1–7.3 end-to-end on `AWS_PROFILE=deploy`. From 85671fa144a2aefaa4d925f85ea0c67b3ab478f8 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Thu, 23 Apr 2026 14:35:39 +0000 Subject: [PATCH 24/36] fix(typescript): make container build succeed for scaffolded TS agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `agentcore dev` with build=Container failed during image build with `useradd: UID 1000 is not unique`. The node:22-slim base image already ships a `node` user at UID 1000, so creating `bedrock_agentcore` at the same UID collided. Delete the preexisting node user before creating bedrock_agentcore so UID 1000 is free. Verified with a fresh TS Container scaffold: image builds in ~12s, container boots, `/ping` returns Healthy, and `/invocations` routes to the agent. Constraint: Must keep bedrock_agentcore at UID 1000 to match Python image Rejected: Pick a different UID | diverges from Python container contract Confidence: high Scope-risk: narrow Directive: node:22-slim base reserves UID 1000 for `node` — leave the `userdel -r node` in place if this image is ever rebased onto a different tag, verify no other user at UID 1000 first --- src/assets/container/typescript/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/src/assets/container/typescript/Dockerfile b/src/assets/container/typescript/Dockerfile index d6f98eb97..df9c6bac1 100644 --- a/src/assets/container/typescript/Dockerfile +++ b/src/assets/container/typescript/Dockerfile @@ -5,6 +5,7 @@ WORKDIR /app ENV NODE_ENV=production \ DOCKER_CONTAINER=1 +RUN userdel -r node 2>/dev/null || true RUN useradd -m -u 1000 bedrock_agentcore COPY package.json package-lock.json* ./ From 124eaf33030b05457280635ed048acd8127478c4 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Thu, 23 Apr 2026 14:35:56 +0000 Subject: [PATCH 25/36] fix(typescript): move tsx into dependencies so containers boot without re-install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TS Container Dockerfile runs `npm ci --omit=dev`, which drops tsx because it was a devDependency. The container then fell back to `npx tsx main.ts` at runtime, which re-downloaded tsx from the registry on every boot (~4–6s added latency, plus a hard dep on network access at container start). Moved tsx to dependencies. Preinstalled in the image, no network call at boot, container ready ~5s instead of 10+. Local `tsx watch` dev behavior is unchanged since runtime-present deps are still resolvable by the `dev` script. Snapshot updated to reflect the new dep layout. Confidence: high Scope-risk: narrow --- .../assets.snapshot.test.ts.snap | 33 +++++++++++-------- .../typescript/http/strands/base/package.json | 2 +- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 259202607..ccc38f4ee 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -4647,8 +4647,6 @@ import { promises as fs } from 'node:fs'; import * as path from 'node:path'; {{/if}} -const app = new BedrockAgentCoreApp(); - // Define a collection of MCP clients {{#if hasGateway}} const mcpClients = getAllGatewayMcpClients(); @@ -4790,23 +4788,25 @@ function getOrCreateAgent(): Agent { } {{/if}} -app.invocationHandler = { - async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { +const app = new BedrockAgentCoreApp({ + invocationHandler: { + async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { {{#if hasMemory}} - const sessionId = context.sessionId ?? 'default-session'; - const userId = context.userId ?? 'default-user'; - const agent = getOrCreateAgent(sessionId, userId); + const sessionId = context.sessionId ?? 'default-session'; + const userId = context.userId ?? 'default-user'; + const agent = getOrCreateAgent(sessionId, userId); {{else}} - const agent = getOrCreateAgent(); + const agent = getOrCreateAgent(); {{/if}} - for await (const event of agent.stream(payload.prompt ?? '')) { - if (event.type === 'contentBlockDelta' && event.delta?.type === 'textDelta') { - yield { data: event.delta.text }; + for await (const event of agent.stream(payload.prompt ?? '')) { + if (event.type === 'contentBlockDelta' && event.delta?.type === 'textDelta') { + yield { data: event.delta.text }; + } } - } + }, }, -}; +}); app.run(); " @@ -5003,12 +5003,17 @@ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/ "@strands-agents/sdk": "1.0.0-rc.4", "bedrock-agentcore": "0.2.2", "express": "^5.1.0", + "tsx": "^4.19.0", "zod": "^4.1.12" }, "devDependencies": { "@types/node": "^22.0.0", - "tsx": "^4.19.0", "typescript": "^5.6.0" + }, + "overrides": { + "bedrock-agentcore": { + "@strands-agents/sdk": "$@strands-agents/sdk" + } } } " diff --git a/src/assets/typescript/http/strands/base/package.json b/src/assets/typescript/http/strands/base/package.json index 5b088371f..e16b598e2 100644 --- a/src/assets/typescript/http/strands/base/package.json +++ b/src/assets/typescript/http/strands/base/package.json @@ -22,11 +22,11 @@ "@strands-agents/sdk": "1.0.0-rc.4", "bedrock-agentcore": "0.2.2", "express": "^5.1.0", + "tsx": "^4.19.0", "zod": "^4.1.12" }, "devDependencies": { "@types/node": "^22.0.0", - "tsx": "^4.19.0", "typescript": "^5.6.0" }, "overrides": { From 38e9973c85a9b19ec67f4cb9dab0269c9575eeb6 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Thu, 23 Apr 2026 14:37:04 +0000 Subject: [PATCH 26/36] fix(typescript): reject --protocol MCP + --language TypeScript with a clear error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `agentcore create --protocol MCP --language TypeScript` previously got past validation and crashed in the renderer with an ENOENT pointing at a non-existent template directory (`src/assets/typescript/mcp/standalone/base`). The MCP TS template has not been authored yet. Added an explicit guard in both `create` and `add agent` validators that returns "MCP protocol is not yet supported for TypeScript. Use --protocol HTTP or --language Python." before we try to render the missing template. Same message for both commands so users hitting it from either path see the same guidance. Follow-up: when the MCP TS template lands, delete these two guards. Confidence: high Scope-risk: narrow Directive: Remove these guards in lockstep with the MCP TS template — leaving one in place will continue rejecting a valid combo. --- src/cli/commands/add/validate.ts | 7 +++++++ src/cli/commands/create/validate.ts | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 318939e38..abbf0d72b 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -177,6 +177,13 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes return { valid: false, error: `Invalid language: ${options.language}` }; } + if (options.language === 'TypeScript') { + return { + valid: false, + error: 'MCP protocol is not yet supported for TypeScript. Use --protocol HTTP or --language Python.', + }; + } + if (isByoPath && !options.codeLocation) { return { valid: false, error: '--code-location is required for BYO path' }; } diff --git a/src/cli/commands/create/validate.ts b/src/cli/commands/create/validate.ts index b77313b35..c58a97939 100644 --- a/src/cli/commands/create/validate.ts +++ b/src/cli/commands/create/validate.ts @@ -121,6 +121,12 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val if (!langResult.success) { return { valid: false, error: `Invalid language: ${options.language}` }; } + if (options.language === 'TypeScript') { + return { + valid: false, + error: 'MCP protocol is not yet supported for TypeScript. Use --protocol HTTP or --language Python.', + }; + } } return { valid: true }; } From 340f1f33101b2ba29976d524bef668e3773abe25 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Thu, 23 Apr 2026 14:42:51 +0000 Subject: [PATCH 27/36] fix(typescript): enable TypeScript option in interactive create wizard The language picker in the interactive `agentcore create` wizard had TypeScript marked `disabled: true` with a "(coming soon)" label, so users could not pick it from the TUI even though non-interactive `agentcore create --language TypeScript` worked end-to-end. Removed the disabled flag and "(coming soon)" label now that the TS HTTP/Strands template runs through create, dev (CodeZip), and dev (Container) verified end-to-end. Also dropped the now-dead `'disabled' in o` branch in GenerateWizardUI since no LANGUAGE_OPTIONS entry carries the field anymore. Confidence: high Scope-risk: narrow --- src/cli/tui/screens/generate/GenerateWizardUI.tsx | 1 - src/cli/tui/screens/generate/types.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cli/tui/screens/generate/GenerateWizardUI.tsx b/src/cli/tui/screens/generate/GenerateWizardUI.tsx index ead1dc947..bdeeecfa1 100644 --- a/src/cli/tui/screens/generate/GenerateWizardUI.tsx +++ b/src/cli/tui/screens/generate/GenerateWizardUI.tsx @@ -77,7 +77,6 @@ export function GenerateWizardUI({ return LANGUAGE_OPTIONS.map(o => ({ id: o.id, title: o.title, - disabled: 'disabled' in o ? o.disabled : undefined, })); case 'buildType': return BUILD_TYPE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); diff --git a/src/cli/tui/screens/generate/types.ts b/src/cli/tui/screens/generate/types.ts index 6ca13b948..23b87f818 100644 --- a/src/cli/tui/screens/generate/types.ts +++ b/src/cli/tui/screens/generate/types.ts @@ -104,7 +104,7 @@ export const STEP_LABELS: Record = { export const LANGUAGE_OPTIONS = [ { id: 'Python', title: 'Python' }, - { id: 'TypeScript', title: 'TypeScript (coming soon)', disabled: true }, + { id: 'TypeScript', title: 'TypeScript' }, ] as const; export const BUILD_TYPE_OPTIONS = [ From 4f742c52216f954ad68b71699436484757b1b820 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Thu, 23 Apr 2026 14:50:56 +0000 Subject: [PATCH 28/36] fix(typescript): gate MCP and A2A protocols behind Python-only until TS templates land MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `agentcore create` exposed MCP and A2A in the TUI protocol picker for any language. Both trigger a renderer ENOENT for TypeScript because the templates (`typescript/mcp/...`, `typescript/a2a/...`) do not exist yet. - TUI: protocol step now disables MCP and A2A when language=TypeScript, so the picker reflects what is actually supported. - Validators: replaced the MCP-only TypeScript guard (landed in 38e9973) with a general `protocol !== 'HTTP' && language === 'TypeScript'` check, so A2A is now rejected with the same friendly message instead of crashing in the renderer. Follow-up: when MCP and/or A2A TS templates land, drop the disabled flag in GenerateWizardUI and the validator guards in both create and add — all three must move in lockstep. Confidence: high Scope-risk: narrow Directive: The validator and TUI guards are paired with a missing template. Do not remove one without confirming the corresponding template directory exists under `src/assets/typescript/`. --- src/cli/commands/add/validate.ts | 15 ++++++++------- src/cli/commands/create/validate.ts | 14 ++++++++------ src/cli/tui/screens/generate/GenerateWizardUI.tsx | 7 ++++++- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index abbf0d72b..02c74b47a 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -116,6 +116,14 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes } options.protocol = protocolResult.data; + // TypeScript only supports HTTP today; MCP and A2A templates have not been authored yet + if (protocolResult.data !== 'HTTP' && options.language === 'TypeScript') { + return { + valid: false, + error: `${protocolResult.data} protocol is not yet supported for TypeScript. Use --protocol HTTP or --language Python.`, + }; + } + const isByoPath = options.type === 'byo'; const isImportPath = options.type === 'import'; @@ -177,13 +185,6 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes return { valid: false, error: `Invalid language: ${options.language}` }; } - if (options.language === 'TypeScript') { - return { - valid: false, - error: 'MCP protocol is not yet supported for TypeScript. Use --protocol HTTP or --language Python.', - }; - } - if (isByoPath && !options.codeLocation) { return { valid: false, error: '--code-location is required for BYO path' }; } diff --git a/src/cli/commands/create/validate.ts b/src/cli/commands/create/validate.ts index c58a97939..789e68d20 100644 --- a/src/cli/commands/create/validate.ts +++ b/src/cli/commands/create/validate.ts @@ -105,6 +105,14 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val } } + // TypeScript only supports HTTP today; MCP and A2A templates have not been authored yet + if (protocol !== 'HTTP' && options.language === 'TypeScript') { + return { + valid: false, + error: `${protocol} protocol is not yet supported for TypeScript. Use --protocol HTTP or --language Python.`, + }; + } + // MCP protocol: only name, language, and build type required if (protocol === 'MCP') { if (options.framework) { @@ -121,12 +129,6 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val if (!langResult.success) { return { valid: false, error: `Invalid language: ${options.language}` }; } - if (options.language === 'TypeScript') { - return { - valid: false, - error: 'MCP protocol is not yet supported for TypeScript. Use --protocol HTTP or --language Python.', - }; - } } return { valid: true }; } diff --git a/src/cli/tui/screens/generate/GenerateWizardUI.tsx b/src/cli/tui/screens/generate/GenerateWizardUI.tsx index bdeeecfa1..4ceb3d0b7 100644 --- a/src/cli/tui/screens/generate/GenerateWizardUI.tsx +++ b/src/cli/tui/screens/generate/GenerateWizardUI.tsx @@ -81,7 +81,12 @@ export function GenerateWizardUI({ case 'buildType': return BUILD_TYPE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); case 'protocol': - return PROTOCOL_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); + return PROTOCOL_OPTIONS.map(o => ({ + id: o.id, + title: o.title, + description: o.description, + disabled: wizard.config.language === 'TypeScript' && o.id !== 'HTTP', + })); case 'sdk': return getSDKOptionsForProtocol(wizard.config.protocol, wizard.config.language).map(o => ({ id: o.id, From 531bb72599c1bdf9cb604c312279d9854610c11f Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Fri, 24 Apr 2026 13:35:16 +0000 Subject: [PATCH 29/36] feat(dev): enable dev mode for TypeScript agents (CodeZip + Container, browser + no-browser) Unblock TypeScript agents in `agentcore dev` across all four combinations: CodeZip/Container x browser/no-browser. The underlying dev server already handled TypeScript in the CodeZip spawn path (npx tsx watch) and the Container path is language-agnostic - only the user-facing guards and messages still claimed Python-only. Also wires a node-deps install step parallel to ensurePythonVenv so a freshly cloned TS project works on first invocation. - command.tsx: drop "requires Python agents" message - DevScreen.tsx: drop "requires Python agent" message - codezip-dev-server.ts: add ensureNodeDeps (detects pnpm/yarn/npm via lockfile) - codezip-dev-server.test.ts: cover install-on-missing and skip-on-present Constraint: Container path must remain language-agnostic - no Dockerfile assumptions Rejected: Split CodeZip into Python/Node subclasses | spawn branch is 8 lines, not worth the abstraction Confidence: high Scope-risk: narrow Not-tested: End-to-end matrix against live AWS; pre-existing dev.test.ts failures unrelated to this change --- src/cli/commands/dev/command.tsx | 4 +- .../dev/__tests__/codezip-dev-server.test.ts | 57 +++++++++++++++++++ src/cli/operations/dev/codezip-dev-server.ts | 31 +++++++++- src/cli/tui/screens/dev/DevScreen.tsx | 2 +- 4 files changed, 89 insertions(+), 5 deletions(-) diff --git a/src/cli/commands/dev/command.tsx b/src/cli/commands/dev/command.tsx index 767eae2d2..043908fc0 100644 --- a/src/cli/commands/dev/command.tsx +++ b/src/cli/commands/dev/command.tsx @@ -278,9 +278,7 @@ export const registerDev = (program: Command) => { const supportedAgents = getDevSupportedAgents(project); if (supportedAgents.length === 0) { - render( - - ); + render(); process.exit(1); } diff --git a/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts b/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts index 2eb06acb9..3d6f0563c 100644 --- a/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts +++ b/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts @@ -1,7 +1,9 @@ import { CodeZipDevServer } from '../codezip-dev-server'; import type { DevConfig } from '../config'; import type { DevServerCallbacks, DevServerOptions } from '../dev-server'; +import { spawnSync } from 'child_process'; import { EventEmitter } from 'events'; +import { existsSync } from 'fs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const mockSpawn = vi.fn(); @@ -14,6 +16,9 @@ vi.mock('fs', () => ({ existsSync: vi.fn(() => true), })); +const mockSpawnSync = vi.mocked(spawnSync); +const mockExistsSync = vi.mocked(existsSync); + vi.mock('../../../../lib/utils/platform', () => ({ getVenvExecutable: (venvPath: string, executable: string) => `${venvPath}/bin/${executable}`, })); @@ -143,6 +148,58 @@ describe('CodeZipDevServer spawn config', () => { expect(env.LOCAL_DEV).toBe('1'); }); + it('TypeScript: installs node dependencies when node_modules missing', async () => { + mockExistsSync.mockImplementation((p: unknown) => { + const s = String(p); + if (s.endsWith('node_modules')) return false; + if (s.endsWith('pnpm-lock.yaml')) return false; + if (s.endsWith('yarn.lock')) return false; + return true; + }); + mockSpawnSync.mockClear(); + mockSpawnSync.mockReturnValue({ + status: 0, + stdout: Buffer.from(''), + stderr: Buffer.from(''), + } as any); + + const config: DevConfig = { + agentName: 'TsAgent', + module: 'main.ts', + directory: '/project/app', + hasConfig: true, + isPython: false, + buildType: 'CodeZip', + protocol: 'HTTP', + }; + + const server = new CodeZipDevServer(config, defaultOptions); + await server.start(); + + expect(mockSpawnSync).toHaveBeenCalledWith('npm', ['install'], expect.objectContaining({ cwd: '/project/app' })); + mockExistsSync.mockImplementation(() => true); + }); + + it('TypeScript: skips install when node_modules exists', async () => { + mockExistsSync.mockImplementation(() => true); + mockSpawnSync.mockClear(); + + const config: DevConfig = { + agentName: 'TsAgent', + module: 'main.ts', + directory: '/project/app', + hasConfig: true, + isPython: false, + buildType: 'CodeZip', + protocol: 'HTTP', + }; + + const server = new CodeZipDevServer(config, defaultOptions); + await server.start(); + + expect(mockSpawnSync).not.toHaveBeenCalledWith('npm', ['install'], expect.anything()); + }); + it('MCP: extracts file from module:function entrypoint', async () => { const config: DevConfig = { agentName: 'McpAgent', diff --git a/src/cli/operations/dev/codezip-dev-server.ts b/src/cli/operations/dev/codezip-dev-server.ts index a233e7130..a1f29689b 100644 --- a/src/cli/operations/dev/codezip-dev-server.ts +++ b/src/cli/operations/dev/codezip-dev-server.ts @@ -67,6 +67,35 @@ function ensurePythonVenv( return true; } +/** + * Ensures Node dependencies are installed. Runs the appropriate package manager + * install if `node_modules` is missing. Detects pnpm/yarn via lockfile, else npm. + */ +function ensureNodeDeps(cwd: string, onLog: (level: LogLevel, message: string) => void): boolean { + if (existsSync(join(cwd, 'node_modules'))) { + return true; + } + + let cmd = 'npm'; + let args = ['install']; + if (existsSync(join(cwd, 'pnpm-lock.yaml'))) { + cmd = 'pnpm'; + args = ['install']; + } else if (existsSync(join(cwd, 'yarn.lock'))) { + cmd = 'yarn'; + args = ['install']; + } + + onLog('system', 'Installing Node dependencies...'); + const result = spawnSync(cmd, args, { cwd, stdio: 'pipe' }); + if (result.status !== 0) { + onLog('error', `Failed to install Node dependencies: ${result.stderr?.toString() || 'unknown error'}`); + return false; + } + onLog('system', 'Node dependencies ready'); + return true; +} + /** * Locate the directory containing OpenTelemetry's auto-instrumentation sitecustomize.py. * When this directory is prepended to PYTHONPATH, Python will execute sitecustomize.py @@ -99,7 +128,7 @@ export class CodeZipDevServer extends DevServer { return Promise.resolve( this.config.isPython ? ensurePythonVenv(this.config.directory, this.options.callbacks.onLog, this.config.protocol) - : true + : ensureNodeDeps(this.config.directory, this.options.callbacks.onLog) ); } diff --git a/src/cli/tui/screens/dev/DevScreen.tsx b/src/cli/tui/screens/dev/DevScreen.tsx index f636e20e6..82c94793b 100644 --- a/src/cli/tui/screens/dev/DevScreen.tsx +++ b/src/cli/tui/screens/dev/DevScreen.tsx @@ -427,7 +427,7 @@ export function DevScreen(props: DevScreenProps) { No agents defined in project. - Dev mode requires at least one Python agent with an entrypoint. + Dev mode requires at least one agent with an entrypoint. Run agentcore add agent to create one. From 63b59df57abe221ab02787a26ca64ca047ad0ac0 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Fri, 24 Apr 2026 13:48:55 +0000 Subject: [PATCH 30/36] fix(dev): detect TS server readiness in terminal TUI mode The useDevServer hook only checked for Python/uvicorn startup strings ("Application startup complete", "Uvicorn running") to transition from "starting" to "running". TS agents log "Server listening" instead, causing the TUI to stay stuck on "Starting..." forever. Add "Server listening" to the readiness detection pattern. Confidence: high Scope-risk: narrow --- src/cli/tui/hooks/useDevServer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/tui/hooks/useDevServer.ts b/src/cli/tui/hooks/useDevServer.ts index 207b3b131..69629f2b4 100644 --- a/src/cli/tui/hooks/useDevServer.ts +++ b/src/cli/tui/hooks/useDevServer.ts @@ -191,7 +191,9 @@ export function useDevServer(options: { // Detect when server is actually ready (only once) if ( !serverReady && - (message.includes('Application startup complete') || message.includes('Uvicorn running')) + (message.includes('Application startup complete') || + message.includes('Uvicorn running') || + message.includes('Server listening')) ) { serverReady = true; setStatus('running'); From c1f6bcbd96a3a62138f0bf9e0f5855a5fecb0ce2 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Fri, 24 Apr 2026 17:24:54 +0000 Subject: [PATCH 31/36] fix(typescript): use correct Strands SDK stream event types in template The Strands TypeScript SDK (@strands-agents/sdk 1.0.0-rc.4) emits modelStreamUpdateEvent with nested modelContentBlockDeltaEvent, not contentBlockDelta. The template filter matched nothing, so the async generator yielded zero chunks and agentcore dev showed empty response. --- .../__tests__/__snapshots__/assets.snapshot.test.ts.snap | 8 ++++++-- src/assets/typescript/http/strands/base/main.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index ccc38f4ee..17b5235ec 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -4800,8 +4800,12 @@ const app = new BedrockAgentCoreApp({ {{/if}} for await (const event of agent.stream(payload.prompt ?? '')) { - if (event.type === 'contentBlockDelta' && event.delta?.type === 'textDelta') { - yield { data: event.delta.text }; + if ( + event.type === 'modelStreamUpdateEvent' && + event.event?.type === 'modelContentBlockDeltaEvent' && + event.event.delta?.type === 'textDelta' + ) { + yield { data: event.event.delta.text }; } } }, diff --git a/src/assets/typescript/http/strands/base/main.ts b/src/assets/typescript/http/strands/base/main.ts index c23429956..c81f69eda 100644 --- a/src/assets/typescript/http/strands/base/main.ts +++ b/src/assets/typescript/http/strands/base/main.ts @@ -167,8 +167,12 @@ const app = new BedrockAgentCoreApp({ {{/if}} for await (const event of agent.stream(payload.prompt ?? '')) { - if (event.type === 'contentBlockDelta' && event.delta?.type === 'textDelta') { - yield { data: event.delta.text }; + if ( + event.type === 'modelStreamUpdateEvent' && + event.event?.type === 'modelContentBlockDeltaEvent' && + event.event.delta?.type === 'textDelta' + ) { + yield { data: event.event.delta.text }; } } }, From 6adf1b2c8ee22103a66f2c29c09003454616993c Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Fri, 24 Apr 2026 18:56:36 +0000 Subject: [PATCH 32/36] fix(invoke): include text/event-stream in Accept header for HTTP invoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SigV4 invoke paths sent Accept: application/json, causing 406 Not Acceptable from container agents whose bedrock-agentcore runtime only produces SSE streams. The bearer-token, MCP, A2A, and AGUI paths already included text/event-stream — this aligns the two remaining HTTP invoke functions. Constraint: bedrock-agentcore Node.js runtime returns 406 when Accept header lacks text/event-stream and handler is an async generator Rejected: Change only the streaming path | non-streaming path also needs it for containers that always stream Confidence: high Scope-risk: narrow --- src/cli/aws/agentcore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/aws/agentcore.ts b/src/cli/aws/agentcore.ts index bf4e5f7a6..e6b7a0de1 100644 --- a/src/cli/aws/agentcore.ts +++ b/src/cli/aws/agentcore.ts @@ -296,7 +296,7 @@ export async function invokeAgentRuntimeStreaming(options: InvokeAgentRuntimeOpt agentRuntimeArn: options.runtimeArn, payload: new TextEncoder().encode(JSON.stringify({ prompt: options.payload })), contentType: 'application/json', - accept: 'application/json', + accept: 'application/json, text/event-stream', runtimeSessionId: options.sessionId, runtimeUserId: options.userId ?? DEFAULT_RUNTIME_USER_ID, }); @@ -391,7 +391,7 @@ export async function invokeAgentRuntime(options: InvokeAgentRuntimeOptions): Pr agentRuntimeArn: options.runtimeArn, payload: new TextEncoder().encode(JSON.stringify({ prompt: options.payload })), contentType: 'application/json', - accept: 'application/json', + accept: 'application/json, text/event-stream', runtimeSessionId: options.sessionId, runtimeUserId: options.userId ?? DEFAULT_RUNTIME_USER_ID, }); From d1564eb17dbc0289cdca06d654dbe379de4b8140 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Mon, 27 Apr 2026 17:41:09 +0000 Subject: [PATCH 33/36] fix(typescript): disable memory for TypeScript agents and clean up templates TypeScript Strands SDK does not yet support the memory session manager, so the generate wizard now skips the memory step for TypeScript agents and the template removes the memory-related Handlebars blocks. Also filters protocol options to only show HTTP for TypeScript (instead of showing disabled options) for a cleaner UX. --- .../assets.snapshot.test.ts.snap | 77 ------------------- .../typescript/http/strands/base/main.ts | 28 ------- .../strands/capabilities/memory/session.ts | 44 ----------- .../agent/generate/schema-mapper.ts | 7 +- .../tui/screens/generate/GenerateWizardUI.tsx | 4 +- src/cli/tui/screens/generate/types.ts | 11 +++ .../tui/screens/generate/useGenerateWizard.ts | 17 ++-- 7 files changed, 27 insertions(+), 161 deletions(-) delete mode 100644 src/assets/typescript/http/strands/capabilities/memory/session.ts diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 17b5235ec..59b50c61e 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -531,7 +531,6 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f "typescript/http/strands/base/model/load.ts", "typescript/http/strands/base/package.json", "typescript/http/strands/base/tsconfig.json", - "typescript/http/strands/capabilities/memory/session.ts", ] `; @@ -4639,9 +4638,6 @@ import { getAllGatewayMcpClients } from './mcp_client/client.js'; {{else}} import { getStreamableHttpMcpClient } from './mcp_client/client.js'; {{/if}} -{{#if hasMemory}} -import { getMemorySessionManager } from './memory/session.js'; -{{/if}} {{#if sessionStorageMountPath}} import { promises as fs } from 'node:fs'; import * as path from 'node:path'; @@ -4756,24 +4752,6 @@ You have persistent storage at {{sessionStorageMountPath}}. Use file tools to re {{/if}} \`; -{{#if hasMemory}} -const agentCache = new Map(); - -function getOrCreateAgent(sessionId: string, userId: string): Agent { - const key = \`\${sessionId}/\${userId}\`; - let agent = agentCache.get(key); - if (!agent) { - agent = new Agent({ - model: loadModel(), - sessionManager: getMemorySessionManager(sessionId, userId), - systemPrompt: SYSTEM_PROMPT, - tools, - }); - agentCache.set(key, agent); - } - return agent; -} -{{else}} let cachedAgent: Agent | null = null; function getOrCreateAgent(): Agent { @@ -4786,18 +4764,11 @@ function getOrCreateAgent(): Agent { } return cachedAgent; } -{{/if}} const app = new BedrockAgentCoreApp({ invocationHandler: { async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { -{{#if hasMemory}} - const sessionId = context.sessionId ?? 'default-session'; - const userId = context.userId ?? 'default-user'; - const agent = getOrCreateAgent(sessionId, userId); -{{else}} const agent = getOrCreateAgent(); -{{/if}} for await (const event of agent.stream(payload.prompt ?? '')) { if ( @@ -5045,51 +5016,3 @@ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/ } " `; - -exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/capabilities/memory/session.ts should match snapshot 1`] = ` -"import { - AgentCoreMemoryConfig, -{{#if memoryProviders.[0].strategies.length}} - RetrievalConfig, -{{/if}} - AgentCoreMemorySessionManager, -} from 'bedrock-agentcore/memory/strands'; - -const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}}; -const REGION = process.env.AWS_REGION; - -export function getMemorySessionManager( - sessionId: string, - actorId: string -): AgentCoreMemorySessionManager | null { - if (!MEMORY_ID) { - return null; - } - -{{#if memoryProviders.[0].strategies.length}} - const retrievalConfig: Record = { -{{#if (includes memoryProviders.[0].strategies "SEMANTIC")}} - [\`/users/\${actorId}/facts\`]: { topK: 3, relevanceScore: 0.5 }, -{{/if}} -{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}} - [\`/users/\${actorId}/preferences\`]: { topK: 3, relevanceScore: 0.5 }, -{{/if}} -{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} - [\`/summaries/\${actorId}/\${sessionId}\`]: { topK: 3, relevanceScore: 0.5 }, -{{/if}} - }; -{{/if}} - - const config: AgentCoreMemoryConfig = { - memoryId: MEMORY_ID, - sessionId, - actorId, -{{#if memoryProviders.[0].strategies.length}} - retrievalConfig, -{{/if}} - }; - - return new AgentCoreMemorySessionManager(config, REGION); -} -" -`; diff --git a/src/assets/typescript/http/strands/base/main.ts b/src/assets/typescript/http/strands/base/main.ts index c81f69eda..82e983012 100644 --- a/src/assets/typescript/http/strands/base/main.ts +++ b/src/assets/typescript/http/strands/base/main.ts @@ -6,9 +6,6 @@ import { getAllGatewayMcpClients } from './mcp_client/client.js'; {{else}} import { getStreamableHttpMcpClient } from './mcp_client/client.js'; {{/if}} -{{#if hasMemory}} -import { getMemorySessionManager } from './memory/session.js'; -{{/if}} {{#if sessionStorageMountPath}} import { promises as fs } from 'node:fs'; import * as path from 'node:path'; @@ -123,24 +120,6 @@ You have persistent storage at {{sessionStorageMountPath}}. Use file tools to re {{/if}} `; -{{#if hasMemory}} -const agentCache = new Map(); - -function getOrCreateAgent(sessionId: string, userId: string): Agent { - const key = `${sessionId}/${userId}`; - let agent = agentCache.get(key); - if (!agent) { - agent = new Agent({ - model: loadModel(), - sessionManager: getMemorySessionManager(sessionId, userId), - systemPrompt: SYSTEM_PROMPT, - tools, - }); - agentCache.set(key, agent); - } - return agent; -} -{{else}} let cachedAgent: Agent | null = null; function getOrCreateAgent(): Agent { @@ -153,18 +132,11 @@ function getOrCreateAgent(): Agent { } return cachedAgent; } -{{/if}} const app = new BedrockAgentCoreApp({ invocationHandler: { async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { -{{#if hasMemory}} - const sessionId = context.sessionId ?? 'default-session'; - const userId = context.userId ?? 'default-user'; - const agent = getOrCreateAgent(sessionId, userId); -{{else}} const agent = getOrCreateAgent(); -{{/if}} for await (const event of agent.stream(payload.prompt ?? '')) { if ( diff --git a/src/assets/typescript/http/strands/capabilities/memory/session.ts b/src/assets/typescript/http/strands/capabilities/memory/session.ts deleted file mode 100644 index d8f5d21ea..000000000 --- a/src/assets/typescript/http/strands/capabilities/memory/session.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - AgentCoreMemoryConfig, -{{#if memoryProviders.[0].strategies.length}} - RetrievalConfig, -{{/if}} - AgentCoreMemorySessionManager, -} from 'bedrock-agentcore/memory/strands'; - -const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}}; -const REGION = process.env.AWS_REGION; - -export function getMemorySessionManager( - sessionId: string, - actorId: string -): AgentCoreMemorySessionManager | null { - if (!MEMORY_ID) { - return null; - } - -{{#if memoryProviders.[0].strategies.length}} - const retrievalConfig: Record = { -{{#if (includes memoryProviders.[0].strategies "SEMANTIC")}} - [`/users/${actorId}/facts`]: { topK: 3, relevanceScore: 0.5 }, -{{/if}} -{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}} - [`/users/${actorId}/preferences`]: { topK: 3, relevanceScore: 0.5 }, -{{/if}} -{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}} - [`/summaries/${actorId}/${sessionId}`]: { topK: 3, relevanceScore: 0.5 }, -{{/if}} - }; -{{/if}} - - const config: AgentCoreMemoryConfig = { - memoryId: MEMORY_ID, - sessionId, - actorId, -{{#if memoryProviders.[0].strategies.length}} - retrievalConfig, -{{/if}} - }; - - return new AgentCoreMemorySessionManager(config, REGION); -} diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index a2393b959..2c17432bc 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -277,12 +277,15 @@ export async function mapGenerateConfigToRenderConfig( sdkFramework: config.sdk, targetLanguage: config.language, modelProvider: config.modelProvider, - hasMemory: isMcp ? false : config.memory !== 'none', + hasMemory: isMcp || config.language === 'TypeScript' ? false : config.memory !== 'none', hasIdentity: isMcp ? false : identityProviders.length > 0, hasGateway: gatewayProviders.length > 0, isVpc: config.networkMode === 'VPC', buildType: config.buildType, - memoryProviders: isMcp ? [] : mapMemoryOptionToMemoryProviders(config.memory, config.projectName), + memoryProviders: + isMcp || config.language === 'TypeScript' + ? [] + : mapMemoryOptionToMemoryProviders(config.memory, config.projectName), identityProviders: isMcp ? [] : identityProviders, gatewayProviders, gatewayAuthTypes: [...new Set(gatewayProviders.map(g => g.authType))], diff --git a/src/cli/tui/screens/generate/GenerateWizardUI.tsx b/src/cli/tui/screens/generate/GenerateWizardUI.tsx index 4ceb3d0b7..f8cfe6c50 100644 --- a/src/cli/tui/screens/generate/GenerateWizardUI.tsx +++ b/src/cli/tui/screens/generate/GenerateWizardUI.tsx @@ -32,6 +32,7 @@ import { PROTOCOL_OPTIONS, STEP_LABELS, getModelProviderOptionsForSdk, + getProtocolOptionsForLanguage, getSDKOptionsForProtocol, } from './types'; import type { useGenerateWizard } from './useGenerateWizard'; @@ -81,11 +82,10 @@ export function GenerateWizardUI({ case 'buildType': return BUILD_TYPE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); case 'protocol': - return PROTOCOL_OPTIONS.map(o => ({ + return getProtocolOptionsForLanguage(wizard.config.language).map(o => ({ id: o.id, title: o.title, description: o.description, - disabled: wizard.config.language === 'TypeScript' && o.id !== 'HTTP', })); case 'sdk': return getSDKOptionsForProtocol(wizard.config.protocol, wizard.config.language).map(o => ({ diff --git a/src/cli/tui/screens/generate/types.ts b/src/cli/tui/screens/generate/types.ts index 23b87f818..9a1eee4d3 100644 --- a/src/cli/tui/screens/generate/types.ts +++ b/src/cli/tui/screens/generate/types.ts @@ -118,6 +118,17 @@ export const PROTOCOL_OPTIONS = [ { id: 'A2A', title: 'A2A', description: 'Agent-to-Agent protocol' }, ] as const; +/** + * Get protocol options filtered by target language. + * TypeScript only supports HTTP. + */ +export function getProtocolOptionsForLanguage(language?: TargetLanguage) { + if (language === 'TypeScript') { + return PROTOCOL_OPTIONS.filter(option => option.id === 'HTTP'); + } + return [...PROTOCOL_OPTIONS]; +} + export const SDK_OPTIONS = [ { id: 'Strands', title: 'Strands Agents SDK', description: 'AWS native agent framework' }, { id: 'LangChain_LangGraph', title: 'LangChain + LangGraph', description: 'Popular open-source frameworks' }, diff --git a/src/cli/tui/screens/generate/useGenerateWizard.ts b/src/cli/tui/screens/generate/useGenerateWizard.ts index 16e016ad6..0de7fbd49 100644 --- a/src/cli/tui/screens/generate/useGenerateWizard.ts +++ b/src/cli/tui/screens/generate/useGenerateWizard.ts @@ -53,7 +53,7 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { if (config.modelProvider === 'Bedrock') { filtered = filtered.filter(s => s !== 'apiKey'); } - if (sdkSelected && config.sdk === 'Strands') { + if (sdkSelected && config.sdk === 'Strands' && config.language !== 'TypeScript') { const advancedIndex = filtered.indexOf('advanced'); filtered = [...filtered.slice(0, advancedIndex), 'memory', ...filtered.slice(advancedIndex)]; } @@ -101,6 +101,7 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { config.buildType, config.modelProvider, config.sdk, + config.language, config.protocol, config.networkMode, config.authorizerType, @@ -125,7 +126,7 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { }, []); const setLanguage = useCallback((language: GenerateConfig['language']) => { - setConfig(c => ({ ...c, language })); + setConfig(c => ({ ...c, language, memory: language === 'TypeScript' ? 'none' : c.memory })); setStep('buildType'); }, []); @@ -163,34 +164,34 @@ export function useGenerateWizard(options?: UseGenerateWizardOptions) { // Non-Bedrock providers need API key step if (modelProvider !== 'Bedrock') { setStep('apiKey'); - } else if (config.sdk === 'Strands') { + } else if (config.sdk === 'Strands' && config.language !== 'TypeScript') { setStep('memory'); } else { setStep('advanced'); } }, - [config.sdk] + [config.sdk, config.language] ); const setApiKey = useCallback( (apiKey: string | undefined) => { setConfig(c => ({ ...c, apiKey })); - if (config.sdk === 'Strands') { + if (config.sdk === 'Strands' && config.language !== 'TypeScript') { setStep('memory'); } else { setStep('advanced'); } }, - [config.sdk] + [config.sdk, config.language] ); const skipApiKey = useCallback(() => { - if (config.sdk === 'Strands') { + if (config.sdk === 'Strands' && config.language !== 'TypeScript') { setStep('memory'); } else { setStep('advanced'); } - }, [config.sdk]); + }, [config.sdk, config.language]); const setMemory = useCallback((memory: MemoryOption) => { setConfig(c => ({ ...c, memory })); From 1cb83e30def6a38d6375f16ca9ca89262cff2295 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 28 Apr 2026 14:54:21 +0000 Subject: [PATCH 34/36] feat(typescript): add Vercel AI SDK framework for TypeScript agents Add VercelAI as a new framework option alongside Strands, LangChain, GoogleADK, and OpenAIAgents. Supports all 4 model providers (Bedrock, Anthropic, OpenAI, Gemini) with HTTP protocol, both CodeZip and Container deploy modes. Includes template assets (main.ts, model/load.ts with per-provider Handlebars conditionals, package.json, tsconfig.json), VercelAIRenderer, renderer factory wiring, TUI wizard option, and validation gate updates. Confidence: high Scope-risk: narrow --- .../assets.snapshot.test.ts.snap | 249 ++++++++++++++++++ .../typescript/http/vercelai/base/README.md | 37 +++ .../http/vercelai/base/gitignore.template | 22 ++ .../typescript/http/vercelai/base/main.ts | 23 ++ .../http/vercelai/base/model/load.ts | 83 ++++++ .../http/vercelai/base/package.json | 35 +++ .../http/vercelai/base/tsconfig.json | 19 ++ src/cli/commands/add/validate.ts | 9 +- src/cli/commands/create/validate.ts | 4 +- src/cli/templates/VercelAIRenderer.ts | 9 + src/cli/templates/index.ts | 4 + src/cli/tui/screens/generate/types.ts | 3 +- src/schema/constants.ts | 7 +- 13 files changed, 497 insertions(+), 7 deletions(-) create mode 100644 src/assets/typescript/http/vercelai/base/README.md create mode 100644 src/assets/typescript/http/vercelai/base/gitignore.template create mode 100644 src/assets/typescript/http/vercelai/base/main.ts create mode 100644 src/assets/typescript/http/vercelai/base/model/load.ts create mode 100644 src/assets/typescript/http/vercelai/base/package.json create mode 100644 src/assets/typescript/http/vercelai/base/tsconfig.json create mode 100644 src/cli/templates/VercelAIRenderer.ts diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 59b50c61e..dd14418ae 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -531,6 +531,12 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f "typescript/http/strands/base/model/load.ts", "typescript/http/strands/base/package.json", "typescript/http/strands/base/tsconfig.json", + "typescript/http/vercelai/base/README.md", + "typescript/http/vercelai/base/gitignore.template", + "typescript/http/vercelai/base/main.ts", + "typescript/http/vercelai/base/model/load.ts", + "typescript/http/vercelai/base/package.json", + "typescript/http/vercelai/base/tsconfig.json", ] `; @@ -5016,3 +5022,246 @@ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/ } " `; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/README.md should match snapshot 1`] = ` +"This is a project generated by the AgentCore CLI! + +# Layout + +The generated application code lives at the agent root directory. At the root, there is a \`.gitignore\` file, an +\`agentcore/\` folder which represents the configurations and state associated with this project. Other \`agentcore\` +commands like \`deploy\`, \`dev\`, and \`invoke\` rely on the configuration stored here. + +## Agent Root + +The main entrypoint to your app is defined in \`main.ts\`. Using the AgentCore SDK \`BedrockAgentCoreApp\`, this file +defines an HTTP app that streams tokens using the Vercel AI SDK's \`streamText\` API. + +\`model/load.ts\` instantiates your chosen model provider. + +## Environment Variables + +| Variable | Required | Description | +| --- | --- | --- | +{{#if hasIdentity}}| \`{{identityProviders.[0].envVarName}}\` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) | +{{/if}}| \`LOCAL_DEV\` | No | Set to \`1\` to use \`.env.local\` instead of AgentCore Identity | + +# Developing locally + +If installation was successful, \`node_modules/\` is already populated with dependencies. + +\`agentcore dev\` will start a local server on 0.0.0.0:8080 using \`npx tsx watch main.ts\` for hot reload. + +In a new terminal, you can invoke that server with: + +\`agentcore invoke --dev "What can you do"\` + +# Deployment + +After providing credentials, \`agentcore deploy\` will deploy your project into Amazon Bedrock AgentCore. + +Use \`agentcore invoke\` to invoke your deployed agent. +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/gitignore.template should match snapshot 1`] = ` +"# Environment variables +.env +.env.* + +# Node +node_modules/ +dist/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/main.ts should match snapshot 1`] = ` +"import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime'; +import { streamText } from 'ai'; +import { loadModel } from './model/load.js'; + +const SYSTEM_PROMPT = \`You are a helpful assistant.\`; + +const app = new BedrockAgentCoreApp({ + invocationHandler: { + async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { + const result = streamText({ + model: loadModel(), + system: SYSTEM_PROMPT, + prompt: payload.prompt ?? '', + }); + + for await (const chunk of result.textStream) { + yield { data: chunk }; + } + }, + }, +}); + +app.run(); +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/model/load.ts should match snapshot 1`] = ` +"{{#if (eq modelProvider "Bedrock")}} +import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; + +const bedrock = createAmazonBedrock({ + region: process.env.AWS_REGION ?? 'us-east-1', +}); + +export function loadModel() { + return bedrock('us.anthropic.claude-sonnet-4-5-20250514-v1:0'); +} +{{/if}} +{{#if (eq modelProvider "Anthropic")}} +import { createAnthropic } from '@ai-sdk/anthropic'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(\`\${IDENTITY_ENV_VAR} not found. Add \${IDENTITY_ENV_VAR}=your-key to .env.local\`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +const anthropic = createAnthropic({ apiKey: getApiKey }); + +export function loadModel() { + return anthropic('claude-sonnet-4-5-20250929'); +} +{{/if}} +{{#if (eq modelProvider "OpenAI")}} +import { createOpenAI } from '@ai-sdk/openai'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(\`\${IDENTITY_ENV_VAR} not found. Add \${IDENTITY_ENV_VAR}=your-key to .env.local\`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +const openai = createOpenAI({ apiKey: getApiKey }); + +export function loadModel() { + return openai('gpt-4.1'); +} +{{/if}} +{{#if (eq modelProvider "Gemini")}} +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(\`\${IDENTITY_ENV_VAR} not found. Add \${IDENTITY_ENV_VAR}=your-key to .env.local\`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +const google = createGoogleGenerativeAI({ apiKey: getApiKey }); + +export function loadModel() { + return google('gemini-2.5-flash'); +} +{{/if}} +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/package.json should match snapshot 1`] = ` +"{ + "name": "{{name}}", + "version": "0.1.0", + "description": "AgentCore Runtime Application using Vercel AI SDK", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/main.js", + "dev": "tsx watch main.ts" + }, + "dependencies": { + "ai": "^4.3.0", + {{#if (eq modelProvider "Bedrock")}} + "@ai-sdk/amazon-bedrock": "^2.2.0", + {{/if}} + {{#if (eq modelProvider "Anthropic")}} + "@ai-sdk/anthropic": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "OpenAI")}} + "@ai-sdk/openai": "^2.2.0", + {{/if}} + {{#if (eq modelProvider "Gemini")}} + "@ai-sdk/google": "^2.2.0", + {{/if}} + "@opentelemetry/api": "^1.9.0", + "bedrock-agentcore": "0.2.2", + "tsx": "^4.19.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} +" +`; + +exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/tsconfig.json should match snapshot 1`] = ` +"{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} +" +`; diff --git a/src/assets/typescript/http/vercelai/base/README.md b/src/assets/typescript/http/vercelai/base/README.md new file mode 100644 index 000000000..d73be5bdf --- /dev/null +++ b/src/assets/typescript/http/vercelai/base/README.md @@ -0,0 +1,37 @@ +This is a project generated by the AgentCore CLI! + +# Layout + +The generated application code lives at the agent root directory. At the root, there is a `.gitignore` file, an +`agentcore/` folder which represents the configurations and state associated with this project. Other `agentcore` +commands like `deploy`, `dev`, and `invoke` rely on the configuration stored here. + +## Agent Root + +The main entrypoint to your app is defined in `main.ts`. Using the AgentCore SDK `BedrockAgentCoreApp`, this file +defines an HTTP app that streams tokens using the Vercel AI SDK's `streamText` API. + +`model/load.ts` instantiates your chosen model provider. + +## Environment Variables + +| Variable | Required | Description | +| --- | --- | --- | +{{#if hasIdentity}}| `{{identityProviders.[0].envVarName}}` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) | +{{/if}}| `LOCAL_DEV` | No | Set to `1` to use `.env.local` instead of AgentCore Identity | + +# Developing locally + +If installation was successful, `node_modules/` is already populated with dependencies. + +`agentcore dev` will start a local server on 0.0.0.0:8080 using `npx tsx watch main.ts` for hot reload. + +In a new terminal, you can invoke that server with: + +`agentcore invoke --dev "What can you do"` + +# Deployment + +After providing credentials, `agentcore deploy` will deploy your project into Amazon Bedrock AgentCore. + +Use `agentcore invoke` to invoke your deployed agent. diff --git a/src/assets/typescript/http/vercelai/base/gitignore.template b/src/assets/typescript/http/vercelai/base/gitignore.template new file mode 100644 index 000000000..feb4f544d --- /dev/null +++ b/src/assets/typescript/http/vercelai/base/gitignore.template @@ -0,0 +1,22 @@ +# Environment variables +.env +.env.* + +# Node +node_modules/ +dist/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/src/assets/typescript/http/vercelai/base/main.ts b/src/assets/typescript/http/vercelai/base/main.ts new file mode 100644 index 000000000..2d71e985b --- /dev/null +++ b/src/assets/typescript/http/vercelai/base/main.ts @@ -0,0 +1,23 @@ +import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime'; +import { streamText } from 'ai'; +import { loadModel } from './model/load.js'; + +const SYSTEM_PROMPT = `You are a helpful assistant.`; + +const app = new BedrockAgentCoreApp({ + invocationHandler: { + async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { + const result = streamText({ + model: loadModel(), + system: SYSTEM_PROMPT, + prompt: payload.prompt ?? '', + }); + + for await (const chunk of result.textStream) { + yield { data: chunk }; + } + }, + }, +}); + +app.run(); diff --git a/src/assets/typescript/http/vercelai/base/model/load.ts b/src/assets/typescript/http/vercelai/base/model/load.ts new file mode 100644 index 000000000..f4abd751d --- /dev/null +++ b/src/assets/typescript/http/vercelai/base/model/load.ts @@ -0,0 +1,83 @@ +{{#if (eq modelProvider "Bedrock")}} +import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; + +const bedrock = createAmazonBedrock({ + region: process.env.AWS_REGION ?? 'us-east-1', +}); + +export function loadModel() { + return bedrock('us.anthropic.claude-sonnet-4-5-20250514-v1:0'); +} +{{/if}} +{{#if (eq modelProvider "Anthropic")}} +import { createAnthropic } from '@ai-sdk/anthropic'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(`${IDENTITY_ENV_VAR} not found. Add ${IDENTITY_ENV_VAR}=your-key to .env.local`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +const anthropic = createAnthropic({ apiKey: getApiKey }); + +export function loadModel() { + return anthropic('claude-sonnet-4-5-20250929'); +} +{{/if}} +{{#if (eq modelProvider "OpenAI")}} +import { createOpenAI } from '@ai-sdk/openai'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(`${IDENTITY_ENV_VAR} not found. Add ${IDENTITY_ENV_VAR}=your-key to .env.local`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +const openai = createOpenAI({ apiKey: getApiKey }); + +export function loadModel() { + return openai('gpt-4.1'); +} +{{/if}} +{{#if (eq modelProvider "Gemini")}} +import { createGoogleGenerativeAI } from '@ai-sdk/google'; +import { withApiKey } from 'bedrock-agentcore/identity'; + +const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; +const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; + +async function getApiKey(): Promise { + if (process.env.LOCAL_DEV === '1') { + const apiKey = process.env[IDENTITY_ENV_VAR]; + if (!apiKey) { + throw new Error(`${IDENTITY_ENV_VAR} not found. Add ${IDENTITY_ENV_VAR}=your-key to .env.local`); + } + return apiKey; + } + return withApiKey({ providerName: IDENTITY_PROVIDER_NAME }, async (apiKey: string) => apiKey)(); +} + +const google = createGoogleGenerativeAI({ apiKey: getApiKey }); + +export function loadModel() { + return google('gemini-2.5-flash'); +} +{{/if}} diff --git a/src/assets/typescript/http/vercelai/base/package.json b/src/assets/typescript/http/vercelai/base/package.json new file mode 100644 index 000000000..c2ee01d68 --- /dev/null +++ b/src/assets/typescript/http/vercelai/base/package.json @@ -0,0 +1,35 @@ +{ + "name": "{{name}}", + "version": "0.1.0", + "description": "AgentCore Runtime Application using Vercel AI SDK", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/main.js", + "dev": "tsx watch main.ts" + }, + "dependencies": { + "ai": "^4.3.0", + {{#if (eq modelProvider "Bedrock")}} + "@ai-sdk/amazon-bedrock": "^2.2.0", + {{/if}} + {{#if (eq modelProvider "Anthropic")}} + "@ai-sdk/anthropic": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "OpenAI")}} + "@ai-sdk/openai": "^2.2.0", + {{/if}} + {{#if (eq modelProvider "Gemini")}} + "@ai-sdk/google": "^2.2.0", + {{/if}} + "@opentelemetry/api": "^1.9.0", + "bedrock-agentcore": "0.2.2", + "tsx": "^4.19.0", + "zod": "^3.24.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + } +} diff --git a/src/assets/typescript/http/vercelai/base/tsconfig.json b/src/assets/typescript/http/vercelai/base/tsconfig.json new file mode 100644 index 000000000..c199ae076 --- /dev/null +++ b/src/assets/typescript/http/vercelai/base/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 02c74b47a..f37d72a54 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -247,10 +247,15 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes if (options.language === 'Other') { return { valid: false, error: 'Create path only supports Python or TypeScript' }; } - if (options.language === 'TypeScript' && options.framework && options.framework !== 'Strands') { + if ( + options.language === 'TypeScript' && + options.framework && + options.framework !== 'Strands' && + options.framework !== 'VercelAI' + ) { return { valid: false, - error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands is supported.`, + error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands and Vercel AI SDK are supported.`, }; } diff --git a/src/cli/commands/create/validate.ts b/src/cli/commands/create/validate.ts index 789e68d20..d69186751 100644 --- a/src/cli/commands/create/validate.ts +++ b/src/cli/commands/create/validate.ts @@ -185,10 +185,10 @@ export function validateCreateOptions(options: CreateOptions, cwd?: string): Val } // TypeScript is Strands-only for now - if (options.language === 'TypeScript' && fwResult.data !== 'Strands') { + if (options.language === 'TypeScript' && fwResult.data !== 'Strands' && fwResult.data !== 'VercelAI') { return { valid: false, - error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands is supported.`, + error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands and Vercel AI SDK are supported.`, }; } diff --git a/src/cli/templates/VercelAIRenderer.ts b/src/cli/templates/VercelAIRenderer.ts new file mode 100644 index 000000000..0fd1f9f93 --- /dev/null +++ b/src/cli/templates/VercelAIRenderer.ts @@ -0,0 +1,9 @@ +import { BaseRenderer } from './BaseRenderer'; +import { TEMPLATE_ROOT } from './templateRoot'; +import type { AgentRenderConfig } from './types'; + +export class VercelAIRenderer extends BaseRenderer { + constructor(config: AgentRenderConfig) { + super(config, 'vercelai', TEMPLATE_ROOT, config.protocol ?? 'http'); + } +} diff --git a/src/cli/templates/index.ts b/src/cli/templates/index.ts index 3e57beb8d..e41e563b3 100644 --- a/src/cli/templates/index.ts +++ b/src/cli/templates/index.ts @@ -4,6 +4,7 @@ import { LangGraphRenderer } from './LangGraphRenderer'; import { McpRenderer } from './McpRenderer'; import { OpenAIAgentsRenderer } from './OpenAIAgentsRenderer'; import { StrandsRenderer } from './StrandsRenderer'; +import { VercelAIRenderer } from './VercelAIRenderer'; import type { AgentRenderConfig } from './types'; export { BaseRenderer, type RendererContext } from './BaseRenderer'; @@ -14,6 +15,7 @@ export { LangGraphRenderer } from './LangGraphRenderer'; export { McpRenderer } from './McpRenderer'; export { OpenAIAgentsRenderer } from './OpenAIAgentsRenderer'; export { StrandsRenderer } from './StrandsRenderer'; +export { VercelAIRenderer } from './VercelAIRenderer'; export type { AgentRenderConfig } from './types'; /** @@ -34,6 +36,8 @@ export function createRenderer(config: AgentRenderConfig): BaseRenderer { return new LangGraphRenderer(config); case 'OpenAIAgents': return new OpenAIAgentsRenderer(config); + case 'VercelAI': + return new VercelAIRenderer(config); default: { const _exhaustive: never = config.sdkFramework; throw new Error(`Unsupported SDK framework: ${String(_exhaustive)}`); diff --git a/src/cli/tui/screens/generate/types.ts b/src/cli/tui/screens/generate/types.ts index 9a1eee4d3..afb44a117 100644 --- a/src/cli/tui/screens/generate/types.ts +++ b/src/cli/tui/screens/generate/types.ts @@ -134,6 +134,7 @@ export const SDK_OPTIONS = [ { id: 'LangChain_LangGraph', title: 'LangChain + LangGraph', description: 'Popular open-source frameworks' }, { id: 'GoogleADK', title: 'Google ADK', description: 'Google Agent Development Kit' }, { id: 'OpenAIAgents', title: 'OpenAI Agents', description: 'OpenAI native agent SDK' }, + { id: 'VercelAI', title: 'Vercel AI SDK', description: 'Vercel AI SDK for TypeScript agents' }, ] as const; /** @@ -144,7 +145,7 @@ export function getSDKOptionsForProtocol(protocol: ProtocolMode, language?: Targ const supportedFrameworks = PROTOCOL_FRAMEWORK_MATRIX[protocol]; const byProtocol = SDK_OPTIONS.filter(option => supportedFrameworks.includes(option.id)); if (language === 'TypeScript') { - return byProtocol.filter(option => option.id === 'Strands'); + return byProtocol.filter(option => option.id === 'Strands' || option.id === 'VercelAI'); } return byProtocol; } diff --git a/src/schema/constants.ts b/src/schema/constants.ts index e85ad0126..a5c34448e 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; // Feature Constants (shared across all schemas) // ============================================================================ -export const SDKFrameworkSchema = z.enum(['Strands', 'LangChain_LangGraph', 'GoogleADK', 'OpenAIAgents']); +export const SDKFrameworkSchema = z.enum(['Strands', 'LangChain_LangGraph', 'GoogleADK', 'OpenAIAgents', 'VercelAI']); export type SDKFramework = z.infer; export const TargetLanguageSchema = z.enum(['Python', 'TypeScript', 'Other']); @@ -47,6 +47,7 @@ export const SDK_MODEL_PROVIDER_MATRIX: Record; * MCP is a standalone tool server with no framework. */ export const PROTOCOL_FRAMEWORK_MATRIX: Record = { - HTTP: ['Strands', 'LangChain_LangGraph', 'GoogleADK', 'OpenAIAgents'] as const, + HTTP: ['Strands', 'LangChain_LangGraph', 'GoogleADK', 'OpenAIAgents', 'VercelAI'] as const, MCP: [] as const, A2A: ['Strands', 'GoogleADK', 'LangChain_LangGraph'] as const, }; From d6aec09d39169ba8a83a03446ba3b305bb20d88f Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 28 Apr 2026 15:28:52 +0000 Subject: [PATCH 35/36] fix(vercelai): fix dependency versions, model ID, and Bedrock credentials The Vercel AI template had outdated dependency versions (ai@^4.3.0) incompatible with bedrock-agentcore@0.2.2 which requires ai@>=6.0.0. The Bedrock model ID was invalid, and the provider lacked credential chain support needed for deployed AgentCore Runtime environments. Constraint: @ai-sdk/amazon-bedrock does not use AWS SDK default credential chain Rejected: Explicit env var credentials | not available in AgentCore Runtime container Confidence: high Scope-risk: narrow --- .../assets.snapshot.test.ts.snap | 22 ++++++++++++++----- .../http/vercelai/base/model/load.ts | 13 ++++++++++- .../http/vercelai/base/package.json | 9 ++++---- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index dd14418ae..2ae9671c8 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -5120,13 +5120,24 @@ app.run(); exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/model/load.ts should match snapshot 1`] = ` "{{#if (eq modelProvider "Bedrock")}} import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; + +const provider = fromNodeProviderChain(); const bedrock = createAmazonBedrock({ region: process.env.AWS_REGION ?? 'us-east-1', + credentialProvider: async () => { + const creds = await provider(); + return { + accessKeyId: creds.accessKeyId, + secretAccessKey: creds.secretAccessKey, + sessionToken: creds.sessionToken, + }; + }, }); export function loadModel() { - return bedrock('us.anthropic.claude-sonnet-4-5-20250514-v1:0'); + return bedrock('us.anthropic.claude-sonnet-4-5-20250929-v1:0'); } {{/if}} {{#if (eq modelProvider "Anthropic")}} @@ -5217,18 +5228,19 @@ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/ "dev": "tsx watch main.ts" }, "dependencies": { - "ai": "^4.3.0", + "ai": "^6.0.0", {{#if (eq modelProvider "Bedrock")}} - "@ai-sdk/amazon-bedrock": "^2.2.0", + "@ai-sdk/amazon-bedrock": "^4.0.0", + "@aws-sdk/credential-providers": "^3.0.0", {{/if}} {{#if (eq modelProvider "Anthropic")}} "@ai-sdk/anthropic": "^3.0.0", {{/if}} {{#if (eq modelProvider "OpenAI")}} - "@ai-sdk/openai": "^2.2.0", + "@ai-sdk/openai": "^3.0.0", {{/if}} {{#if (eq modelProvider "Gemini")}} - "@ai-sdk/google": "^2.2.0", + "@ai-sdk/google": "^3.0.0", {{/if}} "@opentelemetry/api": "^1.9.0", "bedrock-agentcore": "0.2.2", diff --git a/src/assets/typescript/http/vercelai/base/model/load.ts b/src/assets/typescript/http/vercelai/base/model/load.ts index f4abd751d..c42657075 100644 --- a/src/assets/typescript/http/vercelai/base/model/load.ts +++ b/src/assets/typescript/http/vercelai/base/model/load.ts @@ -1,12 +1,23 @@ {{#if (eq modelProvider "Bedrock")}} import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock'; +import { fromNodeProviderChain } from '@aws-sdk/credential-providers'; + +const provider = fromNodeProviderChain(); const bedrock = createAmazonBedrock({ region: process.env.AWS_REGION ?? 'us-east-1', + credentialProvider: async () => { + const creds = await provider(); + return { + accessKeyId: creds.accessKeyId, + secretAccessKey: creds.secretAccessKey, + sessionToken: creds.sessionToken, + }; + }, }); export function loadModel() { - return bedrock('us.anthropic.claude-sonnet-4-5-20250514-v1:0'); + return bedrock('us.anthropic.claude-sonnet-4-5-20250929-v1:0'); } {{/if}} {{#if (eq modelProvider "Anthropic")}} diff --git a/src/assets/typescript/http/vercelai/base/package.json b/src/assets/typescript/http/vercelai/base/package.json index c2ee01d68..e429c27dc 100644 --- a/src/assets/typescript/http/vercelai/base/package.json +++ b/src/assets/typescript/http/vercelai/base/package.json @@ -10,18 +10,19 @@ "dev": "tsx watch main.ts" }, "dependencies": { - "ai": "^4.3.0", + "ai": "^6.0.0", {{#if (eq modelProvider "Bedrock")}} - "@ai-sdk/amazon-bedrock": "^2.2.0", + "@ai-sdk/amazon-bedrock": "^4.0.0", + "@aws-sdk/credential-providers": "^3.0.0", {{/if}} {{#if (eq modelProvider "Anthropic")}} "@ai-sdk/anthropic": "^3.0.0", {{/if}} {{#if (eq modelProvider "OpenAI")}} - "@ai-sdk/openai": "^2.2.0", + "@ai-sdk/openai": "^3.0.0", {{/if}} {{#if (eq modelProvider "Gemini")}} - "@ai-sdk/google": "^2.2.0", + "@ai-sdk/google": "^3.0.0", {{/if}} "@opentelemetry/api": "^1.9.0", "bedrock-agentcore": "0.2.2", From ea7e87e19c7ce6bd0dac6acc5c18083e0bf87e01 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Tue, 28 Apr 2026 16:40:46 +0000 Subject: [PATCH 36/36] fix(dev): prefer explicit credentials over AWS_PROFILE in container dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When both AWS_ACCESS_KEY_ID and AWS_PROFILE are set, the SDK credential chain inside the container prefers AWS_PROFILE. But the mounted ~/.aws files have 600 permissions owned by the host user, so the container's bedrock_agentcore user cannot read them — falling back to EC2 instance metadata which uses the wrong account. Now omits AWS_PROFILE from the container env when explicit credentials are available, so the credential chain finds env var creds first. Constraint: Container user uid differs from host user uid Rejected: chmod the mounted credentials | security risk, read-only mount Confidence: high Scope-risk: moderate --- .../dev/__tests__/container-dev-server.test.ts | 17 +++++++++++++++++ src/cli/operations/dev/container-dev-server.ts | 8 ++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) 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 e8510ce94..4d38c3551 100644 --- a/src/cli/operations/dev/__tests__/container-dev-server.test.ts +++ b/src/cli/operations/dev/__tests__/container-dev-server.test.ts @@ -377,7 +377,24 @@ describe('ContainerDevServer', () => { expect(spawnArgs).toContain('AWS_SESSION_TOKEN=FwoGZXIvYXdzEBY'); expect(spawnArgs).toContain('AWS_REGION=us-east-1'); expect(spawnArgs).toContain('AWS_DEFAULT_REGION=us-west-2'); + expect(spawnArgs).not.toContain('AWS_PROFILE=dev-profile'); + }); + + it('forwards AWS_PROFILE when no explicit credentials are set', async () => { + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; + delete process.env.AWS_SESSION_TOKEN; + process.env.AWS_REGION = 'us-east-1'; + process.env.AWS_PROFILE = 'dev-profile'; + + mockSuccessfulPrepare(); + + const server = new ContainerDevServer(defaultConfig, defaultOptions); + await server.start(); + + const spawnArgs = getSpawnArgs(); expect(spawnArgs).toContain('AWS_PROFILE=dev-profile'); + expect(spawnArgs).toContain('AWS_REGION=us-east-1'); }); it('does not include AWS env vars when not set', async () => { diff --git a/src/cli/operations/dev/container-dev-server.ts b/src/cli/operations/dev/container-dev-server.ts index 10ef21b3f..537ff67a8 100644 --- a/src/cli/operations/dev/container-dev-server.ts +++ b/src/cli/operations/dev/container-dev-server.ts @@ -126,14 +126,18 @@ export class ContainerDevServer extends DevServer { protected getSpawnConfig(): SpawnConfig { const { port, envVars = {} } = this.options; - // Forward AWS credentials from host environment into the container + // Forward AWS credentials from host environment into the container. + // When explicit credentials are present, omit AWS_PROFILE so SDK credential + // chains prefer the env var credentials over profile-based resolution (which + // can fail when the container user cannot read the mounted ~/.aws files). + const hasExplicitCreds = !!process.env.AWS_ACCESS_KEY_ID; const awsEnvKeys = [ 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AWS_REGION', 'AWS_DEFAULT_REGION', - 'AWS_PROFILE', + ...(hasExplicitCreds ? [] : ['AWS_PROFILE']), ]; const awsEnvVars: Record = {}; for (const key of awsEnvKeys) {