The teaching agent no longer runs on pi. Stack:
- ToolLoopAgent (ai@7) + OpenRouter (OPENROUTER_API_KEY), model via LH_MODEL,
default anthropic/claude-sonnet-4.5 with reasoning tokens
- quiz/ask_user_question = client-side tools (no server execute): the call
lands as a typed tool part in input-available state; the card grades via
src/core/question-core.ts; addToolOutput sends the verdict; the teacher's
reaction continues the same assistant message
- resume-mid-quiz is free: the pending tool part persists in the transcript
- persistence: one JSON per chat (data/chats/), full UIMessage transcripts,
saved onFinish (also on abort)
- UI: AI Elements (owned copy-paste components, pruned to the five in use)
+ button-based QuizCard/AnsweredCard (ask answers never graded)
- pi fully removed: TUI, extensions, Fastify+WS server, bridge, custom
protocol all deleted (recoverable from git history)
- tests: question-core units carry over; chat-flow integration drives
handleChatRequest with MockLanguageModelV4 (48 tests, zero tokens);
render smoke tests guard the cards (renderToStaticMarkup)
Carried over unchanged: teacher system prompt, question-core, schema/
ledger/derive/extract pipeline, data model.
Root causes of the v1 browser bugs (all fixed + covered by deterministic tests):
- duplicated messages: per-socket snapshot + global broadcast marking raced;
replaced with a single append-only entry log (exactly-once per socket,
structurally) — covered by tests/server.test.ts
- delayed/missing echo of own messages: optimistic append with sending/failed
status, reconciled by text when the real entry arrives
- no thinking shown: thinking deltas streamed live (kind: thinking) and
persisted in assistant entries so they survive reloads
- ask answers graded as wrong: AnsweredCard no longer applies quiz grading to
ask answers (no ✓/✗, no verdict)
- broken option selection: native radio/checkbox inputs fighting React state
replaced with button-based options (single source of truth, one-click select)
- no URLs: History API routing — / home, /s/<file> per session (deep links,
refresh, back button); session view resumes its file on mount
- lost connection = dead UI: exponential-backoff reconnect with full resync
- stick-to-bottom transcript with jump-to-latest pill
Server: startServer() with injectable runtime factory (scripted model in
tests); createSession returns file for routing; scripted model gained thinking
turns. 51 deterministic tests green.