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/index.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export type { GeminiProviderOptions } from './providers/gemini-provider.js';
|
|
|
32
32
|
export { ALL_BUILTIN_WORKERS, createResearchWorker, createCodingWorker, createArchitectureWorker, createReviewWorker, createDocumentationWorker, createTranslationWorker, createTerminalWorker, createVerificationWorker, createVisionWorker, createFrontendWorker, createBrowserWorker, } from './workers/builtin-workers.js';
|
|
33
33
|
export { KernelCommandRegistry, DEFAULT_COMMANDS } from './command-system.js';
|
|
34
34
|
export type { KernelCommand, CommandContext } from './command-system.js';
|
|
35
|
+
export { MissionPlanner } from './planner.js';
|
|
36
|
+
export type { MissionPlannerDeps, PlanOptions } from './planner.js';
|
|
37
|
+
export { normalizeCapabilities, CANONICAL_CAPABILITIES } from './capability-map.js';
|
|
38
|
+
export { phaseBenchmarkFloor, phaseLabel } from './scheduler.js';
|
|
35
39
|
export * from './federation/index.js';
|
|
36
40
|
export { createMcpServer, serveMcpStdio } from './mcp/index.js';
|
|
37
41
|
export type { McpServerDeps } from './mcp/index.js';
|
package/dist/kernel/index.js
CHANGED
|
@@ -24,6 +24,9 @@ export { OpenAICompatibleProvider } from './providers/openai-compatible-provider
|
|
|
24
24
|
export { GeminiProvider } from './providers/gemini-provider.js';
|
|
25
25
|
export { ALL_BUILTIN_WORKERS, createResearchWorker, createCodingWorker, createArchitectureWorker, createReviewWorker, createDocumentationWorker, createTranslationWorker, createTerminalWorker, createVerificationWorker, createVisionWorker, createFrontendWorker, createBrowserWorker, } from './workers/builtin-workers.js';
|
|
26
26
|
export { KernelCommandRegistry, DEFAULT_COMMANDS } from './command-system.js';
|
|
27
|
+
export { MissionPlanner } from './planner.js';
|
|
28
|
+
export { normalizeCapabilities, CANONICAL_CAPABILITIES } from './capability-map.js';
|
|
29
|
+
export { phaseBenchmarkFloor, phaseLabel } from './scheduler.js';
|
|
27
30
|
// Federation — neutral device mesh (peers, telemetry, config sync, MOLTFED interop)
|
|
28
31
|
export * from './federation/index.js';
|
|
29
32
|
// MCP — north-facing control surface (spawn/dispatch/role/voice/map over the mesh)
|
package/dist/kernel/kernel.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ import { PolicyEngine } from './policies.js';
|
|
|
20
20
|
import { VerificationEngine } from './verification-engine.js';
|
|
21
21
|
import { RecoveryManager } from './recovery-manager.js';
|
|
22
22
|
import { KernelCommandRegistry } from './command-system.js';
|
|
23
|
+
import { MissionPlanner } from './planner.js';
|
|
24
|
+
import type { Federation } from './federation/federation.js';
|
|
23
25
|
import type { CommandResult, EventHandler, KernelEvent, KernelEventType, KernelLike, Logger, Mission, MissionInput, Plugin, Policy, ProviderInterface, WorkerDefinition, MissionStatus } from './types.js';
|
|
24
26
|
export interface KernelOptions {
|
|
25
27
|
logger?: Logger;
|
|
@@ -42,8 +44,18 @@ export declare class Kernel implements KernelLike {
|
|
|
42
44
|
readonly logger: Logger;
|
|
43
45
|
/** Canonical OpenKernel slash-command surface (/status, /workers, /goal …). */
|
|
44
46
|
readonly commands: KernelCommandRegistry;
|
|
47
|
+
/** Decomposes a goal into phases when a mission is run with `autoPlan`. */
|
|
48
|
+
readonly planner: MissionPlanner;
|
|
49
|
+
/**
|
|
50
|
+
* The mesh node this kernel is part of, once `infinicode serve`/`mcp` wires it
|
|
51
|
+
* in via {@link attachFederation}. Undefined when running stand-alone. Node
|
|
52
|
+
* commands (/nodes, /node, /activity …) read it off the command context.
|
|
53
|
+
*/
|
|
54
|
+
federation?: Federation;
|
|
45
55
|
private missions;
|
|
46
56
|
constructor(options?: KernelOptions);
|
|
57
|
+
/** Attach the federation mesh node so node/mesh slash-commands can reach it. */
|
|
58
|
+
attachFederation(federation: Federation): void;
|
|
47
59
|
execute(input: MissionInput): Promise<Mission>;
|
|
48
60
|
pause(missionId: string): Promise<void>;
|
|
49
61
|
resume(missionId: string): Promise<Mission>;
|
package/dist/kernel/kernel.js
CHANGED
|
@@ -22,6 +22,7 @@ import { RecoveryManager } from './recovery-manager.js';
|
|
|
22
22
|
import { ConsoleLogger } from './logger.js';
|
|
23
23
|
import { ALL_BUILTIN_WORKERS } from './workers/builtin-workers.js';
|
|
24
24
|
import { KernelCommandRegistry } from './command-system.js';
|
|
25
|
+
import { MissionPlanner } from './planner.js';
|
|
25
26
|
export class Kernel {
|
|
26
27
|
eventBus;
|
|
27
28
|
providerManager;
|
|
@@ -38,6 +39,14 @@ export class Kernel {
|
|
|
38
39
|
logger;
|
|
39
40
|
/** Canonical OpenKernel slash-command surface (/status, /workers, /goal …). */
|
|
40
41
|
commands;
|
|
42
|
+
/** Decomposes a goal into phases when a mission is run with `autoPlan`. */
|
|
43
|
+
planner;
|
|
44
|
+
/**
|
|
45
|
+
* The mesh node this kernel is part of, once `infinicode serve`/`mcp` wires it
|
|
46
|
+
* in via {@link attachFederation}. Undefined when running stand-alone. Node
|
|
47
|
+
* commands (/nodes, /node, /activity …) read it off the command context.
|
|
48
|
+
*/
|
|
49
|
+
federation;
|
|
41
50
|
missions = new Map();
|
|
42
51
|
constructor(options = {}) {
|
|
43
52
|
this.logger = options.logger ?? new ConsoleLogger();
|
|
@@ -89,17 +98,35 @@ export class Kernel {
|
|
|
89
98
|
});
|
|
90
99
|
this.plugins = new PluginManager(this.eventBus, this.logger);
|
|
91
100
|
this.commands = new KernelCommandRegistry(this);
|
|
101
|
+
this.planner = new MissionPlanner({
|
|
102
|
+
providerManager: this.providerManager,
|
|
103
|
+
router: this.router,
|
|
104
|
+
logger: this.logger,
|
|
105
|
+
});
|
|
92
106
|
if (options.loadBuiltinWorkers ?? true) {
|
|
93
107
|
for (const handler of ALL_BUILTIN_WORKERS) {
|
|
94
108
|
this.workerRuntime.registerHandler(handler);
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
111
|
}
|
|
112
|
+
/** Attach the federation mesh node so node/mesh slash-commands can reach it. */
|
|
113
|
+
attachFederation(federation) {
|
|
114
|
+
this.federation = federation;
|
|
115
|
+
}
|
|
98
116
|
async execute(input) {
|
|
99
|
-
|
|
117
|
+
// Auto-planning: decompose a bare goal into phases before execution when
|
|
118
|
+
// asked and no tasks/phases were supplied. Each phase → its own objective +
|
|
119
|
+
// model tier (see MissionPlanner + scheduler.planPhases).
|
|
120
|
+
let effective = input;
|
|
121
|
+
if (input.autoPlan && !input.tasks?.length && !input.phases?.length) {
|
|
122
|
+
const policy = this.policies.resolveRef(input.policy).policy;
|
|
123
|
+
const phases = await this.planner.plan(input.goal, { description: input.description, policy });
|
|
124
|
+
effective = { ...input, phases };
|
|
125
|
+
}
|
|
126
|
+
const mission = this.missionEngine.create(effective);
|
|
100
127
|
this.missions.set(mission.id, mission);
|
|
101
128
|
try {
|
|
102
|
-
await this.orchestrator.run(mission,
|
|
129
|
+
await this.orchestrator.run(mission, effective);
|
|
103
130
|
}
|
|
104
131
|
catch (err) {
|
|
105
132
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -38,6 +38,20 @@ export class MissionEngine {
|
|
|
38
38
|
mission.tasks = tasks;
|
|
39
39
|
this.transition(mission, 'READY');
|
|
40
40
|
this.deps.logger.info(`Mission planned: ${mission.id} — ${tasks.length} task(s)`);
|
|
41
|
+
// Broadcast the phase breakdown so harnesses (InfiniBot mesh map) can render
|
|
42
|
+
// the plan + each phase's capabilities and model tier.
|
|
43
|
+
this.emit(mission, 'PLAN_CREATED', {
|
|
44
|
+
missionId: mission.id,
|
|
45
|
+
goal: mission.goal,
|
|
46
|
+
phases: objectives.map(o => ({
|
|
47
|
+
id: o.id,
|
|
48
|
+
phase: o.phase,
|
|
49
|
+
description: o.description,
|
|
50
|
+
capabilities: o.capabilities,
|
|
51
|
+
minBenchmark: o.minBenchmark,
|
|
52
|
+
taskCount: o.taskIds.length,
|
|
53
|
+
})),
|
|
54
|
+
});
|
|
41
55
|
return mission;
|
|
42
56
|
}
|
|
43
57
|
start(mission) {
|
|
@@ -36,6 +36,9 @@ export declare class Orchestrator {
|
|
|
36
36
|
private taskOverrides;
|
|
37
37
|
/** Per-task forced worker type override set by the recovery manager. */
|
|
38
38
|
private taskWorkerOverrides;
|
|
39
|
+
/** How many times each task has been replanned (bounds the replan loop). */
|
|
40
|
+
private taskReplans;
|
|
41
|
+
private maxReplans;
|
|
39
42
|
constructor(deps: OrchestratorDeps);
|
|
40
43
|
run(mission: Mission, input: MissionInput): Promise<Mission>;
|
|
41
44
|
private runTasks;
|
|
@@ -7,6 +7,9 @@ export class Orchestrator {
|
|
|
7
7
|
taskOverrides = new Map();
|
|
8
8
|
/** Per-task forced worker type override set by the recovery manager. */
|
|
9
9
|
taskWorkerOverrides = new Map();
|
|
10
|
+
/** How many times each task has been replanned (bounds the replan loop). */
|
|
11
|
+
taskReplans = new Map();
|
|
12
|
+
maxReplans = 2;
|
|
10
13
|
constructor(deps) {
|
|
11
14
|
this.deps = deps;
|
|
12
15
|
}
|
|
@@ -68,6 +71,15 @@ export class Orchestrator {
|
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
async executeTask(mission, task) {
|
|
74
|
+
// Per-phase model assignment: inherit this task's objective (phase) benchmark
|
|
75
|
+
// floor so demanding phases (architecture, verification) route to a more
|
|
76
|
+
// capable model tier than light phases — instead of one model for the whole
|
|
77
|
+
// mission. Escalation may later raise this floor further.
|
|
78
|
+
if (task.minBenchmark === undefined) {
|
|
79
|
+
const objective = mission.objectives.find(o => o.id === task.objectiveId);
|
|
80
|
+
if (objective?.minBenchmark)
|
|
81
|
+
task.minBenchmark = objective.minBenchmark;
|
|
82
|
+
}
|
|
71
83
|
const overrideType = this.taskWorkerOverrides.get(task.id);
|
|
72
84
|
let workerType = overrideType ?? this.deps.workerRuntime.matchWorkerType(task.capabilities);
|
|
73
85
|
if (!workerType) {
|
|
@@ -90,7 +102,8 @@ export class Orchestrator {
|
|
|
90
102
|
// Per-task verification via the Verification Engine pipeline.
|
|
91
103
|
const vresult = await this.deps.verification.verify(mission, task, mission.policy.policy);
|
|
92
104
|
if (!vresult.passed) {
|
|
93
|
-
//
|
|
105
|
+
// Count verification misses — repeated misses escalate the model tier.
|
|
106
|
+
task.verificationFailures = (task.verificationFailures ?? 0) + 1;
|
|
94
107
|
await this.handleRecovery(mission, task, `verification failed: ${vresult.summary}`, instance);
|
|
95
108
|
}
|
|
96
109
|
}
|
|
@@ -112,6 +125,7 @@ export class Orchestrator {
|
|
|
112
125
|
policy: mission.policy.policy,
|
|
113
126
|
currentProviderId: instance.providerId,
|
|
114
127
|
currentModelId: instance.modelId,
|
|
128
|
+
verificationFailures: task.verificationFailures,
|
|
115
129
|
});
|
|
116
130
|
const maxAttempts = mission.policy.policy.retry?.maxAttempts ?? 3;
|
|
117
131
|
switch (decision.action) {
|
|
@@ -144,6 +158,35 @@ export class Orchestrator {
|
|
|
144
158
|
task.error = message;
|
|
145
159
|
break;
|
|
146
160
|
}
|
|
161
|
+
case 'escalate-model': {
|
|
162
|
+
// Climb the benchmark ladder to a more capable model and raise this
|
|
163
|
+
// task's routing floor so it keeps escalating on further failures.
|
|
164
|
+
if (decision.rotateToProviderId && decision.rotateToModelId) {
|
|
165
|
+
this.taskOverrides.set(task.id, {
|
|
166
|
+
providerId: decision.rotateToProviderId,
|
|
167
|
+
modelId: decision.rotateToModelId,
|
|
168
|
+
});
|
|
169
|
+
if (decision.escalateToBenchmark) {
|
|
170
|
+
task.minBenchmark = Math.max(task.minBenchmark ?? 0, decision.escalateToBenchmark);
|
|
171
|
+
}
|
|
172
|
+
this.deps.eventBus.emit({
|
|
173
|
+
type: 'MODEL_CHANGED',
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
missionId: mission.id,
|
|
176
|
+
taskId: task.id,
|
|
177
|
+
data: {
|
|
178
|
+
from: { providerId: instance.providerId, modelId: instance.modelId },
|
|
179
|
+
to: { providerId: decision.rotateToProviderId, modelId: decision.rotateToModelId },
|
|
180
|
+
reason: decision.reason,
|
|
181
|
+
escalated: true,
|
|
182
|
+
minBenchmark: task.minBenchmark,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
task.status = 'PENDING';
|
|
187
|
+
task.error = message;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
147
190
|
case 'spawn-different-worker': {
|
|
148
191
|
if (decision.newWorkerType) {
|
|
149
192
|
this.taskWorkerOverrides.set(task.id, decision.newWorkerType);
|
|
@@ -153,7 +196,36 @@ export class Orchestrator {
|
|
|
153
196
|
task.error = message;
|
|
154
197
|
break;
|
|
155
198
|
}
|
|
156
|
-
case 'replan':
|
|
199
|
+
case 'replan': {
|
|
200
|
+
// Real replan: checkpoint, then restart the task with a CLEAN slate —
|
|
201
|
+
// drop the provider/model + worker overrides so it re-routes from
|
|
202
|
+
// scratch, and reset the attempt/verification counters. Bounded by
|
|
203
|
+
// maxReplans so an unsatisfiable task can't loop forever.
|
|
204
|
+
const replans = (this.taskReplans.get(task.id) ?? 0) + 1;
|
|
205
|
+
this.taskReplans.set(task.id, replans);
|
|
206
|
+
await this.deps.checkpoints.save(mission, this.collectWorkerStates(mission.id), 'crash');
|
|
207
|
+
if (replans > this.maxReplans) {
|
|
208
|
+
task.status = 'FAILED';
|
|
209
|
+
task.error = `${message} (replan limit reached)`;
|
|
210
|
+
this.deps.eventBus.emit({
|
|
211
|
+
type: 'TASK_FAILED',
|
|
212
|
+
timestamp: Date.now(),
|
|
213
|
+
missionId: mission.id,
|
|
214
|
+
taskId: task.id,
|
|
215
|
+
workerId: instance.id,
|
|
216
|
+
data: { error: task.error, attempts: task.attempts, replans },
|
|
217
|
+
});
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
this.taskOverrides.delete(task.id);
|
|
221
|
+
this.taskWorkerOverrides.delete(task.id);
|
|
222
|
+
task.verificationFailures = 0;
|
|
223
|
+
task.attempts = 0;
|
|
224
|
+
task.status = 'PENDING';
|
|
225
|
+
task.error = message;
|
|
226
|
+
this.deps.logger.info(`Task ${task.id}: replan #${replans} — fresh routing + counters`);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
157
229
|
case 'checkpoint': {
|
|
158
230
|
await this.deps.checkpoints.save(mission, this.collectWorkerStates(mission.id), 'crash');
|
|
159
231
|
task.status = 'PENDING';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Mission Planner
|
|
3
|
+
*
|
|
4
|
+
* Decomposes a one-line goal into a sequence of PHASES (each becomes an
|
|
5
|
+
* objective), so a large build spreads across the right models by aspect
|
|
6
|
+
* (architecture → implementation → verification) instead of one model
|
|
7
|
+
* one-shotting the whole thing. Each phase carries the capabilities it needs;
|
|
8
|
+
* the scheduler turns those into a per-phase model benchmark floor.
|
|
9
|
+
*
|
|
10
|
+
* LLM-driven when a capable provider is available, with a deterministic
|
|
11
|
+
* heuristic fallback so planning never blocks on model availability.
|
|
12
|
+
*/
|
|
13
|
+
import type { Logger, MissionPhaseInput, Policy } from './types.js';
|
|
14
|
+
import type { ProviderManager } from './provider-manager.js';
|
|
15
|
+
import type { CapabilityRouter } from './router.js';
|
|
16
|
+
import { normalizeCapabilities } from './capability-map.js';
|
|
17
|
+
export interface MissionPlannerDeps {
|
|
18
|
+
providerManager: ProviderManager;
|
|
19
|
+
router: CapabilityRouter;
|
|
20
|
+
logger: Logger;
|
|
21
|
+
}
|
|
22
|
+
export interface PlanOptions {
|
|
23
|
+
description?: string;
|
|
24
|
+
policy?: Policy;
|
|
25
|
+
/** Max phases to request (default 6). */
|
|
26
|
+
maxPhases?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare class MissionPlanner {
|
|
29
|
+
private deps;
|
|
30
|
+
constructor(deps: MissionPlannerDeps);
|
|
31
|
+
plan(goal: string, opts?: PlanOptions): Promise<MissionPhaseInput[]>;
|
|
32
|
+
private planWithLlm;
|
|
33
|
+
/** Extract + validate a phase array from a model's (possibly noisy) response. */
|
|
34
|
+
private parsePhases;
|
|
35
|
+
/**
|
|
36
|
+
* Deterministic fallback plan. Always yields buildable phases; adds a frontend
|
|
37
|
+
* phase for UI-shaped goals. Capabilities are natural — the scheduler
|
|
38
|
+
* normalizes them and derives each phase's model tier.
|
|
39
|
+
*/
|
|
40
|
+
private heuristicPhases;
|
|
41
|
+
}
|
|
42
|
+
export { normalizeCapabilities };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { CANONICAL_CAPABILITIES, normalizeCapabilities } from './capability-map.js';
|
|
2
|
+
export class MissionPlanner {
|
|
3
|
+
deps;
|
|
4
|
+
constructor(deps) {
|
|
5
|
+
this.deps = deps;
|
|
6
|
+
}
|
|
7
|
+
async plan(goal, opts = {}) {
|
|
8
|
+
const llm = await this.planWithLlm(goal, opts).catch(err => {
|
|
9
|
+
this.deps.logger.warn(`[planner] LLM planning failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
10
|
+
return null;
|
|
11
|
+
});
|
|
12
|
+
if (llm && llm.length) {
|
|
13
|
+
this.deps.logger.info(`[planner] LLM decomposed goal into ${llm.length} phase(s): ${llm.map(p => p.name).join(' → ')}`);
|
|
14
|
+
return llm;
|
|
15
|
+
}
|
|
16
|
+
const heuristic = this.heuristicPhases(goal);
|
|
17
|
+
this.deps.logger.info(`[planner] heuristic plan: ${heuristic.map(p => p.name).join(' → ')}`);
|
|
18
|
+
return heuristic;
|
|
19
|
+
}
|
|
20
|
+
async planWithLlm(goal, opts) {
|
|
21
|
+
const providers = (await this.deps.providerManager.getHealthyProviders()).map(p => p.info);
|
|
22
|
+
const models = await this.deps.providerManager.getAvailableModels();
|
|
23
|
+
if (providers.length === 0 || models.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
// Route the planning call to a capable reasoning model.
|
|
26
|
+
const decision = await this.deps.router.route({
|
|
27
|
+
capabilities: ['planning', 'reasoning'],
|
|
28
|
+
preferences: { reasoning: 85 },
|
|
29
|
+
policy: opts.policy ?? { name: 'balanced', routing: { mode: 'balanced' } },
|
|
30
|
+
availableProviders: providers,
|
|
31
|
+
availableModels: models,
|
|
32
|
+
minBenchmark: 82,
|
|
33
|
+
});
|
|
34
|
+
if (!decision)
|
|
35
|
+
return null;
|
|
36
|
+
const provider = this.deps.providerManager.get(decision.providerId);
|
|
37
|
+
if (!provider)
|
|
38
|
+
return null;
|
|
39
|
+
const maxPhases = opts.maxPhases ?? 6;
|
|
40
|
+
const request = {
|
|
41
|
+
model: decision.modelId,
|
|
42
|
+
systemPrompt: 'You are a senior software project planner for an execution kernel. Break the goal into 2–' +
|
|
43
|
+
`${maxPhases} sequential PHASES. Reply with ONLY minified JSON, no prose, of the form: ` +
|
|
44
|
+
'{"phases":[{"name":"...","description":"...","capabilities":["..."],"dependsOn":["<earlier phase name>"],"acceptanceCriteria":["..."]}]}. ' +
|
|
45
|
+
'Order phases so later ones dependsOn earlier ones. Prefer capability names from this list where they fit: ' +
|
|
46
|
+
CANONICAL_CAPABILITIES.join(', ') +
|
|
47
|
+
'. You may also use natural terms like backend, ui, tests, devops. Keep it concise and buildable.',
|
|
48
|
+
prompt: `Goal: ${goal}${opts.description ? `\nContext: ${opts.description}` : ''}\n\nPhased plan (JSON):`,
|
|
49
|
+
maxTokens: 1200,
|
|
50
|
+
temperature: 0.2,
|
|
51
|
+
};
|
|
52
|
+
const result = await provider.complete(request);
|
|
53
|
+
const parsed = this.parsePhases(result.content);
|
|
54
|
+
if (!parsed || parsed.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
return parsed.slice(0, maxPhases);
|
|
57
|
+
}
|
|
58
|
+
/** Extract + validate a phase array from a model's (possibly noisy) response. */
|
|
59
|
+
parsePhases(text) {
|
|
60
|
+
const json = extractJson(text);
|
|
61
|
+
if (!json)
|
|
62
|
+
return null;
|
|
63
|
+
let raw;
|
|
64
|
+
try {
|
|
65
|
+
raw = JSON.parse(json);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const list = Array.isArray(raw)
|
|
71
|
+
? raw
|
|
72
|
+
: Array.isArray(raw?.phases)
|
|
73
|
+
? raw.phases
|
|
74
|
+
: null;
|
|
75
|
+
if (!list)
|
|
76
|
+
return null;
|
|
77
|
+
const phases = [];
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
for (const item of list) {
|
|
80
|
+
if (!item || typeof item !== 'object')
|
|
81
|
+
continue;
|
|
82
|
+
const p = item;
|
|
83
|
+
const name = String(p.name ?? p.phase ?? `phase-${phases.length + 1}`).trim().slice(0, 40);
|
|
84
|
+
if (!name || seen.has(name.toLowerCase()))
|
|
85
|
+
continue;
|
|
86
|
+
seen.add(name.toLowerCase());
|
|
87
|
+
const description = String(p.description ?? name).trim();
|
|
88
|
+
const capabilities = Array.isArray(p.capabilities) ? p.capabilities.map(String) : undefined;
|
|
89
|
+
const dependsOn = Array.isArray(p.dependsOn) ? p.dependsOn.map(String) : undefined;
|
|
90
|
+
const acceptanceCriteria = Array.isArray(p.acceptanceCriteria)
|
|
91
|
+
? p.acceptanceCriteria.map(String)
|
|
92
|
+
: undefined;
|
|
93
|
+
phases.push({ name, description, capabilities, dependsOn, acceptanceCriteria });
|
|
94
|
+
}
|
|
95
|
+
return phases.length ? phases : null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Deterministic fallback plan. Always yields buildable phases; adds a frontend
|
|
99
|
+
* phase for UI-shaped goals. Capabilities are natural — the scheduler
|
|
100
|
+
* normalizes them and derives each phase's model tier.
|
|
101
|
+
*/
|
|
102
|
+
heuristicPhases(goal) {
|
|
103
|
+
const g = goal.toLowerCase();
|
|
104
|
+
const isUi = /\b(ui|ux|frontend|front-end|web|website|page|css|react|vue|svelte|design|dashboard|landing)\b/.test(g);
|
|
105
|
+
const phases = [
|
|
106
|
+
{
|
|
107
|
+
name: 'architecture',
|
|
108
|
+
description: `Plan the architecture, approach, and file/component breakdown for: ${goal}`,
|
|
109
|
+
capabilities: ['architecture', 'planning', 'reasoning'],
|
|
110
|
+
acceptanceCriteria: ['A concrete plan / component breakdown exists'],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'implementation',
|
|
114
|
+
description: `Implement the core of: ${goal}`,
|
|
115
|
+
capabilities: ['coding', 'filesystem', 'terminal', 'reasoning'],
|
|
116
|
+
dependsOn: ['architecture'],
|
|
117
|
+
acceptanceCriteria: ['Core functionality is implemented'],
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
if (isUi) {
|
|
121
|
+
phases.push({
|
|
122
|
+
name: 'frontend',
|
|
123
|
+
description: `Build the user interface for: ${goal}`,
|
|
124
|
+
capabilities: ['frontend', 'design', 'accessibility'],
|
|
125
|
+
dependsOn: ['architecture'],
|
|
126
|
+
acceptanceCriteria: ['A polished, responsive, accessible UI'],
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
phases.push({
|
|
130
|
+
name: 'verification',
|
|
131
|
+
description: `Test, review, and verify the implementation of: ${goal}`,
|
|
132
|
+
capabilities: ['verification', 'review', 'reasoning'],
|
|
133
|
+
dependsOn: isUi ? ['implementation', 'frontend'] : ['implementation'],
|
|
134
|
+
acceptanceCriteria: ['Builds, tests pass, and the goal is met'],
|
|
135
|
+
});
|
|
136
|
+
return phases;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Pull the first balanced JSON object/array out of a text blob. */
|
|
140
|
+
function extractJson(text) {
|
|
141
|
+
const trimmed = text.trim().replace(/^```(?:json)?/i, '').replace(/```$/, '').trim();
|
|
142
|
+
const start = trimmed.search(/[[{]/);
|
|
143
|
+
if (start === -1)
|
|
144
|
+
return null;
|
|
145
|
+
const open = trimmed[start];
|
|
146
|
+
const close = open === '{' ? '}' : ']';
|
|
147
|
+
let depth = 0;
|
|
148
|
+
let inStr = false;
|
|
149
|
+
let esc = false;
|
|
150
|
+
for (let i = start; i < trimmed.length; i++) {
|
|
151
|
+
const ch = trimmed[i];
|
|
152
|
+
if (inStr) {
|
|
153
|
+
if (esc)
|
|
154
|
+
esc = false;
|
|
155
|
+
else if (ch === '\\')
|
|
156
|
+
esc = true;
|
|
157
|
+
else if (ch === '"')
|
|
158
|
+
inStr = false;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (ch === '"')
|
|
162
|
+
inStr = true;
|
|
163
|
+
else if (ch === open)
|
|
164
|
+
depth++;
|
|
165
|
+
else if (ch === close) {
|
|
166
|
+
depth--;
|
|
167
|
+
if (depth === 0)
|
|
168
|
+
return trimmed.slice(start, i + 1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
// Re-export so harnesses can normalize capabilities the same way.
|
|
174
|
+
export { normalizeCapabilities };
|
|
@@ -27,6 +27,8 @@ export interface RecoveryContext {
|
|
|
27
27
|
policy: Policy;
|
|
28
28
|
currentProviderId?: string;
|
|
29
29
|
currentModelId?: string;
|
|
30
|
+
/** How many times this task has failed verification — drives model escalation. */
|
|
31
|
+
verificationFailures?: number;
|
|
30
32
|
}
|
|
31
33
|
export declare class RecoveryManager {
|
|
32
34
|
private deps;
|
|
@@ -34,5 +36,12 @@ export declare class RecoveryManager {
|
|
|
34
36
|
classify(error: string): FailureType;
|
|
35
37
|
decide(ctx: RecoveryContext): Promise<RecoveryDecision>;
|
|
36
38
|
private findAlternateProvider;
|
|
39
|
+
/**
|
|
40
|
+
* Find a strictly MORE capable model than the one that just failed
|
|
41
|
+
* verification — the escalation ladder. Ranks across all healthy providers by
|
|
42
|
+
* benchmark and returns the best above the current model's tier, or null at
|
|
43
|
+
* the ceiling.
|
|
44
|
+
*/
|
|
45
|
+
private findMoreCapableModel;
|
|
37
46
|
private findAlternateWorker;
|
|
38
47
|
}
|
|
@@ -33,6 +33,7 @@ export class RecoveryManager {
|
|
|
33
33
|
let rotateToProviderId;
|
|
34
34
|
let rotateToModelId;
|
|
35
35
|
let newWorkerType;
|
|
36
|
+
let escalateToBenchmark;
|
|
36
37
|
switch (failureType) {
|
|
37
38
|
case '429':
|
|
38
39
|
case 'provider-down': {
|
|
@@ -61,9 +62,22 @@ export class RecoveryManager {
|
|
|
61
62
|
break;
|
|
62
63
|
}
|
|
63
64
|
case 'hallucination':
|
|
64
|
-
case 'tool-failure':
|
|
65
65
|
case 'verification-failure': {
|
|
66
|
-
|
|
66
|
+
// Quality failures usually mean the model isn't strong enough. After the
|
|
67
|
+
// first verification miss, climb the benchmark ladder to a MORE capable
|
|
68
|
+
// model before anything else; only swap worker type if we're at the top.
|
|
69
|
+
const vFails = ctx.verificationFailures ?? 0;
|
|
70
|
+
// Escalate after MULTIPLE verification misses — the first miss retries or
|
|
71
|
+
// swaps worker; the second climbs to a more capable model.
|
|
72
|
+
const better = vFails >= 2 ? await this.findMoreCapableModel(ctx) : null;
|
|
73
|
+
if (better) {
|
|
74
|
+
action = 'escalate-model';
|
|
75
|
+
reason = `${failureType}: escalate → ${better.providerId}/${better.modelId} (bench ${better.benchmarkScore})`;
|
|
76
|
+
rotateToProviderId = better.providerId;
|
|
77
|
+
rotateToModelId = better.modelId;
|
|
78
|
+
escalateToBenchmark = better.benchmarkScore;
|
|
79
|
+
}
|
|
80
|
+
else if (belowLimit) {
|
|
67
81
|
const altType = this.findAlternateWorker(ctx);
|
|
68
82
|
if (altType) {
|
|
69
83
|
action = 'spawn-different-worker';
|
|
@@ -77,7 +91,26 @@ export class RecoveryManager {
|
|
|
77
91
|
}
|
|
78
92
|
else {
|
|
79
93
|
action = 'replan';
|
|
80
|
-
reason = `${failureType}: attempts exhausted → replan`;
|
|
94
|
+
reason = `${failureType}: attempts exhausted, at model ceiling → replan`;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case 'tool-failure': {
|
|
99
|
+
if (belowLimit) {
|
|
100
|
+
const altType = this.findAlternateWorker(ctx);
|
|
101
|
+
if (altType) {
|
|
102
|
+
action = 'spawn-different-worker';
|
|
103
|
+
reason = `tool-failure: spawn ${altType} worker`;
|
|
104
|
+
newWorkerType = altType;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
action = 'retry';
|
|
108
|
+
reason = `tool-failure: retry (attempt ${ctx.attempts}/${maxAttempts})`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
action = 'replan';
|
|
113
|
+
reason = 'tool-failure: attempts exhausted → replan';
|
|
81
114
|
}
|
|
82
115
|
break;
|
|
83
116
|
}
|
|
@@ -102,6 +135,7 @@ export class RecoveryManager {
|
|
|
102
135
|
rotateToProviderId,
|
|
103
136
|
rotateToModelId,
|
|
104
137
|
newWorkerType,
|
|
138
|
+
escalateToBenchmark,
|
|
105
139
|
};
|
|
106
140
|
this.deps.eventBus.emit({
|
|
107
141
|
type: 'RECOVERY',
|
|
@@ -135,6 +169,33 @@ export class RecoveryManager {
|
|
|
135
169
|
return null;
|
|
136
170
|
return { providerId: ranked[0].providerId, modelId: ranked[0].modelId };
|
|
137
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Find a strictly MORE capable model than the one that just failed
|
|
174
|
+
* verification — the escalation ladder. Ranks across all healthy providers by
|
|
175
|
+
* benchmark and returns the best above the current model's tier, or null at
|
|
176
|
+
* the ceiling.
|
|
177
|
+
*/
|
|
178
|
+
async findMoreCapableModel(ctx) {
|
|
179
|
+
const providers = (await this.deps.providerManager.getHealthyProviders()).map(p => p.info);
|
|
180
|
+
const models = await this.deps.providerManager.getAvailableModels();
|
|
181
|
+
if (providers.length === 0 || models.length === 0)
|
|
182
|
+
return null;
|
|
183
|
+
const escalated = this.deps.router.escalate({
|
|
184
|
+
capabilities: ctx.task.capabilities,
|
|
185
|
+
preferences: { reasoning: 85 },
|
|
186
|
+
policy: ctx.policy,
|
|
187
|
+
availableProviders: providers,
|
|
188
|
+
availableModels: models,
|
|
189
|
+
minBenchmark: ctx.task.minBenchmark,
|
|
190
|
+
}, ctx.currentProviderId, ctx.currentModelId);
|
|
191
|
+
if (!escalated)
|
|
192
|
+
return null;
|
|
193
|
+
return {
|
|
194
|
+
providerId: escalated.providerId,
|
|
195
|
+
modelId: escalated.modelId,
|
|
196
|
+
benchmarkScore: escalated.benchmarkScore,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
138
199
|
findAlternateWorker(ctx) {
|
|
139
200
|
const current = this.deps.workerRuntime.getHandler(this.deps.workerRuntime.matchWorkerType(ctx.task.capabilities) ?? 'coding');
|
|
140
201
|
if (!current)
|
package/dist/kernel/router.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Every policy auto-routes across all healthy providers. The single exception
|
|
13
13
|
* is the `offline` mode, which is LOCKED to local providers.
|
|
14
14
|
*/
|
|
15
|
-
import type { ProviderInfo, RoutingDecision, RoutingRequest } from './types.js';
|
|
15
|
+
import type { ProviderInfo, ProviderModel, RoutingDecision, RoutingRequest } from './types.js';
|
|
16
16
|
import type { Logger } from './types.js';
|
|
17
17
|
export declare class CapabilityRouter {
|
|
18
18
|
private logger;
|
|
@@ -22,6 +22,17 @@ export declare class CapabilityRouter {
|
|
|
22
22
|
providerInfo: ProviderInfo;
|
|
23
23
|
benchmarkScore: number;
|
|
24
24
|
}>;
|
|
25
|
+
/** Public benchmark estimate for a model (0–100) — used by recovery/escalation. */
|
|
26
|
+
benchmarkFor(model: ProviderModel): number;
|
|
27
|
+
/**
|
|
28
|
+
* Pick a strictly MORE capable model than the current one for the same
|
|
29
|
+
* capabilities — the verification-failure escalation ladder. Ranks candidates
|
|
30
|
+
* whose benchmark exceeds the current model's, preferring the highest
|
|
31
|
+
* benchmark (tie-broken by overall score). Returns null at the ceiling.
|
|
32
|
+
*/
|
|
33
|
+
escalate(request: RoutingRequest, currentProviderId?: string, currentModelId?: string): (RoutingDecision & {
|
|
34
|
+
benchmarkScore: number;
|
|
35
|
+
}) | null;
|
|
25
36
|
private scoreCandidates;
|
|
26
37
|
private capabilityScore;
|
|
27
38
|
/**
|