@mono-agent/agent-runtime 0.20.14 → 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 (82) hide show
  1. package/ARCHITECTURE.md +50 -11
  2. package/MIGRATION.md +30 -7
  3. package/README.md +219 -35
  4. package/package.json +9 -4
  5. package/src/agent/tool-bloat.js +145 -9
  6. package/src/agent/tools/agent-tool.js +104 -5
  7. package/src/agent/tools/bash.js +10 -2
  8. package/src/agent/tools/codex-subscription-search.js +122 -28
  9. package/src/agent/tools/exec.js +10 -2
  10. package/src/agent/tools/monitor.js +11 -2
  11. package/src/agent/tools/pi-bridge.js +33 -14
  12. package/src/agent/tools/shared/monitors.js +22 -3
  13. package/src/agent/tools/shared/path-resolver.js +25 -6
  14. package/src/agent/tools/shared/process-jobs.js +6 -1
  15. package/src/agent/tools/shared/process-runner.js +3 -1
  16. package/src/agent/tools/shared/tool-context.js +8 -0
  17. package/src/agent/tools/web-access-interstitial.js +70 -0
  18. package/src/agent/tools/web-browser-render.js +83 -58
  19. package/src/agent/tools/web-controller.js +112 -21
  20. package/src/agent/tools/web-document-extractor.js +379 -0
  21. package/src/agent/tools/web-fetch.js +271 -243
  22. package/src/agent/tools/web-request.js +65 -0
  23. package/src/agent/tools/web-search-output.js +165 -0
  24. package/src/agent/tools/web-search-state.js +75 -0
  25. package/src/agent/tools/web-search.js +532 -71
  26. package/src/ai/failure.js +3 -3
  27. package/src/ai/index.js +1 -0
  28. package/src/ai/observer.js +8 -0
  29. package/src/ai/pi-interop.js +156 -0
  30. package/src/ai/provider-check.js +131 -0
  31. package/src/ai/providers/pi-native/compaction-driver.js +45 -21
  32. package/src/ai/providers/pi-native/compaction-summary.js +140 -0
  33. package/src/ai/providers/pi-native/harness-adapter.js +40 -2
  34. package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
  35. package/src/ai/providers/pi-native/provider-attribution.js +102 -0
  36. package/src/ai/providers/pi-native/result-builder.js +28 -4
  37. package/src/ai/providers/pi-native/session-lifecycle.js +167 -24
  38. package/src/ai/providers/pi-native/stream-subscriber.js +30 -2
  39. package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
  40. package/src/ai/providers/pi-native/turn-runner.js +245 -13
  41. package/src/ai/providers/pi-native.js +159 -40
  42. package/src/ai/runtime/live-input-events.js +250 -54
  43. package/src/ai/runtime/router.js +30 -11
  44. package/src/ai/tool-lifecycle.js +32 -18
  45. package/src/ai/types.js +26 -5
  46. package/src/runtime.js +24 -5
  47. package/types/agent/tool-bloat.d.ts +1 -1
  48. package/types/agent/tools/agent-tool.d.ts +4 -1
  49. package/types/agent/tools/bash.d.ts +5 -3
  50. package/types/agent/tools/codex-subscription-search.d.ts +6 -2
  51. package/types/agent/tools/exec.d.ts +5 -3
  52. package/types/agent/tools/monitor.d.ts +5 -2
  53. package/types/agent/tools/pi-bridge.d.ts +6 -4
  54. package/types/agent/tools/shared/monitors.d.ts +17 -2
  55. package/types/agent/tools/shared/process-jobs.d.ts +5 -1
  56. package/types/agent/tools/shared/process-runner.d.ts +3 -2
  57. package/types/agent/tools/shared/tool-context.d.ts +2 -0
  58. package/types/agent/tools/web-access-interstitial.d.ts +23 -0
  59. package/types/agent/tools/web-browser-render.d.ts +4 -1
  60. package/types/agent/tools/web-controller.d.ts +4 -2
  61. package/types/agent/tools/web-document-extractor.d.ts +27 -0
  62. package/types/agent/tools/web-fetch.d.ts +19 -24
  63. package/types/agent/tools/web-request.d.ts +20 -0
  64. package/types/agent/tools/web-search-output.d.ts +31 -0
  65. package/types/agent/tools/web-search-state.d.ts +21 -0
  66. package/types/agent/tools/web-search.d.ts +10 -45
  67. package/types/ai/index.d.ts +1 -0
  68. package/types/ai/observer.d.ts +6 -0
  69. package/types/ai/pi-interop.d.ts +61 -0
  70. package/types/ai/provider-check.d.ts +53 -0
  71. package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
  72. package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
  73. package/types/ai/providers/pi-native/harness-adapter.d.ts +3 -1
  74. package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
  75. package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
  76. package/types/ai/providers/pi-native/result-builder.d.ts +11 -1
  77. package/types/ai/providers/pi-native/session-lifecycle.d.ts +23 -5
  78. package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
  79. package/types/ai/providers/pi-native/turn-runner.d.ts +36 -5
  80. package/types/ai/runtime/live-input-events.d.ts +32 -8
  81. package/types/ai/tool-lifecycle.d.ts +4 -3
  82. package/types/ai/types.d.ts +140 -12
