/** * WorthDoing.ai * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: src/lib/agent/runner.ts * Description: Investigation lifecycle — create with default budgets, launch/resume the engine in the background. */ import { eq } from "drizzle-orm"; import { db } from "@/lib/db/client"; import { investigations, type BudgetLimits, type BudgetUsed } from "@/lib/db/schema"; import { agentModel } from "@/lib/anthropic/client"; import { PROMPT_VERSION } from "./prompts"; import { TOOL_SCHEMA_VERSION } from "./tools"; import { runInvestigation } from "./engine"; export const DEFAULT_BUDGET: BudgetLimits = { maxAgentSteps: 30, maxSearches: 20, maxScrapes: 60, maxCrawls: 3, maxWallTimeMs: 25 * 60 * 1000, }; const ZERO_USED: BudgetUsed = { agentSteps: 0, searches: 0, scrapes: 0, crawls: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, }; const globalForRunner = globalThis as unknown as { __wdRunning?: Set }; const running = (globalForRunner.__wdRunning ??= new Set()); /** Create an investigation and start the agent in the background. */ export async function createAndStartInvestigation(objective: string, userId?: string) { const [inv] = await db .insert(investigations) .values({ objective, userId: userId ?? null, status: "pending", phase: "scouting", budget: DEFAULT_BUDGET, budgetUsed: ZERO_USED, promptVersion: PROMPT_VERSION, toolSchemaVersion: TOOL_SCHEMA_VERSION, model: agentModel(), }) .returning(); launch(inv.id); return inv; } /** Resume a stuck "running" investigation (e.g. after a server restart). */ export async function resumeInvestigation(investigationId: string): Promise { const [inv] = await db.select().from(investigations).where(eq(investigations.id, investigationId)); if (!inv || inv.status === "completed" || inv.status === "failed" || inv.status === "cancelled") return false; launch(investigationId); return true; } export function isRunning(investigationId: string): boolean { return running.has(investigationId); } function launch(investigationId: string): void { if (running.has(investigationId)) return; running.add(investigationId); // Detached background execution inside the Next.js server process (V1 scope). void runInvestigation(investigationId) .catch((err) => { console.error(`[worthdoing] investigation ${investigationId} crashed:`, err); }) .finally(() => { running.delete(investigationId); }); }