Repository navigation
[FEATURE]: Plugin-based agent orchestration layer — parallel execution without upstream changes #20849
Description
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 (#19999, #12711, #19215, #5887, #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:
- Fire-and-forget investigation — spawn 3 research agents while continuing implementation
- Parallel implementation — split independent features across isolated sessions
- 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
backgroundtool via plugin - Uses
promptAsync+ SSE internally - Result injection via
session.prompt()on parent - Estimated: ~200 LOC
Phase 2: Team Fan-Out
teamtool with DAG schedulingPromise.allfor 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_recvtools- Useful for reviewer ↔ implementer patterns
- Estimated: ~200 LOC additional
Relationship to Existing Issues
| Issue | Overlap | This proposal's stance |
|---|---|---|
| #19999 (Ephemeral Teams) | High — same fan-out concept | Agrees on architecture; adds plugin-only constraint |
| #12711 (Flat Teams) | Medium — similar coordination | Simpler scope; no TUI changes needed |
| #19215 (DB-backed) | Low — different persistence model | Avoids new DB tables; uses existing sessions |
| #5887 (Async Sub-Agent) | High — same background concept | Provides concrete promptAsync path |
| #20460 (Silent Collection) | Complementary | Phase 1 notify flag addresses this |
| #17994 (Isolated Workspaces) | Medium — worktree overlap | Phase 3 addresses; plugin-scoped |
| #17374 (A2A) | Future — protocol alignment | Phase 4 messaging is A2A-compatible primitive |
Open Questions
- 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?
- Abort cascade: Should aborting the parent session abort all children? (Proposed: yes, via
session.abort()on each child) - Token budget: Should each child session have an independent token budget, or share the parent's? (Proposed: independent, with configurable cap)
- 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/plugintool registration - SDK:
@opencode-ai/sdksession management - Reference implementation:
packages/guardrails/thin distribution pattern
Activity
github-actions commentedon Apr 3, 2026
This issue might be a duplicate of existing issues. Please check:
Since you've already catalogued the overlap yourself in the issue, it would be worth coordinating with the authors of those existing issues (especially #19999 and #5887 which you rate as high overlap) rather than opening a parallel thread. The plugin-only constraint you describe could potentially be contributed as a comment or follow-up to those discussions instead.
terisuke commentedon Apr 3, 2026
Note: This issue was mistakenly opened on upstream. The correct location is our fork: Cor-Incorporated#21
This is a feature proposal specific to our thin distribution layer (guardrails package) and does not require upstream changes. Apologies for the noise — please feel free to close this issue.