From a8cd789011cfb9f35425faa9385a5eaa77c9c570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=BE=D1=81=D1=82=D0=BENoob?= Date: Thu, 5 Mar 2026 15:10:24 +0300 Subject: [PATCH 1/2] move parallel agent flow to plugin tools --- .opencode/README.parallel-agents.md | 29 +++++ .opencode/plugins/parallel-agents.ts | 187 +++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 .opencode/README.parallel-agents.md create mode 100644 .opencode/plugins/parallel-agents.ts diff --git a/.opencode/README.parallel-agents.md b/.opencode/README.parallel-agents.md new file mode 100644 index 00000000000..3548a4187b2 --- /dev/null +++ b/.opencode/README.parallel-agents.md @@ -0,0 +1,29 @@ +# Parallel Agents Plugin (ready-to-use) + +## 1) Enable plugin + +Add plugin path to `opencode.json`: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "plugin": ["file://./.opencode/plugins/parallel-agents.ts"] +} +``` + +## 2) What it adds + +- `agents_start`: starts up to 8 subagent jobs in parallel and returns job ids. +- `agents_status`: returns current status/results for jobs. +- `agents_wait`: waits for selected jobs and returns final outputs. + +## 3) Typical flow + +1. Call `agents_start` with multiple tasks. +2. Keep working or do other tool calls. +3. Call `agents_status` for progress. +4. Call `agents_wait` before composing final answer. + +## 4) Why this solves current sequential blocking + +Built-in subtask queue in session loop is sequential in core flow. This plugin runs independent jobs in separate sessions concurrently and lets the main agent continue without blocking on each single subagent call. diff --git a/.opencode/plugins/parallel-agents.ts b/.opencode/plugins/parallel-agents.ts new file mode 100644 index 00000000000..80953b734cd --- /dev/null +++ b/.opencode/plugins/parallel-agents.ts @@ -0,0 +1,187 @@ +import { tool, type Plugin } from "@opencode-ai/plugin" + +const map = new Map< + string, + { + id: string + agent: string + description: string + status: "running" | "done" | "error" + session: string + output?: string + error?: string + started: number + ended?: number + } +>() + +function id() { + return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}` +} + +export const ParallelAgentsPlugin: Plugin = async ({ client }) => { + async function start(input: Array<{ agent: string; prompt: string; description?: string }>) { + const jobs = await Promise.all( + input.slice(0, 8).map(async (item) => { + const jid = id() + const desc = item.description?.trim() || "parallel task" + + const created = await client.session.create({ + body: { + title: `${desc} (@${item.agent} subagent)`, + }, + }) + + if (created.error || !created.data?.id) { + map.set(jid, { + id: jid, + agent: item.agent, + description: desc, + status: "error", + session: "", + error: created.error ? String(created.error) : "failed to create session", + started: Date.now(), + ended: Date.now(), + }) + return jid + } + + const sid = created.data.id + map.set(jid, { + id: jid, + agent: item.agent, + description: desc, + status: "running", + session: sid, + started: Date.now(), + }) + + void client.session + .prompt({ + path: { id: sid }, + body: { + agent: item.agent, + parts: [{ type: "text", text: item.prompt }], + }, + }) + .then((res) => { + const item = map.get(jid) + if (!item) return + if (res.error || !res.data) { + map.set(jid, { + ...item, + status: "error", + error: res.error ? String(res.error) : "subagent failed", + ended: Date.now(), + }) + return + } + const text = + res.data.parts + ?.filter((part) => part.type === "text") + .map((part) => ("text" in part ? part.text : "")) + .join("\n") || "" + map.set(jid, { + ...item, + status: "done", + output: text, + ended: Date.now(), + }) + }) + + return jid + }), + ) + + return jobs + } + + return { + tool: { + agents_start: tool({ + description: "Start multiple subagent jobs in parallel and return job ids immediately.", + args: { + tasks: tool.schema + .array( + tool.schema.object({ + agent: tool.schema.string(), + prompt: tool.schema.string(), + description: tool.schema.string().optional(), + }), + ) + .min(1), + }, + async execute(args) { + const jobs = await start(args.tasks) + return JSON.stringify({ + started: jobs.length, + jobs, + note: "Use agents_status or agents_wait to fetch outputs.", + }) + }, + }), + agents_status: tool({ + description: "Check status/output of parallel subagent jobs.", + args: { + jobs: tool.schema.array(tool.schema.string()).optional(), + }, + async execute(args) { + const ids = args.jobs?.length ? args.jobs : Array.from(map.keys()) + const jobs = ids + .map((id) => map.get(id)) + .filter((item): item is NonNullable => Boolean(item)) + .map((item) => ({ + id: item.id, + agent: item.agent, + description: item.description, + status: item.status, + session: item.session, + output: item.output, + error: item.error, + started: item.started, + ended: item.ended, + })) + return JSON.stringify({ jobs }, null, 2) + }, + }), + agents_wait: tool({ + description: "Wait until selected parallel jobs finish and return outputs.", + args: { + jobs: tool.schema.array(tool.schema.string()), + timeout: tool.schema.number().int().positive().optional(), + }, + async execute(args) { + const until = Date.now() + (args.timeout ?? 600000) + while (Date.now() < until) { + const done = args.jobs.every((id) => { + const item = map.get(id) + if (!item) return true + return item.status === "done" || item.status === "error" + }) + if (done) break + await Bun.sleep(400) + } + + const jobs = args.jobs.map((id) => map.get(id)).filter((item): item is NonNullable => Boolean(item)) + return JSON.stringify( + { + jobs, + done: jobs.every((item) => item.status === "done" || item.status === "error"), + }, + null, + 2, + ) + }, + }), + }, + "experimental.chat.system.transform": async (_input, output) => { + output.system.push(` +If the user asks for multiple independent subagent tasks, prefer: +1) agents_start (launch all jobs in parallel) +2) agents_status (check progress) +3) agents_wait (collect final outputs) +Use built-in task tool for strictly sequential dependencies. +`) + }, + } +} From 9fb3d7509a4956f7141ecce612a8d13e1514fa4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=BE=D1=81=D1=82=D0=BENoob?= Date: Thu, 5 Mar 2026 16:16:33 +0300 Subject: [PATCH 2/2] remove unused helper from parallel agent plugin --- .opencode/README.parallel-agents.md | 38 ++-- .opencode/plugins/parallel-agents.ts | 260 +++++++++++++-------------- 2 files changed, 153 insertions(+), 145 deletions(-) diff --git a/.opencode/README.parallel-agents.md b/.opencode/README.parallel-agents.md index 3548a4187b2..467a6e1306d 100644 --- a/.opencode/README.parallel-agents.md +++ b/.opencode/README.parallel-agents.md @@ -1,8 +1,6 @@ -# Parallel Agents Plugin (ready-to-use) +# Parallel Agents Plugin (chat-visible task flow) -## 1) Enable plugin - -Add plugin path to `opencode.json`: +## Enable plugin ```json { @@ -11,19 +9,29 @@ Add plugin path to `opencode.json`: } ``` -## 2) What it adds +## What is fixed -- `agents_start`: starts up to 8 subagent jobs in parallel and returns job ids. -- `agents_status`: returns current status/results for jobs. -- `agents_wait`: waits for selected jobs and returns final outputs. +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. -## 3) Typical flow +## Tools -1. Call `agents_start` with multiple tasks. -2. Keep working or do other tool calls. -3. Call `agents_status` for progress. -4. Call `agents_wait` before composing final answer. +- `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 -## 4) Why this solves current sequential blocking +## Typical usage -Built-in subtask queue in session loop is sequential in core flow. This plugin runs independent jobs in separate sessions concurrently and lets the main agent continue without blocking on each single subagent call. +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 index 80953b734cd..47fe310fccb 100644 --- a/.opencode/plugins/parallel-agents.ts +++ b/.opencode/plugins/parallel-agents.ts @@ -1,105 +1,67 @@ import { tool, type Plugin } from "@opencode-ai/plugin" -const map = new Map< - string, - { - id: string - agent: string - description: string - status: "running" | "done" | "error" - session: string - output?: string - error?: string - started: number - ended?: number - } ->() - -function id() { - return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}` +type Item = { + agent: string + prompt: string + description: string } -export const ParallelAgentsPlugin: Plugin = async ({ client }) => { - async function start(input: Array<{ agent: string; prompt: string; description?: string }>) { - const jobs = await Promise.all( - input.slice(0, 8).map(async (item) => { - const jid = id() - const desc = item.description?.trim() || "parallel task" +type Run = { + id: string + session: string + items: Item[] + started: number +} - const created = await client.session.create({ - body: { - title: `${desc} (@${item.agent} subagent)`, - }, - }) +const runs = new Map() - if (created.error || !created.data?.id) { - map.set(jid, { - id: jid, - agent: item.agent, - description: desc, - status: "error", - session: "", - error: created.error ? String(created.error) : "failed to create session", - started: Date.now(), - ended: Date.now(), - }) - return jid - } +function rid() { + return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}` +} - const sid = created.data.id - map.set(jid, { - id: jid, - agent: item.agent, - description: desc, - status: "running", - session: sid, - started: Date.now(), - }) - void client.session - .prompt({ - path: { id: sid }, - body: { - agent: item.agent, - parts: [{ type: "text", text: item.prompt }], - }, - }) - .then((res) => { - const item = map.get(jid) - if (!item) return - if (res.error || !res.data) { - map.set(jid, { - ...item, - status: "error", - error: res.error ? String(res.error) : "subagent failed", - ended: Date.now(), - }) - return - } - const text = - res.data.parts - ?.filter((part) => part.type === "text") - .map((part) => ("text" in part ? part.text : "")) - .join("\n") || "" - map.set(jid, { - ...item, - status: "done", - output: text, - ended: Date.now(), - }) - }) +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) + } - return jid - }), - ) + 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, + } + }) - return jobs + const done = rows.every((item) => item.status === "completed" || item.status === "error") + return { rows, done } } return { tool: { agents_start: tool({ - description: "Start multiple subagent jobs in parallel and return job ids immediately.", + description: + "Queue multiple subagents in current chat as task() runs. They will appear in chat with task status updates.", args: { tasks: tool.schema .array( @@ -109,64 +71,104 @@ export const ParallelAgentsPlugin: Plugin = async ({ client }) => { description: tool.schema.string().optional(), }), ) - .min(1), + .min(1) + .max(8), }, - async execute(args) { - const jobs = await start(args.tasks) - return JSON.stringify({ - started: jobs.length, - jobs, - note: "Use agents_status or agents_wait to fetch outputs.", + 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: "Check status/output of parallel subagent jobs.", + description: "Show per-agent task() status for a started run.", args: { - jobs: tool.schema.array(tool.schema.string()).optional(), + run_id: tool.schema.string(), }, async execute(args) { - const ids = args.jobs?.length ? args.jobs : Array.from(map.keys()) - const jobs = ids - .map((id) => map.get(id)) - .filter((item): item is NonNullable => Boolean(item)) - .map((item) => ({ - id: item.id, - agent: item.agent, - description: item.description, - status: item.status, - session: item.session, - output: item.output, - error: item.error, - started: item.started, - ended: item.ended, - })) - return JSON.stringify({ jobs }, null, 2) + 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 selected parallel jobs finish and return outputs.", + description: "Wait until all task() entries in run are completed or failed.", args: { - jobs: tool.schema.array(tool.schema.string()), + run_id: tool.schema.string(), timeout: tool.schema.number().int().positive().optional(), }, async execute(args) { - const until = Date.now() + (args.timeout ?? 600000) - while (Date.now() < until) { - const done = args.jobs.every((id) => { - const item = map.get(id) - if (!item) return true - return item.status === "done" || item.status === "error" - }) - if (done) break + 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 jobs = args.jobs.map((id) => map.get(id)).filter((item): item is NonNullable => Boolean(item)) + const data = await state(run) return JSON.stringify( { - jobs, - done: jobs.every((item) => item.status === "done" || item.status === "error"), + run_id: run.id, + done: data.done, + timeout: true, + tasks: data.rows, }, null, 2, @@ -176,11 +178,9 @@ export const ParallelAgentsPlugin: Plugin = async ({ client }) => { }, "experimental.chat.system.transform": async (_input, output) => { output.system.push(` -If the user asks for multiple independent subagent tasks, prefer: -1) agents_start (launch all jobs in parallel) -2) agents_status (check progress) -3) agents_wait (collect final outputs) -Use built-in task tool for strictly sequential dependencies. +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. `) }, }