practice: untagged problems — target optional, topic sessions work (owner round 12)

A session whose material is not in the map is a SUPPORTED mode now:
target is optional (absent = untagged problem, poses freely), an
explicit [] heals away at the seam (the record's canonical form is
tagged-or-absent), and when targets ARE present the round-9 law is
unchanged — whole-graph validation, names normalized to ids. The
rejection message teaches the exit ('omit target entirely'), so a
model inventing targets for an off-graph topic converges to omitting
on the retry; omitting always works, no brick is possible. Prompt
carries the clause: material in the map → ids; not → omit, problems
stand on their own.
This commit is contained in:
amosblomqvist
2026-10-05 20:23:48 +03:00
parent 1f7a3516fe
commit 4f3bbf61d0
3 changed files with 42 additions and 14 deletions
+29 -13
View File
@@ -161,12 +161,15 @@ export const problemInputObject = Type.Object({
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) this problem exercises, from the session context's [id] list. " +
"Names are normalized to ids at the seam; an unknown proposition is rejected.",
}),
target: Type.Optional(
Type.Array(Type.String(), {
minItems: 1,
description:
"The proposition id(s) this problem exercises, from the session context's [id] list — include when " +
"the material is in the map; OMIT entirely when it is not (a topic session poses untagged problems). " +
"Names are normalized to ids at the seam; an unknown proposition is rejected.",
}),
),
modelSolution: Type.String({
description: "REQUIRED — the full solution, moves in order, committed BEFORE the learner sees the problem. This IS the reveal — conclude shows it verbatim; never re-author it.",
}),
@@ -193,9 +196,19 @@ export function problemTool(
// TARGET NORMALIZATION (owner round 9): names silently become ids —
// the RECORD carries canonical ids (the mastery trace groups on
// them); unresolvable strings pass through for execute to reject.
if (!graphNodes) return healed as never;
// UNTAGGED PROBLEMS (owner round 12): an explicit empty list is
// the model saying "no targets" — healed AWAY, so the record's
// canonical form is tagged-or-absent, never [].
if (!graphNodes) {
const bare = (healed as { target?: unknown }).target;
return Array.isArray(bare) && bare.length === 0
? ({ ...(healed as Record<string, unknown>), target: undefined } as never)
: (healed as never);
}
const target = (healed as { target?: unknown }).target;
if (!Array.isArray(target)) return healed as never;
if (!Array.isArray(target) || target.length === 0) {
return { ...(healed as Record<string, unknown>), target: undefined } as never;
}
const list = graphNodes();
const normalized = target.map((t) =>
typeof t === "string" ? (resolveTargetId(t, list) ?? t) : t,
@@ -209,15 +222,18 @@ export function problemTool(
"a problem is already active — call conclude on it before posing the next one",
]);
}
// TARGET VALIDATION (owner round 9): against the WHOLE graph. A
// one-line rejection — the proposition list already sits in the
// agent's context (the [id] marker); no re-listing.
if (graphNodes) {
// TARGET VALIDATION (owner round 9, round 12): against the WHOLE
// graph, and ONLY when targets are present — absent means an
// untagged problem (topic sessions, owner round 12), which poses
// freely. A one-line rejection — the proposition list already
// sits in the agent's context (the [id] marker); no re-listing.
// The message teaches the exit: omitting always works.
if (graphNodes && input.target && input.target.length > 0) {
const list = graphNodes();
const unknown = input.target.filter((t) => resolveTargetId(t, list) === null);
if (unknown.length > 0) {
return invalidInputResult("problem", [
`target ${JSON.stringify(unknown)} is not a known proposition — targets must be proposition ids from the session context`,
`target ${JSON.stringify(unknown)} is not a known proposition — targets must be proposition ids from the session context; if the material is not in the map, omit target entirely`,
]);
}
}