- switch the math authoring/rendering contract from \(...\)/\[...\] to the
standard $...$ inline / $$...$$ display: prompts (20-question-tools,
40-formatting), singleDollarTextMath on the renderer, legacy reader kept,
and escape-debris recovery extended to dollar spans (mirroring micromark
delimiter-run rules)
- exact answer membership now precedes debris cleanup in
coerceCorrectAnswer (calculus incident: an exact sentence-final-period
label was stripped as model debris, then rejected); option keys are
passed at the boundary/grading seams, the dock no longer pre-coerces
- a complete, phase-correct JSON-escaping example rides the probe/quiz
tool descriptions (serialized from one canonical body; quiz leads with
type+nodeId), plus the one-sentence JSON-escaping rule in 40-formatting
- ask option descriptions render markdown/LaTeX in the pending dock;
settled cards keep the details line and option descriptions
retroactively, read from the persisted call arguments (chat passes
part.input to AnsweredCard)
1566/1566 tests on both navigator configs; typecheck, lint, production
build, standalone packaging, and Electron-runtime smoke all green.
The tools_fails_2 diagnosis (glm-5.3 dropping trailing required fields):
- probe-phase quiz/explain renamed to probe/recall — a shared name carried
the probe call shape into teaching (the first teach quiz dropped nodeId);
display seams accept the old names forever
- teach quiz+explain schemas lead with the required pair (type, nodeId);
explain gains the predict/check type and joins the reveal law (checks
collapse, predictions arrive open; no-type records render check-style)
- plan_init/probe_done results go context-slim (owner, context diet):
content = validated nodeIds + binding-id reminder / fixed ack; details
keeps the tree for the popup and {ok:true} — serializer-verified that
details never reaches the model; failure echoes dropped
- the cards carry the tool's own name (Probe/Recall) and the quiz type
reads on the label (Prediction/Check) in composer and transcript
Authoring refinements (quizInputProblems, nodeId checks, ask/explain
refinements, submit-agent pipeline laws) now return invalidInputResult —
same model-facing failure and retry semantics pi minted from the throw,
but details keeps {tool, problems} as data instead of the throw path's
empty {}. ToolInputError deleted; wire shape unchanged (verified against
pi's own thrown-error conversion in agent-loop).
The teaching cycle gains its motivation question. The teach quiz schema
now carries a REQUIRED type field — predict (posed before the teaching,
arrives open, the learner predicts the next step) or check (posed after,
arrives collapsed behind the reveal). The prober's quiz stays role-less.
- core: quizInputObject.type union + normalizeQuizType + the reveal law
as pure quizNeedsReveal; QuestionData.questionType; note → thinking
rename (legacy reads fold back); readingThinking channel on answers;
compactAnswerForModel emits thinking / thinkingWhileReading
- tools: descriptions stripped to mechanics — QUIZ_OPTION_CRAFT and
EXPLAIN_DOCTRINE deleted, doctrine moved to the prompt layer
(reverses the 2026-09-27 placement, owner 2026-10-03); shared
quizDescription/explainDescription builders differ only in deltas
- prompts: shared/20-question-tools.md rewritten (option craft,
transcribed-speech leniency, the two thinking channels); new
teacher/28-predictions.md (cycle, roles, skip rule, resolution law);
20-teaching + 30-question-principles scoped to the new taxonomy
- UI: predictions render open with a Prediction eyebrow, verdict shows
but the explanation tucks behind a toggle; the reading-thinking field
mounts while a check waits (dictation claims it until reveal);
answered card separates thinking vs thinking-while-reading blocks
Two follow-up rulings on the explain tool:
1. No 'I don't know' row — not needed in free recall. If he doesn't
know, he says so in his own words; his text is the only answer
channel. Dock: text mode is rowless again (the composer IS the
surface); doctrine and the shared prompt chunk updated; the format
branch and card revert to quiz-only dontKnow.
2. explanation + correctAnswer collapse into ONE field — the separate
explanation was quiz remnant (MC needs the why outside its options;
a free-text commitment carries its own reasoning). Schema is now
{question, correctAnswer, details?}; the reveal shows the single
committed answer; no explanation snapshots anywhere on the explain
path.
1047/1047 tests, typecheck/lint/build green.
Sycophancy mitigation as a structural commitment: the model writes its
full correctAnswer (plus explanation, in that order) BEFORE the learner
sees the question; the commitment rides the persisted call args, is
revealed on submit, and grades his free text in the model's next reply —
the client never grades free recall.
- tools.explain setting (segmented 'Quizzing — Multiple choice | Free
recall' in Agent settings' Tools group): free arms explain and drops
quiz for BOTH agents (prober variant carries no nodeId, same pattern
as the probe quiz); mechanics keys never pass filterEnabledTools —
quiz stays unwritable (the 2026-09-19 wedge ruling stands)
- doctrine lives pi-natively in the tool descriptions (owner ruling —
no conditional system-prompt chunks): explain carries the commitment
rule; the MC option-building craft moved out of the prompt into the
quiz description; shared/20-question-tools.md slims to the
quiz-agnostic core
- display seams: parseExplainInput (LaTeX-debris restoration included),
dock text-mode with an 'I don't know' row (kbd row 0), AnsweredCard
reveal (The answer / Why, no verdict chips), [Explain answer] format
- phases/plan seams recognize both quizzing tools' nodeId tags; explain
answers deliberately feed no ledger evidence (no client grading to
key on — a future ruling can parse the model's grade)
1049/1049 tests, typecheck/lint/build green.
Three owner rulings from the gradient-descent session
(chat_muhskq6p_e8c3600b — 7 failed quiz calls, answers leaked in
option descriptions, planning divider floating at the transcript top):
1. RETRYING LINES (general): any tool call that errors back to the
agent (validation failure, malformed args — the retry loop) renders
one quiet "Retrying…" SystemLine instead of silence. Question tools
previously rendered null, so a flailing quiz loop read as a stall.
Aborts are terminal ("Operation aborted") and stay silent. Verified
live: exactly 7 lines render at the 7 failure sites of the session.
2. QUIZ OPTION DESCRIPTIONS: NUKED. The model used them to telegraph
correctness in every quiz of the session ("that moves uphill — it
climbs f instead of descending it") — answerable by elimination.
The field is gone from BOTH quiz schemas (probe + teach); ask keeps
it (ungraded — nothing to leak). The dock additionally suppresses
descriptions for quiz kind, so pre-fix records stop leaking too.
Verified live: the session's pending quiz renders bare labels.
Plus the diagnosed failure-mode fixes: GLM sometimes emits `options`
as a JSON-encoded string (6 of 7 failures). prepareArguments hooks
on all three question tools coerce it BEFORE pi's validation (the
call lands instead of bouncing); the client coerces again in
normalizeOptions (pi persists raw call args). Schema descriptions
hardened ("REQUIRED ... never a JSON-encoded string").
3. DIVIDERS AT THE MARKERS: the probe-handoff SystemLine
("Preparing your plan…") IS NOW the Planning divider; the accepted
verdict IS NOW the Teaching divider ("✓ Plan accepted" line gone) —
one signal per seam, not two. The fold-based divider block is
DELETED: Wave D grouping merged the whole probe into one giant
assistant group, so the inclusive-fold comparison floated the
divider to the transcript top (reproduced against the real record:
6 view messages, phase flipped at the giant group). The markers
render in the right spot by record construction. A DECLINED verdict
is not a seam — it keeps the "↻ Plan revision requested" line.
Verified live: plan divider after the probe concluded, teach
divider after the plan presentation, nothing at the top.
Tests: 1004/1004 — string-coercion pins (normalizeOptions + the
prepareArguments hooks on all three tools, identity fast path),
quiz-schema-has-no-description pin (ask keeps it).
- src/agent/lh-tool.ts deleted: the zod-wrapped LhTool + toPiTool bridge
(z.toJSONSchema per call, clientQuestion flag, hand-rolled result mapping)
is gone. Tools are pi-agent-core native AgentTools now.
- New src/agent/tool-execute.ts owns what the wrapper added: the abort guard
(runWithAbortGuard: run signal + 30s per-call timeout + 250ms grace,
'Operation aborted' / timeout wording preserved), ToolInputError (the
retryable '- problem at path' format), result mapping (string verbatim /
object JSON / image parts, details rides the record's tool part output),
and questionResult() — placeholder + terminate:true (owner Option A:
belt of terminate + slim makeQuestionStopper with the clean-question
rule; maxTurns deleted from the teaching turn entirely per owner).
- All 11 tool files + tools.ts registries converted to TypeBox parameters
(typebox@1.3.7, pinned to pi-agent-core's copy). zod refinements
(quizInputProblems, askOptionProblems, live-nodeId) moved from schemas
into execute — the model sees the same retryable error shape.
- question-core + contracts/probe TypeBox-authored (single source shared
with the UI); tool-parts parse seam runs the boundary's full chain
(Convert → Check → Clean → refinement), fixing the live-smoke lockout:
pi's boundary Value.Converts correctAnswer: 17 → '17' but the record
keeps raw args, so a parse without Convert rejected a posed quiz
(invisible pending question, turn ended). Pinned with the real smoke
emission shape.
- Subagents converted: demo-builder/researcher AgentTool-native,
visual-maker/pdf-reader declaration-only pi-ai Tools with local execute
fns, extraction submit schemas re-authored in TypeBox (owner option 2:
zod defaults → Type.Optional + fill at the submit seam).
- Accepted delta, flagged: TypeBox-native schemas skip pi's JSON-Schema
primitive coercion fast path... (boundary still Value.Converts), and
prober-quiz unknown keys ride through (Value.Clean strips at display).
- Tests: 988 passing (was 967 at the recon baseline); tool-abort and
read-tool rewritten against the native seams; question-core/question-
tools/tool-parts/demos/web-search/graph-tools/plan-tree/probe-done/
tool-toggles converted to Value.Check/execute pins.
Live smoke (PORT=3004, real model): native ls turn streams end-to-end; a
real quiz pose (first attempt missing correctAnswer → tool-input-error →
model retry → posed) terminates the turn cleanly and persists pending.
Fix wave (2026-09-20), two owner-approved fixes:
Fix 1 — the searchAvailable gate was dead code on the live wiring path:
createTeacherTools RECEIVED ctx.searchAvailable but dropped it when
building the inner JobToolContext, so jobTools' research gate always took
its ?? true default (jobTools-direct tests passed; the production path
never saw the signal). Threaded through — the gate now reads the live
server signal on the teacher registry too.
Fix 2 — the prober registry gains the research + pdf dispatch tools
(owner ruling, 2026-09-20): these two are phase-omnipresent — the prober
can verify facts or read learner PDFs while assessing. demo/visual stay
teach-phase-only (they need the lesson's plan context). Same gating as
the teacher side: toggle-filtered, research subject to the search gate
(honestly absent when no engine is live — the prober's dispatch closes
over the SAME jobTools builder, one gate, one semantics). probe_done
stays always-on-by-construction; the learner-fs set untouched.
- ProberToolContext (extends ProbeDoneContext with the job wiring);
createProberAgent takes it; chat-handler injects jobStore + spawn +
searchAvailable into the prober ctx.
- TeacherToolContext.spawn's stale input union fixed (PdfInput was
missing from the TYPE — runtime already passed it).
- Tests: the threaded-gate describe in web-search.test.ts pins the LIVE
factory path both ways (teacher + prober); question-tools' registry
key-sets gain research/pdf; the prober toggle test pins the
research:false path.
Gates: typecheck, 807/807 tests, lint, build — all green.
Reconciliation of the diverged tool-toggles seam (2026-09-19, master's
P2): Platform injected the resolved delta as an ad-hoc intersection on
AgentBuildOptions while the factories' enabled? param sat unread — the
seam was inert on both sides.
- teacher-agent: AgentBuildOptions gains toolToggles?: EnabledTools;
both constructors thread it to the factories (createTeacherTools /
createProberTools). An explicitly injected opts.tools registry is
never filtered — the caller owns it.
- Vocabulary ADJUDICATED with the user (2026-09-19): TOGGLEABLE_TOOLS is
the definitive list — the job tools only (research/demo/visual/pdf).
quiz/ask_user_question stay always-on forever (the prober's flow is
graded-quiz brackets -> probe_done; quiz-off collapses assessment and
teaching doctrine is built on graded verification — the wedge danger);
the learner-fs set, plan tools, and probe_done also always-on.
- settings-body (write side): unknown tool keys reject with 400 + the
supported list, mirroring the prompt-switch validation; the read-side
sanitizer stays lenient for forward-compat.
- chat-handler: the intersection is gone (field now first-class) and the
seam comment states the adjudicated vocabulary.
Gates: typecheck, 28 new/updated tests green (tool-toggles-bridge,
question-tools, settings-body), chat-flow + settings suites green.
User pivot (2026-09-19): no attachments UI at all. Pasting paths is native
for technical users and alien for everyone else - so the agent gets the
tools to find what the learner merely NAMES. The learner communicates in
messages; the agent locates and reads. Zero cross-stream seams: paths
already live in tool parts, so the sources "list" is the transcript.
- ls/find/grep: pi-verbatim schemas, descriptions, and output formats
(fd's glob semantics, rg's path:line: match format, pi's truncation
notices), ported to pure Node - no binaries, no shell-out, read-only by
construction. Deltas from pi: home-anchored resolution (the harness has
no cwd), .gitignore replaced by hard noise exclusions
(node_modules/.git/Library/.Trash), 10s walk timeout with partial results.
- read: description swapped to pi-verbatim (doctrine moved to the prompt);
relative paths now resolve against the learner's home like the rest.
- prompts/shared/05-learner-files.md: one minimal doctrine fragment
composed into BOTH agents (composeInstructions grew a shared/ dir,
before the agent's own fragments). teacher/30-jobs.md keeps only the
pdf-dispatch addendum.
- ToolCallCard: the generic quiet tool-call card the user specced -
collapsed one-liner (tool name + headline argument), expandable to full
input/output, errors red, image payloads never render. Registered for
read/ls/find/grep.
- ui-message.ts: TeacherUITools entries for the four tools (mechanical).
- tests: fs-tools suite (glob semantics, pi formats, exclusions, limits),
registry/card renders, shared-fragment composition pins, tool-registry
key updates, read re-anchor.
Learner source files, on the teacher's own terms (nothing auto-injects):
- read tool (src/agent/tools/read.ts): pi-read port — path/offset/limit,
2000-line/50KB truncation with use-offset continuation notices, image
files (jpg/png/gif/webp/bmp) returned as native image content parts
(new LhToolImageResult escape hatch in lh-tool.ts). Always-on in both
the prober and teacher registries (user-decided 2026-09).
- pdf-reader sub-agent (JobKind 'pdf', toggleable, ack-and-steer): triage
via pdf_info (pdfjs text layer: per-page text/image/math-density),
targeted pdf_extract, regex pdf_search (hits count as consultation),
and pdf_render — pages painted by pdfjs onto a canvas shim in headless
Chrome (the pdfium viewer doesn't paint headless), screenshot fed back
image-in-the-loop (visual-maker idiom, providerSafePng applied). The
unread-finish gate bounces a zero-consultation brief once. Teacher gets
a text brief with page citations only; renders never leave the loop.
- MODELS.pdfReader = qwen/qwen3.8-flash (user override 2026-09-19: the
look-at-renders workload wants the vision-proven model; catalog listed
glm-5.3-flash as image-capable but qwen is the known-good daily driver).
- UI: PdfCompleteBadge + PdfBriefCard (steer-card, job-card), dispatch
card + activity-dock kind entries (type-forced, flagged for UI review).
- Prompts: 'Learner source files' section in teacher/30-jobs.md; read
line in prober/10-probe-doctrine.md. Doctrine rides tool descriptions
per the self-contained-tools principle.
Part 1 — tool availability (the settings seam, machinery generic):
- core/pending.ts: TOGGLEABLE_TOOLS (today: research, demo, visual —
the model-facing tool names), EnabledTools = the JSON-native seam
shape Partial<Record<string, boolean>> (absent key = enabled, so old
records behave exactly as today), filterEnabledTools shared helper.
quiz/ask_user_question are always-on FOR NOW (user-decided); the
filtering is generic so future toggleables (question-tool variants,
the read tool, …) need no factory change.
- createProberTools/createTeacherTools take enabled?: EnabledTools; the
composition root (chat-handler, Platform's lane this wave) passes the
resolved settings through. probe_done is re-added after the filter —
always-on by construction (it ends the phase; a prober that can never
conclude is a lockout).
- Prompts stay STATIC under toggles this wave (user-decided); standing
principle recorded: tools ship self-contained usage guidance in their
descriptions; system prompts reference only always-on tools.
Part 2 — phase-based question-writing principles (user-locked wording,
drafted with the user over three iterations):
- NEW teacher/21-question-principles.md (composes after 20-teaching):
answerable-by-reasoning-from-the-session (the fairness rule that
filters unanswerable questions without forbidding visible answers),
never-just-reciting-your-words-back, PREFER problems that apply the
knowledge (preference, not hard rule — honest recall is the fallback,
not the habit), catch-confusion-early-and-nameable (wrong options as
real misconceptions), answer-is-signal-not-verdict.
- prober/10-probe-doctrine.md: 'no rush' retired; quick-measurement
directive in — every question moves an edge (bracket, narrow, or
characterize); a bracketed strand is done.
Tests: TOGGLEABLE_TOOLS pin, prober/teacher filtering (absent = full,
false drops exactly that tool, probe_done survives), prompt pins (the
principles compose verbatim into the teacher prompt, the no-rush
framing is gone from the prober). 547/547, typecheck/lint/build green.
The Vercel AI SDK survives only as Band D (useChat client + UIMessage
record types); @openrouter/ai-sdk-provider is uninstalled.
- src/agent/models.ts: PiEngine {model, streamFn} via createModels +
openrouterProvider subpath; thinkingLevelFor maps config effort none->off
- src/agent/lh-tool.ts: neutral LhTool (zod authoring, optional execute,
clientQuestion flag); toPiTool converts via z.toJSONSchema and re-runs
zod inside execute so refinements ride the error-result retry path
- src/server/pi/record-adapter.ts: UIMessage records -> pi Messages
(dangling tool calls get synthesized interrupted results)
- src/server/pi/ui-stream.ts: SSE bridge emitting AI-SDK-shaped chunks so
the useChat client parses unchanged (continuation id rule, question
tools never emit output, tool-input-error for bad authoring)
- teacher-agent.ts: stateless per-turn pi Agents; shouldStopAfterTurn
stops only when every question-tool call resolved cleanly (errors retry)
- chat-handler.ts: buildAgent(outgoing) factory seam (stateless doctrine);
assertValidUiMessages owns the retired SDK validation gate
- sub-agents via shared runSubagent (pi-run.ts); extraction stages stop
after a clean submit; visual-maker keeps its hand-rolled loop over
direct streamSimple calls; aborted model calls bill no usage
- tests: all model seams ported to pi-ai faux provider (scripted
FauxResponseSteps, agent factories); handleChatRequest second param
is now an agent factory
Gates: tsc, lint, build, 305/305. Live GLM smoke (disposable driver,
deleted): prober streaming + persistence, question seam, turn-2
continuation same message id, per-turn real token usage - ALL PASS.
Data wiped at the pivot (chats/graph/jobs, .gitkeeps kept).
Diagnosis (2026-09-11 user testing, minimal-effort GLM): sessions stuck on
'plan' forever. Two causes: (1) the three-phase view read the stored tree's
currentNodeId, which nothing ever writes post author-once (2026-09-03) — a
structural dead seam; (2) the teacher stochastically left quiz.nodeId
untagged, and done-by-quiz-result semantics would have falsely completed
nodes anyway.
The redesign (docs/probe-teach-split.md) abolishes derived occupancy in
favor of explicit events:
- Two agents over one transcript: createProberAgent (quiz without a
nodeId field, ask, probe_done) and createTeacherAgent (full toolset,
plan_init onward). The chat-handler construction branch keys on a
probe_done part in the transcript. MODELS.prober added.
- probe_done: server-side seam tool — carries the learner assessment
(strands + free-form edge read), refuses while a question is open or
the probe is already concluded, and queues a handoff steer through the
generalized wake pipeline (steers now dedupe by id, not jobId; job
finalization only for job steers). The teacher auto-wakes and authors
the plan; the learner sees the assessment via the probe-done card.
- phases: probe ends at a concluded probe_done, plan ends at a
successful plan_init — strictly sequential, no skipping, no legacy
compatibility (data wiped at the pivot per doctrine).
- Plan map = position, not completion: derivePlanStatuses reads only
tagged quiz CALLS (state/outcome feed nothing); current = latest
tagged node with consecutive duplicates collapsed, visited = earlier
distinct stays (a revisit stays in both — current wins at render).
PlanTree is structure-only; currentNodeId/doneNodeIds and the dead
mutation helpers are gone. No fallback current.
- Prompts split per agent (prompts/prober/, prompts/teacher/ over the
trimmed shared base); graph-context injection removed entirely
(shelved, docs/probe-teach-split.md §5); knowledge-graph context no
longer reaches either agent.
- Plan blocks are full-only snapshots at plan_init (neighborhood
machinery retired; the persistent DAG window is the future UI wave);
usage capture unchanged.
Gates: tsc clean, 294/294 tests (phases/plan-tree/plan-blocks/question-
tools rewritten to the new semantics; probe-done + handoff-wake tests
added), lint, build. Live GLM smoke PASS: prober concluded with an honest
assessment, teacher auto-woke, authored a 10-node plan with the block
attached, then tagged checks on slope-instinct → secant derived current/
visited exactly as designed; grading honest both ways (driver off-by-one
proved a wrong answer grades false).
- plan_init is now the ONLY plan tool: a mermaid 'graph TD' block parsed by a
constrained-subset parser (mermaid-plan.ts) with line-precise errors; the
seven mutation/tracking tools are gone by design (2026-09-03 design pass).
- Re-call plan_init = sanctioned wholesale rewrite; slugs are permanent.
- derivePlanStatuses (contracts/plan.ts): done = passed check tagged with a
quiz nodeId; current = open check's node else first topo-ready node — the
tracking the teacher no longer declares by hand.
- questionToolsFor(ctx) (tools.ts): the quiz schema chains a dynamic plan-
nodeId validation against the LIVE record — an invalid nodeId is rejected
at the call boundary and the teacher retries mid-turn, before any UI.
Static questionTools stay validator-less for the default path.
- Prompts rewritten for mermaid authoring + derived position.
- Out-of-lane surgical touches (flagged for master review):
src/core/question-core.ts (quizInputObject export + nodeId description),
src/core/contracts/plan.ts (derivePlanStatuses + doc).