@mono-agent/agent-runtime 0.16.0 → 0.17.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/MIGRATION.md +12 -4
- package/package.json +1 -1
- package/src/agent/tools/agent-tool.js +81 -6
- package/src/agent/tools/bash.js +2 -2
- package/src/agent/tools/exec.js +3 -1
- package/src/agent/tools/shared/tool-context.js +22 -0
- package/src/ai/providers/pi-native/result-builder.js +32 -0
- package/src/ai/providers/pi-native/turn-runner.js +1 -0
- package/src/ai/providers/pi-native.js +16 -6
- package/src/ai/runtime/capabilities.js +2 -0
- package/src/ai/runtime/router.js +4 -1
- package/src/ai/types.js +2 -0
- package/src/runtime.js +8 -1
- package/types/agent/tools/agent-tool.d.ts +23 -1
- package/types/agent/tools/shared/tool-context.d.ts +15 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +41 -0
- package/types/ai/runtime/capabilities.d.ts +3 -0
- package/types/ai/types.d.ts +11 -0
package/MIGRATION.md
CHANGED
|
@@ -63,10 +63,18 @@ the configuration schema.
|
|
|
63
63
|
authoritative and the runtime emits a bounded
|
|
64
64
|
`live_input_callback_failed` warning.
|
|
65
65
|
|
|
66
|
-
## 0.
|
|
66
|
+
## 0.17.x baseline
|
|
67
67
|
|
|
68
|
-
This is the current published baseline
|
|
69
|
-
|
|
68
|
+
This is the current published baseline. It carries the 0.16.x contract forward
|
|
69
|
+
and adds a host-only, request-scoped `toolEnvironment` boundary. Hosts may pass
|
|
70
|
+
validated values and PATH prefixes through the request, harness, and runtime;
|
|
71
|
+
the runtime applies them only when Bash, Exec, or a nested subagent process is
|
|
72
|
+
spawned. It does not mutate `process.env` or persist the values in prompts,
|
|
73
|
+
metadata, history, traces, or long-lived tool context.
|
|
74
|
+
|
|
75
|
+
## 0.16.x
|
|
76
|
+
|
|
77
|
+
This baseline carries the whole 0.15.x contract forward and adds:
|
|
70
78
|
|
|
71
79
|
- `skills` and `skillsRoot` on the run options. `skills` is the disclosed
|
|
72
80
|
`{name, description}` set for a run; a non-empty value makes `supports_skills`
|
|
@@ -394,7 +402,7 @@ a compatibility subpath.
|
|
|
394
402
|
|
|
395
403
|
## Version
|
|
396
404
|
|
|
397
|
-
This guide describes the published `0.
|
|
405
|
+
This guide describes the published `0.17.x` package contract. Keep
|
|
398
406
|
`@mono-agent/agent-runtime`, `@mono-agent/runtime-adapter`, and other
|
|
399
407
|
`@mono-agent/*` packages on the same lockstep version when upgrading. The paired
|
|
400
408
|
runtime adapter no longer exposes `piReasoningSummary` in its run-options type.
|
package/package.json
CHANGED
|
@@ -110,8 +110,21 @@ function toolDescription(subagents, definitions, ceiling) {
|
|
|
110
110
|
return `${DESCRIPTION_BASE}${parallel}${named}${shapes}${inline}`;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/** @param {*} value @returns {number} */
|
|
114
|
+
function numberOrZero(value) {
|
|
115
|
+
const numeric = Number(value);
|
|
116
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @returns {{costUsd: number, input: number, output: number, cacheRead: number, cacheWrite: number}} */
|
|
120
|
+
function emptyUsage() {
|
|
121
|
+
return { costUsd: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
122
|
+
}
|
|
123
|
+
|
|
113
124
|
/**
|
|
114
|
-
* Per-logical-run call and
|
|
125
|
+
* Per-logical-run call, byte and subagent-usage budget, shared across router
|
|
126
|
+
* attempts. `usage` rides the same entry so delegated spend inherits the
|
|
127
|
+
* existing per-run keying and eviction instead of needing a second store.
|
|
115
128
|
* @param {*} subagents The run-scoped options object, stable across attempts.
|
|
116
129
|
* @param {string|undefined} parentRunId
|
|
117
130
|
*/
|
|
@@ -129,7 +142,7 @@ function budgetForRun(subagents, parentRunId) {
|
|
|
129
142
|
if (oldest.done) break;
|
|
130
143
|
store.delete(oldest.value);
|
|
131
144
|
}
|
|
132
|
-
const fresh = { total: 0, bytes: 0, warnedQueued: false };
|
|
145
|
+
const fresh = { total: 0, bytes: 0, warnedQueued: false, usage: emptyUsage() };
|
|
133
146
|
store.set(key, fresh);
|
|
134
147
|
return fresh;
|
|
135
148
|
}
|
|
@@ -155,6 +168,32 @@ export function subagentInvocationCount(subagents, parentRunId) {
|
|
|
155
168
|
return Number.isInteger(entry?.total) ? entry.total : 0;
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
/**
|
|
172
|
+
* What this logical run's subagents spent, summed across every delegation.
|
|
173
|
+
*
|
|
174
|
+
* A delegation is work the run asked for, so its cost belongs to the run's
|
|
175
|
+
* total — a provider folds this into its own usage before reporting, which is
|
|
176
|
+
* what makes the console's cost, the TUI status bar and the exported metrics
|
|
177
|
+
* agree with the bill. Same read-only-accessor contract as
|
|
178
|
+
* `subagentInvocationCount`: `__budgets` stays private. All zeroes when nothing
|
|
179
|
+
* delegated, which is the truthful answer for a run that never used the tool.
|
|
180
|
+
*
|
|
181
|
+
* @param {*} subagents The run-scoped options object, or undefined.
|
|
182
|
+
* @param {string|undefined} parentRunId
|
|
183
|
+
* @returns {{costUsd: number, input: number, output: number, cacheRead: number, cacheWrite: number}}
|
|
184
|
+
*/
|
|
185
|
+
export function subagentUsageForRun(subagents, parentRunId) {
|
|
186
|
+
const store = subagents?.__budgets;
|
|
187
|
+
const usage = store instanceof Map ? store.get(parentRunId ?? "unkeyed")?.usage : undefined;
|
|
188
|
+
return {
|
|
189
|
+
costUsd: numberOrZero(usage?.costUsd),
|
|
190
|
+
input: numberOrZero(usage?.input),
|
|
191
|
+
output: numberOrZero(usage?.output),
|
|
192
|
+
cacheRead: numberOrZero(usage?.cacheRead),
|
|
193
|
+
cacheWrite: numberOrZero(usage?.cacheWrite),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
158
197
|
/** @param {*} value @param {number} fallback @returns {number} */
|
|
159
198
|
function positiveInt(value, fallback) {
|
|
160
199
|
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
@@ -164,7 +203,7 @@ function positiveInt(value, fallback) {
|
|
|
164
203
|
* Build the `Agent` tool, or null when subagents are unavailable for this run.
|
|
165
204
|
*
|
|
166
205
|
* @param {RuntimeSubagentsOptions|null|undefined} subagents
|
|
167
|
-
* @param {{model?: *, executionMode?: string, cwd?: string, parentRunId?: string, sandboxPolicy?: *, sandboxEngine?: *, skills?: {name: string, description?: string}[], skillsRoot?: string, onEvent?: (event: *) => void}} [context]
|
|
206
|
+
* @param {{model?: *, executionMode?: string, cwd?: string, parentRunId?: string, sandboxPolicy?: *, sandboxEngine?: *, skills?: {name: string, description?: string}[], skillsRoot?: string, toolEnvironment?: *, onEvent?: (event: *) => void}} [context]
|
|
168
207
|
* @returns {*|null}
|
|
169
208
|
*/
|
|
170
209
|
export function createAgentTool(subagents, context = {}) {
|
|
@@ -336,6 +375,15 @@ export function createAgentTool(subagents, context = {}) {
|
|
|
336
375
|
callIndex,
|
|
337
376
|
...(params.description === undefined ? {} : { label: params.description }),
|
|
338
377
|
...(context.onEvent === undefined ? {} : { emit: context.onEvent }),
|
|
378
|
+
// Summed, not replaced: a turn can delegate a dozen times and the run
|
|
379
|
+
// owns all of it. Lives on the run budget so router attempts share it.
|
|
380
|
+
recordUsage: (spent) => {
|
|
381
|
+
budget.usage.costUsd += spent.costUsd;
|
|
382
|
+
budget.usage.input += spent.input;
|
|
383
|
+
budget.usage.output += spent.output;
|
|
384
|
+
budget.usage.cacheRead += spent.cacheRead;
|
|
385
|
+
budget.usage.cacheWrite += spent.cacheWrite;
|
|
386
|
+
},
|
|
339
387
|
});
|
|
340
388
|
collector.started();
|
|
341
389
|
const startedAt = Date.now();
|
|
@@ -361,6 +409,7 @@ export function createAgentTool(subagents, context = {}) {
|
|
|
361
409
|
// the child's resolved route and deny lists.
|
|
362
410
|
...(context.skills === undefined ? {} : { skills: context.skills }),
|
|
363
411
|
...(context.skillsRoot === undefined ? {} : { skillsRoot: context.skillsRoot }),
|
|
412
|
+
...(context.toolEnvironment === undefined ? {} : { toolEnvironment: context.toolEnvironment }),
|
|
364
413
|
abortSignal: controller.signal,
|
|
365
414
|
maxTurns,
|
|
366
415
|
callId: toolCallId,
|
|
@@ -553,13 +602,15 @@ const WIRE_CONTENT_MAX_CHARS = 2_000;
|
|
|
553
602
|
* answer body, so forwarding them would splice a subagent's prose into the
|
|
554
603
|
* main agent's reply. Its text reaches the parent through the tool result.
|
|
555
604
|
*
|
|
556
|
-
* @param {{callId: string, profileName: string, callIndex: number, label?: string, emit?: (event: *) => void}} options
|
|
605
|
+
* @param {{callId: string, profileName: string, callIndex: number, label?: string, emit?: (event: *) => void, recordUsage?: (usage: {costUsd: number, input: number, output: number, cacheRead: number, cacheWrite: number}) => void}} options
|
|
557
606
|
*/
|
|
558
|
-
function createActivityCollector({ callId, profileName, callIndex, label, emit }) {
|
|
607
|
+
function createActivityCollector({ callId, profileName, callIndex, label, emit, recordUsage }) {
|
|
559
608
|
/** @type {Map<string, {name: string, args: unknown, startedAt: number, ms?: number}>} */
|
|
560
609
|
const open = new Map();
|
|
561
610
|
/** @type {Array<{name: string, args: unknown, ms?: number, isError: boolean}>} */
|
|
562
611
|
const done = [];
|
|
612
|
+
/** What the child reported spending, so the parent run can own it. */
|
|
613
|
+
const usage = emptyUsage();
|
|
563
614
|
const subagent = { id: callId, name: profileName, callIndex, ...(label === undefined ? {} : { label }) };
|
|
564
615
|
|
|
565
616
|
/** @param {*} event */
|
|
@@ -585,10 +636,17 @@ function createActivityCollector({ callId, profileName, callIndex, label, emit }
|
|
|
585
636
|
},
|
|
586
637
|
/** @param {{status: string, durationMs: number}} outcome */
|
|
587
638
|
finished({ status, durationMs }) {
|
|
639
|
+
// Before the bookend, so the run's own usage report can already include
|
|
640
|
+
// it, and so an abandoned child still hands over whatever it spent.
|
|
641
|
+
recordUsage?.(usage);
|
|
588
642
|
publish({
|
|
589
643
|
phase: "agent_completed",
|
|
590
644
|
id: `agent:${callId}`,
|
|
591
645
|
name: `Agent(${profileName})`,
|
|
646
|
+
// The one place the child's price is knowable per delegation: operator
|
|
647
|
+
// surfaces show it on the row so an expensive one is identifiable, not
|
|
648
|
+
// just visible in the run total it disappears into.
|
|
649
|
+
...(usage.costUsd > 0 ? { subagent: { ...subagent, costUsd: usage.costUsd } } : {}),
|
|
592
650
|
isError: status !== "ok",
|
|
593
651
|
executionMs: durationMs,
|
|
594
652
|
content: `${status} · ${done.length} tool call${done.length === 1 ? "" : "s"}`,
|
|
@@ -653,8 +711,25 @@ function createActivityCollector({ callId, profileName, callIndex, label, emit }
|
|
|
653
711
|
}
|
|
654
712
|
return;
|
|
655
713
|
}
|
|
714
|
+
if (type === "cost_accumulated") {
|
|
715
|
+
// Read as a running total, not a delta — the same rule
|
|
716
|
+
// `ai/observer.js` applies to the parent's own events, because a bridge
|
|
717
|
+
// emits one of these per completed provider run carrying that run's
|
|
718
|
+
// totals. Keeping one rule means a child that failed over undercounts
|
|
719
|
+
// exactly as its parent does today rather than inventing a second.
|
|
720
|
+
// Not republished on the parent stream: consumers treat `usage_update`
|
|
721
|
+
// as the run's cumulative figure, and a child's smaller total arriving
|
|
722
|
+
// last would read as the run getting cheaper.
|
|
723
|
+
const tokens = event.tokens && typeof event.tokens === "object" ? event.tokens : {};
|
|
724
|
+
usage.costUsd = numberOrZero(event.cumulativeUsd);
|
|
725
|
+
usage.input = numberOrZero(tokens.input);
|
|
726
|
+
usage.output = numberOrZero(tokens.output);
|
|
727
|
+
usage.cacheRead = numberOrZero(tokens.cacheReadTokens);
|
|
728
|
+
usage.cacheWrite = numberOrZero(tokens.cacheCreationTokens);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
656
731
|
// Child warnings are worth surfacing; everything else (context usage,
|
|
657
|
-
// partial tool output
|
|
732
|
+
// partial tool output) stays inside the subagent for now.
|
|
658
733
|
if (type === "runtime_warning" && emit !== undefined) {
|
|
659
734
|
try {
|
|
660
735
|
emit({ ...event, subagentId: callId });
|
package/src/agent/tools/bash.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
runPreparedProcess,
|
|
16
16
|
} from "./shared/process-runner.js";
|
|
17
17
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
18
|
-
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
18
|
+
import { requestToolProcessEnvironment, resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
19
19
|
|
|
20
20
|
const DEFAULT_BASH_TIMEOUT_MS = 120_000;
|
|
21
21
|
const BASH_STARTUP_ENV_KEYS = new Set([
|
|
@@ -120,7 +120,7 @@ export async function bashToolRun(
|
|
|
120
120
|
command: "/bin/bash",
|
|
121
121
|
args: ["--noprofile", "--norc", "-c", command],
|
|
122
122
|
cwd,
|
|
123
|
-
env: cleanBashEnvironment(),
|
|
123
|
+
env: requestToolProcessEnvironment(resolvedCtx, cleanBashEnvironment()),
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
} catch (error) {
|
package/src/agent/tools/exec.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
runPreparedProcess,
|
|
17
17
|
} from "./shared/process-runner.js";
|
|
18
18
|
import { readToolRuntime } from "./shared/runtime-context.js";
|
|
19
|
-
import { resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
19
|
+
import { requestToolProcessEnvironment, resolveSandboxPolicy } from "./shared/tool-context.js";
|
|
20
20
|
|
|
21
21
|
const DEFAULT_EXEC_TIMEOUT_MS = 120_000;
|
|
22
22
|
const MAX_EXEC_ARGS = 256;
|
|
@@ -75,6 +75,7 @@ export async function execToolRun(
|
|
|
75
75
|
const maxChars = positiveInteger(max_output_chars, DEFAULT_MAX_BASH_OUTPUT_CHARS);
|
|
76
76
|
let prepared;
|
|
77
77
|
try {
|
|
78
|
+
const requestEnvironment = requestToolProcessEnvironment(resolvedCtx);
|
|
78
79
|
prepared = await sandbox.prepareCommand({
|
|
79
80
|
policy,
|
|
80
81
|
engine: sandboxEngine ?? resolvedCtx.sandboxEngine ?? undefined,
|
|
@@ -82,6 +83,7 @@ export async function execToolRun(
|
|
|
82
83
|
command: executable,
|
|
83
84
|
args: [...args],
|
|
84
85
|
cwd,
|
|
86
|
+
...(requestEnvironment === undefined ? {} : { env: requestEnvironment }),
|
|
85
87
|
},
|
|
86
88
|
});
|
|
87
89
|
} catch (error) {
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
|
|
39
39
|
// @ts-check
|
|
40
40
|
|
|
41
|
+
import { delimiter } from "node:path";
|
|
42
|
+
|
|
41
43
|
import { passthroughSandbox } from "../../sandbox-seam.js";
|
|
42
44
|
import { DEFAULT_RUNTIME_BRAND, resolveRuntimeBrand } from "../../../runtime-brand.js";
|
|
43
45
|
|
|
@@ -58,6 +60,7 @@ import { DEFAULT_RUNTIME_BRAND, resolveRuntimeBrand } from "../../../runtime-bra
|
|
|
58
60
|
* @property {RuntimeSandboxEngine} [sandboxEngine]
|
|
59
61
|
* @property {RuntimeSandbox} sandbox
|
|
60
62
|
* @property {RuntimeBrand} runtimeBrand
|
|
63
|
+
* @property {{schema: 1, values: Readonly<Record<string, string>>, pathPrepend?: readonly string[]}} [toolEnvironment]
|
|
61
64
|
*/
|
|
62
65
|
|
|
63
66
|
// The data keys (everything except the always-resolved runtimeBrand). A fixed
|
|
@@ -155,3 +158,22 @@ export function resolveSandboxPolicy(ctx, requestPolicy = undefined) {
|
|
|
155
158
|
const merged = sandbox.mergePolicies(ctx?.sandboxPolicy ?? undefined, requestPolicy ?? undefined);
|
|
156
159
|
return merged && merged.mode !== "off" ? merged : undefined;
|
|
157
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Compose the process environment overlay for Bash/Exec. The caller-supplied
|
|
164
|
+
* base carries each tool's own hardening values; request values are applied
|
|
165
|
+
* only at this final process boundary. No global environment is mutated.
|
|
166
|
+
*
|
|
167
|
+
* @param {ToolContext|undefined} ctx
|
|
168
|
+
* @param {Record<string, string|undefined>} [base]
|
|
169
|
+
* @returns {Record<string, string|undefined>|undefined}
|
|
170
|
+
*/
|
|
171
|
+
export function requestToolProcessEnvironment(ctx, base = undefined) {
|
|
172
|
+
const request = ctx?.toolEnvironment;
|
|
173
|
+
if (request === undefined) return base;
|
|
174
|
+
const env = { ...(base ?? {}), ...request.values };
|
|
175
|
+
if (Array.isArray(request.pathPrepend) && request.pathPrepend.length > 0) {
|
|
176
|
+
env.PATH = [...request.pathPrepend, process.env.PATH].filter(Boolean).join(delimiter);
|
|
177
|
+
}
|
|
178
|
+
return env;
|
|
179
|
+
}
|
|
@@ -32,6 +32,38 @@ export function usageFromMessages(messages = []) {
|
|
|
32
32
|
return usage;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Add what this run's subagents spent to the run's own usage.
|
|
37
|
+
*
|
|
38
|
+
* A delegation is work the run asked for and is billed to the same account, but
|
|
39
|
+
* `usageFromMessages` only ever sees this agent's transcript — a subagent keeps
|
|
40
|
+
* its own — so every consumer of the run's usage was short by the whole cost of
|
|
41
|
+
* every subagent. Folding it in here rather than publishing a separate event
|
|
42
|
+
* makes the console's cost, the TUI status bar and the exported metrics correct
|
|
43
|
+
* with no change at any of them. The trade is attribution: a subagent on
|
|
44
|
+
* another model has its spend reported under this run's model, which is the
|
|
45
|
+
* right answer for a run total and the wrong one for a per-model breakdown.
|
|
46
|
+
*
|
|
47
|
+
* `estimatedCost` is this agent's own fallback price, used when the provider
|
|
48
|
+
* priced nothing (subscription auth). Adding to the raw `cost` alone in that
|
|
49
|
+
* case would have replaced this agent's cost with the subagent's rather than
|
|
50
|
+
* summing them.
|
|
51
|
+
*
|
|
52
|
+
* @param {{input: number, output: number, cacheRead: number, cacheWrite: number, cost: number}} usage
|
|
53
|
+
* @param {{costUsd: number, input: number, output: number, cacheRead: number, cacheWrite: number}} delegated
|
|
54
|
+
* @param {number} estimatedCost
|
|
55
|
+
* @returns {{input: number, output: number, cacheRead: number, cacheWrite: number, cost: number}}
|
|
56
|
+
*/
|
|
57
|
+
export function withSubagentUsage(usage, delegated, estimatedCost) {
|
|
58
|
+
return {
|
|
59
|
+
input: usage.input + delegated.input,
|
|
60
|
+
output: usage.output + delegated.output,
|
|
61
|
+
cacheRead: usage.cacheRead + delegated.cacheRead,
|
|
62
|
+
cacheWrite: usage.cacheWrite + delegated.cacheWrite,
|
|
63
|
+
cost: delegated.costUsd > 0 ? (usage.cost || estimatedCost) + delegated.costUsd : usage.cost,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
/**
|
|
36
68
|
* Normalize one provider request's usage into an exact context snapshot.
|
|
37
69
|
* Unlike usageFromMessages(), this deliberately does not aggregate earlier
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
resolveAgentCompactionPolicy,
|
|
32
32
|
resolveRuntimePolicyInputs,
|
|
33
33
|
} from "../../agent/compaction.js";
|
|
34
|
-
import { subagentInvocationCount } from "../../agent/tools/agent-tool.js";
|
|
34
|
+
import { subagentInvocationCount, subagentUsageForRun } from "../../agent/tools/agent-tool.js";
|
|
35
35
|
import { closePiMcpClients } from "../../agent/tools/pi-bridge.js";
|
|
36
36
|
import { readToolRuntime } from "../../agent/tools/shared/runtime-context.js";
|
|
37
37
|
import { createApprovalManager } from "../../agent/approval.js";
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
emitCapabilitiesResolved,
|
|
58
58
|
emitUsageCostEvents,
|
|
59
59
|
usageFromMessages,
|
|
60
|
+
withSubagentUsage,
|
|
60
61
|
} from "./pi-native/result-builder.js";
|
|
61
62
|
import {
|
|
62
63
|
cleanupSessionOnThrow,
|
|
@@ -646,15 +647,24 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
646
647
|
const { runTranscript, lastAssistant, stopReason, finalText, finalThinking } = state;
|
|
647
648
|
const runAssistantCount = state.assistantMessages.length;
|
|
648
649
|
|
|
649
|
-
const
|
|
650
|
+
const ownUsage = usageFromMessages(runTranscript);
|
|
651
|
+
// Priced from this agent's own tokens: it is the fallback for a run the
|
|
652
|
+
// provider did not price, and only these tokens are this model's.
|
|
650
653
|
const estimatedCost = estimateCost({
|
|
651
654
|
resolveCustomPricing: options.resolveCustomPricing,
|
|
652
655
|
model: reference,
|
|
653
|
-
inputTokens:
|
|
654
|
-
outputTokens:
|
|
655
|
-
cachedTokens:
|
|
656
|
-
cacheWriteTokens:
|
|
656
|
+
inputTokens: ownUsage.input,
|
|
657
|
+
outputTokens: ownUsage.output,
|
|
658
|
+
cachedTokens: ownUsage.cacheRead,
|
|
659
|
+
cacheWriteTokens: ownUsage.cacheWrite,
|
|
657
660
|
});
|
|
661
|
+
// Keyed by the same parentRunId the Agent tool stamps its budget under, the
|
|
662
|
+
// way `subagentInvoked` below reads its count.
|
|
663
|
+
const usage = withSubagentUsage(
|
|
664
|
+
ownUsage,
|
|
665
|
+
subagentUsageForRun(options.subagents, (options.toolContext ?? readToolRuntime())?.runId),
|
|
666
|
+
estimatedCost,
|
|
667
|
+
);
|
|
658
668
|
emitUsageCostEvents({
|
|
659
669
|
onEvent,
|
|
660
670
|
resolved,
|
|
@@ -13,6 +13,7 @@ export const COMMON_CAPABILITIES = {
|
|
|
13
13
|
supports_builtin_tools: true,
|
|
14
14
|
supports_live_input: true,
|
|
15
15
|
supports_native_subagents: true,
|
|
16
|
+
supports_request_tool_environment: false,
|
|
16
17
|
supports_fast_mode: false,
|
|
17
18
|
tool_policy: TOOL_POLICY_PROJECTED,
|
|
18
19
|
};
|
|
@@ -34,6 +35,7 @@ export const RUNTIME_CAPABILITIES = {
|
|
|
34
35
|
// The pi-native bridge does not (yet) wire native subagents / an AskAgent
|
|
35
36
|
// tool, so advertise no support rather than letting callers expect it.
|
|
36
37
|
supports_native_subagents: false,
|
|
38
|
+
supports_request_tool_environment: true,
|
|
37
39
|
},
|
|
38
40
|
codex: {
|
|
39
41
|
runtime: "cli",
|
package/src/ai/runtime/router.js
CHANGED
|
@@ -98,7 +98,7 @@ const RESOLVER_PROTECTED_OPTION_KEYS = new Set([
|
|
|
98
98
|
"sessionId", "providerSessionId", "sessionKeepAlive", "sessionIdleTimeoutMs",
|
|
99
99
|
"diagnosticsSeed", "systemPromptPrefix", "sandboxPolicy", "sandboxEngine", "sandbox",
|
|
100
100
|
"allowedTools", "disallowedTools", "permissionMode", "mcpServers", "skills",
|
|
101
|
-
"outputSchema", "nativeSubagents", "liveInput", "fastMode",
|
|
101
|
+
"outputSchema", "nativeSubagents", "liveInput", "fastMode", "toolEnvironment",
|
|
102
102
|
]);
|
|
103
103
|
|
|
104
104
|
/**
|
|
@@ -1046,6 +1046,9 @@ function entrySatisfiesRequirements(entry, options) {
|
|
|
1046
1046
|
if (options.liveInput) {
|
|
1047
1047
|
effectiveRequires.supports_live_input = true;
|
|
1048
1048
|
}
|
|
1049
|
+
if (options.toolEnvironment !== undefined) {
|
|
1050
|
+
effectiveRequires.supports_request_tool_environment = true;
|
|
1051
|
+
}
|
|
1049
1052
|
if (options.fastMode === true) {
|
|
1050
1053
|
effectiveRequires.supports_fast_mode = true;
|
|
1051
1054
|
}
|
package/src/ai/types.js
CHANGED
|
@@ -156,6 +156,7 @@
|
|
|
156
156
|
* @property {Object} [outputSchema]
|
|
157
157
|
* @property {string} [runArtifactDir]
|
|
158
158
|
* @property {AbortSignal} [abortSignal]
|
|
159
|
+
* @property {{schema: 1, values: Readonly<Record<string, string>>, pathPrepend?: readonly string[]}} [toolEnvironment] Host-only environment for Bash, Exec, and nested subagents in this run.
|
|
159
160
|
* @property {import('../agent/sandbox-seam.js').SandboxPolicy} [sandboxPolicy] Per-run sandbox policy; merged monotonically with the host policy (see resolveSandboxPolicy, agent/tools/shared/tool-context.js).
|
|
160
161
|
* @property {import('../agent/sandbox-seam.js').RuntimeSandboxEngine} [sandboxEngine] Per-run concrete sandbox engine handed to the active sandbox implementation.
|
|
161
162
|
* @property {import('../agent/sandbox-seam.js').RuntimeSandbox} [sandbox] Per-run sandbox IMPLEMENTATION override; when set it enforces this run's tools instead of the host/ToolContext impl (precedence run > host > passthrough). Policy DATA still merges monotonically (I13); this overrides only the enforcing code.
|
|
@@ -279,6 +280,7 @@
|
|
|
279
280
|
* @property {boolean} [supports_builtin_tools]
|
|
280
281
|
* @property {boolean} [supports_live_input]
|
|
281
282
|
* @property {boolean} [supports_native_subagents]
|
|
283
|
+
* @property {boolean} [supports_request_tool_environment]
|
|
282
284
|
* @property {boolean} [supports_fast_mode]
|
|
283
285
|
* @property {"projected"|"allow_all_only"} [tool_policy] Whether the bridge can
|
|
284
286
|
* project named allow/deny policies or accepts only a semantically unrestricted
|
package/src/runtime.js
CHANGED
|
@@ -172,6 +172,7 @@ export function createRuntime(host = {}) {
|
|
|
172
172
|
// list of its own to consult, so it forwards what it was given.
|
|
173
173
|
...(request.skills === undefined ? {} : { skills: request.skills }),
|
|
174
174
|
...(request.skillsRoot === undefined ? {} : { skillsRoot: request.skillsRoot }),
|
|
175
|
+
...(request.toolEnvironment === undefined ? {} : { toolEnvironment: request.toolEnvironment }),
|
|
175
176
|
...(request.executionMode === undefined ? {} : { executionMode: request.executionMode }),
|
|
176
177
|
...(request.cwd === undefined ? {} : { cwd: request.cwd }),
|
|
177
178
|
// A profile that pins effort — declared or authored at call time — means it
|
|
@@ -210,6 +211,12 @@ export function createRuntime(host = {}) {
|
|
|
210
211
|
});
|
|
211
212
|
const liveInput = instrumentLiveInputAppliedEvents(options.liveInput, hub.emit);
|
|
212
213
|
const prompts = resolvePrompts(host.prompts, options.prompts);
|
|
214
|
+
// A request-scoped environment must never mutate the long-lived runtime's
|
|
215
|
+
// shared ToolContext. Clone only for this call, preserving configureTools
|
|
216
|
+
// updates while keeping credentials isolated between concurrent turns.
|
|
217
|
+
const runToolContext = options.toolEnvironment === undefined
|
|
218
|
+
? toolContext
|
|
219
|
+
: { ...toolContext, toolEnvironment: options.toolEnvironment };
|
|
213
220
|
// Default the nested-run callback so the Agent built-in is usable without
|
|
214
221
|
// host wiring; the depth field is left exactly as the caller set it, since
|
|
215
222
|
// defaultSubagentRun is what increments it for the child.
|
|
@@ -226,7 +233,7 @@ export function createRuntime(host = {}) {
|
|
|
226
233
|
model: options.model,
|
|
227
234
|
executionMode,
|
|
228
235
|
runtimeBrand,
|
|
229
|
-
toolContext,
|
|
236
|
+
toolContext: runToolContext,
|
|
230
237
|
observerHub: hub,
|
|
231
238
|
onEvent: hub.emit,
|
|
232
239
|
...(liveInput === undefined ? {} : { liveInput }),
|
|
@@ -13,11 +13,32 @@
|
|
|
13
13
|
* @returns {number}
|
|
14
14
|
*/
|
|
15
15
|
export function subagentInvocationCount(subagents: any, parentRunId: string | undefined): number;
|
|
16
|
+
/**
|
|
17
|
+
* What this logical run's subagents spent, summed across every delegation.
|
|
18
|
+
*
|
|
19
|
+
* A delegation is work the run asked for, so its cost belongs to the run's
|
|
20
|
+
* total — a provider folds this into its own usage before reporting, which is
|
|
21
|
+
* what makes the console's cost, the TUI status bar and the exported metrics
|
|
22
|
+
* agree with the bill. Same read-only-accessor contract as
|
|
23
|
+
* `subagentInvocationCount`: `__budgets` stays private. All zeroes when nothing
|
|
24
|
+
* delegated, which is the truthful answer for a run that never used the tool.
|
|
25
|
+
*
|
|
26
|
+
* @param {*} subagents The run-scoped options object, or undefined.
|
|
27
|
+
* @param {string|undefined} parentRunId
|
|
28
|
+
* @returns {{costUsd: number, input: number, output: number, cacheRead: number, cacheWrite: number}}
|
|
29
|
+
*/
|
|
30
|
+
export function subagentUsageForRun(subagents: any, parentRunId: string | undefined): {
|
|
31
|
+
costUsd: number;
|
|
32
|
+
input: number;
|
|
33
|
+
output: number;
|
|
34
|
+
cacheRead: number;
|
|
35
|
+
cacheWrite: number;
|
|
36
|
+
};
|
|
16
37
|
/**
|
|
17
38
|
* Build the `Agent` tool, or null when subagents are unavailable for this run.
|
|
18
39
|
*
|
|
19
40
|
* @param {RuntimeSubagentsOptions|null|undefined} subagents
|
|
20
|
-
* @param {{model?: *, executionMode?: string, cwd?: string, parentRunId?: string, sandboxPolicy?: *, sandboxEngine?: *, skills?: {name: string, description?: string}[], skillsRoot?: string, onEvent?: (event: *) => void}} [context]
|
|
41
|
+
* @param {{model?: *, executionMode?: string, cwd?: string, parentRunId?: string, sandboxPolicy?: *, sandboxEngine?: *, skills?: {name: string, description?: string}[], skillsRoot?: string, toolEnvironment?: *, onEvent?: (event: *) => void}} [context]
|
|
21
42
|
* @returns {*|null}
|
|
22
43
|
*/
|
|
23
44
|
export function createAgentTool(subagents: RuntimeSubagentsOptions | null | undefined, context?: {
|
|
@@ -32,6 +53,7 @@ export function createAgentTool(subagents: RuntimeSubagentsOptions | null | unde
|
|
|
32
53
|
description?: string;
|
|
33
54
|
}[];
|
|
34
55
|
skillsRoot?: string;
|
|
56
|
+
toolEnvironment?: any;
|
|
35
57
|
onEvent?: (event: any) => void;
|
|
36
58
|
}): any | null;
|
|
37
59
|
/**
|
|
@@ -30,6 +30,16 @@ export function resetToolContext(ctx: ToolContext): ToolContext;
|
|
|
30
30
|
* @returns {SandboxPolicy|undefined}
|
|
31
31
|
*/
|
|
32
32
|
export function resolveSandboxPolicy(ctx: ToolContext | undefined, requestPolicy?: SandboxPolicy): SandboxPolicy | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Compose the process environment overlay for Bash/Exec. The caller-supplied
|
|
35
|
+
* base carries each tool's own hardening values; request values are applied
|
|
36
|
+
* only at this final process boundary. No global environment is mutated.
|
|
37
|
+
*
|
|
38
|
+
* @param {ToolContext|undefined} ctx
|
|
39
|
+
* @param {Record<string, string|undefined>} [base]
|
|
40
|
+
* @returns {Record<string, string|undefined>|undefined}
|
|
41
|
+
*/
|
|
42
|
+
export function requestToolProcessEnvironment(ctx: ToolContext | undefined, base?: Record<string, string | undefined>): Record<string, string | undefined> | undefined;
|
|
33
43
|
export type RuntimeBrand = import("../../../runtime-brand.js").RuntimeBrand;
|
|
34
44
|
export type SandboxPolicy = import("../../sandbox-seam.js").SandboxPolicy;
|
|
35
45
|
export type RuntimeSandboxEngine = import("../../sandbox-seam.js").RuntimeSandboxEngine;
|
|
@@ -45,4 +55,9 @@ export type ToolContext = {
|
|
|
45
55
|
sandboxEngine?: RuntimeSandboxEngine;
|
|
46
56
|
sandbox: RuntimeSandbox;
|
|
47
57
|
runtimeBrand: RuntimeBrand;
|
|
58
|
+
toolEnvironment?: {
|
|
59
|
+
schema: 1;
|
|
60
|
+
values: Readonly<Record<string, string>>;
|
|
61
|
+
pathPrepend?: readonly string[];
|
|
62
|
+
};
|
|
48
63
|
};
|
|
@@ -10,6 +10,47 @@ export function usageFromMessages(messages?: Array<any>): {
|
|
|
10
10
|
cacheWrite: number;
|
|
11
11
|
cost: number;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Add what this run's subagents spent to the run's own usage.
|
|
15
|
+
*
|
|
16
|
+
* A delegation is work the run asked for and is billed to the same account, but
|
|
17
|
+
* `usageFromMessages` only ever sees this agent's transcript — a subagent keeps
|
|
18
|
+
* its own — so every consumer of the run's usage was short by the whole cost of
|
|
19
|
+
* every subagent. Folding it in here rather than publishing a separate event
|
|
20
|
+
* makes the console's cost, the TUI status bar and the exported metrics correct
|
|
21
|
+
* with no change at any of them. The trade is attribution: a subagent on
|
|
22
|
+
* another model has its spend reported under this run's model, which is the
|
|
23
|
+
* right answer for a run total and the wrong one for a per-model breakdown.
|
|
24
|
+
*
|
|
25
|
+
* `estimatedCost` is this agent's own fallback price, used when the provider
|
|
26
|
+
* priced nothing (subscription auth). Adding to the raw `cost` alone in that
|
|
27
|
+
* case would have replaced this agent's cost with the subagent's rather than
|
|
28
|
+
* summing them.
|
|
29
|
+
*
|
|
30
|
+
* @param {{input: number, output: number, cacheRead: number, cacheWrite: number, cost: number}} usage
|
|
31
|
+
* @param {{costUsd: number, input: number, output: number, cacheRead: number, cacheWrite: number}} delegated
|
|
32
|
+
* @param {number} estimatedCost
|
|
33
|
+
* @returns {{input: number, output: number, cacheRead: number, cacheWrite: number, cost: number}}
|
|
34
|
+
*/
|
|
35
|
+
export function withSubagentUsage(usage: {
|
|
36
|
+
input: number;
|
|
37
|
+
output: number;
|
|
38
|
+
cacheRead: number;
|
|
39
|
+
cacheWrite: number;
|
|
40
|
+
cost: number;
|
|
41
|
+
}, delegated: {
|
|
42
|
+
costUsd: number;
|
|
43
|
+
input: number;
|
|
44
|
+
output: number;
|
|
45
|
+
cacheRead: number;
|
|
46
|
+
cacheWrite: number;
|
|
47
|
+
}, estimatedCost: number): {
|
|
48
|
+
input: number;
|
|
49
|
+
output: number;
|
|
50
|
+
cacheRead: number;
|
|
51
|
+
cacheWrite: number;
|
|
52
|
+
cost: number;
|
|
53
|
+
};
|
|
13
54
|
/**
|
|
14
55
|
* Normalize one provider request's usage into an exact context snapshot.
|
|
15
56
|
* Unlike usageFromMessages(), this deliberately does not aggregate earlier
|
|
@@ -9,6 +9,7 @@ export namespace COMMON_CAPABILITIES {
|
|
|
9
9
|
export let supports_builtin_tools: boolean;
|
|
10
10
|
export let supports_live_input: boolean;
|
|
11
11
|
export let supports_native_subagents: boolean;
|
|
12
|
+
export let supports_request_tool_environment: boolean;
|
|
12
13
|
export let supports_fast_mode: boolean;
|
|
13
14
|
export { TOOL_POLICY_PROJECTED as tool_policy };
|
|
14
15
|
}
|
|
@@ -23,6 +24,8 @@ export namespace RUNTIME_CAPABILITIES {
|
|
|
23
24
|
export { supports_session_resume_2 as supports_session_resume };
|
|
24
25
|
let supports_native_subagents_1: boolean;
|
|
25
26
|
export { supports_native_subagents_1 as supports_native_subagents };
|
|
27
|
+
let supports_request_tool_environment_1: boolean;
|
|
28
|
+
export { supports_request_tool_environment_1 as supports_request_tool_environment };
|
|
26
29
|
let runtime_1: string;
|
|
27
30
|
export { runtime_1 as runtime };
|
|
28
31
|
}
|
package/types/ai/types.d.ts
CHANGED
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
* @property {Object} [outputSchema]
|
|
128
128
|
* @property {string} [runArtifactDir]
|
|
129
129
|
* @property {AbortSignal} [abortSignal]
|
|
130
|
+
* @property {{schema: 1, values: Readonly<Record<string, string>>, pathPrepend?: readonly string[]}} [toolEnvironment] Host-only environment for Bash, Exec, and nested subagents in this run.
|
|
130
131
|
* @property {import('../agent/sandbox-seam.js').SandboxPolicy} [sandboxPolicy] Per-run sandbox policy; merged monotonically with the host policy (see resolveSandboxPolicy, agent/tools/shared/tool-context.js).
|
|
131
132
|
* @property {import('../agent/sandbox-seam.js').RuntimeSandboxEngine} [sandboxEngine] Per-run concrete sandbox engine handed to the active sandbox implementation.
|
|
132
133
|
* @property {import('../agent/sandbox-seam.js').RuntimeSandbox} [sandbox] Per-run sandbox IMPLEMENTATION override; when set it enforces this run's tools instead of the host/ToolContext impl (precedence run > host > passthrough). Policy DATA still merges monotonically (I13); this overrides only the enforcing code.
|
|
@@ -243,6 +244,7 @@
|
|
|
243
244
|
* @property {boolean} [supports_builtin_tools]
|
|
244
245
|
* @property {boolean} [supports_live_input]
|
|
245
246
|
* @property {boolean} [supports_native_subagents]
|
|
247
|
+
* @property {boolean} [supports_request_tool_environment]
|
|
246
248
|
* @property {boolean} [supports_fast_mode]
|
|
247
249
|
* @property {"projected"|"allow_all_only"} [tool_policy] Whether the bridge can
|
|
248
250
|
* project named allow/deny policies or accepts only a semantically unrestricted
|
|
@@ -596,6 +598,14 @@ export type RuntimeRunOptions = {
|
|
|
596
598
|
outputSchema?: any;
|
|
597
599
|
runArtifactDir?: string;
|
|
598
600
|
abortSignal?: AbortSignal;
|
|
601
|
+
/**
|
|
602
|
+
* Host-only environment for Bash, Exec, and nested subagents in this run.
|
|
603
|
+
*/
|
|
604
|
+
toolEnvironment?: {
|
|
605
|
+
schema: 1;
|
|
606
|
+
values: Readonly<Record<string, string>>;
|
|
607
|
+
pathPrepend?: readonly string[];
|
|
608
|
+
};
|
|
599
609
|
/**
|
|
600
610
|
* Per-run sandbox policy; merged monotonically with the host policy (see resolveSandboxPolicy, agent/tools/shared/tool-context.js).
|
|
601
611
|
*/
|
|
@@ -837,6 +847,7 @@ export type RuntimeCapabilities = {
|
|
|
837
847
|
supports_builtin_tools?: boolean;
|
|
838
848
|
supports_live_input?: boolean;
|
|
839
849
|
supports_native_subagents?: boolean;
|
|
850
|
+
supports_request_tool_environment?: boolean;
|
|
840
851
|
supports_fast_mode?: boolean;
|
|
841
852
|
/**
|
|
842
853
|
* Whether the bridge can
|