Agentic AITypeScriptverifiedVerified
Tool Use Agent Pattern in TypeScript
Augment an LLM with callable external tools — APIs, code interpreters, databases — so it can take actions and retrieve real-time information beyond its training data.
How to Implement the Tool Use Agent Pattern in TypeScript
1Step 1: Define the tool and message interfaces
interface ToolDefinition {
name: string;
description: string;
parameters: Record<string, { type: string; description: string }>;
}
interface ToolResult {
toolName: string;
output: string;
isError: boolean;
}
interface AgentMessage {
role: "user" | "assistant" | "tool";
content: string;
toolName?: string;
}
type ToolHandler = (params: Record<string, unknown>) => Promise<string>;2Step 2: Create the ToolRegistry for registration and execution
class ToolRegistry {
private tools = new Map<string, { definition: ToolDefinition; handler: ToolHandler }>();
register(definition: ToolDefinition, handler: ToolHandler): void {
this.tools.set(definition.name, { definition, handler });
}
getDefinitions(): ToolDefinition[] {
return [...this.tools.values()].map(t => t.definition);
}
async execute(name: string, params: Record<string, unknown>): Promise<ToolResult> {
const tool = this.tools.get(name);
if (!tool) return { toolName: name, output: `Unknown tool: ${name}`, isError: true };
try {
const output = await tool.handler(params);
return { toolName: name, output, isError: false };
} catch (e) {
return { toolName: name, output: String(e), isError: true };
}
}
}3Step 3: Implement the tool-use loop
async function toolUseLoop(
userMessage: string,
registry: ToolRegistry,
llm: (messages: AgentMessage[], tools: ToolDefinition[]) => Promise<{ content: string; toolCall?: { name: string; params: Record<string, unknown> } }>
): Promise<string> {
const messages: AgentMessage[] = [{ role: "user", content: userMessage }];
for (let i = 0; i < 10; i++) {
const response = await llm(messages, registry.getDefinitions());
messages.push({ role: "assistant", content: response.content });
if (!response.toolCall) return response.content;
const result = await registry.execute(response.toolCall.name, response.toolCall.params);
messages.push({ role: "tool", content: result.output, toolName: result.toolName });
}
return "Max iterations reached";
}// ── Structured Tool-Use Agent with JSON Schema Validation ─────────
import { z } from "zod";
// ── Tool Schema Types ─────────────────────────────────────────────
const JsonSchemaProperty = z.object({
type: z.enum(["string", "number", "boolean", "object", "array"]),
description: z.string(),
enum: z.array(z.string()).optional(),
});
const ToolSchema = z.object({
name: z.string().regex(/^[a-z_][a-z0-9_]*$/),
description: z.string().min(10),
parameters: z.object({
type: z.literal("object"),
properties: z.record(JsonSchemaProperty),
required: z.array(z.string()),
}),
});
type ToolDefinition = z.infer<typeof ToolSchema>;
interface ToolCall {
id: string;
toolName: string;
params: Record<string, unknown>;
}
interface ToolResult {
callId: string;
toolName: string;
output: unknown;
isError: boolean;
durationMs: number;
}
interface LLMMessage {
role: "system" | "user" | "assistant" | "tool";
content: string;
toolCallId?: string;
}
type ToolHandler<P = Record<string, unknown>, R = unknown> =
(params: P) => Promise<R>;
class ToolUseAgent {
private tools = new Map<string, { definition: ToolDefinition; handler: ToolHandler }>();
private history: LLMMessage[] = [];
constructor(private systemPrompt: string) {
this.history.push({ role: "system", content: systemPrompt });
}
registerTool<P extends Record<string, unknown>, R>(
definition: ToolDefinition,
handler: ToolHandler<P, R>
): this {
const parsed = ToolSchema.parse(definition);
this.tools.set(parsed.name, { definition: parsed, handler: handler as ToolHandler });
return this;
}
async run(userMessage: string, signal?: AbortSignal): Promise<string> {
this.history.push({ role: "user", content: userMessage });
for (let i = 0; i < 15; i++) {
if (signal?.aborted) throw new Error("Agent aborted");
const response = await this.callLLM(this.history, [...this.tools.values()].map(t => t.definition));
if (!response.toolCall) {
this.history.push({ role: "assistant", content: response.content });
return response.content;
}
this.history.push({
role: "assistant",
content: `Using tool: ${response.toolCall.toolName}`,
toolCallId: response.toolCall.id,
});
const result = await this.executeTool(response.toolCall);
this.history.push({
role: "tool",
content: result.isError
? `Error: ${result.output}`
: JSON.stringify(result.output),
toolCallId: result.callId,
});
}
throw new Error("Exceeded max tool-use iterations");
}
private async executeTool(call: ToolCall): Promise<ToolResult> {
const entry = this.tools.get(call.toolName);
const start = Date.now();
if (!entry) {
return {
callId: call.id,
toolName: call.toolName,
output: `Tool "${call.toolName}" not registered`,
isError: true,
durationMs: 0,
};
}
// Validate required parameters
const { required } = entry.definition.parameters;
for (const key of required) {
if (!(key in call.params)) {
return {
callId: call.id,
toolName: call.toolName,
output: `Missing required parameter: "${key}"`,
isError: true,
durationMs: Date.now() - start,
};
}
}
try {
const output = await entry.handler(call.params);
return { callId: call.id, toolName: call.toolName, output, isError: false, durationMs: Date.now() - start };
} catch (e) {
return { callId: call.id, toolName: call.toolName, output: String(e), isError: true, durationMs: Date.now() - start };
}
}
private async callLLM(
_messages: LLMMessage[],
_tools: ToolDefinition[]
): Promise<{ content: string; toolCall?: ToolCall }> {
// Replace with actual LLM API call (Anthropic, OpenAI, etc.)
return { content: "Mock LLM response" };
}
}
export { ToolUseAgent, ToolSchema, type ToolDefinition, type ToolResult };Tool Use Agent Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Tool Use Agent Pattern in the Real World
“A lawyer (the LLM) in a courtroom knows the law but needs a paralegal team (the tools) to pull case files, run searches, and retrieve exhibits. The lawyer directs which file to fetch, the paralegal returns it, and the lawyer integrates that information into their argument — the lawyer's intelligence is amplified by the support staff's ability to reach into the real world.”