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
95 lines
4.0 KiB
TypeScript
95 lines
4.0 KiB
TypeScript
/**
|
|
* Probe-phase contracts (the probe→teach split, docs/probe-teach-split.md).
|
|
*
|
|
* The prober concludes its phase with `probe_done` — a server-side tool
|
|
* whose INPUT is the learner assessment (authored by the model, streamed to
|
|
* the learner live, durable in the transcript by the tool result itself).
|
|
* The first `probe_done` in the transcript is the probe→planning seam: the
|
|
* teacher agent is constructed from that point on, and the tool's execute
|
|
* queues the handoff steer through the wake pipeline so the teacher starts
|
|
* immediately after the prober's turn ends (auto-wake, user-decided).
|
|
*
|
|
* Assessment shape (v1 — refine later): the probe was a binary search per
|
|
* dependency strand; the assessment formalizes exactly that. A list of the
|
|
* strands the lesson will depend on, each with a free-form read of where
|
|
* the learner's edge sits.
|
|
*/
|
|
|
|
import { Type, type Static } from "typebox";
|
|
|
|
/** One dependency strand the planned lesson will rest on. */
|
|
export interface ProbeStrand {
|
|
/** Name/description of the strand (e.g. "the derivative as a limit"). */
|
|
strand: string;
|
|
/** Free-form: where the learner's edge sits on this strand — floor,
|
|
* ceiling, misconceptions found, what to build from. */
|
|
assessment: string;
|
|
}
|
|
|
|
/** The prober's read of the learner, carried by probe_done. */
|
|
export interface ProbeAssessment {
|
|
strands: ProbeStrand[];
|
|
/** Optional overall synthesis: the learner's apparent goal and edge. */
|
|
summary?: string;
|
|
}
|
|
|
|
/** probe_done tool input (the assessment the prober authors). */
|
|
export interface ProbeDoneInput {
|
|
assessment: ProbeAssessment;
|
|
}
|
|
|
|
// The TypeBox schema of that input — SINGLE SOURCE OF TRUTH, lived by the
|
|
// tool (src/agent/tools/probe.ts) and by the UI parse seam (src/core/tool-parts.ts):
|
|
// the boundary validation and the consumer parse run literally the same
|
|
// schema, so "schema-valid at the boundary" ⇔ "parseable by a consumer" is
|
|
// structural, not a convention. (Live crash 2026-09-12: the model sent
|
|
// `assessment` as a STRING; the card cast it and died on `.strands.map`.)
|
|
export const probeDoneInputSchema = Type.Object({
|
|
assessment: Type.Object(
|
|
{
|
|
strands: Type.Array(
|
|
Type.Object({
|
|
strand: Type.String({
|
|
description:
|
|
"The dependency strand (e.g. 'the derivative as a limit') — a name the plan can hang a node on.",
|
|
}),
|
|
assessment: Type.String({
|
|
description:
|
|
"Where his edge sits on this strand: the floor (what he reliably gets right), the ceiling (where it runs out), " +
|
|
"misconceptions found, and what the lesson should build from.",
|
|
}),
|
|
}),
|
|
{ minItems: 1, description: "Every strand the planned lesson will depend on, each with your read of his edge on it." },
|
|
),
|
|
summary: Type.Optional(
|
|
Type.String({
|
|
description: "Optional overall synthesis: his apparent goal and where the teaching should enter.",
|
|
}),
|
|
),
|
|
},
|
|
{ description: "The learner assessment — the probe's distilled output." },
|
|
),
|
|
});
|
|
|
|
/** Schema ↔ interface lock (the old `satisfies z.ZodType` guarantee): the
|
|
* declaration and ProbeDoneInput can never drift apart — this only compiles
|
|
* when the schema's static shape IS the interface. */
|
|
export const probeDoneSchemaLock = (v: Static<typeof probeDoneInputSchema>): ProbeDoneInput => v;
|
|
|
|
/** probe_done tool output. */
|
|
export interface ProbeDoneResult {
|
|
ok: boolean;
|
|
/** Refusal reason when ok === false (open question, already concluded). */
|
|
error?: string;
|
|
}
|
|
|
|
/** The probe_done ack's model-facing text (owner, context diet
|
|
* 2026-10-09): the assessment is the CALL's arguments — durable in the
|
|
* transcript, streamed to the learner — so echoing it back in the
|
|
* result would duplicate the probe phase's largest payload in every
|
|
* future context. The agent sees this fixed line instead; `details`
|
|
* carries only {ok:true} for the record/UI. Same doctrine as
|
|
* CONCLUDE_ACK_TEXT. */
|
|
export const PROBE_DONE_ACK_TEXT =
|
|
"Assessment recorded — it is shown to the learner now and the teacher plans from it. Say your closing sentence and end your turn; the teaching phase takes over from here.";
|