@dougbots/avenor-core 0.4.1 → 0.5.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.
package/README.md CHANGED
@@ -59,11 +59,24 @@ Reports status for one or all runs. When `runId` is omitted it lists every run a
59
59
  ```ts
60
60
  import { statusTool } from '@dougbots/avenor-core'
61
61
 
62
- const status = await statusTool({ runId: 'reverse-string' })
62
+ const status = await statusTool({ runId: 'reverse-string', view: 'lifecycle' })
63
63
  // { run_id: 'reverse-string', label: 'reverse-string', status: 'running', ... }
64
64
  ```
65
65
 
66
- Status values: `running`, `done`, `failed`, `timeout`, `killed`. If a permission request is pending, the result includes `pending_permission` with available options.
66
+ Status values: `running`, `done`, `failed`, `timeout`, `killed`. If a permission request is pending, the result includes `pending_permission` with available options. The `lifecycle` view omits final output and diagnostic metadata; `full` remains the default for compatibility.
67
+
68
+ ### `resultTool`
69
+
70
+ Waits for one run to finish and returns its bounded final output without transcript or event details.
71
+
72
+ ```ts
73
+ import { resultTool } from '@dougbots/avenor-core'
74
+
75
+ const result = await resultTool({ runId: 'reverse-string', timeout: '5m' })
76
+ // { run_id: 'reverse-string', status: 'done', ready: true, output: '...' }
77
+ ```
78
+
79
+ Set `wait: false` for a non-blocking availability check. A pending permission returns immediately; an expired result wait returns `ready: false` and `timed_out: true` without stopping the run.
67
80
 
68
81
  ### `answerPermissionTool`
69
82
 
@@ -113,7 +126,7 @@ const result = await shutdownTool({ force: false })
113
126
  ## Quick start
114
127
 
