12 KiB
12 KiB
Agent Patterns
Advanced patterns for building sophisticated agents.
Tool Calling
Agents can expose tools that AI models can call:
import { Agent, Connection } from "agents";
import { z } from "zod";
interface Tool {
name: string;
description: string;
parameters: z.ZodSchema;
handler: (params: any) => Promise<string>;
}
export class ToolAgent extends Agent<Env, State> {
private tools: Map<string, Tool> = new Map();
async onStart() {
// Register tools
this.registerTool({
name: "get_weather",
description: "Get current weather for a city",
parameters: z.object({ city: z.string() }),
handler: async ({ city }) => {
const res = await fetch(`https://api.weather.com/${city}`);
return JSON.stringify(await res.json());
},
});
this.registerTool({
name: "search_database",
description: "Search the document database",
parameters: z.object({ query: z.string(), limit: z.number().default(10) }),
handler: async ({ query, limit }) => {
const results = await this.sql`
SELECT * FROM documents
WHERE content LIKE ${`%${query}%`}
LIMIT ${limit}
`;
return JSON.stringify(results);
},
});
}
private registerTool(tool: Tool) {
this.tools.set(tool.name, tool);
}
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "chat") {
await this.handleChatWithTools(connection, data.content);
}
}
private async handleChatWithTools(connection: Connection, userMessage: string) {
// Build tool descriptions for the AI
const toolDescriptions = Array.from(this.tools.values()).map((t) => ({
type: "function",
function: {
name: t.name,
description: t.description,
parameters: JSON.parse(JSON.stringify(t.parameters)),
},
}));
// First AI call - may request tool use
const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
{ role: "system", content: "You are a helpful assistant with access to tools." },
...this.state.messages,
{ role: "user", content: userMessage },
],
tools: toolDescriptions,
});
// Check if AI wants to use a tool
if (response.tool_calls) {
for (const toolCall of response.tool_calls) {
const tool = this.tools.get(toolCall.function.name);
if (tool) {
const params = JSON.parse(toolCall.function.arguments);
const result = await tool.handler(params);
// Send tool result back to AI
const finalResponse = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
...this.state.messages,
{ role: "user", content: userMessage },
{ role: "assistant", tool_calls: response.tool_calls },
{ role: "tool", tool_call_id: toolCall.id, content: result },
],
});
connection.send(JSON.stringify({
type: "response",
content: finalResponse.response,
toolUsed: toolCall.function.name,
}));
}
}
} else {
connection.send(JSON.stringify({
type: "response",
content: response.response,
}));
}
}
}
RAG (Retrieval Augmented Generation)
Combine Vectorize with Agents for knowledge-grounded responses:
interface Env {
AI: Ai;
VECTORIZE: VectorizeIndex;
}
export class RAGAgent extends Agent<Env, State> {
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "chat") {
// 1. Generate embedding for query
const embedding = await this.env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: data.content,
});
// 2. Search vector database
const results = await this.env.VECTORIZE.query(embedding.data[0], {
topK: 5,
returnMetadata: true,
});
// 3. Build context from results
const context = results.matches
.map((m) => m.metadata?.text || "")
.join("\n\n");
// 4. Generate response with context
const response = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
{
role: "system",
content: `Answer based on this context:\n\n${context}\n\nIf the context doesn't contain relevant information, say so.`,
},
{ role: "user", content: data.content },
],
});
// 5. Update state and respond
this.setState({
messages: [
...this.state.messages,
{ role: "user", content: data.content },
{ role: "assistant", content: response.response },
],
});
connection.send(JSON.stringify({
type: "response",
content: response.response,
sources: results.matches.map((m) => m.metadata?.source),
}));
}
}
// Ingest documents into vector store
async ingestDocument(doc: { id: string; text: string; source: string }) {
const embedding = await this.env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: doc.text,
});
await this.env.VECTORIZE.upsert([{
id: doc.id,
values: embedding.data[0],
metadata: { text: doc.text, source: doc.source },
}]);
}
}
Multi-Agent Orchestration
Coordinate multiple specialized agents:
interface Env {
RESEARCHER: DurableObjectNamespace;
WRITER: DurableObjectNamespace;
REVIEWER: DurableObjectNamespace;
}
export class OrchestratorAgent extends Agent<Env, State> {
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "create_article") {
connection.send(JSON.stringify({ type: "status", step: "researching" }));
// Step 1: Research agent gathers information
const researchResult = await this.callAgent(
this.env.RESEARCHER,
data.topic,
{ action: "research", topic: data.topic }
);
connection.send(JSON.stringify({ type: "status", step: "writing" }));
// Step 2: Writer agent creates draft
const draftResult = await this.callAgent(
this.env.WRITER,
data.topic,
{ action: "write", research: researchResult, topic: data.topic }
);
connection.send(JSON.stringify({ type: "status", step: "reviewing" }));
// Step 3: Reviewer agent improves draft
const finalResult = await this.callAgent(
this.env.REVIEWER,
data.topic,
{ action: "review", draft: draftResult }
);
connection.send(JSON.stringify({
type: "complete",
article: finalResult,
}));
}
}
private async callAgent(
namespace: DurableObjectNamespace,
id: string,
payload: any
): Promise<string> {
const agentId = namespace.idFromName(id);
const agent = namespace.get(agentId);
const response = await agent.fetch("http://agent/task", {
method: "POST",
body: JSON.stringify(payload),
});
return response.text();
}
}
Human-in-the-Loop
Pause agent execution for human approval:
interface State {
pendingApprovals: Array<{
id: string;
action: string;
data: any;
requestedAt: string;
}>;
}
export class ApprovalAgent extends Agent<Env, State> {
initialState: State = { pendingApprovals: [] };
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "request_action") {
// Action requires approval
if (this.requiresApproval(data.action)) {
const approvalId = crypto.randomUUID();
this.setState({
pendingApprovals: [
...this.state.pendingApprovals,
{
id: approvalId,
action: data.action,
data: data.payload,
requestedAt: new Date().toISOString(),
},
],
});
connection.send(JSON.stringify({
type: "approval_required",
approvalId,
action: data.action,
description: this.describeAction(data.action, data.payload),
}));
return;
}
// Execute immediately if no approval needed
await this.executeAction(connection, data.action, data.payload);
}
if (data.type === "approve") {
const approval = this.state.pendingApprovals.find(
(a) => a.id === data.approvalId
);
if (approval) {
// Remove from pending
this.setState({
pendingApprovals: this.state.pendingApprovals.filter(
(a) => a.id !== data.approvalId
),
});
// Execute the approved action
await this.executeAction(connection, approval.action, approval.data);
}
}
if (data.type === "reject") {
this.setState({
pendingApprovals: this.state.pendingApprovals.filter(
(a) => a.id !== data.approvalId
),
});
connection.send(JSON.stringify({
type: "action_rejected",
approvalId: data.approvalId,
}));
}
}
private requiresApproval(action: string): boolean {
const sensitiveActions = ["delete", "send_email", "make_payment", "publish"];
return sensitiveActions.includes(action);
}
private describeAction(action: string, data: any): string {
// Generate human-readable description
return `${action}: ${JSON.stringify(data)}`;
}
private async executeAction(connection: Connection, action: string, data: any) {
// Execute the action
const result = await this.performAction(action, data);
connection.send(JSON.stringify({
type: "action_completed",
action,
result,
}));
}
}
Streaming Responses
Stream AI responses in real-time:
export class StreamingAgent extends Agent<Env, State> {
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "chat") {
// Start streaming response
const stream = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [
{ role: "system", content: "You are a helpful assistant." },
...this.state.messages,
{ role: "user", content: data.content },
],
stream: true,
});
let fullResponse = "";
// Stream chunks to client
for await (const chunk of stream) {
if (chunk.response) {
fullResponse += chunk.response;
connection.send(JSON.stringify({
type: "stream",
content: chunk.response,
done: false,
}));
}
}
// Update state with complete response
this.setState({
messages: [
...this.state.messages,
{ role: "user", content: data.content },
{ role: "assistant", content: fullResponse },
],
});
// Signal completion
connection.send(JSON.stringify({
type: "stream",
content: "",
done: true,
}));
}
}
}
Connecting to MCP Servers
Agents can connect to MCP servers as clients:
export class MCPClientAgent extends Agent<Env, State> {
async onStart() {
// Connect to external MCP server
await this.addMcpServer(
"github",
"https://github-mcp.example.com/sse",
{ headers: { Authorization: `Bearer ${this.env.GITHUB_TOKEN}` } }
);
await this.addMcpServer(
"database",
"https://db-mcp.example.com/sse"
);
}
async onMessage(connection: Connection, message: string) {
const data = JSON.parse(message);
if (data.type === "use_tool") {
// Call tool on connected MCP server
const servers = await this.getMcpServers();
const server = servers.find((s) => s.name === data.server);
if (server) {
const result = await server.callTool(data.tool, data.params);
connection.send(JSON.stringify({ type: "tool_result", result }));
}
}
}
async onClose() {
// Cleanup MCP connections
await this.removeMcpServer("github");
await this.removeMcpServer("database");
}
}