opencode-async-agent 1.0.0 → 1.0.2

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,1289 @@
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
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
42
+ import { join } from "path";
43
+ function parseModel(model) {
44
+ const [providerID, ...rest] = model.split("/");
45
+ return { providerID, modelID: rest.join("/") };
46
+ }
47
+ var ANALYSIS_PROMPT = `You are a session analyst. Analyze the following AI task execution comprehensively so the main agent can make informed next decisions.
48
+
49
+ ## Analysis Criteria
50
+
51
+ ### 1. Anything AI Missed Based on Initial Prompt
52
+ - Compare the original user prompt against what was actually accomplished
53
+ - Identify any requirements, questions, or requests that were never addressed
54
+ - List promises made by the agent that were left unfulfilled
55
+
56
+ ### 2. Wrong Doings
57
+ - Identify incorrect assumptions or bad approaches taken
58
+ - Note any factual errors or wrong technical decisions
59
+ - Call out misinterpretations of the original prompt
60
+
61
+ ### 3. Gave Up / Shortcuts
62
+ - Did the agent abandon parts of the task prematurely?
63
+ - Were steps skipped or incomplete solutions used?
64
+ - Did the agent stop without exhausting reasonable options?
65
+ - Any signs of "good enough" attitude instead of thorough completion?
66
+
67
+ ### 4. Messed Up
68
+ - Did the agent break existing functionality?
69
+ - Were new problems introduced during the task?
70
+ - Any destructive actions or unintended side effects?
71
+
72
+ ### 5. Good Points / Choices
73
+ - What technical decisions were sound and should be replicated?
74
+ - What approaches worked well that future tasks should follow?
75
+ - Notable strengths in this session's execution
76
+
77
+ ### 6. Session Ended Properly or Stream Cut Out
78
+ - **Proper finish:** Agent concluded with clear result or summary
79
+ - **Stream cut out:** Session interrupted mid-task with no conclusion
80
+ - **Ambiguous end:** Final state unclear or incomplete explanation
81
+
82
+ ### 7. Overall Status on the Session
83
+ - Give the main agent a complete picture of what happened
84
+ - Was this session successful, partial, or a failure?
85
+ - Is the output reliable enough to base next decisions on?
86
+
87
+ ## Output Format
88
+
89
+ Provide your analysis in **markdown** with these exact sections:
90
+
91
+ ### Summary
92
+ [2-3 sentence summary of what happened]
93
+
94
+ ### What the AI Missed Based on Initial Prompt
95
+ [List anything not covered from the original prompt]
96
+
97
+ ### Wrong Doings
98
+ [Incorrect assumptions, bad approaches, factual errors]
99
+
100
+ ### Gave Up / Shortcuts
101
+ [Premature abandonment, skipped steps, incomplete solutions]
102
+
103
+ ### Messed Up
104
+ [Broke things, created new problems, unintended side effects]
105
+
106
+ ### Good Points / Choices
107
+ [Sound decisions, approaches worth replicating, notable strengths]
108
+
109
+ ### Session Completion
110
+ - **Status:** [Proper finish / Stream cut out / Ambiguous]
111
+ - **Details:** [explanation of how the session ended]
112
+
113
+ ### Overall Status
114
+ [Complete assessment: Is this session's output reliable for next decisions? What's the verdict?]
115
+
116
+ ### Next Action for Main Agent
117
+ [Specific recommendation on what the main agent should do next based on this session's outcome]
118
+
119
+ ---
120
+
121
+ ## Session Data
122
+
123
+ ### Initial User Prompt
124
+ \`\`\`
125
+ \${initialPrompt}
126
+ \`\`\`
127
+
128
+ ### Full Conversation
129
+ \`\`\`
130
+ \${formattedMessages}
131
+ \`\`\`
132
+
133
+ ### Session Metadata
134
+ - Agent: \${agent}
135
+ - Model: \${model}
136
+ - Duration: \${duration}
137
+ - Status: \${status}
138
+ - Started: \${startTime}
139
+ - Completed: \${completedTime}
140
+ `;
141
+ var DelegationManager = class {
142
+ delegations = /* @__PURE__ */ new Map();
143
+ client;
144
+ log;
145
+ pendingByParent = /* @__PURE__ */ new Map();
146
+ constructor(client, log) {
147
+ this.client = client;
148
+ this.log = log;
149
+ }
150
+ calculateDuration(delegation) {
151
+ return formatDuration(delegation.startedAt, delegation.completedAt);
152
+ }
153
+ // ---- Parent session model ----
154
+ async getParentModel(parentSessionID) {
155
+ try {
156
+ const messagesResult = await this.client.session.messages({
157
+ path: { id: parentSessionID }
158
+ });
159
+ const messageData = messagesResult.data;
160
+ if (!messageData || messageData.length === 0) {
161
+ return null;
162
+ }
163
+ const lastUserMessage = [...messageData].reverse().find((m) => m.info.role === "user");
164
+ if (!lastUserMessage || !lastUserMessage.info.model) {
165
+ return null;
166
+ }
167
+ const model = lastUserMessage.info.model;
168
+ const modelString = `${model.providerID}/${model.modelID}`;
169
+ await this.debugLog(`Got parent model: ${modelString}`);
170
+ return modelString;
171
+ } catch (error) {
172
+ this.log.debug(`Failed to get parent model: ${error instanceof Error ? error.message : "Unknown error"}`);
173
+ return null;
174
+ }
175
+ }
176
+ // ---- Core operations ----
177
+ async delegate(input) {
178
+ await this.debugLog(`delegate() called`);
179
+ const agentsResult = await this.client.app.agents({});
180
+ const agents = agentsResult.data ?? [];
181
+ const validAgent = agents.find((a) => a.name === input.agent);
182
+ if (!validAgent) {
183
+ const available = agents.filter((a) => a.mode === "subagent" || a.mode === "all" || !a.mode).map((a) => `\u2022 ${a.name}${a.description ? ` - ${a.description}` : ""}`).join("\n");
184
+ throw new Error(
185
+ `Agent "${input.agent}" not found.
186
+
187
+ Available agents:
188
+ ${available || "(none)"}`
189
+ );
190
+ }
191
+ const sessionResult = await this.client.session.create({
192
+ body: {
193
+ title: `Delegation: ${input.agent}`,
194
+ parentID: input.parentSessionID
195
+ }
196
+ });
197
+ await this.debugLog(`session.create result: ${JSON.stringify(sessionResult.data)}`);
198
+ if (!sessionResult.data?.id) {
199
+ throw new Error("Failed to create delegation session");
200
+ }
201
+ const sessionID = sessionResult.data.id;
202
+ const parentModel = await this.getParentModel(input.parentSessionID);
203
+ const delegation = {
204
+ id: sessionID,
205
+ sessionID,
206
+ parentSessionID: input.parentSessionID,
207
+ parentMessageID: input.parentMessageID,
208
+ parentAgent: input.parentAgent,
209
+ parentModel,
210
+ prompt: input.prompt,
211
+ agent: input.agent,
212
+ model: input.model,
213
+ status: "running",
214
+ startedAt: /* @__PURE__ */ new Date(),
215
+ progress: {
216
+ toolCalls: 0,
217
+ lastUpdate: /* @__PURE__ */ new Date()
218
+ }
219
+ };
220
+ await this.debugLog(`Created delegation ${delegation.id}`);
221
+ this.delegations.set(delegation.id, delegation);
222
+ const parentId = input.parentSessionID;
223
+ if (!this.pendingByParent.has(parentId)) {
224
+ this.pendingByParent.set(parentId, /* @__PURE__ */ new Set());
225
+ }
226
+ this.pendingByParent.get(parentId)?.add(delegation.id);
227
+ await this.debugLog(
228
+ `Tracking delegation ${delegation.id} for parent ${parentId}. Pending count: ${this.pendingByParent.get(parentId)?.size}`
229
+ );
230
+ setTimeout(() => {
231
+ const current = this.delegations.get(delegation.id);
232
+ if (current && current.status === "running") {
233
+ this.handleTimeout(delegation.id);
234
+ }
235
+ }, MAX_RUN_TIME_MS + 5e3);
236
+ showToast(this.client, "New Background Task", `${delegation.id} (${input.agent})`, "info", 3e3);
237
+ const promptBody = {
238
+ agent: input.agent,
239
+ parts: [{ type: "text", text: input.prompt }],
240
+ tools: {
241
+ task: false,
242
+ delegate: false,
243
+ todowrite: false,
244
+ plan_save: false
245
+ }
246
+ };
247
+ if (input.model) {
248
+ promptBody.model = parseModel(input.model);
249
+ }
250
+ this.client.session.prompt({
251
+ path: { id: delegation.sessionID },
252
+ body: promptBody
253
+ }).catch((error) => {
254
+ delegation.status = "error";
255
+ delegation.error = error.message;
256
+ delegation.completedAt = /* @__PURE__ */ new Date();
257
+ delegation.duration = this.calculateDuration(delegation);
258
+ this.notifyParent(delegation);
259
+ });
260
+ return delegation;
261
+ }
262
+ async resume(delegationId, newPrompt) {
263
+ const delegation = this.delegations.get(delegationId);
264
+ if (!delegation) {
265
+ throw new Error(`Delegation "${delegationId}" not found`);
266
+ }
267
+ if (delegation.status === "running") {
268
+ throw new Error(`Delegation is already running. Wait for it to complete or cancel it first.`);
269
+ }
270
+ delegation.status = "running";
271
+ delegation.completedAt = void 0;
272
+ delegation.error = void 0;
273
+ delegation.startedAt = /* @__PURE__ */ new Date();
274
+ delegation.progress = {
275
+ toolCalls: 0,
276
+ lastUpdate: /* @__PURE__ */ new Date()
277
+ };
278
+ const parentId = delegation.parentSessionID;
279
+ if (!this.pendingByParent.has(parentId)) {
280
+ this.pendingByParent.set(parentId, /* @__PURE__ */ new Set());
281
+ }
282
+ this.pendingByParent.get(parentId)?.add(delegation.id);
283
+ const prompt = newPrompt || "Continue from where you left off.";
284
+ const resumeBody = {
285
+ agent: delegation.agent,
286
+ parts: [{ type: "text", text: prompt }],
287
+ tools: {
288
+ task: false,
289
+ delegate: false,
290
+ todowrite: false,
291
+ plan_save: false
292
+ }
293
+ };
294
+ if (delegation.model) {
295
+ resumeBody.model = parseModel(delegation.model);
296
+ }
297
+ this.client.session.prompt({
298
+ path: { id: delegation.sessionID },
299
+ body: resumeBody
300
+ }).catch((error) => {
301
+ delegation.status = "error";
302
+ delegation.error = error.message;
303
+ delegation.completedAt = /* @__PURE__ */ new Date();
304
+ delegation.duration = this.calculateDuration(delegation);
305
+ this.notifyParent(delegation);
306
+ });
307
+ await this.debugLog(`Resumed delegation ${delegation.id}`);
308
+ return delegation;
309
+ }
310
+ async cancel(delegationId) {
311
+ const delegation = this.delegations.get(delegationId);
312
+ if (!delegation) return false;
313
+ if (delegation.status !== "running") return false;
314
+ delegation.status = "cancelled";
315
+ delegation.completedAt = /* @__PURE__ */ new Date();
316
+ delegation.duration = this.calculateDuration(delegation);
317
+ try {
318
+ await this.client.session.abort({
319
+ path: { id: delegation.sessionID }
320
+ });
321
+ } catch {
322
+ }
323
+ const pendingSet = this.pendingByParent.get(delegation.parentSessionID);
324
+ if (pendingSet) {
325
+ pendingSet.delete(delegationId);
326
+ }
327
+ await this.notifyParent(delegation);
328
+ showToast(this.client, "Task Cancelled", `${delegation.id} cancelled (${delegation.duration})`, "info", 3e3);
329
+ await this.debugLog(`Cancelled delegation ${delegation.id}`);
330
+ return true;
331
+ }
332
+ async cancelAll(parentSessionID) {
333
+ const cancelled = [];
334
+ for (const delegation of this.delegations.values()) {
335
+ if (delegation.parentSessionID === parentSessionID && delegation.status === "running") {
336
+ const success = await this.cancel(delegation.id);
337
+ if (success) {
338
+ cancelled.push(delegation.id);
339
+ }
340
+ }
341
+ }
342
+ return cancelled;
343
+ }
344
+ // ---- Event handlers ----
345
+ async handleTimeout(delegationId) {
346
+ const delegation = this.delegations.get(delegationId);
347
+ if (!delegation || delegation.status !== "running") return;
348
+ await this.debugLog(`handleTimeout for delegation ${delegation.id}`);
349
+ delegation.status = "timeout";
350
+ delegation.completedAt = /* @__PURE__ */ new Date();
351
+ delegation.duration = this.calculateDuration(delegation);
352
+ delegation.error = `Delegation timed out after ${MAX_RUN_TIME_MS / 1e3}s`;
353
+ try {
354
+ await this.client.session.abort({
355
+ path: { id: delegation.sessionID }
356
+ });
357
+ } catch {
358
+ }
359
+ await this.notifyParent(delegation);
360
+ }
361
+ async handleSessionIdle(sessionID) {
362
+ const delegation = this.findBySession(sessionID);
363
+ if (!delegation || delegation.status !== "running") return;
364
+ await this.debugLog(`handleSessionIdle for delegation ${delegation.id}`);
365
+ delegation.status = "completed";
366
+ delegation.completedAt = /* @__PURE__ */ new Date();
367
+ delegation.duration = this.calculateDuration(delegation);
368
+ try {
369
+ const messages = await this.client.session.messages({
370
+ path: { id: delegation.sessionID }
371
+ });
372
+ const messageData = messages.data;
373
+ if (messageData && messageData.length > 0) {
374
+ const firstUser = messageData.find((m) => m.info.role === "user");
375
+ if (firstUser) {
376
+ const textPart = firstUser.parts.find((p) => p.type === "text");
377
+ if (textPart) {
378
+ delegation.description = textPart.text.slice(0, 150);
379
+ delegation.title = textPart.text.split("\n")[0].slice(0, 50);
380
+ }
381
+ }
382
+ }
383
+ } catch {
384
+ }
385
+ showToast(
386
+ this.client,
387
+ "Task Completed",
388
+ `"${delegation.id}" finished in ${delegation.duration}`,
389
+ "success",
390
+ 5e3
391
+ );
392
+ await this.notifyParent(delegation);
393
+ }
394
+ // ---- Read delegation results ----
395
+ async readDelegation(args) {
396
+ const delegation = this.delegations.get(args.id);
397
+ if (!delegation) {
398
+ throw new Error(`Delegation "${args.id}" not found.
399
+
400
+ Use delegation_list() to see available delegations.`);
401
+ }
402
+ if (delegation.status === "running") {
403
+ if (args.ai) {
404
+ return `Delegation "${args.id}" is still running.
405
+
406
+ Status: ${delegation.status}
407
+ Started: ${delegation.startedAt.toISOString()}
408
+
409
+ Wait for completion notification, then call delegation_read() again. AI analysis only available for completed sessions.`;
410
+ }
411
+ return `Delegation "${args.id}" is still running.
412
+
413
+ Status: ${delegation.status}
414
+ Started: ${delegation.startedAt.toISOString()}
415
+
416
+ Wait for completion notification, then call delegation_read() again.`;
417
+ }
418
+ if (delegation.status !== "completed") {
419
+ let statusMessage = `Delegation "${args.id}" ended with status: ${delegation.status}`;
420
+ if (delegation.error) statusMessage += `
421
+
422
+ Error: ${delegation.error}`;
423
+ if (delegation.duration) statusMessage += `
424
+
425
+ Duration: ${delegation.duration}`;
426
+ return statusMessage;
427
+ }
428
+ if (args.ai) {
429
+ let model = args.ai_model;
430
+ if (!model) {
431
+ model = delegation.parentModel || await this.getDefaultModel();
432
+ if (!model) {
433
+ return "\u274C ai_model required when ai=true and no default model configured (parent session has no model)";
434
+ }
435
+ }
436
+ return await this.analyzeSessionWithAI(delegation, model);
437
+ }
438
+ if (!args.mode || args.mode === "simple") {
439
+ return await this.getSimpleResult(delegation);
440
+ }
441
+ if (args.mode === "full") {
442
+ return await this.getFullSession(delegation, args);
443
+ }
444
+ return "Invalid mode. Use 'simple' or 'full'.";
445
+ }
446
+ async getSimpleResult(delegation) {
447
+ try {
448
+ const messages = await this.client.session.messages({
449
+ path: { id: delegation.sessionID }
450
+ });
451
+ const messageData = messages.data;
452
+ if (!messageData || messageData.length === 0) {
453
+ return `Delegation "${delegation.id}" completed but produced no output.`;
454
+ }
455
+ const assistantMessages = messageData.filter(
456
+ (m) => m.info.role === "assistant"
457
+ );
458
+ if (assistantMessages.length === 0) {
459
+ return `Delegation "${delegation.id}" completed but produced no assistant response.`;
460
+ }
461
+ const lastMessage = assistantMessages[assistantMessages.length - 1];
462
+ const textParts = lastMessage.parts.filter((p) => p.type === "text");
463
+ if (textParts.length === 0) {
464
+ return `Delegation "${delegation.id}" completed but produced no text content.`;
465
+ }
466
+ const result = textParts.map((p) => p.text).join("\n");
467
+ const header = `# Task Result: ${delegation.id}
468
+
469
+ **Agent:** ${delegation.agent}
470
+ **Status:** ${delegation.status}
471
+ **Duration:** ${delegation.duration || "N/A"}
472
+ **Started:** ${delegation.startedAt.toISOString()}
473
+ ${delegation.completedAt ? `**Completed:** ${delegation.completedAt.toISOString()}` : ""}
474
+
475
+ ---
476
+
477
+ `;
478
+ return header + result;
479
+ } catch (error) {
480
+ return `Error retrieving result: ${error instanceof Error ? error.message : "Unknown error"}`;
481
+ }
482
+ }
483
+ async getFullSession(delegation, args) {
484
+ try {
485
+ const messages = await this.client.session.messages({
486
+ path: { id: delegation.sessionID }
487
+ });
488
+ const messageData = messages.data;
489
+ if (!messageData || messageData.length === 0) {
490
+ return `Delegation "${delegation.id}" has no messages.`;
491
+ }
492
+ const sortedMessages = [...messageData].sort((a, b) => {
493
+ const timeA = String(a.info.time || "");
494
+ const timeB = String(b.info.time || "");
495
+ return timeA.localeCompare(timeB);
496
+ });
497
+ let filteredMessages = sortedMessages;
498
+ if (args.since_message_id) {
499
+ const index = sortedMessages.findIndex((m) => m.info.id === args.since_message_id);
500
+ if (index !== -1) {
501
+ filteredMessages = sortedMessages.slice(index + 1);
502
+ }
503
+ }
504
+ const limit = args.limit ? Math.min(args.limit, 100) : void 0;
505
+ const hasMore = limit !== void 0 && filteredMessages.length > limit;
506
+ const visibleMessages = limit !== void 0 ? filteredMessages.slice(0, limit) : filteredMessages;
507
+ const lines = [];
508
+ lines.push(`# Full Session: ${delegation.id}`);
509
+ lines.push("");
510
+ lines.push(`**Agent:** ${delegation.agent}`);
511
+ lines.push(`**Status:** ${delegation.status}`);
512
+ lines.push(`**Duration:** ${delegation.duration || "N/A"}`);
513
+ lines.push(`**Total messages:** ${sortedMessages.length}`);
514
+ lines.push(`**Returned:** ${visibleMessages.length}`);
515
+ lines.push(`**Has more:** ${hasMore ? "true" : "false"}`);
516
+ lines.push("");
517
+ lines.push("## Messages");
518
+ lines.push("");
519
+ for (const message of visibleMessages) {
520
+ const role = message.info.role;
521
+ const time = message.info.time || "unknown";
522
+ const id = message.info.id || "unknown";
523
+ lines.push(`### [${role}] ${time} (id: ${id})`);
524
+ lines.push("");
525
+ for (const part of message.parts) {
526
+ if (part.type === "text" && part.text) {
527
+ lines.push(part.text.trim());
528
+ lines.push("");
529
+ }
530
+ if (args.include_thinking && (part.type === "thinking" || part.type === "reasoning")) {
531
+ const thinkingText = part.thinking || part.text || "";
532
+ if (thinkingText) {
533
+ lines.push(`[thinking] ${thinkingText.slice(0, 2e3)}`);
534
+ lines.push("");
535
+ }
536
+ }
537
+ if (args.include_tools && part.type === "tool_result") {
538
+ const content = part.content || part.output || "";
539
+ if (content) {
540
+ lines.push(`[tool result] ${content}`);
541
+ lines.push("");
542
+ }
543
+ }
544
+ }
545
+ }
546
+ return lines.join("\n");
547
+ } catch (error) {
548
+ return `Error fetching session: ${error instanceof Error ? error.message : "Unknown error"}`;
549
+ }
550
+ }
551
+ // ---- Notification ----
552
+ async notifyParent(delegation) {
553
+ try {
554
+ const pendingSet = this.pendingByParent.get(delegation.parentSessionID);
555
+ if (pendingSet) {
556
+ pendingSet.delete(delegation.id);
557
+ }
558
+ const allComplete = !pendingSet || pendingSet.size === 0;
559
+ const remainingCount = pendingSet?.size || 0;
560
+ const statusText = delegation.status === "completed" ? "COMPLETED" : delegation.status === "cancelled" ? "CANCELLED" : delegation.status === "error" ? "ERROR" : delegation.status === "timeout" ? "TIMEOUT" : delegation.status.toUpperCase();
561
+ const duration = delegation.duration || "N/A";
562
+ const errorInfo = delegation.error ? `
563
+ **Error:** ${delegation.error}` : "";
564
+ let notification;
565
+ if (allComplete) {
566
+ const completedTasks = [];
567
+ for (const d of this.delegations.values()) {
568
+ if (d.parentSessionID === delegation.parentSessionID && d.status !== "running") {
569
+ completedTasks.push(d);
570
+ }
571
+ }
572
+ const completedList = completedTasks.map((t) => `- \`${t.id}\`: ${t.title || t.prompt.slice(0, 80)}`).join("\n");
573
+ const sessionHints = completedTasks.map((t) => `opencode -s ${t.id}`).join("\n");
574
+ notification = `<system-reminder>
575
+ [ALL BACKGROUND TASKS COMPLETE]
576
+
577
+ **Completed:**
578
+ ${completedList || `- \`${delegation.id}\`: ${delegation.title || delegation.prompt.slice(0, 80)}`}
579
+
580
+ Use \`delegation_read(id="<id>")\` to retrieve each result.
581
+ </system-reminder>
582
+ To inspect session content(human): ${sessionHints || `opencode -s ${delegation.id}`}`;
583
+ } else {
584
+ notification = `<system-reminder>
585
+ [BACKGROUND TASK ${statusText}]
586
+ **ID:** \`${delegation.id}\`
587
+ **Agent:** ${delegation.agent}
588
+ **Duration:** ${duration}${errorInfo}
589
+
590
+ **${remainingCount} task${remainingCount === 1 ? "" : "s"} still in progress.** You WILL be notified when ALL complete.
591
+ Do NOT poll - continue productive work.
592
+
593
+ Use \`delegation_read(id="${delegation.id}")\` to retrieve this result when ready.
594
+ </system-reminder>
595
+ To inspect session content(human): opencode -s ${delegation.id}`;
596
+ }
597
+ this.client.session.prompt({
598
+ path: { id: delegation.parentSessionID },
599
+ body: {
600
+ noReply: !allComplete,
601
+ agent: delegation.parentAgent,
602
+ parts: [{ type: "text", text: notification }]
603
+ }
604
+ }).catch(() => {
605
+ });
606
+ await this.debugLog(
607
+ `Notified parent session ${delegation.parentSessionID} (status=${statusText}, remaining=${remainingCount})`
608
+ );
609
+ } catch (error) {
610
+ await this.debugLog(
611
+ `Failed to notify parent: ${error instanceof Error ? error.message : "Unknown error"}`
612
+ );
613
+ }
614
+ }
615
+ // ---- Queries ----
616
+ async listDelegations(parentSessionID) {
617
+ const results = [];
618
+ for (const delegation of this.delegations.values()) {
619
+ if (delegation.parentSessionID === parentSessionID) {
620
+ results.push({
621
+ id: delegation.id,
622
+ status: delegation.status,
623
+ title: delegation.title,
624
+ description: delegation.description,
625
+ agent: delegation.agent,
626
+ duration: delegation.duration,
627
+ startedAt: delegation.startedAt
628
+ });
629
+ }
630
+ }
631
+ return results.sort((a, b) => (b.startedAt?.getTime() || 0) - (a.startedAt?.getTime() || 0));
632
+ }
633
+ listAllDelegations() {
634
+ const results = [];
635
+ for (const delegation of this.delegations.values()) {
636
+ results.push({
637
+ id: delegation.id,
638
+ status: delegation.status,
639
+ title: delegation.title,
640
+ description: delegation.description,
641
+ agent: delegation.agent,
642
+ duration: delegation.duration,
643
+ startedAt: delegation.startedAt
644
+ });
645
+ }
646
+ return results.sort((a, b) => (b.startedAt?.getTime() || 0) - (a.startedAt?.getTime() || 0));
647
+ }
648
+ findBySession(sessionID) {
649
+ return Array.from(this.delegations.values()).find((d) => d.sessionID === sessionID);
650
+ }
651
+ handleMessageEvent(sessionID, messageText) {
652
+ const delegation = this.findBySession(sessionID);
653
+ if (!delegation || delegation.status !== "running") return;
654
+ delegation.progress.lastUpdate = /* @__PURE__ */ new Date();
655
+ if (messageText) {
656
+ delegation.progress.lastMessage = messageText;
657
+ delegation.progress.lastMessageAt = /* @__PURE__ */ new Date();
658
+ }
659
+ }
660
+ getPendingCount(parentSessionID) {
661
+ const pendingSet = this.pendingByParent.get(parentSessionID);
662
+ return pendingSet ? pendingSet.size : 0;
663
+ }
664
+ getRunningDelegations() {
665
+ return Array.from(this.delegations.values()).filter((d) => d.status === "running");
666
+ }
667
+ async debugLog(msg) {
668
+ this.log.debug(msg);
669
+ }
670
+ // ---- AI Analysis ----
671
+ async getDefaultModel() {
672
+ try {
673
+ const result = await this.client.config.get();
674
+ const config = result.data;
675
+ return config?.model || null;
676
+ } catch (error) {
677
+ this.log.debug(`Failed to get default model: ${error instanceof Error ? error.message : "Unknown error"}`);
678
+ return null;
679
+ }
680
+ }
681
+ getModelInfo(modelId) {
682
+ const [provider, ...rest] = modelId.split("/");
683
+ if (!provider) {
684
+ throw new Error(`Invalid model format: "${modelId}". Expected "provider/model"`);
685
+ }
686
+ const modelIdOnly = rest.join("/");
687
+ try {
688
+ const modelJsonPath = join(process.env.HOME || "", ".cache", "opencode", "models.json");
689
+ const content = readFileSync(modelJsonPath, "utf-8");
690
+ const modelsData = JSON.parse(content);
691
+ if (!modelsData[provider]) {
692
+ throw new Error(`Provider "${provider}" not found in models.json`);
693
+ }
694
+ const providerData = modelsData[provider];
695
+ const apiUrl = providerData.api || providerData.baseUrl;
696
+ if (!apiUrl) {
697
+ throw new Error(`No API URL found for provider "${provider}"`);
698
+ }
699
+ return { provider, apiUrl, modelId: modelIdOnly };
700
+ } catch (error) {
701
+ if (error instanceof Error) throw error;
702
+ throw new Error(`Failed to parse models.json: ${error}`);
703
+ }
704
+ }
705
+ getApiKey(provider) {
706
+ try {
707
+ const authJsonPath = join(process.env.HOME || "", ".local", "share", "opencode", "auth.json");
708
+ const content = readFileSync(authJsonPath, "utf-8");
709
+ const authData = JSON.parse(content);
710
+ if (!authData[provider]) {
711
+ throw new Error(`Provider "${provider}" not found in auth.json`);
712
+ }
713
+ const providerAuth = authData[provider];
714
+ if (providerAuth.type !== "api") {
715
+ throw new Error(`Provider "${provider}" is not an API key type`);
716
+ }
717
+ return providerAuth.key;
718
+ } catch (error) {
719
+ if (error instanceof Error) throw error;
720
+ throw new Error(`Failed to parse auth.json: ${error}`);
721
+ }
722
+ }
723
+ formatSessionForAI(messages, delegation) {
724
+ let initialPrompt = delegation.prompt;
725
+ const parts = [];
726
+ for (const msg of messages) {
727
+ const role = msg.info.role.toUpperCase();
728
+ const timestamp = msg.info.time?.created ? new Date(msg.info.time.created).toISOString() : "unknown";
729
+ parts.push(`[${role}] ${timestamp}`);
730
+ for (const part of msg.parts) {
731
+ switch (part.type) {
732
+ case "text":
733
+ if (part.text) {
734
+ parts.push(part.text.trim());
735
+ }
736
+ break;
737
+ case "reasoning":
738
+ case "thinking":
739
+ const thinkingText = part.thinking || part.text || "";
740
+ if (thinkingText) {
741
+ parts.push(`[REASONING] ${thinkingText.slice(0, 2e3)}`);
742
+ }
743
+ break;
744
+ case "tool":
745
+ if (part.state) {
746
+ const toolInput = part.state.status === "pending" || part.state.status === "running" ? JSON.stringify(part.state.input || {}) : JSON.stringify(part.state.input || {});
747
+ parts.push(`[TOOL CALL] ${part.tool}: ${toolInput}`);
748
+ }
749
+ break;
750
+ case "tool_result":
751
+ const content = part.content || part.output || "";
752
+ parts.push(`[TOOL RESULT] ${content}`);
753
+ break;
754
+ case "file":
755
+ parts.push(`[FILE] ${part.filename || "unknown file"} (${part.mime})`);
756
+ break;
757
+ case "patch":
758
+ parts.push(`[PATCH] Code diff applied`);
759
+ break;
760
+ case "snapshot":
761
+ parts.push(`[SNAPSHOT] State snapshot`);
762
+ break;
763
+ case "agent":
764
+ parts.push(`[AGENT] Switched to: ${part.name || "unknown"}`);
765
+ break;
766
+ default:
767
+ parts.push(`[${part.type}] ${JSON.stringify(part).slice(0, 200)}`);
768
+ }
769
+ }
770
+ parts.push("");
771
+ }
772
+ return `# Full Conversation
773
+
774
+ ${parts.join("\n")}`;
775
+ }
776
+ async callAIForAnalysis(apiUrl, apiKey, model, prompt, timeoutMs = 6e4) {
777
+ const controller = new AbortController();
778
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
779
+ try {
780
+ const url = apiUrl.endsWith("/") ? `${apiUrl}chat/completions` : `${apiUrl}/chat/completions`;
781
+ const response = await fetch(url, {
782
+ method: "POST",
783
+ headers: {
784
+ "Authorization": `Bearer ${apiKey}`,
785
+ "Content-Type": "application/json"
786
+ },
787
+ body: JSON.stringify({
788
+ model,
789
+ messages: [{ role: "user", content: prompt }],
790
+ max_tokens: 4e3,
791
+ temperature: 0.3
792
+ }),
793
+ signal: controller.signal
794
+ });
795
+ clearTimeout(timeoutId);
796
+ if (!response.ok) {
797
+ const errorText = await response.text();
798
+ throw new Error(`API request failed: ${response.status} ${response.statusText}
799
+ ${errorText}`);
800
+ }
801
+ const data = await response.json();
802
+ if (!data.choices || !data.choices[0] || !data.choices[0].message) {
803
+ throw new Error("Invalid API response format");
804
+ }
805
+ return data.choices[0].message.content;
806
+ } catch (error) {
807
+ if (error.name === "AbortError") {
808
+ throw new Error("AI analysis timed out after 60 seconds");
809
+ }
810
+ throw error;
811
+ }
812
+ }
813
+ logAnalysis(delegationId, model, result, error, duration) {
814
+ if (process.env.OC_ASYNC_DEBUG !== "true") {
815
+ return;
816
+ }
817
+ try {
818
+ const logDir = join(process.env.HOME || "", ".cache", "opencode-delegation-ai");
819
+ if (!existsSync(logDir)) {
820
+ mkdirSync(logDir, { recursive: true });
821
+ }
822
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
823
+ const filename = `analysis-${delegationId}-${timestamp}.json`;
824
+ const logEntry = {
825
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
826
+ delegationId,
827
+ model,
828
+ status: error ? "error" : "success",
829
+ durationMs: duration,
830
+ result: error ? void 0 : result,
831
+ error: error || void 0
832
+ };
833
+ writeFileSync(join(logDir, filename), JSON.stringify(logEntry, null, 2));
834
+ } catch (err) {
835
+ this.log.debug(`Failed to log analysis: ${err}`);
836
+ }
837
+ }
838
+ async analyzeSessionWithAI(delegation, model) {
839
+ const startTime = Date.now();
840
+ try {
841
+ const modelInfo = this.getModelInfo(model);
842
+ const apiKey = this.getApiKey(modelInfo.provider);
843
+ const messagesResult = await this.client.session.messages({
844
+ path: { id: delegation.sessionID }
845
+ });
846
+ const messageData = messagesResult.data;
847
+ if (!messageData || messageData.length === 0) {
848
+ return `Delegation "${delegation.id}" has no messages to analyze.`;
849
+ }
850
+ const formattedMessages = this.formatSessionForAI(messageData, delegation);
851
+ const initialPrompt = messageData.find((m) => m.info.role === "user")?.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n") || delegation.prompt;
852
+ const sessionMetadata = `
853
+ ### Session Metadata
854
+ - Agent: ${delegation.agent}
855
+ - Model: ${delegation.model || "unknown"}
856
+ - Duration: ${delegation.duration || "N/A"}
857
+ - Status: ${delegation.status}
858
+ - Started: ${delegation.startedAt.toISOString()}
859
+ - Completed: ${delegation.completedAt?.toISOString() || "N/A"}
860
+ `;
861
+ const fullPrompt = ANALYSIS_PROMPT.replace("${initialPrompt}", initialPrompt).replace("${formattedMessages}", formattedMessages).replace("${agent}", delegation.agent).replace("${model}", delegation.model || "unknown").replace("${duration}", delegation.duration || "N/A").replace("${status}", delegation.status).replace("${startTime}", delegation.startedAt.toISOString()).replace("${completedTime}", delegation.completedAt?.toISOString() || "N/A");
862
+ const analysis = await this.callAIForAnalysis(modelInfo.apiUrl, apiKey, modelInfo.modelId, fullPrompt);
863
+ const duration = Date.now() - startTime;
864
+ this.logAnalysis(delegation.id, model, analysis, void 0, duration);
865
+ const header = `# AI Analysis for Delegation: ${delegation.id}
866
+
867
+ **Agent:** ${delegation.agent}
868
+ **Analysis Model:** ${model}
869
+ **Duration:** ${delegation.duration || "N/A"}
870
+ **Analysis Time:** ${(duration / 1e3).toFixed(2)}s
871
+
872
+ ---
873
+
874
+ `;
875
+ return header + analysis;
876
+ } catch (error) {
877
+ const duration = Date.now() - startTime;
878
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
879
+ this.logAnalysis(delegation.id, model, "", errorMessage, duration);
880
+ return `\u274C AI analysis failed:
881
+
882
+ ${errorMessage}`;
883
+ }
884
+ }
885
+ };
886
+
887
+ // src/plugin/tools.ts
888
+ import { tool } from "@opencode-ai/plugin";
889
+ function createDelegate(manager) {
890
+ return tool({
891
+ description: `Delegate a task to an agent. Returns immediately with the session ID.
892
+
893
+ Use this for:
894
+ - Research tasks (will be auto-saved)
895
+ - Parallel work that can run in background
896
+ - Any task where you want persistent, retrievable output
897
+
898
+ On completion, a notification will arrive with the session ID, status, duration.
899
+ Use \`delegation_read\` with the session ID to retrieve the result.`,
900
+ args: {
901
+ prompt: tool.schema.string().describe("The full detailed prompt for the agent. Must be in English."),
902
+ agent: tool.schema.string().describe(
903
+ 'Agent to delegate to: "explore" (codebase search), "researcher" (external research), etc.'
904
+ ),
905
+ model: tool.schema.string().optional().describe(
906
+ 'Override model for this delegation. Format: "provider/model" (e.g. "minimax/MiniMax-M2.5"). If not set, uses the agent default.'
907
+ )
908
+ },
909
+ async execute(args, toolCtx) {
910
+ if (!toolCtx?.sessionID) {
911
+ return "\u274C delegate requires sessionID. This is a system error.";
912
+ }
913
+ if (!toolCtx?.messageID) {
914
+ return "\u274C delegate requires messageID. This is a system error.";
915
+ }
916
+ if (manager.findBySession(toolCtx.sessionID)) {
917
+ return "\u274C Sub-agents cannot delegate tasks. Only the main agent can use delegation tools.";
918
+ }
919
+ try {
920
+ const delegation = await manager.delegate({
921
+ parentSessionID: toolCtx.sessionID,
922
+ parentMessageID: toolCtx.messageID,
923
+ parentAgent: toolCtx.agent,
924
+ prompt: args.prompt,
925
+ agent: args.agent,
926
+ model: args.model
927
+ });
928
+ const pendingCount = manager.getPendingCount(toolCtx.sessionID);
929
+ let response = `Delegation started: ${delegation.id}
930
+ Agent: ${args.agent}`;
931
+ if (pendingCount > 1) {
932
+ response += `
933
+
934
+ ${pendingCount} delegations now active.`;
935
+ }
936
+ response += `
937
+
938
+ You WILL be notified via <system-reminder> when complete. Do NOT poll delegation_list().`;
939
+ return response;
940
+ } catch (error) {
941
+ return `\u274C Delegation failed:
942
+
943
+ ${error instanceof Error ? error.message : "Unknown error"}`;
944
+ }
945
+ }
946
+ });
947
+ }
948
+ function createDelegationRead(manager) {
949
+ return tool({
950
+ description: `Read the output of a delegation by its ID.
951
+
952
+ Modes:
953
+ - simple (default): Returns just the final result
954
+ - full: Returns all messages in the session with timestamps
955
+ - ai (requires ai=true): Use AI to analyze and summarize the entire session execution
956
+
957
+ Use filters to get specific parts of the conversation.`,
958
+ args: {
959
+ id: tool.schema.string().describe("The delegation session ID"),
960
+ mode: tool.schema.enum(["simple", "full"]).optional().describe("Output mode: 'simple' for result only, 'full' for all messages"),
961
+ include_thinking: tool.schema.boolean().optional().describe("Include thinking/reasoning blocks in full mode"),
962
+ include_tools: tool.schema.boolean().optional().describe("Include tool results in full mode"),
963
+ since_message_id: tool.schema.string().optional().describe("Return only messages after this message ID (full mode only)"),
964
+ limit: tool.schema.number().optional().describe("Max messages to return, capped at 100 (full mode only)"),
965
+ ai: tool.schema.boolean().optional().describe("Use AI to analyze and summarize the session"),
966
+ ai_model: tool.schema.string().optional().describe("Model for AI analysis (e.g. 'minimax/MiniMax-M2.5'). Required when ai=true if no default model configured")
967
+ },
968
+ async execute(args, toolCtx) {
969
+ if (!toolCtx?.sessionID) {
970
+ return "\u274C delegation_read requires sessionID. This is a system error.";
971
+ }
972
+ try {
973
+ return await manager.readDelegation(args);
974
+ } catch (error) {
975
+ return `\u274C Error reading delegation: ${error instanceof Error ? error.message : "Unknown error"}`;
976
+ }
977
+ }
978
+ });
979
+ }
980
+ function createDelegationList(manager) {
981
+ return tool({
982
+ description: `List all delegations for the current session.
983
+ Shows running, completed, cancelled, and error tasks with metadata.`,
984
+ args: {},
985
+ async execute(_args, toolCtx) {
986
+ if (!toolCtx?.sessionID) {
987
+ return "\u274C delegation_list requires sessionID. This is a system error.";
988
+ }
989
+ const delegations = await manager.listDelegations(toolCtx.sessionID);
990
+ if (delegations.length === 0) {
991
+ return "No delegations found for this session.";
992
+ }
993
+ const lines = delegations.map((d) => {
994
+ const titlePart = d.title ? ` | ${d.title}` : "";
995
+ const durationPart = d.duration ? ` (${d.duration})` : "";
996
+ const descPart = d.description ? `
997
+ \u2192 ${d.description.slice(0, 100)}${d.description.length > 100 ? "..." : ""}` : "";
998
+ return `- **${d.id}**${titlePart} [${d.status}]${durationPart}${descPart}`;
999
+ });
1000
+ return `## Delegations
1001
+
1002
+ ${lines.join("\n")}`;
1003
+ }
1004
+ });
1005
+ }
1006
+ function createDelegationCancel(manager) {
1007
+ return tool({
1008
+ description: `Cancel a running delegation by ID, or cancel all running delegations.
1009
+
1010
+ Cancelled tasks can be resumed later with delegation_resume().`,
1011
+ args: {
1012
+ id: tool.schema.string().optional().describe("Task ID to cancel"),
1013
+ all: tool.schema.boolean().optional().describe("Cancel ALL running delegations")
1014
+ },
1015
+ async execute(args, toolCtx) {
1016
+ if (!toolCtx?.sessionID) {
1017
+ return "\u274C delegation_cancel requires sessionID. This is a system error.";
1018
+ }
1019
+ try {
1020
+ if (args.all) {
1021
+ const cancelled = await manager.cancelAll(toolCtx.sessionID);
1022
+ if (cancelled.length === 0) {
1023
+ return "No running delegations to cancel.";
1024
+ }
1025
+ return `Cancelled ${cancelled.length} delegation(s):
1026
+ ${cancelled.map((id) => `- ${id}`).join("\n")}`;
1027
+ }
1028
+ if (!args.id) {
1029
+ return "\u274C Must provide either 'id' or 'all=true'";
1030
+ }
1031
+ const success = await manager.cancel(args.id);
1032
+ if (!success) {
1033
+ return `\u274C Could not cancel "${args.id}". Task may not exist or is not running.`;
1034
+ }
1035
+ return `\u2705 Cancelled delegation: ${args.id}
1036
+
1037
+ You can resume it later with delegation_resume(id="${args.id}")`;
1038
+ } catch (error) {
1039
+ return `\u274C Error cancelling: ${error instanceof Error ? error.message : "Unknown error"}`;
1040
+ }
1041
+ }
1042
+ });
1043
+ }
1044
+ function createDelegationResume(manager) {
1045
+ return tool({
1046
+ description: `Resume a cancelled or errored delegation by sending a new prompt to the same session.
1047
+
1048
+ The agent will have access to the previous conversation context.`,
1049
+ args: {
1050
+ id: tool.schema.string().describe("Task ID to resume"),
1051
+ prompt: tool.schema.string().optional().describe("Optional prompt to send (default: 'Continue from where you left off.')")
1052
+ },
1053
+ async execute(args, toolCtx) {
1054
+ if (!toolCtx?.sessionID) {
1055
+ return "\u274C delegation_resume requires sessionID. This is a system error.";
1056
+ }
1057
+ try {
1058
+ const delegation = await manager.resume(args.id, args.prompt);
1059
+ return `\u2705 Resumed delegation: ${delegation.id}
1060
+ Agent: ${delegation.agent}
1061
+ Status: ${delegation.status}`;
1062
+ } catch (error) {
1063
+ return `\u274C Error resuming: ${error instanceof Error ? error.message : "Unknown error"}`;
1064
+ }
1065
+ }
1066
+ });
1067
+ }
1068
+
1069
+ // src/plugin/rules.ts
1070
+ var DELEGATION_RULES = `<system-reminder>
1071
+ <delegation-system>
1072
+
1073
+ ## Async Delegation
1074
+
1075
+ You have tools for parallel background work:
1076
+ - \`delegate(prompt, agent)\` - Launch task, returns ID immediately
1077
+ - \`delegate(prompt, agent, model)\` - Launch task with specific model override
1078
+ - \`delegation_read(id)\` - Retrieve completed result
1079
+ - \`delegation_list()\` - List delegations (use sparingly)
1080
+ - \`delegation_cancel(id|all)\` - Cancel running task(s)
1081
+ - \`delegation_resume(id, prompt?)\` - Continue cancelled task (same session)
1082
+
1083
+ ## How It Works
1084
+
1085
+ 1. Call \`delegate()\` - Get task ID immediately, continue working
1086
+ 2. Receive \`<system-reminder>\` notification when complete
1087
+ 3. Call \`delegation_read(id)\` to get the actual result
1088
+
1089
+ ## Model Override
1090
+
1091
+ The \`delegate\` tool accepts an optional \`model\` parameter in "provider/model" format.
1092
+ Example: \`delegate(prompt, agent, model="minimax/MiniMax-M2.5")\`
1093
+ If not specified, the agent's default model is used.
1094
+
1095
+ ## Critical Constraints
1096
+
1097
+ **NEVER poll \`delegation_list\` to check completion.**
1098
+ You WILL be notified via \`<system-reminder>\`. Polling wastes tokens.
1099
+
1100
+ **NEVER wait idle.** Always have productive work while delegations run.
1101
+
1102
+ **Cancelled tasks can be resumed** with \`delegation_resume()\` - same session, full context.
1103
+
1104
+ </delegation-system>
1105
+ </system-reminder>`;
1106
+ async function readBgAgentsConfig() {
1107
+ const { homedir } = await import("os");
1108
+ const { readFile } = await import("fs/promises");
1109
+ const { join: join2 } = await import("path");
1110
+ const configDir = join2(homedir(), ".config", "opencode");
1111
+ for (const name of ["async-agents.md", "async-agent.md"]) {
1112
+ try {
1113
+ return await readFile(join2(configDir, name), "utf-8");
1114
+ } catch {
1115
+ }
1116
+ }
1117
+ return "";
1118
+ }
1119
+ function formatDelegationContext(running, completed) {
1120
+ const sections = ["<delegation-context>"];
1121
+ if (running.length > 0) {
1122
+ sections.push("## Running Delegations");
1123
+ sections.push("");
1124
+ for (const d of running) {
1125
+ sections.push(`### \`${d.id}\`${d.agent ? ` (${d.agent})` : ""}`);
1126
+ if (d.startedAt) {
1127
+ sections.push(`**Started:** ${d.startedAt.toISOString()}`);
1128
+ }
1129
+ sections.push("");
1130
+ }
1131
+ sections.push(
1132
+ "> **Note:** You WILL be notified via `<system-reminder>` when delegations complete."
1133
+ );
1134
+ sections.push("> Do NOT poll `delegation_list` - continue productive work.");
1135
+ sections.push("");
1136
+ }
1137
+ if (completed.length > 0) {
1138
+ sections.push("## Recent Completed Delegations");
1139
+ sections.push("");
1140
+ for (const d of completed) {
1141
+ const statusEmoji = d.status === "completed" ? "\u2705" : d.status === "error" ? "\u274C" : d.status === "timeout" ? "\u23F1\uFE0F" : "\u{1F6AB}";
1142
+ sections.push(`### ${statusEmoji} \`${d.id}\``);
1143
+ sections.push(`**Status:** ${d.status}`);
1144
+ if (d.duration) sections.push(`**Duration:** ${d.duration}`);
1145
+ sections.push("");
1146
+ }
1147
+ sections.push("> Use `delegation_list()` to see all delegations.");
1148
+ sections.push("");
1149
+ }
1150
+ sections.push("## Retrieval");
1151
+ sections.push('Use `delegation_read("id")` to access results.');
1152
+ sections.push('Use `delegation_read(id, mode="full")` for full conversation.');
1153
+ sections.push("</delegation-context>");
1154
+ return sections.join("\n");
1155
+ }
1156
+
1157
+ // src/plugin/plugin.ts
1158
+ var delegationCommand = "delegation";
1159
+ var AsyncAgentPlugin = async (ctx) => {
1160
+ const { client } = ctx;
1161
+ const log = createLogger(client);
1162
+ const manager = new DelegationManager(client, log);
1163
+ await manager.debugLog("AsyncAgentPlugin initialized");
1164
+ return {
1165
+ // Handle /delegation slash command execution
1166
+ "command.execute.before": async (input) => {
1167
+ if (input.command !== delegationCommand) return;
1168
+ const typedClient = client;
1169
+ let childSessions = [];
1170
+ try {
1171
+ const result = await typedClient.session.children({
1172
+ path: { id: input.sessionID }
1173
+ });
1174
+ childSessions = result.data ?? [];
1175
+ } catch {
1176
+ }
1177
+ const delegationSessions = childSessions.filter(
1178
+ (s) => s.title?.startsWith("Delegation:")
1179
+ );
1180
+ const inMemory = manager.listAllDelegations();
1181
+ const inMemoryMap = new Map(inMemory.map((d) => [d.id, d]));
1182
+ let message;
1183
+ if (delegationSessions.length === 0 && inMemory.length === 0) {
1184
+ message = "No delegations found for this session.";
1185
+ } else {
1186
+ const lines = [];
1187
+ const seen = /* @__PURE__ */ new Set();
1188
+ const entries = [];
1189
+ for (const s of delegationSessions) {
1190
+ seen.add(s.id);
1191
+ const mem = inMemoryMap.get(s.id);
1192
+ const agent = mem?.agent ?? s.title?.replace("Delegation: ", "") ?? "unknown";
1193
+ const status = mem?.status?.toUpperCase() ?? "PERSISTED";
1194
+ const duration = mem?.duration ?? "\u2014";
1195
+ const created = s.time?.created ? new Date(s.time.created).toISOString() : mem?.startedAt?.toISOString() ?? "\u2014";
1196
+ entries.push({
1197
+ id: s.id,
1198
+ title: mem?.title ?? s.title ?? "\u2014",
1199
+ agent,
1200
+ status,
1201
+ duration,
1202
+ started: created
1203
+ });
1204
+ }
1205
+ for (const d of inMemory) {
1206
+ if (seen.has(d.id)) continue;
1207
+ entries.push({
1208
+ id: d.id,
1209
+ title: d.title ?? d.description?.slice(0, 60) ?? "\u2014",
1210
+ agent: d.agent ?? "unknown",
1211
+ status: d.status?.toUpperCase() ?? "UNKNOWN",
1212
+ duration: d.duration ?? "\u2014",
1213
+ started: d.startedAt?.toISOString() ?? "\u2014"
1214
+ });
1215
+ }
1216
+ lines.push(`## Delegations (${entries.length})
1217
+ `);
1218
+ for (const e of entries) {
1219
+ lines.push(`**[${e.id}]** ${e.title}`);
1220
+ lines.push(` Status: ${e.status} | Agent: ${e.agent} | Duration: ${e.duration}`);
1221
+ lines.push(` Started: ${e.started}`);
1222
+ lines.push(` \`opencode -s ${e.id}\``);
1223
+ lines.push("");
1224
+ }
1225
+ message = lines.join("\n");
1226
+ }
1227
+ await typedClient.session.prompt({
1228
+ path: { id: input.sessionID },
1229
+ body: {
1230
+ noReply: true,
1231
+ parts: [{ type: "text", text: message }]
1232
+ }
1233
+ });
1234
+ throw new Error("Command handled by async-agent plugin");
1235
+ },
1236
+ tool: {
1237
+ delegate: createDelegate(manager),
1238
+ delegation_read: createDelegationRead(manager),
1239
+ delegation_list: createDelegationList(manager),
1240
+ delegation_cancel: createDelegationCancel(manager),
1241
+ delegation_resume: createDelegationResume(manager)
1242
+ },
1243
+ // Register /delegation slash command
1244
+ config: async (input) => {
1245
+ if (!input.command) input.command = {};
1246
+ input.command[delegationCommand] = {
1247
+ template: "Show all background delegation sessions with their status, IDs, agents, and metadata.",
1248
+ description: "List all background delegations"
1249
+ };
1250
+ },
1251
+ // Inject delegation rules + async-agent.md config into system prompt
1252
+ "experimental.chat.system.transform": async (_input, output) => {
1253
+ output.system.push(DELEGATION_RULES);
1254
+ const bgConfig = await readBgAgentsConfig();
1255
+ if (bgConfig.trim()) {
1256
+ output.system.push(`<async-agent-config>
1257
+ ${bgConfig}
1258
+ </async-agent-config>`);
1259
+ }
1260
+ },
1261
+ // Inject active delegation context during session compaction
1262
+ "experimental.session.compacting": async (_input, output) => {
1263
+ const running = manager.getRunningDelegations().map((d) => ({
1264
+ id: d.id,
1265
+ agent: d.agent,
1266
+ status: d.status,
1267
+ startedAt: d.startedAt
1268
+ }));
1269
+ if (running.length > 0) {
1270
+ output.context.push(formatDelegationContext(running, []));
1271
+ }
1272
+ },
1273
+ event: async (input) => {
1274
+ const { event } = input;
1275
+ if (event.type === "session.idle") {
1276
+ const sessionID = event.properties?.sessionID;
1277
+ const delegation = manager.findBySession(sessionID);
1278
+ if (delegation) {
1279
+ await manager.handleSessionIdle(sessionID);
1280
+ }
1281
+ }
1282
+ }
1283
+ };
1284
+ };
1285
+ var plugin_default = AsyncAgentPlugin;
1286
+ export {
1287
+ AsyncAgentPlugin,
1288
+ plugin_default as default
1289
+ };