@pasko70/pibo 3.0.1 → 3.1.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 (122) hide show
  1. package/README.md +20 -6
  2. package/compute-image/Dockerfile +47 -0
  3. package/compute-image/Dockerfile.dockerignore +5 -0
  4. package/dist/agent-runtime/context-build.js +26 -2
  5. package/dist/agent-runtime/contract.js +78 -28
  6. package/dist/agent-runtime/portable-history.js +19 -9
  7. package/dist/agent-runtime/registry.js +8 -2
  8. package/dist/agent-runtime/routed-session.js +120 -15
  9. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  10. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  11. package/dist/agent-runtimes/omp/adapter.js +3 -7
  12. package/dist/agent-runtimes/pi/adapter.js +5 -3
  13. package/dist/agent-runtimes/pi/routed-session.js +59 -12
  14. package/dist/agent-runtimes/pi/runtime.js +6 -0
  15. package/dist/apps/chat/agent-profiles.js +31 -2
  16. package/dist/apps/chat/agent-store.js +98 -6
  17. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  18. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  19. package/dist/apps/chat/data/project-service.js +119 -15
  20. package/dist/apps/chat/data/room-service.js +21 -0
  21. package/dist/apps/chat/data/session-query-service.js +33 -10
  22. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  23. package/dist/apps/chat/loop-api.js +10 -2
  24. package/dist/apps/chat/output-compactor.js +144 -21
  25. package/dist/apps/chat/output-event-policy.js +179 -2
  26. package/dist/apps/chat/stream.js +16 -7
  27. package/dist/apps/chat/trace-v2.js +1 -0
  28. package/dist/apps/chat/web-app.js +317 -86
  29. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CUQJqggN.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-ClUlQWYN.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-Djul9BmZ.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-GD0JGdCi.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-W5HOqHym.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-BcjkX-iP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-CmqRSbBU.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-JvrPUGvI.js +43 -0
  38. package/dist/apps/chat-vscode-web/index.html +1 -1
  39. package/dist/apps/cli-ui/InkSessionApp.js +111 -12
  40. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  41. package/dist/cli-session/localSessionSource.js +153 -161
  42. package/dist/cli.js +7 -11
  43. package/dist/compute/cli.js +35 -16
  44. package/dist/compute/docker.js +341 -94
  45. package/dist/config/config.js +3 -0
  46. package/dist/core/context-build.js +7 -16
  47. package/dist/core/output-persistence-retry.js +484 -0
  48. package/dist/core/output-render-sequence.js +331 -0
  49. package/dist/core/profiles.js +24 -0
  50. package/dist/core/provider-recovery.js +7 -1
  51. package/dist/core/session-model.js +37 -1
  52. package/dist/core/session-router.js +78 -110
  53. package/dist/data/ingest-service.js +91 -9
  54. package/dist/data/navigation-store.js +5 -5
  55. package/dist/data/pibo-store.js +8 -1
  56. package/dist/data/schema.js +243 -3
  57. package/dist/data/session-store.js +3 -3
  58. package/dist/debug/agents.js +91 -89
  59. package/dist/debug/index.js +46 -2
  60. package/dist/debug/pty.js +127 -48
  61. package/dist/gateway/cli.js +4 -0
  62. package/dist/gateway/web.js +8 -8
  63. package/dist/local/client.js +3 -2
  64. package/dist/loops/channel.js +3 -1
  65. package/dist/loops/cli.js +2 -1
  66. package/dist/loops/service.js +90 -11
  67. package/dist/loops/store.js +100 -74
  68. package/dist/mcp/agent-context.js +13 -26
  69. package/dist/mcp/commands/info.js +3 -1
  70. package/dist/mcp/config-command.js +46 -2
  71. package/dist/mcp/config.js +18 -4
  72. package/dist/plugins/context-files-store.js +36 -6
  73. package/dist/plugins/context-files.js +52 -1
  74. package/dist/plugins/registry.js +2 -0
  75. package/dist/plugins/user-profile-resources.js +22 -0
  76. package/dist/previews/base-url.js +34 -0
  77. package/dist/previews/config.js +2 -32
  78. package/dist/ralph/store.js +5 -0
  79. package/dist/reliability/store.js +378 -106
  80. package/dist/session-ui/terminalRows.js +8 -4
  81. package/dist/sessions/pibo-data-store.js +145 -0
  82. package/dist/sessions/store.js +42 -0
  83. package/dist/setup/cli.js +422 -6
  84. package/dist/setup/installation-profiles.js +464 -0
  85. package/dist/shared/deterministic-digest.js +94 -0
  86. package/dist/shared/trace-engine.js +54 -0
  87. package/dist/shared/trace-event-projection.js +124 -67
  88. package/dist/shared/trace-history.js +47 -12
  89. package/dist/shared/trace-live-reducer.js +23 -4
  90. package/dist/shared/trace-nodes.js +17 -28
  91. package/dist/shared/trace-order.js +41 -5
  92. package/dist/shared/trace-page-merge.js +44 -14
  93. package/dist/shared/trace-patch-nodes.js +2 -0
  94. package/dist/shared/trace-subagent-links.js +4 -1
  95. package/dist/shared/trace-tool-identity.js +15 -8
  96. package/dist/signals/projector.js +28 -6
  97. package/dist/subagents/observation-query.js +121 -0
  98. package/dist/subagents/tool.js +7 -3
  99. package/dist/tools/agent-browser-leases.js +58 -23
  100. package/dist/tools/agent-browser-wrapper.js +1 -1
  101. package/dist/tools/browser-use-leases.js +129 -16
  102. package/dist/tools/browser-use-wrapper.js +1 -1
  103. package/dist/tools/index.js +27 -11
  104. package/dist/tools/python-runtime.js +10 -3
  105. package/dist/tools/registry.js +1 -1
  106. package/dist/tools/runtime/node-backend.js +16 -5
  107. package/dist/tools/runtime/node-worker-source.js +112 -33
  108. package/dist/tools/runtime/python-backend.js +16 -5
  109. package/dist/tools/runtime/python-worker-source.js +167 -16
  110. package/dist/tools/runtime/registry.js +23 -10
  111. package/dist/user-skills/store.js +0 -1
  112. package/docs/ops/vscode-extension-release.md +9 -1
  113. package/npm-shrinkwrap.json +9 -2
  114. package/package.json +9 -2
  115. package/scripts/docker-entrypoint.sh +46 -0
  116. package/scripts/prepare-agent-browser-wrapper.sh +70 -0
  117. package/scripts/prepare-browser-use-wrapper.sh +255 -0
  118. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  119. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  120. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +0 -43
  121. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  122. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
package/README.md CHANGED
@@ -24,6 +24,19 @@ pibo --help
24
24
 
25
25
  The current package declares Node.js 24+. Some Linux distributions still ship Node 22 through `apt`, so install a recent Node release first when needed. A user-local Node/npm setup through `nvm`, `fnm`, or a user-writable npm prefix works well.
26
26
 
27
+ Package installation has no host-level side effects. For a self-hosted server, choose a supported setup profile after installing the CLI:
28
+
29
+ ```bash
30
+ # Recommended complete workstation
31
+ pibo setup plan --profile batteries-included --domain pibo.example.com
32
+ sudo pibo setup install --profile batteries-included --domain pibo.example.com --apply --yes
33
+
34
+ # Minimal gateway and Chat Web only
35
+ pibo setup plan --profile vanilla --domain pibo.example.com
36
+ ```
37
+
38
+ See [Installation profiles](./docs/project/installation-profiles.md) for components, resource and security costs, staging, upgrades, and rollback.
39
+
27
40
  For a user-local global npm prefix:
28
41
 
29
42
  ```bash
@@ -67,17 +80,18 @@ A fresh server install usually needs only three decisions:
67
80
  2. Install Node.js 24+ for that user or system-wide.
68
81
  3. Install Pibo through npm.
69
82
 
70
- Use the setup planner to keep the first run simple:
83
+ Use an installation profile for the normal first run. Batteries Included is recommended; Vanilla is the minimal controlled base:
71
84
 
72
85
  ```bash
73
86
  pibo setup doctor --domain pibo.example.com --expected-ip <server-ip>
74
- pibo setup user-host --domain pibo.example.com --print-files
75
- pibo setup user-host --domain pibo.example.com --write-to /tmp/pibo-setup
87
+ pibo setup plan --profile batteries-included --domain pibo.example.com
88
+ pibo setup install --profile batteries-included --domain pibo.example.com --write-to /tmp/pibo-setup
89
+ sudo pibo setup install --profile batteries-included --domain pibo.example.com --apply --yes
76
90
  ```
77
91
 
78
- This is the normal user path: one gateway, one `PIBO_HOME`, no required Docker, no dev gateway, and no GitHub App setup. After reviewing staged files, use `--apply --yes` to write the generated systemd/Caddy files.
92
+ The package install remains side-effect free. The explicit setup command records ownership, checks pinned downloads, configures the selected components, and exposes status, upgrade, and uninstall plans. See [Installation profiles](./docs/project/installation-profiles.md).
79
93
 
80
- Developer hosts are opt-in and add production/dev separation plus Docker compute workers:
94
+ Core developers can still use the topology-specific developer-host planner to add production/dev separation plus Docker compute workers:
81
95
 
82
96
  ```bash
83
97
  pibo setup developer-host \
@@ -159,7 +173,7 @@ pibo mcp # discover and call configured MCP servers
159
173
  pibo tools # install and inspect curated external CLI tools
160
174
  pibo pi-packages # register Pi Coding Agent packages
161
175
  pibo debug # inspect local Pibo data stores
162
- pibo setup # plan user-host installs and developer-host upgrades
176
+ pibo setup # plan and manage supported host installation profiles
163
177
  pibo profile # inspect runtime profiles
164
178
  pibo tui # start the direct Pi TUI
165
179
  pibo tui:routed # start the routed Pibo TUI
@@ -0,0 +1,47 @@
1
+ FROM node:24-slim
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ python3 python3-venv python3-dev \
5
+ build-essential curl git ca-certificates \
6
+ chromium chromium-driver xvfb xauth x11-xserver-utils \
7
+ fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 \
8
+ libatk1.0-0 libcups2 libdbus-1-3 libdrm2 libgbm1 libgtk-3-0 libnspr4 \
9
+ libnss3 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 xdg-utils \
10
+ dbus dbus-x11 \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ RUN curl -LsSf https://astral.sh/uv/install.sh | sh
14
+ ENV PATH="/root/.local/bin:${PATH}"
15
+ ENV DISPLAY=:99
16
+ ENV PIBO_BROWSER_USE_CHROME=/usr/bin/chromium
17
+
18
+ # The host stages only the published package archive and this Dockerfile, so an
19
+ # installed package's production node_modules never enters the build context.
20
+ COPY pibo-package.tgz /tmp/pibo-package.tgz
21
+
22
+ WORKDIR /app
23
+ RUN npm init -y >/dev/null && \
24
+ npm install --omit=dev --no-audit --no-fund /tmp/pibo-package.tgz && \
25
+ rm -f /tmp/pibo-package.tgz
26
+
27
+ RUN mkdir -p /root/.pibo/tools/browser-use/home/bin && \
28
+ uv venv /root/.pibo/tools/browser-use/.venv --python 3.12 && \
29
+ uv pip install --python /root/.pibo/tools/browser-use/.venv/bin/python \
30
+ 'browser-use[cli]==0.12.6'
31
+
32
+ RUN mkdir -p /root/.pibo/tools/agent-browser/home/bin \
33
+ /root/.pibo/tools/agent-browser/home/profiles \
34
+ /root/.pibo/tools/agent-browser/node && \
35
+ npm install --prefix /root/.pibo/tools/agent-browser/node agent-browser@0.27.0
36
+
37
+ WORKDIR /app/node_modules/@pasko70/pibo
38
+ RUN sed -i 's/\r$//' scripts/*.sh && \
39
+ chmod +x scripts/*.sh && \
40
+ scripts/prepare-browser-use-wrapper.sh && \
41
+ scripts/prepare-agent-browser-wrapper.sh && \
42
+ mkdir -p /root/.pibo /root/.browser-use /root/.pibo/tools/agent-browser/home
43
+
44
+ EXPOSE 4788 4789 56663
45
+
46
+ ENTRYPOINT ["/app/node_modules/@pasko70/pibo/scripts/docker-entrypoint.sh"]
47
+ CMD ["gateway:web"]
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ node_modules/**
3
+ .pibo
4
+ .pibo/**
5
+ *.log
@@ -17,6 +17,7 @@ export function profileWithRuntimeInstance(profile, runtimeInstanceId) {
17
17
  parentSessionId: profile.parentSessionId,
18
18
  model: undefined,
19
19
  mainModel: undefined,
20
+ mainModelFallbacks: [],
20
21
  subagentModel: undefined,
21
22
  thinkingLevel: profile.thinkingLevel,
22
23
  mainThinkingLevel: profile.mainThinkingLevel,
@@ -30,6 +31,7 @@ export function profileWithRuntimeInstance(profile, runtimeInstanceId) {
30
31
  mcpServers: profile.mcpServers,
31
32
  piPackages: profile.piPackages,
32
33
  contextFiles: profile.contextFiles,
34
+ diagnostics: profile.diagnostics,
33
35
  builtinTools: profile.builtinTools,
34
36
  builtinToolNames: profile.builtinToolNames,
35
37
  autoContextFiles: profile.autoContextFiles,
@@ -229,6 +231,7 @@ export function buildPortableRuntimeContextSnapshot(input) {
229
231
  metadata: {
230
232
  activeModel: runtimeManifest.effectiveModel,
231
233
  mainModel: profile.mainModel,
234
+ mainModelFallbacks: profile.mainModelFallbacks,
232
235
  subagentModel: profile.subagentModel,
233
236
  mainThinkingLevel: profile.mainThinkingLevel ?? profile.thinkingLevel,
234
237
  subagentThinkingLevel: profile.subagentThinkingLevel ?? profile.thinkingLevel,
@@ -240,6 +243,21 @@ export function buildPortableRuntimeContextSnapshot(input) {
240
243
  ?? input.runtime.capabilities.nativeSubagents.enabledByDefault,
241
244
  },
242
245
  });
246
+ for (const [index, diagnostic] of profile.diagnostics.entries()) {
247
+ addNode({
248
+ id: `profile-diagnostic-${index}`,
249
+ kind: "diagnostic",
250
+ title: diagnostic.code,
251
+ source: "profile",
252
+ state: diagnostic.severity === "error" ? "error" : "warning",
253
+ badges: [diagnostic.severity.toUpperCase(), "PROFILE"],
254
+ hydratedText: diagnostic.message,
255
+ metadata: diagnostic.resourceName ? {
256
+ resourceKind: diagnostic.resourceKind,
257
+ resourceName: diagnostic.resourceName,
258
+ } : undefined,
259
+ });
260
+ }
243
261
  for (const [index, diagnostic] of input.runtime.diagnostics.entries()) {
244
262
  addNode({
245
263
  id: `runtime-diagnostic-${index}`,
@@ -265,9 +283,11 @@ export function buildPortableRuntimeContextSnapshot(input) {
265
283
  });
266
284
  }
267
285
  const resourceDiagnostics = input.resources?.diagnostics ?? [];
268
- const warnings = input.runtime.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
286
+ const warnings = profile.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
287
+ + input.runtime.diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length
269
288
  + resourceDiagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
270
- const errors = input.runtime.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length
289
+ const errors = profile.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length
290
+ + input.runtime.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length
271
291
  + resourceDiagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
272
292
  return {
273
293
  version: 1,
@@ -287,6 +307,10 @@ export function buildPortableRuntimeContextSnapshot(input) {
287
307
  },
288
308
  nodes,
289
309
  diagnostics: [
310
+ ...profile.diagnostics.map((diagnostic) => ({
311
+ type: diagnostic.severity,
312
+ message: `[${diagnostic.code}] ${diagnostic.message}`,
313
+ })),
290
314
  ...input.runtime.diagnostics.map((diagnostic) => ({ type: diagnostic.severity, message: diagnostic.message })),
291
315
  ...resourceDiagnostics.map((diagnostic) => ({ type: diagnostic.severity, message: diagnostic.message })),
292
316
  ],
@@ -1,46 +1,96 @@
1
1
  import { AgentRuntimeContractError } from "./errors.js";
2
+ import { validateAgentRuntimeCapabilities } from "./capabilities.js";
2
3
  import { hasPendingNativeSession } from "../sessions/runtime-binding.js";
4
+ const REQUIRED_SESSION_METHODS = ["getBinding", "subscribe", "prompt", "abort", "dispose", "getStatus"];
5
+ const BINDING_STATES = new Set(["unbound", "bound", "missing", "error"]);
6
+ function isRecord(value) {
7
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
8
+ }
3
9
  export function validateAgentRuntimeSessionContract(session) {
4
10
  const errors = [];
5
- const capabilities = session.capabilities;
6
- const controls = session.controls;
11
+ if (!isRecord(session))
12
+ return ["session must be an object"];
13
+ for (const field of ["adapterId", "runtimeInstanceId", "cwd"]) {
14
+ if (typeof session[field] !== "string" || !session[field].trim()) {
15
+ errors.push(`session.${field} must be a non-empty string`);
16
+ }
17
+ }
18
+ for (const method of REQUIRED_SESSION_METHODS) {
19
+ if (typeof session[method] !== "function")
20
+ errors.push(`session.${method}() is required`);
21
+ }
22
+ let capabilities;
23
+ if (!isRecord(session.capabilities)) {
24
+ errors.push("session.capabilities must be an object");
25
+ }
26
+ else {
27
+ const capabilityErrors = validateAgentRuntimeCapabilities(session.capabilities);
28
+ errors.push(...capabilityErrors.map((error) => `session.capabilities.${error}`));
29
+ if (capabilityErrors.length === 0)
30
+ capabilities = session.capabilities;
31
+ }
32
+ const controls = isRecord(session.controls) ? session.controls : undefined;
33
+ if (session.controls !== undefined && !controls)
34
+ errors.push("session.controls must be an object when provided");
7
35
  const requireMethod = (enabled, path, method) => {
8
36
  if (enabled && typeof controls?.[method] !== "function") {
9
37
  errors.push(`${path} requires controls.${String(method)}()`);
10
38
  }
11
39
  };
12
- if (capabilities.input.steering && typeof session.steer !== "function") {
40
+ if (capabilities?.input.steering && typeof session.steer !== "function") {
13
41
  errors.push("input.steering requires session.steer()");
14
42
  }
15
- requireMethod(capabilities.lifecycle.listNativeSessions, "lifecycle.listNativeSessions", "listSessions");
16
- requireMethod(capabilities.lifecycle.fork, "lifecycle.fork", "forkSession");
17
- requireMethod(capabilities.lifecycle.clone, "lifecycle.clone", "cloneSession");
18
- requireMethod(capabilities.lifecycle.tree, "lifecycle.tree", "getSessionTree");
19
- requireMethod(capabilities.lifecycle.tree, "lifecycle.tree", "navigateSessionTree");
20
- requireMethod(capabilities.models.switchInSession, "models.switchInSession", "setModel");
21
- requireMethod(capabilities.reasoning.supported, "reasoning.supported", "getReasoning");
22
- requireMethod(capabilities.reasoning.supported, "reasoning.supported", "setReasoning");
23
- requireMethod(capabilities.approvals.supported, "approvals.supported", "respondToApproval");
24
- requireMethod(capabilities.approvals.structuredUserInput, "approvals.structuredUserInput", "respondToUserInput");
25
- requireMethod(capabilities.maintenance.compaction, "maintenance.compaction", "compact");
26
- const binding = session.getBinding();
27
- if (binding.adapterId !== session.adapterId) {
28
- errors.push(`binding.adapterId "${binding.adapterId}" does not match session.adapterId "${session.adapterId}"`);
29
- }
30
- if (binding.runtimeInstanceId !== session.runtimeInstanceId) {
31
- errors.push(`binding.runtimeInstanceId "${binding.runtimeInstanceId}" does not match session.runtimeInstanceId "${session.runtimeInstanceId}"`);
32
- }
33
- if (binding.state === "bound" && !binding.nativeSessionId) {
34
- errors.push("a bound session requires binding.nativeSessionId");
35
- }
36
- if (binding.state === "unbound" && binding.nativeSessionId && !hasPendingNativeSession(binding)) {
37
- errors.push("an unbound session must not expose binding.nativeSessionId");
43
+ if (capabilities) {
44
+ requireMethod(capabilities.lifecycle.listNativeSessions, "lifecycle.listNativeSessions", "listSessions");
45
+ requireMethod(capabilities.lifecycle.fork, "lifecycle.fork", "forkSession");
46
+ requireMethod(capabilities.lifecycle.clone, "lifecycle.clone", "cloneSession");
47
+ requireMethod(capabilities.lifecycle.tree, "lifecycle.tree", "getSessionTree");
48
+ requireMethod(capabilities.lifecycle.tree, "lifecycle.tree", "navigateSessionTree");
49
+ requireMethod(capabilities.models.switchInSession, "models.switchInSession", "setModel");
50
+ requireMethod(capabilities.reasoning.supported, "reasoning.supported", "getReasoning");
51
+ requireMethod(capabilities.reasoning.supported, "reasoning.supported", "setReasoning");
52
+ requireMethod(capabilities.approvals.supported, "approvals.supported", "respondToApproval");
53
+ requireMethod(capabilities.approvals.structuredUserInput, "approvals.structuredUserInput", "respondToUserInput");
54
+ requireMethod(capabilities.maintenance.compaction, "maintenance.compaction", "compact");
55
+ }
56
+ if (typeof session.getBinding === "function") {
57
+ try {
58
+ const binding = session.getBinding();
59
+ if (!isRecord(binding)) {
60
+ errors.push("session.getBinding() must return an object");
61
+ }
62
+ else {
63
+ for (const field of ["piboSessionId", "runtimeInstanceId", "adapterId"]) {
64
+ if (typeof binding[field] !== "string" || !binding[field].trim()) {
65
+ errors.push(`session.getBinding().${field} must be a non-empty string`);
66
+ }
67
+ }
68
+ if (!BINDING_STATES.has(String(binding.state))) {
69
+ errors.push("session.getBinding().state is invalid");
70
+ }
71
+ if (binding.adapterId !== session.adapterId) {
72
+ errors.push(`binding.adapterId "${String(binding.adapterId)}" does not match session.adapterId "${String(session.adapterId)}"`);
73
+ }
74
+ if (binding.runtimeInstanceId !== session.runtimeInstanceId) {
75
+ errors.push(`binding.runtimeInstanceId "${String(binding.runtimeInstanceId)}" does not match session.runtimeInstanceId "${String(session.runtimeInstanceId)}"`);
76
+ }
77
+ if (binding.state === "bound" && !binding.nativeSessionId) {
78
+ errors.push("a bound session requires binding.nativeSessionId");
79
+ }
80
+ if (binding.state === "unbound" && binding.nativeSessionId && !hasPendingNativeSession(binding)) {
81
+ errors.push("an unbound session must not expose binding.nativeSessionId");
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ errors.push("session.getBinding() must not throw during contract validation");
87
+ }
38
88
  }
39
89
  return errors;
40
90
  }
41
- export function assertAgentRuntimeSessionContract(session) {
91
+ export function assertAgentRuntimeSessionContract(session, runtimeInstanceId) {
42
92
  const errors = validateAgentRuntimeSessionContract(session);
43
93
  if (errors.length > 0) {
44
- throw new AgentRuntimeContractError(session.runtimeInstanceId, `Agent runtime session contract failed: ${errors.join("; ")}`);
94
+ throw new AgentRuntimeContractError(runtimeInstanceId ?? (isRecord(session) && typeof session.runtimeInstanceId === "string" ? session.runtimeInstanceId : "unknown"), `Agent runtime session contract failed: ${errors.join("; ")}`);
45
95
  }
46
96
  }
@@ -46,6 +46,9 @@ function boundedIdentifierText(value, fallback, maxLength = 512) {
46
46
  function optionalBoundedIdentifierText(value) {
47
47
  return value ? boundedIdentifierText(value, "portable-id") : undefined;
48
48
  }
49
+ function toolInvocationKey(turnId, toolCallId) {
50
+ return JSON.stringify([turnId ?? null, toolCallId]);
51
+ }
49
52
  function boundedText(value, maxBytes = MAX_ENTRY_BYTES) {
50
53
  if (Buffer.byteLength(value, "utf8") <= maxBytes)
51
54
  return value;
@@ -274,18 +277,22 @@ export class PiboDataPortableHistoryProvider {
274
277
  for (const row of rows) {
275
278
  if (!row.tool_call_id)
276
279
  continue;
280
+ const invocationKey = toolInvocationKey(row.turn_id, row.tool_call_id);
277
281
  if (row.type === "tool_call") {
278
282
  const attributes = parseObject(row.attributes_json);
279
- const existing = calls.get(row.tool_call_id);
283
+ const existing = calls.get(invocationKey);
280
284
  if (!existing || attributes.argsComplete === true)
281
- calls.set(row.tool_call_id, row);
285
+ calls.set(invocationKey, row);
282
286
  }
283
287
  else {
284
- results.set(row.tool_call_id, row);
288
+ results.set(invocationKey, row);
285
289
  }
286
290
  }
287
291
  const entries = [];
288
- for (const [toolCallId, row] of calls) {
292
+ for (const row of calls.values()) {
293
+ const toolCallId = row.tool_call_id;
294
+ if (!toolCallId)
295
+ continue;
289
296
  const attributes = parseObject(row.attributes_json);
290
297
  const portableToolCallId = boundedIdentifierText(toolCallId, `tool-call-${row.stream_id}`);
291
298
  const toolName = boundedIdentifierText(typeof attributes.toolName === "string" ? attributes.toolName : row.preview_text ?? "tool", "tool", 256);
@@ -309,7 +316,10 @@ export class PiboDataPortableHistoryProvider {
309
316
  toolName,
310
317
  });
311
318
  }
312
- for (const [toolCallId, row] of results) {
319
+ for (const row of results.values()) {
320
+ const toolCallId = row.tool_call_id;
321
+ if (!toolCallId)
322
+ continue;
313
323
  const attributes = parseObject(row.attributes_json);
314
324
  const portableToolCallId = boundedIdentifierText(toolCallId, `tool-call-${row.stream_id}`);
315
325
  const toolName = boundedIdentifierText(typeof attributes.toolName === "string" ? attributes.toolName : row.preview_text ?? "tool", "tool", 256);
@@ -374,18 +384,18 @@ function normalizeToolPairs(entries) {
374
384
  if (entry.type !== "message")
375
385
  continue;
376
386
  if (entry.role === "tool" && entry.toolCallId)
377
- resultIds.add(entry.toolCallId);
387
+ resultIds.add(toolInvocationKey(entry.turnId, entry.toolCallId));
378
388
  if (!Array.isArray(entry.content))
379
389
  continue;
380
390
  for (const part of entry.content) {
381
391
  if (part.type === "tool_call")
382
- callIds.add(part.toolCallId);
392
+ callIds.add(toolInvocationKey(entry.turnId, part.toolCallId));
383
393
  }
384
394
  }
385
395
  return entries.map((entry) => {
386
396
  if (entry.type !== "message")
387
397
  return entry;
388
- if (entry.role === "tool" && entry.toolCallId && !callIds.has(entry.toolCallId)) {
398
+ if (entry.role === "tool" && entry.toolCallId && !callIds.has(toolInvocationKey(entry.turnId, entry.toolCallId))) {
389
399
  return {
390
400
  ...entry,
391
401
  role: "user",
@@ -402,7 +412,7 @@ function normalizeToolPairs(entries) {
402
412
  if (entry.role !== "assistant" || !Array.isArray(entry.content))
403
413
  return entry;
404
414
  const content = entry.content.map((part) => {
405
- if (part.type !== "tool_call" || resultIds.has(part.toolCallId))
415
+ if (part.type !== "tool_call" || resultIds.has(toolInvocationKey(entry.turnId, part.toolCallId)))
406
416
  return part;
407
417
  return {
408
418
  type: "text",
@@ -450,10 +450,16 @@ export class AgentRuntimeAdapterRegistry {
450
450
  const adapter = this.requireInstance(instanceId);
451
451
  const session = await adapter.openSession(input);
452
452
  try {
453
- assertAgentRuntimeSessionContract(session);
453
+ assertAgentRuntimeSessionContract(session, instanceId);
454
454
  }
455
455
  catch (error) {
456
- await session.dispose().catch(() => { });
456
+ try {
457
+ if (typeof session?.dispose === "function")
458
+ await session.dispose();
459
+ }
460
+ catch {
461
+ // Preserve the admission error when best-effort cleanup also fails.
462
+ }
457
463
  throw error;
458
464
  }
459
465
  return session;
@@ -1,5 +1,6 @@
1
1
  import { PiboSteeringUnavailableError, } from "../core/events.js";
2
- import { runtimeSessionErrorDetails } from "../core/session-errors.js";
2
+ import { PIBO_PROVIDER_RECOVERY_PROMPT, isPiboProviderFallbackError } from "../core/provider-recovery.js";
3
+ import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "../core/session-errors.js";
3
4
  import { isPiboThinkingLevel } from "../core/thinking.js";
4
5
  import { AgentRuntimeCapabilityUnavailableError } from "./errors.js";
5
6
  const RUN_REMINDER_CAPABILITY_TOOLS = new Set([
@@ -13,7 +14,7 @@ const RUN_REMINDER_CAPABILITY_TOOLS = new Set([
13
14
  const RUN_REMINDER_MAX_TOOL_EXECUTIONS = 64;
14
15
  const RUN_REMINDER_MAX_PROVIDER_ROUNDS = 64;
15
16
  const RUN_REMINDER_MAX_ACTIVE_TOKENS = 2_000_000;
16
- const RUN_REMINDER_MAX_DURATION_MS = 15 * 60 * 1000;
17
+ export const RUN_REMINDER_MAX_DURATION_MS = 15 * 60 * 1000;
17
18
  const RUN_REMINDER_MAX_REPEATED_TOOL_CALLS = 12;
18
19
  function serializedToolArgs(value) {
19
20
  try {
@@ -29,6 +30,23 @@ function errorMessage(error) {
29
30
  function promptSource(source) {
30
31
  return source === "user" || source === "ui" ? "interactive" : "rpc";
31
32
  }
33
+ function sameModel(left, right) {
34
+ return Boolean(left && right && left.provider === right.provider && left.id === right.id);
35
+ }
36
+ function uniqueModels(models) {
37
+ const seen = new Set();
38
+ return models.flatMap((model) => {
39
+ const provider = model.provider.trim();
40
+ const id = model.id.trim();
41
+ if (!provider || !id)
42
+ return [];
43
+ const key = `${provider}\u0000${id}`;
44
+ if (seen.has(key))
45
+ return [];
46
+ seen.add(key);
47
+ return [{ provider, id }];
48
+ });
49
+ }
32
50
  function runtimeCapabilityError(session, capability) {
33
51
  return new AgentRuntimeCapabilityUnavailableError(capability, session.runtimeInstanceId, `Runtime instance "${session.runtimeInstanceId}" does not support ${capability}.`);
34
52
  }
@@ -120,6 +138,10 @@ export class RuntimeRoutedSession {
120
138
  nextThinkingIndex = 0;
121
139
  sessionIdentityOperationInFlight = false;
122
140
  forkCandidatesRequest;
141
+ primaryModel;
142
+ modelFallbacks;
143
+ suppressProviderFailures = false;
144
+ pendingProviderFailure;
123
145
  unsubscribe;
124
146
  constructor(piboSessionId, runtimeSession, emit, pluginRegistry, options = {}) {
125
147
  this.piboSessionId = piboSessionId;
@@ -128,6 +150,10 @@ export class RuntimeRoutedSession {
128
150
  this.pluginRegistry = pluginRegistry;
129
151
  this.options = options;
130
152
  this.runtime = runtimeSession.getNativeCompatibilityHandle?.() ?? runtimeSession;
153
+ this.primaryModel = runtimeSession.getStatus().activeModel
154
+ ? { ...runtimeSession.getStatus().activeModel }
155
+ : undefined;
156
+ this.modelFallbacks = uniqueModels(options.modelFallbacks ?? []);
131
157
  this.unsubscribe = runtimeSession.subscribe((event) => this.handleRuntimeEvent(event));
132
158
  }
133
159
  enqueueMessage(event, onAccepted = () => { }) {
@@ -182,14 +208,20 @@ export class RuntimeRoutedSession {
182
208
  this.assertActive();
183
209
  if (event.action === "compact")
184
210
  return this.enqueueCompactAction(event);
185
- const changesSessionIdentity = event.action === "session.fork" || event.action === "session.clone";
186
- if (changesSessionIdentity) {
187
- this.assertSessionIdentityOperationIdle(event.action === "session.fork" ? "fork" : "clone");
211
+ const sessionIdentityOperation = event.action === "session.fork"
212
+ ? "fork"
213
+ : event.action === "session.clone"
214
+ ? "clone"
215
+ : event.action === "session.switch"
216
+ ? "switch"
217
+ : undefined;
218
+ if (sessionIdentityOperation) {
219
+ this.assertSessionIdentityOperationIdle(sessionIdentityOperation);
188
220
  this.sessionIdentityOperationInFlight = true;
189
221
  this.notifyState();
190
222
  }
191
223
  try {
192
- if (changesSessionIdentity)
224
+ if (sessionIdentityOperation)
193
225
  await this.options.onBeforeSessionIdentityOperation?.(event);
194
226
  const result = await this.runAction(event);
195
227
  if (isSessionOperationResult(result))
@@ -205,7 +237,7 @@ export class RuntimeRoutedSession {
205
237
  return output;
206
238
  }
207
239
  finally {
208
- if (changesSessionIdentity) {
240
+ if (sessionIdentityOperation) {
209
241
  this.sessionIdentityOperationInFlight = false;
210
242
  this.notifyState();
211
243
  this.startDrain();
@@ -407,7 +439,9 @@ export class RuntimeRoutedSession {
407
439
  const setModel = this.runtimeSession.controls?.setModel;
408
440
  if (!setModel)
409
441
  throw runtimeCapabilityError(this.runtimeSession, "in-session model switching");
410
- return await setModel(model);
442
+ const selected = await setModel(model);
443
+ this.primaryModel = { ...selected };
444
+ return selected;
411
445
  }
412
446
  setThinkingLevel(level) {
413
447
  const setReasoning = this.runtimeSession.controls?.setReasoning;
@@ -642,10 +676,10 @@ export class RuntimeRoutedSession {
642
676
  }));
643
677
  return;
644
678
  case "error":
645
- this.emitRuntimeError(event.message, event.details);
679
+ this.handleRuntimeFailure(event.message, event.details);
646
680
  return;
647
681
  case "turn_failed":
648
- this.emitRuntimeError(event.message, event.details);
682
+ this.handleRuntimeFailure(event.message, event.details);
649
683
  return;
650
684
  case "native_event":
651
685
  this.options.onNativeEventTelemetry?.(this.piboSessionId, event.event, {
@@ -661,6 +695,14 @@ export class RuntimeRoutedSession {
661
695
  return;
662
696
  }
663
697
  }
698
+ handleRuntimeFailure(message, details) {
699
+ const normalized = normalizeSessionErrorDetails(message, details);
700
+ if (this.suppressProviderFailures && isPiboProviderFallbackError(message, normalized)) {
701
+ this.pendingProviderFailure = { message, details: normalized };
702
+ return;
703
+ }
704
+ this.emitRuntimeError(message, normalized);
705
+ }
664
706
  emitRuntimeError(message, details) {
665
707
  if (this.activeMessageFailed)
666
708
  return;
@@ -706,6 +748,71 @@ export class RuntimeRoutedSession {
706
748
  this.notifyState();
707
749
  }
708
750
  }
751
+ async promptWithModelFallbacks(event, inFlight) {
752
+ const setModel = this.runtimeSession.controls?.setModel;
753
+ const fallbackModels = this.modelFallbacks.filter((model) => !sameModel(model, this.primaryModel));
754
+ if (!setModel || fallbackModels.length === 0) {
755
+ await this.runtimeSession.prompt({
756
+ text: event.text,
757
+ source: promptSource(event.source),
758
+ capabilityScope: event.capabilityScope,
759
+ });
760
+ return;
761
+ }
762
+ this.suppressProviderFailures = true;
763
+ let switchedModel = false;
764
+ let finalFailure;
765
+ try {
766
+ for (let attempt = 0; attempt <= fallbackModels.length; attempt += 1) {
767
+ if (this.disposed || inFlight.cancelled)
768
+ return;
769
+ if (attempt > 0) {
770
+ try {
771
+ await setModel(fallbackModels[attempt - 1]);
772
+ switchedModel = true;
773
+ }
774
+ catch {
775
+ continue;
776
+ }
777
+ }
778
+ this.pendingProviderFailure = undefined;
779
+ let promptError;
780
+ try {
781
+ await this.runtimeSession.prompt({
782
+ text: attempt === 0 ? event.text : PIBO_PROVIDER_RECOVERY_PROMPT,
783
+ source: attempt === 0 ? promptSource(event.source) : "rpc",
784
+ capabilityScope: event.capabilityScope,
785
+ });
786
+ }
787
+ catch (error) {
788
+ promptError = error;
789
+ }
790
+ if (this.disposed || inFlight.cancelled)
791
+ return;
792
+ const failure = this.pendingProviderFailure;
793
+ if (!failure) {
794
+ if (promptError)
795
+ throw promptError;
796
+ return;
797
+ }
798
+ finalFailure = failure;
799
+ }
800
+ if (finalFailure)
801
+ this.emitRuntimeError(finalFailure.message, finalFailure.details);
802
+ }
803
+ finally {
804
+ this.suppressProviderFailures = false;
805
+ this.pendingProviderFailure = undefined;
806
+ if (switchedModel && this.primaryModel && !this.disposed) {
807
+ try {
808
+ await setModel(this.primaryModel);
809
+ }
810
+ catch (error) {
811
+ console.warn(`Failed to restore primary model for Pibo session "${this.piboSessionId}": ${errorMessage(error)}`);
812
+ }
813
+ }
814
+ }
815
+ }
709
816
  async processQueuedMessage(event) {
710
817
  const inFlight = this.beginInFlightMessage(event);
711
818
  try {
@@ -743,11 +850,7 @@ export class RuntimeRoutedSession {
743
850
  });
744
851
  if (inFlight.cancelled)
745
852
  return;
746
- await this.runtimeSession.prompt({
747
- text: event.text,
748
- source: promptSource(event.source),
749
- capabilityScope: event.capabilityScope,
750
- });
853
+ await this.promptWithModelFallbacks(event, inFlight);
751
854
  if (this.disposed || inFlight.cancelled)
752
855
  return;
753
856
  if (!this.activeMessageFailed) {
@@ -776,6 +879,8 @@ export class RuntimeRoutedSession {
776
879
  }
777
880
  }
778
881
  finally {
882
+ this.suppressProviderFailures = false;
883
+ this.pendingProviderFailure = undefined;
779
884
  this.activeMessage = undefined;
780
885
  this.activeMessageFailed = false;
781
886
  this.resetContentIndices();