SPB Git

spb/worthdoing Public

Autonomous investigation agent that discovers, challenges, and ranks things genuinely worth doing — Claude + Firecrawl, Next.js 16, PostgreSQL

TypeScript 91.5% SQL 5.8% CSS 2.2%
2.5 KB · 82 lines typescript
Raw Blame History
1/**2 * WorthDoing.ai3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: src/lib/agent/runner.ts6 * Description: Investigation lifecycle — create with default budgets, launch/resume the engine in the background.7 */8import { eq } from "drizzle-orm";9import { db } from "@/lib/db/client";10import { investigations, type BudgetLimits, type BudgetUsed } from "@/lib/db/schema";11import { agentModel } from "@/lib/anthropic/client";12import { PROMPT_VERSION } from "./prompts";13import { TOOL_SCHEMA_VERSION } from "./tools";14import { runInvestigation } from "./engine";1516export const DEFAULT_BUDGET: BudgetLimits = {17  maxAgentSteps: 30,18  maxSearches: 20,19  maxScrapes: 60,20  maxCrawls: 3,21  maxWallTimeMs: 25 * 60 * 1000,22};2324const ZERO_USED: BudgetUsed = {25  agentSteps: 0,26  searches: 0,27  scrapes: 0,28  crawls: 0,29  inputTokens: 0,30  outputTokens: 0,31  costUsd: 0,32};3334const globalForRunner = globalThis as unknown as { __wdRunning?: Set<string> };35const running = (globalForRunner.__wdRunning ??= new Set<string>());3637/** Create an investigation and start the agent in the background. */38export async function createAndStartInvestigation(objective: string, userId?: string) {39  const [inv] = await db40    .insert(investigations)41    .values({42      objective,43      userId: userId ?? null,44      status: "pending",45      phase: "scouting",46      budget: DEFAULT_BUDGET,47      budgetUsed: ZERO_USED,48      promptVersion: PROMPT_VERSION,49      toolSchemaVersion: TOOL_SCHEMA_VERSION,50      model: agentModel(),51    })52    .returning();5354  launch(inv.id);55  return inv;56}5758/** Resume a stuck "running" investigation (e.g. after a server restart). */59export async function resumeInvestigation(investigationId: string): Promise<boolean> {60  const [inv] = await db.select().from(investigations).where(eq(investigations.id, investigationId));61  if (!inv || inv.status === "completed" || inv.status === "failed" || inv.status === "cancelled") return false;62  launch(investigationId);63  return true;64}6566export function isRunning(investigationId: string): boolean {67  return running.has(investigationId);68}6970function launch(investigationId: string): void {71  if (running.has(investigationId)) return;72  running.add(investigationId);73  // Detached background execution inside the Next.js server process (V1 scope).74  void runInvestigation(investigationId)75    .catch((err) => {76      console.error(`[worthdoing] investigation ${investigationId} crashed:`, err);77    })78    .finally(() => {79      running.delete(investigationId);80    });81}82