infinicode 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/plugins/home-logo-animation.tsx +120 -0
- package/.opencode/plugins/routing-mode-display.tsx +135 -0
- package/.opencode/themes/infinibot-gold.json +222 -0
- package/.opencode/tui.json +8 -0
- package/README.md +527 -75
- package/dist/ascii-video-animation.d.ts +2 -0
- package/dist/ascii-video-animation.js +243 -0
- package/dist/cli.js +195 -50
- package/dist/commands/console.d.ts +6 -0
- package/dist/commands/console.js +111 -0
- package/dist/commands/kernel-setup.d.ts +3 -0
- package/dist/commands/kernel-setup.js +303 -0
- package/dist/commands/mcp.d.ts +12 -0
- package/dist/commands/mcp.js +96 -0
- package/dist/commands/mission.d.ts +16 -0
- package/dist/commands/mission.js +301 -0
- package/dist/commands/models.d.ts +3 -1
- package/dist/commands/models.js +111 -55
- package/dist/commands/providers.d.ts +6 -0
- package/dist/commands/providers.js +95 -0
- package/dist/commands/run.d.ts +2 -1
- package/dist/commands/run.js +349 -59
- package/dist/commands/serve.d.ts +18 -0
- package/dist/commands/serve.js +127 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +77 -44
- package/dist/commands/status.d.ts +2 -1
- package/dist/commands/status.js +46 -30
- package/dist/commands/workers.d.ts +5 -0
- package/dist/commands/workers.js +103 -0
- package/dist/kernel/autonomy/goal-loop.d.ts +49 -0
- package/dist/kernel/autonomy/goal-loop.js +113 -0
- package/dist/kernel/autonomy/index.d.ts +8 -0
- package/dist/kernel/autonomy/index.js +7 -0
- package/dist/kernel/browser/browser-controller.d.ts +57 -0
- package/dist/kernel/browser/browser-controller.js +175 -0
- package/dist/kernel/checkpoint-engine.d.ts +17 -0
- package/dist/kernel/checkpoint-engine.js +128 -0
- package/dist/kernel/command-system.d.ts +37 -0
- package/dist/kernel/command-system.js +239 -0
- package/dist/kernel/config-schema.d.ts +53 -0
- package/dist/kernel/config-schema.js +36 -0
- package/dist/kernel/event-bus.d.ts +19 -0
- package/dist/kernel/event-bus.js +65 -0
- package/dist/kernel/federation/auto-update.d.ts +36 -0
- package/dist/kernel/federation/auto-update.js +57 -0
- package/dist/kernel/federation/compute-router.d.ts +27 -0
- package/dist/kernel/federation/compute-router.js +44 -0
- package/dist/kernel/federation/config-sync.d.ts +33 -0
- package/dist/kernel/federation/config-sync.js +37 -0
- package/dist/kernel/federation/discovery.d.ts +11 -0
- package/dist/kernel/federation/discovery.js +44 -0
- package/dist/kernel/federation/event-bridge.d.ts +30 -0
- package/dist/kernel/federation/event-bridge.js +61 -0
- package/dist/kernel/federation/federation-plugin.d.ts +24 -0
- package/dist/kernel/federation/federation-plugin.js +35 -0
- package/dist/kernel/federation/federation.d.ts +126 -0
- package/dist/kernel/federation/federation.js +335 -0
- package/dist/kernel/federation/index.d.ts +31 -0
- package/dist/kernel/federation/index.js +23 -0
- package/dist/kernel/federation/moltfed-adapter.d.ts +30 -0
- package/dist/kernel/federation/moltfed-adapter.js +52 -0
- package/dist/kernel/federation/moltfed-connector.d.ts +98 -0
- package/dist/kernel/federation/moltfed-connector.js +193 -0
- package/dist/kernel/federation/node-identity.d.ts +19 -0
- package/dist/kernel/federation/node-identity.js +86 -0
- package/dist/kernel/federation/peer-mesh.d.ts +46 -0
- package/dist/kernel/federation/peer-mesh.js +117 -0
- package/dist/kernel/federation/protocol.d.ts +20 -0
- package/dist/kernel/federation/protocol.js +61 -0
- package/dist/kernel/federation/role-context.d.ts +5 -0
- package/dist/kernel/federation/role-context.js +45 -0
- package/dist/kernel/federation/telemetry.d.ts +21 -0
- package/dist/kernel/federation/telemetry.js +138 -0
- package/dist/kernel/federation/transport-http.d.ts +44 -0
- package/dist/kernel/federation/transport-http.js +207 -0
- package/dist/kernel/federation/types.d.ts +212 -0
- package/dist/kernel/federation/types.js +3 -0
- package/dist/kernel/free-providers.d.ts +28 -0
- package/dist/kernel/free-providers.js +162 -0
- package/dist/kernel/frontend-scoring.d.ts +21 -0
- package/dist/kernel/frontend-scoring.js +125 -0
- package/dist/kernel/index.d.ts +63 -0
- package/dist/kernel/index.js +45 -0
- package/dist/kernel/kernel.d.ts +75 -0
- package/dist/kernel/kernel.js +223 -0
- package/dist/kernel/logger.d.ts +18 -0
- package/dist/kernel/logger.js +26 -0
- package/dist/kernel/mcp/index.d.ts +9 -0
- package/dist/kernel/mcp/index.js +8 -0
- package/dist/kernel/mcp/mcp-server.d.ts +27 -0
- package/dist/kernel/mcp/mcp-server.js +178 -0
- package/dist/kernel/mission-engine.d.ts +42 -0
- package/dist/kernel/mission-engine.js +160 -0
- package/dist/kernel/orchestrator.d.ts +51 -0
- package/dist/kernel/orchestrator.js +226 -0
- package/dist/kernel/plugin-manager.d.ts +28 -0
- package/dist/kernel/plugin-manager.js +76 -0
- package/dist/kernel/plugins/browser-plugin.d.ts +22 -0
- package/dist/kernel/plugins/browser-plugin.js +34 -0
- package/dist/kernel/plugins/dashboard-plugin.d.ts +17 -0
- package/dist/kernel/plugins/dashboard-plugin.js +72 -0
- package/dist/kernel/plugins/discord-plugin.d.ts +11 -0
- package/dist/kernel/plugins/discord-plugin.js +35 -0
- package/dist/kernel/plugins/metrics-plugin.d.ts +33 -0
- package/dist/kernel/plugins/metrics-plugin.js +86 -0
- package/dist/kernel/plugins/search-plugin.d.ts +17 -0
- package/dist/kernel/plugins/search-plugin.js +20 -0
- package/dist/kernel/plugins/slack-plugin.d.ts +11 -0
- package/dist/kernel/plugins/slack-plugin.js +35 -0
- package/dist/kernel/plugins/telegram-plugin.d.ts +20 -0
- package/dist/kernel/plugins/telegram-plugin.js +122 -0
- package/dist/kernel/policies.d.ts +33 -0
- package/dist/kernel/policies.js +105 -0
- package/dist/kernel/provider-discovery.d.ts +41 -0
- package/dist/kernel/provider-discovery.js +179 -0
- package/dist/kernel/provider-manager.d.ts +38 -0
- package/dist/kernel/provider-manager.js +206 -0
- package/dist/kernel/provider-url.d.ts +18 -0
- package/dist/kernel/provider-url.js +45 -0
- package/dist/kernel/providers/gemini-provider.d.ts +43 -0
- package/dist/kernel/providers/gemini-provider.js +203 -0
- package/dist/kernel/providers/ollama-provider.d.ts +44 -0
- package/dist/kernel/providers/ollama-provider.js +241 -0
- package/dist/kernel/providers/openai-compatible-provider.d.ts +43 -0
- package/dist/kernel/providers/openai-compatible-provider.js +233 -0
- package/dist/kernel/recovery-manager.d.ts +38 -0
- package/dist/kernel/recovery-manager.js +156 -0
- package/dist/kernel/router.d.ts +44 -0
- package/dist/kernel/router.js +222 -0
- package/dist/kernel/sample-missions.d.ts +11 -0
- package/dist/kernel/sample-missions.js +132 -0
- package/dist/kernel/scheduler.d.ts +30 -0
- package/dist/kernel/scheduler.js +147 -0
- package/dist/kernel/sdk/harness-sdk.d.ts +37 -0
- package/dist/kernel/sdk/harness-sdk.js +48 -0
- package/dist/kernel/sdk/plugin-sdk.d.ts +27 -0
- package/dist/kernel/sdk/plugin-sdk.js +40 -0
- package/dist/kernel/search/search-controller.d.ts +32 -0
- package/dist/kernel/search/search-controller.js +141 -0
- package/dist/kernel/setup.d.ts +13 -0
- package/dist/kernel/setup.js +59 -0
- package/dist/kernel/types.d.ts +479 -0
- package/dist/kernel/types.js +7 -0
- package/dist/kernel/verification-engine.d.ts +33 -0
- package/dist/kernel/verification-engine.js +287 -0
- package/dist/kernel/worker-runtime.d.ts +66 -0
- package/dist/kernel/worker-runtime.js +261 -0
- package/dist/kernel/workers/browser-worker.d.ts +6 -0
- package/dist/kernel/workers/browser-worker.js +92 -0
- package/dist/kernel/workers/builtin-workers.d.ts +20 -0
- package/dist/kernel/workers/builtin-workers.js +120 -0
- package/dist/kernel/workers/research-worker.d.ts +5 -0
- package/dist/kernel/workers/research-worker.js +90 -0
- package/dist/prompt-ascii-video-animation.d.ts +2 -0
- package/dist/prompt-ascii-video-animation.js +243 -0
- package/package.json +43 -9
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { openAICompatBaseURL } from '../provider-url.js';
|
|
2
|
+
export class OpenAICompatibleProvider {
|
|
3
|
+
info;
|
|
4
|
+
baseURL;
|
|
5
|
+
apiKey;
|
|
6
|
+
headerName;
|
|
7
|
+
timeoutMs;
|
|
8
|
+
knownModels;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
// Normalize to the canonical OpenAI-compatible base (exactly one version
|
|
11
|
+
// segment where the provider expects it) so chat/models URLs are correct
|
|
12
|
+
// regardless of how the base URL was stored.
|
|
13
|
+
this.baseURL = openAICompatBaseURL(options.baseURL);
|
|
14
|
+
this.apiKey = options.apiKey;
|
|
15
|
+
this.headerName = options.headerName ?? 'Authorization';
|
|
16
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
17
|
+
this.knownModels = options.knownModels ?? [];
|
|
18
|
+
this.info = {
|
|
19
|
+
id: options.id,
|
|
20
|
+
name: options.name,
|
|
21
|
+
type: options.type ?? 'cloud',
|
|
22
|
+
healthy: false,
|
|
23
|
+
available: !!this.apiKey || options.type === 'local',
|
|
24
|
+
baseURL: this.baseURL,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async capabilities() {
|
|
28
|
+
const models = await this.fetchModels();
|
|
29
|
+
return {
|
|
30
|
+
streaming: true,
|
|
31
|
+
functionCalling: true,
|
|
32
|
+
vision: models.some(m => m.supportsVision),
|
|
33
|
+
maxContextLength: Math.max(8192, ...models.map(m => m.contextLength)),
|
|
34
|
+
models,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async complete(request) {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: this.headers(),
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
model: request.model,
|
|
44
|
+
messages: this.buildMessages(request),
|
|
45
|
+
temperature: request.temperature ?? 0.3,
|
|
46
|
+
max_tokens: request.maxTokens ?? 4096,
|
|
47
|
+
stream: false,
|
|
48
|
+
}),
|
|
49
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`${this.info.name} completion failed: HTTP ${response.status}`);
|
|
53
|
+
}
|
|
54
|
+
const data = (await response.json());
|
|
55
|
+
return {
|
|
56
|
+
content: data.choices?.[0]?.message?.content ?? '',
|
|
57
|
+
providerId: this.info.id,
|
|
58
|
+
modelId: request.model,
|
|
59
|
+
tokensIn: data.usage?.prompt_tokens ?? 0,
|
|
60
|
+
tokensOut: data.usage?.completion_tokens ?? 0,
|
|
61
|
+
durationMs: Date.now() - start,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async *completeStream(request) {
|
|
65
|
+
const response = await fetch(`${this.baseURL}/chat/completions`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: this.headers(),
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
model: request.model,
|
|
70
|
+
messages: this.buildMessages(request),
|
|
71
|
+
temperature: request.temperature ?? 0.3,
|
|
72
|
+
max_tokens: request.maxTokens ?? 4096,
|
|
73
|
+
stream: true,
|
|
74
|
+
}),
|
|
75
|
+
signal: AbortSignal.timeout(this.timeoutMs * 2),
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok || !response.body) {
|
|
78
|
+
throw new Error(`${this.info.name} stream failed: HTTP ${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
const reader = response.body.getReader();
|
|
81
|
+
const decoder = new TextDecoder();
|
|
82
|
+
let buffer = '';
|
|
83
|
+
while (true) {
|
|
84
|
+
const { done, value } = await reader.read();
|
|
85
|
+
if (done)
|
|
86
|
+
break;
|
|
87
|
+
buffer += decoder.decode(value, { stream: true });
|
|
88
|
+
const lines = buffer.split('\n');
|
|
89
|
+
buffer = lines.pop() ?? '';
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (!trimmed.startsWith('data: '))
|
|
93
|
+
continue;
|
|
94
|
+
const payload = trimmed.slice(6);
|
|
95
|
+
if (payload === '[DONE]') {
|
|
96
|
+
yield { delta: '', done: true, providerId: this.info.id, modelId: request.model };
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const chunk = JSON.parse(payload);
|
|
101
|
+
const delta = chunk.choices?.[0]?.delta?.content ?? '';
|
|
102
|
+
if (delta) {
|
|
103
|
+
yield { delta, done: false, providerId: this.info.id, modelId: request.model };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// skip
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async refresh() {
|
|
113
|
+
await this.fetchModels();
|
|
114
|
+
}
|
|
115
|
+
async verify() {
|
|
116
|
+
const start = Date.now();
|
|
117
|
+
try {
|
|
118
|
+
// Prefer a curated chat model — an arbitrary first /models entry may be a
|
|
119
|
+
// non-chat model (audio/guard) that rejects chat completions.
|
|
120
|
+
let model = this.knownModels[0]?.id;
|
|
121
|
+
if (!model) {
|
|
122
|
+
try {
|
|
123
|
+
model = (await this.fetchModels())[0]?.id;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// network failure — no model; fail below
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!model)
|
|
130
|
+
return { ok: false, latencyMs: Date.now() - start, error: 'no model available to verify' };
|
|
131
|
+
// Raw fetch (not complete()) so we can read the tier's rate-limit headers.
|
|
132
|
+
const res = await fetch(`${this.baseURL}/chat/completions`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: this.headers(),
|
|
135
|
+
body: JSON.stringify({ model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, temperature: 0 }),
|
|
136
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
137
|
+
});
|
|
138
|
+
const latencyMs = Date.now() - start;
|
|
139
|
+
const rateLimitTokens = this.parseTokenLimit(res);
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const body = await res.text().catch(() => '');
|
|
142
|
+
return { ok: false, latencyMs, error: `HTTP ${res.status} ${body.slice(0, 120)}`, rateLimitTokens };
|
|
143
|
+
}
|
|
144
|
+
return { ok: true, latencyMs, rateLimitTokens };
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
return { ok: false, latencyMs: Date.now() - start, error: err instanceof Error ? err.message : String(err) };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
parseTokenLimit(res) {
|
|
151
|
+
const raw = res.headers.get('x-ratelimit-limit-tokens') ??
|
|
152
|
+
res.headers.get('x-ratelimit-limit-tokens-minute') ??
|
|
153
|
+
res.headers.get('ratelimit-limit-tokens');
|
|
154
|
+
if (!raw)
|
|
155
|
+
return undefined;
|
|
156
|
+
const n = parseInt(raw, 10);
|
|
157
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
158
|
+
}
|
|
159
|
+
async fetchModels() {
|
|
160
|
+
let response;
|
|
161
|
+
try {
|
|
162
|
+
response = await fetch(`${this.baseURL}/models`, {
|
|
163
|
+
headers: this.headers(),
|
|
164
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
// Network-level failure (DNS/connection/TLS): the provider is genuinely
|
|
169
|
+
// unreachable. Surface it so the health check marks the provider
|
|
170
|
+
// unhealthy and the router won't route the app to a dead endpoint.
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
// Endpoint is reachable but /models is unsupported or errored — fall back
|
|
175
|
+
// to the known catalog (many providers serve chat without a models list).
|
|
176
|
+
return this.knownModels.map(m => this.toProviderModel(m.id, m.contextLength));
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const data = (await response.json());
|
|
180
|
+
return (data.data ?? []).map(m => this.toProviderModel(m.id, m.context_length ?? m.context_window));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return this.knownModels.map(m => this.toProviderModel(m.id, m.contextLength));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
toProviderModel(id, contextLength) {
|
|
187
|
+
const known = this.knownModels.find(m => m.id === id);
|
|
188
|
+
const lower = id.toLowerCase();
|
|
189
|
+
const capabilities = known?.capabilities ?? ['reasoning'];
|
|
190
|
+
if (!capabilities.includes('reasoning'))
|
|
191
|
+
capabilities.push('reasoning');
|
|
192
|
+
if (lower.includes('code') || lower.includes('coder'))
|
|
193
|
+
capabilities.push('coding');
|
|
194
|
+
if (lower.includes('vision') || lower.includes('vl'))
|
|
195
|
+
capabilities.push('vision');
|
|
196
|
+
return {
|
|
197
|
+
id,
|
|
198
|
+
name: known?.name ?? id,
|
|
199
|
+
providerId: this.info.id,
|
|
200
|
+
contextLength: contextLength ?? known?.contextLength ?? 8192,
|
|
201
|
+
capabilities,
|
|
202
|
+
supportsStreaming: known?.supportsStreaming ?? true,
|
|
203
|
+
supportsFunctionCalling: known?.supportsFunctionCalling ?? true,
|
|
204
|
+
supportsVision: known?.supportsVision ?? false,
|
|
205
|
+
benchmarkScore: known?.benchmarkScore,
|
|
206
|
+
maxRequestTokens: known?.maxRequestTokens,
|
|
207
|
+
costPer1kTokens: known?.costPer1kTokens,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
headers() {
|
|
211
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
212
|
+
if (this.apiKey) {
|
|
213
|
+
headers[this.headerName] = this.headerName === 'Authorization'
|
|
214
|
+
? `Bearer ${this.apiKey}`
|
|
215
|
+
: this.apiKey;
|
|
216
|
+
}
|
|
217
|
+
return headers;
|
|
218
|
+
}
|
|
219
|
+
buildMessages(request) {
|
|
220
|
+
const messages = [];
|
|
221
|
+
if (request.systemPrompt) {
|
|
222
|
+
messages.push({ role: 'system', content: request.systemPrompt });
|
|
223
|
+
}
|
|
224
|
+
if (request.context) {
|
|
225
|
+
const ctxStr = Object.entries(request.context)
|
|
226
|
+
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
227
|
+
.join('\n');
|
|
228
|
+
messages.push({ role: 'system', content: ctxStr });
|
|
229
|
+
}
|
|
230
|
+
messages.push({ role: 'user', content: request.prompt });
|
|
231
|
+
return messages;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Recovery Manager
|
|
3
|
+
*
|
|
4
|
+
* Classifies failures: 429, timeout, provider-down, hallucination,
|
|
5
|
+
* tool-failure, verification-failure, planning-failure, browser-failure.
|
|
6
|
+
*
|
|
7
|
+
* Recovery actions: retry → rotate provider → spawn different worker →
|
|
8
|
+
* replan → checkpoint → continue.
|
|
9
|
+
*/
|
|
10
|
+
import type { FailureType, Logger, Mission, Policy, RecoveryDecision, Task } from './types.js';
|
|
11
|
+
import type { EventBus } from './event-bus.js';
|
|
12
|
+
import type { ProviderManager } from './provider-manager.js';
|
|
13
|
+
import type { WorkerRuntime } from './worker-runtime.js';
|
|
14
|
+
import type { CapabilityRouter } from './router.js';
|
|
15
|
+
export interface RecoveryManagerDeps {
|
|
16
|
+
eventBus: EventBus;
|
|
17
|
+
providerManager: ProviderManager;
|
|
18
|
+
workerRuntime: WorkerRuntime;
|
|
19
|
+
router: CapabilityRouter;
|
|
20
|
+
logger: Logger;
|
|
21
|
+
}
|
|
22
|
+
export interface RecoveryContext {
|
|
23
|
+
mission: Mission;
|
|
24
|
+
task: Task;
|
|
25
|
+
error: string;
|
|
26
|
+
attempts: number;
|
|
27
|
+
policy: Policy;
|
|
28
|
+
currentProviderId?: string;
|
|
29
|
+
currentModelId?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare class RecoveryManager {
|
|
32
|
+
private deps;
|
|
33
|
+
constructor(deps: RecoveryManagerDeps);
|
|
34
|
+
classify(error: string): FailureType;
|
|
35
|
+
decide(ctx: RecoveryContext): Promise<RecoveryDecision>;
|
|
36
|
+
private findAlternateProvider;
|
|
37
|
+
private findAlternateWorker;
|
|
38
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export class RecoveryManager {
|
|
2
|
+
deps;
|
|
3
|
+
constructor(deps) {
|
|
4
|
+
this.deps = deps;
|
|
5
|
+
}
|
|
6
|
+
classify(error) {
|
|
7
|
+
const lower = error.toLowerCase();
|
|
8
|
+
if (/429|rate.?limit|too many requests/.test(lower))
|
|
9
|
+
return '429';
|
|
10
|
+
if (/timeout|timed out|deadline exceeded|abort/.test(lower))
|
|
11
|
+
return 'timeout';
|
|
12
|
+
if (/econnrefused|enotfound|unreachable|provider down|network|socket hang up|fetch failed/.test(lower)) {
|
|
13
|
+
return 'provider-down';
|
|
14
|
+
}
|
|
15
|
+
if (/hallucinat|fabricat|invented|not grounded/.test(lower))
|
|
16
|
+
return 'hallucination';
|
|
17
|
+
if (/tool|command|exec|spawn/.test(lower))
|
|
18
|
+
return 'tool-failure';
|
|
19
|
+
if (/verif|verify|acceptance|did not meet|failed check/.test(lower))
|
|
20
|
+
return 'verification-failure';
|
|
21
|
+
if (/plan|planning|objective|dependency/.test(lower))
|
|
22
|
+
return 'planning-failure';
|
|
23
|
+
if (/browser|playwright|page|dom|selector/.test(lower))
|
|
24
|
+
return 'browser-failure';
|
|
25
|
+
return 'unknown';
|
|
26
|
+
}
|
|
27
|
+
async decide(ctx) {
|
|
28
|
+
const failureType = this.classify(ctx.error);
|
|
29
|
+
const maxAttempts = ctx.policy.retry?.maxAttempts ?? 3;
|
|
30
|
+
const belowLimit = ctx.attempts < maxAttempts;
|
|
31
|
+
let action;
|
|
32
|
+
let reason;
|
|
33
|
+
let rotateToProviderId;
|
|
34
|
+
let rotateToModelId;
|
|
35
|
+
let newWorkerType;
|
|
36
|
+
switch (failureType) {
|
|
37
|
+
case '429':
|
|
38
|
+
case 'provider-down': {
|
|
39
|
+
if (belowLimit) {
|
|
40
|
+
const alt = await this.findAlternateProvider(ctx);
|
|
41
|
+
if (alt) {
|
|
42
|
+
action = 'rotate-provider';
|
|
43
|
+
reason = `${failureType}: rotate to ${alt.providerId}/${alt.modelId}`;
|
|
44
|
+
rotateToProviderId = alt.providerId;
|
|
45
|
+
rotateToModelId = alt.modelId;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
action = belowLimit ? 'retry' : 'abort';
|
|
49
|
+
reason = `${failureType}: ${action} (no alternate provider)`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
action = 'abort';
|
|
54
|
+
reason = `${failureType}: attempts exhausted (${ctx.attempts}/${maxAttempts})`;
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case 'timeout': {
|
|
59
|
+
action = belowLimit ? 'retry' : 'abort';
|
|
60
|
+
reason = `timeout: ${action} (attempt ${ctx.attempts}/${maxAttempts})`;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'hallucination':
|
|
64
|
+
case 'tool-failure':
|
|
65
|
+
case 'verification-failure': {
|
|
66
|
+
if (belowLimit) {
|
|
67
|
+
const altType = this.findAlternateWorker(ctx);
|
|
68
|
+
if (altType) {
|
|
69
|
+
action = 'spawn-different-worker';
|
|
70
|
+
reason = `${failureType}: spawn ${altType} worker`;
|
|
71
|
+
newWorkerType = altType;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
action = 'retry';
|
|
75
|
+
reason = `${failureType}: retry (attempt ${ctx.attempts}/${maxAttempts})`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
action = 'replan';
|
|
80
|
+
reason = `${failureType}: attempts exhausted → replan`;
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'planning-failure': {
|
|
85
|
+
action = 'replan';
|
|
86
|
+
reason = 'planning-failure: replan';
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case 'browser-failure': {
|
|
90
|
+
action = belowLimit ? 'retry' : 'replan';
|
|
91
|
+
reason = `browser-failure: ${action}`;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
default: {
|
|
95
|
+
action = belowLimit ? 'retry' : 'abort';
|
|
96
|
+
reason = `unknown failure: ${action} (attempt ${ctx.attempts}/${maxAttempts})`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const decision = {
|
|
100
|
+
action,
|
|
101
|
+
reason,
|
|
102
|
+
rotateToProviderId,
|
|
103
|
+
rotateToModelId,
|
|
104
|
+
newWorkerType,
|
|
105
|
+
};
|
|
106
|
+
this.deps.eventBus.emit({
|
|
107
|
+
type: 'RECOVERY',
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
missionId: ctx.mission.id,
|
|
110
|
+
taskId: ctx.task.id,
|
|
111
|
+
data: { failureType, decision },
|
|
112
|
+
});
|
|
113
|
+
this.deps.logger.info(`Recovery[${failureType}] task ${ctx.task.id}: ${decision.action} — ${reason}`);
|
|
114
|
+
return decision;
|
|
115
|
+
}
|
|
116
|
+
async findAlternateProvider(ctx) {
|
|
117
|
+
const providers = (await this.deps.providerManager.getHealthyProviders())
|
|
118
|
+
.map(p => p.info)
|
|
119
|
+
.filter(p => p.id !== ctx.currentProviderId);
|
|
120
|
+
if (providers.length === 0)
|
|
121
|
+
return null;
|
|
122
|
+
const models = await this.deps.providerManager.getAvailableModels();
|
|
123
|
+
const candidates = models.filter(m => m.providerId !== ctx.currentProviderId || m.id !== ctx.currentModelId);
|
|
124
|
+
if (candidates.length === 0)
|
|
125
|
+
return null;
|
|
126
|
+
const policy = ctx.policy;
|
|
127
|
+
const ranked = this.deps.router.rank({
|
|
128
|
+
capabilities: ctx.task.capabilities,
|
|
129
|
+
preferences: { reasoning: 80 },
|
|
130
|
+
policy,
|
|
131
|
+
availableProviders: providers,
|
|
132
|
+
availableModels: candidates,
|
|
133
|
+
});
|
|
134
|
+
if (ranked.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
return { providerId: ranked[0].providerId, modelId: ranked[0].modelId };
|
|
137
|
+
}
|
|
138
|
+
findAlternateWorker(ctx) {
|
|
139
|
+
const current = this.deps.workerRuntime.getHandler(this.deps.workerRuntime.matchWorkerType(ctx.task.capabilities) ?? 'coding');
|
|
140
|
+
if (!current)
|
|
141
|
+
return undefined;
|
|
142
|
+
const all = this.deps.workerRuntime.listWorkerTypes();
|
|
143
|
+
for (const type of all) {
|
|
144
|
+
if (type === current.type)
|
|
145
|
+
continue;
|
|
146
|
+
const handler = this.deps.workerRuntime.getHandler(type);
|
|
147
|
+
if (!handler)
|
|
148
|
+
continue;
|
|
149
|
+
const overlap = handler.capabilities.filter(c => ctx.task.capabilities.includes(c)).length;
|
|
150
|
+
if (overlap >= Math.ceil(ctx.task.capabilities.length / 2)) {
|
|
151
|
+
return type;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Capability Router
|
|
3
|
+
*
|
|
4
|
+
* Workers never request "Gemini" — they request capabilities + preferences
|
|
5
|
+
* ("coding, reasoning >90, context >128K"). The router decides the best
|
|
6
|
+
* model + provider.
|
|
7
|
+
*
|
|
8
|
+
* Routing score (spec: "Intelligent Router"):
|
|
9
|
+
* capability + benchmark + reliability + speed + available quota
|
|
10
|
+
* + context + policy weight − cost
|
|
11
|
+
*
|
|
12
|
+
* Every policy auto-routes across all healthy providers. The single exception
|
|
13
|
+
* is the `offline` mode, which is LOCKED to local providers.
|
|
14
|
+
*/
|
|
15
|
+
import type { ProviderInfo, RoutingDecision, RoutingRequest } from './types.js';
|
|
16
|
+
import type { Logger } from './types.js';
|
|
17
|
+
export declare class CapabilityRouter {
|
|
18
|
+
private logger;
|
|
19
|
+
constructor(logger: Logger);
|
|
20
|
+
route(request: RoutingRequest): Promise<RoutingDecision | null>;
|
|
21
|
+
rank(request: RoutingRequest): Array<RoutingDecision & {
|
|
22
|
+
providerInfo: ProviderInfo;
|
|
23
|
+
benchmarkScore: number;
|
|
24
|
+
}>;
|
|
25
|
+
private scoreCandidates;
|
|
26
|
+
private capabilityScore;
|
|
27
|
+
/**
|
|
28
|
+
* Reasoning/quality contribution in [0,1]. Honors the worker preference
|
|
29
|
+
* "reasoning >= N": a model that clears the bar gets full credit for its
|
|
30
|
+
* benchmark; one below the bar is proportionally discounted.
|
|
31
|
+
*/
|
|
32
|
+
private benchmarkScore;
|
|
33
|
+
/**
|
|
34
|
+
* Estimate a 0–100 reasoning/quality benchmark for a model. Uses an explicit
|
|
35
|
+
* `benchmarkScore` when the provider supplies one; otherwise infers it from
|
|
36
|
+
* parameter size, model family, and context length.
|
|
37
|
+
*/
|
|
38
|
+
private estimateBenchmark;
|
|
39
|
+
private parseParamSize;
|
|
40
|
+
private contextScore;
|
|
41
|
+
private costScore;
|
|
42
|
+
private costPreferenceMultiplier;
|
|
43
|
+
private buildReason;
|
|
44
|
+
}
|