惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Vulnerabilities – Threatpost
T
The Blog of Author Tim Ferriss
S
SegmentFault 最新的问题
D
DataBreaches.Net
博客园_首页
罗磊的独立博客
B
Blog
T
Threat Research - Cisco Blogs
C
Cisco Blogs
GbyAI
GbyAI
Engineering at Meta
Engineering at Meta
WordPress大学
WordPress大学
G
GRAHAM CLULEY
H
Help Net Security
酷 壳 – CoolShell
酷 壳 – CoolShell
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
爱范儿
爱范儿
SecWiki News
SecWiki News
T
Threatpost
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Schneier on Security
Schneier on Security
T
The Exploit Database - CXSecurity.com
Google Online Security Blog
Google Online Security Blog
T
Tor Project blog
小众软件
小众软件
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Y
Y Combinator Blog
H
Hacker News: Front Page
V
V2EX
Security Latest
Security Latest
Cloudbric
Cloudbric
Simon Willison's Weblog
Simon Willison's Weblog
Attack and Defense Labs
Attack and Defense Labs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
P
Proofpoint News Feed
博客园 - 三生石上(FineUI控件)
NISL@THU
NISL@THU
S
Secure Thoughts
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
V2EX - 技术
V2EX - 技术
Vercel News
Vercel News
P
Palo Alto Networks Blog
IT之家
IT之家
MyScale Blog
MyScale Blog
有赞技术团队
有赞技术团队
Application and Cybersecurity Blog
Application and Cybersecurity Blog
D
Docker
Google DeepMind News
Google DeepMind News
Webroot Blog
Webroot Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Deep Dive into Open Agent SDK (Part 4): Multi-Agent Collaboration
NEE · 2026-04-27 · via DEV Community

A single Agent, no matter how powerful, is just one executor. Real development tasks are often multi-step and multi-role: someone explores the codebase, someone designs a plan, then someone writes code and runs tests. A single Agent working alone easily bloats its context and loses efficiency.

Open Agent SDK addresses this at three levels:

  1. Sub-Agents — The main Agent dynamically spawns sub-agents during execution, delegating specialized tasks
  2. Task System — Tracks progress and results of multi-step work
  3. Team + Messaging — Multiple Agents form a team, communicating via a mailbox system

This article analyzes each level's implementation, then examines how they combine for task orchestration.

1. Sub-Agents: SubAgentSpawner Protocol and AgentTool

SubAgentSpawner Protocol

Sub-agent spawning isn't AgentTool directly creating a new Agent — there's a protocol layer in between. SubAgentSpawner is defined in Types/AgentTypes.swift:

public protocol SubAgentSpawner: Sendable {
    func spawn(
        prompt: String,
        model: String?,
        systemPrompt: String?,
        allowedTools: [String]?,
        maxTurns: Int?
    ) async -> SubAgentResult

    func spawn(
        prompt: String,
        model: String?,
        systemPrompt: String?,
        allowedTools: [String]?,
        maxTurns: Int?,
        disallowedTools: [String]?,
        mcpServers: [AgentMcpServerSpec]?,
        skills: [String]?,
        runInBackground: Bool?,
        isolation: String?,
        name: String?,
        teamName: String?,
        mode: PermissionMode?,
        resume: String?
    ) async -> SubAgentResult
}

Enter fullscreen mode Exit fullscreen mode

Two methods: a basic version (5 parameters) and an enhanced version (13 parameters). The protocol also provides a default implementation where the enhanced version calls the basic one, so existing implementations don't need changes to be compatible.

Why is the spawner defined in Types/ instead of Core/? Because Tools/Advanced/AgentTool.swift needs it, but Tools/ shouldn't import Core/. The protocol is defined in Types/, with concrete implementation in Core/, injected via ToolContext.agentSpawner — a common dependency inversion pattern in the SDK.

DefaultSubAgentSpawner Implementation

DefaultSubAgentSpawner in Core/DefaultSubAgentSpawner.swift does the following:

final class DefaultSubAgentSpawner: SubAgentSpawner, @unchecked Sendable {
    private let apiKey: String
    private let baseURL: String?
    private let parentModel: String
    private let parentTools: [ToolProtocol]
    private let provider: LLMProvider
    private let client: (any LLMClient)?

