@dougbots/avenor-pi 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +274 -70
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InspectResult, StatusResult, Supervisor, answerPermissionTool, brokerReceiveTool, brokerReplyTool, dial, eventsTool, followUpTool, inspectTool, killTool, observeRun, resultTool, shutdownTool, spawnTool, statusTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
|
|
1
|
+
import { InspectResult, StatusResult, Supervisor, answerPermissionTool, attachOwner, brokerReceiveTool, brokerReplyTool, dial, eventsTool, followUpTool, inspectTool, killTool, observeRun, resultTool, shutdownTool, spawnTool, statusTool, workflowCompleteTool, workflowControllerStatusTool, workflowEventsTool, workflowGateTool, workflowInspectTool, workflowReadyTool, workflowStatusTool, workflowWaitTool } from "@dougbots/avenor-core";
|
|
2
2
|
import { EventBus, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
4
|
//#region src/subagent-broker.d.ts
|
|
@@ -139,6 +139,7 @@ interface ExtensionDeps {
|
|
|
139
139
|
workflowReadyTool: typeof workflowReadyTool;
|
|
140
140
|
observeRun: typeof observeRun;
|
|
141
141
|
dial: typeof dial;
|
|
142
|
+
attachOwner: typeof attachOwner;
|
|
142
143
|
Supervisor: typeof Supervisor;
|
|
143
144
|
}
|
|
144
145
|
declare function statusSupervisorId(requestedSupervisorId: string | undefined, spawnedSupervisorId: string): string | undefined;
|
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
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
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";
|
|
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";
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
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 ?? "";
|
|
@@ -40,17 +42,46 @@ function parseControlMessage(msg) {
|
|
|
40
42
|
function formatInboundAskBody(ask) {
|
|
41
43
|
return `**📨 Inbound ask**${ask.from_run_id ? ` from \`${ask.from_run_id}\`` : ""}\n\n${ask.message}\n\nTo answer, use avenor_reply({ reply_to_message_id: ${JSON.stringify(ask.message_id)}, message: "..." }).`;
|
|
42
44
|
}
|
|
43
|
-
|
|
45
|
+
/** A broker HTTP response error, so callers can classify by status instead of
|
|
46
|
+
* matching digits in the request text. */
|
|
47
|
+
var BrokerHttpError = class extends Error {
|
|
48
|
+
status;
|
|
49
|
+
constructor(path, status, body) {
|
|
50
|
+
super(`broker ${path}: ${status} ${body}`);
|
|
51
|
+
this.name = "BrokerHttpError";
|
|
52
|
+
this.status = status;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/** Build the `/send` body for one agent_message, as the broker child tools do. */
|
|
56
|
+
function buildAgentMessage(fields) {
|
|
57
|
+
return {
|
|
58
|
+
from_run_id: fields.fromRunId,
|
|
59
|
+
to_run_id: fields.toRunId,
|
|
60
|
+
type: "agent_message",
|
|
61
|
+
payload: {
|
|
62
|
+
id: fields.messageId,
|
|
63
|
+
from: fields.fromRunId,
|
|
64
|
+
from_run_id: fields.fromRunId,
|
|
65
|
+
to_run_id: fields.toRunId,
|
|
66
|
+
message: fields.message,
|
|
67
|
+
role: fields.role,
|
|
68
|
+
...fields.expectsReply ? { expects_reply: true } : {},
|
|
69
|
+
...fields.replyTo !== void 0 ? { reply_to: fields.replyTo } : {}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function postBroker(env, path, body, fetchImpl = fetch, signal) {
|
|
44
74
|
const res = await fetchImpl(`${env.brokerUrl}${path}`, {
|
|
45
75
|
method: "POST",
|
|
46
76
|
headers: { "Content-Type": "application/json" },
|
|
47
77
|
body: JSON.stringify({
|
|
78
|
+
...body,
|
|
48
79
|
run_id: env.runId,
|
|
49
|
-
token: env.token
|
|
50
|
-
|
|
51
|
-
}
|
|
80
|
+
token: env.token
|
|
81
|
+
}),
|
|
82
|
+
...signal ? { signal } : {}
|
|
52
83
|
});
|
|
53
|
-
if (!res.ok) throw new
|
|
84
|
+
if (!res.ok) throw new BrokerHttpError(path, res.status, (await res.text()).trim());
|
|
54
85
|
return res.json();
|
|
55
86
|
}
|
|
56
87
|
/**
|
|
@@ -72,6 +103,143 @@ function resolveReplyTarget(pending, fromRunId, replyToMessageId) {
|
|
|
72
103
|
for (const [, entry] of pending) if (!fromRunId || entry.from_run_id === fromRunId) match = entry;
|
|
73
104
|
return match ?? null;
|
|
74
105
|
}
|
|
106
|
+
function combineSignals(...signals) {
|
|
107
|
+
const active = signals.filter((signal) => signal !== void 0);
|
|
108
|
+
if (active.length === 0) return void 0;
|
|
109
|
+
if (active.length === 1) return active[0];
|
|
110
|
+
return AbortSignal.any(active);
|
|
111
|
+
}
|
|
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
|
+
/**
|
|
121
|
+
* Withdraw a pending ask through the broker as this run. The broker's own error
|
|
122
|
+
* propagates so a caller reporting to a model shows its exact text.
|
|
123
|
+
*/
|
|
124
|
+
async function withdrawPendingAsk(env, messageId, fetchImpl = fetch) {
|
|
125
|
+
await postBroker(env, "/cancel_message", { cancel_message_id: messageId }, fetchImpl, AbortSignal.timeout(ASK_CLEANUP_TIMEOUT_MS));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Withdraw a pending ask and classify whether the edge may still exist on the
|
|
129
|
+
* broker: true means the cleanup could not be confirmed, false means it is gone.
|
|
130
|
+
*/
|
|
131
|
+
async function withdrawAsk(env, messageId, fetchImpl) {
|
|
132
|
+
try {
|
|
133
|
+
await withdrawPendingAsk(env, messageId, fetchImpl);
|
|
134
|
+
return false;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof BrokerHttpError) return error.status !== 404;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Fetch causes for a dial that never opened, so no request reached the broker. */
|
|
141
|
+
const NEVER_REACHED_CODES = new Set([
|
|
142
|
+
"ECONNREFUSED",
|
|
143
|
+
"ConnectionRefused",
|
|
144
|
+
"ENOTFOUND",
|
|
145
|
+
"EAI_AGAIN",
|
|
146
|
+
"EHOSTUNREACH",
|
|
147
|
+
"ENETUNREACH",
|
|
148
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
149
|
+
"ERR_INVALID_URL"
|
|
150
|
+
]);
|
|
151
|
+
/**
|
|
152
|
+
* Whether a fetch failure carries only dial-level causes, so no request was
|
|
153
|
+
* carried to the broker and no ask edge can exist. Undici aggregates one error
|
|
154
|
+
* per dialled address, so every attempt must have failed at the dial.
|
|
155
|
+
*/
|
|
156
|
+
function dialNeverOpened(error, depth = 0) {
|
|
157
|
+
if (depth > 5 || error === null || error === void 0) return false;
|
|
158
|
+
const node = error;
|
|
159
|
+
if (typeof node.code === "string" && NEVER_REACHED_CODES.has(node.code)) return true;
|
|
160
|
+
if (Array.isArray(node.errors) && node.errors.length > 0) return node.errors.every((cause) => dialNeverOpened(cause, depth + 1));
|
|
161
|
+
return dialNeverOpened(node.cause, depth + 1);
|
|
162
|
+
}
|
|
163
|
+
/** Whether the failure came from a caller or timeout abort, not from the wire. */
|
|
164
|
+
function abortedBeforeResponse(error, depth = 0) {
|
|
165
|
+
if (depth > 5 || error === null || error === void 0) return false;
|
|
166
|
+
const node = error;
|
|
167
|
+
if (node.name === "AbortError" || node.name === "TimeoutError") return true;
|
|
168
|
+
if (Array.isArray(node.errors) && node.errors.some((cause) => abortedBeforeResponse(cause, depth + 1))) return true;
|
|
169
|
+
return abortedBeforeResponse(node.cause, depth + 1);
|
|
170
|
+
}
|
|
171
|
+
/** Deadline for the reachability probe, short enough to precede a cleanup. */
|
|
172
|
+
const BROKER_PROBE_TIMEOUT_MS = 250;
|
|
173
|
+
/**
|
|
174
|
+
* Whether the broker still answers HTTP at all. The endpoint is unauthenticated
|
|
175
|
+
* and any response counts, so a live broker that rejects the request is still
|
|
176
|
+
* reachable. Only a dial that never opened means the broker is gone: the
|
|
177
|
+
* probe's own deadline, a reset, and every other transport failure are
|
|
178
|
+
* inconclusive, so a slow broker that already processed the send still counts
|
|
179
|
+
* as reachable and the caller withdraws rather than calling the ask gone.
|
|
180
|
+
*/
|
|
181
|
+
async function brokerReachable(env, fetchImpl) {
|
|
182
|
+
try {
|
|
183
|
+
await fetchImpl(`${env.brokerUrl}/health`, { signal: AbortSignal.timeout(BROKER_PROBE_TIMEOUT_MS) });
|
|
184
|
+
return true;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return !dialNeverOpened(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Whether a failed send provably never reached the broker, mirroring the host's
|
|
191
|
+
* rule: a dial that never opened or an unusable broker address carries no
|
|
192
|
+
* request, and a send that drew no HTTP response while the broker no longer
|
|
193
|
+
* accepts a dial cannot have left its edge in a broker that is gone. An HTTP
|
|
194
|
+
* response, a reset, or an abort against a broker that answers at all may have
|
|
195
|
+
* been processed and still needs a withdraw.
|
|
196
|
+
*/
|
|
197
|
+
async function sendNeverReachedBroker(error, env, fetchImpl) {
|
|
198
|
+
if (error instanceof BrokerHttpError) return false;
|
|
199
|
+
if (dialNeverOpened(error)) return true;
|
|
200
|
+
if (abortedBeforeResponse(error)) return false;
|
|
201
|
+
return !await brokerReachable(env, fetchImpl);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Ask another run through the inherited broker as this run, then wait for the
|
|
205
|
+
* correlated reply. A failed send, wait, timeout, or abort withdraws the ask
|
|
206
|
+
* and reports its message id plus whether it is still pending, matching the
|
|
207
|
+
* host-side ask tool's contract.
|
|
208
|
+
*/
|
|
209
|
+
async function askViaBroker(env, args, fetchImpl = fetch) {
|
|
210
|
+
const messageId = randomUUID();
|
|
211
|
+
const role = args.role ?? "agent";
|
|
212
|
+
const timeoutMs = normalizeAskTimeoutMs(args.timeoutMs);
|
|
213
|
+
const signal = combineSignals(args.signal, timeoutMs !== void 0 ? AbortSignal.timeout(timeoutMs) : void 0);
|
|
214
|
+
const fail = async (detail) => {
|
|
215
|
+
const pending = await withdrawAsk(env, messageId, fetchImpl);
|
|
216
|
+
throw new Error(formatAskFailure(messageId, pending, detail));
|
|
217
|
+
};
|
|
218
|
+
try {
|
|
219
|
+
await postBroker(env, "/send", buildAgentMessage({
|
|
220
|
+
fromRunId: env.runId,
|
|
221
|
+
toRunId: args.toRunId,
|
|
222
|
+
messageId,
|
|
223
|
+
message: args.message,
|
|
224
|
+
role,
|
|
225
|
+
expectsReply: true
|
|
226
|
+
}), fetchImpl, signal);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (await sendNeverReachedBroker(error, env, fetchImpl)) throw new Error(formatAskFailure(messageId, false, `send ask: ${describeFailure(error)}`));
|
|
229
|
+
return fail(`send ask: ${describeFailure(error)}`);
|
|
230
|
+
}
|
|
231
|
+
let reply;
|
|
232
|
+
try {
|
|
233
|
+
reply = await postBroker(env, "/wait_reply", { waiting_for: messageId }, fetchImpl, signal);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return fail(`wait for reply: ${describeFailure(error)}`);
|
|
236
|
+
}
|
|
237
|
+
const record = reply && typeof reply === "object" ? reply : {};
|
|
238
|
+
return {
|
|
239
|
+
reply: extractReplyMessage(record),
|
|
240
|
+
from_run_id: record.from_run_id ?? ""
|
|
241
|
+
};
|
|
242
|
+
}
|
|
75
243
|
//#endregion
|
|
76
244
|
//#region src/watch.ts
|
|
77
245
|
const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
@@ -1426,33 +1594,9 @@ const defaultDeps = {
|
|
|
1426
1594
|
workflowReadyTool,
|
|
1427
1595
|
observeRun,
|
|
1428
1596
|
dial,
|
|
1597
|
+
attachOwner,
|
|
1429
1598
|
Supervisor
|
|
1430
1599
|
};
|
|
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
1600
|
function statusSupervisorId(requestedSupervisorId, spawnedSupervisorId) {
|
|
1457
1601
|
return requestedSupervisorId ? spawnedSupervisorId : void 0;
|
|
1458
1602
|
}
|
|
@@ -1695,7 +1839,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1695
1839
|
subAgentPollTimer = null;
|
|
1696
1840
|
}
|
|
1697
1841
|
}
|
|
1698
|
-
async function startSubAgentPoll(
|
|
1842
|
+
async function startSubAgentPoll() {
|
|
1699
1843
|
if (!subAgentEnv) return;
|
|
1700
1844
|
stopSubAgentPoll();
|
|
1701
1845
|
const generation = subAgentPollGeneration;
|
|
@@ -1704,6 +1848,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1704
1848
|
const tick = async () => {
|
|
1705
1849
|
try {
|
|
1706
1850
|
const msgs = await postBroker(subAgentEnv, "/poll-control", {}) ?? [];
|
|
1851
|
+
if (generation !== subAgentPollGeneration) return;
|
|
1707
1852
|
if (consecutiveFailures >= 3) console.error("[avenor] sub-agent broker poll recovered");
|
|
1708
1853
|
consecutiveFailures = 0;
|
|
1709
1854
|
for (const msg of msgs) {
|
|
@@ -1713,7 +1858,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1713
1858
|
from_run_id: ask.from_run_id,
|
|
1714
1859
|
message_id: ask.message_id
|
|
1715
1860
|
});
|
|
1716
|
-
|
|
1861
|
+
pi.sendUserMessage(formatInboundAskBody(ask), { deliverAs: "steer" });
|
|
1717
1862
|
}
|
|
1718
1863
|
} catch (error) {
|
|
1719
1864
|
consecutiveFailures++;
|
|
@@ -1726,6 +1871,40 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
1726
1871
|
subAgentPollTimer = setTimeout(() => void tick(), intervalMs);
|
|
1727
1872
|
subAgentPollTimer.unref?.();
|
|
1728
1873
|
}
|
|
1874
|
+
let hostPollTimer = null;
|
|
1875
|
+
let hostPollGeneration = 0;
|
|
1876
|
+
const hostPendingAsks = /* @__PURE__ */ new Map();
|
|
1877
|
+
function stopHostAskPoll() {
|
|
1878
|
+
hostPollGeneration++;
|
|
1879
|
+
if (hostPollTimer) {
|
|
1880
|
+
clearTimeout(hostPollTimer);
|
|
1881
|
+
hostPollTimer = null;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
function startHostAskPoll() {
|
|
1885
|
+
stopHostAskPoll();
|
|
1886
|
+
const generation = hostPollGeneration;
|
|
1887
|
+
const intervalMs = options.pollIntervalMs ?? 2e3;
|
|
1888
|
+
const tick = async () => {
|
|
1889
|
+
try {
|
|
1890
|
+
const { asks } = await deps.brokerReceiveTool({});
|
|
1891
|
+
if (generation !== hostPollGeneration) return;
|
|
1892
|
+
for (const ask of asks) {
|
|
1893
|
+
hostPendingAsks.set(ask.message_id, {
|
|
1894
|
+
from_run_id: ask.from_run_id,
|
|
1895
|
+
message_id: ask.message_id
|
|
1896
|
+
});
|
|
1897
|
+
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: "..." })`;
|
|
1898
|
+
pi.sendUserMessage(body, { deliverAs: "steer" });
|
|
1899
|
+
}
|
|
1900
|
+
} catch {}
|
|
1901
|
+
if (generation !== hostPollGeneration) return;
|
|
1902
|
+
hostPollTimer = setTimeout(() => void tick(), intervalMs);
|
|
1903
|
+
hostPollTimer.unref?.();
|
|
1904
|
+
};
|
|
1905
|
+
hostPollTimer = setTimeout(() => void tick(), intervalMs);
|
|
1906
|
+
hostPollTimer.unref?.();
|
|
1907
|
+
}
|
|
1729
1908
|
function recordPollingError(source, message, error, runId) {
|
|
1730
1909
|
pollingErrorCount++;
|
|
1731
1910
|
const payload = createPollErrorPayload({
|
|
@@ -2010,7 +2189,7 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2010
2189
|
}
|
|
2011
2190
|
async function withClient(run, work) {
|
|
2012
2191
|
if (!run.supervisorId || deps.Supervisor.isCurrentInstance(run.supervisorId)) return work((await deps.Supervisor.get()).getClient());
|
|
2013
|
-
const client = await deps.
|
|
2192
|
+
const client = await deps.attachOwner(run.supervisorId);
|
|
2014
2193
|
try {
|
|
2015
2194
|
return await work(client);
|
|
2016
2195
|
} finally {
|
|
@@ -2198,15 +2377,15 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2198
2377
|
lastStatusEntries = [];
|
|
2199
2378
|
pollingErrorCount = 0;
|
|
2200
2379
|
pollingErrors.length = 0;
|
|
2201
|
-
startHostAskPoll(
|
|
2202
|
-
startSubAgentPoll(
|
|
2380
|
+
if (!isSubAgentBrokerMode) startHostAskPoll();
|
|
2381
|
+
startSubAgentPoll();
|
|
2203
2382
|
});
|
|
2204
2383
|
pi.on("session_shutdown", async () => {
|
|
2205
|
-
await stopPolling();
|
|
2206
2384
|
stopHostAskPoll();
|
|
2207
2385
|
stopSubAgentPoll();
|
|
2208
2386
|
hostPendingAsks.clear();
|
|
2209
2387
|
subAgentPendingAsks.clear();
|
|
2388
|
+
await stopPolling();
|
|
2210
2389
|
for (const wait of [...pendingWaits]) {
|
|
2211
2390
|
pendingWaits.delete(wait);
|
|
2212
2391
|
wait.interrupt({
|
|
@@ -2735,15 +2914,27 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2735
2914
|
to_run_id: Type.String({ description: "Target run ID" }),
|
|
2736
2915
|
message: Type.String({ description: "Question to ask the other agent" }),
|
|
2737
2916
|
timeout_ms: Type.Optional(Type.Integer({ description: "Optional timeout in milliseconds for the ask" })),
|
|
2738
|
-
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
|
|
2917
|
+
supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path (ignored in a Pi sub-agent, which asks through its inherited broker)" }))
|
|
2739
2918
|
}),
|
|
2740
|
-
async execute(_toolCallId, params) {
|
|
2741
|
-
|
|
2919
|
+
async execute(_toolCallId, params, signal) {
|
|
2920
|
+
const result = isSubAgentBrokerMode && subAgentEnv ? await askViaBroker(subAgentEnv, {
|
|
2921
|
+
toRunId: params.to_run_id,
|
|
2922
|
+
message: params.message,
|
|
2923
|
+
timeoutMs: params.timeout_ms,
|
|
2924
|
+
signal
|
|
2925
|
+
}) : await askTool({
|
|
2742
2926
|
toRunId: params.to_run_id,
|
|
2743
2927
|
message: params.message,
|
|
2744
2928
|
timeoutMs: params.timeout_ms,
|
|
2745
2929
|
supervisorId: params.supervisor_id
|
|
2746
2930
|
});
|
|
2931
|
+
return {
|
|
2932
|
+
content: [{
|
|
2933
|
+
type: "text",
|
|
2934
|
+
text: `Reply from ${result.from_run_id || params.to_run_id}:\n${result.reply}`
|
|
2935
|
+
}],
|
|
2936
|
+
details: result
|
|
2937
|
+
};
|
|
2747
2938
|
}
|
|
2748
2939
|
});
|
|
2749
2940
|
pi.registerTool({
|
|
@@ -2766,23 +2957,19 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2766
2957
|
}],
|
|
2767
2958
|
details: { error: true }
|
|
2768
2959
|
};
|
|
2960
|
+
const generation = subAgentPollGeneration;
|
|
2769
2961
|
subAgentPendingAsks.delete(target.message_id);
|
|
2770
2962
|
try {
|
|
2771
|
-
await postBroker(subAgentEnv, "/send", {
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
message: params.message,
|
|
2780
|
-
role: "agent",
|
|
2781
|
-
reply_to: target.message_id
|
|
2782
|
-
}
|
|
2783
|
-
});
|
|
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
|
+
}));
|
|
2784
2971
|
} catch (error) {
|
|
2785
|
-
subAgentPendingAsks.set(target.message_id, target);
|
|
2972
|
+
if (generation === subAgentPollGeneration) subAgentPendingAsks.set(target.message_id, target);
|
|
2786
2973
|
throw error;
|
|
2787
2974
|
}
|
|
2788
2975
|
return { content: [{
|
|
@@ -2790,27 +2977,27 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2790
2977
|
text: "Reply sent"
|
|
2791
2978
|
}] };
|
|
2792
2979
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
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 {
|
|
2980
|
+
const target = resolveReplyTarget(hostPendingAsks, params.from_run_id, params.reply_to_message_id);
|
|
2981
|
+
if (!target) return {
|
|
2801
2982
|
content: [{
|
|
2802
2983
|
type: "text",
|
|
2803
2984
|
text: "No pending ask found to reply to. Pass from_run_id and/or reply_to_message_id."
|
|
2804
2985
|
}],
|
|
2805
2986
|
details: { error: true }
|
|
2806
2987
|
};
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
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
|
+
}
|
|
2814
3001
|
return { content: [{
|
|
2815
3002
|
type: "text",
|
|
2816
3003
|
text: "Reply sent"
|
|
@@ -2849,7 +3036,24 @@ function createExtension(deps = defaultDeps, options = {}) {
|
|
|
2849
3036
|
description: "Cancel a pending ask by its message ID.",
|
|
2850
3037
|
parameters: Type.Object({ message_id: Type.String({ description: "Message ID of the pending ask to cancel" }) }),
|
|
2851
3038
|
async execute(_toolCallId, params) {
|
|
2852
|
-
|
|
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 });
|
|
3050
|
+
return {
|
|
3051
|
+
content: [{
|
|
3052
|
+
type: "text",
|
|
3053
|
+
text: `Cancelled ${params.message_id}`
|
|
3054
|
+
}],
|
|
3055
|
+
details: result
|
|
3056
|
+
};
|
|
2853
3057
|
}
|
|
2854
3058
|
});
|
|
2855
3059
|
pi.registerTool({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dougbots/avenor-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dev": "tsdown --watch"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@dougbots/avenor-core": "^0.
|
|
38
|
+
"@dougbots/avenor-core": "^0.15.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@earendil-works/pi-coding-agent": ">=0.74.0",
|