Skip to content

[FEATURE]: Plugin-based agent orchestration layer — parallel execution without upstream changes #21

@terisuke

Description

@terisuke

Summary

A plugin-based approach to parallel/background agent orchestration that works as a thin distribution layer over upstream OpenCode, requiring zero core code changes while enabling Claude Code-class multi-agent workflows.

This proposal differs from existing parallel agent discussions (anomalyco#19999, anomalyco#12711, anomalyco#19215, anomalyco#5887, anomalyco#17994) by focusing exclusively on what's achievable today through the plugin tool registration API and existing SDK session primitives — no upstream PRs needed.

Motivation

OpenCode's current task tool creates subagent sessions but blocks the parent until completion. Users migrating from Claude Code (which offers run_in_background, TeamCreate, SendMessage) lose the ability to:

  1. Fire-and-forget investigation — spawn 3 research agents while continuing implementation
  2. Parallel implementation — split independent features across isolated sessions
  3. Agent-to-agent coordination — team leads dispatching work to specialists

These capabilities are critical for complex, multi-file development workflows.

Existing SDK Primitives That Enable This

After tracing the codebase, the following APIs already exist and are sufficient:

Primitive Location Capability
session.promptAsync() routes/session.ts Fire-and-forget (returns 204 immediately)
GET /event (SSE) routes/event.ts Real-time session event subscription
session.create({ parentID }) session/index.ts Parent-child session relationships
session.children() SDK List child sessions of a parent
session.abort() SDK Cancel a running session
Plugin tool hook plugin/src/index.ts Register custom tools available to agents
Plugin tool.execute.after Plugin API Intercept tool completion events
session.prompt({ agent }) session/prompt.ts Route to specific agent per session
Permission per session session/index.ts Granular tool restrictions per child

The key insight: promptAsync + SSE already implement the core of background execution. What's missing is the orchestration glue.

Proposed Design: Plugin with 3 New Tools

Tool 1: team (fan-out with DAG dependencies)

// Plugin-registered tool
tool({
  description: "Launch parallel agent sessions with optional dependencies",
  args: {
    tasks: z.array(z.object({
      id: z.string(),
      agent: z.string().optional(),    // "general", "explore", custom
      prompt: z.string(),
      depends: z.array(z.string()).optional(),  // DAG edges
      worktree: z.boolean().optional(),  // git worktree isolation
    })),
    strategy: z.enum(["parallel", "wave"]).default("parallel"),
  },
  async execute(args, ctx) {
    // 1. Topological sort on depends graph
    // 2. Create child sessions via SDK (parentID = ctx.sessionID)
    // 3. Fire promptAsync for ready tasks
    // 4. Subscribe to SSE for completion events
    // 5. On completion, unblock dependent tasks
    // 6. Return aggregated results
  }
})

Tool 2: background (single fire-and-forget agent)

tool({
  description: "Spawn a background agent session. Returns immediately with task ID.",
  args: {
    prompt: z.string(),
    agent: z.string().optional(),
    notify: z.boolean().default(true),  // inject result when done
  },
  async execute(args, ctx) {
    // 1. Create child session via SDK
    // 2. Call promptAsync (returns 204)
    // 3. Register SSE listener for completion
    // 4. Return task_id immediately
    // 5. On completion: if notify=true, inject result into parent session
  }
})

Tool 3: team_status (monitor running agents)

tool({
  description: "Check status of background/team agents",
  args: {
    task_id: z.string().optional(),  // specific task or all
  },
  async execute(args, ctx) {
    // 1. List child sessions via SDK
    // 2. Check session status for each
    // 3. Return status summary
  }
})

Why Plugin-Based (Not Core Changes)

Concern Plugin approach Core PR approach
Upstream sync git pull works, no conflicts Merge conflicts on every update
Risk Isolated; disable plugin to revert Core regression risk
Iteration speed Deploy instantly, no review cycle Weeks of PR review
Customizability Per-org policies (permissions, limits) One-size-fits-all
Testing Plugin-scoped tests Full regression suite

The guardrails package (packages/guardrails/) already demonstrates this pattern — it adds organization-specific agents, commands, and a runtime plugin without touching upstream code.

Implementation Phases

Phase 1: Background Agent (simplest, highest value)

  • Single background tool via plugin
  • Uses promptAsync + SSE internally
  • Result injection via session.prompt() on parent
  • Estimated: ~200 LOC

Phase 2: Team Fan-Out

  • team tool with DAG scheduling
  • Promise.all for independent tasks, sequential for dependencies
  • Aggregated result formatting
  • Estimated: ~400 LOC

Phase 3: Worktree Isolation

  • Git worktree creation per team member
  • Merge-back with conflict detection
  • Builds on Phase 2
  • Estimated: ~300 LOC additional

Phase 4: Inter-Agent Messaging (optional)

  • In-memory mailbox per team session
  • team_send / team_recv tools
  • Useful for reviewer ↔ implementer patterns
  • Estimated: ~200 LOC additional

Relationship to Existing Issues

Issue Overlap This proposal's stance
anomalyco#19999 (Ephemeral Teams) High — same fan-out concept Agrees on architecture; adds plugin-only constraint
anomalyco#12711 (Flat Teams) Medium — similar coordination Simpler scope; no TUI changes needed
anomalyco#19215 (DB-backed) Low — different persistence model Avoids new DB tables; uses existing sessions
anomalyco#5887 (Async Sub-Agent) High — same background concept Provides concrete promptAsync path
anomalyco#20460 (Silent Collection) Complementary Phase 1 notify flag addresses this
anomalyco#17994 (Isolated Workspaces) Medium — worktree overlap Phase 3 addresses; plugin-scoped
anomalyco#17374 (A2A) Future — protocol alignment Phase 4 messaging is A2A-compatible primitive

Open Questions

  1. Result injection: When a background agent completes, should the result be injected as a system message into the parent session, or queued for explicit retrieval?
  2. Abort cascade: Should aborting the parent session abort all children? (Proposed: yes, via session.abort() on each child)
  3. Token budget: Should each child session have an independent token budget, or share the parent's? (Proposed: independent, with configurable cap)
  4. Concurrency limit: What's the safe default for parallel sessions? (Proposed: 5, matching Claude Code's teammate limit)

Environment

  • OpenCode version: dev branch (latest)
  • Plugin API: @opencode-ai/plugin tool registration
  • SDK: @opencode-ai/sdk session management
  • Reference implementation: packages/guardrails/ thin distribution pattern

Metadata

Metadata

Assignees

No one assigned

    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