@bradtaylorsf/alpha-loop 1.13.1 → 1.14.1

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 (79) hide show
  1. package/README.md +99 -1
  2. package/dist/cli.js +40 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/eval.d.ts +22 -0
  5. package/dist/commands/eval.js +105 -1
  6. package/dist/commands/eval.js.map +1 -1
  7. package/dist/commands/evolve-routing.d.ts +24 -0
  8. package/dist/commands/evolve-routing.js +320 -0
  9. package/dist/commands/evolve-routing.js.map +1 -0
  10. package/dist/commands/history.d.ts +2 -0
  11. package/dist/commands/history.js +95 -1
  12. package/dist/commands/history.js.map +1 -1
  13. package/dist/commands/init.d.ts +6 -0
  14. package/dist/commands/init.js +28 -1
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/report.d.ts +7 -0
  17. package/dist/commands/report.js +27 -0
  18. package/dist/commands/report.js.map +1 -0
  19. package/dist/commands/run.d.ts +16 -0
  20. package/dist/commands/run.js +265 -36
  21. package/dist/commands/run.js.map +1 -1
  22. package/dist/commands/scan.d.ts +1 -1
  23. package/dist/commands/scan.js.map +1 -1
  24. package/dist/engine/agents.d.ts +30 -8
  25. package/dist/engine/agents.js +94 -10
  26. package/dist/engine/agents.js.map +1 -1
  27. package/dist/engine/prerequisites.d.ts +40 -2
  28. package/dist/engine/prerequisites.js +126 -2
  29. package/dist/engine/prerequisites.js.map +1 -1
  30. package/dist/lib/agent.d.ts +39 -2
  31. package/dist/lib/agent.js +106 -4
  32. package/dist/lib/agent.js.map +1 -1
  33. package/dist/lib/config.d.ts +78 -1
  34. package/dist/lib/config.js +217 -1
  35. package/dist/lib/config.js.map +1 -1
  36. package/dist/lib/epics.d.ts +57 -0
  37. package/dist/lib/epics.js +76 -0
  38. package/dist/lib/epics.js.map +1 -0
  39. package/dist/lib/escalation.d.ts +102 -0
  40. package/dist/lib/escalation.js +241 -0
  41. package/dist/lib/escalation.js.map +1 -0
  42. package/dist/lib/eval-matrix.d.ts +125 -0
  43. package/dist/lib/eval-matrix.js +317 -0
  44. package/dist/lib/eval-matrix.js.map +1 -0
  45. package/dist/lib/eval-report.d.ts +12 -0
  46. package/dist/lib/eval-report.js +132 -0
  47. package/dist/lib/eval-report.js.map +1 -0
  48. package/dist/lib/eval-secret-scan.d.ts +41 -0
  49. package/dist/lib/eval-secret-scan.js +163 -0
  50. package/dist/lib/eval-secret-scan.js.map +1 -0
  51. package/dist/lib/eval.js +7 -4
  52. package/dist/lib/eval.js.map +1 -1
  53. package/dist/lib/github.d.ts +25 -0
  54. package/dist/lib/github.js +75 -0
  55. package/dist/lib/github.js.map +1 -1
  56. package/dist/lib/hardware.d.ts +9 -0
  57. package/dist/lib/hardware.js +32 -0
  58. package/dist/lib/hardware.js.map +1 -0
  59. package/dist/lib/pipeline.d.ts +6 -1
  60. package/dist/lib/pipeline.js +223 -19
  61. package/dist/lib/pipeline.js.map +1 -1
  62. package/dist/lib/prerequisites.js +11 -3
  63. package/dist/lib/prerequisites.js.map +1 -1
  64. package/dist/lib/routing-history.d.ts +43 -0
  65. package/dist/lib/routing-history.js +112 -0
  66. package/dist/lib/routing-history.js.map +1 -0
  67. package/dist/lib/routing-promotion.d.ts +95 -0
  68. package/dist/lib/routing-promotion.js +229 -0
  69. package/dist/lib/routing-promotion.js.map +1 -0
  70. package/dist/lib/session.d.ts +10 -1
  71. package/dist/lib/session.js +38 -7
  72. package/dist/lib/session.js.map +1 -1
  73. package/dist/lib/telemetry.d.ts +147 -0
  74. package/dist/lib/telemetry.js +353 -0
  75. package/dist/lib/telemetry.js.map +1 -0
  76. package/dist/lib/verify-epic.d.ts +31 -0
  77. package/dist/lib/verify-epic.js +237 -0
  78. package/dist/lib/verify-epic.js.map +1 -0
  79. package/package.json +1 -1
