W2: async job infra + researcher sub-agent

- src/store/jobs.ts: per-job JSON store in data/jobs (create/get/update/list,
  dedup by toolCallId)
- src/agent/tools/jobs.ts: research tool — client-side (no execute), turn ends
  at the call; input per contracts ResearchToolInput
- src/agent/tools/web.ts: server-side web_search (Google CSE) + web_fetch
  (Readability/Turndown/linkedom, Jina fallback) — errors return as strings,
  never thrown into the agent loop. Deps: @mozilla/readability, linkedom,
  turndown (+@types/turndown dev)
- src/agent/subagents/researcher.ts: isolated ToolLoopAgent (web tools only,
  12-step cap), researcher prompt ported from pi
- src/agent/subagents/runner.ts: spawnPendingJobs hook + runResearchJob —
  fire-and-forget, activity line per tool call, fulfills the part in the
  LATEST chat record (output-available) before flipping job status; failure
  still fulfills the part so the teacher learns the outcome
- src/server/chat-handler.ts: onFinish spawn hook after saveMessages
- app/api/jobs/route.ts: GET ?chatId= polling endpoint
- components/artifacts/job-card.tsx + registry entry: polling activity card
  (2s) → syncFromServer on finish; research-brief card (markdown + sources)
- src/agent/prompts/20-jobs.md: research doctrine for the teacher
- tests/jobs.test.ts: store CRUD, spawn hook via scripted mock teacher,
  runner completion/failure with fake model + stubbed fetch
This commit is contained in:
Amos
2026-08-22 09:36:15 +03:00
parent 92fb8e56d8
commit df16750560
13 changed files with 1456 additions and 7 deletions
+9
View File
@@ -1,6 +1,7 @@
import { createAgentUIStreamResponse, type UIMessage } from "ai";
import { createTeacherAgent, type TeacherAgent } from "../agent/teacher-agent.ts";
import { createTeacherTools } from "../agent/tools.ts";
import { spawnPendingJobs } from "../agent/subagents/runner.ts";
import { getChat, saveMessages } from "../store/chats.ts";
/**
@@ -38,6 +39,14 @@ export async function handleChatRequest(req: Request, agent?: TeacherAgent): Pro
} catch (err) {
console.error(`[chat ${chatId}] persist failed:`, err, { isAborted });
}
// Async sub-agent protocol (contracts.ts): scan the last assistant
// message for pending job tool parts and fire the background runners —
// fire-and-forget, never awaited; the HTTP response is already gone.
try {
spawnPendingJobs(chatId, messages as UIMessage[]);
} catch (err) {
console.error(`[chat ${chatId}] job spawn failed:`, err);
}
},
});
}