@dougbots/avenor-pi 0.15.0 → 0.16.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/README.md +6 -2
- package/dist/index.d.ts +7 -3
- package/dist/index.js +670 -311
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Input, Key, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import {
|
|
4
|
-
import { Supervisor, THINKING_LEVELS, answerPermissionTool, askTool, attachOwner, brokerReceiveTool, brokerReplyTool, cancelTool, dial, eventsTool, extractEventText, extractReplyMessage, followUpTool, formatAskFailure, inspectTool, killTool, normalizeAskTimeoutMs, observeRun, peersTool, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
|
|
3
|
+
import { Supervisor, THINKING_LEVELS, answerPermissionTool, attachOwner, boundFinalOutput, brokerReceiveTool, createChildMessageProvider, createHostMessageProvider, dial, eventsTool, extractEventText, extractReplyMessage, extractTurnText, getSupervisorClient, inspectTool, installAskClaims, isGenerationMismatchError, killTool, messageTool, normalizeAskTimeoutMs, normalizeTurnStopReason, observeRun, peersDetails, peersTool, renderPeerList, resultOutput, resultTool, shutdownTool, socketsRoot, spawnTool, statusTool, validateSpawnSelection, workflowCancelTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
|
|
5
4
|
import * as fs from "node:fs";
|
|
6
5
|
import * as path from "node:path";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
7
|
//#region src/subagent-broker.ts
|
|
8
8
|
/** Deadline for a cleanup call whose caller's own deadline has already expired. */
|
|
9
9
|
const ASK_CLEANUP_TIMEOUT_MS = 5e3;
|
|
@@ -36,25 +36,38 @@ function parseControlMessage(msg) {
|
|
|
36
36
|
return {
|
|
37
37
|
from_run_id: fromRunId,
|
|
38
38
|
message_id: messageId,
|
|
39
|
-
message: p.message
|
|
39
|
+
message: p.message,
|
|
40
|
+
role: typeof p.role === "string" ? p.role : void 0
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Format an inbound ask as a steer body. Both modes share the formatter; only
|
|
45
|
+
* the heading and the optional role metadata differ.
|
|
46
|
+
*/
|
|
47
|
+
function formatInboundAskBody(ask, options) {
|
|
48
|
+
const sender = ask.from_run_id;
|
|
49
|
+
const role = options.role ?? ask.role;
|
|
50
|
+
const from = sender ? role && role !== "agent" ? ` from \`${sender}\` (${role})` : ` from \`${sender}\`` : "";
|
|
51
|
+
return `**📨 ${options.heading}**${from}\n\n${ask.message}\n\nTo answer, use avenor_message({ action: "reply", reply_to: ${JSON.stringify(ask.message_id)}, message: "..." }).`;
|
|
44
52
|
}
|
|
45
53
|
/** A broker HTTP response error, so callers can classify by status instead of
|
|
46
54
|
* matching digits in the request text. */
|
|
47
55
|
var BrokerHttpError = class extends Error {
|
|
48
56
|
status;
|
|
49
|
-
|
|
57
|
+
code;
|
|
58
|
+
body;
|
|
59
|
+
constructor(path, status, body, code = "") {
|
|
50
60
|
super(`broker ${path}: ${status} ${body}`);
|
|
51
61
|
this.name = "BrokerHttpError";
|
|
52
62
|
this.status = status;
|
|
63
|
+
this.code = code;
|
|
64
|
+
this.body = body;
|
|
53
65
|
}
|
|
54
66
|
};
|
|
55
67
|
/** Build the `/send` body for one agent_message, as the broker child tools do. */
|
|
56
68
|
function buildAgentMessage(fields) {
|
|
57
69
|
return {
|
|
70
|
+
...fields.generation !== void 0 ? { generation: fields.generation } : {},
|
|
58
71
|
from_run_id: fields.fromRunId,
|
|
59
72
|
to_run_id: fields.toRunId,
|
|
60
73
|
type: "agent_message",
|
|
@@ -70,6 +83,15 @@ function buildAgentMessage(fields) {
|
|
|
70
83
|
}
|
|
71
84
|
};
|
|
72
85
|
}
|
|
86
|
+
async function codedError(path, response) {
|
|
87
|
+
const body = (await response.text()).trim();
|
|
88
|
+
let code = "";
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(body);
|
|
91
|
+
if (typeof parsed.code === "string") code = parsed.code;
|
|
92
|
+
} catch {}
|
|
93
|
+
return new BrokerHttpError(path, response.status, body, code);
|
|
94
|
+
}
|
|
73
95
|
async function postBroker(env, path, body, fetchImpl = fetch, signal) {
|
|
74
96
|
const res = await fetchImpl(`${env.brokerUrl}${path}`, {
|
|
75
97
|
method: "POST",
|
|
@@ -81,27 +103,45 @@ async function postBroker(env, path, body, fetchImpl = fetch, signal) {
|
|
|
81
103
|
}),
|
|
82
104
|
...signal ? { signal } : {}
|
|
83
105
|
});
|
|
84
|
-
if (!res.ok) throw
|
|
106
|
+
if (!res.ok) throw await codedError(path, res);
|
|
107
|
+
return res.json();
|
|
108
|
+
}
|
|
109
|
+
/** Authenticated GET; broker credentials travel in headers, never the URL. */
|
|
110
|
+
async function getBroker(env, path, fetchImpl = fetch, signal) {
|
|
111
|
+
const res = await fetchImpl(`${env.brokerUrl}${path}`, {
|
|
112
|
+
method: "GET",
|
|
113
|
+
headers: {
|
|
114
|
+
"X-Avenor-Run-Id": env.runId,
|
|
115
|
+
"X-Avenor-Run-Token": env.token
|
|
116
|
+
},
|
|
117
|
+
...signal ? { signal } : {}
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok) throw await codedError(path, res);
|
|
85
120
|
return res.json();
|
|
86
121
|
}
|
|
122
|
+
/** Read the ask generation `/sessions` advertises, or empty for none. */
|
|
123
|
+
async function readGeneration(env, fetchImpl) {
|
|
124
|
+
const body = await getBroker(env, "/sessions", fetchImpl);
|
|
125
|
+
return typeof body?.generation === "string" ? body.generation : "";
|
|
126
|
+
}
|
|
87
127
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
128
|
+
* Pin the generation on first use and reuse it, so one child binds to one broker
|
|
129
|
+
* generation. The broker rotates it when its ask store resets; a pinned child is
|
|
130
|
+
* then refused instead of being retargeted. A broker that advertises no
|
|
131
|
+
* generation is not pinned, so the next call reads it again.
|
|
90
132
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (const [, entry] of pending) if (!fromRunId || entry.from_run_id === fromRunId) match = entry;
|
|
104
|
-
return match ?? null;
|
|
133
|
+
function pinGeneration(env, fetchImpl) {
|
|
134
|
+
let pinned;
|
|
135
|
+
return () => {
|
|
136
|
+
pinned ??= readGeneration(env, fetchImpl).then((generation) => {
|
|
137
|
+
if (!generation) pinned = void 0;
|
|
138
|
+
return generation;
|
|
139
|
+
}, (error) => {
|
|
140
|
+
pinned = void 0;
|
|
141
|
+
throw error;
|
|
142
|
+
});
|
|
143
|
+
return pinned;
|
|
144
|
+
};
|
|
105
145
|
}
|
|
106
146
|
function combineSignals(...signals) {
|
|
107
147
|
const active = signals.filter((signal) => signal !== void 0);
|
|
@@ -109,32 +149,47 @@ function combineSignals(...signals) {
|
|
|
109
149
|
if (active.length === 1) return active[0];
|
|
110
150
|
return AbortSignal.any(active);
|
|
111
151
|
}
|
|
112
|
-
function describeFailure(error) {
|
|
113
|
-
if (error instanceof Error) {
|
|
114
|
-
if (error.name === "TimeoutError") return "timed out";
|
|
115
|
-
if (error.name === "AbortError") return "cancelled";
|
|
116
|
-
return error.message || error.name;
|
|
117
|
-
}
|
|
118
|
-
return String(error);
|
|
119
|
-
}
|
|
120
152
|
/**
|
|
121
153
|
* Withdraw a pending ask through the broker as this run. The broker's own error
|
|
122
154
|
* propagates so a caller reporting to a model shows its exact text.
|
|
123
155
|
*/
|
|
124
|
-
async function withdrawPendingAsk(env, messageId, fetchImpl = fetch) {
|
|
125
|
-
await postBroker(env, "/cancel_message", {
|
|
156
|
+
async function withdrawPendingAsk(env, messageId, generation, fetchImpl = fetch) {
|
|
157
|
+
const body = await postBroker(env, "/cancel_message", {
|
|
158
|
+
cancel_message_id: messageId,
|
|
159
|
+
generation
|
|
160
|
+
}, fetchImpl, AbortSignal.timeout(ASK_CLEANUP_TIMEOUT_MS));
|
|
161
|
+
if (typeof body?.outcome === "string" && body.outcome) return { outcome: body.outcome };
|
|
162
|
+
return { outcome: "withdrawn" };
|
|
126
163
|
}
|
|
127
164
|
/**
|
|
128
165
|
* Withdraw a pending ask and classify whether the edge may still exist on the
|
|
129
166
|
* broker: true means the cleanup could not be confirmed, false means it is gone.
|
|
130
167
|
*/
|
|
131
|
-
async function withdrawAsk(env, messageId, fetchImpl) {
|
|
168
|
+
async function withdrawAsk(env, messageId, generation, fetchImpl) {
|
|
132
169
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
170
|
+
return {
|
|
171
|
+
pending: false,
|
|
172
|
+
outcome: (await withdrawPendingAsk(env, messageId, generation, fetchImpl)).outcome
|
|
173
|
+
};
|
|
135
174
|
} catch (error) {
|
|
136
|
-
if (error instanceof BrokerHttpError)
|
|
137
|
-
|
|
175
|
+
if (error instanceof BrokerHttpError) {
|
|
176
|
+
if (error.code === "fence-capacity") return {
|
|
177
|
+
pending: true,
|
|
178
|
+
outcome: "possibly-pending"
|
|
179
|
+
};
|
|
180
|
+
if (error.status === 404) return {
|
|
181
|
+
pending: false,
|
|
182
|
+
outcome: "edge-absent"
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
pending: true,
|
|
186
|
+
outcome: "possibly-pending"
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
pending: true,
|
|
191
|
+
outcome: "possibly-pending"
|
|
192
|
+
};
|
|
138
193
|
}
|
|
139
194
|
}
|
|
140
195
|
/** Fetch causes for a dial that never opened, so no request reached the broker. */
|
|
@@ -187,12 +242,10 @@ async function brokerReachable(env, fetchImpl) {
|
|
|
187
242
|
}
|
|
188
243
|
}
|
|
189
244
|
/**
|
|
190
|
-
* Whether a failed send provably never reached the broker
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* response, a reset, or an abort against a broker that answers at all may have
|
|
195
|
-
* been processed and still needs a withdraw.
|
|
245
|
+
* Whether a failed send provably never reached the broker: a dial that never
|
|
246
|
+
* opened or an unusable broker address carries no request, and a send that drew
|
|
247
|
+
* no HTTP response while the broker no longer accepts a dial cannot have left
|
|
248
|
+
* its edge in a broker that is gone.
|
|
196
249
|
*/
|
|
197
250
|
async function sendNeverReachedBroker(error, env, fetchImpl) {
|
|
198
251
|
if (error instanceof BrokerHttpError) return false;
|
|
@@ -200,21 +253,84 @@ async function sendNeverReachedBroker(error, env, fetchImpl) {
|
|
|
200
253
|
if (abortedBeforeResponse(error)) return false;
|
|
201
254
|
return !await brokerReachable(env, fetchImpl);
|
|
202
255
|
}
|
|
256
|
+
function brokerOutcome(body) {
|
|
257
|
+
const record = body !== null && typeof body === "object" ? body : {};
|
|
258
|
+
return typeof record.outcome === "string" && record.outcome ? record.outcome : "sent";
|
|
259
|
+
}
|
|
203
260
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
261
|
+
* The outcome the wire contract documents for a known code, or undefined for a
|
|
262
|
+
* code outside it. This mirrors the Go transport's wireErrorClasses table: a
|
|
263
|
+
* code whose documented outcome is a credential or edge refusal keeps it, and
|
|
264
|
+
* every other code names a request that was never admitted.
|
|
208
265
|
*/
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
266
|
+
function knownCodeOutcome(code) {
|
|
267
|
+
switch (code) {
|
|
268
|
+
case "auth-rejected":
|
|
269
|
+
case "sender-mismatch": return "auth-rejected";
|
|
270
|
+
case "not-ask-sender":
|
|
271
|
+
case "not-ask-target": return "edge-absent";
|
|
272
|
+
case "invalid-request":
|
|
273
|
+
case "generation-mismatch":
|
|
274
|
+
case "unsupported-protocol":
|
|
275
|
+
case "malformed-message-id":
|
|
276
|
+
case "message-id-expired":
|
|
277
|
+
case "message-id-horizon":
|
|
278
|
+
case "duplicate-message-id":
|
|
279
|
+
case "mutual-ask":
|
|
280
|
+
case "fence-capacity":
|
|
281
|
+
case "target-unavailable":
|
|
282
|
+
case "transport-closed":
|
|
283
|
+
case "unsupported-operation": return "not-sent";
|
|
284
|
+
default: return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The outcome of a response the messaging envelope does not describe, as the Go
|
|
289
|
+
* local transport classifies it: only a 401 there is a credential rejection, a
|
|
290
|
+
* code-less 403 is the router's not-ask-target refusal, and every other status
|
|
291
|
+
* leaves transmission uncertain.
|
|
292
|
+
*/
|
|
293
|
+
function unenvelopedOutcome(status) {
|
|
294
|
+
if (status === 401) return "auth-rejected";
|
|
295
|
+
if (status === 403) return "edge-absent";
|
|
296
|
+
return "possibly-pending";
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Classify a broker HTTP rejection the way the Go local transport does: the
|
|
300
|
+
* body's outcome wins, then a known wire code, then the status. A rejection that
|
|
301
|
+
* names neither leaves transmission uncertain, so it is never reported
|
|
302
|
+
* `not-sent`; only a send that provably never left the surface is.
|
|
303
|
+
*/
|
|
304
|
+
function brokerErrorOutcome(error) {
|
|
305
|
+
if (!(error instanceof BrokerHttpError)) return void 0;
|
|
306
|
+
try {
|
|
307
|
+
const parsed = JSON.parse(error.body);
|
|
308
|
+
if (parsed && typeof parsed.outcome === "string" && parsed.outcome) return parsed.outcome;
|
|
309
|
+
} catch {}
|
|
310
|
+
if (error.code) return knownCodeOutcome(error.code) ?? "possibly-pending";
|
|
311
|
+
return unenvelopedOutcome(error.status);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The outcome of a generation pin that failed before any message was sent: no
|
|
315
|
+
* request reached the send route, so only a credential refusal keeps its own
|
|
316
|
+
* outcome and everything else is a message that was never sent.
|
|
317
|
+
*/
|
|
318
|
+
function pinFailureOutcome(error) {
|
|
319
|
+
return brokerErrorOutcome(error) === "auth-rejected" ? "auth-rejected" : "not-sent";
|
|
320
|
+
}
|
|
321
|
+
async function brokerAsk(env, args, messageId, fetchImpl, pin) {
|
|
322
|
+
const role = "agent";
|
|
212
323
|
const timeoutMs = normalizeAskTimeoutMs(args.timeoutMs);
|
|
213
324
|
const signal = combineSignals(args.signal, timeoutMs !== void 0 ? AbortSignal.timeout(timeoutMs) : void 0);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
325
|
+
let generation;
|
|
326
|
+
try {
|
|
327
|
+
generation = await pin();
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return {
|
|
330
|
+
outcome: pinFailureOutcome(error),
|
|
331
|
+
messageId
|
|
332
|
+
};
|
|
333
|
+
}
|
|
218
334
|
try {
|
|
219
335
|
await postBroker(env, "/send", buildAgentMessage({
|
|
220
336
|
fromRunId: env.runId,
|
|
@@ -222,22 +338,237 @@ async function askViaBroker(env, args, fetchImpl = fetch) {
|
|
|
222
338
|
messageId,
|
|
223
339
|
message: args.message,
|
|
224
340
|
role,
|
|
225
|
-
expectsReply: true
|
|
341
|
+
expectsReply: true,
|
|
342
|
+
generation
|
|
226
343
|
}), fetchImpl, signal);
|
|
227
344
|
} catch (error) {
|
|
228
|
-
if (await sendNeverReachedBroker(error, env, fetchImpl))
|
|
229
|
-
|
|
345
|
+
if (await sendNeverReachedBroker(error, env, fetchImpl)) return {
|
|
346
|
+
outcome: "not-sent",
|
|
347
|
+
messageId
|
|
348
|
+
};
|
|
349
|
+
const rejected = brokerErrorOutcome(error);
|
|
350
|
+
if (rejected) return {
|
|
351
|
+
outcome: rejected,
|
|
352
|
+
messageId
|
|
353
|
+
};
|
|
354
|
+
return {
|
|
355
|
+
outcome: (await withdrawAsk(env, messageId, generation, fetchImpl)).outcome,
|
|
356
|
+
messageId
|
|
357
|
+
};
|
|
230
358
|
}
|
|
231
359
|
let reply;
|
|
232
360
|
try {
|
|
233
|
-
reply = await postBroker(env, "/wait_reply", {
|
|
361
|
+
reply = await postBroker(env, "/wait_reply", {
|
|
362
|
+
waiting_for: messageId,
|
|
363
|
+
generation
|
|
364
|
+
}, fetchImpl, signal);
|
|
234
365
|
} catch (error) {
|
|
235
|
-
|
|
366
|
+
const timedOut = error instanceof Error && error.name === "TimeoutError" || brokerErrorOutcome(error) === "timed-out";
|
|
367
|
+
const cleaned = await withdrawAsk(env, messageId, generation, fetchImpl);
|
|
368
|
+
if (timedOut) return {
|
|
369
|
+
outcome: "timed-out",
|
|
370
|
+
messageId
|
|
371
|
+
};
|
|
372
|
+
const outcome = brokerErrorOutcome(error);
|
|
373
|
+
if (outcome) return {
|
|
374
|
+
outcome,
|
|
375
|
+
messageId
|
|
376
|
+
};
|
|
377
|
+
if (cleaned.pending) return {
|
|
378
|
+
outcome: "possibly-pending",
|
|
379
|
+
messageId
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
outcome: cleaned.outcome,
|
|
383
|
+
messageId
|
|
384
|
+
};
|
|
236
385
|
}
|
|
237
|
-
const record = reply && typeof reply === "object" ? reply : {};
|
|
386
|
+
const record = reply !== null && typeof reply === "object" ? reply : {};
|
|
387
|
+
const outcome = brokerOutcome(record);
|
|
388
|
+
const result = {
|
|
389
|
+
outcome,
|
|
390
|
+
messageId
|
|
391
|
+
};
|
|
392
|
+
if (outcome === "replied") result.reply = extractReplyMessage(record);
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
function peerRecords(body) {
|
|
396
|
+
const sessions = body !== null && typeof body === "object" ? body.sessions ?? [] : [];
|
|
397
|
+
if (!Array.isArray(sessions)) return [];
|
|
398
|
+
return sessions.map((raw) => {
|
|
399
|
+
const session = raw !== null && typeof raw === "object" ? raw : {};
|
|
400
|
+
const record = { run_id: typeof session.run_id === "string" ? session.run_id : "" };
|
|
401
|
+
for (const key of [
|
|
402
|
+
"label",
|
|
403
|
+
"kind",
|
|
404
|
+
"status",
|
|
405
|
+
"backend",
|
|
406
|
+
"model",
|
|
407
|
+
"dir"
|
|
408
|
+
]) {
|
|
409
|
+
const value = session[key];
|
|
410
|
+
if (typeof value === "string" && value) record[key] = value;
|
|
411
|
+
}
|
|
412
|
+
return record;
|
|
413
|
+
}).filter((record) => record.run_id !== "");
|
|
414
|
+
}
|
|
415
|
+
function wireCode(code) {
|
|
416
|
+
switch (code) {
|
|
417
|
+
case "not-permitted": return "not_permitted";
|
|
418
|
+
case "invalid-name": return "invalid_name";
|
|
419
|
+
case "unknown-target": return "unknown_target";
|
|
420
|
+
case "ambiguous-name": return "ambiguous_name";
|
|
421
|
+
default: return;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/** A broker refusal as the router's typed tool error. */
|
|
425
|
+
function brokerToolError(error) {
|
|
426
|
+
if (error instanceof BrokerHttpError) {
|
|
427
|
+
const code = wireCode(error.code) ?? (error.status === 403 ? "not_permitted" : error.status === 404 ? "unknown_target" : error.status === 409 ? "ambiguous_name" : "refused");
|
|
428
|
+
const text = (() => {
|
|
429
|
+
try {
|
|
430
|
+
const parsed = JSON.parse(error.body);
|
|
431
|
+
if (typeof parsed.error === "string") return parsed.error;
|
|
432
|
+
} catch {}
|
|
433
|
+
return error.body || error.message;
|
|
434
|
+
})();
|
|
435
|
+
const typed = new Error(text);
|
|
436
|
+
typed.name = "ToolError";
|
|
437
|
+
typed.code = code;
|
|
438
|
+
typed.facts = {};
|
|
439
|
+
return typed;
|
|
440
|
+
}
|
|
441
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* The inherited-broker implementation of the core ChildBroker contract. Every
|
|
445
|
+
* request authenticates as this run; a transport failure keeps its own text and
|
|
446
|
+
* becomes a refusal at the router boundary.
|
|
447
|
+
*/
|
|
448
|
+
function createBrokerChildBroker(env, fetchImpl = fetch) {
|
|
449
|
+
const pin = pinGeneration(env, fetchImpl);
|
|
238
450
|
return {
|
|
239
|
-
|
|
240
|
-
|
|
451
|
+
async tell(targetRunId, message, messageId) {
|
|
452
|
+
let generation;
|
|
453
|
+
try {
|
|
454
|
+
generation = await pin();
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return {
|
|
457
|
+
outcome: pinFailureOutcome(error),
|
|
458
|
+
messageId
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
let body;
|
|
462
|
+
try {
|
|
463
|
+
body = await postBroker(env, "/send", buildAgentMessage({
|
|
464
|
+
fromRunId: env.runId,
|
|
465
|
+
toRunId: targetRunId,
|
|
466
|
+
messageId,
|
|
467
|
+
message,
|
|
468
|
+
role: "agent",
|
|
469
|
+
generation
|
|
470
|
+
}), fetchImpl);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
return {
|
|
473
|
+
outcome: brokerErrorOutcome(error) ?? (await sendNeverReachedBroker(error, env, fetchImpl) ? "not-sent" : "possibly-pending"),
|
|
474
|
+
messageId
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
outcome: brokerOutcome(body),
|
|
479
|
+
messageId: typeof body?.message_id === "string" && body.message_id ? body.message_id : messageId
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
ask(targetRunId, message, messageId, timeoutMs, signal) {
|
|
483
|
+
return brokerAsk(env, {
|
|
484
|
+
toRunId: targetRunId,
|
|
485
|
+
message,
|
|
486
|
+
timeoutMs,
|
|
487
|
+
signal
|
|
488
|
+
}, messageId, fetchImpl, pin);
|
|
489
|
+
},
|
|
490
|
+
async reply(askerRunId, inboundAskId, message) {
|
|
491
|
+
let body;
|
|
492
|
+
try {
|
|
493
|
+
body = await postBroker(env, "/send", buildAgentMessage({
|
|
494
|
+
fromRunId: env.runId,
|
|
495
|
+
toRunId: askerRunId,
|
|
496
|
+
messageId: `${inboundAskId}.reply`,
|
|
497
|
+
message,
|
|
498
|
+
role: "agent",
|
|
499
|
+
replyTo: inboundAskId
|
|
500
|
+
}), fetchImpl);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return {
|
|
503
|
+
outcome: brokerErrorOutcome(error) ?? (await sendNeverReachedBroker(error, env, fetchImpl) ? "not-sent" : "possibly-pending"),
|
|
504
|
+
messageId: inboundAskId
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
outcome: brokerOutcome(body),
|
|
509
|
+
messageId: inboundAskId
|
|
510
|
+
};
|
|
511
|
+
},
|
|
512
|
+
async withdraw(messageId) {
|
|
513
|
+
let generation;
|
|
514
|
+
try {
|
|
515
|
+
generation = await pin();
|
|
516
|
+
} catch (error) {
|
|
517
|
+
throw brokerToolError(error);
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
return { outcome: brokerOutcome(await postBroker(env, "/cancel_message", {
|
|
521
|
+
cancel_message_id: messageId,
|
|
522
|
+
generation
|
|
523
|
+
}, fetchImpl)) };
|
|
524
|
+
} catch (error) {
|
|
525
|
+
if (error instanceof BrokerHttpError) {
|
|
526
|
+
if (error.code === "fence-capacity") return { outcome: "possibly-pending" };
|
|
527
|
+
if (error.status === 404) {
|
|
528
|
+
try {
|
|
529
|
+
const parsed = JSON.parse(error.body);
|
|
530
|
+
if (typeof parsed.outcome === "string" && parsed.outcome) return { outcome: parsed.outcome };
|
|
531
|
+
} catch {}
|
|
532
|
+
return { outcome: "edge-absent" };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
throw brokerToolError(error);
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
async peers() {
|
|
539
|
+
return { peers: peerRecords(await getBroker(env, "/sessions", fetchImpl)) };
|
|
540
|
+
},
|
|
541
|
+
async rename(req) {
|
|
542
|
+
try {
|
|
543
|
+
const body = await postBroker(env, "/rename", {
|
|
544
|
+
session_id: req.sessionId ?? "",
|
|
545
|
+
name: req.name
|
|
546
|
+
}, fetchImpl);
|
|
547
|
+
return {
|
|
548
|
+
outcome: "renamed",
|
|
549
|
+
runId: typeof body?.run_id === "string" ? body.run_id : "",
|
|
550
|
+
label: typeof body?.label === "string" ? body.label : "",
|
|
551
|
+
previousLabel: typeof body?.previous_label === "string" ? body.previous_label : ""
|
|
552
|
+
};
|
|
553
|
+
} catch (error) {
|
|
554
|
+
throw brokerToolError(error);
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
async last(runId) {
|
|
558
|
+
try {
|
|
559
|
+
const body = await getBroker(env, `/last_message?target=${encodeURIComponent(runId)}`, fetchImpl);
|
|
560
|
+
return {
|
|
561
|
+
outcome: "sent",
|
|
562
|
+
label: typeof body?.label === "string" ? body.label : void 0,
|
|
563
|
+
hasRecord: body?.has_record === true,
|
|
564
|
+
finalOutput: typeof body?.final_output === "string" ? body.final_output : "",
|
|
565
|
+
stopReason: typeof body?.stop_reason === "string" ? body.stop_reason : void 0,
|
|
566
|
+
truncated: body?.final_output_truncated === true
|
|
567
|
+
};
|
|
568
|
+
} catch (error) {
|
|
569
|
+
throw brokerToolError(error);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
241
572
|
};
|
|
242
573
|
}
|
|
243
574
|
//#endregion
|
|
@@ -569,7 +900,7 @@ var RunInspectorOverlay = class {
|
|
|
569
900
|
}
|
|
570
901
|
if (!isFollowUpable(this.state.status, this.state.snapshot)) return;
|
|
571
902
|
this.runAction(async () => {
|
|
572
|
-
const next = await this.deps.
|
|
903
|
+
const next = await this.deps.resumeRun(this.state.run, text);
|
|
573
904
|
this.state = {
|
|
574
905
|
...this.state,
|
|
575
906
|
run: {
|
|
@@ -1173,7 +1504,7 @@ function guidance(status, pending, runId) {
|
|
|
1173
1504
|
const options = permissionOptions(pending);
|
|
1174
1505
|
if (options.length > 0) return `Guidance: Call avenor_answer_permission with run_id "${runId}" and option_id "${selected(options[0], "option_id")}".`;
|
|
1175
1506
|
if (pending) return `Guidance: Permission input is required, but no options are available; call avenor_inspect with run_id "${runId}".`;
|
|
1176
|
-
return `Guidance: Call
|
|
1507
|
+
return `Guidance: Call avenor_message with action "resume" or avenor_status with run_id "${runId}".`;
|
|
1177
1508
|
}
|
|
1178
1509
|
if (isTerminal(status)) return `Guidance: Call avenor_result or avenor_events with run_id "${runId}".`;
|
|
1179
1510
|
return `Guidance: Call avenor_result with run_id "${runId}".`;
|
|
@@ -1269,18 +1600,33 @@ function permissionLines(details, args) {
|
|
|
1269
1600
|
`Guidance: Call avenor_status with run_id "${runId}" to verify the permission state.`
|
|
1270
1601
|
];
|
|
1271
1602
|
}
|
|
1272
|
-
function
|
|
1603
|
+
function messageLines(details, args) {
|
|
1273
1604
|
const result = asRecord(details);
|
|
1274
|
-
if (!result
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1605
|
+
if (!result) return void 0;
|
|
1606
|
+
const action = selectedOptional(result, "action") ?? stringValue(args.action) ?? "tell";
|
|
1607
|
+
if (action === "resume") {
|
|
1608
|
+
const runId = selected(result, "run_id", args.target);
|
|
1609
|
+
const label = selected(result, "label", args.label, runId);
|
|
1610
|
+
if (!stringValue(runId) || !stringValue(label)) return void 0;
|
|
1611
|
+
return [
|
|
1612
|
+
`Resume: ${label} — dispatched.`,
|
|
1613
|
+
`Source run: ${scalar(args.target)}`,
|
|
1614
|
+
`Run: ${runId}`,
|
|
1615
|
+
`Label: ${label}`,
|
|
1616
|
+
`Guidance: Call avenor_status with run_id "${runId}".`
|
|
1617
|
+
];
|
|
1618
|
+
}
|
|
1619
|
+
const outcome = selectedOptional(result, "outcome") ?? "done";
|
|
1620
|
+
const lines = [`${action.charAt(0).toUpperCase() + action.slice(1)}: ${outcome}.`];
|
|
1621
|
+
const text = selectedOptional(result, "text");
|
|
1622
|
+
if (text) lines.push(scalar(text));
|
|
1623
|
+
const messageId = selectedOptional(result, "message_id") ?? stringValue(args.reply_to) ?? stringValue(args.message_id);
|
|
1624
|
+
if (messageId) lines.push(`Message: ${scalar(messageId)}`);
|
|
1625
|
+
const reply = selectedOptional(result, "reply");
|
|
1626
|
+
if (action === "ask" && reply) lines.push(`Reply: ${scalar(reply)}`);
|
|
1627
|
+
const target = selectedOptional(result, "label") ?? (action === "reply" ? void 0 : stringValue(args.target));
|
|
1628
|
+
if (target) lines.push(`Target: ${scalar(target)}`);
|
|
1629
|
+
return lines;
|
|
1284
1630
|
}
|
|
1285
1631
|
function eventSequence(value) {
|
|
1286
1632
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
@@ -1502,12 +1848,21 @@ function renderAnswerPermissionCall(args, theme) {
|
|
|
1502
1848
|
if (supervisor) pieces.push(`supervisor_id ${supervisor}`);
|
|
1503
1849
|
return linesText([pieces.join(" ")], theme);
|
|
1504
1850
|
}
|
|
1505
|
-
function
|
|
1506
|
-
const
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
if (
|
|
1510
|
-
if (
|
|
1851
|
+
function renderMessageResumeCall(args, theme) {
|
|
1852
|
+
const action = stringValue(args.action) || "tell";
|
|
1853
|
+
const pieces = [`avenor_message action ${quoted(action)}`];
|
|
1854
|
+
if (action === "reply") pieces.push(`reply_to ${quoted(args.reply_to)}`, `message ${quoted(args.message)}`);
|
|
1855
|
+
else if (action === "withdraw") pieces.push(`message_id ${quoted(args.message_id)}`);
|
|
1856
|
+
else if (action === "control_reply") {
|
|
1857
|
+
pieces.push(`target ${quoted(args.target)}`);
|
|
1858
|
+
if (args.payload !== void 0) pieces.push(`payload ${quoted(JSON.stringify(args.payload))}`);
|
|
1859
|
+
} else {
|
|
1860
|
+
pieces.push(`target ${quoted(args.target)}`, `message ${quoted(args.message)}`);
|
|
1861
|
+
if (action === "resume") {
|
|
1862
|
+
const label = optionalQuoted(args.label);
|
|
1863
|
+
if (label) pieces.push(`label ${label}`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1511
1866
|
return linesText([pieces.join(" ")], theme);
|
|
1512
1867
|
}
|
|
1513
1868
|
function renderEventsCall(args, theme) {
|
|
@@ -1541,8 +1896,8 @@ function renderInspectResult(result, options, theme, args) {
|
|
|
1541
1896
|
function renderAnswerPermissionResult(result, options, theme, args) {
|
|
1542
1897
|
return render("answer_permission", result, options, theme, () => permissionLines(result.details, args));
|
|
1543
1898
|
}
|
|
1544
|
-
function
|
|
1545
|
-
return render("
|
|
1899
|
+
function renderMessageResumeResult(result, options, theme, args) {
|
|
1900
|
+
return render("message", result, options, theme, () => messageLines(result.details, args));
|
|
1546
1901
|
}
|
|
1547
1902
|
function renderEventsResult(result, options, theme, args) {
|
|
1548
1903
|
return render("events", result, options, theme, () => eventsLines(result.details, args, options.expanded));
|
|
@@ -1577,11 +1932,10 @@ const defaultDeps = {
|
|
|
1577
1932
|
statusTool,
|
|
1578
1933
|
eventsTool,
|
|
1579
1934
|
answerPermissionTool,
|
|
1580
|
-
|
|
1935
|
+
messageTool,
|
|
1581
1936
|
inspectTool,
|
|
1582
1937
|
resultTool,
|
|
1583
1938
|
brokerReceiveTool,
|
|
1584
|
-
brokerReplyTool,
|
|
1585
1939
|
shutdownTool,
|
|
1586
1940
|
killTool,
|
|
1587
1941
|
workflowStatusTool,
|
|
@@ -1592,6 +1946,8 @@ const defaultDeps = {
|
|
|
1592
1946
|
workflowGateTool,
|
|
1593
1947
|
workflowControllerStatusTool,
|
|
1594
1948
|
workflowReadyTool,
|
|
1949
|
+
workflowCancelTool,
|
|
1950
|
+
getSupervisorClient,
|
|
1595
1951
|
observeRun,
|
|
1596
1952
|
dial,
|
|
1597
1953
|
attachOwner,
|
|
@@ -1633,7 +1989,7 @@ function buildCompletionText(run, result, finalOutput) {
|
|
|
1633
1989
|
if (output) lines.push("", "Final output:", ...quotedLines(output));
|
|
1634
1990
|
if (result.stop_reason && result.stop_reason.toUpperCase() !== result.status.toUpperCase()) lines.push(`Stop reason: ${result.stop_reason}`);
|
|
1635
1991
|
if (result.session_id) lines.push(`Session: \`${result.session_id}\``);
|
|
1636
|
-
lines.push("", `Call \`avenor_inspect\` with \`run_id: "${run.runId}"\` for the bounded transcript/snapshot, or \`
|
|
1992
|
+
lines.push("", `Call \`avenor_inspect\` with \`run_id: "${run.runId}"\` for the bounded transcript/snapshot, or \`avenor_message\` with \`action: "resume"\` to iterate.`);
|
|
1637
1993
|
return lines.join("\n");
|
|
1638
1994
|
}
|
|
1639
1995
|
function buildWaitingText(run, status) {
|
|
@@ -1642,7 +1998,7 @@ function buildWaitingText(run, status) {
|
|
|
1642
1998
|
const lines = [`Sub-agent "${run.label}" (${agent}) is waiting for input.`];
|
|
1643
1999
|
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"`);
|
|
1644
2000
|
else if (status.phase_label) lines.push(`Question: ${status.phase_label}`);
|
|
1645
|
-
if (!perm) lines.push(`Use \`
|
|
2001
|
+
if (!perm) lines.push(`Use \`avenor_message\` with \`action: "resume"\` and target "${run.runId}" to respond, or \`avenor_inspect\` to review context first.`);
|
|
1646
2002
|
return lines.join("\n");
|
|
1647
2003
|
}
|
|
1648
2004
|
function isTerminalStatus(status) {
|
|
@@ -1746,6 +2102,16 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1746
2102
|
const pollingErrors = [];
|
|
1747
2103
|
let lastStatusEntries = [];
|
|
1748
2104
|
const pendingWaits = /* @__PURE__ */ new Set();
|
|
2105
|
+
/** The agent-facing prose clause for a stopping point. A synthetic status
|
|
2106
|
+
* such as `asking` has no run-status spelling, so it must not appear raw in
|
|
2107
|
+
* this prose. Structured payloads still carry the raw status (the
|
|
2108
|
+
* avenor_result JSON reports it as `interrupted_by.status`) so consumers can
|
|
2109
|
+
* check it. */
|
|
2110
|
+
function describeStoppingPoint(stop) {
|
|
2111
|
+
if (stop.status === "waiting") return "reached a permission request";
|
|
2112
|
+
if (stop.status === "asking") return "asked a question for you";
|
|
2113
|
+
return `reached ${stop.status}`;
|
|
2114
|
+
}
|
|
1749
2115
|
function interruptPendingWaits(stop, excludeRunKey) {
|
|
1750
2116
|
for (const wait of [...pendingWaits]) {
|
|
1751
2117
|
if (wait.runKey === excludeRunKey) continue;
|
|
@@ -1761,6 +2127,29 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1761
2127
|
pendingWaits.add(wait);
|
|
1762
2128
|
return wait;
|
|
1763
2129
|
}
|
|
2130
|
+
/** An inbound ask is a stopping point: the asker cannot finish until it is
|
|
2131
|
+
* answered, so every blocking wait returns, the asker's own wait included. */
|
|
2132
|
+
function interruptWaitsForAsk(ask) {
|
|
2133
|
+
interruptPendingWaits({
|
|
2134
|
+
runId: ask.from_run_id,
|
|
2135
|
+
status: "asking"
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
/** Surface one inbound ask: hand the body to the session, remember it so a
|
|
2139
|
+
* reply can reach the asker, then let every blocking wait return. Delivery
|
|
2140
|
+
* comes first so a throw leaves the waits waiting on an ask the agent never
|
|
2141
|
+
* saw, rather than reporting an ask it cannot answer. */
|
|
2142
|
+
function surfaceInboundAsk(pending, ask, heading) {
|
|
2143
|
+
pi.sendUserMessage(formatInboundAskBody(ask, {
|
|
2144
|
+
heading,
|
|
2145
|
+
role: ask.role
|
|
2146
|
+
}), { deliverAs: "steer" });
|
|
2147
|
+
pending.set(ask.message_id, {
|
|
2148
|
+
from_run_id: ask.from_run_id,
|
|
2149
|
+
message_id: ask.message_id
|
|
2150
|
+
});
|
|
2151
|
+
interruptWaitsForAsk(ask);
|
|
2152
|
+
}
|
|
1764
2153
|
function createInterruptibleWait(runKey, signal) {
|
|
1765
2154
|
const controller = new AbortController();
|
|
1766
2155
|
let stopped;
|
|
@@ -1854,11 +2243,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1854
2243
|
for (const msg of msgs) {
|
|
1855
2244
|
const ask = parseControlMessage(msg);
|
|
1856
2245
|
if (!ask) continue;
|
|
1857
|
-
subAgentPendingAsks
|
|
1858
|
-
from_run_id: ask.from_run_id,
|
|
1859
|
-
message_id: ask.message_id
|
|
1860
|
-
});
|
|
1861
|
-
pi.sendUserMessage(formatInboundAskBody(ask), { deliverAs: "steer" });
|
|
2246
|
+
surfaceInboundAsk(subAgentPendingAsks, ask, "Inbound ask");
|
|
1862
2247
|
}
|
|
1863
2248
|
} catch (error) {
|
|
1864
2249
|
consecutiveFailures++;
|
|
@@ -1873,6 +2258,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1873
2258
|
}
|
|
1874
2259
|
let hostPollTimer = null;
|
|
1875
2260
|
let hostPollGeneration = 0;
|
|
2261
|
+
let hostReceiveGeneration;
|
|
1876
2262
|
const hostPendingAsks = /* @__PURE__ */ new Map();
|
|
1877
2263
|
function stopHostAskPoll() {
|
|
1878
2264
|
hostPollGeneration++;
|
|
@@ -1886,20 +2272,27 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1886
2272
|
const generation = hostPollGeneration;
|
|
1887
2273
|
const intervalMs = options.pollIntervalMs ?? 2e3;
|
|
1888
2274
|
const tick = async () => {
|
|
2275
|
+
const startedAt = Date.now();
|
|
1889
2276
|
try {
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2277
|
+
const receive = () => hostReceiveGeneration ? deps.brokerReceiveTool({
|
|
2278
|
+
generation: hostReceiveGeneration,
|
|
2279
|
+
timeout_ms: intervalMs
|
|
2280
|
+
}) : deps.brokerReceiveTool({ timeout_ms: intervalMs });
|
|
2281
|
+
let received;
|
|
2282
|
+
try {
|
|
2283
|
+
received = await receive();
|
|
2284
|
+
} catch (error) {
|
|
2285
|
+
if (!hostReceiveGeneration || !isGenerationMismatchError(error)) throw error;
|
|
2286
|
+
hostReceiveGeneration = void 0;
|
|
2287
|
+
disposeHostProviders();
|
|
2288
|
+
received = await receive();
|
|
1899
2289
|
}
|
|
2290
|
+
if (generation !== hostPollGeneration) return;
|
|
2291
|
+
if (received.generation) hostReceiveGeneration = received.generation;
|
|
2292
|
+
for (const ask of received.asks) surfaceInboundAsk(hostPendingAsks, ask, "Ask from sub-agent");
|
|
1900
2293
|
} catch {}
|
|
1901
2294
|
if (generation !== hostPollGeneration) return;
|
|
1902
|
-
hostPollTimer = setTimeout(() => void tick(), intervalMs);
|
|
2295
|
+
hostPollTimer = setTimeout(() => void tick(), Math.max(0, intervalMs - (Date.now() - startedAt)));
|
|
1903
2296
|
hostPollTimer.unref?.();
|
|
1904
2297
|
};
|
|
1905
2298
|
hostPollTimer = setTimeout(() => void tick(), intervalMs);
|
|
@@ -2227,44 +2620,22 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2227
2620
|
await client.interruptAndPrompt(runtimeId, text);
|
|
2228
2621
|
});
|
|
2229
2622
|
},
|
|
2230
|
-
|
|
2231
|
-
return
|
|
2232
|
-
runId: run.runId,
|
|
2233
|
-
message: text,
|
|
2234
|
-
supervisorId: run.supervisorId
|
|
2235
|
-
});
|
|
2623
|
+
resumeRun(run, text) {
|
|
2624
|
+
return resumeTrackedRun(run, text);
|
|
2236
2625
|
}
|
|
2237
2626
|
};
|
|
2238
2627
|
}
|
|
2239
|
-
async function
|
|
2240
|
-
const
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
upsertTrackedRun({
|
|
2251
|
-
runId: result.run_id,
|
|
2252
|
-
label: result.label,
|
|
2253
|
-
agent: status?.agent ?? run.agent,
|
|
2254
|
-
runtimeId: status?.runtime_id,
|
|
2255
|
-
supervisorId: run.supervisorId
|
|
2256
|
-
}, status, false);
|
|
2257
|
-
startPolling();
|
|
2258
|
-
} catch {
|
|
2259
|
-
upsertTrackedRun({
|
|
2260
|
-
runId: result.run_id,
|
|
2261
|
-
label: result.label,
|
|
2262
|
-
agent: run.agent,
|
|
2263
|
-
supervisorId: run.supervisorId
|
|
2264
|
-
}, void 0, false);
|
|
2265
|
-
startPolling();
|
|
2266
|
-
}
|
|
2267
|
-
return result;
|
|
2628
|
+
async function resumeTrackedRun(run, text) {
|
|
2629
|
+
const provider = await hostMessageProvider(run.supervisorId);
|
|
2630
|
+
const result = await deps.messageTool({
|
|
2631
|
+
action: "resume",
|
|
2632
|
+
target: run.runId,
|
|
2633
|
+
message: text
|
|
2634
|
+
}, provider);
|
|
2635
|
+
return {
|
|
2636
|
+
run_id: result.runId ?? run.runId,
|
|
2637
|
+
label: result.label ?? run.runId
|
|
2638
|
+
};
|
|
2268
2639
|
}
|
|
2269
2640
|
async function waitForRunWithObserver(run, signal, onUpdate) {
|
|
2270
2641
|
let observer = null;
|
|
@@ -2372,6 +2743,102 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2372
2743
|
if (observed.status || observed.aborted) return observed;
|
|
2373
2744
|
return waitForRunByPolling(run, signal, onUpdate);
|
|
2374
2745
|
}
|
|
2746
|
+
const hostProviders = /* @__PURE__ */ new Map();
|
|
2747
|
+
const hostAskClaims = /* @__PURE__ */ new Map();
|
|
2748
|
+
function disposeHostProvider(supervisorId) {
|
|
2749
|
+
const key = supervisorId ?? "";
|
|
2750
|
+
const binding = hostProviders.get(key);
|
|
2751
|
+
hostProviders.delete(key);
|
|
2752
|
+
hostAskClaims.delete(key);
|
|
2753
|
+
if (!binding || binding.isSingleton || !binding.client) return;
|
|
2754
|
+
try {
|
|
2755
|
+
binding.client.close();
|
|
2756
|
+
} catch {}
|
|
2757
|
+
}
|
|
2758
|
+
function disposeHostProviders() {
|
|
2759
|
+
for (const key of [...hostProviders.keys()]) disposeHostProvider(key);
|
|
2760
|
+
hostAskClaims.clear();
|
|
2761
|
+
}
|
|
2762
|
+
async function defaultHostProviderFor(supervisorId) {
|
|
2763
|
+
const key = supervisorId ?? "";
|
|
2764
|
+
const existing = hostProviders.get(key);
|
|
2765
|
+
if (existing) return existing.provider;
|
|
2766
|
+
const { client, isSingleton } = await (deps.getSupervisorClient ?? getSupervisorClient)(supervisorId, { owner: true });
|
|
2767
|
+
const preflight = await client.messagePreflight();
|
|
2768
|
+
const provider = createHostMessageProvider({
|
|
2769
|
+
surface: "pi-host",
|
|
2770
|
+
supervisorId: supervisorId ?? Supervisor.currentInstance()?.supervisorId ?? "",
|
|
2771
|
+
generation: preflight.generation,
|
|
2772
|
+
client,
|
|
2773
|
+
onResumed: (result) => trackResumed(supervisorId, result)
|
|
2774
|
+
});
|
|
2775
|
+
hostProviders.set(key, {
|
|
2776
|
+
provider,
|
|
2777
|
+
client,
|
|
2778
|
+
isSingleton
|
|
2779
|
+
});
|
|
2780
|
+
return provider;
|
|
2781
|
+
}
|
|
2782
|
+
async function trackResumed(supervisorId, result) {
|
|
2783
|
+
const run = {
|
|
2784
|
+
runId: result.runId ?? "",
|
|
2785
|
+
label: result.label ?? result.runId ?? "",
|
|
2786
|
+
supervisorId
|
|
2787
|
+
};
|
|
2788
|
+
try {
|
|
2789
|
+
const status = asStatusResult(await deps.statusTool({
|
|
2790
|
+
runId: run.runId,
|
|
2791
|
+
supervisorId
|
|
2792
|
+
}));
|
|
2793
|
+
upsertTrackedRun({
|
|
2794
|
+
runId: run.runId,
|
|
2795
|
+
label: run.label,
|
|
2796
|
+
agent: status?.agent,
|
|
2797
|
+
runtimeId: status?.runtime_id,
|
|
2798
|
+
supervisorId
|
|
2799
|
+
}, status, false);
|
|
2800
|
+
} catch {
|
|
2801
|
+
upsertTrackedRun(run, void 0, false);
|
|
2802
|
+
}
|
|
2803
|
+
startPolling();
|
|
2804
|
+
}
|
|
2805
|
+
const hostProviderFor = deps.hostProviderFor ?? defaultHostProviderFor;
|
|
2806
|
+
const childProvider = isSubAgentBrokerMode && subAgentEnv ? createChildMessageProvider({
|
|
2807
|
+
surface: "pi-child",
|
|
2808
|
+
runId: subAgentEnv.runId,
|
|
2809
|
+
broker: createBrokerChildBroker(subAgentEnv),
|
|
2810
|
+
pendingAsks: subAgentPendingAsks,
|
|
2811
|
+
pollGeneration: () => subAgentPollGeneration
|
|
2812
|
+
}) : null;
|
|
2813
|
+
async function hostMessageProvider(supervisorId) {
|
|
2814
|
+
const provider = await hostProviderFor(supervisorId);
|
|
2815
|
+
const key = supervisorId ?? "";
|
|
2816
|
+
let claims = hostAskClaims.get(key);
|
|
2817
|
+
if (!claims) {
|
|
2818
|
+
claims = /* @__PURE__ */ new Map();
|
|
2819
|
+
hostAskClaims.set(key, claims);
|
|
2820
|
+
}
|
|
2821
|
+
installAskClaims(provider, claims, hostPendingAsks, () => hostPollGeneration);
|
|
2822
|
+
if (!provider.onResumed) provider.onResumed = (result) => trackResumed(supervisorId, result);
|
|
2823
|
+
return provider;
|
|
2824
|
+
}
|
|
2825
|
+
async function messageProvider(supervisorId) {
|
|
2826
|
+
if (childProvider) return childProvider;
|
|
2827
|
+
return hostMessageProvider(supervisorId);
|
|
2828
|
+
}
|
|
2829
|
+
pi.on("turn_end", async (event) => {
|
|
2830
|
+
if (isSubAgentBrokerMode) return;
|
|
2831
|
+
if (!deps.Supervisor.currentInstance?.()) return;
|
|
2832
|
+
const turn = extractTurnText([event.message]);
|
|
2833
|
+
try {
|
|
2834
|
+
const provider = await hostProviderFor(void 0);
|
|
2835
|
+
if (!provider.recordLastTurn) return;
|
|
2836
|
+
const bounded = boundFinalOutput(turn.text);
|
|
2837
|
+
await provider.recordLastTurn(bounded, normalizeTurnStopReason(turn.stopReason), turn.text.length > bounded.length);
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
console.error("[avenor] session_last_set failed:", error instanceof Error ? error.message : error);
|
|
2840
|
+
}
|
|
2841
|
+
});
|
|
2375
2842
|
pi.on("session_start", async (_event, ctx) => {
|
|
2376
2843
|
sessionCtx = ctx;
|
|
2377
2844
|
lastStatusEntries = [];
|
|
@@ -2383,6 +2850,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2383
2850
|
pi.on("session_shutdown", async () => {
|
|
2384
2851
|
stopHostAskPoll();
|
|
2385
2852
|
stopSubAgentPoll();
|
|
2853
|
+
disposeHostProviders();
|
|
2386
2854
|
hostPendingAsks.clear();
|
|
2387
2855
|
subAgentPendingAsks.clear();
|
|
2388
2856
|
await stopPolling();
|
|
@@ -2422,15 +2890,15 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2422
2890
|
pi.registerTool({
|
|
2423
2891
|
name: "avenor_spawn",
|
|
2424
2892
|
label: "Avenor Spawn",
|
|
2425
|
-
description: `Dispatch an agent run via avenor. Optional thinking accepts ${[...THINKING_LEVELS].slice(0, -1).join(", ")}, or ${THINKING_LEVELS[THINKING_LEVELS.length - 1]}; unsupported backends reject explicit values. Blocks by default, showing live progress. Set wait=false for fire-and-forget. With several blocking spawns in parallel, the first stopping point (permission or completion) interrupts the other waits.`,
|
|
2893
|
+
description: `Dispatch an agent run via avenor. Optional thinking accepts ${[...THINKING_LEVELS].slice(0, -1).join(", ")}, or ${THINKING_LEVELS[THINKING_LEVELS.length - 1]}; unsupported backends reject explicit values. Blocks by default, showing live progress. Set wait=false for fire-and-forget. With several blocking spawns in parallel, the first stopping point (a permission request, an inbound ask, or completion) interrupts the other waits.`,
|
|
2426
2894
|
promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, result, inspect, events, follow-up, shutdown)",
|
|
2427
2895
|
promptGuidelines: [
|
|
2428
2896
|
"Use avenor_spawn to delegate well-defined tasks to sub-agents.",
|
|
2429
2897
|
"Use avenor_status (view \"lifecycle\") only to check progress or pending permissions.",
|
|
2430
|
-
"Use avenor_result to retrieve the final result of a sub-agent; it waits by default, but set wait=false for runs that already completed. When awaiting multiple runs, the first stopping point (permission or completion) interrupts the other waits.",
|
|
2898
|
+
"Use avenor_result to retrieve the final result of a sub-agent; it waits by default, but set wait=false for runs that already completed. When awaiting multiple runs, the first stopping point (a permission request, an inbound ask, or completion) interrupts the other waits.",
|
|
2431
2899
|
"Use avenor_inspect to review a bounded transcript and tool activity from a run.",
|
|
2432
2900
|
"Use avenor_events only when you specifically need raw event payloads.",
|
|
2433
|
-
"Use
|
|
2901
|
+
"Use avenor_message with action \"resume\" to iterate on a completed run.",
|
|
2434
2902
|
"Use avenor_shutdown when done delegating work.",
|
|
2435
2903
|
"When roster_entry is set, the roster selects agent, model, and backend; do not pass those fields alongside it.",
|
|
2436
2904
|
"If a roster spawn fails validation or resolution, do not retry the roster: respawn without roster_file/roster_entry. Direct selectors (agent, model, backend) are optional; omission uses runtime defaults."
|
|
@@ -2551,7 +3019,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2551
3019
|
return {
|
|
2552
3020
|
content: [{
|
|
2553
3021
|
type: "text",
|
|
2554
|
-
text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted: "${stop.label ?? stop.runId}"
|
|
3022
|
+
text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted: "${stop.label ?? stop.runId}" ${describeStoppingPoint(stop)}. Handle that first; this run's final result is delivered automatically.`
|
|
2555
3023
|
}],
|
|
2556
3024
|
details: {
|
|
2557
3025
|
run_id: result.run_id,
|
|
@@ -2717,7 +3185,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2717
3185
|
pi.registerTool({
|
|
2718
3186
|
name: "avenor_result",
|
|
2719
3187
|
label: "Avenor Result",
|
|
2720
|
-
description: `Retrieve the complete final output of a run without transcript or event details. Waits by default; set wait=false to retrieve an already-completed run without blocking. When several runs are pending, the first stopping point (a permission request or completion on any run) interrupts other blocking waits so they can be handled; re-issue the interrupted wait afterwards.`,
|
|
3188
|
+
description: `Retrieve the complete final output of a run without transcript or event details. Waits by default; set wait=false to retrieve an already-completed run without blocking. When several runs are pending, the first stopping point (a permission request, an inbound ask, or completion on any run) interrupts other blocking waits so they can be handled; re-issue the interrupted wait afterwards.`,
|
|
2721
3189
|
parameters: Type.Object({
|
|
2722
3190
|
run_id: Type.String({ description: "Run ID or label" }),
|
|
2723
3191
|
wait: Type.Optional(Type.Boolean({ description: "Wait for a terminal result (default true); false retrieves the current state without blocking" })),
|
|
@@ -2852,205 +3320,94 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2852
3320
|
}
|
|
2853
3321
|
});
|
|
2854
3322
|
pi.registerTool({
|
|
2855
|
-
name: "
|
|
2856
|
-
label: "Avenor
|
|
2857
|
-
description: "
|
|
3323
|
+
name: "avenor_message",
|
|
3324
|
+
label: "Avenor Message",
|
|
3325
|
+
description: "Send a message to another agent run, answer an inbound ask, withdraw a pending ask, or resume a completed run. Every action is explicit and the result states whether a failed ask can still be withdrawn. `control_reply` is available only on the claude-channel surface.",
|
|
2858
3326
|
parameters: Type.Object({
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
3327
|
+
action: Type.Union([
|
|
3328
|
+
Type.Literal("tell"),
|
|
3329
|
+
Type.Literal("ask"),
|
|
3330
|
+
Type.Literal("reply"),
|
|
3331
|
+
Type.Literal("resume"),
|
|
3332
|
+
Type.Literal("withdraw"),
|
|
3333
|
+
Type.Literal("control_reply")
|
|
3334
|
+
], { description: "tell | ask | reply | resume | withdraw | control_reply" }),
|
|
3335
|
+
target: Type.Optional(Type.String({ description: "Recipient for tell/ask/resume (a run id, a peer label, or \"self\"), or the opaque control message id for control_reply" })),
|
|
3336
|
+
message: Type.Optional(Type.String({ description: "Message text for tell/ask/reply/resume" })),
|
|
3337
|
+
reply_to: Type.Optional(Type.String({ description: "Inbound ask id being answered (reply)" })),
|
|
3338
|
+
message_id: Type.Optional(Type.String({ description: "Ask id being withdrawn (withdraw)" })),
|
|
3339
|
+
timeout_ms: Type.Optional(Type.Integer({ description: "Bound on the ask wait in milliseconds" })),
|
|
3340
|
+
label: Type.Optional(Type.String({ description: "Override label for the resumed run (resume)" })),
|
|
3341
|
+
idempotency_key: Type.Optional(Type.String({ description: "Forwarded verbatim to the resume authority (resume)" })),
|
|
3342
|
+
payload: Type.Optional(Type.Unknown({ description: "Control reply body (control_reply)" }))
|
|
2863
3343
|
}),
|
|
2864
|
-
async execute(_toolCallId, params) {
|
|
2865
|
-
const
|
|
2866
|
-
|
|
2867
|
-
message: params.message,
|
|
2868
|
-
label: params.label,
|
|
2869
|
-
supervisorId: params.supervisor_id
|
|
2870
|
-
});
|
|
2871
|
-
try {
|
|
2872
|
-
const status = asStatusResult(await deps.statusTool({
|
|
2873
|
-
runId: result.run_id,
|
|
2874
|
-
supervisorId: params.supervisor_id
|
|
2875
|
-
}));
|
|
2876
|
-
upsertTrackedRun({
|
|
2877
|
-
runId: result.run_id,
|
|
2878
|
-
label: result.label,
|
|
2879
|
-
agent: status?.agent,
|
|
2880
|
-
runtimeId: status?.runtime_id,
|
|
2881
|
-
supervisorId: params.supervisor_id
|
|
2882
|
-
}, status, false);
|
|
2883
|
-
} catch {
|
|
2884
|
-
upsertTrackedRun({
|
|
2885
|
-
runId: result.run_id,
|
|
2886
|
-
label: result.label,
|
|
2887
|
-
supervisorId: params.supervisor_id
|
|
2888
|
-
}, void 0, false);
|
|
2889
|
-
}
|
|
2890
|
-
startPolling();
|
|
3344
|
+
async execute(_toolCallId, params, signal) {
|
|
3345
|
+
const provider = await messageProvider(void 0);
|
|
3346
|
+
const result = await deps.messageTool(params, provider, { signal });
|
|
2891
3347
|
return {
|
|
2892
3348
|
content: [{
|
|
2893
3349
|
type: "text",
|
|
2894
|
-
text:
|
|
3350
|
+
text: result.text ?? ""
|
|
2895
3351
|
}],
|
|
2896
|
-
details: result
|
|
3352
|
+
details: resultOutput(result)
|
|
2897
3353
|
};
|
|
2898
3354
|
},
|
|
2899
|
-
renderCall(args, theme
|
|
2900
|
-
return
|
|
3355
|
+
renderCall(args, theme) {
|
|
3356
|
+
return renderMessageResumeCall(args, theme);
|
|
2901
3357
|
},
|
|
2902
3358
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
2903
|
-
return
|
|
3359
|
+
return renderMessageResumeResult(result, {
|
|
2904
3360
|
expanded,
|
|
2905
3361
|
isPartial
|
|
2906
3362
|
}, theme, context.args);
|
|
2907
3363
|
}
|
|
2908
3364
|
});
|
|
2909
3365
|
pi.registerTool({
|
|
2910
|
-
name: "
|
|
2911
|
-
label: "Avenor
|
|
2912
|
-
description: "
|
|
3366
|
+
name: "avenor_peers",
|
|
3367
|
+
label: "Avenor Peers",
|
|
3368
|
+
description: "List the visible agent sessions, rename a run, or read a run's last agent message. `last` returns the target's final text with its stop reason, or an empty result when the target has no completed turn yet.",
|
|
2913
3369
|
parameters: Type.Object({
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3370
|
+
action: Type.Optional(Type.Union([
|
|
3371
|
+
Type.Literal("list"),
|
|
3372
|
+
Type.Literal("rename"),
|
|
3373
|
+
Type.Literal("last")
|
|
3374
|
+
], { description: "list (the default) | rename | last" })),
|
|
3375
|
+
name: Type.Optional(Type.String({ description: "New display name (rename)" })),
|
|
3376
|
+
session_id: Type.Optional(Type.String({ description: "Run to rename; omitted renames the caller (rename)" })),
|
|
3377
|
+
target: Type.Optional(Type.String({ description: "Peer to read: a run id, a label, or \"self\" (last)" }))
|
|
2918
3378
|
}),
|
|
2919
|
-
async execute(_toolCallId, params
|
|
2920
|
-
const result =
|
|
2921
|
-
toRunId: params.to_run_id,
|
|
2922
|
-
message: params.message,
|
|
2923
|
-
timeoutMs: params.timeout_ms,
|
|
2924
|
-
signal
|
|
2925
|
-
}) : await askTool({
|
|
2926
|
-
toRunId: params.to_run_id,
|
|
2927
|
-
message: params.message,
|
|
2928
|
-
timeoutMs: params.timeout_ms,
|
|
2929
|
-
supervisorId: params.supervisor_id
|
|
2930
|
-
});
|
|
3379
|
+
async execute(_toolCallId, params) {
|
|
3380
|
+
const result = await peersTool(params, await messageProvider(void 0));
|
|
2931
3381
|
return {
|
|
2932
3382
|
content: [{
|
|
2933
3383
|
type: "text",
|
|
2934
|
-
text:
|
|
3384
|
+
text: result.action === "list" ? [result.text, renderPeerList(result.peers)].filter((part) => part !== "").join("\n\n") : result.text
|
|
2935
3385
|
}],
|
|
2936
|
-
details: result
|
|
3386
|
+
details: peersDetails(result)
|
|
2937
3387
|
};
|
|
2938
3388
|
}
|
|
2939
3389
|
});
|
|
2940
3390
|
pi.registerTool({
|
|
2941
|
-
name: "
|
|
2942
|
-
label: "Avenor
|
|
2943
|
-
description: "
|
|
3391
|
+
name: "avenor_workflow_cancel",
|
|
3392
|
+
label: "Avenor Workflow Cancel",
|
|
3393
|
+
description: "Cancel a workflow instance. The actor and reason are required.",
|
|
2944
3394
|
parameters: Type.Object({
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
supervisor_id: Type.Optional(Type.String({ description: "
|
|
3395
|
+
workflow_id: Type.String({ description: "Workflow ID" }),
|
|
3396
|
+
actor: Type.String({ description: "Who is cancelling" }),
|
|
3397
|
+
reason: Type.String({ description: "Why the workflow is being cancelled" }),
|
|
3398
|
+
supervisor_id: Type.Optional(Type.String({ description: "Supervisor ID for multi-supervisor mode" }))
|
|
2949
3399
|
}),
|
|
2950
3400
|
async execute(_toolCallId, params) {
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
}],
|
|
2958
|
-
details: { error: true }
|
|
2959
|
-
};
|
|
2960
|
-
const generation = subAgentPollGeneration;
|
|
2961
|
-
subAgentPendingAsks.delete(target.message_id);
|
|
2962
|
-
try {
|
|
2963
|
-
await postBroker(subAgentEnv, "/send", buildAgentMessage({
|
|
2964
|
-
fromRunId: subAgentEnv.runId,
|
|
2965
|
-
toRunId: target.from_run_id,
|
|
2966
|
-
messageId: randomUUID(),
|
|
2967
|
-
message: params.message,
|
|
2968
|
-
role: "agent",
|
|
2969
|
-
replyTo: target.message_id
|
|
2970
|
-
}));
|
|
2971
|
-
} catch (error) {
|
|
2972
|
-
if (generation === subAgentPollGeneration) subAgentPendingAsks.set(target.message_id, target);
|
|
2973
|
-
throw error;
|
|
2974
|
-
}
|
|
2975
|
-
return { content: [{
|
|
2976
|
-
type: "text",
|
|
2977
|
-
text: "Reply sent"
|
|
2978
|
-
}] };
|
|
2979
|
-
}
|
|
2980
|
-
const target = resolveReplyTarget(hostPendingAsks, params.from_run_id, params.reply_to_message_id);
|
|
2981
|
-
if (!target) return {
|
|
2982
|
-
content: [{
|
|
2983
|
-
type: "text",
|
|
2984
|
-
text: "No pending ask found to reply to. Pass from_run_id and/or reply_to_message_id."
|
|
2985
|
-
}],
|
|
2986
|
-
details: { error: true }
|
|
2987
|
-
};
|
|
2988
|
-
const generation = hostPollGeneration;
|
|
2989
|
-
hostPendingAsks.delete(target.message_id);
|
|
2990
|
-
try {
|
|
2991
|
-
await deps.brokerReplyTool({
|
|
2992
|
-
toRunId: target.from_run_id,
|
|
2993
|
-
replyTo: target.message_id,
|
|
2994
|
-
message: params.message,
|
|
2995
|
-
supervisorId: params.supervisor_id
|
|
2996
|
-
});
|
|
2997
|
-
} catch (error) {
|
|
2998
|
-
if (generation === hostPollGeneration) hostPendingAsks.set(target.message_id, target);
|
|
2999
|
-
throw error;
|
|
3000
|
-
}
|
|
3001
|
-
return { content: [{
|
|
3002
|
-
type: "text",
|
|
3003
|
-
text: "Reply sent"
|
|
3004
|
-
}] };
|
|
3005
|
-
}
|
|
3006
|
-
});
|
|
3007
|
-
pi.registerTool({
|
|
3008
|
-
name: "avenor_peers",
|
|
3009
|
-
label: "Avenor Peers",
|
|
3010
|
-
description: "List all connected agent sessions with their status and metadata.",
|
|
3011
|
-
parameters: Type.Object({}),
|
|
3012
|
-
async execute() {
|
|
3013
|
-
const result = await peersTool({});
|
|
3014
|
-
if (result.peers.length === 0) return { content: [{
|
|
3015
|
-
type: "text",
|
|
3016
|
-
text: "No other agent sessions connected."
|
|
3017
|
-
}] };
|
|
3018
|
-
return { content: [{
|
|
3019
|
-
type: "text",
|
|
3020
|
-
text: `**Active sessions:**\n${result.peers.map((p) => {
|
|
3021
|
-
const parts = [p.label ? `${p.label} (${p.run_id})` : p.run_id];
|
|
3022
|
-
const tags = [
|
|
3023
|
-
p.status,
|
|
3024
|
-
p.backend,
|
|
3025
|
-
p.model
|
|
3026
|
-
].filter(Boolean);
|
|
3027
|
-
if (tags.length) parts.push(`[${tags.join(", ")}]`);
|
|
3028
|
-
return ` ${parts.join(" ")}`;
|
|
3029
|
-
}).join("\n")}`
|
|
3030
|
-
}] };
|
|
3031
|
-
}
|
|
3032
|
-
});
|
|
3033
|
-
pi.registerTool({
|
|
3034
|
-
name: "avenor_cancel",
|
|
3035
|
-
label: "Avenor Cancel",
|
|
3036
|
-
description: "Cancel a pending ask by its message ID.",
|
|
3037
|
-
parameters: Type.Object({ message_id: Type.String({ description: "Message ID of the pending ask to cancel" }) }),
|
|
3038
|
-
async execute(_toolCallId, params) {
|
|
3039
|
-
if (isSubAgentBrokerMode && subAgentEnv) {
|
|
3040
|
-
await withdrawPendingAsk(subAgentEnv, params.message_id);
|
|
3041
|
-
return {
|
|
3042
|
-
content: [{
|
|
3043
|
-
type: "text",
|
|
3044
|
-
text: `Cancelled ${params.message_id}`
|
|
3045
|
-
}],
|
|
3046
|
-
details: { cancelled: true }
|
|
3047
|
-
};
|
|
3048
|
-
}
|
|
3049
|
-
const result = await cancelTool({ messageId: params.message_id });
|
|
3401
|
+
const result = await deps.workflowCancelTool({
|
|
3402
|
+
workflow_id: params.workflow_id,
|
|
3403
|
+
actor: params.actor,
|
|
3404
|
+
reason: params.reason,
|
|
3405
|
+
supervisor_id: params.supervisor_id
|
|
3406
|
+
});
|
|
3050
3407
|
return {
|
|
3051
3408
|
content: [{
|
|
3052
3409
|
type: "text",
|
|
3053
|
-
text:
|
|
3410
|
+
text: JSON.stringify(result, null, 2)
|
|
3054
3411
|
}],
|
|
3055
3412
|
details: result
|
|
3056
3413
|
};
|
|
@@ -3118,6 +3475,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3118
3475
|
supervisorId: params.supervisor_id,
|
|
3119
3476
|
force: params.force
|
|
3120
3477
|
});
|
|
3478
|
+
disposeHostProvider(params.supervisor_id);
|
|
3479
|
+
if (targetIsSingleton && target !== void 0) disposeHostProvider(void 0);
|
|
3121
3480
|
return {
|
|
3122
3481
|
content: [{
|
|
3123
3482
|
type: "text",
|
|
@@ -3410,7 +3769,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3410
3769
|
initialRunId: args?.trim() || void 0,
|
|
3411
3770
|
deps: {
|
|
3412
3771
|
...createWatchDeps(),
|
|
3413
|
-
|
|
3772
|
+
resumeRun: (run, text) => resumeTrackedRun(run, text)
|
|
3414
3773
|
},
|
|
3415
3774
|
onTrackRun: (run, status) => {
|
|
3416
3775
|
upsertTrackedRun(run, status, false);
|