agent-squad TypeScript
July 14, 2026 · View on GitHub
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
Agentsubclass +AgentSquadwith no routing. Or skip the orchestrator entirely and callagent.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
Retrieverto any agent that supportsretriever?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(), // 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<string, any>
);
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
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:
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):
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
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 },
});
// 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<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
-
agentIdis derived fromnameat construction time: non-alphanumeric stripped, spaces replaced with-, lowercased. Changing an agent'snamechanges itsid, 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.streamingbefore iterating. The transform accumulates the full response internally;for await (const chunk of response.output)works becauseTransformimplementsAsyncIterable. Do not callresponse.output.read()manually. -
routeRequestnever throws. Errors are returned as non-streamingAgentResponsewith the error string inoutput. If you need to distinguish errors from real responses, checkresponse.metadata.errorType === 'classification_failed'or inspectmetadata.agentId. -
MCPToolProvider.create(...)must be awaited before the agent is used. The constructor alone does not connect; callingprocessRequestbeforecreateresolves means tool definitions are empty and the agent will behave as if it has no tools. -
BedrockClassifieris the default. If boto3/AWS credentials are not configured and you don't pass an explicitclassifier,AgentSquadwill construct aBedrockClassifierthat will fail at runtime. Passclassifier: new AnthropicClassifier(...)ornew OpenAIClassifier(...)if you're not on AWS. -
Optional peer deps use lazy import/require.
MCPToolProviderusesawait import(...)insideensureConnected();DakeraRetrieverusesrequire(...)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. -
SupervisorAgentrestrictions:leadAgentmust beBedrockLLMAgentorAnthropicAgent; the lead agent must have notoolConfigset (SupervisorAgent wires its own tool loop). Pass additional native tools viaextraTools. -
saveChatdefaults totrue. Every agent persists both sides of each exchange after the turn completes. SetsaveChat: falseon agents that should not write to storage (e.g. a presenter inside aGroundedAgentthat is managed externally). -
additionalParamsflows throughrouteRequest→dispatchToAgent→agent.processRequest. Use it to pass per-request context (tenant ID, request ID, feature flags) without touching agent options. The values end up inresponse.metadata.additionalParams. -
AgentToolsauto-extracts parameter names fromfuncvia.toString(). This breaks with minification and TypeScript arrow functions with destructured arguments. Always supply explicitpropertiesandrequiredarrays toAgentTool. -
ThinkingResponse: when aBedrockLLMAgentis configured withreasoningConfig, the non-streaming path may returnresponse.thinking(a string) alongsideresponse.output. The streaming path does not surface thinking tokens separately.
Go deeper
- Prose & recipes —
docs/src/content/docs/(run the site fromdocs/withnpm 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 viajest.mock(..., { virtual: true })). - Barrel —
typescript/src/index.tsis the definitive list of every public symbol.