infinicode 2.3.5 → 2.4.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.
Files changed (64) hide show
  1. package/.opencode/plugins/home-logo-animation.tsx +5 -1
  2. package/.opencode/plugins/mesh-commands.tsx +4 -3
  3. package/.opencode/plugins/routing-mode-display.tsx +6 -2
  4. package/.opencode/tui.json +2 -1
  5. package/bin/robopark.js +2 -0
  6. package/dist/cli.js +46 -1
  7. package/dist/commands/maintain.d.ts +29 -0
  8. package/dist/commands/maintain.js +186 -0
  9. package/dist/commands/mcp.js +1 -1
  10. package/dist/commands/mesh-install.d.ts +10 -0
  11. package/dist/commands/mesh-install.js +160 -0
  12. package/dist/commands/robopark.d.ts +6 -0
  13. package/dist/commands/robopark.js +450 -0
  14. package/dist/commands/run.js +82 -22
  15. package/dist/commands/serve.d.ts +1 -0
  16. package/dist/commands/serve.js +152 -11
  17. package/dist/kernel/agents/backends/cli-backend.d.ts +41 -0
  18. package/dist/kernel/agents/backends/cli-backend.js +140 -0
  19. package/dist/kernel/agents/backends/detect.d.ts +17 -0
  20. package/dist/kernel/agents/backends/detect.js +140 -0
  21. package/dist/kernel/agents/backends/index.d.ts +11 -0
  22. package/dist/kernel/agents/backends/index.js +3 -0
  23. package/dist/kernel/agents/backends/parse-utils.d.ts +20 -0
  24. package/dist/kernel/agents/backends/parse-utils.js +72 -0
  25. package/dist/kernel/agents/backends/profiles.d.ts +67 -0
  26. package/dist/kernel/agents/backends/profiles.js +48 -0
  27. package/dist/kernel/agents/backends/registry.d.ts +31 -0
  28. package/dist/kernel/agents/backends/registry.js +174 -0
  29. package/dist/kernel/agents/backends/rotation.d.ts +51 -0
  30. package/dist/kernel/agents/backends/rotation.js +107 -0
  31. package/dist/kernel/agents/backends/types.d.ts +67 -0
  32. package/dist/kernel/agents/backends/types.js +1 -0
  33. package/dist/kernel/agents/index.d.ts +8 -0
  34. package/dist/kernel/agents/index.js +8 -0
  35. package/dist/kernel/federation/dashboard-html.d.ts +13 -0
  36. package/dist/kernel/federation/dashboard-html.js +371 -0
  37. package/dist/kernel/federation/federation.d.ts +112 -2
  38. package/dist/kernel/federation/federation.js +270 -10
  39. package/dist/kernel/federation/moltfed-adapter.js +1 -0
  40. package/dist/kernel/federation/telemetry.d.ts +1 -1
  41. package/dist/kernel/federation/telemetry.js +2 -1
  42. package/dist/kernel/federation/transport-http.d.ts +17 -1
  43. package/dist/kernel/federation/transport-http.js +65 -2
  44. package/dist/kernel/federation/types.d.ts +46 -1
  45. package/dist/kernel/free-providers.js +83 -0
  46. package/dist/kernel/gateway/openai-gateway.d.ts +55 -3
  47. package/dist/kernel/gateway/openai-gateway.js +428 -49
  48. package/dist/kernel/index.d.ts +2 -1
  49. package/dist/kernel/index.js +3 -1
  50. package/dist/kernel/logger.d.ts +16 -3
  51. package/dist/kernel/logger.js +38 -0
  52. package/dist/kernel/mcp/mcp-server.js +65 -10
  53. package/dist/kernel/orchestrator.d.ts +4 -0
  54. package/dist/kernel/orchestrator.js +34 -0
  55. package/dist/kernel/provider-manager.d.ts +13 -0
  56. package/dist/kernel/provider-manager.js +58 -5
  57. package/dist/kernel/providers/openai-compatible-provider.d.ts +6 -0
  58. package/dist/kernel/providers/openai-compatible-provider.js +22 -3
  59. package/dist/kernel/recovery-manager.js +55 -15
  60. package/dist/kernel/router.js +15 -2
  61. package/dist/kernel/types.d.ts +12 -2
  62. package/dist/robopark-cli.d.ts +2 -0
  63. package/dist/robopark-cli.js +23 -0
  64. package/package.json +6 -3
