feat: the plan verdict gate — acceptance is the plan→teach seam
The accept-plan UI lands (docs/probe-teach-split.md §4, formerly the interim rule): presenting the plan no longer moves the phase — the learner's ACCEPT does. While the presented plan awaits its verdict, the composer's send square becomes two stacked buttons (green Accept plan / red Decline); typed text rides the verdict as his note, Enter keeps sending plain side questions (phase unmoved), decline asks for a revision and the buttons stay. The seam: accept sends a user message carrying a data-plan-accepted part (PlanAcceptedData in contracts/chat.ts, TeacherUIDataParts 'plan-accepted'); phaseAt3 reads it as the plan→teach event (plan_init demoted to presentation-only); convertToModelMessages drops data parts, so the teacher sees only the text. Mid-lesson rewrites need no fresh verdict — acceptance happens once, at the first presentation. The teacher's planning prompt now ends the turn at presentation and waits for the verdict (never teaches into an unaccepted plan). Gates: tsc clean, 304/304 (phases tests rewritten to the acceptance seam incl. decline/side-question/failed-init cases; composer verdict render tests), lint, build. Live GLM smoke ALL PASS: phase pinned at 'plan' after plan_init, presentation turn carried NO check, accept click flipped the summary to 'teach', then tagged checks moved the map (current=rates, visited=composite); zero console errors.
This commit is contained in:
@@ -123,6 +123,16 @@ export interface TurnUsage {
|
||||
* block renders current node + direct parents + direct children in its own
|
||||
* small container, the current node glowing — clearly a fragment, not the
|
||||
* whole plan. Clicking either opens the full plan in a popup. */
|
||||
/** The learner's plan-acceptance marker (docs/probe-teach-split.md §4): a
|
||||
* data part riding the accept-click user message. It is THE plan→teach
|
||||
* phase seam — phaseAt3 reads it from the transcript;
|
||||
* convertToModelMessages drops data parts, so the teacher only sees the
|
||||
* message's text; the marker is for derivation and the UI. */
|
||||
export interface PlanAcceptedData {
|
||||
/** ISO timestamp of the accept click. */
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface PlanBlockData {
|
||||
/** Only "full" survives (the 2026-09-11 pivot): the plan renders IN
|
||||
* FLOW once, at authoring. The neighborhood fragments are retired —
|
||||
|
||||
+11
-17
@@ -5,10 +5,11 @@
|
||||
*
|
||||
* probe — until the first message containing a `tool-probe_done` part
|
||||
* (the prober concludes; the assessment rides the tool part)
|
||||
* plan — probe concluded, until the first successful `plan_init`
|
||||
* (the teacher presents the map; interim rule until the
|
||||
* accept-plan UI lands — then acceptance becomes the seam)
|
||||
* teach — a plan exists
|
||||
* plan — probe concluded, until the learner ACCEPTS the presented
|
||||
* plan (a `data-plan-accepted` part on the accept button's
|
||||
* user message — the explicit verdict gate; plan_init itself
|
||||
* is only the PRESENTATION and moves no phase)
|
||||
* teach — teaching is sanctioned
|
||||
*
|
||||
* The former derived-occupancy seam (phase-of-tree via `currentNodeId`)
|
||||
* is GONE: nothing ever writes currentNodeId (the stuck-on-plan bug — a
|
||||
@@ -22,9 +23,6 @@
|
||||
/** The three phases of a session, as the picker / header / dividers see them. */
|
||||
export type Phase3 = "probe" | "plan" | "teach";
|
||||
|
||||
/** The advance-only order of phases: a session never visibly regresses. */
|
||||
const PHASE3_RANK: Record<Phase3, number> = { probe: 0, plan: 1, teach: 2 };
|
||||
|
||||
/**
|
||||
* Minimal structural slice of a UI message for the phase scan: parts with
|
||||
* a state and (for concluded calls) an output.
|
||||
@@ -33,10 +31,6 @@ export interface PhaseMessage {
|
||||
parts: ReadonlyArray<{ type: string; state?: string; output?: unknown }>;
|
||||
}
|
||||
|
||||
interface PlanOutput {
|
||||
tree?: { nodes?: unknown };
|
||||
}
|
||||
|
||||
/** The phase at `upToIndex` (inclusive; default: the last message). */
|
||||
export function phaseAt3(messages: readonly PhaseMessage[], upToIndex?: number): Phase3 {
|
||||
let phase: Phase3 = "probe";
|
||||
@@ -55,15 +49,15 @@ function advance(
|
||||
if (!parts) return phase;
|
||||
for (const part of parts) {
|
||||
// STRICTLY SEQUENTIAL: probe → plan only via a concluded probe_done,
|
||||
// plan → teach only via a successful plan_init. A marker out of order
|
||||
// (e.g. a legacy plan_init with no probe) advances nothing — the chain
|
||||
// never skips, and the rank table below only guards regressions.
|
||||
// plan → teach only via the learner's plan acceptance
|
||||
// (data-plan-accepted). A marker out of order advances nothing — the
|
||||
// chain never skips. plan_init PRESENTS the map; only the verdict
|
||||
// (the accept button's marker part) sanctions teaching.
|
||||
if (phase === "probe" && part.type === "tool-probe_done" && part.state === "output-available") {
|
||||
phase = "plan";
|
||||
}
|
||||
if (phase === "plan" && part.type === "tool-plan_init" && part.state === "output-available") {
|
||||
const tree = (part.output as PlanOutput | undefined)?.tree;
|
||||
if (tree && Array.isArray(tree.nodes) && tree.nodes.length > 0) phase = "teach";
|
||||
if (phase === "plan" && part.type === "data-plan-accepted") {
|
||||
phase = "teach";
|
||||
}
|
||||
}
|
||||
return phase;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { UIMessage } from "ai";
|
||||
import type {
|
||||
DemoToolInput,
|
||||
PlanAcceptedData,
|
||||
PlanBlockData,
|
||||
ProbeDoneInput,
|
||||
ProbeDoneResult,
|
||||
@@ -37,6 +38,9 @@ export type TeacherUIDataParts = {
|
||||
"subagent-artifact": SubagentArtifactData;
|
||||
/** Plan blocks (wave 4): frozen plan snapshots rendered in the chat flow. */
|
||||
"plan-block": PlanBlockData;
|
||||
/** The plan-acceptance marker: the accept button's user message carries
|
||||
* it — the plan→teach phase seam (docs/probe-teach-split.md §4). */
|
||||
"plan-accepted": PlanAcceptedData;
|
||||
};
|
||||
|
||||
export type TeacherUIMessage = UIMessage<unknown, TeacherUIDataParts, TeacherUITools>;
|
||||
|
||||
Reference in New Issue
Block a user