    func spawn(...) async -> SubAgentResult {
        // 1. Filter out AgentTool to prevent infinite recursion
        var subTools = parentTools.filter { $0.name != "Agent" }

        // 2. If allowedTools specified, filter further
        if let allowed = allowedTools, !allowed.isEmpty {
            let allowedSet = Set(allowed)
            subTools = subTools.filter { allowedSet.contains($0.name) }
        }

        // 3. disallowedTools filters again (higher priority than allowedTools)
        if let disallowed = disallowedTools, !disallowed.isEmpty {
            let disallowedSet = Set(disallowed)
            subTools = subTools.filter { !disallowedSet.contains($0.name) }
        }

        // 4. Create sub-agent and execute
        let options = AgentOptions(
            apiKey: apiKey,
            model: model ?? parentModel,
            systemPrompt: systemPrompt,
            maxTurns: maxTurns ?? 10,
            tools: subTools
        )
        let agent = Agent(options: options)
        let result = await agent.prompt(prompt)

        return SubAgentResult(
            text: result.text.isEmpty
                ? "(Subagent completed with no text output)"
                : result.text,
            toolCalls: [],
            isError: result.status != .success
        )
    }
}

Enter fullscreen mode Exit fullscreen mode

Key points:

  • Recursion prevention: Sub-agents never receive AgentTool, preventing Agent-in-Agent-in-Agent scenarios
  • Tool inheritance: Sub-agents inherit all parent tools (except AgentTool) by default, but can be restricted via allowedTools/disallowedTools
  • Blocking execution: The parent Agent awaits after calling spawn(), waiting for the sub-agent to finish before continuing

AgentTool: The Sub-Agent Tool as Seen by the LLM

AgentTool is the tool exposed to the LLM. When the LLM calls the Agent tool, it passes a prompt and parameters. AgentTool handles calling the spawner to generate a sub-agent.

It has two built-in sub-agent types:

private let BUILTIN_AGENTS: [String: AgentDefinition] = [
    "Explore": AgentDefinition(
        name: "Explore",
        description: "Fast agent specialized for exploring codebases...",
        systemPrompt: "You are a codebase exploration agent...",
        tools: ["Read", "Glob", "Grep", "Bash"],
        maxTurns: 10
    ),
    "Plan": AgentDefinition(
        name: "Plan",
        description: "Software architect agent for designing implementation plans...",
        systemPrompt: "You are a software architect. Design implementation plans...",
        tools: ["Read", "Glob", "Grep", "Bash"],
        maxTurns: 10
    ),
]

Enter fullscreen mode Exit fullscreen mode

  • Explore: Codebase exploration, using Glob to find files, Grep to search content, Read to read files
  • Plan: Software architect, understanding the codebase then outputting implementation plans

When the LLM calls AgentTool, it specifies the type via the subagent_type field:

{
  "prompt": "Explore the project structure and find all Swift source files",
  "description": "Explore codebase",
  "subagent_type": "Explore"
}

Enter fullscreen mode Exit fullscreen mode

AgentTool also supports optional parameters: model, maxTurns, run_in_background, isolation, team_name, mode. These are passed through to the spawner.

A Complete Example

The SDK includes a SubagentExample demonstrating the full flow of a coordinator main Agent delegating to an Explore sub-agent:

// Main agent system prompt
let systemPrompt = """
You are a coordinator agent. When given a task, you should delegate it to a sub-agent \
using the Agent tool. The Agent tool will spawn a specialized agent (e.g., "Explore" type) \
that can use Read, Glob, Grep, and Bash tools to investigate the codebase. \
After the sub-agent returns its findings, summarize the results for the user.
"""

// Register tools: core tools + AgentTool
let agent = createAgent(options: AgentOptions(
    apiKey: apiKey,
    model: defaultModel,
    systemPrompt: systemPrompt,
    maxTurns: 10,
    tools: getAllBaseTools(tier: .core) + [createAgentTool()]
))

// Send task — main Agent will call AgentTool to delegate to Explore sub-agent
for await message in agent.stream("""
    Explore the current project directory. Find all Swift source files, \
    examine the project structure, and provide a summary. \
    Use the Agent tool to delegate this task to an Explore sub-agent.
""") {
    switch message {
    case .toolUse(let data):
        if data.toolName == "Agent" {
            print("[Sub-agent Delegation: \(data.toolName)]")
        }
    case .toolResult(let data):
        print("[Result: \(data.content.prefix(200))]")
    case .result(let data):
        print("Turns: \(data.numTurns), Cost: $\(data.totalCostUsd)")
    default:
        break
    }
}

