a3aabb3a57a27c815fdf31bb927d29b6ec2ed4fc
23
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5707672a09 |
auditor grounding — a prescribed, bounded exploration procedure + born-in find pointers
Observed live: the auditor looped find for a very long time, never touching list_sessions/list_propositions. Root cause was the prompt: its only procedure text was 'For each proposition, search the graph (its key words, its synonyms, its phrasings)' — a per-proposition find loop with no starting point, no sense of the space's extent, and no termination rule. And find returned id+name+statement with no session pointer, so results never composed with the listing tools (owner diagnosis, 2026-09-29). - AUDITOR_INSTRUCTIONS now prescribes the process: (1) list_sessions ONCE — the graph is small and finite, this shows its entire extent; (2) read plausibly-related sessions with list_propositions (full text) — every realistic candidate seen in a few calls; (3) find as the targeted sweep; (4) SUBMIT once related sessions are read — never re-search seen ground - find hits now carry 'born in: <session id>' — results point at sessions the auditor can read whole (legacy no-provenance nodes name none) - identity doctrine untouched: equivalence bias, linkTo mechanics, verdicts-only deliverable |
||
|
|
1b50013551 |
extraction cleanup: events ledger dies, markdown source view, auditor-as-linker (2026-09-27 rulings)
The graph becomes raw reasoning chains only. Quiz outcomes and understanding observations are no longer formalized; assessment moves to the future practice module. Events ledger removed end-to-end: LedgerEvent/LedgerEventKind/ UnderstandingLevel/ExtractionAttribution/AuditResult.dropped and snapshot nodeObservations deleted from contracts; quizEvent builder and the events step of mergeFindings gone; appendEvent/readEvents dropped from the port and store; LEDGER_FILES loses events.jsonl with a one-time purge of a stale file next to the nodes ledger; graph-deletion and the delete dialog lose event purge + quiz-answer impact counts; GraphSessionProposition becomes a pure birth list. Whole-session markdown source view: new core/session-markdown.ts is the shared formatting engine (id-anchored speaker blocks, quiz/ recall/question blocks, tool one-liners) — the extractor's prompt view and the future user-facing export render from one engine. Coverage fingerprints deliberately stay on the old line format (content-hash semantics; re-formatting never re-opens covered history). The whole session goes to the extractor — no phase cut. Auditor refocused as the identity linker: never sees the source; explores the graph via new auditor-specific tools in agent/tools/graph.ts (find — glob over name/statement/aliases; list_sessions; list_propositions with full statement text; no timestamps) instead of an injected full-graph dump; equivalence bias — paraphrases merge, only genuinely distinct propositions stay apart; no drop channel, the extractor is fully trusted; may draw bridging edges (typed follows/supports) between existing nodes and the batch, resolved by the merger with chat-scoped message-less provenance. Agentic loop, maxTurns 16. UI: the node detail card gains an edges section above the sessions list (follows-from / supported-by / what-follows / what-it-supports, rendered only when present, click-to-select neighbors); the understanding-note chips are gone. Prober list_propositions reworded to what a session covered — names only, no levels. docs/architecture.md + README updated to the as-built pipeline. Gates: typecheck, 1059/1059, lint, build. |
||
|
|
7ab47d4fcb |
Wave B: the native message record — input events, projection, adapter death
The chat record is now pi-native agent history (LhMessage[]), the AI SDK's
UIMessage transcript contract is gone from the server, and every client turn
arrives as ONE explicit input event.
Core (new):
- src/core/native-messages.ts — the LhMessage taxonomy (user/assistant/
toolResult + lhMarker customs: verdict, probe-handoff, subagent-steer,
subagent-abort, plan-block), constructors, and toLlmMessages — the single
model-facing seam (convertToLlm): strips ids, flattens markers to user
text, drops UI-only plan-blocks, synthesizes INTERRUPTED results for
dangling aborted calls, never throws.
- src/core/projection.ts — projectNative: the one-way native → UIMessage
projection (consecutive assistant+toolResult runs group into one
assistant view message; toolResults attach backward by toolCallId;
markers project to data parts; plan-blocks splice after the last
successful plan_init). Every existing consumer reads through it.
- src/core/input-events.ts — the ChatInput union (text/answer/verdict/
continue), parseChatInput validation, and inputAppends (answer targets
the dangling question call or 400s; verdict = optional note user
message + native verdict marker).
Server:
- chat-handler.ts — the turn root takes {chatId, input}; write-ahead
persists input appends + claimed steers before the turn; claimSteers
coalesces the whole queue with the handler's steerDelivered filter as
the dedupe seam; teacher-vs-prober branch on hasProbeDone.
- turn-service.ts — persistTurn stamps ids, drops clean question
placeholder toolResults (the record ends at the unanswered call),
emits plan-block markers idempotently (plan-blocks.ts rewrite), and
persists abort partials.
- ui-stream.ts — PiTurnSpec takes the native base; the wire dialect is
unchanged. jobs.ts steers are native markers (steer_<jobId> dedup ids);
waitForIdle reads danglingQuestionOf. teacher-agent.ts wires
convertToLlm: toLlmMessages. record-adapter.ts DELETED wholesale.
- GET /api/chats/[id] serves messages: projectNative(record.messages).
Client:
- chat-transport.ts — the 409 retry is IDENTITY (re-POST the same event
body after the turn settles — the event was never persisted
pre-acceptance; the merge logic is gone). The transport consumes the
stashed input event via prepareSendMessagesRequest and throws loudly on
a missing stash.
- chat.tsx — every send call site stashes exactly one ChatInput event
(text, answer, verdict, continue) before invoking the SDK send.
Tests: full native migration (chat-flow rewritten to input events and
native record pins; jobs/extraction/demos/graph/usage/plan-blocks/
probe-done/reattach-transport converted; record-adapter.test.ts deleted;
chat-transport tests pin the identity retry). 982/982, typecheck, lint,
build all green.
|
||
|
|
b87a6925af |
Wave A: pi-native tool layer — delete LhTool/toPiTool, TypeBox everywhere, terminate on questions
- 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. |
||
|
|
6b13583e96 |
wave 7: the prober reads its map — graph-context injection dies whole
The owner's zero-configuration ruling (2026-09-23): nothing configures the graph beyond chat models. The prior-knowledge injection — its budget dial, its runner, its marker part, its dock/dispatch entries — is removed across all bands; the prober now reads the learner's persistent knowledge map on demand through two gated tools instead: - list_sessions: every birth session, names + kinds + counts + dates (graphSessionIndex, Platform's core seam) - list_propositions(session): what was born there + the session's own latest understanding level + that evaluation's date (graphSessionPropositions) — names/levels/dating ONLY, no statement text (owner ruling: the map's organization is the prober's business, its prose is not) Registered via createProberTools when chat-handler passes graphStore (the graphToolsEnabled gate decided server-side, per-turn fresh); the prober doctrine gains the map-review nudge at session start. Also: runner's JobKind special-casing collapses (KIND_ROLE/KIND_RUNNERS are plain records again), semantic-clusters.ts dies with its test, the embedder dep's doc tells the wave-7 stub story, and ports.ts's readClusterLabels/readNamingRuns surface stays untouched for Platform's ledger-port interlude. 848/848 tests, typecheck/lint/build green. |
||
|
|
ed264c936d |
feat(search): web_search onto the injected port — Google CSE retired, research honestly disabled
Chunk A of the search port wave (2026-09-20), per master's assignment: - web.ts: the Google Custom Search backend is gone — web_search now runs on the core search port (SearchProvider), injected as a dependency (import law: the tool never imports server code). createWebTools(search) resolves the LIVE provider fresh per call; null = the honest unavailability string (no silent fallback, owner decision). web_fetch unchanged (needs no engine). - runner: JobRunnerDeps.searchProvider — the server composition threaded into the researcher; jobs already in flight when a key disappears still fail honestly through the same seam, never crash. - jobTools: the research dispatch tool DROPS from the teacher's registry when ctx.searchAvailable() is false (owner decision: registry-level drop — the model never sees the tool, no doomed dispatch, no error card). Absent seam = available (tests). chat-handler injects server/searchAvailable + searchProvider. - tests: stubFetch (the Google wire stub) retired; jobs.test.ts threads a recording stub provider through all four research runners and asserts the scripted query hit the port; new web-search.test.ts covers the factory contract (null → honest string, hits, error strings, fresh resolution, web_fetch independence) + the registry drop matrix. Gates: typecheck, 793/793 tests, lint, build — all green. |
||
|
|
f1373d5de5 |
feat(search): Tavily + Brave behind a search-provider port; Google CSE retires
Search port + model slots wave, chunk A (owner design, 2026-09-20):
- core/search.ts: SearchHit + SearchProvider port + SearchStatus —
pure, browser-safe; adapters arrive as injected dependencies.
- server/search.ts: Tavily + Brave adapters (wire shapes VERIFIED
against docs.tavily.com's OpenAPI + Brave's dashboard docs — Tavily
auth is the Bearer header, not the legacy body api_key; Brave is
X-Subscription-Token) + engine resolution per owner decisions: a key
ALONE activates its engine (no engine choice needed), explicit
agent.search.engine pins the preference (both keys: Tavily first),
a pinned engine with a missing key is honestly unavailable — no
silent fallback, no default engine.
- Curated keys catalog: GOOGLE_SEARCH_API_KEY/GOOGLE_CSE_ID retire
(CSE closed to new customers, dies Jan 2027 — stored values become
preserved unknown vars); TAVILY_API_KEY (tvly- shape) + BRAVE_API_KEY
(length guard) join. UI keys-section TOOL_IDS now derives from the
catalog — cannot drift.
- GET /api/search: {available, engine, reason} — the honest status for
the settings gray-out; researcher-disable semantics ride the same
signal (Teacher stream rewires the tool onto the port).
- agent.search settings field (schema + sanitizer + strict parser);
web.ts's WebSearchResult is now the core SearchHit (re-exported).
|
||
|
|
36c5d19437 |
fix(tools): the abort cascade — kill running tool calls, honest activity labels
Root cause of the live-test bugs (2026-09-19): pi passes every run's
AbortSignal to tool execute, but toPiTool dropped it — agent.abort()
only landed at the next step boundary, so a long local grep held its
turn hostage until the dev server's memory restart killed the run
mid-tool, freezing the record at its write-ahead state (the 'went
back in time' rollback).
- lh-tool: execute ctx gains signal (parent + timeout combined); every
call races a 30s system-level timeout with a 250ms cooperative grace —
abort kills a stuck tool but lets a poll-the-signal tool deliver its
partial results. Pre-aborted signals never fire listeners: checked
upfront. toolTimeoutMs is the test seam (pdfModelOverride idiom).
- fs-shared: walkFiles returns {timedOut, aborted} instead of throwing —
abort yields partial results.
- grep: the scan phase was unbounded (the walk's 10s deadline only
bounded collection) — same budget now checked per file; find/grep
check the signal and return partials with a notice.
- pending/chat: streamingToolActivity names the ACTUAL running tool —
a part sits input-streaming through its whole execution, so the old
shimmer called minutes-long greps 'Writing a question…'.
Tests: toPiTool guard (forward, abort, timeout, grace, question
bypass, ToolInputError passthrough), walk mid-abort, find/grep
pre-abort + mid-run abort, activity labels. Gates: 617/617.
|
||
|
|
7a3e19e95f |
feat(sources v1): the agent locates files itself — ls/find/grep + tool-call cards
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. |
||
|
|
9a90dd51d9 |
feat(T2b): sources v0 — read tool + pdf-reader sub-agent
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. |
||
|
|
4ba594bab5 |
fix(arch): parse, don't cast — the UI parse seam for model-authored input
The architectural answer to the validation-crash question: validation IS
first at the model-facing boundary (pi validates arguments before
execute; failures return to the model same-turn — all three live
incidents retried successfully). But the same bytes also exist UNVALIDATED
by design in two other places: the live wire streams raw tool-input
deltas DURING generation, and the transcript keeps the raw input of
REJECTED calls (honesty + the replay invariant). The consumers were
supposed to trust part STATES — but that trust was enforced by
TypeScript casts ('part.input as X'), which are compile-time lies with
no runtime check behind them. That is the hole both crashes came
through, and it should not have been possible.
The structural rule now: model-authored INPUT is consumed only through
parsers running the SAME zod schema the model faced — boundary-valid ⇔
consumer-parseable, mechanically. Server/learner-authored OUTPUT stays
trusted (our code authors it).
- src/core/tool-parts.ts (new): parseQuizInput (display parse over the
shared quizInputBase — strips the teacher-only nodeId, accepts both
prober and teacher boundary shapes), parseAskInput, parseProbeDoneInput.
- contracts/probe.ts: probeDoneInputSchema is now core-owned zod — the
probe_done tool declares it (single source of truth), the seam runs
the literal same schema.
- question-dock.tsx: the dock parses its input at entry; unparsable
renders the quiet malformed-question fallback, never a crash.
- probe-done-card.tsx: the input preview goes through the parse; the
old state reasoning and shape guards collapse into it.
Pinned in tests/tool-parts.test.ts: both exact live crash shapes
(stringified quiz options, stringified assessment), boundary-parity
cases for all three schemas, nodeId stripping, mid-stream partials;
render-cards pins the dock fallback markup.
Gates: tsc, lint, build, 328/328.
|
||
|
|
2d717fd932 |
Swap the server engine to pi-ai/pi-agent-core (Band B+C of docs/pi-engine-swap.md)
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).
|
||
|
|
377d4b6548 |
fix: the handoff freeze + the free-form mermaid pass
Two user-reported problems from the muscle-protein-synthesis session, both diagnosed from the live data (chat_mtxeml9e_6118e59f). The freeze: after probe_done, the open session never showed the teacher's wake turn — only navigating away and back loaded it. Not an agent-switch problem: the server-side handoff worked perfectly (write-ahead persist, liveness registry, the sidebar's busy dot saw the wake the whole time). The gap was client attach: the only mid-session wake detector the open page has is the jobs poll, and probe-phase sessions have zero job traffic, so that poll is OFF; the post-turn adoption poll read turnInFlight=false at ~0ms (the wake fires at ~0.75-1.2s, after the pump's grace), adopted, and stopped. Fix: the post-turn adoption effect now keeps a short WATCH WINDOW after adopting (~4s, 8x500ms) — if turnInFlight flips true, it hands off to the existing wake-adoption poll (busy shimmer, adopt at finish). The mermaid failures: four failed plan_init calls, all caused by ONE hidden rule — underscores are forbidden in node ids (mps_translation, net_balance, per_meal). The id regex rejected '_' BEFORE the slug check, so the specific, actionable message never fired; the generic 'can't read the edge — expected a[title] --> b[title]' lied (the syntax was exactly that), and the teacher spent three attempts hunting punctuation it suspected (colons, commas, multi-word labels) — visible in its own reasoning trail. Per the user's principle — enforce only the slugs, free everything else: underscores are first-class slug chars; the id CANDIDATE class is broad so every bad id reaches the specific slug message; chained edges (a --> b --> c) expand to pairs; ==>, -.-> are edge synonyms; any direction header accepted (we layout ourselves); id["title"] quote pairs are stripped. The tool description, zod describe, and the teacher's planning prompt now state the one strict rule instead of the old subset liturgy. Verified: tsc, 305/305 (the exact muscle-protein mermaid is now a passing test), lint, build; real-browser puppeteer smoke on live GLM — the teacher's wake turn appeared in the open page ~50s after the probe concluded with ZERO navigation, verdict gate + first teach check + plan window all live in-page (the single console 404 is the app's missing favicon, environmental noise). |
||
|
|
5653c77a29 |
feat: probe/teach agent split — explicit phase markers, position-only plan map
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). |
||
|
|
67fbc0575f |
Author-once plan protocol: single mermaid plan_init + derived statuses + live quiz nodeId validation
- 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). |
||
|
|
99fa3f0847 |
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. |
||
|
|
59e1fee850 |
Turn services + agent DI: chat-handler becomes the composition root (stage 4)
The chat turn is now composed, not implemented, by chat-handler.ts:
- src/server/turn-service.ts: one named, error-contained seam per turn
responsibility — buildTeacherContext (W8 read channel), persistTurn
(canonical state write), spawnTurnJobs (async protocol kick-off),
foldTurnIntoGraph (session→graph consolidation; the extraction-pipeline
stage swaps this body behind the same seam). Each takes its deps
explicitly; failures log one [chat <id>] line and never touch the
response.
- chat-handler.ts wires: storage ports (stores), the job runner, the
teacher tools (ctx = chatId + injected ChatStore), and the services.
It no longer contains any logic beyond request validation.
Agent layer now receives dependencies instead of importing them — a new
lint zone enforces it (src/agent never imports src/server or src/store;
verified to fire both ways):
- runner.ts: createJobRunner({ chatStore, jobStore }) factory returning
the spawn hook + the three job runners; the model-override test seam
stays module-level. Bodies otherwise verbatim.
- plan tools: persist through the ctx-injected ChatStore
(TeacherToolContext = { chatId, chatStore }).
- teacher-agent's dependency-free default registry is now the true
client-side set: questionTools + jobTools (jobTools was ctx-parameter-
shaped but never used its ctx — now honestly context-free). Production
always passes createTeacherTools({ chatId, chatStore }); tests that
drive a bare mock model get the client-side set unchanged.
Zero behavior change: tsc clean, 190/190 tests, eslint clean, production
build clean. Tests inject the same fs ports the chat-handler does.
|
||
|
|
1167157c23 |
Storage ports: ChatStore/GraphStore/JobStore + adapters + core inversion (stage 3)
Per-entity semantic ports (src/core/ports.ts), one adapter family
(src/store/*, fs JSON/JSONL), exactly one composition point
(src/server/stores.ts). Dependency direction is now machine-checked:
adapters implement core interfaces; core never imports an adapter.
The inversion:
- core/graph.ts (emitChat/getGraph/graphSnapshot/foldStaleChats) is now
pure — it takes the storage ports as explicit parameters (GraphDeps).
No store imports; newGraphNodeId is local; node id format unchanged.
- core/graph-context.ts buildGraphContext() no longer defaults to the
live ledgers: the chat-handler hands in getGraph(graphStore) per turn.
- store adapters export exactly one object each implementing their port
(chatStore/graphStore/jobStore); bodies otherwise verbatim.
NodeMapping + SessionCheckpoint moved store/graph -> core/contracts
(they ARE contract shapes); CreateJobInit/JobPatch live in ports.
- titleFromPrompt moved store/chats -> core/contracts (pure helper,
planNodeStatus precedent). Store modules now contain persistence only.
- server/stores.ts is the composition root; server/{chats,graph,jobs}.ts
and chat-handler wire from it; agent (plan tools, sub-agent runner)
call the port objects. The shell surface (@/src/server exports) is
unchanged.
Lint law extended: the whole of src/core is now a purity zone (was a
file glob with a graph/graph-context exemption). Verified it fires on
core->store imports; tsc clean, 190/190 tests, eslint clean, prod build
clean (prerender 6/6). Tests pass deps explicitly — graph.test uses the
same fs-adapter pair stores.ts wires in production; graph-context.test
keeps its in-memory ledgers as a GraphStore implementation.
|
||
|
|
e92140f993 |
Shell/core import law: public API barrels + ESLint enforcement (stage 2)
The shell now imports through exactly two barrels, enforced by
import/no-restricted-paths (npm run lint):
components/** -> "@/src/core" (pure core API, browser-safe)
app/** -> "@/src/core" + "@/src/server" (server entries)
Zone violations are lint errors with architecture-pointer messages; the
pure-core modules (contracts, question-core, plan-tree, phases, pending,
math-delims, ui-message) may never import agent/server/store.
Supporting moves (direction: IO layers -> core):
- core/index.ts: the core public API barrel
- core/ui-message.ts: moved from src/agent (it was shell-facing all along)
- quiz/ask input zod schemas moved agent/tools -> core/question-core
(the question contract, alongside the grading logic)
- ChatRecord/ChatSummary moved store/chats -> core/contracts (single schema)
- GraphSnapshot/NodeStateClass moved core/graph -> core/contracts (the
/api/graph payload IS a contract; the pure renderer needs only these)
- src/server/{chats,graph,jobs}.ts + index.ts: thin server entries for the
transport layer; GET /api/graph's fold side-effect now lives behind
graphView() instead of inline in the route
Gates: tsc clean, 190/190 tests, eslint clean (law verified to fire on
store/agent/subpath violations), production build clean.
(Repo repair en route: a git object (app/chat/[id] tree) was missing from
the object store — reconstructed byte-exactly via mktree from its blob;
the broken node_modules that stalled the build was cleanly reinstalled.)
|
||
|
|
6f1b35b2c2 | W6: demo + visual artifacts, verifying builders | ||
|
|
df16750560 |
W2: async job infra + researcher sub-agent
- src/store/jobs.ts: per-job JSON store in data/jobs (create/get/update/list, dedup by toolCallId) - src/agent/tools/jobs.ts: research tool — client-side (no execute), turn ends at the call; input per contracts ResearchToolInput - src/agent/tools/web.ts: server-side web_search (Google CSE) + web_fetch (Readability/Turndown/linkedom, Jina fallback) — errors return as strings, never thrown into the agent loop. Deps: @mozilla/readability, linkedom, turndown (+@types/turndown dev) - src/agent/subagents/researcher.ts: isolated ToolLoopAgent (web tools only, 12-step cap), researcher prompt ported from pi - src/agent/subagents/runner.ts: spawnPendingJobs hook + runResearchJob — fire-and-forget, activity line per tool call, fulfills the part in the LATEST chat record (output-available) before flipping job status; failure still fulfills the part so the teacher learns the outcome - src/server/chat-handler.ts: onFinish spawn hook after saveMessages - app/api/jobs/route.ts: GET ?chatId= polling endpoint - components/artifacts/job-card.tsx + registry entry: polling activity card (2s) → syncFromServer on finish; research-brief card (markdown + sources) - src/agent/prompts/20-jobs.md: research doctrine for the teacher - tests/jobs.test.ts: store CRUD, spawn hook via scripted mock teacher, runner completion/failure with fake model + stubbed fetch |
||
|
|
1e18d804fc |
W1: plan-tree tools — pure validated ops, eight server-side plan_* tools, tree persistence, teacher prompt fragment
- src/core/plan-tree.ts: pure tree ops (createTree/addNode/updateNode/removeNode/
addEdge/removeEdge/setCurrent/markDone), DAG invariants (unique ids, existing
endpoints, no self-loops/duplicates/cycles), cascade remove, version bumps on
state changes only (true no-ops for idempotent setCurrent/markDone).
- src/agent/tools/plan.ts: plan_init (refuses re-init), plan_add_node (optional
parents → requires edges), plan_update_node, plan_remove_node, plan_add_edge,
plan_remove_edge, plan_set_current, plan_mark_done. Execute = getChat → op →
saveTree → PlanToolResult with the full updated tree; PlanTreeError and missing
chats become {ok:false, error, tree}.
- src/agent/prompts/10-plan-tree.md: tree-authoring doctrine (when: post-probe;
grain: one reasoning step per node; discipline: set_current on start,
mark_done on demonstrated mastery; freedom: mid-session restructure).
- tests/plan-tree.test.ts: every validation error path, version semantics,
cascade remove, no-mutation, tool round-trips against the real chat store.
|
||
|
|
c55bae0de6 |
Wave 0: shared contracts + extension points
- src/core/contracts.ts: plan tree / graph / ledger / job protocol / artifact
registry types — the single source of truth for all workstreams
- tools: per-request factory createTeacherTools({chatId}) composing
questionTools + planTools (stub) + jobTools (stub); quiz gains nodeId
- prompt composition: system-prompt.md + sorted src/agent/prompts/*.md
- ChatRecord.tree + saveTree; pending gate scoped to quiz/ask (+findPendingJobCall)
- chat.tsx: artifact renderer extension point + ChatActionsContext.syncFromServer
|