Files
multi-agent-mux/.agents/skills/lib_py/loop/policy.py
T
GodopuandClaude Opus 5 8adbe2245c docs(issue-5): phase 2b scenarios, operations note and task checklist
Three scenarios join the contract set, so the fifteen vectors now cover an
unproven side effect, an undecidable attribution, and an agent whose state goes
unreadable in the window between finalization and notification. All fifteen
match their expected decision.

The side-effect case needed a decision about layering. Authoritative enforcement
belongs in manifest verification, which has the job record and therefore knows
what was declared; the policy honours an explicit negative from the collector so
the case is expressible as a vector. Absence is not read as proof there, because
the manifest check is the layer that fails closed on missing evidence.

The operations note records why the agent declares and the reader re-derives,
the three gates and why the backend allowlist can only be narrowed, the
notification transaction, and the rollback.

It also records a deviation from my own plan. §D2 asked for reviewer sign-off
before promoting an attempt id into the record, since that changes a contract
phase 1 froze. Measurement during implementation showed the change was not
needed: the attempt guard already lives in the evidence layer, where attempt
identity actually is, so auto-completion passes the engine a status precondition
instead. The schema is untouched, the frozen contract holds, and a
characterization test fails the moment a record starts carrying an attempt id,
which is when to revisit it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
2026-09-06 21:32:48 +09:00

169 lines
8.4 KiB
Python

