@dougbots/avenor-pi 0.14.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 +8 -3
- package/dist/index.js +835 -272
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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, brokerReceiveTool, brokerReplyTool, cancelTool, dial, eventsTool, extractEventText, followUpTool, inspectTool, killTool, 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
|
+
/** Deadline for a cleanup call whose caller's own deadline has already expired. */
|
|
9
|
+
const ASK_CLEANUP_TIMEOUT_MS = 5e3;
|
|
8
10
|
function readSubAgentEnv(env = process.env) {
|
|
9
11
|
const brokerUrl = env.AVENOR_BROKER_URL ?? "";
|
|
10
12
|
const runId = env.AVENOR_RUN_ID ?? "";
|
|
@@ -34,43 +36,540 @@ function parseControlMessage(msg) {
|
|
|
34
36
|
return {
|
|
35
37
|
from_run_id: fromRunId,
|
|
36
38
|
message_id: messageId,
|
|
37
|
-
message: p.message
|
|
39
|
+
message: p.message,
|
|
40
|
+
role: typeof p.role === "string" ? p.role : void 0
|
|
38
41
|
};
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
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: "..." }).`;
|
|
52
|
+
}
|
|
53
|
+
/** A broker HTTP response error, so callers can classify by status instead of
|
|
54
|
+
* matching digits in the request text. */
|
|
55
|
+
var BrokerHttpError = class extends Error {
|
|
56
|
+
status;
|
|
57
|
+
code;
|
|
58
|
+
body;
|
|
59
|
+
constructor(path, status, body, code = "") {
|
|
60
|
+
super(`broker ${path}: ${status} ${body}`);
|
|
61
|
+
this.name = "BrokerHttpError";
|
|
62
|
+
this.status = status;
|
|
63
|
+
this.code = code;
|
|
64
|
+
this.body = body;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/** Build the `/send` body for one agent_message, as the broker child tools do. */
|
|
68
|
+
function buildAgentMessage(fields) {
|
|
69
|
+
return {
|
|
70
|
+
...fields.generation !== void 0 ? { generation: fields.generation } : {},
|
|
71
|
+
from_run_id: fields.fromRunId,
|
|
72
|
+
to_run_id: fields.toRunId,
|
|
73
|
+
type: "agent_message",
|
|
74
|
+
payload: {
|
|
75
|
+
id: fields.messageId,
|
|
76
|
+
from: fields.fromRunId,
|
|
77
|
+
from_run_id: fields.fromRunId,
|
|
78
|
+
to_run_id: fields.toRunId,
|
|
79
|
+
message: fields.message,
|
|
80
|
+
role: fields.role,
|
|
81
|
+
...fields.expectsReply ? { expects_reply: true } : {},
|
|
82
|
+
...fields.replyTo !== void 0 ? { reply_to: fields.replyTo } : {}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
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);
|
|
42
94
|
}
|
|
43
|
-
async function postBroker(env, path, body, fetchImpl = fetch) {
|
|
95
|
+
async function postBroker(env, path, body, fetchImpl = fetch, signal) {
|
|
44
96
|
const res = await fetchImpl(`${env.brokerUrl}${path}`, {
|
|
45
97
|
method: "POST",
|
|
46
98
|
headers: { "Content-Type": "application/json" },
|
|
47
99
|
body: JSON.stringify({
|
|
100
|
+
...body,
|
|
48
101
|
run_id: env.runId,
|
|
49
|
-
token: env.token
|
|
50
|
-
|
|
51
|
-
}
|
|
102
|
+
token: env.token
|
|
103
|
+
}),
|
|
104
|
+
...signal ? { signal } : {}
|
|
52
105
|
});
|
|
53
|
-
if (!res.ok) throw
|
|
106
|
+
if (!res.ok) throw await codedError(path, res);
|
|
54
107
|
return res.json();
|
|
55
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);
|
|
120
|
+
return res.json();
|
|
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
|
+
}
|
|
127
|
+
/**
|
|
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.
|
|
132
|
+
*/
|
|
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
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function combineSignals(...signals) {
|
|
147
|
+
const active = signals.filter((signal) => signal !== void 0);
|
|
148
|
+
if (active.length === 0) return void 0;
|
|
149
|
+
if (active.length === 1) return active[0];
|
|
150
|
+
return AbortSignal.any(active);
|
|
151
|
+
}
|
|
56
152
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
153
|
+
* Withdraw a pending ask through the broker as this run. The broker's own error
|
|
154
|
+
* propagates so a caller reporting to a model shows its exact text.
|
|
59
155
|
*/
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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" };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Withdraw a pending ask and classify whether the edge may still exist on the
|
|
166
|
+
* broker: true means the cleanup could not be confirmed, false means it is gone.
|
|
167
|
+
*/
|
|
168
|
+
async function withdrawAsk(env, messageId, generation, fetchImpl) {
|
|
169
|
+
try {
|
|
170
|
+
return {
|
|
171
|
+
pending: false,
|
|
172
|
+
outcome: (await withdrawPendingAsk(env, messageId, generation, fetchImpl)).outcome
|
|
173
|
+
};
|
|
174
|
+
} catch (error) {
|
|
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
|
+
}
|
|
66
189
|
return {
|
|
67
|
-
|
|
68
|
-
|
|
190
|
+
pending: true,
|
|
191
|
+
outcome: "possibly-pending"
|
|
69
192
|
};
|
|
70
193
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
194
|
+
}
|
|
195
|
+
/** Fetch causes for a dial that never opened, so no request reached the broker. */
|
|
196
|
+
const NEVER_REACHED_CODES = new Set([
|
|
197
|
+
"ECONNREFUSED",
|
|
198
|
+
"ConnectionRefused",
|
|
199
|
+
"ENOTFOUND",
|
|
200
|
+
"EAI_AGAIN",
|
|
201
|
+
"EHOSTUNREACH",
|
|
202
|
+
"ENETUNREACH",
|
|
203
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
204
|
+
"ERR_INVALID_URL"
|
|
205
|
+
]);
|
|
206
|
+
/**
|
|
207
|
+
* Whether a fetch failure carries only dial-level causes, so no request was
|
|
208
|
+
* carried to the broker and no ask edge can exist. Undici aggregates one error
|
|
209
|
+
* per dialled address, so every attempt must have failed at the dial.
|
|
210
|
+
*/
|
|
211
|
+
function dialNeverOpened(error, depth = 0) {
|
|
212
|
+
if (depth > 5 || error === null || error === void 0) return false;
|
|
213
|
+
const node = error;
|
|
214
|
+
if (typeof node.code === "string" && NEVER_REACHED_CODES.has(node.code)) return true;
|
|
215
|
+
if (Array.isArray(node.errors) && node.errors.length > 0) return node.errors.every((cause) => dialNeverOpened(cause, depth + 1));
|
|
216
|
+
return dialNeverOpened(node.cause, depth + 1);
|
|
217
|
+
}
|
|
218
|
+
/** Whether the failure came from a caller or timeout abort, not from the wire. */
|
|
219
|
+
function abortedBeforeResponse(error, depth = 0) {
|
|
220
|
+
if (depth > 5 || error === null || error === void 0) return false;
|
|
221
|
+
const node = error;
|
|
222
|
+
if (node.name === "AbortError" || node.name === "TimeoutError") return true;
|
|
223
|
+
if (Array.isArray(node.errors) && node.errors.some((cause) => abortedBeforeResponse(cause, depth + 1))) return true;
|
|
224
|
+
return abortedBeforeResponse(node.cause, depth + 1);
|
|
225
|
+
}
|
|
226
|
+
/** Deadline for the reachability probe, short enough to precede a cleanup. */
|
|
227
|
+
const BROKER_PROBE_TIMEOUT_MS = 250;
|
|
228
|
+
/**
|
|
229
|
+
* Whether the broker still answers HTTP at all. The endpoint is unauthenticated
|
|
230
|
+
* and any response counts, so a live broker that rejects the request is still
|
|
231
|
+
* reachable. Only a dial that never opened means the broker is gone: the
|
|
232
|
+
* probe's own deadline, a reset, and every other transport failure are
|
|
233
|
+
* inconclusive, so a slow broker that already processed the send still counts
|
|
234
|
+
* as reachable and the caller withdraws rather than calling the ask gone.
|
|
235
|
+
*/
|
|
236
|
+
async function brokerReachable(env, fetchImpl) {
|
|
237
|
+
try {
|
|
238
|
+
await fetchImpl(`${env.brokerUrl}/health`, { signal: AbortSignal.timeout(BROKER_PROBE_TIMEOUT_MS) });
|
|
239
|
+
return true;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
return !dialNeverOpened(error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
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.
|
|
249
|
+
*/
|
|
250
|
+
async function sendNeverReachedBroker(error, env, fetchImpl) {
|
|
251
|
+
if (error instanceof BrokerHttpError) return false;
|
|
252
|
+
if (dialNeverOpened(error)) return true;
|
|
253
|
+
if (abortedBeforeResponse(error)) return false;
|
|
254
|
+
return !await brokerReachable(env, fetchImpl);
|
|
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
|
+
}
|
|
260
|
+
/**
|
|
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.
|
|
265
|
+
*/
|
|
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";
|
|
323
|
+
const timeoutMs = normalizeAskTimeoutMs(args.timeoutMs);
|
|
324
|
+
const signal = combineSignals(args.signal, timeoutMs !== void 0 ? AbortSignal.timeout(timeoutMs) : void 0);
|
|
325
|
+
let generation;
|
|
326
|
+
try {
|
|
327
|
+
generation = await pin();
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return {
|
|
330
|
+
outcome: pinFailureOutcome(error),
|
|
331
|
+
messageId
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
await postBroker(env, "/send", buildAgentMessage({
|
|
336
|
+
fromRunId: env.runId,
|
|
337
|
+
toRunId: args.toRunId,
|
|
338
|
+
messageId,
|
|
339
|
+
message: args.message,
|
|
340
|
+
role,
|
|
341
|
+
expectsReply: true,
|
|
342
|
+
generation
|
|
343
|
+
}), fetchImpl, signal);
|
|
344
|
+
} catch (error) {
|
|
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
|
+
};
|
|
358
|
+
}
|
|
359
|
+
let reply;
|
|
360
|
+
try {
|
|
361
|
+
reply = await postBroker(env, "/wait_reply", {
|
|
362
|
+
waiting_for: messageId,
|
|
363
|
+
generation
|
|
364
|
+
}, fetchImpl, signal);
|
|
365
|
+
} catch (error) {
|
|
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
|
+
};
|
|
385
|
+
}
|
|
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);
|
|
450
|
+
return {
|
|
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
|
+
}
|
|
572
|
+
};
|
|
74
573
|
}
|
|
75
574
|
//#endregion
|
|
76
575
|
//#region src/watch.ts
|
|
@@ -401,7 +900,7 @@ var RunInspectorOverlay = class {
|
|
|
401
900
|
}
|
|
402
901
|
if (!isFollowUpable(this.state.status, this.state.snapshot)) return;
|
|
403
902
|
this.runAction(async () => {
|
|
404
|
-
const next = await this.deps.
|
|
903
|
+
const next = await this.deps.resumeRun(this.state.run, text);
|
|
405
904
|
this.state = {
|
|
406
905
|
...this.state,
|
|
407
906
|
run: {
|
|
@@ -1005,7 +1504,7 @@ function guidance(status, pending, runId) {
|
|
|
1005
1504
|
const options = permissionOptions(pending);
|
|
1006
1505
|
if (options.length > 0) return `Guidance: Call avenor_answer_permission with run_id "${runId}" and option_id "${selected(options[0], "option_id")}".`;
|
|
1007
1506
|
if (pending) return `Guidance: Permission input is required, but no options are available; call avenor_inspect with run_id "${runId}".`;
|
|
1008
|
-
return `Guidance: Call
|
|
1507
|
+
return `Guidance: Call avenor_message with action "resume" or avenor_status with run_id "${runId}".`;
|
|
1009
1508
|
}
|
|
1010
1509
|
if (isTerminal(status)) return `Guidance: Call avenor_result or avenor_events with run_id "${runId}".`;
|
|
1011
1510
|
return `Guidance: Call avenor_result with run_id "${runId}".`;
|
|
@@ -1101,18 +1600,33 @@ function permissionLines(details, args) {
|
|
|
1101
1600
|
`Guidance: Call avenor_status with run_id "${runId}" to verify the permission state.`
|
|
1102
1601
|
];
|
|
1103
1602
|
}
|
|
1104
|
-
function
|
|
1603
|
+
function messageLines(details, args) {
|
|
1105
1604
|
const result = asRecord(details);
|
|
1106
|
-
if (!result
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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;
|
|
1116
1630
|
}
|
|
1117
1631
|
function eventSequence(value) {
|
|
1118
1632
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
@@ -1334,12 +1848,21 @@ function renderAnswerPermissionCall(args, theme) {
|
|
|
1334
1848
|
if (supervisor) pieces.push(`supervisor_id ${supervisor}`);
|
|
1335
1849
|
return linesText([pieces.join(" ")], theme);
|
|
1336
1850
|
}
|
|
1337
|
-
function
|
|
1338
|
-
const
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
if (
|
|
1342
|
-
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
|
+
}
|
|
1343
1866
|
return linesText([pieces.join(" ")], theme);
|
|
1344
1867
|
}
|
|
1345
1868
|
function renderEventsCall(args, theme) {
|
|
@@ -1373,8 +1896,8 @@ function renderInspectResult(result, options, theme, args) {
|
|
|
1373
1896
|
function renderAnswerPermissionResult(result, options, theme, args) {
|
|
1374
1897
|
return render("answer_permission", result, options, theme, () => permissionLines(result.details, args));
|
|
1375
1898
|
}
|
|
1376
|
-
function
|
|
1377
|
-
return render("
|
|
1899
|
+
function renderMessageResumeResult(result, options, theme, args) {
|
|
1900
|
+
return render("message", result, options, theme, () => messageLines(result.details, args));
|
|
1378
1901
|
}
|
|
1379
1902
|
function renderEventsResult(result, options, theme, args) {
|
|
1380
1903
|
return render("events", result, options, theme, () => eventsLines(result.details, args, options.expanded));
|
|
@@ -1409,11 +1932,10 @@ const defaultDeps = {
|
|
|
1409
1932
|
statusTool,
|
|
1410
1933
|
eventsTool,
|
|
1411
1934
|
answerPermissionTool,
|
|
1412
|
-
|
|
1935
|
+
messageTool,
|
|
1413
1936
|
inspectTool,
|
|
1414
1937
|
resultTool,
|
|
1415
1938
|
brokerReceiveTool,
|
|
1416
|
-
brokerReplyTool,
|
|
1417
1939
|
shutdownTool,
|
|
1418
1940
|
killTool,
|
|
1419
1941
|
workflowStatusTool,
|
|
@@ -1424,35 +1946,13 @@ const defaultDeps = {
|
|
|
1424
1946
|
workflowGateTool,
|
|
1425
1947
|
workflowControllerStatusTool,
|
|
1426
1948
|
workflowReadyTool,
|
|
1949
|
+
workflowCancelTool,
|
|
1950
|
+
getSupervisorClient,
|
|
1427
1951
|
observeRun,
|
|
1428
1952
|
dial,
|
|
1953
|
+
attachOwner,
|
|
1429
1954
|
Supervisor
|
|
1430
1955
|
};
|
|
1431
|
-
let hostPollTimer = null;
|
|
1432
|
-
const hostPendingAsks = /* @__PURE__ */ new Map();
|
|
1433
|
-
function stopHostAskPoll() {
|
|
1434
|
-
if (hostPollTimer) {
|
|
1435
|
-
clearInterval(hostPollTimer);
|
|
1436
|
-
hostPollTimer = null;
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
async function startHostAskPoll(ctx) {
|
|
1440
|
-
stopHostAskPoll();
|
|
1441
|
-
hostPollTimer = setInterval(async () => {
|
|
1442
|
-
try {
|
|
1443
|
-
const { asks } = await deps.brokerReceiveTool({});
|
|
1444
|
-
for (const ask of asks) {
|
|
1445
|
-
hostPendingAsks.set(ask.message_id, {
|
|
1446
|
-
from_run_id: ask.from_run_id,
|
|
1447
|
-
message_id: ask.message_id
|
|
1448
|
-
});
|
|
1449
|
-
const body = `**📨 Ask from sub-agent** (\`${ask.role && ask.role !== "agent" ? `${ask.from_run_id} (${ask.role})` : ask.from_run_id}\`)\n\n${ask.message}\n\nTo answer, use the intercom tool: avenor_reply({ from_run_id: ${JSON.stringify(ask.from_run_id)}, reply_to_message_id: ${JSON.stringify(ask.message_id)}, message: "..." })`;
|
|
1450
|
-
await ctx.sendUserMessage(body, { deliverAs: "steer" });
|
|
1451
|
-
}
|
|
1452
|
-
} catch (error) {}
|
|
1453
|
-
}, 2e3);
|
|
1454
|
-
hostPollTimer.unref?.();
|
|
1455
|
-
}
|
|
1456
1956
|
function statusSupervisorId(requestedSupervisorId, spawnedSupervisorId) {
|
|
1457
1957
|
return requestedSupervisorId ? spawnedSupervisorId : void 0;
|
|
1458
1958
|
}
|
|
@@ -1489,7 +1989,7 @@ function buildCompletionText(run, result, finalOutput) {
|
|
|
1489
1989
|
if (output) lines.push("", "Final output:", ...quotedLines(output));
|
|
1490
1990
|
if (result.stop_reason && result.stop_reason.toUpperCase() !== result.status.toUpperCase()) lines.push(`Stop reason: ${result.stop_reason}`);
|
|
1491
1991
|
if (result.session_id) lines.push(`Session: \`${result.session_id}\``);
|
|
1492
|
-
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.`);
|
|
1493
1993
|
return lines.join("\n");
|
|
1494
1994
|
}
|
|
1495
1995
|
function buildWaitingText(run, status) {
|
|
@@ -1498,7 +1998,7 @@ function buildWaitingText(run, status) {
|
|
|
1498
1998
|
const lines = [`Sub-agent "${run.label}" (${agent}) is waiting for input.`];
|
|
1499
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"`);
|
|
1500
2000
|
else if (status.phase_label) lines.push(`Question: ${status.phase_label}`);
|
|
1501
|
-
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.`);
|
|
1502
2002
|
return lines.join("\n");
|
|
1503
2003
|
}
|
|
1504
2004
|
function isTerminalStatus(status) {
|
|
@@ -1602,6 +2102,16 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1602
2102
|
const pollingErrors = [];
|
|
1603
2103
|
let lastStatusEntries = [];
|
|
1604
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
|
+
}
|
|
1605
2115
|
function interruptPendingWaits(stop, excludeRunKey) {
|
|
1606
2116
|
for (const wait of [...pendingWaits]) {
|
|
1607
2117
|
if (wait.runKey === excludeRunKey) continue;
|
|
@@ -1617,6 +2127,29 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1617
2127
|
pendingWaits.add(wait);
|
|
1618
2128
|
return wait;
|
|
1619
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
|
+
}
|
|
1620
2153
|
function createInterruptibleWait(runKey, signal) {
|
|
1621
2154
|
const controller = new AbortController();
|
|
1622
2155
|
let stopped;
|
|
@@ -1695,7 +2228,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1695
2228
|
subAgentPollTimer = null;
|
|
1696
2229
|
}
|
|
1697
2230
|
}
|
|
1698
|
-
async function startSubAgentPoll(
|
|
2231
|
+
async function startSubAgentPoll() {
|
|
1699
2232
|
if (!subAgentEnv) return;
|
|
1700
2233
|
stopSubAgentPoll();
|
|
1701
2234
|
const generation = subAgentPollGeneration;
|
|
@@ -1704,16 +2237,13 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1704
2237
|
const tick = async () => {
|
|
1705
2238
|
try {
|
|
1706
2239
|
const msgs = await postBroker(subAgentEnv, "/poll-control", {}) ?? [];
|
|
2240
|
+
if (generation !== subAgentPollGeneration) return;
|
|
1707
2241
|
if (consecutiveFailures >= 3) console.error("[avenor] sub-agent broker poll recovered");
|
|
1708
2242
|
consecutiveFailures = 0;
|
|
1709
2243
|
for (const msg of msgs) {
|
|
1710
2244
|
const ask = parseControlMessage(msg);
|
|
1711
2245
|
if (!ask) continue;
|
|
1712
|
-
subAgentPendingAsks
|
|
1713
|
-
from_run_id: ask.from_run_id,
|
|
1714
|
-
message_id: ask.message_id
|
|
1715
|
-
});
|
|
1716
|
-
await ctx.sendUserMessage(formatInboundAskBody(ask), { deliverAs: "steer" });
|
|
2246
|
+
surfaceInboundAsk(subAgentPendingAsks, ask, "Inbound ask");
|
|
1717
2247
|
}
|
|
1718
2248
|
} catch (error) {
|
|
1719
2249
|
consecutiveFailures++;
|
|
@@ -1726,6 +2256,48 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1726
2256
|
subAgentPollTimer = setTimeout(() => void tick(), intervalMs);
|
|
1727
2257
|
subAgentPollTimer.unref?.();
|
|
1728
2258
|
}
|
|
2259
|
+
let hostPollTimer = null;
|
|
2260
|
+
let hostPollGeneration = 0;
|
|
2261
|
+
let hostReceiveGeneration;
|
|
2262
|
+
const hostPendingAsks = /* @__PURE__ */ new Map();
|
|
2263
|
+
function stopHostAskPoll() {
|
|
2264
|
+
hostPollGeneration++;
|
|
2265
|
+
if (hostPollTimer) {
|
|
2266
|
+
clearTimeout(hostPollTimer);
|
|
2267
|
+
hostPollTimer = null;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
function startHostAskPoll() {
|
|
2271
|
+
stopHostAskPoll();
|
|
2272
|
+
const generation = hostPollGeneration;
|
|
2273
|
+
const intervalMs = options.pollIntervalMs ?? 2e3;
|
|
2274
|
+
const tick = async () => {
|
|
2275
|
+
const startedAt = Date.now();
|
|
2276
|
+
try {
|
|
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();
|
|
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");
|
|
2293
|
+
} catch {}
|
|
2294
|
+
if (generation !== hostPollGeneration) return;
|
|
2295
|
+
hostPollTimer = setTimeout(() => void tick(), Math.max(0, intervalMs - (Date.now() - startedAt)));
|
|
2296
|
+
hostPollTimer.unref?.();
|
|
2297
|
+
};
|
|
2298
|
+
hostPollTimer = setTimeout(() => void tick(), intervalMs);
|
|
2299
|
+
hostPollTimer.unref?.();
|
|
2300
|
+
}
|
|
1729
2301
|
function recordPollingError(source, message, error, runId) {
|
|
1730
2302
|
pollingErrorCount++;
|
|
1731
2303
|
const payload = createPollErrorPayload({
|
|
@@ -2010,7 +2582,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2010
2582
|
}
|
|
2011
2583
|
async function withClient(run, work) {
|
|
2012
2584
|
if (!run.supervisorId || deps.Supervisor.isCurrentInstance(run.supervisorId)) return work((await deps.Supervisor.get()).getClient());
|
|
2013
|
-
const client = await deps.
|
|
2585
|
+
const client = await deps.attachOwner(run.supervisorId);
|
|
2014
2586
|
try {
|
|
2015
2587
|
return await work(client);
|
|
2016
2588
|
} finally {
|
|
@@ -2048,44 +2620,22 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2048
2620
|
await client.interruptAndPrompt(runtimeId, text);
|
|
2049
2621
|
});
|
|
2050
2622
|
},
|
|
2051
|
-
|
|
2052
|
-
return
|
|
2053
|
-
runId: run.runId,
|
|
2054
|
-
message: text,
|
|
2055
|
-
supervisorId: run.supervisorId
|
|
2056
|
-
});
|
|
2623
|
+
resumeRun(run, text) {
|
|
2624
|
+
return resumeTrackedRun(run, text);
|
|
2057
2625
|
}
|
|
2058
2626
|
};
|
|
2059
2627
|
}
|
|
2060
|
-
async function
|
|
2061
|
-
const
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
upsertTrackedRun({
|
|
2072
|
-
runId: result.run_id,
|
|
2073
|
-
label: result.label,
|
|
2074
|
-
agent: status?.agent ?? run.agent,
|
|
2075
|
-
runtimeId: status?.runtime_id,
|
|
2076
|
-
supervisorId: run.supervisorId
|
|
2077
|
-
}, status, false);
|
|
2078
|
-
startPolling();
|
|
2079
|
-
} catch {
|
|
2080
|
-
upsertTrackedRun({
|
|
2081
|
-
runId: result.run_id,
|
|
2082
|
-
label: result.label,
|
|
2083
|
-
agent: run.agent,
|
|
2084
|
-
supervisorId: run.supervisorId
|
|
2085
|
-
}, void 0, false);
|
|
2086
|
-
startPolling();
|
|
2087
|
-
}
|
|
2088
|
-
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
|
+
};
|
|
2089
2639
|
}
|
|
2090
2640
|
async function waitForRunWithObserver(run, signal, onUpdate) {
|
|
2091
2641
|
let observer = null;
|
|
@@ -2193,20 +2743,117 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2193
2743
|
if (observed.status || observed.aborted) return observed;
|
|
2194
2744
|
return waitForRunByPolling(run, signal, onUpdate);
|
|
2195
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
|
+
});
|
|
2196
2842
|
pi.on("session_start", async (_event, ctx) => {
|
|
2197
2843
|
sessionCtx = ctx;
|
|
2198
2844
|
lastStatusEntries = [];
|
|
2199
2845
|
pollingErrorCount = 0;
|
|
2200
2846
|
pollingErrors.length = 0;
|
|
2201
|
-
startHostAskPoll(
|
|
2202
|
-
startSubAgentPoll(
|
|
2847
|
+
if (!isSubAgentBrokerMode) startHostAskPoll();
|
|
2848
|
+
startSubAgentPoll();
|
|
2203
2849
|
});
|
|
2204
2850
|
pi.on("session_shutdown", async () => {
|
|
2205
|
-
await stopPolling();
|
|
2206
2851
|
stopHostAskPoll();
|
|
2207
2852
|
stopSubAgentPoll();
|
|
2853
|
+
disposeHostProviders();
|
|
2208
2854
|
hostPendingAsks.clear();
|
|
2209
2855
|
subAgentPendingAsks.clear();
|
|
2856
|
+
await stopPolling();
|
|
2210
2857
|
for (const wait of [...pendingWaits]) {
|
|
2211
2858
|
pendingWaits.delete(wait);
|
|
2212
2859
|
wait.interrupt({
|
|
@@ -2243,15 +2890,15 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2243
2890
|
pi.registerTool({
|
|
2244
2891
|
name: "avenor_spawn",
|
|
2245
2892
|
label: "Avenor Spawn",
|
|
2246
|
-
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.`,
|
|
2247
2894
|
promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, result, inspect, events, follow-up, shutdown)",
|
|
2248
2895
|
promptGuidelines: [
|
|
2249
2896
|
"Use avenor_spawn to delegate well-defined tasks to sub-agents.",
|
|
2250
2897
|
"Use avenor_status (view \"lifecycle\") only to check progress or pending permissions.",
|
|
2251
|
-
"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.",
|
|
2252
2899
|
"Use avenor_inspect to review a bounded transcript and tool activity from a run.",
|
|
2253
2900
|
"Use avenor_events only when you specifically need raw event payloads.",
|
|
2254
|
-
"Use
|
|
2901
|
+
"Use avenor_message with action \"resume\" to iterate on a completed run.",
|
|
2255
2902
|
"Use avenor_shutdown when done delegating work.",
|
|
2256
2903
|
"When roster_entry is set, the roster selects agent, model, and backend; do not pass those fields alongside it.",
|
|
2257
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."
|
|
@@ -2372,7 +3019,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2372
3019
|
return {
|
|
2373
3020
|
content: [{
|
|
2374
3021
|
type: "text",
|
|
2375
|
-
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.`
|
|
2376
3023
|
}],
|
|
2377
3024
|
details: {
|
|
2378
3025
|
run_id: result.run_id,
|
|
@@ -2538,7 +3185,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2538
3185
|
pi.registerTool({
|
|
2539
3186
|
name: "avenor_result",
|
|
2540
3187
|
label: "Avenor Result",
|
|
2541
|
-
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.`,
|
|
2542
3189
|
parameters: Type.Object({
|
|
2543
3190
|
run_id: Type.String({ description: "Run ID or label" }),
|
|
2544
3191
|
wait: Type.Optional(Type.Boolean({ description: "Wait for a terminal result (default true); false retrieves the current state without blocking" })),
|
|
@@ -2673,183 +3320,97 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2673
3320
|
}
|
|
2674
3321
|
});
|
|
2675
3322
|
pi.registerTool({
|
|
2676
|
-
name: "
|
|
2677
|
-
label: "Avenor
|
|
2678
|
-
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.",
|
|
2679
3326
|
parameters: Type.Object({
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
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)" }))
|
|
2684
3343
|
}),
|
|
2685
|
-
async execute(_toolCallId, params) {
|
|
2686
|
-
const
|
|
2687
|
-
|
|
2688
|
-
message: params.message,
|
|
2689
|
-
label: params.label,
|
|
2690
|
-
supervisorId: params.supervisor_id
|
|
2691
|
-
});
|
|
2692
|
-
try {
|
|
2693
|
-
const status = asStatusResult(await deps.statusTool({
|
|
2694
|
-
runId: result.run_id,
|
|
2695
|
-
supervisorId: params.supervisor_id
|
|
2696
|
-
}));
|
|
2697
|
-
upsertTrackedRun({
|
|
2698
|
-
runId: result.run_id,
|
|
2699
|
-
label: result.label,
|
|
2700
|
-
agent: status?.agent,
|
|
2701
|
-
runtimeId: status?.runtime_id,
|
|
2702
|
-
supervisorId: params.supervisor_id
|
|
2703
|
-
}, status, false);
|
|
2704
|
-
} catch {
|
|
2705
|
-
upsertTrackedRun({
|
|
2706
|
-
runId: result.run_id,
|
|
2707
|
-
label: result.label,
|
|
2708
|
-
supervisorId: params.supervisor_id
|
|
2709
|
-
}, void 0, false);
|
|
2710
|
-
}
|
|
2711
|
-
startPolling();
|
|
3344
|
+
async execute(_toolCallId, params, signal) {
|
|
3345
|
+
const provider = await messageProvider(void 0);
|
|
3346
|
+
const result = await deps.messageTool(params, provider, { signal });
|
|
2712
3347
|
return {
|
|
2713
3348
|
content: [{
|
|
2714
3349
|
type: "text",
|
|
2715
|
-
text:
|
|
3350
|
+
text: result.text ?? ""
|
|
2716
3351
|
}],
|
|
2717
|
-
details: result
|
|
3352
|
+
details: resultOutput(result)
|
|
2718
3353
|
};
|
|
2719
3354
|
},
|
|
2720
|
-
renderCall(args, theme
|
|
2721
|
-
return
|
|
3355
|
+
renderCall(args, theme) {
|
|
3356
|
+
return renderMessageResumeCall(args, theme);
|
|
2722
3357
|
},
|
|
2723
3358
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
2724
|
-
return
|
|
3359
|
+
return renderMessageResumeResult(result, {
|
|
2725
3360
|
expanded,
|
|
2726
3361
|
isPartial
|
|
2727
3362
|
}, theme, context.args);
|
|
2728
3363
|
}
|
|
2729
3364
|
});
|
|
2730
3365
|
pi.registerTool({
|
|
2731
|
-
name: "
|
|
2732
|
-
label: "Avenor
|
|
2733
|
-
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.",
|
|
2734
3369
|
parameters: Type.Object({
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
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)" }))
|
|
2739
3378
|
}),
|
|
2740
3379
|
async execute(_toolCallId, params) {
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
3380
|
+
const result = await peersTool(params, await messageProvider(void 0));
|
|
3381
|
+
return {
|
|
3382
|
+
content: [{
|
|
3383
|
+
type: "text",
|
|
3384
|
+
text: result.action === "list" ? [result.text, renderPeerList(result.peers)].filter((part) => part !== "").join("\n\n") : result.text
|
|
3385
|
+
}],
|
|
3386
|
+
details: peersDetails(result)
|
|
3387
|
+
};
|
|
2747
3388
|
}
|
|
2748
3389
|
});
|
|
2749
3390
|
pi.registerTool({
|
|
2750
|
-
name: "
|
|
2751
|
-
label: "Avenor
|
|
2752
|
-
description: "
|
|
3391
|
+
name: "avenor_workflow_cancel",
|
|
3392
|
+
label: "Avenor Workflow Cancel",
|
|
3393
|
+
description: "Cancel a workflow instance. The actor and reason are required.",
|
|
2753
3394
|
parameters: Type.Object({
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
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" }))
|
|
2758
3399
|
}),
|
|
2759
3400
|
async execute(_toolCallId, params) {
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
details: { error: true }
|
|
2768
|
-
};
|
|
2769
|
-
subAgentPendingAsks.delete(target.message_id);
|
|
2770
|
-
try {
|
|
2771
|
-
await postBroker(subAgentEnv, "/send", {
|
|
2772
|
-
from_run_id: subAgentEnv.runId,
|
|
2773
|
-
to_run_id: target.from_run_id,
|
|
2774
|
-
type: "agent_message",
|
|
2775
|
-
payload: {
|
|
2776
|
-
id: randomUUID(),
|
|
2777
|
-
from_run_id: subAgentEnv.runId,
|
|
2778
|
-
to_run_id: target.from_run_id,
|
|
2779
|
-
message: params.message,
|
|
2780
|
-
role: "agent",
|
|
2781
|
-
reply_to: target.message_id
|
|
2782
|
-
}
|
|
2783
|
-
});
|
|
2784
|
-
} catch (error) {
|
|
2785
|
-
subAgentPendingAsks.set(target.message_id, target);
|
|
2786
|
-
throw error;
|
|
2787
|
-
}
|
|
2788
|
-
return { content: [{
|
|
2789
|
-
type: "text",
|
|
2790
|
-
text: "Reply sent"
|
|
2791
|
-
}] };
|
|
2792
|
-
}
|
|
2793
|
-
let fromRunId = params.from_run_id;
|
|
2794
|
-
let messageId = params.reply_to_message_id;
|
|
2795
|
-
if (!messageId) {
|
|
2796
|
-
let match;
|
|
2797
|
-
for (const [, pending] of hostPendingAsks) if (pending.from_run_id === fromRunId && (!match || pending.message_id > match.message_id)) match = pending;
|
|
2798
|
-
if (match) messageId = match.message_id;
|
|
2799
|
-
}
|
|
2800
|
-
if (!fromRunId || !messageId) return {
|
|
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
|
+
});
|
|
3407
|
+
return {
|
|
2801
3408
|
content: [{
|
|
2802
3409
|
type: "text",
|
|
2803
|
-
text:
|
|
3410
|
+
text: JSON.stringify(result, null, 2)
|
|
2804
3411
|
}],
|
|
2805
|
-
details:
|
|
3412
|
+
details: result
|
|
2806
3413
|
};
|
|
2807
|
-
await deps.brokerReplyTool({
|
|
2808
|
-
toRunId: fromRunId,
|
|
2809
|
-
replyTo: messageId,
|
|
2810
|
-
message: params.message,
|
|
2811
|
-
supervisorId: params.supervisor_id
|
|
2812
|
-
});
|
|
2813
|
-
hostPendingAsks.delete(messageId);
|
|
2814
|
-
return { content: [{
|
|
2815
|
-
type: "text",
|
|
2816
|
-
text: "Reply sent"
|
|
2817
|
-
}] };
|
|
2818
|
-
}
|
|
2819
|
-
});
|
|
2820
|
-
pi.registerTool({
|
|
2821
|
-
name: "avenor_peers",
|
|
2822
|
-
label: "Avenor Peers",
|
|
2823
|
-
description: "List all connected agent sessions with their status and metadata.",
|
|
2824
|
-
parameters: Type.Object({}),
|
|
2825
|
-
async execute() {
|
|
2826
|
-
const result = await peersTool({});
|
|
2827
|
-
if (result.peers.length === 0) return { content: [{
|
|
2828
|
-
type: "text",
|
|
2829
|
-
text: "No other agent sessions connected."
|
|
2830
|
-
}] };
|
|
2831
|
-
return { content: [{
|
|
2832
|
-
type: "text",
|
|
2833
|
-
text: `**Active sessions:**\n${result.peers.map((p) => {
|
|
2834
|
-
const parts = [p.label ? `${p.label} (${p.run_id})` : p.run_id];
|
|
2835
|
-
const tags = [
|
|
2836
|
-
p.status,
|
|
2837
|
-
p.backend,
|
|
2838
|
-
p.model
|
|
2839
|
-
].filter(Boolean);
|
|
2840
|
-
if (tags.length) parts.push(`[${tags.join(", ")}]`);
|
|
2841
|
-
return ` ${parts.join(" ")}`;
|
|
2842
|
-
}).join("\n")}`
|
|
2843
|
-
}] };
|
|
2844
|
-
}
|
|
2845
|
-
});
|
|
2846
|
-
pi.registerTool({
|
|
2847
|
-
name: "avenor_cancel",
|
|
2848
|
-
label: "Avenor Cancel",
|
|
2849
|
-
description: "Cancel a pending ask by its message ID.",
|
|
2850
|
-
parameters: Type.Object({ message_id: Type.String({ description: "Message ID of the pending ask to cancel" }) }),
|
|
2851
|
-
async execute(_toolCallId, params) {
|
|
2852
|
-
return cancelTool({ messageId: params.message_id });
|
|
2853
3414
|
}
|
|
2854
3415
|
});
|
|
2855
3416
|
pi.registerTool({
|
|
@@ -2914,6 +3475,8 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2914
3475
|
supervisorId: params.supervisor_id,
|
|
2915
3476
|
force: params.force
|
|
2916
3477
|
});
|
|
3478
|
+
disposeHostProvider(params.supervisor_id);
|
|
3479
|
+
if (targetIsSingleton && target !== void 0) disposeHostProvider(void 0);
|
|
2917
3480
|
return {
|
|
2918
3481
|
content: [{
|
|
2919
3482
|
type: "text",
|
|
@@ -3206,7 +3769,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
3206
3769
|
initialRunId: args?.trim() || void 0,
|
|
3207
3770
|
deps: {
|
|
3208
3771
|
...createWatchDeps(),
|
|
3209
|
-
|
|
3772
|
+
resumeRun: (run, text) => resumeTrackedRun(run, text)
|
|
3210
3773
|
},
|
|
3211
3774
|
onTrackRun: (run, status) => {
|
|
3212
3775
|
upsertTrackedRun(run, status, false);
|