| 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
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:
import { AgentSquad, BedrockLLMAgent, BedrockClassifier } from 'agent-squad';
const orchestrator = new AgentSquad({
classifier: new BedrockClassifier(),
});
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
);
if (response.streaming) {
for await (const chunk of response.output) {
process.stdout.write(chunk);
}
} else {
console.log(response.output);
}
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
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, , , , |
AgentOptions base fields: name (required), description (required), saveChat? (default
true), logger?, LOG_AGENT_DEBUG_TRACE?.
BedrockLLMAgent toolConfig shape:
toolConfig: {
tool: AgentTools | Tool[],
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):
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(),
: .(),
});
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
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:
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 },
});
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<ConversationMessage | AsyncIterable<any>> | typescript/src/agents/agent.ts |
| Classifier | Classifier | processRequest(inputText, chatHistory) returns Promise<ClassifierResult> | 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: must be or ;
the lead agent must have no set (SupervisorAgent wires its own tool loop). Pass
additional native tools via .
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.