@mono-agent/agent-runtime 0.20.11 → 0.21.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 (148) hide show
  1. package/ARCHITECTURE.md +50 -11
  2. package/MIGRATION.md +288 -26
  3. package/README.md +352 -477
  4. package/package.json +13 -44
  5. package/src/agent/tool-bloat.js +145 -9
  6. package/src/agent/tools/agent-tool.js +108 -9
  7. package/src/agent/tools/bash.js +11 -26
  8. package/src/agent/tools/codex-subscription-search.js +123 -29
  9. package/src/agent/tools/exec.js +10 -2
  10. package/src/agent/tools/index.js +7 -0
  11. package/src/agent/tools/monitor.js +149 -0
  12. package/src/agent/tools/pi-bridge.js +123 -19
  13. package/src/agent/tools/shared/bash-environment.js +31 -0
  14. package/src/agent/tools/shared/monitors.js +293 -0
  15. package/src/agent/tools/shared/path-resolver.js +25 -6
  16. package/src/agent/tools/shared/process-jobs.js +6 -1
  17. package/src/agent/tools/shared/process-runner.js +26 -6
  18. package/src/agent/tools/shared/tool-context.js +8 -0
  19. package/src/agent/tools/web-access-interstitial.js +70 -0
  20. package/src/agent/tools/web-browser-render.js +83 -58
  21. package/src/agent/tools/web-controller.js +112 -21
  22. package/src/agent/tools/web-document-extractor.js +379 -0
  23. package/src/agent/tools/web-fetch.js +271 -243
  24. package/src/agent/tools/web-request.js +65 -0
  25. package/src/agent/tools/web-search-output.js +165 -0
  26. package/src/agent/tools/web-search-state.js +75 -0
  27. package/src/agent/tools/web-search.js +532 -71
  28. package/src/ai/cost.js +13 -68
  29. package/src/ai/failure.js +3 -3
  30. package/src/ai/index.js +5 -17
  31. package/src/ai/observer.js +8 -0
  32. package/src/ai/pi-interop.js +221 -1
  33. package/src/ai/pi-oauth-compat.js +1 -1
  34. package/src/ai/provider-check.js +131 -0
  35. package/src/ai/providers/codex/app-server-client.js +592 -0
  36. package/src/ai/providers/pi-models.js +18 -10
  37. package/src/ai/providers/pi-native/compaction-driver.js +94 -42
  38. package/src/ai/providers/pi-native/compaction-summary.js +140 -0
  39. package/src/ai/providers/pi-native/harness-adapter.js +376 -0
  40. package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
  41. package/src/ai/providers/pi-native/provider-attribution.js +102 -0
  42. package/src/ai/providers/pi-native/result-builder.js +38 -14
  43. package/src/ai/providers/pi-native/session-lifecycle.js +253 -55
  44. package/src/ai/providers/pi-native/stream-subscriber.js +52 -6
  45. package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
  46. package/src/ai/providers/pi-native/turn-runner.js +279 -28
  47. package/src/ai/providers/pi-native.js +206 -61
  48. package/src/ai/runtime/capabilities.js +11 -56
  49. package/src/ai/runtime/live-input-events.js +250 -54
  50. package/src/ai/runtime/model-refs.js +118 -153
  51. package/src/ai/runtime/registry.js +22 -56
  52. package/src/ai/runtime/router.js +76 -417
  53. package/src/ai/runtime/session-liveness.js +3 -4
  54. package/src/ai/runtime/sessions.js +4 -5
  55. package/src/ai/runtime/tool-policy.js +0 -2
  56. package/src/ai/tool-lifecycle.js +32 -18
  57. package/src/ai/types.js +37 -112
  58. package/src/index.js +0 -6
  59. package/src/runtime.js +29 -16
  60. package/types/agent/tool-bloat.d.ts +1 -1
  61. package/types/agent/tools/agent-tool.d.ts +4 -2
  62. package/types/agent/tools/bash.d.ts +5 -3
  63. package/types/agent/tools/codex-subscription-search.d.ts +7 -3
  64. package/types/agent/tools/exec.d.ts +5 -3
  65. package/types/agent/tools/index.d.ts +1 -0
  66. package/types/agent/tools/monitor.d.ts +47 -0
  67. package/types/agent/tools/pi-bridge.d.ts +7 -4
  68. package/types/agent/tools/shared/bash-environment.d.ts +4 -0
  69. package/types/agent/tools/shared/monitors.d.ts +98 -0
  70. package/types/agent/tools/shared/process-jobs.d.ts +5 -1
  71. package/types/agent/tools/shared/process-runner.d.ts +14 -4
  72. package/types/agent/tools/shared/tool-context.d.ts +2 -0
  73. package/types/agent/tools/web-access-interstitial.d.ts +23 -0
  74. package/types/agent/tools/web-browser-render.d.ts +4 -1
  75. package/types/agent/tools/web-controller.d.ts +4 -2
  76. package/types/agent/tools/web-document-extractor.d.ts +27 -0
  77. package/types/agent/tools/web-fetch.d.ts +19 -24
  78. package/types/agent/tools/web-request.d.ts +20 -0
  79. package/types/agent/tools/web-search-output.d.ts +31 -0
  80. package/types/agent/tools/web-search-state.d.ts +21 -0
  81. package/types/agent/tools/web-search.d.ts +10 -45
  82. package/types/ai/cost.d.ts +1 -2
  83. package/types/ai/index.d.ts +2 -4
  84. package/types/ai/observer.d.ts +6 -0
  85. package/types/ai/pi-interop.d.ts +81 -0
  86. package/types/ai/provider-check.d.ts +53 -0
  87. package/types/ai/providers/codex/app-server-client.d.ts +37 -0
  88. package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
  89. package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
  90. package/types/ai/providers/pi-native/harness-adapter.d.ts +58 -0
  91. package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
  92. package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
  93. package/types/ai/providers/pi-native/result-builder.d.ts +14 -4
  94. package/types/ai/providers/pi-native/session-lifecycle.d.ts +25 -6
  95. package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
  96. package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
  97. package/types/ai/providers/pi-native/turn-runner.d.ts +68 -10
  98. package/types/ai/providers/pi-native.d.ts +21 -4
  99. package/types/ai/runtime/capabilities.d.ts +21 -70
  100. package/types/ai/runtime/live-input-events.d.ts +32 -8
  101. package/types/ai/runtime/model-refs.d.ts +0 -24
  102. package/types/ai/runtime/router.d.ts +3 -10
  103. package/types/ai/runtime/tool-policy.d.ts +0 -2
  104. package/types/ai/tool-lifecycle.d.ts +4 -3
  105. package/types/ai/types.d.ts +162 -256
  106. package/types/index.d.ts +0 -1
  107. package/src/ai/providers/acp-client.js +0 -1149
  108. package/src/ai/providers/acp-privacy.js +0 -124
  109. package/src/ai/providers/acp-public.js +0 -21
  110. package/src/ai/providers/acp-session-tokens.js +0 -282
  111. package/src/ai/providers/acp-transport.js +0 -356
  112. package/src/ai/providers/acp.js +0 -543
  113. package/src/ai/providers/claude-cli.js +0 -883
  114. package/src/ai/providers/claude-sandbox.js +0 -71
  115. package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
  116. package/src/ai/providers/claude-sdk-discovery.js +0 -352
  117. package/src/ai/providers/claude-sdk.js +0 -1127
  118. package/src/ai/providers/claude-subagent-activity.js +0 -719
  119. package/src/ai/providers/claude-subagents.js +0 -88
  120. package/src/ai/providers/codex-app.js +0 -2946
  121. package/src/ai/providers/opencode-app.js +0 -1109
  122. package/src/ai/providers/opencode-discovery.js +0 -39
  123. package/src/ai/providers/opencode-server.js +0 -508
  124. package/src/ai/runtime/context-windows.js +0 -46
  125. package/src/ai/runtime/fast-mode.js +0 -8
  126. package/src/ai/streaming/codex-events.js +0 -146
  127. package/src/ai/streaming/opencode-events.js +0 -59
  128. package/types/ai/providers/acp-client.d.ts +0 -227
  129. package/types/ai/providers/acp-privacy.d.ts +0 -25
  130. package/types/ai/providers/acp-public.d.ts +0 -7
  131. package/types/ai/providers/acp-session-tokens.d.ts +0 -41
  132. package/types/ai/providers/acp-transport.d.ts +0 -45
  133. package/types/ai/providers/acp.d.ts +0 -93
  134. package/types/ai/providers/claude-cli.d.ts +0 -305
  135. package/types/ai/providers/claude-sandbox.d.ts +0 -79
  136. package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
  137. package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
  138. package/types/ai/providers/claude-sdk.d.ts +0 -138
  139. package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
  140. package/types/ai/providers/claude-subagents.d.ts +0 -18
  141. package/types/ai/providers/codex-app.d.ts +0 -151
  142. package/types/ai/providers/opencode-app.d.ts +0 -96
  143. package/types/ai/providers/opencode-discovery.d.ts +0 -4
  144. package/types/ai/providers/opencode-server.d.ts +0 -20
  145. package/types/ai/runtime/context-windows.d.ts +0 -9
  146. package/types/ai/runtime/fast-mode.d.ts +0 -2
  147. package/types/ai/streaming/codex-events.d.ts +0 -40
  148. package/types/ai/streaming/opencode-events.d.ts +0 -42
