@dougbots/avenor-pi 0.14.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 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
- async function postBroker(env, path, body, fetchImpl = fetch) {
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
- ...body
51
- })
80
+ token: env.token
81
+ }),
82
+ ...signal ? { signal } : {}
52
83
  });
53
- if (!res.ok) throw new Error(`broker ${path}: ${res.status} ${(await res.text()).trim()}`);
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(ctx) {
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
- await ctx.sendUserMessage(formatInboundAskBody(ask), { deliverAs: "steer" });
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.dial(run.supervisorId);
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(ctx);
2202
- startSubAgentPoll(ctx);
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
- return askTool({
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
- 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
- });
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
- 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 {
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
- await deps.brokerReplyTool({
2808
- toRunId: fromRunId,
2809
- replyTo: messageId,
2810
- message: params.message,
2811
- supervisorId: params.supervisor_id
2812
- });
2813
- hostPendingAsks.delete(messageId);
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
- return cancelTool({ messageId: params.message_id });
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.14.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.14.0"
38
+ "@dougbots/avenor-core": "^0.15.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": ">=0.74.0",