"""Shared fact ledger for MAF roundtable runs (Phase 3). The ledger is the authoritative record of confirmed facts, tool outcomes, and external-action status. Chat text must never be treated as proof that a write succeeded — hosts synthesize conclusions from this structure. """ from __future__ import annotations from typing import Any from uuid import uuid4 _TOOL_STATUSES = frozenset({"completed", "waiting_approval", "failed", "unknown"}) _ACTION_STATUSES = frozenset({"pending", "executed", "failed", "unknown", "rejected"}) def empty_ledger() -> dict[str, Any]: """Return a fresh ledger with all Phase-3 partitions. Phase-3 separation of evidence trust levels: - ``candidate_facts`` — unverified seat claims; the model said it, that does not make it true. Each carries a server-generated ``candidate_id``. - ``confirmed_facts`` — only facts a cross-review confirmed (with evidence and a confirmer) ever land here. - ``tool_results`` — outcomes of tools that actually ran. - ``pending_actions`` / ``executed_actions`` / ``failed_or_unknown_actions`` — external writes partitioned by lifecycle state. """ return { "candidate_facts": [], "confirmed_facts": [], "evidence": [], "tool_results": [], "executed_actions": [], "pending_actions": [], "failed_or_unknown_actions": [], "conflicts": [], # Legacy alias kept for old checkpoints / prompts. "proposed_actions": [], } def normalize_ledger(raw: dict[str, Any] | None) -> dict[str, Any]: """Coerce a possibly-partial / legacy ledger into the full shape (in-place when possible).""" if not isinstance(raw, dict): return empty_ledger() base = empty_ledger() for key in base: if key not in raw or not isinstance(raw.get(key), list): raw[key] = [] return raw def candidate_id_map(ledger: dict[str, Any]) -> dict[str, dict[str, Any]]: """Return ``{candidate_id: candidate_entry}`` for O(1) confirmation lookup.""" ledger = normalize_ledger(ledger) return { str(c["candidate_id"]): c for c in ledger["candidate_facts"] if isinstance(c, dict) and c.get("candidate_id") } def known_evidence_refs(ledger: dict[str, Any]) -> set[str]: """The set of evidence references the ledger actually has backing for. A cross-review may only confirm a candidate using evidence that the ledger already recorded (either in ``evidence`` or as a successful ``tool_results`` entry). This is the trust anchor that stops a weak model from inventing evidence references to promote a seat claim. """ ledger = normalize_ledger(ledger) refs: set[str] = set() for e in ledger["evidence"]: if isinstance(e, dict): r = e.get("ref") or e.get("text") if r: refs.add(str(r)) elif e: refs.add(str(e)) for tr in ledger["tool_results"]: # Only successful tool runs count as evidence (a failed tool proves # nothing). Each tool result is addressable as tool::. if isinstance(tr, dict) and tr.get("status") == "completed": name = tr.get("tool_name", "") turn = tr.get("turn_index") if name: refs.add(f"tool:{name}:{turn}" if turn is not None else f"tool:{name}") return refs def merge_seat_output( ledger: dict[str, Any], seat_output: dict[str, Any], *, agent_id: str, ) -> dict[str, Any]: """Merge a validated seat structured output into the ledger. Phase-3: seat ``findings`` are written to ``candidate_facts`` (unverified claims), NOT to ``confirmed_facts``. Promotion to ``confirmed_facts`` happens only via :func:`confirm_candidate_facts` after cross-review. Proposed actions stay in ``proposed_actions`` — they are never promoted to ``executed_actions`` here. """ ledger = normalize_ledger(ledger) for finding in seat_output.get("findings") or []: text = str(finding).strip() if text: ledger["candidate_facts"].append({ "candidate_id": uuid4().hex, "text": text, "agent_id": agent_id, "source": "seat_finding", "confirmed": False, }) for ref in seat_output.get("evidence_refs") or []: if isinstance(ref, dict): entry = dict(ref) entry.setdefault("agent_id", agent_id) ledger["evidence"].append(entry) elif ref: ledger["evidence"].append({"ref": str(ref), "agent_id": agent_id}) for conflict in seat_output.get("conflicts") or []: if conflict: ledger["conflicts"].append({"text": str(conflict), "agent_id": agent_id}) for action in seat_output.get("proposed_actions") or []: if isinstance(action, dict): entry = dict(action) entry.setdefault("agent_id", agent_id) entry.setdefault("status", "proposed") ledger["proposed_actions"].append(entry) elif action: ledger["proposed_actions"].append({"text": str(action), "agent_id": agent_id, "status": "proposed"}) # Executed actions from model text are NOT trusted — only ActionCoordinator # / resume may write executed_actions. Ignore seat_output.executed_actions. return ledger def confirm_candidates( ledger: dict[str, Any], *, confirmed_candidates: list[dict[str, Any]], confirmed_by: str, rejections: list[dict[str, Any]] | None = None, accepted: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: """Promote candidate facts to ``confirmed_facts`` after cross-review. Phase-3 P1-1 — server-side evidence binding. A candidate is promoted ONLY when **all** of the following hold: 1. ``candidate_id`` exists in ``candidate_facts`` (the model cannot invent a confirmation for a fact a seat never asserted); 2. the candidate carries **at least one** evidence reference; 3. every evidence reference resolves to an entry the ledger actually recorded — either in ``evidence`` or as a **successful** ``tool_results`` entry. An evidence ref the model invented (not in the ledger) is a hallucination and blocks promotion. Candidates that fail any check are left as candidates (``confirmed=False``) and a rejection reason is appended to ``rejections`` so the cross-review summary can explain why a claim was not trusted. When supplied, ``accepted`` receives only the server-validated confirmations, so callers never need to infer success from the model's requested confirmation list. Idempotent: a candidate already present in ``confirmed_facts`` is not re-confirmed. """ ledger = normalize_ledger(ledger) if rejections is None: rejections = [] by_id = candidate_id_map(ledger) valid_refs = known_evidence_refs(ledger) existing_ids = { str(f.get("candidate_id")) for f in ledger["confirmed_facts"] if isinstance(f, dict) and f.get("candidate_id") } for item in confirmed_candidates or []: if not isinstance(item, dict): continue cid = str(item.get("candidate_id", "")).strip() refs = list(item.get("evidence_refs") or []) if not cid: rejections.append({"reason": "missing_candidate_id"}) continue cand = by_id.get(cid) if cand is None: rejections.append({"candidate_id": cid, "reason": "unknown_candidate_id"}) continue if cid in existing_ids: # Already confirmed — idempotent no-op. continue if not refs: rejections.append({"candidate_id": cid, "reason": "no_evidence"}) continue bad_refs = [r for r in refs if str(r) not in valid_refs] if bad_refs: rejections.append({ "candidate_id": cid, "reason": "evidence_not_in_ledger", "evidence_refs": bad_refs, }) continue cand["confirmed"] = True ledger["confirmed_facts"].append({ "candidate_id": cid, "text": cand.get("text", ""), "agent_id": cand.get("agent_id"), "source": "cross_review_confirmed", "confirmed_by": confirmed_by, "evidence_refs": list(refs), }) if accepted is not None: accepted.append({ "candidate_id": cid, "evidence_refs": list(refs), }) existing_ids.add(cid) return ledger def confirm_candidate_facts( ledger: dict[str, Any], *, confirmed_candidate_ids: list[str], confirmed_by: str, evidence_refs: list[str] | None = None, accepted: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: """Back-compat wrapper: confirm a flat id list against one shared evidence set. Phase-3 P1-1 tightened confirmation so each candidate must carry at least one evidence reference that the ledger has backing for. The structured path (:func:`confirm_candidates`) is the primary API; this wrapper exists for callers that still pass a flat ``confirmed_candidate_ids`` list — it promotes a candidate ONLY when ``evidence_refs`` is non-empty AND every ref resolves to ledger evidence / successful tool results. """ shared_refs = list(evidence_refs or []) candidates = [ {"candidate_id": cid, "evidence_refs": shared_refs} for cid in confirmed_candidate_ids or [] ] return confirm_candidates( ledger, confirmed_candidates=candidates, confirmed_by=confirmed_by, accepted=accepted, ) def record_tool_result( ledger: dict[str, Any], *, tool_name: str, status: str, agent_id: str, turn_index: int | None = None, detail: dict[str, Any] | None = None, ) -> dict[str, Any]: """Append a tool result. ``status`` must be one of the allowed tool statuses.""" ledger = normalize_ledger(ledger) st = status if status in _TOOL_STATUSES else "unknown" entry: dict[str, Any] = { "tool_name": tool_name, "status": st, "agent_id": agent_id, } if turn_index is not None: entry["turn_index"] = turn_index if detail: entry["detail"] = detail ledger["tool_results"].append(entry) return ledger def record_action_transition( ledger: dict[str, Any], *, action_id: str, tool_name: str, status: str, agent_id: str | None = None, external_object_id: str | None = None, ) -> dict[str, Any]: """Move / record an external action under the correct ledger partition. Rules: - ``proposed`` never becomes ``executed`` via this helper unless status is explicitly ``executed`` (called only after ToolExecutor success). - ``pending`` entries are removed when transitioning to a terminal status. """ ledger = normalize_ledger(ledger) st = status if status in _ACTION_STATUSES else "unknown" entry: dict[str, Any] = { "action_id": action_id, "tool_name": tool_name, "status": st, } if agent_id: entry["agent_id"] = agent_id if external_object_id: entry["external_object_id"] = external_object_id # Drop matching pending rows when leaving pending. if st != "pending": ledger["pending_actions"] = [ p for p in ledger["pending_actions"] if not (isinstance(p, dict) and p.get("action_id") == action_id) ] if st == "pending": # Replace existing pending with same action_id. ledger["pending_actions"] = [ p for p in ledger["pending_actions"] if not (isinstance(p, dict) and p.get("action_id") == action_id) ] ledger["pending_actions"].append(entry) elif st == "executed": ledger["executed_actions"] = [ e for e in ledger["executed_actions"] if not (isinstance(e, dict) and e.get("action_id") == action_id) ] ledger["executed_actions"].append(entry) # Strip from failed list if previously unknown/failed. ledger["failed_or_unknown_actions"] = [ e for e in ledger["failed_or_unknown_actions"] if not (isinstance(e, dict) and e.get("action_id") == action_id) ] elif st in ("failed", "unknown", "rejected"): ledger["failed_or_unknown_actions"] = [ e for e in ledger["failed_or_unknown_actions"] if not (isinstance(e, dict) and e.get("action_id") == action_id) ] ledger["failed_or_unknown_actions"].append(entry) # Never keep executed copy of a failed/unknown action. ledger["executed_actions"] = [ e for e in ledger["executed_actions"] if not (isinstance(e, dict) and e.get("action_id") == action_id) ] return ledger def assert_ledger_invariants(ledger: dict[str, Any]) -> None: """Raise AssertionError if the ledger violates evidence/action constraints.""" ledger = normalize_ledger(ledger) executed_ids = { e.get("action_id") for e in ledger["executed_actions"] if isinstance(e, dict) and e.get("action_id") } for prop in ledger["proposed_actions"]: if not isinstance(prop, dict): continue # A proposed-only entry must not also appear as executed with status proposed. if prop.get("status") == "proposed" and prop.get("action_id") in executed_ids: # Same action_id may be proposed then later executed — that is OK if # the executed entry has status executed. Only flag if proposed row # itself claims executed. if prop.get("status") == "executed": raise AssertionError("proposed_actions entry has status=executed") for e in ledger["executed_actions"]: if isinstance(e, dict) and e.get("status") not in (None, "executed"): raise AssertionError(f"executed_actions entry has status={e.get('status')!r}") for tr in ledger["tool_results"]: if isinstance(tr, dict) and tr.get("status") not in _TOOL_STATUSES: raise AssertionError(f"invalid tool_results status={tr.get('status')!r}") def proposed_action_ids(ledger: dict[str, Any]) -> set[str]: ledger = normalize_ledger(ledger) return { str(p["action_id"]) for p in ledger["proposed_actions"] if isinstance(p, dict) and p.get("action_id") } def executed_action_ids(ledger: dict[str, Any]) -> set[str]: ledger = normalize_ledger(ledger) return { str(e["action_id"]) for e in ledger["executed_actions"] if isinstance(e, dict) and e.get("action_id") }