ai-runtime-engine 3.0.0 → 3.0.1
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/CHANGELOG.md +63 -0
- package/dist/cli/cli.js +8 -1
- package/dist/cli/commands/cleanup.js +11 -3
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/run.js +6 -0
- package/dist/cli/commands/skills.js +9 -2
- package/dist/cli/interactive/repl.js +12 -2
- package/dist/cli/interactive/session.d.ts +2 -0
- package/dist/cli/interactive/session.js +6 -2
- package/dist/config/schema.js +19 -1
- package/dist/conversations/conversations.d.ts +6 -1
- package/dist/conversations/conversations.js +15 -8
- package/dist/core/fallback/fallback.d.ts +7 -0
- package/dist/core/fallback/fallback.js +15 -2
- package/dist/core/health/monitor.d.ts +6 -0
- package/dist/core/health/monitor.js +15 -2
- package/dist/core/router/confidence.js +10 -5
- package/dist/core/router/dimensions.d.ts +3 -1
- package/dist/core/router/dimensions.js +15 -5
- package/dist/core/router/filter.js +25 -6
- package/dist/core/router/normalize.js +2 -0
- package/dist/core/router/router.js +16 -2
- package/dist/core/router/scorer.d.ts +3 -0
- package/dist/core/router/scorer.js +17 -2
- package/dist/discovery/openapi.js +3 -2
- package/dist/executions/agentTasks.d.ts +4 -4
- package/dist/generation/generateAdapter.js +3 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/mcp/protocol.js +4 -1
- package/dist/memory/bm25.d.ts +7 -0
- package/dist/memory/bm25.js +17 -1
- package/dist/memory/memory.d.ts +7 -1
- package/dist/memory/memory.js +18 -4
- package/dist/plugin/ai.d.ts +6 -0
- package/dist/plugin/ai.js +17 -2
- package/dist/providers/estimate.d.ts +25 -0
- package/dist/providers/estimate.js +55 -0
- package/dist/providers/factory.d.ts +3 -0
- package/dist/providers/factory.js +26 -5
- package/dist/providers/httpClient.js +4 -0
- package/dist/providers/httpProvider.js +4 -3
- package/dist/providers/mock/mockProvider.js +4 -3
- package/dist/runtime/config.d.ts +4 -3
- package/dist/runtime/config.js +13 -22
- package/dist/runtime/events.d.ts +6 -0
- package/dist/runtime/runtime.d.ts +3 -2
- package/dist/runtime/runtime.js +17 -7
- package/dist/runtime/types.d.ts +2 -1
- package/dist/store/area.d.ts +1 -1
- package/dist/store/area.js +34 -10
- package/dist/store/crypto.d.ts +27 -13
- package/dist/store/crypto.js +101 -23
- package/dist/store/errors.d.ts +11 -0
- package/dist/store/errors.js +14 -0
- package/dist/store/store.d.ts +21 -1
- package/dist/store/store.js +74 -19
- package/dist/telemetry/sinks/file.js +4 -2
- package/dist/telemetry/sinks/otlp.d.ts +12 -2
- package/dist/telemetry/sinks/otlp.js +39 -24
- package/dist/telemetry/telemetry.d.ts +5 -0
- package/dist/telemetry/telemetry.js +4 -0
- package/dist/tools/builtins/shell.d.ts +30 -3
- package/dist/tools/builtins/shell.js +218 -7
- package/dist/tools/untrusted.d.ts +1 -1
- package/dist/tools/untrusted.js +5 -3
- package/dist/types.d.ts +14 -0
- package/dist/verification/verify.js +10 -3
- package/docs/GUIDE.md +66 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +5 -1
- package/docs/router.md +1 -1
- package/docs/security.md +26 -7
- package/package.json +4 -2
|
@@ -13,10 +13,25 @@ function preferenceValue(candidate, options) {
|
|
|
13
13
|
return options.preferences?.[candidate.providerId];
|
|
14
14
|
}
|
|
15
15
|
export function scoreCandidates(candidates, task, baseWeights, strategy, options = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// Resolve strategy-adjusted weights per PROVIDER: a provider with a weightOverrides block scores its
|
|
17
|
+
// candidates under (base ⊕ override); everyone else shares the base. Memoized so it is computed once
|
|
18
|
+
// per provider, not once per candidate.
|
|
19
|
+
const overrides = options.weightOverridesByProvider;
|
|
20
|
+
const weightCache = new Map();
|
|
21
|
+
const weightsFor = (providerId) => {
|
|
22
|
+
let entry = weightCache.get(providerId);
|
|
23
|
+
if (!entry) {
|
|
24
|
+
const override = overrides?.[providerId];
|
|
25
|
+
const merged = override ? { ...baseWeights, ...override } : baseWeights;
|
|
26
|
+
const w = strategyWeights(strategy, merged);
|
|
27
|
+
entry = { w, total: weightSum(w) || 1 };
|
|
28
|
+
weightCache.set(providerId, entry);
|
|
29
|
+
}
|
|
30
|
+
return entry;
|
|
31
|
+
};
|
|
18
32
|
const scored = candidates.map((candidate) => {
|
|
19
33
|
const m = candidate.model;
|
|
34
|
+
const { w, total } = weightsFor(candidate.providerId);
|
|
20
35
|
const observed = options.history?.(candidate.providerId, m.id);
|
|
21
36
|
const breakdown = {
|
|
22
37
|
capabilityFit: dim.capabilityFit(m, task),
|
|
@@ -57,15 +57,16 @@ export function analyzeOpenApi(spec) {
|
|
|
57
57
|
// Suggest a config only when we found an OpenAI-compatible chat endpoint and a base URL.
|
|
58
58
|
if (wireShape === 'openai' && baseUrl) {
|
|
59
59
|
const base = baseUrl.replace(/\/chat\/completions$/i, '').replace(/\/+$/, '');
|
|
60
|
+
// No `models` is suggested: 'openai-compatible' has no built-in model catalog, so `models: 'auto'`
|
|
61
|
+
// would resolve to zero models (a load-time CONFIG error). The user must list models explicitly.
|
|
60
62
|
analysis.suggestedProviderConfig = {
|
|
61
63
|
id: 'openapi-provider',
|
|
62
64
|
kind: 'openai-compatible',
|
|
63
65
|
baseUrl: base,
|
|
64
66
|
apiKeyEnv: 'OPENAPI_PROVIDER_API_KEY',
|
|
65
67
|
wireShape: 'openai',
|
|
66
|
-
models: 'auto',
|
|
67
68
|
};
|
|
68
|
-
notes.push('Suggested an openai-compatible provider config — review it, set models, and supply the key via OPENAPI_PROVIDER_API_KEY.');
|
|
69
|
+
notes.push('Suggested an openai-compatible provider config — review it, set `models` (this kind has no built-in catalog), and supply the key via OPENAPI_PROVIDER_API_KEY.');
|
|
69
70
|
}
|
|
70
71
|
return analysis;
|
|
71
72
|
}
|
|
@@ -213,11 +213,11 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
213
213
|
total: z.ZodNumber;
|
|
214
214
|
succeeded: z.ZodNumber;
|
|
215
215
|
}, "strict", z.ZodTypeAny, {
|
|
216
|
-
succeeded: number;
|
|
217
216
|
total: number;
|
|
218
|
-
}, {
|
|
219
217
|
succeeded: number;
|
|
218
|
+
}, {
|
|
220
219
|
total: number;
|
|
220
|
+
succeeded: number;
|
|
221
221
|
}>;
|
|
222
222
|
callsReserved: z.ZodNumber;
|
|
223
223
|
callsUsed: z.ZodNumber;
|
|
@@ -366,8 +366,8 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
366
366
|
callsUsed: number;
|
|
367
367
|
agentId: string;
|
|
368
368
|
innerSteps: {
|
|
369
|
-
succeeded: number;
|
|
370
369
|
total: number;
|
|
370
|
+
succeeded: number;
|
|
371
371
|
};
|
|
372
372
|
toolCallsUsed: number;
|
|
373
373
|
findings: z.objectOutputType<{
|
|
@@ -493,8 +493,8 @@ export declare const persistedAgentTask: z.ZodObject<{
|
|
|
493
493
|
callsUsed: number;
|
|
494
494
|
agentId: string;
|
|
495
495
|
innerSteps: {
|
|
496
|
-
succeeded: number;
|
|
497
496
|
total: number;
|
|
497
|
+
succeeded: number;
|
|
498
498
|
};
|
|
499
499
|
toolCallsUsed: number;
|
|
500
500
|
findings: z.objectInputType<{
|
|
@@ -22,7 +22,9 @@ export function generateProviderConfig(analysis, overrides = {}) {
|
|
|
22
22
|
...(overrides.models ? { models: overrides.models } : {}),
|
|
23
23
|
};
|
|
24
24
|
if (config.models === 'auto' && !overrides.models) {
|
|
25
|
-
//
|
|
25
|
+
// `models: 'auto'` resolves to the kind's built-in catalog, which is empty for baseUrl-based kinds
|
|
26
|
+
// (openai-compatible/custom) — so for a generated adapter it would fail at load. Drop it and require
|
|
27
|
+
// an explicit model list (buildProvider raises a clear CONFIG error if the user leaves it unset).
|
|
26
28
|
delete config.models;
|
|
27
29
|
}
|
|
28
30
|
return config;
|
package/dist/index.d.ts
CHANGED
|
@@ -85,7 +85,9 @@ export { RuntimeStore, STORE_VERSION } from './store/store.js';
|
|
|
85
85
|
export type { RuntimeStoreOptions, StorePaths } from './store/store.js';
|
|
86
86
|
export { FileArea, NullArea } from './store/area.js';
|
|
87
87
|
export type { Area, IntegrityIssue, ContentCodec } from './store/area.js';
|
|
88
|
-
export { makeCodec, deriveKey, encryptString, decryptString, ENVELOPE_PREFIX } from './store/crypto.js';
|
|
88
|
+
export { makeCodec, deriveKey, encryptString, decryptString, encryptStringV2, ENVELOPE_PREFIX, ENVELOPE_PREFIX_V2 } from './store/crypto.js';
|
|
89
|
+
export { StoreDecryptError } from './store/errors.js';
|
|
90
|
+
export type { StoreDecryptCode } from './store/errors.js';
|
|
89
91
|
export { resolveHome, projectId, repositoryId, organizationId, findRepoRoot } from './store/paths.js';
|
|
90
92
|
export { deriveCapabilities, deriveCapabilitiesOffline, candidatesFrom, candidateSlate, DERIVE_MAX_CANDIDATES, DERIVE_MAX_IDS } from './runtime/planning/deriveCapabilities.js';
|
|
91
93
|
export type { CapabilityCandidate, DeriveCapabilitiesInput, DeriveCapabilitiesResult } from './runtime/planning/deriveCapabilities.js';
|
|
@@ -145,7 +147,7 @@ export { resolveInJail, JailError } from './tools/jail.js';
|
|
|
145
147
|
export { defaultRunner, safeEnv } from './tools/runner.js';
|
|
146
148
|
export type { CommandRunner, RunOptions, RunResult } from './tools/runner.js';
|
|
147
149
|
export { filesystemTool } from './tools/builtins/filesystem.js';
|
|
148
|
-
export { shellTool, createShellTool, isDestructive } from './tools/builtins/shell.js';
|
|
150
|
+
export { shellTool, createShellTool, isDestructive, isEvalCapable } from './tools/builtins/shell.js';
|
|
149
151
|
export { gitTool, createGitTool } from './tools/builtins/git.js';
|
|
150
152
|
export { wrapUntrusted, looksLikeInjection } from './tools/untrusted.js';
|
|
151
153
|
export { SkillRegistry } from './skills/registry.js';
|
package/dist/index.js
CHANGED
|
@@ -66,7 +66,8 @@ export { deriveAccessState, buildProviderViews, ProviderViewCache } from './runt
|
|
|
66
66
|
// ── Runtime (Phase 3) — local store, conversations, memory ──
|
|
67
67
|
export { RuntimeStore, STORE_VERSION } from './store/store.js';
|
|
68
68
|
export { FileArea, NullArea } from './store/area.js';
|
|
69
|
-
export { makeCodec, deriveKey, encryptString, decryptString, ENVELOPE_PREFIX } from './store/crypto.js';
|
|
69
|
+
export { makeCodec, deriveKey, encryptString, decryptString, encryptStringV2, ENVELOPE_PREFIX, ENVELOPE_PREFIX_V2 } from './store/crypto.js';
|
|
70
|
+
export { StoreDecryptError } from './store/errors.js';
|
|
70
71
|
export { resolveHome, projectId, repositoryId, organizationId, findRepoRoot } from './store/paths.js';
|
|
71
72
|
// ── Capability-first planning (Phase 3.3) — offline BM25 derivation, one model rung under it ──
|
|
72
73
|
export { deriveCapabilities, deriveCapabilitiesOffline, candidatesFrom, candidateSlate, DERIVE_MAX_CANDIDATES, DERIVE_MAX_IDS } from './runtime/planning/deriveCapabilities.js';
|
|
@@ -107,7 +108,7 @@ export { resolvePermissions } from './tools/permissions.js';
|
|
|
107
108
|
export { resolveInJail, JailError } from './tools/jail.js';
|
|
108
109
|
export { defaultRunner, safeEnv } from './tools/runner.js';
|
|
109
110
|
export { filesystemTool } from './tools/builtins/filesystem.js';
|
|
110
|
-
export { shellTool, createShellTool, isDestructive } from './tools/builtins/shell.js';
|
|
111
|
+
export { shellTool, createShellTool, isDestructive, isEvalCapable } from './tools/builtins/shell.js';
|
|
111
112
|
export { gitTool, createGitTool } from './tools/builtins/git.js';
|
|
112
113
|
export { wrapUntrusted, looksLikeInjection } from './tools/untrusted.js';
|
|
113
114
|
export { SkillRegistry } from './skills/registry.js';
|
package/dist/mcp/protocol.js
CHANGED
|
@@ -100,7 +100,10 @@ export function sanitizeSchema(raw, budget = { nodes: MCP_SCHEMA_MAX_NODES }, de
|
|
|
100
100
|
}
|
|
101
101
|
if (typeof raw !== 'object')
|
|
102
102
|
return undefined;
|
|
103
|
-
|
|
103
|
+
// Null-prototype so a server key literally named `__proto__` (or `constructor`/`prototype`) becomes an
|
|
104
|
+
// OWN data property instead of hitting the inherited setter — which would silently drop it or mutate the
|
|
105
|
+
// object's prototype. JSON.stringify of a null-proto object is unaffected.
|
|
106
|
+
const out = Object.create(null);
|
|
104
107
|
for (const [k, v] of Object.entries(raw).slice(0, 64)) {
|
|
105
108
|
const key = clampText(k, 64);
|
|
106
109
|
if (!key)
|
package/dist/memory/bm25.d.ts
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
* deterministically. An `EmbeddingProvider` seam (memory/memory.ts) lets real embeddings replace this
|
|
4
4
|
* later without changing the retrieval API.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Unicode-aware tokenizer. Matches letter/number runs across ALL scripts (`\p{L}\p{N}`), so accented
|
|
8
|
+
* Latin, Cyrillic, Arabic, Korean, etc. are searchable — the old `[a-z0-9]` dropped every non-ASCII
|
|
9
|
+
* character silently. A run containing an unsegmented-CJK character is split into single characters
|
|
10
|
+
* (length-1 allowed, since one Han character is meaningful); all other tokens keep the ≥2-length filter
|
|
11
|
+
* and the English stopword list. Pure-ASCII input tokenizes byte-identically to the previous behavior.
|
|
12
|
+
*/
|
|
6
13
|
export declare function tokenize(text: string): string[];
|
|
7
14
|
export interface Bm25Doc {
|
|
8
15
|
id: string;
|
package/dist/memory/bm25.js
CHANGED
|
@@ -6,10 +6,26 @@
|
|
|
6
6
|
const STOPWORDS = new Set([
|
|
7
7
|
'the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'was', 'were', 'be', 'been', 'to', 'of', 'in', 'on', 'for', 'with', 'as', 'by', 'at', 'this', 'that', 'these', 'those', 'it', 'its', 'i', 'you', 'we', 'they', 'do', 'does', 'did', 'has', 'have', 'had', 'will', 'would', 'should', 'can', 'could',
|
|
8
8
|
]);
|
|
9
|
+
// Chinese/Japanese scripts (Han/Hiragana/Katakana) are NOT space-delimited, so a whole clause matches as
|
|
10
|
+
// one run; splitting it into single characters gives BM25 something to overlap on. Korean (Hangul) and
|
|
11
|
+
// every other script ARE space-delimited, so their words tokenize whole (kept out of this set).
|
|
12
|
+
const CJK_UNSEGMENTED = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u;
|
|
13
|
+
/**
|
|
14
|
+
* Unicode-aware tokenizer. Matches letter/number runs across ALL scripts (`\p{L}\p{N}`), so accented
|
|
15
|
+
* Latin, Cyrillic, Arabic, Korean, etc. are searchable — the old `[a-z0-9]` dropped every non-ASCII
|
|
16
|
+
* character silently. A run containing an unsegmented-CJK character is split into single characters
|
|
17
|
+
* (length-1 allowed, since one Han character is meaningful); all other tokens keep the ≥2-length filter
|
|
18
|
+
* and the English stopword list. Pure-ASCII input tokenizes byte-identically to the previous behavior.
|
|
19
|
+
*/
|
|
9
20
|
export function tokenize(text) {
|
|
10
21
|
const out = [];
|
|
11
|
-
for (const m of text.toLowerCase().matchAll(/[
|
|
22
|
+
for (const m of text.toLowerCase().matchAll(/[\p{L}\p{N}]+/gu)) {
|
|
12
23
|
const tok = m[0];
|
|
24
|
+
if (CJK_UNSEGMENTED.test(tok)) {
|
|
25
|
+
for (const ch of tok)
|
|
26
|
+
out.push(ch); // one meaningful unit per character
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
13
29
|
if (tok.length >= 2 && !STOPWORDS.has(tok))
|
|
14
30
|
out.push(tok);
|
|
15
31
|
}
|
package/dist/memory/memory.d.ts
CHANGED
|
@@ -59,8 +59,14 @@ export declare class MemoryStore {
|
|
|
59
59
|
private readonly embedder?;
|
|
60
60
|
private readonly clock;
|
|
61
61
|
private counter;
|
|
62
|
-
/** Per-instance cache of fact-text → vector, so a stable fact isn't re-embedded on every query.
|
|
62
|
+
/** Per-instance cache of fact-text → vector, so a stable fact isn't re-embedded on every query. Bounded
|
|
63
|
+
* (see cacheVec) so a long-lived host process cannot grow it without limit. */
|
|
63
64
|
private readonly vecCache;
|
|
65
|
+
/** Cap on distinct cached vectors. On overflow the cache is cleared wholesale — the only cost is
|
|
66
|
+
* re-embedding, never correctness. A session touching more than this many distinct facts is rare. */
|
|
67
|
+
private static readonly VEC_CACHE_MAX;
|
|
68
|
+
/** Store a vector, clearing the cache first if it is at capacity (bounded growth). */
|
|
69
|
+
private cacheVec;
|
|
64
70
|
constructor(store: RuntimeStore, clock?: Clock, embedder?: EmbeddingProvider | undefined);
|
|
65
71
|
get enabled(): boolean;
|
|
66
72
|
private area;
|
package/dist/memory/memory.js
CHANGED
|
@@ -25,8 +25,18 @@ export class MemoryStore {
|
|
|
25
25
|
embedder;
|
|
26
26
|
clock;
|
|
27
27
|
counter = 0;
|
|
28
|
-
/** Per-instance cache of fact-text → vector, so a stable fact isn't re-embedded on every query.
|
|
28
|
+
/** Per-instance cache of fact-text → vector, so a stable fact isn't re-embedded on every query. Bounded
|
|
29
|
+
* (see cacheVec) so a long-lived host process cannot grow it without limit. */
|
|
29
30
|
vecCache = new Map();
|
|
31
|
+
/** Cap on distinct cached vectors. On overflow the cache is cleared wholesale — the only cost is
|
|
32
|
+
* re-embedding, never correctness. A session touching more than this many distinct facts is rare. */
|
|
33
|
+
static VEC_CACHE_MAX = 2000;
|
|
34
|
+
/** Store a vector, clearing the cache first if it is at capacity (bounded growth). */
|
|
35
|
+
cacheVec(text, vec) {
|
|
36
|
+
if (this.vecCache.size >= MemoryStore.VEC_CACHE_MAX && !this.vecCache.has(text))
|
|
37
|
+
this.vecCache.clear();
|
|
38
|
+
this.vecCache.set(text, vec);
|
|
39
|
+
}
|
|
30
40
|
constructor(store, clock = systemClock, embedder) {
|
|
31
41
|
this.store = store;
|
|
32
42
|
this.embedder = embedder;
|
|
@@ -172,7 +182,7 @@ export class MemoryStore {
|
|
|
172
182
|
// Cache only USABLE vectors — a degenerate ([] or non-finite) embedding must never be treated as
|
|
173
183
|
// valid nor poison the cache (a poisoned entry silently excludes that fact for the rest of the session).
|
|
174
184
|
need.forEach((t, i) => { if (isUsableVec(vecs[i]))
|
|
175
|
-
this.
|
|
185
|
+
this.cacheVec(t, vecs[i]); });
|
|
176
186
|
}
|
|
177
187
|
const [queryVec] = await this.embedder.embed([query]);
|
|
178
188
|
const docs = candidates.filter((c) => this.vecCache.has(c.text)).map((c) => ({ id: c.id, vec: this.vecCache.get(c.text) }));
|
|
@@ -197,6 +207,8 @@ export class MemoryStore {
|
|
|
197
207
|
removeCascade(id) {
|
|
198
208
|
const target = this.get(id);
|
|
199
209
|
const inheritor = target?.supersededBy; // if the removed record was itself superseded, heirs re-link to its head
|
|
210
|
+
// Invalidate the cached vector for the removed record's text (if any) — and report whether we did.
|
|
211
|
+
const cacheEvicted = target ? this.vecCache.delete(target.text) : false;
|
|
200
212
|
let primary = false;
|
|
201
213
|
let relationships = 0;
|
|
202
214
|
for (const scope of this.physicalScopes()) {
|
|
@@ -232,8 +244,10 @@ export class MemoryStore {
|
|
|
232
244
|
}
|
|
233
245
|
}
|
|
234
246
|
}
|
|
235
|
-
//
|
|
236
|
-
|
|
247
|
+
// Honest audit: `indexes` reflects the removal only when the primary was actually removed (indexes are
|
|
248
|
+
// query-time-derived, so they change iff the record did); `cache` reports whether a vector was evicted.
|
|
249
|
+
// A delete of a nonexistent id now reports all-false rather than a hardcoded success.
|
|
250
|
+
return { id, primary, indexes: primary, relationships, cache: cacheEvicted };
|
|
237
251
|
}
|
|
238
252
|
/** Remove expired records with full cascade, under the lock. Returns removed ids. */
|
|
239
253
|
purgeExpired(now = this.clock.now()) {
|
package/dist/plugin/ai.d.ts
CHANGED
|
@@ -70,6 +70,12 @@ export declare class AI {
|
|
|
70
70
|
providers(): ProviderInfo[];
|
|
71
71
|
tasksList(): TaskDefinition[];
|
|
72
72
|
telemetryEvents(): TelemetryEvent[];
|
|
73
|
+
/**
|
|
74
|
+
* Release AI-level resources. Today: flush any batching telemetry sink (e.g. OTLP) so a short-lived
|
|
75
|
+
* process does not drop events buffered below the batch threshold. Safe to call more than once, and a
|
|
76
|
+
* failing flush never throws (telemetry must not fail shutdown). Runtime.close() calls this.
|
|
77
|
+
*/
|
|
78
|
+
close(): Promise<void>;
|
|
73
79
|
/** Run each provider's healthCheck(), seed the monitor, and return the statuses (for `doctor`). */
|
|
74
80
|
checkHealth(): Promise<HealthStatus[]>;
|
|
75
81
|
/** Discover provider identity, models, and capabilities (for `providers`/`models`/`capabilities`). */
|
package/dist/plugin/ai.js
CHANGED
|
@@ -71,8 +71,10 @@ export class AI {
|
|
|
71
71
|
for (const providerCfg of this.config.providers) {
|
|
72
72
|
if (providerCfg.kind === 'mock')
|
|
73
73
|
continue;
|
|
74
|
-
const
|
|
75
|
-
|
|
74
|
+
const enabled = providerCfg.enabled ?? true;
|
|
75
|
+
// A disabled provider never routes, so a zero-model config must not abort the whole runtime at load.
|
|
76
|
+
const provider = buildProvider(providerCfg, enabled ? this.buildOpts : { ...this.buildOpts, allowZeroModels: true });
|
|
77
|
+
this.registry.register(provider, enabled);
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
/** Build an AI from a local config file/path or a remote http(s) URL. */
|
|
@@ -138,6 +140,19 @@ export class AI {
|
|
|
138
140
|
telemetryEvents() {
|
|
139
141
|
return this.telemetry.events?.() ?? [];
|
|
140
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Release AI-level resources. Today: flush any batching telemetry sink (e.g. OTLP) so a short-lived
|
|
145
|
+
* process does not drop events buffered below the batch threshold. Safe to call more than once, and a
|
|
146
|
+
* failing flush never throws (telemetry must not fail shutdown). Runtime.close() calls this.
|
|
147
|
+
*/
|
|
148
|
+
async close() {
|
|
149
|
+
try {
|
|
150
|
+
await this.telemetry.flush?.();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* a telemetry flush must never break shutdown */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
141
156
|
/** Run each provider's healthCheck(), seed the monitor, and return the statuses (for `doctor`). */
|
|
142
157
|
async checkHealth() {
|
|
143
158
|
const out = [];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input-size estimation for routing, context-fit, and budget guardrails.
|
|
3
|
+
*
|
|
4
|
+
* This is a deterministic, conservative, LOCAL heuristic — it is deliberately NOT a reproduction of any
|
|
5
|
+
* provider's exact billing or tokenization. Its only guarantees are the ones the router needs:
|
|
6
|
+
* - monotonic in payload size (a larger input never estimates fewer tokens),
|
|
7
|
+
* - binary/multimodal parts contribute a nonzero estimate (an image is never "free"),
|
|
8
|
+
* - text is counted exactly once (input.text and any `kind:'text'` parts, never double-counted).
|
|
9
|
+
*
|
|
10
|
+
* Text is ≈ chars/4 (the same ratio TokenEstimator uses; reimplemented here to avoid a providers→context
|
|
11
|
+
* dependency). Binary parts are sized from their decoded byte length at ~1 token per KiB with a 64-token
|
|
12
|
+
* floor per attachment, so a request with a large image cannot slip through context/budget checks as if
|
|
13
|
+
* it were empty. The floor and ratio are heuristic constants, not provider truth.
|
|
14
|
+
*/
|
|
15
|
+
import type { InputPart } from '../types.js';
|
|
16
|
+
/** Tokens contributed by one input part. Text parts are chars/4; binary parts are bytes-derived. */
|
|
17
|
+
export declare function estimatePartTokens(part: InputPart): number;
|
|
18
|
+
/**
|
|
19
|
+
* Estimate the input token count of a request's `{ text, parts }`. Text from `input.text` and from any
|
|
20
|
+
* `kind:'text'` parts is summed and converted once; each binary part adds its own bytes-derived estimate.
|
|
21
|
+
*/
|
|
22
|
+
export declare function estimateInputTokens(input: {
|
|
23
|
+
text?: string;
|
|
24
|
+
parts?: InputPart[];
|
|
25
|
+
}): number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input-size estimation for routing, context-fit, and budget guardrails.
|
|
3
|
+
*
|
|
4
|
+
* This is a deterministic, conservative, LOCAL heuristic — it is deliberately NOT a reproduction of any
|
|
5
|
+
* provider's exact billing or tokenization. Its only guarantees are the ones the router needs:
|
|
6
|
+
* - monotonic in payload size (a larger input never estimates fewer tokens),
|
|
7
|
+
* - binary/multimodal parts contribute a nonzero estimate (an image is never "free"),
|
|
8
|
+
* - text is counted exactly once (input.text and any `kind:'text'` parts, never double-counted).
|
|
9
|
+
*
|
|
10
|
+
* Text is ≈ chars/4 (the same ratio TokenEstimator uses; reimplemented here to avoid a providers→context
|
|
11
|
+
* dependency). Binary parts are sized from their decoded byte length at ~1 token per KiB with a 64-token
|
|
12
|
+
* floor per attachment, so a request with a large image cannot slip through context/budget checks as if
|
|
13
|
+
* it were empty. The floor and ratio are heuristic constants, not provider truth.
|
|
14
|
+
*/
|
|
15
|
+
const CHARS_PER_TOKEN = 4;
|
|
16
|
+
const BYTES_PER_TOKEN = 1024;
|
|
17
|
+
const MIN_PART_TOKENS = 64;
|
|
18
|
+
/** Decoded byte length of a base64 string (ignoring padding), without allocating a Buffer. */
|
|
19
|
+
function base64Bytes(data) {
|
|
20
|
+
if (!data)
|
|
21
|
+
return 0;
|
|
22
|
+
let len = data.length;
|
|
23
|
+
// Strip a possible data: URI prefix's base64 marker if present (defensive; `data` is base64 by convention).
|
|
24
|
+
const comma = data.indexOf(',');
|
|
25
|
+
if (data.startsWith('data:') && comma >= 0)
|
|
26
|
+
len = data.length - comma - 1;
|
|
27
|
+
let padding = 0;
|
|
28
|
+
if (len >= 1 && data.endsWith('='))
|
|
29
|
+
padding += 1;
|
|
30
|
+
if (len >= 2 && data.endsWith('=='))
|
|
31
|
+
padding += 1;
|
|
32
|
+
return Math.max(0, Math.floor((len * 3) / 4) - padding);
|
|
33
|
+
}
|
|
34
|
+
/** Tokens contributed by one input part. Text parts are chars/4; binary parts are bytes-derived. */
|
|
35
|
+
export function estimatePartTokens(part) {
|
|
36
|
+
if (part.kind === 'text')
|
|
37
|
+
return Math.ceil(part.text.length / CHARS_PER_TOKEN);
|
|
38
|
+
const bytes = base64Bytes(part.data);
|
|
39
|
+
return Math.max(MIN_PART_TOKENS, Math.ceil(bytes / BYTES_PER_TOKEN));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Estimate the input token count of a request's `{ text, parts }`. Text from `input.text` and from any
|
|
43
|
+
* `kind:'text'` parts is summed and converted once; each binary part adds its own bytes-derived estimate.
|
|
44
|
+
*/
|
|
45
|
+
export function estimateInputTokens(input) {
|
|
46
|
+
let textChars = input.text?.length ?? 0;
|
|
47
|
+
let binaryTokens = 0;
|
|
48
|
+
for (const part of input.parts ?? []) {
|
|
49
|
+
if (part.kind === 'text')
|
|
50
|
+
textChars += part.text.length;
|
|
51
|
+
else
|
|
52
|
+
binaryTokens += estimatePartTokens(part);
|
|
53
|
+
}
|
|
54
|
+
return Math.ceil(textChars / CHARS_PER_TOKEN) + binaryTokens;
|
|
55
|
+
}
|
|
@@ -14,5 +14,8 @@ export interface BuildOptions {
|
|
|
14
14
|
fetchImpl?: FetchLike;
|
|
15
15
|
clock?: Clock;
|
|
16
16
|
env?: NodeJS.ProcessEnv;
|
|
17
|
+
/** Tolerate a zero-model result instead of throwing. Set only for providers that will not route (a
|
|
18
|
+
* `enabled: false` provider), so a work-in-progress catalog-less block cannot abort runtime load. */
|
|
19
|
+
allowZeroModels?: boolean;
|
|
17
20
|
}
|
|
18
21
|
export declare function buildProvider(cfg: ProviderConfig, opts?: BuildOptions): AIProvider;
|
|
@@ -28,11 +28,32 @@ export function buildProvider(cfg, opts = {}) {
|
|
|
28
28
|
const credential = new Credential(apiKeyEnv, opts.env);
|
|
29
29
|
const privacyClass = cfg.privacyClass ?? def.privacyClass;
|
|
30
30
|
const wireShape = cfg.wireShape ?? def.wireShape;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
// Resolve the model id list. `models: 'auto'` (and an omitted `models`) fall back to the caller's
|
|
32
|
+
// `defaultModel`, then to the kind's built-in catalog. An explicit `models: []` is an intentional
|
|
33
|
+
// "no models" statement and is treated as a config error, never a silent fallback.
|
|
34
|
+
let modelIds;
|
|
35
|
+
if (Array.isArray(cfg.models)) {
|
|
36
|
+
modelIds = cfg.models; // may be [] — caught by the zero-model guard below
|
|
37
|
+
}
|
|
38
|
+
else if (cfg.defaultModel) {
|
|
39
|
+
modelIds = [cfg.defaultModel];
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
modelIds = def.defaultModels; // 'auto' or omitted → the kind's catalog (empty for openai-compatible/custom)
|
|
43
|
+
}
|
|
44
|
+
// Never silently register an ENABLED provider with zero routable models — that is a deterministic
|
|
45
|
+
// CONFIG problem (no model could be resolved), distinct from a provider being unavailable at runtime
|
|
46
|
+
// (health). A disabled provider (opts.allowZeroModels) is exempt: it will not route, so a missing model
|
|
47
|
+
// list must not abort construction of the whole runtime.
|
|
48
|
+
if (modelIds.length === 0 && !opts.allowZeroModels) {
|
|
49
|
+
const detail = Array.isArray(cfg.models)
|
|
50
|
+
? 'an explicit empty models list'
|
|
51
|
+
: `'${cfg.kind}' has no built-in model catalog`;
|
|
52
|
+
const fix = Array.isArray(cfg.models)
|
|
53
|
+
? 'remove the empty list or populate it (models: [...])'
|
|
54
|
+
: 'list models explicitly (models: [...]) or set defaultModel';
|
|
55
|
+
throw new AIError(`provider '${cfg.id}': no models resolved — ${detail}. ${fix}.`, { category: 'CONFIG' });
|
|
56
|
+
}
|
|
36
57
|
const models = modelIds.map((id) => resolveModelMetadata(cfg.id, cfg.kind, id, cfg.capabilities));
|
|
37
58
|
const config = {
|
|
38
59
|
id: cfg.id,
|
|
@@ -45,6 +45,10 @@ export async function callHttp(input) {
|
|
|
45
45
|
if (!retryable)
|
|
46
46
|
throw err;
|
|
47
47
|
lastError = err;
|
|
48
|
+
// No retry left ⇒ give up now; do not sleep out a backoff (up to 30s of Retry-After) before a
|
|
49
|
+
// failure that will happen regardless. Mirrors the network/timeout branch's last-attempt guard.
|
|
50
|
+
if (attempt > input.maxRetries)
|
|
51
|
+
break;
|
|
48
52
|
const retryAfter = Number(res.headers.get('retry-after'));
|
|
49
53
|
await clock.sleep(Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter * 1000, 30_000) : attempt * 2000);
|
|
50
54
|
continue;
|
|
@@ -13,6 +13,7 @@ import { emptyProfile } from '../core/capabilities/evidence.js';
|
|
|
13
13
|
import { extractJson } from '../util/extractJson.js';
|
|
14
14
|
import { getWire } from './wire/registry.js';
|
|
15
15
|
import { callHttp, callHttpStream } from './httpClient.js';
|
|
16
|
+
import { estimateInputTokens } from './estimate.js';
|
|
16
17
|
function originOf(url) {
|
|
17
18
|
try {
|
|
18
19
|
return new URL(url).origin;
|
|
@@ -44,7 +45,7 @@ export class HttpProvider {
|
|
|
44
45
|
authMode: this.cfg.requiresKey === false ? 'none' : 'env',
|
|
45
46
|
supportsModelListing: this.cfg.supportsModelListing ?? false,
|
|
46
47
|
privacyClass: this.privacyClass,
|
|
47
|
-
models: this.cfg.models,
|
|
48
|
+
models: [...this.cfg.models], // defensive copy — never hand out the internal array by reference
|
|
48
49
|
...(this.cfg.defaultModel ? { defaultModel: this.cfg.defaultModel } : {}),
|
|
49
50
|
};
|
|
50
51
|
}
|
|
@@ -56,14 +57,14 @@ export class HttpProvider {
|
|
|
56
57
|
return { providerId: this.id, state: 'AVAILABLE', routable: true, checkedAt: 0 };
|
|
57
58
|
}
|
|
58
59
|
async listModels() {
|
|
59
|
-
return this.cfg.models;
|
|
60
|
+
return [...this.cfg.models]; // defensive copy — a caller mutating the result must not corrupt config
|
|
60
61
|
}
|
|
61
62
|
async getCapabilities(model) {
|
|
62
63
|
return this.cfg.models.find((m) => m.id === model)?.capabilities ?? emptyProfile();
|
|
63
64
|
}
|
|
64
65
|
async estimate(request) {
|
|
65
66
|
const model = this.cfg.models.find((m) => m.id === request.model);
|
|
66
|
-
const inTokens =
|
|
67
|
+
const inTokens = estimateInputTokens(request.input);
|
|
67
68
|
const estimate = {
|
|
68
69
|
providerId: this.id,
|
|
69
70
|
model: request.model,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { AIError } from '../../core/fallback/errors.js';
|
|
7
7
|
import { emptyProfile } from '../../core/capabilities/evidence.js';
|
|
8
|
+
import { estimateInputTokens } from '../estimate.js';
|
|
8
9
|
export class MockProvider {
|
|
9
10
|
id;
|
|
10
11
|
name;
|
|
@@ -31,7 +32,7 @@ export class MockProvider {
|
|
|
31
32
|
authMode: 'none',
|
|
32
33
|
supportsModelListing: true,
|
|
33
34
|
privacyClass: this.privacyClass,
|
|
34
|
-
models: this.models,
|
|
35
|
+
models: [...this.models],
|
|
35
36
|
...(this.models[0] ? { defaultModel: this.models[0].id } : {}),
|
|
36
37
|
};
|
|
37
38
|
}
|
|
@@ -39,14 +40,14 @@ export class MockProvider {
|
|
|
39
40
|
return { providerId: this.id, state: 'AVAILABLE', routable: true, checkedAt: 0 };
|
|
40
41
|
}
|
|
41
42
|
async listModels() {
|
|
42
|
-
return this.models;
|
|
43
|
+
return [...this.models];
|
|
43
44
|
}
|
|
44
45
|
async getCapabilities(model) {
|
|
45
46
|
return this.models.find((m) => m.id === model)?.capabilities ?? emptyProfile();
|
|
46
47
|
}
|
|
47
48
|
async estimate(request) {
|
|
48
49
|
const model = this.models.find((m) => m.id === request.model);
|
|
49
|
-
const inTokens =
|
|
50
|
+
const inTokens = estimateInputTokens(request.input);
|
|
50
51
|
const estimate = {
|
|
51
52
|
providerId: this.id,
|
|
52
53
|
model: request.model,
|
package/dist/runtime/config.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime configuration loader. Canonical file: `.ai-runtime/config.yaml` (runtime settings + the
|
|
3
3
|
* router config). WRAPS, never forks, the existing strict `parseConfig`: a small strict zod fragment
|
|
4
|
-
* validates/plucks the runtime-level keys (runtime:,
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
4
|
+
* validates/plucks the purely runtime-level keys (runtime:, permissions:, routing:, mcp:), then delegates
|
|
5
|
+
* the remainder — including learning/verification/budget/policy, which became first-class RouterConfig
|
|
6
|
+
* keys on the root schema in 3.0.1 — to parseConfig so its strictness and inline-secret rejection are
|
|
7
|
+
* preserved. Root `ai-runtime.yaml` remains a supported fallback.
|
|
7
8
|
*
|
|
8
9
|
* NOTE: `src/config/load.ts` and `AI.load()` are intentionally untouched — the CLI's existing behavior
|
|
9
10
|
* is frozen until Phase 2 migrates it.
|
package/dist/runtime/config.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime configuration loader. Canonical file: `.ai-runtime/config.yaml` (runtime settings + the
|
|
3
3
|
* router config). WRAPS, never forks, the existing strict `parseConfig`: a small strict zod fragment
|
|
4
|
-
* validates/plucks the runtime-level keys (runtime:,
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
4
|
+
* validates/plucks the purely runtime-level keys (runtime:, permissions:, routing:, mcp:), then delegates
|
|
5
|
+
* the remainder — including learning/verification/budget/policy, which became first-class RouterConfig
|
|
6
|
+
* keys on the root schema in 3.0.1 — to parseConfig so its strictness and inline-secret rejection are
|
|
7
|
+
* preserved. Root `ai-runtime.yaml` remains a supported fallback.
|
|
7
8
|
*
|
|
8
9
|
* NOTE: `src/config/load.ts` and `AI.load()` are intentionally untouched — the CLI's existing behavior
|
|
9
10
|
* is frozen until Phase 2 migrates it.
|
|
@@ -17,8 +18,12 @@ import { parseConfig, STRATEGIES, KEY_LIKE } from '../config/schema.js';
|
|
|
17
18
|
import { findConfigFile } from '../config/load.js';
|
|
18
19
|
import { AIError } from '../core/fallback/errors.js';
|
|
19
20
|
import { RUNTIME_MODES } from './types.js';
|
|
20
|
-
/**
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Runtime-level keys that live in `.ai-runtime/config.yaml` but are NOT part of the strict root schema.
|
|
23
|
+
* `learning`/`verification`/`budget`/`policy` moved to the root schema in 3.0.1 (they are RouterConfig
|
|
24
|
+
* fields), so they now flow through `parseConfig(rest)` directly instead of being folded back here.
|
|
25
|
+
*/
|
|
26
|
+
const RUNTIME_ONLY_KEYS = ['runtime', 'permissions', 'routing', 'mcp'];
|
|
22
27
|
const capabilityRequirementShape = z.object({ group: z.enum(['input', 'output', 'intelligence', 'agent']), key: z.string().min(1), minEvidence: z.enum(['unsupported', 'unknown', 'inferred', 'documented', 'verified']).optional(), weight: z.number().optional() }).strict();
|
|
23
28
|
const routingShape = z.object({ excludeProviders: z.array(z.string()).optional(), excludeModels: z.array(z.string()).optional(), preferProviders: z.array(z.string()).optional(), preferModels: z.array(z.string()).optional() }).strict();
|
|
24
29
|
/**
|
|
@@ -41,12 +46,6 @@ const agentDefinition = z
|
|
|
41
46
|
/** An agent definition id: the same prompt-safe shape an MCP server id must have. */
|
|
42
47
|
const AGENT_ID_RE = /^[a-z0-9][a-z0-9_-]{0,32}$/;
|
|
43
48
|
const runtimeSettings = z.object({ defaultMode: z.enum(RUNTIME_MODES).optional(), defaultStrategy: z.enum(STRATEGIES).optional(), context: z.object({ maxTokens: z.number().optional(), verifyLoss: z.boolean().optional(), summarize: z.boolean().optional() }).strict().optional(), skills: z.object({ paths: z.array(z.string()).optional(), packages: z.array(z.string()).optional(), autoload: z.boolean().optional() }).strict().optional(), embedding: z.object({ provider: z.enum(['local', 'openai-compatible']), baseUrl: z.string().optional(), apiKeyEnv: z.string().optional(), model: z.string().optional() }).strict().optional(), intent: z.object({ aiFallback: z.boolean().optional() }).strict().optional(), organization: z.string().optional(), storage: z.object({ encrypt: z.boolean(), keyEnv: z.string() }).strict().optional(), capabilities: z.object({ catalog: z.boolean().optional(), planning: z.boolean().optional(), aliases: z.record(z.string(), z.string()).optional(), pins: z.record(z.string(), z.string()).optional() }).strict().optional(), concurrency: z.object({ maxParallelSteps: z.number().optional(), perTool: z.record(z.string(), z.number()).optional(), perSkill: z.record(z.string(), z.number()).optional(), perProvider: z.record(z.string(), z.number()).optional(), perAgent: z.record(z.string(), z.number()).optional() }).strict().optional(), agents: z.object({ enabled: z.boolean().optional(), decompose: z.boolean().optional(), maxToolCalls: z.number().optional(), maxDurationMs: z.number().optional(), maxInnerCalls: z.number().optional(), definitions: z.record(z.string().regex(AGENT_ID_RE, 'an agent definition id must be lowercase kebab/snake (max 33 chars)').refine((id) => !id.startsWith('auto_'), 'the `auto_` prefix is reserved for agents the runtime derives — pick another id'), agentDefinition).optional() }).strict().optional() }).strict();
|
|
44
|
-
const learning = z.object({ enabled: z.boolean().optional() }).strict();
|
|
45
|
-
const verification = z.object({ enabled: z.boolean().optional() }).strict();
|
|
46
|
-
const budget = z.object({ maxCostUsd: z.number().optional(), maxCalls: z.number().optional() }).strict();
|
|
47
|
-
const policy = z
|
|
48
|
-
.object({ allowProviders: z.array(z.string()).optional(), denyProviders: z.array(z.string()).optional(), requireLocal: z.boolean().optional(), maxCostUsd: z.number().optional(), strategy: z.enum(STRATEGIES).optional() })
|
|
49
|
-
.strict();
|
|
50
49
|
const permissions = z
|
|
51
50
|
.object({ fsRead: z.boolean().optional(), fsWrite: z.boolean().optional(), shell: z.boolean().optional(), shellAllowedCommands: z.array(z.string()).optional(), gitWrite: z.boolean().optional(), gitCommit: z.boolean().optional(), gitPush: z.boolean().optional(), network: z.boolean().optional(), mcp: z.object({ servers: z.record(z.string(), z.union([z.enum(['off', 'read', 'full']), z.boolean()])).optional() }).strict().optional() })
|
|
52
51
|
.strict();
|
|
@@ -77,7 +76,7 @@ const mcpServer = z
|
|
|
77
76
|
});
|
|
78
77
|
const mcp = z.object({ servers: z.record(z.string().regex(/^[a-z0-9][a-z0-9_-]{0,32}$/, 'an MCP server id must be lowercase kebab/snake (max 33 chars)'), mcpServer).optional() }).strict();
|
|
79
78
|
const runtimeFragment = z
|
|
80
|
-
.object({ runtime: runtimeSettings.optional(),
|
|
79
|
+
.object({ runtime: runtimeSettings.optional(), permissions: permissions.optional(), routing: routing.optional(), mcp: mcp.optional() })
|
|
81
80
|
.partial();
|
|
82
81
|
/**
|
|
83
82
|
* Parse a runtime config object (from `.ai-runtime/config.yaml` or a legacy root file). Runtime-level
|
|
@@ -94,20 +93,12 @@ export function parseRuntimeConfig(raw) {
|
|
|
94
93
|
throw new AIError(`invalid ai-runtime config:\n${issues}`, { category: 'CONFIG' });
|
|
95
94
|
}
|
|
96
95
|
const frag = fragResult.data;
|
|
97
|
-
// Delegate the remainder
|
|
96
|
+
// Delegate the remainder to the strict router schema. This now includes learning/verification/budget/
|
|
97
|
+
// policy (root-schema keys since 3.0.1), so they land on the RouterConfig directly — no fold-back needed.
|
|
98
98
|
const rest = omit(obj, RUNTIME_ONLY_KEYS);
|
|
99
99
|
if (!('providers' in rest))
|
|
100
100
|
rest.providers = [];
|
|
101
101
|
const router = parseConfig(rest);
|
|
102
|
-
// Fold the runtime-only router keys back onto the RouterConfig (they exist on the type; resolveConfig reads them).
|
|
103
|
-
if (frag.learning)
|
|
104
|
-
router.learning = frag.learning;
|
|
105
|
-
if (frag.verification)
|
|
106
|
-
router.verification = frag.verification;
|
|
107
|
-
if (frag.budget)
|
|
108
|
-
router.budget = frag.budget;
|
|
109
|
-
if (frag.policy)
|
|
110
|
-
router.policy = frag.policy;
|
|
111
102
|
// Top-level `routing:` is folded into the runtime settings (a runtime concern, resolved with env + per-run).
|
|
112
103
|
const runtimeOut = { ...(frag.runtime ?? {}), ...(frag.routing ? { routing: frag.routing } : {}) };
|
|
113
104
|
return { ...(Object.keys(runtimeOut).length ? { runtime: runtimeOut } : {}), ...(frag.permissions ? { permissions: frag.permissions } : {}), ...(frag.mcp ? { mcp: frag.mcp } : {}), router };
|
package/dist/runtime/events.d.ts
CHANGED
|
@@ -59,6 +59,12 @@ export type RuntimeEvent = {
|
|
|
59
59
|
ts: number;
|
|
60
60
|
runId: string;
|
|
61
61
|
text: string;
|
|
62
|
+
} | {
|
|
63
|
+
type: 'response.stream_abandoned';
|
|
64
|
+
ts: number;
|
|
65
|
+
runId: string;
|
|
66
|
+
providerId: string;
|
|
67
|
+
model: string;
|
|
62
68
|
} | {
|
|
63
69
|
type: 'agent.task.started';
|
|
64
70
|
ts: number;
|