@dougbots/avenor-opencode 0.2.1 → 0.3.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/dist/index.js +127 -12
- 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.",
|
|
@@ -99,6 +199,7 @@ const AvenorPlugin = async (ctx) => {
|
|
|
99
199
|
agent: z.string().describe("Agent name (required, no default)"),
|
|
100
200
|
prompt: z.string().optional().describe("Prompt text"),
|
|
101
201
|
prompt_file: z.string().optional().describe("Path to prompt file"),
|
|
202
|
+
dir: z.string().optional().describe("Working directory for the run (defaults to the session project directory)"),
|
|
102
203
|
label: z.string().optional().describe("Human-readable label for the run"),
|
|
103
204
|
timeout: z.string().optional().describe("Timeout duration (e.g. 3600s)"),
|
|
104
205
|
model: z.string().optional().describe("Model override"),
|
|
@@ -108,27 +209,36 @@ const AvenorPlugin = async (ctx) => {
|
|
|
108
209
|
wait: z.boolean().default(true).describe("Block until complete with live status updates. False = fire-and-forget.")
|
|
109
210
|
},
|
|
110
211
|
async execute(args, context) {
|
|
212
|
+
const parentRunId = ensureParentRunId();
|
|
111
213
|
const result = await spawnTool({
|
|
112
214
|
agent: args.agent,
|
|
113
215
|
prompt: args.prompt,
|
|
114
216
|
promptFile: args.prompt_file,
|
|
115
217
|
label: args.label,
|
|
116
|
-
dir: context.directory,
|
|
218
|
+
dir: args.dir ?? context.directory,
|
|
117
219
|
timeout: args.timeout,
|
|
118
220
|
model: args.model,
|
|
119
221
|
backend: args.backend,
|
|
120
222
|
serverUrl: args.server_url,
|
|
121
|
-
supervisorId: args.supervisor_id
|
|
223
|
+
supervisorId: args.supervisor_id,
|
|
224
|
+
parent_run_id: parentRunId
|
|
122
225
|
});
|
|
226
|
+
if (result.broker_url) brokerUrl = result.broker_url;
|
|
227
|
+
if (result.parent_token) parentToken = result.parent_token;
|
|
123
228
|
const supervisorId = result.supervisor_id || args.supervisor_id;
|
|
124
229
|
trackedRuns.set(result.run_id, {
|
|
125
230
|
runId: result.run_id,
|
|
231
|
+
agent: args.agent,
|
|
126
232
|
orchestratorSessionId: context.sessionID,
|
|
127
233
|
label: result.label,
|
|
128
234
|
supervisorId,
|
|
129
235
|
monitoring: args.wait
|
|
130
236
|
});
|
|
131
|
-
if (
|
|
237
|
+
if (result.runtime_id) runtimeIdToRunId.set(result.runtime_id, result.run_id);
|
|
238
|
+
if (!args.wait) return {
|
|
239
|
+
title: `${result.label} — dispatched`,
|
|
240
|
+
output: `Dispatched "${result.label}" (run_id: ${result.run_id}). Call avenor_status with run_id to check progress, or wait for the completion notification.`
|
|
241
|
+
};
|
|
132
242
|
context.metadata({ title: `Starting ${result.label}…` });
|
|
133
243
|
let firstPoll = true;
|
|
134
244
|
while (!context.abort.aborted) {
|
|
@@ -146,7 +256,7 @@ const AvenorPlugin = async (ctx) => {
|
|
|
146
256
|
}
|
|
147
257
|
const status = Array.isArray(raw) ? raw[0] : raw;
|
|
148
258
|
if (!status) continue;
|
|
149
|
-
registerSessionId(status.session_id, result.run_id);
|
|
259
|
+
registerSessionId(status.session_id, status.runtime_id, result.run_id);
|
|
150
260
|
if (status.pending_permission) context.metadata({
|
|
151
261
|
title: `[blocked] ${result.label}`,
|
|
152
262
|
metadata: {
|
|
@@ -204,10 +314,11 @@ const AvenorPlugin = async (ctx) => {
|
|
|
204
314
|
supervisor_id: z.string().optional().describe("Reuse an existing supervisor by socket path")
|
|
205
315
|
},
|
|
206
316
|
async execute(args, _context) {
|
|
207
|
-
|
|
317
|
+
const result = await statusTool({
|
|
208
318
|
runId: args.run_id,
|
|
209
319
|
supervisorId: args.supervisor_id
|
|
210
320
|
});
|
|
321
|
+
return JSON.stringify(result, null, 2);
|
|
211
322
|
}
|
|
212
323
|
}),
|
|
213
324
|
avenor_answer_permission: tool({
|
|
@@ -219,12 +330,13 @@ const AvenorPlugin = async (ctx) => {
|
|
|
219
330
|
supervisor_id: z.string().optional().describe("Reuse an existing supervisor by socket path")
|
|
220
331
|
},
|
|
221
332
|
async execute(args, _context) {
|
|
222
|
-
|
|
333
|
+
const result = await answerPermissionTool({
|
|
223
334
|
runId: args.run_id,
|
|
224
335
|
optionId: args.option_id,
|
|
225
336
|
requestId: args.request_id,
|
|
226
337
|
supervisorId: args.supervisor_id
|
|
227
338
|
});
|
|
339
|
+
return JSON.stringify(result, null, 2);
|
|
228
340
|
}
|
|
229
341
|
}),
|
|
230
342
|
avenor_follow_up: tool({
|
|
@@ -236,12 +348,13 @@ const AvenorPlugin = async (ctx) => {
|
|
|
236
348
|
supervisor_id: z.string().optional().describe("Reuse an existing supervisor by socket path")
|
|
237
349
|
},
|
|
238
350
|
async execute(args, _context) {
|
|
239
|
-
|
|
351
|
+
const result = await followUpTool({
|
|
240
352
|
runId: args.run_id,
|
|
241
353
|
message: args.message,
|
|
242
354
|
label: args.label,
|
|
243
355
|
supervisorId: args.supervisor_id
|
|
244
356
|
});
|
|
357
|
+
return JSON.stringify(result, null, 2);
|
|
245
358
|
}
|
|
246
359
|
}),
|
|
247
360
|
avenor_events: tool({
|
|
@@ -253,12 +366,13 @@ const AvenorPlugin = async (ctx) => {
|
|
|
253
366
|
supervisor_id: z.string().optional().describe("Reuse an existing supervisor by socket path")
|
|
254
367
|
},
|
|
255
368
|
async execute(args, _context) {
|
|
256
|
-
|
|
369
|
+
const result = await eventsTool({
|
|
257
370
|
runId: args.run_id,
|
|
258
371
|
types: args.types,
|
|
259
372
|
limit: args.limit,
|
|
260
373
|
supervisorId: args.supervisor_id
|
|
261
374
|
});
|
|
375
|
+
return JSON.stringify(result, null, 2);
|
|
262
376
|
}
|
|
263
377
|
}),
|
|
264
378
|
avenor_shutdown: tool({
|
|
@@ -268,10 +382,11 @@ const AvenorPlugin = async (ctx) => {
|
|
|
268
382
|
force: z.boolean().optional().describe("Force shutdown instead of graceful")
|
|
269
383
|
},
|
|
270
384
|
async execute(args, _context) {
|
|
271
|
-
|
|
385
|
+
const result = await shutdownTool({
|
|
272
386
|
supervisorId: args.supervisor_id,
|
|
273
387
|
force: args.force
|
|
274
388
|
});
|
|
389
|
+
return JSON.stringify(result, null, 2);
|
|
275
390
|
}
|
|
276
391
|
})
|
|
277
392
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougbots/avenor-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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": {
|