task_tree: the disposition schema states what each choice costs

The three child-result dispositions were hand-written literals in two places
with no per-value meaning attached, even though choosing `deferred` really
does keep the obligation open and force an honest degraded terminal answer.
The model reads the schema, so that is where the consequence belongs.

Both payload sites now share one fragment whose enum reads the validator's own
frozenset, sorted so the order is stable across worker processes, and whose
description says what each value does. No new token, no host detector, no
behaviour change.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
Ouroboros 2026-09-03 18:54:30 +03:00
parent 60c10bf49c
commit fc255a0693
3 changed files with 45 additions and 10 deletions

View file

@ -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

View file

@ -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": [

View file

@ -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"]