@@ -32,12 +32,30 @@ function toolResultOutcome(result) {
32
32
  code: "code",
33
33
  backend: "backend",
34
34
  signal: "signal",
35
+ contentKind: "content_kind",
36
+ charset: "charset",
37
+ charsetSource: "charset_source",
38
+ extractionStage: "extraction_stage",
39
+ renderReason: "render_reason",
40
+ retryAt: "retry_at",
41
+ nextAction: "next_action",
35
42
  };
36
43
  const numbers = {
37
44
  attempts: "attempts",
45
+ queueWaitMs: "queue_wait_ms",
46
+ backendDurationMs: "backend_duration_ms",
47
+ retryAfterMs: "retry_after_ms",
48
+ maxRequestsPerRun: "max_requests_per_run",
49
+ requestsThisCall: "requests_this_call",
50
+ requestsUsed: "requests_used",
51
+ requestsRemaining: "requests_remaining",
52
+ cooldownSkipCount: "cooldown_skip_count",
53
+ quotaSkipCount: "quota_skip_count",
38
54
  bytes: "bytes",
39
55
  exitCode: "exit_code",
40
56
  statusCode: "status_code",
57
+ redirectCount: "redirect_count",
58
+ parserFailureCount: "parser_failure_count",
41
59
  };
42
60
  const booleans = {
43
61
  retryable: "retryable",
@@ -45,6 +63,11 @@ function toolResultOutcome(result) {
45
63
  truncated: "truncated",
46
64
  timedOut: "timed_out",
47
65
  rendered: "rendered",
66
+ renderFailed: "render_failed",
67
+ browserRecommended: "browser_recommended",
68
+ retryInRun: "retry_in_run",
69
+ fallbackUsed: "fallback_used",
70
+ hadDecodingReplacement: "had_decoding_replacement",
48
71
  };
49
72
  for (const [input, output] of Object.entries(strings)) {
50
73
  if (typeof source[input] === "string") bounded[output] = source[input].slice(0, 120);
@@ -76,8 +99,8 @@ function toolResultOutcome(result) {
76
99
 
77
100
  /**
78
101
  * Build the harness subscribe handler. `harness` is passed for the maxTurns
79
- * abort; it is already constructed when this is wired (subscribe follows the
80
- * AgentHarness constructor).
102
+ * abort; it is already constructed when this is wired (subscribe follows
103
+ * AgentHarness.create()).
81
104
  * @param {StreamSubscriberState} runState
82
105
  * @param {{onEvent: (event: any) => void, options: any, toolLimits: any, harness: any, sdk: string, model: string}} deps
83
106
  * @returns {(event: any) => void}
@@ -107,21 +130,44 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
107
130
  onEvent({ type: "assistant", message: { content: [{ type: "thinking", text: streamEvent.content }] } });
108
131
  }
109
132
  }
110
- } else if (event.type === "message_end") {
133
+ } else if (event.type === "message_end" && event.message?.role === "assistant") {
111
134
  const contextUsage = contextUsageFromAssistantMessage(event.message);
112
135
  if (contextUsage) {
136
+ const { costUsd, ...contextTokens } = contextUsage;
113
137
  const contextWindow = Number(harness?.getModel?.()?.contextWindow) || 0;
114
138
  const measurementId = typeof event.message?.id === "string" && event.message.id.trim().length > 0
115
139
  ? event.message.id
116
140
  : undefined;
117
141
  onEvent({
118
142
  type: "context_usage",
143
+ ...harness?.getPromptCacheRequest?.(),
119
144
  sdk,
120
145
  model,
121
146
  timestamp: Date.now(),
122
147
  ...(measurementId === undefined ? {} : { measurementId }),
123
148
  ...(contextWindow > 0 ? { contextWindow } : {}),
124
- tokens: contextUsage,
149
+ tokens: contextTokens,
150
+ costUsd,
151
+ providerCostUsd: typeof event.message.usage?.cost?.total === "number" && Number.isFinite(event.message.usage.cost.total) && event.message.usage.cost.total >= 0 ? event.message.usage.cost.total : null,
152
+ costSource: "pi_usage",
153
+ });
154
+ }
155
+ const hasVisibleContent = Array.isArray(event.message?.content)
156
+ && event.message.content.some((block) => block
157
+ && typeof block === "object"
158
+ && ((block.type === "text"
159
+ && typeof block.text === "string"
160
+ && block.text.length > 0)
161
+ || (block.type === "thinking"
162
+ && ((typeof block.thinking === "string" && block.thinking.length > 0)
163
+ || (typeof block.text === "string" && block.text.length > 0)))));
164
+ if (hasVisibleContent) {
165
+ const messageId = typeof event.message?.id === "string" && event.message.id.trim().length > 0
166
+ ? event.message.id
167
+ : undefined;
168
+ onEvent({
169
+ type: "assistant_message_boundary",
170
+ ...(messageId === undefined ? {} : { messageId }),
125
171
  });
126
172
  }
127
173
  } else if (event.type === "tool_execution_start") {
@@ -198,7 +244,7 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
198
244
  });
199
245
  } else if (event.type === "turn_end") {
200
246
  runState.turnCount += 1;
201
- // NON-DELEGABLE (verified against @earendil-works/pi-agent-core 0.80.3).
247
+ // NON-DELEGABLE (verified against @earendil-works/pi-agent-core 0.85.1).
202
248
  // pi's only after-turn stop hook is `shouldStopAfterTurn` on the LOW-LEVEL
203
249
  // `AgentLoopConfig` (dist/types.d.ts) — the config passed to the raw
204
250
  // `agentLoop`. It is NOT surfaced on `AgentHarnessOptions`
@@ -217,7 +263,7 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
217
263
  && runState.turnCount >= Number(options.maxTurns)
218
264
  && event.message?.stopReason === "toolUse") {
219
265
  runState.maxTurnsHit = true;
220
- harness.abort();
266
+ void Promise.resolve(harness.abort()).catch(() => {});
221
267
  }
222
268
  }
223
269
  };
@@ -0,0 +1,40 @@
1
+ // @ts-check
2
+ import { transformMessages } from "@earendil-works/pi-ai/api/transform-messages";
3
+
4
+ /** Validate the effective provider projection; Pi supplies honest missing results. */
5
+ export function validRecoveryProjection(messages, model) {
6
+ try {
7
+ const projected = transformMessages(messages, model);
8
+ const seen = new Set();
9
+ const pending = new Map();
10
+ for (const message of projected) {
11
+ if (!["user", "assistant", "toolResult"].includes(message?.role)) return false;
12
+ if (message.role !== "toolResult" && pending.size) return false;
13
+ if (message.role === "user" && typeof message.content === "string") continue;
14
+ if (!Array.isArray(message.content)) return false;
15
+ if (message.role === "toolResult") {
16
+ if (!pending.has(message.toolCallId) || pending.get(message.toolCallId) !== message.toolName) return false;
17
+ pending.delete(message.toolCallId);
18
+ }
19
+ if (message.role === "assistant" && ["error", "aborted", "deferred"].includes(message.stopReason)) return false;
20
+ for (const block of message.content) {
21
+ if (block?.type === "text") { if (typeof block.text !== "string") return false; }
22
+ else if (block?.type === "image") { if (typeof block.data !== "string" || typeof block.mimeType !== "string") return false; }
23
+ else if (block?.type === "thinking") {
24
+ if (message.role !== "assistant" || typeof block.thinking !== "string"
25
+ || (block.thinkingSignature !== undefined && (typeof block.thinkingSignature !== "string" || !block.thinkingSignature))) return false;
26
+ if (block.thinkingSignature && String(model.api).includes("responses")) {
27
+ const signature = JSON.parse(block.thinkingSignature);
28
+ if (signature?.type !== "reasoning" || typeof signature.id !== "string" || !Array.isArray(signature.summary)) return false;
29
+ }
30
+ } else if (block?.type === "toolCall") {
31
+ if (message.role !== "assistant" || typeof block.id !== "string" || !block.id || seen.has(block.id)
32
+ || typeof block.name !== "string" || !block.name || !block.arguments || typeof block.arguments !== "object") return false;
33
+ seen.add(block.id);
34
+ pending.set(block.id, block.name);
35
+ } else return false;
36
+ }
37
+ }
38
+ return pending.size === 0;
39
+ } catch { return false; }
40
+ }
@@ -7,7 +7,6 @@
7
7
  // state; run state (harness, removeAbortHandler, externalAbort) lives on the
8
8
  // caller-owned runState.
9
9
 
10
- import { AgentHarness } from "@earendil-works/pi-agent-core";
11
10
  import {
12
11
  createStructuredOutputTool,
13
12
  getPiBuiltinTools,
@@ -17,6 +16,7 @@ import { createNodeReplController } from "../../../agent/tools/node-repl.js";
17
16
  import { createWebToolController } from "../../../agent/tools/web-controller.js";
18
17
  import { readToolRuntime } from "../../../agent/tools/shared/runtime-context.js";
19
18
  import { formatLiveInputGuidance } from "../../live-input-prompt.js";
19
+ import { createPiHarnessAdapter } from "./harness-adapter.js";
20
20
  import { appendStructuredOutputInstruction } from "./structured-output.js";
21
21
  import { createStreamSubscriber } from "./stream-subscriber.js";
22
22
 
@@ -78,6 +78,8 @@ export async function buildTurnTools(runState, {
78
78
  ? null
79
79
  : createWebToolController({
80
80
  searchConfig: options.webSearchConfig,
81
+ searchState: options.webSearchState,
82
+ coordinator: options.webRequestCoordinator,
81
83
  fetchConfig: options.webFetchConfig,
82
84
  sandboxPolicy: options.sandboxPolicy,
83
85
  sandboxEngine,
@@ -121,13 +123,14 @@ export async function buildTurnTools(runState, {
121
123
  nodeReplController,
122
124
  webController,
123
125
  processJobsController: options.processJobs,
126
+ processJobsAvailability: options.processJobsAvailability,
127
+ monitorsController: options.monitors,
124
128
  toolExecutionMode,
125
129
  subagents: options.subagents,
126
130
  // The child inherits the parent's route and workspace unless its profile
127
131
  // pins a model; the tool closure reads these to build each child request.
128
132
  subagentContext: {
129
133
  model: options.model,
130
- executionMode: options.executionMode,
131
134
  cwd: options.cwd,
132
135
  parentRunId: runCtx?.runId,
133
136
  // Same policy + engine this turn's own tools are confined by, so a
@@ -143,6 +146,9 @@ export async function buildTurnTools(runState, {
143
146
  skills: options.skills,
144
147
  skillsRoot: options.skillsRoot,
145
148
  toolEnvironment: options.toolEnvironment,
149
+ webSearchConfig: options.webSearchConfig,
150
+ webRequestCoordinator: options.webRequestCoordinator,
151
+ webFetchConfig: options.webFetchConfig,
146
152
  },
147
153
  ctx: runCtx,
148
154
  }));
@@ -163,6 +169,7 @@ export async function buildTurnTools(runState, {
163
169
  qaOutputDir,
164
170
  onTruncate,
165
171
  limits: toolLimits,
172
+ mcpCallNoTotalTimeoutTools: options.mcpCallNoTotalTimeoutTools,
166
173
  toolPayloadMaxBytes: toolLimits.toolPayloadMaxBytes,
167
174
  sandboxPolicy: options.sandboxPolicy,
168
175
  sandboxEngine,
@@ -251,7 +258,7 @@ export function toolResultErrorOverride(details) {
251
258
  return failed ? { isError: true } : undefined;
252
259
  }
253
260
 
254
- export function buildTurnHarness(runState, {
261
+ export async function buildTurnHarness(runState, {
255
262
  session,
256
263
  piModels,
257
264
  model,
@@ -263,18 +270,9 @@ export function buildTurnHarness(runState, {
263
270
  maxRetries,
264
271
  maxRetryDelayMs,
265
272
  steeringMode,
266
- onEvent,
267
273
  options,
268
- toolLimits,
269
- sdk,
270
- reference,
271
274
  }) {
272
- // pi-agent-core 0.83.0 removed `env` from AgentHarnessOptions: an
273
- // ExecutionEnv now reaches tools through the generic per-turn `toolContext`
274
- // instead. mono-agent needs neither — it uses none of pi's built-in
275
- // file/shell tools, and its own tools close over what they need — so the
276
- // option is dropped rather than migrated.
277
- const harness = new AgentHarness({
275
+ const harness = await createPiHarnessAdapter(session, {
278
276
  session,
279
277
  models: piModels,
280
278
  model,
@@ -284,6 +282,8 @@ export function buildTurnHarness(runState, {
284
282
  streamOptions: { transport, maxRetries, maxRetryDelayMs },
285
283
  steeringMode,
286
284
  followUpMode: steeringMode,
285
+ promptCacheDiagnostics: options.promptCacheDiagnostics,
286
+ onEvent: options.onEvent,
287
287
  });
288
288
  // MCP `CallToolResult.isError` is a successful protocol response, so pi's
289
289
  // execute() promise resolves. The bridge records that bit in result details;
@@ -297,8 +297,37 @@ export function buildTurnHarness(runState, {
297
297
  // every resolved execute(), so without this the model would be told a failed,
298
298
  // timed-out, or empty delegation succeeded.
299
299
  harness.on("tool_result", (event) => toolResultErrorOverride(event?.details));
300
+ // Pi 0.85 records in-flight work durably. mono-agent tools do not yet use the
301
+ // new replay-memo contract, so abort an interrupted operation instead of
302
+ // risking a duplicate external effect. Do this before subscribing so stale
303
+ // recovery events cannot contaminate the new run's transcript counters.
304
+ try {
305
+ await harness.abortOpenOperations();
306
+ } catch (error) {
307
+ try { await harness.close(); } catch { /* best-effort */ }
308
+ throw error;
309
+ }
310
+
300
311
  runState.harness = harness;
301
312
 
313
+ return harness;
314
+ }
315
+
316
+ /**
317
+ * Subscribe only after prior history has been seeded. Pi emits message
318
+ * lifecycle events for manual appendMessage() calls; subscribing earlier makes
319
+ * restored assistant messages look like fresh output boundaries.
320
+ * @param {any} runState
321
+ * @param {{harness: any, onEvent: (event: any) => void, options: any, toolLimits: any, sdk: string, reference: string}} deps
322
+ */
323
+ export function activateTurnHarness(runState, {
324
+ harness,
325
+ onEvent,
326
+ options,
327
+ toolLimits,
328
+ sdk,
329
+ reference,
330
+ }) {
302
331
  harness.subscribe(createStreamSubscriber(runState, {
303
332
  onEvent,
304
333
  options,
@@ -310,13 +339,12 @@ export function buildTurnHarness(runState, {
310
339
 
311
340
  const abortHandler = () => {
312
341
  runState.externalAbort = true;
313
- harness.abort();
342
+ void harness.abort().catch(() => {});
314
343
  };
315
344
  if (options.abortSignal) {
316
345
  options.abortSignal.addEventListener("abort", abortHandler, { once: true });
317
346
  runState.removeAbortHandler = () => options.abortSignal.removeEventListener?.("abort", abortHandler);
318
347
  }
319
- return harness;
320
348
  }
321
349
 
322
350
  /**
@@ -324,10 +352,10 @@ export function buildTurnHarness(runState, {
324
352
  * the harness mid-run; the consumer is tied to run completion (an internal
325
353
  * runComplete flag) so it stops steering once the run finishes and does not
326
354
  * swallow a follow-up meant for a later turn. Returns a `stop()` teardown.
327
- * @param {{harness: any, options: any, onEvent: (event: any) => void}} deps
355
+ * @param {{harness: any, options: any, onEvent: (event: any) => void, promptEpoch?: any}} deps
328
356
  * @returns {{stop: () => Promise<void>}}
329
357
  */
330
- export function startLiveInput({ harness, options, onEvent }) {
358
+ export function startLiveInput({ harness, options, onEvent, promptEpoch }) {
331
359
  if (!options.liveInput) return { stop: async () => {} };
332
360
  const iterator = typeof options.liveInput[Symbol.asyncIterator] === "function"
333
361
  ? options.liveInput[Symbol.asyncIterator]()
@@ -336,6 +364,8 @@ export function startLiveInput({ harness, options, onEvent }) {
336
364
  /** @type {() => void} */
337
365
  let signalStop = () => {};
338
366
  const stopped = new Promise((resolve) => { signalStop = () => resolve(); });
367
+ /** @type {Array<{entryId: string, message: any}>} */
368
+ const acceptedEntries = [];
339
369
  const task = (async () => {
340
370
  try {
341
371
  while (!runComplete && !options.abortSignal?.aborted) {
@@ -345,8 +375,19 @@ export function startLiveInput({ harness, options, onEvent }) {
345
375
  ]);
346
376
  if (next.done || runComplete || options.abortSignal?.aborted) break;
347
377
  try {
348
- await harness.steer(formatLiveInputGuidance(next.value.body, options.prompts));
349
- next.value.acknowledge?.();
378
+ const entryId = await harness.steer(formatLiveInputGuidance(next.value.body, options.prompts));
379
+ if (typeof entryId !== "string" || entryId.length === 0) {
380
+ next.value.accepted?.();
381
+ next.value.uncertain?.({ reason: "delivery_uncertain" });
382
+ continue;
383
+ }
384
+ const evidence = {
385
+ providerEntryId: entryId,
386
+ ...(promptEpoch?.ownedRunId() === undefined ? {} : { providerRunId: promptEpoch.ownedRunId() }),
387
+ };
388
+ acceptedEntries.push({ entryId, message: next.value });
389
+ next.value.accepted?.(evidence);
390
+ promptEpoch?.register(entryId, next.value);
350
391
  } catch (err) {
351
392
  next.value.reject?.(err);
352
393
  throw err;
@@ -360,6 +401,8 @@ export function startLiveInput({ harness, options, onEvent }) {
360
401
  });
361
402
  }
362
403
  })();
404
+ /** @type {Promise<void>|undefined} */
405
+ let stopPromise;
363
406
  return {
364
407
  // The run is done: stop the live-steering consumer so it cannot steer a
365
408
  // finished harness or swallow a follow-up meant for the next turn. We signal
@@ -367,15 +410,214 @@ export function startLiveInput({ harness, options, onEvent }) {
367
410
  // race releases the task even when a third-party iterator's return() does
368
411
  // not unblock its pending next(); awaiting the task still closes any steer
369
412
  // acknowledgement already in progress.
370
- stop: async () => {
413
+ stop: () => {
414
+ stopPromise ??= performStop();
415
+ return stopPromise;
416
+ },
417
+ };
418
+
419
+ async function performStop() {
371
420
  runComplete = true;
372
421
  signalStop();
373
422
  if (iterator && typeof iterator.return === "function") {
374
423
  try { void Promise.resolve(iterator.return()).catch(() => {}); } catch { /* best-effort */ }
375
424
  }
376
425
  await task;
426
+ for (const accepted of acceptedEntries) {
427
+ if (promptEpoch?.isConsumed(accepted.entryId)) continue;
428
+ try {
429
+ if (typeof harness.cancelQueued !== "function") {
430
+ accepted.message.uncertain?.({
431
+ reason: "delivery_uncertain",
432
+ providerEntryId: accepted.entryId,
433
+ ...(promptEpoch?.ownedRunId() === undefined ? {} : { providerRunId: promptEpoch.ownedRunId() }),
434
+ });
435
+ continue;
436
+ }
437
+ const cancellation = await harness.cancelQueued(accepted.entryId);
438
+ if (cancellation?.kind === "cancelled") {
439
+ accepted.message.reject?.({ code: "native_queue_removed" });
440
+ } else if (!promptEpoch?.isConsumed(accepted.entryId)) {
441
+ accepted.message.uncertain?.({
442
+ reason: "delivery_uncertain",
443
+ providerEntryId: accepted.entryId,
444
+ ...(promptEpoch?.ownedRunId() === undefined ? {} : { providerRunId: promptEpoch.ownedRunId() }),
445
+ });
446
+ }
447
+ } catch {
448
+ accepted.message.uncertain?.({
449
+ reason: "delivery_uncertain",
450
+ providerEntryId: accepted.entryId,
451
+ ...(promptEpoch?.ownedRunId() === undefined ? {} : { providerRunId: promptEpoch.ownedRunId() }),
452
+ });
453
+ onEvent({
454
+ type: "runtime_warning",
455
+ warning_kind: "live_input_cancellation_failed",
456
+ message: "Unable to prove whether queued live input was removed.",
457
+ });
458
+ }
459
+ }
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Own exact Pi run/entry correlation for the one main prompt in this Mono run.
465
+ * @param {{harness: any, onEvent: (event: any) => void}} deps
466
+ */
467
+ export function createLiveInputPromptEpoch({ harness, onEvent }) {
468
+ const BUFFER_LIMIT = 101;
469
+ /** @type {string|undefined} */
470
+ let runId;
471
+ let eventWindowClosed = false;
472
+ let invalid = false;
473
+ /** @type {Array<{entryId: string, runId: string}>} */
474
+ const buffered = [];
475
+ /** @type {Map<string, {message: any, observed: boolean, consumed: boolean}>} */
476
+ const entries = new Map();
477
+ // The operation id Pi admitted for the main prompt (via confirm) or that the
478
+ // settled prompt reported (via finish). Consumption is only acknowledged once
479
+ // the observed main-lane run_start carries this exact id.
480
+ /** @type {string|undefined} */
481
+ let admittedOperationId;
482
+ let operationConfirmed = false;
483
+
484
+ const remove = harness.subscribe((event) => {
485
+ if (!event || event.lane !== "main") return;
486
+ if (event.type === "run_start" && typeof event.runId === "string" && event.runId.length > 0) {
487
+ if (runId === undefined) {
488
+ runId = event.runId;
489
+ if (admittedOperationId !== undefined && admittedOperationId !== runId) {
490
+ invalidate("operation_mismatch");
491
+ return;
492
+ }
493
+ if (admittedOperationId !== undefined) operationConfirmed = true;
494
+ consumeBuffered();
495
+ } else if (runId !== event.runId) {
496
+ invalidate("multiple_run_start");
497
+ }
498
+ return;
499
+ }
500
+ if (event.type === "run_end" && event.runId === runId) {
501
+ eventWindowClosed = true;
502
+ return;
503
+ }
504
+ if (
505
+ event.type !== "message_end"
506
+ || eventWindowClosed
507
+ || event.message?.role !== "user"
508
+ || typeof event.entryId !== "string"
509
+ || event.entryId.length === 0
510
+ || typeof event.runId !== "string"
511
+ || event.runId.length === 0
512
+ ) return;
513
+ if (buffered.some((item) => item.entryId === event.entryId && item.runId === event.runId)) {
514
+ consumeBuffered();
515
+ return;
516
+ }
517
+ if (buffered.length >= BUFFER_LIMIT) {
518
+ invalidate("event_buffer_overflow");
519
+ return;
520
+ }
521
+ buffered.push({ entryId: event.entryId, runId: event.runId });
522
+ consumeBuffered();
523
+ });
524
+
525
+ return {
526
+ ownedRunId: () => runId,
527
+ consumedInputIds: () => invalid ? null : [...entries.values()].filter((entry) => entry.consumed).map((entry) => entry.message.id),
528
+ register(entryId, message) {
529
+ if (!entries.has(entryId)) entries.set(entryId, { message, observed: false, consumed: false });
530
+ if (invalid) {
531
+ settleUncertain(entries.get(entryId), entryId);
532
+ return;
533
+ }
534
+ consumeBuffered();
535
+ },
536
+ isConsumed: (entryId) => entries.get(entryId)?.consumed === true,
537
+ /**
538
+ * Own the admitted operation as soon as Pi reports its id, before the run
539
+ * settles, so entries consumed mid-run are acknowledged when their
540
+ * message_end arrives rather than in one batch at the end of the run.
541
+ * Safe in either order with run_start; a conflicting id invalidates.
542
+ */
543
+ confirm(operationId) {
544
+ if (invalid) return;
545
+ if (
546
+ typeof operationId !== "string"
547
+ || operationId.length === 0
548
+ || (admittedOperationId !== undefined && admittedOperationId !== operationId)
549
+ || (runId !== undefined && runId !== operationId)
550
+ ) {
551
+ invalidate("operation_mismatch");
552
+ return;
553
+ }
554
+ admittedOperationId = operationId;
555
+ if (runId === undefined) return;
556
+ operationConfirmed = true;
557
+ confirmObserved();
558
+ },
559
+ finish(operationId) {
560
+ if (
561
+ typeof operationId !== "string"
562
+ || operationId.length === 0
563
+ || runId === undefined
564
+ || operationId !== runId
565
+ || (admittedOperationId !== undefined && admittedOperationId !== operationId)
566
+ ) {
567
+ invalidate("operation_mismatch");
568
+ return;
569
+ }
570
+ admittedOperationId = operationId;
571
+ operationConfirmed = true;
572
+ confirmObserved();
573
+ },
574
+ close() {
575
+ remove?.();
377
576
  },
378
577
  };
578
+
579
+ function consumeBuffered() {
580
+ if (invalid || runId === undefined) return;
581
+ for (const evidence of buffered) {
582
+ if (evidence.runId !== runId) continue;
583
+ const entry = entries.get(evidence.entryId);
584
+ if (entry === undefined || entry.consumed) continue;
585
+ entry.observed = true;
586
+ }
587
+ if (operationConfirmed) confirmObserved();
588
+ }
589
+
590
+ function confirmObserved() {
591
+ if (!operationConfirmed || runId === undefined || invalid) return;
592
+ for (const [entryId, entry] of entries) {
593
+ if (!entry.observed || entry.consumed) continue;
594
+ entry.consumed = true;
595
+ entry.message.acknowledge?.({ providerEntryId: entryId, providerRunId: runId });
596
+ }
597
+ }
598
+
599
+ function invalidate(reason) {
600
+ if (invalid) return;
601
+ invalid = true;
602
+ try {
603
+ onEvent({
604
+ type: "runtime_warning",
605
+ warning_kind: "live_input_correlation_invalid",
606
+ message: "Live-input consumption could not be correlated to exactly one provider operation.",
607
+ reason,
608
+ });
609
+ } catch { /* diagnostics do not alter settlement */ }
610
+ for (const [entryId, entry] of entries) settleUncertain(entry, entryId);
611
+ }
612
+
613
+ function settleUncertain(entry, entryId) {
614
+ if (entry === undefined || entry.consumed) return;
615
+ entry.message.uncertain?.({
616
+ reason: "delivery_uncertain",
617
+ providerEntryId: entryId,
618
+ ...(runId === undefined ? {} : { providerRunId: runId }),
619
+ });
620
+ }
379
621
  }
380
622
 
381
623
  /**
@@ -385,23 +627,32 @@ export function startLiveInput({ harness, options, onEvent }) {
385
627
  * @param {any} harness
386
628
  * @param {string} promptText
387
629
  * @param {Array<any>} promptImages
388
- * @returns {Promise<{runError: any}>}
630
+ * @param {{onOperationAdmitted?: (operationId: string) => void}} [hooks]
631
+ * `onOperationAdmitted` fires as soon as Pi admits the run, before any
632
+ * provider request, so the live-input epoch can own the operation up front.
633
+ * @returns {Promise<{runError: any, operationId?: string}>}
389
634
  */
390
- export async function runHarnessPrompt(harness, promptText, promptImages) {
635
+ export async function runHarnessPrompt(harness, promptText, promptImages, hooks) {
391
636
  let runError = null;
637
+ let operationId;
392
638
  try {
393
639
  // Pass structured images (when present) so multimodal input reaches the
394
640
  // model as image blocks rather than stringified text. AgentHarness.prompt
395
641
  // takes them under an options object (`{ images }`); a bare array would be
396
642
  // read as `options` and silently dropped (options?.images === undefined).
397
- if (Array.isArray(promptImages) && promptImages.length > 0) {
398
- await harness.prompt(promptText, { images: promptImages });
399
- } else {
400
- await harness.prompt(promptText);
401
- }
643
+ const promptOptions = {
644
+ ...(Array.isArray(promptImages) && promptImages.length > 0 ? { images: promptImages } : {}),
645
+ ...(typeof hooks?.onOperationAdmitted === "function"
646
+ ? { onOperationAdmitted: hooks.onOperationAdmitted }
647
+ : {}),
648
+ };
649
+ const result = Object.keys(promptOptions).length > 0
650
+ ? await harness.prompt(promptText, promptOptions)
651
+ : await harness.prompt(promptText);
652
+ operationId = result?.operationId;
402
653
  } catch (err) {
403
654
  runError = err;
404
655
  }
405
656
  await harness.waitForIdle();
406
- return { runError };
657
+ return { runError, ...(operationId === undefined ? {} : { operationId }) };
407
658
  }