@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.
- package/MIGRATION.md +50 -14
- package/package.json +3 -3
- package/src/agent/tools/agent-tool.js +118 -8
- 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/pi-interop.js +7 -5
- package/src/ai/pi-oauth-compat.js +193 -0
- package/src/ai/providers/pi-native/result-builder.js +32 -0
- package/src/ai/providers/pi-native/turn-runner.js +14 -3
- package/src/ai/providers/pi-native.js +33 -7
- package/src/ai/runtime/capabilities.js +2 -0
- package/src/ai/runtime/router.js +14 -3
- package/src/ai/types.js +4 -1
- package/src/pi-auth.js +2 -2
- package/src/runtime.js +15 -1
- package/types/agent/tools/agent-tool.d.ts +43 -1
- package/types/agent/tools/shared/tool-context.d.ts +15 -0
- package/types/ai/pi-oauth-compat.d.ts +57 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +41 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +2 -3
- package/types/ai/runtime/capabilities.d.ts +3 -0
- package/types/ai/types.d.ts +22 -3
- package/types/ai/backend.d.ts +0 -57
- package/types/ai/registry.d.ts +0 -1
package/MIGRATION.md
CHANGED
|
@@ -42,8 +42,8 @@ the configuration schema.
|
|
|
42
42
|
switch to `listPiBuiltinModels`, `getPiBuiltinModel`,
|
|
43
43
|
`reasoningLevelsForPiModel`, `resolvePiOAuthApiKey`, and `loginPiOAuth` from
|
|
44
44
|
`@mono-agent/agent-runtime/ai`. The runtime keeps Pi AI and Pi Agent Core
|
|
45
|
-
exact-pinned at `0.
|
|
46
|
-
snapshots rather than exposing
|
|
45
|
+
exact-pinned at `0.83.0`; the façade returns cloned model and credential
|
|
46
|
+
snapshots rather than exposing upstream provider objects.
|
|
47
47
|
- **Claude test seam:** downstream tests should pass
|
|
48
48
|
`RuntimeRunOptions.claudeAgentQuery` instead of mocking
|
|
49
49
|
`@anthropic-ai/claude-agent-sdk` by package name. Normal runs omit this option
|
|
@@ -63,12 +63,35 @@ 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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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:
|
|
78
|
+
|
|
79
|
+
- `skills` and `skillsRoot` on the run options. `skills` is the disclosed
|
|
80
|
+
`{name, description}` set for a run; a non-empty value makes `supports_skills`
|
|
81
|
+
a routing requirement, so a chain entry lacking it is skipped. `skillsRoot`
|
|
82
|
+
names the directory holding `<name>/SKILL.md` and is required alongside
|
|
83
|
+
`skills` for `ReadSkill` to exist. A subagent run now inherits both from its
|
|
84
|
+
parent unless a host-supplied `run` withholds them, so a child no longer has
|
|
85
|
+
to rediscover by trial and error what its parent could look up.
|
|
86
|
+
- Provider failover detail reaches whoever is watching the run rather than being
|
|
87
|
+
flattened at the boundary.
|
|
88
|
+
- Pi SDK 0.83.0.
|
|
89
|
+
|
|
90
|
+
## 0.15.x
|
|
91
|
+
|
|
92
|
+
- The explicit exports map, the five-bridge lazy registry, typed runtime
|
|
93
|
+
policies, runtime-owned provider dependencies, and the public-surface cleanup
|
|
94
|
+
described in this guide.
|
|
72
95
|
|
|
73
96
|
## 0.12.x
|
|
74
97
|
|
|
@@ -313,9 +336,22 @@ falls back to its own env vars, exactly as returning `undefined` from the old ho
|
|
|
313
336
|
did). **No host action needed** — `resolvePiApiKey` behaves as before.
|
|
314
337
|
|
|
315
338
|
Dependency bump: **`@earendil-works/pi-ai` and `@earendil-works/pi-agent-core` are
|
|
316
|
-
now `0.
|
|
317
|
-
`^0.79.1
|
|
318
|
-
|
|
339
|
+
now `0.83.0`** (the initial Pi 0.80 migration landed at `0.80.5`, from
|
|
340
|
+
`^0.79.1`, and ran at `0.80.6` until the 0.83 upgrade). Compaction is driven
|
|
341
|
+
natively (section 3), and model-native `max` reasoning plus Pi's request-wide
|
|
342
|
+
pricing tiers are preserved.
|
|
343
|
+
|
|
344
|
+
The 0.83 upgrade carries two upstream removals, both absorbed inside the runtime
|
|
345
|
+
so hosts need no action:
|
|
346
|
+
|
|
347
|
+
- `@earendil-works/pi-ai/oauth` became a type-only entry point. The generic
|
|
348
|
+
registry (`getOAuthApiKey`, `getOAuthProvider`, `getOAuthProviders`) is gone,
|
|
349
|
+
and the per-provider flows are not importable. `src/ai/pi-oauth-compat.js`
|
|
350
|
+
rebuilds the same contracts over `provider.auth.oauth`, so `resolvePiApiKey`,
|
|
351
|
+
`resolvePiOAuthApiKey`, and `loginPiOAuth` keep their existing signatures and
|
|
352
|
+
behaviour, including the refresh-on-expiry trigger.
|
|
353
|
+
- `AgentHarnessOptions.env` was removed in favour of a per-turn `toolContext`.
|
|
354
|
+
The runtime passes neither: it uses none of Pi's built-in file/shell tools.
|
|
319
355
|
|
|
320
356
|
### 11. Exports map: wildcards removed (explicit deep-path map)
|
|
321
357
|
|
|
@@ -366,7 +402,7 @@ a compatibility subpath.
|
|
|
366
402
|
|
|
367
403
|
## Version
|
|
368
404
|
|
|
369
|
-
This guide describes the published `0.
|
|
405
|
+
This guide describes the published `0.17.x` package contract. Keep
|
|
370
406
|
`@mono-agent/agent-runtime`, `@mono-agent/runtime-adapter`, and other
|
|
371
407
|
`@mono-agent/*` packages on the same lockstep version when upgrading. The paired
|
|
372
408
|
runtime adapter no longer exposes `piReasoningSummary` in its run-options type.
|
|
@@ -387,14 +423,14 @@ Worklab's runtime fork:
|
|
|
387
423
|
`@earendil-works/pi-ai`, its separate Pi version constraint, and local copies
|
|
388
424
|
of provider bridge code. Move tests off Pi's faux-provider helpers too; until
|
|
389
425
|
that is complete, isolate the fixture or pin its development-only Pi
|
|
390
|
-
dependency to exact `0.
|
|
426
|
+
dependency to exact `0.83.0` rather than a floating range. Do not restore the
|
|
391
427
|
removed `pi-sdk.js` subpath.
|
|
392
428
|
3. **Use the public Pi surfaces.** Run models through
|
|
393
429
|
`generatePiNativeResponse` or the runtime registry. Use
|
|
394
430
|
`listPiBuiltinModels`, `getPiBuiltinModel`,
|
|
395
431
|
`reasoningLevelsForPiModel`, `resolvePiOAuthApiKey`, and `loginPiOAuth` for
|
|
396
|
-
catalog and OAuth integration. Those façades keep Pi
|
|
397
|
-
exact `0.
|
|
432
|
+
catalog and OAuth integration. Those façades keep Pi provider objects and the
|
|
433
|
+
exact `0.83.0` compatibility pin inside the runtime. OAuth login adapters
|
|
398
434
|
must supply `onAuth`, `onDeviceCode`, `onPrompt`, and `onSelect`; the façade
|
|
399
435
|
rejects an incomplete callback contract before starting provider login.
|
|
400
436
|
4. **Inject Claude tests.** Replace package-level mocks of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mono-agent/agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Agent runtime supporting Claude SDK/CLI, Codex, OpenCode, and Pi SDK bridges out of the box",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
@@ -133,8 +133,8 @@
|
|
|
133
133
|
"dependencies": {
|
|
134
134
|
"@anthropic-ai/claude-agent-sdk": "0.3.206",
|
|
135
135
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
136
|
-
"@earendil-works/pi-agent-core": "0.
|
|
137
|
-
"@earendil-works/pi-ai": "0.
|
|
136
|
+
"@earendil-works/pi-agent-core": "0.83.0",
|
|
137
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
138
138
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
139
139
|
"@mozilla/readability": "0.6.0",
|
|
140
140
|
"@opencode-ai/sdk": "^1.15.13",
|
|
@@ -93,16 +93,38 @@ function toolDescription(subagents, definitions, ceiling) {
|
|
|
93
93
|
const named = definitions.length === 0
|
|
94
94
|
? ""
|
|
95
95
|
: `\n\nAvailable subagents:\n${definitions.map((d) => `- ${d.name}: ${d.description}`).join("\n")}\n- ${GENERAL_PURPOSE_SUBAGENT}: read-only researcher inheriting the main model. Used when \`name\` is omitted.`;
|
|
96
|
+
// With authoring on, `name` is a free string rather than an enum, so it is
|
|
97
|
+
// the model's only signal for which of the two shapes it is writing. Left
|
|
98
|
+
// implicit, a caller that wants a configured profile AND a descriptive label
|
|
99
|
+
// splits those across two fields — label into `name`, profile into an
|
|
100
|
+
// invented one — and the closed schema rejects the whole call before any of
|
|
101
|
+
// the handler's precise errors can run.
|
|
102
|
+
const shapes = ceiling === null
|
|
103
|
+
? ""
|
|
104
|
+
: `\n\nExactly two ways to call this, and \`name\` carries the agent's identity in both:\n- Use a configured one: set \`name\` to a name from the list above. Nothing else.\n- Build one for this task: set \`name\` to a NEW kebab-case name AND \`systemPrompt\` to its full instructions (optionally \`tools\`, \`effort\`). Do that when no configured one fits — a dedicated prompt beats stuffing constraints into \`prompt\`.\n\n\`description\` is the short label shown in the activity log, never the agent's name. There is no separate field for choosing a configured agent.`;
|
|
96
105
|
// The ceiling is listed because the model has no other way to discover it: a
|
|
97
106
|
// tool it cannot see is indistinguishable from one it forgot to ask for.
|
|
98
107
|
const inline = ceiling === null
|
|
99
108
|
? ""
|
|
100
|
-
: `\n\
|
|
101
|
-
return `${DESCRIPTION_BASE}${parallel}${named}${inline}`;
|
|
109
|
+
: `\n\nTools you may grant a subagent you build: ${ceiling.join(", ")}. Anything else is dropped. Omit \`tools\` for a read-only helper.`;
|
|
110
|
+
return `${DESCRIPTION_BASE}${parallel}${named}${shapes}${inline}`;
|
|
111
|
+
}
|
|
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 };
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
/**
|
|
105
|
-
* 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.
|
|
106
128
|
* @param {*} subagents The run-scoped options object, stable across attempts.
|
|
107
129
|
* @param {string|undefined} parentRunId
|
|
108
130
|
*/
|
|
@@ -120,11 +142,58 @@ function budgetForRun(subagents, parentRunId) {
|
|
|
120
142
|
if (oldest.done) break;
|
|
121
143
|
store.delete(oldest.value);
|
|
122
144
|
}
|
|
123
|
-
const fresh = { total: 0, bytes: 0, warnedQueued: false };
|
|
145
|
+
const fresh = { total: 0, bytes: 0, warnedQueued: false, usage: emptyUsage() };
|
|
124
146
|
store.set(key, fresh);
|
|
125
147
|
return fresh;
|
|
126
148
|
}
|
|
127
149
|
|
|
150
|
+
/**
|
|
151
|
+
* How many subagents this logical run actually spawned.
|
|
152
|
+
*
|
|
153
|
+
* A read-only accessor so a provider can report `subagent_invoked` truthfully
|
|
154
|
+
* without reaching into `__budgets`, which is a deliberately private,
|
|
155
|
+
* non-enumerable implementation detail. Returns 0 when nothing was ever
|
|
156
|
+
* registered — a run with no `Agent` tool never creates a budget entry, and that
|
|
157
|
+
* is indistinguishable from one that had the tool and never used it, which is
|
|
158
|
+
* exactly what "no subagent was invoked" means for this signal.
|
|
159
|
+
*
|
|
160
|
+
* @param {*} subagents The run-scoped options object, or undefined.
|
|
161
|
+
* @param {string|undefined} parentRunId
|
|
162
|
+
* @returns {number}
|
|
163
|
+
*/
|
|
164
|
+
export function subagentInvocationCount(subagents, parentRunId) {
|
|
165
|
+
const store = subagents?.__budgets;
|
|
166
|
+
if (!(store instanceof Map)) return 0;
|
|
167
|
+
const entry = store.get(parentRunId ?? "unkeyed");
|
|
168
|
+
return Number.isInteger(entry?.total) ? entry.total : 0;
|
|
169
|
+
}
|
|
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
|
+
|
|
128
197
|
/** @param {*} value @param {number} fallback @returns {number} */
|
|
129
198
|
function positiveInt(value, fallback) {
|
|
130
199
|
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
@@ -134,7 +203,7 @@ function positiveInt(value, fallback) {
|
|
|
134
203
|
* Build the `Agent` tool, or null when subagents are unavailable for this run.
|
|
135
204
|
*
|
|
136
205
|
* @param {RuntimeSubagentsOptions|null|undefined} subagents
|
|
137
|
-
* @param {{model?: *, executionMode?: string, cwd?: string, parentRunId?: string, sandboxPolicy?: *, sandboxEngine?: *, 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]
|
|
138
207
|
* @returns {*|null}
|
|
139
208
|
*/
|
|
140
209
|
export function createAgentTool(subagents, context = {}) {
|
|
@@ -306,6 +375,15 @@ export function createAgentTool(subagents, context = {}) {
|
|
|
306
375
|
callIndex,
|
|
307
376
|
...(params.description === undefined ? {} : { label: params.description }),
|
|
308
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
|
+
},
|
|
309
387
|
});
|
|
310
388
|
collector.started();
|
|
311
389
|
const startedAt = Date.now();
|
|
@@ -326,6 +404,12 @@ export function createAgentTool(subagents, context = {}) {
|
|
|
326
404
|
// Inherited, never widened: a profile cannot loosen confinement.
|
|
327
405
|
...(context.sandboxPolicy === undefined ? {} : { sandboxPolicy: context.sandboxPolicy }),
|
|
328
406
|
...(context.sandboxEngine === undefined ? {} : { sandboxEngine: context.sandboxEngine }),
|
|
407
|
+
// The parent's disclosed skills. Offered, not imposed — the host's
|
|
408
|
+
// `run` decides whether this child may have them, since only it knows
|
|
409
|
+
// the child's resolved route and deny lists.
|
|
410
|
+
...(context.skills === undefined ? {} : { skills: context.skills }),
|
|
411
|
+
...(context.skillsRoot === undefined ? {} : { skillsRoot: context.skillsRoot }),
|
|
412
|
+
...(context.toolEnvironment === undefined ? {} : { toolEnvironment: context.toolEnvironment }),
|
|
329
413
|
abortSignal: controller.signal,
|
|
330
414
|
maxTurns,
|
|
331
415
|
callId: toolCallId,
|
|
@@ -518,13 +602,15 @@ const WIRE_CONTENT_MAX_CHARS = 2_000;
|
|
|
518
602
|
* answer body, so forwarding them would splice a subagent's prose into the
|
|
519
603
|
* main agent's reply. Its text reaches the parent through the tool result.
|
|
520
604
|
*
|
|
521
|
-
* @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
|
|
522
606
|
*/
|
|
523
|
-
function createActivityCollector({ callId, profileName, callIndex, label, emit }) {
|
|
607
|
+
function createActivityCollector({ callId, profileName, callIndex, label, emit, recordUsage }) {
|
|
524
608
|
/** @type {Map<string, {name: string, args: unknown, startedAt: number, ms?: number}>} */
|
|
525
609
|
const open = new Map();
|
|
526
610
|
/** @type {Array<{name: string, args: unknown, ms?: number, isError: boolean}>} */
|
|
527
611
|
const done = [];
|
|
612
|
+
/** What the child reported spending, so the parent run can own it. */
|
|
613
|
+
const usage = emptyUsage();
|
|
528
614
|
const subagent = { id: callId, name: profileName, callIndex, ...(label === undefined ? {} : { label }) };
|
|
529
615
|
|
|
530
616
|
/** @param {*} event */
|
|
@@ -550,10 +636,17 @@ function createActivityCollector({ callId, profileName, callIndex, label, emit }
|
|
|
550
636
|
},
|
|
551
637
|
/** @param {{status: string, durationMs: number}} outcome */
|
|
552
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);
|
|
553
642
|
publish({
|
|
554
643
|
phase: "agent_completed",
|
|
555
644
|
id: `agent:${callId}`,
|
|
556
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 } } : {}),
|
|
557
650
|
isError: status !== "ok",
|
|
558
651
|
executionMs: durationMs,
|
|
559
652
|
content: `${status} · ${done.length} tool call${done.length === 1 ? "" : "s"}`,
|
|
@@ -618,8 +711,25 @@ function createActivityCollector({ callId, profileName, callIndex, label, emit }
|
|
|
618
711
|
}
|
|
619
712
|
return;
|
|
620
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
|
+
}
|
|
621
731
|
// Child warnings are worth surfacing; everything else (context usage,
|
|
622
|
-
// partial tool output
|
|
732
|
+
// partial tool output) stays inside the subagent for now.
|
|
623
733
|
if (type === "runtime_warning" && emit !== undefined) {
|
|
624
734
|
try {
|
|
625
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
|
+
}
|
package/src/ai/pi-interop.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// directly so the runtime's known-good Pi version remains authoritative.
|
|
4
4
|
|
|
5
5
|
import { getBuiltinModel, getBuiltinModels } from "@earendil-works/pi-ai/providers/all";
|
|
6
|
-
import {
|
|
6
|
+
import { getPiOAuthAuth, resolveOAuthApiKey, toAuthInteraction } from "./pi-oauth-compat.js";
|
|
7
7
|
import { reasoningLevelsForPiModel as resolveReasoningLevels } from "./providers/pi-models.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -122,7 +122,7 @@ export function reasoningLevelsForPiModel(model) {
|
|
|
122
122
|
* @returns {Promise<{apiKey: string, newCredentials: PiOAuthCredentialsSnapshot}|null>}
|
|
123
123
|
*/
|
|
124
124
|
export async function resolvePiOAuthApiKey(providerId, credentials) {
|
|
125
|
-
const result = await
|
|
125
|
+
const result = await resolveOAuthApiKey(
|
|
126
126
|
providerId,
|
|
127
127
|
/** @type {any} */ (cloneInteropValue(credentials)),
|
|
128
128
|
);
|
|
@@ -142,8 +142,8 @@ export async function resolvePiOAuthApiKey(providerId, credentials) {
|
|
|
142
142
|
* @returns {Promise<PiOAuthCredentialsSnapshot>}
|
|
143
143
|
*/
|
|
144
144
|
export async function loginPiOAuth(providerId, callbacks) {
|
|
145
|
-
const
|
|
146
|
-
if (!
|
|
145
|
+
const oauth = getPiOAuthAuth(providerId);
|
|
146
|
+
if (!oauth || typeof oauth.login !== "function") {
|
|
147
147
|
throw new Error(`Pi OAuth provider is unavailable: ${providerId}`);
|
|
148
148
|
}
|
|
149
149
|
for (const callbackName of ["onAuth", "onDeviceCode", "onPrompt", "onSelect"]) {
|
|
@@ -151,6 +151,8 @@ export async function loginPiOAuth(providerId, callbacks) {
|
|
|
151
151
|
throw new TypeError(`loginPiOAuth requires callbacks.${callbackName}()`);
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
const credentials = await
|
|
154
|
+
const credentials = await oauth.login(
|
|
155
|
+
toAuthInteraction(/** @type {any} */ ({ ...callbacks })),
|
|
156
|
+
);
|
|
155
157
|
return cloneInteropValue(credentials);
|
|
156
158
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
// Adapter over pi-ai's provider-owned OAuth surface.
|
|
4
|
+
//
|
|
5
|
+
// pi-ai 0.83.0 removed the generic OAuth registry — `getOAuthApiKey`,
|
|
6
|
+
// `getOAuthProvider` and `getOAuthProviders` are gone, and the
|
|
7
|
+
// `@earendil-works/pi-ai/oauth` entry point is now type-only (`export {}` at
|
|
8
|
+
// runtime). The per-provider implementations (`anthropicOAuth`, …) live under
|
|
9
|
+
// `dist/auth/oauth/*`, which has no entry in the package's `exports` map, so
|
|
10
|
+
// they cannot be imported. The supported surface is `provider.auth.oauth`,
|
|
11
|
+
// reached through the provider factories.
|
|
12
|
+
//
|
|
13
|
+
// mono-agent resolves providers dynamically from `pi:<provider>:<model>`, so it
|
|
14
|
+
// needs a lookup by id — this module rebuilds that over `builtinProviders()` and
|
|
15
|
+
// preserves the old call contracts exactly, keeping the migration confined here.
|
|
16
|
+
//
|
|
17
|
+
// Deliberately NOT using `createModels({credentials})`: that hands credential
|
|
18
|
+
// locking and persistence to pi, while mono-agent already owns `auth.json`
|
|
19
|
+
// through `pi-auth.js` (serialized writes, atomic 0600 rename). `refresh()` and
|
|
20
|
+
// `toAuth()` are callable directly, so the pure caller-persists contract stays.
|
|
21
|
+
|
|
22
|
+
import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {import("@earendil-works/pi-ai").OAuthAuth} OAuthAuth
|
|
26
|
+
* @typedef {import("@earendil-works/pi-ai").OAuthCredential} OAuthCredential
|
|
27
|
+
* @typedef {import("@earendil-works/pi-ai").AuthInteraction} AuthInteraction
|
|
28
|
+
* @typedef {import("@earendil-works/pi-ai").AuthPrompt} AuthPrompt
|
|
29
|
+
* @typedef {import("@earendil-works/pi-ai").AuthEvent} AuthEvent
|
|
30
|
+
* @typedef {import("@earendil-works/pi-ai/oauth").OAuthLoginCallbacks} OAuthLoginCallbacks
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** @type {Map<string, import("@earendil-works/pi-ai").Provider>|undefined} */
|
|
34
|
+
let providerIndexCache;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `builtinProviders()` freshly constructs every provider (~37 objects) on each
|
|
38
|
+
* call, and `pi-auth.js` sits on the per-request credential path. The catalog is
|
|
39
|
+
* static for the process lifetime, so index it once.
|
|
40
|
+
*
|
|
41
|
+
* @returns {Map<string, import("@earendil-works/pi-ai").Provider>}
|
|
42
|
+
*/
|
|
43
|
+
function providerIndex() {
|
|
44
|
+
if (providerIndexCache === undefined) {
|
|
45
|
+
providerIndexCache = new Map(builtinProviders().map((provider) => [provider.id, provider]));
|
|
46
|
+
}
|
|
47
|
+
return providerIndexCache;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @internal Exported only so tests can force a rebuild of the memoized index. */
|
|
51
|
+
export function resetPiProviderIndexForTests() {
|
|
52
|
+
providerIndexCache = undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The OAuth implementation for a Pi provider id, or undefined when the provider
|
|
57
|
+
* is unknown or supports only API-key auth (e.g. `opencode-go`).
|
|
58
|
+
*
|
|
59
|
+
* @param {string} providerId
|
|
60
|
+
* @returns {OAuthAuth|undefined}
|
|
61
|
+
*/
|
|
62
|
+
export function getPiOAuthAuth(providerId) {
|
|
63
|
+
if (typeof providerId !== "string" || providerId.length === 0) return undefined;
|
|
64
|
+
return providerIndex().get(providerId)?.auth?.oauth;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Every Pi provider id that supports OAuth. Replaces
|
|
69
|
+
* `getOAuthProviders().map((provider) => provider.id)`.
|
|
70
|
+
*
|
|
71
|
+
* @returns {string[]}
|
|
72
|
+
*/
|
|
73
|
+
export function getPiOAuthProviderIds() {
|
|
74
|
+
const ids = [];
|
|
75
|
+
for (const [id, provider] of providerIndex()) {
|
|
76
|
+
if (provider.auth?.oauth !== undefined) ids.push(id);
|
|
77
|
+
}
|
|
78
|
+
return ids;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve an API key from stored OAuth credentials, refreshing first when the
|
|
83
|
+
* token has expired.
|
|
84
|
+
*
|
|
85
|
+
* Reproduces pi-ai 0.80.6's `getOAuthApiKey(providerId, credentials)` contract
|
|
86
|
+
* so its call sites keep their shape: takes the whole provider-keyed credential
|
|
87
|
+
* map, returns `null` when this provider has no stored credential, and is
|
|
88
|
+
* *pure* — the refreshed credential comes back as `newCredentials` for the
|
|
89
|
+
* caller to persist rather than being written here.
|
|
90
|
+
*
|
|
91
|
+
* The refresh trigger is deliberately the old exact-expiry check. pi's own
|
|
92
|
+
* `Models.getAuth()` refreshes five minutes ahead of expiry; matching that would
|
|
93
|
+
* change live token rotation timing, which this migration does not intend.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} providerId
|
|
96
|
+
* @param {Record<string, *>|undefined} credentials Provider-keyed credential map.
|
|
97
|
+
* @returns {Promise<{newCredentials: OAuthCredential, apiKey: string|undefined}|null>}
|
|
98
|
+
*/
|
|
99
|
+
export async function resolveOAuthApiKey(providerId, credentials) {
|
|
100
|
+
const oauth = getPiOAuthAuth(providerId);
|
|
101
|
+
if (oauth === undefined) {
|
|
102
|
+
throw new Error(`Unknown OAuth provider: ${providerId}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const stored = credentials?.[providerId];
|
|
106
|
+
if (stored === undefined || stored === null) return null;
|
|
107
|
+
|
|
108
|
+
// 0.83.0's OAuthCredential carries a `type: "oauth"` discriminant that the
|
|
109
|
+
// 0.80.6 shape did not. Stored entries written by mono-agent already have it;
|
|
110
|
+
// tag defensively so hand-edited or externally written files still work.
|
|
111
|
+
let credential = /** @type {OAuthCredential} */ ({ ...stored, type: "oauth" });
|
|
112
|
+
|
|
113
|
+
if (Date.now() >= credential.expires) {
|
|
114
|
+
try {
|
|
115
|
+
credential = await oauth.refresh(credential);
|
|
116
|
+
} catch {
|
|
117
|
+
throw new Error(`Failed to refresh OAuth token for ${providerId}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// `toAuth()` also derives a per-credential baseUrl (GitHub Copilot's
|
|
122
|
+
// per-account proxy). 0.80.6 discarded it and callers here have no field for
|
|
123
|
+
// it, so it stays dropped — behaviour preserved, worth revisiting separately.
|
|
124
|
+
const auth = await oauth.toAuth(credential);
|
|
125
|
+
return { newCredentials: credential, apiKey: auth?.apiKey };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Bridge the legacy six-callback OAuth surface onto 0.83.0's single
|
|
130
|
+
* `prompt`/`notify` pair.
|
|
131
|
+
*
|
|
132
|
+
* `manual_code` must stay wired to `onManualCodeInput`: Anthropic races its
|
|
133
|
+
* localhost callback against a pasted redirect URL, and that path is the reason
|
|
134
|
+
* `agent-app`'s `runPiOAuthLogin` exists at all.
|
|
135
|
+
*
|
|
136
|
+
* @param {OAuthLoginCallbacks} callbacks
|
|
137
|
+
* @returns {AuthInteraction}
|
|
138
|
+
*/
|
|
139
|
+
export function toAuthInteraction(callbacks) {
|
|
140
|
+
if (callbacks === null || typeof callbacks !== "object") {
|
|
141
|
+
throw new TypeError("toAuthInteraction requires an OAuthLoginCallbacks object");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...(callbacks.signal === undefined ? {} : { signal: callbacks.signal }),
|
|
146
|
+
|
|
147
|
+
/** @param {AuthPrompt} prompt */
|
|
148
|
+
async prompt(prompt) {
|
|
149
|
+
if (prompt.type === "select") {
|
|
150
|
+
const selected = await callbacks.onSelect({
|
|
151
|
+
message: prompt.message,
|
|
152
|
+
options: prompt.options.map((option) => ({ id: option.id, label: option.label })),
|
|
153
|
+
});
|
|
154
|
+
// The legacy callback resolves undefined when the user cancels; the new
|
|
155
|
+
// contract requires a string and rejects on cancel.
|
|
156
|
+
if (typeof selected !== "string") {
|
|
157
|
+
throw new Error("OAuth provider selection was cancelled.");
|
|
158
|
+
}
|
|
159
|
+
return selected;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (prompt.type === "manual_code" && typeof callbacks.onManualCodeInput === "function") {
|
|
163
|
+
return await callbacks.onManualCodeInput();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return await callbacks.onPrompt({
|
|
167
|
+
message: prompt.message,
|
|
168
|
+
...(prompt.placeholder === undefined ? {} : { placeholder: prompt.placeholder }),
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/** @param {AuthEvent} event */
|
|
173
|
+
notify(event) {
|
|
174
|
+
if (event.type === "auth_url") {
|
|
175
|
+
callbacks.onAuth({
|
|
176
|
+
url: event.url,
|
|
177
|
+
...(event.instructions === undefined ? {} : { instructions: event.instructions }),
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (event.type === "device_code") {
|
|
182
|
+
callbacks.onDeviceCode({
|
|
183
|
+
userCode: event.userCode,
|
|
184
|
+
verificationUri: event.verificationUri,
|
|
185
|
+
...(event.intervalSeconds === undefined ? {} : { intervalSeconds: event.intervalSeconds }),
|
|
186
|
+
...(event.expiresInSeconds === undefined ? {} : { expiresInSeconds: event.expiresInSeconds }),
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
callbacks.onProgress?.(event.message);
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|