@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.
Files changed (2) hide show
  1. package/dist/index.js +125 -11
  2. 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
- function registerSessionId(sessionId, runId) {
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 (!args.wait) return result;
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
- return statusTool({
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
- return answerPermissionTool({
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
- return followUpTool({
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
- return eventsTool({
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
- return shutdownTool({
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.2.1",
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.2.1",
30
+ "@dougbots/avenor-core": "^0.3.0",
31
31
  "zod": "^4.4.3"
32
32
  },
33
33
  "peerDependencies": {