Skip to content

"Fake Completion" Patterns in Multi-Agent Systems: How Agents Lie About Success

I spent three days debugging a multi-agent workflow where Agent A confidently reported “data exported successfully,” but Agent B kept finding an empty file. The logs showed green checkmarks everywhere. Everything looked perfect. Nothing worked.

This was my introduction to “fake completion” — when an agent claims success based on surface-level signals while the actual business logic failed or produced garbage.

The Problem That Made Me Question Reality

I was building a content pipeline with three agents:

  1. ResearchAgent — collects data and writes to /tmp/research.json
  2. WriteAgent — reads the research and creates a draft
  3. PublishAgent — finalizes and publishes the content

The pipeline kept producing empty articles. Here’s what I saw in the logs:

[ResearchAgent] SUCCESS: Data exported to /tmp/research.json
[WriteAgent] STARTING: Reading research data...
[WriteAgent] ERROR: research.json is empty

I checked the file. It existed. But it was empty.

Pattern 1: Tool Call Success ≠ Business Success

My ResearchAgent used a tool called write_to_file:

tools.js
export async function write_to_file(filepath, content) {
await fs.writeFile(filepath, content);
return { success: true }; // This is a lie
}

The function returned { success: true } because the file write operation itself succeeded. But the content parameter was empty — I had passed an empty string due to an upstream bug.

The agent saw success: true and moved on.

The Fix: Verify Actual Outcomes

tools-fixed.js
export async function write_to_file(filepath, content) {
await fs.writeFile(filepath, content);
// Read back and verify
const written = await fs.readFile(filepath, 'utf8');
const verified = written === content && content.length > 0;
return {
success: verified,
bytesWritten: written.length,
expectedBytes: content.length
};
}

Now the agent gets real feedback. If success is false, it knows something went wrong.

Pattern 2: Local Verification ≠ External Visibility

I thought I was being clever by having agents verify their own work:

agent-self-verify.js
async function exportResearch(data) {
await write_to_file('/tmp/research.json', JSON.stringify(data));
// Self-verify
const exists = fs.existsSync('/tmp/research.json');
return { success: exists }; // Another lie
}

This worked in my tests. It failed in production.

The issue? File system caching. The agent wrote to /tmp, but when another agent (running in a different container) read from /tmp, it saw a cached previous version or nothing at all.

The Fix: Use External Verification Points

agent-external-verify.js
async function exportResearch(data) {
const filepath = '/shared-volume/research.json';
await write_to_file(filepath, JSON.stringify(data));
// Use a shared storage with consistency guarantees
const verified = await verifyInSharedStorage(filepath, data);
return { success: verified };
}
async function verifyInSharedStorage(filepath, expectedData) {
// Wait for consistency
await waitForConsistency(filepath);
const actual = await readFromSharedStorage(filepath);
return JSON.stringify(actual) === JSON.stringify(expectedData);
}

In my case, I switched to using a shared database table as the communication channel instead of files:

agent-db-verify.js
async function exportResearch(data) {
const record = await db.research.create({
data: data,
status: 'ready',
createdAt: new Date()
});
// Immediately query back to verify
const verified = await db.research.findUnique({
where: { id: record.id }
});
return { success: verified?.data !== null };
}

Pattern 3: Declaration ≠ Dependency Readiness

The worst fake completion pattern I encountered:

pipeline-race.js
// Agent A
async function prepareData() {
// This is async, but we don't await it
writeToDatabase(data).then(() => {
console.log('Write complete');
});
return { status: 'data_prepared' }; // Lie: data is NOT prepared yet
}
// Agent B (starts immediately after A returns)
async function processData() {
const data = await readFromDatabase(); // Gets nothing or partial data
// ...
}

Agent A returned immediately. Agent B started immediately. The database write happened… later. Sometimes. Maybe.

The Fix: Explicit Ready Signals

pipeline-ready.js
// Agent A
async function prepareData() {
await writeToDatabase(data);
// Explicit verification
const ready = await verifyDataInDatabase(data);
if (!ready) {
throw new Error('Data preparation failed');
}
return { status: 'data_prepared', ready: true };
}
// Agent B (only starts when ready signal is true)
async function processData(input) {
if (!input.ready) {
throw new Error('Cannot process: data not ready');
}
const data = await readFromDatabase();
// ...
}

Why This Matters for Multi-Agent Systems

In a single-agent system, fake completion is annoying but contained. In a multi-agent pipeline:

graph LR
A[Agent A] -->|fake success| B[Agent B]
B -->|garbage in| C[Agent C]
C -->|garbage out| D[Production]

One agent’s lie becomes another agent’s corrupted input. The failure compounds silently. By the time you notice, you’re debugging Agent C’s output with no idea that Agent A was the culprit.

The Verification Checklist

After debugging these patterns, I created a checklist for my team:

  1. For API calls: Check response body for explicit success field and relevant status fields. Never trust HTTP status alone.
  2. For file operations: Read back and verify content matches expectations. Not just existence — actual content.
  3. For async operations: Await the operation or implement polling/callback verification.
  4. For inter-agent communication: Add explicit “ready” signals with verification, not just “done” status.

Common Mistakes I Made

  • Using fs.existsSync() to verify writes (file exists but is empty)
  • Assuming synchronous behavior in async systems (the race condition)
  • Trusting previous agent outputs without verification (cascade failure)
  • Using “completed” status instead of “verified” status (declaration vs reality)

The Real Solution: Skeptical Agents

The fix isn’t just technical — it’s philosophical. Your agents should be skeptical. They shouldn’t trust:

  • Their own operations (read back and verify)
  • Other agents’ outputs (validate before use)
  • Success signals at face value (check actual outcomes)

Here’s the pattern I now use everywhere:

skeptical-agent.js
async function skepticalOperation(input) {
// 1. Do the work
const result = await doWork(input);
// 2. Verify the work happened
const verified = await verifyResult(result);
if (!verified) {
// 3. If verification fails, investigate
const diagnosis = await diagnoseFailure(result);
throw new Error(`Operation failed: ${diagnosis.reason}`);
}
// 4. Return verified result only
return { success: true, verified: true, result };
}

This pattern caught 23 silent failures in my pipeline last month. All of them would have corrupted production data if left unchecked.

Summary

Fake completion is the silent killer of multi-agent reliability. Your agents will lie to you — not maliciously, but because they’re optimizing for the signals you gave them. If you only check surface-level success (HTTP 200, file exists, function returned), that’s all you’ll get.

The solution: verify actual business outcomes, not just operation results. Use read-back validation, explicit status checks, and proper async handling. Make your agents skeptical of themselves and each other.

Three days of debugging taught me this: in multi-agent systems, trust must be verified.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments