@pasko70/pibo 2.4.2 → 2.5.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.
Files changed (44) hide show
  1. package/dist/agent-runtime/context-build.js +100 -11
  2. package/dist/agent-runtime/profile-validation.js +3 -0
  3. package/dist/agent-runtime/resource-service.js +16 -0
  4. package/dist/agent-runtime/routed-session.js +30 -23
  5. package/dist/agent-runtime/testing/fake-adapter.js +7 -0
  6. package/dist/agent-runtimes/pi/adapter.js +2 -0
  7. package/dist/agent-runtimes/pi/routed-session.js +42 -27
  8. package/dist/agent-runtimes/pi/runtime.js +8 -5
  9. package/dist/apps/chat/web-app.js +21 -6
  10. package/dist/apps/chat-ui/assets/{dist-CrDtveZB.js → dist-BxJuOpXP.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-3YG57JXi.js → dist-CvA-WTQT.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-Cw9po47P.js → dist-DPzsIE8h.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-DTRjeLwO.js → dist-Dj5P89Wy.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-BeqHbnGN.js → dist-Sll26U24.js} +1 -1
  15. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +1 -0
  16. package/dist/apps/chat-ui/assets/index-BW5XFgYP.js +228 -0
  17. package/dist/apps/chat-ui/index.html +2 -2
  18. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  19. package/dist/apps/vscode-artifacts/{pibo-vscode-ext-2.4.2.vsix → pibo-vscode-ext-2.5.0.vsix} +0 -0
  20. package/dist/cli.js +16 -6
  21. package/dist/core/context-build.js +66 -6
  22. package/dist/core/model-defaults.js +11 -3
  23. package/dist/core/session-router.js +152 -69
  24. package/dist/gateway/server.js +1 -0
  25. package/dist/loops/accounting.js +80 -0
  26. package/dist/loops/service.js +51 -16
  27. package/dist/loops/store.js +50 -14
  28. package/dist/runs/lifecycle.js +26 -1
  29. package/dist/runs/registry.js +29 -47
  30. package/dist/runs/tools.js +23 -22
  31. package/dist/subagents/context.js +48 -0
  32. package/dist/subagents/runtime-selection.js +28 -0
  33. package/dist/subagents/tool.js +28 -6
  34. package/dist/tools/codex-compat.js +1 -0
  35. package/dist/tools/contract.js +10 -0
  36. package/dist/tools/mcp-bridge.js +4 -2
  37. package/dist/tools/runtime/node-backend.js +8 -2
  38. package/dist/tools/runtime/python-backend.js +8 -2
  39. package/dist/tools/runtime/tool.js +1 -0
  40. package/dist/tools/session-tool-set.js +19 -12
  41. package/npm-shrinkwrap.json +2 -2
  42. package/package.json +1 -1
  43. package/dist/apps/chat-ui/assets/index-AjnP3ci-.js +0 -228
  44. package/dist/apps/chat-ui/assets/index-BJ56TREg.css +0 -1
@@ -5,7 +5,7 @@ import { RuntimeRoutedSession as RoutedSession, } from "../agent-runtime/routed-
5
5
  import { runtimeSessionErrorDetails } from "./session-errors.js";
6
6
  import { normalizePiboAgentObservationCursor, normalizePiboAgentObservationLimit, normalizePiboAgentObservationOrder, parsePiboAgentObservationTimestamp, piboAgentObservationDetails, piboAgentObservationKind, piboAgentObservationRole, piboAgentObservationSourceFromEvent, piboAgentObservationText, } from "../subagents/observations.js";
7
7
  import { PiboRunRegistry } from "../runs/registry.js";
8
- import { PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError } from "../runs/lifecycle.js";
8
+ import { PiboRunCancellationError, PiboRunCancelledError, PiboRunExecutionTimeoutError, waitForRunCancellationSettlement } from "../runs/lifecycle.js";
9
9
  import { PiboRunResourceLimitError } from "../runs/resource-isolation.js";
10
10
  import { createPiboSignalRegistry } from "../signals/registry.js";
11
11
  import { createDefaultPiboReliabilityStore } from "../reliability/store.js";
@@ -29,7 +29,6 @@ import { createPiboToolPayloadWriter } from "../tools/payload-writer.js";
29
29
  import { PiboPortableToolService, } from "../tools/session-service.js";
30
30
  import { PiboRuntimeResourceService, } from "../agent-runtime/resource-service.js";
31
31
  import { PORTABLE_HISTORY_HANDOFF_METADATA_KEY, PORTABLE_HISTORY_LAST_IMPORT_METADATA_KEY, PiboDataPortableHistoryProvider, createPortableHistoryHandoffMetadata, readPortableHistoryHandoffMetadata, withoutPortableHistoryHandoffMetadata, withPortableHistoryHandoffMetadata, } from "../agent-runtime/portable-history.js";
32
- const DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS = 10 * 60 * 1000;
33
32
  const DEFAULT_SUBAGENT_MAX_DEPTH = 1;