"""The completion decision. A pure function, deliberately.
No I/O, no clock, no globals: that is what lets the phase-0 scenario fixtures be
fed to it verbatim as test vectors. Those seven scenarios and their
`expected_policy.decision` survived three-reviewer scrutiny, so they are the
contract this implements rather than a fresh set of rules.
Fail-closed throughout: every gate must pass for `candidate_approved`. Anything
unproven holds.
"""
from __future__ import annotations
from lib_py.loop import attribution
from lib_py.loop.evidence import Decision
# Native states that mean "not doing anything right now". Idle alone proves
# nothing about completion; it only makes completion *possible* to consider.
QUIESCENT = ("idle", "done")
# The four identifiers an external collector must supply before a completion can
# be attributed to this attempt.
ATTRIBUTION_FIELDS = ("job_id", "attempt_id", "agent_session_uuid",
"pane_process_generation")
def _timeline_by_kind(scenario):
out = {}
for item in scenario.get("timeline") or []:
out.setdefault(item.get("source_kind"), []).append(item)
return out
def collect_observed_identity(scenario) -> dict:
"""Gather identifiers the *evidence* carries, for comparison with dispatch.
Two sources, and the distinction matters. Association of a job or attempt to
an observation is the external collector's business, so it lives in a
``collector_attribution`` envelope beside the payload -- never inside it,
because a native Herdr payload must stay native (phase 0 forbids MAM fields
there, and a fabricated one would be indistinguishable from a real reading).
The report manifest is a filesystem fact and stays in that payload.
Every occurrence is kept. Taking only the first would drop a later,
contradicting observation, which is precisely what this exists to catch.
"""
observed = {}
for item in scenario.get("timeline") or []:
envelope = item.get("collector_attribution") or {}
for field in ATTRIBUTION_FIELDS:
if envelope.get(field) is not None:
observed.setdefault(field, []).append(envelope[field])
payload = item.get("payload") or {}
if item.get("source_kind") != "herdr":
# A MAM-side source (an event, a filesystem fact) legitimately names
# the job or attempt in its own payload. A native Herdr payload never
# may: phase 0 forbids MAM fields there, so an identifier appearing
# inside one is either fabricated or a contract violation, and either
# way it is not evidence.
for field in ATTRIBUTION_FIELDS:
if payload.get(field) is not None:
observed.setdefault(field, []).append(payload[field])
manifest = payload.get("report_manifest")
if isinstance(manifest, dict) and manifest.get("attempt_id") is not None:
observed.setdefault("manifest_attempt_id", []).append(manifest["attempt_id"])
return observed
def decide(scenario) -> Decision:
"""Map one composite scenario to a decision.
Order matters: transport doubt and staleness invalidate the evidence itself,
so they are answered before anything is inferred from it.
"""
dispatch = scenario.get("dispatch_context") or {}
by_kind = _timeline_by_kind(scenario)
herdr_items = by_kind.get("herdr") or []
latest = herdr_items[-1]["payload"] if herdr_items else {}
filesystem = scenario.get("filesystem") or {}
# 1. Transport doubt: evidence gathered before a gap cannot be trusted.
for item in herdr_items:
payload = item.get("payload") or {}
if payload.get("transport_healthy") is False or payload.get("error"):
return Decision("hold_reconnect",
"herdr transport failed; cached observations are void")
# 2. A signal from a previous attempt or process generation decides nothing.
for item in herdr_items:
payload = item.get("payload") or {}
pane_generation = payload.get("pane_process_generation")
if pane_generation and dispatch.get("pane_process_generation") and \
pane_generation != dispatch["pane_process_generation"]:
return Decision("reject_stale",
"signal belongs to a different process generation")
pane_id = payload.get("pane_id")
if pane_id and dispatch.get("pane_process_generation") and \
not attribution.pane_matches_generation(
pane_id, dispatch["pane_process_generation"]):
return Decision("reject_stale", "signal belongs to a different pane")
# 3. A blocked agent is waiting for a human. Completion is forbidden.
if latest.get("agent_status") == "blocked" or latest.get("matched_rule"):
return Decision("hold_blocked", "agent is blocked on an interactive prompt")
# 4. Attribution: the dispatch record must be well formed AND agree with the
# identifiers the evidence itself carries. Flags on the dispatch record
# alone are self-assertion, not correspondence.
observed = collect_observed_identity(scenario)
manifest_attempts = observed.pop("manifest_attempt_id", [])
attributed = attribution.check(dispatch)
if not attributed.ok:
return Decision("hold", attributed.reason)
# Absence of proof is not proof. Comparing only the identifiers that happen
# to be present would let evidence carrying none at all be approved on the
# dispatch record's own say-so, which is the self-assertion this check exists
# to replace. Every identifier must be supplied, then must agree.
for field in ATTRIBUTION_FIELDS:
if not observed.get(field):
return Decision("hold",
"no observed {} to attribute this completion to".format(field))
for value in observed[field]:
if value != dispatch.get(field):
return Decision("hold", "attribution mismatch on {}: observed {!r}, "
"dispatched {!r}".format(field, value,
dispatch.get(field)))
# The finalized report must belong to the attempt being judged. A missing
# manifest attempt holds for the same reason: it proves nothing.
if not manifest_attempts:
return Decision("hold", "no report manifest attempt to associate with this attempt")
for manifest_attempt in manifest_attempts:
if manifest_attempt != dispatch.get("attempt_id"):
return Decision("hold",
"report manifest belongs to attempt {!r}, not {!r}".format(
manifest_attempt, dispatch.get("attempt_id")))
# 5. Still working, or a tool is running underneath a ready prompt.
if latest.get("agent_status") not in QUIESCENT:
return Decision("hold", "agent is not quiescent (status={!r})".format(
latest.get("agent_status")), attribution_ok=True)
for item in by_kind.get("harness") or []:
if (item.get("payload") or {}).get("active"):
return Decision("hold", "a tool is still executing", attribution_ok=True)
# 6. The report must be finalized, not merely paused. A debounce alone is a
# candidate filter, never an authority to complete.
if not filesystem.get("exists"):
return Decision("hold", "no report present", attribution_ok=True)
if not filesystem.get("manifest_committed"):
return Decision("hold", "report is not finalized (no commit manifest)",
attribution_ok=True)
if not filesystem.get("hash_verified"):
return Decision("hold", "report manifest hash unverified", attribution_ok=True)
# Side-effect evidence is enforced authoritatively in finalize.verify_manifest,
# which has the job record and therefore knows what was declared. The policy
# honours an explicit negative from the collector so the case is expressible
# as a scenario vector; absence here is not read as proof, because the
# manifest check is the layer that fails closed on missing evidence.
if filesystem.get("side_effects_verified") is False:
return Decision("hold", "declared side effect has no evidence in the commit manifest",
attribution_ok=True)
return Decision("candidate_approved",
"attribution, quiescence and a verified manifest all hold",
attribution_ok=True)