Enter fullscreen mode Exit fullscreen mode

Execution flow: user sends prompt → main Agent decides it needs to explore the codebase → calls AgentTool → AgentTool spawns Explore sub-agent via spawner → sub-agent uses Glob/Grep/Read → results returned to main Agent → main Agent summarizes and responds to user.

2. Task System: Task Tracking and State Machine

Sub-agents solve the "who does the work" problem. The Task system solves "how much work is done, who's doing it, and what are the results."

TaskStore: Thread-Safe Actor

TaskStore is a Swift Actor, ensuring concurrency safety:

public actor TaskStore {
    private var tasks: [String: Task] = [:]
    private var taskCounter: Int = 0

    public func create(
        subject: String,
        description: String? = nil,
        owner: String? = nil,
        status: TaskStatus = .pending
    ) -> Task {
        taskCounter += 1
        let id = "task_\(taskCounter)"
        let now = dateFormatter.string(from: Date())
        let task = Task(
            id: id, subject: subject, description: description,
            status: status, owner: owner,
            createdAt: now, updatedAt: now
        )
        tasks[id] = task
        return task
    }
}

Enter fullscreen mode Exit fullscreen mode

Using an Actor instead of a regular class means all methods are implicitly serialized — no manual locking needed. Multiple Agents creating tasks simultaneously won't cause race conditions.

Task State Machine

Tasks have 5 states with clear transition rules:

public enum TaskStatus: String, Sendable, Equatable, Codable {
    case pending      // Waiting to start
    case inProgress   // In progress
    case completed    // Completed
    case failed       // Failed
    case cancelled    // Cancelled
}

Enter fullscreen mode Exit fullscreen mode

State transitions have constraints: pending and inProgress can transition to any state, but completed, failed, and cancelled are terminal states that cannot change:

private func isValidTransition(from: TaskStatus, to: TaskStatus) -> Bool {
    switch from {
    case .pending, .inProgress:
        return true
    case .completed, .failed, .cancelled:
        return false  // Terminal state, cannot transition
    }
}

Enter fullscreen mode Exit fullscreen mode

As a state diagram:

pending ──→ inProgress ──→ completed
   │            │
   │            ├──→ failed
   │            │
   └──→ cancelled ←──┘

Enter fullscreen mode Exit fullscreen mode

TaskStatus also has a convenient parse() method supporting both camelCase (inProgress) and snake_case (in_progress), since LLM JSON formats aren't always consistent:

public static func parse(_ string: String) -> TaskStatus? {
    if let direct = TaskStatus(rawValue: string) { return direct }
    // snake_case → camelCase
    let camel = string
        .split(separator: "_")
        .enumerated()
        .map { $0.offset == 0 ? String($0.element) : String($0.element).capitalized }
        .joined()
    return TaskStatus(rawValue: camel)
}

Enter fullscreen mode Exit fullscreen mode

Task Struct

A Task instance includes dependency relationships and metadata beyond basic status tracking:

public struct Task: Sendable, Equatable, Codable {
    public let id: String
    public var subject: String
    public var description: String?
    public var status: TaskStatus
    public var owner: String?        // Who's working on it
    public let createdAt: String
    public var updatedAt: String
    public var output: String?       // Result
    public var blockedBy: [String]?  // Blocked by which tasks
    public var blocks: [String]?     // Which tasks this blocks
    public var metadata: [String: String]?
}

Enter fullscreen mode Exit fullscreen mode

The blockedBy and blocks fields show the Task system has built-in task dependency support — Task A can declare "I need Tasks B and C to complete before I can start."

Three Task Tools

The SDK provides three tools for the LLM to operate the Task system:

TaskCreate — Create a task:

