Files
learnharness/docs/probe-teach-split.md
T
amosblomqvist 487f5d4cea wave 1 (platform stream): background-observability accuracy + owner-driven follow-ups
- observability: liveness-gated stale-job recovery, extraction run registry on globalThis with
  explicit 'embedding' phase, /api/jobs force-dynamic, client refresh nudges at every seam
- headless: providerSafePng canonicalizer (multi-IDAT Chrome PNGs → single-IDAT) fixing qwen 400s
- composer: Enter routes through pure enterAction — accepts a pending plan verdict (owner decision)
- markdown chrome: session-scoped mermaid overlay + md-blocks component set + pan-zoom math,
  corner-copy (clipboard grant in electron main), math/table/code block fixes, heading weight 900

543/543 tests; typecheck/lint/build clean
2026-09-19 13:16:34 +03:00

9.4 KiB

The probe→teach agent split & plan-movement semantics

Shared understanding reached 2026-09-11 (user + master session). This is the convergence record — the decided design, with reasoning. It is not a task list; implementation waves are planned against it.


1. Two agents, one conversation

Probing and teaching are structurally so different that they get separate agents: separate system prompts and separate tool sets, each fully fleshed-out with a major focus on its own phase. The split extracts more system-instruction alignment: neither agent carries the other phase's bulk (neither needs the specifics — each gets only a brief summary of the other's philosophy so they share a view of the whole process).

  • The prober runs the probing phase: quiz + ask_user_question + probe_done. Its quiz is a different tool (different schema: no nodeId field at all) under the same name, so the model is never shown a field it must not use. No plan tools, no jobs, no demos.
  • The teacher runs everything from planning onward: full tool set (teach quiz with nodeId, plan_init, jobs, demos). Constructed the moment the transcript carries a probe_done.
  • The swap happens between probing and planning — the planning phase needs full knowledge of how teaching is done, so the teacher agent starts at planning.
  • They share the message history. No fresh start: the history holds subtleties an assessment summary would lose. History tool parts are content, not live schemas — the teacher reads the prober's quiz calls as past conversation; the one foreign tool (probe_done) reads as a concluded past action, and even a hypothetical re-call degrades to a returned tool-error (the return-to-agent doctrine).
  • Invisible to the user: same chat, same record, same UI. The agents are constructed per-request by the same composition root; "which agent" is a construction branch, not a handoff.

2. probe_done — the seam tool

The prober concludes explicitly:

  • Server-side tool. Input: the learner assessment. Output: ok/error.
  • Refuses while a question is open ("resolve the open question first") — same doctrine as nodeId validation.
  • Refuses a second conclusion (idempotent: the probe concludes once).
  • The assessment is the tool call's input, so it streams to the learner live and is durable in the transcript by the tool result itself. A dedicated assessment card/viewer comes later (UI).
  • Assessment shape (v1, refine later): a list of dependency strands + a free-form read of the learner's edge on each. The probe was already a binary search per strand the lesson depends on; the assessment formalizes exactly that. Optional overall summary.
  • Side effect: it queues the handoff. When the prober's turn ends, the wake pipeline (the same machinery that delivers sub-agent results and abort steers) fires a server-side turn with a handoff steer — the teacher is constructed (probe_done is in the transcript) and authors the plan. The learner experiences assessment → plan as one continuous flow. If the wake is lost (crash window), the next client message still constructs the teacher — the transcript is the source of truth.

3. nodeId semantics — position, not completion

The old contract read a correct answer on a tagged check as "node done." That is the wrong signal and is abolished. The new contract:

nodeId on a quiz means: "this question is about this node." Nothing else. It is a position signal, never a completion signal.

  • Current node = the node referenced by the most recent tagged quiz call (the call, not the result — any state counts).
  • Visited = every node that appeared in an earlier tagged quiz call. Sticky, monotonic. There is no done ledger and no done bookkeeping of any kind — everything is inferred from the session history. The stored tree carries structure only (the author-once protocol taken to its conclusion: currentNodeId/doneNodeIds are removed from the tree).
  • A node spans multiple turns and multiple checks naturally. Several questions may tag the same node — the map stays put. The teacher moving to a new node is the teacher's judgment that the previous one is finished, regardless of individual check outcomes (a failed check followed by a re-teach and a move-on is still a visited node).
  • Failed checks move nothing. Results no longer feed derivation at all.
  • Untagged checks are legal ad-hoc checks — they don't move the map (quick comprehension checks don't need nodes). Not every check needs a node. The prompt stresses tagging when teaching a map node: an untagged stream leaves the map frozen (lost signal).
  • No fallback current. Before the first tagged check nothing is current. No derived-from-prereqs guess.
  • Present-but-invalid ids are still rejected at the call boundary (existing validation, return-to-agent retry).
  • Visualization: current node gets a special highlight; visited nodes render visible (not grayed); upcoming nodes gray out. "Where have we been, where are we."

