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
|
@@ -8,6 +8,61 @@ function pickMission(kernel, arg, status) {
|
|
|
8
8
|
const pool = status ? missions.filter(m => m.status === status) : missions;
|
|
9
9
|
return pool[pool.length - 1];
|
|
10
10
|
}
|
|
11
|
+
const NO_MESH = 'Not part of a mesh. Start one with `infinicode serve` (or `infinicode mcp`).';
|
|
12
|
+
/** The command needs the mesh — return the federation or an error result. */
|
|
13
|
+
function needMesh(ctx) {
|
|
14
|
+
return ctx.federation ?? err(NO_MESH);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a node by display name (case-insensitive), nodeId, or nodeId prefix.
|
|
18
|
+
* "self"/"me"/"this" resolve to this node. Returns undefined if no unique match.
|
|
19
|
+
*/
|
|
20
|
+
function resolveNode(fed, query) {
|
|
21
|
+
const nodes = fed.nodeStatuses();
|
|
22
|
+
const self = nodes[0]; // nodeStatuses() lists self first
|
|
23
|
+
const q = query.trim().toLowerCase();
|
|
24
|
+
if (q === 'self' || q === 'me' || q === 'this')
|
|
25
|
+
return self;
|
|
26
|
+
const exact = nodes.find(n => n.displayName.toLowerCase() === q || n.nodeId.toLowerCase() === q);
|
|
27
|
+
if (exact)
|
|
28
|
+
return exact;
|
|
29
|
+
const prefix = nodes.filter(n => n.nodeId.toLowerCase().startsWith(q) || n.displayName.toLowerCase().startsWith(q));
|
|
30
|
+
return prefix.length === 1 ? prefix[0] : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** ISO HH:MM:SS from an epoch-ms timestamp. */
|
|
33
|
+
function clock(ts) {
|
|
34
|
+
return new Date(ts).toISOString().slice(11, 19);
|
|
35
|
+
}
|
|
36
|
+
/** "3s ago" / "2m ago" from an epoch-ms timestamp. */
|
|
37
|
+
function ago(ts) {
|
|
38
|
+
const s = Math.max(0, Math.round((Date.now() - ts) / 1000));
|
|
39
|
+
if (s < 60)
|
|
40
|
+
return `${s}s ago`;
|
|
41
|
+
if (s < 3600)
|
|
42
|
+
return `${Math.round(s / 60)}m ago`;
|
|
43
|
+
return `${Math.round(s / 3600)}h ago`;
|
|
44
|
+
}
|
|
45
|
+
/** One-line rendering of a mesh node status. */
|
|
46
|
+
function nodeLine(n, self = false) {
|
|
47
|
+
const dot = n.connected || self ? '●' : '○';
|
|
48
|
+
const load = typeof n.load === 'number' ? `${String(Math.round(n.load)).padStart(3)}%` : ' ?%';
|
|
49
|
+
const tag = self ? ' (this node)' : '';
|
|
50
|
+
return `${dot} ${n.displayName.padEnd(16)} ${n.role.padEnd(9)} v${(n.version ?? '?').padEnd(8)} load ${load} ${ago(n.lastSeenAt)}${tag}`;
|
|
51
|
+
}
|
|
52
|
+
/** Compact one-line summary of an activity stream frame. */
|
|
53
|
+
function frameLine(entry) {
|
|
54
|
+
const { f } = entry;
|
|
55
|
+
const kinds = { ev: 'event', hw: 'telem', hb: 'beat', nd: 'nodes', rt: 'route', log: 'log' };
|
|
56
|
+
const d = f.d;
|
|
57
|
+
let detail = '';
|
|
58
|
+
if (f.t === 'ev' && d)
|
|
59
|
+
detail = `${d.k ?? ''}${d.desc ? ' ' + d.desc : d.run ? ' ' + d.run : ''}`.trim();
|
|
60
|
+
else if (f.t === 'rt' && d)
|
|
61
|
+
detail = `${d.model ?? d.node ?? ''}`;
|
|
62
|
+
else if (f.t === 'log' && d)
|
|
63
|
+
detail = String(d.msg ?? d.line ?? '');
|
|
64
|
+
return `${clock(f.ts)} ${f.n.slice(0, 8).padEnd(8)} ${kinds[f.t].padEnd(6)} ${detail}`.trimEnd();
|
|
65
|
+
}
|
|
11
66
|
export const DEFAULT_COMMANDS = [
|
|
12
67
|
{
|
|
13
68
|
name: 'help',
|
|
@@ -174,29 +229,278 @@ export const DEFAULT_COMMANDS = [
|
|
|
174
229
|
{
|
|
175
230
|
name: 'goal',
|
|
176
231
|
aliases: ['g'],
|
|
177
|
-
description: '
|
|
232
|
+
description: 'Plan a goal into phases and run it (each phase gets its own model tier)',
|
|
178
233
|
usage: '/goal <text>',
|
|
179
234
|
async run({ kernel, args }) {
|
|
180
235
|
const goal = args.join(' ').trim();
|
|
181
236
|
if (!goal)
|
|
182
237
|
return err('Usage: /goal <text>');
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
tasks: [{ description: goal, capabilities: ['reasoning', 'coding'], input: { prompt: goal } }],
|
|
187
|
-
});
|
|
238
|
+
// autoPlan → the MissionPlanner decomposes the goal into phases, each
|
|
239
|
+
// routed to an appropriate model tier by its capabilities.
|
|
240
|
+
const mission = await kernel.execute({ description: goal, goal, autoPlan: true });
|
|
188
241
|
const done = mission.tasks.filter(t => t.status === 'COMPLETED').length;
|
|
242
|
+
const phases = mission.objectives
|
|
243
|
+
.map(o => `${o.phase ?? 'phase'}${o.minBenchmark ? ` ≥${o.minBenchmark}` : ''}`)
|
|
244
|
+
.join(' → ');
|
|
189
245
|
const preview = mission.tasks
|
|
190
|
-
.map(t => t.output?.content?.slice(0,
|
|
246
|
+
.map(t => t.output?.content?.slice(0, 300))
|
|
191
247
|
.filter(Boolean)
|
|
192
248
|
.join('\n---\n');
|
|
193
249
|
return {
|
|
194
250
|
ok: mission.status === 'COMPLETED',
|
|
195
|
-
text: `Mission ${mission.id} ${mission.status} (${done}/${mission.tasks.length})
|
|
251
|
+
text: `Mission ${mission.id} ${mission.status} (${done}/${mission.tasks.length})` +
|
|
252
|
+
`${phases ? `\nphases: ${phases}` : ''}${preview ? '\n' + preview : ''}`,
|
|
196
253
|
data: { missionId: mission.id },
|
|
197
254
|
};
|
|
198
255
|
},
|
|
199
256
|
},
|
|
257
|
+
{
|
|
258
|
+
name: 'mission',
|
|
259
|
+
aliases: ['mi'],
|
|
260
|
+
description: 'Detailed view of one mission (tasks + outputs)',
|
|
261
|
+
usage: '/mission [missionId|latest]',
|
|
262
|
+
run({ kernel, args }) {
|
|
263
|
+
const m = pickMission(kernel, args[0] ?? 'latest');
|
|
264
|
+
if (!m)
|
|
265
|
+
return err('No mission found. Start one with /goal <text>.');
|
|
266
|
+
const done = m.tasks.filter(t => t.status === 'COMPLETED').length;
|
|
267
|
+
const head = `${m.id} ${m.status} — ${m.name} (${done}/${m.tasks.length} done)`;
|
|
268
|
+
const tasks = m.tasks.map((t, i) => {
|
|
269
|
+
const out = t.output?.content?.replace(/\s+/g, ' ').slice(0, 120);
|
|
270
|
+
const e = t.error ? ` !${t.error.replace(/\s+/g, ' ').slice(0, 80)}` : '';
|
|
271
|
+
return ` ${String(i + 1).padStart(2)}. [${t.status.padEnd(9)}] ${t.description.slice(0, 60)}${out ? ' → ' + out : ''}${e}`;
|
|
272
|
+
});
|
|
273
|
+
return ok(`${head}\n${tasks.join('\n')}`);
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'nodes',
|
|
278
|
+
aliases: ['mesh', 'fleet'],
|
|
279
|
+
description: 'List mesh nodes: role, version, load, last seen',
|
|
280
|
+
run(ctx) {
|
|
281
|
+
const fed = needMesh(ctx);
|
|
282
|
+
if ('ok' in fed)
|
|
283
|
+
return fed;
|
|
284
|
+
const nodes = fed.nodeStatuses();
|
|
285
|
+
if (!nodes.length)
|
|
286
|
+
return ok('No nodes.');
|
|
287
|
+
const lines = nodes.map((n, i) => nodeLine(n, i === 0));
|
|
288
|
+
const connected = nodes.filter((n, i) => i === 0 || n.connected).length;
|
|
289
|
+
return ok(`${connected}/${nodes.length} node(s) online\n${lines.join('\n')}`);
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'node',
|
|
294
|
+
description: 'Inspect a node, run a command on it, or send it a task',
|
|
295
|
+
usage: '/node <name> [/command | task text]',
|
|
296
|
+
async run(ctx) {
|
|
297
|
+
const fed = needMesh(ctx);
|
|
298
|
+
if ('ok' in fed)
|
|
299
|
+
return fed;
|
|
300
|
+
const { kernel, args } = ctx;
|
|
301
|
+
const name = args[0];
|
|
302
|
+
if (!name)
|
|
303
|
+
return err('Usage: /node <name> [/command | task text]');
|
|
304
|
+
const node = resolveNode(fed, name);
|
|
305
|
+
if (!node)
|
|
306
|
+
return err(`No node matching "${name}". Try /nodes.`);
|
|
307
|
+
const isSelf = node.nodeId === fed.identity.nodeId;
|
|
308
|
+
const rest = args.slice(1).join(' ').trim();
|
|
309
|
+
// No payload → show the node's detail card.
|
|
310
|
+
if (!rest) {
|
|
311
|
+
const role = isSelf ? fed.getRole() : await fed.getRoleOf(node.nodeId).catch(() => null);
|
|
312
|
+
const caps = node.capabilities?.length ? [...new Set(node.capabilities)].join(', ') : '—';
|
|
313
|
+
const lines = [
|
|
314
|
+
nodeLine(node, isSelf),
|
|
315
|
+
` id: ${node.nodeId}`,
|
|
316
|
+
` role: ${node.role}${role?.role ? ` · agent "${role.role}"` : ''}`,
|
|
317
|
+
role?.architecture ? ` arch: ${role.architecture}` : '',
|
|
318
|
+
` caps: ${caps}`,
|
|
319
|
+
` cmds: ${node.commands?.length ?? 0} exposed`,
|
|
320
|
+
].filter(Boolean);
|
|
321
|
+
return ok(lines.join('\n'));
|
|
322
|
+
}
|
|
323
|
+
// A slash-command → run it on that node.
|
|
324
|
+
if (rest.startsWith('/')) {
|
|
325
|
+
const data = isSelf ? await kernel.command(rest) : await fed.runCommandOn(node.nodeId, rest);
|
|
326
|
+
const text = data && typeof data === 'object' && 'text' in data
|
|
327
|
+
? String(data.text)
|
|
328
|
+
: typeof data === 'string'
|
|
329
|
+
? data
|
|
330
|
+
: JSON.stringify(data);
|
|
331
|
+
return ok(`[${node.displayName}] ${rest}\n${text}`);
|
|
332
|
+
}
|
|
333
|
+
// Otherwise → dispatch it as a task to that node.
|
|
334
|
+
if (isSelf) {
|
|
335
|
+
const mission = await kernel.execute({
|
|
336
|
+
description: rest,
|
|
337
|
+
goal: rest,
|
|
338
|
+
tasks: [{ description: rest, capabilities: ['reasoning', 'coding'], input: { prompt: rest } }],
|
|
339
|
+
});
|
|
340
|
+
const preview = mission.tasks.map(t => t.output?.content?.slice(0, 400)).filter(Boolean).join('\n---\n');
|
|
341
|
+
return ok(`[${node.displayName}] ${mission.status}\n${preview || '(no output)'}`);
|
|
342
|
+
}
|
|
343
|
+
const res = await fed.dispatch({ description: rest, capabilities: ['reasoning', 'coding'], prompt: rest }, { preferNodeId: node.nodeId }, { wait: true, timeoutMs: 180_000 });
|
|
344
|
+
if (!res)
|
|
345
|
+
return err(`Dispatch to ${node.displayName} failed (node unreachable or not capable).`);
|
|
346
|
+
const body = res.record.outputs?.map(o => o.content ?? o.error ?? `[${o.status}]`).join('\n---\n');
|
|
347
|
+
return ok(`[${node.displayName}] run ${res.runId} ${res.record.status}\n${body || '(no output)'}`);
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: 'dispatch',
|
|
352
|
+
aliases: ['d'],
|
|
353
|
+
description: 'Send a task to the best-fit node (compute-routed)',
|
|
354
|
+
usage: '/dispatch <task text>',
|
|
355
|
+
async run(ctx) {
|
|
356
|
+
const fed = needMesh(ctx);
|
|
357
|
+
if ('ok' in fed)
|
|
358
|
+
return fed;
|
|
359
|
+
const text = ctx.args.join(' ').trim();
|
|
360
|
+
if (!text)
|
|
361
|
+
return err('Usage: /dispatch <task text>');
|
|
362
|
+
const res = await fed.dispatch({ description: text, capabilities: ['reasoning', 'coding'], prompt: text }, {}, { wait: true, timeoutMs: 180_000 });
|
|
363
|
+
if (!res)
|
|
364
|
+
return err('No capable node available to take this task.');
|
|
365
|
+
const body = res.record.outputs?.map(o => o.content ?? o.error ?? `[${o.status}]`).join('\n---\n');
|
|
366
|
+
return ok(`dispatched to ${res.peerId} — run ${res.runId} ${res.record.status}\n${body || '(no output)'}`);
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: 'build',
|
|
371
|
+
aliases: ['b'],
|
|
372
|
+
description: 'Dispatch a PHASED build (auto-planned) to a node — non-blocking',
|
|
373
|
+
usage: '/build <node> <goal>',
|
|
374
|
+
async run(ctx) {
|
|
375
|
+
const fed = needMesh(ctx);
|
|
376
|
+
if ('ok' in fed)
|
|
377
|
+
return fed;
|
|
378
|
+
const name = ctx.args[0];
|
|
379
|
+
const goal = ctx.args.slice(1).join(' ').trim();
|
|
380
|
+
if (!name || !goal)
|
|
381
|
+
return err('Usage: /build <node> <goal>');
|
|
382
|
+
const node = resolveNode(fed, name);
|
|
383
|
+
if (!node)
|
|
384
|
+
return err(`No node matching "${name}". Try /nodes.`);
|
|
385
|
+
// Non-blocking: the node decomposes the goal into phases and runs them; we
|
|
386
|
+
// return a runId immediately and monitor via /activity + /running + /follow.
|
|
387
|
+
const res = await fed.dispatch({ description: goal.slice(0, 80), capabilities: ['architecture', 'coding', 'reasoning'], prompt: goal, autoPlan: true }, { preferNodeId: node.nodeId }, { wait: false });
|
|
388
|
+
if (!res)
|
|
389
|
+
return err(`Dispatch to ${node.displayName} failed (node unreachable or not capable).`);
|
|
390
|
+
return ok(`▶ phased build dispatched to ${node.displayName} — run ${res.runId}\n` +
|
|
391
|
+
` monitor live: /activity\n in-flight: /running\n result: /follow ${res.runId}`);
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'follow',
|
|
396
|
+
aliases: ['f'],
|
|
397
|
+
description: 'Check a dispatched run\'s status + output',
|
|
398
|
+
usage: '/follow <runId>',
|
|
399
|
+
async run(ctx) {
|
|
400
|
+
const fed = needMesh(ctx);
|
|
401
|
+
if ('ok' in fed)
|
|
402
|
+
return fed;
|
|
403
|
+
const runId = ctx.args[0];
|
|
404
|
+
if (!runId)
|
|
405
|
+
return err('Usage: /follow <runId>');
|
|
406
|
+
const followed = await fed.followRun(runId);
|
|
407
|
+
if (!followed)
|
|
408
|
+
return err(`No run ${runId} (unknown or the holding node is gone).`);
|
|
409
|
+
const r = followed.record;
|
|
410
|
+
const body = r.outputs?.map(o => o.content ?? o.error ?? `[${o.status}]`).join('\n---\n');
|
|
411
|
+
return ok(`run ${r.runId} ${r.status}${r.error ? ' — ' + r.error : ''}\n${body || '(no output yet)'}`);
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
name: 'running',
|
|
416
|
+
aliases: ['ps'],
|
|
417
|
+
description: 'What is running now: missions, workers, mesh runs',
|
|
418
|
+
run(ctx) {
|
|
419
|
+
const { kernel, federation } = ctx;
|
|
420
|
+
const missions = kernel.listMissions().filter(m => m.status === 'RUNNING' || m.status === 'VERIFYING');
|
|
421
|
+
const workers = kernel.workerRuntime.getActive();
|
|
422
|
+
const sections = [];
|
|
423
|
+
sections.push(missions.length
|
|
424
|
+
? `Missions (${missions.length}):\n` +
|
|
425
|
+
missions.map(m => ` ${m.id} ${m.status} — ${m.name} (${m.tasks.filter(t => t.status === 'RUNNING').length} task(s) running)`).join('\n')
|
|
426
|
+
: 'Missions: none running');
|
|
427
|
+
sections.push(workers.length
|
|
428
|
+
? `Workers (${workers.length}):\n` + workers.map(w => ` ${w.type}:${w.status}`).join('\n')
|
|
429
|
+
: 'Workers: none active');
|
|
430
|
+
if (federation) {
|
|
431
|
+
const runs = federation.localRuns().filter(r => r.status === 'running');
|
|
432
|
+
const out = federation.outboundRuns();
|
|
433
|
+
sections.push(runs.length
|
|
434
|
+
? `Mesh runs here (${runs.length}):\n` + runs.map(r => ` ${r.runId} — ${r.description.slice(0, 60)} (${ago(r.startedAt)})`).join('\n')
|
|
435
|
+
: 'Mesh runs here: none');
|
|
436
|
+
if (out.length)
|
|
437
|
+
sections.push(`Dispatched out (${out.length}):\n` + out.map(o => ` ${o.runId} → ${o.peerId}`).join('\n'));
|
|
438
|
+
}
|
|
439
|
+
return ok(sections.join('\n'));
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: 'activity',
|
|
444
|
+
aliases: ['act', 'feed'],
|
|
445
|
+
description: 'Recent activity across the mesh (or local events)',
|
|
446
|
+
usage: '/activity [count]',
|
|
447
|
+
run(ctx) {
|
|
448
|
+
const n = Math.max(1, Math.min(200, parseInt(ctx.args[0] ?? '20', 10) || 20));
|
|
449
|
+
if (ctx.federation) {
|
|
450
|
+
const frames = ctx.federation.recentFrames(n);
|
|
451
|
+
if (!frames.length)
|
|
452
|
+
return ok('No mesh activity yet.');
|
|
453
|
+
return ok(frames.map(frameLine).join('\n'));
|
|
454
|
+
}
|
|
455
|
+
// Stand-alone: fall back to the local event stream.
|
|
456
|
+
const hist = ctx.kernel.getEventHistory().slice(-n);
|
|
457
|
+
if (!hist.length)
|
|
458
|
+
return ok('No activity yet.');
|
|
459
|
+
return ok(hist
|
|
460
|
+
.map(e => `${clock(e.timestamp)} ${e.type}${e.missionId ? ' ' + e.missionId : ''}${e.taskId ? '/' + e.taskId : ''}`)
|
|
461
|
+
.join('\n'));
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: 'whoami',
|
|
466
|
+
aliases: ['me', 'id'],
|
|
467
|
+
description: 'This node: identity, role, and agent context',
|
|
468
|
+
run(ctx) {
|
|
469
|
+
if (!ctx.federation)
|
|
470
|
+
return ok('Stand-alone kernel (not in a mesh). Run `infinicode serve` to join one.');
|
|
471
|
+
const id = ctx.federation.identity;
|
|
472
|
+
const role = ctx.federation.getRole();
|
|
473
|
+
const peers = ctx.federation.peers().length;
|
|
474
|
+
const lines = [
|
|
475
|
+
`${id.displayName} (${id.role})`,
|
|
476
|
+
` id: ${id.nodeId}`,
|
|
477
|
+
` peers: ${peers} connected`,
|
|
478
|
+
role?.role ? ` agent: "${role.role}"` : '',
|
|
479
|
+
role?.architecture ? ` arch: ${role.architecture}` : '',
|
|
480
|
+
].filter(Boolean);
|
|
481
|
+
return ok(lines.join('\n'));
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: 'role',
|
|
486
|
+
description: 'Show or set this node\'s persistent agent role/architecture',
|
|
487
|
+
usage: '/role [name] [architecture…]',
|
|
488
|
+
run(ctx) {
|
|
489
|
+
const fed = needMesh(ctx);
|
|
490
|
+
if ('ok' in fed)
|
|
491
|
+
return fed;
|
|
492
|
+
if (!ctx.args.length) {
|
|
493
|
+
const role = fed.getRole();
|
|
494
|
+
if (!role?.role)
|
|
495
|
+
return ok('No role set. Set one with /role <name> [architecture…].');
|
|
496
|
+
return ok(`role: "${role.role}"${role.architecture ? `\narch: ${role.architecture}` : ''}`);
|
|
497
|
+
}
|
|
498
|
+
const name = ctx.args[0];
|
|
499
|
+
const architecture = ctx.args.slice(1).join(' ') || undefined;
|
|
500
|
+
const saved = fed.setRole({ role: name, architecture });
|
|
501
|
+
return ok(`role set → "${saved.role}"${saved.architecture ? `\narch: ${saved.architecture}` : ''}`);
|
|
502
|
+
},
|
|
503
|
+
},
|
|
200
504
|
];
|
|
201
505
|
export class KernelCommandRegistry {
|
|
202
506
|
kernel;
|
|
@@ -220,9 +524,10 @@ export class KernelCommandRegistry {
|
|
|
220
524
|
return this.commands.get(key) ?? this.commands.get(this.aliases.get(key) ?? '');
|
|
221
525
|
}
|
|
222
526
|
async execute(input) {
|
|
527
|
+
const federation = this.kernel.federation;
|
|
223
528
|
const trimmed = (input ?? '').trim().replace(/^\//, '').trim();
|
|
224
529
|
if (!trimmed)
|
|
225
|
-
return this.resolve('help').run({ kernel: this.kernel, args: [], raw: input });
|
|
530
|
+
return this.resolve('help').run({ kernel: this.kernel, federation, args: [], raw: input });
|
|
226
531
|
const parts = trimmed.split(/\s+/);
|
|
227
532
|
const name = parts[0];
|
|
228
533
|
const args = parts.slice(1);
|
|
@@ -230,7 +535,7 @@ export class KernelCommandRegistry {
|
|
|
230
535
|
if (!cmd)
|
|
231
536
|
return err(`Unknown command: /${name}. Try /help.`);
|
|
232
537
|
try {
|
|
233
|
-
return await cmd.run({ kernel: this.kernel, args, raw: input });
|
|
538
|
+
return await cmd.run({ kernel: this.kernel, federation, args, raw: input });
|
|
234
539
|
}
|
|
235
540
|
catch (e) {
|
|
236
541
|
return err(`/${name} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -8,6 +8,12 @@ import type { WorkerType } from '../kernel/types.js';
|
|
|
8
8
|
export interface CloudProviderConfig {
|
|
9
9
|
id: string;
|
|
10
10
|
name: string;
|
|
11
|
+
/**
|
|
12
|
+
* Provider kind. 'local' registers the provider as a local (offline-eligible)
|
|
13
|
+
* endpoint — the router applies its localBonus and 'offline' mode includes it,
|
|
14
|
+
* and no API key is required. Defaults to 'cloud' when omitted.
|
|
15
|
+
*/
|
|
16
|
+
type?: 'local' | 'cloud';
|
|
11
17
|
baseURL: string;
|
|
12
18
|
apiKey?: string;
|
|
13
19
|
headerName?: string;
|
|
@@ -50,6 +56,11 @@ export interface InfinicodeConfig {
|
|
|
50
56
|
policy?: string;
|
|
51
57
|
workerModels?: Partial<Record<WorkerType, WorkerModelPref>>;
|
|
52
58
|
cloudProviders?: CloudProviderConfig[];
|
|
59
|
+
/**
|
|
60
|
+
* Local OpenAI-compatible servers (LM Studio, vLLM, SGLang, llama.cpp).
|
|
61
|
+
* Registered with type 'local'; no API key required.
|
|
62
|
+
*/
|
|
63
|
+
localProviders?: CloudProviderConfig[];
|
|
53
64
|
federation?: FederationConfig;
|
|
54
65
|
}
|
|
55
66
|
export declare const DEFAULT_CONFIG: InfinicodeConfig;
|
|
@@ -2,8 +2,10 @@ import { frame } from './protocol.js';
|
|
|
2
2
|
/** Events that should render instantly (map animation triggers). */
|
|
3
3
|
const IMMEDIATE = new Set([
|
|
4
4
|
'MISSION_STARTED', 'MISSION_COMPLETED', 'MISSION_FAILED',
|
|
5
|
+
'PLAN_CREATED', 'OBJECTIVE_STARTED', 'OBJECTIVE_COMPLETED',
|
|
5
6
|
'TASK_STARTED', 'TASK_COMPLETED', 'TASK_FAILED',
|
|
6
|
-
'WORKER_SPAWNED', 'WORKER_EXITED', 'ROUTING_DECISION',
|
|
7
|
+
'WORKER_SPAWNED', 'WORKER_EXITED', 'ROUTING_DECISION',
|
|
8
|
+
'MODEL_CHANGED', 'PROVIDER_CHANGED', 'RECOVERY', 'NOTIFICATION',
|
|
7
9
|
]);
|
|
8
10
|
/** High-frequency, low-value chatter to coalesce. */
|
|
9
11
|
const COALESCE = new Set(['LOG', 'PROVIDER_HEALTH']);
|
|
@@ -23,6 +23,9 @@ export interface DispatchTask {
|
|
|
23
23
|
prompt: string;
|
|
24
24
|
role?: string;
|
|
25
25
|
context?: Record<string, unknown>;
|
|
26
|
+
/** When true, the receiving node auto-plans the goal into phases (a phased
|
|
27
|
+
* build) instead of running it as one atomic task. */
|
|
28
|
+
autoPlan?: boolean;
|
|
26
29
|
}
|
|
27
30
|
export interface FederationOptions {
|
|
28
31
|
version: string;
|
|
@@ -100,6 +103,13 @@ export declare class Federation {
|
|
|
100
103
|
runId: string;
|
|
101
104
|
record: RunRecord;
|
|
102
105
|
} | null>;
|
|
106
|
+
/** Runs received + executing on THIS node (async handoff), newest last. */
|
|
107
|
+
localRuns(): RunRecord[];
|
|
108
|
+
/** Runs WE dispatched to peers → { runId, peerId } for monitoring. */
|
|
109
|
+
outboundRuns(): Array<{
|
|
110
|
+
runId: string;
|
|
111
|
+
peerId: string;
|
|
112
|
+
}>;
|
|
103
113
|
/** Query a dispatched run's current record (from whichever peer holds it). */
|
|
104
114
|
followRun(runId: string): Promise<{
|
|
105
115
|
record: RunRecord;
|
|
@@ -61,6 +61,7 @@ export class Federation {
|
|
|
61
61
|
logger,
|
|
62
62
|
getManifest: () => buildManifest(identity, this.deps.describe()),
|
|
63
63
|
getNodes: () => this.nodeStatuses(),
|
|
64
|
+
runCommand: (input) => this.deps.kernel.command(input),
|
|
64
65
|
onRpc: (env, remote) => this.onRpc(env, remote),
|
|
65
66
|
});
|
|
66
67
|
await this.server.start();
|
|
@@ -174,15 +175,26 @@ export class Federation {
|
|
|
174
175
|
const localRole = loadRole();
|
|
175
176
|
const preamble = rolePreamble(localRole);
|
|
176
177
|
const prompt = preamble ? `${preamble}\n\n${task.prompt}` : task.prompt;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
// Auto-planned dispatch → the remote kernel decomposes the goal into phases
|
|
179
|
+
// (architecture → implementation → verification …), each routed to its own
|
|
180
|
+
// model tier. The phase/task events stream back to the dispatcher for
|
|
181
|
+
// monitoring. Otherwise run it as a single atomic task.
|
|
182
|
+
const mission = task.autoPlan
|
|
183
|
+
? await this.deps.kernel.execute({
|
|
184
|
+
description: task.description,
|
|
185
|
+
goal: task.prompt || task.description,
|
|
186
|
+
autoPlan: true,
|
|
187
|
+
metadata: { role: task.role ?? localRole?.role, architecture: localRole?.architecture, ...task.context },
|
|
188
|
+
})
|
|
189
|
+
: await this.deps.kernel.execute({
|
|
190
|
+
description: task.description,
|
|
191
|
+
goal: task.description,
|
|
192
|
+
tasks: [{
|
|
193
|
+
description: task.description,
|
|
194
|
+
capabilities: task.capabilities.length ? task.capabilities : ['reasoning'],
|
|
195
|
+
input: { prompt, context: { role: task.role ?? localRole?.role, architecture: localRole?.architecture, ...task.context } },
|
|
196
|
+
}],
|
|
197
|
+
});
|
|
186
198
|
return {
|
|
187
199
|
missionId: mission.id,
|
|
188
200
|
status: mission.status,
|
|
@@ -238,6 +250,14 @@ export class Federation {
|
|
|
238
250
|
}
|
|
239
251
|
return { peerId: target.nodeId, runId, record };
|
|
240
252
|
}
|
|
253
|
+
/** Runs received + executing on THIS node (async handoff), newest last. */
|
|
254
|
+
localRuns() {
|
|
255
|
+
return [...this.runs.values()];
|
|
256
|
+
}
|
|
257
|
+
/** Runs WE dispatched to peers → { runId, peerId } for monitoring. */
|
|
258
|
+
outboundRuns() {
|
|
259
|
+
return [...this.dispatchedRuns.entries()].map(([runId, peerId]) => ({ runId, peerId }));
|
|
260
|
+
}
|
|
241
261
|
/** Query a dispatched run's current record (from whichever peer holds it). */
|
|
242
262
|
async followRun(runId) {
|
|
243
263
|
const local = this.runs.get(runId);
|
|
@@ -9,6 +9,9 @@ export interface MeshServerOptions {
|
|
|
9
9
|
getManifest: () => NodeManifest;
|
|
10
10
|
/** Optional: full mesh view (self + peers) for the GET /fed/nodes probe. */
|
|
11
11
|
getNodes?: () => unknown;
|
|
12
|
+
/** Optional: run a kernel slash-command on this node (POST /fed/command).
|
|
13
|
+
* Powers the in-TUI mesh command palette. */
|
|
14
|
+
runCommand?: (input: string) => Promise<unknown>;
|
|
12
15
|
onRpc: RpcHandler;
|
|
13
16
|
logger: Logger;
|
|
14
17
|
}
|
|
@@ -78,6 +78,31 @@ export class MeshServer {
|
|
|
78
78
|
this.openStream(req, res);
|
|
79
79
|
return;
|
|
80
80
|
}
|
|
81
|
+
if (req.method === 'POST' && url.startsWith('/fed/command')) {
|
|
82
|
+
if (!this.opts.runCommand) {
|
|
83
|
+
res.writeHead(404).end('no command handler');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.readBody(req)
|
|
87
|
+
.then(async (body) => {
|
|
88
|
+
let input = '';
|
|
89
|
+
try {
|
|
90
|
+
input = JSON.parse(body).input ?? '';
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
res.writeHead(400).end('bad json');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const result = await this.opts.runCommand(input);
|
|
97
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
98
|
+
res.end(JSON.stringify(result ?? { ok: false, text: 'no result' }));
|
|
99
|
+
})
|
|
100
|
+
.catch(err => {
|
|
101
|
+
this.opts.logger.warn(`[federation] command error: ${err instanceof Error ? err.message : String(err)}`);
|
|
102
|
+
res.writeHead(500).end('command failed');
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
81
106
|
if (req.method === 'POST' && url.startsWith('/fed/rpc')) {
|
|
82
107
|
this.readBody(req)
|
|
83
108
|
.then(async (body) => {
|
|
@@ -9,6 +9,11 @@ import type { ProviderModel, WorkerType } from './types.js';
|
|
|
9
9
|
export interface FreeProviderPreset {
|
|
10
10
|
id: string;
|
|
11
11
|
name: string;
|
|
12
|
+
/**
|
|
13
|
+
* Provider kind. 'local' presets register as local (offline-eligible)
|
|
14
|
+
* providers and require no API key. Defaults to 'cloud' when omitted.
|
|
15
|
+
*/
|
|
16
|
+
type?: 'local' | 'cloud';
|
|
12
17
|
baseURL: string;
|
|
13
18
|
headerName?: string;
|
|
14
19
|
/** Where to get the API key. */
|
|
@@ -24,5 +29,23 @@ export interface FreeProviderPreset {
|
|
|
24
29
|
notes?: string;
|
|
25
30
|
}
|
|
26
31
|
export declare const FREE_PROVIDERS: FreeProviderPreset[];
|
|
32
|
+
/**
|
|
33
|
+
* Local OpenAI-compatible servers the wizard can register. These speak the same
|
|
34
|
+
* /v1/chat/completions + /v1/models API as the cloud providers but run on the
|
|
35
|
+
* user's machine, so they need NO API key and register with type 'local' (which
|
|
36
|
+
* makes the router apply its localBonus and include them in 'offline' mode).
|
|
37
|
+
*
|
|
38
|
+
* knownModels are intentionally empty: every one of these servers exposes the
|
|
39
|
+
* currently loaded model(s) dynamically via /v1/models, so there is no fixed
|
|
40
|
+
* catalog to hard-code. Unreachable servers are marked unhealthy by the health
|
|
41
|
+
* check and simply skipped by the router.
|
|
42
|
+
*/
|
|
43
|
+
export declare const LOCAL_PROVIDERS: FreeProviderPreset[];
|
|
27
44
|
export declare function getProviderPreset(id: string): FreeProviderPreset | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Build ready-to-register config entries for every built-in local provider.
|
|
47
|
+
* Used to register LM Studio / vLLM / SGLang / llama.cpp out of the box when the
|
|
48
|
+
* user has not customized `config.localProviders`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function localProviderConfigs(): CloudProviderConfig[];
|
|
28
51
|
export declare function toCloudProviderConfig(preset: FreeProviderPreset, apiKey: string): CloudProviderConfig;
|
|
@@ -147,8 +147,80 @@ export const FREE_PROVIDERS = [
|
|
|
147
147
|
notes: 'Google Generative Language API. Free tier (15 RPM / 1500 RPD on Flash). Long context.',
|
|
148
148
|
},
|
|
149
149
|
];
|
|
150
|
+
/**
|
|
151
|
+
* Local OpenAI-compatible servers the wizard can register. These speak the same
|
|
152
|
+
* /v1/chat/completions + /v1/models API as the cloud providers but run on the
|
|
153
|
+
* user's machine, so they need NO API key and register with type 'local' (which
|
|
154
|
+
* makes the router apply its localBonus and include them in 'offline' mode).
|
|
155
|
+
*
|
|
156
|
+
* knownModels are intentionally empty: every one of these servers exposes the
|
|
157
|
+
* currently loaded model(s) dynamically via /v1/models, so there is no fixed
|
|
158
|
+
* catalog to hard-code. Unreachable servers are marked unhealthy by the health
|
|
159
|
+
* check and simply skipped by the router.
|
|
160
|
+
*/
|
|
161
|
+
export const LOCAL_PROVIDERS = [
|
|
162
|
+
{
|
|
163
|
+
id: 'lmstudio',
|
|
164
|
+
name: 'LM Studio',
|
|
165
|
+
type: 'local',
|
|
166
|
+
baseURL: 'http://localhost:1234/v1',
|
|
167
|
+
keyUrl: 'https://lmstudio.ai',
|
|
168
|
+
free: true,
|
|
169
|
+
recommended: {},
|
|
170
|
+
knownModels: [],
|
|
171
|
+
notes: 'LM Studio local server. Start it from the "Developer" tab. No API key required.',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'vllm',
|
|
175
|
+
name: 'vLLM',
|
|
176
|
+
type: 'local',
|
|
177
|
+
baseURL: 'http://localhost:8000/v1',
|
|
178
|
+
keyUrl: 'https://docs.vllm.ai',
|
|
179
|
+
free: true,
|
|
180
|
+
recommended: {},
|
|
181
|
+
knownModels: [],
|
|
182
|
+
notes: 'vLLM OpenAI-compatible server (`vllm serve <model>`). No API key required.',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'sglang',
|
|
186
|
+
name: 'SGLang',
|
|
187
|
+
type: 'local',
|
|
188
|
+
baseURL: 'http://localhost:30000/v1',
|
|
189
|
+
keyUrl: 'https://docs.sglang.ai',
|
|
190
|
+
free: true,
|
|
191
|
+
recommended: {},
|
|
192
|
+
knownModels: [],
|
|
193
|
+
notes: 'SGLang OpenAI-compatible server (`python -m sglang.launch_server`). No API key required.',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'llamacpp',
|
|
197
|
+
name: 'llama.cpp',
|
|
198
|
+
type: 'local',
|
|
199
|
+
baseURL: 'http://localhost:8080/v1',
|
|
200
|
+
keyUrl: 'https://github.com/ggml-org/llama.cpp',
|
|
201
|
+
free: true,
|
|
202
|
+
recommended: {},
|
|
203
|
+
knownModels: [],
|
|
204
|
+
notes: 'llama.cpp server (`llama-server`). No API key required.',
|
|
205
|
+
},
|
|
206
|
+
];
|
|
150
207
|
export function getProviderPreset(id) {
|
|
151
|
-
return FREE_PROVIDERS.find(p => p.id === id);
|
|
208
|
+
return FREE_PROVIDERS.find(p => p.id === id) ?? LOCAL_PROVIDERS.find(p => p.id === id);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Build ready-to-register config entries for every built-in local provider.
|
|
212
|
+
* Used to register LM Studio / vLLM / SGLang / llama.cpp out of the box when the
|
|
213
|
+
* user has not customized `config.localProviders`.
|
|
214
|
+
*/
|
|
215
|
+
export function localProviderConfigs() {
|
|
216
|
+
return LOCAL_PROVIDERS.map((preset) => ({
|
|
217
|
+
id: preset.id,
|
|
218
|
+
name: preset.name,
|
|
219
|
+
type: 'local',
|
|
220
|
+
baseURL: preset.baseURL,
|
|
221
|
+
headerName: preset.headerName,
|
|
222
|
+
enabled: true,
|
|
223
|
+
}));
|
|
152
224
|
}
|
|
153
225
|
export function toCloudProviderConfig(preset, apiKey) {
|
|
154
226
|
return {
|