34
33
  const MAX_SUBAGENT_THREAD_KEY_BYTES = 512;
35
34
  const MAX_AGENT_OBSERVATIONS = 5_000;
@@ -70,6 +69,9 @@ function subagentAbortError() {
70
69
  error.name = "AbortError";
71
70
  return error;
72
71
  }
72
+ function subagentRequestEventKey(piboSessionId, eventId) {
73
+ return `${piboSessionId}\u0000${eventId}`;
74
+ }
73
75
  function profileForSession(baseProfile, runtimeInstanceId, nativeSessionId, parentNativeSessionId, subagentDepth) {
74
76
  const usesProfileRuntime = baseProfile.runtimeInstanceId === runtimeInstanceId;
75
77
  const options = {
@@ -258,12 +260,14 @@ export class PiboSessionRouter {
258
260
  runtimeResourceSessions = new Map();
259
261
  runtimeAuthFingerprints = new Map();
260
262
  activeSubagentRequests = new Map();
263
+ subagentRequestIdsByEvent = new Map();
261
264
  agentObservations = [];
262
265
  agentObservationEvictedThroughByParent = new Map();
263
266
  nextAgentObservationSequence = 1;
264
267
  scheduledRunReminders = new Map();
265
268
  runReminderGenerations = new Map();
266
269
  runCancellationHandlers = new Map();
270
+ activeRunExecutions = new Set();
267
271
  quiescingSessions = new Set();
268
272
  disposingSessions = new Map();
269
273
  idleSessionTimers = new Map();
@@ -481,24 +485,24 @@ export class PiboSessionRouter {
481
485
  const failures = [];
482
486
  for (const id of ids) {
483
487
  const session = this.sessions.get(id);
484
- if (session) {
485
- this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason: "kill" });
486
- try {
487
- killed.push(await session.kill());
488
- }
489
- catch (error) {
490
- failures.push(error);
491
- }
488
+ if (!session)
489
+ continue;
490
+ this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason: "kill" });
491
+ try {
492
+ killed.push(await session.kill());
492
493
  }
493
- if (options?.includeRuns) {
494
- const runs = this.runRegistry.cancelControllerRuns(id);
495
- cancelledRuns.push(...runs.map((run) => run.runId));
496
- try {
497
- await this.invokeRunCancellationHandlers(runs);
498
- }
499
- catch (error) {
500
- failures.push(error);
501
- }
494
+ catch (error) {
495
+ failures.push(error);
496
+ }
497
+ }
498
+ if (options?.includeRuns) {
499
+ const runs = ids.flatMap((id) => this.runRegistry.listActiveControllerRuns(id));
500
+ try {
501
+ const cancelled = await this.cancelRunsAfterSettlement(runs, "Pibo session subtree was killed.");
502
+ cancelledRuns.push(...cancelled.map((run) => run.runId));
503
+ }
504
+ catch (error) {
505
+ failures.push(error);
502
506
  }
503
507
  }
504
508
  try {
@@ -555,10 +559,9 @@ export class PiboSessionRouter {
555
559
  const operation = (async () => {
556
560
  await startGate;
557
561
  const runCancellationResults = options.cancelRuns
558
- ? await Promise.allSettled(ids.map(async (id) => {
559
- const runs = this.runRegistry.cancelControllerRuns(id);
560
- await this.invokeRunCancellationHandlers(runs);
561
- }))
562
+ ? await Promise.allSettled([
563
+ this.cancelRunsAfterSettlement(ids.flatMap((id) => this.runRegistry.listActiveControllerRuns(id)), reason),
564
+ ])
562
565
  : [];
563
566
  const pending = ids.map((id) => this.pendingSessions.get(id)).filter((value) => Boolean(value));
564
567
  if (pending.length > 0)
@@ -632,11 +635,11 @@ export class PiboSessionRouter {
632
635
  const childSession = this.sessions.get(id);
633
636
  if (childSession)
634
637
  killed.push(await childSession.kill());
635
- if (options?.includeRuns) {
636
- const runs = this.runRegistry.cancelControllerRuns(id);
637
- cancelledRuns.push(...runs.map((run) => run.runId));
638
- await this.invokeRunCancellationHandlers(runs);
639
- }
638
+ if (!options?.includeRuns)
639
+ continue;
640
+ const runs = this.runRegistry.listActiveControllerRuns(id);
641
+ const cancelled = await this.cancelRunsAfterSettlement(runs, `Child Pibo session "${id}" was killed.`);
642
+ cancelledRuns.push(...cancelled.map((run) => run.runId));
640
643
  }
641
644
  return { killed, cancelledRuns };
642
645
  }
@@ -687,6 +690,18 @@ export class PiboSessionRouter {
687
690
  const session = this.sessionStore.get(piboSessionId);
688
691
  return session ? structuredClone(this.resolveSessionRuntimeBinding(session)) : undefined;
689
692
  }
693
+ getSessionRuntimeProfile(piboSessionId) {
694
+ const session = this.resolvePiboSession(piboSessionId);
695
+ const binding = this.resolveSessionRuntimeBinding(session);
696
+ const parent = session.parentId ? this.resolvePiboSession(session.parentId) : undefined;
697
+ const parentBinding = parent ? this.resolveSessionRuntimeBinding(parent) : undefined;
698
+ const parentNativeSessionId = parentBinding
699
+ && parentBinding.runtimeInstanceId === binding.runtimeInstanceId
700
+ && parentBinding.adapterId === binding.adapterId
701
+ ? parentBinding.nativeSessionId
702
+ : undefined;
703
+ return profileForSession(createPiboProfileFromRegistryOrDefault(this.pluginRegistry, session.profile), binding.runtimeInstanceId, binding.nativeSessionId, parentNativeSessionId, this.getSubagentDepth(session.id));
704
+ }
690
705
  async rebindSessionRuntime(piboSessionId, input) {
691
706
  if (this.quiescingSessions.has(piboSessionId)) {
692
707
  throw new Error(`Pibo session "${piboSessionId}" is already quiescing.`);
@@ -868,7 +883,7 @@ export class PiboSessionRouter {
868
883
  subscribeSignalStatuses(listener) {
869
884
  return this.signalRegistry.subscribeAll(listener);
870
885
  }
871
- async emitMessageAndWaitForReply(event, timeoutMs = 120000, signal) {
886
+ async emitMessageAndWaitForReply(event, timeoutMs, signal) {
872
887
  const eventWithId = { ...event, id: event.id ?? randomUUID() };
873
888
  return await new Promise((resolve, reject) => {
874
889
  let settled = false;
@@ -932,9 +947,11 @@ export class PiboSessionRouter {
932
947
  return;
933
948
  }
934
949
  signal?.addEventListener("abort", onAbort, { once: true });
935
- timeout = setTimeout(() => {
936
- rejectAfterMessageCancellation(new PiboRunExecutionTimeoutError(`Timed out waiting for assistant reply from Pibo session "${eventWithId.piboSessionId}"`, "lifetime"));
937
- }, timeoutMs);
950
+ if (timeoutMs !== undefined) {
951
+ timeout = setTimeout(() => {
952
+ rejectAfterMessageCancellation(new PiboRunExecutionTimeoutError(`Timed out waiting for assistant reply from Pibo session "${eventWithId.piboSessionId}"`, "lifetime"));
953
+ }, timeoutMs);
954
+ }
938
955
  dispatchPromise = this.emit(eventWithId);
939
956
  dispatchPromise.catch((error) => {
940
957
  finish(error instanceof Error ? error : new Error(String(error)));
@@ -963,8 +980,9 @@ export class PiboSessionRouter {
963
980
  for (const timer of this.idleSessionTimers.values())
964
981
  clearTimeout(timer);
965
982
  this.idleSessionTimers.clear();
966
- const cancelledRuns = this.runRegistry.cancelAll("Pibo session router was disposed.");
967
- const runCancellationResult = await Promise.allSettled([this.invokeRunCancellationHandlers(cancelledRuns)]);
983
+ const runCancellationResult = await Promise.allSettled([
984
+ this.cancelRunsAfterSettlement(this.runRegistry.listActiveRuns(), "Pibo session router was disposed."),
985
+ ]);
968
986
  this.scheduledRunReminders.clear();
969
987
  const closeResult = await Promise.allSettled([this.runtimeRegistry.closeAll({ force: true })]);
970
988
  const disposeResults = await Promise.allSettled(sessions.map(([id, session]) => this.disposeRoutedSession(id, session, "router disposed")));
@@ -994,6 +1012,7 @@ export class PiboSessionRouter {
994
1012
  await this.runtimeResourceService.dispose();
995
1013
  this.runtimeResourceSessions.clear();
996
1014
  this.activeSubagentRequests.clear();
1015
+ this.subagentRequestIdsByEvent.clear();
997
1016
  this.agentObservations.length = 0;
998
1017
  this.agentObservationEvictedThroughByParent.clear();
999
1018
  await this.telemetryWriter?.dispose();
@@ -1078,19 +1097,13 @@ export class PiboSessionRouter {
1078
1097
  const piboSession = this.resolvePiboSession(piboSessionId);
1079
1098
  let session;
1080
1099
  this.signalRegistry.project({ type: "session_created", session: piboSession });
1081
- const profile = createPiboProfileFromRegistryOrDefault(this.pluginRegistry, piboSession.profile);
1082
1100
  let binding = this.resolveSessionRuntimeBinding(piboSession);
1083
1101
  const parent = piboSession.parentId ? this.resolvePiboSession(piboSession.parentId) : undefined;
1084
1102
  const parentBinding = parent ? this.resolveSessionRuntimeBinding(parent) : undefined;
1085
1103
  const parentModelScopeId = parent ? parentBinding?.nativeSessionId ?? parent.id : undefined;
1086
- const runtimeParentNativeSessionId = parentBinding
1087
- && parentBinding.runtimeInstanceId === binding.runtimeInstanceId
1088
- && parentBinding.adapterId === binding.adapterId
1089
- ? parentBinding.nativeSessionId
1090
- : undefined;
1091
1104
  const modelDefaults = this.resolveModelDefaults();
1092
1105
  const initialThinkingLevel = resolvePiboSessionInitialThinkingLevel(piboSession);
1093
- const sessionProfile = profileForSession(profile, binding.runtimeInstanceId, binding.nativeSessionId, runtimeParentNativeSessionId, this.getSubagentDepth(piboSession.id));
1106
+ const sessionProfile = this.getSessionRuntimeProfile(piboSession.id);
1094
1107
  const persistedHistoryHandoff = readPortableHistoryHandoffMetadata(binding.metadata);
1095
1108
  if (binding.metadata?.[PORTABLE_HISTORY_HANDOFF_METADATA_KEY] !== undefined && !persistedHistoryHandoff) {
1096
1109
  throw new Error("The pending portable history handoff metadata is invalid; refusing to start a contextless target runtime.");
@@ -1640,22 +1653,43 @@ export class PiboSessionRouter {
1640
1653
  }
1641
1654
  createAgentsController(parentPiboSessionId) {
1642
1655
  return {
1643
- sendMessage: async ({ subagent, message, threadKey, toolCallId, signal }) => {
1656
+ sendMessage: async ({ subagent, message, threadKey, toolCallId, requestId, parentProvenance, signal }) => {
1644
1657
  if (signal?.aborted)
1645
1658
  throw subagentAbortError();
1659
+ if (typeof requestId !== "string" || !requestId.trim())
1660
+ throw new Error("Delegated agent requestId is required.");
1646
1661
  this.assertSubagentDepth(parentPiboSessionId, subagent);
1647
1662
  const child = this.resolveSubagentSession(parentPiboSessionId, subagent, threadKey);
1648
1663
  const resolvedThreadKey = typeof child.metadata?.threadKey === "string" ? child.metadata.threadKey : "";
1664
+ const loopJobId = parentProvenance?.kind === "loop-run"
1665
+ ? parentProvenance.jobId
1666
+ : parentProvenance?.kind === "subagent-request"
1667
+ ? parentProvenance.loopJobId
1668
+ : undefined;
1669
+ const loopRunId = parentProvenance?.kind === "loop-run"
1670
+ ? parentProvenance.runId
1671
+ : parentProvenance?.kind === "subagent-request"
1672
+ ? parentProvenance.loopRunId
1673
+ : undefined;
1649
1674
  const event = {
1650
1675
  type: "message",
1651
1676
  piboSessionId: child.id,
1652
1677
  text: message,
1653
1678
  source: "actor",
1654
1679
  id: randomUUID(),
1680
+ provenance: {
1681
+ kind: "subagent-request",
1682
+ requestId,
1683
+ controllerPiboSessionId: parentPiboSessionId,
1684
+ ...(loopJobId ? { loopJobId } : {}),
1685
+ ...(loopRunId ? { loopRunId } : {}),
1686
+ },
1655
1687
  };
1688
+ this.subagentRequestIdsByEvent.set(subagentRequestEventKey(child.id, event.id), requestId);
1656
1689
  this.emitOutput({
1657
1690
  type: "subagent_session",
1658
1691
  piboSessionId: parentPiboSessionId,
1692
+ requestId,
1659
1693
  toolCallId,
1660
1694
  toolName: "pibo_agents_send_message",
1661
1695
  subagentName: subagent.name,
@@ -1676,13 +1710,15 @@ export class PiboSessionRouter {
1676
1710
  });
1677
1711
  let settlement = { status: "fulfilled" };
1678
1712
  try {
1679
- const reply = await this.emitMessageAndWaitForReply(event, subagent.timeoutMs ?? DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS, requestSignal);
1713
+ const reply = await this.emitMessageAndWaitForReply(event, undefined, requestSignal);
1680
1714
  return {
1715
+ requestId,
1681
1716
  agentId: child.id,
1682
1717
  name: subagent.name,
1683
1718
  profile: child.profile,
1684
1719
  threadKey: resolvedThreadKey,
1685
1720
  eventId: event.id,
1721
+ finalMessage: reply.text,
1686
1722
  reply,
1687
1723
  };
1688
1724
  }
@@ -1695,6 +1731,7 @@ export class PiboSessionRouter {
1695
1731
  throw error;
1696
1732
  }
1697
1733
  finally {
1734
+ this.subagentRequestIdsByEvent.delete(subagentRequestEventKey(child.id, event.id));
1698
1735
  untrack();
1699
1736
  resolveSettled?.(settlement);
1700
1737
  }
@@ -1744,6 +1781,7 @@ export class PiboSessionRouter {
1744
1781
  if (since !== undefined && until !== undefined && since > until) {
1745
1782
  throw new Error("Agent observation since must not be after until.");
1746
1783
  }
1784
+ const requestIds = input.requestIds ? new Set(input.requestIds) : undefined;
1747
1785
  const agentIds = input.agentIds ? new Set(input.agentIds) : undefined;
1748
1786
  if (agentIds) {
1749
1787
  for (const agentId of agentIds)
@@ -1753,10 +1791,13 @@ export class PiboSessionRouter {
1753
1791
  const threadKeys = input.threadKeys ? new Set(input.threadKeys) : undefined;
1754
1792
  const eventTypes = input.eventTypes ? new Set(input.eventTypes) : undefined;
1755
1793
  const kinds = input.kinds ? new Set(input.kinds) : undefined;
1794
+ const roles = input.roles ? new Set(input.roles) : undefined;
1756
1795
  const textContains = input.textContains?.toLowerCase();
1757
1796
  const matches = this.agentObservations.filter((observation) => {
1758
1797
  if (observation.managingParentId !== parentPiboSessionId)
1759
1798
  return false;
1799
+ if (requestIds && (!observation.requestId || !requestIds.has(observation.requestId)))
1800
+ return false;
1760
1801
  if (agentIds && !agentIds.has(observation.agentId))
1761
1802
  return false;
1762
1803
  if (names && !names.has(observation.name))
@@ -1767,6 +1808,8 @@ export class PiboSessionRouter {
1767
1808
  return false;
1768
1809
  if (kinds && !kinds.has(observation.kind))
1769
1810
  return false;
1811
+ if (roles && (!observation.role || !roles.has(observation.role)))
1812
+ return false;
1770
1813
  if (afterSequence !== undefined && observation.sequence <= afterSequence)
1771
1814
  return false;
1772
1815
  const createdAt = Date.parse(observation.createdAt);
@@ -1808,9 +1851,8 @@ export class PiboSessionRouter {
1808
1851
  const child = this.requireManagedAgent(parentPiboSessionId, agentId);
1809
1852
  const ids = [agentId, ...this.descendantSessionIds(agentId)];
1810
1853
  const idSet = new Set(ids);
1811
- const cancelledRuns = this.runRegistry.listAll({ includeConsumed: true, includeDetached: true })
1812
- .filter((run) => idSet.has(run.controllerPiboSessionId) && !isTerminalRunStatus(run.status))
1813
- .map((run) => run.runId);
1854
+ const cancellableRuns = this.runRegistry.listAll({ includeConsumed: true, includeDetached: true })
1855
+ .filter((run) => idSet.has(run.controllerPiboSessionId) && !isTerminalRunStatus(run.status));
1814
1856
  await Promise.allSettled(ids.flatMap((id) => this.sessions.has(id)
1815
1857
  ? [this.emit({ type: "execution", piboSessionId: id, action: "abort", id: randomUUID() })]
1816
1858
  : []));
@@ -1824,20 +1866,42 @@ export class PiboSessionRouter {
1824
1866
  });
1825
1867
  }
1826
1868
  await this.disposeSessionSubtree(agentId, `killed by parent ${parentPiboSessionId}`, { cancelRuns: true });
1869
+ const cancelledRuns = cancellableRuns.flatMap((run) => {
1870
+ const current = this.runRegistry.status(run.controllerPiboSessionId, run.runId);
1871
+ return current.status === "cancelled" ? [run.runId] : [];
1872
+ });
1827
1873
  return { agentId, killed: ids, cancelledRuns };
1828
1874
  }
1875
+ async invokeRunCancellationHandler(run) {
1876
+ const cancel = this.runCancellationHandlers.get(run.runId);
1877
+ if (!cancel) {
1878
+ const current = this.runRegistry.status(run.controllerPiboSessionId, run.runId);
1879
+ if (isTerminalRunStatus(current.status) || !this.activeRunExecutions.has(run.runId))
1880
+ return;
1881
+ throw new PiboRunCancellationError(`Yielded run "${run.runId}" has active execution but does not expose a cancellation handler.`);
1882
+ }
1883
+ await cancel();
1884
+ if (this.runCancellationHandlers.get(run.runId) === cancel)
1885
+ this.runCancellationHandlers.delete(run.runId);
1886
+ }
1829
1887
  async invokeRunCancellationHandlers(runs) {
1888
+ const results = await Promise.allSettled(runs.map((run) => this.invokeRunCancellationHandler(run)));
1889
+ const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1890
+ if (failures.length > 0)
1891
+ throw new AggregateError(failures, "Failed to terminate yielded runs.");
1892
+ }
1893
+ async cancelRunsAfterSettlement(runs, reason) {
1830
1894
  const results = await Promise.allSettled(runs.map(async (run) => {
1831
- const cancel = this.runCancellationHandlers.get(run.runId);
1832
- if (!cancel)
1833
- return;
1834
- await cancel();
1835
- if (this.runCancellationHandlers.get(run.runId) === cancel)
1836
- this.runCancellationHandlers.delete(run.runId);
1895
+ await this.invokeRunCancellationHandler(run);
1896
+ const current = this.runRegistry.status(run.controllerPiboSessionId, run.runId);
1897
+ return isTerminalRunStatus(current.status)
1898
+ ? current
1899
+ : this.runRegistry.cancel(run.controllerPiboSessionId, run.runId, reason);
1837
1900
  }));
1838
1901
  const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
1839
1902
  if (failures.length > 0)
1840
- throw new AggregateError(failures, "Failed to terminate cancelled yielded runs.");
1903
+ throw new AggregateError(failures, "Failed to terminate yielded runs before cancellation settlement.");
1904
+ return results.flatMap((result) => result.status === "fulfilled" && result.value.status === "cancelled" ? [result.value] : []);
1841
1905
  }
1842
1906
  createRunToolController(parentPiboSessionId) {
1843
1907
  return {
@@ -1868,28 +1932,37 @@ export class PiboSessionRouter {
1868
1932
  admission.release();
1869
1933
  throw error;
1870
1934
  }
1935
+ this.activeRunExecutions.add(run.runId);
1871
1936
  const cancellation = { state: "none" };
1937
+ let resolveRunTaskSettled;
1938
+ const runTaskSettled = new Promise((resolve) => { resolveRunTaskSettled = resolve; });
1872
1939
  if (cancel) {
1873
- this.runCancellationHandlers.set(run.runId, async () => {
1874
- cancellation.state = "pending";
1875
- let resolveDecision;
1876
- cancellation.decision = new Promise((resolve) => { resolveDecision = resolve; });
1877
- try {
1878
- await cancel();
1879
- cancellation.state = "confirmed";
1880
- }
1881
- catch (error) {
1882
- cancellation.state = "failed";
1883
- throw error;
1884
- }
1885
- finally {
1886
- resolveDecision?.();
1887
- }
1940
+ let cancellationAttempt;
1941
+ this.runCancellationHandlers.set(run.runId, () => {
1942
+ cancellationAttempt ??= (async () => {
1943
+ cancellation.state = "pending";
1944
+ let resolveDecision;
1945
+ cancellation.decision = new Promise((resolve) => { resolveDecision = resolve; });
1946
+ try {
1947
+ await waitForRunCancellationSettlement(Promise.resolve().then(cancel));
1948
+ cancellation.state = "confirmed";
1949
+ resolveDecision?.();
1950
+ await waitForRunCancellationSettlement(runTaskSettled);
1951
+ }
1952
+ catch (error) {
1953
+ cancellation.state = "failed";
1954
+ throw error;
1955
+ }
1956
+ finally {
1957
+ resolveDecision?.();
1958
+ }
1959
+ })();
1960
+ return cancellationAttempt;
1888
1961
  });
1889
1962
  }
1890
1963
  void (async () => {
1891
1964
  try {
1892
- const result = await execute();
1965
+ const result = await execute(run.runId);
1893
1966
  if (resources)
1894
1967
  this.runRegistry.updateResources(run.runId, resources);
1895
1968
  const completed = this.runRegistry.complete(run.runId, result);
@@ -1916,7 +1989,9 @@ export class PiboSessionRouter {
1916
1989
  }
1917
1990
  finally {
1918
1991
  this.runCancellationHandlers.delete(run.runId);
1992
+ this.activeRunExecutions.delete(run.runId);
1919
1993
  admission.release();
1994
+ resolveRunTaskSettled?.();
1920
1995
  }
1921
1996
  })();
1922
1997
  return run;
@@ -2034,10 +2109,18 @@ export class PiboSessionRouter {
2034
2109
  const source = piboAgentObservationSourceFromEvent(event);
2035
2110
  const role = piboAgentObservationRole(source);
2036
2111
  const text = piboAgentObservationText(source);
2112
+ const provenance = "provenance" in event ? event.provenance : undefined;
2113
+ const eventId = "eventId" in event && typeof event.eventId === "string" ? event.eventId : undefined;
2114
+ const requestId = provenance?.kind === "subagent-request"
2115
+ ? provenance.requestId
2116
+ : eventId
2117
+ ? this.subagentRequestIdsByEvent.get(subagentRequestEventKey(session.id, eventId))
2118
+ : undefined;
2037
2119
  const observation = {
2038
2120
  managingParentId: session.parentId,
2039
2121
  sequence: this.nextAgentObservationSequence++,
2040
2122
  createdAt: new Date().toISOString(),
2123
+ ...(requestId ? { requestId } : {}),
2041
2124
  agentId: session.id,
2042
2125
  name,
2043
2126
  ...(typeof session.metadata?.threadKey === "string" ? { threadKey: session.metadata.threadKey } : {}),
@@ -358,6 +358,7 @@ export class PiboGatewayServer {
358
358
  findSessions: (input) => this.requireSessionStore().find(input),
359
359
  listSessions: () => this.requireSessionStore().list?.() ?? [],
360
360
  getSessionRuntimeBinding: (piboSessionId) => this.requireRouter().getSessionRuntimeBinding(piboSessionId),
361
+ getSessionRuntimeProfile: (piboSessionId) => this.requireRouter().getSessionRuntimeProfile(piboSessionId),
361
362
  inspectSessionRuntimeHistory: async (piboSessionId) => {
362
363
  const session = this.requireSessionStore().get(piboSessionId);
363
364
  if (!session)
@@ -1,4 +1,84 @@
1
1
  export const LOOP_TOKEN_ACCOUNTING_VERSION = 1;
2
+ export function emptyLoopUsageTotals() {
3
+ return {
4
+ inputTokens: 0,
5
+ outputTokens: 0,
6
+ cacheReadTokens: 0,
7
+ cacheWriteTokens: 0,
8
+ reasoningTokens: 0,
9
+ totalTokens: 0,
10
+ costUsd: 0,
11
+ costReportedTurns: 0,
12
+ assistantTurns: 0,
13
+ };
14
+ }
15
+ export function emptyLoopRecursiveUsage() {
16
+ return {
17
+ controller: emptyLoopUsageTotals(),
18
+ descendants: emptyLoopUsageTotals(),
19
+ total: emptyLoopUsageTotals(),
20
+ sessionIds: [],
21
+ };
22
+ }
23
+ export function assistantUsageTotals(usage) {
24
+ return {
25
+ inputTokens: normalizedTokenCount(usage.inputTokens),
26
+ outputTokens: normalizedTokenCount(usage.outputTokens),
27
+ cacheReadTokens: normalizedTokenCount(usage.cacheReadTokens),
28
+ cacheWriteTokens: normalizedTokenCount(usage.cacheWriteTokens),
29
+ reasoningTokens: normalizedTokenCount(usage.reasoningTokens),
30
+ totalTokens: normalizedTokenCount(usage.totalTokens),
31
+ costUsd: typeof usage.costUsd === 'number' && Number.isFinite(usage.costUsd) ? Math.max(0, usage.costUsd) : 0,
32
+ costReportedTurns: typeof usage.costUsd === 'number' && Number.isFinite(usage.costUsd) ? 1 : 0,
33
+ assistantTurns: 1,
34
+ };
35
+ }
36
+ function normalizeLoopUsageTotals(value) {
37
+ const costUsd = typeof value?.costUsd === 'number' && Number.isFinite(value.costUsd) ? Math.max(0, value.costUsd) : 0;
38
+ return {
39
+ inputTokens: normalizedTokenCount(value?.inputTokens),
40
+ outputTokens: normalizedTokenCount(value?.outputTokens),
41
+ cacheReadTokens: normalizedTokenCount(value?.cacheReadTokens),
42
+ cacheWriteTokens: normalizedTokenCount(value?.cacheWriteTokens),
43
+ reasoningTokens: normalizedTokenCount(value?.reasoningTokens),
44
+ totalTokens: normalizedTokenCount(value?.totalTokens),
45
+ costUsd,
46
+ costReportedTurns: value?.costReportedTurns === undefined && costUsd > 0 ? 1 : normalizedTokenCount(value?.costReportedTurns),
47
+ assistantTurns: normalizedTokenCount(value?.assistantTurns),
48
+ };
49
+ }
50
+ function addUsageTotals(left, right) {
51
+ const normalizedLeft = normalizeLoopUsageTotals(left);
52
+ const normalizedRight = normalizeLoopUsageTotals(right);
53
+ return {
54
+ inputTokens: normalizedLeft.inputTokens + normalizedRight.inputTokens,
55
+ outputTokens: normalizedLeft.outputTokens + normalizedRight.outputTokens,
56
+ cacheReadTokens: normalizedLeft.cacheReadTokens + normalizedRight.cacheReadTokens,
57
+ cacheWriteTokens: normalizedLeft.cacheWriteTokens + normalizedRight.cacheWriteTokens,
58
+ reasoningTokens: normalizedLeft.reasoningTokens + normalizedRight.reasoningTokens,
59
+ totalTokens: normalizedLeft.totalTokens + normalizedRight.totalTokens,
60
+ costUsd: normalizedLeft.costUsd + normalizedRight.costUsd,
61
+ costReportedTurns: normalizedLeft.costReportedTurns + normalizedRight.costReportedTurns,
62
+ assistantTurns: normalizedLeft.assistantTurns + normalizedRight.assistantTurns,
63
+ };
64
+ }
65
+ export function addLoopAssistantUsage(current, usage, input) {
66
+ const base = current
67
+ ? {
68
+ controller: normalizeLoopUsageTotals(current.controller),
69
+ descendants: normalizeLoopUsageTotals(current.descendants),
70
+ total: normalizeLoopUsageTotals(current.total),
71
+ sessionIds: Array.isArray(current.sessionIds) ? current.sessionIds.filter((id) => typeof id === 'string') : [],
72
+ }
73
+ : emptyLoopRecursiveUsage();
74
+ const increment = assistantUsageTotals(usage);
75
+ return {
76
+ controller: input.descendant ? { ...base.controller } : addUsageTotals(base.controller, increment),
77
+ descendants: input.descendant ? addUsageTotals(base.descendants, increment) : { ...base.descendants },
78
+ total: addUsageTotals(base.total, increment),
79
+ sessionIds: base.sessionIds.includes(input.piboSessionId) ? [...base.sessionIds] : [...base.sessionIds, input.piboSessionId],
80
+ };
81
+ }
2
82
  function normalizedTokenCount(value) {
3
83
  return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
4
84
  }
@@ -519,30 +519,65 @@ export class PiboLoopService {
519
519
  getStopConditionDefinitions() { return this.options.context.getLoopStopConditionDefinitions?.() ?? this.options.context.getRalphStopConditionDefinitions?.() ?? createBuiltInLoopStopConditions(); }
520
520
  handleOutputEvent(event) {
521
521
  const eventId = 'eventId' in event ? event.eventId : undefined;
522
- if (!eventId)
523
- return;
524
- if (event.type === 'message_queued')
525
- this.store.updateRunMessageState(eventId, 'queued');
526
- else if (event.type === 'message_started')
527
- this.store.updateRunMessageState(eventId, 'active');
528
- else if (event.type === 'message_finished')
529
- this.store.updateRunMessageState(eventId, 'finished');
530
- else if (event.type === 'session_error' && event.errorDetails?.code === 'loop_continuation_invalidated')
531
- this.store.updateRunMessageState(eventId, 'invalidated');
522
+ if (eventId) {
523
+ if (event.type === 'message_queued')
524
+ this.store.updateRunMessageState(eventId, 'queued');
525
+ else if (event.type === 'message_started')
526
+ this.store.updateRunMessageState(eventId, 'active');
527
+ else if (event.type === 'message_finished')
528
+ this.store.updateRunMessageState(eventId, 'finished');
529
+ else if (event.type === 'session_error' && event.errorDetails?.code === 'loop_continuation_invalidated')
530
+ this.store.updateRunMessageState(eventId, 'invalidated');
531
+ }
532
532
  if (event.type !== 'assistant_usage')
533
533
  return;
534
- const provenance = event.provenance?.kind === 'loop-run' ? event.provenance : undefined;
535
- const run = this.store.getRunByMessageEventId(eventId) ?? (provenance ? this.store.getRun(provenance.runId) : undefined);
536
- if (!run || run.piboSessionId !== event.piboSessionId)
534
+ const provenance = event.provenance;
535
+ const provenanceRunId = provenance?.kind === 'loop-run'
536
+ ? provenance.runId
537
+ : provenance?.kind === 'subagent-request'
538
+ ? provenance.loopRunId
539
+ : undefined;
540
+ const provenanceJobId = provenance?.kind === 'loop-run'
541
+ ? provenance.jobId
542
+ : provenance?.kind === 'subagent-request'
543
+ ? provenance.loopJobId
544
+ : undefined;
545
+ const run = provenanceRunId
546
+ ? this.store.getRun(provenanceRunId)
547
+ : eventId
548
+ ? this.store.getRunByMessageEventId(eventId)
549
+ : undefined;
550
+ if (!run || (provenanceJobId && run.jobId !== provenanceJobId) || !this.isRunSessionOrDescendant(run, event.piboSessionId))
537
551
  return;
538
- if (provenance && (run.jobId !== provenance.jobId
539
- || (provenance.cause === 'run-reminder' && run.messageEventId !== provenance.rootEventId)))
552
+ if (provenance?.kind === 'loop-run'
553
+ && provenance.cause === 'run-reminder'
554
+ && run.messageEventId !== provenance.rootEventId)
540
555
  return;
541
556
  const job = this.store.getJob(run.jobId);
542
557
  if (!job || job.mode !== 'goal')
543
558
  return;
544
559
  const basis = run.accounting?.tokenAccounting?.basis ?? goalTokenAccounting(job).basis;
545
- this.store.recordGoalTurnUsage(job.id, run.id, goalBudgetTokens(event, basis));
560
+ this.store.recordGoalAssistantUsage(job.id, run.id, {
561
+ usage: event,
562
+ budgetTokens: goalBudgetTokens(event, basis),
563
+ piboSessionId: event.piboSessionId,
564
+ descendant: event.piboSessionId !== run.piboSessionId,
565
+ });
566
+ }
567
+ isRunSessionOrDescendant(run, piboSessionId) {
568
+ if (!run.piboSessionId)
569
+ return false;
570
+ if (run.piboSessionId === piboSessionId)
571
+ return true;
572
+ let current = this.options.context.getSession(piboSessionId);
573
+ const seen = new Set();
574
+ while (current?.parentId && !seen.has(current.parentId)) {
575
+ if (current.parentId === run.piboSessionId)
576
+ return true;
577
+ seen.add(current.parentId);
578
+ current = this.options.context.getSession(current.parentId);
579
+ }
580
+ return false;
546
581
  }
547
582
  handleProductEvent(event) {
548
583
  if (event.type !== 'pibo.loop.fact' && event.type !== 'loop.fact' && event.type !== 'pibo.ralph.fact' && event.type !== 'ralph.fact')