The job tools (research/demo/visual) switch from client-side turn-breaking calls to pi's model: execute resolves IMMEDIATELY with a fire-and-forget ack, the teacher's turn continues, and the result arrives later as an injected user-role steer message that wakes the teacher in a new server-side turn. - contracts.ts: THE ASYNC PROTOCOL rewritten (ack now, steer later); steer types (SubagentSteerMessage, metadata.subagentResult + data-subagent- artifact part), pure buildSubagentSteer/steerDelivered/isSubagentFailure - tools/jobs.ts: jobTools(ctx) factory — server-side execute creates the job (deduped by toolCallId), fires spawn with the dispatch-time input (the part isn't persisted yet at execute — a transcript scan would race) - runner.ts: runJob(jobId, input?) builds the steer and hands it to the injected deliver seam; pipeline owns the done/error flip (a polling client re-syncs once, at the end); failures still steer - server/jobs.ts (new pipeline): per-chat serialized queues on globalThis, idle gates (turn liveness + pending question, 1.5s grace), 409 retry with backoff, idempotent per jobId, stale-job recovery sweep (re-deliver or re-spawn); wake turns run through the same handleChatRequest composition via setTurnRunner injection - chat-handler.ts: wires runner+deliver+tools ctx; drops spawnTurnJobs from onFinish; teacher default registry = question tools only - plan.ts: PlanToolContext split out (plan tools need no job wiring) - visual-maker: edit_svg added (pi parity — exact-match single replacement, fuzzy context on a miss); researcher: pi's kept/dropped source accounting restored - prompts 20-jobs/40-demos: async discipline rewritten (announce and keep teaching, no waiting/polling/fabricating, parallel dispatch, teach with the artifact when it arrives) - tests: jobs.test.ts rewritten (dispatch ack mid-turn, runner steers, wake pipeline: deferral/409/idempotency/failure), demos.test.tsx runner tests ported to runJob + fake deliver; 222 green FLAGGED surgical touches outside the stream's owned files, for master review at merge: src/core/contracts.ts (protocol doc + steer types), src/core/ui-message.ts (job tool + data-part types), src/server/ chat-handler.ts (wiring), src/server/turn-service.ts (spawnTurnJobs retirement), src/agent/teacher-agent.ts (default tools), src/agent/tools/ plan.ts (context type split). Verified live on PORT=3003 with real models: teacher dispatched research mid-turn, researcher ran real searches/fetches, steer injected on quiz answer, wake turn wove the brief in citing its sources, job flipped done; a client/wake race correctly 409'd.
70 lines
2.9 KiB
TypeScript
70 lines
2.9 KiB
TypeScript
import { ToolLoopAgent, stepCountIs, type LanguageModel } from "ai";
|
|
import { webTools } from "../tools/web.ts";
|
|
import { modelFor } from "../models.ts";
|
|
|
|
/**
|
|
* The researcher sub-agent (vision §2.5): a SEPARATE ToolLoopAgent instance
|
|
* from the teacher — an isolated loop with only web_search + web_fetch, so
|
|
* raw search/fetch churn never touches the teacher's context window.
|
|
*
|
|
* System prompt adapted from the pi researcher agent
|
|
* (~/.pi/agent/extensions/interactive-subagents/agents/researcher.md).
|
|
* The model is injectable so tests run the loop with a MockLanguageModelV4.
|
|
*/
|
|
|
|
const INSTRUCTIONS = `You are a research specialist. You work for a teacher agent inside a learning
|
|
application: given a research question, conduct thorough web research and produce a focused,
|
|
well-sourced brief that the teacher can teach from.
|
|
|
|
You operate in an isolated context with no knowledge of the lesson or any prior conversation.
|
|
All necessary context is in the task description.
|
|
|
|
Process:
|
|
1. Break the question into 2-4 searchable facets.
|
|
2. Search with web_search using varied angles:
|
|
- the direct answer query (the obvious one)
|
|
- an authoritative-source query (official docs, specs, primary sources)
|
|
- a practical-experience query (case studies, benchmarks, real-world usage)
|
|
- a recent-developments query (only if the topic is time-sensitive)
|
|
3. Read the results. Identify what's well-covered and what has gaps. If the first round
|
|
doesn't fully answer the question, search again with refined queries targeting the gaps.
|
|
4. For the 2-3 most promising source URLs, use web_fetch to get the full page content.
|
|
5. Synthesize everything into a brief that directly answers the question.
|
|
|
|
Evaluation — what to keep vs drop:
|
|
- Official docs and primary sources outweigh blog posts and forum threads.
|
|
- Recent sources outweigh stale ones.
|
|
- Sources that directly address the question outweigh tangentially related ones.
|
|
- Drop SEO filler, outdated info, and beginner tutorials (unless that's the audience).
|
|
|
|
Tool outputs may be error strings (e.g. search unconfigured, a page that failed to fetch).
|
|
Adapt: try another URL or another search angle; if research is genuinely impossible, say so
|
|
in the brief instead of inventing facts.
|
|
|
|
Your FINAL assistant message is your entire deliverable — it must stand alone, using this format:
|
|
|
|
## Summary
|
|
2-3 sentence direct answer.
|
|
|
|
## Findings
|
|
Numbered findings with inline source citations:
|
|
1. **Finding** — explanation. [Source](url)
|
|
|
|
## Sources
|
|
- Kept: Source Title (url) — why relevant
|
|
- Dropped: Source Title — why excluded
|
|
|
|
## Gaps
|
|
What couldn't be answered. Suggested next steps.`;
|
|
|
|
export function createResearcherAgent(model?: LanguageModel) {
|
|
return new ToolLoopAgent({
|
|
model: model ?? modelFor("researcher"),
|
|
instructions: INSTRUCTIONS,
|
|
tools: webTools,
|
|
stopWhen: stepCountIs(12),
|
|
});
|
|
}
|
|
|
|
export type ResearcherAgent = ReturnType<typeof createResearcherAgent>;
|