- tools/practice.ts: problem (terminate pose; commitment hidden in args) and conclude (non-terminating; outcome/verdict/reveal/rung/missClass/ retest), gated by activeProblemOf — pose-while-active and conclude- with-nothing-active both return invalidInputResult. - prompts/practice/00-base.md (voice + accuracy + tool mechanics) and 10-doctrine.md (the owner's doctrine verbatim; one approved edit — §I material arrival generalized). Practice composes its own directory ALONE: no teaching base, no shared fragments. - createPracticeAgent (teacher-agent.ts); chat-handler branch — kind 'practice' builds the practitioner from turn 1 and skips extraction. - problemResult/concludeResult acks in tool-execute (conclude text single-sourced: CONCLUDE_ACK_TEXT).
151 lines
6.6 KiB
TypeScript
151 lines
6.6 KiB
TypeScript
/**
|
|
* THE PRACTICE TOOLS (practice sessions, owner design 2026-10-04): the
|
|
* practice agent's ENTIRE toolset — `problem` (pose: terminates the turn;
|
|
* the commitment rides the call's arguments, hidden from the learner UI)
|
|
* and `conclude` (end the problem: normal result, the turn continues so
|
|
* the same reply can reveal + pose the next). The gating law (owner
|
|
* ruling): an agent cannot pose while a problem is active and cannot
|
|
* conclude with nothing active — activeProblemOf is the single fold both
|
|
* gates read.
|
|
*
|
|
* Mechanics-only descriptions (the doctrine ruling, 2026-10-04): the tool
|
|
* descriptions carry WHAT to pass and WHAT happens — the theory, the
|
|
* commit-before-pose law, the hint ladder all live in the system prompt
|
|
* (prompts/practice/10-doctrine.md).
|
|
*/
|
|
|
|
import { Type, type Static } from "typebox";
|
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
import { activeProblemOf } from "../../core/native-messages.ts";
|
|
import type { LhMessage } from "../../core/native-messages.ts";
|
|
import { concludeResult, invalidInputResult, problemResult } from "../tool-execute.ts";
|
|
|
|
/** The record-reading context the gates need (the plan-tools idiom). */
|
|
export interface PracticeToolContext {
|
|
chatId: string;
|
|
chatStore: { getChat(id: string): { messages: LhMessage[]; kind?: string } | null };
|
|
}
|
|
|
|
// ─── problem: the pose ───────────────────────────────────────────────────────
|
|
|
|
export const problemInputObject = Type.Object({
|
|
statement: Type.String({
|
|
description:
|
|
"REQUIRED — the problem as the learner sees it (markdown + LaTeX). Must be self-contained and " +
|
|
"solvable from the session's material. NEVER announces the target proposition.",
|
|
}),
|
|
mode: Type.Union([Type.Literal("recall"), Type.Literal("discrimination"), Type.Literal("composition")], {
|
|
description: 'REQUIRED — the demand mode: "recall" | "discrimination" | "composition".',
|
|
}),
|
|
target: Type.Array(Type.String(), { minItems: 1, description: "REQUIRED — the proposition id(s) or names this problem exercises." }),
|
|
modelSolution: Type.String({
|
|
description: "REQUIRED — the full solution, moves in order, committed BEFORE the learner sees the problem.",
|
|
}),
|
|
decisionPoints: Type.Array(Type.String(), {
|
|
description: "REQUIRED — each juncture where a not-yet-active proposition must be selected.",
|
|
}),
|
|
expectedWrongMoves: Type.Array(
|
|
Type.Object({
|
|
move: Type.String({ description: "The wrong move an honest attempt may take." }),
|
|
resolution: Type.String({ description: "What resolves it back to the right move." }),
|
|
}),
|
|
{ description: "REQUIRED — the wrong moves you expect, each with its resolution." },
|
|
),
|
|
strategy: Type.String({ description: "REQUIRED — why this shape: the situation class, parse cost, density." }),
|
|
rationale: Type.String({
|
|
description: "REQUIRED — why this problem now: target + mode + difficulty against the session's last outcomes.",
|
|
}),
|
|
});
|
|
|
|
export type ProblemInput = Static<typeof problemInputObject>;
|
|
|
|
export function problemTool(ctx: PracticeToolContext): AgentTool<typeof problemInputObject> {
|
|
return {
|
|
name: "problem",
|
|
label: "problem",
|
|
description:
|
|
"Pose one practice problem. Ends the turn — the learner works in the thinking field; their thinking " +
|
|
"traces and answer commits arrive back as labeled messages. The full commitment (solution, decision points, " +
|
|
"expected wrong moves) rides this call's arguments and stays hidden from the learner until you conclude. " +
|
|
"Only one problem may be active — conclude it before posing the next.",
|
|
parameters: problemInputObject,
|
|
executionMode: "sequential",
|
|
execute: async (_toolCallId, _input) => {
|
|
const record = ctx.chatStore.getChat(ctx.chatId);
|
|
if (record && activeProblemOf(record.messages) !== null) {
|
|
return invalidInputResult("problem", [
|
|
"a problem is already active — call conclude on it before posing the next one",
|
|
]);
|
|
}
|
|
return problemResult();
|
|
},
|
|
};
|
|
}
|
|
|
|
// ─── conclude: the end of a problem ──────────────────────────────────────────
|
|
|
|
export const concludeInputObject = Type.Object({
|
|
outcome: Type.Union(
|
|
[
|
|
Type.Literal("solved"),
|
|
Type.Literal("solvedWithHints"),
|
|
Type.Literal("movedOn"),
|
|
Type.Literal("surrendered"),
|
|
Type.Literal("dismissed"),
|
|
],
|
|
{ description: 'REQUIRED — "solved" | "solvedWithHints" | "movedOn" | "surrendered" ("dismissed" is synthesized, never yours).' },
|
|
),
|
|
verdict: Type.String({
|
|
description: "REQUIRED — the plain-language verdict: what was credited, where it broke. On a wrong answer: localize the break, do NOT give the correction.",
|
|
}),
|
|
solutionReveal: Type.String({
|
|
description: "REQUIRED — the committed solution, shown to the learner now (the reveal spend; moving on spends it).",
|
|
}),
|
|
rung: Type.Union(
|
|
[Type.Literal("R0"), Type.Literal("R1"), Type.Literal("R2"), Type.Literal("R3"), Type.Literal("R4"), Type.Literal("R5"), Type.Null()],
|
|
{ description: "REQUIRED — the hint-ladder rung the problem needed (null = no hint was given)." },
|
|
),
|
|
missClass: Type.Union(
|
|
[
|
|
Type.Literal("cue"),
|
|
Type.Literal("discrimination"),
|
|
Type.Literal("execution"),
|
|
Type.Literal("careless"),
|
|
Type.Literal("structural"),
|
|
Type.Null(),
|
|
],
|
|
{ description: "REQUIRED — the miss classification (null = no miss)." },
|
|
),
|
|
retest: Type.Boolean({ description: "REQUIRED — true marks the problem for a fresh-surface retest before the session ends." }),
|
|
});
|
|
|
|
export type ConcludeInput = Static<typeof concludeInputObject>;
|
|
|
|
export function concludeTool(ctx: PracticeToolContext): AgentTool<typeof concludeInputObject> {
|
|
return {
|
|
name: "conclude",
|
|
label: "conclude",
|
|
description:
|
|
"Conclude the active problem: records the outcome + verdict, reveals the committed solution, and frees " +
|
|
"the learner's composer. The turn CONTINUES after conclude — reveal, then pose the next problem (or wrap " +
|
|
"up in text). Nothing may conclude unless a problem is active.",
|
|
parameters: concludeInputObject,
|
|
executionMode: "sequential",
|
|
execute: async (_toolCallId, _input) => {
|
|
const record = ctx.chatStore.getChat(ctx.chatId);
|
|
if (!record || activeProblemOf(record.messages) === null) {
|
|
return invalidInputResult("conclude", ["no active problem to conclude — pose one first"]);
|
|
}
|
|
return concludeResult();
|
|
},
|
|
};
|
|
}
|
|
|
|
/** The practice toolset (the practice agent's whole registry). */
|
|
export function practiceTools(ctx: PracticeToolContext): Record<string, AgentTool<any>> {
|
|
return {
|
|
problem: problemTool(ctx),
|
|
conclude: concludeTool(ctx),
|
|
};
|
|
}
|