@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 +16 -1
- package/dist/index.js +363 -155
- package/package.json +2 -2
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
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
405
|
+
"",
|
|
406
|
+
"To answer: call `avenor_answer_permission` with:",
|
|
188
407
|
` run_id: "${runId}"`,
|
|
189
408
|
` request_id: "${permission.id}"`,
|
|
190
|
-
|
|
409
|
+
" option_id: \"allow_once\" | \"allow_always\" | \"deny\""
|
|
191
410
|
];
|
|
192
|
-
await
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
251
|
-
}
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
"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.
|
|
30
|
+
"@dougbots/avenor-core": "^0.4.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@opencode-ai/plugin": "1.15.3"
|