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,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime — the high-level facade OVER the existing `AI` router. It resolves a mode + policy, assembles
|
|
3
|
+
* a RuntimeContext, executes chat through `AI.run()`, and streams redacted lifecycle events. It never
|
|
4
|
+
* routes or scores; all model selection stays in the one router. Unimplemented modes degrade to chat.
|
|
5
|
+
*/
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { AI } from '../plugin/ai.js';
|
|
8
|
+
import { setCredentialResolver } from '../security/credentials.js';
|
|
9
|
+
import { loadRuntimeConfig } from './config.js';
|
|
10
|
+
import { detectWorkspace } from './workspace/workspace.js';
|
|
11
|
+
import { HeuristicIntentClassifier } from './intent/classifier.js';
|
|
12
|
+
import { resolveMode, resolveStrategy } from './modes/modeResolver.js';
|
|
13
|
+
import { resolvePolicy } from './policy.js';
|
|
14
|
+
import { CHAT_TASK, buildChatRequest, toRuntimeResult, inputText } from './modes/chat.js';
|
|
15
|
+
import { RuntimeEmitter } from './events.js';
|
|
16
|
+
import { systemClock } from '../util/clock.js';
|
|
17
|
+
import { RuntimeStore } from '../store/store.js';
|
|
18
|
+
import { ConversationStore } from '../conversations/conversations.js';
|
|
19
|
+
import { MemoryStore } from '../memory/memory.js';
|
|
20
|
+
import { classifyMemory } from '../memory/classifier.js';
|
|
21
|
+
import { summarizeWorkspace } from './workspace/workspace.js';
|
|
22
|
+
import { compileContext } from '../context/compiler.js';
|
|
23
|
+
import { resolveContextBudget } from '../context/budget.js';
|
|
24
|
+
import { TokenEstimator } from '../context/tokens.js';
|
|
25
|
+
import { ToolRegistry } from '../tools/registry.js';
|
|
26
|
+
import { resolvePermissions } from '../tools/permissions.js';
|
|
27
|
+
import { filesystemTool } from '../tools/builtins/filesystem.js';
|
|
28
|
+
import { shellTool } from '../tools/builtins/shell.js';
|
|
29
|
+
import { gitTool } from '../tools/builtins/git.js';
|
|
30
|
+
import { SkillRegistry } from '../skills/registry.js';
|
|
31
|
+
import { fileAnalyzerSkill } from '../skills/builtins/fileAnalyzer.js';
|
|
32
|
+
import { repositoryAnalyzerSkill } from '../skills/builtins/repositoryAnalyzer.js';
|
|
33
|
+
import { discoverSkills, scanRepoForSkills, loadSkillSource, loadSkillPackage } from '../skills/discovery.js';
|
|
34
|
+
import { orchestrate } from '../orchestration/orchestrator.js';
|
|
35
|
+
import { executePlan } from '../orchestration/executor.js';
|
|
36
|
+
import { ExecutionStore } from '../executions/store.js';
|
|
37
|
+
import { RESUMABLE, TERMINAL } from '../executions/execution.js';
|
|
38
|
+
import { captureCheckpoint, reconcile } from '../executions/checkpoint.js';
|
|
39
|
+
import { ArtifactStore } from '../artifacts/artifacts.js';
|
|
40
|
+
import { compare } from '../comparison/comparator.js';
|
|
41
|
+
import { renderComparison } from '../comparison/render.js';
|
|
42
|
+
import { LearningStore } from '../learning/learningStore.js';
|
|
43
|
+
import { resolveRoutingPrefs, withPreferredProviders } from './routing.js';
|
|
44
|
+
import { tokenize } from '../memory/bm25.js';
|
|
45
|
+
let runCounter = 0;
|
|
46
|
+
function nextRunId() {
|
|
47
|
+
runCounter += 1;
|
|
48
|
+
return `run_${runCounter.toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
49
|
+
}
|
|
50
|
+
function numFromEnv(env, key) {
|
|
51
|
+
const raw = env[key];
|
|
52
|
+
if (raw === undefined || raw === '')
|
|
53
|
+
return undefined;
|
|
54
|
+
const n = Number(raw);
|
|
55
|
+
return Number.isFinite(n) ? n : undefined;
|
|
56
|
+
}
|
|
57
|
+
export class Runtime {
|
|
58
|
+
_ai;
|
|
59
|
+
settingsValue;
|
|
60
|
+
classifier;
|
|
61
|
+
env;
|
|
62
|
+
workspace;
|
|
63
|
+
emitter;
|
|
64
|
+
_store;
|
|
65
|
+
_memory;
|
|
66
|
+
_conversations;
|
|
67
|
+
estimator = new TokenEstimator();
|
|
68
|
+
_tools = new ToolRegistry();
|
|
69
|
+
_skills = new SkillRegistry();
|
|
70
|
+
_executions;
|
|
71
|
+
_artifacts;
|
|
72
|
+
_learning;
|
|
73
|
+
workspaceRoot;
|
|
74
|
+
configPermissions;
|
|
75
|
+
approval;
|
|
76
|
+
_configFile;
|
|
77
|
+
_loadedSkillSources = [];
|
|
78
|
+
constructor(config = { router: { providers: [] } }, options = {}, workspace) {
|
|
79
|
+
this._ai = new AI(config.router, options.ai);
|
|
80
|
+
this.settingsValue = config.runtime ?? {};
|
|
81
|
+
this.classifier = options.classifier ?? new HeuristicIntentClassifier();
|
|
82
|
+
this.env = options.ai?.env ?? process.env;
|
|
83
|
+
if (workspace)
|
|
84
|
+
this.workspace = workspace;
|
|
85
|
+
this.configPermissions = config.permissions ?? {};
|
|
86
|
+
if (options.host?.approval)
|
|
87
|
+
this.approval = options.host.approval;
|
|
88
|
+
if (options.configFile)
|
|
89
|
+
this._configFile = options.configFile;
|
|
90
|
+
// Local store (conversations + memory). Disabled → NullAreas (stateless mode).
|
|
91
|
+
const clock = options.ai?.clock ?? systemClock;
|
|
92
|
+
const root = workspace?.root ?? resolveCwd(options.workspace ?? options.host?.workspace);
|
|
93
|
+
this.workspaceRoot = root;
|
|
94
|
+
this._store = new RuntimeStore({ workspaceRoot: root, ...(workspace?.name ? { workspaceName: workspace.name } : {}), env: this.env, clock, enabled: options.persistence !== 'disabled' });
|
|
95
|
+
this._memory = new MemoryStore(this._store, clock);
|
|
96
|
+
this._conversations = new ConversationStore(this._store.conversations(), clock);
|
|
97
|
+
this._executions = new ExecutionStore(this._store.executions(), { clock });
|
|
98
|
+
this._artifacts = new ArtifactStore(this._store.artifacts(), clock);
|
|
99
|
+
this._learning = new LearningStore(this._store.learning());
|
|
100
|
+
// Built-in tools + generic skills (QA-neutral). Users add more via registerTool/registerSkill.
|
|
101
|
+
this._tools.register(filesystemTool).register(shellTool).register(gitTool);
|
|
102
|
+
this._skills.register(fileAnalyzerSkill).register(repositoryAnalyzerSkill);
|
|
103
|
+
// Host integration: attach an event sink and register a credential resolver if supplied.
|
|
104
|
+
const host = options.host;
|
|
105
|
+
this.emitter = new RuntimeEmitter({
|
|
106
|
+
...(options.ai?.clock ? { clock: options.ai.clock } : {}),
|
|
107
|
+
...(host?.eventSink ? { sinks: [host.eventSink] } : {}),
|
|
108
|
+
});
|
|
109
|
+
if (host?.credentialResolver)
|
|
110
|
+
setCredentialResolver(host.credentialResolver);
|
|
111
|
+
// Register the generic chat task only if the user hasn't defined their own.
|
|
112
|
+
const hasChat = this._ai.tasksList().some((t) => t.id === 'chat' || t.aliases?.includes('chat'));
|
|
113
|
+
if (!hasChat)
|
|
114
|
+
this._ai.registerTask(CHAT_TASK);
|
|
115
|
+
}
|
|
116
|
+
/** Build a Runtime from a workspace: load config (.ai-runtime/config.yaml > root fallback), detect workspace. */
|
|
117
|
+
static async load(options = {}) {
|
|
118
|
+
const workspaceRoot = resolveCwd(options.workspace ?? options.host?.workspace);
|
|
119
|
+
const loaded = loadRuntimeConfig({ workspaceRoot, ...(options.config ? { explicitPath: options.config } : {}) });
|
|
120
|
+
const workspace = options.detectWorkspace === false ? undefined : detectWorkspace(workspaceRoot, options.ai?.clock);
|
|
121
|
+
const rt = new Runtime(loaded.config, { ...options, ...(loaded.configFile ? { configFile: loaded.configFile } : {}) }, workspace);
|
|
122
|
+
await rt.autoloadSkills();
|
|
123
|
+
return rt;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Load consented local skills — the conventional `.ai-runtime/skills/` dir plus any `skills.paths` in
|
|
127
|
+
* config — and register them. Manifests are data; module skills run the user's own code, which is why
|
|
128
|
+
* only these consented locations auto-load. Off when `skills.autoload: false`. Resilient: a broken
|
|
129
|
+
* source is recorded (see `skillSources()`), never fatal. Called by `Runtime.load`.
|
|
130
|
+
*/
|
|
131
|
+
async autoloadSkills() {
|
|
132
|
+
const settings = this.settingsValue.skills ?? {};
|
|
133
|
+
if (settings.autoload === false)
|
|
134
|
+
return;
|
|
135
|
+
let sources;
|
|
136
|
+
try {
|
|
137
|
+
sources = discoverSkills({ root: this.workspaceRoot, ...(settings.paths ? { paths: settings.paths } : {}) });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return; // discovery must never break construction
|
|
141
|
+
}
|
|
142
|
+
const loaded = await Promise.all(sources.map((s) => loadSkillSource(s)));
|
|
143
|
+
// npm skill packs (trusted dependencies, resolved from node_modules — not workspace-jailed).
|
|
144
|
+
const pkgLoaded = await Promise.all((settings.packages ?? []).map((p) => loadSkillPackage(p)));
|
|
145
|
+
const all = [...loaded, ...pkgLoaded];
|
|
146
|
+
for (const l of all)
|
|
147
|
+
for (const sk of l.skills)
|
|
148
|
+
this._skills.register(sk);
|
|
149
|
+
this._loadedSkillSources = all;
|
|
150
|
+
}
|
|
151
|
+
/** What autoloadSkills loaded (or failed to), for `info`/`doctor`/`skills` reporting. */
|
|
152
|
+
skillSources() {
|
|
153
|
+
return this._loadedSkillSources;
|
|
154
|
+
}
|
|
155
|
+
/** Scan the workspace for skill-looking files elsewhere (report only — nothing is loaded). */
|
|
156
|
+
discoverSkillCandidates() {
|
|
157
|
+
return scanRepoForSkills({ root: this.workspaceRoot });
|
|
158
|
+
}
|
|
159
|
+
/** Explicitly load chosen skill sources (e.g. candidates the user picked) and register them. */
|
|
160
|
+
async loadSkillSources(sources) {
|
|
161
|
+
const loaded = await Promise.all(sources.map((s) => loadSkillSource(s)));
|
|
162
|
+
for (const l of loaded)
|
|
163
|
+
for (const sk of l.skills)
|
|
164
|
+
this._skills.register(sk);
|
|
165
|
+
this._loadedSkillSources = [...this._loadedSkillSources, ...loaded];
|
|
166
|
+
return loaded;
|
|
167
|
+
}
|
|
168
|
+
/** The config file this runtime resolved (`.ai-runtime/config.yaml` or a fallback), if any. */
|
|
169
|
+
get configFile() {
|
|
170
|
+
return this._configFile;
|
|
171
|
+
}
|
|
172
|
+
/** The inner router facade — register providers/tasks/MCP sources here. */
|
|
173
|
+
get ai() {
|
|
174
|
+
return this._ai;
|
|
175
|
+
}
|
|
176
|
+
/** The local runtime store (conversations, memory, indexes, cache). */
|
|
177
|
+
get store() {
|
|
178
|
+
return this._store;
|
|
179
|
+
}
|
|
180
|
+
/** Durable scoped memory. */
|
|
181
|
+
get memory() {
|
|
182
|
+
return this._memory;
|
|
183
|
+
}
|
|
184
|
+
/** Persistent conversation history. */
|
|
185
|
+
get conversations() {
|
|
186
|
+
return this._conversations;
|
|
187
|
+
}
|
|
188
|
+
memoryEnabled() {
|
|
189
|
+
return this._memory.enabled && this.env.AI_MEMORY_ENABLED !== 'false';
|
|
190
|
+
}
|
|
191
|
+
/** Durable cross-run learning (provider/skill/workflow/plan outcomes; feeds soft prefer signals). */
|
|
192
|
+
get learning() {
|
|
193
|
+
return this._learning;
|
|
194
|
+
}
|
|
195
|
+
learningEnabled() {
|
|
196
|
+
return this._learning.enabled && this.env.AI_LEARNING_ENABLED !== 'false';
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Effective exclude/prefer routing for a run: env + project config + per-run (unioned), then learned
|
|
200
|
+
* soft-prefer folded in — but never re-admitting an excluded provider. Learning stays subordinate to
|
|
201
|
+
* every hard gate (exclude/privacy/pin/capability/budget), which the router filter enforces.
|
|
202
|
+
*/
|
|
203
|
+
effectiveRouting(perRun) {
|
|
204
|
+
const base = resolveRoutingPrefs({
|
|
205
|
+
env: this.env,
|
|
206
|
+
...(this.settingsValue.routing ? { config: this.settingsValue.routing } : {}),
|
|
207
|
+
...(perRun ? { perRun } : {}),
|
|
208
|
+
});
|
|
209
|
+
if (!this.learningEnabled())
|
|
210
|
+
return base;
|
|
211
|
+
return withPreferredProviders(base, this._learning.preferredProviders());
|
|
212
|
+
}
|
|
213
|
+
/** Apply free-text feedback (e.g. "that worked" / "wrong root cause") to the most recent outcome. */
|
|
214
|
+
feedback(text, opts) {
|
|
215
|
+
return this._learning.feedback(text, opts ?? {});
|
|
216
|
+
}
|
|
217
|
+
/** A coarse goal pattern (first few significant tokens) keying workflow/plan learning. */
|
|
218
|
+
goalType(text) {
|
|
219
|
+
const toks = tokenize(text).slice(0, 3);
|
|
220
|
+
return toks.length ? toks.join('-') : 'general';
|
|
221
|
+
}
|
|
222
|
+
/** Resolve exact-version SkillRefs for the skill steps of a plan (for skill/workflow learning). */
|
|
223
|
+
planSkillRefs(plan) {
|
|
224
|
+
if (!plan)
|
|
225
|
+
return [];
|
|
226
|
+
const refs = [];
|
|
227
|
+
for (const step of plan.steps) {
|
|
228
|
+
if (!step.skill)
|
|
229
|
+
continue;
|
|
230
|
+
const version = this._skills.get(step.skill)?.version;
|
|
231
|
+
if (version)
|
|
232
|
+
refs.push({ id: step.skill, version });
|
|
233
|
+
}
|
|
234
|
+
return refs;
|
|
235
|
+
}
|
|
236
|
+
/** Register a tool. Chainable. */
|
|
237
|
+
registerTool(tool) {
|
|
238
|
+
this._tools.register(tool);
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
tools() {
|
|
242
|
+
return this._tools.list();
|
|
243
|
+
}
|
|
244
|
+
/** Register a skill. Chainable. */
|
|
245
|
+
registerSkill(skill) {
|
|
246
|
+
this._skills.register(skill);
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
/** Skills whose required tools are all registered. */
|
|
250
|
+
skills() {
|
|
251
|
+
return this._skills.match({ tools: this._tools.ids() });
|
|
252
|
+
}
|
|
253
|
+
/** The resolved tool permissions for this runtime (config grants over deny-by-default defaults). */
|
|
254
|
+
permissions() {
|
|
255
|
+
return resolvePermissions(this.configPermissions);
|
|
256
|
+
}
|
|
257
|
+
toolContext(overrides) {
|
|
258
|
+
return {
|
|
259
|
+
workspaceRoot: this.workspaceRoot,
|
|
260
|
+
permissions: resolvePermissions({ ...this.configPermissions, ...(overrides?.permissions ?? {}) }),
|
|
261
|
+
...(this.approval ? { approval: this.approval } : {}),
|
|
262
|
+
...(overrides?.signal ? { signal: overrides.signal } : {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Invoke a registered tool with the runtime's resolved permissions + host approval. */
|
|
266
|
+
async runTool(id, input, overrides) {
|
|
267
|
+
const tool = this._tools.get(id);
|
|
268
|
+
if (!tool)
|
|
269
|
+
return { ok: false, error: { code: 'NOT_FOUND', message: `no tool '${id}'` } };
|
|
270
|
+
return tool.execute(input, this.toolContext(overrides));
|
|
271
|
+
}
|
|
272
|
+
/** Run a skill end-to-end (plan → execute → validate) with tools bound to the runtime's permissions. */
|
|
273
|
+
async runSkill(id, input, overrides) {
|
|
274
|
+
const skill = this._skills.get(id);
|
|
275
|
+
if (!skill)
|
|
276
|
+
throw new Error(`no skill '${id}'`);
|
|
277
|
+
const toolCtx = this.toolContext(overrides);
|
|
278
|
+
const ctx = {
|
|
279
|
+
input,
|
|
280
|
+
ai: this._ai,
|
|
281
|
+
callTool: async (toolId, toolInput) => {
|
|
282
|
+
const tool = this._tools.get(toolId);
|
|
283
|
+
if (!tool)
|
|
284
|
+
return { ok: false, error: { code: 'NOT_FOUND', message: `no tool '${toolId}'` } };
|
|
285
|
+
return tool.execute(toolInput, toolCtx);
|
|
286
|
+
},
|
|
287
|
+
...(overrides?.signal ? { signal: overrides.signal } : {}),
|
|
288
|
+
};
|
|
289
|
+
const plan = await skill.plan(ctx);
|
|
290
|
+
const result = await skill.execute(ctx, plan);
|
|
291
|
+
const validation = await skill.validate(result, ctx);
|
|
292
|
+
return { plan, result, validation };
|
|
293
|
+
}
|
|
294
|
+
workspaceInfo() {
|
|
295
|
+
return this.workspace;
|
|
296
|
+
}
|
|
297
|
+
settings() {
|
|
298
|
+
return this.settingsValue;
|
|
299
|
+
}
|
|
300
|
+
/** Subscribe to runtime lifecycle events; returns an unsubscribe function. */
|
|
301
|
+
on(listener) {
|
|
302
|
+
return this.emitter.on(listener);
|
|
303
|
+
}
|
|
304
|
+
/** Recent runtime events (redacted, metadata only). */
|
|
305
|
+
events() {
|
|
306
|
+
return this.emitter.recent();
|
|
307
|
+
}
|
|
308
|
+
async run(input) {
|
|
309
|
+
const req = typeof input === 'string' ? { input } : input;
|
|
310
|
+
const runId = nextRunId();
|
|
311
|
+
const text = inputText(req.input);
|
|
312
|
+
this.emitter.emit({ type: 'runtime.started', runId, requested: req.mode ?? 'auto' });
|
|
313
|
+
const modeResult = await resolveMode({
|
|
314
|
+
...(req.mode !== undefined ? { explicit: req.mode } : {}),
|
|
315
|
+
...(this.settingsValue.defaultMode !== undefined ? { configDefault: this.settingsValue.defaultMode } : {}),
|
|
316
|
+
...(this.env.AI_DEFAULT_MODE !== undefined ? { envDefault: this.env.AI_DEFAULT_MODE } : {}),
|
|
317
|
+
intent: { text, hasParts: typeof req.input !== 'string' && !!req.input.parts?.length, ...(this.workspace ? { workspace: this.workspace } : {}) },
|
|
318
|
+
classifier: this.classifier,
|
|
319
|
+
});
|
|
320
|
+
const resolution = modeResult.resolution;
|
|
321
|
+
this.emitter.emit({ type: 'mode.selected', runId, requested: resolution.requested, selected: resolution.selected, executed: resolution.executed, source: resolution.source, confidence: resolution.confidence });
|
|
322
|
+
const strategy = resolveStrategy({
|
|
323
|
+
...(req.strategy !== undefined ? { explicit: req.strategy } : {}),
|
|
324
|
+
...(this.settingsValue.defaultStrategy !== undefined ? { configDefault: this.settingsValue.defaultStrategy } : {}),
|
|
325
|
+
...(this.env.AI_DEFAULT_STRATEGY !== undefined ? { envDefault: this.env.AI_DEFAULT_STRATEGY } : {}),
|
|
326
|
+
});
|
|
327
|
+
const envLimits = { maxCostUsd: numFromEnv(this.env, 'AI_MAX_COST_USD'), maxCalls: numFromEnv(this.env, 'AI_MAX_CALLS') };
|
|
328
|
+
const overrides = { ...(req.dryRun ? { dryRun: true } : {}), ...(this.configPermissions ? { permissions: this.configPermissions } : {}) };
|
|
329
|
+
const policy = resolvePolicy({ mode: resolution.executed, overrides, settings: this.settingsValue, env: envLimits });
|
|
330
|
+
const routing = this.effectiveRouting(req.routing);
|
|
331
|
+
const context = {
|
|
332
|
+
runId,
|
|
333
|
+
...(req.requestId ? { requestId: req.requestId } : {}),
|
|
334
|
+
request: req,
|
|
335
|
+
...(this.workspace ? { workspace: this.workspace } : {}),
|
|
336
|
+
policy,
|
|
337
|
+
...(envLimits.maxCostUsd !== undefined || envLimits.maxCalls !== undefined ? { budget: { ...(envLimits.maxCostUsd !== undefined ? { maxCostUsd: envLimits.maxCostUsd } : {}), ...(envLimits.maxCalls !== undefined ? { maxCalls: envLimits.maxCalls } : {}) } } : {}),
|
|
338
|
+
};
|
|
339
|
+
// Orchestration modes (plan/execute/orchestrate) plan + drive skills/tools; they do not use the
|
|
340
|
+
// chat memory/context path. Clarification here comes from the planner, not mode ambiguity.
|
|
341
|
+
if (resolution.executed === 'plan' || resolution.executed === 'execute' || resolution.executed === 'orchestrate' || resolution.executed === 'agent' || resolution.executed === 'debug') {
|
|
342
|
+
return this.runOrchestration(resolution.executed, text, resolution, runId, policy, req.requestId, routing);
|
|
343
|
+
}
|
|
344
|
+
// Compare mode: fan out the same task across pins and analyze — read-only, no approval gate.
|
|
345
|
+
if (resolution.executed === 'compare') {
|
|
346
|
+
return this.runCompareMode(req, text, resolution, runId, policy, routing);
|
|
347
|
+
}
|
|
348
|
+
// ── chat path ──
|
|
349
|
+
// Ambiguity: attach non-blocking clarification metadata (a real ask-back UX arrives with the REPL).
|
|
350
|
+
const clarification = buildClarification(modeResult);
|
|
351
|
+
if (clarification)
|
|
352
|
+
this.emitter.emit({ type: 'clarification.requested', runId, question: clarification.question });
|
|
353
|
+
// Memory: capture an explicit "remember …" and retrieve relevant facts into context.
|
|
354
|
+
const memTrace = this.applyMemory(text);
|
|
355
|
+
// Compile the model context (workspace summary + memory facts + user system) under a token budget.
|
|
356
|
+
const budget = resolveContextBudget({
|
|
357
|
+
...(req.context?.maxTokens !== undefined ? { perRun: req.context.maxTokens } : {}),
|
|
358
|
+
...(this.settingsValue.context?.maxTokens !== undefined ? { config: this.settingsValue.context.maxTokens } : {}),
|
|
359
|
+
...(this.env.AI_CONTEXT_MAX_TOKENS !== undefined ? { env: this.env.AI_CONTEXT_MAX_TOKENS } : {}),
|
|
360
|
+
});
|
|
361
|
+
const compiled = compileContext(this.contextBlocks(req, context.workspace, memTrace?.retrieved), { budgetTokens: budget, estimator: this.estimator });
|
|
362
|
+
const contextReport = { metrics: compiled.metrics, validation: compiled.validation };
|
|
363
|
+
const runRequest = buildChatRequest(req, strategy, compiled.system || undefined, routing);
|
|
364
|
+
const runResult = await this._ai.run(runRequest);
|
|
365
|
+
this.calibrate(runRequest.system, text, runResult);
|
|
366
|
+
const sel = runResult.routing.selected;
|
|
367
|
+
if (this.learningEnabled()) {
|
|
368
|
+
this._learning.record({ goalType: this.goalType(text), mode: 'chat', ok: runResult.ok, ...(sel ? { providerId: sel.providerId, model: sel.model } : {}), latencyMs: attemptsLatency(runResult) });
|
|
369
|
+
}
|
|
370
|
+
if (sel)
|
|
371
|
+
this.emitter.emit({ type: 'provider.selected', runId, providerId: sel.providerId, model: sel.model });
|
|
372
|
+
else {
|
|
373
|
+
const lastFail = [...runResult.routing.attempts].reverse().find((a) => a.outcome !== 'success');
|
|
374
|
+
if (lastFail)
|
|
375
|
+
this.emitter.emit({ type: 'provider.failed', runId, providerId: lastFail.providerId, model: lastFail.model, ...(lastFail.category ? { category: lastFail.category } : {}) });
|
|
376
|
+
}
|
|
377
|
+
this.emitter.emit({ type: 'run.completed', runId, ok: runResult.ok, status: clarification ? 'waiting_for_clarification' : 'completed', confidence: runResult.confidence });
|
|
378
|
+
const result = toRuntimeResult(runResult, resolution, runId, clarification);
|
|
379
|
+
if (memTrace)
|
|
380
|
+
result.memory = memTrace;
|
|
381
|
+
result.context = contextReport;
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
/** The persistent execution store and its owner-lease machinery. */
|
|
385
|
+
get executionStore() {
|
|
386
|
+
return this._executions;
|
|
387
|
+
}
|
|
388
|
+
/** The artifact store (content owner behind ArtifactRef). */
|
|
389
|
+
get artifacts() {
|
|
390
|
+
return this._artifacts;
|
|
391
|
+
}
|
|
392
|
+
/** Recent persisted executions, newest first. */
|
|
393
|
+
executions() {
|
|
394
|
+
return this._executions.list();
|
|
395
|
+
}
|
|
396
|
+
/** plan/execute/orchestrate/agent/debug: run the orchestrator and persist a resumable Execution. */
|
|
397
|
+
async runOrchestration(mode, goal, resolution, runId, policy, requestId, routing) {
|
|
398
|
+
const effectiveGoal = mode === 'debug' ? `Investigate and diagnose the following, gathering evidence before concluding: ${goal}` : goal;
|
|
399
|
+
// plan mode and dry-run produce no durable work → run without persisting an execution.
|
|
400
|
+
if (mode === 'plan' || policy.dryRun || !this._executions.enabled) {
|
|
401
|
+
const outcome = await orchestrate(this.orchestrateInput(mode, effectiveGoal, policy, routing));
|
|
402
|
+
this.recordOrchestration(mode, goal, outcome);
|
|
403
|
+
return this.mapOutcome(outcome, resolution, runId, undefined);
|
|
404
|
+
}
|
|
405
|
+
// Reserve the execution BEFORE running (persist-in-progress + claim the requestId under the store
|
|
406
|
+
// lock so a concurrent retry with the same requestId dedupes to it).
|
|
407
|
+
const reserved = this._store.withLock(() => {
|
|
408
|
+
if (requestId) {
|
|
409
|
+
const ex = this._executions.byRequestId(requestId);
|
|
410
|
+
if (ex)
|
|
411
|
+
return { exec: ex, existed: true };
|
|
412
|
+
}
|
|
413
|
+
return { exec: this._executions.create({ ...(requestId ? { requestId } : {}), goal, mode, planVersion: 0, status: 'running' }), existed: false };
|
|
414
|
+
});
|
|
415
|
+
if (reserved.existed) {
|
|
416
|
+
this._executions.release(reserved.exec.id);
|
|
417
|
+
return this.resultFromExecution(reserved.exec, resolution, runId);
|
|
418
|
+
}
|
|
419
|
+
const exec = reserved.exec;
|
|
420
|
+
try {
|
|
421
|
+
// Renew the lease while the (possibly long) run is in flight so it can't expire mid-run.
|
|
422
|
+
const outcome = await this.withHeartbeat(exec.id, () => orchestrate(this.orchestrateInput(mode, effectiveGoal, policy, routing)));
|
|
423
|
+
exec.status = this.execStatus(outcome.status);
|
|
424
|
+
exec.observations = outcome.observations;
|
|
425
|
+
if (outcome.plan) {
|
|
426
|
+
exec.plan = outcome.plan;
|
|
427
|
+
exec.planVersion = outcome.plan.version;
|
|
428
|
+
exec.completedSteps = outcome.plan.steps.filter((s) => s.status === 'succeeded').map((s) => s.id);
|
|
429
|
+
}
|
|
430
|
+
if (outcome.status === 'waiting_for_approval')
|
|
431
|
+
exec.pending = { kind: 'approval', action: goal };
|
|
432
|
+
else if (outcome.status === 'waiting_for_clarification' && outcome.clarification)
|
|
433
|
+
exec.pending = { kind: 'clarification', question: outcome.clarification };
|
|
434
|
+
exec.checkpoints.push(captureCheckpoint({ root: this.workspaceRoot, ...(exec.plan ? { plan: exec.plan } : {}), skills: this.skills(), completedSteps: exec.completedSteps }));
|
|
435
|
+
this._executions.commit(exec); // ownership-checked: never clobber a newer owner
|
|
436
|
+
this.recordOrchestration(mode, goal, outcome);
|
|
437
|
+
return this.mapOutcome(outcome, resolution, runId, exec.id);
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
this._executions.release(exec.id);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** Run `fn` while heartbeating the execution lease so a long run never lets the lease expire. */
|
|
444
|
+
async withHeartbeat(id, fn) {
|
|
445
|
+
const timer = setInterval(() => this._executions.heartbeat(id), this._executions.heartbeatMs);
|
|
446
|
+
if (typeof timer.unref === 'function')
|
|
447
|
+
timer.unref();
|
|
448
|
+
try {
|
|
449
|
+
return await fn();
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
clearInterval(timer);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
orchestrateInput(mode, goal, policy, routing) {
|
|
456
|
+
return {
|
|
457
|
+
goal,
|
|
458
|
+
mode,
|
|
459
|
+
ai: this._ai,
|
|
460
|
+
skills: this.skills(),
|
|
461
|
+
tools: this._tools.ids(),
|
|
462
|
+
policy,
|
|
463
|
+
...(routing ? { routing } : {}),
|
|
464
|
+
...(this.approval ? { approval: this.approval } : {}),
|
|
465
|
+
runSkill: (id, input) => this.runSkill(id, input).then((o) => ({ result: o.result, validation: o.validation })),
|
|
466
|
+
runTool: (id, input) => this.runTool(id, input),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/** Record an EXECUTED orchestration outcome for learning. plan-only, dry-run, and waiting states are
|
|
470
|
+
* skipped — no skill ran, so there is no success/failure to learn (recording them would teach noise). */
|
|
471
|
+
recordOrchestration(mode, goal, outcome) {
|
|
472
|
+
if (!this.learningEnabled())
|
|
473
|
+
return;
|
|
474
|
+
if (mode === 'plan' || outcome.status === 'dry-run' || outcome.status.startsWith('waiting'))
|
|
475
|
+
return;
|
|
476
|
+
this._learning.record({ goalType: this.goalType(goal), mode, ok: outcome.status === 'completed', skills: this.planSkillRefs(outcome.plan) });
|
|
477
|
+
}
|
|
478
|
+
execStatus(s) {
|
|
479
|
+
if (s === 'completed' || s === 'dry-run')
|
|
480
|
+
return 'completed';
|
|
481
|
+
if (s === 'failed')
|
|
482
|
+
return 'failed';
|
|
483
|
+
if (s === 'waiting_for_approval')
|
|
484
|
+
return 'waiting_for_input';
|
|
485
|
+
return 'waiting_for_clarification';
|
|
486
|
+
}
|
|
487
|
+
mapOutcome(outcome, resolution, runId, execId) {
|
|
488
|
+
const status = outcome.status === 'dry-run' ? 'completed' : outcome.status;
|
|
489
|
+
const ok = outcome.status === 'completed' || outcome.status === 'dry-run' || outcome.status.startsWith('waiting');
|
|
490
|
+
this.emitter.emit({ type: 'run.completed', runId, ok, status, confidence: ok ? 1 : 0 });
|
|
491
|
+
return {
|
|
492
|
+
ok,
|
|
493
|
+
runId,
|
|
494
|
+
mode: resolution,
|
|
495
|
+
status,
|
|
496
|
+
response: { text: outcome.summary },
|
|
497
|
+
...(outcome.plan ? { plan: outcome.plan } : {}),
|
|
498
|
+
execution: { ...(execId ? { id: execId } : {}), observations: outcome.observations, planHistory: outcome.planHistory },
|
|
499
|
+
...(outcome.clarification ? { clarification: { question: outcome.clarification } } : {}),
|
|
500
|
+
artifacts: [],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
resultFromExecution(exec, resolution, runId) {
|
|
504
|
+
const ok = exec.status !== 'failed' && exec.status !== 'cancelled';
|
|
505
|
+
const status = exec.status === 'completed' ? 'completed' : exec.status === 'failed' || exec.status === 'cancelled' ? 'failed' : exec.status === 'waiting_for_clarification' ? 'waiting_for_clarification' : exec.status === 'waiting_for_input' ? 'waiting_for_approval' : 'completed';
|
|
506
|
+
return {
|
|
507
|
+
ok,
|
|
508
|
+
runId,
|
|
509
|
+
mode: resolution,
|
|
510
|
+
status,
|
|
511
|
+
response: { text: `execution ${exec.id} (${exec.status})` },
|
|
512
|
+
...(exec.plan ? { plan: exec.plan } : {}),
|
|
513
|
+
execution: { id: exec.id, observations: exec.observations, planHistory: exec.plan ? [exec.plan] : [] },
|
|
514
|
+
...(exec.pending?.kind === 'clarification' && exec.pending.question ? { clarification: { question: exec.pending.question } } : {}),
|
|
515
|
+
artifacts: exec.artifacts,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Compare a task across several models/providers and return a normalized comparison (agreement,
|
|
520
|
+
* differences, contradictions, missing info, and an EVIDENCE-first ranking). Runs entirely through the
|
|
521
|
+
* ONE router (`AI.run()` pins) — no second selection engine. Pass a `validate` for evidence-based
|
|
522
|
+
* ranking (beats any judge), or a `judge` pin for a labeled opinion; with neither it reports only.
|
|
523
|
+
*/
|
|
524
|
+
async compare(input) {
|
|
525
|
+
return compare({ ...input, ai: this._ai });
|
|
526
|
+
}
|
|
527
|
+
/** compare mode: fan out over pins (default: every enabled, non-excluded provider) and render. Read-only. */
|
|
528
|
+
async runCompareMode(req, goal, resolution, runId, policy, routing) {
|
|
529
|
+
const opts = req.compare ?? {};
|
|
530
|
+
const pins = opts.pins && opts.pins.length ? opts.pins : this.defaultComparePins(routing);
|
|
531
|
+
if (pins.length < 2) {
|
|
532
|
+
const msg = `Comparison needs at least two providers/models; ${pins.length} available. Configure more providers or pass compare.pins.`;
|
|
533
|
+
this.emitter.emit({ type: 'run.completed', runId, ok: false, status: 'failed', confidence: 0 });
|
|
534
|
+
return { ok: false, runId, mode: resolution, status: 'failed', response: { text: msg }, artifacts: [] };
|
|
535
|
+
}
|
|
536
|
+
const result = await this.compare({
|
|
537
|
+
goal,
|
|
538
|
+
input: req.input,
|
|
539
|
+
...(req.system ? { system: req.system } : {}),
|
|
540
|
+
...(req.output ? { output: req.output } : {}),
|
|
541
|
+
pins,
|
|
542
|
+
...(routing ? { routing } : {}),
|
|
543
|
+
...(opts.judge ? { judge: opts.judge } : {}),
|
|
544
|
+
...(opts.task ? { task: opts.task } : {}),
|
|
545
|
+
...(opts.agreementThreshold !== undefined ? { agreementThreshold: opts.agreementThreshold } : {}),
|
|
546
|
+
policy: {
|
|
547
|
+
...(policy.maxCalls !== undefined ? { maxCalls: policy.maxCalls } : {}),
|
|
548
|
+
...(policy.maxCostUsd !== undefined ? { maxCostUsd: policy.maxCostUsd } : {}),
|
|
549
|
+
maxParallelSteps: policy.maxParallelSteps ?? 2,
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
if (this.learningEnabled())
|
|
553
|
+
this._learning.ingestComparisonSignals(result.signals);
|
|
554
|
+
for (const c of result.candidates)
|
|
555
|
+
if (c.ok && c.selected)
|
|
556
|
+
this.emitter.emit({ type: 'provider.selected', runId, providerId: c.selected.providerId, model: c.selected.model });
|
|
557
|
+
const ok = result.candidates.some((c) => c.ok);
|
|
558
|
+
const status = ok ? 'completed' : 'failed';
|
|
559
|
+
this.emitter.emit({ type: 'run.completed', runId, ok, status, confidence: ok ? 1 : 0 });
|
|
560
|
+
return { ok, runId, mode: resolution, status, response: { text: renderComparison(result).join('\n') }, comparison: result, artifacts: [] };
|
|
561
|
+
}
|
|
562
|
+
/** Default compare pins: one per enabled, non-excluded provider (model left to the provider default). */
|
|
563
|
+
defaultComparePins(routing) {
|
|
564
|
+
const excluded = new Set(routing?.excludeProviders ?? []);
|
|
565
|
+
return this._ai.providers().filter((p) => p.enabled && !excluded.has(p.id)).map((p) => ({ provider: p.id, label: p.id }));
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Resume a persisted execution: acquire its lease, reconcile the workspace, then continue (if
|
|
569
|
+
* unchanged) or replan (on drift) — never blindly. Answers a pending clarification/approval.
|
|
570
|
+
*/
|
|
571
|
+
async resumeExecution(id, opts = {}) {
|
|
572
|
+
const runId = nextRunId();
|
|
573
|
+
const resolution = { requested: 'auto', selected: 'orchestrate', executed: 'orchestrate', source: 'explicit', confidence: 1, reason: 'resume' };
|
|
574
|
+
const acq = this._executions.acquire(id);
|
|
575
|
+
if (!acq.ok || !acq.execution) {
|
|
576
|
+
return { ok: false, runId, mode: resolution, status: 'failed', response: { text: `cannot resume ${id}: ${acq.reason ?? 'unavailable'}` }, artifacts: [] };
|
|
577
|
+
}
|
|
578
|
+
const exec = acq.execution;
|
|
579
|
+
try {
|
|
580
|
+
if (TERMINAL.has(exec.status))
|
|
581
|
+
return this.resultFromExecution(exec, resolution, runId);
|
|
582
|
+
if (!RESUMABLE.has(exec.status))
|
|
583
|
+
return this.resultFromExecution(exec, resolution, runId);
|
|
584
|
+
// Approval gate is PRESERVED across resume: a pending approval requires an explicit decision.
|
|
585
|
+
// approve===false cancels; approve===true proceeds; anything else keeps waiting (never auto-runs).
|
|
586
|
+
if (exec.pending?.kind === 'approval') {
|
|
587
|
+
if (opts.approve === false) {
|
|
588
|
+
exec.status = 'cancelled';
|
|
589
|
+
this._executions.commit(exec);
|
|
590
|
+
return this.resultFromExecution(exec, resolution, runId);
|
|
591
|
+
}
|
|
592
|
+
if (opts.approve !== true) {
|
|
593
|
+
// Still waiting — do NOT execute a plan that was never approved.
|
|
594
|
+
return this.resultFromExecution(exec, resolution, runId);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Reconcile against the checkpoint — drift forces a replan rather than a blind continue.
|
|
598
|
+
const checkpoint = exec.checkpoints[exec.checkpoints.length - 1];
|
|
599
|
+
const recon = checkpoint ? reconcile(checkpoint, this.workspaceRoot, this.skills()) : { drifted: true, reasons: ['no checkpoint'] };
|
|
600
|
+
const goal = exec.pending?.kind === 'clarification' && opts.clarificationAnswer ? `${exec.goal}\n\nClarification: ${opts.clarificationAnswer}` : exec.goal;
|
|
601
|
+
const policy = resolvePolicy({ mode: 'orchestrate', overrides: { autonomy: 'autonomous', approval: 'none', ...(this.configPermissions ? { permissions: this.configPermissions } : {}) }, settings: this.settingsValue });
|
|
602
|
+
// We only reach here for an approval-pending execution if it was explicitly approved.
|
|
603
|
+
const approvedNow = exec.pending?.kind === 'approval';
|
|
604
|
+
const canContinue = !recon.drifted && !!exec.plan && (approvedNow || (exec.completedSteps.length > 0 && !exec.pending));
|
|
605
|
+
let outcome;
|
|
606
|
+
if (canContinue && exec.plan) {
|
|
607
|
+
// No drift → continue: execute the (approved / partially-done) plan, skipping completed steps.
|
|
608
|
+
const exe = await this.withHeartbeat(exec.id, () => executePlan(exec.plan, { ...this.orchestrateRunners(), maxParallelSteps: policy.maxParallelSteps ?? 2, skip: new Set(exec.completedSteps), ...(opts.signal ? { signal: opts.signal } : {}) }));
|
|
609
|
+
outcome = { status: exe.ok ? 'completed' : 'failed', plan: exe.plan, planHistory: [exe.plan], observations: exe.observations, summary: exe.ok ? `resumed and completed "${exec.goal}"` : `resume did not complete "${exec.goal}"` };
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
// Drift, or a pending clarification answer, or no partial progress → replan from the goal.
|
|
613
|
+
// Re-resolve routing so env/config excludes (and learned prefer) still apply to the replan's
|
|
614
|
+
// planning-model call — a hard exclude must not be dropped just because we're resuming.
|
|
615
|
+
outcome = await this.withHeartbeat(exec.id, () => orchestrate(this.orchestrateInput('orchestrate', goal, policy, this.effectiveRouting())));
|
|
616
|
+
}
|
|
617
|
+
exec.status = this.execStatus(outcome.status);
|
|
618
|
+
exec.observations = [...exec.observations, ...outcome.observations];
|
|
619
|
+
if (outcome.plan) {
|
|
620
|
+
exec.plan = outcome.plan;
|
|
621
|
+
exec.planVersion = outcome.plan.version;
|
|
622
|
+
exec.completedSteps = outcome.plan.steps.filter((s) => s.status === 'succeeded').map((s) => s.id);
|
|
623
|
+
}
|
|
624
|
+
delete exec.pending;
|
|
625
|
+
exec.checkpoints.push(captureCheckpoint({ root: this.workspaceRoot, ...(exec.plan ? { plan: exec.plan } : {}), skills: this.skills(), completedSteps: exec.completedSteps }));
|
|
626
|
+
this._executions.commit(exec);
|
|
627
|
+
return this.mapOutcome(outcome, resolution, runId, exec.id);
|
|
628
|
+
}
|
|
629
|
+
finally {
|
|
630
|
+
this._executions.release(id);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
orchestrateRunners() {
|
|
634
|
+
return {
|
|
635
|
+
runSkill: (skillId, input) => this.runSkill(skillId, input).then((o) => ({ result: o.result, validation: o.validation })),
|
|
636
|
+
runTool: (toolId, input) => this.runTool(toolId, input),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/** Mark an execution paused (it can be resumed later). */
|
|
640
|
+
pauseExecution(id) {
|
|
641
|
+
const exec = this._executions.get(id);
|
|
642
|
+
if (!exec || TERMINAL.has(exec.status))
|
|
643
|
+
return false;
|
|
644
|
+
exec.status = 'paused';
|
|
645
|
+
this._executions.save(exec);
|
|
646
|
+
this._executions.release(id);
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
/** Cancel an execution (terminal). */
|
|
650
|
+
cancelExecution(id) {
|
|
651
|
+
const exec = this._executions.get(id);
|
|
652
|
+
if (!exec || TERMINAL.has(exec.status))
|
|
653
|
+
return false;
|
|
654
|
+
exec.status = 'cancelled';
|
|
655
|
+
this._executions.save(exec);
|
|
656
|
+
this._executions.release(id);
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
/** Assemble the context blocks for a chat run: user system (critical), workspace (high), memory (normal). */
|
|
660
|
+
contextBlocks(req, workspace, facts) {
|
|
661
|
+
const blocks = [];
|
|
662
|
+
if (req.system)
|
|
663
|
+
blocks.push({ id: 'user-system', kind: 'system', text: req.system, retention: 'critical' });
|
|
664
|
+
if (req.workspaceContext !== false && workspace)
|
|
665
|
+
blocks.push({ id: 'workspace', kind: 'workspace', text: summarizeWorkspace(workspace), retention: 'high' });
|
|
666
|
+
if (facts && facts.length)
|
|
667
|
+
blocks.push({ id: 'memory', kind: 'memory', text: `Known facts:\n${facts.map((f) => `- ${f}`).join('\n')}`, retention: 'normal' });
|
|
668
|
+
return blocks;
|
|
669
|
+
}
|
|
670
|
+
/** Best-effort estimator calibration from provider-reported input usage (estimates never claim to be exact). */
|
|
671
|
+
calibrate(system, inputText, run) {
|
|
672
|
+
const tokens = run.response?.usage?.inputTokens;
|
|
673
|
+
if (tokens && tokens > 0)
|
|
674
|
+
this.estimator.calibrate((system?.length ?? 0) + inputText.length, tokens);
|
|
675
|
+
}
|
|
676
|
+
/** Capture an explicit "remember …" fact and retrieve relevant facts for context. Off when disabled. */
|
|
677
|
+
applyMemory(text) {
|
|
678
|
+
if (!this.memoryEnabled())
|
|
679
|
+
return undefined;
|
|
680
|
+
try {
|
|
681
|
+
const trace = { retrieved: [] };
|
|
682
|
+
const candidate = classifyMemory(text);
|
|
683
|
+
if (candidate) {
|
|
684
|
+
const rec = this._memory.remember(candidate);
|
|
685
|
+
trace.captured = { id: rec.id, scope: rec.scope };
|
|
686
|
+
}
|
|
687
|
+
trace.retrieved = this._memory.search(text, { limit: 3 }).map((h) => h.text);
|
|
688
|
+
if (trace.retrieved.length === 0 && !trace.captured)
|
|
689
|
+
return undefined;
|
|
690
|
+
return trace;
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
// Memory is an enhancement; a store problem must never break the chat itself.
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
/** Build clarification metadata when auto-detection was ambiguous (mode-level ambiguity in Phase 1). */
|
|
699
|
+
function buildClarification(modeResult) {
|
|
700
|
+
const c = modeResult.classification;
|
|
701
|
+
if (!c || !c.ambiguous)
|
|
702
|
+
return undefined;
|
|
703
|
+
const competitors = Object.entries(c.scores)
|
|
704
|
+
.sort((a, b) => (b[1] ?? 0) - (a[1] ?? 0))
|
|
705
|
+
.slice(0, 3)
|
|
706
|
+
.map(([m]) => m);
|
|
707
|
+
return {
|
|
708
|
+
question: `I wasn't sure which mode fits — did you mean one of: ${competitors.join(', ')}? (Answering helps me route; I've answered as chat for now.)`,
|
|
709
|
+
options: competitors,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function resolveCwd(workspace) {
|
|
713
|
+
return workspace ? resolve(workspace) : process.cwd();
|
|
714
|
+
}
|
|
715
|
+
/** Total latency across a run's attempts (primary + fallbacks), for learning outcomes. */
|
|
716
|
+
function attemptsLatency(run) {
|
|
717
|
+
return run.routing.attempts.reduce((sum, a) => sum + (a.latencyMs ?? 0), 0);
|
|
718
|
+
}
|