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.
This commit is contained in:
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
||||
import { ToolLoopAgent, stepCountIs, type LanguageModel, type ToolSet } from "ai";
|
||||
import { openrouter } from "@openrouter/ai-sdk-provider";
|
||||
import { PATHS } from "../../config.ts";
|
||||
import { createTeacherTools } from "./tools.ts";
|
||||
import { createTeacherTools, jobTools, questionTools } from "./tools.ts";
|
||||
|
||||
/**
|
||||
* The teacher agent. Everything pi used to provide (agent loop, tool calling,
|
||||
@@ -62,10 +62,11 @@ export function createTeacherAgent(model?: LanguageModel, tools?: ToolSet, graph
|
||||
reasoning: { enabled: true, effort: "medium" },
|
||||
}),
|
||||
instructions: withGraphContext(graphContext),
|
||||
// Tools are per-request (plan tools close over chatId); chat-handler
|
||||
// passes createTeacherTools({ chatId }). Tests pass a mock model and get
|
||||
// the default question-only set.
|
||||
tools: tools ?? createTeacherTools({ chatId: "" }),
|
||||
// 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() },
|
||||
// Question and job tools are client-side — a call ends the server turn
|
||||
// automatically. stepCountIs is just the safety net.
|
||||
stopWhen: stepCountIs(20),
|
||||
|
||||
Reference in New Issue
Block a user