2026-08-22 02:18:27 +03:00
|
|
|
import * as fs from "node:fs";
|
2026-08-22 09:06:24 +03:00
|
|
|
import * as path from "node:path";
|
|
|
|
|
import { ToolLoopAgent, stepCountIs, type LanguageModel, type ToolSet } from "ai";
|
2026-08-22 02:18:27 +03:00
|
|
|
import { PATHS } from "../../config.ts";
|
2026-08-31 22:16:03 +03:00
|
|
|
import { createTeacherTools, jobTools, questionTools } from "./tools.ts";
|
2026-09-01 01:47:47 +03:00
|
|
|
import { modelFor } from "./models.ts";
|
2026-08-22 02:18:27 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The teacher agent. Everything pi used to provide (agent loop, tool calling,
|
|
|
|
|
* streaming, multi-step) is these few lines now.
|
|
|
|
|
*
|
|
|
|
|
* The model is injectable so tests can pass a MockLanguageModelV4 and drive
|
|
|
|
|
* deterministic scripted lessons (see tests/chat-flow.test.ts).
|
2026-08-22 09:06:24 +03:00
|
|
|
*
|
2026-08-22 11:41:50 +03:00
|
|
|
* The optional graphContext (buildGraphContext() in src/core/graph-context.ts,
|
|
|
|
|
* wired by the chat-handler per request) is appended after the composed
|
|
|
|
|
* fragments; absent/empty means a new learner — nothing is injected.
|
|
|
|
|
*
|
2026-08-22 09:06:24 +03:00
|
|
|
* The system prompt is COMPOSED: system-prompt.md is the base, then every
|
|
|
|
|
* src/agent/prompts/*.md fragment in sorted filename order (workers add
|
|
|
|
|
* fragments — nobody edits shared sections of the base prompt). Prefix
|
|
|
|
|
* fragments with a numeric order key: 10-plan-tree.md, 20-jobs.md, …
|
2026-08-22 02:18:27 +03:00
|
|
|
*/
|
|
|
|
|
|
2026-08-22 09:06:24 +03:00
|
|
|
const PROMPTS_DIR = path.join(PATHS.root, "src", "agent", "prompts");
|
2026-08-22 02:18:27 +03:00
|
|
|
|
2026-08-22 09:06:24 +03:00
|
|
|
function composeInstructions(): string {
|
|
|
|
|
const base = fs.readFileSync(PATHS.teacherPrompt, "utf-8");
|
|
|
|
|
if (!fs.existsSync(PROMPTS_DIR)) return base;
|
|
|
|
|
const fragments = fs
|
|
|
|
|
.readdirSync(PROMPTS_DIR)
|
|
|
|
|
.filter((f) => f.endsWith(".md"))
|
|
|
|
|
.sort()
|
|
|
|
|
.map((f) => fs.readFileSync(path.join(PROMPTS_DIR, f), "utf-8").trim())
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
return [base.trim(), ...fragments].join("\n\n---\n\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const instructions = composeInstructions();
|
|
|
|
|
|
2026-08-22 11:41:50 +03:00
|
|
|
/**
|
|
|
|
|
* Compose the full instruction string: base prompt + fragments, then — when
|
|
|
|
|
* the session carries graph context (W8, vision §1 "the graph as session
|
|
|
|
|
* context") — the auto-generated knowledge-graph block appended after them as
|
|
|
|
|
* a clearly-delimited section (the 50-graph-context.md fragment teaches the
|
|
|
|
|
* teacher how to read it). The fragment mechanism itself stays untouched.
|
|
|
|
|
*/
|
|
|
|
|
function withGraphContext(graphContext: string | null | undefined): string {
|
|
|
|
|
if (!graphContext) return instructions;
|
|
|
|
|
return `${instructions}\n\n---\n\n${graphContext}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createTeacherAgent(model?: LanguageModel, tools?: ToolSet, graphContext?: string | null) {
|
2026-08-22 02:18:27 +03:00
|
|
|
return new ToolLoopAgent({
|
2026-09-01 01:47:47 +03:00
|
|
|
// Model choice + thinking budget live in config.ts (MODELS.teacher).
|
|
|
|
|
// Reasoning tokens stream as reasoning parts into the UI's collapsible block.
|
|
|
|
|
model: model ?? modelFor("teacher"),
|
2026-08-22 11:41:50 +03:00
|
|
|
instructions: withGraphContext(graphContext),
|
2026-08-31 22:16:03 +03:00
|
|
|
// Tools are per-request (plan tools close over the injected ctx);
|
|
|
|
|
// chat-handler passes createTeacherTools({ chatId, chatStore }). The
|
|
|
|
|
// dependency-free default is the question-only set (client-side tools,
|
|
|
|
|
// no storage needed) — used when tests pass a bare mock model.
|
|
|
|
|
tools: tools ?? { ...questionTools, ...jobTools() },
|
2026-08-22 09:06:24 +03:00
|
|
|
// Question and job tools are client-side — a call ends the server turn
|
|
|
|
|
// automatically. stepCountIs is just the safety net.
|
2026-08-22 02:18:27 +03:00
|
|
|
stopWhen: stepCountIs(20),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type TeacherAgent = ReturnType<typeof createTeacherAgent>;
|