question tools: phase-visible names, hardened teach schemas, context-slim seams
The tools_fails_2 diagnosis (glm-5.3 dropping trailing required fields):
- probe-phase quiz/explain renamed to probe/recall — a shared name carried
the probe call shape into teaching (the first teach quiz dropped nodeId);
display seams accept the old names forever
- teach quiz+explain schemas lead with the required pair (type, nodeId);
explain gains the predict/check type and joins the reveal law (checks
collapse, predictions arrive open; no-type records render check-style)
- plan_init/probe_done results go context-slim (owner, context diet):
content = validated nodeIds + binding-id reminder / fixed ack; details
keeps the tree for the popup and {ok:true} — serializer-verified that
details never reaches the model; failure echoes dropped
- the cards carry the tool's own name (Probe/Recall) and the quiz type
reads on the label (Prediction/Check) in composer and transcript
This commit is contained in:
+34
-11
@@ -3,9 +3,17 @@ import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import { serverExecute } from "../tool-execute.ts";
|
||||
import type { PlanToolResult, PlanTree } from "../../core/contracts.ts";
|
||||
import type { ChatStore } from "../../core/ports.ts";
|
||||
import { createTree, emptyTree, PlanTreeError } from "../../core/plan-tree.ts";
|
||||
import { createTree, PlanTreeError } from "../../core/plan-tree.ts";
|
||||
import { parseMermaidPlan } from "./mermaid-plan.ts";
|
||||
|
||||
/** The binding-id reminder riding plan_init's slim content (owner,
|
||||
* 2026-10-09): plan_init is the exact moment the node ids become
|
||||
* load-bearing — every later quiz/explain call must reference one. The
|
||||
* field-drop incidents (tools_fails_2) showed the turn's tail is where
|
||||
* discipline slips; the reminder lands right there. */
|
||||
const PLAN_NODE_IDS_REMINDER =
|
||||
"These are the plan's node ids — the only valid nodeId values for every quiz and explain call from here on. Reference them exactly.";
|
||||
|
||||
/** What the plan tools close over (the plan-tree half of the tool context). */
|
||||
export interface PlanToolContext {
|
||||
chatId: string;
|
||||
@@ -33,10 +41,13 @@ export interface PlanToolContext {
|
||||
*
|
||||
* The former mutation tools (add/update/remove node/edge, set_current,
|
||||
* mark_done) are gone by design: restructuring is a rewrite, tracking is
|
||||
* derived. SERVER-SIDE with execute: parse → validate → persist → return
|
||||
* the FULL canonicalized tree (PlanToolResult), so the tree panel renders the
|
||||
* latest state straight from the transcript's tool outputs (the plan
|
||||
* window and the plan blocks).
|
||||
* derived. SERVER-SIDE with execute: parse → validate → persist. The
|
||||
* result is CONTEXT-SLIM (owner, context diet 2026-10-09): the model
|
||||
* WROTE the mermaid one message ago, so content never echoes the tree —
|
||||
* it carries the validated nodeIds + the binding-nodeId reminder (the
|
||||
* exact moment the ids become load-bearing); `details` keeps the full
|
||||
* tree for the record and the plan popup (latestTree reads it — the
|
||||
* agent-invisible channel, serializer-verified).
|
||||
*/
|
||||
const planInitInput = Type.Object({
|
||||
mermaid: Type.String({
|
||||
@@ -60,25 +71,37 @@ export function planTools(ctx: PlanToolContext): Record<string, AgentTool<any>>
|
||||
"`-->` edges meaning 'from must be understood before to' (chains fine). 5–15 nodes: a map, not a syllabus — one reasoning step per node, compress. " +
|
||||
"Every check you fire names the node it assesses via `nodeId`; where the lesson stands derives from those tags.",
|
||||
parameters: planInitInput,
|
||||
execute: serverExecute("plan_init", planInitInput, async ({ mermaid }): Promise<PlanToolResult> => {
|
||||
execute: serverExecute("plan_init", planInitInput, async ({ mermaid }) => {
|
||||
const { chatId, chatStore } = ctx;
|
||||
const record = chatStore.getChat(chatId);
|
||||
if (!record) return { ok: false, error: `Chat not found: ${chatId}`, tree: emptyTree() };
|
||||
const current = record.tree ?? emptyTree();
|
||||
if (!record) return { ok: false, error: `Chat not found: ${chatId}` };
|
||||
|
||||
const parsed = parseMermaidPlan(mermaid);
|
||||
if (!parsed.ok || !parsed.plan) {
|
||||
// The parser collects every problem; the teacher fixes and retries.
|
||||
return { ok: false, error: parsed.problems.join("\n"), tree: current };
|
||||
// No tree echo — the model needs the problems, not a restatement of
|
||||
// the map it is rewriting (owner, context diet 2026-10-09).
|
||||
return { ok: false, error: parsed.problems.join("\n") };
|
||||
}
|
||||
try {
|
||||
// createTree validates the structure (duplicate ids, missing
|
||||
// edge endpoints, self-loops, cycles → PlanTreeError).
|
||||
const next = createTree(parsed.plan.nodes, parsed.plan.edges);
|
||||
chatStore.saveTree(chatId, next);
|
||||
return { ok: true, tree: next };
|
||||
// Context diet: content = the validated nodeIds + the reminder (the
|
||||
// ids just became binding for every quiz/explain call); details =
|
||||
// the full tree for the record/UI — the plan popup's latestTree
|
||||
// seam, agent-invisible (the serializer sends content only).
|
||||
return {
|
||||
text: JSON.stringify({
|
||||
ok: true,
|
||||
nodeIds: next.nodes.map((n) => n.id),
|
||||
reminder: PLAN_NODE_IDS_REMINDER,
|
||||
}),
|
||||
details: { ok: true, tree: next },
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof PlanTreeError) return { ok: false, error: err.message, tree: current };
|
||||
if (err instanceof PlanTreeError) return { ok: false, error: err.message };
|
||||
throw err;
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import { serverExecute } from "../tool-execute.ts";
|
||||
import { probeDoneInputSchema, type ProbeDoneResult } from "../../core/contracts.ts";
|
||||
import { probeDoneInputSchema, PROBE_DONE_ACK_TEXT } from "../../core/contracts.ts";
|
||||
import { danglingQuestionOf, hasProbeDone, type LhMessage } from "../../core/native-messages.ts";
|
||||
import type { ChatStore } from "../../core/ports.ts";
|
||||
import { buildProbeHandoffSteer, type SteerMessage } from "../../core/contracts.ts";
|
||||
@@ -37,7 +37,10 @@ function probeConcluded(messages: readonly LhMessage[]): boolean {
|
||||
* steer through the wake pipeline that fires the teacher's first turn
|
||||
* (plan authoring) the moment the prober's turn ends. Auto-wake is the
|
||||
* user-decided trigger — the learner experiences assessment → plan as
|
||||
* one continuous flow.
|
||||
* one continuous flow. CONTEXT-SLIM (owner, context diet 2026-10-09):
|
||||
* the call's arguments already carry the assessment (durable, streamed
|
||||
* live), so the agent sees the fixed PROBE_DONE_ACK_TEXT — never an
|
||||
* echo. details = {ok:true} only.
|
||||
*
|
||||
* The first probe_done in the transcript is also the construction seam:
|
||||
* from that message on, every request builds the TEACHER agent.
|
||||
@@ -58,7 +61,7 @@ export function probeDoneTool(ctx: ProbeDoneContext): Record<string, AgentTool<a
|
||||
// The core-owned schema (contracts/probe.ts) — the SAME schema the UI
|
||||
// parse seam (tool-parts.ts) runs, so boundary-valid ⇔ consumer-parseable.
|
||||
parameters: probeDoneInputSchema,
|
||||
execute: serverExecute("probe_done", probeDoneInputSchema, async (input): Promise<ProbeDoneResult> => {
|
||||
execute: serverExecute("probe_done", probeDoneInputSchema, async (input) => {
|
||||
const record = ctx.chatStore.getChat(ctx.chatId);
|
||||
if (!record) return { ok: false, error: `Chat not found: ${ctx.chatId}` };
|
||||
|
||||
@@ -81,7 +84,14 @@ export function probeDoneTool(ctx: ProbeDoneContext): Record<string, AgentTool<a
|
||||
// constructs the teacher off the transcript seam) — record the
|
||||
// assessment anyway; the wake is an optimization, not a gate.
|
||||
}
|
||||
return { ok: true, assessment: input.assessment };
|
||||
// Context diet (owner, 2026-10-09): the assessment lives ONCE — as
|
||||
// this call's arguments. content is the fixed ack (the agent never
|
||||
// sees an echo); details = {ok:true} for the record/UI — the
|
||||
// probe-done card reads the call's input, which it always preferred.
|
||||
return {
|
||||
text: PROBE_DONE_ACK_TEXT,
|
||||
details: { ok: true },
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user