@@ -9,6 +9,7 @@ import {
9
9
  createCompactionSummaryMessage,
10
10
  getOrThrow,
11
11
  } from "@earendil-works/pi-agent-core";
12
+ import { installPromptCacheDiagnostics, promptCacheRequest } from "./prompt-cache-diagnostics.js";
12
13
 
13
14
  export const PI_CONTEXT = BACKGROUND_CONTEXT;
14
15
 
@@ -201,6 +202,7 @@ export async function createPiHarnessAdapter(session, options) {
201
202
  let rawHarness;
202
203
  /** @type {any} */
203
204
  let lane;
205
+ let removePromptCacheDiagnostics = () => {};
204
206
  try {
205
207
  created = await AgentHarness.create({
206
208
  ...options,
@@ -231,6 +233,7 @@ export async function createPiHarnessAdapter(session, options) {
231
233
  rawHarness.hooks.on("before_compaction", (event) => (
232
234
  event.reason === "manual" ? undefined : { decline: true }
233
235
  ), { id: "mono-agent-compaction-owner" });
236
+ removePromptCacheDiagnostics = installPromptCacheDiagnostics(rawHarness, options);
234
237
  } catch (error) {
235
238
  try { await session.close(); } catch { /* preserve the construction error */ }
236
239
  throw error;
@@ -243,6 +246,7 @@ export async function createPiHarnessAdapter(session, options) {
243
246
  const manuallyAppendedEntryIds = new Set();
244
247
 
245
248
  const adapter = {
249
+ getPromptCacheRequest: () => promptCacheRequest(rawHarness),
246
250
  models: options.models,
247
251
  getModel: () => currentModel,
248
252
  getThinkingLevel: () => currentThinkingLevel,
@@ -259,11 +263,44 @@ export async function createPiHarnessAdapter(session, options) {
259
263
  manuallyAppendedEntryIds.add(entryId);
260
264
  return entryId;
261
265
  },
266
+ // Mirror pi's own lane.prompt() (accept → drive) rather than calling it,
267
+ // so the operation id is known the moment Pi admits the run instead of
268
+ // only when it settles. The live-input epoch needs it up front: without
269
+ // it every steer consumed mid-run stays "pending" until the whole run
270
+ // ends and is only acknowledged in one batch at the end.
262
271
  async prompt(text, promptOptions) {
263
- return getOrThrow(await lane.prompt(text, promptOptions?.images, PI_CONTEXT));
272
+ const images = promptOptions?.images;
273
+ const admission = getOrThrow(await lane.accept({
274
+ kind: "prompt",
275
+ prompt: text,
276
+ ...(Array.isArray(images) && images.length > 0 ? { images } : {}),
277
+ }, PI_CONTEXT));
278
+ const { operationId } = admission;
279
+ if (typeof operationId !== "string" || operationId.length === 0) {
280
+ throw new Error("Pi run was admitted without an operation id");
281
+ }
282
+ promptOptions?.onOperationAdmitted?.(operationId);
283
+ const driven = getOrThrow(await lane.drive({ operationId, waitForRetry: true }, PI_CONTEXT));
284
+ if (driven.kind === "settled") return driven.outcome;
285
+ if (driven.kind === "waiting" && driven.reason === "deferred") {
286
+ return { operationId, status: "suspended", deferred: driven.deferred };
287
+ }
288
+ throw new Error(`Pi run ${operationId} returned an unwaited retry`);
264
289
  },
290
+ // Pi's QueueResult carries `{ entryId }`; the live-input runner keys prompt
291
+ // epoch registration, `cancelQueued` and `message_end` correlation on the
292
+ // bare entry id, so unwrap it here (as appendMessage does) rather than hand
293
+ // the runner an object it would settle as "uncertain" without ever
294
+ // registering the steer.
265
295
  async steer(message) {
266
- return getOrThrow(await lane.steer(message, undefined, PI_CONTEXT));
296
+ const { entryId } = getOrThrow(await lane.steer(message, undefined, PI_CONTEXT));
297
+ if (typeof entryId !== "string" || entryId.length === 0) {
298
+ throw new Error("Pi steer settled without a queue entry id");
299
+ }
300
+ return entryId;
301
+ },
302
+ async cancelQueued(entryId) {
303
+ return getOrThrow(await lane.cancelQueued(entryId, PI_CONTEXT));
267
304
  },
268
305
  async abort() {
269
306
  const result = await lane.abort(PI_CONTEXT);
@@ -330,6 +367,7 @@ export async function createPiHarnessAdapter(session, options) {
330
367
  async close() {
331
368
  if (closed) return;
332
369
  closed = true;
370
+ removePromptCacheDiagnostics();
333
371
  await session.close();
334
372
  },
335
373
  };
@@ -0,0 +1,103 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+
3
+ const activeRequests = new WeakMap();
4
+ export const promptCacheRequest = (harness) => activeRequests.get(harness);
5
+
6
+ const MAX_MESSAGE_FINGERPRINTS = 128;
7
+ const safeString = (value) => typeof value === "string" ? value.slice(0, 160) : undefined;
8
+ const serialized = (value) => JSON.stringify(value) ?? "null";
9
+ const fingerprint = (value) => createHash("sha256").update(serialized(value)).digest("hex").slice(0, 16);
10
+ const bytes = (value) => value == null ? 0 : Buffer.byteLength(typeof value === "string" ? value : serialized(value));
11
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
12
+ const list = (value) => Array.isArray(value) ? value : [];
13
+
14
+ function normalizedPayload(event) {
15
+ const payload = record(event?.payload);
16
+ if (!payload) return { family: "unsupported", reason: "payload_not_object" };
17
+ const api = safeString(event?.model?.api) ?? "unknown";
18
+ if (api === "google-generative-ai" || api === "google-vertex") {
19
+ const config = record(payload.config) ?? {};
20
+ const tools = list(config.tools).flatMap((tool) => list(record(tool)?.functionDeclarations).length > 0
21
+ ? list(record(tool)?.functionDeclarations) : [tool]);
22
+ return { family: "google", system: config.systemInstruction ?? null, tools, messages: list(payload.contents), payload };
23
+ }
24
+ if (api === "pi-messages") {
25
+ const context = record(payload.context);
26
+ if (context && ("messages" in context || "systemPrompt" in context || "tools" in context)) {
27
+ return { family: "pi-messages", system: context.systemPrompt ?? null, tools: list(context.tools), messages: list(context.messages), payload };
28
+ }
29
+ }
30
+ if (api === "bedrock-converse-stream") {
31
+ return { family: "bedrock", system: payload.system ?? null, tools: list(record(payload.toolConfig)?.tools), messages: list(payload.messages), payload };
32
+ }
33
+ if (api === "anthropic-messages") {
34
+ return { family: "anthropic", system: payload.system ?? null, tools: list(payload.tools), messages: list(payload.messages), payload };
35
+ }
36
+ if (["openai-responses", "azure-openai-responses", "openai-codex-responses"].includes(api)) {
37
+ return { family: api === "openai-codex-responses" ? "openai-codex" : "openai-responses", system: payload.instructions ?? null, tools: list(payload.tools), messages: list(payload.input), payload };
38
+ }
39
+ return { family: "unsupported", reason: `unrecognized_api:${api}` };
40
+ }
41
+
42
+ function cacheMetadata(payload, family) {
43
+ const disabled = payload.prompt_cache_retention === "none" || record(payload.options)?.cacheRetention === "none";
44
+ const key = payload.prompt_cache_key ?? payload.cache_key ?? payload.cachedContent
45
+ ?? (family === "pi-messages" ? record(payload.options)?.sessionId : undefined);
46
+ let explicit = false;
47
+ const pending = [{ value: payload, depth: 0 }];
48
+ let visited = 0;
49
+ while (pending.length > 0 && visited < 10_000) {
50
+ const { value, depth } = pending.pop();
51
+ visited += 1;
52
+ if (depth > 12 || value === null || typeof value !== "object") continue;
53
+ for (const [name, child] of Object.entries(value)) {
54
+ if (["cache_control", "cachePoint", "cache_point", "prompt_cache_retention", "prompt_cache_options"].includes(name) && child != null) explicit = true;
55
+ if (typeof child === "object" && child !== null) pending.push({ value: child, depth: depth + 1 });
56
+ }
57
+ }
58
+ return {
59
+ cacheMode: disabled ? "disabled" : [key === undefined ? "" : "keyed", explicit ? "explicit" : ""].filter(Boolean).join("+") || "provider-default",
60
+ ...(disabled || key === undefined ? {} : { cacheKeyFingerprint: fingerprint(key) }),
61
+ };
62
+ }
63
+
64
+ /** Install a metadata-only, request-lifecycle-bounded Pi payload diagnostic. */
65
+ export function installPromptCacheDiagnostics(harness, options) {
66
+ if (options.promptCacheDiagnostics !== true || typeof options.onEvent !== "function") return () => {};
67
+ let ordinal = 0;
68
+ const remove = harness.hooks.on("before_payload", (event) => {
69
+ const correlation = { phase: "assistant", requestId: randomUUID() };
70
+ activeRequests.set(harness, correlation);
71
+ const normalized = normalizedPayload(event);
72
+ const model = event?.model ?? options.model ?? {};
73
+ const base = {
74
+ type: "prompt_cache_diagnostic",
75
+ ...correlation,
76
+ requestOrdinal: ++ordinal,
77
+ model: [safeString(model.provider), safeString(model.id)].filter(Boolean).join(":") || "unknown",
78
+ api: safeString(model.api) ?? "unknown",
79
+ payloadFamily: normalized.family,
80
+ };
81
+ if (normalized.family === "unsupported") {
82
+ options.onEvent({ ...base, supported: false, unsupportedReason: normalized.reason });
83
+ return;
84
+ }
85
+ const messages = normalized.messages;
86
+ const logicalInputInterpretation = normalized.payload.previous_response_id === undefined ? "full" : "delta";
87
+ const codexWireUnknown = normalized.family === "openai-codex" && logicalInputInterpretation === "full";
88
+ options.onEvent({
89
+ ...base,
90
+ supported: true,
91
+ systemBytes: bytes(normalized.system), systemFingerprint: fingerprint(normalized.system),
92
+ toolDefinitionCount: normalized.tools.length, toolDefinitionsFingerprint: fingerprint(normalized.tools),
93
+ messageCount: messages.length,
94
+ messageFingerprints: messages.slice(0, MAX_MESSAGE_FINGERPRINTS).map(fingerprint),
95
+ messageFingerprintsTruncated: messages.length > MAX_MESSAGE_FINGERPRINTS,
96
+ ...cacheMetadata(normalized.payload, normalized.family),
97
+ logicalInputInterpretation,
98
+ inputInterpretation: codexWireUnknown ? "unavailable" : logicalInputInterpretation,
99
+ inputInterpretationSource: codexWireUnknown ? "pre_transport_payload" : "provider_payload",
100
+ });
101
+ }, { id: "mono-agent-prompt-cache-diagnostics" });
102
+ return () => { activeRequests.delete(harness); remove(); };
103
+ }
@@ -0,0 +1,102 @@
1
+ // @ts-check
2
+
3
+ const OPENCODE_HOST = "opencode.ai";
4
+ const OPENCODE_PROVIDERS = new Set(["opencode", "opencode-go"]);
5
+ const REQUEST_METHODS = new Set([
6
+ "stream",
7
+ "complete",
8
+ "streamSimple",
9
+ "completeSimple",
10
+ "streamDeferred",
11
+ "fetchDeferred",
12
+ "cancelDeferred",
13
+ ]);
14
+
15
+ /** @typedef {import("@earendil-works/pi-ai").Models} Models */
16
+ /** @typedef {import("@earendil-works/pi-ai").ProviderHeaders} ProviderHeaders */
17
+
18
+ /**
19
+ * Match Pi's OpenCode attribution boundary without accepting deceptive suffixes
20
+ * or subdomains. Provider ids remain authoritative for callers that deliberately
21
+ * route OpenCode through a nonstandard endpoint.
22
+ *
23
+ * @param {{provider?: string, baseUrl?: string}} model
24
+ */
25
+ export function isOpenCodeModel(model) {
26
+ if (OPENCODE_PROVIDERS.has(model?.provider)) return true;
27
+ if (typeof model?.baseUrl !== "string") return false;
28
+ try {
29
+ return new URL(model.baseUrl).hostname === OPENCODE_HOST;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Add one default without replacing an auth/model/request value, including a
37
+ * caller's case-insensitive null suppression.
38
+ *
39
+ * @param {ProviderHeaders} headers
40
+ * @param {string} name
41
+ * @param {string} value
42
+ * @returns {ProviderHeaders}
43
+ */
44
+ function addDefaultHeader(headers, name, value) {
45
+ const lowerName = name.toLowerCase();
46
+ if (Object.keys(headers).some((existingName) => existingName.toLowerCase() === lowerName)) {
47
+ return headers;
48
+ }
49
+ return { ...headers, [name]: value };
50
+ }
51
+
52
+ /**
53
+ * @param {Object<string, *>|undefined} options
54
+ * @param {string} sessionId
55
+ * @returns {Object<string, *>}
56
+ */
57
+ function withOpenCodeHeaderTransform(options, sessionId) {
58
+ const previousTransform = options?.transformHeaders;
59
+ return {
60
+ ...(options ?? {}),
61
+ transformHeaders: async (headers) => {
62
+ let attributed = addDefaultHeader(headers, "x-opencode-session", sessionId);
63
+ attributed = addDefaultHeader(attributed, "x-opencode-client", "mono-agent");
64
+ return typeof previousTransform === "function"
65
+ ? await previousTransform(attributed)
66
+ : attributed;
67
+ },
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Decorate every Pi Models request path for one run. Non-request methods stay
73
+ * bound to the original Models instance because its state lives in private
74
+ * fields; matching is performed on the actual model dispatched by Pi, covering
75
+ * builtin, custom-provider, compaction, deferred, and advanced/test models.
76
+ *
77
+ * @param {Models} models
78
+ * @param {string} sessionId
79
+ * @returns {Models}
80
+ */
81
+ export function withOpenCodeSessionHeaders(models, sessionId) {
82
+ const wrappers = new Map();
83
+ return /** @type {Models} */ (new Proxy(models, {
84
+ get(target, property) {
85
+ const value = Reflect.get(target, property, target);
86
+ if (typeof property !== "string" || typeof value !== "function") return value;
87
+ if (!REQUEST_METHODS.has(property)) return value.bind(target);
88
+
89
+ let wrapper = wrappers.get(property);
90
+ if (wrapper === undefined) {
91
+ wrapper = (model, input, options) => value.call(
92
+ target,
93
+ model,
94
+ input,
95
+ isOpenCodeModel(model) ? withOpenCodeHeaderTransform(options, sessionId) : options,
96
+ );
97
+ wrappers.set(property, wrapper);
98
+ }
99
+ return wrapper;
100
+ },
101
+ }));
102
+ }
@@ -32,6 +32,24 @@ export function usageFromMessages(messages = []) {
32
32
  return usage;
33
33
  }
34
34
 
35
+ /**
36
+ * Pi initializes failed assistant messages with an all-zero usage object before
37
+ * any provider telemetry arrives. A successful response makes those zeros
38
+ * measured; a failed response is measured only when real usage is non-zero.
39
+ * @param {Array<any>} [messages]
40
+ */
41
+ export function hasMeasuredUsage(messages = []) {
42
+ return messages.some((message) => {
43
+ if (message?.role !== "assistant" || !message.usage) return false;
44
+ if (message.stopReason !== "error" && message.stopReason !== "aborted") return true;
45
+ const usage = message.usage;
46
+ return [usage.input, usage.output, usage.cacheRead, usage.cacheWrite, usage.totalTokens,
47
+ usage.cost?.input, usage.cost?.output, usage.cost?.cacheRead,
48
+ usage.cost?.cacheWrite, usage.cost?.total]
49
+ .some((value) => Number.isFinite(Number(value)) && Number(value) > 0);
50
+ });
51
+ }
52
+
35
53
  /**
36
54
  * Add what this run's subagents spent to the run's own usage.
37
55
  *
@@ -70,7 +88,7 @@ export function withSubagentUsage(usage, delegated, estimatedCost) {
70
88
  * requests in the run: the last assistant usage is the same provider-counted
71
89
  * value Pi's compaction logic trusts, so it can decrease after compaction.
72
90
  * @param {any} assistantMessage
73
- * @returns {{input: number, output: number, cacheRead: number, cacheCreation: number, total: number}|null}
91
+ * @returns {{input: number, output: number, cacheRead: number, cacheCreation: number, total: number, costUsd: number}|null}
74
92
  */
75
93
  export function contextUsageFromAssistantMessage(assistantMessage) {
76
94
  if (assistantMessage?.role !== "assistant" || !assistantMessage.usage) return null;
@@ -83,6 +101,7 @@ export function contextUsageFromAssistantMessage(assistantMessage) {
83
101
  cacheRead: Number(assistantMessage.usage.cacheRead) || 0,
84
102
  cacheCreation: Number(assistantMessage.usage.cacheWrite) || 0,
85
103
  total,
104
+ costUsd: Number(assistantMessage.usage.cost?.total) || 0,
86
105
  };
87
106
  }
88
107
 
@@ -211,6 +230,8 @@ export function buildSuccessResult(params) {
211
230
  runtimeWarnings,
212
231
  capabilitiesUsed,
213
232
  structuredResult,
233
+ usageMeasured = true,
234
+ effectiveEffort,
214
235
  } = params;
215
236
  return {
216
237
  text: finalText,
@@ -219,15 +240,16 @@ export function buildSuccessResult(params) {
219
240
  usage: {
220
241
  input_tokens: usage.input || null,
221
242
  output_tokens: usage.output || null,
222
- cache_read_tokens: usage.cacheRead || null,
223
- cache_creation_tokens: usage.cacheWrite || null,
224
- cache_write_tokens: usage.cacheWrite || null,
243
+ cache_read_tokens: usageMeasured ? usage.cacheRead : null,
244
+ cache_creation_tokens: usageMeasured ? usage.cacheWrite : null,
245
+ cache_write_tokens: usageMeasured ? usage.cacheWrite : null,
225
246
  cost_usd: usage.cost || estimatedCost,
226
247
  },
227
248
  durationMs: Date.now() - start,
228
249
  numTurns: turnCount || runAssistantCount,
229
250
  model: resolved.reference || `${resolved.provider}:${resolved.model}`,
230
251
  effort: options.effort || null,
252
+ effectiveEffort: effectiveEffort || null,
231
253
  sdk: "pi",
232
254
  cancelled: externalAbort,
233
255
  error: errorMessage,
@@ -266,6 +288,7 @@ export function buildErrorResult(params) {
266
288
  runtimeWarnings,
267
289
  isRetryable,
268
290
  piTransport,
291
+ effectiveEffort,
269
292
  } = params;
270
293
  return {
271
294
  text: assistantTexts.join("") || null,
@@ -275,6 +298,7 @@ export function buildErrorResult(params) {
275
298
  numTurns: turnCount,
276
299
  model: resolved?.reference || (resolved?.provider && resolved?.model ? `${resolved.provider}:${resolved.model}` : null),
277
300
  effort: options.effort || null,
301
+ effectiveEffort: effectiveEffort || null,
278
302
  sdk: "pi",
279
303
  cancelled: externalAbort,
280
304
  error: externalAbort ? null : errorMessage,