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/plugin.ts
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* async-agent
|
|
3
|
-
* Unified delegation system for OpenCode
|
|
4
|
-
*
|
|
5
|
-
* Based on oh-my-opencode by @code-yeongyu (MIT License)
|
|
6
|
-
* https://github.com/code-yeongyu/oh-my-opencode
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { Plugin } from "@opencode-ai/plugin"
|
|
10
|
-
import type { Event } from "@opencode-ai/sdk"
|
|
11
|
-
import type { OpencodeClient } from "./types"
|
|
12
|
-
import { createLogger } from "./utils"
|
|
13
|
-
import { DelegationManager } from "./manager"
|
|
14
|
-
import {
|
|
15
|
-
createDelegate,
|
|
16
|
-
createDelegationRead,
|
|
17
|
-
createDelegationList,
|
|
18
|
-
createDelegationCancel,
|
|
19
|
-
createDelegationResume,
|
|
20
|
-
} from "./tools"
|
|
21
|
-
import { DELEGATION_RULES, formatDelegationContext, readBgAgentsConfig } from "./rules"
|
|
22
|
-
|
|
23
|
-
// Slash command name — user types /delegation in chat
|
|
24
|
-
const delegationCommand = "delegation"
|
|
25
|
-
|
|
26
|
-
export const AsyncAgentPlugin: Plugin = async (ctx) => {
|
|
27
|
-
const { client } = ctx
|
|
28
|
-
|
|
29
|
-
const log = createLogger(client as OpencodeClient)
|
|
30
|
-
const manager = new DelegationManager(client as OpencodeClient, log)
|
|
31
|
-
|
|
32
|
-
await manager.debugLog("AsyncAgentPlugin initialized")
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
// Handle /delegation slash command execution
|
|
36
|
-
"command.execute.before": async (input: { command: string; sessionID: string }) => {
|
|
37
|
-
if (input.command !== delegationCommand) return
|
|
38
|
-
|
|
39
|
-
const typedClient = client as OpencodeClient
|
|
40
|
-
|
|
41
|
-
// Query child sessions of current session from OpenCode API (survives reboots)
|
|
42
|
-
let childSessions: any[] = []
|
|
43
|
-
try {
|
|
44
|
-
const result = await typedClient.session.children({
|
|
45
|
-
path: { id: input.sessionID },
|
|
46
|
-
})
|
|
47
|
-
childSessions = (result.data ?? []) as any[]
|
|
48
|
-
} catch {
|
|
49
|
-
// Fallback to in-memory only if API fails
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Filter for delegation sessions — created with title "Delegation: <agent>"
|
|
53
|
-
const delegationSessions = childSessions.filter(
|
|
54
|
-
(s: any) => s.title?.startsWith("Delegation:"),
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
// Get in-memory state for extra metadata (status, duration, agent)
|
|
58
|
-
const inMemory = manager.listAllDelegations()
|
|
59
|
-
const inMemoryMap = new Map(inMemory.map((d) => [d.id, d]))
|
|
60
|
-
|
|
61
|
-
let message: string
|
|
62
|
-
if (delegationSessions.length === 0 && inMemory.length === 0) {
|
|
63
|
-
message = "No delegations found for this session."
|
|
64
|
-
} else {
|
|
65
|
-
const lines: string[] = []
|
|
66
|
-
const seen = new Set<string>()
|
|
67
|
-
const entries: {
|
|
68
|
-
id: string
|
|
69
|
-
title: string
|
|
70
|
-
agent: string
|
|
71
|
-
status: string
|
|
72
|
-
duration: string
|
|
73
|
-
started: string
|
|
74
|
-
}[] = []
|
|
75
|
-
|
|
76
|
-
// Persisted child sessions from API — survive reboots
|
|
77
|
-
for (const s of delegationSessions) {
|
|
78
|
-
seen.add(s.id)
|
|
79
|
-
const mem = inMemoryMap.get(s.id)
|
|
80
|
-
// Extract agent name from title "Delegation: <agent>"
|
|
81
|
-
const agent = mem?.agent ?? s.title?.replace("Delegation: ", "") ?? "unknown"
|
|
82
|
-
const status = mem?.status?.toUpperCase() ?? "PERSISTED"
|
|
83
|
-
const duration = mem?.duration ?? "—"
|
|
84
|
-
// time.created is already in ms — do NOT multiply by 1000
|
|
85
|
-
const created = s.time?.created
|
|
86
|
-
? new Date(s.time.created).toISOString()
|
|
87
|
-
: mem?.startedAt?.toISOString() ?? "—"
|
|
88
|
-
|
|
89
|
-
entries.push({
|
|
90
|
-
id: s.id,
|
|
91
|
-
title: mem?.title ?? s.title ?? "—",
|
|
92
|
-
agent,
|
|
93
|
-
status,
|
|
94
|
-
duration,
|
|
95
|
-
started: created,
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// In-memory delegations not in API yet (just launched, or API miss)
|
|
100
|
-
for (const d of inMemory) {
|
|
101
|
-
if (seen.has(d.id)) continue
|
|
102
|
-
// Only include delegations belonging to this parent session
|
|
103
|
-
entries.push({
|
|
104
|
-
id: d.id,
|
|
105
|
-
title: d.title ?? d.description?.slice(0, 60) ?? "—",
|
|
106
|
-
agent: d.agent ?? "unknown",
|
|
107
|
-
status: d.status?.toUpperCase() ?? "UNKNOWN",
|
|
108
|
-
duration: d.duration ?? "—",
|
|
109
|
-
started: d.startedAt?.toISOString() ?? "—",
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
lines.push(`## Delegations (${entries.length})\n`)
|
|
114
|
-
|
|
115
|
-
for (const e of entries) {
|
|
116
|
-
lines.push(`**[${e.id}]** ${e.title}`)
|
|
117
|
-
lines.push(` Status: ${e.status} | Agent: ${e.agent} | Duration: ${e.duration}`)
|
|
118
|
-
lines.push(` Started: ${e.started}`)
|
|
119
|
-
lines.push(` \`opencode -s ${e.id}\``)
|
|
120
|
-
lines.push("")
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
message = lines.join("\n")
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Send output to the user's session
|
|
127
|
-
await typedClient.session.prompt({
|
|
128
|
-
path: { id: input.sessionID },
|
|
129
|
-
body: {
|
|
130
|
-
noReply: true,
|
|
131
|
-
parts: [{ type: "text", text: message }],
|
|
132
|
-
},
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
throw new Error("Command handled by async-agent plugin")
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
tool: {
|
|
139
|
-
delegate: createDelegate(manager),
|
|
140
|
-
delegation_read: createDelegationRead(manager),
|
|
141
|
-
delegation_list: createDelegationList(manager),
|
|
142
|
-
delegation_cancel: createDelegationCancel(manager),
|
|
143
|
-
delegation_resume: createDelegationResume(manager),
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
// Register /delegation slash command
|
|
147
|
-
config: async (input: any) => {
|
|
148
|
-
if (!input.command) input.command = {}
|
|
149
|
-
input.command[delegationCommand] = {
|
|
150
|
-
template: "Show all background delegation sessions with their status, IDs, agents, and metadata.",
|
|
151
|
-
description: "List all background delegations",
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
// Inject delegation rules + async-agent.md config into system prompt
|
|
156
|
-
"experimental.chat.system.transform": async (
|
|
157
|
-
_input: { sessionID?: string; model: any },
|
|
158
|
-
output: { system: string[] },
|
|
159
|
-
): Promise<void> => {
|
|
160
|
-
output.system.push(DELEGATION_RULES)
|
|
161
|
-
|
|
162
|
-
// Read user's model config from ~/.config/opencode/async-agent.md
|
|
163
|
-
const bgConfig = await readBgAgentsConfig()
|
|
164
|
-
if (bgConfig.trim()) {
|
|
165
|
-
output.system.push(`<async-agent-config>\n${bgConfig}\n</async-agent-config>`)
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
|
|
169
|
-
// Inject active delegation context during session compaction
|
|
170
|
-
"experimental.session.compacting": async (
|
|
171
|
-
_input: { sessionID: string },
|
|
172
|
-
output: { context: string[] },
|
|
173
|
-
): Promise<void> => {
|
|
174
|
-
const running = manager.getRunningDelegations().map((d) => ({
|
|
175
|
-
id: d.id,
|
|
176
|
-
agent: d.agent,
|
|
177
|
-
status: d.status,
|
|
178
|
-
startedAt: d.startedAt,
|
|
179
|
-
}))
|
|
180
|
-
|
|
181
|
-
// Only inject if there are active delegations
|
|
182
|
-
if (running.length > 0) {
|
|
183
|
-
output.context.push(formatDelegationContext(running, []))
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
event: async (input: { event: Event }): Promise<void> => {
|
|
188
|
-
const { event } = input
|
|
189
|
-
if (event.type === "session.idle") {
|
|
190
|
-
const sessionID = (event.properties as any)?.sessionID
|
|
191
|
-
const delegation = manager.findBySession(sessionID)
|
|
192
|
-
if (delegation) {
|
|
193
|
-
await manager.handleSessionIdle(sessionID)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export default AsyncAgentPlugin
|
package/src/plugin/rules.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// System prompt rules injected to teach the agent how to use delegation tools
|
|
2
|
-
export const DELEGATION_RULES = `<system-reminder>
|
|
3
|
-
<delegation-system>
|
|
4
|
-
|
|
5
|
-
## Async Delegation
|
|
6
|
-
|
|
7
|
-
You have tools for parallel background work:
|
|
8
|
-
- \`delegate(prompt, agent)\` - Launch task, returns ID immediately
|
|
9
|
-
- \`delegate(prompt, agent, model)\` - Launch task with specific model override
|
|
10
|
-
- \`delegation_read(id)\` - Retrieve completed result
|
|
11
|
-
- \`delegation_list()\` - List delegations (use sparingly)
|
|
12
|
-
- \`delegation_cancel(id|all)\` - Cancel running task(s)
|
|
13
|
-
- \`delegation_resume(id, prompt?)\` - Continue cancelled task (same session)
|
|
14
|
-
|
|
15
|
-
## How It Works
|
|
16
|
-
|
|
17
|
-
1. Call \`delegate()\` - Get task ID immediately, continue working
|
|
18
|
-
2. Receive \`<system-reminder>\` notification when complete
|
|
19
|
-
3. Call \`delegation_read(id)\` to get the actual result
|
|
20
|
-
|
|
21
|
-
## Model Override
|
|
22
|
-
|
|
23
|
-
The \`delegate\` tool accepts an optional \`model\` parameter in "provider/model" format.
|
|
24
|
-
Example: \`delegate(prompt, agent, model="minimax/MiniMax-M2.5")\`
|
|
25
|
-
If not specified, the agent's default model is used.
|
|
26
|
-
|
|
27
|
-
## Critical Constraints
|
|
28
|
-
|
|
29
|
-
**NEVER poll \`delegation_list\` to check completion.**
|
|
30
|
-
You WILL be notified via \`<system-reminder>\`. Polling wastes tokens.
|
|
31
|
-
|
|
32
|
-
**NEVER wait idle.** Always have productive work while delegations run.
|
|
33
|
-
|
|
34
|
-
**Cancelled tasks can be resumed** with \`delegation_resume()\` - same session, full context.
|
|
35
|
-
|
|
36
|
-
</delegation-system>
|
|
37
|
-
</system-reminder>`
|
|
38
|
-
|
|
39
|
-
// Read user's async-agent(s).md config file — contains model preferences for delegation
|
|
40
|
-
// Tries both async-agents.md and async-agent.md, returns first found or empty string
|
|
41
|
-
export async function readBgAgentsConfig(): Promise<string> {
|
|
42
|
-
const { homedir } = await import("os")
|
|
43
|
-
const { readFile } = await import("fs/promises")
|
|
44
|
-
const { join } = await import("path")
|
|
45
|
-
|
|
46
|
-
const configDir = join(homedir(), ".config", "opencode")
|
|
47
|
-
for (const name of ["async-agents.md", "async-agent.md"]) {
|
|
48
|
-
try {
|
|
49
|
-
return await readFile(join(configDir, name), "utf-8")
|
|
50
|
-
} catch {}
|
|
51
|
-
}
|
|
52
|
-
return ""
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Context injected during compaction so the agent remembers active delegations
|
|
56
|
-
interface DelegationForContext {
|
|
57
|
-
id: string
|
|
58
|
-
agent?: string
|
|
59
|
-
title?: string
|
|
60
|
-
description?: string
|
|
61
|
-
status: string
|
|
62
|
-
startedAt?: Date
|
|
63
|
-
duration?: string
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function formatDelegationContext(
|
|
67
|
-
running: DelegationForContext[],
|
|
68
|
-
completed: DelegationForContext[],
|
|
69
|
-
): string {
|
|
70
|
-
const sections: string[] = ["<delegation-context>"]
|
|
71
|
-
|
|
72
|
-
if (running.length > 0) {
|
|
73
|
-
sections.push("## Running Delegations")
|
|
74
|
-
sections.push("")
|
|
75
|
-
for (const d of running) {
|
|
76
|
-
sections.push(`### \`${d.id}\`${d.agent ? ` (${d.agent})` : ""}`)
|
|
77
|
-
if (d.startedAt) {
|
|
78
|
-
sections.push(`**Started:** ${d.startedAt.toISOString()}`)
|
|
79
|
-
}
|
|
80
|
-
sections.push("")
|
|
81
|
-
}
|
|
82
|
-
sections.push("> **Note:** You WILL be notified via \`<system-reminder>\` when delegations complete.",
|
|
83
|
-
)
|
|
84
|
-
sections.push("> Do NOT poll \`delegation_list\` - continue productive work.")
|
|
85
|
-
sections.push("")
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (completed.length > 0) {
|
|
89
|
-
sections.push("## Recent Completed Delegations")
|
|
90
|
-
sections.push("")
|
|
91
|
-
for (const d of completed) {
|
|
92
|
-
const statusEmoji =
|
|
93
|
-
d.status === "completed"
|
|
94
|
-
? "✅"
|
|
95
|
-
: d.status === "error"
|
|
96
|
-
? "❌"
|
|
97
|
-
: d.status === "timeout"
|
|
98
|
-
? "⏱️"
|
|
99
|
-
: "🚫"
|
|
100
|
-
sections.push(`### ${statusEmoji} \`${d.id}\``)
|
|
101
|
-
sections.push(`**Status:** ${d.status}`)
|
|
102
|
-
if (d.duration) sections.push(`**Duration:** ${d.duration}`)
|
|
103
|
-
sections.push("")
|
|
104
|
-
}
|
|
105
|
-
sections.push("> Use \`delegation_list()\` to see all delegations.")
|
|
106
|
-
sections.push("")
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
sections.push("## Retrieval")
|
|
110
|
-
sections.push('Use \`delegation_read("id")\` to access results.')
|
|
111
|
-
sections.push("Use \`delegation_read(id, mode=\"full\")\` for full conversation.")
|
|
112
|
-
sections.push("</delegation-context>")
|
|
113
|
-
|
|
114
|
-
return sections.join("\n")
|
|
115
|
-
}
|
package/src/plugin/tools.ts
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
2
|
-
import type { ReadDelegationArgs } from "./types"
|
|
3
|
-
import type { DelegationManager } from "./manager"
|
|
4
|
-
|
|
5
|
-
// ---- Arg interfaces ----
|
|
6
|
-
|
|
7
|
-
interface DelegateArgs {
|
|
8
|
-
prompt: string
|
|
9
|
-
agent: string
|
|
10
|
-
model?: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface CancelArgs {
|
|
14
|
-
id?: string
|
|
15
|
-
all?: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface ResumeArgs {
|
|
19
|
-
id: string
|
|
20
|
-
prompt?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ---- Tool creators ----
|
|
24
|
-
|
|
25
|
-
export function createDelegate(manager: DelegationManager): ReturnType<typeof tool> {
|
|
26
|
-
return tool({
|
|
27
|
-
description: `Delegate a task to an agent. Returns immediately with the session ID.
|
|
28
|
-
|
|
29
|
-
Use this for:
|
|
30
|
-
- Research tasks (will be auto-saved)
|
|
31
|
-
- Parallel work that can run in background
|
|
32
|
-
- Any task where you want persistent, retrievable output
|
|
33
|
-
|
|
34
|
-
On completion, a notification will arrive with the session ID, status, duration.
|
|
35
|
-
Use \`delegation_read\` with the session ID to retrieve the result.`,
|
|
36
|
-
args: {
|
|
37
|
-
prompt: tool.schema
|
|
38
|
-
.string()
|
|
39
|
-
.describe("The full detailed prompt for the agent. Must be in English."),
|
|
40
|
-
agent: tool.schema
|
|
41
|
-
.string()
|
|
42
|
-
.describe(
|
|
43
|
-
'Agent to delegate to: "explore" (codebase search), "researcher" (external research), etc.',
|
|
44
|
-
),
|
|
45
|
-
model: tool.schema
|
|
46
|
-
.string()
|
|
47
|
-
.optional()
|
|
48
|
-
.describe(
|
|
49
|
-
'Override model for this delegation. Format: "provider/model" (e.g. "minimax/MiniMax-M2.5"). If not set, uses the agent default.',
|
|
50
|
-
),
|
|
51
|
-
},
|
|
52
|
-
async execute(args: DelegateArgs, toolCtx: ToolContext): Promise<string> {
|
|
53
|
-
if (!toolCtx?.sessionID) {
|
|
54
|
-
return "❌ delegate requires sessionID. This is a system error."
|
|
55
|
-
}
|
|
56
|
-
if (!toolCtx?.messageID) {
|
|
57
|
-
return "❌ delegate requires messageID. This is a system error."
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Block sub-agents from using delegation
|
|
61
|
-
if (manager.findBySession(toolCtx.sessionID)) {
|
|
62
|
-
return "❌ Sub-agents cannot delegate tasks. Only the main agent can use delegation tools."
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const delegation = await manager.delegate({
|
|
67
|
-
parentSessionID: toolCtx.sessionID,
|
|
68
|
-
parentMessageID: toolCtx.messageID,
|
|
69
|
-
parentAgent: toolCtx.agent,
|
|
70
|
-
prompt: args.prompt,
|
|
71
|
-
agent: args.agent,
|
|
72
|
-
model: args.model,
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const pendingCount = manager.getPendingCount(toolCtx.sessionID)
|
|
76
|
-
|
|
77
|
-
let response = `Delegation started: ${delegation.id}\nAgent: ${args.agent}`
|
|
78
|
-
if (pendingCount > 1) {
|
|
79
|
-
response += `\n\n${pendingCount} delegations now active.`
|
|
80
|
-
}
|
|
81
|
-
response += `\n\nYou WILL be notified via <system-reminder> when complete. Do NOT poll delegation_list().`
|
|
82
|
-
|
|
83
|
-
return response
|
|
84
|
-
} catch (error) {
|
|
85
|
-
return `❌ Delegation failed:\n\n${error instanceof Error ? error.message : "Unknown error"}`
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function createDelegationRead(manager: DelegationManager): ReturnType<typeof tool> {
|
|
92
|
-
return tool({
|
|
93
|
-
description: `Read the output of a delegation by its ID.
|
|
94
|
-
|
|
95
|
-
Modes:
|
|
96
|
-
- simple (default): Returns just the final result
|
|
97
|
-
- full: Returns all messages in the session with timestamps
|
|
98
|
-
|
|
99
|
-
Use filters to get specific parts of the conversation.`,
|
|
100
|
-
args: {
|
|
101
|
-
id: tool.schema.string().describe("The delegation session ID"),
|
|
102
|
-
mode: tool.schema
|
|
103
|
-
.enum(["simple", "full"])
|
|
104
|
-
.optional()
|
|
105
|
-
.describe("Output mode: 'simple' for result only, 'full' for all messages"),
|
|
106
|
-
include_thinking: tool.schema
|
|
107
|
-
.boolean()
|
|
108
|
-
.optional()
|
|
109
|
-
.describe("Include thinking/reasoning blocks in full mode"),
|
|
110
|
-
include_tools: tool.schema
|
|
111
|
-
.boolean()
|
|
112
|
-
.optional()
|
|
113
|
-
.describe("Include tool results in full mode"),
|
|
114
|
-
since_message_id: tool.schema
|
|
115
|
-
.string()
|
|
116
|
-
.optional()
|
|
117
|
-
.describe("Return only messages after this message ID (full mode only)"),
|
|
118
|
-
limit: tool.schema
|
|
119
|
-
.number()
|
|
120
|
-
.optional()
|
|
121
|
-
.describe("Max messages to return, capped at 100 (full mode only)"),
|
|
122
|
-
},
|
|
123
|
-
async execute(args: ReadDelegationArgs, toolCtx: ToolContext): Promise<string> {
|
|
124
|
-
if (!toolCtx?.sessionID) {
|
|
125
|
-
return "❌ delegation_read requires sessionID. This is a system error."
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
return await manager.readDelegation(args)
|
|
130
|
-
} catch (error) {
|
|
131
|
-
return `❌ Error reading delegation: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function createDelegationList(manager: DelegationManager): ReturnType<typeof tool> {
|
|
138
|
-
return tool({
|
|
139
|
-
description: `List all delegations for the current session.
|
|
140
|
-
Shows running, completed, cancelled, and error tasks with metadata.`,
|
|
141
|
-
args: {},
|
|
142
|
-
async execute(_args: Record<string, never>, toolCtx: ToolContext): Promise<string> {
|
|
143
|
-
if (!toolCtx?.sessionID) {
|
|
144
|
-
return "❌ delegation_list requires sessionID. This is a system error."
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const delegations = await manager.listDelegations(toolCtx.sessionID)
|
|
148
|
-
|
|
149
|
-
if (delegations.length === 0) {
|
|
150
|
-
return "No delegations found for this session."
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const lines = delegations.map((d) => {
|
|
154
|
-
const titlePart = d.title ? ` | ${d.title}` : ""
|
|
155
|
-
const durationPart = d.duration ? ` (${d.duration})` : ""
|
|
156
|
-
const descPart = d.description ? `\n → ${d.description.slice(0, 100)}${d.description.length > 100 ? "..." : ""}` : ""
|
|
157
|
-
return `- **${d.id}**${titlePart} [${d.status}]${durationPart}${descPart}`
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
return `## Delegations\n\n${lines.join("\n")}`
|
|
161
|
-
},
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function createDelegationCancel(manager: DelegationManager): ReturnType<typeof tool> {
|
|
166
|
-
return tool({
|
|
167
|
-
description: `Cancel a running delegation by ID, or cancel all running delegations.
|
|
168
|
-
|
|
169
|
-
Cancelled tasks can be resumed later with delegation_resume().`,
|
|
170
|
-
args: {
|
|
171
|
-
id: tool.schema.string().optional().describe("Task ID to cancel"),
|
|
172
|
-
all: tool.schema.boolean().optional().describe("Cancel ALL running delegations"),
|
|
173
|
-
},
|
|
174
|
-
async execute(args: CancelArgs, toolCtx: ToolContext): Promise<string> {
|
|
175
|
-
if (!toolCtx?.sessionID) {
|
|
176
|
-
return "❌ delegation_cancel requires sessionID. This is a system error."
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
if (args.all) {
|
|
181
|
-
const cancelled = await manager.cancelAll(toolCtx.sessionID)
|
|
182
|
-
if (cancelled.length === 0) {
|
|
183
|
-
return "No running delegations to cancel."
|
|
184
|
-
}
|
|
185
|
-
return `Cancelled ${cancelled.length} delegation(s):\n${cancelled.map(id => `- ${id}`).join("\n")}`
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (!args.id) {
|
|
189
|
-
return "❌ Must provide either 'id' or 'all=true'"
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const success = await manager.cancel(args.id)
|
|
193
|
-
if (!success) {
|
|
194
|
-
return `❌ Could not cancel "${args.id}". Task may not exist or is not running.`
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return `✅ Cancelled delegation: ${args.id}\n\nYou can resume it later with delegation_resume(id="${args.id}")`
|
|
198
|
-
} catch (error) {
|
|
199
|
-
return `❌ Error cancelling: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function createDelegationResume(manager: DelegationManager): ReturnType<typeof tool> {
|
|
206
|
-
return tool({
|
|
207
|
-
description: `Resume a cancelled or errored delegation by sending a new prompt to the same session.
|
|
208
|
-
|
|
209
|
-
The agent will have access to the previous conversation context.`,
|
|
210
|
-
args: {
|
|
211
|
-
id: tool.schema.string().describe("Task ID to resume"),
|
|
212
|
-
prompt: tool.schema
|
|
213
|
-
.string()
|
|
214
|
-
.optional()
|
|
215
|
-
.describe("Optional prompt to send (default: 'Continue from where you left off.')"),
|
|
216
|
-
},
|
|
217
|
-
async execute(args: ResumeArgs, toolCtx: ToolContext): Promise<string> {
|
|
218
|
-
if (!toolCtx?.sessionID) {
|
|
219
|
-
return "❌ delegation_resume requires sessionID. This is a system error."
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const delegation = await manager.resume(args.id, args.prompt)
|
|
224
|
-
return `✅ Resumed delegation: ${delegation.id}\nAgent: ${delegation.agent}\nStatus: ${delegation.status}`
|
|
225
|
-
} catch (error) {
|
|
226
|
-
return `❌ Error resuming: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
})
|
|
230
|
-
}
|
package/src/plugin/types.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { Message, Part } from "@opencode-ai/sdk"
|
|
2
|
-
import type { createOpencodeClient } from "@opencode-ai/sdk"
|
|
3
|
-
|
|
4
|
-
// OpenCode client instance type
|
|
5
|
-
export type OpencodeClient = ReturnType<typeof createOpencodeClient>
|
|
6
|
-
|
|
7
|
-
// Logger returned by createLogger
|
|
8
|
-
export type Logger = {
|
|
9
|
-
debug: (msg: string) => void
|
|
10
|
-
info: (msg: string) => void
|
|
11
|
-
warn: (msg: string) => void
|
|
12
|
-
error: (msg: string) => void
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// 15 minute max run time per delegation
|
|
16
|
-
export const MAX_RUN_TIME_MS = 15 * 60 * 1000
|
|
17
|
-
|
|
18
|
-
export interface SessionMessageItem {
|
|
19
|
-
info: Message
|
|
20
|
-
parts: Part[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface AssistantSessionMessageItem {
|
|
24
|
-
info: Message & { role: "assistant" }
|
|
25
|
-
parts: Part[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface DelegationProgress {
|
|
29
|
-
toolCalls: number
|
|
30
|
-
lastUpdate: Date
|
|
31
|
-
lastMessage?: string
|
|
32
|
-
lastMessageAt?: Date
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface Delegation {
|
|
36
|
-
id: string // OpenCode session ID (same as sessionID — used for delegation_read, opencode -s, etc.)
|
|
37
|
-
sessionID: string // Same as id — kept for clarity in API calls
|
|
38
|
-
parentSessionID: string
|
|
39
|
-
parentMessageID: string
|
|
40
|
-
parentAgent: string
|
|
41
|
-
prompt: string
|
|
42
|
-
agent: string
|
|
43
|
-
model?: string // Full "provider/model" string (e.g. "minimax/MiniMax-M2.5")
|
|
44
|
-
status: "running" | "completed" | "error" | "cancelled" | "timeout"
|
|
45
|
-
startedAt: Date
|
|
46
|
-
completedAt?: Date
|
|
47
|
-
duration?: string
|
|
48
|
-
progress: DelegationProgress
|
|
49
|
-
error?: string
|
|
50
|
-
title?: string
|
|
51
|
-
description?: string
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface DelegateInput {
|
|
55
|
-
parentSessionID: string
|
|
56
|
-
parentMessageID: string
|
|
57
|
-
parentAgent: string
|
|
58
|
-
prompt: string
|
|
59
|
-
agent: string
|
|
60
|
-
model?: string // Full "provider/model" string — split via parseModel() before passing to session.prompt()
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface DelegationListItem {
|
|
64
|
-
id: string
|
|
65
|
-
status: string
|
|
66
|
-
title?: string
|
|
67
|
-
description?: string
|
|
68
|
-
agent?: string
|
|
69
|
-
duration?: string
|
|
70
|
-
startedAt?: Date
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface ReadDelegationArgs {
|
|
74
|
-
id: string
|
|
75
|
-
mode?: "simple" | "full"
|
|
76
|
-
include_thinking?: boolean
|
|
77
|
-
include_tools?: boolean
|
|
78
|
-
since_message_id?: string
|
|
79
|
-
limit?: number
|
|
80
|
-
}
|
package/src/plugin/utils.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { OpencodeClient, Logger } from "./types"
|
|
2
|
-
|
|
3
|
-
// Structured logger → OpenCode log API
|
|
4
|
-
// Catches errors silently to avoid disrupting tool execution
|
|
5
|
-
export function createLogger(client: OpencodeClient): Logger {
|
|
6
|
-
const log = (level: "debug" | "info" | "warn" | "error", message: string) =>
|
|
7
|
-
client.app.log({ body: { service: "async-agent", level, message } }).catch(() => {})
|
|
8
|
-
return {
|
|
9
|
-
debug: (msg: string) => log("debug", msg),
|
|
10
|
-
info: (msg: string) => log("info", msg),
|
|
11
|
-
warn: (msg: string) => log("warn", msg),
|
|
12
|
-
error: (msg: string) => log("error", msg),
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Shows toast in OpenCode TUI (top-right notification)
|
|
17
|
-
// Casts client to any + optional chaining = graceful no-op if unavailable
|
|
18
|
-
export function showToast(
|
|
19
|
-
client: OpencodeClient,
|
|
20
|
-
title: string,
|
|
21
|
-
message: string,
|
|
22
|
-
variant: "info" | "success" | "error" = "info",
|
|
23
|
-
duration = 3000,
|
|
24
|
-
) {
|
|
25
|
-
const tuiClient = client as any
|
|
26
|
-
if (!tuiClient.tui?.showToast) return
|
|
27
|
-
tuiClient.tui.showToast({
|
|
28
|
-
body: { title, message, variant, duration },
|
|
29
|
-
}).catch(() => {})
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Format ms duration into human-readable string (e.g. "2m 30s")
|
|
33
|
-
export function formatDuration(startedAt: Date, completedAt?: Date): string {
|
|
34
|
-
const end = completedAt || new Date()
|
|
35
|
-
const diffMs = end.getTime() - startedAt.getTime()
|
|
36
|
-
const diffSec = Math.floor(diffMs / 1000)
|
|
37
|
-
|
|
38
|
-
if (diffSec < 60) {
|
|
39
|
-
return `${diffSec}s`
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const diffMin = Math.floor(diffSec / 60)
|
|
43
|
-
if (diffMin < 60) {
|
|
44
|
-
const secs = diffSec % 60
|
|
45
|
-
return secs > 0 ? `${diffMin}m ${secs}s` : `${diffMin}m`
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const diffHour = Math.floor(diffMin / 60)
|
|
49
|
-
const mins = diffMin % 60
|
|
50
|
-
return mins > 0 ? `${diffHour}h ${mins}m` : `${diffHour}h`
|
|
51
|
-
}
|