@mono-agent/agent-runtime 0.15.4 → 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.
@@ -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
@@ -8,7 +8,6 @@
8
8
  // caller-owned runState.
9
9
 
10
10
  import { AgentHarness } from "@earendil-works/pi-agent-core";
11
- import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
12
11
  import {
13
12
  createStructuredOutputTool,
14
13
  getPiBuiltinTools,
@@ -134,6 +133,15 @@ export async function buildTurnTools(runState, {
134
133
  // child is never less sandboxed than the parent that spawned it.
135
134
  sandboxPolicy: options.sandboxPolicy,
136
135
  sandboxEngine,
136
+ // The same skills this turn was disclosed. A child runs OUTSIDE the
137
+ // harness, so without this it gets no index and — because ReadSkill is
138
+ // only built when `skills` is non-empty — no way to read one either. It
139
+ // then rediscovers by trial and error whatever its parent could simply
140
+ // have looked up. Pass-through only: whether the child actually receives
141
+ // them is the host's decision, not this layer's.
142
+ skills: options.skills,
143
+ skillsRoot: options.skillsRoot,
144
+ toolEnvironment: options.toolEnvironment,
137
145
  },
138
146
  ctx: runCtx,
139
147
  }));
@@ -241,7 +249,6 @@ export function toolResultErrorOverride(details) {
241
249
  }
242
250
 
