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