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/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_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/docs/TYPESCRIPT_SUPPORT_PROGRESS.md b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md new file mode 100644 index 000000000..c65de8f56 --- /dev/null +++ b/docs/TYPESCRIPT_SUPPORT_PROGRESS.md @@ -0,0 +1,259 @@ +# 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) + +- [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. + +--- + +## 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`. + +- [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`, `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. + +--- + +## Phase 4 — Container template + +Under `src/assets/container/typescript/`. + +- [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. + +--- + +## Phase 5 — Node setup helper + create-flow wiring + +- [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:** + +--- + +## 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:** +- [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:** 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:** 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. +- [x] **TUI harness walkthrough** — mirror an existing Python walkthrough under `integ-tests/tui/` selecting TypeScript + → Strands. + - **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. + - **Notes:** +- [x] **Non-Strands rejection test** — confirm + `agentcore add agent --language TypeScript --framework LangChain_LangGraph` fails fast. + - **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). + +--- + +## Phase 7 — Documentation + +- [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. + +--- + +## Phase 8 — Verification (end-to-end, manual) + +Run from a clean scratch dir against the deploy profile. Record results inline. + +- [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`. + +--- + +## 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). +- `3417f9a` — Phase 0: add progress tracker doc (verification sweep results baked in). +- `a487f19` — Phase 2: drop Python-only guard in isDevSupported so TS agents reach the dev server. +- `6f1aeed` — Phase 2: log a487f19 in progress tracker. +- `f6ed2e9` — Phase 3: author TS/Strands HTTP template assets + align stale tests + extend .prettierignore. +- `003f672` — Phase 3: log 6f1aeed + f6ed2e9 in progress tracker. +- `076a4aa` — Phase 4: add TS container template (Dockerfile + dockerignore). +- `f015ce7` — Phase 4: log 003f672 + 076a4aa in progress tracker. +- `5c2af7d` — Phase 5: Node setup helper + create-flow wiring + unit tests. +- `c22147d` — Phase 6: TS dev-server spec + create integ block; fix spawn entrypoint rewrite bug. +- `ba49229` — Phase 6: log c22147d in progress tracker. +- `7af265e` — Phase 6: add TUI walkthrough for create TypeScript + Strands. +- `c406af9` — Phase 7/8: replace Python-only guard in create validator with Strands gate + update test. +- _(next commit goes here)_ diff --git a/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md b/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md new file mode 100644 index 000000000..99b0db066 --- /dev/null +++ b/docs/TYPESCRIPT_SUPPORT_TEST_PLAN.md @@ -0,0 +1,433 @@ +# 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 + +--- + +## 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`). +- [ ] 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. + +**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 +``` + +- [ ] 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. + +**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 +``` + +- [ ] 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). + +**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 +``` + +- [ ] 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`.) + +**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 +``` + +- [ ] 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. + +**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 +``` + +- [ ] `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. + +**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. + +- [ ] `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. 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`. diff --git a/docs/commands.md b/docs/commands.md index f2ea60a0e..e34d67464 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -51,6 +51,13 @@ agentcore create \ # Skip agent creation agentcore create --name MyProject --no-agent +# TypeScript (Strands-only) +agentcore create \ + --name MyTsProject \ + --language TypeScript \ + --framework Strands \ + --model-provider Bedrock + # Preview without creating agentcore create --name MyProject --defaults --dry-run @@ -71,7 +78,7 @@ agentcore create \ | `--defaults` | Use defaults (Python, Strands, Bedrock, no memory) | | `--no-agent` | Skip agent creation | | `--type ` | `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`: 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/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); +}); diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 1a51b6b29..2ae9671c8 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", @@ -522,6 +524,19 @@ 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/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", ] `; @@ -4552,3 +4567,713 @@ 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 sessionStorageMountPath}} +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; +{{/if}} + +// 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}} +\`; + +let cachedAgent: Agent | null = null; + +function getOrCreateAgent(): Agent { + if (!cachedAgent) { + cachedAgent = new Agent({ + model: loadModel(), + systemPrompt: SYSTEM_PROMPT, + tools, + }); + } + return cachedAgent; +} + +const app = new BedrockAgentCoreApp({ + invocationHandler: { + async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { + const agent = getOrCreateAgent(); + + for await (const event of agent.stream(payload.prompt ?? '')) { + if ( + event.type === 'modelStreamUpdateEvent' && + event.event?.type === 'modelContentBlockDeltaEvent' && + event.event.delta?.type === 'textDelta' + ) { + yield { data: event.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": { + "@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", + "express": "^5.1.0", + "tsx": "^4.19.0", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + }, + "overrides": { + "bedrock-agentcore": { + "@strands-agents/sdk": "$@strands-agents/sdk" + } + } +} +" +`; + +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/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'; +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-20250929-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": "^6.0.0", + {{#if (eq modelProvider "Bedrock")}} + "@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": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "Gemini")}} + "@ai-sdk/google": "^3.0.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/container/typescript/Dockerfile b/src/assets/container/typescript/Dockerfile new file mode 100644 index 000000000..df9c6bac1 --- /dev/null +++ b/src/assets/container/typescript/Dockerfile @@ -0,0 +1,25 @@ +FROM public.ecr.aws/docker/library/node:22-slim + +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* ./ +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 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..82e983012 --- /dev/null +++ b/src/assets/typescript/http/strands/base/main.ts @@ -0,0 +1,154 @@ +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 sessionStorageMountPath}} +import { promises as fs } from 'node:fs'; +import * as path from 'node:path'; +{{/if}} + +// 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}} +`; + +let cachedAgent: Agent | null = null; + +function getOrCreateAgent(): Agent { + if (!cachedAgent) { + cachedAgent = new Agent({ + model: loadModel(), + systemPrompt: SYSTEM_PROMPT, + tools, + }); + } + return cachedAgent; +} + +const app = new BedrockAgentCoreApp({ + invocationHandler: { + async *process(payload: { prompt?: string }, context: { sessionId?: string; userId?: string }) { + const agent = getOrCreateAgent(); + + for await (const event of agent.stream(payload.prompt ?? '')) { + if ( + event.type === 'modelStreamUpdateEvent' && + event.event?.type === 'modelContentBlockDeltaEvent' && + event.event.delta?.type === 'textDelta' + ) { + yield { data: event.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..e16b598e2 --- /dev/null +++ b/src/assets/typescript/http/strands/base/package.json @@ -0,0 +1,37 @@ +{ + "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": { + "@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", + "express": "^5.1.0", + "tsx": "^4.19.0", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0" + }, + "overrides": { + "bedrock-agentcore": { + "@strands-agents/sdk": "$@strands-agents/sdk" + } + } +} 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/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..c42657075 --- /dev/null +++ b/src/assets/typescript/http/vercelai/base/model/load.ts @@ -0,0 +1,94 @@ +{{#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-20250929-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..e429c27dc --- /dev/null +++ b/src/assets/typescript/http/vercelai/base/package.json @@ -0,0 +1,36 @@ +{ + "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": "^6.0.0", + {{#if (eq modelProvider "Bedrock")}} + "@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": "^3.0.0", + {{/if}} + {{#if (eq modelProvider "Gemini")}} + "@ai-sdk/google": "^3.0.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/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, }); 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/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 81f3d3678..f37d72a54 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'; @@ -236,11 +244,19 @@ 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' && + options.framework !== 'VercelAI' + ) { + return { + valid: false, + error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands and Vercel AI SDK are supported.`, + }; } if (!options.memory) { 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/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 { 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/commands/create/validate.ts b/src/cli/commands/create/validate.ts index 24823938c..d69186751 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) { @@ -153,7 +161,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 +184,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' && fwResult.data !== 'VercelAI') { + return { + valid: false, + error: `Framework ${options.framework} is not yet available for TypeScript. Only Strands and Vercel AI SDK are supported.`, + }; } // Validate framework/model compatibility 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/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index 1afd0a255..2c17432bc 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' && @@ -270,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/operations/dev/__tests__/codezip-dev-server.test.ts b/src/cli/operations/dev/__tests__/codezip-dev-server.test.ts index e5a9b6566..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}`, })); @@ -119,6 +124,82 @@ 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('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/__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', () => { 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/codezip-dev-server.ts b/src/cli/operations/dev/codezip-dev-server.ts index 695659c73..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) ); } @@ -118,9 +147,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, }; 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 }; } 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) { 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/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/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'); 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/create/useCreateFlow.ts b/src/cli/tui/screens/create/useCreateFlow.ts index 04c003ad0..e571659b6 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,36 @@ 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 { + 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++; + } } // Step: Create CDK project 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. diff --git a/src/cli/tui/screens/generate/GenerateWizardUI.tsx b/src/cli/tui/screens/generate/GenerateWizardUI.tsx index 4b61e4689..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'; @@ -77,14 +78,17 @@ 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 })); case 'protocol': - return PROTOCOL_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })); + return getProtocolOptionsForLanguage(wizard.config.language).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..afb44a117 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 = [ @@ -118,19 +118,36 @@ 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' }, { 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; /** - * 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' || option.id === 'VercelAI'); + } + return byProtocol; } export const MODEL_PROVIDER_OPTIONS = [ 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 })); diff --git a/src/schema/constants.ts b/src/schema/constants.ts index fc546276a..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; +/** 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; @@ -165,7 +183,7 @@ export type ProtocolMode = z.infer; * 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, };