@@ -2,15 +2,17 @@
2
2
  * Agent Spawn Module
3
3
  * ==================
4
4
  *
5
- * Handles spawning different AI CLI agents (Claude, Codex, OpenCode)
5
+ * Handles spawning different AI CLI agents (Claude, Codex, OpenCode, LM Studio, Ollama)
6
6
  * with the correct CLI flags per agent type.
7
7
  *
8
+ * lmstudio and ollama piggy-back on existing CLIs: lmstudio uses the `claude`
9
+ * CLI pointed at an Anthropic-compatible local endpoint, ollama uses the
10
+ * `codex` CLI pointed at an OpenAI-compatible local endpoint. Endpoint
11
+ * selection is done via env vars (see `buildEndpointEnv`).
12
+ *
8
13
  * Adding a new agent: add a case to AGENT_CLI_MAP and buildAgentArgs().
9
14
  */
10
15
  import { spawn } from 'node:child_process';
11
- // ============================================================================
12
- // Agent CLI Mapping
13
- // ============================================================================
14
16
  /**
15
17
  * CLI reference for each supported agent.
16
18
  * Extend this map to add new agent types.
@@ -23,6 +25,7 @@ export const AGENT_CLI_MAP = {
23
25
  permissionFlag: '--dangerously-skip-permissions',
24
26
  supportsMaxTurns: true,
25
27
  maxTurnsFlag: '--max-turns',
28
+ isSubcommand: false,
26
29
  },
27
30
  codex: {
28
31
  command: 'codex',
@@ -30,12 +33,37 @@ export const AGENT_CLI_MAP = {
30
33
  modelFlag: '--model',
31
34
  permissionFlag: '--full-auto',
32
35
  supportsMaxTurns: false,
36
+ isSubcommand: true,
33
37
  },
34
38
  opencode: {
35
39
  command: 'opencode',
36
40
  promptFlag: 'run',
37
41
  modelFlag: '--model',
38
42
  supportsMaxTurns: false,
43
+ isSubcommand: true,
44
+ },
45
+ // lmstudio: LM Studio 0.4.1+ exposes an Anthropic-compatible /v1/messages
46
+ // endpoint, so we invoke the `claude` CLI and point it at the local server
47
+ // via ANTHROPIC_BASE_URL / ANTHROPIC_MODEL (see buildEndpointEnv).
48
+ lmstudio: {
49
+ command: 'claude',
50
+ promptFlag: '-p',
51
+ modelFlag: '--model',
52
+ permissionFlag: '--dangerously-skip-permissions',
53
+ supportsMaxTurns: true,
54
+ maxTurnsFlag: '--max-turns',
55
+ isSubcommand: false,
56
+ },
57
+ // ollama: Ollama exposes an OpenAI-compatible /v1/chat/completions endpoint,
58
+ // so we invoke the `codex` CLI and point it at the local server via
59
+ // OPENAI_BASE_URL / OPENAI_MODEL (see buildEndpointEnv).
60
+ ollama: {
61
+ command: 'codex',
62
+ promptFlag: 'exec',
63
+ modelFlag: '--model',
64
+ permissionFlag: '--full-auto',
65
+ supportsMaxTurns: false,
66
+ isSubcommand: true,
39
67
  },
40
68
  };
41
69
  /**
@@ -48,9 +76,7 @@ export function buildAgentArgs(config, prompt) {
48
76
  throw new Error(`Unknown agent type: "${config.agent}". Supported agents: ${Object.keys(AGENT_CLI_MAP).join(', ')}`);
49
77
  }
50
78
  const args = [];
51
- // For agents that use subcommands (codex 'exec', opencode 'run'), add it first
52
- const isSubcommand = config.agent === 'codex' || config.agent === 'opencode';
53
- if (isSubcommand) {
79
+ if (agentDef.isSubcommand) {
54
80
  args.push(agentDef.promptFlag);
55
81
  }
56
82
  // Model flag (skip if empty — let agent CLI use its default)
@@ -66,7 +92,7 @@ export function buildAgentArgs(config, prompt) {
66
92
  args.push(agentDef.maxTurnsFlag, String(config.maxTurns));
67
93
  }
68
94
  // Prompt: subcommand agents take it as positional arg; flag agents use promptFlag
69
- if (isSubcommand) {
95
+ if (agentDef.isSubcommand) {
70
96
  args.push(prompt);
71
97
  }
72
98
  else {
@@ -75,18 +101,76 @@ export function buildAgentArgs(config, prompt) {
75
101
  return { command: agentDef.command, args };
76
102
  }
77
103
  // ============================================================================
104
+ // Endpoint Env Vars
105
+ // ============================================================================
106
+ /**
107
+ * Build the env-var overrides needed to point a child CLI at a specific
108
+ * routing endpoint. Anthropic-shaped endpoints (`anthropic`, `anthropic_compat`)
109
+ * set ANTHROPIC_BASE_URL / ANTHROPIC_MODEL; OpenAI-compatible endpoints set
110
+ * OPENAI_BASE_URL / OPENAI_MODEL.
111
+ *
112
+ * Callers MUST compute this per stage and not share envs across stages, so
113
+ * that a frontier stage does not inherit a local endpoint from a prior stage.
114
+ */
115
+ export function buildEndpointEnv(endpoint, model) {
116
+ const env = {};
117
+ if (!endpoint || !endpoint.base_url)
118
+ return env;
119
+ switch (endpoint.type) {
120
+ case 'anthropic':
121
+ case 'anthropic_compat':
122
+ env.ANTHROPIC_BASE_URL = endpoint.base_url;
123
+ if (model)
124
+ env.ANTHROPIC_MODEL = model;
125
+ break;
126
+ case 'openai_compat':
127
+ env.OPENAI_BASE_URL = endpoint.base_url;
128
+ if (model)
129
+ env.OPENAI_MODEL = model;
130
+ break;
131
+ }
132
+ return env;
133
+ }
134
+ /** Default base URLs for single-agent lmstudio/ollama mode. */
135
+ export const DEFAULT_LMSTUDIO_BASE_URL = 'http://localhost:1234';
136
+ export const DEFAULT_OLLAMA_BASE_URL = 'http://localhost:11434/v1';
137
+ /**
138
+ * Auto-injected env vars for single-agent `lmstudio` / `ollama` mode, so
139
+ * `agent: lmstudio` actually targets the local server instead of silently
140
+ * hitting the real Anthropic API. Respects pre-existing env vars — users who
141
+ * export `ANTHROPIC_BASE_URL` themselves keep full control.
142
+ */
143
+ function defaultLocalEnv(agent, model) {
144
+ const env = {};
145
+ if (agent === 'lmstudio') {
146
+ if (!process.env.ANTHROPIC_BASE_URL)
147
+ env.ANTHROPIC_BASE_URL = DEFAULT_LMSTUDIO_BASE_URL;
148
+ if (model && !process.env.ANTHROPIC_MODEL)
149
+ env.ANTHROPIC_MODEL = model;
150
+ }
151
+ else if (agent === 'ollama') {
152
+ if (!process.env.OPENAI_BASE_URL)
153
+ env.OPENAI_BASE_URL = DEFAULT_OLLAMA_BASE_URL;
154
+ if (model && !process.env.OPENAI_MODEL)
155
+ env.OPENAI_MODEL = model;
156
+ }
157
+ return env;
158
+ }
159
+ // ============================================================================
78
160
  // Spawn Agent
