@mono-agent/agent-runtime 0.15.3 → 0.15.4
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 +43 -6
- package/package.json +5 -1
- package/src/agent/tools/agent-tool.js +859 -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/providers/pi-native/stream-subscriber.js +37 -0
- package/src/ai/providers/pi-native/turn-runner.js +60 -5
- package/src/ai/providers/pi-native.js +49 -5
- package/src/ai/runtime/router.js +302 -166
- package/src/ai/types.js +52 -1
- package/src/runtime.js +51 -1
- package/types/agent/tools/agent-tool.d.ts +60 -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/providers/pi-native/turn-runner.d.ts +34 -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 +163 -1
package/src/ai/runtime/router.js
CHANGED
|
@@ -55,11 +55,13 @@ import { instrumentLiveInputAppliedEvents } from "./live-input-events.js";
|
|
|
55
55
|
/**
|
|
56
56
|
* @typedef {Object} RouterChainEntryInput
|
|
57
57
|
* A chain entry as accepted by createRouterRuntime: either the shorthand bare
|
|
58
|
-
* RuntimeModelRef, or the full `{model, executionMode?, effort?, requires
|
|
58
|
+
* RuntimeModelRef, or the full `{model, executionMode?, effort?, requires?,
|
|
59
|
+
* attempts?}` form.
|
|
59
60
|
* @property {RuntimeModelRef} model
|
|
60
61
|
* @property {string} [executionMode]
|
|
61
62
|
* @property {string|null} [effort]
|
|
62
63
|
* @property {Object<string, *>} [requires]
|
|
64
|
+
* @property {number} [attempts]
|
|
63
65
|
*/
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -68,6 +70,13 @@ import { instrumentLiveInputAppliedEvents } from "./live-input-events.js";
|
|
|
68
70
|
* @property {string|null} executionMode
|
|
69
71
|
* @property {string|null|undefined} effort
|
|
70
72
|
* @property {Object<string, *>|null} requires
|
|
73
|
+
* @property {number} attempts Total attempts on this route including the first.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {Object} RouterRetryPolicy
|
|
78
|
+
* @property {number} backoffMs Delay before the first retry; doubles per retry.
|
|
79
|
+
* @property {number} maxBackoffMs Ceiling for the doubled delay.
|
|
71
80
|
*/
|
|
72
81
|
|
|
73
82
|
/**
|
|
@@ -97,13 +106,16 @@ const RESOLVER_PROTECTED_OPTION_KEYS = new Set([
|
|
|
97
106
|
* @param {AgentRuntimeHostOptions} [options.host]
|
|
98
107
|
* @param {ReadonlyArray<RuntimeModelRef|RouterChainEntryInput>} [options.chain]
|
|
99
108
|
* @param {"uniform"|"per-route-native"} [options.routeSafety]
|
|
100
|
-
* @param {(input: {model: RuntimeModelRef, executionMode: string|null, attemptIndex: number, routeSafety: "uniform"|"per-route-native"}) => (RouterAttemptResolution|Promise<RouterAttemptResolution>)} [options.resolveAttempt]
|
|
109
|
+
* @param {(input: {model: RuntimeModelRef, executionMode: string|null, attemptIndex: number, retryIndex: number, routeSafety: "uniform"|"per-route-native"}) => (RouterAttemptResolution|Promise<RouterAttemptResolution>)} [options.resolveAttempt]
|
|
110
|
+
* @param {Partial<RouterRetryPolicy>} [options.retry] Backoff shape for same-model
|
|
111
|
+
* retries. Per-route retry counts live on each chain entry's `attempts`.
|
|
101
112
|
* @returns {AgentRuntimeInstance & {chain: () => Array<RouterChainEntry>}}
|
|
102
113
|
*/
|
|
103
|
-
export function createRouterRuntime({ host = {}, chain = [], routeSafety = "uniform", resolveAttempt } = {}) {
|
|
114
|
+
export function createRouterRuntime({ host = {}, chain = [], routeSafety = "uniform", resolveAttempt, retry } = {}) {
|
|
104
115
|
if (!ROUTE_SAFETY_MODES.has(routeSafety)) {
|
|
105
116
|
throw new Error("createRouterRuntime routeSafety must be uniform or per-route-native");
|
|
106
117
|
}
|
|
118
|
+
const retryPolicy = normalizeRetryPolicy(retry);
|
|
107
119
|
const entries = normaliseChain(chain);
|
|
108
120
|
if (entries.length === 0) {
|
|
109
121
|
throw new Error("createRouterRuntime requires a non-empty chain");
|
|
@@ -160,7 +172,7 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
|
|
|
160
172
|
for (let i = 0; i < entries.length; i += 1) {
|
|
161
173
|
const entry = entries[i];
|
|
162
174
|
const effectiveToolOptions = effectiveRouterToolOptions(host, configuredTools);
|
|
163
|
-
|
|
175
|
+
const entrySafetyContract = routeSafetyContract(
|
|
164
176
|
routeSafety,
|
|
165
177
|
entry,
|
|
166
178
|
effectivePiSandboxPolicy(effectiveToolOptions, options),
|
|
@@ -179,16 +191,21 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
|
|
|
179
191
|
failureKind: "skipped_capability_mismatch",
|
|
180
192
|
requirements: entry.requires,
|
|
181
193
|
routeSafety,
|
|
182
|
-
safetyContract,
|
|
194
|
+
safetyContract: entrySafetyContract,
|
|
183
195
|
});
|
|
184
|
-
const skippedRecord = routeSafetyRecord(i, entry,
|
|
196
|
+
const skippedRecord = routeSafetyRecord(i, entry, entrySafetyContract, "skipped_capability_mismatch");
|
|
185
197
|
routeSafetyHistory.push(skippedRecord);
|
|
186
198
|
emit(options, { type: "provider_route_safety", ...skippedRecord });
|
|
187
199
|
continue;
|
|
188
200
|
}
|
|
189
201
|
|
|
202
|
+
// Attempt-scoped stripping is a property of the ROUTE (chain index), not
|
|
203
|
+
// of one attempt, so it is decided once here. Every same-model retry
|
|
204
|
+
// derives a fresh mutable callOptions from this immutable base, because
|
|
205
|
+
// the per-attempt bag is mutated in place (effort, session deletes,
|
|
206
|
+
// snapshot seed) and reassigned by mergeAttemptOptions.
|
|
190
207
|
/** @type {*} */
|
|
191
|
-
|
|
208
|
+
const entryOptionsBase = {
|
|
192
209
|
...options,
|
|
193
210
|
model: entry.model,
|
|
194
211
|
executionMode: entry.executionMode || options.executionMode,
|
|
@@ -197,185 +214,248 @@ export function createRouterRuntime({ host = {}, chain = [], routeSafety = "unif
|
|
|
197
214
|
// route. Without a route resolver there is no authoritative metadata
|
|
198
215
|
// for a different fallback, so never let the primary's credentials or
|
|
199
216
|
// model capabilities contaminate later attempts.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
217
|
+
const entryCallBase = resolveAttempt === undefined && i > 0
|
|
218
|
+
? withoutAttemptScopedOptions(entryOptionsBase)
|
|
219
|
+
: entryOptionsBase;
|
|
220
|
+
|
|
221
|
+
/** @type {RuntimeResult|null} A failure that ends the whole logical run. */
|
|
222
|
+
let terminalResult = null;
|
|
223
|
+
|
|
224
|
+
for (let retryIndex = 0; retryIndex < entry.attempts; retryIndex += 1) {
|
|
225
|
+
let safetyContract = entrySafetyContract;
|
|
226
|
+
/** @type {*} */
|
|
227
|
+
let callOptions = { ...entryCallBase };
|
|
228
|
+
/** @type {AgentRuntimeInstance} */
|
|
229
|
+
let attemptRuntime = inner;
|
|
230
|
+
/** @type {(() => (void|Promise<void>))|undefined} */
|
|
231
|
+
let attemptCleanup;
|
|
232
|
+
try {
|
|
233
|
+
const resolved = resolveAttempt === undefined
|
|
234
|
+
? undefined
|
|
235
|
+
: await resolveAttempt({
|
|
236
|
+
model: entry.model,
|
|
237
|
+
executionMode: entry.executionMode,
|
|
238
|
+
attemptIndex: i,
|
|
239
|
+
retryIndex,
|
|
240
|
+
routeSafety,
|
|
241
|
+
});
|
|
242
|
+
const resolution = normalizeAttemptResolution(resolved);
|
|
243
|
+
attemptCleanup = resolution?.cleanup;
|
|
244
|
+
if (resolveAttempt !== undefined) {
|
|
245
|
+
callOptions = mergeAttemptOptions(callOptions, resolution?.options);
|
|
246
|
+
}
|
|
247
|
+
if (routeSafety === "per-route-native") {
|
|
248
|
+
callOptions = projectPerRouteNativeOptions(entry, callOptions);
|
|
249
|
+
const key = routeRuntimeKey(entry, i);
|
|
250
|
+
const resolvedRuntime = resolution?.runtime;
|
|
251
|
+
if (resolvedRuntime !== undefined) {
|
|
252
|
+
assertRuntimeLike(resolvedRuntime);
|
|
253
|
+
const previousRuntime = routeRuntimes.get(key);
|
|
254
|
+
if (previousRuntime !== undefined && previousRuntime !== resolvedRuntime) {
|
|
255
|
+
try { await previousRuntime.disposeAllSessions?.(); } catch { /* best-effort replacement */ }
|
|
256
|
+
}
|
|
257
|
+
routeRuntimes.set(key, resolvedRuntime);
|
|
258
|
+
if (entry.model.sdk !== "pi") {
|
|
259
|
+
resolvedRuntime.configureTools?.(projectPerRouteNativeToolOptions(entry, configuredTools));
|
|
260
|
+
}
|
|
230
261
|
}
|
|
231
|
-
routeRuntimes.
|
|
232
|
-
if (entry.model.sdk
|
|
233
|
-
|
|
262
|
+
attemptRuntime = routeRuntimes.get(key) ?? createRouteRuntime(key, entry, host, routeRuntimes, configuredTools);
|
|
263
|
+
if (entry.model.sdk === "pi") {
|
|
264
|
+
projectPiRuntimeToolContext(attemptRuntime, effectiveToolOptions);
|
|
265
|
+
// Derive the attestation from the same complete base context and
|
|
266
|
+
// request-scoped inputs that the supplied/runtime-owned Pi
|
|
267
|
+
// bridge will actually receive. Resolver options cannot alter
|
|
268
|
+
// these protected fields.
|
|
269
|
+
safetyContract = routeSafetyContract(
|
|
270
|
+
routeSafety,
|
|
271
|
+
entry,
|
|
272
|
+
effectivePiSandboxPolicy(effectiveToolOptions, callOptions),
|
|
273
|
+
);
|
|
234
274
|
}
|
|
275
|
+
} else if (resolution?.runtime !== undefined && resolution.runtime !== inner) {
|
|
276
|
+
throw new Error("uniform route safety cannot replace the shared monotonic runtime");
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
try { await attemptCleanup?.(); } catch { /* cleanup is additive */ }
|
|
280
|
+
const failure = safetyUnavailableResult(error);
|
|
281
|
+
lastRouteSkip = failure;
|
|
282
|
+
failoverHistory.push({
|
|
283
|
+
model: entry.model,
|
|
284
|
+
failureKind: "safety_unavailable",
|
|
285
|
+
routeSafety,
|
|
286
|
+
safetyContract,
|
|
287
|
+
});
|
|
288
|
+
const unavailableRecord = routeSafetyRecord(i, entry, safetyContract, "safety_unavailable");
|
|
289
|
+
routeSafetyHistory.push(unavailableRecord);
|
|
290
|
+
emit(callOptions, { type: "provider_route_safety", ...unavailableRecord });
|
|
291
|
+
// A resolver fault is a config/credential problem, not a transient
|
|
292
|
+
// provider blip: retrying the same route cannot fix it. Advance.
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
applyEntryEffort(callOptions, entry.effort);
|
|
297
|
+
// A provider session belongs to the route AND to the attempt that
|
|
298
|
+
// created it. The entire chain is stateless whenever a fallback exists,
|
|
299
|
+
// keeping the full logical run replayable regardless of which route is
|
|
300
|
+
// attempted. A same-model retry re-sends the whole logical turn, so
|
|
301
|
+
// resuming the session the failed attempt already appended into would
|
|
302
|
+
// duplicate the turn or hit session_busy.
|
|
303
|
+
if (entries.length > 1 || i > 0 || retryIndex > 0 || !entrySupportsSessionResume(entry)) {
|
|
304
|
+
delete callOptions.sessionId;
|
|
305
|
+
delete callOptions.providerSessionId;
|
|
306
|
+
delete callOptions.sessionKeepAlive;
|
|
307
|
+
delete callOptions.sessionIdleTimeoutMs;
|
|
308
|
+
}
|
|
309
|
+
let attemptSystemPrompt = promptBase;
|
|
310
|
+
if (pendingSnapshot) {
|
|
311
|
+
callOptions.diagnosticsSeed = {
|
|
312
|
+
...(callOptions.diagnosticsSeed || {}),
|
|
313
|
+
resume_snapshot: pendingSnapshot,
|
|
314
|
+
};
|
|
315
|
+
// Also prepend the rendered snapshot to the system prompt so SDK
|
|
316
|
+
// backends that don't read diagnosticsSeed still continue from the
|
|
317
|
+
const rendered = renderResumeSnapshot(pendingSnapshot);
|
|
318
|
+
if (rendered) {
|
|
319
|
+
callOptions.systemPromptPrefix = rendered;
|
|
320
|
+
attemptSystemPrompt = `${rendered}\n\n${promptBase}`;
|
|
235
321
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// The safety contract is a property of the route: resolver options can
|
|
325
|
+
// never reach the sandbox/tool policy (RESOLVER_PROTECTED_OPTION_KEYS)
|
|
326
|
+
// and effectivePiSandboxPolicy reads only protected fields, so every
|
|
327
|
+
// retry of one entry derives an identical contract. Record it once so
|
|
328
|
+
// the bounded safety telemetry stays one record per chain entry.
|
|
329
|
+
if (retryIndex === 0) {
|
|
330
|
+
const safetyRecord = routeSafetyRecord(i, entry, safetyContract, "attempted");
|
|
331
|
+
routeSafetyHistory.push(safetyRecord);
|
|
332
|
+
emit(callOptions, { type: "provider_route_safety", ...safetyRecord });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// A same-model retry is not a failover: only the first attempt of a new
|
|
336
|
+
// route announces a transition.
|
|
337
|
+
if (retryIndex === 0 && failoverHistory.length > 0) {
|
|
338
|
+
emit(callOptions, {
|
|
339
|
+
type: "provider_failover_started",
|
|
340
|
+
from: modelKey(failoverHistory[failoverHistory.length - 1]?.model),
|
|
341
|
+
to: modelKey(entry.model),
|
|
342
|
+
attemptIndex: i,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let result;
|
|
347
|
+
try {
|
|
348
|
+
result = await attemptRuntime.run(attemptSystemPrompt, callOptions);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
// The inner runtime usually surfaces errors as structured result
|
|
351
|
+
// fields, but a bridge can still throw synchronously (e.g. spawn
|
|
352
|
+
// failures). Convert to a result-like shape so the chain logic
|
|
353
|
+
// is uniform.
|
|
354
|
+
result = {
|
|
355
|
+
text: null,
|
|
356
|
+
error: err?.message || String(err),
|
|
357
|
+
failureKind: "provider_unavailable",
|
|
358
|
+
events: [],
|
|
359
|
+
cancelled: false,
|
|
360
|
+
usage: {},
|
|
361
|
+
};
|
|
362
|
+
} finally {
|
|
363
|
+
try { await attemptCleanup?.(); } catch { /* cleanup is additive */ }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
result = normalizeProviderAuthFailure(result);
|
|
367
|
+
|
|
368
|
+
const retryability = retryableProviderFailureInfo({
|
|
369
|
+
errorText: result.error || "",
|
|
370
|
+
stderrTail: result.stderrTail || "",
|
|
371
|
+
failureKind: result.failureKind,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const successful = !result.error && !result.failureKind && !result.cancelled;
|
|
375
|
+
if (successful) {
|
|
376
|
+
// Only a genuine route change is a completed failover: succeeding
|
|
377
|
+
// after a same-model retry must not render as "answered by X
|
|
378
|
+
// (failover)" when X is still the route the operator asked for.
|
|
379
|
+
if (failoverHistory.some((attempt) => modelKey(attempt.model) !== modelKey(entry.model))) {
|
|
380
|
+
emit(callOptions, {
|
|
381
|
+
type: "provider_failover_completed",
|
|
382
|
+
attemptIndex: i,
|
|
383
|
+
model: entry.model,
|
|
384
|
+
});
|
|
248
385
|
}
|
|
249
|
-
|
|
250
|
-
throw new Error("uniform route safety cannot replace the shared monotonic runtime");
|
|
386
|
+
return { ...result, failoverHistory, routeSafetyHistory };
|
|
251
387
|
}
|
|
252
|
-
|
|
253
|
-
try { await attemptCleanup?.(); } catch { /* cleanup is additive */ }
|
|
254
|
-
const failure = safetyUnavailableResult(error);
|
|
255
|
-
lastRouteSkip = failure;
|
|
388
|
+
|
|
256
389
|
failoverHistory.push({
|
|
257
390
|
model: entry.model,
|
|
258
|
-
failureKind:
|
|
391
|
+
failureKind: result.failureKind || null,
|
|
392
|
+
requestId: retryability.requestId,
|
|
393
|
+
retryableSubkind: retryability.subkind,
|
|
394
|
+
...(retryIndex > 0 ? { retryIndex } : {}),
|
|
259
395
|
routeSafety,
|
|
260
396
|
safetyContract,
|
|
261
397
|
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
398
|
+
if (result.failureKind === "skipped_capability_mismatch") {
|
|
399
|
+
lastRouteSkip = result;
|
|
400
|
+
// A bridge-level mismatch is about this route, not the logical run.
|
|
401
|
+
// Try the next entry and do not derive a transcript snapshot from it.
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
lastResult = result;
|
|
267
405
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
let attemptSystemPrompt = promptBase;
|
|
279
|
-
if (pendingSnapshot) {
|
|
280
|
-
callOptions.diagnosticsSeed = {
|
|
281
|
-
...(callOptions.diagnosticsSeed || {}),
|
|
282
|
-
resume_snapshot: pendingSnapshot,
|
|
283
|
-
};
|
|
284
|
-
// Also prepend the rendered snapshot to the system prompt so SDK
|
|
285
|
-
// backends that don't read diagnosticsSeed still continue from the
|
|
286
|
-
const rendered = renderResumeSnapshot(pendingSnapshot);
|
|
287
|
-
if (rendered) {
|
|
288
|
-
callOptions.systemPromptPrefix = rendered;
|
|
289
|
-
attemptSystemPrompt = `${rendered}\n\n${promptBase}`;
|
|
406
|
+
// Provider auth is terminal for one provider, but chain-retryable: a
|
|
407
|
+
// fallback provider may have working credentials. Other non-retryable
|
|
408
|
+
// provider/request errors remain terminal.
|
|
409
|
+
const shouldFallback = (retryability.retryable || result.failureKind === "provider_auth")
|
|
410
|
+
&& !result.cancelled
|
|
411
|
+
&& !isMidTurnSafetyFailure(result.failureKind);
|
|
412
|
+
if (!shouldFallback) {
|
|
413
|
+
terminalResult = result;
|
|
414
|
+
break;
|
|
290
415
|
}
|
|
291
|
-
}
|
|
292
416
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
417
|
+
// Build a transcript-tail snapshot from this run's events so the next
|
|
418
|
+
// attempt — same model or next route — can continue. A run that
|
|
419
|
+
// produced no usable events yields a falsy snapshot and merges to a
|
|
420
|
+
// no-op, so the common "died before the first token" retry costs
|
|
421
|
+
// nothing. Keep one bounded snapshot object across the logical run
|
|
422
|
+
// instead of nesting a new <resume_context> block per transition.
|
|
423
|
+
pendingSnapshot = mergeResumeSnapshots(
|
|
424
|
+
pendingSnapshot,
|
|
425
|
+
buildTranscriptTailSnapshot(result.events, { runtimeBrand }),
|
|
426
|
+
);
|
|
296
427
|
|
|
297
|
-
|
|
428
|
+
// context_limit is forced retryable so the chain can reach a model with
|
|
429
|
+
// a bigger window, but it is deterministic against the SAME window:
|
|
430
|
+
// another attempt here is a guaranteed second failure. Advance instead.
|
|
431
|
+
const sameModelRetryable = retryability.retryable
|
|
432
|
+
&& retryability.subkind !== "context_limit"
|
|
433
|
+
&& retryIndex + 1 < entry.attempts;
|
|
434
|
+
if (!sameModelRetryable) break;
|
|
435
|
+
|
|
436
|
+
const backoffMs = Math.min(retryPolicy.maxBackoffMs, retryPolicy.backoffMs * (2 ** retryIndex));
|
|
298
437
|
emit(callOptions, {
|
|
299
|
-
type: "
|
|
300
|
-
|
|
301
|
-
to: entry.model,
|
|
438
|
+
type: "provider_retry_started",
|
|
439
|
+
model: modelKey(entry.model),
|
|
302
440
|
attemptIndex: i,
|
|
441
|
+
retryIndex: retryIndex + 1,
|
|
442
|
+
attempts: entry.attempts,
|
|
443
|
+
delayMs: backoffMs,
|
|
444
|
+
reason: retryability.subkind || result.failureKind || null,
|
|
303
445
|
});
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// The inner runtime usually surfaces errors as structured result
|
|
311
|
-
// fields, but a bridge can still throw synchronously (e.g. spawn
|
|
312
|
-
// failures). Convert to a result-like shape so the chain logic
|
|
313
|
-
// is uniform.
|
|
314
|
-
result = {
|
|
315
|
-
text: null,
|
|
316
|
-
error: err?.message || String(err),
|
|
317
|
-
failureKind: "provider_unavailable",
|
|
318
|
-
events: [],
|
|
319
|
-
cancelled: false,
|
|
320
|
-
usage: {},
|
|
321
|
-
};
|
|
322
|
-
} finally {
|
|
323
|
-
try { await attemptCleanup?.(); } catch { /* cleanup is additive */ }
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
result = normalizeProviderAuthFailure(result);
|
|
327
|
-
|
|
328
|
-
const retryability = retryableProviderFailureInfo({
|
|
329
|
-
errorText: result.error || "",
|
|
330
|
-
stderrTail: result.stderrTail || "",
|
|
331
|
-
failureKind: result.failureKind,
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
const successful = !result.error && !result.failureKind && !result.cancelled;
|
|
335
|
-
if (successful) {
|
|
336
|
-
if (failoverHistory.length > 0) {
|
|
337
|
-
emit(callOptions, {
|
|
338
|
-
type: "provider_failover_completed",
|
|
339
|
-
attemptIndex: i,
|
|
340
|
-
model: entry.model,
|
|
341
|
-
});
|
|
446
|
+
if (callOptions.abortSignal?.aborted) {
|
|
447
|
+
return { ...result, cancelled: true, failoverHistory, routeSafetyHistory };
|
|
448
|
+
}
|
|
449
|
+
await delay(backoffMs, callOptions.abortSignal);
|
|
450
|
+
if (callOptions.abortSignal?.aborted) {
|
|
451
|
+
return { ...result, cancelled: true, failoverHistory, routeSafetyHistory };
|
|
342
452
|
}
|
|
343
|
-
return { ...result, failoverHistory, routeSafetyHistory };
|
|
344
453
|
}
|
|
345
454
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
failureKind: result.failureKind || null,
|
|
349
|
-
requestId: retryability.requestId,
|
|
350
|
-
retryableSubkind: retryability.subkind,
|
|
351
|
-
routeSafety,
|
|
352
|
-
safetyContract,
|
|
353
|
-
});
|
|
354
|
-
if (result.failureKind === "skipped_capability_mismatch") {
|
|
355
|
-
lastRouteSkip = result;
|
|
356
|
-
// A bridge-level mismatch is about this route, not the logical run.
|
|
357
|
-
// Try the next entry and do not derive a transcript snapshot from it.
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
lastResult = result;
|
|
361
|
-
|
|
362
|
-
// Provider auth is terminal for one provider, but chain-retryable: a
|
|
363
|
-
// fallback provider may have working credentials. Other non-retryable
|
|
364
|
-
// provider/request errors remain terminal.
|
|
365
|
-
const shouldFallback = (retryability.retryable || result.failureKind === "provider_auth")
|
|
366
|
-
&& !result.cancelled
|
|
367
|
-
&& !isMidTurnSafetyFailure(result.failureKind);
|
|
368
|
-
if (!shouldFallback) {
|
|
369
|
-
return { ...result, failoverHistory, routeSafetyHistory };
|
|
455
|
+
if (terminalResult !== null) {
|
|
456
|
+
return { ...terminalResult, failoverHistory, routeSafetyHistory };
|
|
370
457
|
}
|
|
371
|
-
|
|
372
|
-
// Build a transcript-tail snapshot from this run's events so the
|
|
373
|
-
// next provider can continue. If the run produced no usable events,
|
|
374
|
-
// skip the snapshot (the next attempt starts fresh).
|
|
375
|
-
const snapshot = buildTranscriptTailSnapshot(result.events, { runtimeBrand });
|
|
376
|
-
// Keep one bounded snapshot object across the logical run instead of
|
|
377
|
-
// nesting a new <resume_context> block on every provider transition.
|
|
378
|
-
pendingSnapshot = mergeResumeSnapshots(pendingSnapshot, snapshot);
|
|
458
|
+
// Every other inner break falls through to the next chain entry.
|
|
379
459
|
}
|
|
380
460
|
|
|
381
461
|
const exhaustedResult = lastResult || lastRouteSkip || {
|
|
@@ -467,7 +547,7 @@ function normaliseChain(chain) {
|
|
|
467
547
|
if (!entry) return null;
|
|
468
548
|
if (entry.sdk && entry.model) {
|
|
469
549
|
// ModelRef shorthand: { sdk, model, ... }
|
|
470
|
-
return { model: entry, executionMode: null, effort: undefined, requires: null };
|
|
550
|
+
return { model: entry, executionMode: null, effort: undefined, requires: null, attempts: 1 };
|
|
471
551
|
}
|
|
472
552
|
if (entry.model) {
|
|
473
553
|
return {
|
|
@@ -475,6 +555,7 @@ function normaliseChain(chain) {
|
|
|
475
555
|
executionMode: typeof entry.executionMode === "string" ? entry.executionMode : null,
|
|
476
556
|
effort: normalizeChainEffort(entry.effort),
|
|
477
557
|
requires: entry.requires && typeof entry.requires === "object" ? entry.requires : null,
|
|
558
|
+
attempts: normalizeChainAttempts(entry.attempts),
|
|
478
559
|
};
|
|
479
560
|
}
|
|
480
561
|
return null;
|
|
@@ -482,6 +563,61 @@ function normaliseChain(chain) {
|
|
|
482
563
|
.filter(Boolean));
|
|
483
564
|
}
|
|
484
565
|
|
|
566
|
+
/**
|
|
567
|
+
* The kernel default is ONE attempt per entry. Enabling same-model retries is a
|
|
568
|
+
* host policy decision (`@mono-agent/config` supplies the product default), so
|
|
569
|
+
* the router stays mechanism and existing callers keep single-shot behavior.
|
|
570
|
+
* @param {*} attempts
|
|
571
|
+
* @returns {number}
|
|
572
|
+
*/
|
|
573
|
+
function normalizeChainAttempts(attempts) {
|
|
574
|
+
if (attempts === undefined || attempts === null) return 1;
|
|
575
|
+
if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10) {
|
|
576
|
+
throw new Error("createRouterRuntime chain attempts must be an integer between 1 and 10");
|
|
577
|
+
}
|
|
578
|
+
return attempts;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @param {Partial<RouterRetryPolicy>|undefined} retry
|
|
583
|
+
* @returns {RouterRetryPolicy}
|
|
584
|
+
*/
|
|
585
|
+
function normalizeRetryPolicy(retry) {
|
|
586
|
+
const backoffMs = normalizeRetryDelay(retry?.backoffMs, 1000, "backoffMs");
|
|
587
|
+
const maxBackoffMs = normalizeRetryDelay(retry?.maxBackoffMs, 15000, "maxBackoffMs");
|
|
588
|
+
return { backoffMs, maxBackoffMs };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** @param {*} value @param {number} fallback @param {string} name @returns {number} */
|
|
592
|
+
function normalizeRetryDelay(value, fallback, name) {
|
|
593
|
+
if (value === undefined || value === null) return fallback;
|
|
594
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
595
|
+
throw new Error(`createRouterRuntime retry.${name} must be a non-negative finite number`);
|
|
596
|
+
}
|
|
597
|
+
return value;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Abortable sleep. agent-runtime is the kernel and cannot reach the app-layer
|
|
602
|
+
* backoff helpers, so this mirrors the local `delay` in the codex bridge.
|
|
603
|
+
* @param {number} ms
|
|
604
|
+
* @param {AbortSignal} [signal]
|
|
605
|
+
* @returns {Promise<void>}
|
|
606
|
+
*/
|
|
607
|
+
function delay(ms, signal) {
|
|
608
|
+
if (!(ms > 0)) return Promise.resolve();
|
|
609
|
+
return new Promise((resolve) => {
|
|
610
|
+
const done = () => {
|
|
611
|
+
clearTimeout(timer);
|
|
612
|
+
signal?.removeEventListener("abort", done);
|
|
613
|
+
resolve();
|
|
614
|
+
};
|
|
615
|
+
const timer = setTimeout(done, ms);
|
|
616
|
+
/** @type {*} */ (timer).unref?.();
|
|
617
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
485
621
|
/** @param {*} effort @returns {string|null|undefined} */
|
|
486
622
|
function normalizeChainEffort(effort) {
|
|
487
623
|
if (effort === undefined || effort === null) return effort;
|
package/src/ai/types.js
CHANGED
|
@@ -161,8 +161,13 @@
|
|
|
161
161
|
* @property {RuntimeToolLimits} [toolLimits] Typed per-run tool-output limits (supported replacement for the deprecated `settings` tool keys).
|
|
162
162
|
* @property {RuntimeCompactionPolicy} [compaction] Typed per-run compaction policy (supported replacement for the deprecated `settings` compaction keys).
|
|
163
163
|
* @property {RuntimePromptOverrides} [prompts] Per-run prompt-fragment overrides (run wins over the host default).
|
|
164
|
+
* @property {{backend?: "auto"|"searxng"|"keyless", endpoint?: string}} [webSearchConfig] Run-scoped WebSearch backend configuration.
|
|
165
|
+
* @property {{render?: "never"|"auto", browserCommand?: string}} [webFetchConfig] Run-scoped WebFetch extraction/render configuration.
|
|
166
|
+
* @property {"sequential"|"safe-parallel"} [piToolExecutionMode] Pi built-in tool scheduling mode. Safe parallelism is the default.
|
|
167
|
+
* @property {"one-at-a-time"|"all"} [piToolParallelismMode] DEPRECATED. Compatibility alias mapped to piToolExecutionMode.
|
|
164
168
|
* @property {Object} [settings] DEPRECATED. Legacy flat settings bag; consumed only as a per-group FALLBACK when the corresponding typed object (`toolLimits` / `compaction`) is absent. Consuming any key emits one `deprecated_settings_option` runtime_warning per run. Migrate via resolveRuntimePolicies (@mono-agent/runtime-adapter).
|
|
165
169
|
* @property {Object} [nativeSubagents] Same-runtime teammate helpers exposed through native provider subagent surfaces.
|
|
170
|
+
* @property {RuntimeSubagentsOptions} [subagents] In-process `Agent` built-in: profiles, caps, and the nested-run callback.
|
|
166
171
|
* @property {Object} [diagnosticsSeed] Set by createRouterRuntime (ai/runtime/router.js) with a `resume_snapshot` when
|
|
167
172
|
* failing over mid-chain; a host-level coordinator may relay it forward (see agent/transcript.js), not read by any
|
|
168
173
|
* bridge in this package today.
|
|
@@ -185,6 +190,52 @@
|
|
|
185
190
|
* hub's emit). `systemPrompt` is passed positionally, not folded into this object.
|
|
186
191
|
*/
|
|
187
192
|
|
|
193
|
+
/**
|
|
194
|
+
* @typedef {Object} RuntimeSubagentDefinition
|
|
195
|
+
* One named subagent profile the `Agent` built-in can deploy.
|
|
196
|
+
* @property {string} name Model-visible identifier and the tool's `name` enum value.
|
|
197
|
+
* @property {string} description Model-visible: when to pick this profile.
|
|
198
|
+
* @property {string} systemPrompt Full system prompt for the child run.
|
|
199
|
+
* @property {RuntimeModelRef} [model] Absent inherits the parent's configured route.
|
|
200
|
+
* @property {string} [effort]
|
|
201
|
+
* @property {ReadonlyArray<string>} [allowedTools] Absent uses the safe read-only default set.
|
|
202
|
+
* @property {ReadonlyArray<string>} [disallowedTools]
|
|
203
|
+
* @property {Object<string, Object>} [mcpServers]
|
|
204
|
+
* @property {number} [maxTurns]
|
|
205
|
+
* @property {number} [timeoutMs]
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @callback RuntimeSubagentRun
|
|
210
|
+
* Owning-layer callback that actually executes one child turn. The kernel
|
|
211
|
+
* supplies a self-run fallback so `createRuntime` works without host wiring;
|
|
212
|
+
* agent-app replaces it so subagent runs get the configured fallback chain,
|
|
213
|
+
* same-model retries, and run recording.
|
|
214
|
+
* @param {Object} request
|
|
215
|
+
* @returns {Promise<RuntimeResult>}
|
|
216
|
+
*/
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @typedef {Object} RuntimeInlineSubagentsOptions
|
|
220
|
+
* Policy for subagents the model authors at call time rather than picking from
|
|
221
|
+
* `definitions`. Absent suppresses authoring entirely.
|
|
222
|
+
* @property {boolean} [enabled] Only `false` turns authoring off.
|
|
223
|
+
* @property {ReadonlyArray<string>} [allowedTools] Ceiling on what an authored subagent may
|
|
224
|
+
* request. Absent means the safe read-only default set, never every built-in.
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @typedef {Object} RuntimeSubagentsOptions
|
|
229
|
+
* @property {ReadonlyArray<RuntimeSubagentDefinition>} [definitions] Named profiles.
|
|
230
|
+
* @property {RuntimeInlineSubagentsOptions} [inline] Call-time authoring policy.
|
|
231
|
+
* @property {number} [maxConcurrent] In-flight subagents per parent turn. Default 5.
|
|
232
|
+
* @property {number} [maxPerTurn] Total Agent calls per parent turn. Default 20.
|
|
233
|
+
* @property {number} [maxTurns] Default per-subagent turn cap. Default 100.
|
|
234
|
+
* @property {number} [timeoutMs] Default per-subagent wall clock.
|
|
235
|
+
* @property {RuntimeSubagentRun} [run] Nested-run callback; absent uses the kernel self-run.
|
|
236
|
+
* @property {number} [depth] Kernel-owned. Absent/0 is the parent; >=1 suppresses the `Agent` tool.
|
|
237
|
+
*/
|
|
238
|
+
|
|
188
239
|
/**
|
|
189
240
|
* @typedef {Object} RuntimeResult
|
|
190
241
|
* @property {string|null} [text]
|
|
@@ -206,7 +257,7 @@
|
|
|
206
257
|
* @property {Array<Object>} [runtimeWarnings]
|
|
207
258
|
* @property {Object} [diagnostics]
|
|
208
259
|
* @property {Object} [capabilitiesUsed]
|
|
209
|
-
* @property {Array<{model: RuntimeModelRef, failureKind: (string|null), requestId?: (string|null), retryableSubkind?: (string|null), requirements?: Object, routeSafety?: RuntimeRouteSafetyMode, safetyContract?: RuntimeRouteSafetyContract}>} [failoverHistory] Set by createRouterRuntime (ai/runtime/router.js) on every failed/skipped attempt.
|
|
260
|
+
* @property {Array<{model: RuntimeModelRef, failureKind: (string|null), requestId?: (string|null), retryableSubkind?: (string|null), retryIndex?: number, requirements?: Object, routeSafety?: RuntimeRouteSafetyMode, safetyContract?: RuntimeRouteSafetyContract}>} [failoverHistory] Set by createRouterRuntime (ai/runtime/router.js) on every failed/skipped attempt.
|
|
210
261
|
* @property {Array<{attemptIndex: number, model: RuntimeModelRef, routeSafety: RuntimeRouteSafetyMode, safetyContract: RuntimeRouteSafetyContract, status: string}>} [routeSafetyHistory] Bounded route-safety audit emitted by createRouterRuntime.
|
|
211
262
|
*/
|
|
212
263
|
|