infinicode 2.3.2 → 2.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/plugins/mesh-commands.tsx +139 -0
- package/dist/cli.js +2 -1
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/run.js +100 -3
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +36 -1
- package/dist/kernel/capability-map.d.ts +18 -0
- package/dist/kernel/capability-map.js +84 -0
- package/dist/kernel/checkpoint-engine.js +9 -0
- package/dist/kernel/command-system.d.ts +3 -0
- package/dist/kernel/command-system.js +315 -10
- package/dist/kernel/config-schema.d.ts +11 -0
- package/dist/kernel/config-schema.js +1 -0
- package/dist/kernel/federation/event-bridge.js +3 -1
- package/dist/kernel/federation/federation.d.ts +10 -0
- package/dist/kernel/federation/federation.js +29 -9
- package/dist/kernel/federation/transport-http.d.ts +3 -0
- package/dist/kernel/federation/transport-http.js +25 -0
- package/dist/kernel/free-providers.d.ts +23 -0
- package/dist/kernel/free-providers.js +73 -1
- package/dist/kernel/index.d.ts +4 -0
- package/dist/kernel/index.js +3 -0
- package/dist/kernel/kernel.d.ts +12 -0
- package/dist/kernel/kernel.js +29 -2
- package/dist/kernel/mission-engine.js +14 -0
- package/dist/kernel/orchestrator.d.ts +3 -0
- package/dist/kernel/orchestrator.js +74 -2
- package/dist/kernel/planner.d.ts +42 -0
- package/dist/kernel/planner.js +174 -0
- package/dist/kernel/recovery-manager.d.ts +9 -0
- package/dist/kernel/recovery-manager.js +64 -3
- package/dist/kernel/router.d.ts +12 -1
- package/dist/kernel/router.js +36 -1
- package/dist/kernel/scheduler.d.ts +17 -0
- package/dist/kernel/scheduler.js +97 -1
- package/dist/kernel/setup.d.ts +9 -0
- package/dist/kernel/setup.js +38 -0
- package/dist/kernel/types.d.ts +65 -2
- package/dist/kernel/verification-engine.js +83 -27
- package/dist/kernel/verification-exec.d.ts +14 -0
- package/dist/kernel/verification-exec.js +65 -0
- package/dist/kernel/worker-runtime.js +3 -2
- package/package.json +1 -1
package/dist/kernel/router.js
CHANGED
|
@@ -41,6 +41,34 @@ export class CapabilityRouter {
|
|
|
41
41
|
benchmarkScore: c.benchmarkScore,
|
|
42
42
|
}));
|
|
43
43
|
}
|
|
44
|
+
/** Public benchmark estimate for a model (0–100) — used by recovery/escalation. */
|
|
45
|
+
benchmarkFor(model) {
|
|
46
|
+
return this.estimateBenchmark(model);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Pick a strictly MORE capable model than the current one for the same
|
|
50
|
+
* capabilities — the verification-failure escalation ladder. Ranks candidates
|
|
51
|
+
* whose benchmark exceeds the current model's, preferring the highest
|
|
52
|
+
* benchmark (tie-broken by overall score). Returns null at the ceiling.
|
|
53
|
+
*/
|
|
54
|
+
escalate(request, currentProviderId, currentModelId) {
|
|
55
|
+
const current = request.availableModels.find(m => m.providerId === currentProviderId && m.id === currentModelId);
|
|
56
|
+
const currentBench = current ? this.estimateBenchmark(current) : (request.minBenchmark ?? 0);
|
|
57
|
+
const candidates = this.scoreCandidates(request)
|
|
58
|
+
.filter(c => !(c.providerId === currentProviderId && c.modelId === currentModelId))
|
|
59
|
+
.filter(c => c.benchmarkScore > currentBench + 1)
|
|
60
|
+
.sort((a, b) => b.benchmarkScore - a.benchmarkScore || b.score - a.score);
|
|
61
|
+
const top = candidates[0];
|
|
62
|
+
if (!top)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
providerId: top.providerId,
|
|
66
|
+
modelId: top.modelId,
|
|
67
|
+
score: top.score,
|
|
68
|
+
benchmarkScore: top.benchmarkScore,
|
|
69
|
+
reason: `escalate ↑bench ${currentBench}→${top.benchmarkScore}: ${top.reason}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
44
72
|
scoreCandidates(request) {
|
|
45
73
|
const { capabilities, preferences, policy, availableProviders, availableModels } = request;
|
|
46
74
|
const mode = (policy.routing?.mode ?? 'balanced');
|
|
@@ -86,6 +114,12 @@ export class CapabilityRouter {
|
|
|
86
114
|
// Heavy penalty when the model can't fit the estimated request — keeps it
|
|
87
115
|
// as a last resort but routes to a capable model when one exists.
|
|
88
116
|
const capacityPenalty = needTokens > 0 && effectiveLimit < needTokens ? 6 : 0;
|
|
117
|
+
// Phase/escalation floor: models below the requested benchmark are pushed
|
|
118
|
+
// down (proportional, not hard-excluded) so demanding phases climb to a
|
|
119
|
+
// capable model and the escalation ladder can keep ascending.
|
|
120
|
+
const floorPenalty = request.minBenchmark && benchmark < request.minBenchmark
|
|
121
|
+
? (request.minBenchmark - benchmark) / 5
|
|
122
|
+
: 0;
|
|
89
123
|
const score = capScore * weights.capability +
|
|
90
124
|
benchScore * weights.benchmark +
|
|
91
125
|
reliability * weights.reliability +
|
|
@@ -96,7 +130,8 @@ export class CapabilityRouter {
|
|
|
96
130
|
preferredBonus +
|
|
97
131
|
fastApiBonus -
|
|
98
132
|
costPenalty -
|
|
99
|
-
capacityPenalty
|
|
133
|
+
capacityPenalty -
|
|
134
|
+
floorPenalty;
|
|
100
135
|
scored.push({
|
|
101
136
|
providerId: provider.id,
|
|
102
137
|
modelId: model.id,
|
|
@@ -8,6 +8,13 @@ export declare class Scheduler {
|
|
|
8
8
|
objectives: Objective[];
|
|
9
9
|
tasks: Task[];
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* Build one objective per phase. Capabilities are normalized (natural names
|
|
13
|
+
* allowed), each phase's tasks depend on ALL tasks of the phases it dependsOn
|
|
14
|
+
* (so phases run in order but tasks within a phase can parallelize), and each
|
|
15
|
+
* phase gets its own model benchmark floor → different models per phase.
|
|
16
|
+
*/
|
|
17
|
+
private planPhases;
|
|
11
18
|
createTask(missionId: string, objectiveId: string, description: string, capabilities: Capability[], dependencies: string[], input: TaskInput): Task;
|
|
12
19
|
/**
|
|
13
20
|
* Returns the next batch of tasks that are ready to execute:
|
|
@@ -28,3 +35,13 @@ export declare class Scheduler {
|
|
|
28
35
|
reconcileObjectives(mission: Mission): void;
|
|
29
36
|
private aggregateCapabilities;
|
|
30
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Per-phase model benchmark floor (0–100). Demanding phases (architecture,
|
|
40
|
+
* verification, review, reasoning-heavy) demand a more capable model; lighter
|
|
41
|
+
* phases (docs, translation, plain search) leave the floor open so the router
|
|
42
|
+
* can pick a cheaper/faster model. This is what lets one large mission spread
|
|
43
|
+
* across models by aspect instead of one-shotting with a single model.
|
|
44
|
+
*/
|
|
45
|
+
export declare function phaseBenchmarkFloor(caps: Capability[]): number;
|
|
46
|
+
/** Human-readable phase label from an objective's dominant capability. */
|
|
47
|
+
export declare function phaseLabel(caps: Capability[]): string;
|
package/dist/kernel/scheduler.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Supports: sequential, parallel, dependency graphs
|
|
6
6
|
*/
|
|
7
7
|
import { nanoid } from 'nanoid';
|
|
8
|
+
import { normalizeCapabilities } from './capability-map.js';
|
|
8
9
|
export class Scheduler {
|
|
9
10
|
eventBus;
|
|
10
11
|
logger;
|
|
@@ -13,27 +14,81 @@ export class Scheduler {
|
|
|
13
14
|
this.logger = logger;
|
|
14
15
|
}
|
|
15
16
|
planMission(missionId, input) {
|
|
17
|
+
// Phase-based plan (each phase → one objective, with cross-phase deps).
|
|
18
|
+
if (input.phases && input.phases.length > 0) {
|
|
19
|
+
return this.planPhases(missionId, input.phases);
|
|
20
|
+
}
|
|
16
21
|
const taskInputs = input.tasks ?? [];
|
|
17
22
|
const objectiveId = nanoid(10);
|
|
18
23
|
const tasks = [];
|
|
19
24
|
for (const ti of taskInputs) {
|
|
20
25
|
tasks.push(this.createTask(missionId, objectiveId, ti.description, ti.capabilities, ti.dependencies ?? [], ti.input));
|
|
21
26
|
}
|
|
27
|
+
const objCaps = this.aggregateCapabilities(tasks);
|
|
22
28
|
const objective = {
|
|
23
29
|
id: objectiveId,
|
|
24
30
|
missionId,
|
|
25
31
|
description: input.goal,
|
|
26
|
-
capabilities:
|
|
32
|
+
capabilities: objCaps,
|
|
27
33
|
acceptanceCriteria: [],
|
|
28
34
|
taskIds: tasks.map(t => t.id),
|
|
29
35
|
status: tasks.length > 0 ? 'PENDING' : 'COMPLETED',
|
|
30
36
|
createdAt: Date.now(),
|
|
37
|
+
phase: phaseLabel(objCaps),
|
|
38
|
+
minBenchmark: tasks.reduce((max, t) => Math.max(max, t.minBenchmark ?? 0), 0) || undefined,
|
|
31
39
|
};
|
|
32
40
|
this.logger.info(`Mission planned: ${missionId} with ${tasks.length} task(s) across 1 objective`);
|
|
33
41
|
return { objectives: [objective], tasks };
|
|
34
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Build one objective per phase. Capabilities are normalized (natural names
|
|
45
|
+
* allowed), each phase's tasks depend on ALL tasks of the phases it dependsOn
|
|
46
|
+
* (so phases run in order but tasks within a phase can parallelize), and each
|
|
47
|
+
* phase gets its own model benchmark floor → different models per phase.
|
|
48
|
+
*/
|
|
49
|
+
planPhases(missionId, phases) {
|
|
50
|
+
const objectives = [];
|
|
51
|
+
const tasks = [];
|
|
52
|
+
const phaseTaskIds = new Map(); // phase name (lower) → task ids
|
|
53
|
+
for (const phase of phases) {
|
|
54
|
+
const objectiveId = nanoid(10);
|
|
55
|
+
const phaseCaps = normalizeCapabilities(phase.capabilities);
|
|
56
|
+
const deps = (phase.dependsOn ?? []).flatMap(n => phaseTaskIds.get(n.trim().toLowerCase()) ?? []);
|
|
57
|
+
const taskInputs = phase.tasks && phase.tasks.length > 0
|
|
58
|
+
? phase.tasks
|
|
59
|
+
: [{ description: phase.description, capabilities: phaseCaps, input: { prompt: phase.description } }];
|
|
60
|
+
const phaseTasks = taskInputs.map(ti => {
|
|
61
|
+
const tCaps = ti.capabilities && ti.capabilities.length > 0
|
|
62
|
+
? normalizeCapabilities(ti.capabilities)
|
|
63
|
+
: phaseCaps;
|
|
64
|
+
const task = this.createTask(missionId, objectiveId, ti.description, tCaps, [...deps, ...(ti.dependencies ?? [])], ti.input ?? { prompt: ti.description });
|
|
65
|
+
// Explicit phase floor overrides the capability-derived one.
|
|
66
|
+
if (phase.minBenchmark)
|
|
67
|
+
task.minBenchmark = phase.minBenchmark;
|
|
68
|
+
return task;
|
|
69
|
+
});
|
|
70
|
+
tasks.push(...phaseTasks);
|
|
71
|
+
phaseTaskIds.set(phase.name.trim().toLowerCase(), phaseTasks.map(t => t.id));
|
|
72
|
+
const derivedFloor = phaseTasks.reduce((max, t) => Math.max(max, t.minBenchmark ?? 0), 0);
|
|
73
|
+
objectives.push({
|
|
74
|
+
id: objectiveId,
|
|
75
|
+
missionId,
|
|
76
|
+
description: phase.description,
|
|
77
|
+
capabilities: phaseCaps,
|
|
78
|
+
acceptanceCriteria: phase.acceptanceCriteria ?? [],
|
|
79
|
+
taskIds: phaseTasks.map(t => t.id),
|
|
80
|
+
status: phaseTasks.length > 0 ? 'PENDING' : 'COMPLETED',
|
|
81
|
+
createdAt: Date.now(),
|
|
82
|
+
phase: phase.name,
|
|
83
|
+
minBenchmark: (phase.minBenchmark ?? derivedFloor) || undefined,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
this.logger.info(`Mission planned: ${missionId} with ${tasks.length} task(s) across ${objectives.length} phase(s): ${phases.map(p => p.name).join(' → ')}`);
|
|
87
|
+
return { objectives, tasks };
|
|
88
|
+
}
|
|
35
89
|
createTask(missionId, objectiveId, description, capabilities, dependencies, input) {
|
|
36
90
|
const id = nanoid(10);
|
|
91
|
+
const floor = phaseBenchmarkFloor(capabilities);
|
|
37
92
|
const task = {
|
|
38
93
|
id,
|
|
39
94
|
objectiveId,
|
|
@@ -46,6 +101,7 @@ export class Scheduler {
|
|
|
46
101
|
attempts: 0,
|
|
47
102
|
maxAttempts: 3,
|
|
48
103
|
createdAt: Date.now(),
|
|
104
|
+
minBenchmark: floor > 0 ? floor : undefined,
|
|
49
105
|
};
|
|
50
106
|
this.eventBus.emit({ type: 'TASK_QUEUED', timestamp: Date.now(), missionId, objectiveId, taskId: id, data: { description } });
|
|
51
107
|
return task;
|
|
@@ -145,3 +201,43 @@ export class Scheduler {
|
|
|
145
201
|
return [...set];
|
|
146
202
|
}
|
|
147
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Per-phase model benchmark floor (0–100). Demanding phases (architecture,
|
|
206
|
+
* verification, review, reasoning-heavy) demand a more capable model; lighter
|
|
207
|
+
* phases (docs, translation, plain search) leave the floor open so the router
|
|
208
|
+
* can pick a cheaper/faster model. This is what lets one large mission spread
|
|
209
|
+
* across models by aspect instead of one-shotting with a single model.
|
|
210
|
+
*/
|
|
211
|
+
export function phaseBenchmarkFloor(caps) {
|
|
212
|
+
const has = (c) => caps.includes(c);
|
|
213
|
+
if (has('architecture') || has('planning'))
|
|
214
|
+
return 85;
|
|
215
|
+
if (has('verification') || has('review'))
|
|
216
|
+
return 84;
|
|
217
|
+
if (has('coding') || has('frontend'))
|
|
218
|
+
return 78;
|
|
219
|
+
if (has('vision') || has('ocr'))
|
|
220
|
+
return 74;
|
|
221
|
+
if (has('reasoning'))
|
|
222
|
+
return 80;
|
|
223
|
+
return 0; // search / crawl / summarize / documentation / translation → open
|
|
224
|
+
}
|
|
225
|
+
/** Human-readable phase label from an objective's dominant capability. */
|
|
226
|
+
export function phaseLabel(caps) {
|
|
227
|
+
const has = (c) => caps.includes(c);
|
|
228
|
+
if (has('architecture') || has('planning'))
|
|
229
|
+
return 'architecture';
|
|
230
|
+
if (has('frontend'))
|
|
231
|
+
return 'frontend';
|
|
232
|
+
if (has('coding'))
|
|
233
|
+
return 'implementation';
|
|
234
|
+
if (has('verification') || has('review'))
|
|
235
|
+
return 'verification';
|
|
236
|
+
if (has('vision') || has('ocr'))
|
|
237
|
+
return 'vision';
|
|
238
|
+
if (has('search') || has('crawl'))
|
|
239
|
+
return 'research';
|
|
240
|
+
if (has('documentation') || has('translation'))
|
|
241
|
+
return 'documentation';
|
|
242
|
+
return 'general';
|
|
243
|
+
}
|
package/dist/kernel/setup.d.ts
CHANGED
|
@@ -15,6 +15,15 @@ export type { InfinicodeConfig, CloudProviderConfig, WorkerModelPref } from './c
|
|
|
15
15
|
* count of enabled providers registered.
|
|
16
16
|
*/
|
|
17
17
|
export declare function registerCloudProviders(kernel: Kernel, cloudProviders: CloudProviderConfig[]): number;
|
|
18
|
+
/**
|
|
19
|
+
* Register (or re-register) local OpenAI-compatible providers (LM Studio, vLLM,
|
|
20
|
+
* SGLang, llama.cpp) on a live kernel from config. Providers register with
|
|
21
|
+
* type 'local' and require NO API key — they are never skipped for a missing
|
|
22
|
+
* key the way cloud providers can be, and the local server does not need to be
|
|
23
|
+
* running (unreachable ones are marked unhealthy by the health check). Returns
|
|
24
|
+
* the count of enabled providers registered.
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerLocalProviders(kernel: Kernel, localProviders: CloudProviderConfig[]): number;
|
|
18
27
|
export declare function buildKernelFromConfig(config: Conf<InfinicodeConfig>, options?: {
|
|
19
28
|
logger?: Logger;
|
|
20
29
|
}): Kernel;
|
package/dist/kernel/setup.js
CHANGED
|
@@ -27,6 +27,35 @@ export function registerCloudProviders(kernel, cloudProviders) {
|
|
|
27
27
|
const options = {
|
|
28
28
|
id: cfg.id,
|
|
29
29
|
name: cfg.name,
|
|
30
|
+
type: cfg.type,
|
|
31
|
+
baseURL: cfg.baseURL,
|
|
32
|
+
apiKey: cfg.apiKey,
|
|
33
|
+
headerName: cfg.headerName,
|
|
34
|
+
knownModels: preset?.knownModels ?? [],
|
|
35
|
+
};
|
|
36
|
+
kernel.registerProvider(cfg.id, new OpenAICompatibleProvider(options));
|
|
37
|
+
n++;
|
|
38
|
+
}
|
|
39
|
+
return n;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register (or re-register) local OpenAI-compatible providers (LM Studio, vLLM,
|
|
43
|
+
* SGLang, llama.cpp) on a live kernel from config. Providers register with
|
|
44
|
+
* type 'local' and require NO API key — they are never skipped for a missing
|
|
45
|
+
* key the way cloud providers can be, and the local server does not need to be
|
|
46
|
+
* running (unreachable ones are marked unhealthy by the health check). Returns
|
|
47
|
+
* the count of enabled providers registered.
|
|
48
|
+
*/
|
|
49
|
+
export function registerLocalProviders(kernel, localProviders) {
|
|
50
|
+
let n = 0;
|
|
51
|
+
for (const cfg of localProviders) {
|
|
52
|
+
if (!cfg.enabled)
|
|
53
|
+
continue;
|
|
54
|
+
const preset = getProviderPreset(cfg.id);
|
|
55
|
+
const options = {
|
|
56
|
+
id: cfg.id,
|
|
57
|
+
name: cfg.name,
|
|
58
|
+
type: 'local',
|
|
30
59
|
baseURL: cfg.baseURL,
|
|
31
60
|
apiKey: cfg.apiKey,
|
|
32
61
|
headerName: cfg.headerName,
|
|
@@ -52,6 +81,15 @@ export function buildKernelFromConfig(config, options) {
|
|
|
52
81
|
kernel.registerProvider('ollama', new OllamaProvider(ollamaOptions));
|
|
53
82
|
// Register cloud providers
|
|
54
83
|
registerCloudProviders(kernel, config.get('cloudProviders') ?? []);
|
|
84
|
+
// Register local OpenAI-compatible providers (LM Studio, vLLM, SGLang,
|
|
85
|
+
// llama.cpp) the user has configured in `localProviders`. No API key is
|
|
86
|
+
// required; an unreachable server is marked unhealthy by the health check.
|
|
87
|
+
// Only configured ones are registered — this avoids probing dead localhost
|
|
88
|
+
// ports on every machine. The presets (`localProviderConfigs()` in
|
|
89
|
+
// free-providers.ts / the setup wizard) make enabling one a one-liner.
|
|
90
|
+
const localProviders = config.get('localProviders') ?? [];
|
|
91
|
+
if (localProviders.length > 0)
|
|
92
|
+
registerLocalProviders(kernel, localProviders);
|
|
55
93
|
// Apply per-worker model pins — the worker runtime honors them when routing.
|
|
56
94
|
const workerModels = config.get('workerModels') ?? {};
|
|
57
95
|
if (workerModels && Object.keys(workerModels).length > 0) {
|
package/dist/kernel/types.d.ts
CHANGED
|
@@ -19,6 +19,15 @@ export interface Objective {
|
|
|
19
19
|
status: TaskStatus;
|
|
20
20
|
createdAt: number;
|
|
21
21
|
completedAt?: number;
|
|
22
|
+
/** Human phase label for large projects (e.g. "architecture", "frontend"). */
|
|
23
|
+
phase?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Minimum model benchmark (0–100) this phase should route to. Lets the
|
|
26
|
+
* orchestrator assign a more capable model to demanding phases (architecture,
|
|
27
|
+
* verification) and a cheaper one to light phases (docs) — instead of one
|
|
28
|
+
* model for the whole mission. Flows into each task's routing floor.
|
|
29
|
+
*/
|
|
30
|
+
minBenchmark?: number;
|
|
22
31
|
}
|
|
23
32
|
export interface Task {
|
|
24
33
|
id: string;
|
|
@@ -39,6 +48,11 @@ export interface Task {
|
|
|
39
48
|
startedAt?: number;
|
|
40
49
|
completedAt?: number;
|
|
41
50
|
error?: string;
|
|
51
|
+
/** Routing floor for this task/phase (0–100). Escalates upward on repeated
|
|
52
|
+
* verification failure so the task climbs to a more capable model. */
|
|
53
|
+
minBenchmark?: number;
|
|
54
|
+
/** How many times this task's output failed verification — drives model escalation. */
|
|
55
|
+
verificationFailures?: number;
|
|
42
56
|
}
|
|
43
57
|
export interface TaskInput {
|
|
44
58
|
prompt: string;
|
|
@@ -258,6 +272,12 @@ export interface Checkpoint {
|
|
|
258
272
|
workerStates: WorkerInstanceState[];
|
|
259
273
|
timestamp: number;
|
|
260
274
|
reason: 'task' | 'interval' | 'pause' | 'crash' | 'manual';
|
|
275
|
+
/** Recent event log for this mission — restored so long-running/crash recovery
|
|
276
|
+
* keeps its activity trail. */
|
|
277
|
+
logs?: KernelEvent[];
|
|
278
|
+
/** Free-form memory bag persisted with the checkpoint (mission.metadata plus
|
|
279
|
+
* any harness-supplied working memory). */
|
|
280
|
+
memory?: Record<string, unknown>;
|
|
261
281
|
}
|
|
262
282
|
export interface WorkerInstanceState {
|
|
263
283
|
workerId: string;
|
|
@@ -267,7 +287,7 @@ export interface WorkerInstanceState {
|
|
|
267
287
|
modelId?: string;
|
|
268
288
|
partialOutput?: string;
|
|
269
289
|
}
|
|
270
|
-
export type KernelEventType = 'MISSION_STARTED' | 'MISSION_COMPLETED' | 'MISSION_FAILED' | 'MISSION_PAUSED' | 'MISSION_RESUMED' | 'MISSION_CANCELLED' | 'OBJECTIVE_STARTED' | 'OBJECTIVE_COMPLETED' | 'TASK_QUEUED' | 'TASK_STARTED' | 'TASK_COMPLETED' | 'TASK_FAILED' | 'WORKER_SPAWNED' | 'WORKER_EXITED' | 'ROUTING_DECISION' | 'MODEL_CHANGED' | 'PROVIDER_CHANGED' | 'CHECKPOINT' | 'VERIFY_SUCCESS' | 'VERIFY_FAILED' | 'RECOVERY' | 'PROVIDER_HEALTH' | 'NOTIFICATION' | 'LOG';
|
|
290
|
+
export type KernelEventType = 'MISSION_STARTED' | 'MISSION_COMPLETED' | 'MISSION_FAILED' | 'MISSION_PAUSED' | 'MISSION_RESUMED' | 'MISSION_CANCELLED' | 'PLAN_CREATED' | 'OBJECTIVE_STARTED' | 'OBJECTIVE_COMPLETED' | 'TASK_QUEUED' | 'TASK_STARTED' | 'TASK_COMPLETED' | 'TASK_FAILED' | 'WORKER_SPAWNED' | 'WORKER_EXITED' | 'ROUTING_DECISION' | 'MODEL_CHANGED' | 'PROVIDER_CHANGED' | 'CHECKPOINT' | 'VERIFY_SUCCESS' | 'VERIFY_FAILED' | 'RECOVERY' | 'PROVIDER_HEALTH' | 'NOTIFICATION' | 'LOG';
|
|
271
291
|
export interface KernelEvent<T = unknown> {
|
|
272
292
|
type: KernelEventType;
|
|
273
293
|
timestamp: number;
|
|
@@ -290,6 +310,12 @@ export interface RoutingRequest {
|
|
|
290
310
|
* preventing "request too large" failures on rate-limited tiers.
|
|
291
311
|
*/
|
|
292
312
|
estimatedRequestTokens?: number;
|
|
313
|
+
/**
|
|
314
|
+
* Minimum acceptable model benchmark (0–100). Models below the floor are
|
|
315
|
+
* heavily penalized (not hard-excluded), so demanding phases route to capable
|
|
316
|
+
* models and escalation can climb the tier ladder.
|
|
317
|
+
*/
|
|
318
|
+
minBenchmark?: number;
|
|
293
319
|
}
|
|
294
320
|
export interface RoutingDecision {
|
|
295
321
|
providerId: string;
|
|
@@ -382,13 +408,16 @@ export interface LlmJudgeOptions {
|
|
|
382
408
|
modelId?: string;
|
|
383
409
|
}
|
|
384
410
|
export type FailureType = '429' | 'timeout' | 'provider-down' | 'hallucination' | 'tool-failure' | 'verification-failure' | 'planning-failure' | 'browser-failure' | 'unknown';
|
|
385
|
-
export type RecoveryAction = 'retry' | 'rotate-provider' | 'spawn-different-worker' | 'replan' | 'checkpoint' | 'continue' | 'abort';
|
|
411
|
+
export type RecoveryAction = 'retry' | 'rotate-provider' | 'escalate-model' | 'spawn-different-worker' | 'replan' | 'checkpoint' | 'continue' | 'abort';
|
|
386
412
|
export interface RecoveryDecision {
|
|
387
413
|
action: RecoveryAction;
|
|
388
414
|
reason: string;
|
|
389
415
|
rotateToProviderId?: string;
|
|
390
416
|
rotateToModelId?: string;
|
|
391
417
|
newWorkerType?: WorkerType;
|
|
418
|
+
/** For 'escalate-model': the benchmark of the more-capable target, so the
|
|
419
|
+
* orchestrator can raise the task's routing floor and keep climbing. */
|
|
420
|
+
escalateToBenchmark?: number;
|
|
392
421
|
}
|
|
393
422
|
export interface PluginManifest {
|
|
394
423
|
name: string;
|
|
@@ -442,6 +471,21 @@ export interface MissionInput {
|
|
|
442
471
|
policy?: PolicyName;
|
|
443
472
|
workspace?: WorkspaceRef;
|
|
444
473
|
tasks?: MissionTaskInput[];
|
|
474
|
+
/**
|
|
475
|
+
* Explicit phase breakdown (each phase → one objective). When present it wins
|
|
476
|
+
* over `tasks`. Harnesses (e.g. InfiniBot) can pass phases directly with
|
|
477
|
+
* capabilities written naturally ("backend", "ui", "tests") — they're
|
|
478
|
+
* normalized to canonical capabilities and each phase routes to its own model
|
|
479
|
+
* tier via its benchmark floor.
|
|
480
|
+
*/
|
|
481
|
+
phases?: MissionPhaseInput[];
|
|
482
|
+
/**
|
|
483
|
+
* When true and neither `tasks` nor `phases` is given, the MissionPlanner
|
|
484
|
+
* decomposes `goal` into phases (LLM-driven, heuristic fallback) before
|
|
485
|
+
* execution — turning a one-line goal into architecture → implementation →
|
|
486
|
+
* verification phases, each with an appropriate model.
|
|
487
|
+
*/
|
|
488
|
+
autoPlan?: boolean;
|
|
445
489
|
metadata?: Record<string, unknown>;
|
|
446
490
|
}
|
|
447
491
|
export interface MissionTaskInput {
|
|
@@ -450,6 +494,25 @@ export interface MissionTaskInput {
|
|
|
450
494
|
dependencies?: string[];
|
|
451
495
|
input: TaskInput;
|
|
452
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* A phase of a mission — becomes one Objective plus its tasks. Capabilities may
|
|
499
|
+
* be written naturally (natural language / synonyms) and are normalized; the
|
|
500
|
+
* phase's model tier is derived from them unless `minBenchmark` is set.
|
|
501
|
+
*/
|
|
502
|
+
export interface MissionPhaseInput {
|
|
503
|
+
/** Short phase label, e.g. "architecture", "backend", "frontend", "tests". */
|
|
504
|
+
name: string;
|
|
505
|
+
description: string;
|
|
506
|
+
/** Needed capabilities — natural names allowed ("backend", "ui", "testing"). */
|
|
507
|
+
capabilities?: string[];
|
|
508
|
+
acceptanceCriteria?: string[];
|
|
509
|
+
/** Names of phases that must finish before this one starts. */
|
|
510
|
+
dependsOn?: string[];
|
|
511
|
+
/** Explicit tasks; if omitted the phase runs as a single task from its description. */
|
|
512
|
+
tasks?: MissionTaskInput[];
|
|
513
|
+
/** Explicit model benchmark floor (0–100); else derived from capabilities. */
|
|
514
|
+
minBenchmark?: number;
|
|
515
|
+
}
|
|
453
516
|
export interface ProviderVerifyResult {
|
|
454
517
|
ok: boolean;
|
|
455
518
|
latencyMs: number;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { scoreFrontend } from './frontend-scoring.js';
|
|
2
|
+
import { runCommand, tail } from './verification-exec.js';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
2
5
|
const PIPELINE_ORDER = [
|
|
3
6
|
'objective',
|
|
4
7
|
'compile',
|
|
@@ -169,38 +172,91 @@ function objectiveVerifier() {
|
|
|
169
172
|
};
|
|
170
173
|
}
|
|
171
174
|
function compileVerifier() {
|
|
172
|
-
return
|
|
173
|
-
stage: 'compile',
|
|
174
|
-
enabled: ctx => hasArtifact(ctx.output, 'code') || hasArtifact(ctx.output, 'file'),
|
|
175
|
-
async run(ctx) {
|
|
176
|
-
const codeArtifacts = ctx.output.artifacts?.filter(a => a.type === 'code' || a.type === 'file') ?? [];
|
|
177
|
-
if (codeArtifacts.length === 0) {
|
|
178
|
-
return { name: 'compile', passed: true, message: 'no code artifacts' };
|
|
179
|
-
}
|
|
180
|
-
const hasSyntax = codeArtifacts.every(a => (a.content ?? '').trim().length > 0);
|
|
181
|
-
return {
|
|
182
|
-
name: 'compile',
|
|
183
|
-
passed: hasSyntax,
|
|
184
|
-
message: hasSyntax ? `${codeArtifacts.length} artifact(s) non-empty` : 'empty code artifact',
|
|
185
|
-
};
|
|
186
|
-
},
|
|
187
|
-
};
|
|
175
|
+
return commandVerifier('compile', 'compile');
|
|
188
176
|
}
|
|
189
177
|
function testsVerifier() {
|
|
190
|
-
return
|
|
191
|
-
stage: 'tests',
|
|
192
|
-
enabled: () => false,
|
|
193
|
-
async run() {
|
|
194
|
-
return { name: 'tests', passed: true, message: 'test runner not configured' };
|
|
195
|
-
},
|
|
196
|
-
};
|
|
178
|
+
return commandVerifier('tests', 'test');
|
|
197
179
|
}
|
|
198
180
|
function lintVerifier() {
|
|
181
|
+
return commandVerifier('lint', 'lint');
|
|
182
|
+
}
|
|
183
|
+
/** package.json script names to auto-detect per stage, in priority order. */
|
|
184
|
+
const SCRIPT_CANDIDATES = {
|
|
185
|
+
compile: ['build', 'compile', 'typecheck'],
|
|
186
|
+
test: ['test'],
|
|
187
|
+
lint: ['lint'],
|
|
188
|
+
};
|
|
189
|
+
/** True when the task actually produced code/file artifacts to verify. */
|
|
190
|
+
function producedCode(output) {
|
|
191
|
+
return hasArtifact(output, 'code') || hasArtifact(output, 'file');
|
|
192
|
+
}
|
|
193
|
+
/** Read `<root>/package.json` scripts, or undefined if missing/invalid. */
|
|
194
|
+
function readScripts(root) {
|
|
195
|
+
try {
|
|
196
|
+
const raw = readFileSync(join(root, 'package.json'), 'utf8');
|
|
197
|
+
const pkg = JSON.parse(raw);
|
|
198
|
+
return pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : undefined;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** Pull an explicit command for `kind` from task context, then mission metadata. */
|
|
205
|
+
function explicitCommand(ctx, kind) {
|
|
206
|
+
const sources = [ctx.task.input.context?.verify, ctx.mission.metadata?.verify];
|
|
207
|
+
for (const src of sources) {
|
|
208
|
+
if (src && typeof src === 'object') {
|
|
209
|
+
const cmd = src[kind];
|
|
210
|
+
if (typeof cmd === 'string' && cmd.trim().length > 0)
|
|
211
|
+
return cmd.trim();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolve the command for a stage: explicit context/metadata command first,
|
|
218
|
+
* else an auto-detected `npm run <script>` from the workspace package.json.
|
|
219
|
+
* Returns undefined when nothing can be resolved (stage stays disabled).
|
|
220
|
+
*/
|
|
221
|
+
function resolveCommand(ctx, kind) {
|
|
222
|
+
const explicit = explicitCommand(ctx, kind);
|
|
223
|
+
if (explicit)
|
|
224
|
+
return explicit;
|
|
225
|
+
const scripts = readScripts(ctx.workspace.root);
|
|
226
|
+
if (!scripts)
|
|
227
|
+
return undefined;
|
|
228
|
+
for (const name of SCRIPT_CANDIDATES[kind]) {
|
|
229
|
+
if (typeof scripts[name] === 'string' && scripts[name].trim().length > 0) {
|
|
230
|
+
return `npm run ${name}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* A verifier that runs a real project command (compile/tests/lint) in the
|
|
237
|
+
* mission workspace root and passes/fails on the process exit code. Enabled
|
|
238
|
+
* only when a command resolves AND the task produced code/file artifacts.
|
|
239
|
+
*/
|
|
240
|
+
function commandVerifier(stage, kind) {
|
|
199
241
|
return {
|
|
200
|
-
stage
|
|
201
|
-
enabled
|
|
202
|
-
|
|
203
|
-
|
|
242
|
+
stage,
|
|
243
|
+
enabled(ctx) {
|
|
244
|
+
return producedCode(ctx.output) && resolveCommand(ctx, kind) !== undefined;
|
|
245
|
+
},
|
|
246
|
+
async run(ctx) {
|
|
247
|
+
const command = resolveCommand(ctx, kind);
|
|
248
|
+
if (!command) {
|
|
249
|
+
// Should not happen (gated by enabled), but stay safe & non-blocking.
|
|
250
|
+
return { name: stage, passed: true, message: `no ${kind} command resolved` };
|
|
251
|
+
}
|
|
252
|
+
const result = await runCommand(command, ctx.workspace.root);
|
|
253
|
+
const passed = result.code === 0 && !result.timedOut;
|
|
254
|
+
const combined = tail(`${result.stdout}\n${result.stderr}`.trim());
|
|
255
|
+
const status = result.timedOut ? 'timed out' : `exit ${result.code}`;
|
|
256
|
+
const message = passed
|
|
257
|
+
? `\`${command}\` passed (${status})`
|
|
258
|
+
: `\`${command}\` failed (${status})${combined ? `\n${combined}` : ''}`;
|
|
259
|
+
return { name: stage, passed, message };
|
|
204
260
|
},
|
|
205
261
|
};
|
|
206
262
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ExecResult {
|
|
2
|
+
code: number | null;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
timedOut: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Keep only the last `max` characters of a string. */
|
|
8
|
+
export declare function tail(text: string, max?: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* Run `command` in `cwd` via a shell, capturing stdout/stderr. On Windows we
|
|
11
|
+
* use `shell: true` so npm scripts / .cmd shims resolve. Resolves (never
|
|
12
|
+
* rejects) with the exit code and captured output; kills the process on timeout.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runCommand(command: string, cwd: string, timeoutMs?: number): Promise<ExecResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Verification Exec Helper
|
|
3
|
+
*
|
|
4
|
+
* Runs a real project command (compile/tests/lint) in a workspace directory
|
|
5
|
+
* and reports pass/fail on the process exit code. Effect-free, no new deps —
|
|
6
|
+
* just node:child_process.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
10
|
+
const MAX_TAIL_CHARS = 4000;
|
|
11
|
+
/** Keep only the last `max` characters of a string. */
|
|
12
|
+
export function tail(text, max = MAX_TAIL_CHARS) {
|
|
13
|
+
if (text.length <= max)
|
|
14
|
+
return text;
|
|
15
|
+
return `…${text.slice(text.length - max)}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Run `command` in `cwd` via a shell, capturing stdout/stderr. On Windows we
|
|
19
|
+
* use `shell: true` so npm scripts / .cmd shims resolve. Resolves (never
|
|
20
|
+
* rejects) with the exit code and captured output; kills the process on timeout.
|
|
21
|
+
*/
|
|
22
|
+
export function runCommand(command, cwd, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
23
|
+
return new Promise(resolve => {
|
|
24
|
+
let stdout = '';
|
|
25
|
+
let stderr = '';
|
|
26
|
+
let timedOut = false;
|
|
27
|
+
let settled = false;
|
|
28
|
+
const child = spawn(command, {
|
|
29
|
+
cwd,
|
|
30
|
+
shell: true,
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
timedOut = true;
|
|
35
|
+
try {
|
|
36
|
+
child.kill('SIGKILL');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* already gone */
|
|
40
|
+
}
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
child.stdout?.on('data', (chunk) => {
|
|
43
|
+
stdout += chunk.toString();
|
|
44
|
+
if (stdout.length > MAX_TAIL_CHARS * 2)
|
|
45
|
+
stdout = tail(stdout, MAX_TAIL_CHARS * 2);
|
|
46
|
+
});
|
|
47
|
+
child.stderr?.on('data', (chunk) => {
|
|
48
|
+
stderr += chunk.toString();
|
|
49
|
+
if (stderr.length > MAX_TAIL_CHARS * 2)
|
|
50
|
+
stderr = tail(stderr, MAX_TAIL_CHARS * 2);
|
|
51
|
+
});
|
|
52
|
+
const finish = (code) => {
|
|
53
|
+
if (settled)
|
|
54
|
+
return;
|
|
55
|
+
settled = true;
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
resolve({ code, stdout, stderr, timedOut });
|
|
58
|
+
};
|
|
59
|
+
child.on('error', err => {
|
|
60
|
+
stderr += `\n${err instanceof Error ? err.message : String(err)}`;
|
|
61
|
+
finish(1);
|
|
62
|
+
});
|
|
63
|
+
child.on('close', code => finish(code));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -109,7 +109,7 @@ export class WorkerRuntime {
|
|
|
109
109
|
instance.status = 'INITIALIZED';
|
|
110
110
|
const plan = override
|
|
111
111
|
? this.lockedPlan(override, 'recovery override')
|
|
112
|
-
: await this.planRoute(task.capabilities, handler.preferences, instance.type, estimateRequestTokens(task));
|
|
112
|
+
: await this.planRoute(task.capabilities, handler.preferences, instance.type, estimateRequestTokens(task), task.minBenchmark);
|
|
113
113
|
// Surface the orchestrator's choice (ranked candidates + winner) so the
|
|
114
114
|
// CLI / TUI can animate and highlight it.
|
|
115
115
|
this.emitRouting(instance, task, plan);
|
|
@@ -189,7 +189,7 @@ export class WorkerRuntime {
|
|
|
189
189
|
* router considered. Honors worker pins (a hard lock), otherwise defers to
|
|
190
190
|
* the policy-driven capability router.
|
|
191
191
|
*/
|
|
192
|
-
async planRoute(capabilities, preferences, workerType, estimatedRequestTokens) {
|
|
192
|
+
async planRoute(capabilities, preferences, workerType, estimatedRequestTokens, minBenchmark) {
|
|
193
193
|
const providers = (await this.ctx.providerManager.getHealthyProviders()).map(p => p.info);
|
|
194
194
|
const models = await this.ctx.providerManager.getAvailableModels();
|
|
195
195
|
const policy = this.ctx.policies.get(this.defaultPolicyName);
|
|
@@ -214,6 +214,7 @@ export class WorkerRuntime {
|
|
|
214
214
|
availableProviders: providers,
|
|
215
215
|
availableModels: models,
|
|
216
216
|
estimatedRequestTokens,
|
|
217
|
+
minBenchmark,
|
|
217
218
|
});
|
|
218
219
|
if (ranked.length === 0) {
|
|
219
220
|
return { chosen: null, candidates: [], locked: false, mode };
|
package/package.json
CHANGED