@mono-agent/agent-runtime 0.6.2 → 0.9.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/README.md +36 -16
- package/package.json +14 -7
- package/src/agent/approval.js +52 -17
- package/src/agent/sandbox-seam.js +1 -0
- package/src/agent/tools/pi-bridge.js +7 -0
- package/src/agent/tools/shared/ripgrep.js +12 -8
- package/src/ai/index.js +8 -0
- package/src/ai/providers/claude-cli.js +109 -5
- package/src/ai/providers/claude-sandbox.js +71 -0
- package/src/ai/providers/claude-sdk-discovery-worker.js +53 -0
- package/src/ai/providers/claude-sdk-discovery.js +352 -0
- package/src/ai/providers/claude-sdk.js +313 -35
- package/src/ai/providers/codex-app.js +823 -78
- package/src/ai/providers/opencode-app.js +682 -96
- package/src/ai/providers/opencode-server.js +508 -0
- package/src/ai/providers/pi-native/turn-runner.js +8 -0
- package/src/ai/runtime/capabilities.js +12 -0
- package/src/ai/runtime/context-windows.js +8 -0
- package/src/ai/runtime/registry.js +8 -2
- package/src/ai/runtime/router.js +627 -29
- package/src/ai/types.js +29 -2
- package/src/index.js +6 -0
- package/src/runtime.js +17 -1
- package/types/agent/approval.d.ts +4 -7
- package/types/agent/sandbox-seam.d.ts +5 -0
- package/types/ai/backend.d.ts +16 -0
- package/types/ai/index.d.ts +1 -0
- package/types/ai/providers/claude-cli.d.ts +116 -0
- package/types/ai/providers/claude-sandbox.d.ts +79 -0
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +1 -0
- package/types/ai/providers/claude-sdk-discovery.d.ts +97 -0
- package/types/ai/providers/claude-sdk.d.ts +81 -5
- package/types/ai/providers/codex-app.d.ts +11 -7
- package/types/ai/providers/opencode-app.d.ts +15 -16
- package/types/ai/providers/opencode-server.d.ts +20 -0
- package/types/ai/runtime/capabilities.d.ts +19 -0
- package/types/ai/runtime/context-windows.d.ts +1 -0
- package/types/ai/runtime/router.d.ts +24 -23
- package/types/ai/types.d.ts +75 -2
- package/types/index.d.ts +1 -0
package/src/ai/runtime/router.js
CHANGED
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
// so the router is a drop-in replacement for createRuntime(host).
|
|
19
19
|
//
|
|
20
20
|
// chain entries:
|
|
21
|
-
// { model: ModelRef, executionMode?: "sdk" | "cli",
|
|
21
|
+
// { model: ModelRef, executionMode?: "sdk" | "cli", effort?: string|null,
|
|
22
|
+
// requires?: Capabilities }
|
|
22
23
|
// shorthand: a bare ModelRef is also accepted (no requirements).
|
|
24
|
+
// effort string = fixed for that route, undefined = inherit the legacy run
|
|
25
|
+
// effort, null = omit effort so the provider chooses its default.
|
|
23
26
|
//
|
|
24
27
|
// Result:
|
|
25
28
|
// The success run's result, with `failoverHistory` appended describing every
|
|
@@ -34,6 +37,7 @@ import { createRuntime } from "../../runtime.js";
|
|
|
34
37
|
import { isProviderAuthFailureText, retryableProviderFailureInfo } from "../failure.js";
|
|
35
38
|
import { runtimeCapabilities } from "./capabilities.js";
|
|
36
39
|
import { buildTranscriptTailSnapshot, renderResumeSnapshot } from "../../agent/transcript.js";
|
|
40
|
+
import { passthroughSandbox } from "../../agent/sandbox-seam.js";
|
|
37
41
|
import { resolveRuntimeBrand } from "../../runtime-brand.js";
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -47,9 +51,10 @@ import { resolveRuntimeBrand } from "../../runtime-brand.js";
|
|
|
47
51
|
/**
|
|
48
52
|
* @typedef {Object} RouterChainEntryInput
|
|
49
53
|
* A chain entry as accepted by createRouterRuntime: either the shorthand bare
|
|
50
|
-
* RuntimeModelRef, or the full `{model, executionMode?, requires?}` form.
|
|
54
|
+
* RuntimeModelRef, or the full `{model, executionMode?, effort?, requires?}` form.
|
|
51
55
|
* @property {RuntimeModelRef} model
|
|
52
56
|
* @property {string} [executionMode]
|
|
57
|
+
* @property {string|null} [effort]
|
|
53
58
|
* @property {Object<string, *>} [requires]
|
|
54
59
|
*/
|
|
55
60
|
|
|
@@ -57,21 +62,54 @@ import { resolveRuntimeBrand } from "../../runtime-brand.js";
|
|
|
57
62
|
* @typedef {Object} RouterChainEntry
|
|
58
63
|
* @property {RuntimeModelRef} model
|
|
59
64
|
* @property {string|null} executionMode
|
|
65
|
+
* @property {string|null|undefined} effort
|
|
60
66
|
* @property {Object<string, *>|null} requires
|
|
61
67
|
*/
|
|
62
68
|
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {Object} RouterAttemptResolution
|
|
71
|
+
* Private host seam for route-specific provider options/runtime ownership.
|
|
72
|
+
* Returned options are never copied into router telemetry.
|
|
73
|
+
* @property {AgentRuntimeInstance} [runtime]
|
|
74
|
+
* @property {Object<string, *>} [options]
|
|
75
|
+
* @property {() => (void|Promise<void>)} [cleanup]
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
const ROUTE_SAFETY_MODES = new Set(["uniform", "per-route-native"]);
|
|
79
|
+
const ATTEMPT_SCOPED_OPTION_KEYS = ["customProvider", "customModel", "modelCapabilities", "isPrivateProvider"];
|
|
80
|
+
const ROUTER_TOOL_CONTEXT_KEYS = [
|
|
81
|
+
"workspace", "repoRoot", "ripgrepPath", "qaOutputDir", "sandboxPolicy", "sandboxEngine",
|
|
82
|
+
];
|
|
83
|
+
const RESOLVER_PROTECTED_OPTION_KEYS = new Set([
|
|
84
|
+
"model", "executionMode", "effort", "messages", "abortSignal", "onEvent",
|
|
85
|
+
"sessionId", "providerSessionId", "sessionKeepAlive", "sessionIdleTimeoutMs",
|
|
86
|
+
"diagnosticsSeed", "systemPromptPrefix", "sandboxPolicy", "sandboxEngine", "sandbox",
|
|
87
|
+
"allowedTools", "disallowedTools", "permissionMode", "mcpServers", "skills",
|
|
88
|
+
"outputSchema", "nativeSubagents", "liveInput", "fastMode",
|
|
89
|
+
]);
|
|
90
|
+
|
|
63
91
|
/**
|
|
64
92
|
* @param {Object} [options]
|
|
65
93
|
* @param {AgentRuntimeHostOptions} [options.host]
|
|
66
94
|
* @param {ReadonlyArray<RuntimeModelRef|RouterChainEntryInput>} [options.chain]
|
|
95
|
+
* @param {"uniform"|"per-route-native"} [options.routeSafety]
|
|
96
|
+
* @param {(input: {model: RuntimeModelRef, executionMode: string|null, attemptIndex: number, routeSafety: "uniform"|"per-route-native"}) => (RouterAttemptResolution|Promise<RouterAttemptResolution>)} [options.resolveAttempt]
|
|
67
97
|
* @returns {AgentRuntimeInstance & {chain: () => Array<RouterChainEntry>}}
|
|
68
98
|
*/
|
|
69
|
-
export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
99
|
+
export function createRouterRuntime({ host = {}, chain = [], routeSafety = "uniform", resolveAttempt } = {}) {
|
|
100
|
+
if (!ROUTE_SAFETY_MODES.has(routeSafety)) {
|
|
101
|
+
throw new Error("createRouterRuntime routeSafety must be uniform or per-route-native");
|
|
102
|
+
}
|
|
70
103
|
const entries = normaliseChain(chain);
|
|
71
104
|
if (entries.length === 0) {
|
|
72
105
|
throw new Error("createRouterRuntime requires a non-empty chain");
|
|
73
106
|
}
|
|
107
|
+
assertUniqueEntries(entries);
|
|
74
108
|
const inner = createRuntime(host);
|
|
109
|
+
/** @type {Map<string, AgentRuntimeInstance>} */
|
|
110
|
+
const routeRuntimes = new Map();
|
|
111
|
+
/** @type {import('../types.js').AgentRuntimeToolOptions|undefined} */
|
|
112
|
+
let configuredTools;
|
|
75
113
|
// The router builds transcript-tail snapshots outside the inner runtime's
|
|
76
114
|
// bridge call (which is where the per-instance toolContext lives), so resolve
|
|
77
115
|
// the host brand here to stamp the snapshot schema id with the same brand the
|
|
@@ -87,44 +125,155 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
87
125
|
* @returns {Promise<RuntimeResult>}
|
|
88
126
|
*/
|
|
89
127
|
async run(systemPrompt, options = {}) {
|
|
90
|
-
/** @type {Array<{model: RuntimeModelRef, failureKind: (string|null), requestId?: (string|null|undefined), retryableSubkind?: (string|null|undefined), requirements?: (Object<string,*>|null)}>} */
|
|
128
|
+
/** @type {Array<{model: RuntimeModelRef, failureKind: (string|null), requestId?: (string|null|undefined), retryableSubkind?: (string|null|undefined), requirements?: (Object<string,*>|null), routeSafety?: import('../types.js').RuntimeRouteSafetyMode, safetyContract?: import('../types.js').RuntimeRouteSafetyContract}>} */
|
|
91
129
|
const failoverHistory = [];
|
|
92
130
|
/** @type {RuntimeResult|null} */
|
|
93
131
|
let lastResult = null;
|
|
132
|
+
/** @type {RuntimeResult|null} */
|
|
133
|
+
let lastRouteSkip = null;
|
|
134
|
+
const promptBase = systemPrompt;
|
|
94
135
|
/** @type {*} */
|
|
95
|
-
let
|
|
136
|
+
let pendingSnapshot = null;
|
|
137
|
+
/** @type {Array<{attemptIndex: number, model: RuntimeModelRef, routeSafety: import('../types.js').RuntimeRouteSafetyMode, safetyContract: import('../types.js').RuntimeRouteSafetyContract, status: string}>} */
|
|
138
|
+
const routeSafetyHistory = [];
|
|
96
139
|
|
|
97
140
|
for (let i = 0; i < entries.length; i += 1) {
|
|
98
141
|
const entry = entries[i];
|
|
142
|
+
const effectiveToolOptions = effectiveRouterToolOptions(host, configuredTools);
|
|
143
|
+
let safetyContract = routeSafetyContract(
|
|
144
|
+
routeSafety,
|
|
145
|
+
entry,
|
|
146
|
+
effectivePiSandboxPolicy(effectiveToolOptions, options),
|
|
147
|
+
);
|
|
99
148
|
if (!entrySatisfiesRequirements(entry, options)) {
|
|
149
|
+
lastRouteSkip = {
|
|
150
|
+
text: null,
|
|
151
|
+
error: `Route ${modelKey(entry.model)} does not satisfy the logical run's required capabilities.`,
|
|
152
|
+
failureKind: "skipped_capability_mismatch",
|
|
153
|
+
events: [],
|
|
154
|
+
cancelled: false,
|
|
155
|
+
usage: {},
|
|
156
|
+
};
|
|
100
157
|
failoverHistory.push({
|
|
101
158
|
model: entry.model,
|
|
102
159
|
failureKind: "skipped_capability_mismatch",
|
|
103
160
|
requirements: entry.requires,
|
|
161
|
+
routeSafety,
|
|
162
|
+
safetyContract,
|
|
104
163
|
});
|
|
164
|
+
const skippedRecord = routeSafetyRecord(i, entry, safetyContract, "skipped_capability_mismatch");
|
|
165
|
+
routeSafetyHistory.push(skippedRecord);
|
|
166
|
+
emit(options, { type: "provider_route_safety", ...skippedRecord });
|
|
105
167
|
continue;
|
|
106
168
|
}
|
|
107
169
|
|
|
108
|
-
|
|
170
|
+
/** @type {*} */
|
|
171
|
+
let callOptions = {
|
|
109
172
|
...options,
|
|
110
173
|
model: entry.model,
|
|
111
174
|
executionMode: entry.executionMode || options.executionMode,
|
|
112
175
|
};
|
|
113
|
-
|
|
176
|
+
// The legacy run-level custom-provider bag describes the primary
|
|
177
|
+
// route. Without a route resolver there is no authoritative metadata
|
|
178
|
+
// for a different fallback, so never let the primary's credentials or
|
|
179
|
+
// model capabilities contaminate later attempts.
|
|
180
|
+
if (resolveAttempt === undefined && i > 0) {
|
|
181
|
+
callOptions = withoutAttemptScopedOptions(callOptions);
|
|
182
|
+
}
|
|
183
|
+
/** @type {AgentRuntimeInstance} */
|
|
184
|
+
let attemptRuntime = inner;
|
|
185
|
+
/** @type {(() => (void|Promise<void>))|undefined} */
|
|
186
|
+
let attemptCleanup;
|
|
187
|
+
try {
|
|
188
|
+
const resolved = resolveAttempt === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: await resolveAttempt({
|
|
191
|
+
model: entry.model,
|
|
192
|
+
executionMode: entry.executionMode,
|
|
193
|
+
attemptIndex: i,
|
|
194
|
+
routeSafety,
|
|
195
|
+
});
|
|
196
|
+
const resolution = normalizeAttemptResolution(resolved);
|
|
197
|
+
attemptCleanup = resolution?.cleanup;
|
|
198
|
+
if (resolveAttempt !== undefined) {
|
|
199
|
+
callOptions = mergeAttemptOptions(callOptions, resolution?.options);
|
|
200
|
+
}
|
|
201
|
+
if (routeSafety === "per-route-native") {
|
|
202
|
+
callOptions = projectPerRouteNativeOptions(entry, callOptions);
|
|
203
|
+
const key = routeRuntimeKey(entry, i);
|
|
204
|
+
const resolvedRuntime = resolution?.runtime;
|
|
205
|
+
if (resolvedRuntime !== undefined) {
|
|
206
|
+
assertRuntimeLike(resolvedRuntime);
|
|
207
|
+
const previousRuntime = routeRuntimes.get(key);
|
|
208
|
+
if (previousRuntime !== undefined && previousRuntime !== resolvedRuntime) {
|
|
209
|
+
try { await previousRuntime.disposeAllSessions?.(); } catch { /* best-effort replacement */ }
|
|
210
|
+
}
|
|
211
|
+
routeRuntimes.set(key, resolvedRuntime);
|
|
212
|
+
if (entry.model.sdk !== "pi") {
|
|
213
|
+
resolvedRuntime.configureTools?.(projectPerRouteNativeToolOptions(entry, configuredTools));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
attemptRuntime = routeRuntimes.get(key) ?? createRouteRuntime(key, entry, host, routeRuntimes, configuredTools);
|
|
217
|
+
if (entry.model.sdk === "pi") {
|
|
218
|
+
projectPiRuntimeToolContext(attemptRuntime, effectiveToolOptions);
|
|
219
|
+
// Derive the attestation from the same complete base context and
|
|
220
|
+
// request-scoped inputs that the supplied/runtime-owned Pi
|
|
221
|
+
// bridge will actually receive. Resolver options cannot alter
|
|
222
|
+
// these protected fields.
|
|
223
|
+
safetyContract = routeSafetyContract(
|
|
224
|
+
routeSafety,
|
|
225
|
+
entry,
|
|
226
|
+
effectivePiSandboxPolicy(effectiveToolOptions, callOptions),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
} else if (resolution?.runtime !== undefined && resolution.runtime !== inner) {
|
|
230
|
+
throw new Error("uniform route safety cannot replace the shared monotonic runtime");
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
try { await attemptCleanup?.(); } catch { /* cleanup is additive */ }
|
|
234
|
+
const failure = safetyUnavailableResult(error);
|
|
235
|
+
lastRouteSkip = failure;
|
|
236
|
+
failoverHistory.push({
|
|
237
|
+
model: entry.model,
|
|
238
|
+
failureKind: "safety_unavailable",
|
|
239
|
+
routeSafety,
|
|
240
|
+
safetyContract,
|
|
241
|
+
});
|
|
242
|
+
const unavailableRecord = routeSafetyRecord(i, entry, safetyContract, "safety_unavailable");
|
|
243
|
+
routeSafetyHistory.push(unavailableRecord);
|
|
244
|
+
emit(callOptions, { type: "provider_route_safety", ...unavailableRecord });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
applyEntryEffort(callOptions, entry.effort);
|
|
249
|
+
// A provider session belongs to the route that created it. The entire
|
|
250
|
+
// chain is stateless whenever a fallback exists, keeping the full
|
|
251
|
+
// logical run replayable regardless of which route is attempted.
|
|
252
|
+
if (entries.length > 1 || i > 0 || !entrySupportsSessionResume(entry)) {
|
|
253
|
+
delete callOptions.sessionId;
|
|
254
|
+
delete callOptions.providerSessionId;
|
|
255
|
+
delete callOptions.sessionKeepAlive;
|
|
256
|
+
delete callOptions.sessionIdleTimeoutMs;
|
|
257
|
+
}
|
|
258
|
+
let attemptSystemPrompt = promptBase;
|
|
259
|
+
if (pendingSnapshot) {
|
|
114
260
|
callOptions.diagnosticsSeed = {
|
|
115
261
|
...(callOptions.diagnosticsSeed || {}),
|
|
116
|
-
resume_snapshot:
|
|
262
|
+
resume_snapshot: pendingSnapshot,
|
|
117
263
|
};
|
|
118
264
|
// Also prepend the rendered snapshot to the system prompt so SDK
|
|
119
265
|
// backends that don't read diagnosticsSeed still continue from the
|
|
120
|
-
|
|
121
|
-
const rendered = renderResumeSnapshot(resumeSnapshot);
|
|
266
|
+
const rendered = renderResumeSnapshot(pendingSnapshot);
|
|
122
267
|
if (rendered) {
|
|
123
268
|
callOptions.systemPromptPrefix = rendered;
|
|
124
|
-
|
|
269
|
+
attemptSystemPrompt = `${rendered}\n\n${promptBase}`;
|
|
125
270
|
}
|
|
126
271
|
}
|
|
127
272
|
|
|
273
|
+
const safetyRecord = routeSafetyRecord(i, entry, safetyContract, "attempted");
|
|
274
|
+
routeSafetyHistory.push(safetyRecord);
|
|
275
|
+
emit(callOptions, { type: "provider_route_safety", ...safetyRecord });
|
|
276
|
+
|
|
128
277
|
if (failoverHistory.length > 0) {
|
|
129
278
|
emit(callOptions, {
|
|
130
279
|
type: "provider_failover_started",
|
|
@@ -136,7 +285,7 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
136
285
|
|
|
137
286
|
let result;
|
|
138
287
|
try {
|
|
139
|
-
result = await
|
|
288
|
+
result = await attemptRuntime.run(attemptSystemPrompt, callOptions);
|
|
140
289
|
} catch (err) {
|
|
141
290
|
// The inner runtime usually surfaces errors as structured result
|
|
142
291
|
// fields, but a bridge can still throw synchronously (e.g. spawn
|
|
@@ -150,6 +299,8 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
150
299
|
cancelled: false,
|
|
151
300
|
usage: {},
|
|
152
301
|
};
|
|
302
|
+
} finally {
|
|
303
|
+
try { await attemptCleanup?.(); } catch { /* cleanup is additive */ }
|
|
153
304
|
}
|
|
154
305
|
|
|
155
306
|
result = normalizeProviderAuthFailure(result);
|
|
@@ -169,7 +320,7 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
169
320
|
model: entry.model,
|
|
170
321
|
});
|
|
171
322
|
}
|
|
172
|
-
return { ...result, failoverHistory };
|
|
323
|
+
return { ...result, failoverHistory, routeSafetyHistory };
|
|
173
324
|
}
|
|
174
325
|
|
|
175
326
|
failoverHistory.push({
|
|
@@ -177,26 +328,37 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
177
328
|
failureKind: result.failureKind || null,
|
|
178
329
|
requestId: retryability.requestId,
|
|
179
330
|
retryableSubkind: retryability.subkind,
|
|
331
|
+
routeSafety,
|
|
332
|
+
safetyContract,
|
|
180
333
|
});
|
|
334
|
+
if (result.failureKind === "skipped_capability_mismatch") {
|
|
335
|
+
lastRouteSkip = result;
|
|
336
|
+
// A bridge-level mismatch is about this route, not the logical run.
|
|
337
|
+
// Try the next entry and do not derive a transcript snapshot from it.
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
181
340
|
lastResult = result;
|
|
182
341
|
|
|
183
342
|
// Provider auth is terminal for one provider, but chain-retryable: a
|
|
184
343
|
// fallback provider may have working credentials. Other non-retryable
|
|
185
344
|
// provider/request errors remain terminal.
|
|
186
345
|
const shouldFallback = (retryability.retryable || result.failureKind === "provider_auth")
|
|
187
|
-
&& !result.cancelled
|
|
346
|
+
&& !result.cancelled
|
|
347
|
+
&& !isMidTurnSafetyFailure(result.failureKind);
|
|
188
348
|
if (!shouldFallback) {
|
|
189
|
-
return { ...result, failoverHistory };
|
|
349
|
+
return { ...result, failoverHistory, routeSafetyHistory };
|
|
190
350
|
}
|
|
191
351
|
|
|
192
352
|
// Build a transcript-tail snapshot from this run's events so the
|
|
193
353
|
// next provider can continue. If the run produced no usable events,
|
|
194
354
|
// skip the snapshot (the next attempt starts fresh).
|
|
195
355
|
const snapshot = buildTranscriptTailSnapshot(result.events, { runtimeBrand });
|
|
196
|
-
|
|
356
|
+
// Keep one bounded snapshot object across the logical run instead of
|
|
357
|
+
// nesting a new <resume_context> block on every provider transition.
|
|
358
|
+
pendingSnapshot = mergeResumeSnapshots(pendingSnapshot, snapshot);
|
|
197
359
|
}
|
|
198
360
|
|
|
199
|
-
const exhaustedResult = lastResult || {
|
|
361
|
+
const exhaustedResult = lastResult || lastRouteSkip || {
|
|
200
362
|
text: null,
|
|
201
363
|
events: [],
|
|
202
364
|
error: "router chain exhausted with no executions",
|
|
@@ -208,17 +370,33 @@ export function createRouterRuntime({ host = {}, chain = [] } = {}) {
|
|
|
208
370
|
...exhaustedResult,
|
|
209
371
|
failureKind: lastResult ? "provider_unavailable_exhausted" : exhaustedResult.failureKind,
|
|
210
372
|
failoverHistory,
|
|
373
|
+
routeSafetyHistory,
|
|
211
374
|
};
|
|
212
375
|
},
|
|
213
376
|
chain: () => entries.slice(),
|
|
214
|
-
configureTools(next) {
|
|
215
|
-
|
|
377
|
+
configureTools(next = {}) {
|
|
378
|
+
configuredTools = { ...(configuredTools || {}), ...next };
|
|
379
|
+
if (routeSafety === "uniform") {
|
|
380
|
+
inner.configureTools?.(next);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
entries.forEach((entry, index) => {
|
|
384
|
+
const runtime = routeRuntimes.get(routeRuntimeKey(entry, index));
|
|
385
|
+
runtime?.configureTools?.(projectPerRouteNativeToolOptions(entry, next));
|
|
386
|
+
});
|
|
387
|
+
// `inner` is deliberately not configured in per-route-native mode: all
|
|
388
|
+
// attempts use their isolated route runtime, and applying one route's
|
|
389
|
+
// policy to this shared standby would defeat that isolation.
|
|
216
390
|
},
|
|
217
391
|
async disposeSession(providerSessionId) {
|
|
218
|
-
|
|
392
|
+
let disposed = false;
|
|
393
|
+
for (const runtime of allRuntimes(inner, routeRuntimes)) {
|
|
394
|
+
disposed = Boolean(await runtime.disposeSession?.(providerSessionId)) || disposed;
|
|
395
|
+
}
|
|
396
|
+
return disposed;
|
|
219
397
|
},
|
|
220
398
|
async disposeAllSessions() {
|
|
221
|
-
await inner.disposeAllSessions?.();
|
|
399
|
+
await Promise.all(allRuntimes(inner, routeRuntimes).map(async (runtime) => runtime.disposeAllSessions?.()));
|
|
222
400
|
},
|
|
223
401
|
};
|
|
224
402
|
}
|
|
@@ -236,12 +414,13 @@ function normaliseChain(chain) {
|
|
|
236
414
|
if (!entry) return null;
|
|
237
415
|
if (entry.sdk && entry.model) {
|
|
238
416
|
// ModelRef shorthand: { sdk, model, ... }
|
|
239
|
-
return { model: entry, executionMode: null, requires: null };
|
|
417
|
+
return { model: entry, executionMode: null, effort: undefined, requires: null };
|
|
240
418
|
}
|
|
241
419
|
if (entry.model) {
|
|
242
420
|
return {
|
|
243
421
|
model: entry.model,
|
|
244
422
|
executionMode: typeof entry.executionMode === "string" ? entry.executionMode : null,
|
|
423
|
+
effort: normalizeChainEffort(entry.effort),
|
|
245
424
|
requires: entry.requires && typeof entry.requires === "object" ? entry.requires : null,
|
|
246
425
|
};
|
|
247
426
|
}
|
|
@@ -250,6 +429,388 @@ function normaliseChain(chain) {
|
|
|
250
429
|
.filter(Boolean));
|
|
251
430
|
}
|
|
252
431
|
|
|
432
|
+
/** @param {*} effort @returns {string|null|undefined} */
|
|
433
|
+
function normalizeChainEffort(effort) {
|
|
434
|
+
if (effort === undefined || effort === null) return effort;
|
|
435
|
+
if (typeof effort !== "string" || effort.length === 0 || effort.trim() !== effort) {
|
|
436
|
+
throw new Error("createRouterRuntime chain effort must be a non-empty trimmed string, null, or omitted");
|
|
437
|
+
}
|
|
438
|
+
return effort;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** @param {Array<RouterChainEntry>} entries */
|
|
442
|
+
function assertUniqueEntries(entries) {
|
|
443
|
+
const seen = new Map();
|
|
444
|
+
entries.forEach((entry, index) => {
|
|
445
|
+
const key = modelKey(entry.model);
|
|
446
|
+
const first = seen.get(key);
|
|
447
|
+
if (first !== undefined) {
|
|
448
|
+
throw new Error(`createRouterRuntime duplicate model ${key} at chain entries ${first} and ${index}`);
|
|
449
|
+
}
|
|
450
|
+
seen.set(key, index);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** @param {RuntimeModelRef} model */
|
|
455
|
+
function modelKey(model) {
|
|
456
|
+
const provider = typeof model.provider === "string" && model.provider.length > 0 ? `${model.provider}:` : "";
|
|
457
|
+
return `${model.sdk}:${provider}${model.model}`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* A small fixed vocabulary keeps safety telemetry bounded and prevents route
|
|
462
|
+
* credentials/options from accidentally entering events or persisted results.
|
|
463
|
+
* @param {"uniform"|"per-route-native"} mode
|
|
464
|
+
* @param {RouterChainEntry} entry
|
|
465
|
+
* @param {Object<string, *>|undefined} piSandboxPolicy
|
|
466
|
+
* @returns {import('../types.js').RuntimeRouteSafetyContract}
|
|
467
|
+
*/
|
|
468
|
+
function routeSafetyContract(mode, entry, piSandboxPolicy) {
|
|
469
|
+
if (mode === "uniform") {
|
|
470
|
+
return {
|
|
471
|
+
mode,
|
|
472
|
+
sandbox: "mono-agent-monotonic",
|
|
473
|
+
tools: "mono-agent-monotonic",
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
switch (entry.model.sdk) {
|
|
477
|
+
case "pi":
|
|
478
|
+
return {
|
|
479
|
+
mode,
|
|
480
|
+
sandbox: piSandboxContract(piSandboxPolicy),
|
|
481
|
+
tools: "mono-agent-policy",
|
|
482
|
+
};
|
|
483
|
+
case "claude":
|
|
484
|
+
return { mode, sandbox: "provider-native", tools: "provider-representable" };
|
|
485
|
+
case "codex":
|
|
486
|
+
return { mode, sandbox: "codex-native", tools: "exact-allow-all" };
|
|
487
|
+
case "opencode":
|
|
488
|
+
return { mode, sandbox: "provider-native", tools: "exact-allow-all" };
|
|
489
|
+
default:
|
|
490
|
+
return { mode, sandbox: "unsupported", tools: "unsupported" };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Describe the Pi sandbox posture without claiming that SRT is enforced when
|
|
496
|
+
* the effective policy explicitly permits an unavailable engine to fall back
|
|
497
|
+
* to an unsandboxed host process. Both fields are required because the runtime
|
|
498
|
+
* adapter treats an unsafe fallback without its explicit opt-in as fail-closed.
|
|
499
|
+
*
|
|
500
|
+
* @param {Object<string, *>|undefined} policy
|
|
501
|
+
* @returns {import('../types.js').RuntimeRouteSandboxContract}
|
|
502
|
+
*/
|
|
503
|
+
function piSandboxContract(policy) {
|
|
504
|
+
if (policy === undefined) return "disabled";
|
|
505
|
+
if (policy.fallback === "unsafe-host-process" && policy.unsafeAllowHostProcess === true) {
|
|
506
|
+
return "mono-agent-srt-unsafe-host-fallback";
|
|
507
|
+
}
|
|
508
|
+
return "mono-agent-srt";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Describe the policy Pi tools actually receive after host/configure/run
|
|
513
|
+
* precedence. A request-scoped `off` policy cannot weaken an active host
|
|
514
|
+
* policy, while an active request policy can tighten an absent/off host.
|
|
515
|
+
* `configureTools({ sandboxPolicy: undefined })` explicitly clears the host
|
|
516
|
+
* tool-context policy and must therefore be distinguished from an omitted key.
|
|
517
|
+
*
|
|
518
|
+
* @param {import('../types.js').AgentRuntimeToolOptions} toolOptions
|
|
519
|
+
* @param {Object<string, *>} runOptions
|
|
520
|
+
* @returns {Object<string, *>|undefined}
|
|
521
|
+
*/
|
|
522
|
+
function effectivePiSandboxPolicy(toolOptions, runOptions) {
|
|
523
|
+
// Match the Pi turn runner's implementation precedence: a run-scoped
|
|
524
|
+
// RuntimeSandbox wins, followed by configureTools/host, then the kernel's
|
|
525
|
+
// fail-closed passthrough. Delegating the merge is essential here: the real
|
|
526
|
+
// adapter makes fail-closed dominate unsafe-host-process, so inspecting only
|
|
527
|
+
// the first non-off policy would produce false safety telemetry.
|
|
528
|
+
const sandbox = runOptions.sandbox
|
|
529
|
+
?? toolOptions.sandbox
|
|
530
|
+
?? passthroughSandbox;
|
|
531
|
+
const policy = sandbox.mergePolicies(toolOptions.sandboxPolicy, runOptions.sandboxPolicy);
|
|
532
|
+
return policy && typeof policy === "object" && policy.mode !== "off"
|
|
533
|
+
? policy
|
|
534
|
+
: undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Resolve the router-owned base ToolContext exactly as createRuntime(host)
|
|
539
|
+
* followed by the router's configureTools calls would. Every data key is
|
|
540
|
+
* present so projecting into a resolver-supplied runtime also clears hidden
|
|
541
|
+
* state. RuntimeSandbox is special: configureTools intentionally ignores an
|
|
542
|
+
* undefined implementation, so the configured value only replaces the host
|
|
543
|
+
* seam when it is concrete.
|
|
544
|
+
*
|
|
545
|
+
* @param {AgentRuntimeHostOptions} host
|
|
546
|
+
* @param {import('../types.js').AgentRuntimeToolOptions|undefined} configuredTools
|
|
547
|
+
* @returns {import('../types.js').AgentRuntimeToolOptions}
|
|
548
|
+
*/
|
|
549
|
+
function effectiveRouterToolOptions(host, configuredTools) {
|
|
550
|
+
/** @type {Object<string, *>} */
|
|
551
|
+
const effective = {};
|
|
552
|
+
for (const key of ROUTER_TOOL_CONTEXT_KEYS) {
|
|
553
|
+
effective[key] = configuredTools !== undefined && Object.hasOwn(configuredTools, key)
|
|
554
|
+
? configuredTools[key]
|
|
555
|
+
: host[key];
|
|
556
|
+
}
|
|
557
|
+
effective.sandbox = configuredTools?.sandbox
|
|
558
|
+
?? host.sandbox
|
|
559
|
+
?? passthroughSandbox;
|
|
560
|
+
return effective;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* A resolver-supplied Pi runtime is allowed to own credentials/provider
|
|
565
|
+
* lifecycle, never the route's safety posture. Replace its complete mutable
|
|
566
|
+
* ToolContext before every execution so a blank, stale, or weaker runtime
|
|
567
|
+
* cannot diverge from the router's host/configured/run policy or telemetry.
|
|
568
|
+
*
|
|
569
|
+
* @param {AgentRuntimeInstance} runtime
|
|
570
|
+
* @param {import('../types.js').AgentRuntimeToolOptions} toolOptions
|
|
571
|
+
*/
|
|
572
|
+
function projectPiRuntimeToolContext(runtime, toolOptions) {
|
|
573
|
+
if (typeof runtime.configureTools !== "function") {
|
|
574
|
+
throw new Error("per-route-native Pi runtime must expose configureTools() for safety projection");
|
|
575
|
+
}
|
|
576
|
+
runtime.configureTools(toolOptions);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* @param {number} attemptIndex
|
|
581
|
+
* @param {RouterChainEntry} entry
|
|
582
|
+
* @param {import('../types.js').RuntimeRouteSafetyContract} contract
|
|
583
|
+
* @param {string} status
|
|
584
|
+
* @returns {{attemptIndex: number, model: RuntimeModelRef, routeSafety: import('../types.js').RuntimeRouteSafetyMode, safetyContract: import('../types.js').RuntimeRouteSafetyContract, status: string}}
|
|
585
|
+
*/
|
|
586
|
+
function routeSafetyRecord(attemptIndex, entry, contract, status) {
|
|
587
|
+
return {
|
|
588
|
+
attemptIndex,
|
|
589
|
+
model: entry.model,
|
|
590
|
+
routeSafety: contract.mode,
|
|
591
|
+
safetyContract: contract,
|
|
592
|
+
status,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** @param {RouterAttemptResolution|undefined} value */
|
|
597
|
+
function normalizeAttemptResolution(value) {
|
|
598
|
+
if (value === undefined) return undefined;
|
|
599
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
600
|
+
throw new Error("route attempt resolver must return an object or undefined");
|
|
601
|
+
}
|
|
602
|
+
if (value.options !== undefined && (value.options === null || typeof value.options !== "object" || Array.isArray(value.options))) {
|
|
603
|
+
throw new Error("route attempt resolver options must be an object");
|
|
604
|
+
}
|
|
605
|
+
if (value.cleanup !== undefined && typeof value.cleanup !== "function") {
|
|
606
|
+
throw new Error("route attempt resolver cleanup must be a function");
|
|
607
|
+
}
|
|
608
|
+
return value;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Removes credentials and model metadata belonging to the previous route,
|
|
613
|
+
* then applies the current route's private options. Logical request, safety,
|
|
614
|
+
* effort, and session fields remain router-owned.
|
|
615
|
+
* @param {Object<string, *>} base
|
|
616
|
+
* @param {Object<string, *>|undefined} resolved
|
|
617
|
+
*/
|
|
618
|
+
function mergeAttemptOptions(base, resolved) {
|
|
619
|
+
const merged = withoutAttemptScopedOptions(base);
|
|
620
|
+
if (resolved === undefined) return merged;
|
|
621
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
622
|
+
if (RESOLVER_PROTECTED_OPTION_KEYS.has(key)) {
|
|
623
|
+
throw new Error(`route attempt resolver cannot override ${key}`);
|
|
624
|
+
}
|
|
625
|
+
if (value !== undefined) merged[key] = value;
|
|
626
|
+
}
|
|
627
|
+
return merged;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* @param {Object<string, *>} options
|
|
632
|
+
* @returns {Object<string, *>}
|
|
633
|
+
*/
|
|
634
|
+
function withoutAttemptScopedOptions(options) {
|
|
635
|
+
const projected = { ...options };
|
|
636
|
+
for (const key of ATTEMPT_SCOPED_OPTION_KEYS) delete projected[key];
|
|
637
|
+
return projected;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Explicit mixed-route projection. Capability-bearing inputs (MCP, skills,
|
|
642
|
+
* schema, live input, native subagents) are never removed here; the capability
|
|
643
|
+
* gate either proves support or skips the route before execution.
|
|
644
|
+
* @param {RouterChainEntry} entry
|
|
645
|
+
* @param {Object<string, *>} options
|
|
646
|
+
*/
|
|
647
|
+
function projectPerRouteNativeOptions(entry, options) {
|
|
648
|
+
const projected = { ...options };
|
|
649
|
+
switch (entry.model.sdk) {
|
|
650
|
+
case "pi":
|
|
651
|
+
return projected;
|
|
652
|
+
case "claude":
|
|
653
|
+
delete projected.sandboxPolicy;
|
|
654
|
+
delete projected.sandboxEngine;
|
|
655
|
+
return projected;
|
|
656
|
+
case "codex":
|
|
657
|
+
case "opencode":
|
|
658
|
+
delete projected.sandboxPolicy;
|
|
659
|
+
delete projected.sandboxEngine;
|
|
660
|
+
projected.allowedTools = ["*"];
|
|
661
|
+
projected.disallowedTools = [];
|
|
662
|
+
return projected;
|
|
663
|
+
default:
|
|
664
|
+
throw new Error(`per-route-native safety has no contract for sdk ${entry.model.sdk}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* @param {string} key
|
|
670
|
+
* @param {RouterChainEntry} entry
|
|
671
|
+
* @param {AgentRuntimeHostOptions} host
|
|
672
|
+
* @param {Map<string, AgentRuntimeInstance>} runtimes
|
|
673
|
+
* @param {import('../types.js').AgentRuntimeToolOptions|undefined} configuredTools
|
|
674
|
+
*/
|
|
675
|
+
function createRouteRuntime(key, entry, host, runtimes, configuredTools) {
|
|
676
|
+
const runtime = createRuntime(projectPerRouteNativeHost(entry, host));
|
|
677
|
+
if (configuredTools !== undefined) {
|
|
678
|
+
runtime.configureTools?.(projectPerRouteNativeToolOptions(entry, configuredTools));
|
|
679
|
+
}
|
|
680
|
+
runtimes.set(key, runtime);
|
|
681
|
+
return runtime;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Provider-native routes retain the injected sandbox implementation seam, but
|
|
686
|
+
* must never inherit mono-agent policy data or a concrete srt engine. Assigning
|
|
687
|
+
* explicit `undefined` values (rather than deleting the keys) also clears a
|
|
688
|
+
* previously configured runtime when configureTools is called again.
|
|
689
|
+
* @param {RouterChainEntry} entry
|
|
690
|
+
* @param {import('../types.js').AgentRuntimeToolOptions|undefined} configuredTools
|
|
691
|
+
* @returns {import('../types.js').AgentRuntimeToolOptions}
|
|
692
|
+
*/
|
|
693
|
+
function projectPerRouteNativeToolOptions(entry, configuredTools = {}) {
|
|
694
|
+
const projected = { ...configuredTools };
|
|
695
|
+
if (entry.model.sdk !== "pi") {
|
|
696
|
+
projected.sandboxPolicy = undefined;
|
|
697
|
+
projected.sandboxEngine = undefined;
|
|
698
|
+
}
|
|
699
|
+
return projected;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Host-level policy must be isolated alongside request-level policy. The
|
|
704
|
+
* sandbox implementation itself remains available; only enforcing policy data
|
|
705
|
+
* and its route-specific engine are removed for provider-native bridges.
|
|
706
|
+
* @param {RouterChainEntry} entry
|
|
707
|
+
* @param {AgentRuntimeHostOptions} host
|
|
708
|
+
*/
|
|
709
|
+
function projectPerRouteNativeHost(entry, host) {
|
|
710
|
+
if (entry.model.sdk === "pi") return host;
|
|
711
|
+
const projected = { ...host };
|
|
712
|
+
delete projected.sandboxPolicy;
|
|
713
|
+
delete projected.sandboxEngine;
|
|
714
|
+
return projected;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** @param {RouterChainEntry} entry @param {number} index */
|
|
718
|
+
function routeRuntimeKey(entry, index) {
|
|
719
|
+
return `${index}:${modelKey(entry.model)}:${entry.executionMode ?? "default"}`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** @param {AgentRuntimeInstance} runtime */
|
|
723
|
+
function assertRuntimeLike(runtime) {
|
|
724
|
+
if (runtime === null || typeof runtime !== "object" || typeof runtime.run !== "function") {
|
|
725
|
+
throw new Error("route attempt resolver runtime must expose run()");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** @param {Object<string, *>} options @param {string|null|undefined} effort */
|
|
730
|
+
function applyEntryEffort(options, effort) {
|
|
731
|
+
if (effort === null) {
|
|
732
|
+
delete options.effort;
|
|
733
|
+
} else if (typeof effort === "string") {
|
|
734
|
+
options.effort = effort;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** @param {unknown} error @returns {RuntimeResult} */
|
|
739
|
+
function safetyUnavailableResult(error) {
|
|
740
|
+
// Host resolvers may handle credentials. Never echo their exception text
|
|
741
|
+
// into persisted results or route telemetry.
|
|
742
|
+
void error;
|
|
743
|
+
return {
|
|
744
|
+
text: null,
|
|
745
|
+
error: "The route safety contract could not be established before execution.",
|
|
746
|
+
failureKind: "safety_unavailable",
|
|
747
|
+
events: [],
|
|
748
|
+
cancelled: false,
|
|
749
|
+
usage: {},
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** @param {string|null|undefined} failureKind */
|
|
754
|
+
function isMidTurnSafetyFailure(failureKind) {
|
|
755
|
+
return typeof failureKind === "string"
|
|
756
|
+
&& (failureKind.startsWith("sandbox_") || failureKind.startsWith("safety_"));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* @param {AgentRuntimeInstance} inner
|
|
761
|
+
* @param {Map<string, AgentRuntimeInstance>} routeRuntimes
|
|
762
|
+
*/
|
|
763
|
+
function allRuntimes(inner, routeRuntimes) {
|
|
764
|
+
return [...new Set([inner, ...routeRuntimes.values()])];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Merge progress into one bounded snapshot so prompts never accumulate nested
|
|
769
|
+
* resume blocks across a long provider chain.
|
|
770
|
+
* @param {*} previous
|
|
771
|
+
* @param {*} next
|
|
772
|
+
*/
|
|
773
|
+
function mergeResumeSnapshots(previous, next) {
|
|
774
|
+
if (!next) return previous || null;
|
|
775
|
+
if (!previous) return next;
|
|
776
|
+
const previousTurns = Array.isArray(previous.turns) ? previous.turns : [];
|
|
777
|
+
const nextTurns = Array.isArray(next.turns) ? next.turns : [];
|
|
778
|
+
const allTurns = [...previousTurns, ...nextTurns];
|
|
779
|
+
const turns = allTurns.slice(-3);
|
|
780
|
+
const dropped = allTurns.slice(0, Math.max(0, allTurns.length - turns.length));
|
|
781
|
+
const existingSummaries = [
|
|
782
|
+
...(Array.isArray(previous.earlier_turn_summaries) ? previous.earlier_turn_summaries : []),
|
|
783
|
+
...(Array.isArray(next.earlier_turn_summaries) ? next.earlier_turn_summaries : []),
|
|
784
|
+
].map((entry) => String(entry?.summary ?? "").slice(0, 320)).filter(Boolean);
|
|
785
|
+
const droppedSummaries = dropped.map(summarizeSnapshotTurn);
|
|
786
|
+
const summaries = [...existingSummaries, ...droppedSummaries].slice(-9);
|
|
787
|
+
const turnCount = Math.max(
|
|
788
|
+
turns.length + summaries.length,
|
|
789
|
+
Number(previous.turn_count || 0) + Number(next.turn_count || 0),
|
|
790
|
+
);
|
|
791
|
+
return {
|
|
792
|
+
...next,
|
|
793
|
+
turn_count: turnCount,
|
|
794
|
+
earlier_turn_summaries: summaries.map((summary, index) => ({
|
|
795
|
+
turn_index: Math.max(1, turnCount - turns.length - summaries.length + index + 1),
|
|
796
|
+
summary,
|
|
797
|
+
})),
|
|
798
|
+
turns,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/** @param {*} turn */
|
|
803
|
+
function summarizeSnapshotTurn(turn) {
|
|
804
|
+
const assistant = typeof turn?.assistant_text === "string" ? turn.assistant_text.trim() : "";
|
|
805
|
+
const tools = Array.isArray(turn?.tool_uses)
|
|
806
|
+
? turn.tool_uses.map((tool) => tool?.name).filter(Boolean).slice(0, 5)
|
|
807
|
+
: [];
|
|
808
|
+
const pieces = [];
|
|
809
|
+
if (assistant) pieces.push(assistant.split(/\r?\n/u)[0].slice(0, 220));
|
|
810
|
+
if (tools.length > 0) pieces.push(`tools: ${tools.join(", ")}`);
|
|
811
|
+
return (pieces.join("; ") || "provider attempt made progress").slice(0, 320);
|
|
812
|
+
}
|
|
813
|
+
|
|
253
814
|
/**
|
|
254
815
|
* @param {RouterChainEntry} entry
|
|
255
816
|
* @param {Partial<RuntimeRunOptions>} options
|
|
@@ -263,18 +824,39 @@ function entrySatisfiesRequirements(entry, options) {
|
|
|
263
824
|
// one today) still respects option-implied capability needs. Each option
|
|
264
825
|
// inference defers to an explicit entry pin, never overriding it.
|
|
265
826
|
const effectiveRequires = { ...(requires || null) };
|
|
266
|
-
//
|
|
267
|
-
// entry
|
|
268
|
-
|
|
827
|
+
// Infer required capabilities from request-time options. These requirements
|
|
828
|
+
// override a contradictory entry pin (`requires: false`): the caller's actual
|
|
829
|
+
// request cannot be silently weakened. Empty JSON Schema `{}` still counts.
|
|
830
|
+
if (
|
|
831
|
+
options.outputSchema !== undefined
|
|
832
|
+
&& options.outputSchema !== null
|
|
833
|
+
) {
|
|
269
834
|
effectiveRequires.structured_output = true;
|
|
270
835
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
836
|
+
if (
|
|
837
|
+
options.mcpServers !== undefined
|
|
838
|
+
&& options.mcpServers !== null
|
|
839
|
+
&& Object.keys(options.mcpServers).length > 0
|
|
840
|
+
) {
|
|
841
|
+
effectiveRequires.supports_mcp = true;
|
|
842
|
+
}
|
|
843
|
+
if (
|
|
844
|
+
Array.isArray(options.skills)
|
|
845
|
+
&& options.skills.length > 0
|
|
846
|
+
) {
|
|
847
|
+
effectiveRequires.supports_skills = true;
|
|
848
|
+
}
|
|
849
|
+
if (options.liveInput) {
|
|
850
|
+
effectiveRequires.supports_live_input = true;
|
|
851
|
+
}
|
|
852
|
+
if (options.fastMode === true) {
|
|
853
|
+
effectiveRequires.supports_fast_mode = true;
|
|
854
|
+
}
|
|
855
|
+
// Native teammates likewise require a capable route; Pi/OpenCode must skip
|
|
856
|
+
// rather than silently dropping them.
|
|
274
857
|
if (
|
|
275
858
|
Array.isArray(options.nativeSubagents?.teammates)
|
|
276
859
|
&& options.nativeSubagents.teammates.length > 0
|
|
277
|
-
&& effectiveRequires.supports_native_subagents === undefined
|
|
278
860
|
) {
|
|
279
861
|
effectiveRequires.supports_native_subagents = true;
|
|
280
862
|
}
|
|
@@ -291,6 +873,22 @@ function entrySatisfiesRequirements(entry, options) {
|
|
|
291
873
|
return true;
|
|
292
874
|
}
|
|
293
875
|
|
|
876
|
+
/**
|
|
877
|
+
* Session identifiers belong to the bridge that created them. Never forward
|
|
878
|
+
* one into a bridge that declares no resume support (notably isolated
|
|
879
|
+
* per-run OpenCode), including when that bridge is reached through fallback.
|
|
880
|
+
* Unknown SDKs retain the existing fail-later behavior.
|
|
881
|
+
* @param {RouterChainEntry} entry
|
|
882
|
+
* @returns {boolean}
|
|
883
|
+
*/
|
|
884
|
+
function entrySupportsSessionResume(entry) {
|
|
885
|
+
try {
|
|
886
|
+
return runtimeCapabilities(entry.model).supports_session_resume === true;
|
|
887
|
+
} catch {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
294
892
|
/**
|
|
295
893
|
* @param {Partial<RuntimeRunOptions>} callOptions
|
|
296
894
|
* @param {import('../types.js').RuntimeEvent} event
|