Async agents can be powerful, but orchestration is at best finicky; they fire and forget, orphan work, lose context, waste time... and tokens. This plugin extends the native opencode subagent paradigm to provide closed loop, resilient, async agents, blocking main thread execution. A "pocket universe".
This ships with three tools creating a robust system for parallel agents to communicate and coordinate work
broadcastis the live messaging system, subagents can send or reply to others and update own statussubagentspawns an async agent.subagentoutput is sent back to the caller, which resumes if idlerecallallows access to current and past subagents' status history and results, disabled by default
Every broadcast messages or subagent results are piped to the proper recipient as they happen
- attention is all they need, agents are made acutely aware of each other
- message any subagents directly with
/pocket @agentX message - resume idle agents on
subagentresult,broadcastor/pocketmessage - block main session until all subagents in the pocket universe complete
- tools are configurable and work standalone or together
- depth control to limit runaway subagent spawning
- worktree support for isolated agent workspaces (disabled by default)
Perfectly suited for personnal use, you don't need to build crazy machinery on top of it to make it work. That being said, harness mods such as Oh My Opencode would benefit from using this under the hood.
Needs this opencode PR merged to function correctly (namely async subagents, session resumption, main thread block) and this one for properly scoping tools to subagents only.
Add Pocket Universe to your opencode config's plugin array
"plugin": ["@spoons-and-mirrors/pocket-universe@latest"]Features Documentation
Tools
broadcast(message="...") # Status update (visible to all, not queued)
broadcast(send_to="agentB", message="...") # Direct message (queued, replyable)
broadcast(reply_to=1, message="...") # Reply to message #1
| Parameter | Required | Description |
|---|---|---|
message |
Yes | Your message content |
send_to |
No | Target agent alias |
reply_to |
No | Message ID to reply to (auto-wires recipient) |
Status vs Messages:
- Without
send_to: Updates your status history (other agents see it in the agent list). Passive visibility only — does NOT send messages or wake agents. - With
send_to: Sends a direct message that appears in the recipient's inbox and is replyable.
Status history: Each agent's status updates are tracked as a history. When you see an agent, you see all their status updates in order, showing what they've been doing.
subagent(prompt="Build the login form", description="Login UI")
| Parameter | Required | Description |
|---|---|---|
prompt |
Yes | Task for the new agent |
description |
No | Short description (3-5 words) |
Key behavior:
- Async firing:
subagent()returns immediately, caller continues working - Output piping: When subagent completes, its output arrives as a message in the caller session (and wakes it if idle)
- Main thread block: The main session waits for ALL subagents to complete before continuing
- Model inheritance: Subagents automatically inherit the caller's agent and model (no fallback to defaults)
recall() # Get all agents' status histories
recall(agent_name="agentA") # Get specific agent's history
recall(agent_name="agentA", show_output=true) # Include agent's final output
| Parameter | Required | Description |
|---|---|---|
agent_name |
No | Specific agent to recall (omit for all agents) |
show_output |
No | Include final output (only works with agent_name specified) |
Key behavior:
- Persists across cleanups: History survives pocket universe cleanup, so agents in batch 2 can recall what batch 1 agents did
- Status history only by default: Output is only shown when BOTH
agent_nameANDshow_output=trueare specified - Active agent handling: If you request output for an agent that's still active, you'll get
[Agent is still active - no output yet]
Example response:
{
"agents": [
{
"name": "agentA",
"status_history": ["Working on login form", "Integrating API"],
"state": "completed",
"output": "Login form implemented with OAuth support..."
}
]
}Use cases:
- New agent needs to know what previous agents accomplished
- Agent wants to check if another agent finished and see their results
- Coordinating work based on what others have done
Commands
While a pocket universe is running, you can send messages directly to any of its agent.
You don't need to create the /pocket command file, it is registered automatically
/pocket @agentB wrap it up → sends to agentB specifically
/pocket wrap it up → sends to first subagent of the pocket
Behavior
- Message sender appears as
"user"so agents know it came from you - If the target agent is idle, it will be resumed automatically
- Cannot message agents from previous pockets (already completed)
- Returns an error if no pocket is currently active
- Returns an error if the specified agent doesn't exist
Examples
- Give agents mid-task instructions:
/pocket @agentB also add unit tests please - Redirect work:
/pocket @agentA hand off the API work to agentB when it's done, thanks!
Configuration
Pocket Universe uses feature flags to control optional functionality. Configuration is loaded from two locations (in priority order):
- Project-specific:
.pocket-universe.jsoncin your current directory - Global:
~/.config/opencode/pocket-universe.jsonc(auto-created if missing)
tools
| Flag | Default | Description |
|---|---|---|
broadcast |
true |
Enable the broadcast tool for inter-agent messaging |
tools.subagent
| Parameter | Default | Description |
|---|---|---|
enabled |
true |
Enable the subagent tool for creating sibling agents |
max_depth |
3 |
Max session depth allowed to spawn subagents (main session = 0, exclusive) |
forced_attention |
true |
When true, subagent output appears in broadcast inbox; when false, injected as persisted user message |
tools.recall
| Parameter | Default | Description |
|---|---|---|
enabled |
false |
Enable the recall tool for querying agent history |
cross_pocket |
true |
When true, recall can access agents from prior pockets |
Top-level
| Flag | Default | Description |
|---|---|---|
worktree |
false |
Create isolated git worktrees for each agent |
logging |
false |
Write debug logs to .logs/pocket-universe.log |
Worktrees
Each agent operates in its own git worktree — a clean checkout from the last commit (HEAD). This provides isolation so agents can work in parallel without conflicting with each other.
repo/
├── .worktrees/
│ ├── agentA/ ← agentA's isolated working directory
│ ├── agentB/ ← agentB's isolated working directory
│ └── agentC/ ← agentC's isolated working directory
└── (main repo) ← main session's working directory
When an agent is created (via task or subagent):
- A new worktree is created at
.worktrees/<alias>(detached from HEAD) - The agent sees its worktree path in its system prompt
- All sibling agents can see each other's worktree paths via
broadcast
Each agent receives its worktree path:
<worktree>
Your isolated working directory: /repo/.worktrees/agentB
ALL file operations (read, write, edit, bash) should use paths within this directory.
Do NOT modify files outside this worktree.
</worktree>When agents see each other via broadcast, worktree paths are included so they know where each agent is working.
The main session’s Pocket Universe Summary includes each agent’s worktree path when worktrees are enabled.
| Event | Behavior |
|---|---|
| Agent created | Worktree created at .worktrees/<alias> from HEAD |
| Agent completes | Worktree preserved with all changes |
| Stale worktree exists | Automatically cleaned up before creating new one |
Important: Worktrees are not deleted when agents complete. The agent's changes are preserved for you to review and merge manually.
Worktree isolation relies on agents following instructions to use their assigned paths. The LLM may occasionally write to the wrong location. For guaranteed isolation, OpenCode core changes would be needed (per-session working directory).
Technical Documentation
Diagrams
How it works
sequenceDiagram
participant Main as Main Session
participant A as AgentA
participant B as AgentB (subagent)
Main->>A: task tool
Note over A: AgentA starts work
A->>B: subagent(prompt="...")
Note over A: Returns immediately (fire-and-forget)
Note over B: AgentB works in parallel
A->>A: Continues own work
A->>A: Finishes, about to complete
Note over A: Waits for agentB...
B-->>A: Completes, output piped as message
Note over A: Unread message detected
A->>A: Session resumed automatically
Note over A: Processes agentB's output
A-->>Main: Finally completes
Note over Main: Continues with full result
Session Lifecycle
flowchart TD
A[Agent finishes work] --> B{Has pending subagents?}
B -->|Yes| C[Wait for subagents to complete]
C --> D[Subagents pipe output as messages]
D --> E{Has unread messages?}
B -->|No| E
E -->|Yes| F[Resume session]
F --> G[Agent processes messages]
G --> A
E -->|No| H[Session completes]
H --> I[Main continues]
The session.before_complete hook ensures no work is left behind:
- Agent finishes its work
- Hook checks for pending subagents → waits for them
- Subagents pipe output to caller as messages
- Hook checks for unread messages → resumes session
- Agent processes messages, hook fires again
- Only when nothing pending does the session complete
- Main session continues with the complete result
Session Resumption
Idle agents automatically wake up when they receive messages:
sequenceDiagram
participant A as AgentA
participant B as AgentB
A->>A: Completes task, goes idle
B->>A: broadcast(send_to="agentA", message="Question?")
Note over A: Message arrives while idle
A->>A: Resumed automatically
Note over A: Sees message in inbox
A->>B: broadcast(reply_to=1, message="Answer!")
`broadcast` System
Messages appear as synthetic broadcast tool results:
{
"tool": "broadcast",
"state": {
"input": { "synthetic": true },
"output": {
"you_are": "agentB",
"agents": [
{
"name": "agentA",
"status": ["searching for X", "found X in file.ts"],
"idle": true
}
],
"messages": [{ "id": 1, "from": "agentA", "content": "Need help?" }]
}
}
}synthetic: true— Injected by Pocket Universe, not a real tool callyou_are— Your agent name (always included so you know your identity)agents— All sibling agents and their status history (array of status updates)idle— True if the agent has completed and is no longer activemessages— Inbox messages, reply usingreply_to
Reply audit trail: When you reply using reply_to, the tool output includes the FULL original message you're replying to, providing a complete audit trail.
Pocket Universe Summary
When all parallel agent work completes, the main session receives a Pocket Universe Summary as a persisted user message. This includes:
- All agents that ran
- Their full status history
- Their worktree paths (if enabled)
Example summary:
[Pocket Universe Summary]
The following agents completed their work:
## agentA
Worktree: /repo/.worktrees/agentA
Status history:
→ searching for auth implementation
→ found auth in src/auth.ts
→ sending findings to agentB
## agentB
Worktree: /repo/.worktrees/agentB
Status history:
→ implementing login form
→ integrating auth from agentA
→ completed login feature
Note: Agent changes are preserved in their worktrees. Review and merge as needed.
This summary is persisted to the database so it survives crashes and is part of the conversation history.
Automatic cleanup: After the summary is injected, all agent tracking state is cleared. This means the next batch of tasks starts fresh — no stale agents from previous work appear in getParallelAgents().
Parallel task handling: If the main session spawns multiple task tools in parallel (e.g., task(agentA) and task(agentB) simultaneously), the summary is only injected after ALL first-level children complete. This ensures the summary contains work from all agents, not just the first one to finish.
{ "worktree": false, "logging": false, "session_update": { "broadcast": { "status_update": false, "message_sent": false, }, "subagent": { "creation": false, "completion": false, "resumption": false, }, "user": { "message_sent": false, }, }, "tools": { "broadcast": true, "subagent": { "enabled": true, "max_depth": 3, "forced_attention": true, }, "recall": { "enabled": false, "cross_pocket": true, }, }, }