@mono-agent/agent-runtime 0.15.3 → 0.16.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 +41 -13
- package/README.md +43 -6
- package/package.json +7 -3
- package/src/agent/tools/agent-tool.js +894 -0
- package/src/agent/tools/bash.js +241 -123
- package/src/agent/tools/exec.js +238 -0
- package/src/agent/tools/index.js +10 -3
- package/src/agent/tools/node-repl.js +231 -95
- package/src/agent/tools/pi-bridge.js +115 -24
- package/src/agent/tools/shared/process-runner.js +162 -0
- package/src/agent/tools/shared/semaphore.js +73 -0
- package/src/agent/tools/web-browser-render.js +221 -0
- package/src/agent/tools/web-controller.js +160 -0
- package/src/agent/tools/web-fetch.js +653 -68
- package/src/agent/tools/web-search.js +568 -16
- package/src/ai/pi-interop.js +7 -5
- package/src/ai/pi-oauth-compat.js +193 -0
- package/src/ai/providers/pi-native/stream-subscriber.js +37 -0
- package/src/ai/providers/pi-native/turn-runner.js +73 -8
- package/src/ai/providers/pi-native.js +67 -7
- package/src/ai/runtime/router.js +310 -166
- package/src/ai/types.js +54 -2
- package/src/pi-auth.js +2 -2
- package/src/runtime.js +58 -1
- package/types/agent/tools/agent-tool.d.ts +80 -0
- package/types/agent/tools/bash.d.ts +55 -7
- package/types/agent/tools/exec.d.ts +53 -0
- package/types/agent/tools/index.d.ts +5 -3
- package/types/agent/tools/node-repl.d.ts +28 -3
- package/types/agent/tools/pi-bridge.d.ts +6 -2
- package/types/agent/tools/shared/process-runner.d.ts +33 -0
- package/types/agent/tools/shared/semaphore.d.ts +29 -0
- package/types/agent/tools/web-browser-render.d.ts +16 -0
- package/types/agent/tools/web-controller.d.ts +20 -0
- package/types/agent/tools/web-fetch.d.ts +74 -5
- package/types/agent/tools/web-search.d.ts +81 -5
- package/types/ai/pi-oauth-compat.d.ts +57 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +33 -2
- package/types/ai/providers/pi-native.d.ts +12 -0
- package/types/ai/runtime/router.d.ts +23 -3
- package/types/ai/types.d.ts +174 -4
- package/types/ai/backend.d.ts +0 -57
- package/types/ai/registry.d.ts +0 -1
|
@@ -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
|
+
}
|
|
@@ -22,6 +22,41 @@ function toolResultFileChange(result) {
|
|
|
22
22
|
: null;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function toolResultOutcome(result) {
|
|
26
|
+
const source = result?.details?.outcome;
|
|
27
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) return null;
|
|
28
|
+
const bounded = {};
|
|
29
|
+
const strings = {
|
|
30
|
+
status: "status",
|
|
31
|
+
code: "code",
|
|
32
|
+
backend: "backend",
|
|
33
|
+
signal: "signal",
|
|
34
|
+
};
|
|
35
|
+
const numbers = {
|
|
36
|
+
attempts: "attempts",
|
|
37
|
+
bytes: "bytes",
|
|
38
|
+
exitCode: "exit_code",
|
|
39
|
+
statusCode: "status_code",
|
|
40
|
+
};
|
|
41
|
+
const booleans = {
|
|
42
|
+
retryable: "retryable",
|
|
43
|
+
cacheHit: "cache_hit",
|
|
44
|
+
truncated: "truncated",
|
|
45
|
+
timedOut: "timed_out",
|
|
46
|
+
rendered: "rendered",
|
|
47
|
+
};
|
|
48
|
+
for (const [input, output] of Object.entries(strings)) {
|
|
49
|
+
if (typeof source[input] === "string") bounded[output] = source[input].slice(0, 120);
|
|
50
|
+
}
|
|
51
|
+
for (const [input, output] of Object.entries(numbers)) {
|
|
52
|
+
if (Number.isFinite(Number(source[input]))) bounded[output] = Number(source[input]);
|
|
53
|
+
}
|
|
54
|
+
for (const [input, output] of Object.entries(booleans)) {
|
|
55
|
+
if (typeof source[input] === "boolean") bounded[output] = source[input];
|
|
56
|
+
}
|
|
57
|
+
return bounded;
|
|
58
|
+
}
|
|
59
|
+
|
|
25
60
|
/**
|
|
26
61
|
* The slice of run state the stream subscriber reads and mutates. A structural
|
|
27
62
|
* subset of the orchestrator's runState.
|
|
@@ -107,6 +142,7 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
|
|
|
107
142
|
} else if (event.type === "tool_execution_end") {
|
|
108
143
|
const resultContent = toolResultContent(event.result);
|
|
109
144
|
const fileChange = toolResultFileChange(event.result);
|
|
145
|
+
const outcome = toolResultOutcome(event.result);
|
|
110
146
|
if (!event.isError) runState.toolResultsSeen += 1;
|
|
111
147
|
const startedAt = runState.toolStartTimes.get(event.toolCallId);
|
|
112
148
|
if (startedAt !== undefined) {
|
|
@@ -117,6 +153,7 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
|
|
|
117
153
|
name: event.toolName,
|
|
118
154
|
execution_ms: Date.now() - startedAt,
|
|
119
155
|
is_error: !!event.isError,
|
|
156
|
+
...(outcome || {}),
|
|
120
157
|
});
|
|
121
158
|
}
|
|
122
159
|
onEvent({
|
|
@@ -8,13 +8,13 @@
|
|
|
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,
|
|
15
14
|
initPiMcpTools,
|
|
16
15
|
} from "../../../agent/tools/pi-bridge.js";
|
|
17
16
|
import { createNodeReplController } from "../../../agent/tools/node-repl.js";
|
|
17
|
+
import { createWebToolController } from "../../../agent/tools/web-controller.js";
|
|
18
18
|
import { readToolRuntime } from "../../../agent/tools/shared/runtime-context.js";
|
|
19
19
|
import { formatLiveInputGuidance } from "../../live-input-prompt.js";
|
|
20
20
|
import { appendStructuredOutputInstruction } from "./structured-output.js";
|
|
@@ -40,6 +40,7 @@ export async function buildTurnTools(runState, {
|
|
|
40
40
|
resolved,
|
|
41
41
|
onEvent,
|
|
42
42
|
runtimeWarnings,
|
|
43
|
+
toolExecutionMode = "safe-parallel",
|
|
43
44
|
}) {
|
|
44
45
|
const onTruncate = (info) => {
|
|
45
46
|
try {
|
|
@@ -73,6 +74,15 @@ export async function buildTurnTools(runState, {
|
|
|
73
74
|
sandboxEngine,
|
|
74
75
|
ctx: runCtx,
|
|
75
76
|
});
|
|
77
|
+
const webController = capabilities.tool_use === false
|
|
78
|
+
? null
|
|
79
|
+
: createWebToolController({
|
|
80
|
+
searchConfig: options.webSearchConfig,
|
|
81
|
+
fetchConfig: options.webFetchConfig,
|
|
82
|
+
sandboxPolicy: options.sandboxPolicy,
|
|
83
|
+
sandboxEngine,
|
|
84
|
+
ctx: runCtx,
|
|
85
|
+
});
|
|
76
86
|
|
|
77
87
|
// REUSED custom pieces: built-in tool sandboxing + allowlist/bloat filter +
|
|
78
88
|
// approval gates. These are identical to the legacy bridge.
|
|
@@ -109,6 +119,29 @@ export async function buildTurnTools(runState, {
|
|
|
109
119
|
approvalManager,
|
|
110
120
|
approvalModel: runtime.model?.id || runtime.model?.name || resolved.model,
|
|
111
121
|
nodeReplController,
|
|
122
|
+
webController,
|
|
123
|
+
toolExecutionMode,
|
|
124
|
+
subagents: options.subagents,
|
|
125
|
+
// The child inherits the parent's route and workspace unless its profile
|
|
126
|
+
// pins a model; the tool closure reads these to build each child request.
|
|
127
|
+
subagentContext: {
|
|
128
|
+
model: options.model,
|
|
129
|
+
executionMode: options.executionMode,
|
|
130
|
+
cwd: options.cwd,
|
|
131
|
+
parentRunId: runCtx?.runId,
|
|
132
|
+
// Same policy + engine this turn's own tools are confined by, so a
|
|
133
|
+
// child is never less sandboxed than the parent that spawned it.
|
|
134
|
+
sandboxPolicy: options.sandboxPolicy,
|
|
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
|
+
},
|
|
112
145
|
ctx: runCtx,
|
|
113
146
|
}));
|
|
114
147
|
|
|
@@ -143,14 +176,19 @@ export async function buildTurnTools(runState, {
|
|
|
143
176
|
|
|
144
177
|
const tools = [
|
|
145
178
|
...builtIns,
|
|
146
|
-
...mcpInit.tools,
|
|
179
|
+
...mcpInit.tools.map((tool) => ({ ...tool, executionMode: "sequential" })),
|
|
147
180
|
...(structuredTool ? [structuredTool] : []),
|
|
148
181
|
];
|
|
149
182
|
return {
|
|
150
183
|
tools,
|
|
151
184
|
structuredTool,
|
|
152
185
|
mcpClients: mcpInit.clients,
|
|
153
|
-
closeRunTools: async () => {
|
|
186
|
+
closeRunTools: async () => {
|
|
187
|
+
await Promise.allSettled([
|
|
188
|
+
nodeReplController?.close(),
|
|
189
|
+
webController?.close(),
|
|
190
|
+
].filter(Boolean));
|
|
191
|
+
},
|
|
154
192
|
};
|
|
155
193
|
}
|
|
156
194
|
|
|
@@ -189,8 +227,27 @@ export function thinkingLevelForEffort(effort, capabilities) {
|
|
|
189
227
|
* @param {any} params
|
|
190
228
|
* @returns {any}
|
|
191
229
|
*/
|
|
230
|
+
/**
|
|
231
|
+
* Restore the error flag on a tool result pi resolved successfully.
|
|
232
|
+
*
|
|
233
|
+
* pi hardcodes `isError: false` for every `execute()` that returns rather than
|
|
234
|
+
* throws, so any tool that reports failure in its payload needs this hook or
|
|
235
|
+
* the model is told the call succeeded.
|
|
236
|
+
*
|
|
237
|
+
* @param {*} details Tool-result details recorded by the bridge.
|
|
238
|
+
* @returns {{isError: true}|undefined}
|
|
239
|
+
*/
|
|
240
|
+
export function toolResultErrorOverride(details) {
|
|
241
|
+
const subagentStatus = details?.subagent?.status;
|
|
242
|
+
const failed = details?.mcp_result_is_error === true
|
|
243
|
+
|| details?.outcome?.status === "error"
|
|
244
|
+
// A failed subagent returns its answer-plus-log instead of throwing,
|
|
245
|
+
// precisely so a failed delegation keeps its activity log.
|
|
246
|
+
|| (typeof subagentStatus === "string" && subagentStatus !== "ok");
|
|
247
|
+
return failed ? { isError: true } : undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
192
250
|
export function buildTurnHarness(runState, {
|
|
193
|
-
cwd,
|
|
194
251
|
session,
|
|
195
252
|
piModels,
|
|
196
253
|
model,
|
|
@@ -208,8 +265,12 @@ export function buildTurnHarness(runState, {
|
|
|
208
265
|
sdk,
|
|
209
266
|
reference,
|
|
210
267
|
}) {
|
|
268
|
+
// pi-agent-core 0.83.0 removed `env` from AgentHarnessOptions: an
|
|
269
|
+
// ExecutionEnv now reaches tools through the generic per-turn `toolContext`
|
|
270
|
+
// instead. mono-agent needs neither — it uses none of pi's built-in
|
|
271
|
+
// file/shell tools, and its own tools close over what they need — so the
|
|
272
|
+
// option is dropped rather than migrated.
|
|
211
273
|
const harness = new AgentHarness({
|
|
212
|
-
env: new NodeExecutionEnv({ cwd: cwd || process.cwd() }),
|
|
213
274
|
session,
|
|
214
275
|
models: piModels,
|
|
215
276
|
model,
|
|
@@ -225,9 +286,13 @@ export function buildTurnHarness(runState, {
|
|
|
225
286
|
// this after-tool hook restores the error flag while preserving the already
|
|
226
287
|
// bounded content/details verbatim. Downstream tool_execution_end and timing
|
|
227
288
|
// events therefore report the failure accurately.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
289
|
+
//
|
|
290
|
+
// A failed subagent is the same shape of problem: the `Agent` tool returns
|
|
291
|
+
// its formatted answer-plus-activity-log rather than throwing, precisely so a
|
|
292
|
+
// failed delegation keeps its log — but pi hardcodes `isError: false` for
|
|
293
|
+
// every resolved execute(), so without this the model would be told a failed,
|
|
294
|
+
// timed-out, or empty delegation succeeded.
|
|
295
|
+
harness.on("tool_result", (event) => toolResultErrorOverride(event?.details));
|
|
231
296
|
runState.harness = harness;
|
|
232
297
|
|
|
233
298
|
harness.subscribe(createStreamSubscriber(runState, {
|
|
@@ -31,7 +31,9 @@ import {
|
|
|
31
31
|
resolveAgentCompactionPolicy,
|
|
32
32
|
resolveRuntimePolicyInputs,
|
|
33
33
|
} from "../../agent/compaction.js";
|
|
34
|
+
import { subagentInvocationCount } 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";
|
|
@@ -78,6 +80,37 @@ import {
|
|
|
78
80
|
} from "./pi-native/turn-runner.js";
|
|
79
81
|
import { resolvePiTransport } from "./pi-native/transport.js";
|
|
80
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Pi 0.80.6 exposes per-tool execution markers but not AgentHarness's global
|
|
85
|
+
* toolExecution option. Resolve mono-agent's programmatic mode once per run;
|
|
86
|
+
* individual tool builders then mark stateful/mutating tools sequential.
|
|
87
|
+
*/
|
|
88
|
+
export function resolvePiToolExecutionMode(options = {}) {
|
|
89
|
+
const warnings = [];
|
|
90
|
+
const requested = options.piToolExecutionMode;
|
|
91
|
+
let mode = requested === "sequential" || requested === "safe-parallel"
|
|
92
|
+
? requested
|
|
93
|
+
: null;
|
|
94
|
+
if (requested !== undefined && mode === null) {
|
|
95
|
+
warnings.push({
|
|
96
|
+
warning_kind: "invalid_pi_tool_execution_mode",
|
|
97
|
+
message: `Unknown piToolExecutionMode ${JSON.stringify(requested)}; using safe-parallel.`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (options.piToolParallelismMode !== undefined) {
|
|
101
|
+
warnings.push({
|
|
102
|
+
warning_kind: "deprecated_pi_tool_parallelism_mode",
|
|
103
|
+
message: "piToolParallelismMode is deprecated; use piToolExecutionMode (sequential or safe-parallel).",
|
|
104
|
+
});
|
|
105
|
+
if (mode === null) {
|
|
106
|
+
mode = options.piToolParallelismMode === "one-at-a-time"
|
|
107
|
+
? "sequential"
|
|
108
|
+
: "safe-parallel";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { mode: mode || "safe-parallel", warnings };
|
|
112
|
+
}
|
|
113
|
+
|
|
81
114
|
async function resolveApiKey(provider, { apiKeys, resolvePiApiKey, runtimeWarnings }) {
|
|
82
115
|
if (apiKeys?.has(provider)) return apiKeys.get(provider);
|
|
83
116
|
if (typeof resolvePiApiKey !== "function") return undefined;
|
|
@@ -307,13 +340,21 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
307
340
|
const piTransport = resolvePiTransport(options.piTransport);
|
|
308
341
|
|
|
309
342
|
const onEvent = (event) => emitCaptured(events, options.onEvent, event);
|
|
343
|
+
const approvalRiskTiers = {
|
|
344
|
+
...(options.toolRiskTiers || {}),
|
|
345
|
+
...(
|
|
346
|
+
options.toolRiskTiers?.Exec === undefined && options.toolRiskTiers?.Bash !== undefined
|
|
347
|
+
? { Exec: options.toolRiskTiers.Bash }
|
|
348
|
+
: {}
|
|
349
|
+
),
|
|
350
|
+
};
|
|
310
351
|
const approvalManager = options.onToolApprovalRequest
|
|
311
352
|
? createApprovalManager({
|
|
312
353
|
onToolApprovalRequest: options.onToolApprovalRequest,
|
|
313
354
|
defaultRiskTier: options.approvalDefaultRiskTier,
|
|
314
355
|
timeoutMs: options.approvalTimeoutMs,
|
|
315
356
|
onEvent,
|
|
316
|
-
riskTiersByTool:
|
|
357
|
+
riskTiersByTool: approvalRiskTiers,
|
|
317
358
|
alwaysAllowTools: options.approvalAlwaysAllowTools,
|
|
318
359
|
})
|
|
319
360
|
: null;
|
|
@@ -396,6 +437,11 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
396
437
|
// normalization. Restores configurable clamping (toolTextLimitChars,
|
|
397
438
|
// searchResultLimit, ...) on top of the 256KB hard ceiling.
|
|
398
439
|
const toolLimits = resolveAgentCompactionPolicy(settingsLike, runtime.model);
|
|
440
|
+
const toolExecution = resolvePiToolExecutionMode(options);
|
|
441
|
+
for (const warning of toolExecution.warnings) {
|
|
442
|
+
runtimeWarnings.push(warning);
|
|
443
|
+
onEvent({ type: "runtime_warning", ...warning });
|
|
444
|
+
}
|
|
399
445
|
|
|
400
446
|
// Build the turn's tools (builtins + MCP bridge + StructuredOutput). The
|
|
401
447
|
// StructuredOutput callback writes runState.structuredResult; the MCP clients
|
|
@@ -414,6 +460,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
414
460
|
resolved,
|
|
415
461
|
onEvent,
|
|
416
462
|
runtimeWarnings,
|
|
463
|
+
toolExecutionMode: toolExecution.mode,
|
|
417
464
|
});
|
|
418
465
|
mcpClients = builtMcpClients;
|
|
419
466
|
closeRunTools = builtCloseRunTools;
|
|
@@ -426,10 +473,9 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
426
473
|
const maxRetryDelayMs = Number.isFinite(Number(options.maxRetryDelayMs))
|
|
427
474
|
? Number(options.maxRetryDelayMs)
|
|
428
475
|
: 60_000;
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
const toolSteeringMode = options.piToolParallelismMode === "all" ? "all" : "one-at-a-time";
|
|
476
|
+
// Steering controls user follow-up delivery, not tool scheduling. Keep it
|
|
477
|
+
// independent from piToolExecutionMode; tools carry their own executionMode.
|
|
478
|
+
const toolSteeringMode = "one-at-a-time";
|
|
433
479
|
|
|
434
480
|
const piModels = buildRunModels(runtime, options, runtimeWarnings);
|
|
435
481
|
|
|
@@ -437,7 +483,6 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
437
483
|
// external abort handler (which sets runState.externalAbort and aborts the
|
|
438
484
|
// harness). Sets runState.harness + runState.removeAbortHandler.
|
|
439
485
|
harness = buildTurnHarness(runState, {
|
|
440
|
-
cwd: options.cwd,
|
|
441
486
|
session: runState.session,
|
|
442
487
|
piModels,
|
|
443
488
|
model: runtime.model,
|
|
@@ -665,8 +710,23 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
665
710
|
promptCacheActive: usage.cacheRead > 0 || usage.cacheWrite > 0,
|
|
666
711
|
thinkingEnabled: effectiveThinkingLevel !== "off" && effectiveThinkingLevel !== "low",
|
|
667
712
|
structuredOutputEnforced: !!options.outputSchema,
|
|
668
|
-
|
|
713
|
+
// What actually happened, not what was configured. This used to be a
|
|
714
|
+
// hardcoded `false`, which reported "no subagent ran" for runs that had
|
|
715
|
+
// just spawned several — the one signal that would have shown delegation
|
|
716
|
+
// working said it never happened.
|
|
717
|
+
//
|
|
718
|
+
// The count is keyed by the same parentRunId the Agent tool stamps its
|
|
719
|
+
// budget under (turn-runner threads `runCtx?.runId`); `runId` survives the
|
|
720
|
+
// sandbox-branch spread there, so reading it off the tool context directly
|
|
721
|
+
// yields the same key.
|
|
722
|
+
subagentInvoked: subagentInvocationCount(
|
|
723
|
+
options.subagents,
|
|
724
|
+
(options.toolContext ?? readToolRuntime())?.runId,
|
|
725
|
+
) > 0,
|
|
669
726
|
mcpServersUsed: mcpClients.map((entry) => entry?.name).filter(Boolean),
|
|
727
|
+
// Empty by contract, not by omission: "native" means provider-native
|
|
728
|
+
// subagents (Claude's Task tool). mono-agent's `Agent` is its own, so pi
|
|
729
|
+
// has none — `subagent_invoked` above is where pi delegation is reported.
|
|
670
730
|
nativeSubagentsUsed: [],
|
|
671
731
|
toolCompactionApplied: toolCompactionAppliedFromWarnings(runtimeWarnings),
|
|
672
732
|
// Tristate: true = a compaction fired this run (proactive or reactive),
|