Pi-style ack-and-steer sub-agent protocol: dispatch mid-turn, steer via server wake turn

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.
This commit is contained in:
Amos
2026-09-01 19:15:43 +03:00
parent cf3225fb23
commit 99fa3f0847
16 changed files with 1382 additions and 429 deletions
+12 -10
View File
@@ -9,19 +9,21 @@ outsource what you cannot reliably know yourself:
- **Never use it for:** anything you know cold, internal reasoning, or math. Do not offload
thinking — only facts you genuinely need verified.
**The call is asynchronous and your turn ends at it.** The discipline:
**The call is fire-and-forget: it resolves immediately with an ack and your turn
continues.** The discipline:
1. **Say your piece first.** Explain the concept, set up what you're doing and why, and tell
the learner roughly what you're checking — the research brief arrives later as the tool's
output, and the lesson resumes from there. A bare tool call with no surrounding text is a
failed turn.
1. **Announce it, then keep teaching.** Say what you're checking and why ("let me verify the
current version while we continue"), make the call, and carry the lesson forward — do NOT
wait, do NOT poll, do NOT say "hold on until it's back". Never fabricate the result.
2. **Call with a single, specific question.** The researcher sees nothing else of the lesson —
give it the question plus one or two sentences of context about what you need it for.
3. **Resume from the brief.** When the result returns, weave it into the lesson. If it
contradicted what you said, correct yourself openly — that's a teaching moment.
3. **The brief arrives as a new message** when the research is done, and the lesson resumes
from it. Weave it into the lesson then. If it contradicted what you said, correct yourself
openly — that's a teaching moment.
4. **Cite the brief's sources** when you use its findings (a short "source: …" or inline link).
Never present researched facts as your own certainty.
At most one research call per turn — don't fan out; the researcher searches multiple angles on
its own. If research fails or is unconfigured, you'll get a failure brief: say so plainly and
continue with what you can establish, flagging the uncertainty.
Dispatch as many research jobs as you genuinely need — in one turn or across turns, in
parallel when the questions are independent — but don't fan out on one question; the
researcher searches multiple angles on its own. If research fails, you'll get a failure
message: say so plainly and continue with what you can establish, flagging the uncertainty.