diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 26491f43e..b71a5c623 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1675,7 +1675,9 @@ by "Provider Independence" above. Call-site imperatives: Binding the complete-result SHA-256 means a parent cannot claim it integrated a result that later changed. `deferred` suppresses only the reminder and forces an honest degraded/best-effort terminal answer until - resolved. A child wedged in the legacy `cancel_requested` latch is intent, + resolved. That per-value consequence is carried by the + `tree_note` payload schema itself, which is the SSOT for when to choose each + value, and its enum reads the validator's own set. A child wedged in the legacy `cancel_requested` latch is intent, not outcome — it stays visible as cancel-pending until custody settles it. - Host task acceptance is root-only; eligibility uses structured facts (`outcomes.turn_has_reviewable_effects` plus a typed diff --git a/ouroboros/tools/task_tree.py b/ouroboros/tools/task_tree.py index e81ec4141..5757ec7c2 100644 --- a/ouroboros/tools/task_tree.py +++ b/ouroboros/tools/task_tree.py @@ -84,7 +84,24 @@ def _tree_read( def get_tools() -> List[ToolEntry]: - from ouroboros.task_tree_ledger import DELEGATION_CONSTRAINT_DIRECTIVES, LEDGER_KINDS + from ouroboros.task_tree_ledger import ( + CHILD_RESULT_DISPOSITIONS, DELEGATION_CONSTRAINT_DIRECTIVES, LEDGER_KINDS, + ) + + # The schema is where the model learns WHEN to choose each value, so the + # per-value consequence belongs here rather than in prose elsewhere. Read + # from the validator's own frozenset, sorted so the enum order is stable + # across worker processes (a frozenset's iteration order is not). + disposition_schema = { + "type": "string", + "enum": sorted(CHILD_RESULT_DISPOSITIONS), + "description": ( + "integrated = consumed by this answer (item closes); irrelevant = not needed " + "here (the child result stays as evidence; item closes); deferred = STILL OWED: " + "your terminal answer reads degraded/best_effort until you re-disposition this " + "exact child_result_sha256." + ), + } return [ ToolEntry("tree_note", { @@ -125,10 +142,7 @@ def get_tools() -> List[ToolEntry]: "properties": { "type": {"type": "string", "enum": ["child_result_disposition"]}, "child_task_id": {"type": "string"}, - "disposition": { - "type": "string", - "enum": ["integrated", "irrelevant", "deferred"], - }, + "disposition": disposition_schema, "child_result_sha256": {"type": "string"}, "children": { "type": "array", @@ -140,10 +154,7 @@ def get_tools() -> List[ToolEntry]: "type": "object", "properties": { "child_task_id": {"type": "string"}, - "disposition": { - "type": "string", - "enum": ["integrated", "irrelevant", "deferred"], - }, + "disposition": disposition_schema, "child_result_sha256": {"type": "string"}, }, "required": [ diff --git a/tests/test_child_result_disposition.py b/tests/test_child_result_disposition.py index 07c005050..3fbfddb13 100644 --- a/tests/test_child_result_disposition.py +++ b/tests/test_child_result_disposition.py @@ -914,3 +914,25 @@ def test_full_blackboard_never_locks_out_validated_dispositions(tmp_path, monkey ) assert "ledger is full" not in accepted assert not accepted.startswith("⚠️") + + +def test_the_disposition_enum_and_its_cost_come_from_one_place(): + """The enum was hand-written twice with no per-value meaning, although the + consequence of ``deferred`` is real and host-enforced. Both payload sites + now read the validator's own set, in a stable order, and the schema states + what each value costs — the schema is where the model actually reads it.""" + from ouroboros.task_tree_ledger import CHILD_RESULT_DISPOSITIONS + from ouroboros.tools import task_tree + + entry = next(tool for tool in task_tree.get_tools() if tool.name == "tree_note") + payload = entry.schema["parameters"]["properties"]["payload"]["properties"] + single = payload["disposition"] + batch = payload["children"]["items"]["properties"]["disposition"] + + assert single is batch or single == batch + for schema in (single, batch): + assert schema["enum"] == sorted(CHILD_RESULT_DISPOSITIONS) + assert schema["enum"] == ["deferred", "integrated", "irrelevant"] + for value in CHILD_RESULT_DISPOSITIONS: + assert value in schema["description"], value + assert "degraded/best_effort" in schema["description"]