@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.
- package/ARCHITECTURE.md +50 -11
- package/MIGRATION.md +30 -7
- package/README.md +219 -35
- package/package.json +9 -4
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +104 -5
- package/src/agent/tools/bash.js +10 -2
- package/src/agent/tools/codex-subscription-search.js +122 -28
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/monitor.js +11 -2
- package/src/agent/tools/pi-bridge.js +33 -14
- package/src/agent/tools/shared/monitors.js +22 -3
- package/src/agent/tools/shared/path-resolver.js +25 -6
- package/src/agent/tools/shared/process-jobs.js +6 -1
- package/src/agent/tools/shared/process-runner.js +3 -1
- package/src/agent/tools/shared/tool-context.js +8 -0
- package/src/agent/tools/web-access-interstitial.js +70 -0
- package/src/agent/tools/web-browser-render.js +83 -58
- package/src/agent/tools/web-controller.js +112 -21
- package/src/agent/tools/web-document-extractor.js +379 -0
- package/src/agent/tools/web-fetch.js +271 -243
- package/src/agent/tools/web-request.js +65 -0
- package/src/agent/tools/web-search-output.js +165 -0
- package/src/agent/tools/web-search-state.js +75 -0
- package/src/agent/tools/web-search.js +532 -71
- package/src/ai/failure.js +3 -3
- package/src/ai/index.js +1 -0
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +156 -0
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/pi-native/compaction-driver.js +45 -21
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +40 -2
- package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
- package/src/ai/providers/pi-native/provider-attribution.js +102 -0
- package/src/ai/providers/pi-native/result-builder.js +28 -4
- package/src/ai/providers/pi-native/session-lifecycle.js +167 -24
- package/src/ai/providers/pi-native/stream-subscriber.js +30 -2
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +245 -13
- package/src/ai/providers/pi-native.js +159 -40
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/router.js +30 -11
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +26 -5
- package/src/runtime.js +24 -5
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -1
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +6 -2
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/monitor.d.ts +5 -2
- package/types/agent/tools/pi-bridge.d.ts +6 -4
- package/types/agent/tools/shared/monitors.d.ts +17 -2
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +3 -2
- package/types/agent/tools/shared/tool-context.d.ts +2 -0
- package/types/agent/tools/web-access-interstitial.d.ts +23 -0
- package/types/agent/tools/web-browser-render.d.ts +4 -1
- package/types/agent/tools/web-controller.d.ts +4 -2
- package/types/agent/tools/web-document-extractor.d.ts +27 -0
- package/types/agent/tools/web-fetch.d.ts +19 -24
- package/types/agent/tools/web-request.d.ts +20 -0
- package/types/agent/tools/web-search-output.d.ts +31 -0
- package/types/agent/tools/web-search-state.d.ts +21 -0
- package/types/agent/tools/web-search.d.ts +10 -45
- package/types/ai/index.d.ts +1 -0
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +61 -0
- package/types/ai/provider-check.d.ts +53 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
- package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +3 -1
- package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
- package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +11 -1
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +23 -5
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +36 -5
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/tool-lifecycle.d.ts +4 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
|
223
|
-
cache_creation_tokens: usage.cacheWrite
|
|
224
|
-
cache_write_tokens: usage.cacheWrite
|
|
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,
|