Multi-agent coordination is how complex tasks escape the single-context trap. One agent has one context window, one model, and one thread of execution — that ceiling is real. Multi-agent systems break it by decomposing work into parallel streams, assigning specialized roles, and isolating execution environments so agents can't corrupt each other's state. The pattern matters because the hardest real-world tasks — incident response, large-scale code refactors, competitive research — are inherently parallel and inherently risky to run in a single shared context.
Claude Code's AgentTool
is the orchestration hub. It supports five distinct execution modes, each solving a different coordination problem:
Fork subagents and prompt cache sharing. The fork path is the most cache-efficient mode. When a fork agent is spawned, the parent's already-rendered system prompt is passed directly to the child:
// src/tools/AgentTool/AgentTool.tsx
// Fork path: pass parent's rendered system prompt to child so the API
// sees identical bytes and can reuse the cached KV computation.
if (isForkPath) {
if (toolUseContext.renderedSystemPrompt) {
forkParentSystemPrompt = toolUseContext.renderedSystemPrompt
} else {
// Fallback: recompute — may diverge from parent's cached bytes
forkParentSystemPrompt = buildEffectiveSystemPrompt({ ... })
}
promptMessages = buildForkedMessages(prompt, assistantMessage)
}
The child also receives useExactTools: true
, meaning it gets the parent's exact tool array rather than a filtered subset. This is what makes fork agents feel like continuations of the parent rather than fresh agents.
Worktree isolation. When isolation: 'worktree'
is set, the agent gets its own git worktree — a separate filesystem branch that can't contaminate the parent's working tree:
// Create a git worktree scoped to this agent's ID
if (effectiveIsolation === 'worktree') {
const slug = `agent-${earlyAgentId.slice(0, 8)}`
worktreeInfo = await createAgentWorktree(slug)
}
// After completion: keep worktree only if it has changes
const changed = await hasWorktreeChanges(worktreePath, headCommit)
if (!changed) {
await removeAgentWorktree(worktreePath, worktreeBranch, gitRoot)
}
Async backgrounding mid-execution. A sync agent can be promoted to background while it's running. The parent races the agent's iterator against a background signal:
// Race: next agent message vs. user-triggered background promotion
const raceResult = backgroundPromise
? await Promise.race([
nextMessagePromise.then(r => ({ type: 'message', result: r })),
backgroundPromise // resolves if user clicks "send to background"
])
: { type: 'message', result: await nextMessagePromise }
SendMessage routing. Named agents are registered in a global name registry so any agent can address them by name:
// Register name → agentId so SendMessage can route to this agent
if (name) {
rootSetAppState(prev => {
const next = new Map(prev.agentNameRegistry)
next.set(name, asAgentId(asyncAgentId))
return { ...prev, agentNameRegistry: next }
})
}
Teammate constraints. Teams are flat — a teammate cannot spawn other teammates, and in-process teammates cannot spawn background agents. This prevents unbounded nesting:
// Flat roster enforcement: teammates cannot spawn teammates
if (isTeammate() && teamName && name) {
throw new Error('Teammates cannot spawn other teammates — the team roster is flat.')
}
Hermes takes a Python-native approach: child agents are full AIAgent
instances constructed on the main thread and run in a ThreadPoolExecutor
. The key files are delegate_tool.py
(single and batch delegation) and mixture_of_agents_tool.py
(parallel model aggregation).
_run_single_child() — the execution unit. Each child runs run_conversation()
in its own thread and returns a structured result dict:
# hermes-agent/tools/delegate_tool.py
def _run_single_child(task_index, goal, child=None, parent_agent=None, **_kwargs):
# Each child is a full AIAgent with its own iteration budget.
# The parent blocks on this thread until the child's run_conversation() returns.
child_start = time.monotonic()
try:
result = child.run_conversation(user_message=goal)
summary = result.get("final_response") or ""
# Status is derived from the child's exit condition, not just completion
status = "interrupted" if result.get("interrupted") else (
"completed" if summary else "failed"
)
return {
"task_index": task_index,
"status": status,
"summary": summary,
"exit_reason": "completed" if result.get("completed") else "max_iterations",
"tokens": {
"input": getattr(child, "session_prompt_tokens", 0),
"output": getattr(child, "session_completion_tokens", 0),
},
}
finally:
# Always unregister child from interrupt propagation list
if hasattr(parent_agent, '_active_children'):
with parent_agent._active_children_lock:
parent_agent._active_children.remove(child)
_active_children — interrupt propagation. The parent maintains a list of all running children. When the parent receives an interrupt, it propagates to every child:
# hermes-agent/run_agent.py
# Initialized in AIAgent.__init__:
self._delegate_depth = 0 # 0 = top-level; incremented per child generation
self._active_children = [] # All currently running child AIAgents
self._active_children_lock = threading.Lock()
# In AIAgent.interrupt():
with self._active_children_lock:
children_copy = list(self._active_children)
for child in children_copy:
try:
child.interrupt(message) # Cascade interrupt down the tree
except Exception as e:
logger.debug("Failed to propagate interrupt to child agent: %s", e)
_delegate_depth — infinite delegation guard. Children cannot spawn grandchildren beyond MAX_DEPTH = 2
:
# hermes-agent/tools/delegate_tool.py
MAX_DEPTH = 2 # parent(0) -> child(1) -> grandchild rejected(2)
depth = getattr(parent_agent, '_delegate_depth', 0)
if depth >= MAX_DEPTH:
return json.dumps({"error": "Delegation depth limit reached. Subagents cannot spawn further subagents."})
# When building a child, increment its depth
child._delegate_depth = getattr(parent_agent, '_delegate_depth', 0) + 1
Batch parallel execution. Multiple tasks run concurrently via ThreadPoolExecutor
, capped at MAX_CONCURRENT_CHILDREN = 3
:
# hermes-agent/tools/delegate_tool.py
# Batch mode: submit all children to the thread pool, collect as they complete
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_CHILDREN) as executor:
futures = {
executor.submit(_run_single_child, i, t["goal"], child, parent_agent): i
for i, t, child in children
}
for future in as_completed(futures):
entry = future.result()
results.append(entry)
# Print per-task completion line above the spinner in real time
icon = "✓" if entry["status"] == "completed" else "✗"
spinner_ref.print_above(f"{icon} [{entry['task_index']+1}/{n_tasks}] ...")
mixture_of_agents_tool.py — parallel model aggregation. This is a different coordination pattern: instead of delegating tasks to specialized agents, it fans out the same query to multiple frontier models and aggregates the results:
# hermes-agent/tools/mixture_of_agents_tool.py
# Layer 1: Query all reference models in parallel using asyncio.gather
# Temperature 0.6 encourages diverse perspectives across models
model_results = await asyncio.gather(*[
_run_reference_model_safe(model, user_prompt, REFERENCE_TEMPERATURE)
for model in ref_models # claude-opus-4.6, gemini-3-pro, gpt-5.4-pro, deepseek-v3.2
])
# Layer 2: Aggregator synthesizes the successful responses into one answer
# Temperature 0.4 keeps the synthesis focused and consistent
aggregator_system_prompt = _construct_aggregator_prompt(
AGGREGATOR_SYSTEM_PROMPT,
[content for _, content, success in model_results if success]
)
final_response = await _run_aggregator_model(aggregator_system_prompt, user_prompt)
Terminal backends. Hermes children can execute in six different environments depending on the platform
parameter: local shell, Docker container, SSH remote, Modal serverless, Daytona workspace, or Singularity container. This is configured at the AIAgent
level and inherited by children.