opencode-async-agent 1.0.0 → 1.0.1

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.
@@ -0,0 +1,932 @@
1
+ // src/plugin/utils.ts
2
+ function createLogger(client) {
3
+ const log = (level, message) => client.app.log({ body: { service: "async-agent", level, message } }).catch(() => {
4
+ });
5
+ return {
6
+ debug: (msg) => log("debug", msg),
7
+ info: (msg) => log("info", msg),
8
+ warn: (msg) => log("warn", msg),
9
+ error: (msg) => log("error", msg)
10
+ };
11
+ }
12
+ function showToast(client, title, message, variant = "info", duration = 3e3) {
13
+ const tuiClient = client;
14
+ if (!tuiClient.tui?.showToast) return;
15
+ tuiClient.tui.showToast({
16
+ body: { title, message, variant, duration }
17
+ }).catch(() => {
18
+ });
19
+ }
20
+ function formatDuration(startedAt, completedAt) {
21
+ const end = completedAt || /* @__PURE__ */ new Date();
22
+ const diffMs = end.getTime() - startedAt.getTime();
23
+ const diffSec = Math.floor(diffMs / 1e3);
24
+ if (diffSec < 60) {
25
+ return `${diffSec}s`;
26
+ }
27
+ const diffMin = Math.floor(diffSec / 60);
28
+ if (diffMin < 60) {
29
+ const secs = diffSec % 60;
30
+ return secs > 0 ? `${diffMin}m ${secs}s` : `${diffMin}m`;
31
+ }
32
+ const diffHour = Math.floor(diffMin / 60);
33
+ const mins = diffMin % 60;
34
+ return mins > 0 ? `${diffHour}h ${mins}m` : `${diffHour}h`;
35
+ }
36
+
37
+ // src/plugin/types.ts
38
+ var MAX_RUN_TIME_MS = 15 * 60 * 1e3;
39
+
40
+ // src/plugin/manager.ts
41
+ function parseModel(model) {
42
+ const [providerID, ...rest] = model.split("/");
43
+ return { providerID, modelID: rest.join("/") };
44
+ }
45
+ var DelegationManager = class {
46
+ delegations = /* @__PURE__ */ new Map();
47
+ client;
48
+ log;
49
+ pendingByParent = /* @__PURE__ */ new Map();
50
+ constructor(client, log) {
51
+ this.client = client;
52
+ this.log = log;
53
+ }
54
+ calculateDuration(delegation) {
55
+ return formatDuration(delegation.startedAt, delegation.completedAt);
56
+ }
57
+ // ---- Core operations ----
58
+ async delegate(input) {
59
+ await this.debugLog(`delegate() called`);
60
+ const agentsResult = await this.client.app.agents({});
61
+ const agents = agentsResult.data ?? [];
62
+ const validAgent = agents.find((a) => a.name === input.agent);
63
+ if (!validAgent) {
64
+ const available = agents.filter((a) => a.mode === "subagent" || a.mode === "all" || !a.mode).map((a) => `\u2022 ${a.name}${a.description ? ` - ${a.description}` : ""}`).join("\n");
65
+ throw new Error(
66
+ `Agent "${input.agent}" not found.
67
+
68
+ Available agents:
69
+ ${available || "(none)"}`
70
+ );
71
+ }
72
+ const sessionResult = await this.client.session.create({
73
+ body: {
74
+ title: `Delegation: ${input.agent}`,
75
+ parentID: input.parentSessionID
76
+ }
77
+ });
78
+ await this.debugLog(`session.create result: ${JSON.stringify(sessionResult.data)}`);
79
+ if (!sessionResult.data?.id) {
80
+ throw new Error("Failed to create delegation session");
81
+ }
82
+ const sessionID = sessionResult.data.id;
83
+ const delegation = {
84
+ id: sessionID,
85
+ sessionID,
86
+ parentSessionID: input.parentSessionID,
87
+ parentMessageID: input.parentMessageID,
88
+ parentAgent: input.parentAgent,
89
+ prompt: input.prompt,
90
+ agent: input.agent,
91
+ model: input.model,
92
+ status: "running",
93
+ startedAt: /* @__PURE__ */ new Date(),
94
+ progress: {
95
+ toolCalls: 0,
96
+ lastUpdate: /* @__PURE__ */ new Date()
97
+ }
98
+ };
99
+ await this.debugLog(`Created delegation ${delegation.id}`);
100
+ this.delegations.set(delegation.id, delegation);
101
+ const parentId = input.parentSessionID;
102
+ if (!this.pendingByParent.has(parentId)) {
103
+ this.pendingByParent.set(parentId, /* @__PURE__ */ new Set());
104
+ }
105
+ this.pendingByParent.get(parentId)?.add(delegation.id);
106
+ await this.debugLog(
107
+ `Tracking delegation ${delegation.id} for parent ${parentId}. Pending count: ${this.pendingByParent.get(parentId)?.size}`
108
+ );
109
+ setTimeout(() => {
110
+ const current = this.delegations.get(delegation.id);
111
+ if (current && current.status === "running") {
112
+ this.handleTimeout(delegation.id);
113
+ }
114
+ }, MAX_RUN_TIME_MS + 5e3);
115
+ showToast(this.client, "New Background Task", `${delegation.id} (${input.agent})`, "info", 3e3);
116
+ const promptBody = {
117
+ agent: input.agent,
118
+ parts: [{ type: "text", text: input.prompt }],
119
+ tools: {
120
+ task: false,
121
+ delegate: false,
122
+ todowrite: false,
123
+ plan_save: false
124
+ }
125
+ };
126
+ if (input.model) {
127
+ promptBody.model = parseModel(input.model);
128
+ }
129
+ this.client.session.prompt({
130
+ path: { id: delegation.sessionID },
131
+ body: promptBody
132
+ }).catch((error) => {
133
+ delegation.status = "error";
134
+ delegation.error = error.message;
135
+ delegation.completedAt = /* @__PURE__ */ new Date();
136
+ delegation.duration = this.calculateDuration(delegation);
137
+ this.notifyParent(delegation);
138
+ });
139
+ return delegation;
140
+ }
141
+ async resume(delegationId, newPrompt) {
142
+ const delegation = this.delegations.get(delegationId);
143
+ if (!delegation) {
144
+ throw new Error(`Delegation "${delegationId}" not found`);
145
+ }
146
+ if (delegation.status === "running") {
147
+ throw new Error(`Delegation is already running. Wait for it to complete or cancel it first.`);
148
+ }
149
+ delegation.status = "running";
150
+ delegation.completedAt = void 0;
151
+ delegation.error = void 0;
152
+ delegation.startedAt = /* @__PURE__ */ new Date();
153
+ delegation.progress = {
154
+ toolCalls: 0,
155
+ lastUpdate: /* @__PURE__ */ new Date()
156
+ };
157
+ const parentId = delegation.parentSessionID;
158
+ if (!this.pendingByParent.has(parentId)) {
159
+ this.pendingByParent.set(parentId, /* @__PURE__ */ new Set());
160
+ }
161
+ this.pendingByParent.get(parentId)?.add(delegation.id);
162
+ const prompt = newPrompt || "Continue from where you left off.";
163
+ const resumeBody = {
164
+ agent: delegation.agent,
165
+ parts: [{ type: "text", text: prompt }],
166
+ tools: {
167
+ task: false,
168
+ delegate: false,
169
+ todowrite: false,
170
+ plan_save: false
171
+ }
172
+ };
173
+ if (delegation.model) {
174
+ resumeBody.model = parseModel(delegation.model);
175
+ }
176
+ this.client.session.prompt({
177
+ path: { id: delegation.sessionID },
178
+ body: resumeBody
179
+ }).catch((error) => {
180
+ delegation.status = "error";
181
+ delegation.error = error.message;
182
+ delegation.completedAt = /* @__PURE__ */ new Date();
183
+ delegation.duration = this.calculateDuration(delegation);
184
+ this.notifyParent(delegation);
185
+ });
186
+ await this.debugLog(`Resumed delegation ${delegation.id}`);
187
+ return delegation;
188
+ }
189
+ async cancel(delegationId) {
190
+ const delegation = this.delegations.get(delegationId);
191
+ if (!delegation) return false;
192
+ if (delegation.status !== "running") return false;
193
+ delegation.status = "cancelled";
194
+ delegation.completedAt = /* @__PURE__ */ new Date();
195
+ delegation.duration = this.calculateDuration(delegation);
196
+ try {
197
+ await this.client.session.abort({
198
+ path: { id: delegation.sessionID }
199
+ });
200
+ } catch {
201
+ }
202
+ const pendingSet = this.pendingByParent.get(delegation.parentSessionID);
203
+ if (pendingSet) {
204
+ pendingSet.delete(delegationId);
205
+ }
206
+ await this.notifyParent(delegation);
207
+ showToast(this.client, "Task Cancelled", `${delegation.id} cancelled (${delegation.duration})`, "info", 3e3);
208
+ await this.debugLog(`Cancelled delegation ${delegation.id}`);
209
+ return true;
210
+ }
211
+ async cancelAll(parentSessionID) {
212
+ const cancelled = [];
213
+ for (const delegation of this.delegations.values()) {
214
+ if (delegation.parentSessionID === parentSessionID && delegation.status === "running") {
215
+ const success = await this.cancel(delegation.id);
216
+ if (success) {
217
+ cancelled.push(delegation.id);
218
+ }
219
+ }
220
+ }
221
+ return cancelled;
222
+ }
223
+ // ---- Event handlers ----
224
+ async handleTimeout(delegationId) {
225
+ const delegation = this.delegations.get(delegationId);
226
+ if (!delegation || delegation.status !== "running") return;
227
+ await this.debugLog(`handleTimeout for delegation ${delegation.id}`);
228
+ delegation.status = "timeout";
229
+ delegation.completedAt = /* @__PURE__ */ new Date();
230
+ delegation.duration = this.calculateDuration(delegation);
231
+ delegation.error = `Delegation timed out after ${MAX_RUN_TIME_MS / 1e3}s`;
232
+ try {
233
+ await this.client.session.abort({
234
+ path: { id: delegation.sessionID }
235
+ });
236
+ } catch {
237
+ }
238
+ await this.notifyParent(delegation);
239
+ }
240
+ async handleSessionIdle(sessionID) {
241
+ const delegation = this.findBySession(sessionID);
242
+ if (!delegation || delegation.status !== "running") return;
243
+ await this.debugLog(`handleSessionIdle for delegation ${delegation.id}`);
244
+ delegation.status = "completed";
245
+ delegation.completedAt = /* @__PURE__ */ new Date();
246
+ delegation.duration = this.calculateDuration(delegation);
247
+ try {
248
+ const messages = await this.client.session.messages({
249
+ path: { id: delegation.sessionID }
250
+ });
251
+ const messageData = messages.data;
252
+ if (messageData && messageData.length > 0) {
253
+ const firstUser = messageData.find((m) => m.info.role === "user");
254
+ if (firstUser) {
255
+ const textPart = firstUser.parts.find((p) => p.type === "text");
256
+ if (textPart) {
257
+ delegation.description = textPart.text.slice(0, 150);
258
+ delegation.title = textPart.text.split("\n")[0].slice(0, 50);
259
+ }
260
+ }
261
+ }
262
+ } catch {
263
+ }
264
+ showToast(
265
+ this.client,
266
+ "Task Completed",
267
+ `"${delegation.id}" finished in ${delegation.duration}`,
268
+ "success",
269
+ 5e3
270
+ );
271
+ await this.notifyParent(delegation);
272
+ }
273
+ // ---- Read delegation results ----
274
+ async readDelegation(args) {
275
+ const delegation = this.delegations.get(args.id);
276
+ if (!delegation) {
277
+ throw new Error(`Delegation "${args.id}" not found.
278
+
279
+ Use delegation_list() to see available delegations.`);
280
+ }
281
+ if (delegation.status === "running") {
282
+ return `Delegation "${args.id}" is still running.
283
+
284
+ Status: ${delegation.status}
285
+ Started: ${delegation.startedAt.toISOString()}
286
+
287
+ Wait for completion notification, then call delegation_read() again.`;
288
+ }
289
+ if (delegation.status !== "completed") {
290
+ let statusMessage = `Delegation "${args.id}" ended with status: ${delegation.status}`;
291
+ if (delegation.error) statusMessage += `
292
+
293
+ Error: ${delegation.error}`;
294
+ if (delegation.duration) statusMessage += `
295
+
296
+ Duration: ${delegation.duration}`;
297
+ return statusMessage;
298
+ }
299
+ if (!args.mode || args.mode === "simple") {
300
+ return await this.getSimpleResult(delegation);
301
+ }
302
+ if (args.mode === "full") {
303
+ return await this.getFullSession(delegation, args);
304
+ }
305
+ return "Invalid mode. Use 'simple' or 'full'.";
306
+ }
307
+ async getSimpleResult(delegation) {
308
+ try {
309
+ const messages = await this.client.session.messages({
310
+ path: { id: delegation.sessionID }
311
+ });
312
+ const messageData = messages.data;
313
+ if (!messageData || messageData.length === 0) {
314
+ return `Delegation "${delegation.id}" completed but produced no output.`;
315
+ }
316
+ const assistantMessages = messageData.filter(
317
+ (m) => m.info.role === "assistant"
318
+ );
319
+ if (assistantMessages.length === 0) {
320
+ return `Delegation "${delegation.id}" completed but produced no assistant response.`;
321
+ }
322
+ const lastMessage = assistantMessages[assistantMessages.length - 1];
323
+ const textParts = lastMessage.parts.filter((p) => p.type === "text");
324
+ if (textParts.length === 0) {
325
+ return `Delegation "${delegation.id}" completed but produced no text content.`;
326
+ }
327
+ const result = textParts.map((p) => p.text).join("\n");
328
+ const header = `# Task Result: ${delegation.id}
329
+
330
+ **Agent:** ${delegation.agent}
331
+ **Status:** ${delegation.status}
332
+ **Duration:** ${delegation.duration || "N/A"}
333
+ **Started:** ${delegation.startedAt.toISOString()}
334
+ ${delegation.completedAt ? `**Completed:** ${delegation.completedAt.toISOString()}` : ""}
335
+
336
+ ---
337
+
338
+ `;
339
+ return header + result;
340
+ } catch (error) {
341
+ return `Error retrieving result: ${error instanceof Error ? error.message : "Unknown error"}`;
342
+ }
343
+ }
344
+ async getFullSession(delegation, args) {
345
+ try {
346
+ const messages = await this.client.session.messages({
347
+ path: { id: delegation.sessionID }
348
+ });
349
+ const messageData = messages.data;
350
+ if (!messageData || messageData.length === 0) {
351
+ return `Delegation "${delegation.id}" has no messages.`;
352
+ }
353
+ const sortedMessages = [...messageData].sort((a, b) => {
354
+ const timeA = String(a.info.time || "");
355
+ const timeB = String(b.info.time || "");
356
+ return timeA.localeCompare(timeB);
357
+ });
358
+ let filteredMessages = sortedMessages;
359
+ if (args.since_message_id) {
360
+ const index = sortedMessages.findIndex((m) => m.info.id === args.since_message_id);
361
+ if (index !== -1) {
362
+ filteredMessages = sortedMessages.slice(index + 1);
363
+ }
364
+ }
365
+ const limit = args.limit ? Math.min(args.limit, 100) : void 0;
366
+ const hasMore = limit !== void 0 && filteredMessages.length > limit;
367
+ const visibleMessages = limit !== void 0 ? filteredMessages.slice(0, limit) : filteredMessages;
368
+ const lines = [];
369
+ lines.push(`# Full Session: ${delegation.id}`);
370
+ lines.push("");
371
+ lines.push(`**Agent:** ${delegation.agent}`);
372
+ lines.push(`**Status:** ${delegation.status}`);
373
+ lines.push(`**Duration:** ${delegation.duration || "N/A"}`);
374
+ lines.push(`**Total messages:** ${sortedMessages.length}`);
375
+ lines.push(`**Returned:** ${visibleMessages.length}`);
376
+ lines.push(`**Has more:** ${hasMore ? "true" : "false"}`);
377
+ lines.push("");
378
+ lines.push("## Messages");
379
+ lines.push("");
380
+ for (const message of visibleMessages) {
381
+ const role = message.info.role;
382
+ const time = message.info.time || "unknown";
383
+ const id = message.info.id || "unknown";
384
+ lines.push(`### [${role}] ${time} (id: ${id})`);
385
+ lines.push("");
386
+ for (const part of message.parts) {
387
+ if (part.type === "text" && part.text) {
388
+ lines.push(part.text.trim());
389
+ lines.push("");
390
+ }
391
+ if (args.include_thinking && (part.type === "thinking" || part.type === "reasoning")) {
392
+ const thinkingText = part.thinking || part.text || "";
393
+ if (thinkingText) {
394
+ lines.push(`[thinking] ${thinkingText.slice(0, 2e3)}`);
395
+ lines.push("");
396
+ }
397
+ }
398
+ if (args.include_tools && part.type === "tool_result") {
399
+ const content = part.content || part.output || "";
400
+ if (content) {
401
+ lines.push(`[tool result] ${content}`);
402
+ lines.push("");
403
+ }
404
+ }
405
+ }
406
+ }
407
+ return lines.join("\n");
408
+ } catch (error) {
409
+ return `Error fetching session: ${error instanceof Error ? error.message : "Unknown error"}`;
410
+ }
411
+ }
412
+ // ---- Notification ----
413
+ async notifyParent(delegation) {
414
+ try {
415
+ const pendingSet = this.pendingByParent.get(delegation.parentSessionID);
416
+ if (pendingSet) {
417
+ pendingSet.delete(delegation.id);
418
+ }
419
+ const allComplete = !pendingSet || pendingSet.size === 0;
420
+ const remainingCount = pendingSet?.size || 0;
421
+ const statusText = delegation.status === "completed" ? "COMPLETED" : delegation.status === "cancelled" ? "CANCELLED" : delegation.status === "error" ? "ERROR" : delegation.status === "timeout" ? "TIMEOUT" : delegation.status.toUpperCase();
422
+ const duration = delegation.duration || "N/A";
423
+ const errorInfo = delegation.error ? `
424
+ **Error:** ${delegation.error}` : "";
425
+ let notification;
426
+ if (allComplete) {
427
+ const completedTasks = [];
428
+ for (const d of this.delegations.values()) {
429
+ if (d.parentSessionID === delegation.parentSessionID && d.status !== "running") {
430
+ completedTasks.push(d);
431
+ }
432
+ }
433
+ const completedList = completedTasks.map((t) => `- \`${t.id}\`: ${t.title || t.prompt.slice(0, 80)}`).join("\n");
434
+ const sessionHints = completedTasks.map((t) => `opencode -s ${t.id}`).join("\n");
435
+ notification = `<system-reminder>
436
+ [ALL BACKGROUND TASKS COMPLETE]
437
+
438
+ **Completed:**
439
+ ${completedList || `- \`${delegation.id}\`: ${delegation.title || delegation.prompt.slice(0, 80)}`}
440
+
441
+ Use \`delegation_read(id="<id>")\` to retrieve each result.
442
+ </system-reminder>
443
+ To inspect session content(human): ${sessionHints || `opencode -s ${delegation.id}`}`;
444
+ } else {
445
+ notification = `<system-reminder>
446
+ [BACKGROUND TASK ${statusText}]
447
+ **ID:** \`${delegation.id}\`
448
+ **Agent:** ${delegation.agent}
449
+ **Duration:** ${duration}${errorInfo}
450
+
451
+ **${remainingCount} task${remainingCount === 1 ? "" : "s"} still in progress.** You WILL be notified when ALL complete.
452
+ Do NOT poll - continue productive work.
453
+
454
+ Use \`delegation_read(id="${delegation.id}")\` to retrieve this result when ready.
455
+ </system-reminder>
456
+ To inspect session content(human): opencode -s ${delegation.id}`;
457
+ }
458
+ this.client.session.prompt({
459
+ path: { id: delegation.parentSessionID },
460
+ body: {
461
+ noReply: !allComplete,
462
+ agent: delegation.parentAgent,
463
+ parts: [{ type: "text", text: notification }]
464
+ }
465
+ }).catch(() => {
466
+ });
467
+ await this.debugLog(
468
+ `Notified parent session ${delegation.parentSessionID} (status=${statusText}, remaining=${remainingCount})`
469
+ );
470
+ } catch (error) {
471
+ await this.debugLog(
472
+ `Failed to notify parent: ${error instanceof Error ? error.message : "Unknown error"}`
473
+ );
474
+ }
475
+ }
476
+ // ---- Queries ----
477
+ async listDelegations(parentSessionID) {
478
+ const results = [];
479
+ for (const delegation of this.delegations.values()) {
480
+ if (delegation.parentSessionID === parentSessionID) {
481
+ results.push({
482
+ id: delegation.id,
483
+ status: delegation.status,
484
+ title: delegation.title,
485
+ description: delegation.description,
486
+ agent: delegation.agent,
487
+ duration: delegation.duration,
488
+ startedAt: delegation.startedAt
489
+ });
490
+ }
491
+ }
492
+ return results.sort((a, b) => (b.startedAt?.getTime() || 0) - (a.startedAt?.getTime() || 0));
493
+ }
494
+ listAllDelegations() {
495
+ const results = [];
496
+ for (const delegation of this.delegations.values()) {
497
+ results.push({
498
+ id: delegation.id,
499
+ status: delegation.status,
500
+ title: delegation.title,
501
+ description: delegation.description,
502
+ agent: delegation.agent,
503
+ duration: delegation.duration,
504
+ startedAt: delegation.startedAt
505
+ });
506
+ }
507
+ return results.sort((a, b) => (b.startedAt?.getTime() || 0) - (a.startedAt?.getTime() || 0));
508
+ }
509
+ findBySession(sessionID) {
510
+ return Array.from(this.delegations.values()).find((d) => d.sessionID === sessionID);
511
+ }
512
+ handleMessageEvent(sessionID, messageText) {
513
+ const delegation = this.findBySession(sessionID);
514
+ if (!delegation || delegation.status !== "running") return;
515
+ delegation.progress.lastUpdate = /* @__PURE__ */ new Date();
516
+ if (messageText) {
517
+ delegation.progress.lastMessage = messageText;
518
+ delegation.progress.lastMessageAt = /* @__PURE__ */ new Date();
519
+ }
520
+ }
521
+ getPendingCount(parentSessionID) {
522
+ const pendingSet = this.pendingByParent.get(parentSessionID);
523
+ return pendingSet ? pendingSet.size : 0;
524
+ }
525
+ getRunningDelegations() {
526
+ return Array.from(this.delegations.values()).filter((d) => d.status === "running");
527
+ }
528
+ async debugLog(msg) {
529
+ this.log.debug(msg);
530
+ }
531
+ };
532
+
533
+ // src/plugin/tools.ts
534
+ import { tool } from "@opencode-ai/plugin";
535
+ function createDelegate(manager) {
536
+ return tool({
537
+ description: `Delegate a task to an agent. Returns immediately with the session ID.
538
+
539
+ Use this for:
540
+ - Research tasks (will be auto-saved)
541
+ - Parallel work that can run in background
542
+ - Any task where you want persistent, retrievable output
543
+
544
+ On completion, a notification will arrive with the session ID, status, duration.
545
+ Use \`delegation_read\` with the session ID to retrieve the result.`,
546
+ args: {
547
+ prompt: tool.schema.string().describe("The full detailed prompt for the agent. Must be in English."),
548
+ agent: tool.schema.string().describe(
549
+ 'Agent to delegate to: "explore" (codebase search), "researcher" (external research), etc.'
550
+ ),
551
+ model: tool.schema.string().optional().describe(
552
+ 'Override model for this delegation. Format: "provider/model" (e.g. "minimax/MiniMax-M2.5"). If not set, uses the agent default.'
553
+ )
554
+ },
555
+ async execute(args, toolCtx) {
556
+ if (!toolCtx?.sessionID) {
557
+ return "\u274C delegate requires sessionID. This is a system error.";
558
+ }
559
+ if (!toolCtx?.messageID) {
560
+ return "\u274C delegate requires messageID. This is a system error.";
561
+ }
562
+ if (manager.findBySession(toolCtx.sessionID)) {
563
+ return "\u274C Sub-agents cannot delegate tasks. Only the main agent can use delegation tools.";
564
+ }
565
+ try {
566
+ const delegation = await manager.delegate({
567
+ parentSessionID: toolCtx.sessionID,
568
+ parentMessageID: toolCtx.messageID,
569
+ parentAgent: toolCtx.agent,
570
+ prompt: args.prompt,
571
+ agent: args.agent,
572
+ model: args.model
573
+ });
574
+ const pendingCount = manager.getPendingCount(toolCtx.sessionID);
575
+ let response = `Delegation started: ${delegation.id}
576
+ Agent: ${args.agent}`;
577
+ if (pendingCount > 1) {
578
+ response += `
579
+
580
+ ${pendingCount} delegations now active.`;
581
+ }
582
+ response += `
583
+
584
+ You WILL be notified via <system-reminder> when complete. Do NOT poll delegation_list().`;
585
+ return response;
586
+ } catch (error) {
587
+ return `\u274C Delegation failed:
588
+
589
+ ${error instanceof Error ? error.message : "Unknown error"}`;
590
+ }
591
+ }
592
+ });
593
+ }
594
+ function createDelegationRead(manager) {
595
+ return tool({
596
+ description: `Read the output of a delegation by its ID.
597
+
598
+ Modes:
599
+ - simple (default): Returns just the final result
600
+ - full: Returns all messages in the session with timestamps
601
+
602
+ Use filters to get specific parts of the conversation.`,
603
+ args: {
604
+ id: tool.schema.string().describe("The delegation session ID"),
605
+ mode: tool.schema.enum(["simple", "full"]).optional().describe("Output mode: 'simple' for result only, 'full' for all messages"),
606
+ include_thinking: tool.schema.boolean().optional().describe("Include thinking/reasoning blocks in full mode"),
607
+ include_tools: tool.schema.boolean().optional().describe("Include tool results in full mode"),
608
+ since_message_id: tool.schema.string().optional().describe("Return only messages after this message ID (full mode only)"),
609
+ limit: tool.schema.number().optional().describe("Max messages to return, capped at 100 (full mode only)")
610
+ },
611
+ async execute(args, toolCtx) {
612
+ if (!toolCtx?.sessionID) {
613
+ return "\u274C delegation_read requires sessionID. This is a system error.";
614
+ }
615
+ try {
616
+ return await manager.readDelegation(args);
617
+ } catch (error) {
618
+ return `\u274C Error reading delegation: ${error instanceof Error ? error.message : "Unknown error"}`;
619
+ }
620
+ }
621
+ });
622
+ }
623
+ function createDelegationList(manager) {
624
+ return tool({
625
+ description: `List all delegations for the current session.
626
+ Shows running, completed, cancelled, and error tasks with metadata.`,
627
+ args: {},
628
+ async execute(_args, toolCtx) {
629
+ if (!toolCtx?.sessionID) {
630
+ return "\u274C delegation_list requires sessionID. This is a system error.";
631
+ }
632
+ const delegations = await manager.listDelegations(toolCtx.sessionID);
633
+ if (delegations.length === 0) {
634
+ return "No delegations found for this session.";
635
+ }
636
+ const lines = delegations.map((d) => {
637
+ const titlePart = d.title ? ` | ${d.title}` : "";
638
+ const durationPart = d.duration ? ` (${d.duration})` : "";
639
+ const descPart = d.description ? `
640
+ \u2192 ${d.description.slice(0, 100)}${d.description.length > 100 ? "..." : ""}` : "";
641
+ return `- **${d.id}**${titlePart} [${d.status}]${durationPart}${descPart}`;
642
+ });
643
+ return `## Delegations
644
+
645
+ ${lines.join("\n")}`;
646
+ }
647
+ });
648
+ }
649
+ function createDelegationCancel(manager) {
650
+ return tool({
651
+ description: `Cancel a running delegation by ID, or cancel all running delegations.
652
+
653
+ Cancelled tasks can be resumed later with delegation_resume().`,
654
+ args: {
655
+ id: tool.schema.string().optional().describe("Task ID to cancel"),
656
+ all: tool.schema.boolean().optional().describe("Cancel ALL running delegations")
657
+ },
658
+ async execute(args, toolCtx) {
659
+ if (!toolCtx?.sessionID) {
660
+ return "\u274C delegation_cancel requires sessionID. This is a system error.";
661
+ }
662
+ try {
663
+ if (args.all) {
664
+ const cancelled = await manager.cancelAll(toolCtx.sessionID);
665
+ if (cancelled.length === 0) {
666
+ return "No running delegations to cancel.";
667
+ }
668
+ return `Cancelled ${cancelled.length} delegation(s):
669
+ ${cancelled.map((id) => `- ${id}`).join("\n")}`;
670
+ }
671
+ if (!args.id) {
672
+ return "\u274C Must provide either 'id' or 'all=true'";
673
+ }
674
+ const success = await manager.cancel(args.id);
675
+ if (!success) {
676
+ return `\u274C Could not cancel "${args.id}". Task may not exist or is not running.`;
677
+ }
678
+ return `\u2705 Cancelled delegation: ${args.id}
679
+
680
+ You can resume it later with delegation_resume(id="${args.id}")`;
681
+ } catch (error) {
682
+ return `\u274C Error cancelling: ${error instanceof Error ? error.message : "Unknown error"}`;
683
+ }
684
+ }
685
+ });
686
+ }
687
+ function createDelegationResume(manager) {
688
+ return tool({
689
+ description: `Resume a cancelled or errored delegation by sending a new prompt to the same session.
690
+
691
+ The agent will have access to the previous conversation context.`,
692
+ args: {
693
+ id: tool.schema.string().describe("Task ID to resume"),
694
+ prompt: tool.schema.string().optional().describe("Optional prompt to send (default: 'Continue from where you left off.')")
695
+ },
696
+ async execute(args, toolCtx) {
697
+ if (!toolCtx?.sessionID) {
698
+ return "\u274C delegation_resume requires sessionID. This is a system error.";
699
+ }
700
+ try {
701
+ const delegation = await manager.resume(args.id, args.prompt);
702
+ return `\u2705 Resumed delegation: ${delegation.id}
703
+ Agent: ${delegation.agent}
704
+ Status: ${delegation.status}`;
705
+ } catch (error) {
706
+ return `\u274C Error resuming: ${error instanceof Error ? error.message : "Unknown error"}`;
707
+ }
708
+ }
709
+ });
710
+ }
711
+
712
+ // src/plugin/rules.ts
713
+ var DELEGATION_RULES = `<system-reminder>
714
+ <delegation-system>
715
+
716
+ ## Async Delegation
717
+
718
+ You have tools for parallel background work:
719
+ - \`delegate(prompt, agent)\` - Launch task, returns ID immediately
720
+ - \`delegate(prompt, agent, model)\` - Launch task with specific model override
721
+ - \`delegation_read(id)\` - Retrieve completed result
722
+ - \`delegation_list()\` - List delegations (use sparingly)
723
+ - \`delegation_cancel(id|all)\` - Cancel running task(s)
724
+ - \`delegation_resume(id, prompt?)\` - Continue cancelled task (same session)
725
+
726
+ ## How It Works
727
+
728
+ 1. Call \`delegate()\` - Get task ID immediately, continue working
729
+ 2. Receive \`<system-reminder>\` notification when complete
730
+ 3. Call \`delegation_read(id)\` to get the actual result
731
+
732
+ ## Model Override
733
+
734
+ The \`delegate\` tool accepts an optional \`model\` parameter in "provider/model" format.
735
+ Example: \`delegate(prompt, agent, model="minimax/MiniMax-M2.5")\`
736
+ If not specified, the agent's default model is used.
737
+
738
+ ## Critical Constraints
739
+
740
+ **NEVER poll \`delegation_list\` to check completion.**
741
+ You WILL be notified via \`<system-reminder>\`. Polling wastes tokens.
742
+
743
+ **NEVER wait idle.** Always have productive work while delegations run.
744
+
745
+ **Cancelled tasks can be resumed** with \`delegation_resume()\` - same session, full context.
746
+
747
+ </delegation-system>
748
+ </system-reminder>`;
749
+ async function readBgAgentsConfig() {
750
+ const { homedir } = await import("os");
751
+ const { readFile } = await import("fs/promises");
752
+ const { join } = await import("path");
753
+ const configDir = join(homedir(), ".config", "opencode");
754
+ for (const name of ["async-agents.md", "async-agent.md"]) {
755
+ try {
756
+ return await readFile(join(configDir, name), "utf-8");
757
+ } catch {
758
+ }
759
+ }
760
+ return "";
761
+ }
762
+ function formatDelegationContext(running, completed) {
763
+ const sections = ["<delegation-context>"];
764
+ if (running.length > 0) {
765
+ sections.push("## Running Delegations");
766
+ sections.push("");
767
+ for (const d of running) {
768
+ sections.push(`### \`${d.id}\`${d.agent ? ` (${d.agent})` : ""}`);
769
+ if (d.startedAt) {
770
+ sections.push(`**Started:** ${d.startedAt.toISOString()}`);
771
+ }
772
+ sections.push("");
773
+ }
774
+ sections.push(
775
+ "> **Note:** You WILL be notified via `<system-reminder>` when delegations complete."
776
+ );
777
+ sections.push("> Do NOT poll `delegation_list` - continue productive work.");
778
+ sections.push("");
779
+ }
780
+ if (completed.length > 0) {
781
+ sections.push("## Recent Completed Delegations");
782
+ sections.push("");
783
+ for (const d of completed) {
784
+ const statusEmoji = d.status === "completed" ? "\u2705" : d.status === "error" ? "\u274C" : d.status === "timeout" ? "\u23F1\uFE0F" : "\u{1F6AB}";
785
+ sections.push(`### ${statusEmoji} \`${d.id}\``);
786
+ sections.push(`**Status:** ${d.status}`);
787
+ if (d.duration) sections.push(`**Duration:** ${d.duration}`);
788
+ sections.push("");
789
+ }
790
+ sections.push("> Use `delegation_list()` to see all delegations.");
791
+ sections.push("");
792
+ }
793
+ sections.push("## Retrieval");
794
+ sections.push('Use `delegation_read("id")` to access results.');
795
+ sections.push('Use `delegation_read(id, mode="full")` for full conversation.');
796
+ sections.push("</delegation-context>");
797
+ return sections.join("\n");
798
+ }
799
+
800
+ // src/plugin/plugin.ts
801
+ var delegationCommand = "delegation";
802
+ var AsyncAgentPlugin = async (ctx) => {
803
+ const { client } = ctx;
804
+ const log = createLogger(client);
805
+ const manager = new DelegationManager(client, log);
806
+ await manager.debugLog("AsyncAgentPlugin initialized");
807
+ return {
808
+ // Handle /delegation slash command execution
809
+ "command.execute.before": async (input) => {
810
+ if (input.command !== delegationCommand) return;
811
+ const typedClient = client;
812
+ let childSessions = [];
813
+ try {
814
+ const result = await typedClient.session.children({
815
+ path: { id: input.sessionID }
816
+ });
817
+ childSessions = result.data ?? [];
818
+ } catch {
819
+ }
820
+ const delegationSessions = childSessions.filter(
821
+ (s) => s.title?.startsWith("Delegation:")
822
+ );
823
+ const inMemory = manager.listAllDelegations();
824
+ const inMemoryMap = new Map(inMemory.map((d) => [d.id, d]));
825
+ let message;
826
+ if (delegationSessions.length === 0 && inMemory.length === 0) {
827
+ message = "No delegations found for this session.";
828
+ } else {
829
+ const lines = [];
830
+ const seen = /* @__PURE__ */ new Set();
831
+ const entries = [];
832
+ for (const s of delegationSessions) {
833
+ seen.add(s.id);
834
+ const mem = inMemoryMap.get(s.id);
835
+ const agent = mem?.agent ?? s.title?.replace("Delegation: ", "") ?? "unknown";
836
+ const status = mem?.status?.toUpperCase() ?? "PERSISTED";
837
+ const duration = mem?.duration ?? "\u2014";
838
+ const created = s.time?.created ? new Date(s.time.created).toISOString() : mem?.startedAt?.toISOString() ?? "\u2014";
839
+ entries.push({
840
+ id: s.id,
841
+ title: mem?.title ?? s.title ?? "\u2014",
842
+ agent,
843
+ status,
844
+ duration,
845
+ started: created
846
+ });
847
+ }
848
+ for (const d of inMemory) {
849
+ if (seen.has(d.id)) continue;
850
+ entries.push({
851
+ id: d.id,
852
+ title: d.title ?? d.description?.slice(0, 60) ?? "\u2014",
853
+ agent: d.agent ?? "unknown",
854
+ status: d.status?.toUpperCase() ?? "UNKNOWN",
855
+ duration: d.duration ?? "\u2014",
856
+ started: d.startedAt?.toISOString() ?? "\u2014"
857
+ });
858
+ }
859
+ lines.push(`## Delegations (${entries.length})
860
+ `);
861
+ for (const e of entries) {
862
+ lines.push(`**[${e.id}]** ${e.title}`);
863
+ lines.push(` Status: ${e.status} | Agent: ${e.agent} | Duration: ${e.duration}`);
864
+ lines.push(` Started: ${e.started}`);
865
+ lines.push(` \`opencode -s ${e.id}\``);
866
+ lines.push("");
867
+ }
868
+ message = lines.join("\n");
869
+ }
870
+ await typedClient.session.prompt({
871
+ path: { id: input.sessionID },
872
+ body: {
873
+ noReply: true,
874
+ parts: [{ type: "text", text: message }]
875
+ }
876
+ });
877
+ throw new Error("Command handled by async-agent plugin");
878
+ },
879
+ tool: {
880
+ delegate: createDelegate(manager),
881
+ delegation_read: createDelegationRead(manager),
882
+ delegation_list: createDelegationList(manager),
883
+ delegation_cancel: createDelegationCancel(manager),
884
+ delegation_resume: createDelegationResume(manager)
885
+ },
886
+ // Register /delegation slash command
887
+ config: async (input) => {
888
+ if (!input.command) input.command = {};
889
+ input.command[delegationCommand] = {
890
+ template: "Show all background delegation sessions with their status, IDs, agents, and metadata.",
891
+ description: "List all background delegations"
892
+ };
893
+ },
894
+ // Inject delegation rules + async-agent.md config into system prompt
895
+ "experimental.chat.system.transform": async (_input, output) => {
896
+ output.system.push(DELEGATION_RULES);
897
+ const bgConfig = await readBgAgentsConfig();
898
+ if (bgConfig.trim()) {
899
+ output.system.push(`<async-agent-config>
900
+ ${bgConfig}
901
+ </async-agent-config>`);
902
+ }
903
+ },
904
+ // Inject active delegation context during session compaction
905
+ "experimental.session.compacting": async (_input, output) => {
906
+ const running = manager.getRunningDelegations().map((d) => ({
907
+ id: d.id,
908
+ agent: d.agent,
909
+ status: d.status,
910
+ startedAt: d.startedAt
911
+ }));
912
+ if (running.length > 0) {
913
+ output.context.push(formatDelegationContext(running, []));
914
+ }
915
+ },
916
+ event: async (input) => {
917
+ const { event } = input;
918
+ if (event.type === "session.idle") {
919
+ const sessionID = event.properties?.sessionID;
920
+ const delegation = manager.findBySession(sessionID);
921
+ if (delegation) {
922
+ await manager.handleSessionIdle(sessionID);
923
+ }
924
+ }
925
+ }
926
+ };
927
+ };
928
+ var plugin_default = AsyncAgentPlugin;
929
+ export {
930
+ AsyncAgentPlugin,
931
+ plugin_default as default
932
+ };