infinicode 2.3.1 → 2.3.3
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/dist/cli.js +2 -1
- package/dist/commands/mcp.js +2 -0
- 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 +28 -9
- 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 +7 -7
|
@@ -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
|
/**
|
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) {
|