practice: the between-problems pause — conclude ends the turn, the learner's go-ahead paces the stream (round 16)
Owner ruling (2026-10-06): every concluded problem opens a pause. The
agent may speak its closing words about the learner's last thinking
after the conclude result, then ENDS ITS TURN — a system-prompt law
deliberately, per owner correction, NOT tool-level terminate. The next
problem is never posed until the learner explicitly moves on; the pause
is the session's teaching space (repairs, clarifications, follow-ups —
still surgical, never re-teaching).
The move-on signal is a first-class input: { type: "next" } appends a
practice-next marker (content = single-sourced NEXT_PROBLEM_TEXT — the
model's unambiguous go-ahead: pose per the selection rules or wrap if
the arc is complete). Honest 400s: practice sessions only, nothing
active (mid-problem input rides the dock), riding after any pending
dismissal synthesis exactly like text.
The fast path (owner approved: composer-band chip + Enter-on-empty):
- Composer.enterAction gains "next" — Enter on an EMPTY composer in
the pause fires the signal; typing anything makes Enter a send again;
busy (steer/stop) outranks the pause.
- The Next chip hovers at the composer band's top-right (pick-pill
chrome register) whenever the pause is live and a problem has ever
been posed; the placeholder becomes the hint ("Ask about this
problem — or press Enter for the next one…", exact);
- the transcript renders the signal as a compact centered NEXT PROBLEM
line (mono-caps chrome); session-core appends it optimistically.
CONCLUDE_ACK_TEXT rewritten to carry the law; doctrine §VIII gains the
pause law (owner-sanctioned edit #3); 00-base's conclude/dismissal
bullets rewritten + the between-problems section added.
This commit is contained in:
@@ -152,7 +152,9 @@ describe("practice flow (scripted model)", () => {
|
||||
(m) => (m as ToolResultMessage).role === "toolResult" && (m as ToolResultMessage).toolName === "conclude",
|
||||
) as LhMessage & ToolResultMessage;
|
||||
assert.ok(concludeAck, "the conclude ack persisted");
|
||||
assert.ok((concludeAck.content[0] as { text: string }).text.includes("composer is free"));
|
||||
// Owner round 16: the ack carries the turn-ends law (no more tools,
|
||||
// the learner's go-ahead paces the next problem).
|
||||
assert.ok((concludeAck.content[0] as { text: string }).text.includes("END YOUR TURN"));
|
||||
// The turn continued past conclude to the reveal text.
|
||||
assert.ok(afterConclude.some((m) => (m as { role?: string }).role === "assistant" && JSON.stringify((m as { content?: unknown }).content).includes("Clean proof.")));
|
||||
assert.equal(activeProblemOf(afterConclude), null);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
asAgentMessages,
|
||||
concludedByDismissalPair,
|
||||
newProblemDismissedMarker,
|
||||
newNextMessage,
|
||||
newThinkingMessage,
|
||||
needsContinuation,
|
||||
newUserMessage,
|
||||
@@ -233,6 +234,37 @@ describe("the thinking input", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── the move-on signal (owner round 16, 2026-10-06) ─────────────────────────
|
||||
|
||||
describe("the next input", () => {
|
||||
it("parses bare and appends the practice-next marker in an idle practice session", () => {
|
||||
const input = parseChatInput({ type: "next" });
|
||||
assert.deepEqual(input, { type: "next" });
|
||||
const appends = inputAppends({ messages: [], kind: "practice" }, input);
|
||||
assert.equal(appends.length, 1);
|
||||
const marker = appends[0] as { customType: string; content: string };
|
||||
assert.equal(marker.customType, "practice-next");
|
||||
// The model's unambiguous go-ahead, single-sourced in contracts.
|
||||
assert.match(marker.content, /\[NEXT PROBLEM\]/);
|
||||
});
|
||||
|
||||
it("400s outside practice sessions (the affordance is practice-only)", () => {
|
||||
assert.throws(() => inputAppends({ messages: [], kind: "teach" }, { type: "next" }), /practice-session/);
|
||||
assert.throws(() => inputAppends({ messages: [] }, { type: "next" }), /practice-session/);
|
||||
});
|
||||
|
||||
it("400s while a problem is active (mid-problem input rides the dock)", () => {
|
||||
assert.throws(() => inputAppends({ messages: problemPose(), kind: "practice" }, { type: "next" }), /no active problem/);
|
||||
});
|
||||
|
||||
it("rides AFTER a pending dismissal conclusion, exactly like text", () => {
|
||||
const dismissed = [...problemPose(), newProblemDismissedMarker("call_p1")];
|
||||
const appends = inputAppends({ messages: dismissed, kind: "practice" }, { type: "next" });
|
||||
assert.equal(appends.length, 3); // conclude pair + the next marker
|
||||
assert.equal((appends[2] as { customType: string }).customType, "practice-next");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the dismissal's conclude synthesis rides the next input", () => {
|
||||
it("text after a dismissal prepends the conclude pair before the user message", () => {
|
||||
const record = { messages: [...problemPose(), newProblemDismissedMarker("call_p1")] };
|
||||
@@ -374,3 +406,16 @@ describe("modelSolutionForConclude (the conclusion's hero is the pose's commitme
|
||||
assert.equal(conclude!.reveal, "Show that…", "the view-seam join carries the committed solution");
|
||||
});
|
||||
});
|
||||
|
||||
// ── the transcript face of the signal ───────────────────────────────────────
|
||||
|
||||
describe("projectView renders the move-on signal", () => {
|
||||
it("the practice-next marker projects the compact part (the centered NEXT PROBLEM line)", () => {
|
||||
const view = projectView([newNextMessage()]);
|
||||
const parts = view[0]?.parts ?? [];
|
||||
assert.ok(
|
||||
parts.some((p) => p.type === "data-practice-next"),
|
||||
"the data-practice-next part rides the view",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,8 +75,21 @@ describe("enterAction", () => {
|
||||
assert.equal(enterAction({ busy: true, canSend: false }), "stop");
|
||||
});
|
||||
|
||||
it("idle: send when there is text, nothing when there is not", () => {
|
||||
it("idle: send when there is the text, nothing when there is not", () => {
|
||||
assert.equal(enterAction({ busy: false, canSend: true }), "send");
|
||||
assert.equal(enterAction({ busy: false, canSend: false }), "none");
|
||||
});
|
||||
|
||||
it("the practice pause (owner round 16): Enter on an EMPTY composer fires the move-on signal", () => {
|
||||
assert.equal(enterAction({ busy: false, canSend: false, nextProblem: true }), "next");
|
||||
});
|
||||
|
||||
it("typing anything outranks the pause — text makes Enter a send again", () => {
|
||||
assert.equal(enterAction({ busy: false, canSend: true, nextProblem: true }), "send");
|
||||
});
|
||||
|
||||
it("busy outranks the pause — steering and Stop keep their lanes", () => {
|
||||
assert.equal(enterAction({ busy: true, canSend: false, nextProblem: true }), "stop");
|
||||
assert.equal(enterAction({ busy: true, canSend: true, nextProblem: true }), "send");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user