@dougbots/avenor-pi 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +186 -44
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1393,6 +1393,9 @@ function renderWorkflowControllerStatusResult(result, options, theme, args) {
1393
1393
  }
1394
1394
  //#endregion
1395
1395
  //#region src/index.ts
1396
+ function asStatusResult(raw) {
1397
+ return raw;
1398
+ }
1396
1399
  const POLL_INTERVAL_MS = 3e3;
1397
1400
  const INSPECT_LIMIT = 128;
1398
1401
  const COMPLETION_OUTPUT_CHARS = 600;
@@ -1597,6 +1600,88 @@ function createExtension(deps = defaultDeps, options = {}) {
1597
1600
  let pollingErrorCount = 0;
1598
1601
  const pollingErrors = [];
1599
1602
  let lastStatusEntries = [];
1603
+ const pendingWaits = /* @__PURE__ */ new Set();
1604
+ function interruptPendingWaits(stop, excludeRunKey) {
1605
+ for (const wait of [...pendingWaits]) {
1606
+ if (wait.runKey === excludeRunKey) continue;
1607
+ pendingWaits.delete(wait);
1608
+ wait.interrupt(stop);
1609
+ }
1610
+ }
1611
+ function registerPendingWait(runKey, interrupt) {
1612
+ const wait = {
1613
+ runKey,
1614
+ interrupt
1615
+ };
1616
+ pendingWaits.add(wait);
1617
+ return wait;
1618
+ }
1619
+ function createInterruptibleWait(runKey, signal) {
1620
+ const controller = new AbortController();
1621
+ let stopped;
1622
+ const onSignalAbort = () => controller.abort();
1623
+ if (signal?.aborted) controller.abort();
1624
+ else signal?.addEventListener("abort", onSignalAbort);
1625
+ const pending = registerPendingWait(runKey, (stop) => {
1626
+ if (controller.signal.aborted) return;
1627
+ stopped = stop;
1628
+ controller.abort();
1629
+ });
1630
+ return {
1631
+ signal: controller.signal,
1632
+ get stopped() {
1633
+ return stopped;
1634
+ },
1635
+ dispose() {
1636
+ pendingWaits.delete(pending);
1637
+ signal?.removeEventListener("abort", onSignalAbort);
1638
+ }
1639
+ };
1640
+ }
1641
+ /** Build the early return for a wait interrupted by a sibling stopping
1642
+ * point. When the awaited run itself has since reached a stopping point
1643
+ * (possible when the caller omitted supervisor_id and the registry key
1644
+ * diverged), deliver its real state instead of an interruption notice. */
1645
+ async function resultAfterInterrupt(params, stop, tracked) {
1646
+ const runId = tracked?.runId ?? params.run_id;
1647
+ const supervisorId = tracked?.supervisorId ?? params.supervisor_id;
1648
+ let current;
1649
+ try {
1650
+ current = asStatusResult(await deps.statusTool({
1651
+ runId,
1652
+ supervisorId,
1653
+ view: "full"
1654
+ }));
1655
+ } catch {}
1656
+ if (current !== void 0 && (isTerminalStatus(current.status) || current.status === "waiting")) try {
1657
+ return {
1658
+ ...await deps.resultTool({
1659
+ runId,
1660
+ supervisorId,
1661
+ wait: true,
1662
+ timeout: params.timeout,
1663
+ signal: params.signal
1664
+ }),
1665
+ interrupted_by: {
1666
+ run_id: stop.runId,
1667
+ label: stop.label,
1668
+ status: stop.status
1669
+ }
1670
+ };
1671
+ } catch {}
1672
+ return {
1673
+ run_id: current?.run_id ?? runId,
1674
+ label: current?.label ?? runId,
1675
+ status: current?.status ?? "running",
1676
+ ready: current !== void 0 && isTerminalStatus(current.status),
1677
+ ...current?.pending_permission && { pending_permission: current.pending_permission },
1678
+ interrupted_by: {
1679
+ run_id: stop.runId,
1680
+ label: stop.label,
1681
+ status: stop.status
1682
+ }
1683
+ };
1684
+ }
1600
1685
  const subAgentEnv = options.subAgentEnv !== void 0 ? options.subAgentEnv : readSubAgentEnv();
1601
1686
  const isSubAgentBrokerMode = subAgentEnv !== null;
1602
1687
  let subAgentPollTimer = null;
@@ -1714,7 +1799,7 @@ function createExtension(deps = defaultDeps, options = {}) {
1714
1799
  const liveMap = /* @__PURE__ */ new Map();
1715
1800
  try {
1716
1801
  const allLive = await deps.statusTool({});
1717
- const liveList = Array.isArray(allLive) ? allLive : [allLive];
1802
+ const liveList = "runs" in allLive ? allLive.runs : [allLive];
1718
1803
  for (const result of liveList) liveMap.set(result.run_id, result);
1719
1804
  } catch (err) {
1720
1805
  recordPollingError("singleton-list", "failed to list singleton runs", err);
@@ -1744,11 +1829,10 @@ function createExtension(deps = defaultDeps, options = {}) {
1744
1829
  continue;
1745
1830
  }
1746
1831
  try {
1747
- const raw = await deps.statusTool({
1832
+ const result = asStatusResult(await deps.statusTool({
1748
1833
  runId: run.runId,
1749
1834
  supervisorId: run.supervisorId
1750
- });
1751
- const result = Array.isArray(raw) ? raw[0] : raw;
1835
+ }));
1752
1836
  if (!result) throw new Error("status response was empty");
1753
1837
  run.lastStatus = result;
1754
1838
  run.agent = result.agent ?? run.agent;
@@ -1831,7 +1915,10 @@ function createExtension(deps = defaultDeps, options = {}) {
1831
1915
  const tick = async () => {
1832
1916
  if (!pollingActive || generation !== pollingGeneration) return;
1833
1917
  const prevStatuses = /* @__PURE__ */ new Map();
1834
- for (const [id, run] of trackedRuns) prevStatuses.set(id, run.lastStatus?.status);
1918
+ for (const [id, run] of trackedRuns) prevStatuses.set(id, {
1919
+ status: run.lastStatus?.status,
1920
+ pendingPermission: !!run.lastStatus?.pending_permission
1921
+ });
1835
1922
  const currentPoll = pollRuns();
1836
1923
  pollInFlight = currentPoll;
1837
1924
  const entries = await currentPoll.finally(() => {
@@ -1844,13 +1931,19 @@ function createExtension(deps = defaultDeps, options = {}) {
1844
1931
  const entryKey = trackedRunKey(entry.supervisorId, entry.runId);
1845
1932
  const run = trackedRuns.get(entryKey);
1846
1933
  if (!run) continue;
1934
+ const prevStatus = prevStatuses.get(entryKey);
1935
+ const wasStopped = prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status) || !!prevStatus?.pendingPermission;
1936
+ if ((entry.pendingPermission || isTerminalStatus(entry.status)) && !wasStopped) interruptPendingWaits({
1937
+ runId: entry.runId,
1938
+ label: entry.label,
1939
+ status: isTerminalStatus(entry.status) ? entry.status : "waiting"
1940
+ }, entryKey);
1847
1941
  if (isTerminalStatus(entry.status)) {
1848
1942
  if (decideCompletion(run) === "skip") {
1849
1943
  if (run.consumed) trackedRuns.delete(entryKey);
1850
1944
  continue;
1851
1945
  }
1852
- const prevStatus = prevStatuses.get(entryKey);
1853
- if (!(prevStatus && isTerminalStatus(prevStatus))) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
1946
+ if (!(prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status))) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
1854
1947
  const finalOutput = await resolveFinalOutput(run, run.lastStatus);
1855
1948
  try {
1856
1949
  pi.sendUserMessage(buildCompletionText({
@@ -1970,11 +2063,10 @@ function createExtension(deps = defaultDeps, options = {}) {
1970
2063
  supervisorId: run.supervisorId
1971
2064
  });
1972
2065
  try {
1973
- const rawStatus = await deps.statusTool({
2066
+ const status = asStatusResult(await deps.statusTool({
1974
2067
  runId: result.run_id,
1975
2068
  supervisorId: run.supervisorId
1976
- });
1977
- const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
2069
+ }));
1978
2070
  upsertTrackedRun({
1979
2071
  runId: result.run_id,
1980
2072
  label: result.label,
@@ -2052,12 +2144,11 @@ function createExtension(deps = defaultDeps, options = {}) {
2052
2144
  await observer?.close().catch(() => {});
2053
2145
  if (outcome === "aborted") return { aborted: true };
2054
2146
  try {
2055
- const raw = await deps.statusTool({
2056
- runId: run.runId,
2057
- supervisorId: run.supervisorId
2058
- });
2059
2147
  return {
2060
- status: Array.isArray(raw) ? raw[0] : raw,
2148
+ status: asStatusResult(await deps.statusTool({
2149
+ runId: run.runId,
2150
+ supervisorId: run.supervisorId
2151
+ })),
2061
2152
  aborted: false
2062
2153
  };
2063
2154
  } catch {
@@ -2070,11 +2161,10 @@ function createExtension(deps = defaultDeps, options = {}) {
2070
2161
  if (!firstPoll) await sleep(pollIntervalMs);
2071
2162
  firstPoll = false;
2072
2163
  try {
2073
- const raw = await deps.statusTool({
2164
+ const status = asStatusResult(await deps.statusTool({
2074
2165
  runId: run.runId,
2075
2166
  supervisorId: run.supervisorId
2076
- });
2077
- const status = Array.isArray(raw) ? raw[0] : raw;
2167
+ }));
2078
2168
  if (!status) continue;
2079
2169
  const text = status.pending_permission ? `[blocked] ${run.label ?? run.runId} — permission: ${status.pending_permission.description}` : `${run.label ?? run.runId}${status.phase_label ? ` (${status.phase_label})` : ""}`;
2080
2170
  onUpdate?.({
@@ -2116,6 +2206,14 @@ function createExtension(deps = defaultDeps, options = {}) {
2116
2206
  stopSubAgentPoll();
2117
2207
  hostPendingAsks.clear();
2118
2208
  subAgentPendingAsks.clear();
2209
+ for (const wait of [...pendingWaits]) {
2210
+ pendingWaits.delete(wait);
2211
+ wait.interrupt({
2212
+ runId: "",
2213
+ status: "shutdown",
2214
+ synthetic: true
2215
+ });
2216
+ }
2119
2217
  sessionCtx = null;
2120
2218
  });
2121
2219
  pi.on("before_agent_start", async () => {
@@ -2144,12 +2242,12 @@ function createExtension(deps = defaultDeps, options = {}) {
2144
2242
  pi.registerTool({
2145
2243
  name: "avenor_spawn",
2146
2244
  label: "Avenor Spawn",
2147
- 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.`,
2245
+ 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.`,
2148
2246
  promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, result, inspect, events, follow-up, shutdown)",
2149
2247
  promptGuidelines: [
2150
2248
  "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
2151
2249
  "Use avenor_status (view \"lifecycle\") only to check progress or pending permissions.",
2152
- "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.",
2250
+ "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.",
2153
2251
  "Use avenor_inspect to review a bounded transcript and tool activity from a run.",
2154
2252
  "Use avenor_events only when you specifically need raw event payloads.",
2155
2253
  "Use avenor_follow_up to iterate on a completed run.",
@@ -2251,21 +2349,56 @@ function createExtension(deps = defaultDeps, options = {}) {
2251
2349
  run_id: result.run_id
2252
2350
  }
2253
2351
  });
2254
- const waitResult = await waitForRun(runRef, signal, onUpdate);
2352
+ const blockingWait = createInterruptibleWait(trackedRunKey(supervisorId, result.run_id), signal);
2353
+ let waitResult;
2354
+ try {
2355
+ waitResult = await waitForRun(runRef, blockingWait.signal, onUpdate);
2356
+ } finally {
2357
+ blockingWait.dispose();
2358
+ }
2359
+ let waitStatus = waitResult.status;
2255
2360
  if (waitResult.aborted) {
2256
- trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
2257
- return {
2258
- content: [{
2259
- type: "text",
2260
- text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status or avenor_inspect to check.`
2261
- }],
2262
- details: {
2263
- run_id: result.run_id,
2264
- ...spawnIdentityMetadata(hostParams)
2361
+ const stop = blockingWait.stopped;
2362
+ if (stop && !stop.synthetic) {
2363
+ const probed = asStatusResult(await deps.statusTool({
2364
+ runId: result.run_id,
2365
+ supervisorId
2366
+ }).catch(() => void 0));
2367
+ if (probed && (isTerminalStatus(probed.status) || probed.status === "waiting")) waitStatus = probed;
2368
+ else {
2369
+ const tracked = getTrackedRun(result.run_id, supervisorId);
2370
+ if (tracked) tracked.blocking = false;
2371
+ return {
2372
+ content: [{
2373
+ type: "text",
2374
+ text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted: "${stop.label ?? stop.runId}" reached ${stop.status === "waiting" ? "a permission request" : stop.status}. Handle that first; this run's final result is delivered automatically.`
2375
+ }],
2376
+ details: {
2377
+ run_id: result.run_id,
2378
+ interrupted_by: {
2379
+ run_id: stop.runId,
2380
+ label: stop.label,
2381
+ status: stop.status
2382
+ },
2383
+ ...spawnIdentityMetadata(hostParams)
2384
+ }
2385
+ };
2265
2386
  }
2266
- };
2387
+ } else {
2388
+ trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
2389
+ return {
2390
+ content: [{
2391
+ type: "text",
2392
+ text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status or avenor_inspect to check.`
2393
+ }],
2394
+ details: {
2395
+ run_id: result.run_id,
2396
+ ...spawnIdentityMetadata(hostParams)
2397
+ }
2398
+ };
2399
+ }
2267
2400
  }
2268
- const status = waitResult.status;
2401
+ const status = waitStatus;
2269
2402
  if (!status) return {
2270
2403
  content: [{
2271
2404
  type: "text",
@@ -2404,7 +2537,7 @@ function createExtension(deps = defaultDeps, options = {}) {
2404
2537
  pi.registerTool({
2405
2538
  name: "avenor_result",
2406
2539
  label: "Avenor Result",
2407
- 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.",
2540
+ 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.`,
2408
2541
  parameters: Type.Object({
2409
2542
  run_id: Type.String({ description: "Run ID or label" }),
2410
2543
  wait: Type.Optional(Type.Boolean({ description: "Wait for a terminal result (default true); false retrieves the current state without blocking" })),
@@ -2416,14 +2549,21 @@ function createExtension(deps = defaultDeps, options = {}) {
2416
2549
  const previousBlocking = tracked?.blocking;
2417
2550
  if (tracked) tracked.blocking = true;
2418
2551
  let completed = false;
2552
+ const blockingWait = params.wait ?? true ? createInterruptibleWait(trackedRunKey(tracked?.supervisorId ?? params.supervisor_id, tracked?.runId ?? params.run_id), signal) : void 0;
2419
2553
  try {
2420
- const result = await deps.resultTool({
2421
- runId: params.run_id,
2422
- supervisorId: params.supervisor_id,
2423
- wait: params.wait,
2424
- timeout: params.timeout,
2425
- signal
2426
- });
2554
+ let result;
2555
+ try {
2556
+ result = await deps.resultTool({
2557
+ runId: params.run_id,
2558
+ supervisorId: params.supervisor_id,
2559
+ wait: params.wait,
2560
+ timeout: params.timeout,
2561
+ signal: blockingWait?.signal ?? signal
2562
+ });
2563
+ } catch (err) {
2564
+ if (!blockingWait?.stopped || blockingWait.stopped.synthetic) throw err;
2565
+ result = await resultAfterInterrupt(params, blockingWait.stopped, tracked);
2566
+ }
2427
2567
  completed = result.ready;
2428
2568
  if (completed) {
2429
2569
  if (tracked) tracked.consumed = true;
@@ -2445,6 +2585,7 @@ function createExtension(deps = defaultDeps, options = {}) {
2445
2585
  details: result
2446
2586
  };
2447
2587
  } finally {
2588
+ blockingWait?.dispose();
2448
2589
  if (!completed && tracked && getTrackedRun(params.run_id, params.supervisor_id) === tracked) tracked.blocking = previousBlocking ?? false;
2449
2590
  }
2450
2591
  },
@@ -2548,11 +2689,10 @@ function createExtension(deps = defaultDeps, options = {}) {
2548
2689
  supervisorId: params.supervisor_id
2549
2690
  });
2550
2691
  try {
2551
- const rawStatus = await deps.statusTool({
2692
+ const status = asStatusResult(await deps.statusTool({
2552
2693
  runId: result.run_id,
2553
2694
  supervisorId: params.supervisor_id
2554
- });
2555
- const status = Array.isArray(rawStatus) ? rawStatus[0] : rawStatus;
2695
+ }));
2556
2696
  upsertTrackedRun({
2557
2697
  runId: result.run_id,
2558
2698
  label: result.label,
@@ -2589,16 +2729,18 @@ function createExtension(deps = defaultDeps, options = {}) {
2589
2729
  pi.registerTool({
2590
2730
  name: "avenor_ask",
2591
2731
  label: "Avenor Ask",
2592
- description: "Ask another agent run a question and block until it replies.",
2732
+ description: "Ask another agent run a question and block until it replies. An optional timeout_ms bounds the wait; a failed ask reports its message_id and whether it is still pending (withdrawable via avenor_cancel) or already withdrawn.",
2593
2733
  parameters: Type.Object({
2594
2734
  to_run_id: Type.String({ description: "Target run ID" }),
2595
2735
  message: Type.String({ description: "Question to ask the other agent" }),
2736
+ timeout_ms: Type.Optional(Type.Integer({ description: "Optional timeout in milliseconds for the ask" })),
2596
2737
  supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
2597
2738
  }),
2598
2739
  async execute(_toolCallId, params) {
2599
2740
  return askTool({
2600
2741
  toRunId: params.to_run_id,
2601
2742
  message: params.message,
2743
+ timeoutMs: params.timeout_ms,
2602
2744
  supervisorId: params.supervisor_id
2603
2745
  });
2604
2746
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-pi",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
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.12.0"
38
+ "@dougbots/avenor-core": "^0.12.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@earendil-works/pi-coding-agent": ">=0.74.0",