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 +93 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +446 -0
- package/package.json +51 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
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
|
+
}
|