/** * Sub-Agents Example — Multi-Perspective Analysis * * The coordinator receives a question from the user, then spawns three * PerspectiveAgent facets in parallel. Each facet independently calls the * LLM with its own role/persona and produces an analysis. The coordinator * waits for all three, then synthesizes them into a final response. * * Each PerspectiveAgent is a facet — a child DurableObject with its own * isolated SQLite. It persists its analysis history independently. The * coordinator can abort any slow facet without affecting the others. * * ┌─────────── CoordinatorAgent ──────────────────────────────────┐ * │ │ * │ User question ──▶ analyze() ──┬──▶ subAgent("technical") │ * │ ├──▶ subAgent("business") │ * │ └──▶ subAgent("skeptic") │ * │ │ * │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ * │ │ Technical │ │ Business │ │ Skeptic │ │ * │ │ Expert │ │ Analyst │ │ (Devil's │ │ * │ │ (facet) │ │ (facet) │ │ Advocate) │ │ * │ │ │ │ │ │ (facet) │ │ * │ │ own SQLite │ │ own SQLite │ │ own SQLite │ │ * │ │ own LLM call │ │ own LLM call │ │ own LLM call │ │ * │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ * │ │ │ │ │ * │ └────────────────┼────────────────┘ │ * │ ▼ │ * │ synthesize() │ * │ │ │ * │ ▼ │ * │ Final response │ * └────────────────────────────────────────────────────────────────┘ */ import { createWorkersAI } from "workers-ai-provider"; import { Agent, routeAgentRequest, callable } from "agents"; import { AIChatAgent } from "@cloudflare/ai-chat"; import { generateText, streamText, convertToModelMessages, tool, stepCountIs } from "ai"; import { z } from "zod"; // ───────────────────────────────────────────────────────────────────────────── // Types // ───────────────────────────────────────────────────────────────────────────── /** The three perspectives used for analysis. */ export const PERSPECTIVES = { technical: { name: "Technical Expert", icon: "gear", system: "You are a senior technical expert. Analyze the question from a purely " + "technical standpoint: feasibility, architecture, performance, scalability, " + "security implications. Be specific and cite concrete technical concerns. " + "Keep your response to 2-3 focused paragraphs." }, business: { name: "Business Analyst", icon: "chart", system: "You are a sharp business analyst. Analyze the question from a business " + "perspective: market impact, cost/benefit, competitive advantage, risk, " + "timeline, and ROI. Be pragmatic and numbers-oriented where possible. " + "Keep your response to 2-3 focused paragraphs." }, skeptic: { name: "Devil's Advocate", icon: "warning", system: "You are a constructive devil's advocate. Challenge the premise of the " + "question. What could go wrong? What are the hidden assumptions? What " + "alternatives haven't been considered? Be provocative but fair. " + "Keep your response to 2-3 focused paragraphs." } } as const; export type PerspectiveId = keyof typeof PERSPECTIVES; export type PerspectiveResult = { perspectiveId: PerspectiveId; name: string; analysis: string; timestamp: string; }; export type AnalysisRound = { id: string; question: string; perspectives: PerspectiveResult[]; synthesis: string | null; timestamp: string; }; export type SubagentState = { analyses: AnalysisRound[]; }; // ───────────────────────────────────────────────────────────────────────────── // PerspectiveAgent — sub-agent that independently calls the LLM // // Each instance has its own role (system prompt), its own SQLite for // persisting past analyses, and makes its own LLM calls. The coordinator // cannot see the sub-agent's internal state — it only gets back the // analysis text through the analyze() RPC method. // ───────────────────────────────────────────────────────────────────────────── export class PerspectiveAgent extends Agent { onStart() { this.sql` CREATE TABLE IF NOT EXISTS analyses ( id TEXT PRIMARY KEY, question TEXT NOT NULL, analysis TEXT NOT NULL, timestamp TEXT NOT NULL DEFAULT (datetime('now')) ) `; } /** * Analyze a question from this perspective. Calls the LLM independently * with this perspective's system prompt. Stores the result in the * sub-agent's own SQLite. * * The coordinator calls this on each sub-agent in parallel — three LLM * calls running concurrently, each in its own isolated context. */ async analyze(perspectiveId: string, question: string): Promise { const perspective = PERSPECTIVES[perspectiveId as PerspectiveId] ?? PERSPECTIVES.technical; const workersai = createWorkersAI({ binding: this.env.AI }); const result = await generateText({ model: workersai("@cf/moonshotai/kimi-k2.5", { sessionAffinity: this.sessionAffinity }), system: perspective.system, prompt: question }); const id = crypto.randomUUID(); this.sql` INSERT INTO analyses (id, question, analysis) VALUES (${id}, ${question}, ${result.text}) `; return result.text; } /** Return past analyses from this sub-agent's storage. */ getHistory(): { question: string; analysis: string; timestamp: string }[] { return this.sql<{ question: string; analysis: string; timestamp: string }>` SELECT question, analysis, timestamp FROM analyses ORDER BY timestamp DESC LIMIT 10 `; } } // ───────────────────────────────────────────────────────────────────────────── // CoordinatorAgent — orchestrates the sub-agents // ───────────────────────────────────────────────────────────────────────────── export class CoordinatorAgent extends AIChatAgent { initialState: SubagentState = { analyses: [] }; async onStart() { this._initTables(); this._syncState(); } // ─── Storage ───────────────────────────────────────────────────────── private _initTables() { this.sql` CREATE TABLE IF NOT EXISTS analysis_rounds ( id TEXT PRIMARY KEY, question TEXT NOT NULL, synthesis TEXT, timestamp TEXT NOT NULL DEFAULT (datetime('now')) ) `; this.sql` CREATE TABLE IF NOT EXISTS perspective_results ( id TEXT PRIMARY KEY, round_id TEXT NOT NULL, perspective_id TEXT NOT NULL, name TEXT NOT NULL, analysis TEXT NOT NULL, timestamp TEXT NOT NULL DEFAULT (datetime('now')) ) `; } private _syncState() { const rounds = this.sql<{ id: string; question: string; synthesis: string | null; timestamp: string; }>` SELECT id, question, synthesis, timestamp FROM analysis_rounds ORDER BY timestamp DESC LIMIT 10 `; const analyses: AnalysisRound[] = rounds.map((round) => { const perspectives = this.sql` SELECT perspective_id as perspectiveId, name, analysis, timestamp FROM perspective_results WHERE round_id = ${round.id} ORDER BY perspective_id `; return { ...round, perspectives }; }); this.setState({ analyses }); } // ─── Multi-perspective analysis ────────────────────────────────────── /** * Run all three perspective agents in parallel, then synthesize. * * This is the core pattern: fan out to facets, fan in the results. * Each facet makes its own independent LLM call with its own context. */ @callable() async analyzeQuestion(question: string): Promise { const roundId = crypto.randomUUID(); // Store the round this.sql` INSERT INTO analysis_rounds (id, question) VALUES (${roundId}, ${question}) `; this._syncState(); // Fan out: call all three sub-agents in parallel const perspectiveIds: PerspectiveId[] = [ "technical", "business", "skeptic" ]; const results = await Promise.all( perspectiveIds.map(async (pid) => { const agent = await this.subAgent(PerspectiveAgent, pid); const analysis = await agent.analyze(pid, question); const perspective = PERSPECTIVES[pid]; // Store each result in the coordinator's own storage const resultId = crypto.randomUUID(); this.sql` INSERT INTO perspective_results (id, round_id, perspective_id, name, analysis) VALUES (${resultId}, ${roundId}, ${pid}, ${perspective.name}, ${analysis}) `; this._syncState(); return { perspectiveId: pid, name: perspective.name, analysis, timestamp: new Date().toISOString() }; }) ); // Synthesize: ask the LLM to combine the three perspectives const workersai = createWorkersAI({ binding: this.env.AI }); const synthesisResult = await generateText({ model: workersai("@cf/moonshotai/kimi-k2.5", { sessionAffinity: this.sessionAffinity }), system: "You are a senior advisor synthesizing multiple perspectives into a " + "balanced, actionable recommendation. Be concise — 2-3 paragraphs max.", prompt: `Question: ${question}\n\n` + results.map((r) => `## ${r.name}\n${r.analysis}`).join("\n\n") + "\n\nSynthesize these three perspectives into a balanced recommendation." }); // Store synthesis this.sql` UPDATE analysis_rounds SET synthesis = ${synthesisResult.text} WHERE id = ${roundId} `; this._syncState(); return { id: roundId, question, perspectives: results, synthesis: synthesisResult.text, timestamp: new Date().toISOString() }; } // ─── Chat ──────────────────────────────────────────────────────────── async onChatMessage() { const workersai = createWorkersAI({ binding: this.env.AI }); const agent = this; const result = streamText({ model: workersai("@cf/moonshotai/kimi-k2.5", { sessionAffinity: this.sessionAffinity }), system: `You are a coordinator that manages three specialist sub-agents to analyze questions from multiple perspectives. When the user asks a question or presents a topic for analysis, use the analyzeFromAllPerspectives tool. This will: 1. Send the question to three independent sub-agents (Technical Expert, Business Analyst, Devil's Advocate) 2. Each sub-agent independently calls an LLM with its own specialized system prompt 3. All three run in parallel, each in its own isolated context 4. A synthesis combines all three perspectives After receiving the analysis, present a brief summary to the user highlighting key points from each perspective and the synthesis. For simple conversation (greetings, follow-up questions about results), respond directly without invoking the tool.`, messages: await convertToModelMessages(this.messages), tools: { analyzeFromAllPerspectives: tool({ description: "Analyze a question from three perspectives in parallel: " + "Technical Expert, Business Analyst, and Devil's Advocate. " + "Each perspective agent runs independently with its own context. " + "Returns all three analyses plus a synthesis.", inputSchema: z.object({ question: z.string().describe("The question or topic to analyze") }), execute: async ({ question }) => { const round = await agent.analyzeQuestion(question); return { question: round.question, perspectives: round.perspectives.map((p) => ({ role: p.name, analysis: p.analysis })), synthesis: round.synthesis }; } }) }, stopWhen: stepCountIs(3) }); return result.toUIMessageStreamResponse(); } } // ───────────────────────────────────────────────────────────────────────────── export default { async fetch(request: Request, env: Env) { return ( (await routeAgentRequest(request, env)) || new Response("Not found", { status: 404 }) ); } } satisfies ExportedHandler;