How should agents communicate when they disagree on whether a subagent succeeded?

**Date of Record:** 2026 October 27 **Scope:** Applies when two or more orchestrator agents receive results from a shared sub-agent and disagree on whether the sub-agent's outcome constitutes success, partial success, or failure. Does not apply to strictly hierarchical architectures with a single orchestrator, or cases where sub-agent outcome is determined by a single ground-truth signal.

Core Distinction

The separation between **outcome evidence** (what the sub-agent reported or what verifiable effects exist) and **outcome interpretation** (each orchestrator's judgment of whether the evidence meets the success criterion). Relying on a single orchestrator's interpretation creates the **Outcome Contradiction Lock**: two orchestrators interpret the same sub-agent result differently, each takes a divergent next action, and their state permanently diverges because neither can be proven wrong.

What Was Observed

In multi-orchestrator agent architectures, failing to handle outcome disagreement produces three primary failure modes:

  1. **Duplicate Compensation:** Orchestrator A judges the sub-agent failed and initiates compensation. Orchestrator B judges it succeeded and proceeds. Both actions execute, causing conflicting external effects.
  2. **Stalled Pipeline:** Both orchestrators disagree and both halt, waiting for the other to resolve. The pipeline stalls until human intervention.
  3. **Divergent Audit Trails:** Each orchestrator logs its own interpretation, and downstream consumers see two contradictory records of the same sub-agent outcome.

Recorded Elements

  1. **`subagent_outcome_evidence_hash`:** The cryptographic hash of all evidence (result payloads, exit codes, external side-effect receipts) available about the sub-agent's outcome.
  2. **`success_criterion_signature`:** The exact success criteria each orchestrator was applying, allowing the source of disagreement to be isolated.
  3. **`interpretation_divergence_receipt`:** A cryptographic proof that two orchestrators reached opposite interpretations, capturing the exact divergence point.
  4. **`ground_truth_arbiter_predicate`:** The exact external probe (e.g., a database query, an oracle call) that would definitively resolve the disagreement.

Condition of Validity

* **Applies when:** Multiple orchestrators can independently evaluate a shared sub-agent's outcome, their success criteria may differ, and downstream actions depend on the outcome interpretation. * **Does not apply when:** There is a single orchestrator (no disagreement possible), or the sub-agent's outcome is unambiguously verifiable from a single ground-truth source.

Decision Boundary

  1. **Did multiple orchestrators evaluate the same sub-agent?** If no, standard single-orchestrator logging applies. If yes, proceed.
  2. **Do the orchestrators agree on the success criterion?** If yes, disagreements are about evidence; hash the evidence and resolve. If no, the disagreement is about criteria.
  3. **Is the `ground_truth_arbiter_predicate` executable?** If yes, execute it and use the result to override both interpretations. If no, escalate to human reconciliation.
  4. **Conversely, when can orchestrators proceed without resolution?** Only when the disagreeing parties have disjoint downstream actions (their divergence has no external consequence) or when a designated arbiter orchestrator is authorized to make the final decision.

Operational Effect

  1. **Contradiction Lock Prevention:** Prevents two orchestrators from taking divergent actions based on the same sub-agent.
  2. **Ground-Truth Override:** Provides a deterministic arbiter predicate, avoiding indefinite disagreement.
  3. **Audit Consistency:** Produces a single reconciled record instead of contradictory audit trails.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class DisagreementVerdict(Enum):
    SINGLE_ORCHESTRATOR = auto()
    CRITERIA_MATCH_EVIDENCE_DIFFERS = auto()
    CRITERIA_DIFFER = auto()
    GROUND_TRUTH_RESOLVES = auto()

@dataclass(frozen=True)
class OrchestratorInterpretation:
    orchestrator_id: str
    success_criterion_hash: str
    outcome_interpretation: str

@dataclass(frozen=True)
class SharedSubagentResult:
    subagent_id: str
    outcome_evidence_hash: str
    interpretations: tuple[OrchestratorInterpretation, ...]
    ground_truth_arbiter_available: bool
    ground_truth_result: str | None

@dataclass(frozen=True)
class DisagreementManifest:
    verdict: DisagreementVerdict
    directive: str
    manifest_digest: str

class SubagentOutcomeDisagreementGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_disagreement(
        cls, result: SharedSubagentResult
    ) -> DisagreementManifest:
        
        # Gate 1: Single orchestrator
        if len(result.interpretations) <= 1:
            digest = cls._hash(json.dumps({
                "subagent": result.subagent_id,
                "interpretations": str(len(result.interpretations))
            }, sort_keys=True))
            return DisagreementManifest(
                verdict=DisagreementVerdict.SINGLE_ORCHESTRATOR,
                directive="PROCEED: Single interpretation, no disagreement.",
                manifest_digest=digest
            )

        # Gate 2: Criteria match check
        criteria_hashes = {i.success_criterion_hash for i in result.interpretations}
        interpretations = {i.outcome_interpretation for i in result.interpretations}

        # Gate 3: Ground truth override
        if result.ground_truth_arbiter_available and result.ground_truth_result:
            digest = cls._hash(json.dumps({
                "subagent": result.subagent_id,
                "ground_truth": result.ground_truth_result,
                "criterion_count": str(len(criteria_hashes))
            }, sort_keys=True))
            return DisagreementManifest(
                verdict=DisagreementVerdict.GROUND_TRUTH_RESOLVES,
                directive=f"PROCEED: Ground truth resolves to '{result.ground_truth_result}'.",
                manifest_digest=digest
            )

        # Gate 4: Same criteria
        if len(criteria_hashes) == 1 and len(interpretations) > 1:
            digest = cls._hash(json.dumps({
                "subagent": result.subagent_id,
                "criteria": tuple(sorted(criteria_hashes)),
                "interpretations": tuple(sorted(interpretations))
            }, sort_keys=True))
            return DisagreementManifest(
                verdict=DisagreementVerdict.CRITERIA_MATCH_EVIDENCE_DIFFERS,
                directive="HALT: Same criteria, different interpretations. Evidence disagreement.",
                manifest_digest=digest
            )

        # Gate 5: Different criteria
        digest = cls._hash(json.dumps({
            "subagent": result.subagent_id,
            "criteria_count": str(len(criteria_hashes)),
            "interpretations_count": str(len(interpretations))
        }, sort_keys=True))
        
        return DisagreementManifest(
            verdict=DisagreementVerdict.CRITERIA_DIFFER,
            directive=f"HALT: {len(criteria_hashes)} distinct criteria. Escalate to arbiter.",
            manifest_digest=digest
        )