Skip to main content

Capability Agents — IntentClassifier and Summarizer

Capability agents are specialised BaseAgent subclasses used internally by interceptors and history assembly. They are not channel-facing — register them without channels to use them as pure compute workers.

Contents


IntentClassifierAgent

Classifies whether a message is addressed to a specific agent. Used internally by createIntentClassifierInterceptor and createAddressCheckInterceptor to handle ambiguous mentions.

Types

import { IntentClassifierAgent, IntentClassifierInput, IntentClassification } from '@toolpack-sdk/agents';

type IntentClassification = 'direct' | 'indirect' | 'passive' | 'ignore';

interface IntentClassifierInput {
message: string; // the message to classify
agentName: string; // agent's display name
agentId: string; // agent's stable identifier
senderName: string; // who sent the message
channelName: string; // channel the message came from
isDirectMessage?: boolean; // true for DMs (lower bar for 'direct')
recentContext?: Array<{ // last few turns for context
sender: string;
content: string;
}>;
includeExamples?: boolean; // include few-shot examples in the prompt
}

Classification meanings

ClassificationMeaning
'direct'The agent is the explicit intended recipient
'indirect'The agent is mentioned but not the primary target
'passive'The agent is referenced but not being communicated with
'ignore'The message is clearly not meant for this agent

Usage

The classifier is typically invoked automatically by interceptors. For manual use:

const classifier = new IntentClassifierAgent({ apiKey: process.env.ANTHROPIC_API_KEY! });
await classifier._ensureToolpack();

const result = await classifier.invokeAgent({
data: {
message: 'Hey @support can you help me with my order?',
agentName: 'support-agent',
agentId: 'support-agent',
senderName: 'Alice',
channelName: 'general',
isDirectMessage: false,
recentContext: [{ sender: 'Bob', content: 'Good morning everyone' }],
} satisfies IntentClassifierInput,
conversationId: 'classify-001',
});

// result.output is 'direct' | 'indirect' | 'passive' | 'ignore'
const classification = result.output as IntentClassification;

SummarizerAgent

Compresses conversation history into a compact summary. Used by assemblePrompt() when the conversation exceeds rollingSummaryThreshold turns.

Types

import { SummarizerAgent, SummarizerInput, SummarizerOutput, HistoryTurn } from '@toolpack-sdk/agents';

interface HistoryTurn {
id: string;
participant: Participant;
content: string;
timestamp: string;
metadata?: {
isToolCall?: boolean;
toolName?: string;
toolResult?: unknown;
};
}

interface SummarizerInput {
turns: HistoryTurn[]; // turns to summarise
agentName: string; // agent's name (for pronoun resolution)
agentId: string; // agent's identifier
maxTokens?: number; // target summary length (default: 500 tokens)
extractDecisions?: boolean; // include key decisions in output
}

interface SummarizerOutput {
summary: string; // compressed narrative
turnsSummarized: number; // how many turns were compressed
hasDecisions: boolean; // whether decisions were extracted
estimatedTokens: number; // approximate token count of summary
}

Usage

The summarizer is typically invoked automatically by assemblePrompt(). For manual use:

import { SummarizerAgent, SummarizerInput } from '@toolpack-sdk/agents';

const summarizer = new SummarizerAgent({ apiKey: process.env.ANTHROPIC_API_KEY! });
await summarizer._ensureToolpack();

const result = await summarizer.invokeAgent({
data: {
turns: olderTurns, // HistoryTurn[] to compress
agentName: 'support-agent',
agentId: 'support-agent',
maxTokens: 500,
extractDecisions: true,
} satisfies SummarizerInput,
conversationId: 'summarize-001',
});

const output = JSON.parse(result.output) as SummarizerOutput;
console.log(output.summary);
console.log(`Compressed ${output.turnsSummarized} turns`);

Wiring into assemblePrompt()

import { assemblePrompt, SummarizerAgent } from '@toolpack-sdk/agents';

// Create summarizer once
const summarizer = new SummarizerAgent({ apiKey: process.env.ANTHROPIC_API_KEY! });
await summarizer._ensureToolpack();

// In your custom history loading logic
const assembled = await assemblePrompt(
store,
conversationId,
'my-agent',
'my-agent',
{
rollingSummaryThreshold: 30, // compress when turns > 30
tokenBudget: 3000,
},
summarizer, // ← pass the summarizer here
);

assemblePrompt() calls SummarizerAgent automatically when the history slice exceeds rollingSummaryThreshold. The resulting summary is inserted as a system message before the recent turns in the assembled context.


Using capabilities as standalone agents

Register capability agents without channels. They operate purely as compute workers:

const classifier = new IntentClassifierAgent({ apiKey: process.env.ANTHROPIC_API_KEY! });
const summarizer = new SummarizerAgent({ apiKey: process.env.ANTHROPIC_API_KEY! });

const registry = new AgentRegistry([
myMainAgent,
classifier, // no channels — pure compute worker
summarizer, // no channels — pure compute worker
]);

await registry.start();

// Main agent can delegate to them
// (Usually this happens via interceptors, not direct delegation)

Because they extend BaseAgent, they get conversation history, lifecycle hooks, and events — but since they have no channels, they can only be invoked via delegation or the registry.


IntentClassifier vs AddressCheck interceptors

FeaturecreateAddressCheckInterceptorcreateIntentClassifierInterceptor
MethodPattern matching (regex, heuristics)LLM call
SpeedFast (no API call)Slower (API call)
Best forClear @-mentions, DMsAmbiguous natural language
UsageFirst-pass filterDisambiguation of ambiguous cases

Recommended: chain createAddressCheckInterceptor (cheap, pattern-based) immediately before createIntentClassifierInterceptor (LLM-based). The intent classifier reads _addressCheck from context and only makes an LLM call for 'ambiguous'/'indirect' cases:

agent.interceptors = [
createAddressCheckInterceptor({
agentName: agent.name,
getMessageText: (input) => input.message ?? '',
}),
createIntentClassifierInterceptor({
agentName: agent.name,
agentId: agent.name,
getMessageText: (input) => input.message ?? '',
getSenderName: (input) => input.participant?.displayName ?? 'Unknown',
getChannelName: (input) => input.context?.channelName as string ?? 'general',
}),
];