How to Dispatch Multiple Sub-Agents in Parallel Using Fleet Mode in the Copilot SDK
Fleet Mode is the Copilot runtime's built-in pattern for orchestrating many independent sub-agents (workers) in parallel through a shared SQLite coordination state and automatic task dispatching.
The Copilot SDK (github/copilot-sdk) enables complex multi-agent workflows through Fleet Mode, a runtime pattern that manages concurrent execution without explicit thread management. Instead of chaining sub-agents sequentially, you can dispatch multiple sub-agents in parallel using Fleet Mode to process independent work units simultaneously. This approach leverages durable SQL tables for coordination and the runtime's automatic task distribution to handle complex batches of work across your codebase.
Understanding Fleet Mode Architecture
Fleet Mode replaces implicit shared memory with explicit coordination via SQL tables, making parallel execution deterministic and recoverable across restarts. The architecture centers on a parent session that defines work units called todos, which persist in a SQLite database that survives process interruptions.
The coordination schema consists of two core tables defined in the SDK documentation at docs/features/fleet-mode.md:
CREATE TABLE todos (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'pending'
);
CREATE TABLE todo_deps (
todo_id TEXT,
depends_on TEXT,
PRIMARY KEY (todo_id, depends_on)
);
Each todo transitions through a strict state machine: pending → in_progress → done (or blocked when dependencies fail). The runtime automatically dispatches sub-agents only for todos with status = 'pending' and all dependencies marked done.
Starting Fleet Mode
To begin parallel execution, invoke the generated RPC call session.rpc.fleet.start with an optional prompt that supplies high-level instructions for the fleet. The RPC interfaces are defined in nodejs/src/generated/rpc.ts as FleetStartRequest and FleetStartResult.
Key parameters:
prompt: High-level instruction passed to all sub-agents in the fleetstarted: Boolean returned in the result confirming fleet activation
When the orchestrator successfully initializes, the runtime returns started: true and begins monitoring the coordination tables for ready work units.
Creating the Coordination State
After starting Fleet Mode, the parent agent initializes the durable state tables. Each row in the todos table represents an independent unit of work—such as a file to refactor or a microservice to analyze—while todo_deps defines any execution prerequisites.
Insert work items using standard SQL. For example, to create independent tasks for multiple packages:
const packages = ["core", "java", "python"];
const stmt = db.prepare(
"INSERT INTO todos (id, title, description) VALUES (?, ?, ?)"
);
for (const pkg of packages) {
stmt.run(`todo-${pkg}`, `Refactor ${pkg}`, `Refactor the ${pkg} SDK package`);
}
Sub-agents claim work by atomically updating the status from pending to in_progress, preventing duplicate processing across concurrent workers.
Dispatching and Monitoring Sub-Agents
The Copilot runtime automatically invokes the task tool for each ready todo, launching sub-agents with agent_type specifications without blocking the parent session. While the SDK does not expose dedicated subagentStart or subagentStop hooks, the generic session event stream emits lifecycle events that you can observe via session.on listeners.
Available event types:
subagent.started: Emitted when a worker claims a todo and begins executionsubagent.completed: Emitted when the worker sets status todonesubagent.failed: Emitted when processing errors occur
These events are validated in nodejs/test/e2e/subagent_hooks.e2e.test.ts, which demonstrates reliable monitoring of parallel worker lifecycles without explicit polling loops.
Aggregating Results
Once all todos reach done status (or after implementing timeout logic), the parent session queries the coordination tables to collect outputs. Sub-agents typically write results back to the session context or files before marking completion, enabling the parent to generate combined summaries or proceed with aggregated data.
Implementation Examples
The following examples demonstrate dispatching multiple sub-agents in parallel using Fleet Mode across TypeScript, Python, and Go.
Node.js / TypeScript
This example uses an in-memory SQLite database to coordinate refactoring tasks across SDK packages:
import { CopilotClient } from "copilot-sdk";
import { sql } from "better-sqlite3";
async function runFleet() {
const client = new CopilotClient();
const session = await client.createSession();
// 1️⃣ Start fleet mode
const fleet = await session.rpc.fleet.start({
prompt: "Refactor each SDK package independently, then summarize the changes."
});
if (!fleet.started) throw new Error("Fleet failed to start");
// 2️⃣ Create coordination tables
const db = sql(":memory:");
db.exec(`
CREATE TABLE todos (id TEXT PRIMARY KEY, title TEXT, description TEXT, status TEXT DEFAULT 'pending');
CREATE TABLE todo_deps (todo_id TEXT, depends_on TEXT, PRIMARY KEY (todo_id, depends_on));
`);
// 3️⃣ Insert independent work items
const packages = ["core", "java", "python"];
const stmt = db.prepare(
"INSERT INTO todos (id, title, description) VALUES (?, ?, ?)"
);
for (const pkg of packages) {
stmt.run(`todo-${pkg}`, `Refactor ${pkg}`, `Refactor the ${pkg} SDK package`);
}
// 4️⃣ Listen to sub-agent lifecycle events
session.on((e) => {
if (e.type === "subagent.started") console.log(`🟢 ${e.data.agentDisplayName} started`);
if (e.type === "subagent.completed") console.log(`✅ ${e.data.agentDisplayName} finished`);
});
// Sub-agents automatically claim and process todos via the task tool
// Parent aggregates results after completion
const all = db.prepare("SELECT * FROM todos").all();
console.log("All todos:", all);
}
Python
The Python SDK mirrors the RPC surface with FleetStartRequest and automatic task dispatching:
import asyncio
from copilot import CopilotClient
from copilot.rpc import FleetStartRequest
async def main():
client = CopilotClient()
await client.start()
session = await client.create_session()
# 1️⃣ Start fleet mode
result = await session.rpc.fleet.start(
FleetStartRequest(prompt="Review each microservice independently, then summarize risks.")
)
if not result.started:
raise RuntimeError("Fleet start failed")
# 2️⃣ Create coordination tables
await session.exec_sql("""
CREATE TABLE todos (id TEXT PRIMARY KEY, title TEXT, description TEXT, status TEXT DEFAULT 'pending');
CREATE TABLE todo_deps (todo_id TEXT, depends_on TEXT, PRIMARY KEY (todo_id, depends_on));
""")
# 3️⃣ Insert work items
services = ["auth", "billing", "notifications"]
for svc in services:
await session.exec_sql(
"INSERT INTO todos (id, title, description) VALUES (?, ?, ?)",
(f"todo-{svc}", f"Review {svc}", f"Security review of the {svc} service")
)
# 4️⃣ Subscribe to sub-agent events
def handle(event):
if event.type == "subagent.started":
print(f"🟢 {event.data.agent_display_name} started")
if event.type == "subagent.completed":
print(f"✅ {event.data.agent_display_name} completed")
session.on(handle)
# 5️⃣ Wait for completion
while True:
rows = await session.query_sql("SELECT * FROM todos WHERE status != 'done'")
if not rows:
break
await asyncio.sleep(1)
# 6️⃣ Gather results
all_todos = await session.query_sql("SELECT * FROM todos")
print("All todos:", all_todos)
asyncio.run(main())
Go
The Go bindings in github.com/github/copilot-sdk/go provide the same RPC interfaces:
package main
import (
"context"
"fmt"
"log"
"time"
copilot "github.com/github/copilot-sdk/go"
"github.com/github/copilot-sdk/go/rpc"
)
func main() {
ctx := context.Background()
client := copilot.NewClient(nil)
// 1️⃣ Start fleet mode
fleetResult, err := client.Session().RPC.Fleet.Start(ctx, &rpc.FleetStartRequest{
Prompt: stringPtr("Refactor each package independently, then report validation results."),
})
if err != nil || !fleetResult.Started {
log.Fatalf("failed to start fleet: %v", err)
}
// 2️⃣ Create coordination tables
if _, err := client.Session().ExecSQL(ctx, `
CREATE TABLE todos (id TEXT PRIMARY KEY, title TEXT, description TEXT, status TEXT DEFAULT 'pending');
CREATE TABLE todo_deps (todo_id TEXT, depends_on TEXT, PRIMARY KEY (todo_id, depends_on));
`); err != nil {
log.Fatalf("sql error: %v", err)
}
// 3️⃣ Insert work items
pkgs := []string{"core", "java", "python"}
for _, p := range pkgs {
_, err := client.Session().ExecSQL(ctx, `
INSERT INTO todos (id, title, description) VALUES (?, ?, ?);
`, fmt.Sprintf("todo-%s", p), fmt.Sprintf("Refactor %s", p), fmt.Sprintf("Refactor the %s SDK package", p))
if err != nil {
log.Fatalf("insert error: %v", err)
}
}
// 4️⃣ Subscribe to sub-agent events
client.Session().On(func(e copilot.Event) {
switch e.Type {
case "subagent.started":
fmt.Printf("🟢 %s started\n", e.Data["agentDisplayName"])
case "subagent.completed":
fmt.Printf("✅ %s completed\n", e.Data["agentDisplayName"])
}
})
// 5️⃣ Wait for completion
for {
rows, _ := client.Session().QuerySQL(ctx, "SELECT * FROM todos WHERE status != 'done'")
if len(rows) == 0 {
break
}
time.Sleep(time.Second)
}
// 6️⃣ Final aggregation
all, _ := client.Session().QuerySQL(ctx, "SELECT * FROM todos")
fmt.Println("All todos:", all)
}
func stringPtr(s string) *string {
return &s
}
Summary
- Fleet Mode enables parallel sub-agent execution through the
session.rpc.fleet.startRPC call defined innodejs/src/generated/rpc.ts - Coordination relies on explicit SQL state management via
todosandtodo_depstables rather than implicit shared memory - Automatic dispatch occurs when the runtime's
tasktool picks up ready todos (statuspendingwith satisfied dependencies) - Lifecycle monitoring uses the generic event stream (
subagent.started,subagent.completed) tested innodejs/test/e2e/subagent_hooks.e2e.test.ts - Deterministic recovery persists state across restarts, making long-running fleet operations resilient to interruptions
Frequently Asked Questions
What is the difference between Fleet Mode and sequential sub-agent calls?
Sequential calls execute one sub-agent at a time, blocking until each returns. Fleet Mode launches many sub-agents concurrently using the runtime's task distribution system, coordinated through the SQL todos table. This reduces total execution time for independent work units and is validated in nodejs/test/e2e/rpc_shell_and_fleet.e2e.test.ts.
Can sub-agents in Fleet Mode communicate with each other?
No. Sub-agents are intentionally isolated and communicate only through the durable coordination tables by updating their status and writing results to shared storage. They cannot directly share memory or call each other's functions, which prevents coupling and ensures deterministic execution.
How does the runtime handle failed sub-agents?
When a sub-agent encounters an error, it sets its todo status to blocked or emits a subagent.failed event. The parent session can query the todos table to identify blocked items and implement retry logic, or proceed with partial results from successfully completed workers.
Is Fleet Mode suitable for tasks with complex dependencies?
Yes. The todo_deps table allows modeling directed acyclic graphs where a todo only becomes ready when specific dependencies reach done. However, for tightly coupled sequential steps where each step requires the concrete output of the previous, standard sequential calls remain more appropriate than Fleet Mode's parallel coordination.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →