Files
learnharness/config.ts
T
amosblomqvist 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).
2026-09-11 06:44:20 +03:00

73 lines
3.3 KiB
TypeScript

import path from "node:path";
import { fileURLToPath } from "node:url";
/** Repository root (this file is at <root>/config.ts). */
export const ROOT = path.dirname(fileURLToPath(import.meta.url));
export const PATHS = {
root: ROOT,
/** Teacher system prompt — the agent's instructions (src/agent/). */
teacherPrompt: path.join(ROOT, "src", "agent", "system-prompt.md"),
data: path.join(ROOT, "data"),
/** Chat sessions (AI SDK UIMessage transcripts, one JSON file per chat). */
chats: path.join(ROOT, "data", "chats"),
graph: path.join(ROOT, "data", "graph"),
} as const;
// ─── Models ────────────────────────────────────────────────────────────────
// THE single place every LLM is chosen and tuned. .env stays for secrets
// (API keys) only — model choice and thinking budget are product tuning,
// not deployment environment, and this section is the one place they grow.
// `effort` is OpenRouter's reasoning scale (docs: reasoning-tokens):
// "none" | "minimal" | "low" | "medium" | "high" | "xhigh".
// "none" disables reasoning tokens entirely.
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
export interface ModelSpec {
/** OpenRouter model id (e.g. "z-ai/glm-5.3-flash"). */
model: string;
/** Thinking budget — what the model may spend on reasoning tokens. */
effort: ReasoningEffort;
}
export const MODELS = {
/** The prober (agent A) — edge-bracketing questions; keep it lively and adaptive. */
prober: { model: "z-ai/glm-5.3-flash", effort: "minimal" },
/** The teacher (agent B) — planning + streaming, lesson-facing: smart, but keep it lively. */
teacher: { model: "z-ai/glm-5.3-flash", effort: "minimal" },
/** Background sub-agents: fast and cheap beats deep thought. */
researcher: { model: "z-ai/glm-5.3-flash", effort: "minimal" },
demoBuilder: { model: "z-ai/glm-5.3-flash", effort: "minimal" },
/** Must be vision-capable: it looks at rendered SVGs. */
visualMaker: { model: "qwen/qwen3.8-max", effort: "minimal" },
/** Extraction is judgment over given evidence — light thinking suffices. */
extractor: { model: "z-ai/glm-5.3-flash", effort: "minimal" },
/** The auditor verifies evidence exists; keep it cheap. */
auditor: { model: "z-ai/glm-5.3-flash", effort: "minimal" },
} satisfies Record<string, ModelSpec>;
export type ModelRole = keyof typeof MODELS;
/**
* The extraction pipeline (vision §1 construction): the sole graph writer.
* Triggered per chat once this many NEW quiz answers have landed since the
* chat's last extraction checkpoint. The trigger mechanism is a v1
* placeholder — deliberately isolated behind one seam
* (server/turn-service.ts) so it can be swapped without touching the
* pipeline.
*/
export const EXTRACTION = { everyNQuizAnswers: 3 } as const;
/**
* The LIVE graph store (src/store/graph.ts) — owned by the app's extraction
* pipeline. Line shapes: src/core/contracts.ts.
*/
export const LEDGER_FILES = {
nodes: path.join(PATHS.graph, "nodes.jsonl"),
edges: path.join(PATHS.graph, "edges.jsonl"),
events: path.join(PATHS.graph, "events.jsonl"),
mappings: path.join(PATHS.graph, "mappings.jsonl"),
sessions: path.join(PATHS.graph, "sessions.jsonl"),
} as const;