public func createTaskCreateTool() -> ToolProtocol {
    return defineTool(
        name: "TaskCreate",
        description: "Create a new task for tracking work progress.",
        inputSchema: taskCreateSchema,
        isReadOnly: false
    ) { (input: TaskCreateInput, context: ToolContext) in
        guard let taskStore = context.taskStore else {
            return ToolExecuteResult(content: "Error: TaskStore not available.", isError: true)
        }
        let initialStatus: TaskStatus = input.status.flatMap { TaskStatus.parse($0) } ?? .pending
        let task = await taskStore.create(
            subject: input.subject,
            description: input.description,
            owner: input.owner,
            status: initialStatus
        )
        return ToolExecuteResult(
            content: "Task created: \(task.id) - \"\(task.subject)\" (\(task.status.rawValue))",
            isError: false
        )
    }
}

Enter fullscreen mode Exit fullscreen mode

TaskList — List tasks (supports filtering by status and owner):

// LLM can query "list all pending tasks" or "list tasks assigned to agent-1"
let tasks = await taskStore.list(status: status, owner: input.owner)

Enter fullscreen mode Exit fullscreen mode

TaskUpdate — Update a task (status, description, owner, output):

do {
    let task = try await taskStore.update(
        id: input.id,
        status: status,
        description: input.description,
        owner: input.owner,
        output: input.output
    )
    return ToolExecuteResult(
        content: "Task updated: \(task.id) - \(task.status.rawValue) - \"\(task.subject)\"",
        isError: false
    )
} catch let error as TaskStoreError {
    return ToolExecuteResult(content: "Error: \(error.localizedDescription)", isError: true)
}

Enter fullscreen mode Exit fullscreen mode

Note that TaskUpdate throws invalidStatusTransition errors — e.g., trying to change a completed task to inProgress. The LLM receives the error message and can adjust its strategy.

3. Team System: Team Formation and Management

The Task system tracks "what to do." The Team system answers "who works with whom."

TeamStore

Like TaskStore, TeamStore is an Actor:

public actor TeamStore {
    private var teams: [String: Team] = [:]
    private var teamCounter: Int = 0

    public func create(
        name: String,
        members: [TeamMember] = [],
        leaderId: String = "self"
    ) -> Team {
        teamCounter += 1
        let id = "team_\(teamCounter)"
        let team = Team(
            id: id, name: name, members: members,
            leaderId: leaderId,
            createdAt: dateFormatter.string(from: Date()),
            status: .active
        )
        teams[id] = team
        return team
    }
}

Enter fullscreen mode Exit fullscreen mode

Teams have two states: active and disbanded. Deleting a Team doesn't actually delete it — the status changes to disbanded. Disbanded Teams cannot have members added or removed.

TeamMember and Roles

public enum TeamRole: String, Sendable, Equatable, Codable {
    case leader   // Team leader
    case member   // Regular member
}

public struct TeamMember: Sendable, Equatable, Codable {
    public let name: String
    public let role: TeamRole
}

Enter fullscreen mode Exit fullscreen mode

When TeamCreateTool creates a Team, all members default to member role, and leaderId defaults to "self" (the creator):

let members: [TeamMember] = input.members?.map { TeamMember(name: $0) } ?? []
let team = await teamStore.create(
    name: input.name,
    members: members,
    leaderId: "self"
)

Enter fullscreen mode Exit fullscreen mode

TeamStore also supports dynamic member management:

// Add member
try teamStore.addMember(teamId: "team_1", member: TeamMember(name: "agent-coder"))

// Remove member
try teamStore.removeMember(teamId: "team_1", agentName: "agent-coder")

// Find which team an Agent belongs to
let team = await teamStore.getTeamForAgent(agentName: "agent-coder")

Enter fullscreen mode Exit fullscreen mode

getTeamForAgent is important for messaging — when sending a message, you need to know which Team the sender belongs to in order to verify the recipient is a teammate.

AgentRegistry: Agent Registration

Besides TeamStore, there's an AgentRegistry tracking all active Agents:

public actor AgentRegistry {
    private var agents: [String: AgentRegistryEntry] = [:]
    private var nameIndex: [String: String] = [:]  // name -> agentId

    public func register(agentId: String, name: String, agentType: String) throws -> AgentRegistryEntry {
        if nameIndex[name] != nil {
            throw AgentRegistryError.duplicateAgentName(name: name)
        }
        let entry = AgentRegistryEntry(...)
        agents[agentId] = entry
        nameIndex[name] = agentId
        return entry
    }

    public func getByName(name: String) -> AgentRegistryEntry? {
        guard let agentId = nameIndex[name] else { return nil }
        return agents[agentId]
    }
}

