opencode-agent-teams 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # opencode-agent-teams
2
+
3
+ Plugin for [OpenCode](https://opencode.ai) that brings parallel agent execution, background tasks, and team coordination to your sessions. Define teams of agents, run tasks in parallel respecting dependencies, and fire off background work without blocking your main session.
4
+
5
+ No more waiting for one agent to finish before starting the next.
6
+
7
+ ## Tools
8
+
9
+ ### `team_create`
10
+
11
+ Define a reusable team plan with multiple tasks, each with a prompt, optional agent type, and optional dependencies.
12
+
13
+ | Field | Type | Description |
14
+ |-------|------|-------------|
15
+ | `plan_id` | `string` | Unique name for the plan |
16
+ | `tasks` | `array` | Tasks with `id`, `prompt`, `agent` (optional), `depends_on` (optional) |
17
+
18
+ ### `team_run`
19
+
20
+ Execute a team plan. Tasks within the same dependency level run in parallel. The agent waits for all tasks to complete and returns a summary.
21
+
22
+ | Field | Type | Description |
23
+ |-------|------|-------------|
24
+ | `plan_id` | `string` | Plan to execute |
25
+
26
+ ### `team_status`
27
+
28
+ Check status of running or finished teams.
29
+
30
+ | Field | Type | Description |
31
+ |-------|------|-------------|
32
+ | `plan_id` | `string` (optional) | Omit to list all teams and background tasks |
33
+
34
+ ### `background`
35
+
36
+ Fire-and-forget a single agent in the background. Returns immediately with a task ID. Optionally injects the result into your session when done.
37
+
38
+ | Field | Type | Description |
39
+ |-------|------|-------------|
40
+ | `prompt` | `string` | Instructions for the background agent |
41
+ | `agent` | `string` (optional) | Agent type: `general`, `explore`, `build`. Defaults to `general`. |
42
+ | `notify` | `boolean` (optional) | Inject result into current session when done. Default: `true`. |
43
+
44
+ ## How it works
45
+
46
+ Under the hood, the plugin uses the OpenCode SDK to create child sessions and send prompts programmatically:
47
+
48
+ 1. `team_create` stores a plan with a DAG of task dependencies
49
+ 2. `team_run` topologically sorts the tasks, runs each dependency level in parallel via `Promise.all`
50
+ 3. Each task gets its own child session via `client.session.create` with `parentID` set to the current session
51
+ 4. Tasks run via `client.session.prompt` (blocking) or `client.session.promptAsync` (background)
52
+ 5. `team_status` reads session state from the OpenCode server
53
+ 6. Background tasks poll for completion and inject results back into the parent session
54
+
55
+ ## Install
56
+
57
+ ```json
58
+ {
59
+ "plugin": ["opencode-agent-teams"]
60
+ }
61
+ ```
62
+
63
+ Restart OpenCode.
64
+
65
+ ## Examples
66
+
67
+ ### Parallel research team
68
+
69
+ ```
70
+ team_create plan_id="research-feature" tasks=[
71
+ {id:"api", prompt:"Research the API endpoints needed for user auth", agent:"explore"},
72
+ {id:"db", prompt:"Research database schema for user auth", agent:"explore"},
73
+ {id:"summary", prompt:"Synthesize the API and DB findings into a plan", agent:"general", depends_on:["api","db"]}
74
+ ]
75
+ team_run plan_id="research-feature"
76
+ ```
77
+
78
+ ### Background code review
79
+
80
+ ```
81
+ background prompt="Review all changed files in this project for security issues" agent="explore"
82
+ team_status
83
+ ```
84
+
85
+ ## Mantenido por
86
+
87
+ **Maycol B.T** ([@SoyMaycol](https://github.com/SoyMaycol))
88
+
89
+ ```
90
+ Email: soymaycol.cn@gmail.com
91
+ Web: https://soymaycol.icu
92
+ GitHub: https://github.com/SoyMaycol
93
+ ```
@@ -0,0 +1,3 @@
1
+ import { type Plugin } from "@opencode-ai/plugin";
2
+ declare const plugin: Plugin;
3
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,446 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ const TEAMS_DIR = join(homedir(), ".config", "opencode", "teams");
6
+ function ensureDir() {
7
+ if (!existsSync(TEAMS_DIR))
8
+ mkdirSync(TEAMS_DIR, { recursive: true });
9
+ }
10
+ function planPath(id) {
11
+ return join(TEAMS_DIR, `plan-${id}.json`);
12
+ }
13
+ function runPath(id) {
14
+ return join(TEAMS_DIR, `run-${id}.json`);
15
+ }
16
+ function bgPath(id) {
17
+ return join(TEAMS_DIR, `bg-${id}.json`);
18
+ }
19
+ function savePlan(plan) {
20
+ ensureDir();
21
+ writeFileSync(planPath(plan.id), JSON.stringify(plan, null, 2));
22
+ }
23
+ function saveRun(run) {
24
+ ensureDir();
25
+ writeFileSync(runPath(run.planID), JSON.stringify(run, null, 2));
26
+ }
27
+ function saveBg(task) {
28
+ ensureDir();
29
+ writeFileSync(bgPath(task.id), JSON.stringify(task, null, 2));
30
+ }
31
+ function loadPlan(id) {
32
+ const p = planPath(id);
33
+ if (!existsSync(p))
34
+ return null;
35
+ return JSON.parse(readFileSync(p, "utf-8"));
36
+ }
37
+ function loadRun(planID) {
38
+ const p = runPath(planID);
39
+ if (!existsSync(p))
40
+ return null;
41
+ return JSON.parse(readFileSync(p, "utf-8"));
42
+ }
43
+ function loadBg(id) {
44
+ const p = bgPath(id);
45
+ if (!existsSync(p))
46
+ return null;
47
+ return JSON.parse(readFileSync(p, "utf-8"));
48
+ }
49
+ function listPlans() {
50
+ ensureDir();
51
+ const files = readFileSync(TEAMS_DIR, "utf-8")
52
+ ? readdirSync(TEAMS_DIR).filter((f) => f.startsWith("plan-"))
53
+ : [];
54
+ return files.map((f) => JSON.parse(readFileSync(join(TEAMS_DIR, f), "utf-8")));
55
+ }
56
+ function listRuns() {
57
+ ensureDir();
58
+ const files = readFileSync(TEAMS_DIR, "utf-8")
59
+ ? readdirSync(TEAMS_DIR).filter((f) => f.startsWith("run-"))
60
+ : [];
61
+ return files.map((f) => JSON.parse(readFileSync(join(TEAMS_DIR, f), "utf-8")));
62
+ }
63
+ function listBg() {
64
+ ensureDir();
65
+ const files = readFileSync(TEAMS_DIR, "utf-8")
66
+ ? readdirSync(TEAMS_DIR).filter((f) => f.startsWith("bg-"))
67
+ : [];
68
+ return files.map((f) => JSON.parse(readFileSync(join(TEAMS_DIR, f), "utf-8")));
69
+ }
70
+ function topologicalSort(tasks) {
71
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
72
+ const inDegree = new Map();
73
+ const children = new Map();
74
+ for (const t of tasks) {
75
+ if (!inDegree.has(t.id))
76
+ inDegree.set(t.id, 0);
77
+ if (!children.has(t.id))
78
+ children.set(t.id, []);
79
+ for (const dep of t.depends_on) {
80
+ if (!children.has(dep))
81
+ children.set(dep, []);
82
+ children.get(dep).push(t.id);
83
+ inDegree.set(t.id, (inDegree.get(t.id) || 0) + 1);
84
+ }
85
+ }
86
+ const levels = [];
87
+ let queue = tasks.filter((t) => (inDegree.get(t.id) || 0) === 0);
88
+ while (queue.length > 0) {
89
+ levels.push(queue);
90
+ const next = [];
91
+ for (const t of queue) {
92
+ for (const child of children.get(t.id) || []) {
93
+ const deg = (inDegree.get(child) || 1) - 1;
94
+ inDegree.set(child, deg);
95
+ if (deg === 0 && taskMap.has(child)) {
96
+ next.push(taskMap.get(child));
97
+ }
98
+ }
99
+ }
100
+ queue = next;
101
+ }
102
+ return levels;
103
+ }
104
+ const plugin = async (ctx) => {
105
+ return {
106
+ tool: {
107
+ team_create: tool({
108
+ description: "Define a team of agents with multiple tasks and optional dependencies. " +
109
+ "Each task has an id, a prompt, an optional agent type, and an optional list of task IDs it depends on. " +
110
+ "Use this before team_run to create reusable team plans.",
111
+ args: {
112
+ plan_id: tool.schema
113
+ .string()
114
+ .describe("Unique name for this team plan, e.g. 'refactor-auth'"),
115
+ tasks: tool.schema
116
+ .array(tool.schema.object({
117
+ id: tool.schema
118
+ .string()
119
+ .describe("Unique task ID within this plan"),
120
+ prompt: tool.schema
121
+ .string()
122
+ .describe("The prompt/instructions for this agent"),
123
+ agent: tool.schema
124
+ .string()
125
+ .optional()
126
+ .describe("Agent type to use (e.g. 'general', 'explore', 'build'). Defaults to 'general'"),
127
+ depends_on: tool.schema
128
+ .array(tool.schema.string())
129
+ .optional()
130
+ .describe("Task IDs that must complete before this one starts"),
131
+ }))
132
+ .describe("Array of tasks the team will execute"),
133
+ },
134
+ async execute(args) {
135
+ const tasks = (args.tasks || []).map((t) => ({
136
+ id: t.id,
137
+ prompt: t.prompt,
138
+ agent: t.agent || "general",
139
+ depends_on: t.depends_on || [],
140
+ }));
141
+ if (tasks.length === 0)
142
+ return "No tasks provided.";
143
+ const ids = new Set(tasks.map((t) => t.id));
144
+ if (ids.size !== tasks.length)
145
+ return "Duplicate task IDs found.";
146
+ for (const t of tasks) {
147
+ for (const d of t.depends_on) {
148
+ if (!ids.has(d))
149
+ return `Task "${t.id}" depends on "${d}" which does not exist.`;
150
+ }
151
+ }
152
+ const plan = {
153
+ id: args.plan_id,
154
+ tasks,
155
+ created: Date.now(),
156
+ };
157
+ savePlan(plan);
158
+ const levels = topologicalSort(tasks);
159
+ const summary = levels
160
+ .map((level, i) => ` Level ${i + 1}: ${level.map((t) => t.id).join(", ")}`)
161
+ .join("\n");
162
+ return [
163
+ `Team plan "${args.plan_id}" created with ${tasks.length} tasks across ${levels.length} execution levels.`,
164
+ "",
165
+ summary,
166
+ "",
167
+ `Run it with: team_run plan_id="${args.plan_id}"`,
168
+ ].join("\n");
169
+ },
170
+ }),
171
+ team_run: tool({
172
+ description: "Execute a team plan. Tasks run in parallel within each dependency level. " +
173
+ "The agent waits for all tasks to complete and returns a summary of results. " +
174
+ "Use team_status to check progress of longer running teams.",
175
+ args: {
176
+ plan_id: tool.schema
177
+ .string()
178
+ .describe("The plan ID to execute (created with team_create)"),
179
+ },
180
+ async execute(args, context) {
181
+ const plan = loadPlan(args.plan_id);
182
+ if (!plan)
183
+ return `Plan "${args.plan_id}" not found. Create it first with team_create.`;
184
+ const run = {
185
+ planID: plan.id,
186
+ tasks: plan.tasks.map((t) => ({
187
+ id: t.id,
188
+ sessionID: "",
189
+ status: "pending",
190
+ })),
191
+ started: Date.now(),
192
+ };
193
+ saveRun(run);
194
+ const taskMap = new Map(plan.tasks.map((t) => [t.id, t]));
195
+ const runMap = new Map(run.tasks.map((t) => [t.id, t]));
196
+ const levels = topologicalSort(plan.tasks);
197
+ for (const level of levels) {
198
+ const promises = level.map(async (taskDef) => {
199
+ const runEntry = runMap.get(taskDef.id);
200
+ runEntry.status = "running";
201
+ saveRun(run);
202
+ try {
203
+ const { data: session, error: createErr } = await ctx.client.session.create({
204
+ body: {
205
+ parentID: context.sessionID,
206
+ title: `[team] ${plan.id}/${taskDef.id}`,
207
+ },
208
+ });
209
+ if (createErr || !session) {
210
+ runEntry.status = "failed";
211
+ runEntry.error = String(createErr || "session creation failed");
212
+ saveRun(run);
213
+ return;
214
+ }
215
+ runEntry.sessionID = session.id;
216
+ saveRun(run);
217
+ const { data: result, error: promptErr } = await ctx.client.session.prompt({
218
+ path: { id: session.id },
219
+ body: {
220
+ agent: taskDef.agent || "general",
221
+ parts: [{ type: "text", text: taskDef.prompt }],
222
+ },
223
+ });
224
+ if (promptErr || !result) {
225
+ runEntry.status = "failed";
226
+ runEntry.error = String(promptErr || "prompt failed");
227
+ }
228
+ else {
229
+ runEntry.status = "done";
230
+ }
231
+ }
232
+ catch (e) {
233
+ runEntry.status = "failed";
234
+ runEntry.error = e?.message || String(e);
235
+ }
236
+ saveRun(run);
237
+ });
238
+ await Promise.all(promises);
239
+ }
240
+ const finalRun = loadRun(plan.id);
241
+ if (!finalRun)
242
+ return "Run data lost.";
243
+ const lines = [
244
+ `Team "${plan.id}" finished. ${finalRun.tasks.filter((t) => t.status === "done").length}/${finalRun.tasks.length} tasks succeeded.`,
245
+ "",
246
+ ];
247
+ for (const t of finalRun.tasks) {
248
+ const icon = t.status === "done" ? "✓" : t.status === "failed" ? "✗" : "○";
249
+ const session = t.sessionID ? ` (session: ${t.sessionID})` : "";
250
+ lines.push(` ${icon} ${t.id} — ${t.status}${session}`);
251
+ if (t.error)
252
+ lines.push(` error: ${t.error}`);
253
+ }
254
+ lines.push("");
255
+ lines.push("Use team_status to get detailed results from individual sessions.");
256
+ return lines.join("\n");
257
+ },
258
+ }),
259
+ team_status: tool({
260
+ description: "Check the execution status of a team plan. Shows each task's status, session ID, and any errors. " +
261
+ "Omit plan_id to list all tracked teams and background tasks.",
262
+ args: {
263
+ plan_id: tool.schema
264
+ .string()
265
+ .optional()
266
+ .describe("Specific plan ID to check, or omit to list all"),
267
+ },
268
+ async execute(_args) {
269
+ if (_args.plan_id) {
270
+ const run = loadRun(_args.plan_id);
271
+ if (!run)
272
+ return `No execution data for plan "${_args.plan_id}".`;
273
+ const lines = [
274
+ `Plan: ${run.planID}`,
275
+ `Started: ${new Date(run.started).toISOString()}`,
276
+ "",
277
+ ];
278
+ for (const t of run.tasks) {
279
+ const icon = t.status === "done"
280
+ ? "✓"
281
+ : t.status === "failed"
282
+ ? "✗"
283
+ : t.status === "running"
284
+ ? "◌"
285
+ : "○";
286
+ lines.push(` ${icon} ${t.id} — ${t.status}`);
287
+ if (t.sessionID)
288
+ lines.push(` session: ${t.sessionID}`);
289
+ if (t.error)
290
+ lines.push(` error: ${t.error}`);
291
+ }
292
+ return lines.join("\n");
293
+ }
294
+ const allRuns = listRuns();
295
+ const bgTasks = listBg();
296
+ const sections = ["**Team runs:**"];
297
+ if (allRuns.length === 0) {
298
+ sections.push(" No team runs yet.");
299
+ }
300
+ else {
301
+ for (const r of allRuns) {
302
+ const done = r.tasks.filter((t) => t.status === "done").length;
303
+ const failed = r.tasks.filter((t) => t.status === "failed").length;
304
+ const running = r.tasks.filter((t) => t.status === "running").length;
305
+ sections.push(` • ${r.planID}: ${done}✓ ${failed > 0 ? failed + "✗ " : ""}${running > 0 ? running + "◌ " : ""}(${r.tasks.length} tasks)`);
306
+ }
307
+ }
308
+ sections.push("", "**Background tasks:**");
309
+ if (bgTasks.length === 0) {
310
+ sections.push(" No background tasks.");
311
+ }
312
+ else {
313
+ for (const b of bgTasks) {
314
+ const icon = b.status === "done"
315
+ ? "✓"
316
+ : b.status === "failed"
317
+ ? "✗"
318
+ : "◌";
319
+ sections.push(` ${icon} ${b.id} — ${b.status} [${b.agent || "general"}]`);
320
+ }
321
+ }
322
+ return sections.join("\n");
323
+ },
324
+ }),
325
+ background: tool({
326
+ description: "Launch a fire-and-forget agent in the background. " +
327
+ "Returns immediately with a task ID so you can check status later. " +
328
+ "Useful for long-running tasks like code reviews, tests, research, or documentation generation.",
329
+ args: {
330
+ prompt: tool.schema
331
+ .string()
332
+ .describe("The prompt/instructions for the background agent"),
333
+ agent: tool.schema
334
+ .string()
335
+ .optional()
336
+ .describe("Agent type (e.g. 'general', 'explore', 'build'). Defaults to 'general'"),
337
+ notify: tool.schema
338
+ .boolean()
339
+ .optional()
340
+ .describe("When true, the result will be injected into the current session when done. Default: true"),
341
+ },
342
+ async execute(args, context) {
343
+ const taskID = `bg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
344
+ try {
345
+ const { data: session, error: createErr } = await ctx.client.session.create({
346
+ body: {
347
+ parentID: context.sessionID,
348
+ title: `[bg] ${taskID}`,
349
+ },
350
+ });
351
+ if (createErr || !session) {
352
+ return `Failed to create background session: ${String(createErr || "unknown")}`;
353
+ }
354
+ const bgTask = {
355
+ id: taskID,
356
+ prompt: args.prompt,
357
+ agent: args.agent || "general",
358
+ sessionID: session.id,
359
+ status: "running",
360
+ started: Date.now(),
361
+ };
362
+ saveBg(bgTask);
363
+ const { error: promptErr } = await ctx.client.session.promptAsync({
364
+ path: { id: session.id },
365
+ body: {
366
+ agent: args.agent || "general",
367
+ parts: [{ type: "text", text: args.prompt }],
368
+ },
369
+ });
370
+ if (promptErr) {
371
+ bgTask.status = "failed";
372
+ saveBg(bgTask);
373
+ return `Background task started but promptAsync failed: ${promptErr}`;
374
+ }
375
+ // Poll for completion
376
+ (async () => {
377
+ let attempts = 0;
378
+ const maxAttempts = 600;
379
+ while (attempts < maxAttempts) {
380
+ await sleep(2000);
381
+ attempts++;
382
+ try {
383
+ const { data: statuses } = await ctx.client.session.status();
384
+ const s = statuses?.[session.id];
385
+ if (!s || s.type === "idle") {
386
+ // Session finished
387
+ const { data: msgs } = await ctx.client.session.messages({
388
+ path: { id: session.id },
389
+ query: { limit: 1 },
390
+ });
391
+ bgTask.status = "done";
392
+ saveBg(bgTask);
393
+ // If notify is false, skip injection
394
+ if (args.notify !== false && msgs?.length) {
395
+ const last = msgs[msgs.length - 1];
396
+ const text = last.parts
397
+ .filter((p) => p.type === "text")
398
+ .map((p) => p.text)
399
+ .join("\n");
400
+ await ctx.client.session.promptAsync({
401
+ path: { id: context.sessionID },
402
+ body: {
403
+ parts: [
404
+ {
405
+ type: "text",
406
+ text: `[Background task "${taskID}" completed]\n\n${text}`,
407
+ synthetic: true,
408
+ },
409
+ ],
410
+ noReply: true,
411
+ },
412
+ });
413
+ }
414
+ return;
415
+ }
416
+ }
417
+ catch {
418
+ // keep polling
419
+ }
420
+ }
421
+ bgTask.status = "failed";
422
+ bgTask.error = "timed out";
423
+ saveBg(bgTask);
424
+ })();
425
+ return [
426
+ `Background task launched.`,
427
+ ` Task ID: ${taskID}`,
428
+ ` Agent: ${args.agent || "general"}`,
429
+ ` Session: ${session.id}`,
430
+ "",
431
+ `Check status with: team_status`,
432
+ `Or: team_status plan_id="${taskID}"`,
433
+ ].join("\n");
434
+ }
435
+ catch (e) {
436
+ return `Failed to launch background task: ${e?.message || String(e)}`;
437
+ }
438
+ },
439
+ }),
440
+ },
441
+ };
442
+ };
443
+ function sleep(ms) {
444
+ return new Promise((r) => setTimeout(r, ms));
445
+ }
446
+ export default plugin;
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "opencode-agent-teams",
3
+ "version": "1.0.0",
4
+ "description": "OpenCode plugin for parallel agent execution, background tasks, and team coordination. Create teams of agents that work together, run tasks in parallel respecting dependencies, and fire off background work without blocking.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": ["dist"],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "opencode",
21
+ "plugin",
22
+ "teams",
23
+ "agents",
24
+ "parallel",
25
+ "background",
26
+ "orchestration"
27
+ ],
28
+ "author": {
29
+ "name": "Maycol B.T",
30
+ "email": "soymaycol.cn@gmail.com",
31
+ "url": "https://github.com/SoyMaycol"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/SoyMaycol/opencode-teams"
40
+ },
41
+ "peerDependencies": {
42
+ "@opencode-ai/plugin": ">=1.0.0",
43
+ "@opencode-ai/sdk": ">=1.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@opencode-ai/plugin": "1.4.6",
47
+ "@opencode-ai/sdk": "latest",
48
+ "@types/node": "^22.0.0",
49
+ "typescript": "^5.0.0"
50
+ }
51
+ }