ai-runtime-engine 2.9.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 +108 -0
- package/README.md +30 -0
- package/dist/agents/admit.d.ts +9 -1
- package/dist/agents/admit.js +10 -2
- package/dist/agents/envelope.d.ts +21 -0
- package/dist/agents/envelope.js +39 -5
- package/dist/agents/finding.d.ts +9 -3
- package/dist/agents/finding.js +14 -3
- package/dist/agents/worker.d.ts +3 -0
- package/dist/agents/worker.js +4 -1
- 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/orchestration/orchestrator.d.ts +2 -1
- package/dist/orchestration/planner.d.ts +2 -1
- 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 +14 -23
- package/dist/runtime/events.d.ts +6 -0
- package/dist/runtime/runtime.d.ts +43 -5
- package/dist/runtime/runtime.js +133 -25
- package/dist/runtime/types.d.ts +8 -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
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
|
/**
|
|
@@ -40,13 +45,7 @@ const agentDefinition = z
|
|
|
40
45
|
.strict();
|
|
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
|
-
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(), 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)'), 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();
|
|
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();
|
|
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;
|
|
@@ -178,6 +178,15 @@ export declare class Runtime {
|
|
|
178
178
|
* This run's agent envelopes. THE ONLY call site of `narrowEnvelope` — never re-derive an inner
|
|
179
179
|
* catalog, a permission clamp, or a reservation anywhere else (see the header of agents/envelope.ts).
|
|
180
180
|
*/
|
|
181
|
+
/**
|
|
182
|
+
* Agents synthesized from the registry for this goal (Phase 3.7). Empty unless
|
|
183
|
+
* `runtime.agents.decompose` is on — so with the flag off nothing about planning changes.
|
|
184
|
+
*
|
|
185
|
+
* Deterministic and offline: no model call, no clock, no randomness. That is a requirement, not a
|
|
186
|
+
* preference — a derived definition is hashed into `agentDefHash`, and a resume that synthesized
|
|
187
|
+
* even slightly differently would discard every persisted inner plan as stale.
|
|
188
|
+
*/
|
|
189
|
+
private derivedAgents;
|
|
181
190
|
private agentEnvelopes;
|
|
182
191
|
/** The MCP server manager: `list()`, `status(id)`, `test(id)`, `addServer`, `removeServer`, `setEnabled`. */
|
|
183
192
|
mcp(): McpManager;
|
|
@@ -210,8 +219,9 @@ export declare class Runtime {
|
|
|
210
219
|
/** Warnings from MCP wiring (id collisions) plus the manager's own (invalid store files, etc.). */
|
|
211
220
|
mcpWarningsList(): string[];
|
|
212
221
|
/**
|
|
213
|
-
* Release long-lived resources
|
|
214
|
-
*
|
|
222
|
+
* Release long-lived resources: MCP stdio child processes, and any batching telemetry sink (so a
|
|
223
|
+
* short-lived run does not drop OTLP events buffered below the batch threshold). A one-shot CLI command
|
|
224
|
+
* and the REPL both call this on completion/exit.
|
|
215
225
|
*/
|
|
216
226
|
close(): Promise<void>;
|
|
217
227
|
/** Skills whose required tools are all registered. */
|
|
@@ -299,9 +309,17 @@ export declare class Runtime {
|
|
|
299
309
|
* skipped — no skill ran, so there is no success/failure to learn (recording them would teach noise). */
|
|
300
310
|
private recordOrchestration;
|
|
301
311
|
/**
|
|
302
|
-
* A capped, FENCED action-capability snapshot for the planner prompt (Phase 3.1
|
|
303
|
-
*
|
|
304
|
-
*
|
|
312
|
+
* A capped, FENCED action-capability snapshot for the planner prompt (Phase 3.1; ON by default from
|
|
313
|
+
* 3.0.0 — set `runtime.capabilities.catalog: false` to remove it).
|
|
314
|
+
*
|
|
315
|
+
* The fence is real as of 3.0.0 and was not before: this block carries ids that come from MCP servers
|
|
316
|
+
* and third-party skills, and it renders them as trusted-looking prompt structure on every planning
|
|
317
|
+
* iteration of every run. Flattening (`promptSafe`) bounds their shape but says nothing about their
|
|
318
|
+
* provenance, so the whole block is wrapped as untrusted data. Three comments claimed "fenced" while
|
|
319
|
+
* no fence existed; shipping that ON by default would have made a false safety claim load-bearing.
|
|
320
|
+
*
|
|
321
|
+
* Both halves are bounded. The blocked-skill list had no cap at all — measured at ~24k characters
|
|
322
|
+
* with 300 blocked skills, silently, in every prompt.
|
|
305
323
|
*/
|
|
306
324
|
private capabilityCatalogText;
|
|
307
325
|
/** Any call/cost ceiling declared in the config file's `budget:` block (router-level, not policy). */
|
|
@@ -373,6 +391,26 @@ export declare class Runtime {
|
|
|
373
391
|
* an agent step would have failed every one of those steps.
|
|
374
392
|
*/
|
|
375
393
|
private orchestrateRunners;
|
|
394
|
+
/**
|
|
395
|
+
* Reconcile findings that contradict each other, across ALL of this execution's agent tasks
|
|
396
|
+
* (Phase 3.7).
|
|
397
|
+
*
|
|
398
|
+
* `resolveConflicts` has existed since 3.4 with no caller, so two agents reaching opposite
|
|
399
|
+
* conclusions about the same subject both stayed `active` — and both were rendered into the next
|
|
400
|
+
* planning prompt, as if the runtime had no opinion about which was better supported. It does: it
|
|
401
|
+
* weighs evidence-based `confidence`, with `executionCoverage` only as a tiebreak.
|
|
402
|
+
*
|
|
403
|
+
* Runs at the ONE point new findings can appear — a task reaching a terminal state — and writes the
|
|
404
|
+
* outcome back onto the owning records, so a supersession survives a restart rather than being
|
|
405
|
+
* recomputed (and possibly recomputed differently) on every read.
|
|
406
|
+
*
|
|
407
|
+
* EVERY finding is passed in, not just the active ones. Resolving over the active subset makes the
|
|
408
|
+
* result depend on the order tasks happen to finish: a finding that beat a weak rival in wave 1 can
|
|
409
|
+
* itself lose in wave 2, and the wave-1 loser is then left pointing at a superseded finding — a
|
|
410
|
+
* broken chain nothing heals. Re-resolving the whole set each round is order-independent and gives
|
|
411
|
+
* the same answer as one pass over the final set.
|
|
412
|
+
*/
|
|
413
|
+
private resolveFindingConflicts;
|
|
376
414
|
/**
|
|
377
415
|
* A bounded, fenced brief of what the agents have already established (Phase 3.5).
|
|
378
416
|
*
|