4. Phases — explicit events, no occupancy derivation

The phase chain is event-driven:

probe (prober) — probe_done → planning (teacher) — plan_init presents the plan → accept → teach (teacher)

  • The broken derived-occupancy seam (phase-of-tree via currentNodeId nothing ever sets — the stuck-on-plan bug) is replaced, not patched.
  • The verdict gate is LIVE (landed 2026-09-11, same day): presenting the plan (plan_init) does NOT move the phase — the learner's accept does. The composer's send square becomes two stacked buttons (green Accept plan / red Decline) while the verdict pends; accept sends a user message carrying a data-plan-accepted data part — THE plan→teach seam (phaseAt3 reads it; convertToModelMessages drops data parts, so the teacher sees only the text). Decline asks for a revision. While the verdict pends, Enter ACCEPTS the plan — typed text rides the acceptance as its note, an empty box accepts too (user-decided 2026-09-19; formerly Enter sent a plain message, which left the verdict still pending on the next message — a side question now rides the acceptance note instead). Mid-lesson rewrites need no fresh verdict: acceptance happens once.
  • The session picker phase derives from transcript markers the same way.

5. No global-graph context in sessions (for now)

Both agents run fully standalone — no knowledge-graph injection into either prompt. The graph-as-session-context idea (W8) is shelved until there's a considered design for what it should actually feed. The global knowledge graph remains what the extraction pipeline builds; nothing in lesson prompts reads it.

6. The plan UI: persistent DAG window (direction)

The injected-blocks approach is superseded: the plan should live in a persistent DAG window in the conversation view (like the background- processes dock): always present, zoomed on the current node, auto-moving as the lesson advances; click → full plan (the existing popup survives as the expanded view). Exact layout/design is refined in the UI session.

The window is LIVE (landed 2026-09-11, one day after the design): bottom-left, 2:1 zoomed viewBox centered on the current node, clamped to the layout, click opens the popup. The full plan block also still renders in the flow at plan authoring (the presentation moment; the block sits immediately after the plan_init call) as a second popup entry point. The neighborhood-block machinery (frozen fragments) is retired — snapshots can't represent live-derived movement.

7. Deferred / later waves (designed, not now)

  • Accept-plan UI — landed 2026-09-11 (see §4): stacked Accept/Decline buttons; the data-plan-accepted part is the seam.
  • Assessment viewer — the strand assessments displayed to the learner as a proper card (data already durable in the transcript; the plain v1 probe-done card renders the assessment meanwhile).
  • Model / thinking control — currently hardcoded per role in config.ts MODELS. The split gives natural MODELS.prober / MODELS.teacher entries; runtime control comes after.
  • Graph context, properly designed — what the global graph should feed into sessions (§5).

8. No backwards compatibility — ever

Legacy sessions are not supported. At major pivots the testing data is wiped and re-grown from new testing. No compat shims, no legacy marker fallbacks, no migration of old trees. probe_done is the only teacher construction key; sessions without it are prober sessions (dead legacy data is garbage to be wiped, not accommodated).


Diagnosis context (why this pivot)

The 2026-09-11 user testing round (Nietzsche + Lacan sessions, minimal- effort GLM) exposed: (1) the teacher never tagged nodeId — every quiz in both sessions was untagged, so nothing ever derived; (2) the phase system structurally could not leave "plan" — phaseOfTree read currentNodeId, which the author-once protocol never sets, so no session could reach "teach"; (3) with result-derived "done," one passed check would have falsely finished a node. Model-compliance issues (untagged checks, free-text correctAnswer) are expected to shrink with higher thinking budgets and runtime model control — but the semantics had to be right first. Model mistakes are good test material: they exercise the return-to-agent validation loops.