Enter fullscreen mode Exit fullscreen mode

Name uniqueness constraint — no two Agents with the same name in one AgentRegistry. nameIndex is a reverse lookup index supporting O(1) name lookups.

4. Messaging: MailboxStore and SendMessage

With Teams in place, Agents need to communicate. The SDK uses a Mailbox pattern — messages aren't pushed directly to the recipient but placed in their mailbox for them to pick up.

MailboxStore

public actor MailboxStore {
    private var mailboxes: [String: [AgentMessage]] = [:]

    // Point-to-point send
    public func send(from: String, to: String, content: String, type: AgentMessageType = .text) {
        let message = AgentMessage(from: from, to: to, content: content,
                                   timestamp: dateFormatter.string(from: Date()), type: type)
        if mailboxes[to] == nil { mailboxes[to] = [] }
        mailboxes[to]?.append(message)
    }

    // Broadcast — to all Agents with mailboxes
    public func broadcast(from: String, content: String, type: AgentMessageType = .text) {
        let timestamp = dateFormatter.string(from: Date())
        for (agentName, _) in mailboxes {
            let message = AgentMessage(from: from, to: agentName, content: content,
                                       timestamp: timestamp, type: type)
            mailboxes[agentName]?.append(message)
        }
    }

    // Read and clear mailbox
    public func read(agentName: String) -> [AgentMessage] {
        guard let messages = mailboxes[agentName] else { return [] }
        mailboxes[agentName] = []  // Clear after reading
        return messages
    }
}

Enter fullscreen mode Exit fullscreen mode

Three core operations: send (point-to-point), broadcast (broadcast), read (read). read is destructive — reading clears the mailbox. broadcast only sends to Agents that already have mailboxes, not creating new ones.

Message types beyond plain text (.text) include .shutdownRequest, .shutdownResponse, .planApprovalResponse — special types for team management coordination.

SendMessage Tool

SendMessageTool performs three layers of validation:

// 1. Must have MailboxStore
guard let mailboxStore = context.mailboxStore else { ... }
// 2. Must have TeamStore
guard let teamStore = context.teamStore else { ... }
// 3. Must know who the sender is
guard let senderName = context.senderName else { ... }

// 4. Sender must be in a Team
guard let team = await teamStore.getTeamForAgent(agentName: senderName) else { ... }

// 5. Recipient must be a teammate
let isMember = team.members.contains { $0.name == input.to }
guard isMember else { ... }

Enter fullscreen mode Exit fullscreen mode

Broadcast uses "*" as recipient:

{ "to": "*", "message": "Phase 1 complete, starting Phase 2." }

Enter fullscreen mode Exit fullscreen mode

Point-to-point uses a specific name:

{ "to": "agent-coder", "message": "Here's the spec for module A." }

Enter fullscreen mode Exit fullscreen mode

Failed validations return error messages. The LLM can see which members are available and adjust the target.

5. Orchestration Patterns: Combining These Capabilities

Individual Agent, Task, Team, and Mailbox capabilities are clear. How do they combine in practice?

Pattern 1: Main Agent + Parallel Sub-Agents

The simplest pattern. The main Agent receives a complex task and launches multiple sub-agents simultaneously, each handling a portion:

let agent = createAgent(options: AgentOptions(
    apiKey: apiKey,
    model: "claude-sonnet-4-6",
    systemPrompt: """
    You are a coordinator. Break complex tasks into subtasks, \
    delegate each to an Explore sub-agent, then synthesize results.
    """,
    maxTurns: 20,
    tools: getAllBaseTools(tier: .core) + [
        createAgentTool(),
        createTaskCreateTool(),
        createTaskUpdateTool(),
        createTaskListTool()
    ],
    taskStore: TaskStore()
))

Enter fullscreen mode Exit fullscreen mode

The LLM might orchestrate like this:

  1. TaskCreate("Analyze module A") — Create task
  2. Agent(prompt: "Analyze module A", subagent_type: "Explore") — Delegate to sub-agent
  3. TaskUpdate(id: "task_1", status: "completed", output: result) — Mark complete
  4. Repeat steps 1-3 for other modules
  5. Synthesize all results

Pattern 2: Team Collaboration + Messaging

