# TPMJS Planner: Building a Multi-Tool Agent System A tutorial on implementing a hierarchical planning agent that dynamically loads tools from the TPMJS registry. --- ## System Overview ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ USER QUERY │ │ "Scrape competitor prices, │ │ analyze trends, make report" │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ PLAN GENERATOR │ │ │ │ │ │ │ │ Query ──► Tool Search ──► Generate Y Plans ──► Infer Skills │ │ │ │ │ │ │ └──────────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────┼──────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ Plan A │ │ Plan B │ │ Plan C │ │ │ │12 steps│ │ 6 steps│ │ 8 steps│ │ │ └────┬───┘ └────────┘ └────────┘ │ │ │ │ │ ▼ (user selects or auto-select) │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ CASCADING EXECUTOR │ │ │ │ │ │ │ │ Load Context ──► Execute Step ──► Update State ──► Next Step │ │ │ │ ▲ │ │ │ │ │ └────────────────────────────────────┘ │ │ │ └──────────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ FINAL OUTPUT │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Part 1: Core Types ```typescript // ============================================================================ // types/planner.ts // ============================================================================ interface Tool { id: string; packageName: string; name: string; description: string; parameters: Parameter[]; returns: ReturnType; category: string; qualityScore: number; } interface ExecutionPlan { id: string; steps: PlanStep[]; estimatedCost: number; confidence: number; skills: Skill[]; // Inferred from tools } interface PlanStep { id: string; tool: Tool; purpose: string; input: StepInput; dependsOn: string[]; // Step IDs fallbacks: Tool[]; // Alternative tools } interface Skill { domain: string; // "seo", "scraping", "data-analysis" context: string; // Relevant docs/knowledge tools: Tool[]; // Tools in this domain } interface ExecutionContext { query: string; variables: Map; completed: Set; errors: Error[]; } ``` --- ## Part 2: Tool Discovery ```typescript // ============================================================================ // lib/tool-discovery.ts // ============================================================================ /** * Search registry for tools matching a capability * * ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ * │ Query │────►│ Registry │────►│ Ranked │ * │ "scrape" │ │ Search │ │ Tools │ * └─────────────┘ └─────────────┘ └─────────────┘ */ async function discoverTools( capability: string, limit: number = 10 ): Promise { // 1. Search by keyword const keywordResults = await fetch( `/api/tools?search=${encodeURIComponent(capability)}&limit=${limit}` ).then(r => r.json()); // 2. Search by category const category = inferCategory(capability); const categoryResults = await fetch( `/api/tools?category=${category}&limit=${limit}` ).then(r => r.json()); // 3. Merge and rank const merged = mergeAndDedupe(keywordResults.data, categoryResults.data); return rankTools(merged, capability); } /** * Rank tools by relevance + quality */ function rankTools(tools: Tool[], capability: string): Tool[] { return tools .map(tool => ({ tool, score: textSimilarity(tool.description, capability) * 0.4 + tool.qualityScore * 0.3 + (tool.healthStatus === 'HEALTHY' ? 0.3 : 0) })) .sort((a, b) => b.score - a.score) .map(t => t.tool); } /** * Category inference from natural language * * "scrape website" ──► "web-scraping" * "analyze data" ──► "data-analysis" * "generate pdf" ──► "file-generation" */ function inferCategory(text: string): string { const patterns = [ { pattern: /scrape|crawl|fetch.*web/i, category: 'web-scraping' }, { pattern: /analyz|statistic|trend/i, category: 'data-analysis' }, { pattern: /pdf|excel|csv|export/i, category: 'file-generation' }, { pattern: /image|photo|picture/i, category: 'image-processing' }, { pattern: /email|notify|alert/i, category: 'communication' }, ]; for (const { pattern, category } of patterns) { if (pattern.test(text)) return category; } return 'general'; } ``` --- ## Part 3: Plan Generation ```typescript // ============================================================================ // lib/plan-generator.ts // ============================================================================ /** * Generate Y alternative plans for a query * * ┌─────────────┐ * │ Query │ * └──────┬──────┘ * │ * ┌──────────┼──────────┐ * ▼ ▼ ▼ * ┌────────┐ ┌────────┐ ┌────────┐ * │ Plan A │ │ Plan B │ │ Plan C │ * │Thorough│ │ Quick │ │Balanced│ * └────────┘ └────────┘ └────────┘ */ async function generatePlans( query: string, numPlans: number = 3 ): Promise { // 1. Analyze query const analysis = await analyzeQuery(query); // 2. Discover relevant tools const tools = await discoverToolsForAnalysis(analysis); // 3. Generate plan variants const strategies: PlanStrategy[] = [ { name: 'thorough', maxSteps: 15, preferQuality: true }, { name: 'quick', maxSteps: 5, preferSpeed: true }, { name: 'balanced', maxSteps: 10, balanced: true }, ]; const plans = await Promise.all( strategies.slice(0, numPlans).map(strategy => generateSinglePlan(analysis, tools, strategy) ) ); // 4. Infer skills for each plan return plans.map(plan => ({ ...plan, skills: inferSkills(plan.steps.map(s => s.tool)) })); } /** * Query analysis extracts intent and entities */ async function analyzeQuery(query: string): Promise { // Use LLM to extract structured info const response = await generateText({ model: openai('gpt-4-turbo'), prompt: `Analyze this query and extract: - intent (what user wants to accomplish) - entities (URLs, names, values mentioned) - required_capabilities (list of needed operations) - output_format (what format user expects) Query: "${query}" Return JSON.` }); return JSON.parse(response.text); } /** * Generate a single plan with given strategy */ async function generateSinglePlan( analysis: QueryAnalysis, tools: Tool[], strategy: PlanStrategy ): Promise { const steps: PlanStep[] = []; const usedTools = new Set(); for (const capability of analysis.required_capabilities) { // Find best tool for this capability const candidates = tools.filter(t => matchesCapability(t, capability) && !usedTools.has(t.id) ); if (candidates.length === 0) continue; const tool = strategy.preferQuality ? candidates.sort((a, b) => b.qualityScore - a.qualityScore)[0] : candidates[0]; usedTools.add(tool.id); steps.push({ id: `step_${steps.length + 1}`, tool, purpose: capability, input: inferInput(tool, analysis, steps), dependsOn: inferDependencies(tool, steps), fallbacks: candidates.slice(1, 3) // Keep alternatives }); if (steps.length >= strategy.maxSteps) break; } return { id: generateId(), steps, estimatedCost: estimateCost(steps), confidence: calculateConfidence(steps), skills: [] // Filled in later }; } ``` --- ## Part 4: Skill Inference ```typescript // ============================================================================ // lib/skill-inference.ts // ============================================================================ /** * Infer skills (contextual knowledge) from tools in a plan * * Tools in Plan Inferred Skills * ───────────── ─────────────── * ┌─────────────────┐ ┌─────────────────┐ * │ web-scraper │──┐ │ WEB SCRAPING │ * │ html-parser │──┼──────────►│ • Rate limiting │ * │ url-validator │──┘ │ • Robots.txt │ * └─────────────────┘ │ • Selectors │ * └─────────────────┘ * ┌─────────────────┐ ┌─────────────────┐ * │ price-extractor │──┐ │ DATA ANALYSIS │ * │ trend-analyzer │──┼──────────►│ • Normalization │ * │ stats-calculator│──┘ │ • Outliers │ * └─────────────────┘ └─────────────────┘ */ function inferSkills(tools: Tool[]): Skill[] { // Group tools by category const byCategory = groupBy(tools, t => t.category); const skills: Skill[] = []; for (const [category, categoryTools] of Object.entries(byCategory)) { const skillContext = SKILL_CONTEXTS[category]; if (skillContext) { skills.push({ domain: category, context: skillContext, tools: categoryTools }); } } return skills; } /** * Skill context library - domain knowledge for each category */ const SKILL_CONTEXTS: Record = { 'web-scraping': ` ## Web Scraping Best Practices - Always check robots.txt before scraping - Implement rate limiting (1 req/sec default) - Handle pagination with cursor or offset - Use CSS selectors over XPath when possible - Handle JavaScript-rendered content with headless browser `, 'data-analysis': ` ## Data Analysis Guidelines - Normalize numerical data before comparison - Handle missing values: impute or exclude - Identify outliers using IQR or z-score - Use appropriate statistical tests - Visualize distributions before drawing conclusions `, 'file-generation': ` ## File Generation Standards - PDF: Use A4/Letter, embed fonts, compress images - Excel: Use proper data types, add headers, format numbers - CSV: Use UTF-8, escape special characters, consistent delimiters `, // ... more domains }; ``` --- ## Part 5: Cascading Executor ```typescript // ============================================================================ // lib/cascading-executor.ts // ============================================================================ /** * Execute a plan with cascading context loading * * Step 1 Step 2 Step 3 * ────── ────── ────── * ┌────────────┐ ┌────────────┐ ┌────────────┐ * │ Load: │ │ Load: │ │ Load: │ * │ • Tool A │ │ • Tool B │ │ • Tool C │ * │ • Skill X │ ───► │ • Skill X │ ───► │ • Skill Y │ * │ • Next: B │ │ • Next: C │ │ • Next: D │ * └────────────┘ └────────────┘ └────────────┘ * ~800 tok ~900 tok ~1100 tok * * vs Loading Everything: ~15000 tokens */ async function* executeWithCascading( plan: ExecutionPlan, context: ExecutionContext ): AsyncGenerator { for (let i = 0; i < plan.steps.length; i++) { const step = plan.steps[i]; const nextSteps = plan.steps.slice(i + 1, i + 3); // Preload next 2 yield { type: 'step_start', stepId: step.id }; // 1. Build minimal context for this step const stepContext = buildStepContext(step, nextSteps, plan.skills, context); yield { type: 'context_loaded', tokens: estimateTokens(stepContext) }; // 2. Resolve input values const input = resolveInput(step.input, context); // 3. Execute with fallbacks const result = await executeWithFallbacks(step, input, stepContext); // 4. Store result context.variables.set(step.id, result); context.completed.add(step.id); yield { type: 'step_complete', stepId: step.id, result: summarize(result) }; } yield { type: 'plan_complete', output: context.variables }; } /** * Build minimal context for a single step */ function buildStepContext( current: PlanStep, upcoming: PlanStep[], skills: Skill[], context: ExecutionContext ): string { const parts: string[] = []; // 1. Current tool description parts.push(`## Current Tool: ${current.tool.name}`); parts.push(current.tool.description); parts.push(formatParameters(current.tool.parameters)); // 2. Relevant skill context (only for this tool's domain) const relevantSkill = skills.find(s => s.tools.some(t => t.id === current.tool.id) ); if (relevantSkill) { parts.push(`## Domain Knowledge`); parts.push(relevantSkill.context); } // 3. Upcoming tools (just names, for continuity) if (upcoming.length > 0) { parts.push(`## Coming Next`); parts.push(upcoming.map(s => `- ${s.tool.name}: ${s.purpose}`).join('\n')); } // 4. Relevant prior results (summarized) for (const depId of current.dependsOn) { const prior = context.variables.get(depId); if (prior) { parts.push(`## Input from ${depId}`); parts.push(summarize(prior, 500)); // Max 500 chars } } return parts.join('\n\n'); } /** * Execute step with automatic fallback on failure * * ┌──────────┐ ┌──────────┐ ┌──────────┐ * │ Primary │──X──│ Fallback │──X──│ Fallback │──► Error * │ Tool │ │ #1 │ │ #2 │ * └──────────┘ └──────────┘ └──────────┘ * │ │ │ * ▼ ▼ ▼ * Result Result Result */ async function executeWithFallbacks( step: PlanStep, input: any, context: string ): Promise { const tools = [step.tool, ...step.fallbacks]; for (const tool of tools) { try { return await executeTool(tool, input, context); } catch (error) { console.log(`Tool ${tool.name} failed, trying fallback...`); } } throw new Error(`All tools failed for step ${step.id}`); } /** * Execute a single tool via the registry */ async function executeTool( tool: Tool, input: any, context: string ): Promise { const response = await fetch( `/api/tools/execute/${tool.packageName}/${tool.name}`, { method: 'POST', body: JSON.stringify({ input, context }), } ); if (!response.ok) { throw new Error(`Tool execution failed: ${response.statusText}`); } return response.json(); } ``` --- ## Part 6: Pathway Learning ```typescript // ============================================================================ // lib/pathway-learning.ts // ============================================================================ /** * Track and learn from pathway execution * * ┌─────────────────────────────────────────────────────────┐ * │ PATHWAY STORE │ * │ │ * │ Query Pattern Path Success Weight │ * │ ───────────── ──── ─────── ────── │ * │ "scrape.*price" A→B→D→E 847/892 0.95 │ * │ "scrape.*price" A→B→C→D→E 38/52 0.73 │ * │ "analyze.*trend" X→Y→Z 412/445 0.93 │ * │ │ * └─────────────────────────────────────────────────────────┘ */ interface PathwayRecord { queryPattern: string; steps: string[]; // Tool IDs in order successes: number; failures: number; lastUsed: Date; } class PathwayLearner { private records: Map = new Map(); /** * Record outcome of a pathway execution */ recordOutcome( query: string, steps: string[], success: boolean ): void { const pattern = extractPattern(query); const pathKey = steps.join('→'); let records = this.records.get(pattern) || []; let record = records.find(r => r.steps.join('→') === pathKey); if (!record) { record = { queryPattern: pattern, steps, successes: 0, failures: 0, lastUsed: new Date() }; records.push(record); } if (success) record.successes++; else record.failures++; record.lastUsed = new Date(); this.records.set(pattern, records); } /** * Get recommended pathway using Thompson Sampling * * Path A: Beta(91, 11) ──► Sample: 0.88 * Path B: Beta(5, 2) ──► Sample: 0.91 ◄── Winner (uncertain but sampled high) */ recommendPathway(query: string): string[] | null { const pattern = extractPattern(query); const records = this.records.get(pattern); if (!records || records.length === 0) return null; // Thompson Sampling: sample from Beta distribution for each path let bestPath: string[] | null = null; let bestSample = -1; for (const record of records) { // Beta(successes + 1, failures + 1) const sample = sampleBeta( record.successes + 1, record.failures + 1 ); // Apply recency decay const daysSinceUse = daysBetween(record.lastUsed, new Date()); const recencyFactor = Math.pow(0.99, daysSinceUse); const adjustedSample = sample * recencyFactor; if (adjustedSample > bestSample) { bestSample = adjustedSample; bestPath = record.steps; } } return bestPath; } /** * Get K-factor (convergence toward determinism) * * K → 0.0: No dominant path, explore freely * K → 1.0: Strong dominant path, nearly deterministic */ getKFactor(query: string): number { const pattern = extractPattern(query); const records = this.records.get(pattern); if (!records || records.length === 0) return 0; const total = records.reduce((sum, r) => sum + r.successes + r.failures, 0); if (total < 10) return 0; // Not enough data const topPath = records.sort((a, b) => (b.successes / (b.successes + b.failures)) - (a.successes / (a.successes + a.failures)) )[0]; const topUsage = (topPath.successes + topPath.failures) / total; const topSuccess = topPath.successes / (topPath.successes + topPath.failures); return topUsage * topSuccess; } } /** * Sample from Beta distribution */ function sampleBeta(alpha: number, beta: number): number { // Simplified: use gamma sampling const x = sampleGamma(alpha); const y = sampleGamma(beta); return x / (x + y); } ``` --- ## Part 7: The Main Orchestrator ```typescript // ============================================================================ // lib/planner-orchestrator.ts // ============================================================================ /** * Main entry point: Query → Plans → Execution → Result * * ┌─────────────────────────────────────────────────────────────────────┐ * │ │ * │ 1. QUERY 2. PLANS 3. SELECT 4. EXECUTE │ * │ ─────── ─────── ──────── ───────── │ * │ │ * │ "analyze ┌────────┐ │ * │ competitor │ Plan A │───┐ User picks ┌──────────┐ │ * │ prices" ───►│ Plan B │───┼───► or auto ───► │ Cascade │───► │ * │ │ Plan C │───┘ select │ Execute │ │ * │ └────────┘ └──────────┘ │ * │ │ * └─────────────────────────────────────────────────────────────────────┘ */ class PlannerOrchestrator { private learner = new PathwayLearner(); async processQuery( query: string, options: OrchestratorOptions = {} ): AsyncGenerator { const { numPlans = 3, autoSelect = false } = options; // ───────────────────────────────────────────────────────────────── // Step 1: Check for learned pathway // ───────────────────────────────────────────────────────────────── const kFactor = this.learner.getKFactor(query); yield { type: 'k_factor', value: kFactor }; if (kFactor > 0.9) { // Near-deterministic: use learned path directly const learnedPath = this.learner.recommendPathway(query); if (learnedPath) { yield { type: 'using_learned_path', path: learnedPath }; const plan = await this.pathToExecutionPlan(learnedPath, query); yield* this.executePlan(plan, query); return; } } // ───────────────────────────────────────────────────────────────── // Step 2: Generate Y plans // ───────────────────────────────────────────────────────────────── yield { type: 'generating_plans' }; const plans = await generatePlans(query, numPlans); yield { type: 'plans_ready', plans: plans.map(p => ({ id: p.id, steps: p.steps.length, cost: p.estimatedCost, confidence: p.confidence, skills: p.skills.map(s => s.domain) })) }; // ───────────────────────────────────────────────────────────────── // Step 3: Select plan // ───────────────────────────────────────────────────────────────── let selectedPlan: ExecutionPlan; if (autoSelect) { // Auto-select: use Thompson Sampling across plans selectedPlan = this.selectPlanThompson(plans); yield { type: 'auto_selected', planId: selectedPlan.id }; } else { // Wait for user selection yield { type: 'awaiting_selection' }; const selection = await this.waitForUserSelection(plans); selectedPlan = plans.find(p => p.id === selection)!; } // ───────────────────────────────────────────────────────────────── // Step 4: Execute with cascading context // ───────────────────────────────────────────────────────────────── yield* this.executePlan(selectedPlan, query); } private async* executePlan( plan: ExecutionPlan, query: string ): AsyncGenerator { yield { type: 'execution_start', totalSteps: plan.steps.length, skills: plan.skills.map(s => s.domain) }; const context: ExecutionContext = { query, variables: new Map(), completed: new Set(), errors: [] }; let success = true; try { for await (const event of executeWithCascading(plan, context)) { yield { type: 'execution_event', event }; } } catch (error) { success = false; yield { type: 'execution_error', error }; } // Record outcome for learning const pathSteps = plan.steps.map(s => s.tool.id); this.learner.recordOutcome(query, pathSteps, success); yield { type: 'execution_complete', success, output: Object.fromEntries(context.variables) }; } /** * Thompson Sampling for plan selection */ private selectPlanThompson(plans: ExecutionPlan[]): ExecutionPlan { let best = plans[0]; let bestScore = -1; for (const plan of plans) { // Sample based on confidence + randomness const sample = sampleBeta( plan.confidence * 100, (1 - plan.confidence) * 100 ); if (sample > bestScore) { bestScore = sample; best = plan; } } return best; } } ``` --- ## Part 8: React Integration ```typescript // ============================================================================ // components/PlannerPlayground.tsx // ============================================================================ /** * UI for the planner system * * ┌─────────────────────────────────────────────────────────────────┐ * │ Query: [_______________________________________] [Generate] │ * ├─────────────────────────────────────────────────────────────────┤ * │ │ * │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ * │ │ ◉ Plan A │ │ ○ Plan B │ │ ○ Plan C │ │ * │ │ 12 steps │ │ 6 steps │ │ 8 steps │ │ * │ │ $0.45 │ │ $0.15 │ │ $0.28 │ │ * │ │ 94% conf │ │ 87% conf │ │ 91% conf │ │ * │ └─────────────┘ └─────────────┘ └─────────────┘ │ * │ │ * │ Skills: [SEO] [Scraping] [Data Analysis] │ * │ │ * │ [Execute Selected Plan] │ * │ │ * ├─────────────────────────────────────────────────────────────────┤ * │ Execution Progress: │ * │ ✓ Step 1: Fetch webpage (823ms) │ * │ ✓ Step 2: Parse HTML (234ms) │ * │ ► Step 3: Extract prices... [████████░░] 80% │ * │ ○ Step 4: Analyze trends │ * │ ○ Step 5: Generate report │ * │ │ * │ Context: 1,240 tokens (vs 12,400 if loaded all) │ * └─────────────────────────────────────────────────────────────────┘ */ function PlannerPlayground() { const [query, setQuery] = useState(''); const [plans, setPlans] = useState([]); const [selectedPlan, setSelectedPlan] = useState(null); const [execution, setExecution] = useState(null); const [kFactor, setKFactor] = useState(0); const orchestrator = useRef(new PlannerOrchestrator()); async function handleGenerate() { setPlans([]); setExecution(null); for await (const event of orchestrator.current.processQuery(query)) { switch (event.type) { case 'k_factor': setKFactor(event.value); break; case 'plans_ready': setPlans(event.plans); break; case 'execution_start': setExecution({ status: 'running', totalSteps: event.totalSteps, currentStep: 0, skills: event.skills, events: [] }); break; case 'execution_event': setExecution(prev => ({ ...prev!, events: [...prev!.events, event.event], currentStep: event.event.type === 'step_complete' ? prev!.currentStep + 1 : prev!.currentStep })); break; case 'execution_complete': setExecution(prev => ({ ...prev!, status: event.success ? 'complete' : 'error', output: event.output })); break; } } } return (
{/* Query Input */} {/* K-Factor Indicator */} {kFactor > 0 && ( )} {/* Plan Selection */} {plans.length > 0 && ( )} {/* Execution Progress */} {execution && ( )}
); } ``` --- ## Part 9: API Routes ```typescript // ============================================================================ // app/api/planner/generate/route.ts // ============================================================================ export async function POST(req: Request) { const { query, numPlans } = await req.json(); const plans = await generatePlans(query, numPlans); return Response.json({ success: true, data: plans.map(p => ({ id: p.id, steps: p.steps.map(s => ({ id: s.id, tool: s.tool.name, purpose: s.purpose })), estimatedCost: p.estimatedCost, confidence: p.confidence, skills: p.skills.map(s => s.domain) })) }); } // ============================================================================ // app/api/planner/execute/route.ts // ============================================================================ export async function POST(req: Request) { const { planId, query } = await req.json(); // Get full plan from cache/DB const plan = await getPlan(planId); // Create SSE stream const stream = new ReadableStream({ async start(controller) { const encoder = new TextEncoder(); const context: ExecutionContext = { query, variables: new Map(), completed: new Set(), errors: [] }; for await (const event of executeWithCascading(plan, context)) { controller.enqueue( encoder.encode(`data: ${JSON.stringify(event)}\n\n`) ); } controller.close(); } }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' } }); } ``` --- ## Summary: The Complete Flow ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ TPMJS PLANNER ARCHITECTURE │ │ │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ USER QUERY │ │ │ │ "Scrape competitor prices and analyze" │ │ │ └─────────────────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ 1. CHECK LEARNED PATHS │ │ │ │ │ │ │ │ K-factor = 0.85 ──► Strong pattern exists │ │ │ │ K-factor = 0.30 ──► Generate new plans │ │ │ └─────────────────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ 2. DISCOVER TOOLS │ │ │ │ │ │ │ │ Query Registry ──► Rank by relevance ──► Top 20 tools │ │ │ └─────────────────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ 3. GENERATE Y PLANS │ │ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ Plan A │ │ Plan B │ │ Plan C │ │ │ │ │ │Thorough │ │ Quick │ │Balanced │ │ │ │ │ │12 steps │ │ 5 steps │ │ 8 steps │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ └─────────────────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ 4. INFER SKILLS │ │ │ │ │ │ │ │ Tools ──► Cluster by domain ──► Load domain context │ │ │ │ │ │ │ │ [web-scraping: robots.txt, selectors, rate limits] │ │ │ │ [data-analysis: normalization, outliers, trends] │ │ │ └─────────────────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ 5. USER SELECTS (or auto) │ │ │ │ │ │ │ │ Thompson Sampling if auto ──► Pick plan with highest sample │ │ │ └─────────────────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ 6. CASCADE EXECUTE │ │ │ │ │ │ │ │ For each step: │ │ │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ │ │ • Load current tool + relevant skill context (~1k tokens) │ │ │ │ │ │ • Preload next 2 likely tools │ │ │ │ │ │ • Execute via registry │ │ │ │ │ │ • Try fallbacks on failure │ │ │ │ │ │ • Store result, update state │ │ │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ 7. RECORD OUTCOME │ │ │ │ │ │ │ │ Success/Failure ──► Update pathway stats ──► Adjust K-factor │ │ │ └─────────────────────────────────┬─────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ FINAL OUTPUT │ │ │ └───────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## Key Files to Create ``` apps/web/ ├── src/ │ ├── lib/ │ │ ├── planner/ │ │ │ ├── tool-discovery.ts # Part 2 │ │ │ ├── plan-generator.ts # Part 3 │ │ │ ├── skill-inference.ts # Part 4 │ │ │ ├── cascading-executor.ts # Part 5 │ │ │ ├── pathway-learning.ts # Part 6 │ │ │ └── orchestrator.ts # Part 7 │ │ └── types/ │ │ └── planner.ts # Part 1 │ ├── components/ │ │ └── PlannerPlayground.tsx # Part 8 │ └── app/ │ └── api/ │ └── planner/ │ ├── generate/route.ts # Part 9 │ └── execute/route.ts # Part 9 packages/db/prisma/ └── schema.prisma # Add PathwayRecord model ```