DEV Community

Cover image for Code Agent Anatomy (18): AgentTeams — The Parallel Mechanism of TeamFanout and TeamCollect
WonderLab
WonderLab

Posted on

Code Agent Anatomy (18): AgentTeams — The Parallel Mechanism of TeamFanout and TeamCollect

Building on the Previous Article

The previous article covered the message ACK three states: messages go from pending to delivered to processed, each state has a clear trigger moment, and the coordinator can query at any time.

Now we move up a layer and look at how tasks are batch-distributed (TeamFanout) and how results are aggregated back (TeamCollect).

These two operations together represent AgentTeams' core capability: break a large task into multiple smaller tasks, execute in parallel, and reassemble the results.


Conclusions First

Tool Responsibility Key Design
TeamFanout Distribute tasks to multiple member agents Each member gets an independent context slice, no interference
TeamCollect Wait for all members to finish, aggregate results Supports timeout, partial results, conflict detection

The core idea behind their coordination: divide and conquer — context isolation when splitting, explicit conflict handling when aggregating.


1. TeamFanout: Distribution Is Not Copying

The most intuitive multi-agent distribution approach: copy the entire task description and send it to each member, letting them each do their part.

This approach has a fundamental problem: each member's context contains all the information, but they actually only need to process a portion of it.

For example, 50 files need refactoring. You send all 50 file paths and contents to all 5 agents — each agent's context window is stuffed to capacity, and each agent has to search through 50 files when making decisions, wasting massive amounts of tokens.

TeamFanout doesn't work this way.

Coordinator calls TeamFanout:
  tasks = [
    {"member": "agent_1", "scope": ["file_1.py", ..., "file_10.py"], "instruction": "Refactor to async"},
    {"member": "agent_2", "scope": ["file_11.py", ..., "file_20.py"], "instruction": "Refactor to async"},
    ...
  ]
Enter fullscreen mode Exit fullscreen mode

Each member agent only receives its own portion of the task, including:

  • Its own list of files to handle (scope)
  • The processing instruction for this batch of files (instruction)
  • Necessary shared context (such as shared interface definitions)

The member agent starts up and only sees this information — it doesn't know and doesn't need to know what other members are doing.

This is context isolation: each agent's cognitive boundary is deliberately defined, focused on its own subtask.


2. How Member Agents "Truly Run in Parallel"

The previous article mentioned three execution modes: in-process, tmux, auto.

To truly understand parallelism, you need to understand the fundamental difference between these three modes.

in-process Mode

Main process
  ├── Primary agent loop (thread A)
  ├── Member agent 1 loop (thread B)
  ├── Member agent 2 loop (thread C)
  └── Member agent 3 loop (thread D)
Enter fullscreen mode Exit fullscreen mode

All agents run in the same Python process, in different threads.

Pros: Fast startup, shared memory, low communication overhead.

Cons: Limited by Python GIL (Global Interpreter Lock) — CPU-intensive operations can't truly parallelize. But since LLM calls are IO-intensive operations (most time spent waiting for network responses), the GIL's impact is less than you'd think — the GIL is released during waiting, allowing other threads to run.

tmux Mode

Main process          tmux session
  │                        │
  │                ┌───────┴───────┐
  │           Terminal1  Terminal2  Terminal3
  │                │              │
  │           Separate process  Separate process
  │                        │
  └────── Communicate via message queue ──────┘
Enter fullscreen mode Exit fullscreen mode

Each member agent is an independent OS process running in a dedicated tmux pane.

Pros: True OS-level parallelism, mutually isolated — one member crashing doesn't affect others.

Cons: Starting a tmux process takes hundreds of milliseconds; if there are many tasks, startup overhead can be significant.

auto Mode

The framework automatically selects based on the current environment: use tmux if available, otherwise use in-process.


3. The TeamFanout Distribution Process

Now let's walk through the complete execution process of TeamFanout:

Coordinator calls TeamFanout(tasks=[task1, task2, task3])
    │
    ▼
1. Generate one message per task, write to message queue
   (task1 → message_id_1, task2 → message_id_2, task3 → message_id_3)
   All messages initial state: pending
    │
    ▼
2. Start member agents based on execution mode
   (in-process: create new thread; tmux: fork new process)
    │
    ▼
3. Each member agent after starting:
   a. Check its own "inbox", pick up pending message
   b. Message state changes to delivered
   c. Begin executing task
    │
    ▼
