[PR #12730] feat: add agent team core — Team namespace, messaging, recovery, and events #14343

Open
opened 2026-02-16 18:19:08 -05:00 by yindo · 0 comments
Owner

Original Pull Request: https://github.com/anomalyco/opencode/pull/12730

State: open
Merged: No


Summary

Adds the foundational layer for agent teams (Fixes #12711), gated behind OPENCODE_EXPERIMENTAL_AGENT_TEAMS=1.

This is PR 1/3 in the agent teams stack. It introduces the core Team namespace with full CRUD, persistence, recovery, and inter-agent messaging — everything downstream PRs (#12731 tools, #12732 TUI) build on top of.

What's included

Team namespace (team/index.ts — 392 lines)

  • CRUD: create, get, list, addMember, removeMember, cleanup
  • Storage-backed persistence: Teams stored via the Storage namespace with project-scoped keys (["team", projectId, name] and ["team_tasks", projectId, name]). No raw Bun.file/Bun.write or filesystem paths — Storage handles atomicity and locking internally.
  • Runtime validation: create and get wrapped with fn() for Zod input validation
  • Atomic mutations: All state changes (addMember, setMemberStatus, setDelegate, setMemberPlanApproval, removeMember) use Storage.update() — read-modify-write under lock
  • addMember validations: Case-insensitive name duplicate check, sessionID duplicate check, reserved name "lead" rejection — all inside Storage.update callback for atomicity
  • setMemberStatus guard: Optional { guard: true } parameter prevents overwriting "shutdown" status (TOCTOU prevention for teammate completion callbacks)
  • Single-team-per-session constraint: A session cannot lead multiple teams, and teammates cannot create nested teams
  • Member status tracking: active, idle, shutdown, interrupted states with atomic updates
  • Delegate mode: setDelegate() toggles lead's write permissions — when enabled, the lead is restricted to coordination-only tools
  • Plan approval state: setMemberPlanApproval() manages nonependingapproved/rejected flow per member
  • Event-based cleanup: cleanup() publishes TeamEvent.Cleaned with leadSessionID and delegate fields so listeners can handle side-effects (e.g. restoring lead session permissions) — no direct Session import in the cleanup path

Task system (TeamTasks in team/index.ts)

  • Shared task list: list, add, update, complete, claim — all use Storage.update for concurrency safety
  • Atomic task claiming: claim() checks status, assignee, and dependency resolution inside a Storage.update callback
  • Dependency resolution: Same depends_on logic as the per-session Todo system — auto-blocks tasks with unresolved deps, auto-unblocks when deps complete
  • Event publishing: TaskUpdated and TaskClaimed events propagate to TUI in real-time

Messaging (team/messaging.ts — 134 lines)

  • Directed messaging: send() routes messages between teammates and the lead by injecting synthetic user messages into the recipient's session
  • Broadcast: broadcast() sends to all active members (skips shutdown and interrupted), includes the lead if sender isn't the lead
  • Auto-wake: When a message is injected into an idle session, autoWake() starts a new prompt loop so the LLM picks up and processes the message immediately
  • Session injection: Messages arrive as [Team message from <name>]: <text> user messages with synthetic: true

Events (team/events.ts — 130 lines)

  • Zod-validated bus events: team.created, team.member.spawned, team.member.status, team.message, team.broadcast, team.task.updated, team.task.claimed, team.shutdown.request, team.plan.approval, team.cleaned
  • Full type safety: TeamInfoSchema, TeamMemberSchema, TeamTaskSchema with Zod, re-exported for downstream use
  • Expanded Cleaned event: TeamEvent.Cleaned includes leadSessionID and delegate fields for event-based permission restoration
  • Member schema: name, sessionID, agent, status, prompt, model (providerID/modelID format), planApproval

Bootstrap hook (project/bootstrap.ts)

  • Fire-and-forget Team.recover() call during instance startup

Recovery (Team.recover())

  • Scans all teams for members with status: "active" (stale from a crash)
  • Marks them as interrupted
  • Injects a synthetic notification into the lead session: "The following teammates were interrupted and need to be resumed"
  • Lead's LLM sees the notification on next prompt and can re-spawn or message them

Feature flag (flag/flag.ts)

  • OPENCODE_EXPERIMENTAL_AGENT_TEAMS — dynamic getter from environment variable, gates all team functionality

Tests

4 test files, 26 tests:

  • team-autowake.test.ts — Auto-wake behavior when messages arrive for idle sessions
  • team-persistence.test.ts — Storage persistence, config read/write, task serialization (verified via API, not raw file paths)
  • team-recovery.test.ts — Recovery logic for interrupted teammates after server restart (with Storage cleanup)
  • team-recovery-e2e.test.ts — End-to-end recovery scenarios with session injection

Files changed

File Change Lines
src/team/index.ts New +392
src/team/messaging.ts New +134
src/team/events.ts New +130
src/flag/flag.ts Modified +12
src/project/bootstrap.ts Modified +11
test/team/team-autowake.test.ts New +639
test/team/team-persistence.test.ts New +198
test/team/team-recovery.test.ts New +324
test/team/team-recovery-e2e.test.ts New +321
Total +2,161

Stack

PR 1/3 — merge in order:

  1. This PR — Core team primitives
  2. #12731 — Tools + routes
  3. #12732 — TUI integration
**Original Pull Request:** https://github.com/anomalyco/opencode/pull/12730 **State:** open **Merged:** No --- ## Summary Adds the foundational layer for **agent teams** (Fixes #12711), gated behind `OPENCODE_EXPERIMENTAL_AGENT_TEAMS=1`. This is PR 1/3 in the agent teams stack. It introduces the core Team namespace with full CRUD, persistence, recovery, and inter-agent messaging — everything downstream PRs (#12731 tools, #12732 TUI) build on top of. ## What's included ### Team namespace (`team/index.ts` — 392 lines) - **CRUD**: `create`, `get`, `list`, `addMember`, `removeMember`, `cleanup` - **Storage-backed persistence**: Teams stored via the `Storage` namespace with project-scoped keys (`["team", projectId, name]` and `["team_tasks", projectId, name]`). No raw `Bun.file`/`Bun.write` or filesystem paths — Storage handles atomicity and locking internally. - **Runtime validation**: `create` and `get` wrapped with `fn()` for Zod input validation - **Atomic mutations**: All state changes (`addMember`, `setMemberStatus`, `setDelegate`, `setMemberPlanApproval`, `removeMember`) use `Storage.update()` — read-modify-write under lock - **addMember validations**: Case-insensitive name duplicate check, sessionID duplicate check, reserved name `"lead"` rejection — all inside `Storage.update` callback for atomicity - **setMemberStatus guard**: Optional `{ guard: true }` parameter prevents overwriting `"shutdown"` status (TOCTOU prevention for teammate completion callbacks) - **Single-team-per-session constraint**: A session cannot lead multiple teams, and teammates cannot create nested teams - **Member status tracking**: `active`, `idle`, `shutdown`, `interrupted` states with atomic updates - **Delegate mode**: `setDelegate()` toggles lead's write permissions — when enabled, the lead is restricted to coordination-only tools - **Plan approval state**: `setMemberPlanApproval()` manages `none` → `pending` → `approved`/`rejected` flow per member - **Event-based cleanup**: `cleanup()` publishes `TeamEvent.Cleaned` with `leadSessionID` and `delegate` fields so listeners can handle side-effects (e.g. restoring lead session permissions) — no direct Session import in the cleanup path ### Task system (`TeamTasks` in `team/index.ts`) - **Shared task list**: `list`, `add`, `update`, `complete`, `claim` — all use `Storage.update` for concurrency safety - **Atomic task claiming**: `claim()` checks status, assignee, and dependency resolution inside a `Storage.update` callback - **Dependency resolution**: Same `depends_on` logic as the per-session Todo system — auto-blocks tasks with unresolved deps, auto-unblocks when deps complete - **Event publishing**: `TaskUpdated` and `TaskClaimed` events propagate to TUI in real-time ### Messaging (`team/messaging.ts` — 134 lines) - **Directed messaging**: `send()` routes messages between teammates and the lead by injecting synthetic user messages into the recipient's session - **Broadcast**: `broadcast()` sends to all active members (skips `shutdown` and `interrupted`), includes the lead if sender isn't the lead - **Auto-wake**: When a message is injected into an idle session, `autoWake()` starts a new prompt loop so the LLM picks up and processes the message immediately - **Session injection**: Messages arrive as `[Team message from <name>]: <text>` user messages with `synthetic: true` ### Events (`team/events.ts` — 130 lines) - **Zod-validated bus events**: `team.created`, `team.member.spawned`, `team.member.status`, `team.message`, `team.broadcast`, `team.task.updated`, `team.task.claimed`, `team.shutdown.request`, `team.plan.approval`, `team.cleaned` - **Full type safety**: `TeamInfoSchema`, `TeamMemberSchema`, `TeamTaskSchema` with Zod, re-exported for downstream use - **Expanded Cleaned event**: `TeamEvent.Cleaned` includes `leadSessionID` and `delegate` fields for event-based permission restoration - **Member schema**: name, sessionID, agent, status, prompt, model (`providerID/modelID` format), planApproval ### Bootstrap hook (`project/bootstrap.ts`) - Fire-and-forget `Team.recover()` call during instance startup ### Recovery (`Team.recover()`) - Scans all teams for members with `status: "active"` (stale from a crash) - Marks them as `interrupted` - Injects a synthetic notification into the lead session: _"The following teammates were interrupted and need to be resumed"_ - Lead's LLM sees the notification on next prompt and can re-spawn or message them ### Feature flag (`flag/flag.ts`) - `OPENCODE_EXPERIMENTAL_AGENT_TEAMS` — dynamic getter from environment variable, gates all team functionality ## Tests 4 test files, 26 tests: - `team-autowake.test.ts` — Auto-wake behavior when messages arrive for idle sessions - `team-persistence.test.ts` — Storage persistence, config read/write, task serialization (verified via API, not raw file paths) - `team-recovery.test.ts` — Recovery logic for interrupted teammates after server restart (with Storage cleanup) - `team-recovery-e2e.test.ts` — End-to-end recovery scenarios with session injection ## Files changed | File | Change | Lines | |------|--------|-------| | `src/team/index.ts` | New | +392 | | `src/team/messaging.ts` | New | +134 | | `src/team/events.ts` | New | +130 | | `src/flag/flag.ts` | Modified | +12 | | `src/project/bootstrap.ts` | Modified | +11 | | `test/team/team-autowake.test.ts` | New | +639 | | `test/team/team-persistence.test.ts` | New | +198 | | `test/team/team-recovery.test.ts` | New | +324 | | `test/team/team-recovery-e2e.test.ts` | New | +321 | | **Total** | | **+2,161** | ## Stack PR 1/3 — merge in order: 1. **This PR** — Core team primitives 2. #12731 — Tools + routes 3. #12732 — TUI integration
yindo added the pull-request label 2026-02-16 18:19:08 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#14343