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.
Files changed (43) hide show
  1. package/.opencode/plugins/mesh-commands.tsx +139 -0
  2. package/dist/cli.js +2 -1
  3. package/dist/commands/mcp.js +2 -0
  4. package/dist/commands/run.js +100 -3
  5. package/dist/commands/serve.d.ts +1 -0
  6. package/dist/commands/serve.js +36 -1
  7. package/dist/kernel/capability-map.d.ts +18 -0
  8. package/dist/kernel/capability-map.js +84 -0
  9. package/dist/kernel/checkpoint-engine.js +9 -0
  10. package/dist/kernel/command-system.d.ts +3 -0
  11. package/dist/kernel/command-system.js +315 -10
  12. package/dist/kernel/config-schema.d.ts +11 -0
  13. package/dist/kernel/config-schema.js +1 -0
  14. package/dist/kernel/federation/event-bridge.js +3 -1
  15. package/dist/kernel/federation/federation.d.ts +10 -0
  16. package/dist/kernel/federation/federation.js +29 -9
  17. package/dist/kernel/federation/transport-http.d.ts +3 -0
  18. package/dist/kernel/federation/transport-http.js +25 -0
  19. package/dist/kernel/free-providers.d.ts +23 -0
  20. package/dist/kernel/free-providers.js +73 -1
  21. package/dist/kernel/index.d.ts +4 -0
  22. package/dist/kernel/index.js +3 -0
  23. package/dist/kernel/kernel.d.ts +12 -0
  24. package/dist/kernel/kernel.js +29 -2
  25. package/dist/kernel/mission-engine.js +14 -0
  26. package/dist/kernel/orchestrator.d.ts +3 -0
  27. package/dist/kernel/orchestrator.js +74 -2
  28. package/dist/kernel/planner.d.ts +42 -0
  29. package/dist/kernel/planner.js +174 -0
  30. package/dist/kernel/recovery-manager.d.ts +9 -0
  31. package/dist/kernel/recovery-manager.js +64 -3
  32. package/dist/kernel/router.d.ts +12 -1
  33. package/dist/kernel/router.js +36 -1
  34. package/dist/kernel/scheduler.d.ts +17 -0
  35. package/dist/kernel/scheduler.js +97 -1
  36. package/dist/kernel/setup.d.ts +9 -0
  37. package/dist/kernel/setup.js +38 -0
  38. package/dist/kernel/types.d.ts +65 -2
  39. package/dist/kernel/verification-engine.js +83 -27
  40. package/dist/kernel/verification-exec.d.ts +14 -0
  41. package/dist/kernel/verification-exec.js +65 -0
  42. package/dist/kernel/worker-runtime.js +3 -2
  43. 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: 'Start and run a new mission from a goal',
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
- const mission = await kernel.execute({
184
- description: goal,
185
- goal,
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, 400))
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})${preview ? '\n' + preview : ''}`,
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;
@@ -7,6 +7,7 @@ export const DEFAULT_CONFIG = {
7
7
  policy: 'balanced',
8
8
  workerModels: {},
9
9
  cloudProviders: [],
10
+ localProviders: [],
10
11
  };
11
12
  export const WORKER_TYPES_ORDER = [
12
13
  'coding',
@@ -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', 'RECOVERY', 'NOTIFICATION',
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
- const mission = await this.deps.kernel.execute({
178
- description: task.description,
179
- goal: task.description,
180
- tasks: [{
181
- description: task.description,
182
- capabilities: task.capabilities.length ? task.capabilities : ['reasoning'],
183
- input: { prompt, context: { role: task.role ?? localRole?.role, architecture: localRole?.architecture, ...task.context } },
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 {