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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions