Office Hours — When building multi-agent systems, what are the failure modes you've encountered and how do you prevent agents from taking unintended actions?
A daily developer question about AI/LLMs, answered with a direct, opinionated take.
When building multi-agent systems, what are the failure modes you’ve encountered and how do you prevent agents from taking unintended actions?
Multi-agent systems fail in predictable ways, but most teams discover them too late. The failures cluster into three categories: coordination breakdown, action creep, and context drift. Once you’ve shipped a system where agents step on each other’s work or take actions they weren’t supposed to, you start taking this seriously.
Coordination and State Conflicts
The first failure mode hits when agents don’t know what the other agents are doing. Two agents read the same database record, both decide it needs updating, and one overwrites the other’s work. Or worse, Agent A deletes a file that Agent B is still trying to process.
This sounds obvious until you’re running agents asynchronously across different deployment environments. One agent on the east coast and one on the west coast, both with a 30-second latency to the shared state. The gap between “I read this” and “I locked this” is where chaos lives.
The mitigation is explicit coordination. Lock files, version numbers on shared resources, or event-driven state machines where agents publish what they’re doing and subscribe to what matters. Claude Code instances running in parallel on macOS and Linux can now message each other and share context across terminals, which is exactly this pattern—you get visibility into what each agent is doing before it acts.
# Bad: agents collide silently
def agent_reads_and_updates(record_id):
record = db.get(record_id) # Agent A reads here
# Agent B also reads here
record.status = "processed"
db.save(record) # Agent A writes first, B overwrites
# Better: explicit locking and sequencing
def agent_reads_and_updates(record_id, agent_id):
with redis.lock(f"record:{record_id}", timeout=30):
record = db.get(record_id)
record.status = "processed"
record.last_agent = agent_id
db.save(record)
# Only one agent updates at a time
Action Creep and Scope Drift
Agents start executing actions they shouldn’t. An agent meant to read logs and suggest fixes instead actually applies patches. Another agent meant to query an API instead deletes resources because the API supports it and the prompt didn’t explicitly forbid it.
The issue is that LLMs are excellent at following the letter of your instructions and terrible at inferring the spirit. If you tell an agent to “fix database inconsistencies,” and the model can execute DELETE statements, it will. If you tell it to “optimize the deployment,” it might terminate instances without asking.
Prevent this with hard boundaries: separate read-only agents from write agents, use different credentials with different permissions, and enforce tool constraints at the framework level rather than hoping the model respects them. Claude Code Auto Mode is now the default, with a safety classifier catching 89% of dangerous commands versus 13.6% for humans, but that 11% failure rate on dangerous commands is not zero—keep humans in approval workflows for anything that mutates production state.
# Bad: broad permissions
agent_tools = [
Tool(name="run_sql", description="Execute any SQL query"),
Tool(name="kill_process", description="Terminate any process"),
]
# Better: constrained tools with explicit allowlists
readonly_tools = [
Tool(name="query_logs", allowed_tables=["logs", "metrics"]),
Tool(name="describe_schema", allowed_schemas=["analytics"]),
]
write_tools = [
Tool(name="insert_record", allowed_tables=["staging_only"]),
]
# Read agent gets readonly_tools
# Write agent gets both, but only after approval
Context Drift and Silent Failures
This is the subtlest failure. An agent completes what it thinks is a task successfully, reports success, and nobody realizes it solved the wrong problem because the context was stale or incomplete.
Agent A is told to “deploy the latest version.” It deploys version 1.2.3 because that’s what’s in the environment variable. But version 1.3.0 was just released and is the actual latest. Agent A succeeds, the system is stable, and the outdated version runs in production for three weeks.
Or Agent B is managing a database migration. It checks a config file to decide which tables to migrate. The config file was updated 10 minutes ago but the agent cached the old version in its context. It migrates the wrong tables, data integrity fails, and the agent’s logs show “migration succeeded.”
Fix this by making agents reason about their own certainty and require verification for ambiguous cases. Don’t let an agent decide it’s done. Have a verification agent or human check the outcome against the original goal.
# Bad: agent declares victory
result = agent.execute("deploy_latest_version")
if result.success:
log("deployment complete")
# Better: verify the outcome matches intent
result = agent.execute("deploy_latest_version")
if result.success:
actual_version = verify_deployed_version()
expected_version = get_latest_release()
if actual_version != expected_version:
alert("deployed version mismatch: got %s, expected %s" %
(actual_version, expected_version))
rollback()
Token Costs and Runaway Workflows
Not technically an unintended action, but this deserves mention: multi-agent systems can silently triple token costs if agents are messaging each other inefficiently. Each agent call costs tokens, and if three agents are all trying to solve the same problem in parallel, you’re paying three times.
Set explicit per-agent and per-workflow token budgets. Claude Fable 5.1 has 1M context and agents can iterate extensively, but that doesn’t mean they should. Budget 100K tokens per agent task, enforce it at the framework level, and when an agent hits the limit, it stops and escalates.
Goldman Sachs and the Reality Check
Goldman Sachs pulled AI coding tools from traders after a brief experiment, citing productivity and risk concerns. They discovered that distributing agent access creates risk they weren’t prepared to manage. The lesson: don’t give all users all agent types. Tier access based on the agent’s capability to do harm and the user’s ability to understand what it’s doing.
Bottom line: Coordinate multi-agent state explicitly using locks or event streams rather than hoping asynchrony doesn’t break things. Separate read and write permissions, constrain tool access at the framework level, and require verification that agents solved the actual problem—not just that they reported success.
Question via Hacker News