diff --git a/.opencode/README.parallel-agents.md b/.opencode/README.parallel-agents.md new file mode 100644 index 00000000000..467a6e1306d --- /dev/null +++ b/.opencode/README.parallel-agents.md @@ -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 diff --git a/.opencode/plugins/parallel-agents.ts b/.opencode/plugins/parallel-agents.ts new file mode 100644 index 00000000000..47fe310fccb --- /dev/null +++ b/.opencode/plugins/parallel-agents.ts @@ -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() + +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. +`) + }, + } +}