243
251
  export function buildTurnHarness(runState, {
244
- cwd,
245
252
  session,
246
253
  piModels,
247
254
  model,
@@ -259,8 +266,12 @@ export function buildTurnHarness(runState, {
259
266
  sdk,
260
267
  reference,
261
268
  }) {
269
+ // pi-agent-core 0.83.0 removed `env` from AgentHarnessOptions: an
270
+ // ExecutionEnv now reaches tools through the generic per-turn `toolContext`
271
+ // instead. mono-agent needs neither — it uses none of pi's built-in
272
+ // file/shell tools, and its own tools close over what they need — so the
273
+ // option is dropped rather than migrated.
262
274
  const harness = new AgentHarness({
263
- env: new NodeExecutionEnv({ cwd: cwd || process.cwd() }),
264
275
  session,
265
276
  models: piModels,
266
277
  model,
@@ -31,7 +31,9 @@ import {
31
31
  resolveAgentCompactionPolicy,
32
32
  resolveRuntimePolicyInputs,
33
33
  } from "../../agent/compaction.js";
34
+ import { subagentInvocationCount, subagentUsageForRun } from "../../agent/tools/agent-tool.js";
34
35
  import { closePiMcpClients } from "../../agent/tools/pi-bridge.js";
36
+ import { readToolRuntime } from "../../agent/tools/shared/runtime-context.js";
35
37
  import { createApprovalManager } from "../../agent/approval.js";
36
38
  import { buildCapabilitiesUsed, toolCompactionAppliedFromWarnings } from "../runtime/capabilities-used.js";
37
39
  import { reasoningLevelsForPiModel, resolvePiRuntimeModel } from "./pi-models.js";
@@ -55,6 +57,7 @@ import {
55
57
  emitCapabilitiesResolved,
56
58
  emitUsageCostEvents,
57
59
  usageFromMessages,
60
+ withSubagentUsage,
58
61
  } from "./pi-native/result-builder.js";
59
62
  import {
60
63
  cleanupSessionOnThrow,
@@ -481,7 +484,6 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
481
484
  // external abort handler (which sets runState.externalAbort and aborts the
482
485
  // harness). Sets runState.harness + runState.removeAbortHandler.
483
486
  harness = buildTurnHarness(runState, {
484
- cwd: options.cwd,
485
487
  session: runState.session,
486
488
  piModels,
487
489
  model: runtime.model,
@@ -645,15 +647,24 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
645
647
  const { runTranscript, lastAssistant, stopReason, finalText, finalThinking } = state;
646
648
  const runAssistantCount = state.assistantMessages.length;
647
649
 
648
- const usage = usageFromMessages(runTranscript);
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.
649
653
  const estimatedCost = estimateCost({
650
654
  resolveCustomPricing: options.resolveCustomPricing,
651
655
  model: reference,
652
- inputTokens: usage.input,
653
- outputTokens: usage.output,
654
- cachedTokens: usage.cacheRead,
655
- cacheWriteTokens: usage.cacheWrite,
656
+ inputTokens: ownUsage.input,
657
+ outputTokens: ownUsage.output,
658
+ cachedTokens: ownUsage.cacheRead,
659
+ cacheWriteTokens: ownUsage.cacheWrite,
656
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
+ );
657
668
  emitUsageCostEvents({
658
669
  onEvent,
659
670
  resolved,
@@ -709,8 +720,23 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
709
720
  promptCacheActive: usage.cacheRead > 0 || usage.cacheWrite > 0,
710
721
  thinkingEnabled: effectiveThinkingLevel !== "off" && effectiveThinkingLevel !== "low",
711
722
  structuredOutputEnforced: !!options.outputSchema,
712
- subagentInvoked: false,
723
+ // What actually happened, not what was configured. This used to be a
724
+ // hardcoded `false`, which reported "no subagent ran" for runs that had
725
+ // just spawned several — the one signal that would have shown delegation
726
+ // working said it never happened.
727
+ //
728
+ // The count is keyed by the same parentRunId the Agent tool stamps its
729
+ // budget under (turn-runner threads `runCtx?.runId`); `runId` survives the
730
+ // sandbox-branch spread there, so reading it off the tool context directly
731
+ // yields the same key.
732
+ subagentInvoked: subagentInvocationCount(
733
+ options.subagents,
734
+ (options.toolContext ?? readToolRuntime())?.runId,
735
+ ) > 0,
713
736
  mcpServersUsed: mcpClients.map((entry) => entry?.name).filter(Boolean),
737
+ // Empty by contract, not by omission: "native" means provider-native
738
+ // subagents (Claude's Task tool). mono-agent's `Agent` is its own, so pi
739
+ // has none — `subagent_invoked` above is where pi delegation is reported.
714
740
  nativeSubagentsUsed: [],
715
741
  toolCompactionApplied: toolCompactionAppliedFromWarnings(runtimeWarnings),
716
742
  // Tristate: true = a compaction fired this run (proactive or reactive),
@@ -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",
@@ -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
  /**
@@ -335,11 +335,16 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
335
335
  // A same-model retry is not a failover: only the first attempt of a new
336
336
  // route announces a transition.
337
337
  if (retryIndex === 0 && failoverHistory.length > 0) {
338
+ const previous = failoverHistory[failoverHistory.length - 1];
338
339
  emit(callOptions, {
339
340
  type: "provider_failover_started",
340
- from: modelKey(failoverHistory[failoverHistory.length - 1]?.model),
341
+ from: modelKey(previous?.model),
341
342
  to: modelKey(entry.model),
342
343
  attemptIndex: i,
344
+ // Why the route changed, in the same vocabulary provider_retry_started
345
+ // uses. Operators reading a transcript need the cause next to the
346
+ // transition, not only in the run artifact's failoverHistory.
347
+ reason: previous?.retryableSubkind || previous?.failureKind || null,
343
348
  });
344
349
  }
345
350
 
@@ -378,9 +383,12 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
378
383
  // (failover)" when X is still the route the operator asked for.
379
384
  if (failoverHistory.some((attempt) => modelKey(attempt.model) !== modelKey(entry.model))) {
380
385
  emit(callOptions, {
386
+ // modelKey, not the ModelRef: every consumer of this event reads
387
+ // `model` as a string (responder.ts's stringField is string-only),
388
+ // so an object here is dropped silently rather than rendered.
381
389
  type: "provider_failover_completed",
382
390
  attemptIndex: i,
383
- model: entry.model,
391
+ model: modelKey(entry.model),
384
392
  });
385
393
  }
386
394
  return { ...result, failoverHistory, routeSafetyHistory };
@@ -1038,6 +1046,9 @@ function entrySatisfiesRequirements(entry, options) {
1038
1046
  if (options.liveInput) {
1039
1047
  effectiveRequires.supports_live_input = true;
1040
1048
  }
1049
+ if (options.toolEnvironment !== undefined) {
1050
+ effectiveRequires.supports_request_tool_environment = true;
1051
+ }
1041
1052
  if (options.fastMode === true) {
1042
1053
  effectiveRequires.supports_fast_mode = true;
1043
1054
  }
package/src/ai/types.js CHANGED
@@ -147,7 +147,8 @@
147
147
  * @property {boolean} [fastMode]
148
148
  * @property {string} [cwd]
149
149
  * @property {Object<string, Object>} [mcpServers]
150
- * @property {ReadonlyArray<Object>} [skills] Runtime skill metadata for progressive disclosure.
150
+ * @property {ReadonlyArray<{name: string, description?: string}>} [skills] Skills disclosed to this run, as `{name, description}`. Non-empty makes `supports_skills` a routing requirement (see router.js), so a chain entry that lacks it is skipped.
151
+ * @property {string} [skillsRoot] Directory holding `<name>/SKILL.md`. Required alongside `skills` for `ReadSkill` to be built.
151
152
  * @property {ReadonlyArray<string>} [allowedTools]
152
153
  * @property {ReadonlyArray<string>} [disallowedTools]
153
154
  * @property {string} [permissionMode]
@@ -155,6 +156,7 @@
155
156
  * @property {Object} [outputSchema]
156
157
  * @property {string} [runArtifactDir]
157
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.
158
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).
159
161
  * @property {import('../agent/sandbox-seam.js').RuntimeSandboxEngine} [sandboxEngine] Per-run concrete sandbox engine handed to the active sandbox implementation.
160
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.
@@ -278,6 +280,7 @@
278
280
  * @property {boolean} [supports_builtin_tools]
279
281
  * @property {boolean} [supports_live_input]
280
282
  * @property {boolean} [supports_native_subagents]
283
+ * @property {boolean} [supports_request_tool_environment]
281
284
  * @property {boolean} [supports_fast_mode]
282
285
  * @property {"projected"|"allow_all_only"} [tool_policy] Whether the bridge can
283
286
  * project named allow/deny policies or accepts only a semantically unrestricted
package/src/pi-auth.js CHANGED
@@ -4,7 +4,7 @@ import { realpathSync } from "node:fs";
4
4
  import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
5
5
  import { basename, dirname, join, resolve } from "node:path";
6
6
 
7
- import { getOAuthApiKey } from "@earendil-works/pi-ai/oauth";
7
+ import { resolveOAuthApiKey } from "./ai/pi-oauth-compat.js";
8
8
 
9
9
  /**
10
10
  * @typedef {{type: "api_key", key?: string, env?: Object<string, *>}} PiApiKeyCredential
@@ -41,7 +41,7 @@ export function createPiOAuthApiKeyResolver(options = {}) {
41
41
  return undefined;
42
42
  }
43
43
 
44
- const result = await getOAuthApiKey(provider, cloneAuth(auth));
44
+ const result = await resolveOAuthApiKey(provider, cloneAuth(auth));
45
45
  if (result === null || result === undefined || typeof result.apiKey !== "string" || result.apiKey.length === 0) {
46
46
  return undefined;
47
47
  }
package/src/runtime.js CHANGED
@@ -165,6 +165,14 @@ export function createRuntime(host = {}) {
165
165
  // WebSearch, so even a read-only profile could bypass network policy.
166
166
  ...(request.sandboxPolicy === undefined ? {} : { sandboxPolicy: request.sandboxPolicy }),
167
167
  ...(request.sandboxEngine === undefined ? {} : { sandboxEngine: request.sandboxEngine }),
168
+ // The parent's disclosed skills, for the same reason: they are a per-run
169
+ // option, so a child that does not receive them has no ReadSkill tool and no
170
+ // index, and must rediscover by trial and error what its parent could look
171
+ // up. A host-supplied `run` may gate this; the default has no route or deny
172
+ // list of its own to consult, so it forwards what it was given.
173
+ ...(request.skills === undefined ? {} : { skills: request.skills }),
174
+ ...(request.skillsRoot === undefined ? {} : { skillsRoot: request.skillsRoot }),
175
+ ...(request.toolEnvironment === undefined ? {} : { toolEnvironment: request.toolEnvironment }),
168
176
  ...(request.executionMode === undefined ? {} : { executionMode: request.executionMode }),
169
177
  ...(request.cwd === undefined ? {} : { cwd: request.cwd }),
170
178
  // A profile that pins effort — declared or authored at call time — means it
@@ -203,6 +211,12 @@ export function createRuntime(host = {}) {
203
211
  });
204
212
  const liveInput = instrumentLiveInputAppliedEvents(options.liveInput, hub.emit);
205
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 };
206
220
  // Default the nested-run callback so the Agent built-in is usable without
207
221
  // host wiring; the depth field is left exactly as the caller set it, since
208
222
  // defaultSubagentRun is what increments it for the child.
@@ -219,7 +233,7 @@ export function createRuntime(host = {}) {
219
233
  model: options.model,
220
234
  executionMode,
221
235
  runtimeBrand,
222
- toolContext,
236
+ toolContext: runToolContext,
223
237
  observerHub: hub,
224
238
  onEvent: hub.emit,
225
239
  ...(liveInput === undefined ? {} : { liveInput }),
@@ -1,8 +1,44 @@
1
+ /**
2
+ * How many subagents this logical run actually spawned.
3
+ *
4
+ * A read-only accessor so a provider can report `subagent_invoked` truthfully
5
+ * without reaching into `__budgets`, which is a deliberately private,
6
+ * non-enumerable implementation detail. Returns 0 when nothing was ever
7
+ * registered — a run with no `Agent` tool never creates a budget entry, and that
8
+ * is indistinguishable from one that had the tool and never used it, which is
9
+ * exactly what "no subagent was invoked" means for this signal.
10
+ *
11
+ * @param {*} subagents The run-scoped options object, or undefined.
12
+ * @param {string|undefined} parentRunId
13
+ * @returns {number}
14
+ */
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
+ };
1
37
  /**
2
38
  * Build the `Agent` tool, or null when subagents are unavailable for this run.
3
39
  *
4
40
  * @param {RuntimeSubagentsOptions|null|undefined} subagents
5
- * @param {{model?: *, executionMode?: string, cwd?: string, parentRunId?: string, sandboxPolicy?: *, sandboxEngine?: *, 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]
6
42
  * @returns {*|null}
7
43
  */
8
44
  export function createAgentTool(subagents: RuntimeSubagentsOptions | null | undefined, context?: {
@@ -12,6 +48,12 @@ export function createAgentTool(subagents: RuntimeSubagentsOptions | null | unde
12
48
  parentRunId?: string;
13
49
  sandboxPolicy?: any;
14
50
  sandboxEngine?: any;
51
+ skills?: {
52
+ name: string;
53
+ description?: string;
54
+ }[];
55
+ skillsRoot?: string;
56
+ toolEnvironment?: any;
15
57
  onEvent?: (event: any) => void;
16
58
  }): any | null;
17
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
  };
@@ -0,0 +1,57 @@
1
+ /** @internal Exported only so tests can force a rebuild of the memoized index. */
2
+ export function resetPiProviderIndexForTests(): void;
3
+ /**
4
+ * The OAuth implementation for a Pi provider id, or undefined when the provider
5
+ * is unknown or supports only API-key auth (e.g. `opencode-go`).
6
+ *
7
+ * @param {string} providerId
8
+ * @returns {OAuthAuth|undefined}
9
+ */
10
+ export function getPiOAuthAuth(providerId: string): OAuthAuth | undefined;
11
+ /**
12
+ * Every Pi provider id that supports OAuth. Replaces
13
+ * `getOAuthProviders().map((provider) => provider.id)`.
14
+ *
15
+ * @returns {string[]}
16
+ */
17
+ export function getPiOAuthProviderIds(): string[];
18
+ /**
19
+ * Resolve an API key from stored OAuth credentials, refreshing first when the
20
+ * token has expired.
21
+ *
22
+ * Reproduces pi-ai 0.80.6's `getOAuthApiKey(providerId, credentials)` contract
23
+ * so its call sites keep their shape: takes the whole provider-keyed credential
24
+ * map, returns `null` when this provider has no stored credential, and is
25
+ * *pure* — the refreshed credential comes back as `newCredentials` for the
26
+ * caller to persist rather than being written here.
27
+ *
28
+ * The refresh trigger is deliberately the old exact-expiry check. pi's own
29
+ * `Models.getAuth()` refreshes five minutes ahead of expiry; matching that would
30
+ * change live token rotation timing, which this migration does not intend.
31
+ *
32
+ * @param {string} providerId
33
+ * @param {Record<string, *>|undefined} credentials Provider-keyed credential map.
34
+ * @returns {Promise<{newCredentials: OAuthCredential, apiKey: string|undefined}|null>}
35
+ */
36
+ export function resolveOAuthApiKey(providerId: string, credentials: Record<string, any> | undefined): Promise<{
37
+ newCredentials: OAuthCredential;
38
+ apiKey: string | undefined;
39
+ } | null>;
40
+ /**
41
+ * Bridge the legacy six-callback OAuth surface onto 0.83.0's single
42
+ * `prompt`/`notify` pair.
43
+ *
44
+ * `manual_code` must stay wired to `onManualCodeInput`: Anthropic races its
45
+ * localhost callback against a pasted redirect URL, and that path is the reason
46
+ * `agent-app`'s `runPiOAuthLogin` exists at all.
47
+ *
48
+ * @param {OAuthLoginCallbacks} callbacks
49
+ * @returns {AuthInteraction}
50
+ */
51
+ export function toAuthInteraction(callbacks: OAuthLoginCallbacks): AuthInteraction;
52
+ export type OAuthAuth = import("@earendil-works/pi-ai").OAuthAuth;
53
+ export type OAuthCredential = import("@earendil-works/pi-ai").OAuthCredential;
54
+ export type AuthInteraction = import("@earendil-works/pi-ai").AuthInteraction;
55
+ export type AuthPrompt = import("@earendil-works/pi-ai").AuthPrompt;
56
+ export type AuthEvent = import("@earendil-works/pi-ai").AuthEvent;
57
+ export type OAuthLoginCallbacks = import("@earendil-works/pi-ai/oauth").OAuthLoginCallbacks;
@@ -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
@@ -45,8 +45,7 @@ export function thinkingLevelForEffort(effort: string, capabilities: any): strin
45
45
  export function toolResultErrorOverride(details: any): {
46
46
  isError: true;
47
47
  } | undefined;
48
- export function buildTurnHarness(runState: any, { cwd, session, piModels, model, thinkingLevel, systemPrompt, outputSchema, tools, transport, maxRetries, maxRetryDelayMs, steeringMode, onEvent, options, toolLimits, sdk, reference, }: {
49
- cwd: any;
48
+ export function buildTurnHarness(runState: any, { session, piModels, model, thinkingLevel, systemPrompt, outputSchema, tools, transport, maxRetries, maxRetryDelayMs, steeringMode, onEvent, options, toolLimits, sdk, reference, }: {
50
49
  session: any;
51
50
  piModels: any;
52
51
  model: any;
@@ -63,7 +62,7 @@ export function buildTurnHarness(runState: any, { cwd, session, piModels, model,
63
62
  toolLimits: any;
64
63
  sdk: any;
65
64
  reference: any;
66
- }): AgentHarness<import("@earendil-works/pi-agent-core").Skill, import("@earendil-works/pi-agent-core").PromptTemplate, import("@earendil-works/pi-agent-core").AgentTool<import("@earendil-works/pi-ai").TSchema, any>>;
65
+ }): AgentHarness<undefined, import("@earendil-works/pi-agent-core").Skill, import("@earendil-works/pi-agent-core").PromptTemplate, import("@earendil-works/pi-agent-core").AgentHarnessTool<undefined>>;
67
66
  /**
68
67
  * Start the live-input steering consumer. Consumes follow-up messages and steers
69
68
  * the harness mid-run; the consumer is tied to run completion (an internal
@@ -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
  }
@@ -118,7 +118,8 @@
118
118
  * @property {boolean} [fastMode]
119
119
  * @property {string} [cwd]
120
120
  * @property {Object<string, Object>} [mcpServers]
121
- * @property {ReadonlyArray<Object>} [skills] Runtime skill metadata for progressive disclosure.
121
+ * @property {ReadonlyArray<{name: string, description?: string}>} [skills] Skills disclosed to this run, as `{name, description}`. Non-empty makes `supports_skills` a routing requirement (see router.js), so a chain entry that lacks it is skipped.
122
+ * @property {string} [skillsRoot] Directory holding `<name>/SKILL.md`. Required alongside `skills` for `ReadSkill` to be built.
122
123
  * @property {ReadonlyArray<string>} [allowedTools]
123
124
  * @property {ReadonlyArray<string>} [disallowedTools]
124
125
  * @property {string} [permissionMode]
@@ -126,6 +127,7 @@
126
127
  * @property {Object} [outputSchema]
127
128
  * @property {string} [runArtifactDir]
128
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.
129
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).
130
132
  * @property {import('../agent/sandbox-seam.js').RuntimeSandboxEngine} [sandboxEngine] Per-run concrete sandbox engine handed to the active sandbox implementation.
131
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.
@@ -242,6 +244,7 @@
242
244
  * @property {boolean} [supports_builtin_tools]
243
245
  * @property {boolean} [supports_live_input]
244
246
  * @property {boolean} [supports_native_subagents]
247
+ * @property {boolean} [supports_request_tool_environment]
245
248
  * @property {boolean} [supports_fast_mode]
246
249
  * @property {"projected"|"allow_all_only"} [tool_policy] Whether the bridge can
247
250
  * project named allow/deny policies or accepts only a semantically unrestricted
@@ -578,9 +581,16 @@ export type RuntimeRunOptions = {
578
581
  [x: string]: any;
579
582
  };
580
583
  /**
581
- * Runtime skill metadata for progressive disclosure.
584
+ * Skills disclosed to this run, as `{name, description}`. Non-empty makes `supports_skills` a routing requirement (see router.js), so a chain entry that lacks it is skipped.
582
585
  */
583
- skills?: ReadonlyArray<any>;
586
+ skills?: ReadonlyArray<{
587
+ name: string;
588
+ description?: string;
589
+ }>;
590
+ /**
591
+ * Directory holding `<name>/SKILL.md`. Required alongside `skills` for `ReadSkill` to be built.
592
+ */
593
+ skillsRoot?: string;
584
594
  allowedTools?: ReadonlyArray<string>;
585
595
  disallowedTools?: ReadonlyArray<string>;
586
596
  permissionMode?: string;
@@ -588,6 +598,14 @@ export type RuntimeRunOptions = {
588
598
  outputSchema?: any;
589
599
  runArtifactDir?: string;
590
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
+ };
591
609
  /**
592
610
  * Per-run sandbox policy; merged monotonically with the host policy (see resolveSandboxPolicy, agent/tools/shared/tool-context.js).
593
611
  */
@@ -829,6 +847,7 @@ export type RuntimeCapabilities = {
829
847
  supports_builtin_tools?: boolean;
830
848
  supports_live_input?: boolean;
831
849
  supports_native_subagents?: boolean;
850
+ supports_request_tool_environment?: boolean;
832
851
  supports_fast_mode?: boolean;
833
852
  /**
834
853
  * Whether the bridge can