@dougbots/avenor-opencode 0.3.7 → 0.4.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.d.ts CHANGED
@@ -1,6 +1,21 @@
1
1
  import { Plugin } from "@opencode-ai/plugin";
2
+ import { RunSnapshot, StatusResult } from "@dougbots/avenor-core";
2
3
 
3
4
  //#region src/plugin.d.ts
5
+ type TrackedRun = {
6
+ runId: string;
7
+ runtimeId?: string;
8
+ sessionId?: string;
9
+ agent: string;
10
+ orchestratorSessionId: string;
11
+ label: string;
12
+ supervisorId?: string;
13
+ monitoring: boolean;
14
+ latestSeq?: number;
15
+ lastSnapshot?: RunSnapshot;
16
+ };
17
+ declare function terminalStatus(snapshot: RunSnapshot, fallback?: StatusResult): string;
18
+ declare function buildWaitingText(run: TrackedRun, snapshot: RunSnapshot): string;
4
19
  declare const AvenorPlugin: Plugin;
5
20
  //#endregion
6
- export { AvenorPlugin };
21
+ export { AvenorPlugin, buildWaitingText, terminalStatus };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import * as crypto from "node:crypto";
3
- import { answerPermissionTool, eventsTool, followUpTool, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
3
+ import { Supervisor, answerPermissionTool, createRunSnapshot, dial, eventsTool, followUpTool, inspectTool, observeRun, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
4
4
  //#region src/plugin.ts
5
5
  const TERMINAL_STATUSES = new Set([
6
6
  "done",
@@ -9,22 +9,83 @@ const TERMINAL_STATUSES = new Set([
9
9
  "killed"
10
10
  ]);
11
11
  const POLL_INTERVAL_MS = 3e3;
12
+ const FINAL_OUTPUT_CHARS = 1500;
13
+ const METADATA_TEXT_CHARS = 400;
14
+ const TRANSCRIPT_PREVIEW_ENTRIES = 3;
12
15
  function sleep(ms) {
13
16
  return new Promise((resolve) => setTimeout(resolve, ms));
14
17
  }
15
- function buildCompletionText(run, result) {
16
- const lines = [`Sub-agent "${run.label}" finished with status **${result.status}**.`];
17
- if (result.stop_reason && result.stop_reason.toUpperCase() !== result.status.toUpperCase()) lines.push(`Stop reason: ${result.stop_reason}`);
18
- if (result.session_id) lines.push(`Session: \`${result.session_id}\``);
19
- lines.push(`\nCall \`avenor_events\` with \`run_id: "${run.runId}"\` to review the output, or \`avenor_follow_up\` to iterate.`);
18
+ function clipTail(text, limit) {
19
+ if (!text) return void 0;
20
+ if (text.length <= limit) return text;
21
+ return text.slice(text.length - limit);
22
+ }
23
+ function terminalStatus(snapshot, fallback) {
24
+ if (fallback?.status) return fallback.status;
25
+ if (snapshot.phase && TERMINAL_STATUSES.has(snapshot.phase)) return snapshot.phase;
26
+ return snapshot.stop_reason ?? "done";
27
+ }
28
+ function formatLiveTool(snapshot) {
29
+ const tool = snapshot.live_tools[snapshot.live_tools.length - 1];
30
+ if (!tool) return void 0;
31
+ const base = tool.title ?? tool.kind ?? tool.id ?? tool.key;
32
+ const status = tool.status ? ` (${tool.status})` : "";
33
+ const preview = clipTail(tool.preview, 120);
34
+ return preview ? `${base}${status}: ${preview}` : `${base}${status}`;
35
+ }
36
+ function formatTranscriptEntry(entry) {
37
+ const text = clipTail(entry.text?.trim(), 120);
38
+ switch (entry.kind) {
39
+ case "assistant": return text ? `assistant: ${text}` : void 0;
40
+ case "thought": return text ? `thought: ${text}` : void 0;
41
+ case "user": return text ? `user: ${text}` : void 0;
42
+ case "tool": {
43
+ const title = entry.title ?? "tool";
44
+ const status = entry.status ? ` (${entry.status})` : "";
45
+ return text ? `${title}${status}: ${text}` : `${title}${status}`;
46
+ }
47
+ case "permission": return text ? `permission: ${text}` : void 0;
48
+ case "status": return text ? `status: ${text}` : void 0;
49
+ case "session": return text ? `session: ${text}` : void 0;
50
+ }
51
+ }
52
+ function formatTranscriptPreview(snapshot) {
53
+ const lines = snapshot.transcript.slice(-TRANSCRIPT_PREVIEW_ENTRIES).map(formatTranscriptEntry).filter((line) => Boolean(line));
54
+ if (lines.length === 0) return void 0;
55
+ return clipTail(lines.join("\n"), METADATA_TEXT_CHARS);
56
+ }
57
+ function buildSnapshotMetadata(snapshot) {
58
+ const metadata = {};
59
+ if (snapshot.phase) metadata.phase = snapshot.phase;
60
+ if (snapshot.latest_seq !== void 0) metadata.latest_seq = snapshot.latest_seq;
61
+ const liveTool = formatLiveTool(snapshot);
62
+ if (liveTool) metadata.live_tool = liveTool;
63
+ const transcriptPreview = formatTranscriptPreview(snapshot);
64
+ if (transcriptPreview) metadata.transcript_preview = transcriptPreview;
65
+ const pendingPermission = clipTail(snapshot.pending_permission?.description ?? snapshot.pending_permission?.question ?? snapshot.phase_label, METADATA_TEXT_CHARS);
66
+ if (pendingPermission) metadata.pending_permission = pendingPermission;
67
+ return metadata;
68
+ }
69
+ function buildWaitingText(run, snapshot) {
70
+ const permission = snapshot.pending_permission;
71
+ const lines = [`Sub-agent "${run.label}" is waiting for input.`];
72
+ if (permission) lines.push(`Permission request: ${permission.description ?? permission.question ?? permission.tool ?? "permission requested"}`, `To answer: call \`avenor_answer_permission\` with run_id "${run.runId}", option_id "allow_once" | "allow_always" | "deny"`);
73
+ else if (snapshot.phase_label) lines.push(`Question: ${snapshot.phase_label}`);
74
+ const transcriptPreview = formatTranscriptPreview(snapshot);
75
+ if (transcriptPreview) lines.push("", `Recent context:\n${transcriptPreview}`);
76
+ if (!permission) lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`avenor_status\` to re-check.`);
20
77
  return lines.join("\n");
21
78
  }
22
- function summarizeEvent(evt) {
23
- const type = String(evt.type ?? evt.event ?? "");
24
- if (type === "tool.call" || type === "tool.use") return `called ${String(evt.tool ?? evt.name ?? type)}`;
25
- if (type === "agent.message_chunk") return "thinking…";
26
- if (type.startsWith("permission.")) return `permission requested`;
27
- return type;
79
+ function buildCompletionText(run, snapshot, status, finalOutput) {
80
+ const lines = [`Sub-agent "${run.label}" finished with status **${terminalStatus(snapshot, status)}**.`];
81
+ const stopReason = status?.stop_reason ?? snapshot.stop_reason;
82
+ if (stopReason && stopReason.toUpperCase() !== terminalStatus(snapshot, status).toUpperCase()) lines.push(`Stop reason: ${stopReason}`);
83
+ const sessionId = status?.session_id ?? snapshot.identity.session_id;
84
+ if (sessionId) lines.push(`Session: \`${sessionId}\``);
85
+ const boundedOutput = clipTail(finalOutput ?? snapshot.final_output, FINAL_OUTPUT_CHARS);
86
+ if (boundedOutput) lines.push("", "Final output:", "```text", boundedOutput, "```");
87
+ lines.push("", `Call \`avenor_events\` with \`run_id: "${run.runId}"\` for raw events, or \`avenor_follow_up\` to iterate.`);
88
+ return lines.join("\n");
28
89
  }
29
90
  const CHANNEL_RE = /<channel\s+source="([^"]*)"(?:\s+from_run_id="([^"]*)")?(?:\s+from_role="([^"]*)")?[^>]*>([\s\S]*?)<\/channel>/g;
30
91
  function formatAgentMessage(source, fromRunId, fromRole, content) {
@@ -45,6 +106,7 @@ function formatChannelBlocks(text) {
45
106
  }
46
107
  const AvenorPlugin = async (ctx) => {
47
108
  const trackedRuns = /* @__PURE__ */ new Map();
109
+ const activeMonitors = /* @__PURE__ */ new Map();
48
110
  const sessionIdToRunId = /* @__PURE__ */ new Map();
49
111
  const runtimeIdToRunId = /* @__PURE__ */ new Map();
50
112
  let parentRunId = "";
@@ -55,55 +117,215 @@ const AvenorPlugin = async (ctx) => {
55
117
  if (!parentRunId) parentRunId = crypto.randomUUID();
56
118
  return parentRunId;
57
119
  }
120
+ function unregisterRun(runId) {
121
+ trackedRuns.delete(runId);
122
+ for (const [sessionId, mappedRunId] of [...sessionIdToRunId.entries()]) if (mappedRunId === runId) sessionIdToRunId.delete(sessionId);
123
+ for (const [runtimeId, mappedRunId] of [...runtimeIdToRunId.entries()]) if (mappedRunId === runId) runtimeIdToRunId.delete(runtimeId);
124
+ }
58
125
  function registerSessionId(sessionId, runtimeId, runId) {
126
+ const run = trackedRuns.get(runId);
127
+ if (run) {
128
+ if (sessionId) run.sessionId = sessionId;
129
+ if (runtimeId) run.runtimeId = runtimeId;
130
+ }
59
131
  if (sessionId && !sessionIdToRunId.has(sessionId)) sessionIdToRunId.set(sessionId, runId);
60
132
  if (runtimeId && !runtimeIdToRunId.has(runtimeId)) runtimeIdToRunId.set(runtimeId, runId);
61
133
  }
62
- async function monitorRun(run) {
63
- let firstPoll = true;
64
- while (true) {
65
- if (!firstPoll) await sleep(POLL_INTERVAL_MS);
66
- firstPoll = false;
67
- let raw;
134
+ function syncRunSnapshot(run, snapshot) {
135
+ run.lastSnapshot = snapshot;
136
+ if (snapshot.latest_seq !== void 0) run.latestSeq = snapshot.latest_seq;
137
+ registerSessionId(snapshot.identity.session_id, snapshot.identity.runtime_id, run.runId);
138
+ }
139
+ async function primeTrackedRun(run, options = {}) {
140
+ try {
141
+ const raw = await statusTool({
142
+ runId: run.runId,
143
+ supervisorId: run.supervisorId
144
+ });
145
+ const status = Array.isArray(raw) ? raw[0] : raw;
146
+ if (!status) return void 0;
147
+ registerSessionId(status.session_id, status.runtime_id, run.runId);
148
+ if (status.latest_seq !== void 0 && options.captureLatestSeq) run.latestSeq = status.latest_seq;
149
+ return status;
150
+ } catch {
151
+ return;
152
+ }
153
+ }
154
+ async function createObserver(run) {
155
+ if (!run.supervisorId) run.supervisorId = Supervisor.currentInstance()?.supervisorId;
156
+ if (!run.runtimeId) await primeTrackedRun(run);
157
+ if (!run.supervisorId || !run.runtimeId) throw new Error(`unable to observe run ${run.runId}`);
158
+ return observeRun(await dial(run.supervisorId), {
159
+ runtime_id: run.runtimeId,
160
+ replay: true,
161
+ ...run.latestSeq !== void 0 ? { after_seq: run.latestSeq } : {},
162
+ close_client: true
163
+ });
164
+ }
165
+ async function stopMonitor(runId, reason) {
166
+ const active = activeMonitors.get(runId);
167
+ if (!active) return;
168
+ active.stopReason = active.stopReason ?? reason;
169
+ await active.observer?.close().catch(() => {});
170
+ }
171
+ async function loadInspection(run) {
172
+ try {
173
+ return await inspectTool({
174
+ runId: run.runId,
175
+ supervisorId: run.supervisorId
176
+ });
177
+ } catch {
178
+ return;
179
+ }
180
+ }
181
+ async function observeTrackedRun(run, options = {}) {
182
+ const existing = activeMonitors.get(run.runId);
183
+ if (existing) return existing.promise;
184
+ run.monitoring = true;
185
+ const handle = { promise: Promise.resolve({
186
+ kind: "stopped",
187
+ snapshot: run.lastSnapshot
188
+ }) };
189
+ handle.promise = (async () => {
190
+ let observer;
191
+ let unsubscribe;
192
+ let abortListener;
68
193
  try {
69
- raw = await statusTool({
70
- runId: run.runId,
71
- supervisorId: run.supervisorId
194
+ observer = await createObserver(run);
195
+ handle.observer = observer;
196
+ if (handle.stopReason) {
197
+ await observer.close();
198
+ const current = observer.snapshot();
199
+ syncRunSnapshot(run, current);
200
+ if (handle.stopReason === "permission-routed") return {
201
+ kind: "waiting",
202
+ snapshot: current,
203
+ notified: true
204
+ };
205
+ if (handle.stopReason === "abort") return {
206
+ kind: "aborted",
207
+ snapshot: current
208
+ };
209
+ return {
210
+ kind: "shutdown",
211
+ snapshot: current
212
+ };
213
+ }
214
+ if (options.abort) {
215
+ const onAbort = () => {
216
+ stopMonitor(run.runId, "abort");
217
+ };
218
+ options.abort.addEventListener("abort", onAbort, { once: true });
219
+ abortListener = () => options.abort?.removeEventListener("abort", onAbort);
220
+ if (options.abort.aborted) await stopMonitor(run.runId, "abort");
221
+ }
222
+ unsubscribe = observer.subscribe((snapshot) => {
223
+ syncRunSnapshot(run, snapshot);
224
+ options.onSnapshot?.(snapshot);
72
225
  });
73
- } catch {
74
- continue;
75
- }
76
- const result = Array.isArray(raw) ? raw[0] : raw;
77
- if (!result) continue;
78
- registerSessionId(result.session_id, result.runtime_id, run.runId);
79
- if (TERMINAL_STATUSES.has(result.status)) {
80
- trackedRuns.delete(run.runId);
81
- await ctx.client.session.promptAsync({
82
- path: { id: run.orchestratorSessionId },
83
- body: { parts: [{
84
- type: "text",
85
- text: buildCompletionText(run, result)
86
- }] }
87
- }).catch(() => {});
88
- return;
89
- }
90
- if (result.status === "waiting") {
91
- const perm = result.pending_permission;
92
- const lines = [`Sub-agent "${run.label}" is waiting for input.`];
93
- if (perm) lines.push(`Permission request: ${perm.description}`, `To answer: call \`avenor_answer_permission\` with run_id "${run.runId}", option_id "allow_once" | "allow_always" | "deny"`);
94
- else if (result.phase_label) lines.push(`Question: ${result.phase_label}`);
95
- lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`avenor_status\` to re-check.`);
96
- await ctx.client.session.promptAsync({
97
- path: { id: run.orchestratorSessionId },
98
- body: { parts: [{
99
- type: "text",
100
- text: lines.join("\n")
101
- }] }
102
- }).catch(() => {});
226
+ while (true) {
227
+ const snapshot = await observer.next();
228
+ if (!snapshot) {
229
+ const current = observer.snapshot();
230
+ syncRunSnapshot(run, current);
231
+ if (handle.stopReason === "permission-routed") return {
232
+ kind: "waiting",
233
+ snapshot: current,
234
+ notified: true
235
+ };
236
+ if (handle.stopReason === "abort") return {
237
+ kind: "aborted",
238
+ snapshot: current
239
+ };
240
+ if (handle.stopReason === "shutdown") return {
241
+ kind: "shutdown",
242
+ snapshot: current
243
+ };
244
+ if (current.pending_permission || current.phase === "waiting") return {
245
+ kind: "waiting",
246
+ snapshot: current
247
+ };
248
+ if (current.ended) return {
249
+ kind: "terminal",
250
+ snapshot: current
251
+ };
252
+ return {
253
+ kind: "stopped",
254
+ snapshot: current
255
+ };
256
+ }
257
+ syncRunSnapshot(run, snapshot);
258
+ if (snapshot.pending_permission || snapshot.phase === "waiting") return {
259
+ kind: "waiting",
260
+ snapshot
261
+ };
262
+ if (snapshot.ended) return {
263
+ kind: "terminal",
264
+ snapshot
265
+ };
266
+ }
267
+ } catch (error) {
268
+ const message = error instanceof Error ? error.message : String(error);
269
+ const snapshot = observer?.snapshot() ?? run.lastSnapshot ?? {
270
+ ...createRunSnapshot(),
271
+ identity: {
272
+ run_id: run.runId,
273
+ run_label: run.label,
274
+ runtime_id: run.runtimeId,
275
+ agent: run.agent
276
+ }
277
+ };
278
+ syncRunSnapshot(run, snapshot);
279
+ return {
280
+ kind: "error",
281
+ snapshot,
282
+ error: message
283
+ };
284
+ } finally {
285
+ abortListener?.();
286
+ unsubscribe?.();
287
+ await observer?.close().catch(() => {});
288
+ if (activeMonitors.get(run.runId) === handle) activeMonitors.delete(run.runId);
103
289
  run.monitoring = false;
104
- return;
105
290
  }
291
+ })();
292
+ activeMonitors.set(run.runId, handle);
293
+ return handle.promise;
294
+ }
295
+ async function promptSession(sessionId, text) {
296
+ await ctx.client.session.promptAsync({
297
+ path: { id: sessionId },
298
+ body: { parts: [{
299
+ type: "text",
300
+ text
301
+ }] }
302
+ }).catch(() => {});
303
+ }
304
+ async function monitorRun(run) {
305
+ const outcome = await observeTrackedRun(run);
306
+ if (outcome.kind === "error") {
307
+ await promptSession(run.orchestratorSessionId, `Monitoring of "${run.label}" (run_id: ${run.runId}) failed: ${outcome.error ?? "unknown error"}. The run may still be active — use avenor_status to check.`);
308
+ return;
309
+ }
310
+ if (outcome.kind === "waiting") {
311
+ if (!outcome.notified) await promptSession(run.orchestratorSessionId, buildWaitingText(run, outcome.snapshot));
312
+ return;
106
313
  }
314
+ if (outcome.kind === "terminal") {
315
+ const inspection = await loadInspection(run);
316
+ const snapshot = inspection?.snapshot ?? outcome.snapshot;
317
+ await promptSession(run.orchestratorSessionId, buildCompletionText(run, snapshot, inspection?.status, inspection?.final_output));
318
+ unregisterRun(run.runId);
319
+ return;
320
+ }
321
+ if (outcome.kind === "aborted" || outcome.kind === "shutdown") unregisterRun(run.runId);
322
+ }
323
+ async function stopTrackedRuns(predicate) {
324
+ const runs = [...trackedRuns.values()].filter(predicate);
325
+ await Promise.all(runs.map(async (run) => {
326
+ await stopMonitor(run.runId, "shutdown");
327
+ unregisterRun(run.runId);
328
+ }));
107
329
  }
108
330
  async function pollChannelMessages(sessionId) {
109
331
  if (channelPolling || !brokerUrl || !parentRunId || !parentToken) return;
@@ -169,10 +391,7 @@ const AvenorPlugin = async (ctx) => {
169
391
  if (event.type !== "session.idle") return;
170
392
  const { sessionID } = event.properties;
171
393
  pollChannelMessages(sessionID).catch(console.error);
172
- for (const run of trackedRuns.values()) if (run.orchestratorSessionId === sessionID && !run.monitoring) {
173
- run.monitoring = true;
174
- monitorRun(run).catch(console.error);
175
- }
394
+ for (const run of trackedRuns.values()) if (run.orchestratorSessionId === sessionID && !run.monitoring) monitorRun(run).catch(console.error);
176
395
  },
177
396
  "permission.ask": async (permission, _output) => {
178
397
  const runId = sessionIdToRunId.get(permission.sessionID);
@@ -183,19 +402,15 @@ const AvenorPlugin = async (ctx) => {
183
402
  const lines = [
184
403
  `Sub-agent "${run.label}" is requesting permission for \`${permission.type}\`.`,
185
404
  ...patternStr ? [`Pattern: \`${patternStr}\``] : [],
186
- ``,
187
- `To answer: call \`avenor_answer_permission\` with:`,
405
+ "",
406
+ "To answer: call `avenor_answer_permission` with:",
188
407
  ` run_id: "${runId}"`,
189
408
  ` request_id: "${permission.id}"`,
190
- ` option_id: "allow_once" | "allow_always" | "deny"`
409
+ " option_id: \"allow_once\" | \"allow_always\" | \"deny\""
191
410
  ];
192
- await ctx.client.session.promptAsync({
193
- path: { id: run.orchestratorSessionId },
194
- body: { parts: [{
195
- type: "text",
196
- text: lines.join("\n")
197
- }] }
198
- }).catch(() => {});
411
+ await primeTrackedRun(run, { captureLatestSeq: true });
412
+ await stopMonitor(run.runId, "permission-routed");
413
+ await promptSession(run.orchestratorSessionId, lines.join("\n"));
199
414
  },
200
415
  "chat.message": async (message, _output) => {
201
416
  if (!message?.parts) return;
@@ -224,7 +439,7 @@ const AvenorPlugin = async (ctx) => {
224
439
  wait: tool.schema.boolean().default(true).describe("Block until complete with live status updates. False = fire-and-forget.")
225
440
  },
226
441
  async execute(args, context) {
227
- const parentRunId = ensureParentRunId();
442
+ const ownerRunId = ensureParentRunId();
228
443
  const result = await spawnTool({
229
444
  agent: args.agent,
230
445
  prompt: args.prompt,
@@ -236,111 +451,73 @@ const AvenorPlugin = async (ctx) => {
236
451
  backend: args.backend,
237
452
  serverUrl: args.server_url,
238
453
  supervisorId: args.supervisor_id,
239
- parent_run_id: parentRunId
454
+ parent_run_id: ownerRunId
240
455
  });
241
456
  if (result.broker_url) brokerUrl = result.broker_url;
242
457
  if (result.parent_token) parentToken = result.parent_token;
243
- const supervisorId = result.supervisor_id || args.supervisor_id;
244
- trackedRuns.set(result.run_id, {
458
+ const run = {
245
459
  runId: result.run_id,
460
+ runtimeId: result.runtime_id,
246
461
  agent: args.agent,
247
462
  orchestratorSessionId: context.sessionID,
248
463
  label: result.label,
249
- supervisorId,
250
- monitoring: args.wait
251
- });
252
- if (result.runtime_id) runtimeIdToRunId.set(result.runtime_id, result.run_id);
464
+ supervisorId: result.supervisor_id || args.supervisor_id,
465
+ monitoring: false
466
+ };
467
+ trackedRuns.set(result.run_id, run);
468
+ registerSessionId(void 0, result.runtime_id, result.run_id);
469
+ await primeTrackedRun(run);
253
470
  if (!args.wait) {
254
- const run = trackedRuns.get(result.run_id);
255
- if (run) {
256
- run.monitoring = true;
257
- monitorRun(run).catch(console.error);
258
- }
471
+ monitorRun(run).catch(console.error);
259
472
  return {
260
473
  title: `${result.label} — dispatched`,
261
474
  output: `Dispatched "${result.label}" (run_id: ${result.run_id}). Call avenor_status with run_id to check progress, or wait for the completion notification.`
262
475
  };
263
476
  }
264
477
  context.metadata({ title: `Starting ${result.label}…` });
265
- let firstPoll = true;
266
- while (!context.abort.aborted) {
267
- if (!firstPoll) await sleep(POLL_INTERVAL_MS);
268
- firstPoll = false;
269
- if (context.abort.aborted) break;
270
- let raw;
271
- try {
272
- raw = await statusTool({
273
- runId: result.run_id,
274
- supervisorId
478
+ const outcome = await observeTrackedRun(run, {
479
+ abort: context.abort,
480
+ onSnapshot: (snapshot) => {
481
+ context.metadata({
482
+ title: snapshot.pending_permission ? `[blocked] ${result.label}` : result.label,
483
+ metadata: buildSnapshotMetadata(snapshot)
275
484
  });
276
- } catch {
277
- continue;
278
485
  }
279
- const status = Array.isArray(raw) ? raw[0] : raw;
280
- if (!status) continue;
281
- registerSessionId(status.session_id, status.runtime_id, result.run_id);
282
- if (status.pending_permission) context.metadata({
283
- title: `[blocked] ${result.label}`,
486
+ });
487
+ if (outcome.kind === "terminal") {
488
+ const inspection = await loadInspection(run);
489
+ const snapshot = inspection?.snapshot ?? outcome.snapshot;
490
+ unregisterRun(result.run_id);
491
+ return {
492
+ title: `${result.label} — ${terminalStatus(snapshot, inspection?.status)}`,
493
+ output: buildCompletionText(run, snapshot, inspection?.status, inspection?.final_output),
284
494
  metadata: {
285
- status: "permission",
286
- permission: status.pending_permission.description
495
+ run_id: result.run_id,
496
+ session_id: inspection?.status.session_id ?? snapshot.identity.session_id,
497
+ final_output: clipTail(inspection?.final_output ?? snapshot.final_output, FINAL_OUTPUT_CHARS),
498
+ ...buildSnapshotMetadata(snapshot)
287
499
  }
288
- });
289
- else {
290
- let lastAction = "";
291
- if (!supervisorId) try {
292
- const { events } = await eventsTool({
293
- runId: result.run_id,
294
- limit: 3
295
- });
296
- const last = events[events.length - 1];
297
- if (last) lastAction = summarizeEvent(last);
298
- } catch {}
299
- context.metadata({
300
- title: result.label,
301
- metadata: {
302
- status: status.status,
303
- ...status.phase_label && { phase: status.phase_label },
304
- ...lastAction && { last: lastAction }
305
- }
306
- });
307
- }
308
- if (TERMINAL_STATUSES.has(status.status)) {
309
- trackedRuns.delete(result.run_id);
310
- return {
311
- title: `${result.label} — ${status.status}`,
312
- output: buildCompletionText({
313
- runId: result.run_id,
314
- label: result.label
315
- }, status),
316
- metadata: {
317
- status: status.status,
318
- run_id: result.run_id,
319
- session_id: status.session_id
320
- }
321
- };
322
- }
323
- if (status.status === "waiting") {
324
- const perm = status.pending_permission;
325
- const lines = [`Sub-agent "${result.label}" is waiting for input.`];
326
- if (perm) lines.push(`Permission request: ${perm.description}`, `To answer: call \`avenor_answer_permission\` with run_id "${result.run_id}", option_id "allow_once" | "allow_always" | "deny"`);
327
- else if (status.phase_label) lines.push(`Question: ${status.phase_label}`);
328
- lines.push(`Use \`avenor_follow_up\` with run_id "${result.run_id}" to respond, or \`avenor_status\` to re-check.`);
329
- const tracked = trackedRuns.get(result.run_id);
330
- if (tracked) tracked.monitoring = false;
331
- return {
332
- title: `${result.label} — waiting`,
333
- output: lines.join("\n"),
334
- metadata: {
335
- status: "waiting",
336
- run_id: result.run_id,
337
- session_id: status.session_id,
338
- ...perm && { pending_permission: perm }
339
- }
340
- };
341
- }
500
+ };
342
501
  }
343
- trackedRuns.delete(result.run_id);
502
+ if (outcome.kind === "waiting") {
503
+ const inspection = await loadInspection(run);
504
+ const snapshot = inspection?.snapshot ?? outcome.snapshot;
505
+ return {
506
+ title: `${result.label} — waiting`,
507
+ output: buildWaitingText(run, snapshot),
508
+ metadata: {
509
+ run_id: result.run_id,
510
+ session_id: inspection?.status.session_id ?? snapshot.identity.session_id,
511
+ ...buildSnapshotMetadata(snapshot)
512
+ }
513
+ };
514
+ }
515
+ if (outcome.kind === "error") return {
516
+ title: `${result.label} — monitoring error`,
517
+ output: `Monitoring failed: ${outcome.error ?? "unknown error"}. The run may still be active — use avenor_status to check.`,
518
+ metadata: { run_id: result.run_id }
519
+ };
520
+ unregisterRun(result.run_id);
344
521
  return {
345
522
  title: `${result.label} — aborted`,
346
523
  output: `Monitoring of "${result.label}" (run_id: ${result.run_id}) was interrupted. The run may still be active — use avenor_status to check.`,
@@ -388,13 +565,25 @@ const AvenorPlugin = async (ctx) => {
388
565
  label: tool.schema.string().optional().describe("Override label (defaults to <original>-followup)"),
389
566
  supervisor_id: tool.schema.string().optional().describe("Reuse an existing supervisor by socket path")
390
567
  },
391
- async execute(args, _context) {
568
+ async execute(args, context) {
392
569
  const result = await followUpTool({
393
570
  runId: args.run_id,
394
571
  message: args.message,
395
572
  label: args.label,
396
573
  supervisorId: args.supervisor_id
397
574
  });
575
+ const priorRun = trackedRuns.get(args.run_id);
576
+ const run = {
577
+ runId: result.run_id,
578
+ agent: priorRun?.agent ?? "agent",
579
+ orchestratorSessionId: context.sessionID,
580
+ label: result.label,
581
+ supervisorId: args.supervisor_id ?? Supervisor.currentInstance()?.supervisorId,
582
+ monitoring: false
583
+ };
584
+ trackedRuns.set(result.run_id, run);
585
+ await primeTrackedRun(run);
586
+ monitorRun(run).catch(console.error);
398
587
  return JSON.stringify(result, null, 2);
399
588
  }
400
589
  }),
@@ -416,6 +605,24 @@ const AvenorPlugin = async (ctx) => {
416
605
  return JSON.stringify(result, null, 2);
417
606
  }
418
607
  }),
608
+ avenor_inspect: tool({
609
+ description: "Inspect a run with reduced snapshot, transcript, tools, permissions, and final output.",
610
+ args: {
611
+ run_id: tool.schema.string().describe("Run ID to inspect"),
612
+ limit: tool.schema.number().optional().describe("Max events to reduce into the snapshot (default 50)"),
613
+ after_seq: tool.schema.number().optional().describe("Only include events after this sequence number"),
614
+ supervisor_id: tool.schema.string().optional().describe("Reuse an existing supervisor by socket path")
615
+ },
616
+ async execute(args, _context) {
617
+ const result = await inspectTool({
618
+ runId: args.run_id,
619
+ limit: args.limit,
620
+ after_seq: args.after_seq,
621
+ supervisorId: args.supervisor_id
622
+ });
623
+ return JSON.stringify(result, null, 2);
624
+ }
625
+ }),
419
626
  avenor_shutdown: tool({
420
627
  description: "Shut down the avenor supervisor and clean up temp files.",
421
628
  args: {
@@ -423,6 +630,7 @@ const AvenorPlugin = async (ctx) => {
423
630
  force: tool.schema.boolean().optional().describe("Force shutdown instead of graceful")
424
631
  },
425
632
  async execute(args, _context) {
633
+ await stopTrackedRuns((run) => !args.supervisor_id || run.supervisorId === args.supervisor_id);
426
634
  const result = await shutdownTool({
427
635
  supervisorId: args.supervisor_id,
428
636
  force: args.force
@@ -434,4 +642,4 @@ const AvenorPlugin = async (ctx) => {
434
642
  };
435
643
  };
436
644
  //#endregion
437
- export { AvenorPlugin };
645
+ export { AvenorPlugin, buildWaitingText, terminalStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-opencode",
3
- "version": "0.3.7",
3
+ "version": "0.4.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.3.7"
30
+ "@dougbots/avenor-core": "^0.4.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@opencode-ai/plugin": "1.15.3"