@@ -0,0 +1,72 @@
1
+ /**
2
+ * OpenKernel — Backend output parsing helpers
3
+ *
4
+ * Third-party CLIs emit different JSON shapes (and different versions change
5
+ * them), so parsing is deliberately defensive: try JSON, pull the common
6
+ * fields, and always fall back to raw text. Nothing here throws.
7
+ */
8
+ export function tryJson(line) {
9
+ const s = line.trim();
10
+ if (!s || (s[0] !== '{' && s[0] !== '['))
11
+ return null;
12
+ try {
13
+ const v = JSON.parse(s);
14
+ return v && typeof v === 'object' ? v : null;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ export function str(v) {
21
+ return typeof v === 'string' && v.length ? v : undefined;
22
+ }
23
+ /** Append a line to an accumulating text body. */
24
+ export function appendLine(acc, line) {
25
+ return acc ? `${acc}\n${line}` : line;
26
+ }
27
+ /**
28
+ * Pull assistant text out of a wide range of JSON event shapes:
29
+ * Anthropic/Claude content arrays, plain `content` strings, and the usual
30
+ * `text`/`response`/`result`/`message` fields other CLIs use.
31
+ */
32
+ export function extractText(obj) {
33
+ const msg = obj.message;
34
+ const content = (msg?.content ?? obj.content);
35
+ if (Array.isArray(content)) {
36
+ const parts = content
37
+ .map(c => (c && typeof c === 'object' && c.type === 'text'
38
+ ? str(c.text)
39
+ : undefined))
40
+ .filter((t) => !!t);
41
+ if (parts.length)
42
+ return parts.join('');
43
+ }
44
+ if (typeof content === 'string' && content.length)
45
+ return content;
46
+ for (const k of ['text', 'response', 'result', 'output', 'answer']) {
47
+ const t = str(obj[k]);
48
+ if (t)
49
+ return t;
50
+ }
51
+ if (msg) {
52
+ const t = str(msg.text) ?? str(msg.content);
53
+ if (t)
54
+ return t;
55
+ }
56
+ return undefined;
57
+ }
58
+ const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
59
+ /** Common session-id keys across the CLIs, plus a bare-UUID fallback. */
60
+ export function extractSessionId(obj) {
61
+ return (str(obj.session_id) ??
62
+ str(obj.sessionID) ??
63
+ str(obj.sessionId) ??
64
+ str(obj.thread_id) ??
65
+ str(obj.conversation_id) ??
66
+ str(obj.session) ??
67
+ str(obj.id));
68
+ }
69
+ export function sessionIdFromText(line) {
70
+ const m = line.match(UUID_RE);
71
+ return m ? m[0] : undefined;
72
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * OpenKernel — Coder profiles
3
+ *
4
+ * A *profile* is a named, reusable coder configuration: which backend CLI to
5
+ * run (claude / codex / opencode / gemini), which model, and any extra args/env.
6
+ * Profiles are what the rotation engine (see `rotation.ts`) chooses between, so
7
+ * the orchestrator can say "run this on the `deep` profile" or "rotate across
8
+ * the cheap tier" without hard-coding a backend + model at every call site.
9
+ *
10
+ * Profiles carry lightweight cost/speed/quality metadata so a policy tier
11
+ * (free-first / fastest / highest-quality / …) can order them, plus `tags` a
12
+ * task-type router can match against.
13
+ *
14
+ * Config lives under `coder` in the node config; anything omitted falls back to
15
+ * the built-in defaults below so the mesh works out of the box.
16
+ */
17
+ import type { BackendId } from './types.js';
18
+ /** A named coder configuration the rotation engine can select. */
19
+ export interface CoderProfile {
20
+ /** Stable name, e.g. "deep", "cheap". Filled in by the loader from the map key. */
21
+ name: string;
22
+ /** Which CLI backend runs it. */
23
+ backend: BackendId;
24
+ /** Model id in that backend's native format (e.g. "opus", "gpt-5-codex"). */
25
+ model?: string;
26
+ /** Extra CLI args appended for this profile (via the backend's ARGS env). */
27
+ args?: string[];
28
+ /** Extra process env for this profile. */
29
+ env?: Record<string, string>;
30
+ /** Relative $ per task — lower is cheaper. Used by cost policies. */
31
+ cost?: number;
32
+ /** Relative throughput — higher is faster. Used by the `fastest` policy. */
33
+ speed?: number;
34
+ /** Relative capability — higher is stronger. Used by `highest-quality`. */
35
+ quality?: number;
36
+ /** Task-type tags this profile is a good fit for (matched by the router). */
37
+ tags?: string[];
38
+ }
39
+ export type RotationStrategy = 'auto' | 'tasktype' | 'policy' | 'roundrobin' | 'fallback';
40
+ export type CoderPolicy = 'free-first' | 'cheapest' | 'fastest' | 'highest-quality' | 'balanced';
41
+ /** The `coder` config block. Everything is optional; defaults fill the gaps. */
42
+ export interface CoderConfig {
43
+ profiles: Record<string, CoderProfile>;
44
+ /** Default ordered profile names used by roundrobin/fallback. */
45
+ rotation: string[];
46
+ /** Default strategy when a caller doesn't specify one. */
47
+ strategy: RotationStrategy;
48
+ /** Default policy when strategy resolves to policy ordering. */
49
+ policy: CoderPolicy;
50
+ /** Task-type → ordered profile names (task-type routing). */
51
+ routes: Record<string, string[]>;
52
+ /** Fallback profile when nothing else matches. */
53
+ defaultProfile: string;
54
+ }
55
+ /**
56
+ * Built-in profiles — one per backend plus a couple of tiers. Models are the
57
+ * common ids; override in config for your accounts. `cost`/`speed`/`quality`
58
+ * are relative (1–10) and only used to order policy tiers.
59
+ */
60
+ export declare const DEFAULT_PROFILES: Record<string, CoderProfile>;
61
+ /**
62
+ * Merge a raw `coder` config object (from node config) over the built-in
63
+ * defaults, filling `name` from the map key. Safe to call with `undefined`.
64
+ */
65
+ export declare function loadCoderConfig(raw?: Partial<CoderConfig> & {
66
+ profiles?: Record<string, Partial<CoderProfile>>;
67
+ }): CoderConfig;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Built-in profiles — one per backend plus a couple of tiers. Models are the
3
+ * common ids; override in config for your accounts. `cost`/`speed`/`quality`
4
+ * are relative (1–10) and only used to order policy tiers.
5
+ */
6
+ export const DEFAULT_PROFILES = {
7
+ deep: { name: 'deep', backend: 'claude', model: 'opus', cost: 9, speed: 4, quality: 10, tags: ['refactor', 'architecture', 'debug', 'hard'] },
8
+ fast: { name: 'fast', backend: 'claude', model: 'sonnet', cost: 5, speed: 7, quality: 8, tags: ['edit', 'feature', 'test'] },
9
+ precise: { name: 'precise', backend: 'codex', model: 'gpt-5-codex', cost: 6, speed: 6, quality: 8, tags: ['precision', 'polyglot', 'fix'] },
10
+ cheap: { name: 'cheap', backend: 'gemini', model: 'gemini-2.5-flash', cost: 1, speed: 9, quality: 6, tags: ['bulk', 'docs', 'boilerplate', 'cheap'] },
11
+ oss: { name: 'oss', backend: 'opencode', cost: 2, speed: 6, quality: 6, tags: ['fallback', 'oss'] },
12
+ };
13
+ /** Default rotation across all four backends (balances cost + capability). */
14
+ const DEFAULT_ROTATION = ['fast', 'precise', 'cheap', 'oss', 'deep'];
15
+ /** Sensible task-type routes → ordered profiles (head is primary, rest fallback). */
16
+ const DEFAULT_ROUTES = {
17
+ refactor: ['deep', 'precise', 'fast'],
18
+ architecture: ['deep', 'fast'],
19
+ debug: ['deep', 'precise', 'fast'],
20
+ feature: ['fast', 'precise', 'deep'],
21
+ edit: ['fast', 'cheap', 'precise'],
22
+ test: ['fast', 'precise'],
23
+ docs: ['cheap', 'fast'],
24
+ bulk: ['cheap', 'oss', 'fast'],
25
+ boilerplate: ['cheap', 'oss'],
26
+ fix: ['precise', 'fast', 'deep'],
27
+ };
28
+ /**
29
+ * Merge a raw `coder` config object (from node config) over the built-in
30
+ * defaults, filling `name` from the map key. Safe to call with `undefined`.
31
+ */
32
+ export function loadCoderConfig(raw) {
33
+ const profiles = {};
34
+ const merged = { ...DEFAULT_PROFILES, ...(raw?.profiles ?? {}) };
35
+ for (const [key, p] of Object.entries(merged)) {
36
+ if (!p?.backend)
37
+ continue; // a profile must name a backend
38
+ profiles[key] = { ...(DEFAULT_PROFILES[key] ?? {}), ...p, name: key, backend: p.backend };
39
+ }
40
+ return {
41
+ profiles,
42
+ rotation: (raw?.rotation ?? DEFAULT_ROTATION).filter(n => profiles[n]),
43
+ strategy: raw?.strategy ?? 'auto',
44
+ policy: raw?.policy ?? 'balanced',
45
+ routes: { ...DEFAULT_ROUTES, ...(raw?.routes ?? {}) },
46
+ defaultProfile: raw?.defaultProfile && profiles[raw.defaultProfile] ? raw.defaultProfile : (profiles['fast'] ? 'fast' : Object.keys(profiles)[0]),
47
+ };
48
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * OpenKernel — Agent Backend registry + built-in CLI specs
3
+ *
4
+ * Holds the executors a node can route a task to. 'kernel' (the native path) is
5
+ * always present and handled inline by the Federation manager; the CLI backends
6
+ * are advertised only when their binary is actually installed, so the mesh's
7
+ * compute-router won't send a `claude` task to a node without Claude Code.
8
+ *
9
+ * Each CLI is invoked in headless / non-interactive mode. Flags are the stable
10
+ * public ones for each tool; a device can override the binary or append args via
11
+ * env (INFINICODE_<CLI>_BIN / INFINICODE_<CLI>_ARGS) without a code change.
12
+ */
13
+ import type { AgentBackend, BackendId } from './types.js';
14
+ import { type CliBackendSpec } from './cli-backend.js';
15
+ declare const CLAUDE_SPEC: CliBackendSpec;
16
+ declare const OPENCODE_SPEC: CliBackendSpec;
17
+ declare const CODEX_SPEC: CliBackendSpec;
18
+ declare const GEMINI_SPEC: CliBackendSpec;
19
+ export declare class AgentBackendRegistry {
20
+ private map;
21
+ register(backend: AgentBackend): this;
22
+ get(id: BackendId): AgentBackend | undefined;
23
+ /** True if this node can run `id` (the native kernel, or an installed CLI). */
24
+ has(id: BackendId): boolean;
25
+ all(): AgentBackend[];
26
+ /** Backend ids runnable here — 'kernel' plus every installed CLI. */
27
+ availableIds(): BackendId[];
28
+ }
29
+ /** Registry seeded with the built-in CLI backends (availability checked lazily). */
30
+ export declare function createDefaultBackendRegistry(): AgentBackendRegistry;
31
+ export { CLAUDE_SPEC, OPENCODE_SPEC, CODEX_SPEC, GEMINI_SPEC };
@@ -0,0 +1,174 @@
1
+ import { CliAgentBackend } from './cli-backend.js';
2
+ import { tryJson, extractText, extractSessionId, sessionIdFromText } from './parse-utils.js';
3
+ // ── Claude Code ──────────────────────────────────────────────────────────────
4
+ // `claude -p` = headless print mode. stream-json emits JSONL: a system/init
5
+ // object carrying session_id, assistant messages, then a terminal `result`.
6
+ const CLAUDE_SPEC = {
7
+ id: 'claude',
8
+ displayName: 'Claude Code',
9
+ bin: 'claude',
10
+ binEnv: 'INFINICODE_CLAUDE_BIN',
11
+ extraArgsEnv: 'INFINICODE_CLAUDE_ARGS',
12
+ runArgs: (prompt, task) => [
13
+ '-p', prompt,
14
+ ...(task.model ? ['--model', task.model] : []),
15
+ '--output-format', 'stream-json',
16
+ '--verbose',
17
+ '--dangerously-skip-permissions',
18
+ ],
19
+ resumeArgs: (sessionId, message, task) => [
20
+ '--resume', sessionId,
21
+ '-p', message,
22
+ ...(task.model ? ['--model', task.model] : []),
23
+ '--output-format', 'stream-json',
24
+ '--verbose',
25
+ '--dangerously-skip-permissions',
26
+ ],
27
+ parseLine: (line, acc) => {
28
+ const obj = tryJson(line);
29
+ if (!obj)
30
+ return;
31
+ const sid = extractSessionId(obj);
32
+ if (sid)
33
+ acc.sessionId = sid;
34
+ if (obj.type === 'result') {
35
+ const t = extractText(obj);
36
+ if (t)
37
+ acc.content = t;
38
+ if (obj.is_error === true)
39
+ acc.isError = true;
40
+ if (typeof obj.subtype === 'string' && obj.subtype !== 'success')
41
+ acc.isError = true;
42
+ }
43
+ },
44
+ };
45
+ // ── OpenCode ─────────────────────────────────────────────────────────────────
46
+ // `opencode run "<task>" --format json` — non-interactive, resumable by session.
47
+ const OPENCODE_SPEC = {
48
+ id: 'opencode',
49
+ displayName: 'OpenCode',
50
+ bin: 'opencode',
51
+ binEnv: 'INFINICODE_OPENCODE_BIN',
52
+ extraArgsEnv: 'INFINICODE_OPENCODE_ARGS',
53
+ runArgs: (prompt, task) => [
54
+ 'run', prompt,
55
+ '--format', 'json',
56
+ ...(task.model ? ['--model', task.model] : []),
57
+ '--dangerously-skip-permissions',
58
+ ],
59
+ resumeArgs: (sessionId, message, task) => [
60
+ 'run', message,
61
+ '--format', 'json',
62
+ '--session', sessionId,
63
+ ...(task.model ? ['--model', task.model] : []),
64
+ '--dangerously-skip-permissions',
65
+ ],
66
+ parseLine: (line, acc) => {
67
+ const obj = tryJson(line);
68
+ if (!obj)
69
+ return;
70
+ const sid = extractSessionId(obj);
71
+ if (sid)
72
+ acc.sessionId ??= sid;
73
+ const t = extractText(obj);
74
+ if (t)
75
+ acc.content = t;
76
+ if (obj.error)
77
+ acc.isError = true;
78
+ },
79
+ };
80
+ // ── Codex ────────────────────────────────────────────────────────────────────
81
+ // `codex exec "<prompt>"` = non-interactive. Output shape varies by version, so
82
+ // parse defensively: JSON when present, otherwise accumulate text + sniff a UUID.
83
+ const CODEX_SPEC = {
84
+ id: 'codex',
85
+ displayName: 'Codex',
86
+ bin: 'codex',
87
+ binEnv: 'INFINICODE_CODEX_BIN',
88
+ extraArgsEnv: 'INFINICODE_CODEX_ARGS',
89
+ runArgs: (prompt, task) => ['exec', ...(task.model ? ['--model', task.model] : []), prompt],
90
+ resumeArgs: (sessionId, message) => ['exec', 'resume', sessionId, message],
91
+ parseLine: (line, acc) => {
92
+ const obj = tryJson(line);
93
+ if (obj) {
94
+ const sid = extractSessionId(obj);
95
+ if (sid)
96
+ acc.sessionId ??= sid;
97
+ const t = extractText(obj);
98
+ if (t)
99
+ acc.content = t;
100
+ if (obj.error || obj.is_error === true)
101
+ acc.isError = true;
102
+ return;
103
+ }
104
+ const sid = sessionIdFromText(line);
105
+ if (sid)
106
+ acc.sessionId ??= sid;
107
+ acc.content = acc.content ? `${acc.content}\n${line}` : line;
108
+ },
109
+ };
110
+ // ── Gemini ───────────────────────────────────────────────────────────────────
111
+ // `gemini -p "<prompt>"` = non-interactive. `-y` (yolo) auto-approves tool
112
+ // actions. Output is plain text by default, so parse defensively: JSON when a
113
+ // tool version emits it, otherwise accumulate the printed answer.
114
+ const GEMINI_SPEC = {
115
+ id: 'gemini',
116
+ displayName: 'Gemini CLI',
117
+ bin: 'gemini',
118
+ binEnv: 'INFINICODE_GEMINI_BIN',
119
+ extraArgsEnv: 'INFINICODE_GEMINI_ARGS',
120
+ runArgs: (prompt, task) => [
121
+ ...(task.model ? ['-m', task.model] : []),
122
+ '-y',
123
+ '-p', prompt,
124
+ ],
125
+ parseLine: (line, acc) => {
126
+ const obj = tryJson(line);
127
+ if (obj) {
128
+ const sid = extractSessionId(obj);
129
+ if (sid)
130
+ acc.sessionId ??= sid;
131
+ const t = extractText(obj);
132
+ if (t)
133
+ acc.content = t;
134
+ if (obj.error || obj.is_error === true)
135
+ acc.isError = true;
136
+ return;
137
+ }
138
+ acc.content = acc.content ? `${acc.content}\n${line}` : line;
139
+ },
140
+ };
141
+ export class AgentBackendRegistry {
142
+ map = new Map();
143
+ register(backend) {
144
+ this.map.set(backend.id, backend);
145
+ return this;
146
+ }
147
+ get(id) {
148
+ return this.map.get(id);
149
+ }
150
+ /** True if this node can run `id` (the native kernel, or an installed CLI). */
151
+ has(id) {
152
+ if (id === 'kernel')
153
+ return true;
154
+ const b = this.map.get(id);
155
+ return !!b && b.available();
156
+ }
157
+ all() {
158
+ return [...this.map.values()];
159
+ }
160
+ /** Backend ids runnable here — 'kernel' plus every installed CLI. */
161
+ availableIds() {
162
+ const cli = this.all().filter(b => b.available()).map(b => b.id);
163
+ return ['kernel', ...cli];
164
+ }
165
+ }
166
+ /** Registry seeded with the built-in CLI backends (availability checked lazily). */
167
+ export function createDefaultBackendRegistry() {
168
+ return new AgentBackendRegistry()
169
+ .register(new CliAgentBackend(CLAUDE_SPEC))
170
+ .register(new CliAgentBackend(OPENCODE_SPEC))
171
+ .register(new CliAgentBackend(CODEX_SPEC))
172
+ .register(new CliAgentBackend(GEMINI_SPEC));
173
+ }
174
+ export { CLAUDE_SPEC, OPENCODE_SPEC, CODEX_SPEC, GEMINI_SPEC };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * OpenKernel — Coder rotation engine
3
+ *
4
+ * Turns a dispatch request into an *ordered chain* of concrete backend configs.
5
+ * The four strategies are not alternatives you pick between — they compose:
6
+ *
7
+ * 1. task-type routing / policy tier → choose WHICH profiles and their order
8
+ * 2. round-robin → choose WHERE in that set to start
9
+ * 3. fallback → the returned order IS the retry chain
10
+ *
11
+ * The caller runs the head; if it fails, it walks to the next entry. Because an
12
+ * uninstalled backend simply reports "not available" (a failure), availability
13
+ * filtering falls out of fallback for free — no need to know remote state here.
14
+ */
15
+ import type { BackendId } from './types.js';
16
+ import type { CoderConfig, CoderPolicy, RotationStrategy } from './profiles.js';
17
+ /** What the orchestrator asks for. All fields optional — defaults come from config. */
18
+ export interface SelectRequest {
19
+ /** Force a strategy; otherwise config.strategy (default 'auto'). */
20
+ strategy?: RotationStrategy;
21
+ /** Pin an explicit profile by name (still yields config fallbacks behind it). */
22
+ profile?: string;
23
+ /** Pin an explicit backend (legacy path — no rotation, exact behaviour as before). */
24
+ backend?: BackendId;
25
+ /** Model override applied to the chain head. */
26
+ model?: string;
27
+ /** Task-type key for routing (e.g. 'refactor', 'docs'). */
28
+ taskType?: string;
29
+ /** Policy tier for ordering when strategy is policy/auto. */
30
+ policy?: CoderPolicy;
31
+ }
32
+ /** A concrete, directly-runnable backend selection. */
33
+ export interface ResolvedBackend {
34
+ backend: BackendId;
35
+ model?: string;
36
+ args?: string[];
37
+ env?: Record<string, string>;
38
+ profileName?: string;
39
+ }
40
+ /** Per-node round-robin cursor, keyed by candidate-set signature. */
41
+ export declare class RotationState {
42
+ private cursors;
43
+ /** Advance and return the starting offset for a candidate set of length `len`. */
44
+ next(key: string, len: number): number;
45
+ }
46
+ /**
47
+ * Build the ordered backend chain for a request. The head is what to run; the
48
+ * remainder is the fallback order. Always returns at least one entry (falls back
49
+ * to the config default profile, then any profile, then a bare 'claude').
50
+ */
51
+ export declare function selectChain(cfg: CoderConfig, req: SelectRequest, state: RotationState): ResolvedBackend[];
@@ -0,0 +1,107 @@
1
+ /** Per-node round-robin cursor, keyed by candidate-set signature. */
2
+ export class RotationState {
3
+ cursors = new Map();
4
+ /** Advance and return the starting offset for a candidate set of length `len`. */
5
+ next(key, len) {
6
+ if (len <= 0)
7
+ return 0;
8
+ const cur = this.cursors.get(key) ?? 0;
9
+ this.cursors.set(key, (cur + 1) % len);
10
+ return cur % len;
11
+ }
12
+ }
13
+ function profileToBackend(p) {
14
+ return { backend: p.backend, model: p.model, args: p.args, env: p.env, profileName: p.name };
15
+ }
16
+ /** Order profile names by a policy tier (returns a fresh array). */
17
+ function orderByPolicy(names, profiles, policy) {
18
+ const val = (n) => {
19
+ const p = profiles[n];
20
+ if (!p)
21
+ return -Infinity;
22
+ switch (policy) {
23
+ case 'free-first':
24
+ case 'cheapest': return -(p.cost ?? 5); // lower cost first
25
+ case 'fastest': return (p.speed ?? 5); // higher speed first
26
+ case 'highest-quality': return (p.quality ?? 5); // higher quality first
27
+ case 'balanced': return (p.quality ?? 5) + (p.speed ?? 5) - (p.cost ?? 5);
28
+ }
29
+ };
30
+ return [...names].sort((a, b) => val(b) - val(a));
31
+ }
32
+ /** Rotate a list so it starts at `offset` (wrapping), preserving relative order. */
33
+ function rotate(list, offset) {
34
+ if (list.length <= 1)
35
+ return [...list];
36
+ const i = ((offset % list.length) + list.length) % list.length;
37
+ return [...list.slice(i), ...list.slice(0, i)];
38
+ }
39
+ /** De-dupe by backend+model+profile so the chain never retries the identical config. */
40
+ function dedupe(chain) {
41
+ const seen = new Set();
42
+ const out = [];
43
+ for (const r of chain) {
44
+ const key = `${r.backend}|${r.model ?? ''}|${r.profileName ?? ''}`;
45
+ if (seen.has(key))
46
+ continue;
47
+ seen.add(key);
48
+ out.push(r);
49
+ }
50
+ return out;
51
+ }
52
+ /**
53
+ * Build the ordered backend chain for a request. The head is what to run; the
54
+ * remainder is the fallback order. Always returns at least one entry (falls back
55
+ * to the config default profile, then any profile, then a bare 'claude').
56
+ */
57
+ export function selectChain(cfg, req, state) {
58
+ const { profiles } = cfg;
59
+ // 1. Explicit backend pin — legacy exact behaviour, no rotation.
60
+ if (req.backend && req.backend !== 'kernel') {
61
+ return [{ backend: req.backend, model: req.model, profileName: undefined }];
62
+ }
63
+ // 2. Explicit profile pin — that profile first, then the rotation list behind it.
64
+ if (req.profile && profiles[req.profile]) {
65
+ const head = profileToBackend(profiles[req.profile]);
66
+ if (req.model)
67
+ head.model = req.model;
68
+ const rest = cfg.rotation.map(n => profiles[n]).filter(Boolean).map(profileToBackend);
69
+ return dedupe([head, ...rest]);
70
+ }
71
+ const strategy = req.strategy ?? cfg.strategy;
72
+ // 3. Determine the candidate set + its base order.
73
+ let names;
74
+ const routeNames = req.taskType ? cfg.routes[req.taskType] : undefined;
75
+ if ((strategy === 'tasktype' || strategy === 'auto') && routeNames?.length) {
76
+ names = routeNames.filter(n => profiles[n]);
77
+ }
78
+ else if (strategy === 'policy' || (strategy === 'auto' && req.policy)) {
79
+ names = orderByPolicy(cfg.rotation, profiles, req.policy ?? cfg.policy);
80
+ }
81
+ else {
82
+ names = cfg.rotation.filter(n => profiles[n]);
83
+ }
84
+ if (!names.length)
85
+ names = Object.keys(profiles);
86
+ // 4. Round-robin: advance the entry point across the candidate set. Applied for
87
+ // roundrobin explicitly, and for 'auto' when neither routing nor policy pinned
88
+ // a deliberate head (i.e. we're just cycling the default rotation).
89
+ const rotateEntry = strategy === 'roundrobin' ||
90
+ (strategy === 'auto' && !routeNames?.length && !req.policy);
91
+ if (rotateEntry) {
92
+ const offset = state.next(names.join(','), names.length);
93
+ names = rotate(names, offset);
94
+ }
95
+ // 5. Map to concrete backends; apply model override to the head only.
96
+ const chain = names.map(n => profileToBackend(profiles[n]));
97
+ if (req.model && chain[0])
98
+ chain[0] = { ...chain[0], model: req.model };
99
+ const deduped = dedupe(chain);
100
+ if (deduped.length)
101
+ return deduped;
102
+ // 6. Last-resort fallbacks.
103
+ const def = profiles[cfg.defaultProfile];
104
+ if (def)
105
+ return [profileToBackend(def)];
106
+ return [{ backend: 'claude', model: req.model }];
107
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * OpenKernel — Agent Backend types
3
+ *
4
+ * A backend is *who actually runs a dispatched task* on a node. The mesh's
5
+ * default backend is the local kernel (`kernel.execute`), but a node can also
6
+ * run a third-party coding CLI — Claude Code, Codex, OpenCode — as the executor.
7
+ * That is what lets a task dispatched across the mesh be fulfilled by a real
8
+ * external agent, and (with a persisted session id) lets those agents be
9
+ * messaged again later.
10
+ *
11
+ * Everything here is transport-agnostic: the Federation manager owns spawning +
12
+ * streaming; a backend just knows how to drive one CLI and parse its output.
13
+ */
14
+ import type { Capability } from '../../types.js';
15
+ /** Identifier of a backend. 'kernel' is the built-in native path. */
16
+ export type BackendId = 'kernel' | 'claude' | 'codex' | 'opencode' | 'gemini' | (string & {});
17
+ /** A unit of work handed to a backend. */
18
+ export interface BackendTask {
19
+ prompt: string;
20
+ capabilities?: Capability[];
21
+ /** Optional model hint (provider/model or a CLI-native model id). */
22
+ model?: string;
23
+ /** Extra CLI args for this run (e.g. from a coder profile). Appended verbatim. */
24
+ args?: string[];
25
+ /** Resume an existing CLI session instead of starting fresh. */
26
+ sessionId?: string;
27
+ }
28
+ /** A streamed activity fragment emitted while a backend runs. */
29
+ export interface BackendFrame {
30
+ kind: 'log' | 'event' | 'output' | 'error';
31
+ text?: string;
32
+ data?: unknown;
33
+ }
34
+ /** Context passed to a backend run — where to stream, how to cancel. */
35
+ export interface BackendRunContext {
36
+ runId: string;
37
+ /** Called for each activity fragment (streamed up the mesh SSE feed). */
38
+ onFrame: (frame: BackendFrame) => void;
39
+ /** Abort the underlying process. */
40
+ signal?: AbortSignal;
41
+ /** Working directory for the spawned CLI (defaults to process.cwd()). */
42
+ cwd?: string;
43
+ /** Persistent device role, prepended to the prompt so the agent is in-character. */
44
+ rolePreamble?: string;
45
+ }
46
+ /** Terminal result of a backend run. */
47
+ export interface BackendRunResult {
48
+ status: 'completed' | 'failed';
49
+ /** The CLI's native session id — enables resume + later messaging. */
50
+ sessionId?: string;
51
+ /** Final assistant text, if captured. */
52
+ content?: string;
53
+ error?: string;
54
+ }
55
+ /** Drives one executor (a CLI or the native kernel). */
56
+ export interface AgentBackend {
57
+ id: BackendId;
58
+ displayName: string;
59
+ /** True when this backend can run here (e.g. the CLI is on PATH). Sync + cheap. */
60
+ available(): boolean;
61
+ /** Run a task to completion, streaming frames along the way. */
62
+ run(task: BackendTask, ctx: BackendRunContext): Promise<BackendRunResult>;
63
+ /** Send a follow-up message into an existing session (mailbox — slice 2). */
64
+ send?(sessionId: string, message: string, ctx: BackendRunContext): Promise<BackendRunResult>;
65
+ /** Best-effort stop of a running session. */
66
+ stop?(sessionId: string): Promise<void>;
67
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * OpenKernel — Agents public API
3
+ *
4
+ * How a task actually gets run on a node: the native kernel, or a third-party
5
+ * coding CLI acting as the executor (so a mesh-dispatched task can be fulfilled
6
+ * by Claude Code, Codex, or OpenCode).
7
+ */
8
+ export * from './backends/index.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * OpenKernel — Agents public API
3
+ *
4
+ * How a task actually gets run on a node: the native kernel, or a third-party
5
+ * coding CLI acting as the executor (so a mesh-dispatched task can be fulfilled
6
+ * by Claude Code, Codex, or OpenCode).
7
+ */
8
+ export * from './backends/index.js';