79
161
  // ============================================================================
80
162
  /**
81
163
  * Spawns an agent subprocess with the correct CLI flags.
82
164
  * Returns the ChildProcess for the caller to manage (listen to events, pipe stdio, etc.).
83
165
  */
84
- export function spawnAgent(config, prompt, cwd) {
166
+ export function spawnAgent(config, prompt, cwd, envOverrides) {
85
167
  const { command, args } = buildAgentArgs(config, prompt);
168
+ // Caller overrides win > agent-default local base URLs > process.env
169
+ const localDefaults = defaultLocalEnv(config.agent, config.model);
86
170
  return spawn(command, args, {
87
171
  cwd,
88
172
  stdio: ['pipe', 'pipe', 'pipe'],
89
- env: { ...process.env },
173
+ env: { ...process.env, ...localDefaults, ...envOverrides },
90
174
  });
91
175
  }
92
176
  //# sourceMappingURL=agents.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents.js","sourceRoot":"","sources":["../../src/engine/agents.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agents.js","sourceRoot":"","sources":["../../src/engine/agents.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Verifies that the configured AI CLI agent is installed before starting the pipeline.
6
6
  */
7
- import type { Config } from '../lib/config.js';
7
+ import type { Config, RoutingStageName } from '../lib/config.js';
8
8
  export interface AgentCheckResult {
9
9
  agent: string;
10
10
  installed: boolean;
@@ -18,7 +18,9 @@ export interface PrerequisiteResult {
18
18
  */
19
19
  export declare function isCommandAvailable(command: string): boolean;
20
20
  /**
21
- * Checks that the configured agent CLI is installed.
21
+ * Checks that the configured agent CLI is installed. lmstudio/ollama
22
+ * piggy-back on the claude/codex CLIs, so we probe those instead of looking
23
+ * for a literal "lmstudio" / "ollama" binary on PATH.
22
24
  */
23
25
  export declare function checkAgents(config: Config): PrerequisiteResult;
24
26
  /**
@@ -29,3 +31,39 @@ export declare function formatCheckResults(result: PrerequisiteResult): string;
29
31
  * Formats the pipeline startup summary.
30
32
  */
31
33
  export declare function formatPipelineSummary(config: Config): string;
34
+ export interface LocalModelCheckResult {
35
+ ok: boolean;
36
+ /** List of model ids reported by the endpoint's /v1/models response. */
37
+ loaded: string[];
38
+ error?: string;
39
+ }
40
+ /**
41
+ * Return true when the endpoint's base_url looks like a local/loopback server
42
+ * — i.e. a check against it should be cheap and safe to run on every stage
43
+ * start. Remote endpoints (api.anthropic.com etc.) aren't probed here.
44
+ */
45
+ export declare function isLocalEndpoint(baseUrl: string): boolean;
46
+ /**
47
+ * Verify that a local model server (LM Studio, Ollama, vLLM, etc.) is reachable
48
+ * at `baseUrl` and is currently serving `model`. Uses the OpenAI-compatible
49
+ * `/v1/models` response shape, which both LM Studio and Ollama expose even for
50
+ * their Anthropic-compatible endpoints.
51
+ *
52
+ * Returns `{ ok: true }` iff the model id appears in the response. Otherwise
53
+ * returns an actionable error describing what to do next.
54
+ */
55
+ export declare function checkLocalModel(baseUrl: string, model: string): Promise<LocalModelCheckResult>;
56
+ export interface StagePrerequisiteResult {
57
+ ok: boolean;
58
+ /** Undefined when no local endpoint check was needed. */
59
+ checked?: boolean;
60
+ error?: string;
61
+ }
62
+ /**
63
+ * Verify that the configured routing target for a stage is reachable before
64
+ * the stage runs. No-op when the stage has no routing override or when the
65
+ * resolved endpoint is remote. Returns an actionable error like
66
+ * "Start LM Studio and load model <model>" when the local server is down or
67
+ * the expected model isn't loaded.
68
+ */
69
+ export declare function checkStagePrerequisites(config: Config, stage: RoutingStageName): Promise<StagePrerequisiteResult>;
@@ -5,6 +5,7 @@
5
5
  * Verifies that the configured AI CLI agent is installed before starting the pipeline.
6
6
  */
7
7
  import { execSync } from 'node:child_process';
8
+ import { resolveRoutingStage } from '../lib/config.js';
8
9
  // ============================================================================
9
10
  // Agent Check
10
11
  // ============================================================================
@@ -25,12 +26,17 @@ export function isCommandAvailable(command) {
25
26
  }
26
27
  }
27
28
  /**
28
- * Checks that the configured agent CLI is installed.
29
+ * Checks that the configured agent CLI is installed. lmstudio/ollama
30
+ * piggy-back on the claude/codex CLIs, so we probe those instead of looking
31
+ * for a literal "lmstudio" / "ollama" binary on PATH.
29
32
  */
30
33
  export function checkAgents(config) {
34
+ const cliCommand = config.agent === 'lmstudio' ? 'claude'
35
+ : config.agent === 'ollama' ? 'codex'
36
+ : config.agent;
31
37
  const result = {
32
38
  agent: config.agent,
33
- installed: isCommandAvailable(config.agent),
39
+ installed: isCommandAvailable(cliCommand),
34
40
  };
35
41
  return {
36
42
  ok: result.installed,
@@ -63,4 +69,122 @@ export function formatCheckResults(result) {
63
69
  export function formatPipelineSummary(config) {
64
70
  return `Pipeline: ${config.agent}/${config.model}`;
65
71
  }
72
+ /**
73
+ * Compose the /v1/models URL for an endpoint base URL. Accepts base URLs
74
+ * with or without a trailing `/v1` segment.
75
+ */
76
+ function buildModelsUrl(baseUrl) {
77
+ const base = baseUrl.replace(/\/+$/, '');
78
+ return /\/v1$/.test(base) ? `${base}/models` : `${base}/v1/models`;
79
+ }
80
+ /**
81
+ * Return true when the endpoint's base_url looks like a local/loopback server
82
+ * — i.e. a check against it should be cheap and safe to run on every stage
83
+ * start. Remote endpoints (api.anthropic.com etc.) aren't probed here.
84
+ */
85
+ export function isLocalEndpoint(baseUrl) {
86
+ try {
87
+ // URL.hostname wraps IPv6 addresses in brackets (e.g. "[::1]") — strip them.
88
+ const host = new URL(baseUrl).hostname.replace(/^\[|\]$/g, '');
89
+ return (host === 'localhost' ||
90
+ host === '127.0.0.1' ||
91
+ host === '::1' ||
92
+ host === '0.0.0.0' ||
93
+ host.endsWith('.local'));
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ /**
100
+ * Verify that a local model server (LM Studio, Ollama, vLLM, etc.) is reachable
101
+ * at `baseUrl` and is currently serving `model`. Uses the OpenAI-compatible
102
+ * `/v1/models` response shape, which both LM Studio and Ollama expose even for
103
+ * their Anthropic-compatible endpoints.
104
+ *
105
+ * Returns `{ ok: true }` iff the model id appears in the response. Otherwise
106
+ * returns an actionable error describing what to do next.
107
+ */
108
+ export async function checkLocalModel(baseUrl, model) {
109
+ if (!baseUrl) {
110
+ return { ok: false, loaded: [], error: 'Missing base URL' };
111
+ }
112
+ const url = buildModelsUrl(baseUrl);
113
+ let res;
114
+ try {
115
+ res = await fetch(url);
116
+ }
117
+ catch (err) {
118
+ const msg = err instanceof Error ? err.message : String(err);
119
+ return { ok: false, loaded: [], error: `Could not reach ${url}: ${msg}` };
120
+ }
121
+ if (!res.ok) {
122
+ return { ok: false, loaded: [], error: `HTTP ${res.status} from ${url}` };
123
+ }
124
+ let body;
125
+ try {
126
+ body = await res.json();
127
+ }
128
+ catch (err) {
129
+ const msg = err instanceof Error ? err.message : String(err);
130
+ return { ok: false, loaded: [], error: `Invalid JSON from ${url}: ${msg}` };
131
+ }
132
+ const data = body?.data;
133
+ const loaded = Array.isArray(data)
134
+ ? data
135
+ .map((m) => (typeof m?.id === 'string' ? m.id : ''))
136
+ .filter((s) => s.length > 0)
137
+ : [];
138
+ if (!model) {
139
+ return { ok: false, loaded, error: 'No model specified' };
140
+ }
141
+ if (!loaded.includes(model)) {
142
+ const available = loaded.length > 0 ? loaded.join(', ') : '(none)';
143
+ return {
144
+ ok: false,
145
+ loaded,
146
+ error: `Model "${model}" is not loaded at ${baseUrl}. Available: ${available}`,
147
+ };
148
+ }
149
+ return { ok: true, loaded };
150
+ }
151
+ /** Friendly label for common local endpoints, for use in error messages. */
152
+ function endpointLabel(endpoint) {
153
+ try {
154
+ const port = new URL(endpoint.base_url).port;
155
+ if (port === '1234')
156
+ return 'LM Studio';
157
+ if (port === '11434')
158
+ return 'Ollama';
159
+ }
160
+ catch {
161
+ // fall through
162
+ }
163
+ return 'Local model server';
164
+ }
165
+ /**
166
+ * Verify that the configured routing target for a stage is reachable before
167
+ * the stage runs. No-op when the stage has no routing override or when the
168
+ * resolved endpoint is remote. Returns an actionable error like
169
+ * "Start LM Studio and load model <model>" when the local server is down or
170
+ * the expected model isn't loaded.
171
+ */
172
+ export async function checkStagePrerequisites(config, stage) {
173
+ const resolved = resolveRoutingStage(config, stage);
174
+ if (!resolved || !resolved.endpoint)
175
+ return { ok: true };
176
+ const endpoint = resolved.endpoint;
177
+ if (!isLocalEndpoint(endpoint.base_url))
178
+ return { ok: true };
179
+ const result = await checkLocalModel(endpoint.base_url, resolved.model);
180
+ if (result.ok)
181
+ return { ok: true, checked: true };
182
+ const label = endpointLabel(endpoint);
183
+ const actionable = `Start ${label} and load model ${resolved.model}`;
184
+ return {
185
+ ok: false,
186
+ checked: true,
187
+ error: `${actionable} (${result.error ?? 'server did not respond'})`,
188
+ };
189
+ }
66
190
  //# sourceMappingURL=prerequisites.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prerequisites.js","sourceRoot":"","sources":["../../src/engine/prerequisites.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAiB9C,+EAA+E;AAC/E,cAAc;AACd,+EAA+E;AAE/E;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,+EAA+E;IAC/E,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,QAAQ,CAAC,SAAS,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc;IACxC,MAAM,MAAM,GAAqB;QAC/B,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,SAAS,EAAE,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC;KAC5C,CAAC;IAEF,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,SAAS;QACpB,OAAO,EAAE,CAAC,MAAM,CAAC;KAClB,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,aAAa;AACb,+EAA+E;AAE/E;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA0B;IAC3D,MAAM,KAAK,GAAa,CAAC,oBAAoB,CAAC,CAAC;IAE/C,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC/C,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACzD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,KAAK,qBAAqB,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAc;IAClD,OAAO,aAAa,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"prerequisites.js","sourceRoot":"","sources":["../../src/engine/prerequisites.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,10 @@
1
+ import type { RoutingEndpoint } from './config.js';
2
+ /**
3
+ * Supported agent CLI shapes. `lmstudio` piggy-backs on the `claude` CLI (since
4
+ * LM Studio exposes an Anthropic-compatible endpoint); `ollama` piggy-backs on
5
+ * the `codex` CLI (OpenAI-compatible).
6
+ */
7
+ export type AgentType = 'claude' | 'codex' | 'opencode' | 'lmstudio' | 'ollama';
1
8
  export type AgentResult = {
2
9
  exitCode: number;
3
10
  output: string;
@@ -10,9 +17,13 @@ export type AgentResult = {
10
17
  outputTokens?: number;
11
18
  /** Model used for the invocation. */
12
19
  model?: string;
20
+ /** Number of tool_use blocks emitted during the run. */
21
+ toolCalls?: number;
22
+ /** Number of tool_result blocks with is_error === true. */
23
+ toolErrors?: number;
13
24
  };
14
25
  export type AgentOptions = {
15
- agent: 'claude' | 'codex' | 'opencode';
26
+ agent: AgentType;
16
27
  model: string;
17
28
  prompt: string;
18
29
  cwd: string;
@@ -24,9 +35,19 @@ export type AgentOptions = {
24
35
  maxTurns?: number;
25
36
  /** Resume the most recent agent session in the CWD instead of starting fresh. */
26
37
  resume?: boolean;
38
+ /**
39
+ * Env-var overrides merged over `process.env` when spawning the child.
40
+ * Callers MUST compute this per stage (see `buildEndpointEnv`) so that a
41
+ * frontier stage does not inherit a local-endpoint env from a prior stage.
42
+ */
43
+ env?: Record<string, string>;
27
44
  };
28
45
  /**
29
46
  * Build CLI command and args for a given agent type.
47
+ *
48
+ * `lmstudio` delegates to the claude CLI shape (Anthropic-compatible); `ollama`
49
+ * delegates to the codex CLI shape (OpenAI-compatible). Endpoint selection is
50
+ * wired via env vars (see `buildEndpointEnv`), not CLI flags.
30
51
  */
31
52
  export declare function buildAgentArgs(options: AgentOptions): {
32
53
  command: string;
@@ -36,7 +57,23 @@ export declare function buildAgentArgs(options: AgentOptions): {
36
57
  * Build a shell command string for one-shot agent prompts (scan, vision).
37
58
  * Reads prompt from stdin. Returns the command to pipe into.
38
59
  */
39
- export declare function buildOneShotCommand(agent: 'claude' | 'codex' | 'opencode', model: string): string;
60
+ export declare function buildOneShotCommand(agent: AgentType, model: string): string;
61
+ /**
62
+ * Build the env-var overrides needed to point a child CLI at a specific
63
+ * routing endpoint. Anthropic-shaped endpoints set ANTHROPIC_BASE_URL /
64
+ * ANTHROPIC_MODEL; OpenAI-compatible endpoints set OPENAI_BASE_URL /
65
+ * OPENAI_MODEL.
66
+ *
67
+ * Callers MUST compute this per stage and not share envs across stages, so
68
+ * that a frontier stage does not inherit a local endpoint from a prior stage.
69
+ */
70
+ export declare function buildEndpointEnv(endpoint: RoutingEndpoint, model: string): Record<string, string>;
71
+ /**
72
+ * Default local-server base URLs for single-agent `lmstudio` / `ollama` mode.
73
+ * Exported so tests and callers can reference the same constants.
74
+ */
75
+ export declare const DEFAULT_LMSTUDIO_BASE_URL = "http://localhost:1234";
76
+ export declare const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434/v1";
40
77
  /**
41
78
  * Spawn an AI agent with a prompt.
42
79
  * Streams output to terminal in real-time while capturing it.
package/dist/lib/agent.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { spawn } from 'node:child_process';
5
5
  import { createWriteStream } from 'node:fs';
6
6
  import { log } from './logger.js';
7
+ import { classifyToolErrors } from './escalation.js';
7
8
  /**
8
9
  * Parse a Claude stream-json line into a human-readable log line.
9
10
  * Returns null for lines that shouldn't be logged.
@@ -69,10 +70,15 @@ function formatStreamJsonLine(line) {
69
70
  const DEFAULT_AGENT_TIMEOUT_MS = 30 * 60 * 1000;
70
71
  /**
71
72
  * Build CLI command and args for a given agent type.
73
+ *
74
+ * `lmstudio` delegates to the claude CLI shape (Anthropic-compatible); `ollama`
75
+ * delegates to the codex CLI shape (OpenAI-compatible). Endpoint selection is
76
+ * wired via env vars (see `buildEndpointEnv`), not CLI flags.
72
77
  */
73
78
  export function buildAgentArgs(options) {
74
79
  switch (options.agent) {
75
- case 'claude': {
80
+ case 'claude':
81
+ case 'lmstudio': {
76
82
  const args = [];
77
83
  if (options.resume)
78
84
  args.push('--continue');
@@ -85,7 +91,8 @@ export function buildAgentArgs(options) {
85
91
  }
86
92
  return { command: 'claude', args };
87
93
  }
88
- case 'codex': {
94
+ case 'codex':
95
+ case 'ollama': {
89
96
  const args = [];
90
97
  if (options.resume) {
91
98
  args.push('exec', 'resume', '--last');
@@ -114,14 +121,16 @@ export function buildAgentArgs(options) {
114
121
  */
115
122
  export function buildOneShotCommand(agent, model) {
116
123
  switch (agent) {
117
- case 'claude': {
124
+ case 'claude':
125
+ case 'lmstudio': {
118
126
  const parts = ['claude', '-p'];
119
127
  if (model)
120
128
  parts.push('--model', model);
121
129
  parts.push('--dangerously-skip-permissions', '--output-format', 'text');
122
130
  return parts.join(' ');
123
131
  }
124
- case 'codex': {
132
+ case 'codex':
133
+ case 'ollama': {
125
134
  const parts = ['codex', 'exec'];
126
135
  if (model)
127
136
  parts.push('--model', model);
@@ -138,6 +147,65 @@ export function buildOneShotCommand(agent, model) {
138
147
  throw new Error(`Unknown agent type: ${agent}`);
139
148
  }
140
149
  }
150
+ /**
151
+ * Build the env-var overrides needed to point a child CLI at a specific
152
+ * routing endpoint. Anthropic-shaped endpoints set ANTHROPIC_BASE_URL /
153
+ * ANTHROPIC_MODEL; OpenAI-compatible endpoints set OPENAI_BASE_URL /
154
+ * OPENAI_MODEL.
155
+ *
156
+ * Callers MUST compute this per stage and not share envs across stages, so
157
+ * that a frontier stage does not inherit a local endpoint from a prior stage.
158
+ */
159
+ export function buildEndpointEnv(endpoint, model) {
160
+ const env = {};
161
+ if (!endpoint || !endpoint.base_url)
162
+ return env;
163
+ switch (endpoint.type) {
164
+ case 'anthropic':
165
+ case 'anthropic_compat':
166
+ env.ANTHROPIC_BASE_URL = endpoint.base_url;
167
+ if (model)
168
+ env.ANTHROPIC_MODEL = model;
169
+ break;
170
+ case 'openai_compat':
171
+ env.OPENAI_BASE_URL = endpoint.base_url;
172
+ if (model)
173
+ env.OPENAI_MODEL = model;
174
+ break;
175
+ }
176
+ return env;
177
+ }
178
+ /**
179
+ * Default local-server base URLs for single-agent `lmstudio` / `ollama` mode.
180
+ * Exported so tests and callers can reference the same constants.
181
+ */
182
+ export const DEFAULT_LMSTUDIO_BASE_URL = 'http://localhost:1234';
183
+ export const DEFAULT_OLLAMA_BASE_URL = 'http://localhost:11434/v1';
184
+ /**
185
+ * Auto-injected env vars for single-agent `lmstudio` / `ollama` mode.
186
+ *
187
+ * Without this, `agent: lmstudio` would spawn the claude CLI with no base URL
188
+ * override and silently hit the real Anthropic API. We only inject defaults
189
+ * when the corresponding env var isn't already set in the parent process, so
190
+ * users who export `ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL` to point at a
191
+ * non-default port keep full control.
192
+ */
193
+ function defaultLocalEnv(agent, model) {
194
+ const env = {};
195
+ if (agent === 'lmstudio') {
196
+ if (!process.env.ANTHROPIC_BASE_URL)
197
+ env.ANTHROPIC_BASE_URL = DEFAULT_LMSTUDIO_BASE_URL;
198
+ if (model && !process.env.ANTHROPIC_MODEL)
199
+ env.ANTHROPIC_MODEL = model;
200
+ }
201
+ else if (agent === 'ollama') {
202
+ if (!process.env.OPENAI_BASE_URL)
203
+ env.OPENAI_BASE_URL = DEFAULT_OLLAMA_BASE_URL;
204
+ if (model && !process.env.OPENAI_MODEL)
205
+ env.OPENAI_MODEL = model;
206
+ }
207
+ return env;
208
+ }
141
209
  /**
142
210
  * Spawn an AI agent with a prompt.
143
211
  * Streams output to terminal in real-time while capturing it.
@@ -156,10 +224,18 @@ export async function spawnAgent(options) {
156
224
  logStream = createWriteStream(options.logFile, { flags: 'w' });
157
225
  }
158
226
  const timeoutMs = options.timeout ?? DEFAULT_AGENT_TIMEOUT_MS;
227
+ // Compose env: caller overrides win > agent-default local base URLs > process.env
228
+ const localDefaults = defaultLocalEnv(options.agent, options.model);
229
+ const hasOverrides = options.env && Object.keys(options.env).length > 0;
230
+ const hasLocalDefaults = Object.keys(localDefaults).length > 0;
231
+ const spawnEnv = hasOverrides || hasLocalDefaults
232
+ ? { ...process.env, ...localDefaults, ...(options.env ?? {}) }
233
+ : process.env;
159
234
  return new Promise((resolve) => {
160
235
  const child = spawn(command, args, {
161
236
  cwd: options.cwd,
162
237
  stdio: ['pipe', 'pipe', 'pipe'],
238
+ env: spawnEnv,
163
239
  });
164
240
  let resolved = false;
165
241
  // For stream-json: accumulate partial lines, extract final result text
@@ -169,6 +245,9 @@ export async function spawnAgent(options) {
169
245
  let parsedCostUsd;
170
246
  let parsedInputTokens;
171
247
  let parsedOutputTokens;
248
+ // Tool-use telemetry (per-stage metrics aggregation).
249
+ let toolUseCount = 0;
250
+ let toolErrorCount = 0;
172
251
  // Pipe prompt via stdin (like: echo "$prompt" | claude -p)
173
252
  child.stdin.write(options.prompt);
174
253
  child.stdin.end();
@@ -228,6 +307,22 @@ export async function spawnAgent(options) {
228
307
  parsedOutputTokens = usage.output_tokens;
229
308
  }
230
309
  }
310
+ else if (obj.type === 'assistant') {
311
+ const msg = obj.message;
312
+ const content = (msg?.content ?? []);
313
+ for (const block of content) {
314
+ if (block.type === 'tool_use')
315
+ toolUseCount++;
316
+ }
317
+ }
318
+ else if (obj.type === 'user') {
319
+ const msg = obj.message;
320
+ const content = (msg?.content ?? []);
321
+ for (const block of content) {
322
+ if (block.type === 'tool_result' && block.is_error === true)
323
+ toolErrorCount++;
324
+ }
325
+ }
231
326
  }
232
327
  catch { /* not valid JSON, ignore */ }
233
328
  const formatted = formatStreamJsonLine(line);
@@ -264,6 +359,11 @@ export async function spawnAgent(options) {
264
359
  resolved = true;
265
360
  clearTimeout(timer);
266
361
  const duration = Date.now() - startTime;
362
+ // Fall back to output-string classification when we didn't see structured
363
+ // tool_result error blocks (e.g. non-stream-json agents like codex).
364
+ const toolErrorsFinal = toolErrorCount > 0
365
+ ? toolErrorCount
366
+ : classifyToolErrors(output).length;
267
367
  const result = {
268
368
  exitCode,
269
369
  output,
@@ -272,6 +372,8 @@ export async function spawnAgent(options) {
272
372
  costUsd: parsedCostUsd,
273
373
  inputTokens: parsedInputTokens,
274
374
  outputTokens: parsedOutputTokens,
375
+ toolCalls: toolUseCount,
376
+ toolErrors: toolErrorsFinal,
275
377
  };
276
378
  if (logStream) {
277
379
  logStream.end(() => {