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.
- package/README.md +25 -7
- package/dist/async-agent.js +932 -0
- package/package.json +3 -1
- package/AGENTS.md +0 -119
- package/src/plugin/manager.ts +0 -630
- package/src/plugin/plugin.ts +0 -200
- package/src/plugin/rules.ts +0 -115
- package/src/plugin/tools.ts +0 -230
- package/src/plugin/types.ts +0 -80
- package/src/plugin/utils.ts +0 -51
package/src/plugin/manager.ts
DELETED
|
@@ -1,630 +0,0 @@
|
|
|
1
|
-
import type { TextPart } from "@opencode-ai/sdk"
|
|
2
|
-
import {
|
|
3
|
-
MAX_RUN_TIME_MS,
|
|
4
|
-
type OpencodeClient,
|
|
5
|
-
type Logger,
|
|
6
|
-
type Delegation,
|
|
7
|
-
type DelegateInput,
|
|
8
|
-
type DelegationListItem,
|
|
9
|
-
type ReadDelegationArgs,
|
|
10
|
-
type SessionMessageItem,
|
|
11
|
-
type AssistantSessionMessageItem,
|
|
12
|
-
} from "./types"
|
|
13
|
-
import { showToast, formatDuration } from "./utils"
|
|
14
|
-
|
|
15
|
-
// Same logic as OpenCode's Provider.parseModel() — first "/" splits provider from model
|
|
16
|
-
function parseModel(model: string): { providerID: string; modelID: string } {
|
|
17
|
-
const [providerID, ...rest] = model.split("/")
|
|
18
|
-
return { providerID, modelID: rest.join("/") }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class DelegationManager {
|
|
22
|
-
private delegations: Map<string, Delegation> = new Map()
|
|
23
|
-
private client: OpencodeClient
|
|
24
|
-
private log: Logger
|
|
25
|
-
private pendingByParent: Map<string, Set<string>> = new Map()
|
|
26
|
-
|
|
27
|
-
constructor(client: OpencodeClient, log: Logger) {
|
|
28
|
-
this.client = client
|
|
29
|
-
this.log = log
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private calculateDuration(delegation: Delegation): string {
|
|
33
|
-
return formatDuration(delegation.startedAt, delegation.completedAt)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ---- Core operations ----
|
|
37
|
-
|
|
38
|
-
async delegate(input: DelegateInput): Promise<Delegation> {
|
|
39
|
-
await this.debugLog(`delegate() called`)
|
|
40
|
-
|
|
41
|
-
// Validate agent exists
|
|
42
|
-
const agentsResult = await this.client.app.agents({})
|
|
43
|
-
const agents = (agentsResult.data ?? []) as {
|
|
44
|
-
name: string
|
|
45
|
-
description?: string
|
|
46
|
-
mode?: string
|
|
47
|
-
}[]
|
|
48
|
-
const validAgent = agents.find((a) => a.name === input.agent)
|
|
49
|
-
|
|
50
|
-
if (!validAgent) {
|
|
51
|
-
const available = agents
|
|
52
|
-
.filter((a) => a.mode === "subagent" || a.mode === "all" || !a.mode)
|
|
53
|
-
.map((a) => `• ${a.name}${a.description ? ` - ${a.description}` : ""}`)
|
|
54
|
-
.join("\n")
|
|
55
|
-
|
|
56
|
-
throw new Error(
|
|
57
|
-
`Agent "${input.agent}" not found.\n\nAvailable agents:\n${available || "(none)"}`,
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Create isolated session — its ID becomes the delegation ID
|
|
62
|
-
const sessionResult = await this.client.session.create({
|
|
63
|
-
body: {
|
|
64
|
-
title: `Delegation: ${input.agent}`,
|
|
65
|
-
parentID: input.parentSessionID,
|
|
66
|
-
},
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
await this.debugLog(`session.create result: ${JSON.stringify(sessionResult.data)}`)
|
|
70
|
-
|
|
71
|
-
if (!sessionResult.data?.id) {
|
|
72
|
-
throw new Error("Failed to create delegation session")
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Use OpenCode session ID as the delegation ID
|
|
76
|
-
const sessionID = sessionResult.data.id
|
|
77
|
-
|
|
78
|
-
const delegation: Delegation = {
|
|
79
|
-
id: sessionID,
|
|
80
|
-
sessionID: sessionID,
|
|
81
|
-
parentSessionID: input.parentSessionID,
|
|
82
|
-
parentMessageID: input.parentMessageID,
|
|
83
|
-
parentAgent: input.parentAgent,
|
|
84
|
-
prompt: input.prompt,
|
|
85
|
-
agent: input.agent,
|
|
86
|
-
model: input.model,
|
|
87
|
-
status: "running",
|
|
88
|
-
startedAt: new Date(),
|
|
89
|
-
progress: {
|
|
90
|
-
toolCalls: 0,
|
|
91
|
-
lastUpdate: new Date(),
|
|
92
|
-
},
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
await this.debugLog(`Created delegation ${delegation.id}`)
|
|
96
|
-
this.delegations.set(delegation.id, delegation)
|
|
97
|
-
|
|
98
|
-
// Track for batched notification
|
|
99
|
-
const parentId = input.parentSessionID
|
|
100
|
-
if (!this.pendingByParent.has(parentId)) {
|
|
101
|
-
this.pendingByParent.set(parentId, new Set())
|
|
102
|
-
}
|
|
103
|
-
this.pendingByParent.get(parentId)?.add(delegation.id)
|
|
104
|
-
await this.debugLog(
|
|
105
|
-
`Tracking delegation ${delegation.id} for parent ${parentId}. Pending count: ${this.pendingByParent.get(parentId)?.size}`,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
// Timeout timer
|
|
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 + 5000)
|
|
115
|
-
|
|
116
|
-
// Toast: task launched
|
|
117
|
-
showToast(this.client, "New Background Task", `${delegation.id} (${input.agent})`, "info", 3000)
|
|
118
|
-
|
|
119
|
-
// Fire the prompt — optionally override model if specified
|
|
120
|
-
const promptBody: any = {
|
|
121
|
-
agent: input.agent,
|
|
122
|
-
parts: [{ type: "text", text: input.prompt }],
|
|
123
|
-
tools: {
|
|
124
|
-
task: false,
|
|
125
|
-
delegate: false,
|
|
126
|
-
todowrite: false,
|
|
127
|
-
plan_save: false,
|
|
128
|
-
},
|
|
129
|
-
}
|
|
130
|
-
if (input.model) {
|
|
131
|
-
promptBody.model = parseModel(input.model)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
this.client.session
|
|
135
|
-
.prompt({
|
|
136
|
-
path: { id: delegation.sessionID },
|
|
137
|
-
body: promptBody,
|
|
138
|
-
})
|
|
139
|
-
.catch((error: Error) => {
|
|
140
|
-
delegation.status = "error"
|
|
141
|
-
delegation.error = error.message
|
|
142
|
-
delegation.completedAt = new Date()
|
|
143
|
-
delegation.duration = this.calculateDuration(delegation)
|
|
144
|
-
this.notifyParent(delegation)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
return delegation
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async resume(delegationId: string, newPrompt?: string): Promise<Delegation> {
|
|
151
|
-
const delegation = this.delegations.get(delegationId)
|
|
152
|
-
if (!delegation) {
|
|
153
|
-
throw new Error(`Delegation "${delegationId}" not found`)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (delegation.status === "running") {
|
|
157
|
-
throw new Error(`Delegation is already running. Wait for it to complete or cancel it first.`)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Reset status
|
|
161
|
-
delegation.status = "running"
|
|
162
|
-
delegation.completedAt = undefined
|
|
163
|
-
delegation.error = undefined
|
|
164
|
-
delegation.startedAt = new Date()
|
|
165
|
-
delegation.progress = {
|
|
166
|
-
toolCalls: 0,
|
|
167
|
-
lastUpdate: new Date(),
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Track again
|
|
171
|
-
const parentId = delegation.parentSessionID
|
|
172
|
-
if (!this.pendingByParent.has(parentId)) {
|
|
173
|
-
this.pendingByParent.set(parentId, new Set())
|
|
174
|
-
}
|
|
175
|
-
this.pendingByParent.get(parentId)?.add(delegation.id)
|
|
176
|
-
|
|
177
|
-
// Send continue prompt to same session — reuse model if set
|
|
178
|
-
const prompt = newPrompt || "Continue from where you left off."
|
|
179
|
-
|
|
180
|
-
const resumeBody: any = {
|
|
181
|
-
agent: delegation.agent,
|
|
182
|
-
parts: [{ type: "text", text: prompt }],
|
|
183
|
-
tools: {
|
|
184
|
-
task: false,
|
|
185
|
-
delegate: false,
|
|
186
|
-
todowrite: false,
|
|
187
|
-
plan_save: false,
|
|
188
|
-
},
|
|
189
|
-
}
|
|
190
|
-
if (delegation.model) {
|
|
191
|
-
resumeBody.model = parseModel(delegation.model)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
this.client.session
|
|
195
|
-
.prompt({
|
|
196
|
-
path: { id: delegation.sessionID },
|
|
197
|
-
body: resumeBody,
|
|
198
|
-
})
|
|
199
|
-
.catch((error: Error) => {
|
|
200
|
-
delegation.status = "error"
|
|
201
|
-
delegation.error = error.message
|
|
202
|
-
delegation.completedAt = new Date()
|
|
203
|
-
delegation.duration = this.calculateDuration(delegation)
|
|
204
|
-
this.notifyParent(delegation)
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
await this.debugLog(`Resumed delegation ${delegation.id}`)
|
|
208
|
-
return delegation
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async cancel(delegationId: string): Promise<boolean> {
|
|
212
|
-
const delegation = this.delegations.get(delegationId)
|
|
213
|
-
if (!delegation) return false
|
|
214
|
-
if (delegation.status !== "running") return false
|
|
215
|
-
|
|
216
|
-
// Set cancelled BEFORE abort to prevent race with session.idle event
|
|
217
|
-
// Otherwise handleSessionIdle() sees status="running" and marks it "completed"
|
|
218
|
-
delegation.status = "cancelled"
|
|
219
|
-
delegation.completedAt = new Date()
|
|
220
|
-
delegation.duration = this.calculateDuration(delegation)
|
|
221
|
-
|
|
222
|
-
// Abort the session
|
|
223
|
-
try {
|
|
224
|
-
await this.client.session.abort({
|
|
225
|
-
path: { id: delegation.sessionID },
|
|
226
|
-
})
|
|
227
|
-
} catch {
|
|
228
|
-
// Ignore abort errors
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Remove from pending
|
|
232
|
-
const pendingSet = this.pendingByParent.get(delegation.parentSessionID)
|
|
233
|
-
if (pendingSet) {
|
|
234
|
-
pendingSet.delete(delegationId)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
await this.notifyParent(delegation)
|
|
238
|
-
|
|
239
|
-
// Toast: task cancelled
|
|
240
|
-
showToast(this.client, "Task Cancelled", `${delegation.id} cancelled (${delegation.duration})`, "info", 3000)
|
|
241
|
-
|
|
242
|
-
await this.debugLog(`Cancelled delegation ${delegation.id}`)
|
|
243
|
-
return true
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async cancelAll(parentSessionID: string): Promise<string[]> {
|
|
247
|
-
const cancelled: string[] = []
|
|
248
|
-
|
|
249
|
-
for (const delegation of this.delegations.values()) {
|
|
250
|
-
if (delegation.parentSessionID === parentSessionID && delegation.status === "running") {
|
|
251
|
-
const success = await this.cancel(delegation.id)
|
|
252
|
-
if (success) {
|
|
253
|
-
cancelled.push(delegation.id)
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return cancelled
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// ---- Event handlers ----
|
|
262
|
-
|
|
263
|
-
private async handleTimeout(delegationId: string): Promise<void> {
|
|
264
|
-
const delegation = this.delegations.get(delegationId)
|
|
265
|
-
if (!delegation || delegation.status !== "running") return
|
|
266
|
-
|
|
267
|
-
await this.debugLog(`handleTimeout for delegation ${delegation.id}`)
|
|
268
|
-
|
|
269
|
-
delegation.status = "timeout"
|
|
270
|
-
delegation.completedAt = new Date()
|
|
271
|
-
delegation.duration = this.calculateDuration(delegation)
|
|
272
|
-
delegation.error = `Delegation timed out after ${MAX_RUN_TIME_MS / 1000}s`
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
await this.client.session.abort({
|
|
276
|
-
path: { id: delegation.sessionID },
|
|
277
|
-
})
|
|
278
|
-
} catch {
|
|
279
|
-
// Ignore
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
await this.notifyParent(delegation)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async handleSessionIdle(sessionID: string): Promise<void> {
|
|
286
|
-
const delegation = this.findBySession(sessionID)
|
|
287
|
-
if (!delegation || delegation.status !== "running") return
|
|
288
|
-
|
|
289
|
-
await this.debugLog(`handleSessionIdle for delegation ${delegation.id}`)
|
|
290
|
-
|
|
291
|
-
delegation.status = "completed"
|
|
292
|
-
delegation.completedAt = new Date()
|
|
293
|
-
delegation.duration = this.calculateDuration(delegation)
|
|
294
|
-
|
|
295
|
-
// Extract title/description from first user message
|
|
296
|
-
try {
|
|
297
|
-
const messages = await this.client.session.messages({
|
|
298
|
-
path: { id: delegation.sessionID },
|
|
299
|
-
})
|
|
300
|
-
const messageData = messages.data as SessionMessageItem[] | undefined
|
|
301
|
-
if (messageData && messageData.length > 0) {
|
|
302
|
-
const firstUser = messageData.find(m => m.info.role === "user")
|
|
303
|
-
if (firstUser) {
|
|
304
|
-
const textPart = firstUser.parts.find((p): p is TextPart => p.type === "text")
|
|
305
|
-
if (textPart) {
|
|
306
|
-
delegation.description = textPart.text.slice(0, 150)
|
|
307
|
-
delegation.title = textPart.text.split('\n')[0].slice(0, 50)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
} catch {
|
|
312
|
-
// Ignore
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
showToast(
|
|
316
|
-
this.client,
|
|
317
|
-
"Task Completed",
|
|
318
|
-
`"${delegation.id}" finished in ${delegation.duration}`,
|
|
319
|
-
"success",
|
|
320
|
-
5000,
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
await this.notifyParent(delegation)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// ---- Read delegation results ----
|
|
327
|
-
|
|
328
|
-
async readDelegation(args: ReadDelegationArgs): Promise<string> {
|
|
329
|
-
const delegation = this.delegations.get(args.id)
|
|
330
|
-
if (!delegation) {
|
|
331
|
-
throw new Error(`Delegation "${args.id}" not found.\n\nUse delegation_list() to see available delegations.`)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (delegation.status === "running") {
|
|
335
|
-
return `Delegation "${args.id}" is still running.\n\nStatus: ${delegation.status}\nStarted: ${delegation.startedAt.toISOString()}\n\nWait for completion notification, then call delegation_read() again.`
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (delegation.status !== "completed") {
|
|
339
|
-
let statusMessage = `Delegation "${args.id}" ended with status: ${delegation.status}`
|
|
340
|
-
if (delegation.error) statusMessage += `\n\nError: ${delegation.error}`
|
|
341
|
-
if (delegation.duration) statusMessage += `\n\nDuration: ${delegation.duration}`
|
|
342
|
-
return statusMessage
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (!args.mode || args.mode === "simple") {
|
|
346
|
-
return await this.getSimpleResult(delegation)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (args.mode === "full") {
|
|
350
|
-
return await this.getFullSession(delegation, args)
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return "Invalid mode. Use 'simple' or 'full'."
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
private async getSimpleResult(delegation: Delegation): Promise<string> {
|
|
357
|
-
try {
|
|
358
|
-
const messages = await this.client.session.messages({
|
|
359
|
-
path: { id: delegation.sessionID },
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
const messageData = messages.data as SessionMessageItem[] | undefined
|
|
363
|
-
|
|
364
|
-
if (!messageData || messageData.length === 0) {
|
|
365
|
-
return `Delegation "${delegation.id}" completed but produced no output.`
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const assistantMessages = messageData.filter(
|
|
369
|
-
(m): m is AssistantSessionMessageItem => m.info.role === "assistant"
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
if (assistantMessages.length === 0) {
|
|
373
|
-
return `Delegation "${delegation.id}" completed but produced no assistant response.`
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const lastMessage = assistantMessages[assistantMessages.length - 1]
|
|
377
|
-
const textParts = lastMessage.parts.filter((p): p is TextPart => p.type === "text")
|
|
378
|
-
|
|
379
|
-
if (textParts.length === 0) {
|
|
380
|
-
return `Delegation "${delegation.id}" completed but produced no text content.`
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const result = textParts.map((p) => p.text).join("\n")
|
|
384
|
-
|
|
385
|
-
const header = `# Task Result: ${delegation.id}
|
|
386
|
-
|
|
387
|
-
**Agent:** ${delegation.agent}
|
|
388
|
-
**Status:** ${delegation.status}
|
|
389
|
-
**Duration:** ${delegation.duration || "N/A"}
|
|
390
|
-
**Started:** ${delegation.startedAt.toISOString()}
|
|
391
|
-
${delegation.completedAt ? `**Completed:** ${delegation.completedAt.toISOString()}` : ""}
|
|
392
|
-
|
|
393
|
-
---
|
|
394
|
-
|
|
395
|
-
`
|
|
396
|
-
|
|
397
|
-
return header + result
|
|
398
|
-
} catch (error) {
|
|
399
|
-
return `Error retrieving result: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
private async getFullSession(delegation: Delegation, args: ReadDelegationArgs): Promise<string> {
|
|
404
|
-
try {
|
|
405
|
-
const messages = await this.client.session.messages({
|
|
406
|
-
path: { id: delegation.sessionID },
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
const messageData = messages.data as SessionMessageItem[] | undefined
|
|
410
|
-
|
|
411
|
-
if (!messageData || messageData.length === 0) {
|
|
412
|
-
return `Delegation "${delegation.id}" has no messages.`
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const sortedMessages = [...messageData].sort((a, b) => {
|
|
416
|
-
const timeA = String(a.info.time || "")
|
|
417
|
-
const timeB = String(b.info.time || "")
|
|
418
|
-
return timeA.localeCompare(timeB)
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
let filteredMessages = sortedMessages
|
|
422
|
-
if (args.since_message_id) {
|
|
423
|
-
const index = sortedMessages.findIndex((m) => m.info.id === args.since_message_id)
|
|
424
|
-
if (index !== -1) {
|
|
425
|
-
filteredMessages = sortedMessages.slice(index + 1)
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const limit = args.limit ? Math.min(args.limit, 100) : undefined
|
|
430
|
-
const hasMore = limit !== undefined && filteredMessages.length > limit
|
|
431
|
-
const visibleMessages = limit !== undefined ? filteredMessages.slice(0, limit) : filteredMessages
|
|
432
|
-
|
|
433
|
-
const lines: string[] = []
|
|
434
|
-
lines.push(`# Full Session: ${delegation.id}`)
|
|
435
|
-
lines.push("")
|
|
436
|
-
lines.push(`**Agent:** ${delegation.agent}`)
|
|
437
|
-
lines.push(`**Status:** ${delegation.status}`)
|
|
438
|
-
lines.push(`**Duration:** ${delegation.duration || "N/A"}`)
|
|
439
|
-
lines.push(`**Total messages:** ${sortedMessages.length}`)
|
|
440
|
-
lines.push(`**Returned:** ${visibleMessages.length}`)
|
|
441
|
-
lines.push(`**Has more:** ${hasMore ? "true" : "false"}`)
|
|
442
|
-
lines.push("")
|
|
443
|
-
lines.push("## Messages")
|
|
444
|
-
lines.push("")
|
|
445
|
-
|
|
446
|
-
for (const message of visibleMessages) {
|
|
447
|
-
const role = message.info.role
|
|
448
|
-
const time = message.info.time || "unknown"
|
|
449
|
-
const id = message.info.id || "unknown"
|
|
450
|
-
|
|
451
|
-
lines.push(`### [${role}] ${time} (id: ${id})`)
|
|
452
|
-
lines.push("")
|
|
453
|
-
|
|
454
|
-
for (const part of message.parts) {
|
|
455
|
-
if (part.type === "text" && part.text) {
|
|
456
|
-
lines.push(part.text.trim())
|
|
457
|
-
lines.push("")
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (args.include_thinking && (part.type === "thinking" || part.type === "reasoning")) {
|
|
461
|
-
const thinkingText = (part as any).thinking || (part as any).text || ""
|
|
462
|
-
if (thinkingText) {
|
|
463
|
-
lines.push(`[thinking] ${thinkingText.slice(0, 2000)}`)
|
|
464
|
-
lines.push("")
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
if (args.include_tools && part.type === "tool_result") {
|
|
469
|
-
const content = (part as any).content || (part as any).output || ""
|
|
470
|
-
if (content) {
|
|
471
|
-
lines.push(`[tool result] ${content}`)
|
|
472
|
-
lines.push("")
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return lines.join("\n")
|
|
479
|
-
} catch (error) {
|
|
480
|
-
return `Error fetching session: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// ---- Notification ----
|
|
485
|
-
|
|
486
|
-
private async notifyParent(delegation: Delegation): Promise<void> {
|
|
487
|
-
try {
|
|
488
|
-
const pendingSet = this.pendingByParent.get(delegation.parentSessionID)
|
|
489
|
-
if (pendingSet) {
|
|
490
|
-
pendingSet.delete(delegation.id)
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const allComplete = !pendingSet || pendingSet.size === 0
|
|
494
|
-
const remainingCount = pendingSet?.size || 0
|
|
495
|
-
|
|
496
|
-
const statusText = delegation.status === "completed" ? "COMPLETED"
|
|
497
|
-
: delegation.status === "cancelled" ? "CANCELLED"
|
|
498
|
-
: delegation.status === "error" ? "ERROR"
|
|
499
|
-
: delegation.status === "timeout" ? "TIMEOUT"
|
|
500
|
-
: delegation.status.toUpperCase()
|
|
501
|
-
const duration = delegation.duration || "N/A"
|
|
502
|
-
const errorInfo = delegation.error ? `\n**Error:** ${delegation.error}` : ""
|
|
503
|
-
|
|
504
|
-
let notification: string
|
|
505
|
-
|
|
506
|
-
if (allComplete) {
|
|
507
|
-
const completedTasks: Delegation[] = []
|
|
508
|
-
for (const d of this.delegations.values()) {
|
|
509
|
-
if (d.parentSessionID === delegation.parentSessionID && d.status !== "running") {
|
|
510
|
-
completedTasks.push(d)
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
const completedList = completedTasks
|
|
514
|
-
.map(t => `- \`${t.id}\`: ${t.title || t.prompt.slice(0, 80)}`)
|
|
515
|
-
.join("\n")
|
|
516
|
-
|
|
517
|
-
const sessionHints = completedTasks
|
|
518
|
-
.map(t => `opencode -s ${t.id}`)
|
|
519
|
-
.join("\n")
|
|
520
|
-
|
|
521
|
-
notification = `<system-reminder>
|
|
522
|
-
[ALL BACKGROUND TASKS COMPLETE]
|
|
523
|
-
|
|
524
|
-
**Completed:**
|
|
525
|
-
${completedList || `- \`${delegation.id}\`: ${delegation.title || delegation.prompt.slice(0, 80)}`}
|
|
526
|
-
|
|
527
|
-
Use \`delegation_read(id="<id>")\` to retrieve each result.
|
|
528
|
-
</system-reminder>
|
|
529
|
-
To inspect session content(human): ${sessionHints || `opencode -s ${delegation.id}`}`
|
|
530
|
-
} else {
|
|
531
|
-
notification = `<system-reminder>
|
|
532
|
-
[BACKGROUND TASK ${statusText}]
|
|
533
|
-
**ID:** \`${delegation.id}\`
|
|
534
|
-
**Agent:** ${delegation.agent}
|
|
535
|
-
**Duration:** ${duration}${errorInfo}
|
|
536
|
-
|
|
537
|
-
**${remainingCount} task${remainingCount === 1 ? "" : "s"} still in progress.** You WILL be notified when ALL complete.
|
|
538
|
-
Do NOT poll - continue productive work.
|
|
539
|
-
|
|
540
|
-
Use \`delegation_read(id="${delegation.id}")\` to retrieve this result when ready.
|
|
541
|
-
</system-reminder>
|
|
542
|
-
To inspect session content(human): opencode -s ${delegation.id}`
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Fire and forget - don't await to avoid deadlock when parent session is busy
|
|
546
|
-
this.client.session.prompt({
|
|
547
|
-
path: { id: delegation.parentSessionID },
|
|
548
|
-
body: {
|
|
549
|
-
noReply: !allComplete,
|
|
550
|
-
agent: delegation.parentAgent,
|
|
551
|
-
parts: [{ type: "text", text: notification }],
|
|
552
|
-
},
|
|
553
|
-
}).catch(() => {})
|
|
554
|
-
|
|
555
|
-
await this.debugLog(
|
|
556
|
-
`Notified parent session ${delegation.parentSessionID} (status=${statusText}, remaining=${remainingCount})`,
|
|
557
|
-
)
|
|
558
|
-
} catch (error) {
|
|
559
|
-
await this.debugLog(
|
|
560
|
-
`Failed to notify parent: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
561
|
-
)
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// ---- Queries ----
|
|
566
|
-
|
|
567
|
-
async listDelegations(parentSessionID: string): Promise<DelegationListItem[]> {
|
|
568
|
-
const results: DelegationListItem[] = []
|
|
569
|
-
|
|
570
|
-
for (const delegation of this.delegations.values()) {
|
|
571
|
-
if (delegation.parentSessionID === parentSessionID) {
|
|
572
|
-
results.push({
|
|
573
|
-
id: delegation.id,
|
|
574
|
-
status: delegation.status,
|
|
575
|
-
title: delegation.title,
|
|
576
|
-
description: delegation.description,
|
|
577
|
-
agent: delegation.agent,
|
|
578
|
-
duration: delegation.duration,
|
|
579
|
-
startedAt: delegation.startedAt,
|
|
580
|
-
})
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return results.sort((a, b) => (b.startedAt?.getTime() || 0) - (a.startedAt?.getTime() || 0))
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
listAllDelegations(): DelegationListItem[] {
|
|
588
|
-
const results: DelegationListItem[] = []
|
|
589
|
-
for (const delegation of this.delegations.values()) {
|
|
590
|
-
results.push({
|
|
591
|
-
id: delegation.id,
|
|
592
|
-
status: delegation.status,
|
|
593
|
-
title: delegation.title,
|
|
594
|
-
description: delegation.description,
|
|
595
|
-
agent: delegation.agent,
|
|
596
|
-
duration: delegation.duration,
|
|
597
|
-
startedAt: delegation.startedAt,
|
|
598
|
-
})
|
|
599
|
-
}
|
|
600
|
-
return results.sort((a, b) => (b.startedAt?.getTime() || 0) - (a.startedAt?.getTime() || 0))
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
findBySession(sessionID: string): Delegation | undefined {
|
|
604
|
-
return Array.from(this.delegations.values()).find((d) => d.sessionID === sessionID)
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
handleMessageEvent(sessionID: string, messageText?: string): void {
|
|
608
|
-
const delegation = this.findBySession(sessionID)
|
|
609
|
-
if (!delegation || delegation.status !== "running") return
|
|
610
|
-
|
|
611
|
-
delegation.progress.lastUpdate = new Date()
|
|
612
|
-
if (messageText) {
|
|
613
|
-
delegation.progress.lastMessage = messageText
|
|
614
|
-
delegation.progress.lastMessageAt = new Date()
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
getPendingCount(parentSessionID: string): number {
|
|
619
|
-
const pendingSet = this.pendingByParent.get(parentSessionID)
|
|
620
|
-
return pendingSet ? pendingSet.size : 0
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
getRunningDelegations(): Delegation[] {
|
|
624
|
-
return Array.from(this.delegations.values()).filter((d) => d.status === "running")
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
async debugLog(msg: string): Promise<void> {
|
|
628
|
-
this.log.debug(msg)
|
|
629
|
-
}
|
|
630
|
-
}
|