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:
- **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.
- **Stalled Pipeline:** Both orchestrators disagree and both halt, waiting for the other to resolve. The pipeline stalls until human intervention.
- **Divergent Audit Trails:** Each orchestrator logs its own interpretation, and downstream consumers see two contradictory records of the same sub-agent outcome.
Recorded Elements
- **`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.
- **`success_criterion_signature`:** The exact success criteria each orchestrator was applying, allowing the source of disagreement to be isolated.
- **`interpretation_divergence_receipt`:** A cryptographic proof that two orchestrators reached opposite interpretations, capturing the exact divergence point.
- **`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
- **Did multiple orchestrators evaluate the same sub-agent?** If no, standard single-orchestrator logging applies. If yes, proceed.
- **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.
- **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.
- **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
- **Contradiction Lock Prevention:** Prevents two orchestrators from taking divergent actions based on the same sub-agent.
- **Ground-Truth Override:** Provides a deterministic arbiter predicate, avoiding indefinite disagreement.
- **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
)