--- name: agent-squad-typescript description: >- Use when building or modifying a Node.js / TypeScript app that uses the agent-squad npm package — multi-agent orchestration: orchestrator, agents (all built-in types + GroundedAgent), classifier routing (Bedrock / Anthropic / OpenAI), storage (in-memory / DynamoDB / SQL), retrievers (Amazon KB / Dakera), and tools (AgentTools + MCPToolProvider). --- # agent-squad TypeScript — assistant guide Node.js / TypeScript multi-agent orchestration framework (npm package `agent-squad`). All public symbols are exported from a single barrel `typescript/src/index.ts`. This file is guidance and a map — **not an API reference**. Read exact signatures from `typescript/src/` and worked recipes from `docs/src/content/docs/`; this file tells you *what to use, when, and what to watch out for*. ## When to use what - **One assistant** → a single `Agent` subclass + `AgentSquad` with no routing. Or skip the orchestrator entirely and call `agent.processRequest(...)` directly. - **Several specialists** → multiple agents registered with `orchestrator.addAgent(agent)`, a classifier routes each turn. - **Answers must not drift from data** (prices, balances, live lookups) → `GroundedAgent`: a gatherer LLM calls tools, an isolated presenter LLM speaks only from the curated results. - **Fixed pipeline** → `ChainAgent`: each agent's output is the next agent's input. - **One lead LLM coordinating a team** → `SupervisorAgent`: the lead calls sub-agents as tools. - **External tools via MCP** → `MCPToolProvider` (async factory pattern, optional peer dep). - **RAG context** → attach a `Retriever` to any agent that supports `retriever?` in its options. ## How to install ```bash npm install agent-squad ``` Optional peer dependencies — install only what you use: | Package | Used by | |---|---| | `@aws-sdk/client-bedrock-runtime` | `BedrockLLMAgent`, `BedrockClassifier` (already a hard dep in current releases) | | `@anthropic-ai/sdk` | `AnthropicAgent`, `AnthropicClassifier` (already a hard dep) | | `openai` | `OpenAIAgent`, `OpenAIClassifier` (already a hard dep) | | `@modelcontextprotocol/sdk` | `MCPToolProvider` — lazy `await import()` at connect time | | `@dakera-ai/dakera` | `DakeraRetriever` — lazy `require()` at construction time | `@modelcontextprotocol/sdk` and `@dakera-ai/dakera` are the only two true optional peer deps; everything else ships as a hard dependency at the moment. ## How a turn works `routeRequest` is the single entry point. It classifies the input, dispatches to the selected agent, saves the exchange, and returns an `AgentResponse`. The response is either a plain string or a Node.js `Transform` stream: ```typescript import { AgentSquad, BedrockLLMAgent, BedrockClassifier } from 'agent-squad'; const orchestrator = new AgentSquad({ classifier: new BedrockClassifier(), // default when omitted // storage: new DynamoDbChatStorage(...), // config: { LOG_AGENT_CHAT: true, MAX_MESSAGE_PAIRS_PER_AGENT: 50 }, }); orchestrator.addAgent(new BedrockLLMAgent({ name: 'Tech Support', description: 'Handles technical questions about software and hardware', streaming: true, })); const response = await orchestrator.routeRequest( userInput, userId, sessionId, additionalParams // optional Record ); if (response.streaming) { // response.output is an AccumulatorTransform (Node.js Transform) for await (const chunk of response.output) { process.stdout.write(chunk); } } else { // response.output is a string console.log(response.output); // response.thinking? is set when the agent used extended thinking } // response.metadata: { agentId, agentName, userId, sessionId, userInput, additionalParams } ``` `routeRequest` never throws — it catches all errors and returns them as a non-streaming `AgentResponse` with the error string in `output` (configurable via `GENERAL_ROUTING_ERROR_MSG_MESSAGE`). ## The pieces ### Orchestrator: `AgentSquad` ```typescript new AgentSquad(options?: OrchestratorOptions) ``` Key `OrchestratorOptions` fields: | Field | Default | Notes | |---|---|---| | `classifier` | `new BedrockClassifier()` | Any `Classifier` subclass | | `storage` | `new InMemoryChatStorage()` | Any `ChatStorage` subclass | | `defaultAgent` | `undefined` | Used when classifier returns no match and `USE_DEFAULT_AGENT_IF_NONE_IDENTIFIED` is true | | `config.USE_DEFAULT_AGENT_IF_NONE_IDENTIFIED` | `true` | Fall back to `defaultAgent` or return `NO_SELECTED_AGENT_MESSAGE` | | `config.MAX_MESSAGE_PAIRS_PER_AGENT` | `100` | Per-agent history cap (pairs = user+assistant) | | `config.MAX_RETRIES` | `3` | Classifier retries on bad XML response | | `config.LOG_AGENT_CHAT` | `false` | | Useful methods: `addAgent(agent)`, `setDefaultAgent(agent)`, `getDefaultAgent()`, `getAllAgents()`, `analyzeAgentOverlap()`, `classifyRequest(...)`, `agentProcessRequest(...)`. The classifier is exposed as a public field (`orchestrator.classifier`) so its system prompt can be overridden after construction. ### Agents All agents extend `Agent` and require at minimum `{ name, description }` in their options. **`agent.id`** is derived automatically from `name`: non-alphanumeric stripped, spaces → hyphens, lowercased. "Tech Support" → `"tech-support"`. This is the key used for storage and classifier matching — it must be stable across restarts. | Class | Options type | Notes | |---|---|---| | `BedrockLLMAgent` | `BedrockLLMAgentOptions` | Bedrock Converse API; supports `streaming`, `modelId`, `inferenceConfig`, `guardrailConfig`, `reasoningConfig`, `retriever`, `toolConfig`, `customSystemPrompt`, `client`, `callbacks` | | `AnthropicAgent` | `AnthropicAgentOptions` | Direct Anthropic SDK; similar options shape | | `OpenAIAgent` | `OpenAIAgentOptions` | OpenAI Chat Completions | | `AmazonBedrockAgent` | `AmazonBedrockAgentOptions` | Amazon Bedrock Agents (pre-built agents, not Converse) | | `BedrockInlineAgent` | `BedrockInlineAgentOptions` | Bedrock inline agents | | `BedrockFlowsAgent` | `BedrockFlowsAgentOptions` | Bedrock Flows | | `LambdaAgent` | `LambdaAgentOptions` | Invokes a Lambda function as an agent | | `LexBotAgent` | `LexBotAgentOptions` | Amazon Lex V2 bot | | `ChainAgent` | `ChainAgentOptions` | Fixed pipeline; `agents: Agent[]`, `defaultOutput?` | | `SupervisorAgent` | `SupervisorAgentOptions` | Lead + team; `leadAgent` must be `BedrockLLMAgent` or `AnthropicAgent`; lead must have no `toolConfig` (SupervisorAgent manages tools) | | `GroundedAgent` | `GroundedAgentOptions` | 2-LLM anti-hallucination; `gatherer`, `presenter`, `tools`, `curator?`, `presenterPrompt?` | `AgentOptions` base fields: `name` (required), `description` (required), `saveChat?` (default `true`), `logger?`, `LOG_AGENT_DEBUG_TRACE?`. `BedrockLLMAgent` `toolConfig` shape: ```typescript toolConfig: { tool: AgentTools | Tool[], // AgentTools instance or raw Bedrock Tool array useToolHandler: (response: any, conversation: ConversationMessage[]) => any, toolMaxRecursions?: number, } ``` When using `MCPToolProvider`, pass it as `toolConfig.tool` and omit `useToolHandler` — the provider overrides `toolHandler` internally. ### GroundedAgent Two-LLM anti-hallucination pattern. The gatherer calls tools; the presenter receives only the curated facts (never raw tool output, never chat history from the gatherer): ```typescript import { GroundedAgent, DataBlockCurator, PerToolCurator, PresenterPrompt, BedrockLLMAgent, AgentTools, AgentTool, } from 'agent-squad'; const tools = new AgentTools([ new AgentTool({ name: 'get_price', description: '...', func: async ({ sku }) => fetchPrice(sku) }), ]); const gatherer = new BedrockLLMAgent({ name: 'Gatherer', description: '...', toolConfig: { tool: tools, useToolHandler: ... } }); const presenter = new BedrockLLMAgent({ name: 'Presenter', description: '...' }); const agent = new GroundedAgent({ name: 'Price Agent', description: 'Answers pricing questions grounded in live data', gatherer, presenter, tools, curator: new DataBlockCurator(), // default; or PerToolCurator for per-tool formatting presenterPrompt: PresenterPrompt.default(), // generic grounding prompt; or per-tool map }); ``` A no-tool turn (chit-chat) is answered by the gatherer directly, skipping the presenter. ### Classifiers | Class | Options type | Notes | |---|---|---| | `BedrockClassifier` | `BedrockClassifierOptions` | Default when no classifier is passed to `AgentSquad` | | `AnthropicClassifier` | `AnthropicClassifierOptions` | | | `OpenAIClassifier` | `OpenAIClassifierOptions` | | All classifiers support `setSystemPrompt(template?, variables?)` to override the routing prompt. Template variables use `{{VAR_NAME}}` syntax; `AGENT_DESCRIPTIONS` and `HISTORY` are always injected automatically. ### Storage | Class | Notes | |---|---| | `InMemoryChatStorage` | Default; non-persistent; fine for dev and tests | | `DynamoDbChatStorage` | Requires `@aws-sdk/client-dynamodb` and `@aws-sdk/lib-dynamodb` (hard deps) | | `SqlChatStorage` | Requires `@libsql/client` (hard dep); works with Turso or local libsql | | `SummarizingChatStorage` | Wraps any storage; compresses history via a user-supplied `ChatSummarizer` callable when `fetchChat` returns more than `triggerAt * 2` messages; cache-based save-back | Storage is keyed by `(userId, sessionId, agentId)`. `fetchAllChats(userId, sessionId)` is used by the classifier to get cross-agent history for context. ### Retrievers | Class | Options type | Notes | |---|---|---| | `AmazonKnowledgeBasesRetriever` | `AmazonKnowledgeBasesRetrieverOptions` | Amazon Bedrock Knowledge Bases | | `DakeraRetriever` | `DakeraRetrieverOptions` | Dakera memory server; optional peer dep `@dakera-ai/dakera` | `DakeraRetrieverOptions`: `namespace` (required), `apiKey?` (falls back to `DAKERA_API_KEY` env), `url?` (falls back to `DAKERA_URL` then `http://localhost:3000`), `topK?` (default 10), `filter?`. Attach to a `BedrockLLMAgent` via `retriever:` option. The agent calls `retriever.retrieveAndCombineResults(inputText)` to augment its system prompt. `DakeraRetriever.retrieveAndGenerate()` always throws — Dakera is retrieval-only. ### Tools: `AgentTools` and `AgentTool` ```typescript import { AgentTools, AgentTool } from 'agent-squad'; const myTools = new AgentTools([ new AgentTool({ name: 'search_web', description: 'Search the web for current information', properties: { query: { type: 'string', description: 'The search query' }, }, required: ['query'], func: async ({ query }) => webSearch(query), }), ]); ``` `AgentTool` constructor will auto-extract parameter names from `func` if `properties` is omitted — but this is fragile with minification. Always pass explicit `properties` and `required`. ### MCPToolProvider `MCPToolProvider` extends `AgentTools`. Always use the async factory — never `new MCPToolProvider(...)` directly — so that tool definitions are fetched before the agent makes its first API call: ```typescript import { MCPToolProvider } from 'agent-squad'; const provider = await MCPToolProvider.create([ { type: 'stdio', command: 'uvx', args: ['my-mcp-server'] }, { type: 'sse', url: 'http://localhost:3000/sse', headers: { Authorization: 'Bearer tok' } }, ]); const agent = new BedrockLLMAgent({ name: 'MCP Agent', description: 'Agent with MCP tools', toolConfig: { tool: provider }, }); // Clean up when done (closes stdio processes and SSE connections) await provider.disconnect(); ``` `MCPServerConfig.type` is `"stdio"` or `"sse"`. For `stdio`: `command` is required, `args?` and `env?` are optional. For `sse`: `url` is required, `headers?` is optional. `MCPToolProvider` overrides `toBedrockFormat()`, `toAnthropicFormat()`, and `toOpenAIFormat()` to pass MCP `inputSchema` through unchanged rather than re-serializing it. Requires `npm install @modelcontextprotocol/sdk`. The SDK is imported lazily via `await import()` inside `ensureConnected()` — installing agent-squad without the SDK is safe as long as you don't instantiate `MCPToolProvider`. ## Custom implementations Extend the abstract base class and pass your type where the built-in goes. | Seam | Base class | Method to implement | Source | |---|---|---|---| | Agent | `Agent` | `processRequest(inputText, userId, sessionId, chatHistory, additionalParams?)` returns `Promise>` | `typescript/src/agents/agent.ts` | | Classifier | `Classifier` | `processRequest(inputText, chatHistory)` returns `Promise` | `typescript/src/classifiers/classifier.ts` | | Storage | `ChatStorage` | `saveChatMessage`, `fetchChat`, `fetchAllChats` | `typescript/src/storage/chatStorage.ts` | | Retriever | `Retriever` | `retrieve`, `retrieveAndCombineResults`, `retrieveAndGenerate` | `typescript/src/retrievers/retriever.ts` | `ClassifierResult` shape: `{ selectedAgent: Agent | null, confidence: number }`. `Classifier` base class provides `setAgents`, `setHistory`, `setSystemPrompt`, and `getAgentById(agentId)` — use `getAgentById` in your `processRequest` to look up the selected agent from the classifier's registered map. ## Gotchas - **`agentId` is derived from `name`** at construction time: non-alphanumeric stripped, spaces replaced with `-`, lowercased. Changing an agent's `name` changes its `id`, which breaks chat history lookups in storage. Keep names stable across deployments. - **Streaming response is a Node.js Transform stream**, not an async generator. Check `response.streaming` before iterating. The transform accumulates the full response internally; `for await (const chunk of response.output)` works because `Transform` implements `AsyncIterable`. Do not call `response.output.read()` manually. - **`routeRequest` never throws**. Errors are returned as non-streaming `AgentResponse` with the error string in `output`. If you need to distinguish errors from real responses, check `response.metadata.errorType === 'classification_failed'` or inspect `metadata.agentId`. - **`MCPToolProvider.create(...)` must be awaited before the agent is used**. The constructor alone does not connect; calling `processRequest` before `create` resolves means tool definitions are empty and the agent will behave as if it has no tools. - **`BedrockClassifier` is the default**. If boto3/AWS credentials are not configured and you don't pass an explicit `classifier`, `AgentSquad` will construct a `BedrockClassifier` that will fail at runtime. Pass `classifier: new AnthropicClassifier(...)` or `new OpenAIClassifier(...)` if you're not on AWS. - **Optional peer deps use lazy import/require**. `MCPToolProvider` uses `await import(...)` inside `ensureConnected()`; `DakeraRetriever` uses `require(...)` inside the constructor. Neither adds a top-level import, so a missing peer dep is only discovered at instantiation time — not at module load. Throw the missing-dep error early, before user input arrives. - **`SupervisorAgent` restrictions**: `leadAgent` must be `BedrockLLMAgent` or `AnthropicAgent`; the lead agent must have no `toolConfig` set (SupervisorAgent wires its own tool loop). Pass additional native tools via `extraTools`. - **`saveChat` defaults to `true`**. Every agent persists both sides of each exchange after the turn completes. Set `saveChat: false` on agents that should not write to storage (e.g. a presenter inside a `GroundedAgent` that is managed externally). - **`additionalParams`** flows through `routeRequest` → `dispatchToAgent` → `agent.processRequest`. Use it to pass per-request context (tenant ID, request ID, feature flags) without touching agent options. The values end up in `response.metadata.additionalParams`. - **`AgentTools` auto-extracts parameter names from `func` via `.toString()`**. This breaks with minification and TypeScript arrow functions with destructured arguments. Always supply explicit `properties` and `required` arrays to `AgentTool`. - **`ThinkingResponse`**: when a `BedrockLLMAgent` is configured with `reasoningConfig`, the non-streaming path may return `response.thinking` (a string) alongside `response.output`. The streaming path does not surface thinking tokens separately. ## Go deeper - **Prose & recipes** — `docs/src/content/docs/` (run the site from `docs/` with `npm run dev`): `orchestrator/overview`, `agents/built-in/bedrock-llm-agent`, `agents/built-in/grounded-agent`, `classifiers/overview`, `storage/overview`, `retrievers/overview`, `tools/mcp`. - **Exact signatures** — `typescript/src/` (`orchestrator.ts`, `agents/`, `classifiers/`, `storage/`, `retrievers/`, `tools/mcpToolProvider.ts`, `utils/tool.ts`, `types/index.ts`). - **Tests** — `typescript/tests/` for usage patterns and mock strategies (virtual mocks for optional peer deps via `jest.mock(..., { virtual: true })`). - **Barrel** — `typescript/src/index.ts` is the definitive list of every public symbol.