115
128
  ```ts
116
- import { Supervisor, spawnTool, statusTool, shutdownTool } from '@dougbots/avenor-core'
129
+ import { Supervisor, resultTool, spawnTool, shutdownTool } from '@dougbots/avenor-core'
117
130
 
118
131
  // 1. The supervisor auto-starts avenor. Call get() early — it's a singleton.
119
132
  const sup = await Supervisor.get()
@@ -125,13 +138,9 @@ const { run_id } = await spawnTool({
125
138
  label: 'security-review',
126
139
  })
127
140
 
128
- // 3. Poll until complete
129
- let state = await statusTool({ runId: run_id })
130
- while (state.status === 'running' || state.status === undefined) {
131
- await new Promise(r => setTimeout(r, 2000))
132
- state = await statusTool({ runId: run_id })
133
- }
134
- console.log(state) // { run_id, label, status: 'done', ... }
141
+ // 3. Wait for the bounded final output
142
+ const result = await resultTool({ runId: run_id })
143
+ console.log(result) // { run_id, label, status: 'done', ready: true, output: '...' }
135
144
 
136
145
  // 4. Clean up
137
146
  await shutdownTool()
@@ -146,7 +155,8 @@ await shutdownTool()
146
155
  | `Supervisor` | class | Singleton avenor process lifecycle manager |
147
156
  | `findAvenorBinary` | function | Discover the avenor binary path |
148
157
  | `spawnTool` | function | Start a new agent run |
149
- | `statusTool` | function | Query run status (single or all) |
158
+ | `statusTool` | function | Query run lifecycle status (single or all) |
159
+ | `resultTool` | function | Wait for and retrieve one run's bounded final output |
150
160
  | `answerPermissionTool` | function | Answer a pending permission request |
151
161
  | `followUpTool` | function | Resume a session with a follow-up prompt |
152
162
  | `eventsTool` | function | Read recent events from a run's event log |
package/dist/index.d.ts CHANGED
@@ -289,6 +289,7 @@ declare function spawnTool(args: {
289
289
  }>;
290
290
  //#endregion
291
291
  //#region src/tools/status.d.ts
292
+ type StatusView = 'lifecycle' | 'full';
292
293
  interface StatusResult {
293
294
  run_id: string;
294
295
  label: string;
@@ -321,8 +322,51 @@ interface StatusResult {
321
322
  declare function statusTool(args: {
322
323
  runId?: string;
323
324
  supervisorId?: string;
325
+ view?: StatusView;
324
326
  }): Promise<StatusResult | StatusResult[]>;
325
327
  //#endregion
328
+ //#region src/tools/inspect.d.ts
329
+ interface InspectResult {
330
+ run_id: string;
331
+ label: string;
332
+ status: StatusResult;
333
+ snapshot: Omit<RunSnapshot, '_seen_seq_by_scope' | '_recent_legacy_keys' | '_last_semantic_fingerprint' | '_last_source_event'>;
334
+ transcript: RunSnapshot['transcript'];
335
+ tools: RunSnapshot['tools'];
336
+ live_tools: RunSnapshot['live_tools'];
337
+ permissions: RunSnapshot['permissions'];
338
+ pending_permission?: RunSnapshot['pending_permission'];
339
+ final_output?: string;
340
+ }
341
+ declare function inspectTool(args: {
342
+ runId: string;
343
+ supervisorId?: string;
344
+ limit?: number;
345
+ after_seq?: number;
346
+ }): Promise<InspectResult>;
347
+ //#endregion
348
+ //#region src/tools/result.d.ts
349
+ interface ResultResult {
350
+ run_id: string;
351
+ label: string;
352
+ status: string;
353
+ ready: boolean;
354
+ runtime_id?: string;
355
+ session_id?: string;
356
+ stop_reason?: string;
357
+ pending_permission?: StatusResult['pending_permission'];
358
+ output?: string;
359
+ timed_out?: boolean;
360
+ }
361
+ interface ResultToolArgs {
362
+ runId: string;
363
+ supervisorId?: string;
364
+ wait?: boolean;
365
+ timeout?: string;
366
+ signal?: AbortSignal;
367
+ }
368
+ declare function resultTool(args: ResultToolArgs): Promise<ResultResult>;
369
+ //#endregion
326
370
  //#region src/tools/answer-permission.d.ts
327
371
  declare function answerPermissionTool(args: {
328
372
  runId: string;
@@ -360,26 +404,6 @@ declare function eventsTool(args: {
360
404
  }>;
361
405
  }>;
362
406
  //#endregion
363
- //#region src/tools/inspect.d.ts
364
- interface InspectResult {
365
- run_id: string;
366
- label: string;
367
- status: StatusResult;
368
- snapshot: Omit<RunSnapshot, '_seen_seq_by_scope' | '_recent_legacy_keys' | '_last_semantic_fingerprint' | '_last_source_event'>;
369
- transcript: RunSnapshot['transcript'];
370
- tools: RunSnapshot['tools'];
371
- live_tools: RunSnapshot['live_tools'];
372
- permissions: RunSnapshot['permissions'];
373
- pending_permission?: RunSnapshot['pending_permission'];
374
- final_output?: string;
375
- }
376
- declare function inspectTool(args: {
377
- runId: string;
378
- supervisorId?: string;
379
- limit?: number;
380
- after_seq?: number;
381
- }): Promise<InspectResult>;
382
- //#endregion
383
407
  //#region src/tools/shutdown.d.ts
384
408
  declare function shutdownTool(args: {
385
409
  supervisorId?: string;
@@ -389,4 +413,4 @@ declare function shutdownTool(args: {
389
413
  cleaned_up: string[];
390
414
  }>;
391
415
  //#endregion
392
- export { type Client, DEFAULT_RUN_REDUCER_LIMITS, type Event, type HistoryOptions, type HistoryResult, type InspectResult, type NormalizedRunEvent, type ObserveRunOptions, type RunChunkRole, type RunIdentity, type RunInfo, type RunObserver, type RunObserverClient, type RunPermissionOption, type RunPermissionSummary, RunReducer, type RunReducerLimits, type RunSnapshot, type RunSnapshotSeed, type RunToolState, type RunToolSummary, type RunTranscriptEntry, type SpawnParams, type StatusResult, type SubscribeOptions, Supervisor, type SupervisorOptions, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, normalizeRunEvent, observeRun, reduceRunSnapshot, shutdownTool, spawnTool, statusTool };
416
+ export { type Client, DEFAULT_RUN_REDUCER_LIMITS, type Event, type HistoryOptions, type HistoryResult, type InspectResult, type NormalizedRunEvent, type ObserveRunOptions, type ResultResult, type ResultToolArgs, type RunChunkRole, type RunIdentity, type RunInfo, type RunObserver, type RunObserverClient, type RunPermissionOption, type RunPermissionSummary, RunReducer, type RunReducerLimits, type RunSnapshot, type RunSnapshotSeed, type RunToolState, type RunToolSummary, type RunTranscriptEntry, type SpawnParams, type StatusResult, type StatusView, type SubscribeOptions, Supervisor, type SupervisorOptions, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, normalizeRunEvent, observeRun, reduceRunSnapshot, resultTool, shutdownTool, spawnTool, statusTool };
package/dist/index.js CHANGED
@@ -1479,7 +1479,7 @@ function extractPendingPermission(liveStatus) {
1479
1479
  }
1480
1480
  function buildBaseStatus(source, fallback, translatedStatus) {
1481
1481
  return {
1482
- run_id: stringField(source, "run_id") ?? fallback.run_id,
1482
+ run_id: fallback.prefer_fallback_run_id ? fallback.run_id : stringField(source, "run_id") ?? fallback.run_id,
1483
1483
  label: stringField(source, "label") ?? fallback.label,
1484
1484
  status: translatedStatus,
1485
1485
  runtime_id: stringField(source, "runtime_id", "id") ?? fallback.runtime_id,
@@ -1500,6 +1500,20 @@ function buildBaseStatus(source, fallback, translatedStatus) {
1500
1500
  final_output: stringField(source, "final_output", "finalOutput")
1501
1501
  };
1502
1502
  }
1503
+ function shapeStatusResult(result, view = "full") {
1504
+ if (view === "full") return result;
1505
+ if (view !== "lifecycle") throw new Error(`invalid status view: ${String(view)}`);
1506
+ return {
1507
+ run_id: result.run_id,
1508
+ label: result.label,
1509
+ status: result.status,
1510
+ ...result.runtime_id !== void 0 && { runtime_id: result.runtime_id },
1511
+ ...result.phase !== void 0 && { phase: result.phase },
1512
+ ...result.phase_label !== void 0 && { phase_label: result.phase_label },
1513
+ ...result.pending_permission !== void 0 && { pending_permission: result.pending_permission },
1514
+ ...result.latest_seq !== void 0 && { latest_seq: result.latest_seq }
1515
+ };
1516
+ }
1503
1517
  async function buildRunStatus(runInfo, liveStatus) {
1504
1518
  let sentinel = null;
1505
1519
  if (await sentinelExists(runInfo.sentinelPath)) sentinel = await parseSentinel$1(runInfo.sentinelPath);
@@ -1507,6 +1521,7 @@ async function buildRunStatus(runInfo, liveStatus) {
1507
1521
  return buildBaseStatus(liveStatus ?? {}, {
1508
1522
  run_id: runInfo.runId,
1509
1523
  label: runInfo.label,
1524
+ prefer_fallback_run_id: true,
1510
1525
  runtime_id: runInfo.runtimeId,
1511
1526
  session_id: sentinel?.SESSION ?? liveStatus?.session_id ?? runInfo.sessionId,
1512
1527
  stop_reason: sentinel?._status
@@ -1527,7 +1542,7 @@ async function statusTool(args) {
1527
1542
  if (args.runId) {
1528
1543
  validateRunId(args.runId);
1529
1544
  const runInfo = isSingleton && sup ? findRunByLabel$3(sup, args.runId) : void 0;
1530
- if (runInfo) return buildRunStatus(runInfo, await queryLiveStatus(client, runInfo.runtimeId));
1545
+ if (runInfo) return shapeStatusResult(await buildRunStatus(runInfo, await queryLiveStatus(client, runInfo.runtimeId)), args.view);
1531
1546
  let liveStatus = await queryLiveStatus(client, args.runId);
1532
1547
  if (!liveStatus) try {
1533
1548
  const entry = (await client.list()).find((item) => {
@@ -1541,20 +1556,20 @@ async function statusTool(args) {
1541
1556
  const sentinelPath = liveStatus?.sentinel_file ?? path.join(runsRoot(), args.runId, "sentinel.done");
1542
1557
  let sentinel = null;
1543
1558
  if (await sentinelExists(sentinelPath)) sentinel = await parseSentinel$1(sentinelPath);
1544
- return buildBaseStatus(liveStatus ?? {}, {
1559
+ return shapeStatusResult(buildBaseStatus(liveStatus ?? {}, {
1545
1560
  run_id: args.runId,
1546
1561
  label: args.runId,
1547
1562
  runtime_id: stringField(liveStatus ?? {}, "runtime_id", "id"),
1548
1563
  session_id: sentinel?.SESSION ?? liveStatus?.session_id,
1549
1564
  stop_reason: sentinel?._status
1550
- }, translateStatus(liveStatus?.phase ?? liveStatus?.status ?? "running", sentinel));
1565
+ }, translateStatus(liveStatus?.phase ?? liveStatus?.status ?? "running", sentinel)), args.view);
1551
1566
  }
1552
- return (await client.list()).map((entry) => buildBaseStatus(entry, {
1567
+ return (await client.list()).map((entry) => shapeStatusResult(buildBaseStatus(entry, {
1553
1568
  run_id: String(entry.run_id ?? entry.runtime_id ?? entry.id ?? ""),
1554
1569
  label: String(entry.label ?? entry.runtime_id ?? entry.id ?? ""),
1555
1570
  runtime_id: String(entry.runtime_id ?? entry.id ?? ""),
1556
1571
  session_id: entry.session_id
1557
- }, translateStatus(entry.phase ?? entry.status ?? "running", null)));
1572
+ }, translateStatus(entry.phase ?? entry.status ?? "running", null)), args.view));
1558
1573
  } finally {
1559
1574
  if (!isSingleton) client.close();
1560
1575
  }
@@ -1563,19 +1578,19 @@ async function statusTool(args) {
1563
1578
  const client = sup.getClient();
1564
1579
  if (args.runId) {
1565
1580
  const runInfo = findRunByLabel$3(sup, args.runId);
1566
- if (runInfo) return buildRunStatus(runInfo, await queryLiveStatus(client, runInfo.runtimeId));
1581
+ if (runInfo) return shapeStatusResult(await buildRunStatus(runInfo, await queryLiveStatus(client, runInfo.runtimeId)), args.view);
1567
1582
  validateRunId(args.runId);
1568
1583
  const liveStatus = await queryLiveStatus(client, args.runId);
1569
1584
  const sentinelPath = path.join(runsRoot(), args.runId, "sentinel.done");
1570
1585
  let sentinel = null;
1571
1586
  if (await sentinelExists(sentinelPath)) sentinel = await parseSentinel$1(sentinelPath);
1572
- return buildBaseStatus(liveStatus ?? {}, {
1587
+ return shapeStatusResult(buildBaseStatus(liveStatus ?? {}, {
1573
1588
  run_id: args.runId,
1574
1589
  label: args.runId,
1575
1590
  runtime_id: stringField(liveStatus ?? {}, "runtime_id", "id"),
1576
1591
  session_id: sentinel?.SESSION ?? liveStatus?.session_id,
1577
1592
  stop_reason: sentinel?._status
1578
- }, translateStatus(liveStatus?.phase ?? liveStatus?.status ?? "running", sentinel));
1593
+ }, translateStatus(liveStatus?.phase ?? liveStatus?.status ?? "running", sentinel)), args.view);
1579
1594
  }
1580
1595
  const list = await client.list();
1581
1596
  const results = [];
@@ -1584,134 +1599,23 @@ async function statusTool(args) {
1584
1599
  const entryLabel = String(entry.label ?? entryId);
1585
1600
  const runInfo = findRunByLabel$3(sup, entryId) ?? findRunByLabel$3(sup, entryLabel);
1586
1601
  const liveStatus = runInfo ? await queryLiveStatus(client, runInfo.runtimeId) : null;
1587
- if (runInfo && liveStatus) results.push(await buildRunStatus(runInfo, liveStatus));
1588
- else results.push(buildBaseStatus(entry, {
1602
+ if (runInfo && liveStatus) results.push(shapeStatusResult(await buildRunStatus(runInfo, liveStatus), args.view));
1603
+ else results.push(shapeStatusResult(buildBaseStatus(entry, {
1589
1604
  run_id: String(entry.run_id ?? entryId),
1590
1605
  label: entryLabel,
1591
1606
  runtime_id: entryId,
1592
1607
  session_id: entry.session_id
1593
- }, translateStatus(entry.phase ?? entry.status ?? "running", null)));
1608
+ }, translateStatus(entry.phase ?? entry.status ?? "running", null)), args.view));
1594
1609
  }
1595
1610
  return results;
1596
1611
  }
1597
1612
  //#endregion
1598
- //#region src/tools/answer-permission.ts
1599
- function findRunByLabel$2(sup, runId) {
1600
- const runs = sup.runs;
1601
- const byKey = runs.get(runId);
1602
- if (byKey) return byKey;
1603
- for (const info of runs.values()) if (info.label === runId) return info;
1604
- }
1605
- async function answerPermissionTool(args) {
1606
- let client;
1607
- let sup = null;
1608
- let requestId = args.requestId;
1609
- let supervisorId;
1610
- let isSingleton = false;
1611
- if (args.supervisorId) ({client, isSingleton, sup, supervisorId} = await getSupervisorClient(args.supervisorId));
1612
- else {
1613
- sup = await Supervisor.get();
1614
- client = sup.getClient();
1615
- }
1616
- try {
1617
- validateRunId(args.runId);
1618
- const runtimeId = (sup ? findRunByLabel$2(sup, args.runId) : void 0)?.runtimeId ?? args.runId;
1619
- if (!requestId) {
1620
- const pp = (await client.status(runtimeId))?.pending_permission;
1621
- if (!pp?.request_id) throw new Error("no pending permission request for this run");
1622
- requestId = pp.request_id;
1623
- }
1624
- await client.answerPermission(runtimeId, requestId, args.optionId);
1625
- return { ok: true };
1626
- } finally {
1627
- if (supervisorId && !isSingleton) client.close();
1628
- }
1629
- }
1630
- //#endregion
1631
- //#region src/tools/follow-up.ts
1632
- function findRunByLabel$1(sup, runId) {
1633
- const runs = sup.runs;
1634
- const byKey = runs.get(runId);
1635
- if (byKey) return byKey;
1636
- for (const info of runs.values()) if (info.label === runId) return info;
1637
- }
1638
- async function parseSentinel(filePath) {
1639
- try {
1640
- const content = (await fs.promises.readFile(filePath, "utf-8")).trim();
1641
- if (!content) return null;
1642
- const lines = content.split("\n");
1643
- const result = {};
1644
- result._status = lines[0];
1645
- for (let i = 1; i < lines.length; i++) {
1646
- const line = lines[i].trim();
1647
- if (!line) continue;
1648
- const eq = line.indexOf("=");
1649
- if (eq > 0) result[line.substring(0, eq)] = line.substring(eq + 1);
1650
- }
1651
- return result;
1652
- } catch {
1653
- return null;
1654
- }
1655
- }
1656
- async function followUpTool(args) {
1657
- if (args.supervisorId) {
1658
- const { client, isSingleton } = await getSupervisorClient(args.supervisorId);
1659
- try {
1660
- validateRunId(args.runId);
1661
- let liveStatus = null;
1662
- try {
1663
- liveStatus = await client.status(args.runId);
1664
- } catch {}
1665
- const sentinel = await parseSentinel(liveStatus?.sentinel_file ?? path.join(runsRoot(), args.runId, "sentinel.done"));
1666
- if (!sentinel?.SESSION) throw new Error("run has no session to resume");
1667
- const agent = liveStatus?.agent ?? "codex";
1668
- const followUpRunId = crypto.randomUUID();
1669
- const followUpLabel = args.label ?? `${args.runId}-followup`;
1670
- const { sentinelPath: followUpSentinelPath, eventLogPath } = ensureRunPaths(followUpRunId);
1671
- return {
1672
- run_id: (await client.spawn({
1673
- agent,
1674
- prompt: args.message,
1675
- label: followUpLabel,
1676
- session_id: sentinel.SESSION,
1677
- sentinel_file: followUpSentinelPath,
1678
- on_event: eventLogPath
1679
- })).runtime_id ?? followUpRunId,
1680
- label: followUpLabel
1681
- };
1682
- } finally {
1683
- if (!isSingleton) client.close();
1684
- }
1685
- }
1686
- const sup = await Supervisor.get();
1687
- const runInfo = findRunByLabel$1(sup, args.runId);
1688
- if (!runInfo) throw new Error(`run not found: ${args.runId}`);
1689
- const sentinel = await parseSentinel(runInfo.sentinelPath);
1690
- if (!sentinel?.SESSION) throw new Error("run has no session to resume");
1691
- const client = sup.getClient();
1692
- let agent = "codex";
1693
- if (runInfo.runtimeId) try {
1694
- agent = (await client.status(runInfo.runtimeId))?.agent ?? "codex";
1695
- } catch {}
1696
- const followUpLabel = args.label ?? `${runInfo.label}-followup`;
1697
- const followUpRun = await sup.spawn({
1698
- agent,
1699
- prompt: args.message,
1700
- session_id: sentinel.SESSION,
1701
- label: followUpLabel
1702
- });
1703
- return {
1704
- run_id: followUpRun.runId,
1705
- label: followUpRun.label
1706
- };
1707
- }
1708
- //#endregion
1709
1613
  //#region src/tools/events.ts
1710
1614
  const MAX_EVENT_LIMIT = 1e3;
1711
1615
  const MAX_STRING_CHARS = 4e3;
1712
1616
  const MAX_ARRAY_ITEMS = 32;
1713
1617
  const MAX_OBJECT_KEYS = 64;
1714
- function findRunByLabel(sup, runId) {
1618
+ function findRunByLabel$2(sup, runId) {
1715
1619
  const runs = sup.runs;
1716
1620
  const byKey = runs.get(runId);
1717
1621
  if (byKey) return byKey;
@@ -1787,7 +1691,7 @@ async function readNdjsonEvents(filePath, args) {
1787
1691
  async function resolveRun(args) {
1788
1692
  if (args.supervisorId) {
1789
1693
  const { client, isSingleton, sup } = await getSupervisorClient(args.supervisorId);
1790
- const runInfo = isSingleton && sup ? findRunByLabel(sup, args.runId) : void 0;
1694
+ const runInfo = isSingleton && sup ? findRunByLabel$2(sup, args.runId) : void 0;
1791
1695
  if (runInfo) return {
1792
1696
  client,
1793
1697
  isSingleton,
@@ -1830,7 +1734,7 @@ async function resolveRun(args) {
1830
1734
  };
1831
1735
  }
1832
1736
  const sup = await Supervisor.get();
1833
- const runInfo = findRunByLabel(sup, args.runId);
1737
+ const runInfo = findRunByLabel$2(sup, args.runId);
1834
1738
  if (runInfo) return {
1835
1739
  client: sup.getClient(),
1836
1740
  isSingleton: true,
@@ -1943,6 +1847,200 @@ async function inspectTool(args) {
1943
1847
  };
1944
1848
  }
1945
1849
  //#endregion
1850
+ //#region src/tools/result.ts
1851
+ const DEFAULT_POLL_INTERVAL_MS = 1e3;
1852
+ const TERMINAL_STATUSES = new Set([
1853
+ "done",
1854
+ "failed",
1855
+ "timeout",
1856
+ "killed"
1857
+ ]);
1858
+ function abortError() {
1859
+ const error = /* @__PURE__ */ new Error("result wait cancelled");
1860
+ error.name = "AbortError";
1861
+ return error;
1862
+ }
1863
+ function sleep(ms, signal) {
1864
+ if (signal?.aborted) return Promise.reject(abortError());
1865
+ return new Promise((resolve, reject) => {
1866
+ const timer = setTimeout(() => {
1867
+ signal?.removeEventListener("abort", onAbort);
1868
+ resolve();
1869
+ }, ms);
1870
+ const onAbort = () => {
1871
+ clearTimeout(timer);
1872
+ reject(abortError());
1873
+ };
1874
+ signal?.addEventListener("abort", onAbort, { once: true });
1875
+ });
1876
+ }
1877
+ const defaultDeps = {
1878
+ status: statusTool,
1879
+ inspect: inspectTool,
1880
+ sleep,
1881
+ now: Date.now,
1882
+ pollIntervalMs: DEFAULT_POLL_INTERVAL_MS
1883
+ };
1884
+ function toResult(status, timedOut = false) {
1885
+ const ready = TERMINAL_STATUSES.has(status.status);
1886
+ return {
1887
+ run_id: status.run_id,
1888
+ label: status.label,
1889
+ status: status.status,
1890
+ ready,
1891
+ runtime_id: status.runtime_id,
1892
+ session_id: status.session_id,
1893
+ stop_reason: status.stop_reason,
1894
+ pending_permission: status.pending_permission,
1895
+ output: ready ? status.final_output : void 0,
1896
+ timed_out: timedOut || void 0
1897
+ };
1898
+ }
1899
+ async function executeResultTool(args, deps) {
1900
+ const wait = args.wait ?? true;
1901
+ const timeoutMs = args.timeout === void 0 ? void 0 : validateTimeout(args.timeout) * 1e3;
1902
+ const deadline = timeoutMs === void 0 ? void 0 : deps.now() + timeoutMs;
1903
+ const pollIntervalMs = Math.max(1, deps.pollIntervalMs);
1904
+ while (true) {
1905
+ if (args.signal?.aborted) throw abortError();
1906
+ const raw = await deps.status({
1907
+ runId: args.runId,
1908
+ supervisorId: args.supervisorId,
1909
+ view: "full"
1910
+ });
1911
+ const status = Array.isArray(raw) ? raw[0] : raw;
1912
+ if (!status) throw new Error(`run not found: ${args.runId}`);
1913
+ if (TERMINAL_STATUSES.has(status.status)) {
1914
+ if (!status.final_output) try {
1915
+ const inspected = await deps.inspect({
1916
+ runId: args.runId,
1917
+ supervisorId: args.supervisorId
1918
+ });
1919
+ status.final_output = inspected.final_output ?? inspected.snapshot.final_output ?? inspected.snapshot.assistant_text;
1920
+ } catch {}
1921
+ return toResult(status);
1922
+ }
1923
+ if (status.status === "waiting" || !wait) return toResult(status);
1924
+ if (deadline !== void 0 && deps.now() >= deadline) return toResult(status, true);
1925
+ const delay = deadline === void 0 ? pollIntervalMs : Math.min(pollIntervalMs, Math.max(1, deadline - deps.now()));
1926
+ await deps.sleep(delay, args.signal);
1927
+ }
1928
+ }
1929
+ async function resultTool(args) {
1930
+ return executeResultTool(args, defaultDeps);
1931
+ }
1932
+ //#endregion
1933
+ //#region src/tools/answer-permission.ts
1934
+ function findRunByLabel$1(sup, runId) {
1935
+ const runs = sup.runs;
1936
+ const byKey = runs.get(runId);
1937
+ if (byKey) return byKey;
1938
+ for (const info of runs.values()) if (info.label === runId) return info;
1939
+ }
1940
+ async function answerPermissionTool(args) {
1941
+ let client;
1942
+ let sup = null;
1943
+ let requestId = args.requestId;
1944
+ let supervisorId;
1945
+ let isSingleton = false;
1946
+ if (args.supervisorId) ({client, isSingleton, sup, supervisorId} = await getSupervisorClient(args.supervisorId));
1947
+ else {
1948
+ sup = await Supervisor.get();
1949
+ client = sup.getClient();
1950
+ }
1951
+ try {
1952
+ validateRunId(args.runId);
1953
+ const runtimeId = (sup ? findRunByLabel$1(sup, args.runId) : void 0)?.runtimeId ?? args.runId;
1954
+ if (!requestId) {
1955
+ const pp = (await client.status(runtimeId))?.pending_permission;
1956
+ if (!pp?.request_id) throw new Error("no pending permission request for this run");
1957
+ requestId = pp.request_id;
1958
+ }
1959
+ await client.answerPermission(runtimeId, requestId, args.optionId);
1960
+ return { ok: true };
1961
+ } finally {
1962
+ if (supervisorId && !isSingleton) client.close();
1963
+ }
1964
+ }
1965
+ //#endregion
1966
+ //#region src/tools/follow-up.ts
1967
+ function findRunByLabel(sup, runId) {
1968
+ const runs = sup.runs;
1969
+ const byKey = runs.get(runId);
1970
+ if (byKey) return byKey;
1971
+ for (const info of runs.values()) if (info.label === runId) return info;
1972
+ }
1973
+ async function parseSentinel(filePath) {
1974
+ try {
1975
+ const content = (await fs.promises.readFile(filePath, "utf-8")).trim();
1976
+ if (!content) return null;
1977
+ const lines = content.split("\n");
1978
+ const result = {};
1979
+ result._status = lines[0];
1980
+ for (let i = 1; i < lines.length; i++) {
1981
+ const line = lines[i].trim();
1982
+ if (!line) continue;
1983
+ const eq = line.indexOf("=");
1984
+ if (eq > 0) result[line.substring(0, eq)] = line.substring(eq + 1);
1985
+ }
1986
+ return result;
1987
+ } catch {
1988
+ return null;
1989
+ }
1990
+ }
1991
+ async function followUpTool(args) {
1992
+ if (args.supervisorId) {
1993
+ const { client, isSingleton } = await getSupervisorClient(args.supervisorId);
1994
+ try {
1995
+ validateRunId(args.runId);
1996
+ let liveStatus = null;
1997
+ try {
1998
+ liveStatus = await client.status(args.runId);
1999
+ } catch {}
2000
+ const sentinel = await parseSentinel(liveStatus?.sentinel_file ?? path.join(runsRoot(), args.runId, "sentinel.done"));
2001
+ if (!sentinel?.SESSION) throw new Error("run has no session to resume");
2002
+ const agent = liveStatus?.agent ?? "codex";
2003
+ const followUpRunId = crypto.randomUUID();
2004
+ const followUpLabel = args.label ?? `${args.runId}-followup`;
2005
+ const { sentinelPath: followUpSentinelPath, eventLogPath } = ensureRunPaths(followUpRunId);
2006
+ return {
2007
+ run_id: (await client.spawn({
2008
+ agent,
2009
+ prompt: args.message,
2010
+ label: followUpLabel,
2011
+ session_id: sentinel.SESSION,
2012
+ sentinel_file: followUpSentinelPath,
2013
+ on_event: eventLogPath
2014
+ })).runtime_id ?? followUpRunId,
2015
+ label: followUpLabel
2016
+ };
2017
+ } finally {
2018
+ if (!isSingleton) client.close();
2019
+ }
2020
+ }
2021
+ const sup = await Supervisor.get();
2022
+ const runInfo = findRunByLabel(sup, args.runId);
2023
+ if (!runInfo) throw new Error(`run not found: ${args.runId}`);
2024
+ const sentinel = await parseSentinel(runInfo.sentinelPath);
2025
+ if (!sentinel?.SESSION) throw new Error("run has no session to resume");
2026
+ const client = sup.getClient();
2027
+ let agent = "codex";
2028
+ if (runInfo.runtimeId) try {
2029
+ agent = (await client.status(runInfo.runtimeId))?.agent ?? "codex";
2030
+ } catch {}
2031
+ const followUpLabel = args.label ?? `${runInfo.label}-followup`;
2032
+ const followUpRun = await sup.spawn({
2033
+ agent,
2034
+ prompt: args.message,
2035
+ session_id: sentinel.SESSION,
2036
+ label: followUpLabel
2037
+ });
2038
+ return {
2039
+ run_id: followUpRun.runId,
2040
+ label: followUpRun.label
2041
+ };
2042
+ }
2043
+ //#endregion
1946
2044
  //#region src/tools/shutdown.ts
1947
2045
  async function shutdownTool(args) {
1948
2046
  const cleanedUp = [];
@@ -1999,4 +2097,4 @@ async function shutdownTool(args) {
1999
2097
  };
2000
2098
  }
2001
2099
  //#endregion
2002
- export { DEFAULT_RUN_REDUCER_LIMITS, RunReducer, Supervisor, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, normalizeRunEvent, observeRun, reduceRunSnapshot, shutdownTool, spawnTool, statusTool };
2100
+ export { DEFAULT_RUN_REDUCER_LIMITS, RunReducer, Supervisor, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, findAvenorBinary, followUpTool, getInstallDir, getVersion, inspectTool, installerBinaryPath, normalizeRunEvent, observeRun, reduceRunSnapshot, resultTool, shutdownTool, spawnTool, statusTool };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-core",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",