ai-runtime-engine 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/LICENSE +15 -0
- package/README.md +489 -0
- package/dist/artifacts/artifacts.d.ts +28 -0
- package/dist/artifacts/artifacts.js +46 -0
- package/dist/benchmark/benchmark.d.ts +23 -0
- package/dist/benchmark/benchmark.js +40 -0
- package/dist/cli/cli.d.ts +6 -0
- package/dist/cli/cli.js +161 -0
- package/dist/cli/commands/cleanup.d.ts +47 -0
- package/dist/cli/commands/cleanup.js +96 -0
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.js +28 -0
- package/dist/cli/commands/doctor.d.ts +57 -0
- package/dist/cli/commands/doctor.js +86 -0
- package/dist/cli/commands/executions.d.ts +9 -0
- package/dist/cli/commands/executions.js +25 -0
- package/dist/cli/commands/info.d.ts +43 -0
- package/dist/cli/commands/info.js +53 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.js +75 -0
- package/dist/cli/commands/inspect.d.ts +16 -0
- package/dist/cli/commands/inspect.js +60 -0
- package/dist/cli/commands/phase2.d.ts +22 -0
- package/dist/cli/commands/phase2.js +83 -0
- package/dist/cli/commands/route.d.ts +14 -0
- package/dist/cli/commands/route.js +49 -0
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +37 -0
- package/dist/cli/commands/setup.d.ts +34 -0
- package/dist/cli/commands/setup.js +104 -0
- package/dist/cli/commands/skills.d.ts +28 -0
- package/dist/cli/commands/skills.js +48 -0
- package/dist/cli/commands/test.d.ts +7 -0
- package/dist/cli/commands/test.js +29 -0
- package/dist/cli/context.d.ts +12 -0
- package/dist/cli/context.js +16 -0
- package/dist/cli/interactive/repl.d.ts +6 -0
- package/dist/cli/interactive/repl.js +45 -0
- package/dist/cli/interactive/session.d.ts +36 -0
- package/dist/cli/interactive/session.js +356 -0
- package/dist/cli/prompt.d.ts +6 -0
- package/dist/cli/prompt.js +18 -0
- package/dist/cli/render.d.ts +7 -0
- package/dist/cli/render.js +14 -0
- package/dist/comparison/analysis.d.ts +46 -0
- package/dist/comparison/analysis.js +177 -0
- package/dist/comparison/comparator.d.ts +46 -0
- package/dist/comparison/comparator.js +270 -0
- package/dist/comparison/comparison.d.ts +140 -0
- package/dist/comparison/comparison.js +9 -0
- package/dist/comparison/render.d.ts +7 -0
- package/dist/comparison/render.js +66 -0
- package/dist/config/defaults.d.ts +52 -0
- package/dist/config/defaults.js +56 -0
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +50 -0
- package/dist/config/providerDefaults.d.ts +17 -0
- package/dist/config/providerDefaults.js +61 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/schema.js +78 -0
- package/dist/context/budget.d.ts +13 -0
- package/dist/context/budget.js +17 -0
- package/dist/context/compiler.d.ts +61 -0
- package/dist/context/compiler.js +125 -0
- package/dist/context/tokens.d.ts +19 -0
- package/dist/context/tokens.js +38 -0
- package/dist/conversations/conversations.d.ts +38 -0
- package/dist/conversations/conversations.js +64 -0
- package/dist/core/capabilities/evidence.d.ts +40 -0
- package/dist/core/capabilities/evidence.js +102 -0
- package/dist/core/capabilities/overlay.d.ts +15 -0
- package/dist/core/capabilities/overlay.js +0 -0
- package/dist/core/capabilities/taxonomy.d.ts +19 -0
- package/dist/core/capabilities/taxonomy.js +25 -0
- package/dist/core/fallback/errors.d.ts +30 -0
- package/dist/core/fallback/errors.js +80 -0
- package/dist/core/fallback/fallback.d.ts +40 -0
- package/dist/core/fallback/fallback.js +82 -0
- package/dist/core/fallback/retryPolicy.d.ts +11 -0
- package/dist/core/fallback/retryPolicy.js +14 -0
- package/dist/core/health/health.d.ts +3 -0
- package/dist/core/health/health.js +5 -0
- package/dist/core/health/monitor.d.ts +23 -0
- package/dist/core/health/monitor.js +82 -0
- package/dist/core/policies/budget.d.ts +19 -0
- package/dist/core/policies/budget.js +37 -0
- package/dist/core/registry/builtinTasks.d.ts +8 -0
- package/dist/core/registry/builtinTasks.js +54 -0
- package/dist/core/registry/registry.d.ts +18 -0
- package/dist/core/registry/registry.js +33 -0
- package/dist/core/registry/taskRegistry.d.ts +15 -0
- package/dist/core/registry/taskRegistry.js +30 -0
- package/dist/core/router/confidence.d.ts +7 -0
- package/dist/core/router/confidence.js +20 -0
- package/dist/core/router/dimensions.d.ts +16 -0
- package/dist/core/router/dimensions.js +60 -0
- package/dist/core/router/executor.d.ts +16 -0
- package/dist/core/router/executor.js +25 -0
- package/dist/core/router/filter.d.ts +34 -0
- package/dist/core/router/filter.js +113 -0
- package/dist/core/router/normalize.d.ts +30 -0
- package/dist/core/router/normalize.js +119 -0
- package/dist/core/router/request.d.ts +4 -0
- package/dist/core/router/request.js +21 -0
- package/dist/core/router/router.d.ts +32 -0
- package/dist/core/router/router.js +195 -0
- package/dist/core/router/routingPrefs.d.ts +11 -0
- package/dist/core/router/routingPrefs.js +30 -0
- package/dist/core/router/scorer.d.ts +19 -0
- package/dist/core/router/scorer.js +50 -0
- package/dist/core/router/weights.d.ts +9 -0
- package/dist/core/router/weights.js +31 -0
- package/dist/core/validation/validator.d.ts +16 -0
- package/dist/core/validation/validator.js +33 -0
- package/dist/discovery/modelCatalog.d.ts +28 -0
- package/dist/discovery/modelCatalog.js +105 -0
- package/dist/discovery/openapi.d.ts +25 -0
- package/dist/discovery/openapi.js +76 -0
- package/dist/executions/checkpoint.d.ts +26 -0
- package/dist/executions/checkpoint.js +114 -0
- package/dist/executions/execution.d.ts +51 -0
- package/dist/executions/execution.js +8 -0
- package/dist/executions/store.d.ts +52 -0
- package/dist/executions/store.js +124 -0
- package/dist/generation/generateAdapter.d.ts +17 -0
- package/dist/generation/generateAdapter.js +30 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +107 -0
- package/dist/learning/feedback.d.ts +9 -0
- package/dist/learning/feedback.js +18 -0
- package/dist/learning/learningStore.d.ts +68 -0
- package/dist/learning/learningStore.js +138 -0
- package/dist/learning/performanceStore.d.ts +27 -0
- package/dist/learning/performanceStore.js +0 -0
- package/dist/marketplace/presets.d.ts +24 -0
- package/dist/marketplace/presets.js +52 -0
- package/dist/mcp/mcp.d.ts +31 -0
- package/dist/mcp/mcp.js +54 -0
- package/dist/memory/bm25.d.ts +16 -0
- package/dist/memory/bm25.js +56 -0
- package/dist/memory/classifier.d.ts +14 -0
- package/dist/memory/classifier.js +17 -0
- package/dist/memory/memory.d.ts +80 -0
- package/dist/memory/memory.js +191 -0
- package/dist/orchestration/executor.d.ts +35 -0
- package/dist/orchestration/executor.js +65 -0
- package/dist/orchestration/orchestrator.d.ts +42 -0
- package/dist/orchestration/orchestrator.js +63 -0
- package/dist/orchestration/plan.d.ts +37 -0
- package/dist/orchestration/plan.js +70 -0
- package/dist/orchestration/planner.d.ts +29 -0
- package/dist/orchestration/planner.js +69 -0
- package/dist/plugin/ai.d.ts +82 -0
- package/dist/plugin/ai.js +167 -0
- package/dist/probing/probe.d.ts +25 -0
- package/dist/probing/probe.js +63 -0
- package/dist/providers/factory.d.ts +18 -0
- package/dist/providers/factory.js +54 -0
- package/dist/providers/httpClient.d.ts +34 -0
- package/dist/providers/httpClient.js +80 -0
- package/dist/providers/httpProvider.d.ts +49 -0
- package/dist/providers/httpProvider.js +135 -0
- package/dist/providers/mock/demo.d.ts +13 -0
- package/dist/providers/mock/demo.js +58 -0
- package/dist/providers/mock/mockProvider.d.ts +35 -0
- package/dist/providers/mock/mockProvider.js +121 -0
- package/dist/providers/mock/scenarios.d.ts +44 -0
- package/dist/providers/mock/scenarios.js +30 -0
- package/dist/providers/provider.d.ts +26 -0
- package/dist/providers/provider.js +11 -0
- package/dist/providers/wire/anthropicWire.d.ts +6 -0
- package/dist/providers/wire/anthropicWire.js +83 -0
- package/dist/providers/wire/openaiWire.d.ts +7 -0
- package/dist/providers/wire/openaiWire.js +81 -0
- package/dist/providers/wire/registry.d.ts +8 -0
- package/dist/providers/wire/registry.js +20 -0
- package/dist/providers/wire/types.d.ts +39 -0
- package/dist/providers/wire/types.js +24 -0
- package/dist/runtime/config.d.ts +31 -0
- package/dist/runtime/config.js +121 -0
- package/dist/runtime/context.d.ts +34 -0
- package/dist/runtime/context.js +11 -0
- package/dist/runtime/events.d.ts +99 -0
- package/dist/runtime/events.js +82 -0
- package/dist/runtime/host.d.ts +27 -0
- package/dist/runtime/host.js +7 -0
- package/dist/runtime/intent/classifier.d.ts +30 -0
- package/dist/runtime/intent/classifier.js +60 -0
- package/dist/runtime/intent/signals.d.ts +19 -0
- package/dist/runtime/intent/signals.js +46 -0
- package/dist/runtime/modes/availability.d.ts +11 -0
- package/dist/runtime/modes/availability.js +17 -0
- package/dist/runtime/modes/chat.d.ts +18 -0
- package/dist/runtime/modes/chat.js +67 -0
- package/dist/runtime/modes/modeResolver.d.ts +43 -0
- package/dist/runtime/modes/modeResolver.js +78 -0
- package/dist/runtime/policy.d.ts +72 -0
- package/dist/runtime/policy.js +59 -0
- package/dist/runtime/providerView.d.ts +62 -0
- package/dist/runtime/providerView.js +105 -0
- package/dist/runtime/routing.d.ts +26 -0
- package/dist/runtime/routing.js +65 -0
- package/dist/runtime/runtime.d.ts +191 -0
- package/dist/runtime/runtime.js +718 -0
- package/dist/runtime/types.d.ts +153 -0
- package/dist/runtime/types.js +9 -0
- package/dist/runtime/workspace/detectors.d.ts +15 -0
- package/dist/runtime/workspace/detectors.js +57 -0
- package/dist/runtime/workspace/workspace.d.ts +29 -0
- package/dist/runtime/workspace/workspace.js +116 -0
- package/dist/security/credentials.d.ts +26 -0
- package/dist/security/credentials.js +34 -0
- package/dist/security/redact.d.ts +16 -0
- package/dist/security/redact.js +57 -0
- package/dist/skills/builtins/fileAnalyzer.d.ts +7 -0
- package/dist/skills/builtins/fileAnalyzer.js +47 -0
- package/dist/skills/builtins/repositoryAnalyzer.d.ts +6 -0
- package/dist/skills/builtins/repositoryAnalyzer.js +47 -0
- package/dist/skills/discovery.d.ts +61 -0
- package/dist/skills/discovery.js +211 -0
- package/dist/skills/manifest.d.ts +30 -0
- package/dist/skills/manifest.js +75 -0
- package/dist/skills/registry.d.ts +15 -0
- package/dist/skills/registry.js +22 -0
- package/dist/skills/skill.d.ts +64 -0
- package/dist/skills/skill.js +8 -0
- package/dist/store/area.d.ts +54 -0
- package/dist/store/area.js +164 -0
- package/dist/store/paths.d.ts +15 -0
- package/dist/store/paths.js +48 -0
- package/dist/store/store.d.ts +59 -0
- package/dist/store/store.js +140 -0
- package/dist/telemetry/sinks/file.d.ts +12 -0
- package/dist/telemetry/sinks/file.js +28 -0
- package/dist/telemetry/telemetry.d.ts +36 -0
- package/dist/telemetry/telemetry.js +63 -0
- package/dist/tools/builtins/filesystem.d.ts +7 -0
- package/dist/tools/builtins/filesystem.js +53 -0
- package/dist/tools/builtins/git.d.ts +10 -0
- package/dist/tools/builtins/git.js +66 -0
- package/dist/tools/builtins/shell.d.ts +17 -0
- package/dist/tools/builtins/shell.js +91 -0
- package/dist/tools/jail.d.ts +12 -0
- package/dist/tools/jail.js +98 -0
- package/dist/tools/permissions.d.ts +25 -0
- package/dist/tools/permissions.js +24 -0
- package/dist/tools/registry.d.ts +10 -0
- package/dist/tools/registry.js +20 -0
- package/dist/tools/runner.d.ts +23 -0
- package/dist/tools/runner.js +64 -0
- package/dist/tools/tool.d.ts +53 -0
- package/dist/tools/tool.js +24 -0
- package/dist/tools/untrusted.d.ts +13 -0
- package/dist/tools/untrusted.js +30 -0
- package/dist/types.d.ts +460 -0
- package/dist/types.js +12 -0
- package/dist/util/clock.d.ts +6 -0
- package/dist/util/clock.js +4 -0
- package/dist/util/extractJson.d.ts +8 -0
- package/dist/util/extractJson.js +54 -0
- package/dist/verification/verify.d.ts +26 -0
- package/dist/verification/verify.js +67 -0
- package/docs/GUIDE.md +358 -0
- package/docs/README.md +21 -0
- package/docs/architecture.md +78 -0
- package/docs/router.md +376 -0
- package/docs/security.md +55 -0
- package/package.json +67 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic-native wire (`POST {baseUrl}/v1/messages`). A distinct shape from OpenAI-compat:
|
|
3
|
+
* `x-api-key` + `anthropic-version` headers, top-level `system`, `content[]`/`stop_reason` response.
|
|
4
|
+
*/
|
|
5
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
6
|
+
function mapStop(reason) {
|
|
7
|
+
switch (reason) {
|
|
8
|
+
case 'end_turn':
|
|
9
|
+
case 'stop_sequence':
|
|
10
|
+
return 'stop';
|
|
11
|
+
case 'max_tokens':
|
|
12
|
+
return 'length';
|
|
13
|
+
case 'tool_use':
|
|
14
|
+
return 'tool_calls';
|
|
15
|
+
default:
|
|
16
|
+
return 'stop';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function userContent(req) {
|
|
20
|
+
const parts = req.input.parts;
|
|
21
|
+
if (!parts || parts.length === 0)
|
|
22
|
+
return req.input.text ?? '';
|
|
23
|
+
const blocks = [];
|
|
24
|
+
if (req.input.text)
|
|
25
|
+
blocks.push({ type: 'text', text: req.input.text });
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
if (part.kind === 'text')
|
|
28
|
+
blocks.push({ type: 'text', text: part.text });
|
|
29
|
+
else if (part.kind === 'image') {
|
|
30
|
+
blocks.push({ type: 'image', source: { type: 'base64', media_type: part.mime, data: part.data.replace(/^data:[^,]+,/, '') } });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return blocks;
|
|
34
|
+
}
|
|
35
|
+
export const anthropicWire = {
|
|
36
|
+
shape: 'anthropic',
|
|
37
|
+
buildRequest(req, ctx) {
|
|
38
|
+
const base = String(ctx.baseUrl).replace(/\/+$/, '');
|
|
39
|
+
const body = {
|
|
40
|
+
model: ctx.model,
|
|
41
|
+
max_tokens: ctx.maxTokens,
|
|
42
|
+
temperature: req.params?.temperature ?? 0,
|
|
43
|
+
messages: [{ role: 'user', content: userContent(req) }],
|
|
44
|
+
};
|
|
45
|
+
if (req.system)
|
|
46
|
+
body.system = req.system;
|
|
47
|
+
if (req.params?.topP !== undefined)
|
|
48
|
+
body.top_p = req.params.topP;
|
|
49
|
+
if (req.params?.stop !== undefined)
|
|
50
|
+
body.stop_sequences = req.params.stop;
|
|
51
|
+
if (req.tools && req.tools.length > 0) {
|
|
52
|
+
body.tools = req.tools.map((t) => ({ name: t.name, ...(t.description ? { description: t.description } : {}), input_schema: t.parameters ?? { type: 'object' } }));
|
|
53
|
+
}
|
|
54
|
+
const headers = {
|
|
55
|
+
'content-type': 'application/json',
|
|
56
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
57
|
+
...(ctx.headers ?? {}),
|
|
58
|
+
};
|
|
59
|
+
if (ctx.apiKey)
|
|
60
|
+
headers['x-api-key'] = ctx.apiKey;
|
|
61
|
+
return { url: `${base}/v1/messages`, headers, body };
|
|
62
|
+
},
|
|
63
|
+
readResponse(json) {
|
|
64
|
+
const j = json;
|
|
65
|
+
const blocks = j.content ?? [];
|
|
66
|
+
const text = blocks
|
|
67
|
+
.filter((b) => b.type === 'text')
|
|
68
|
+
.map((b) => b.text ?? '')
|
|
69
|
+
.join('');
|
|
70
|
+
const parsed = {
|
|
71
|
+
finishReason: mapStop(j.stop_reason),
|
|
72
|
+
usage: { inputTokens: j.usage?.input_tokens, outputTokens: j.usage?.output_tokens },
|
|
73
|
+
};
|
|
74
|
+
if (text)
|
|
75
|
+
parsed.text = text;
|
|
76
|
+
const toolBlocks = blocks.filter((b) => b.type === 'tool_use');
|
|
77
|
+
if (toolBlocks.length > 0) {
|
|
78
|
+
const toolCalls = toolBlocks.map((b) => ({ ...(b.id ? { id: b.id } : {}), name: b.name ?? '', arguments: b.input }));
|
|
79
|
+
parsed.toolCalls = toolCalls;
|
|
80
|
+
}
|
|
81
|
+
return parsed;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-compatible wire (`POST {baseUrl}/chat/completions`). This one shape covers Groq, Cerebras,
|
|
3
|
+
* OpenRouter, Mistral, Ollama's OpenAI endpoint, Gemini's OpenAI-compat endpoint, and any generic
|
|
4
|
+
* gateway, with a `tools` branch for function-calling.
|
|
5
|
+
*/
|
|
6
|
+
import type { WireModule } from './types.js';
|
|
7
|
+
export declare const openaiWire: WireModule;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI-compatible wire (`POST {baseUrl}/chat/completions`). This one shape covers Groq, Cerebras,
|
|
3
|
+
* OpenRouter, Mistral, Ollama's OpenAI endpoint, Gemini's OpenAI-compat endpoint, and any generic
|
|
4
|
+
* gateway, with a `tools` branch for function-calling.
|
|
5
|
+
*/
|
|
6
|
+
import { toMultimodalContent } from './types.js';
|
|
7
|
+
function mapFinish(reason) {
|
|
8
|
+
switch (reason) {
|
|
9
|
+
case 'stop':
|
|
10
|
+
return 'stop';
|
|
11
|
+
case 'length':
|
|
12
|
+
return 'length';
|
|
13
|
+
case 'tool_calls':
|
|
14
|
+
return 'tool_calls';
|
|
15
|
+
case 'content_filter':
|
|
16
|
+
return 'content_filter';
|
|
17
|
+
default:
|
|
18
|
+
return 'stop';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const openaiWire = {
|
|
22
|
+
shape: 'openai',
|
|
23
|
+
buildRequest(req, ctx, jsonMode) {
|
|
24
|
+
const base = String(ctx.baseUrl).replace(/\/+$/, '');
|
|
25
|
+
const messages = [];
|
|
26
|
+
if (req.system)
|
|
27
|
+
messages.push({ role: 'system', content: req.system });
|
|
28
|
+
messages.push({ role: 'user', content: toMultimodalContent(req.input.text, req.input.parts) });
|
|
29
|
+
const wantsJson = jsonMode && (req.output?.format === 'json' || req.output?.format === 'structured_output');
|
|
30
|
+
const body = {
|
|
31
|
+
model: ctx.model,
|
|
32
|
+
temperature: req.params?.temperature ?? 0,
|
|
33
|
+
max_tokens: ctx.maxTokens,
|
|
34
|
+
messages,
|
|
35
|
+
};
|
|
36
|
+
if (req.params?.topP !== undefined)
|
|
37
|
+
body.top_p = req.params.topP;
|
|
38
|
+
if (req.params?.stop !== undefined)
|
|
39
|
+
body.stop = req.params.stop;
|
|
40
|
+
if (wantsJson)
|
|
41
|
+
body.response_format = { type: 'json_object' };
|
|
42
|
+
if (req.tools && req.tools.length > 0) {
|
|
43
|
+
body.tools = req.tools.map((t) => ({
|
|
44
|
+
type: 'function',
|
|
45
|
+
function: { name: t.name, ...(t.description ? { description: t.description } : {}), ...(t.parameters ? { parameters: t.parameters } : {}) },
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
const headers = { 'content-type': 'application/json', ...(ctx.headers ?? {}) };
|
|
49
|
+
if (ctx.apiKey)
|
|
50
|
+
headers.authorization = `Bearer ${ctx.apiKey}`;
|
|
51
|
+
return { url: `${base}/chat/completions`, headers, body };
|
|
52
|
+
},
|
|
53
|
+
readResponse(json) {
|
|
54
|
+
const j = json;
|
|
55
|
+
const choice = j.choices?.[0];
|
|
56
|
+
const parsed = {
|
|
57
|
+
finishReason: mapFinish(choice?.finish_reason),
|
|
58
|
+
usage: { inputTokens: j.usage?.prompt_tokens, outputTokens: j.usage?.completion_tokens },
|
|
59
|
+
};
|
|
60
|
+
const content = choice?.message?.content;
|
|
61
|
+
if (content)
|
|
62
|
+
parsed.text = content;
|
|
63
|
+
const rawCalls = choice?.message?.tool_calls;
|
|
64
|
+
if (rawCalls && rawCalls.length > 0) {
|
|
65
|
+
const toolCalls = rawCalls.map((c) => {
|
|
66
|
+
let args = c.function?.arguments;
|
|
67
|
+
if (typeof args === 'string') {
|
|
68
|
+
try {
|
|
69
|
+
args = JSON.parse(args);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* leave raw string if not JSON */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { ...(c.id ? { id: c.id } : {}), name: c.function?.name ?? '', arguments: args };
|
|
76
|
+
});
|
|
77
|
+
parsed.toolCalls = toolCalls;
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wireShape -> wire-module factory. Open for extension: register a new shape without editing the
|
|
3
|
+
* existing wires. Lives in the providers layer, never in core, so adding a novel protocol never
|
|
4
|
+
* touches routing/scoring.
|
|
5
|
+
*/
|
|
6
|
+
import type { WireModule, WireShape } from './types.js';
|
|
7
|
+
export declare function getWire(shape: WireShape): WireModule;
|
|
8
|
+
export declare function registerWire(wire: WireModule): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wireShape -> wire-module factory. Open for extension: register a new shape without editing the
|
|
3
|
+
* existing wires. Lives in the providers layer, never in core, so adding a novel protocol never
|
|
4
|
+
* touches routing/scoring.
|
|
5
|
+
*/
|
|
6
|
+
import { openaiWire } from './openaiWire.js';
|
|
7
|
+
import { anthropicWire } from './anthropicWire.js';
|
|
8
|
+
const WIRES = new Map([
|
|
9
|
+
['openai', openaiWire],
|
|
10
|
+
['anthropic', anthropicWire],
|
|
11
|
+
]);
|
|
12
|
+
export function getWire(shape) {
|
|
13
|
+
const wire = WIRES.get(shape);
|
|
14
|
+
if (!wire)
|
|
15
|
+
throw new Error(`unknown wire shape '${shape}'`);
|
|
16
|
+
return wire;
|
|
17
|
+
}
|
|
18
|
+
export function registerWire(wire) {
|
|
19
|
+
WIRES.set(wire.shape, wire);
|
|
20
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire-module interface. A wire is the PURE mapping between the normalized AIRequest/AIResponse
|
|
3
|
+
* and one provider WIRE SHAPE (OpenAI-compatible `/chat/completions`, Anthropic `/v1/messages`, ...).
|
|
4
|
+
* Purity means the request is assertable without a socket, with no network dependency. Wires live in
|
|
5
|
+
* the providers layer; `core/` never sees them.
|
|
6
|
+
*/
|
|
7
|
+
import type { AIRequest, FinishReason, ToolCall } from '../../types.js';
|
|
8
|
+
export type WireShape = 'openai' | 'anthropic';
|
|
9
|
+
export interface WireContext {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
model: string;
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
maxTokens: number;
|
|
15
|
+
}
|
|
16
|
+
export interface WireRequest {
|
|
17
|
+
url: string;
|
|
18
|
+
headers: Record<string, string>;
|
|
19
|
+
body: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface WireParsed {
|
|
22
|
+
text?: string;
|
|
23
|
+
json?: unknown;
|
|
24
|
+
toolCalls?: ToolCall[];
|
|
25
|
+
usage?: {
|
|
26
|
+
inputTokens?: number;
|
|
27
|
+
outputTokens?: number;
|
|
28
|
+
};
|
|
29
|
+
finishReason: FinishReason;
|
|
30
|
+
}
|
|
31
|
+
export interface WireModule {
|
|
32
|
+
readonly shape: WireShape;
|
|
33
|
+
/** Pure request builder. `jsonMode` requests a JSON response format where the shape supports it. */
|
|
34
|
+
buildRequest(req: AIRequest, ctx: WireContext, jsonMode: boolean): WireRequest;
|
|
35
|
+
/** Parse a successful response body into normalized pieces. */
|
|
36
|
+
readResponse(json: unknown): WireParsed;
|
|
37
|
+
}
|
|
38
|
+
/** Turn input parts into an OpenAI-style content value (string when text-only, array when multimodal). */
|
|
39
|
+
export declare function toMultimodalContent(text: string | undefined, parts: AIRequest['input']['parts']): unknown;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire-module interface. A wire is the PURE mapping between the normalized AIRequest/AIResponse
|
|
3
|
+
* and one provider WIRE SHAPE (OpenAI-compatible `/chat/completions`, Anthropic `/v1/messages`, ...).
|
|
4
|
+
* Purity means the request is assertable without a socket, with no network dependency. Wires live in
|
|
5
|
+
* the providers layer; `core/` never sees them.
|
|
6
|
+
*/
|
|
7
|
+
/** Turn input parts into an OpenAI-style content value (string when text-only, array when multimodal). */
|
|
8
|
+
export function toMultimodalContent(text, parts) {
|
|
9
|
+
if (!parts || parts.length === 0)
|
|
10
|
+
return text ?? '';
|
|
11
|
+
const content = [];
|
|
12
|
+
if (text)
|
|
13
|
+
content.push({ type: 'text', text });
|
|
14
|
+
for (const part of parts) {
|
|
15
|
+
if (part.kind === 'text')
|
|
16
|
+
content.push({ type: 'text', text: part.text });
|
|
17
|
+
else if (part.kind === 'image') {
|
|
18
|
+
const url = part.data.startsWith('http') || part.data.startsWith('data:') ? part.data : `data:${part.mime};base64,${part.data}`;
|
|
19
|
+
content.push({ type: 'image_url', image_url: { url } });
|
|
20
|
+
}
|
|
21
|
+
// audio/video/file/pdf are provider-specific and left to native adapters; ignored here.
|
|
22
|
+
}
|
|
23
|
+
return content;
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration loader. Canonical file: `.ai-runtime/config.yaml` (runtime settings + the
|
|
3
|
+
* router config). WRAPS, never forks, the existing strict `parseConfig`: a small strict zod fragment
|
|
4
|
+
* validates/plucks the runtime-level keys (runtime:, and learning/verification/budget/policy — which
|
|
5
|
+
* the root schema rejects), then delegates the remainder to parseConfig so its strictness and
|
|
6
|
+
* inline-secret rejection are preserved. Root `ai-runtime.yaml` remains a supported fallback.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: `src/config/load.ts` and `AI.load()` are intentionally untouched — the CLI's existing behavior
|
|
9
|
+
* is frozen until Phase 2 migrates it.
|
|
10
|
+
*/
|
|
11
|
+
import type { RuntimeConfig } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Parse a runtime config object (from `.ai-runtime/config.yaml` or a legacy root file). Runtime-level
|
|
14
|
+
* keys are validated and split off; everything else is validated by the existing strict router schema.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseRuntimeConfig(raw: unknown): RuntimeConfig;
|
|
17
|
+
export interface LoadedRuntimeConfig {
|
|
18
|
+
config: RuntimeConfig;
|
|
19
|
+
configFile?: string;
|
|
20
|
+
source: 'runtime-dir' | 'root' | 'explicit' | 'none';
|
|
21
|
+
}
|
|
22
|
+
/** Locate `<root>/.ai-runtime/config.{yaml,yml,json}`. */
|
|
23
|
+
export declare function findRuntimeConfigFile(workspaceRoot: string): string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Load the runtime config for a workspace. dotenv from the workspace root first; then explicit path >
|
|
26
|
+
* `.ai-runtime/config.*` > legacy root `ai-runtime.*` > empty.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadRuntimeConfig(opts: {
|
|
29
|
+
workspaceRoot: string;
|
|
30
|
+
explicitPath?: string;
|
|
31
|
+
}): LoadedRuntimeConfig;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration loader. Canonical file: `.ai-runtime/config.yaml` (runtime settings + the
|
|
3
|
+
* router config). WRAPS, never forks, the existing strict `parseConfig`: a small strict zod fragment
|
|
4
|
+
* validates/plucks the runtime-level keys (runtime:, and learning/verification/budget/policy — which
|
|
5
|
+
* the root schema rejects), then delegates the remainder to parseConfig so its strictness and
|
|
6
|
+
* inline-secret rejection are preserved. Root `ai-runtime.yaml` remains a supported fallback.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: `src/config/load.ts` and `AI.load()` are intentionally untouched — the CLI's existing behavior
|
|
9
|
+
* is frozen until Phase 2 migrates it.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { resolve, join } from 'node:path';
|
|
13
|
+
import { parse as parseYaml } from 'yaml';
|
|
14
|
+
import { config as loadDotenv } from 'dotenv';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { parseConfig, STRATEGIES } from '../config/schema.js';
|
|
17
|
+
import { findConfigFile } from '../config/load.js';
|
|
18
|
+
import { AIError } from '../core/fallback/errors.js';
|
|
19
|
+
import { RUNTIME_MODES } from './types.js';
|
|
20
|
+
/** Runtime-level keys that live in `.ai-runtime/config.yaml` but are NOT part of the strict root schema. */
|
|
21
|
+
const RUNTIME_ONLY_KEYS = ['runtime', 'learning', 'verification', 'budget', 'policy', 'permissions', 'routing'];
|
|
22
|
+
const runtimeSettings = z.object({ defaultMode: z.enum(RUNTIME_MODES).optional(), defaultStrategy: z.enum(STRATEGIES).optional(), context: z.object({ maxTokens: z.number().optional() }).strict().optional(), skills: z.object({ paths: z.array(z.string()).optional(), packages: z.array(z.string()).optional(), autoload: z.boolean().optional() }).strict().optional() }).strict();
|
|
23
|
+
const learning = z.object({ enabled: z.boolean().optional() }).strict();
|
|
24
|
+
const verification = z.object({ enabled: z.boolean().optional() }).strict();
|
|
25
|
+
const budget = z.object({ maxCostUsd: z.number().optional(), maxCalls: z.number().optional() }).strict();
|
|
26
|
+
const policy = z
|
|
27
|
+
.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() })
|
|
28
|
+
.strict();
|
|
29
|
+
const permissions = z
|
|
30
|
+
.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() })
|
|
31
|
+
.strict();
|
|
32
|
+
const routing = z
|
|
33
|
+
.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() })
|
|
34
|
+
.strict();
|
|
35
|
+
const runtimeFragment = z
|
|
36
|
+
.object({ runtime: runtimeSettings.optional(), learning: learning.optional(), verification: verification.optional(), budget: budget.optional(), policy: policy.optional(), permissions: permissions.optional(), routing: routing.optional() })
|
|
37
|
+
.partial();
|
|
38
|
+
/**
|
|
39
|
+
* Parse a runtime config object (from `.ai-runtime/config.yaml` or a legacy root file). Runtime-level
|
|
40
|
+
* keys are validated and split off; everything else is validated by the existing strict router schema.
|
|
41
|
+
*/
|
|
42
|
+
export function parseRuntimeConfig(raw) {
|
|
43
|
+
if (raw === null || typeof raw !== 'object')
|
|
44
|
+
throw new AIError('invalid ai-runtime config: expected a mapping', { category: 'CONFIG' });
|
|
45
|
+
const obj = raw;
|
|
46
|
+
// Validate the runtime-level fragment.
|
|
47
|
+
const fragResult = runtimeFragment.safeParse(pick(obj, RUNTIME_ONLY_KEYS));
|
|
48
|
+
if (!fragResult.success) {
|
|
49
|
+
const issues = fragResult.error.issues.map((i) => ` - ${i.path.join('.') || '(root)'}: ${i.message}`).join('\n');
|
|
50
|
+
throw new AIError(`invalid ai-runtime config:\n${issues}`, { category: 'CONFIG' });
|
|
51
|
+
}
|
|
52
|
+
const frag = fragResult.data;
|
|
53
|
+
// Delegate the remainder (providers, strategy, weights, defaults, privacy, telemetry, tasks) to the strict router schema.
|
|
54
|
+
const rest = omit(obj, RUNTIME_ONLY_KEYS);
|
|
55
|
+
if (!('providers' in rest))
|
|
56
|
+
rest.providers = [];
|
|
57
|
+
const router = parseConfig(rest);
|
|
58
|
+
// Fold the runtime-only router keys back onto the RouterConfig (they exist on the type; resolveConfig reads them).
|
|
59
|
+
if (frag.learning)
|
|
60
|
+
router.learning = frag.learning;
|
|
61
|
+
if (frag.verification)
|
|
62
|
+
router.verification = frag.verification;
|
|
63
|
+
if (frag.budget)
|
|
64
|
+
router.budget = frag.budget;
|
|
65
|
+
if (frag.policy)
|
|
66
|
+
router.policy = frag.policy;
|
|
67
|
+
// Top-level `routing:` is folded into the runtime settings (a runtime concern, resolved with env + per-run).
|
|
68
|
+
const runtimeOut = { ...(frag.runtime ?? {}), ...(frag.routing ? { routing: frag.routing } : {}) };
|
|
69
|
+
return { ...(Object.keys(runtimeOut).length ? { runtime: runtimeOut } : {}), ...(frag.permissions ? { permissions: frag.permissions } : {}), router };
|
|
70
|
+
}
|
|
71
|
+
const RUNTIME_DIR_FILES = ['config.yaml', 'config.yml', 'config.json'];
|
|
72
|
+
/** Locate `<root>/.ai-runtime/config.{yaml,yml,json}`. */
|
|
73
|
+
export function findRuntimeConfigFile(workspaceRoot) {
|
|
74
|
+
for (const name of RUNTIME_DIR_FILES) {
|
|
75
|
+
const full = join(workspaceRoot, '.ai-runtime', name);
|
|
76
|
+
if (existsSync(full))
|
|
77
|
+
return full;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Load the runtime config for a workspace. dotenv from the workspace root first; then explicit path >
|
|
83
|
+
* `.ai-runtime/config.*` > legacy root `ai-runtime.*` > empty.
|
|
84
|
+
*/
|
|
85
|
+
export function loadRuntimeConfig(opts) {
|
|
86
|
+
loadDotenv({ path: resolve(opts.workspaceRoot, '.env'), quiet: true });
|
|
87
|
+
if (opts.explicitPath) {
|
|
88
|
+
const file = resolve(opts.workspaceRoot, opts.explicitPath);
|
|
89
|
+
// A missing explicit path yields an empty config (matches the legacy loader; a typo'd -c never throws).
|
|
90
|
+
if (!existsSync(file))
|
|
91
|
+
return { config: { router: { providers: [] } }, source: 'none' };
|
|
92
|
+
return { config: parseRuntimeConfig(readParsed(file)), configFile: file, source: 'explicit' };
|
|
93
|
+
}
|
|
94
|
+
const runtimeFile = findRuntimeConfigFile(opts.workspaceRoot);
|
|
95
|
+
if (runtimeFile)
|
|
96
|
+
return { config: parseRuntimeConfig(readParsed(runtimeFile)), configFile: runtimeFile, source: 'runtime-dir' };
|
|
97
|
+
const rootFile = findConfigFile(opts.workspaceRoot);
|
|
98
|
+
if (rootFile)
|
|
99
|
+
return { config: parseRuntimeConfig(readParsed(rootFile)), configFile: rootFile, source: 'root' };
|
|
100
|
+
return { config: { router: { providers: [] } }, source: 'none' };
|
|
101
|
+
}
|
|
102
|
+
// ── helpers ──
|
|
103
|
+
function readParsed(file) {
|
|
104
|
+
const text = readFileSync(file, 'utf8');
|
|
105
|
+
return file.endsWith('.json') ? JSON.parse(text) : parseYaml(text);
|
|
106
|
+
}
|
|
107
|
+
function pick(obj, keys) {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const k of keys)
|
|
110
|
+
if (k in obj)
|
|
111
|
+
out[k] = obj[k];
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function omit(obj, keys) {
|
|
115
|
+
const drop = new Set(keys);
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const [k, v] of Object.entries(obj))
|
|
118
|
+
if (!drop.has(k))
|
|
119
|
+
out[k] = v;
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuntimeContext — the ONE runtime-state container, assembled once per run and passed by reference to
|
|
3
|
+
* every Runtime layer (mode resolution, context compiler, planner, orchestrator, executor, memory,
|
|
4
|
+
* skills, tools). No other overlapping runtime-state abstraction exists. NOTE: this is runtime STATE;
|
|
5
|
+
* the ContextCompiler's output (compiled model input) is a different, transient thing.
|
|
6
|
+
*
|
|
7
|
+
* Slots are optional and fill in as phases land (P1: runId/request/workspace/policy/budget/provider
|
|
8
|
+
* state; P3: conversation/memory; P6: plan; P7: execution). Later-phase slots are typed `unknown` for
|
|
9
|
+
* now so they widen without a breaking change.
|
|
10
|
+
*/
|
|
11
|
+
import type { ExecutionPolicy } from './policy.js';
|
|
12
|
+
import type { RuntimeRunInput } from './types.js';
|
|
13
|
+
import type { WorkspaceInfo } from './workspace/workspace.js';
|
|
14
|
+
export interface RuntimeContext {
|
|
15
|
+
/** Identity of this runtime.run() invocation. */
|
|
16
|
+
runId: string;
|
|
17
|
+
/** Caller idempotency identity, if supplied. */
|
|
18
|
+
requestId?: string;
|
|
19
|
+
request: RuntimeRunInput;
|
|
20
|
+
workspace?: WorkspaceInfo;
|
|
21
|
+
/** The single policy authority for this run. */
|
|
22
|
+
policy: ExecutionPolicy;
|
|
23
|
+
budget?: {
|
|
24
|
+
maxCostUsd?: number;
|
|
25
|
+
maxCalls?: number;
|
|
26
|
+
};
|
|
27
|
+
/** Read-only provider/model state views (discovery/health), attached by later phases. */
|
|
28
|
+
providerState?: unknown;
|
|
29
|
+
conversation?: unknown;
|
|
30
|
+
memory?: unknown;
|
|
31
|
+
plan?: unknown;
|
|
32
|
+
execution?: unknown;
|
|
33
|
+
metadata?: Record<string, unknown>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RuntimeContext — the ONE runtime-state container, assembled once per run and passed by reference to
|
|
3
|
+
* every Runtime layer (mode resolution, context compiler, planner, orchestrator, executor, memory,
|
|
4
|
+
* skills, tools). No other overlapping runtime-state abstraction exists. NOTE: this is runtime STATE;
|
|
5
|
+
* the ContextCompiler's output (compiled model input) is a different, transient thing.
|
|
6
|
+
*
|
|
7
|
+
* Slots are optional and fill in as phases land (P1: runId/request/workspace/policy/budget/provider
|
|
8
|
+
* state; P3: conversation/memory; P6: plan; P7: execution). Later-phase slots are typed `unknown` for
|
|
9
|
+
* now so they widen without a breaking change.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime lifecycle events — a SEPARATE union from the router's TelemetryEvent (which is frozen). The
|
|
3
|
+
* emitter fans out redacted, metadata-only events to sinks, and never lets a throwing observer break a
|
|
4
|
+
* run. Every event carries the correlation `runId` so a host can join a live stream to a RunResult.
|
|
5
|
+
*
|
|
6
|
+
* Token-streaming extension point: `response.delta` is reserved here (content-bearing, redacted like
|
|
7
|
+
* everything else). Provider token streaming is NOT implemented in 1.0 — no delta is ever emitted yet —
|
|
8
|
+
* but declaring the arm keeps the host/emitter architecture ready for it without a breaking change.
|
|
9
|
+
*/
|
|
10
|
+
import type { Clock } from '../util/clock.js';
|
|
11
|
+
import type { ErrorCategory } from '../types.js';
|
|
12
|
+
import type { ExecutableMode, ModeSource, RuntimeMode, RuntimeStatus } from './types.js';
|
|
13
|
+
export type RuntimeEvent = {
|
|
14
|
+
type: 'runtime.started';
|
|
15
|
+
ts: number;
|
|
16
|
+
runId: string;
|
|
17
|
+
requested: RuntimeMode;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'mode.selected';
|
|
20
|
+
ts: number;
|
|
21
|
+
runId: string;
|
|
22
|
+
requested: RuntimeMode;
|
|
23
|
+
selected: ExecutableMode;
|
|
24
|
+
executed: ExecutableMode;
|
|
25
|
+
source: ModeSource;
|
|
26
|
+
confidence: number;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'clarification.requested';
|
|
29
|
+
ts: number;
|
|
30
|
+
runId: string;
|
|
31
|
+
question: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'provider.selected';
|
|
34
|
+
ts: number;
|
|
35
|
+
runId: string;
|
|
36
|
+
providerId: string;
|
|
37
|
+
model: string;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'provider.failed';
|
|
40
|
+
ts: number;
|
|
41
|
+
runId: string;
|
|
42
|
+
providerId: string;
|
|
43
|
+
model: string;
|
|
44
|
+
category?: ErrorCategory;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'run.completed';
|
|
47
|
+
ts: number;
|
|
48
|
+
runId: string;
|
|
49
|
+
ok: boolean;
|
|
50
|
+
status: RuntimeStatus;
|
|
51
|
+
confidence: number;
|
|
52
|
+
} | {
|
|
53
|
+
type: 'response.delta';
|
|
54
|
+
ts: number;
|
|
55
|
+
runId: string;
|
|
56
|
+
text: string;
|
|
57
|
+
};
|
|
58
|
+
/** Distributive Omit so an event can be emitted without pre-stamping `ts`. */
|
|
59
|
+
type WithoutTs<T> = T extends unknown ? Omit<T, 'ts'> : never;
|
|
60
|
+
export type RuntimeEventInput = WithoutTs<RuntimeEvent>;
|
|
61
|
+
/** A sink for runtime events. Mirrors the telemetry sink shape (emit + optional events query). */
|
|
62
|
+
export interface RuntimeEventSink {
|
|
63
|
+
emit(event: RuntimeEvent): void;
|
|
64
|
+
events?(): RuntimeEvent[];
|
|
65
|
+
}
|
|
66
|
+
/** In-memory ring buffer of recent events. */
|
|
67
|
+
export declare class MemoryEventSink implements RuntimeEventSink {
|
|
68
|
+
private readonly limit;
|
|
69
|
+
private readonly buffer;
|
|
70
|
+
constructor(limit?: number);
|
|
71
|
+
emit(event: RuntimeEvent): void;
|
|
72
|
+
events(): RuntimeEvent[];
|
|
73
|
+
}
|
|
74
|
+
/** Forwards each event to a user callback; a throwing callback can never break a run. */
|
|
75
|
+
export declare class CallbackEventSink implements RuntimeEventSink {
|
|
76
|
+
private readonly forward;
|
|
77
|
+
constructor(forward: (event: RuntimeEvent) => void);
|
|
78
|
+
emit(event: RuntimeEvent): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fans out redacted events to a built-in memory buffer plus any attached sinks. Redaction happens once
|
|
82
|
+
* here, at entry, so downstream sinks receive already-scrubbed metadata.
|
|
83
|
+
*/
|
|
84
|
+
export declare class RuntimeEmitter {
|
|
85
|
+
private readonly clock;
|
|
86
|
+
private readonly memory;
|
|
87
|
+
private readonly sinks;
|
|
88
|
+
constructor(options?: {
|
|
89
|
+
clock?: Clock;
|
|
90
|
+
sinks?: RuntimeEventSink[];
|
|
91
|
+
});
|
|
92
|
+
/** Stamp `ts`, redact once, and fan out. A throwing sink can never break a run. */
|
|
93
|
+
emit(input: RuntimeEventInput): void;
|
|
94
|
+
/** Subscribe to future events; returns an unsubscribe function. */
|
|
95
|
+
on(listener: (event: RuntimeEvent) => void): () => void;
|
|
96
|
+
/** Recent events from the built-in buffer. */
|
|
97
|
+
recent(): RuntimeEvent[];
|
|
98
|
+
}
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime lifecycle events — a SEPARATE union from the router's TelemetryEvent (which is frozen). The
|
|
3
|
+
* emitter fans out redacted, metadata-only events to sinks, and never lets a throwing observer break a
|
|
4
|
+
* run. Every event carries the correlation `runId` so a host can join a live stream to a RunResult.
|
|
5
|
+
*
|
|
6
|
+
* Token-streaming extension point: `response.delta` is reserved here (content-bearing, redacted like
|
|
7
|
+
* everything else). Provider token streaming is NOT implemented in 1.0 — no delta is ever emitted yet —
|
|
8
|
+
* but declaring the arm keeps the host/emitter architecture ready for it without a breaking change.
|
|
9
|
+
*/
|
|
10
|
+
import { systemClock } from '../util/clock.js';
|
|
11
|
+
import { redact } from '../security/redact.js';
|
|
12
|
+
/** In-memory ring buffer of recent events. */
|
|
13
|
+
export class MemoryEventSink {
|
|
14
|
+
limit;
|
|
15
|
+
buffer = [];
|
|
16
|
+
constructor(limit = 500) {
|
|
17
|
+
this.limit = limit;
|
|
18
|
+
}
|
|
19
|
+
emit(event) {
|
|
20
|
+
this.buffer.push(event);
|
|
21
|
+
if (this.buffer.length > this.limit)
|
|
22
|
+
this.buffer.shift();
|
|
23
|
+
}
|
|
24
|
+
events() {
|
|
25
|
+
return [...this.buffer];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Forwards each event to a user callback; a throwing callback can never break a run. */
|
|
29
|
+
export class CallbackEventSink {
|
|
30
|
+
forward;
|
|
31
|
+
constructor(forward) {
|
|
32
|
+
this.forward = forward;
|
|
33
|
+
}
|
|
34
|
+
emit(event) {
|
|
35
|
+
try {
|
|
36
|
+
this.forward(event);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* observability must never break a run */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Fans out redacted events to a built-in memory buffer plus any attached sinks. Redaction happens once
|
|
45
|
+
* here, at entry, so downstream sinks receive already-scrubbed metadata.
|
|
46
|
+
*/
|
|
47
|
+
export class RuntimeEmitter {
|
|
48
|
+
clock;
|
|
49
|
+
memory = new MemoryEventSink();
|
|
50
|
+
sinks;
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.clock = options.clock ?? systemClock;
|
|
53
|
+
this.sinks = options.sinks ?? [];
|
|
54
|
+
}
|
|
55
|
+
/** Stamp `ts`, redact once, and fan out. A throwing sink can never break a run. */
|
|
56
|
+
emit(input) {
|
|
57
|
+
const event = redact({ ...input, ts: this.clock.now() });
|
|
58
|
+
this.memory.emit(event);
|
|
59
|
+
for (const sink of this.sinks) {
|
|
60
|
+
try {
|
|
61
|
+
sink.emit(event);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* observability must never break a run */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Subscribe to future events; returns an unsubscribe function. */
|
|
69
|
+
on(listener) {
|
|
70
|
+
const sink = new CallbackEventSink(listener);
|
|
71
|
+
this.sinks.push(sink);
|
|
72
|
+
return () => {
|
|
73
|
+
const i = this.sinks.indexOf(sink);
|
|
74
|
+
if (i >= 0)
|
|
75
|
+
this.sinks.splice(i, 1);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Recent events from the built-in buffer. */
|
|
79
|
+
recent() {
|
|
80
|
+
return this.memory.events();
|
|
81
|
+
}
|
|
82
|
+
}
|