4. TeamFanout returns immediately (doesn't wait for members to finish!)
   Returns: {message_ids: [message_id_1, message_id_2, message_id_3]}
    │
    ▼
Coordinator gets message_ids, can continue doing other things
(like processing the first batch itself, or preparing merge logic)
Enter fullscreen mode Exit fullscreen mode

There's a key point here: TeamFanout is non-blocking. It distributes the tasks and returns immediately — the coordinator doesn't need to wait here.

This is different from how we typically think of "function calls" — call a function, wait for result, function returns. TeamFanout is "call function, function returns immediately, get result later."


4. TeamCollect: Reassembling the Results

After member agents each complete their tasks, the coordinator needs to collect the results. That's TeamCollect's job.

results = TeamCollect(
    message_ids=[message_id_1, message_id_2, message_id_3],
    timeout=300,          # wait up to 5 minutes
    require_all=False,    # whether to require all members to complete
)
Enter fullscreen mode Exit fullscreen mode

TeamCollect will:

  1. Wait: continuously query message states until all specified messages become processed, or timeout
  2. Collect: retrieve the processing result for each message
  3. Return: package all results and return to the coordinator

The returned results have roughly this structure:

{
  "completed": [
    {"message_id": "id_1", "member": "agent_1", "result": "file_1.py refactored", "files_modified": ["file_1.py"]},
    {"message_id": "id_2", "member": "agent_2", "result": "file_11.py refactored", "files_modified": ["file_11.py"]},
  ],
  "failed": [
    {"message_id": "id_3", "member": "agent_3", "error": "timeout", "status": "delivered"}
  ],
  "pending": []
}
Enter fullscreen mode Exit fullscreen mode

Note the require_all=False parameter: allows returning on partial completion. This is important — if one member crashes, you don't want the entire task stuck waiting forever. Instead, get the completed parts first and handle the failed parts separately.


5. What Happens When Results Conflict

With multiple member agents working in parallel, the trickiest problem is conflicts: two members both modified the same file — whose version wins?

AgentTeams' approach to this problem is: detect conflicts, but don't automatically resolve them.

When TeamCollect aggregates results, it checks the file lists modified by each member. If it finds overlap, it marks this in the returned results:

{
  "conflicts": [
    {
      "file": "config.py",
      "modified_by": ["agent_1", "agent_3"],
      "message": "This file was modified by multiple members, requires manual or coordinator intervention to merge"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Then the coordinator (or a human) decides how to resolve it.

This design choice is pragmatic: automatically merging code modifications from two LLMs is nearly impossible to get right. Rather than building a complex automatic merge algorithm, explicitly mark the conflicts and let whoever has better judgment handle them.


6. A Complete Flow Example

Putting together articles 16-18, a complete AgentTeams collaboration flow looks like this:

1. Primary agent analyzes task, decides how to split it
   ↓
2. Call TeamCreate, create a team
   Returns: team_id = "team_abc"
   ↓
3. Prepare task list, each member gets an independent scope
   tasks = [
     {member: "agent_1", scope: ["file_1..10.py"]},
     {member: "agent_2", scope: ["file_11..20.py"]},
     ...
   ]
   ↓
4. Call TeamFanout, distribute tasks (non-blocking, returns immediately)
   Returns: message_ids = ["msg_1", "msg_2", ...]
   ↓
5. Primary agent continues doing its own work (e.g., writing framework code, preparing tests)
   Meanwhile, member agents work in parallel...
   ↓
6. Primary agent finishes its work, calls TeamCollect to wait for member results
   results = TeamCollect(message_ids, timeout=300)
   ↓
7. Check results: what succeeded, what failed, any conflicts
   ↓
8. Handle failures and conflicts, merge all results
   ↓
9. Call TeamDelete, clean up team, release resources
Enter fullscreen mode Exit fullscreen mode

In this flow, step 5 is the key — the primary agent isn't idle while waiting for members but is processing other tasks in parallel. This is the true value of a multi-agent system.


Design Highlights

1. Context Trimmed to Member Granularity

Each member only receives the information it needs. This isn't just about saving tokens — more importantly it keeps the member agent's decision-making focused: the cleaner the context it sees, the more accurate the results it produces.

2. Non-Blocking Fanout

TeamFanout returns immediately, allowing the coordinator to continue working during the wait. This is the core efficiency gain of multi-agent systems: coordinator and members are truly parallel — the coordinator isn't just watching members work.

3. Explicit Conflicts Rather Than Auto-Merge

Mark conflicts and hand them off to whoever has better judgment. This is more robust in engineering than automatic merging — you can never know if two LLMs' modifications to the same file are semantically compatible.

4. Partial Results Are Usable (require_all=False)

Allows returning results even when some members have failed. This means the system, when facing a single member failure, doesn't start all over — it preserves completed work and only redoes the failed parts.


Summary

Design Choice Approach Engineering Value
Task distribution TeamFanout non-blocking, each member has independent scope Coordinator and members truly parallel, context isolated
Parallel execution in-process / tmux choose one Balance startup overhead vs. true parallelism
Result collection TeamCollect wait + timeout protection Tolerates partial failure, doesn't wait forever
Conflict handling Detect but don't auto-resolve More robust than auto-merge, judgment handed to more capable party

The next article is the final one in this series, and also the most interesting one: why was AgentTeams ultimately removed from the stable release, and what lessons does its failure teach us?


About the Source Code for This Series

All analysis in this series is based on the open-source project MyCodeAgent.

AgentTeams' implementation has been removed from the stable release. The specific design record is in docs/archives/legacy-harness/HARNESS_ROADMAP.md Phase 7 section, and the removal plan is in docs/plans/2026-07-12-lean-runtime/tasks/M2-03-remove-agent-teams.md.

git clone https://github.com/chendongqi/MyCodeAgent
cd MyCodeAgent
cp .env.example .env
uv sync
uv run python main.py
Enter fullscreen mode Exit fullscreen mode

Visit PrimeSkills — a curated AI Agent and skills marketplace where every piece of content is validated against real enterprise workflows. No hype, only things that actually work.

For more practical insights and interesting products, visit my homepage

Top comments (0)