When multiple Agents need to collaborate long-term, use Team + Mailbox:

let mailboxStore = MailboxStore()
let teamStore = TeamStore()

let agent = createAgent(options: AgentOptions(
    apiKey: apiKey,
    model: "claude-sonnet-4-6",
    agentName: "coordinator",
    mailboxStore: mailboxStore,
    teamStore: teamStore,
    tools: getAllBaseTools(tier: .core) + [
        createAgentTool(),
        createTeamCreateTool(),
        createTeamDeleteTool(),
        createSendMessageTool(),
        createTaskCreateTool(),
        createTaskListTool(),
        createTaskUpdateTool()
    ]
))

Enter fullscreen mode Exit fullscreen mode

The LLM's orchestration might look like:

  1. TeamCreate(name: "refactor-team", members: ["explorer", "planner", "coder"]) — Form team
  2. TaskCreate("Explore codebase", owner: "explorer") — Create task
  3. Agent(prompt: "...", name: "explorer", subagent_type: "Explore") — Launch explore agent
  4. SendMessage(to: "planner", message: "Exploration done, here's the summary...") — Notify planner
  5. TaskCreate("Write implementation plan", owner: "planner") — Next task
  6. Continue progressing...

Pattern 3: Work Queue

Use the Task system as a work queue. The main Agent creates a batch of tasks, and sub-agents claim and execute them one by one:

Main Agent:
  TaskCreate("Fix bug #1")     → task_1 (pending)
  TaskCreate("Fix bug #2")     → task_2 (pending)
  TaskCreate("Add feature X")  → task_3 (pending)

Sub-Agent A:
  TaskList(status: "pending")       → [task_1, task_2, task_3]
  TaskUpdate(task_1, status: "in_progress", owner: "agent-a")
  ... do work ...
  TaskUpdate(task_1, status: "completed", output: "Fixed by ...")

Sub-Agent B:
  TaskList(status: "pending")       → [task_2, task_3]
  TaskUpdate(task_2, status: "in_progress", owner: "agent-b")
  ... do work ...

Enter fullscreen mode Exit fullscreen mode

TaskStore is an Actor, so multiple Agents concurrently updating the same task won't cause issues (first-come-first-served), but there's no automatic assignment — the LLM coordinates who claims which task.

Design Trade-offs

This multi-agent collaboration mechanism involves several deliberate design choices:

Why can't sub-agents spawn their own sub-agents? DefaultSubAgentSpawner filters out AgentTool when creating sub-agents. This is an intentional limit — without it, an Agent spawning an Agent spawning an Agent leads to uncontrollable recursion depth and exponential token consumption.

Why is messaging pull-based instead of push-based? MailboxStore.read() is destructive reading — Agents must actively call to receive messages. This is much simpler than push mode — no callbacks to maintain, no handling for offline Agents. The trade-off is reduced real-time responsiveness, but at the frequency of tool calls in the Agent Loop (tools can be called every turn), pull latency is acceptable.

Why doesn't the Task state machine auto-transition? The blockedBy field declares dependency relationships, but TaskStore.update() doesn't automatically check whether prerequisite tasks are complete. This means "wait for Task A before doing Task B" logic must be implemented by the LLM — calling TaskList to check status, then deciding next steps. This is a pragmatic trade-off: automatic dependency resolution could be added, but for the LLM, explicit checking is more controllable.

Summary

Open Agent SDK's multi-agent collaboration consists of three layers:

  • Sub-Agents: Via SubAgentSpawner protocol and AgentTool, the main Agent dynamically spawns sub-agents at runtime for task delegation, with built-in Explore and Plan types
  • Task System: Task tracking based on TaskStore Actor with a clear state machine (pending → inProgress → completed/failed/cancelled), where terminal states are irreversible
  • Team + Mailbox: TeamStore manages teams and members, MailboxStore implements mailbox-style messaging, supporting point-to-point and broadcast

All three layers can be used independently or combined — use Tasks to track progress, Teams to organize members, Mailbox for coordination, and sub-agents to execute the actual work.

The next article covers the SDK's session persistence: how Agent conversation history is stored, restored, and how to continue previous work after a restart.


Deep Dive into Open Agent SDK (Swift) Series:

GitHub: terryso/open-agent-sdk-swift