@dougbots/avenor-pi 0.3.7 → 0.4.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.d.ts +36 -2
- package/dist/index.js +1370 -598
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,58 +1,533 @@
|
|
|
1
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
1
|
+
import { Input, Key, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { answerPermissionTool, eventsTool, followUpTool, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
|
|
3
|
+
import { Supervisor, answerPermissionTool, dial, eventsTool, followUpTool, inspectTool, observeRun, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
|
|
4
4
|
//#region src/watch.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
6
|
+
const TRANSCRIPT_SCROLL_STEP = 4;
|
|
7
|
+
const ACTION_STATUS_CHARS = 180;
|
|
8
|
+
const PREVIEW_CHARS = 240;
|
|
9
|
+
const TOOL_PREVIEW_CHARS = 120;
|
|
10
|
+
const FINAL_OUTPUT_CHARS = 600;
|
|
11
|
+
const TRANSCRIPT_PADDING = 2;
|
|
12
|
+
const INSPECT_ERROR_PREFIX = "error: ";
|
|
13
|
+
const TERMINAL_STATUSES$1 = new Set([
|
|
14
|
+
"done",
|
|
15
|
+
"failed",
|
|
16
|
+
"timeout",
|
|
17
|
+
"killed"
|
|
18
|
+
]);
|
|
19
|
+
function emptySnapshot() {
|
|
20
|
+
return {
|
|
21
|
+
identity: {},
|
|
22
|
+
limits: {
|
|
23
|
+
transcript_entries: 64,
|
|
24
|
+
text_chars: 4e3,
|
|
25
|
+
tool_preview_chars: 1e3,
|
|
26
|
+
tools: 16,
|
|
27
|
+
permissions: 16,
|
|
28
|
+
legacy_keys: 128
|
|
29
|
+
},
|
|
30
|
+
ended: false,
|
|
31
|
+
assistant_text: "",
|
|
32
|
+
thought_text: "",
|
|
33
|
+
user_text: "",
|
|
34
|
+
transcript: [],
|
|
35
|
+
tools: [],
|
|
36
|
+
live_tools: [],
|
|
37
|
+
permissions: [],
|
|
38
|
+
_seen_seq_by_scope: {},
|
|
39
|
+
_recent_legacy_keys: []
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function sanitizeText(text) {
|
|
43
|
+
return text.replace(ANSI_PATTERN, "").replaceAll(" ", " ").replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
|
|
44
|
+
}
|
|
45
|
+
function clipText(text, limit) {
|
|
46
|
+
if (!text) return void 0;
|
|
47
|
+
const clean = sanitizeText(text).trim();
|
|
48
|
+
if (!clean) return void 0;
|
|
49
|
+
if (clean.length <= limit) return clean;
|
|
50
|
+
return `${clean.slice(0, Math.max(0, limit - 1))}…`;
|
|
51
|
+
}
|
|
52
|
+
function compactWhitespace$1(text, limit) {
|
|
53
|
+
const clean = clipText(text, limit);
|
|
54
|
+
if (!clean) return void 0;
|
|
55
|
+
return clean.replace(/\s+/g, " ").trim() || void 0;
|
|
56
|
+
}
|
|
57
|
+
function padLine(text, width) {
|
|
58
|
+
const clipped = truncateToWidth(text, width);
|
|
59
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
60
|
+
}
|
|
61
|
+
function wrapPrefixed(text, width, firstPrefix, nextPrefix = " ".repeat(Math.max(0, visibleWidth(firstPrefix)))) {
|
|
62
|
+
const clean = clipText(text, PREVIEW_CHARS);
|
|
63
|
+
if (!clean) return [];
|
|
64
|
+
return wrapTextWithAnsi(clean, Math.max(10, width - Math.max(visibleWidth(firstPrefix), visibleWidth(nextPrefix)))).map((line, index) => truncateToWidth(`${index === 0 ? firstPrefix : nextPrefix}${line}`, width));
|
|
65
|
+
}
|
|
66
|
+
function renderKv(theme, label, value, width) {
|
|
67
|
+
if (!value) return [];
|
|
68
|
+
return wrapPrefixed(value, width, theme.fg("dim", `${label}: `));
|
|
69
|
+
}
|
|
70
|
+
function usageSummary(usage) {
|
|
71
|
+
if (!usage) return void 0;
|
|
72
|
+
const order = [
|
|
73
|
+
"input_tokens",
|
|
74
|
+
"output_tokens",
|
|
75
|
+
"total_tokens",
|
|
76
|
+
"tokens"
|
|
77
|
+
];
|
|
78
|
+
const parts = [];
|
|
79
|
+
for (const key of order) {
|
|
80
|
+
const value = usage[key];
|
|
81
|
+
if (typeof value === "number" && Number.isFinite(value)) parts.push(`${key}=${value}`);
|
|
13
82
|
}
|
|
14
|
-
|
|
15
|
-
|
|
83
|
+
if (parts.length === 0) {
|
|
84
|
+
const entries = Object.entries(usage).filter(([, value]) => typeof value === "number" || typeof value === "string" || typeof value === "boolean").slice(0, 4).map(([key, value]) => `${key}=${String(value)}`);
|
|
85
|
+
if (entries.length === 0) return void 0;
|
|
86
|
+
return entries.join(" · ");
|
|
16
87
|
}
|
|
17
|
-
|
|
18
|
-
|
|
88
|
+
return parts.join(" · ");
|
|
89
|
+
}
|
|
90
|
+
function finalOutputPreview(status, snapshot) {
|
|
91
|
+
return clipText(status?.final_output ?? snapshot?.final_output ?? snapshot?.assistant_text, FINAL_OUTPUT_CHARS);
|
|
92
|
+
}
|
|
93
|
+
function currentStatusWord(status, snapshot) {
|
|
94
|
+
const raw = status?.status?.toLowerCase();
|
|
95
|
+
if (raw && TERMINAL_STATUSES$1.has(raw)) return raw;
|
|
96
|
+
if (snapshot.pending_permission) return "waiting";
|
|
97
|
+
if (snapshot.ended) {
|
|
98
|
+
const stop = snapshot.stop_reason?.toLowerCase();
|
|
99
|
+
if (stop && TERMINAL_STATUSES$1.has(stop)) return stop;
|
|
100
|
+
return raw && raw !== "running" ? raw : "done";
|
|
19
101
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
102
|
+
return raw ?? "running";
|
|
103
|
+
}
|
|
104
|
+
function isPromptable(status, snapshot) {
|
|
105
|
+
const current = currentStatusWord(status, snapshot);
|
|
106
|
+
return !TERMINAL_STATUSES$1.has(current) && current !== "unavailable";
|
|
107
|
+
}
|
|
108
|
+
function isFollowUpable(status, snapshot) {
|
|
109
|
+
return TERMINAL_STATUSES$1.has(currentStatusWord(status, snapshot)) || snapshot.ended;
|
|
110
|
+
}
|
|
111
|
+
function transcriptLabel(entry, theme) {
|
|
112
|
+
switch (entry.kind) {
|
|
113
|
+
case "assistant": return theme.fg("text", "A ");
|
|
114
|
+
case "thought": return theme.fg("muted", "~ ");
|
|
115
|
+
case "user": return theme.fg("accent", "> ");
|
|
116
|
+
case "tool": return theme.fg("toolTitle", `→ ${entry.title ?? "tool"}${entry.status ? ` [${entry.status}]` : ""} `);
|
|
117
|
+
case "permission": return theme.fg("warning", "🔒 ");
|
|
118
|
+
case "session": return theme.fg("success", "■ ");
|
|
119
|
+
case "status": return theme.fg("dim", "· ");
|
|
23
120
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
121
|
+
}
|
|
122
|
+
function transcriptText(entry) {
|
|
123
|
+
if (entry.kind === "tool") return compactWhitespace$1(entry.text, TOOL_PREVIEW_CHARS) ?? entry.title;
|
|
124
|
+
if (entry.kind === "session") return compactWhitespace$1(entry.text, PREVIEW_CHARS) ?? entry.status;
|
|
125
|
+
return compactWhitespace$1(entry.text, PREVIEW_CHARS);
|
|
126
|
+
}
|
|
127
|
+
function renderTranscriptLines(snapshot, width, theme) {
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const entry of snapshot.transcript) {
|
|
130
|
+
const text = transcriptText(entry);
|
|
131
|
+
if (!text) continue;
|
|
132
|
+
const prefix = transcriptLabel(entry, theme);
|
|
133
|
+
out.push(...wrapPrefixed(text, width, prefix));
|
|
134
|
+
}
|
|
135
|
+
if (snapshot.live_tools.length > 0) {
|
|
136
|
+
if (out.length > 0) out.push("");
|
|
137
|
+
out.push(theme.fg("dim", "live tools:"));
|
|
138
|
+
for (const tool of snapshot.live_tools) out.push(...renderToolState(tool, width, theme));
|
|
139
|
+
}
|
|
140
|
+
if (snapshot.pending_permission) {
|
|
141
|
+
if (out.length > 0) out.push("");
|
|
142
|
+
out.push(...wrapPrefixed(snapshot.pending_permission.description, width, theme.fg("warning", "pending: ")));
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function renderToolState(tool, width, theme) {
|
|
147
|
+
const preview = compactWhitespace$1(tool.preview, TOOL_PREVIEW_CHARS);
|
|
148
|
+
const firstPrefix = `${theme.fg("toolTitle", `→ ${tool.title ?? tool.kind ?? "tool"} `)}${(tool.live ? theme.fg("warning", "[running] ") : tool.status && `${theme.fg("dim", `[${tool.status}] `)}`) ?? ""}`;
|
|
149
|
+
return wrapPrefixed(preview ?? "(no preview)", width, firstPrefix);
|
|
150
|
+
}
|
|
151
|
+
function helpText(state) {
|
|
152
|
+
const actions = [
|
|
153
|
+
"↑/↓ scroll",
|
|
154
|
+
"pgup/pgdn page",
|
|
155
|
+
"home/end jump",
|
|
156
|
+
"esc close"
|
|
157
|
+
];
|
|
158
|
+
if (state.status && isPromptable(state.status, state.snapshot)) actions.unshift("enter send prompt", "ctrl-c cancel run");
|
|
159
|
+
else if (isFollowUpable(state.status, state.snapshot)) actions.unshift("enter follow up");
|
|
160
|
+
return actions.join(" · ");
|
|
161
|
+
}
|
|
162
|
+
var RunInspectorOverlay = class {
|
|
163
|
+
tui;
|
|
164
|
+
theme;
|
|
165
|
+
keybindings;
|
|
166
|
+
deps;
|
|
167
|
+
done;
|
|
168
|
+
onTrackRun;
|
|
169
|
+
input = new Input();
|
|
170
|
+
state;
|
|
171
|
+
observer = null;
|
|
172
|
+
observerUnsubscribe = null;
|
|
173
|
+
refreshPending = false;
|
|
174
|
+
scrollOffset = 0;
|
|
175
|
+
closed = false;
|
|
176
|
+
actionPending = false;
|
|
177
|
+
actionError;
|
|
178
|
+
_focused = false;
|
|
179
|
+
get focused() {
|
|
180
|
+
return this._focused;
|
|
181
|
+
}
|
|
182
|
+
set focused(value) {
|
|
183
|
+
this._focused = value;
|
|
184
|
+
this.input.focused = value;
|
|
185
|
+
}
|
|
186
|
+
constructor(tui, theme, keybindings, deps, run, done, onTrackRun) {
|
|
187
|
+
this.tui = tui;
|
|
188
|
+
this.theme = theme;
|
|
189
|
+
this.keybindings = keybindings;
|
|
190
|
+
this.deps = deps;
|
|
191
|
+
this.done = done;
|
|
192
|
+
this.onTrackRun = onTrackRun;
|
|
193
|
+
this.state = {
|
|
194
|
+
run,
|
|
195
|
+
snapshot: emptySnapshot(),
|
|
196
|
+
loading: true
|
|
197
|
+
};
|
|
198
|
+
this.input.onSubmit = (value) => {
|
|
199
|
+
const text = value.trim();
|
|
200
|
+
if (!text) return;
|
|
201
|
+
this.input.setValue("");
|
|
202
|
+
this.submit(text);
|
|
203
|
+
};
|
|
204
|
+
this.reload(true);
|
|
205
|
+
}
|
|
206
|
+
requestRender() {
|
|
207
|
+
this.tui.requestRender();
|
|
208
|
+
}
|
|
209
|
+
close() {
|
|
210
|
+
if (this.closed) return;
|
|
211
|
+
this.closed = true;
|
|
212
|
+
this.cleanup();
|
|
213
|
+
this.done();
|
|
214
|
+
}
|
|
215
|
+
dispose() {
|
|
216
|
+
if (this.closed) return;
|
|
217
|
+
this.closed = true;
|
|
218
|
+
this.cleanup();
|
|
219
|
+
}
|
|
220
|
+
invalidate() {
|
|
221
|
+
this.input.invalidate();
|
|
222
|
+
}
|
|
223
|
+
async cleanup() {
|
|
224
|
+
this.observerUnsubscribe?.();
|
|
225
|
+
this.observerUnsubscribe = null;
|
|
226
|
+
if (this.observer) {
|
|
227
|
+
const observer = this.observer;
|
|
228
|
+
this.observer = null;
|
|
229
|
+
await observer.close().catch(() => {});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async reload(withObserver) {
|
|
233
|
+
this.state = {
|
|
234
|
+
...this.state,
|
|
235
|
+
loading: true,
|
|
236
|
+
error: void 0,
|
|
237
|
+
actionMessage: void 0
|
|
238
|
+
};
|
|
239
|
+
this.requestRender();
|
|
240
|
+
try {
|
|
241
|
+
const result = await this.deps.inspectRun(this.state.run);
|
|
242
|
+
if (this.closed) return;
|
|
243
|
+
const nextRun = {
|
|
244
|
+
...this.state.run,
|
|
245
|
+
runId: result.status.run_id ?? this.state.run.runId,
|
|
246
|
+
label: result.label ?? this.state.run.label,
|
|
247
|
+
agent: result.status.agent ?? this.state.run.agent,
|
|
248
|
+
runtimeId: result.status.runtime_id ?? result.snapshot.identity.runtime_id ?? this.state.run.runtimeId
|
|
249
|
+
};
|
|
250
|
+
this.state = {
|
|
251
|
+
run: nextRun,
|
|
252
|
+
status: result.status,
|
|
253
|
+
snapshot: result.snapshot,
|
|
254
|
+
loading: false
|
|
255
|
+
};
|
|
256
|
+
this.onTrackRun?.(nextRun, result.status);
|
|
257
|
+
this.requestRender();
|
|
258
|
+
await this.cleanup();
|
|
259
|
+
if (!withObserver) return;
|
|
260
|
+
if (TERMINAL_STATUSES$1.has(currentStatusWord(result.status, result.snapshot))) return;
|
|
261
|
+
if (!(nextRun.runtimeId ?? result.snapshot.identity.runtime_id)) return;
|
|
262
|
+
const observer = await this.deps.observeRun(nextRun, result.snapshot.latest_seq, result.snapshot);
|
|
263
|
+
if (this.closed || !observer) {
|
|
264
|
+
await observer?.close().catch(() => {});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const applySnapshot = (snapshot) => {
|
|
268
|
+
this.state = {
|
|
269
|
+
...this.state,
|
|
270
|
+
snapshot: {
|
|
271
|
+
...snapshot,
|
|
272
|
+
identity: {
|
|
273
|
+
...snapshot.identity,
|
|
274
|
+
run_id: snapshot.identity.run_id ?? this.state.status?.run_id ?? this.state.run.runId
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
if (snapshot.ended && !this.refreshPending) {
|
|
279
|
+
this.refreshPending = true;
|
|
280
|
+
this.reload(false).finally(() => {
|
|
281
|
+
this.refreshPending = false;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
this.requestRender();
|
|
285
|
+
};
|
|
286
|
+
this.observer = observer;
|
|
287
|
+
this.observerUnsubscribe = observer.subscribe((snapshot) => {
|
|
288
|
+
applySnapshot(snapshot);
|
|
289
|
+
});
|
|
290
|
+
const current = observer.snapshot();
|
|
291
|
+
if (current.last_event || current.transcript.length > 0 || current.pending_permission || current.ended) applySnapshot(current);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (this.closed) return;
|
|
294
|
+
this.state = {
|
|
295
|
+
...this.state,
|
|
296
|
+
loading: false,
|
|
297
|
+
error: error instanceof Error ? error.message : String(error)
|
|
298
|
+
};
|
|
299
|
+
this.requestRender();
|
|
35
300
|
}
|
|
36
|
-
|
|
301
|
+
}
|
|
302
|
+
runAction(work, message) {
|
|
303
|
+
if (this.actionPending) return;
|
|
304
|
+
this.actionPending = true;
|
|
305
|
+
this.actionError = void 0;
|
|
306
|
+
if (message) this.state = {
|
|
307
|
+
...this.state,
|
|
308
|
+
actionMessage: clipText(message, ACTION_STATUS_CHARS)
|
|
309
|
+
};
|
|
310
|
+
this.requestRender();
|
|
311
|
+
work().catch((error) => {
|
|
312
|
+
this.actionError = error instanceof Error ? error.message : String(error);
|
|
313
|
+
}).finally(() => {
|
|
314
|
+
this.actionPending = false;
|
|
315
|
+
this.requestRender();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
submit(text) {
|
|
319
|
+
const runtimeId = this.state.run.runtimeId ?? this.state.status?.runtime_id ?? this.state.snapshot.identity.runtime_id;
|
|
320
|
+
if (this.state.status && isPromptable(this.state.status, this.state.snapshot) && runtimeId) {
|
|
321
|
+
this.runAction(async () => {
|
|
322
|
+
await this.deps.promptRun(this.state.run, runtimeId, text);
|
|
323
|
+
this.state = {
|
|
324
|
+
...this.state,
|
|
325
|
+
actionMessage: clipText(`Queued prompt: ${text}`, ACTION_STATUS_CHARS)
|
|
326
|
+
};
|
|
327
|
+
}, "sending prompt…");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!isFollowUpable(this.state.status, this.state.snapshot)) return;
|
|
331
|
+
this.runAction(async () => {
|
|
332
|
+
const next = await this.deps.followUpRun(this.state.run, text);
|
|
333
|
+
this.state = {
|
|
334
|
+
...this.state,
|
|
335
|
+
run: {
|
|
336
|
+
...this.state.run,
|
|
337
|
+
runId: next.run_id,
|
|
338
|
+
label: next.label,
|
|
339
|
+
runtimeId: void 0
|
|
340
|
+
},
|
|
341
|
+
actionMessage: clipText(`Started follow-up run ${next.label}`, ACTION_STATUS_CHARS)
|
|
342
|
+
};
|
|
343
|
+
this.scrollOffset = 0;
|
|
344
|
+
await this.reload(true);
|
|
345
|
+
}, "starting follow-up…");
|
|
346
|
+
}
|
|
347
|
+
cancelCurrentRun() {
|
|
348
|
+
const runtimeId = this.state.run.runtimeId ?? this.state.status?.runtime_id ?? this.state.snapshot.identity.runtime_id;
|
|
349
|
+
if (!runtimeId) return;
|
|
350
|
+
this.runAction(async () => {
|
|
351
|
+
await this.deps.cancelRun(this.state.run, runtimeId);
|
|
352
|
+
this.state = {
|
|
353
|
+
...this.state,
|
|
354
|
+
actionMessage: "cancel requested"
|
|
355
|
+
};
|
|
356
|
+
}, "cancelling run…");
|
|
37
357
|
}
|
|
38
358
|
handleInput(data) {
|
|
39
|
-
if (data
|
|
359
|
+
if (matchesKey(data, Key.escape)) {
|
|
360
|
+
this.close();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (matchesKey(data, Key.up)) {
|
|
364
|
+
this.scrollOffset += TRANSCRIPT_SCROLL_STEP;
|
|
365
|
+
this.requestRender();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (matchesKey(data, Key.down)) {
|
|
369
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - TRANSCRIPT_SCROLL_STEP);
|
|
370
|
+
this.requestRender();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
374
|
+
this.scrollOffset += this.viewportHeight();
|
|
375
|
+
this.requestRender();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
379
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - this.viewportHeight());
|
|
380
|
+
this.requestRender();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (matchesKey(data, Key.home)) {
|
|
384
|
+
this.scrollOffset = Number.MAX_SAFE_INTEGER;
|
|
385
|
+
this.requestRender();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (matchesKey(data, Key.end)) {
|
|
389
|
+
this.scrollOffset = 0;
|
|
390
|
+
this.requestRender();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (matchesKey(data, Key.ctrl("c")) && this.state.status && isPromptable(this.state.status, this.state.snapshot)) {
|
|
394
|
+
this.cancelCurrentRun();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
this.input.handleInput(data);
|
|
398
|
+
this.requestRender();
|
|
40
399
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
400
|
+
viewportHeight() {
|
|
401
|
+
const rows = this.tui.terminal.rows || 30;
|
|
402
|
+
return Math.max(8, rows - 15);
|
|
403
|
+
}
|
|
404
|
+
render(width) {
|
|
405
|
+
const theme = this.theme;
|
|
406
|
+
const lines = [];
|
|
407
|
+
const horizontal = theme.fg("borderAccent", "─".repeat(Math.max(1, width)));
|
|
408
|
+
const innerWidth = Math.max(10, width);
|
|
409
|
+
lines.push(horizontal);
|
|
410
|
+
const title = this.state.run.label ?? this.state.run.runId;
|
|
411
|
+
const statusWord = currentStatusWord(this.state.status, this.state.snapshot);
|
|
412
|
+
const heading = `${theme.fg("accent", theme.bold(title))} ${theme.fg("dim", `(${this.state.run.runId})`)} ${theme.fg(statusWord === "done" ? "success" : statusWord === "failed" ? "error" : statusWord === "waiting" ? "warning" : "muted", statusWord)}`;
|
|
413
|
+
lines.push(truncateToWidth(heading, innerWidth));
|
|
414
|
+
const phase = this.state.snapshot.phase_label ?? this.state.status?.phase_label ?? this.state.snapshot.phase ?? this.state.status?.phase;
|
|
415
|
+
const summary = [
|
|
416
|
+
phase ? `phase=${phase}` : void 0,
|
|
417
|
+
this.state.snapshot.latest_seq !== void 0 ? `seq=${this.state.snapshot.latest_seq}` : void 0,
|
|
418
|
+
this.state.snapshot.identity.agent ?? this.state.status?.agent,
|
|
419
|
+
this.state.snapshot.identity.model ?? this.state.status?.model,
|
|
420
|
+
this.state.snapshot.identity.backend ?? this.state.status?.backend
|
|
421
|
+
].filter((value) => !!value).join(" · ");
|
|
422
|
+
lines.push(...renderKv(theme, "status", summary || void 0, innerWidth));
|
|
423
|
+
const identity = [
|
|
424
|
+
this.state.snapshot.identity.runtime_id ?? this.state.status?.runtime_id,
|
|
425
|
+
this.state.snapshot.identity.session_id ?? this.state.status?.session_id,
|
|
426
|
+
this.state.snapshot.identity.dir ?? this.state.status?.dir
|
|
427
|
+
].filter((value) => !!value).join(" · ");
|
|
428
|
+
lines.push(...renderKv(theme, "identity", identity || void 0, innerWidth));
|
|
429
|
+
lines.push(...renderKv(theme, "usage", usageSummary(this.state.snapshot.usage ?? this.state.status?.usage), innerWidth));
|
|
430
|
+
const finalOutput = finalOutputPreview(this.state.status, this.state.snapshot);
|
|
431
|
+
if (finalOutput && (this.state.snapshot.ended || TERMINAL_STATUSES$1.has(statusWord))) lines.push(...wrapPrefixed(finalOutput, innerWidth, theme.fg("success", "final: ")));
|
|
432
|
+
if (this.state.snapshot.pending_permission) {
|
|
433
|
+
lines.push(...wrapPrefixed(this.state.snapshot.pending_permission.description, innerWidth, theme.fg("warning", "permission: ")));
|
|
434
|
+
const optionText = this.state.snapshot.pending_permission.options.map((option) => `${option.option_id} (${option.label})`).join(", ");
|
|
435
|
+
lines.push(...wrapPrefixed(optionText, innerWidth, theme.fg("dim", "options: ")));
|
|
436
|
+
} else if (this.state.status?.pending_permission) {
|
|
437
|
+
lines.push(...wrapPrefixed(this.state.status.pending_permission.description, innerWidth, theme.fg("warning", "permission: ")));
|
|
438
|
+
const optionText = this.state.status.pending_permission.options.map((option) => `${option.option_id} (${option.label})`).join(", ");
|
|
439
|
+
lines.push(...wrapPrefixed(optionText, innerWidth, theme.fg("dim", "options: ")));
|
|
49
440
|
}
|
|
50
|
-
|
|
441
|
+
const liveTools = this.state.snapshot.live_tools.slice(-4);
|
|
442
|
+
if (liveTools.length > 0) {
|
|
443
|
+
lines.push(theme.fg("dim", "live tools:"));
|
|
444
|
+
for (const tool of liveTools) lines.push(...renderToolState(tool, innerWidth, theme));
|
|
445
|
+
}
|
|
446
|
+
if (this.state.loading) lines.push(theme.fg("muted", "loading…"));
|
|
447
|
+
if (this.state.actionMessage) lines.push(...wrapPrefixed(this.state.actionMessage, innerWidth, theme.fg("accent", "action: ")));
|
|
448
|
+
if (this.actionError) lines.push(...wrapPrefixed(this.actionError, innerWidth, theme.fg("error", INSPECT_ERROR_PREFIX)));
|
|
449
|
+
else if (this.state.error) lines.push(...wrapPrefixed(this.state.error, innerWidth, theme.fg("error", INSPECT_ERROR_PREFIX)));
|
|
450
|
+
lines.push(horizontal);
|
|
451
|
+
lines.push(theme.fg("dim", "transcript:"));
|
|
452
|
+
const transcript = renderTranscriptLines(this.state.snapshot, Math.max(10, innerWidth - TRANSCRIPT_PADDING), theme).map((line) => truncateToWidth(` ${line}`, innerWidth));
|
|
453
|
+
const viewportHeight = this.viewportHeight();
|
|
454
|
+
const scrollNotice = this.scrollOffset > 0 ? truncateToWidth(theme.fg("dim", `… ${this.scrollOffset} line${this.scrollOffset === 1 ? "" : "s"} below`), innerWidth) : void 0;
|
|
455
|
+
const transcriptCapacity = Math.max(1, viewportHeight - (scrollNotice ? 1 : 0));
|
|
456
|
+
const maxOffset = Math.max(0, transcript.length - transcriptCapacity);
|
|
457
|
+
if (this.scrollOffset > maxOffset) this.scrollOffset = maxOffset;
|
|
458
|
+
const end = transcript.length - this.scrollOffset;
|
|
459
|
+
const start = Math.max(0, end - transcriptCapacity);
|
|
460
|
+
const body = transcript.slice(start, end);
|
|
461
|
+
while (body.length < transcriptCapacity) body.push("");
|
|
462
|
+
if (scrollNotice) body.push(scrollNotice);
|
|
463
|
+
for (const line of body) lines.push(padLine(line, innerWidth));
|
|
464
|
+
lines.push(horizontal);
|
|
465
|
+
lines.push(...this.input.render(innerWidth).map((line) => truncateToWidth(line, innerWidth)));
|
|
466
|
+
const promptHint = this.state.status && isPromptable(this.state.status, this.state.snapshot) ? "send prompt to running/waiting run" : isFollowUpable(this.state.status, this.state.snapshot) ? "start a follow-up run from this transcript" : "read-only view";
|
|
467
|
+
lines.push(truncateToWidth(theme.fg("dim", `${promptHint} · ${helpText(this.state)}`), innerWidth));
|
|
468
|
+
lines.push(horizontal);
|
|
469
|
+
return lines.map((line) => truncateToWidth(line, innerWidth));
|
|
51
470
|
}
|
|
52
|
-
invalidate() {}
|
|
53
471
|
};
|
|
472
|
+
function matchingWatchRuns(runs, selector) {
|
|
473
|
+
const query = selector.trim().toLowerCase();
|
|
474
|
+
if (!query) return [];
|
|
475
|
+
const byId = runs.find((run) => run.runId.toLowerCase() === query);
|
|
476
|
+
if (byId) return [byId];
|
|
477
|
+
return runs.filter((run) => run.label?.toLowerCase() === query || run.agent?.toLowerCase() === query);
|
|
478
|
+
}
|
|
479
|
+
function watchRunChoice(run) {
|
|
480
|
+
return `${sanitizeText(run.label ?? run.runId)}${run.agent ? ` · ${sanitizeText(run.agent)}` : ""} · ${sanitizeText(run.runId).slice(0, 8)}`;
|
|
481
|
+
}
|
|
482
|
+
async function selectWatchRun(ctx, runs) {
|
|
483
|
+
const choices = runs.map(watchRunChoice);
|
|
484
|
+
const selected = await ctx.ui.select("Watch run:", choices);
|
|
485
|
+
if (!selected) return void 0;
|
|
486
|
+
return runs.find((_run, index) => choices[index] === selected);
|
|
487
|
+
}
|
|
488
|
+
async function openRunInspector(ctx, options) {
|
|
489
|
+
if (!ctx.hasUI) {
|
|
490
|
+
ctx.ui.notify("avenor-watch requires TUI mode", "warning");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const selector = options.initialRunId?.trim();
|
|
494
|
+
let tracked;
|
|
495
|
+
let runId;
|
|
496
|
+
if (selector) {
|
|
497
|
+
const matches = matchingWatchRuns(options.trackedRuns, selector);
|
|
498
|
+
tracked = matches.length === 1 ? matches[0] : matches.length > 1 ? await selectWatchRun(ctx, matches) : void 0;
|
|
499
|
+
if (matches.length > 1 && !tracked) return;
|
|
500
|
+
runId = tracked?.runId ?? selector;
|
|
501
|
+
} else {
|
|
502
|
+
if (options.trackedRuns.length === 0) {
|
|
503
|
+
ctx.ui.notify("No tracked runs", "warning");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
tracked = await selectWatchRun(ctx, options.trackedRuns);
|
|
507
|
+
if (!tracked) return;
|
|
508
|
+
runId = tracked.runId;
|
|
509
|
+
}
|
|
510
|
+
await ctx.ui.custom((tui, theme, keybindings, done) => new RunInspectorOverlay(tui, theme, keybindings, options.deps, tracked ?? { runId }, () => done(), options.onTrackRun), {
|
|
511
|
+
overlay: true,
|
|
512
|
+
overlayOptions: {
|
|
513
|
+
anchor: "center",
|
|
514
|
+
width: "95%",
|
|
515
|
+
maxHeight: "95%",
|
|
516
|
+
minWidth: 80
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
54
520
|
//#endregion
|
|
55
521
|
//#region src/types.ts
|
|
522
|
+
function findLiveStatusForTrackedRun(run, liveStatuses) {
|
|
523
|
+
const knownRunIds = new Set([run.runId, run.lastStatus?.run_id].filter((id) => !!id));
|
|
524
|
+
let runtimeMatch;
|
|
525
|
+
for (const status of liveStatuses) {
|
|
526
|
+
if (knownRunIds.has(status.run_id)) return status;
|
|
527
|
+
if (!run.supervisorId && run.runtimeId === status.runtime_id) runtimeMatch = status;
|
|
528
|
+
}
|
|
529
|
+
return runtimeMatch;
|
|
530
|
+
}
|
|
56
531
|
const TERMINAL_STATUSES = new Set([
|
|
57
532
|
"done",
|
|
58
533
|
"failed",
|
|
@@ -80,17 +555,48 @@ function formatRunLine(entry) {
|
|
|
80
555
|
//#endregion
|
|
81
556
|
//#region src/index.ts
|
|
82
557
|
const POLL_INTERVAL_MS = 3e3;
|
|
558
|
+
const INSPECT_LIMIT = 128;
|
|
559
|
+
const COMPLETION_OUTPUT_CHARS = 600;
|
|
560
|
+
const INSPECT_JSON_STRING_CHARS = 600;
|
|
561
|
+
const INSPECT_JSON_ARRAY_ITEMS = 24;
|
|
562
|
+
const INSPECT_JSON_OBJECT_KEYS = 24;
|
|
563
|
+
const INSPECT_JSON_DEPTH = 8;
|
|
564
|
+
const defaultDeps = {
|
|
565
|
+
spawnTool,
|
|
566
|
+
statusTool,
|
|
567
|
+
eventsTool,
|
|
568
|
+
answerPermissionTool,
|
|
569
|
+
followUpTool,
|
|
570
|
+
inspectTool,
|
|
571
|
+
shutdownTool,
|
|
572
|
+
observeRun,
|
|
573
|
+
dial,
|
|
574
|
+
Supervisor
|
|
575
|
+
};
|
|
83
576
|
function statusSupervisorId(requestedSupervisorId, spawnedSupervisorId) {
|
|
84
577
|
return requestedSupervisorId ? spawnedSupervisorId : void 0;
|
|
85
578
|
}
|
|
86
579
|
function sleep(ms) {
|
|
87
580
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
88
581
|
}
|
|
89
|
-
function
|
|
582
|
+
function normalizeMultilinePreview(text, limit = COMPLETION_OUTPUT_CHARS) {
|
|
583
|
+
if (!text) return void 0;
|
|
584
|
+
const clean = sanitizeText(text).trim();
|
|
585
|
+
if (!clean) return void 0;
|
|
586
|
+
return clean.length <= limit ? clean : `${clean.slice(0, Math.max(0, limit - 1))}…`;
|
|
587
|
+
}
|
|
588
|
+
function quotedLines(text) {
|
|
589
|
+
const preview = normalizeMultilinePreview(text);
|
|
590
|
+
if (!preview) return [];
|
|
591
|
+
return preview.split("\n").slice(0, 8).map((line) => `> ${line}`);
|
|
592
|
+
}
|
|
593
|
+
function buildCompletionText(run, result, finalOutput) {
|
|
90
594
|
const lines = [`Sub-agent "${run.label}" finished with status **${result.status}**.`];
|
|
595
|
+
const output = normalizeMultilinePreview(finalOutput ?? result.final_output);
|
|
596
|
+
if (output) lines.push("", "Final output:", ...quotedLines(output));
|
|
91
597
|
if (result.stop_reason && result.stop_reason.toUpperCase() !== result.status.toUpperCase()) lines.push(`Stop reason: ${result.stop_reason}`);
|
|
92
598
|
if (result.session_id) lines.push(`Session: \`${result.session_id}\``);
|
|
93
|
-
lines.push(
|
|
599
|
+
lines.push("", `Call \`avenor_inspect\` with \`run_id: "${run.runId}"\` for the bounded transcript/snapshot, or \`avenor_follow_up\` to iterate.`);
|
|
94
600
|
return lines.join("\n");
|
|
95
601
|
}
|
|
96
602
|
function buildWaitingText(run, status) {
|
|
@@ -98,342 +604,598 @@ function buildWaitingText(run, status) {
|
|
|
98
604
|
const lines = [`Sub-agent "${run.label}" (${run.agent}) is waiting for input.`];
|
|
99
605
|
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"`);
|
|
100
606
|
else if (status.phase_label) lines.push(`Question: ${status.phase_label}`);
|
|
101
|
-
if (!perm) lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`
|
|
607
|
+
if (!perm) lines.push(`Use \`avenor_follow_up\` with run_id "${run.runId}" to respond, or \`avenor_inspect\` to review context first.`);
|
|
102
608
|
return lines.join("\n");
|
|
103
609
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
610
|
+
function isTerminalStatus(status) {
|
|
611
|
+
return !!status && TERMINAL_STATUSES.has(status);
|
|
612
|
+
}
|
|
613
|
+
function renderStatusLines(entries) {
|
|
614
|
+
const active = entries.filter((e) => !isTerminalStatus(e.status));
|
|
615
|
+
if (active.length === 0) return [];
|
|
616
|
+
return active.map((e) => {
|
|
617
|
+
const emoji = statusEmoji(e.status);
|
|
618
|
+
const perm = e.pendingPermission ? " 🔒" : "";
|
|
619
|
+
const phase = e.phaseLabel ? ` [${e.phaseLabel}]` : "";
|
|
620
|
+
return `${emoji} ${e.label} — ${e.status}${phase}${perm}`;
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
function compactWhitespace(text, limit = 100) {
|
|
624
|
+
const clipped = clipText(text, limit);
|
|
625
|
+
return clipped ? clipped.replace(/\s+/g, " ").trim() : void 0;
|
|
626
|
+
}
|
|
627
|
+
function snapshotProgressText(label, snapshot) {
|
|
628
|
+
if (snapshot.pending_permission) return `[blocked] ${label} — permission: ${snapshot.pending_permission.description}`;
|
|
629
|
+
const phase = snapshot.phase_label ? ` (${snapshot.phase_label})` : snapshot.phase ? ` (${snapshot.phase})` : "";
|
|
630
|
+
const liveTool = snapshot.live_tools[snapshot.live_tools.length - 1];
|
|
631
|
+
if (liveTool) {
|
|
632
|
+
const toolName = liveTool.title ?? liveTool.kind ?? "tool";
|
|
633
|
+
const preview = compactWhitespace(liveTool.preview, 80);
|
|
634
|
+
return `${label}${phase} — ${toolName}${preview ? `: ${preview}` : ""}`;
|
|
635
|
+
}
|
|
636
|
+
const last = snapshot.transcript[snapshot.transcript.length - 1];
|
|
637
|
+
const transcriptWidth = 160;
|
|
638
|
+
const rendered = last ? compactWhitespace(renderTranscriptLines({
|
|
639
|
+
...snapshot,
|
|
640
|
+
transcript: [last],
|
|
641
|
+
live_tools: [],
|
|
642
|
+
pending_permission: void 0
|
|
643
|
+
}, transcriptWidth, {
|
|
644
|
+
fg: (_color, text) => text,
|
|
645
|
+
bg: (_color, text) => text,
|
|
646
|
+
bold: (text) => text,
|
|
647
|
+
italic: (text) => text
|
|
648
|
+
})[0], 80) : void 0;
|
|
649
|
+
return `${label}${phase}${rendered ? ` — ${rendered}` : ""}`;
|
|
650
|
+
}
|
|
651
|
+
function boundJsonValue(value, depth = 0) {
|
|
652
|
+
if (depth >= INSPECT_JSON_DEPTH) return "[truncated]";
|
|
653
|
+
if (typeof value === "string") {
|
|
654
|
+
const clean = sanitizeText(value);
|
|
655
|
+
return clean.length <= INSPECT_JSON_STRING_CHARS ? clean : `${clean.slice(0, INSPECT_JSON_STRING_CHARS - 1)}…`;
|
|
656
|
+
}
|
|
657
|
+
if (Array.isArray(value)) return value.slice(Math.max(0, value.length - INSPECT_JSON_ARRAY_ITEMS)).map((item) => boundJsonValue(item, depth + 1));
|
|
658
|
+
if (!value || typeof value !== "object") return value;
|
|
659
|
+
const result = {};
|
|
660
|
+
for (const [key, item] of Object.entries(value).slice(0, INSPECT_JSON_OBJECT_KEYS)) result[key] = boundJsonValue(item, depth + 1);
|
|
661
|
+
return result;
|
|
662
|
+
}
|
|
663
|
+
function buildInspectPayload(result) {
|
|
664
|
+
const finalOutput = finalOutputPreview(result.status, result.snapshot);
|
|
665
|
+
return boundJsonValue({
|
|
666
|
+
run_id: result.run_id,
|
|
667
|
+
label: result.label,
|
|
668
|
+
status: result.status,
|
|
669
|
+
snapshot: result.snapshot,
|
|
670
|
+
transcript: result.transcript,
|
|
671
|
+
tools: result.tools,
|
|
672
|
+
live_tools: result.live_tools,
|
|
673
|
+
permissions: result.permissions,
|
|
674
|
+
pending_permission: result.pending_permission,
|
|
675
|
+
final_output: finalOutput
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function toWatchRunRef(run) {
|
|
679
|
+
return {
|
|
680
|
+
runId: run.runId,
|
|
681
|
+
label: run.label,
|
|
682
|
+
agent: run.agent,
|
|
683
|
+
runtimeId: run.runtimeId,
|
|
684
|
+
supervisorId: run.supervisorId
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function createExtension(deps = defaultDeps) {
|
|
688
|
+
return async function(pi) {
|
|
689
|
+
const trackedRuns = /* @__PURE__ */ new Map();
|
|
690
|
+
let pollingActive = false;
|
|
691
|
+
let pollingGeneration = 0;
|
|
692
|
+
let pollInFlight = null;
|
|
693
|
+
let sessionCtx = null;
|
|
694
|
+
function upsertTrackedRun(run, status, blocking = false) {
|
|
695
|
+
const current = trackedRuns.get(run.runId);
|
|
696
|
+
trackedRuns.set(run.runId, {
|
|
697
|
+
runId: run.runId,
|
|
698
|
+
agent: run.agent ?? status?.agent ?? current?.agent ?? "unknown",
|
|
699
|
+
label: run.label ?? status?.label ?? current?.label ?? run.runId,
|
|
700
|
+
supervisorId: run.supervisorId ?? current?.supervisorId,
|
|
701
|
+
runtimeId: run.runtimeId ?? status?.runtime_id ?? current?.runtimeId,
|
|
702
|
+
startTime: current?.startTime ?? Date.now(),
|
|
703
|
+
lastStatus: status ?? current?.lastStatus,
|
|
704
|
+
blocking: current?.blocking ?? blocking,
|
|
705
|
+
permissionNotified: current?.permissionNotified
|
|
706
|
+
});
|
|
125
707
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
run.lastStatus = live;
|
|
132
|
-
entries.push({
|
|
708
|
+
async function resolveFinalOutput(run, status) {
|
|
709
|
+
const preview = finalOutputPreview(status);
|
|
710
|
+
if (preview) return preview;
|
|
711
|
+
try {
|
|
712
|
+
const inspected = await deps.inspectTool({
|
|
133
713
|
runId: run.runId,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
phase: live.phase,
|
|
137
|
-
phaseLabel: live.phase_label,
|
|
138
|
-
agent: run.agent,
|
|
139
|
-
pendingPermission: !!live.pending_permission,
|
|
140
|
-
permissionDescription: live.pending_permission?.description
|
|
714
|
+
supervisorId: run.supervisorId,
|
|
715
|
+
limit: INSPECT_LIMIT
|
|
141
716
|
});
|
|
142
|
-
|
|
717
|
+
return finalOutputPreview(inspected.status, inspected.snapshot);
|
|
718
|
+
} catch {
|
|
719
|
+
return;
|
|
143
720
|
}
|
|
721
|
+
}
|
|
722
|
+
async function pollRuns() {
|
|
723
|
+
const liveMap = /* @__PURE__ */ new Map();
|
|
144
724
|
try {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
149
|
-
const result = Array.isArray(raw) ? raw[0] : raw;
|
|
150
|
-
if (!result) throw new Error("status response was empty");
|
|
151
|
-
run.lastStatus = result;
|
|
152
|
-
entries.push({
|
|
153
|
-
runId: run.runId,
|
|
154
|
-
label: run.label,
|
|
155
|
-
status: result.status,
|
|
156
|
-
phase: result.phase,
|
|
157
|
-
phaseLabel: result.phase_label,
|
|
158
|
-
agent: run.agent,
|
|
159
|
-
pendingPermission: !!result.pending_permission,
|
|
160
|
-
permissionDescription: result.pending_permission?.description
|
|
161
|
-
});
|
|
725
|
+
const allLive = await deps.statusTool({});
|
|
726
|
+
const liveList = Array.isArray(allLive) ? allLive : [allLive];
|
|
727
|
+
for (const result of liveList) liveMap.set(result.run_id, result);
|
|
162
728
|
} catch (err) {
|
|
163
|
-
console.error(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
729
|
+
console.error("avenor pollRuns: failed to list singleton runs", err);
|
|
730
|
+
}
|
|
731
|
+
const entries = [];
|
|
732
|
+
for (const run of trackedRuns.values()) {
|
|
733
|
+
const live = findLiveStatusForTrackedRun(run, liveMap.values());
|
|
734
|
+
if (live) {
|
|
735
|
+
liveMap.delete(live.run_id);
|
|
736
|
+
run.lastStatus = live;
|
|
737
|
+
entries.push({
|
|
738
|
+
runId: run.runId,
|
|
739
|
+
label: run.label,
|
|
740
|
+
status: live.status,
|
|
741
|
+
phase: live.phase,
|
|
742
|
+
phaseLabel: live.phase_label,
|
|
743
|
+
agent: run.agent,
|
|
744
|
+
pendingPermission: !!live.pending_permission,
|
|
745
|
+
permissionDescription: live.pending_permission?.description
|
|
746
|
+
});
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const raw = await deps.statusTool({
|
|
751
|
+
runId: run.runId,
|
|
752
|
+
supervisorId: run.supervisorId
|
|
753
|
+
});
|
|
754
|
+
const result = Array.isArray(raw) ? raw[0] : raw;
|
|
755
|
+
if (!result) throw new Error("status response was empty");
|
|
756
|
+
run.lastStatus = result;
|
|
757
|
+
entries.push({
|
|
758
|
+
runId: run.runId,
|
|
759
|
+
label: run.label,
|
|
760
|
+
status: result.status,
|
|
761
|
+
phase: result.phase,
|
|
762
|
+
phaseLabel: result.phase_label,
|
|
763
|
+
agent: run.agent,
|
|
764
|
+
pendingPermission: !!result.pending_permission,
|
|
765
|
+
permissionDescription: result.pending_permission?.description
|
|
766
|
+
});
|
|
767
|
+
} catch (err) {
|
|
768
|
+
console.error(`avenor pollRuns: statusTool failed for ${run.runId}`, err);
|
|
769
|
+
const previous = run.lastStatus;
|
|
770
|
+
entries.push({
|
|
771
|
+
runId: run.runId,
|
|
772
|
+
label: run.label,
|
|
773
|
+
status: previous?.status ?? "unavailable",
|
|
774
|
+
phase: previous?.phase,
|
|
775
|
+
phaseLabel: previous?.phase_label,
|
|
776
|
+
agent: run.agent,
|
|
777
|
+
pendingPermission: !!previous?.pending_permission,
|
|
778
|
+
permissionDescription: previous?.pending_permission?.description
|
|
779
|
+
});
|
|
780
|
+
}
|
|
175
781
|
}
|
|
782
|
+
for (const live of liveMap.values()) entries.push({
|
|
783
|
+
runId: live.run_id,
|
|
784
|
+
label: live.label,
|
|
785
|
+
status: live.status,
|
|
786
|
+
phase: live.phase,
|
|
787
|
+
phaseLabel: live.phase_label,
|
|
788
|
+
agent: live.agent ?? "unknown",
|
|
789
|
+
pendingPermission: !!live.pending_permission,
|
|
790
|
+
permissionDescription: live.pending_permission?.description
|
|
791
|
+
});
|
|
792
|
+
return entries;
|
|
176
793
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return entries;
|
|
188
|
-
}
|
|
189
|
-
function renderStatusLines(entries) {
|
|
190
|
-
const active = entries.filter((e) => !TERMINAL_STATUSES.has(e.status));
|
|
191
|
-
if (active.length === 0) return [];
|
|
192
|
-
return active.map((e) => {
|
|
193
|
-
const emoji = statusEmoji(e.status);
|
|
194
|
-
const perm = e.pendingPermission ? ` 🔒` : "";
|
|
195
|
-
const phase = e.phaseLabel ? ` [${e.phaseLabel}]` : "";
|
|
196
|
-
return `${emoji} ${e.label} — ${e.status}${phase}${perm}`;
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
function updateWidget(entries) {
|
|
200
|
-
if (!sessionCtx) return;
|
|
201
|
-
const lines = renderStatusLines(entries);
|
|
202
|
-
if (lines.length > 0) {
|
|
203
|
-
sessionCtx.ui.setWidget("avenor-status", lines);
|
|
204
|
-
const parts = lines.map((l) => l.slice(0, 60));
|
|
205
|
-
sessionCtx.ui.setStatus("avenor", parts.join(" "));
|
|
206
|
-
} else {
|
|
207
|
-
sessionCtx.ui.setWidget("avenor-status", void 0);
|
|
208
|
-
sessionCtx.ui.setStatus("avenor", "");
|
|
794
|
+
function updateWidget(entries) {
|
|
795
|
+
if (!sessionCtx) return;
|
|
796
|
+
const lines = renderStatusLines(entries);
|
|
797
|
+
if (lines.length > 0) {
|
|
798
|
+
sessionCtx.ui.setWidget("avenor-status", lines);
|
|
799
|
+
sessionCtx.ui.setStatus("avenor", lines.map((line) => line.slice(0, 60)).join(" "));
|
|
800
|
+
} else {
|
|
801
|
+
sessionCtx.ui.setWidget("avenor-status", void 0);
|
|
802
|
+
sessionCtx.ui.setStatus("avenor", "");
|
|
803
|
+
}
|
|
209
804
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
805
|
+
function startPolling() {
|
|
806
|
+
if (pollingActive) return;
|
|
807
|
+
pollingActive = true;
|
|
808
|
+
const generation = ++pollingGeneration;
|
|
809
|
+
const tick = async () => {
|
|
810
|
+
if (!pollingActive || generation !== pollingGeneration) return;
|
|
811
|
+
const prevStatuses = /* @__PURE__ */ new Map();
|
|
812
|
+
for (const [id, run] of trackedRuns) prevStatuses.set(id, run.lastStatus?.status);
|
|
813
|
+
const currentPoll = pollRuns();
|
|
814
|
+
pollInFlight = currentPoll;
|
|
815
|
+
const entries = await currentPoll.finally(() => {
|
|
816
|
+
if (pollInFlight === currentPoll) pollInFlight = null;
|
|
817
|
+
});
|
|
818
|
+
if (!pollingActive || generation !== pollingGeneration) return;
|
|
819
|
+
updateWidget(entries);
|
|
820
|
+
for (const entry of entries) {
|
|
821
|
+
const run = trackedRuns.get(entry.runId);
|
|
822
|
+
if (!run) continue;
|
|
823
|
+
if (isTerminalStatus(entry.status)) {
|
|
824
|
+
const prevStatus = prevStatuses.get(entry.runId);
|
|
825
|
+
if (!prevStatus || !isTerminalStatus(prevStatus)) {
|
|
826
|
+
sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
|
|
827
|
+
if (!run.blocking && run.lastStatus) {
|
|
828
|
+
const finalOutput = await resolveFinalOutput(run, run.lastStatus);
|
|
829
|
+
try {
|
|
830
|
+
pi.sendUserMessage(buildCompletionText({
|
|
831
|
+
runId: run.runId,
|
|
832
|
+
label: run.label
|
|
833
|
+
}, run.lastStatus, finalOutput), { deliverAs: "followUp" });
|
|
834
|
+
} catch (err) {
|
|
835
|
+
console.error("avenor tick: sendUserMessage failed", err);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
trackedRuns.delete(entry.runId);
|
|
840
|
+
} else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
|
|
841
|
+
run.permissionNotified = true;
|
|
842
|
+
if (run.lastStatus) try {
|
|
843
|
+
pi.sendUserMessage(buildWaitingText({
|
|
235
844
|
runId: run.runId,
|
|
236
|
-
label: run.label
|
|
845
|
+
label: run.label,
|
|
846
|
+
agent: run.agent
|
|
237
847
|
}, run.lastStatus), { deliverAs: "followUp" });
|
|
238
848
|
} catch (err) {
|
|
239
|
-
console.error("avenor tick: sendUserMessage failed", err);
|
|
849
|
+
console.error("avenor tick: sendUserMessage for permission failed", err);
|
|
240
850
|
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}, run.lastStatus), { deliverAs: "followUp" });
|
|
251
|
-
} catch (err) {
|
|
252
|
-
console.error("avenor tick: sendUserMessage for permission failed", err);
|
|
253
|
-
}
|
|
254
|
-
} else if (!entry.pendingPermission && run.permissionNotified) run.permissionNotified = false;
|
|
255
|
-
}
|
|
256
|
-
if (entries.some((e) => !TERMINAL_STATUSES.has(e.status)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
|
|
257
|
-
tick().catch((err) => {
|
|
851
|
+
} else if (!entry.pendingPermission && run.permissionNotified) run.permissionNotified = false;
|
|
852
|
+
}
|
|
853
|
+
if (entries.some((entry) => !isTerminalStatus(entry.status)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
|
|
854
|
+
tick().catch((err) => {
|
|
855
|
+
pollingActive = false;
|
|
856
|
+
console.error("avenor polling loop failed", err);
|
|
857
|
+
});
|
|
858
|
+
}, POLL_INTERVAL_MS);
|
|
859
|
+
else {
|
|
258
860
|
pollingActive = false;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
861
|
+
sessionCtx?.ui.setStatus("avenor", "");
|
|
862
|
+
sessionCtx?.ui.setWidget("avenor-status", void 0);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
tick().catch((err) => {
|
|
263
866
|
pollingActive = false;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
tick().catch((err) => {
|
|
867
|
+
console.error("avenor polling loop failed", err);
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
async function stopPolling() {
|
|
269
871
|
pollingActive = false;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
pollingActive = false;
|
|
275
|
-
pollingGeneration++;
|
|
276
|
-
await pollInFlight?.catch(() => {});
|
|
277
|
-
sessionCtx?.ui.setStatus("avenor", "");
|
|
278
|
-
sessionCtx?.ui.setWidget("avenor-status", void 0);
|
|
279
|
-
}
|
|
280
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
281
|
-
sessionCtx = ctx;
|
|
282
|
-
});
|
|
283
|
-
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
284
|
-
await stopPolling();
|
|
285
|
-
sessionCtx = null;
|
|
286
|
-
});
|
|
287
|
-
pi.on("before_agent_start", async (event, _ctx) => {
|
|
288
|
-
if (trackedRuns.size === 0) return;
|
|
289
|
-
const active = (await pollRuns()).filter((e) => !TERMINAL_STATUSES.has(e.status));
|
|
290
|
-
if (active.length === 0) return;
|
|
291
|
-
return { message: {
|
|
292
|
-
customType: "avenor-active-runs",
|
|
293
|
-
content: `Active sub-agents:\n${active.map((e) => {
|
|
294
|
-
const perm = e.pendingPermission ? ` [blocked: ${e.permissionDescription ?? "permission"}]` : "";
|
|
295
|
-
const phase = e.phaseLabel ? ` (${e.phaseLabel})` : "";
|
|
296
|
-
return ` • ${e.label} (${e.agent}): ${e.status}${phase}${perm}`;
|
|
297
|
-
}).join("\n")}`,
|
|
298
|
-
display: true
|
|
299
|
-
} };
|
|
300
|
-
});
|
|
301
|
-
pi.registerMessageRenderer("avenor-active-runs", (message, options, theme) => {
|
|
302
|
-
const lines = message.content.split("\n");
|
|
303
|
-
let text = "";
|
|
304
|
-
for (let i = 0; i < lines.length; i++) {
|
|
305
|
-
if (i === 0) text += theme.fg("info", theme.bold(lines[i]));
|
|
306
|
-
else text += theme.fg("muted", lines[i]);
|
|
307
|
-
if (i < lines.length - 1) text += "\n";
|
|
872
|
+
pollingGeneration++;
|
|
873
|
+
await pollInFlight?.catch(() => {});
|
|
874
|
+
sessionCtx?.ui.setStatus("avenor", "");
|
|
875
|
+
sessionCtx?.ui.setWidget("avenor-status", void 0);
|
|
308
876
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
"Use avenor_shutdown when done delegating work."
|
|
322
|
-
],
|
|
323
|
-
parameters: Type.Object({
|
|
324
|
-
agent: Type.String({ description: "Agent name (required, no default)" }),
|
|
325
|
-
prompt: Type.Optional(Type.String({ description: "Prompt text" })),
|
|
326
|
-
prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
|
|
327
|
-
dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
|
|
328
|
-
label: Type.Optional(Type.String({ description: "Human-readable label for the run" })),
|
|
329
|
-
timeout: Type.Optional(Type.String({ description: "Timeout duration (e.g. 3600s)" })),
|
|
330
|
-
model: Type.Optional(Type.String({ description: "Model override" })),
|
|
331
|
-
backend: Type.Optional(Type.String({ description: "Backend override" })),
|
|
332
|
-
server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
|
|
333
|
-
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
|
|
334
|
-
wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
|
|
335
|
-
}),
|
|
336
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
337
|
-
if (signal?.aborted) return { content: [{
|
|
338
|
-
type: "text",
|
|
339
|
-
text: "Cancelled"
|
|
340
|
-
}] };
|
|
341
|
-
const wait = params.wait ?? true;
|
|
342
|
-
const dir = params.dir ?? ctx.cwd;
|
|
343
|
-
const label = params.label ?? `${params.agent}-${Date.now()}`;
|
|
344
|
-
const result = await spawnTool({
|
|
345
|
-
agent: params.agent,
|
|
346
|
-
prompt: params.prompt,
|
|
347
|
-
promptFile: params.prompt_file,
|
|
348
|
-
label,
|
|
349
|
-
dir,
|
|
350
|
-
timeout: params.timeout,
|
|
351
|
-
model: params.model,
|
|
352
|
-
backend: params.backend,
|
|
353
|
-
serverUrl: params.server_url,
|
|
354
|
-
supervisorId: params.supervisor_id
|
|
355
|
-
});
|
|
356
|
-
const supervisorId = statusSupervisorId(params.supervisor_id, result.supervisor_id);
|
|
357
|
-
trackedRuns.set(result.run_id, {
|
|
358
|
-
runId: result.run_id,
|
|
359
|
-
agent: params.agent,
|
|
360
|
-
label,
|
|
361
|
-
supervisorId,
|
|
362
|
-
runtimeId: result.runtime_id,
|
|
363
|
-
startTime: Date.now(),
|
|
364
|
-
blocking: wait
|
|
365
|
-
});
|
|
366
|
-
startPolling();
|
|
367
|
-
if (!wait) return {
|
|
368
|
-
content: [{
|
|
369
|
-
type: "text",
|
|
370
|
-
text: `Dispatched "${label}" (run_id: ${result.run_id}). Call avenor_status to check progress.`
|
|
371
|
-
}],
|
|
372
|
-
details: {
|
|
373
|
-
run_id: result.run_id,
|
|
374
|
-
label
|
|
877
|
+
function createWatchDeps() {
|
|
878
|
+
async function resolveSupervisorSocket(run) {
|
|
879
|
+
if (run.supervisorId) return run.supervisorId;
|
|
880
|
+
return (await deps.Supervisor.get()).supervisorId;
|
|
881
|
+
}
|
|
882
|
+
async function withClient(run, work) {
|
|
883
|
+
const socket = await resolveSupervisorSocket(run);
|
|
884
|
+
const client = await deps.dial(socket);
|
|
885
|
+
try {
|
|
886
|
+
return await work(client);
|
|
887
|
+
} finally {
|
|
888
|
+
client.close();
|
|
375
889
|
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
inspectRun(run) {
|
|
893
|
+
return deps.inspectTool({
|
|
894
|
+
runId: run.runId,
|
|
895
|
+
supervisorId: run.supervisorId,
|
|
896
|
+
limit: INSPECT_LIMIT
|
|
897
|
+
});
|
|
898
|
+
},
|
|
899
|
+
async observeRun(run, afterSeq, initialSnapshot) {
|
|
900
|
+
const runtimeId = run.runtimeId;
|
|
901
|
+
if (!runtimeId) return null;
|
|
902
|
+
const socket = await resolveSupervisorSocket(run);
|
|
903
|
+
const client = await deps.dial(socket);
|
|
904
|
+
return deps.observeRun(client, {
|
|
905
|
+
runtime_id: runtimeId,
|
|
906
|
+
replay: true,
|
|
907
|
+
after_seq: afterSeq,
|
|
908
|
+
initial_snapshot: initialSnapshot,
|
|
909
|
+
close_client: true
|
|
910
|
+
});
|
|
911
|
+
},
|
|
912
|
+
cancelRun(run, runtimeId) {
|
|
913
|
+
return withClient(run, async (client) => {
|
|
914
|
+
await client.cancel(runtimeId);
|
|
915
|
+
});
|
|
916
|
+
},
|
|
917
|
+
promptRun(run, runtimeId, text) {
|
|
918
|
+
return withClient(run, async (client) => {
|
|
919
|
+
await client.prompt(runtimeId, text);
|
|
920
|
+
});
|
|
921
|
+
},
|
|
922
|
+
followUpRun(run, text) {
|
|
923
|
+
return deps.followUpTool({
|
|
924
|
+
runId: run.runId,
|
|
925
|
+
message: text,
|
|
926
|
+
supervisorId: run.supervisorId
|
|
927
|
+
});
|
|
385
928
|
}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async function followUpTrackedRun(run, text) {
|
|
932
|
+
const result = await deps.followUpTool({
|
|
933
|
+
runId: run.runId,
|
|
934
|
+
message: text,
|
|
935
|
+
supervisorId: run.supervisorId
|
|
936
|
+
});
|
|
937
|
+
try {
|
|
938
|
+
const rawStatus = await deps.statusTool({
|
|
939
|
+
runId: result.run_id,
|
|
940
|
+
supervisorId: run.supervisorId
|
|
941
|
+
});
|
|
942
|
+
const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
|
|
943
|
+
upsertTrackedRun({
|
|
944
|
+
runId: result.run_id,
|
|
945
|
+
label: result.label,
|
|
946
|
+
agent: status?.agent ?? run.agent,
|
|
947
|
+
runtimeId: status?.runtime_id,
|
|
948
|
+
supervisorId: run.supervisorId
|
|
949
|
+
}, status, false);
|
|
950
|
+
startPolling();
|
|
951
|
+
} catch {
|
|
952
|
+
upsertTrackedRun({
|
|
953
|
+
runId: result.run_id,
|
|
954
|
+
label: result.label,
|
|
955
|
+
agent: run.agent,
|
|
956
|
+
supervisorId: run.supervisorId
|
|
957
|
+
}, void 0, false);
|
|
958
|
+
startPolling();
|
|
959
|
+
}
|
|
960
|
+
return result;
|
|
961
|
+
}
|
|
962
|
+
async function waitForRunWithObserver(run, signal, onUpdate) {
|
|
963
|
+
let observer = null;
|
|
964
|
+
let unsubscribe;
|
|
965
|
+
let lastUpdate = "";
|
|
966
|
+
let abortListener;
|
|
967
|
+
const emit = (snapshot) => {
|
|
968
|
+
const text = snapshotProgressText(run.label ?? run.runId, snapshot);
|
|
969
|
+
if (text === lastUpdate) return;
|
|
970
|
+
lastUpdate = text;
|
|
971
|
+
onUpdate?.({
|
|
972
|
+
content: [{
|
|
973
|
+
type: "text",
|
|
974
|
+
text
|
|
975
|
+
}],
|
|
976
|
+
details: {
|
|
977
|
+
status: snapshot.pending_permission ? "waiting" : snapshot.ended ? "done" : "running",
|
|
978
|
+
run_id: run.runId
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
};
|
|
982
|
+
const waitPromise = new Promise((resolve) => {
|
|
983
|
+
const complete = () => resolve("settled");
|
|
984
|
+
(async () => {
|
|
985
|
+
try {
|
|
986
|
+
observer = await createWatchDeps().observeRun(run);
|
|
987
|
+
if (!observer) {
|
|
988
|
+
complete();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const maybeComplete = (snapshot) => {
|
|
992
|
+
emit(snapshot);
|
|
993
|
+
if (snapshot.pending_permission || snapshot.ended) complete();
|
|
994
|
+
};
|
|
995
|
+
unsubscribe = observer.subscribe((snapshot) => {
|
|
996
|
+
maybeComplete(snapshot);
|
|
997
|
+
});
|
|
998
|
+
const current = observer.snapshot();
|
|
999
|
+
if (current.last_event || current.transcript.length > 0 || current.pending_permission || current.ended) maybeComplete(current);
|
|
1000
|
+
} catch {
|
|
1001
|
+
complete();
|
|
1002
|
+
}
|
|
1003
|
+
})();
|
|
386
1004
|
});
|
|
1005
|
+
const abortPromise = signal ? new Promise((resolve) => {
|
|
1006
|
+
if (signal.aborted) {
|
|
1007
|
+
resolve("aborted");
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const onAbort = () => resolve("aborted");
|
|
1011
|
+
abortListener = onAbort;
|
|
1012
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1013
|
+
}) : void 0;
|
|
1014
|
+
const outcome = abortPromise ? await Promise.race([waitPromise, abortPromise]) : await waitPromise;
|
|
1015
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
1016
|
+
unsubscribe?.();
|
|
1017
|
+
await observer?.close().catch(() => {});
|
|
1018
|
+
if (outcome === "aborted") return { aborted: true };
|
|
1019
|
+
try {
|
|
1020
|
+
const raw = await deps.statusTool({
|
|
1021
|
+
runId: run.runId,
|
|
1022
|
+
supervisorId: run.supervisorId
|
|
1023
|
+
});
|
|
1024
|
+
return {
|
|
1025
|
+
status: Array.isArray(raw) ? raw[0] : raw,
|
|
1026
|
+
aborted: false
|
|
1027
|
+
};
|
|
1028
|
+
} catch {
|
|
1029
|
+
return { aborted: false };
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async function waitForRunByPolling(run, signal, onUpdate) {
|
|
387
1033
|
let firstPoll = true;
|
|
388
1034
|
while (!signal?.aborted) {
|
|
389
1035
|
if (!firstPoll) await sleep(POLL_INTERVAL_MS);
|
|
390
1036
|
firstPoll = false;
|
|
391
|
-
let raw;
|
|
392
1037
|
try {
|
|
393
|
-
raw = await statusTool({
|
|
394
|
-
runId:
|
|
395
|
-
supervisorId
|
|
1038
|
+
const raw = await deps.statusTool({
|
|
1039
|
+
runId: run.runId,
|
|
1040
|
+
supervisorId: run.supervisorId
|
|
1041
|
+
});
|
|
1042
|
+
const status = Array.isArray(raw) ? raw[0] : raw;
|
|
1043
|
+
if (!status) continue;
|
|
1044
|
+
const text = status.pending_permission ? `[blocked] ${run.label ?? run.runId} — permission: ${status.pending_permission.description}` : `${run.label ?? run.runId}${status.phase_label ? ` (${status.phase_label})` : ""}`;
|
|
1045
|
+
onUpdate?.({
|
|
1046
|
+
content: [{
|
|
1047
|
+
type: "text",
|
|
1048
|
+
text
|
|
1049
|
+
}],
|
|
1050
|
+
details: {
|
|
1051
|
+
status: status.status,
|
|
1052
|
+
run_id: run.runId
|
|
1053
|
+
}
|
|
396
1054
|
});
|
|
1055
|
+
if (status.status === "waiting" || isTerminalStatus(status.status)) return {
|
|
1056
|
+
status,
|
|
1057
|
+
aborted: false
|
|
1058
|
+
};
|
|
397
1059
|
} catch (err) {
|
|
398
1060
|
console.error("avenor spawn poll: statusTool failed", err);
|
|
399
|
-
continue;
|
|
400
1061
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1062
|
+
}
|
|
1063
|
+
return { aborted: true };
|
|
1064
|
+
}
|
|
1065
|
+
async function waitForRun(run, signal, onUpdate) {
|
|
1066
|
+
const observed = await waitForRunWithObserver(run, signal, onUpdate);
|
|
1067
|
+
if (observed.status || observed.aborted) return observed;
|
|
1068
|
+
return waitForRunByPolling(run, signal, onUpdate);
|
|
1069
|
+
}
|
|
1070
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1071
|
+
sessionCtx = ctx;
|
|
1072
|
+
});
|
|
1073
|
+
pi.on("session_shutdown", async () => {
|
|
1074
|
+
await stopPolling();
|
|
1075
|
+
sessionCtx = null;
|
|
1076
|
+
});
|
|
1077
|
+
pi.on("before_agent_start", async () => {
|
|
1078
|
+
if (trackedRuns.size === 0) return;
|
|
1079
|
+
const active = (await pollRuns()).filter((entry) => !isTerminalStatus(entry.status));
|
|
1080
|
+
if (active.length === 0) return;
|
|
1081
|
+
return { message: {
|
|
1082
|
+
customType: "avenor-active-runs",
|
|
1083
|
+
content: `Active sub-agents:\n${active.map((entry) => {
|
|
1084
|
+
const perm = entry.pendingPermission ? ` [blocked: ${entry.permissionDescription ?? "permission"}]` : "";
|
|
1085
|
+
const phase = entry.phaseLabel ? ` (${entry.phaseLabel})` : "";
|
|
1086
|
+
return ` • ${entry.label} (${entry.agent}): ${entry.status}${phase}${perm}`;
|
|
1087
|
+
}).join("\n")}`,
|
|
1088
|
+
display: true
|
|
1089
|
+
} };
|
|
1090
|
+
});
|
|
1091
|
+
pi.registerMessageRenderer("avenor-active-runs", (message, _options, theme) => {
|
|
1092
|
+
const lines = message.content.split("\n");
|
|
1093
|
+
let text = "";
|
|
1094
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1095
|
+
text += i === 0 ? theme.fg("accent", theme.bold(lines[i] ?? "")) : theme.fg("muted", lines[i] ?? "");
|
|
1096
|
+
if (i < lines.length - 1) text += "\n";
|
|
1097
|
+
}
|
|
1098
|
+
return new Text(text, 0, 0);
|
|
1099
|
+
});
|
|
1100
|
+
pi.registerTool({
|
|
1101
|
+
name: "avenor_spawn",
|
|
1102
|
+
label: "Avenor Spawn",
|
|
1103
|
+
description: "Dispatch an agent run via avenor. Blocks by default, showing live progress. Set wait=false for fire-and-forget.",
|
|
1104
|
+
promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, inspect, events, follow-up, shutdown)",
|
|
1105
|
+
promptGuidelines: [
|
|
1106
|
+
"Use avenor_spawn to delegate well-defined tasks to sub-agents.",
|
|
1107
|
+
"Use avenor_status to check progress of fire-and-forget runs.",
|
|
1108
|
+
"Use avenor_inspect to review a bounded transcript, tools, and final output from a run.",
|
|
1109
|
+
"Use avenor_events only when raw event payloads are specifically needed.",
|
|
1110
|
+
"Use avenor_follow_up to iterate on a completed run.",
|
|
1111
|
+
"Use avenor_shutdown when done delegating work."
|
|
1112
|
+
],
|
|
1113
|
+
parameters: Type.Object({
|
|
1114
|
+
agent: Type.String({ description: "Agent name (required, no default)" }),
|
|
1115
|
+
prompt: Type.Optional(Type.String({ description: "Prompt text" })),
|
|
1116
|
+
prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
|
|
1117
|
+
dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
|
|
1118
|
+
label: Type.Optional(Type.String({ description: "Human-readable label for the run" })),
|
|
1119
|
+
timeout: Type.Optional(Type.String({ description: "Timeout duration (e.g. 3600s)" })),
|
|
1120
|
+
model: Type.Optional(Type.String({ description: "Model override" })),
|
|
1121
|
+
backend: Type.Optional(Type.String({ description: "Backend override" })),
|
|
1122
|
+
server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
|
|
1123
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
|
|
1124
|
+
wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
|
|
1125
|
+
}),
|
|
1126
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1127
|
+
if (signal?.aborted) return { content: [{
|
|
1128
|
+
type: "text",
|
|
1129
|
+
text: "Cancelled"
|
|
1130
|
+
}] };
|
|
1131
|
+
const wait = params.wait ?? true;
|
|
1132
|
+
const dir = params.dir ?? ctx.cwd;
|
|
1133
|
+
const label = params.label ?? `${params.agent}-${Date.now()}`;
|
|
1134
|
+
const result = await deps.spawnTool({
|
|
1135
|
+
agent: params.agent,
|
|
1136
|
+
prompt: params.prompt,
|
|
1137
|
+
promptFile: params.prompt_file,
|
|
1138
|
+
label,
|
|
1139
|
+
dir,
|
|
1140
|
+
timeout: params.timeout,
|
|
1141
|
+
model: params.model,
|
|
1142
|
+
backend: params.backend,
|
|
1143
|
+
serverUrl: params.server_url,
|
|
1144
|
+
supervisorId: params.supervisor_id
|
|
1145
|
+
});
|
|
1146
|
+
const supervisorId = statusSupervisorId(params.supervisor_id, result.supervisor_id);
|
|
1147
|
+
const runRef = {
|
|
1148
|
+
runId: result.run_id,
|
|
1149
|
+
agent: params.agent,
|
|
1150
|
+
label,
|
|
1151
|
+
supervisorId,
|
|
1152
|
+
runtimeId: result.runtime_id
|
|
1153
|
+
};
|
|
1154
|
+
upsertTrackedRun(runRef, void 0, wait);
|
|
1155
|
+
startPolling();
|
|
1156
|
+
if (!wait) return {
|
|
1157
|
+
content: [{
|
|
1158
|
+
type: "text",
|
|
1159
|
+
text: `Dispatched "${label}" (run_id: ${result.run_id}). Call avenor_status to check progress or avenor_inspect for a bounded transcript snapshot.`
|
|
1160
|
+
}],
|
|
1161
|
+
details: {
|
|
1162
|
+
run_id: result.run_id,
|
|
1163
|
+
label
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
onUpdate?.({
|
|
404
1167
|
content: [{
|
|
405
1168
|
type: "text",
|
|
406
|
-
text: `
|
|
1169
|
+
text: `Starting ${label}…`
|
|
407
1170
|
}],
|
|
408
1171
|
details: {
|
|
409
|
-
status: "
|
|
1172
|
+
status: "running",
|
|
410
1173
|
run_id: result.run_id
|
|
411
1174
|
}
|
|
412
1175
|
});
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
runId: result.run_id,
|
|
418
|
-
limit: 3
|
|
419
|
-
});
|
|
420
|
-
const last = eventsResult.events[eventsResult.events.length - 1];
|
|
421
|
-
if (last) lastAction = summarizeEvent(last);
|
|
422
|
-
} catch (err) {
|
|
423
|
-
console.error("avenor spawn poll: eventsTool failed", err);
|
|
424
|
-
}
|
|
425
|
-
onUpdate?.({
|
|
1176
|
+
const waitResult = await waitForRun(runRef, signal, onUpdate);
|
|
1177
|
+
if (waitResult.aborted) {
|
|
1178
|
+
trackedRuns.delete(result.run_id);
|
|
1179
|
+
return {
|
|
426
1180
|
content: [{
|
|
427
1181
|
type: "text",
|
|
428
|
-
text:
|
|
1182
|
+
text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status or avenor_inspect to check.`
|
|
429
1183
|
}],
|
|
430
|
-
details: {
|
|
431
|
-
|
|
432
|
-
run_id: result.run_id,
|
|
433
|
-
lastAction
|
|
434
|
-
}
|
|
435
|
-
});
|
|
1184
|
+
details: { run_id: result.run_id }
|
|
1185
|
+
};
|
|
436
1186
|
}
|
|
1187
|
+
const status = waitResult.status;
|
|
1188
|
+
if (!status) return {
|
|
1189
|
+
content: [{
|
|
1190
|
+
type: "text",
|
|
1191
|
+
text: `Sub-agent "${label}" is still running. Use avenor_status or avenor_inspect to check progress.`
|
|
1192
|
+
}],
|
|
1193
|
+
details: {
|
|
1194
|
+
status: "running",
|
|
1195
|
+
run_id: result.run_id
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
upsertTrackedRun(runRef, status, wait);
|
|
437
1199
|
if (status.status === "waiting") {
|
|
438
1200
|
const tracked = trackedRuns.get(result.run_id);
|
|
439
1201
|
if (tracked) tracked.permissionNotified = true;
|
|
@@ -454,7 +1216,11 @@ async function src_default(pi) {
|
|
|
454
1216
|
}
|
|
455
1217
|
};
|
|
456
1218
|
}
|
|
457
|
-
if (
|
|
1219
|
+
if (isTerminalStatus(status.status)) {
|
|
1220
|
+
const finalOutput = await resolveFinalOutput({
|
|
1221
|
+
runId: result.run_id,
|
|
1222
|
+
supervisorId
|
|
1223
|
+
}, status);
|
|
458
1224
|
trackedRuns.delete(result.run_id);
|
|
459
1225
|
return {
|
|
460
1226
|
content: [{
|
|
@@ -462,286 +1228,292 @@ async function src_default(pi) {
|
|
|
462
1228
|
text: buildCompletionText({
|
|
463
1229
|
runId: result.run_id,
|
|
464
1230
|
label
|
|
465
|
-
}, status)
|
|
1231
|
+
}, status, finalOutput)
|
|
466
1232
|
}],
|
|
467
1233
|
details: {
|
|
468
1234
|
status: status.status,
|
|
469
1235
|
run_id: result.run_id,
|
|
470
|
-
session_id: status.session_id
|
|
1236
|
+
session_id: status.session_id,
|
|
1237
|
+
final_output: finalOutput
|
|
471
1238
|
}
|
|
472
1239
|
};
|
|
473
1240
|
}
|
|
1241
|
+
return {
|
|
1242
|
+
content: [{
|
|
1243
|
+
type: "text",
|
|
1244
|
+
text: `${label} is ${status.status}. Use avenor_status or avenor_inspect to check progress.`
|
|
1245
|
+
}],
|
|
1246
|
+
details: {
|
|
1247
|
+
status: status.status,
|
|
1248
|
+
run_id: result.run_id
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
},
|
|
1252
|
+
renderCall(args, theme) {
|
|
1253
|
+
const agent = theme.fg("toolTitle", theme.bold("avenor_spawn "));
|
|
1254
|
+
const agentName = theme.fg("accent", args.agent);
|
|
1255
|
+
const label = args.label ? theme.fg("dim", ` "${args.label}"`) : "";
|
|
1256
|
+
const wait = args.wait === false ? theme.fg("warning", " [async]") : "";
|
|
1257
|
+
return new Text(agent + agentName + label + wait, 0, 0);
|
|
1258
|
+
},
|
|
1259
|
+
renderResult(result, { isPartial }, theme) {
|
|
1260
|
+
if (isPartial) {
|
|
1261
|
+
const status = result.details?.status ?? "running";
|
|
1262
|
+
const emoji = statusEmoji(String(status));
|
|
1263
|
+
return new Text(theme.fg("accent", `${emoji} ${result.content?.[0]?.text ?? "Running…"}`), 0, 0);
|
|
1264
|
+
}
|
|
1265
|
+
const status = String(result.details?.status ?? "");
|
|
1266
|
+
if (status && isTerminalStatus(status)) {
|
|
1267
|
+
const emoji = statusEmoji(status);
|
|
1268
|
+
return new Text(`${theme.fg(status === "done" ? "success" : "error", `${emoji} ${status}`)}\n${(result.content?.[0]?.text ?? "").split("\n").map((line) => theme.fg("muted", ` ${line}`)).join("\n")}`, 0, 0);
|
|
1269
|
+
}
|
|
1270
|
+
return new Text((result.content?.[0]?.text ?? "").split("\n").map((line) => theme.fg("muted", line)).join("\n"), 0, 0);
|
|
474
1271
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const status = result.details?.status;
|
|
497
|
-
if (status && TERMINAL_STATUSES.has(status)) {
|
|
498
|
-
const emoji = statusEmoji(status);
|
|
499
|
-
const header = theme.fg(status === "done" ? "success" : "error", `${emoji} ${status}`);
|
|
500
|
-
const lines = (result.content?.[0]?.text ?? "").split("\n");
|
|
501
|
-
return new Text(header + "\n" + lines.map((l) => theme.fg("muted", ` ${l}`)).join("\n"), 0, 0);
|
|
1272
|
+
});
|
|
1273
|
+
pi.registerTool({
|
|
1274
|
+
name: "avenor_status",
|
|
1275
|
+
label: "Avenor Status",
|
|
1276
|
+
description: "Get status of a run or all runs. Surfaces pending permission requests.",
|
|
1277
|
+
parameters: Type.Object({
|
|
1278
|
+
run_id: Type.Optional(Type.String({ description: "Specific run ID to query" })),
|
|
1279
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
1280
|
+
}),
|
|
1281
|
+
async execute(_toolCallId, params) {
|
|
1282
|
+
const result = await deps.statusTool({
|
|
1283
|
+
runId: params.run_id,
|
|
1284
|
+
supervisorId: params.supervisor_id
|
|
1285
|
+
});
|
|
1286
|
+
return {
|
|
1287
|
+
content: [{
|
|
1288
|
+
type: "text",
|
|
1289
|
+
text: JSON.stringify(result, null, 2)
|
|
1290
|
+
}],
|
|
1291
|
+
details: result
|
|
1292
|
+
};
|
|
502
1293
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
pi.registerTool({
|
|
529
|
-
name: "avenor_answer_permission",
|
|
530
|
-
label: "Avenor Answer Permission",
|
|
531
|
-
description: "Answer a pending permission request for a sub-agent. Pass option_id from avenor_status pending_permission.options.",
|
|
532
|
-
parameters: Type.Object({
|
|
533
|
-
run_id: Type.String({ description: "Run ID with the pending permission" }),
|
|
534
|
-
option_id: Type.String({ description: "allow_once | allow_always | deny" }),
|
|
535
|
-
request_id: Type.Optional(Type.String({ description: "Permission request ID (auto-discovered if omitted)" })),
|
|
536
|
-
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
537
|
-
}),
|
|
538
|
-
async execute(_toolCallId, params) {
|
|
539
|
-
const result = await answerPermissionTool({
|
|
540
|
-
runId: params.run_id,
|
|
541
|
-
optionId: params.option_id,
|
|
542
|
-
requestId: params.request_id,
|
|
543
|
-
supervisorId: params.supervisor_id
|
|
544
|
-
});
|
|
545
|
-
return {
|
|
546
|
-
content: [{
|
|
547
|
-
type: "text",
|
|
548
|
-
text: JSON.stringify(result, null, 2)
|
|
549
|
-
}],
|
|
550
|
-
details: result
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
pi.registerTool({
|
|
555
|
-
name: "avenor_follow_up",
|
|
556
|
-
label: "Avenor Follow Up",
|
|
557
|
-
description: "Resume a completed run with a follow-up message.",
|
|
558
|
-
parameters: Type.Object({
|
|
559
|
-
run_id: Type.String({ description: "Completed run ID to resume" }),
|
|
560
|
-
message: Type.String({ description: "Follow-up message text" }),
|
|
561
|
-
label: Type.Optional(Type.String({ description: "Override label (defaults to <original>-followup)" })),
|
|
562
|
-
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
563
|
-
}),
|
|
564
|
-
async execute(_toolCallId, params) {
|
|
565
|
-
const result = await followUpTool({
|
|
566
|
-
runId: params.run_id,
|
|
567
|
-
message: params.message,
|
|
568
|
-
label: params.label,
|
|
569
|
-
supervisorId: params.supervisor_id
|
|
570
|
-
});
|
|
571
|
-
return {
|
|
572
|
-
content: [{
|
|
573
|
-
type: "text",
|
|
574
|
-
text: JSON.stringify(result, null, 2)
|
|
575
|
-
}],
|
|
576
|
-
details: result
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
pi.registerTool({
|
|
581
|
-
name: "avenor_events",
|
|
582
|
-
label: "Avenor Events",
|
|
583
|
-
description: "Read events from a run. Filter by type. Returns last N events.",
|
|
584
|
-
parameters: Type.Object({
|
|
585
|
-
run_id: Type.String({ description: "Run ID to read events from" }),
|
|
586
|
-
types: Type.Optional(Type.Array(Type.String(), { description: "Filter by event types" })),
|
|
587
|
-
limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
|
|
588
|
-
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
589
|
-
}),
|
|
590
|
-
async execute(_toolCallId, params) {
|
|
591
|
-
const result = await eventsTool({
|
|
592
|
-
runId: params.run_id,
|
|
593
|
-
types: params.types,
|
|
594
|
-
limit: params.limit,
|
|
595
|
-
supervisorId: params.supervisor_id
|
|
596
|
-
});
|
|
597
|
-
return {
|
|
598
|
-
content: [{
|
|
599
|
-
type: "text",
|
|
600
|
-
text: JSON.stringify(result, null, 2)
|
|
601
|
-
}],
|
|
602
|
-
details: result
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
pi.registerTool({
|
|
607
|
-
name: "avenor_shutdown",
|
|
608
|
-
label: "Avenor Shutdown",
|
|
609
|
-
description: "Shut down the avenor supervisor and clean up temp files.",
|
|
610
|
-
parameters: Type.Object({
|
|
611
|
-
supervisor_id: Type.Optional(Type.String({ description: "Supervisor to shut down (defaults to singleton)" })),
|
|
612
|
-
force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
|
|
613
|
-
}),
|
|
614
|
-
async execute(_toolCallId, params) {
|
|
615
|
-
await stopPolling();
|
|
616
|
-
trackedRuns.clear();
|
|
617
|
-
const result = await shutdownTool({
|
|
618
|
-
supervisorId: params.supervisor_id,
|
|
619
|
-
force: params.force
|
|
620
|
-
});
|
|
621
|
-
return {
|
|
622
|
-
content: [{
|
|
623
|
-
type: "text",
|
|
624
|
-
text: JSON.stringify(result, null, 2)
|
|
625
|
-
}],
|
|
626
|
-
details: result
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
pi.registerCommand("avenor-status", {
|
|
631
|
-
description: "Show status of all avenor runs",
|
|
632
|
-
handler: async (_args, ctx) => {
|
|
633
|
-
const entries = await pollRuns();
|
|
634
|
-
if (entries.length === 0) {
|
|
635
|
-
ctx.ui.notify("No active avenor runs", "info");
|
|
636
|
-
return;
|
|
1294
|
+
});
|
|
1295
|
+
pi.registerTool({
|
|
1296
|
+
name: "avenor_inspect",
|
|
1297
|
+
label: "Avenor Inspect",
|
|
1298
|
+
description: "Inspect a run with bounded snapshot, transcript, tool, permission, and final output details.",
|
|
1299
|
+
parameters: Type.Object({
|
|
1300
|
+
run_id: Type.String({ description: "Run ID to inspect" }),
|
|
1301
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
|
|
1302
|
+
limit: Type.Optional(Type.Number({ description: "Replay event limit (default 128)" })),
|
|
1303
|
+
after_seq: Type.Optional(Type.Number({ description: "Only include events after this sequence number" }))
|
|
1304
|
+
}),
|
|
1305
|
+
async execute(_toolCallId, params) {
|
|
1306
|
+
const payload = buildInspectPayload(await deps.inspectTool({
|
|
1307
|
+
runId: params.run_id,
|
|
1308
|
+
supervisorId: params.supervisor_id,
|
|
1309
|
+
limit: params.limit,
|
|
1310
|
+
after_seq: params.after_seq
|
|
1311
|
+
}));
|
|
1312
|
+
return {
|
|
1313
|
+
content: [{
|
|
1314
|
+
type: "text",
|
|
1315
|
+
text: JSON.stringify(payload, null, 2)
|
|
1316
|
+
}],
|
|
1317
|
+
details: payload
|
|
1318
|
+
};
|
|
637
1319
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1320
|
+
});
|
|
1321
|
+
pi.registerTool({
|
|
1322
|
+
name: "avenor_answer_permission",
|
|
1323
|
+
label: "Avenor Answer Permission",
|
|
1324
|
+
description: "Answer a pending permission request for a sub-agent. Pass option_id from avenor_status pending_permission.options.",
|
|
1325
|
+
parameters: Type.Object({
|
|
1326
|
+
run_id: Type.String({ description: "Run ID with the pending permission" }),
|
|
1327
|
+
option_id: Type.String({ description: "allow_once | allow_always | deny" }),
|
|
1328
|
+
request_id: Type.Optional(Type.String({ description: "Permission request ID (auto-discovered if omitted)" })),
|
|
1329
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
1330
|
+
}),
|
|
1331
|
+
async execute(_toolCallId, params) {
|
|
1332
|
+
const result = await deps.answerPermissionTool({
|
|
1333
|
+
runId: params.run_id,
|
|
1334
|
+
optionId: params.option_id,
|
|
1335
|
+
requestId: params.request_id,
|
|
1336
|
+
supervisorId: params.supervisor_id
|
|
1337
|
+
});
|
|
1338
|
+
return {
|
|
1339
|
+
content: [{
|
|
1340
|
+
type: "text",
|
|
1341
|
+
text: JSON.stringify(result, null, 2)
|
|
1342
|
+
}],
|
|
1343
|
+
details: result
|
|
1344
|
+
};
|
|
655
1345
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1346
|
+
});
|
|
1347
|
+
pi.registerTool({
|
|
1348
|
+
name: "avenor_follow_up",
|
|
1349
|
+
label: "Avenor Follow Up",
|
|
1350
|
+
description: "Resume a completed run with a follow-up message.",
|
|
1351
|
+
parameters: Type.Object({
|
|
1352
|
+
run_id: Type.String({ description: "Completed run ID to resume" }),
|
|
1353
|
+
message: Type.String({ description: "Follow-up message text" }),
|
|
1354
|
+
label: Type.Optional(Type.String({ description: "Override label (defaults to <original>-followup)" })),
|
|
1355
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
1356
|
+
}),
|
|
1357
|
+
async execute(_toolCallId, params) {
|
|
1358
|
+
const result = await deps.followUpTool({
|
|
1359
|
+
runId: params.run_id,
|
|
1360
|
+
message: params.message,
|
|
1361
|
+
label: params.label,
|
|
1362
|
+
supervisorId: params.supervisor_id
|
|
1363
|
+
});
|
|
1364
|
+
try {
|
|
1365
|
+
const rawStatus = await deps.statusTool({
|
|
1366
|
+
runId: result.run_id,
|
|
1367
|
+
supervisorId: params.supervisor_id
|
|
1368
|
+
});
|
|
1369
|
+
const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
|
|
1370
|
+
upsertTrackedRun({
|
|
1371
|
+
runId: result.run_id,
|
|
1372
|
+
label: result.label,
|
|
1373
|
+
agent: status?.agent,
|
|
1374
|
+
runtimeId: status?.runtime_id,
|
|
1375
|
+
supervisorId: params.supervisor_id
|
|
1376
|
+
}, status, false);
|
|
1377
|
+
} catch {
|
|
1378
|
+
upsertTrackedRun({
|
|
1379
|
+
runId: result.run_id,
|
|
1380
|
+
label: result.label,
|
|
1381
|
+
supervisorId: params.supervisor_id
|
|
1382
|
+
}, void 0, false);
|
|
662
1383
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1384
|
+
startPolling();
|
|
1385
|
+
return {
|
|
1386
|
+
content: [{
|
|
1387
|
+
type: "text",
|
|
1388
|
+
text: JSON.stringify(result, null, 2)
|
|
1389
|
+
}],
|
|
1390
|
+
details: result
|
|
1391
|
+
};
|
|
668
1392
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
1393
|
+
});
|
|
1394
|
+
pi.registerTool({
|
|
1395
|
+
name: "avenor_events",
|
|
1396
|
+
label: "Avenor Events",
|
|
1397
|
+
description: "Read events from a run. Filter by type. Returns last N events.",
|
|
1398
|
+
parameters: Type.Object({
|
|
1399
|
+
run_id: Type.String({ description: "Run ID to read events from" }),
|
|
1400
|
+
types: Type.Optional(Type.Array(Type.String(), { description: "Filter by event types" })),
|
|
1401
|
+
limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
|
|
1402
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
1403
|
+
}),
|
|
1404
|
+
async execute(_toolCallId, params) {
|
|
1405
|
+
const result = await deps.eventsTool({
|
|
1406
|
+
runId: params.run_id,
|
|
1407
|
+
types: params.types,
|
|
1408
|
+
limit: params.limit,
|
|
1409
|
+
supervisorId: params.supervisor_id
|
|
675
1410
|
});
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1411
|
+
return {
|
|
1412
|
+
content: [{
|
|
1413
|
+
type: "text",
|
|
1414
|
+
text: JSON.stringify(result, null, 2)
|
|
1415
|
+
}],
|
|
1416
|
+
details: result
|
|
1417
|
+
};
|
|
680
1418
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1419
|
+
});
|
|
1420
|
+
pi.registerTool({
|
|
1421
|
+
name: "avenor_shutdown",
|
|
1422
|
+
label: "Avenor Shutdown",
|
|
1423
|
+
description: "Shut down the avenor supervisor and clean up temp files.",
|
|
1424
|
+
parameters: Type.Object({
|
|
1425
|
+
supervisor_id: Type.Optional(Type.String({ description: "Supervisor to shut down (defaults to singleton)" })),
|
|
1426
|
+
force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
|
|
1427
|
+
}),
|
|
1428
|
+
async execute(_toolCallId, params) {
|
|
1429
|
+
await stopPolling();
|
|
1430
|
+
trackedRuns.clear();
|
|
1431
|
+
const result = await deps.shutdownTool({
|
|
1432
|
+
supervisorId: params.supervisor_id,
|
|
1433
|
+
force: params.force
|
|
685
1434
|
});
|
|
686
|
-
return
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1435
|
+
return {
|
|
1436
|
+
content: [{
|
|
1437
|
+
type: "text",
|
|
1438
|
+
text: JSON.stringify(result, null, 2)
|
|
1439
|
+
}],
|
|
1440
|
+
details: result
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
pi.registerCommand("avenor-status", {
|
|
1445
|
+
description: "Show status of all avenor runs",
|
|
1446
|
+
handler: async (_args, ctx) => {
|
|
1447
|
+
const entries = await pollRuns();
|
|
1448
|
+
if (entries.length === 0) {
|
|
1449
|
+
ctx.ui.notify("No active avenor runs", "info");
|
|
1450
|
+
return;
|
|
697
1451
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1452
|
+
ctx.ui.setWidget("avenor-status-detail", entries.map((entry) => formatRunLine(entry)));
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
pi.registerCommand("avenor-watch", {
|
|
1456
|
+
description: "Open a live run inspector by run ID, label, or agent name",
|
|
1457
|
+
getArgumentCompletions: (prefix) => {
|
|
1458
|
+
const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
|
|
1459
|
+
value: run.runId,
|
|
1460
|
+
label: `${sanitizeText(run.label)} (${sanitizeText(run.agent)}, ${sanitizeText(run.runId).slice(0, 8)})`
|
|
1461
|
+
}));
|
|
1462
|
+
return items.length > 0 ? items : null;
|
|
1463
|
+
},
|
|
1464
|
+
handler: async (args, ctx) => {
|
|
1465
|
+
await openRunInspector(ctx, {
|
|
1466
|
+
trackedRuns: Array.from(trackedRuns.values()).map(toWatchRunRef),
|
|
1467
|
+
initialRunId: args?.trim() || void 0,
|
|
1468
|
+
deps: {
|
|
1469
|
+
...createWatchDeps(),
|
|
1470
|
+
followUpRun: (run, text) => followUpTrackedRun(run, text)
|
|
1471
|
+
},
|
|
1472
|
+
onTrackRun: (run, status) => {
|
|
1473
|
+
upsertTrackedRun(run, status, false);
|
|
1474
|
+
if (status && !isTerminalStatus(status.status)) startPolling();
|
|
712
1475
|
}
|
|
713
|
-
}
|
|
714
|
-
};
|
|
715
|
-
watchTick();
|
|
716
|
-
}
|
|
717
|
-
});
|
|
718
|
-
pi.registerCommand("avenor-cancel", {
|
|
719
|
-
description: "Cancel a running avenor sub-agent",
|
|
720
|
-
getArgumentCompletions: (prefix) => {
|
|
721
|
-
const items = Array.from(trackedRuns.values()).filter((r) => r.label.startsWith(prefix) || r.runId.startsWith(prefix)).map((r) => ({
|
|
722
|
-
value: r.runId,
|
|
723
|
-
label: `${r.label} (${r.runId.slice(0, 8)})`
|
|
724
|
-
}));
|
|
725
|
-
return items.length > 0 ? items : null;
|
|
726
|
-
},
|
|
727
|
-
handler: async (args, ctx) => {
|
|
728
|
-
if (!args?.trim()) {
|
|
729
|
-
ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
|
|
730
|
-
return;
|
|
1476
|
+
});
|
|
731
1477
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1478
|
+
});
|
|
1479
|
+
pi.registerCommand("avenor-cancel", {
|
|
1480
|
+
description: "Cancel a running avenor sub-agent",
|
|
1481
|
+
getArgumentCompletions: (prefix) => {
|
|
1482
|
+
const items = Array.from(trackedRuns.values()).filter((run) => run.label.startsWith(prefix) || run.agent.startsWith(prefix) || run.runId.startsWith(prefix)).map((run) => ({
|
|
1483
|
+
value: run.runId,
|
|
1484
|
+
label: `${sanitizeText(run.label)} (${sanitizeText(run.agent)}, ${sanitizeText(run.runId).slice(0, 8)})`
|
|
1485
|
+
}));
|
|
1486
|
+
return items.length > 0 ? items : null;
|
|
1487
|
+
},
|
|
1488
|
+
handler: async (args, ctx) => {
|
|
1489
|
+
if (!args?.trim()) {
|
|
1490
|
+
ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const runId = args.trim();
|
|
1494
|
+
const run = trackedRuns.get(runId);
|
|
1495
|
+
const label = run?.label ?? runId;
|
|
1496
|
+
const runtimeId = run?.runtimeId ?? run?.lastStatus?.runtime_id;
|
|
1497
|
+
if (!runtimeId) {
|
|
1498
|
+
ctx.ui.notify(`No runtime id available for "${label}"`, "warning");
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (!await ctx.ui.confirm("Cancel run", `Cancel "${label}" (${runId.slice(0, 8)})?`)) return;
|
|
1502
|
+
try {
|
|
1503
|
+
await createWatchDeps().cancelRun(toWatchRunRef(run ?? {
|
|
1504
|
+
runId,
|
|
1505
|
+
agent: "unknown",
|
|
1506
|
+
label,
|
|
1507
|
+
startTime: Date.now()
|
|
1508
|
+
}), runtimeId);
|
|
1509
|
+
ctx.ui.notify(`Cancelled "${label}"`, "info");
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
ctx.ui.notify(`Failed to cancel: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
1512
|
+
}
|
|
742
1513
|
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
1514
|
+
});
|
|
1515
|
+
};
|
|
745
1516
|
}
|
|
1517
|
+
var src_default = createExtension();
|
|
746
1518
|
//#endregion
|
|
747
|
-
export { src_default as default, statusSupervisorId };
|
|
1519
|
+
export { buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, src_default as default, isTerminalStatus, renderStatusLines, statusSupervisorId };
|