@dougbots/avenor-opencode 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +125 -11
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
3
4
|
import { answerPermissionTool, eventsTool, followUpTool, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
|
|
4
5
|
//#region src/plugin.ts
|
|
5
6
|
const TERMINAL_STATUSES = new Set([
|
|
@@ -26,15 +27,44 @@ function summarizeEvent(evt) {
|
|
|
26
27
|
if (type.startsWith("permission.")) return `permission requested`;
|
|
27
28
|
return type;
|
|
28
29
|
}
|
|
30
|
+
const CHANNEL_RE = /<channel\s+source="([^"]*)"(?:\s+from_run_id="([^"]*)")?(?:\s+from_role="([^"]*)")?[^>]*>([\s\S]*?)<\/channel>/g;
|
|
31
|
+
function formatAgentMessage(source, fromRunId, fromRole, content) {
|
|
32
|
+
return [
|
|
33
|
+
`📨 ${source.replace(/^agent-/, "") || source} (${fromRunId ? fromRunId.slice(0, 8) : ""})`,
|
|
34
|
+
"",
|
|
35
|
+
...content.trim().split("\n").map((l) => ` ${l}`)
|
|
36
|
+
].join("\n") + "\n";
|
|
37
|
+
}
|
|
38
|
+
function renderChannelMessage(match, source, fromRunId, fromRole, content) {
|
|
39
|
+
return formatAgentMessage(source, fromRunId, fromRole, content);
|
|
40
|
+
}
|
|
41
|
+
function formatChannelBlocks(text) {
|
|
42
|
+
CHANNEL_RE.lastIndex = 0;
|
|
43
|
+
return text.replace(CHANNEL_RE, (...args) => {
|
|
44
|
+
return renderChannelMessage(args[0], args[1], args[2], args[3], args[4]);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
29
47
|
const AvenorPlugin = async (ctx) => {
|
|
30
48
|
const trackedRuns = /* @__PURE__ */ new Map();
|
|
31
49
|
const sessionIdToRunId = /* @__PURE__ */ new Map();
|
|
32
|
-
|
|
50
|
+
const runtimeIdToRunId = /* @__PURE__ */ new Map();
|
|
51
|
+
let parentRunId = "";
|
|
52
|
+
let parentToken = "";
|
|
53
|
+
let brokerUrl = "";
|
|
54
|
+
let channelPolling = false;
|
|
55
|
+
function ensureParentRunId() {
|
|
56
|
+
if (!parentRunId) parentRunId = crypto.randomUUID();
|
|
57
|
+
return parentRunId;
|
|
58
|
+
}
|
|
59
|
+
function registerSessionId(sessionId, runtimeId, runId) {
|
|
33
60
|
if (sessionId && !sessionIdToRunId.has(sessionId)) sessionIdToRunId.set(sessionId, runId);
|
|
61
|
+
if (runtimeId && !runtimeIdToRunId.has(runtimeId)) runtimeIdToRunId.set(runtimeId, runId);
|
|
34
62
|
}
|
|
35
63
|
async function monitorRun(run) {
|
|
64
|
+
let firstPoll = true;
|
|
36
65
|
while (true) {
|
|
37
|
-
await sleep(POLL_INTERVAL_MS);
|
|
66
|
+
if (!firstPoll) await sleep(POLL_INTERVAL_MS);
|
|
67
|
+
firstPoll = false;
|
|
38
68
|
let raw;
|
|
39
69
|
try {
|
|
40
70
|
raw = await statusTool({
|
|
@@ -46,7 +76,7 @@ const AvenorPlugin = async (ctx) => {
|
|
|
46
76
|
}
|
|
47
77
|
const result = Array.isArray(raw) ? raw[0] : raw;
|
|
48
78
|
if (!result) continue;
|
|
49
|
-
registerSessionId(result.session_id, run.runId);
|
|
79
|
+
registerSessionId(result.session_id, result.runtime_id, run.runId);
|
|
50
80
|
if (TERMINAL_STATUSES.has(result.status)) {
|
|
51
81
|
trackedRuns.delete(run.runId);
|
|
52
82
|
await ctx.client.session.promptAsync({
|
|
@@ -60,10 +90,70 @@ const AvenorPlugin = async (ctx) => {
|
|
|
60
90
|
}
|
|
61
91
|
}
|
|
62
92
|
}
|
|
93
|
+
async function pollChannelMessages(sessionId) {
|
|
94
|
+
if (channelPolling || !brokerUrl || !parentRunId || !parentToken) return;
|
|
95
|
+
channelPolling = true;
|
|
96
|
+
let consecutiveErrors = 0;
|
|
97
|
+
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
98
|
+
try {
|
|
99
|
+
while (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) {
|
|
100
|
+
await sleep(POLL_INTERVAL_MS);
|
|
101
|
+
let msgs;
|
|
102
|
+
try {
|
|
103
|
+
const resp = await fetch(`${brokerUrl}/poll-control`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
run_id: parentRunId,
|
|
108
|
+
token: parentToken
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
if (!resp.ok) {
|
|
112
|
+
consecutiveErrors++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
msgs = await resp.json();
|
|
116
|
+
consecutiveErrors = 0;
|
|
117
|
+
} catch {
|
|
118
|
+
consecutiveErrors++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
for (const msg of msgs ?? []) {
|
|
122
|
+
if (msg.type !== "agent_message") continue;
|
|
123
|
+
let payload;
|
|
124
|
+
try {
|
|
125
|
+
payload = typeof msg.payload === "string" ? JSON.parse(msg.payload) : msg.payload;
|
|
126
|
+
} catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const text = typeof payload?.message === "string" ? payload.message : "";
|
|
130
|
+
if (!text) continue;
|
|
131
|
+
const from = payload.from_run_id ?? msg.from_run_id ?? "unknown";
|
|
132
|
+
const role = payload.role ?? "";
|
|
133
|
+
const resolvedRunId = trackedRuns.has(from) ? from : sessionIdToRunId.get(from) ?? runtimeIdToRunId.get(from);
|
|
134
|
+
const channelText = formatAgentMessage((resolvedRunId ? trackedRuns.get(resolvedRunId) : void 0)?.agent ?? (role ? `agent-${role}` : "agent"), from, role, text);
|
|
135
|
+
try {
|
|
136
|
+
await ctx.client.session.promptAsync({
|
|
137
|
+
path: { id: sessionId },
|
|
138
|
+
body: { parts: [{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: channelText
|
|
141
|
+
}] }
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
channelPolling = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
63
152
|
return {
|
|
64
153
|
event: async ({ event }) => {
|
|
65
154
|
if (event.type !== "session.idle") return;
|
|
66
155
|
const { sessionID } = event.properties;
|
|
156
|
+
pollChannelMessages(sessionID).catch(console.error);
|
|
67
157
|
for (const run of trackedRuns.values()) if (run.orchestratorSessionId === sessionID && !run.monitoring) {
|
|
68
158
|
run.monitoring = true;
|
|
69
159
|
monitorRun(run).catch(console.error);
|
|
@@ -92,6 +182,16 @@ const AvenorPlugin = async (ctx) => {
|
|
|
92
182
|
}] }
|
|
93
183
|
}).catch(() => {});
|
|
94
184
|
},
|
|
185
|
+
"chat.message": async (message, _output) => {
|
|
186
|
+
if (!message?.parts) return;
|
|
187
|
+
for (const part of message.parts) if (part?.type === "text" && typeof part?.text === "string" && part.text.includes("<channel ")) part.text = formatChannelBlocks(part.text);
|
|
188
|
+
},
|
|
189
|
+
"experimental.text.complete": async (_input, output) => {
|
|
190
|
+
if (typeof output.text !== "string" || !output.text.includes("<channel ")) return;
|
|
191
|
+
try {
|
|
192
|
+
output.text = formatChannelBlocks(output.text);
|
|
193
|
+
} catch {}
|
|
194
|
+
},
|
|
95
195
|
tool: {
|
|
96
196
|
avenor_spawn: tool({
|
|
97
197
|
description: "Dispatch an agent run via avenor. Blocks by default, showing live progress as an updating tool call. Set wait=false for fire-and-forget — you will be re-prompted automatically on completion.",
|
|
@@ -108,6 +208,7 @@ const AvenorPlugin = async (ctx) => {
|
|
|
108
208
|
wait: z.boolean().default(true).describe("Block until complete with live status updates. False = fire-and-forget.")
|
|
109
209
|
},
|
|
110
210
|
async execute(args, context) {
|
|
211
|
+
const parentRunId = ensureParentRunId();
|
|
111
212
|
const result = await spawnTool({
|
|
112
213
|
agent: args.agent,
|
|
113
214
|
prompt: args.prompt,
|
|
@@ -118,17 +219,25 @@ const AvenorPlugin = async (ctx) => {
|
|
|
118
219
|
model: args.model,
|
|
119
220
|
backend: args.backend,
|
|
120
221
|
serverUrl: args.server_url,
|
|
121
|
-
supervisorId: args.supervisor_id
|
|
222
|
+
supervisorId: args.supervisor_id,
|
|
223
|
+
parent_run_id: parentRunId
|
|
122
224
|
});
|
|
225
|
+
if (result.broker_url) brokerUrl = result.broker_url;
|
|
226
|
+
if (result.parent_token) parentToken = result.parent_token;
|
|
123
227
|
const supervisorId = result.supervisor_id || args.supervisor_id;
|
|
124
228
|
trackedRuns.set(result.run_id, {
|
|
125
229
|
runId: result.run_id,
|
|
230
|
+
agent: args.agent,
|
|
126
231
|
orchestratorSessionId: context.sessionID,
|
|
127
232
|
label: result.label,
|
|
128
233
|
supervisorId,
|
|
129
234
|
monitoring: args.wait
|
|
130
235
|
});
|
|
131
|
-
if (
|
|
236
|
+
if (result.runtime_id) runtimeIdToRunId.set(result.runtime_id, result.run_id);
|
|
237
|
+
if (!args.wait) return {
|
|
238
|
+
title: `${result.label} — dispatched`,
|
|
239
|
+
output: `Dispatched "${result.label}" (run_id: ${result.run_id}). Call avenor_status with run_id to check progress, or wait for the completion notification.`
|
|
240
|
+
};
|
|
132
241
|
context.metadata({ title: `Starting ${result.label}…` });
|
|
133
242
|
let firstPoll = true;
|
|
134
243
|
while (!context.abort.aborted) {
|
|
@@ -146,7 +255,7 @@ const AvenorPlugin = async (ctx) => {
|
|
|
146
255
|
}
|
|
147
256
|
const status = Array.isArray(raw) ? raw[0] : raw;
|
|
148
257
|
if (!status) continue;
|
|
149
|
-
registerSessionId(status.session_id, result.run_id);
|
|
258
|
+
registerSessionId(status.session_id, status.runtime_id, result.run_id);
|
|
150
259
|
if (status.pending_permission) context.metadata({
|
|
151
260
|
title: `[blocked] ${result.label}`,
|
|
152
261
|
metadata: {
|
|
@@ -204,10 +313,11 @@ const AvenorPlugin = async (ctx) => {
|
|
|
204
313
|
supervisor_id: z.string().optional().describe("Reuse an existing supervisor by socket path")
|
|
205
314
|
},
|
|
206
315
|
async execute(args, _context) {
|
|
207
|
-
|
|
316
|
+
const result = await statusTool({
|
|
208
317
|
runId: args.run_id,
|
|
209
318
|
supervisorId: args.supervisor_id
|
|
210
319
|
});
|
|
320
|
+
return JSON.stringify(result, null, 2);
|
|
211
321
|
}
|
|
212
322
|
}),
|
|
213
323
|
avenor_answer_permission: tool({
|
|
@@ -219,12 +329,13 @@ const AvenorPlugin = async (ctx) => {
|
|
|
219
329
|
supervisor_id: z.string().optional().describe("Reuse an existing supervisor by socket path")
|
|
220
330
|
},
|
|
221
331
|
async execute(args, _context) {
|
|
222
|
-
|
|
332
|
+
const result = await answerPermissionTool({
|
|
223
333
|
runId: args.run_id,
|
|
224
334
|
optionId: args.option_id,
|
|
225
335
|
requestId: args.request_id,
|
|
226
336
|
supervisorId: args.supervisor_id
|
|
227
337
|
});
|
|
338
|
+
return JSON.stringify(result, null, 2);
|
|
228
339
|
}
|
|
229
340
|
}),
|
|
230
341
|
avenor_follow_up: tool({
|
|
@@ -236,12 +347,13 @@ const AvenorPlugin = async (ctx) => {
|
|
|
236
347
|
supervisor_id: z.string().optional().describe("Reuse an existing supervisor by socket path")
|
|
237
348
|
},
|
|
238
349
|
async execute(args, _context) {
|
|
239
|
-
|
|
350
|
+
const result = await followUpTool({
|
|
240
351
|
runId: args.run_id,
|
|
241
352
|
message: args.message,
|
|
242
353
|
label: args.label,
|
|
243
354
|
supervisorId: args.supervisor_id
|
|
244
355
|
});
|
|
356
|
+
return JSON.stringify(result, null, 2);
|
|
245
357
|
}
|
|
246
358
|
}),
|
|
247
359
|
avenor_events: tool({
|
|
@@ -253,12 +365,13 @@ const AvenorPlugin = async (ctx) => {
|
|
|
253
365
|
supervisor_id: z.string().optional().describe("Reuse an existing supervisor by socket path")
|
|
254
366
|
},
|
|
255
367
|
async execute(args, _context) {
|
|
256
|
-
|
|
368
|
+
const result = await eventsTool({
|
|
257
369
|
runId: args.run_id,
|
|
258
370
|
types: args.types,
|
|
259
371
|
limit: args.limit,
|
|
260
372
|
supervisorId: args.supervisor_id
|
|
261
373
|
});
|
|
374
|
+
return JSON.stringify(result, null, 2);
|
|
262
375
|
}
|
|
263
376
|
}),
|
|
264
377
|
avenor_shutdown: tool({
|
|
@@ -268,10 +381,11 @@ const AvenorPlugin = async (ctx) => {
|
|
|
268
381
|
force: z.boolean().optional().describe("Force shutdown instead of graceful")
|
|
269
382
|
},
|
|
270
383
|
async execute(args, _context) {
|
|
271
|
-
|
|
384
|
+
const result = await shutdownTool({
|
|
272
385
|
supervisorId: args.supervisor_id,
|
|
273
386
|
force: args.force
|
|
274
387
|
});
|
|
388
|
+
return JSON.stringify(result, null, 2);
|
|
275
389
|
}
|
|
276
390
|
})
|
|
277
391
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougbots/avenor-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"dev": "tsdown --watch"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@dougbots/avenor-core": "^0.
|
|
30
|
+
"@dougbots/avenor-core": "^0.3.0",
|
|
31
31
|
"zod": "^4.4.3"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|