Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .opencode/README.parallel-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Parallel Agents Plugin (chat-visible task flow)

## Enable plugin

```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["file://./.opencode/plugins/parallel-agents.ts"]
}
```

## What is fixed

This plugin now queues subagents as native `subtask` parts in the **current chat session**.
So during execution you see regular `task()` entries with running/completed/error status,
not only one generic custom-tool spinner.

## Tools

- `agents_start`
- input: `tasks[]` (`agent`, `prompt`, optional `description`)
- action: pushes subtask parts into current session (`noReply: true`)
- output: `run_id`
- `agents_status`
- input: `run_id`
- output: per-task status from native `task` tool parts
- `agents_wait`
- input: `run_id`, optional `timeout`
- output: waits until all tasks are completed/error

## Typical usage

1. `agents_start`
2. do other work
3. `agents_status`
4. `agents_wait`
5. summarize results
187 changes: 187 additions & 0 deletions .opencode/plugins/parallel-agents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { tool, type Plugin } from "@opencode-ai/plugin"

type Item = {
agent: string
prompt: string
description: string
}

type Run = {
id: string
session: string
items: Item[]
started: number
}

const runs = new Map<string, Run>()

function rid() {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
}


export const ParallelAgentsPlugin: Plugin = async ({ client }) => {
async function parts(session: string) {
const res = await client.session.get({ path: { id: session } })
if (res.error || !res.data) return []
return res.data.messages.flatMap((msg) => msg.parts)
}

async function state(run: Run) {
const all = await parts(run.session)
const rows = run.items.map((item) => {
const hit = all.findLast(
(part) =>
part.type === "tool" &&
part.tool === "task" &&
part.state.input.subagent_type === item.agent &&
part.state.input.description === item.description,
)
if (!hit || hit.type !== "tool") {
return {
agent: item.agent,
description: item.description,
status: "pending",
}
}
return {
agent: item.agent,
description: item.description,
status: hit.state.status,
output: hit.state.status === "completed" ? hit.state.output : undefined,
error: hit.state.status === "error" ? hit.state.error : undefined,
}
})

const done = rows.every((item) => item.status === "completed" || item.status === "error")
return { rows, done }
}

return {
tool: {
agents_start: tool({
description:
"Queue multiple subagents in current chat as task() runs. They will appear in chat with task status updates.",
args: {
tasks: tool.schema
.array(
tool.schema.object({
agent: tool.schema.string(),
prompt: tool.schema.string(),
description: tool.schema.string().optional(),
}),
)
.min(1)
.max(8),
},
async execute(args, ctx) {
const items = args.tasks.map((item, idx) => ({
agent: item.agent,
prompt: item.prompt,
description: item.description?.trim() || `parallel task ${idx + 1}`,
}))
const id = rid()
runs.set(id, {
id,
session: ctx.sessionID,
items,
started: Date.now(),
})

await client.session.prompt({
path: { id: ctx.sessionID },
body: {
noReply: true,
parts: items.map((item) => ({
type: "subtask" as const,
agent: item.agent,
prompt: item.prompt,
description: item.description,
})),
},
})

return JSON.stringify(
{
run_id: id,
started: items.length,
note: "Subagents are queued as native subtask/task entries in this chat. Use agents_status or agents_wait.",
tasks: items.map((item) => ({ agent: item.agent, description: item.description })),
},
null,
2,
)
},
}),
agents_status: tool({
description: "Show per-agent task() status for a started run.",
args: {
run_id: tool.schema.string(),
},
async execute(args) {
const run = runs.get(args.run_id)
if (!run) return JSON.stringify({ error: "run_id not found" }, null, 2)
const data = await state(run)
return JSON.stringify(
{
run_id: run.id,
session: run.session,
started: run.started,
done: data.done,
tasks: data.rows,
},
null,
2,
)
},
}),
agents_wait: tool({
description: "Wait until all task() entries in run are completed or failed.",
args: {
run_id: tool.schema.string(),
timeout: tool.schema.number().int().positive().optional(),
},
async execute(args) {
const run = runs.get(args.run_id)
if (!run) return JSON.stringify({ error: "run_id not found" }, null, 2)
const end = Date.now() + (args.timeout ?? 600000)

while (Date.now() < end) {
const data = await state(run)
if (data.done) {
return JSON.stringify(
{
run_id: run.id,
done: true,
tasks: data.rows,
},
null,
2,
)
}
await Bun.sleep(400)
}

const data = await state(run)
return JSON.stringify(
{
run_id: run.id,
done: data.done,
timeout: true,
tasks: data.rows,
},
null,
2,
)
},
}),
},
"experimental.chat.system.transform": async (_input, output) => {
output.system.push(`
When user asks to run multiple subagents and wants visible task() progress in chat,
use agents_start first, then agents_status / agents_wait.
This keeps native task status lines visible in this same chat.
`)
},
}
}