Skip to content

[DESIGN]: Agent Teams — flat teams with named messaging, multi-model support, and TUI integration #12711

@ugoenyioha

Description

@ugoenyioha

Problem

OpenCode's task tool spawns subagents that run sequentially, return a result, and terminate. There's no way for multiple agents to work in parallel, communicate with each other, or coordinate on shared tasks. Users working on complex problems (multi-file refactors, research + implementation, code review across concerns) are limited to one agent at a time.

Claude Code shipped agent teams (Feb 5, 2026). Several community requests exist: #12661, #9649, #6478. PR #7756 proposes subagent-to-subagent delegation with a tree hierarchy. This proposal takes a different approach: flat teams with named messaging, built as an adjunct to the existing session/task system rather than a redesign of it.

Design Goals

  1. Non-invasive — builds on existing sessions, agents, and tools. No changes to the core session or task architecture.
  2. Flat, not hierarchical — one lead, N teammates. No nested teams, no tree traversal.
  3. Name-addressed messaging — teammates communicate by name, not by session ID or parent-child relationship. The lead doesn't route messages.
  4. Multi-model — each teammate can use a different provider/model (e.g., Gemini for research, Claude for implementation).
  5. Opt-in — behind OPENCODE_EXPERIMENTAL_AGENT_TEAMS flag. Default behavior unchanged.

Architecture

Data Model

A team is a named group of sessions:

Team {
  name: string              // e.g. "auth-review"
  leadSessionID: string     // the session that created the team
  delegate?: boolean        // lead restricted to coordination-only tools
  members: [{
    name: string            // unique within team, e.g. "security-reviewer"
    sessionID: string       // child session
    agent: string           // agent type (e.g. "general", "explore")
    status: "active" | "idle" | "shutdown" | "interrupted"
    model?: string          // "providerID/modelID"
    planApproval?: "none" | "pending" | "approved" | "rejected"
  }]
}

Team state is persisted to disk as JSON files under .opencode/teams/{teamName}/ (config.json and tasks.json). All reads go directly to disk — no in-memory cache. Task state (shared task list) uses file-based storage with file locking for concurrency. No database schema changes.

Tools

Nine tools, split by role:

Lead-only:

Tool Purpose
team_create Create a team, become the lead
team_spawn Spawn a teammate (creates child session, starts prompt loop)
team_approve_plan Approve/reject a teammate's plan (unlocks write tools)
team_shutdown Request a teammate to shut down
team_cleanup Remove team resources

Available to all team members (lead + teammates):

Tool Purpose
team_message Send a message to a specific teammate or "lead" by name
team_broadcast Send a message to all teammates
team_tasks View/add/complete tasks on a shared task list
team_claim Claim a pending task (file-locked to prevent races)

Tool isolation: Task subagents spawned by teammates get NO team tools. Subagents are private utilities — the teammate relays relevant findings. This prevents uncoordinated noise in team communication.

Communication

Messages are injected as synthetic user messages into the recipient's session, which triggers auto-wake if the recipient is idle. No file-based mailboxes, no polling.

TeamMessaging.send({ teamName, from: "security-reviewer", to: "lead", text: "Found 3 SQL injection vulnerabilities" })
// → injects a user message into the lead's session
// → if lead is idle, auto-wake kicks off the prompt loop

Messages between teammates are persisted through OpenCode's existing session storage (messages and parts written as JSON files to ~/.local/share/opencode/storage/).

Teammate Lifecycle

  1. Lead calls team_spawn with agent type, name, model, and initial prompt
  2. A child session is created with permission rules denying lead-only tools
  3. The teammate's prompt loop starts in the background (non-blocking)
  4. When the teammate's loop completes or errors, an idle notification is sent to the lead via team_message
  5. Lead can send follow-up work via team_message, which auto-wakes the teammate

Recovery

On server restart, Team.recover() scans all teams for members with status: "active" (stale from a crash), marks them as interrupted, and injects a synthetic notification into the lead session. The lead's LLM sees the notification on next prompt and can re-spawn or message them. Team structure, member state, and conversation history all survive a restart — the remaining gap is that teammate prompt loops are not auto-restarted.

Optional Features

Delegate mode: Lead is restricted to coordination-only tools (no write/edit/bash). Forces clean separation between orchestration and execution.

Plan approval: Teammates start in read-only mode. They research, formulate a plan, send it to the lead. Lead approves (unlocks write tools) or rejects (teammate revises). Prevents wasted work on wrong approaches.

Shared task list: Teams can maintain a task list with priorities, dependencies, and assignees. Tasks auto-unblock when dependencies complete. Any member can view, add, claim, or complete tasks.

TUI Integration

  • Header: Team badge showing team name and member count/status summary
  • Sidebar: Team section with per-member status, per-member todo progress, current tool activity (live spinner), and shared task list
  • Prompt: Aggregated busy indicator ("3 teammates working") when lead is idle but teammates are active
  • Session list: Spinner on busy teammate sessions
  • Team dialog: Keyboard-driven team creation interface

What This Does NOT Change

  • The existing task tool and subagent system are untouched
  • Session creation, storage, and lifecycle are unchanged
  • The prompt loop, processor, and message system are unchanged
  • No database schema changes
  • No new dependencies

Example Usage

User: Review this codebase for security issues across frontend, backend, and infrastructure

Lead (Claude Opus):
  → team_create("security-audit")
  → team_spawn("frontend", agent="general", model="anthropic/claude-sonnet-4-20250514", prompt="Review React components for XSS...")
  → team_spawn("backend", agent="general", model="google/gemini-2.5-pro", prompt="Review API endpoints for injection...")  
  → team_spawn("infra", agent="explore", prompt="Check Docker, CI, and dependency configs...")
  
  [3 teammates work in parallel, each searching and analyzing independently]
  
  frontend → team_message(to="lead", "Found 3 XSS vulnerabilities in...")
  backend → team_message(to="frontend", "The sanitization you flagged is also missing in the API layer")
  infra → team_tasks(action="complete", task_id="deps")
  
  Lead synthesizes findings into a final report

Screenshots

Super Bowl prediction — 3 teammates researching in parallel with live sidebar activity:

Super Bowl prediction — 3 teammates researching in parallel with sidebar showing live task activity and busy indicator

Precious metals forecast — 3 teammates with task dependencies and claim operations:

Precious metals forecast — 3 teammates with task dependencies and claim operations

Implementation Status

Working implementation submitted as a 3-PR stack:

By the numbers:

  • ~1,500 lines of core implementation (team state, messaging, events, 9 tools)
  • ~1,100 lines of TUI integration (sidebar panels, header badges, sync store, team dialog, prompt bar)
  • ~8,500 lines of tests across 14 test files (unit, integration, e2e, edge cases, persistence, recovery, plan approval, spawn permissions, auto-wake, delegate cleanup, scaling scenarios)
  • Multi-model tested (Claude + Gemini + OpenAI teammates in one team)

Open Questions

  1. Should there be a max team size limit?
  2. Should teammate prompt loops auto-restart on server recovery, or is lead-driven re-spawn sufficient?

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions