@voybio/ace-swarm 2.4.0 → 2.4.2

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 (80) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +502 -56
  3. package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
  4. package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
  5. package/assets/agent-state/runtime-tool-specs.json +70 -2
  6. package/assets/instructions/ACE_Coder.instructions.md +13 -0
  7. package/assets/instructions/ACE_UI.instructions.md +11 -0
  8. package/dist/ace-context.js +70 -11
  9. package/dist/ace-internal-tools.d.ts +3 -1
  10. package/dist/ace-internal-tools.js +10 -2
  11. package/dist/agent-runtime/role-adapters.d.ts +18 -1
  12. package/dist/agent-runtime/role-adapters.js +49 -5
  13. package/dist/astgrep-index.d.ts +48 -0
  14. package/dist/astgrep-index.js +126 -1
  15. package/dist/cli.js +487 -17
  16. package/dist/discovery-runtime-wrappers.d.ts +108 -0
  17. package/dist/discovery-runtime-wrappers.js +615 -0
  18. package/dist/helpers/bootstrap.js +1 -1
  19. package/dist/helpers/constants.d.ts +4 -2
  20. package/dist/helpers/constants.js +8 -0
  21. package/dist/helpers/path-utils.d.ts +8 -1
  22. package/dist/helpers/path-utils.js +27 -8
  23. package/dist/helpers/store-resolution.js +7 -3
  24. package/dist/hermes/bridge-protocol.d.ts +41 -0
  25. package/dist/hermes/bridge-protocol.js +70 -0
  26. package/dist/hermes/launch-profile.d.ts +19 -0
  27. package/dist/hermes/launch-profile.js +81 -0
  28. package/dist/hermes/session-manager.d.ts +42 -0
  29. package/dist/hermes/session-manager.js +187 -0
  30. package/dist/job-scheduler.js +30 -4
  31. package/dist/json-sanitizer.d.ts +16 -0
  32. package/dist/json-sanitizer.js +26 -0
  33. package/dist/local-model-policy.d.ts +27 -0
  34. package/dist/local-model-policy.js +84 -0
  35. package/dist/local-model-runtime.d.ts +17 -0
  36. package/dist/local-model-runtime.js +77 -20
  37. package/dist/model-bridge.d.ts +6 -1
  38. package/dist/model-bridge.js +338 -21
  39. package/dist/orchestrator-supervisor.d.ts +42 -0
  40. package/dist/orchestrator-supervisor.js +110 -3
  41. package/dist/plan-proposal.d.ts +115 -0
  42. package/dist/plan-proposal.js +1073 -0
  43. package/dist/runtime-executor.d.ts +6 -1
  44. package/dist/runtime-executor.js +72 -5
  45. package/dist/runtime-tool-specs.d.ts +19 -1
  46. package/dist/runtime-tool-specs.js +67 -26
  47. package/dist/schemas.js +30 -1
  48. package/dist/server.d.ts +3 -0
  49. package/dist/server.js +73 -4
  50. package/dist/shared.d.ts +1 -0
  51. package/dist/shared.js +2 -0
  52. package/dist/store/bootstrap-store.d.ts +1 -0
  53. package/dist/store/bootstrap-store.js +8 -2
  54. package/dist/store/materializers/vericify-projector.js +3 -0
  55. package/dist/store/repositories/local-model-runtime-repository.d.ts +13 -1
  56. package/dist/store/repositories/local-model-runtime-repository.js +4 -1
  57. package/dist/store/repositories/vericify-repository.d.ts +1 -1
  58. package/dist/tools-agent.d.ts +20 -0
  59. package/dist/tools-agent.js +544 -29
  60. package/dist/tools-discovery.js +135 -0
  61. package/dist/tools-files.js +768 -66
  62. package/dist/tools-framework.js +80 -61
  63. package/dist/tools.d.ts +4 -1
  64. package/dist/tools.js +35 -13
  65. package/dist/tui/chat.d.ts +8 -0
  66. package/dist/tui/chat.js +74 -0
  67. package/dist/tui/index.d.ts +7 -0
  68. package/dist/tui/index.js +45 -2
  69. package/dist/tui/layout.d.ts +1 -0
  70. package/dist/tui/layout.js +4 -1
  71. package/dist/tui/ollama.d.ts +8 -1
  72. package/dist/tui/ollama.js +53 -12
  73. package/dist/tui/openai-compatible.d.ts +13 -0
  74. package/dist/tui/openai-compatible.js +305 -5
  75. package/dist/tui/provider-discovery.d.ts +1 -0
  76. package/dist/tui/provider-discovery.js +50 -24
  77. package/dist/vericify-bridge.d.ts +4 -1
  78. package/dist/vericify-bridge.js +3 -0
  79. package/package.json +2 -1
  80. package/scripts/hermes_bridge_worker.py +136 -0
@@ -5,6 +5,24 @@
5
5
  * Zero dependencies — uses Node.js built-in fetch/http.
6
6
  * Supports model listing, pulling, chat streaming, and health checks.
7
7
  */
8
+ function sleep(ms) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+ function requestedModelFromOptions(options) {
12
+ if (typeof options?.body !== "string")
13
+ return undefined;
14
+ try {
15
+ const parsed = JSON.parse(options.body);
16
+ return typeof parsed.model === "string"
17
+ ? parsed.model
18
+ : typeof parsed.name === "string"
19
+ ? parsed.name
20
+ : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
8
26
  // ── Client ───────────────────────────────────────────────────────────
9
27
  export class OllamaClient {
10
28
  baseUrl;
@@ -124,19 +142,36 @@ export class OllamaClient {
124
142
  throw new OllamaError("Ollama base URL is not configured. Set one explicitly or run `ace doctor --scan`.", 0, "");
125
143
  }
126
144
  const url = `${this.baseUrl}${path}`;
127
- const res = await fetch(url, {
128
- ...options,
129
- headers: {
130
- "Content-Type": "application/json",
131
- Accept: "application/json",
132
- ...(options?.headers ?? {}),
133
- },
134
- });
135
- if (!res.ok) {
145
+ const maxAttempts = 3;
146
+ let lastError;
147
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
148
+ const res = await fetch(url, {
149
+ ...options,
150
+ headers: {
151
+ "Content-Type": "application/json",
152
+ Accept: "application/json",
153
+ ...(options?.headers ?? {}),
154
+ },
155
+ });
156
+ if (res.ok)
157
+ return res;
136
158
  const text = await res.text().catch(() => "");
137
- throw new OllamaError(`Ollama API error: ${res.status} ${res.statusText}`, res.status, text);
159
+ const requestedModel = requestedModelFromOptions(options);
160
+ if (res.status >= 500 && /unable to load model/i.test(text)) {
161
+ throw new OllamaError(`Ollama was unable to load model${requestedModel ? ` '${requestedModel}'` : ""}. ` +
162
+ `Run \`ollama pull ${requestedModel ?? "<model>"}\` or \`ace doctor --repair-ollama\`.`, res.status, text, {
163
+ kind: "ollama_model_load_error",
164
+ model: requestedModel,
165
+ suggested_remediation: `run ollama pull ${requestedModel ?? "<model>"} or ace doctor --repair-ollama`,
166
+ });
167
+ }
168
+ lastError = new OllamaError(`Ollama API error: ${res.status} ${res.statusText}`, res.status, text);
169
+ if (res.status < 500 || attempt === maxAttempts) {
170
+ throw lastError;
171
+ }
172
+ await sleep(100 * attempt);
138
173
  }
139
- return res;
174
+ throw lastError ?? new OllamaError("Ollama API request failed", 0, "");
140
175
  }
141
176
  /** Stream newline-delimited JSON from a ReadableStream */
142
177
  async *streamJsonLines(body) {
@@ -182,11 +217,17 @@ export class OllamaClient {
182
217
  export class OllamaError extends Error {
183
218
  statusCode;
184
219
  responseBody;
185
- constructor(message, statusCode, responseBody) {
220
+ kind;
221
+ meta;
222
+ suggested_remediation;
223
+ constructor(message, statusCode, responseBody, options) {
186
224
  super(message);
187
225
  this.statusCode = statusCode;
188
226
  this.responseBody = responseBody;
189
227
  this.name = "OllamaError";
228
+ this.kind = options?.kind;
229
+ this.meta = options?.model ? { model: options.model } : undefined;
230
+ this.suggested_remediation = options?.suggested_remediation;
190
231
  }
191
232
  }
192
233
  //# sourceMappingURL=ollama.js.map
@@ -15,6 +15,7 @@ export interface OpenAICompatibleChatRequest {
15
15
  messages: OpenAICompatibleChatMessage[];
16
16
  temperature?: number;
17
17
  topP?: number;
18
+ onProviderEvent?: (event: OpenAICompatibleProviderEvent) => void;
18
19
  }
19
20
  export interface OpenAICompatibleChatChunk {
20
21
  text: string;
@@ -22,6 +23,18 @@ export interface OpenAICompatibleChatChunk {
22
23
  promptTokens?: number;
23
24
  completionTokens?: number;
24
25
  }
26
+ export type OpenAICompatibleAdapterStage = "streaming_chat" | "non_streaming_chat" | "completions";
27
+ export interface OpenAICompatibleProviderEvent {
28
+ provider: string;
29
+ stage: OpenAICompatibleAdapterStage;
30
+ event: "attempt" | "success" | "fallback" | "parse_error";
31
+ reason?: string;
32
+ statusCode?: number;
33
+ next_stage?: OpenAICompatibleAdapterStage;
34
+ fallback_count?: number;
35
+ sample_hex?: string;
36
+ sample_text?: string;
37
+ }
25
38
  export interface ProviderConfigOverride {
26
39
  baseUrl?: string;
27
40
  apiKey?: string;
@@ -5,7 +5,104 @@
5
5
  * - codex: defaults to OpenAI API endpoint/key env vars
6
6
  * - others: require provider-specific base URL + API key env vars
7
7
  */
8
+ import { appendFileSync } from "node:fs";
8
9
  import { buildOpenAiCompatibleBaseUrl } from "./provider-discovery.js";
10
+ function sanitizeTransportText(input) {
11
+ return input
12
+ .replace(/^\uFEFF/, "")
13
+ .replace(/\r\n?/g, "\n")
14
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
15
+ }
16
+ function sampleBytes(bytes, decoded) {
17
+ const buffer = Buffer.from(bytes);
18
+ return {
19
+ sample_hex: buffer.toString("hex").slice(0, 512),
20
+ sample_text: sanitizeTransportText(decoded).slice(0, 512),
21
+ };
22
+ }
23
+ function captureRawBytes(label, bytes) {
24
+ const target = process.env.ACE_RAW_RESPONSE_LOG?.trim();
25
+ if (!target)
26
+ return;
27
+ const decoded = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
28
+ const sample = sampleBytes(bytes, decoded);
29
+ try {
30
+ appendFileSync(target, `${JSON.stringify({
31
+ at: new Date().toISOString(),
32
+ label,
33
+ byte_length: bytes.byteLength,
34
+ ...sample,
35
+ })}\n`, "utf8");
36
+ }
37
+ catch {
38
+ // Raw capture is opt-in diagnostics; never fail the provider call because logging failed.
39
+ }
40
+ }
41
+ async function readResponseText(response, label) {
42
+ const bytes = new Uint8Array(await response.arrayBuffer());
43
+ captureRawBytes(label, bytes);
44
+ const decoded = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
45
+ const text = sanitizeTransportText(decoded);
46
+ const sample = sampleBytes(bytes, decoded);
47
+ return {
48
+ text,
49
+ byteLength: bytes.byteLength,
50
+ ...sample,
51
+ };
52
+ }
53
+ function sanitizeChatMessages(messages) {
54
+ return (Array.isArray(messages) ? messages : [])
55
+ .map((msg) => {
56
+ const role = msg?.role === "system" || msg?.role === "assistant" || msg?.role === "user"
57
+ ? msg.role
58
+ : "user";
59
+ const content = typeof msg?.content === "string"
60
+ ? sanitizeTransportText(msg.content)
61
+ : sanitizeTransportText(String(msg?.content ?? ""));
62
+ return { role, content };
63
+ })
64
+ .filter((msg) => msg.role === "assistant" || msg.content.trim().length > 0);
65
+ }
66
+ function extractChoiceText(payload) {
67
+ if (!payload || typeof payload !== "object")
68
+ return "";
69
+ const choices = Array.isArray(payload.choices)
70
+ ? (payload.choices ?? [])
71
+ : [];
72
+ const firstChoice = choices[0];
73
+ if (!firstChoice || typeof firstChoice !== "object")
74
+ return "";
75
+ const choice = firstChoice;
76
+ if (typeof choice.message?.content === "string")
77
+ return sanitizeTransportText(choice.message.content);
78
+ if (typeof choice.text === "string")
79
+ return sanitizeTransportText(choice.text);
80
+ if (typeof choice.delta?.content === "string")
81
+ return sanitizeTransportText(choice.delta.content);
82
+ return "";
83
+ }
84
+ function extractProviderBodyText(bodyText) {
85
+ const trimmed = sanitizeTransportText(bodyText).trim();
86
+ if (!trimmed)
87
+ return { text: "", parsedJson: false };
88
+ try {
89
+ const payload = JSON.parse(trimmed);
90
+ const extracted = extractChoiceText(payload);
91
+ return {
92
+ text: extracted || trimmed,
93
+ parsedJson: true,
94
+ };
95
+ }
96
+ catch {
97
+ return {
98
+ text: trimmed,
99
+ parsedJson: false,
100
+ };
101
+ }
102
+ }
103
+ function formatErrorMessage(error) {
104
+ return error instanceof Error ? error.message : String(error);
105
+ }
9
106
  export class OpenAICompatibleError extends Error {
10
107
  statusCode;
11
108
  responseBody;
@@ -46,14 +143,216 @@ export class OpenAICompatibleClient {
46
143
  const resolvedModel = provider === "copilot"
47
144
  ? normalizeCopilotModelName(request.model)
48
145
  : request.model;
146
+ if (provider === "llama.cpp") {
147
+ const emitProviderEvent = (event) => {
148
+ request.onProviderEvent?.({ provider, ...event });
149
+ };
150
+ let fallbackCount = 0;
151
+ const emitFallback = (stage, nextStage, reason, extra = {}) => {
152
+ fallbackCount += 1;
153
+ emitProviderEvent({
154
+ event: "fallback",
155
+ stage,
156
+ next_stage: nextStage,
157
+ reason,
158
+ fallback_count: fallbackCount,
159
+ statusCode: extra.statusCode,
160
+ sample_hex: extra.sample_hex,
161
+ sample_text: extra.sample_text,
162
+ });
163
+ };
164
+ const messagesToSend = sanitizeChatMessages(request.messages);
165
+ const messagesToPrompt = messagesToSend.map((m) => `${m.role}: ${m.content}`).join("\n");
166
+ // Attempt 1: streaming chat (SSE)
167
+ try {
168
+ emitProviderEvent({ event: "attempt", stage: "streaming_chat" });
169
+ const streamBody = {
170
+ model: resolvedModel,
171
+ messages: messagesToSend,
172
+ stream: true,
173
+ temperature: request.temperature,
174
+ top_p: request.topP,
175
+ };
176
+ const streamRes = await fetch(`${config.baseUrl}/chat/completions`, {
177
+ method: "POST",
178
+ signal: controller.signal,
179
+ headers: {
180
+ "Content-Type": "application/json",
181
+ Accept: "text/event-stream",
182
+ ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),
183
+ ...(config.headers ?? {}),
184
+ },
185
+ body: JSON.stringify(streamBody),
186
+ });
187
+ if (streamRes.ok && streamRes.body) {
188
+ let streamContentSeen = false;
189
+ let streamParsedFrames = 0;
190
+ let streamParseErrors = 0;
191
+ for await (const data of this.streamSseData(streamRes.body)) {
192
+ if (data === "[DONE]") {
193
+ continue;
194
+ }
195
+ let parsed;
196
+ try {
197
+ parsed = JSON.parse(data);
198
+ }
199
+ catch {
200
+ streamParseErrors += 1;
201
+ emitProviderEvent({
202
+ event: "parse_error",
203
+ stage: "streaming_chat",
204
+ reason: "sse_data_json_parse_error",
205
+ sample_text: data.slice(0, 512),
206
+ });
207
+ continue;
208
+ }
209
+ streamParsedFrames += 1;
210
+ const usage = (parsed.usage ?? {});
211
+ if (typeof usage.prompt_tokens === "number")
212
+ promptTokens = usage.prompt_tokens;
213
+ if (typeof usage.completion_tokens === "number")
214
+ completionTokens = usage.completion_tokens;
215
+ const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
216
+ const firstChoice = (choices[0] ?? {});
217
+ const delta = firstChoice.delta;
218
+ const text = typeof delta?.content === "string" ? sanitizeTransportText(delta.content) : "";
219
+ if (text.length > 0) {
220
+ streamContentSeen = true;
221
+ yield { text, done: false };
222
+ }
223
+ }
224
+ if (streamContentSeen) {
225
+ if (!doneSent) {
226
+ doneSent = true;
227
+ yield {
228
+ text: "",
229
+ done: true,
230
+ promptTokens,
231
+ completionTokens,
232
+ };
233
+ }
234
+ emitProviderEvent({ event: "success", stage: "streaming_chat" });
235
+ return;
236
+ }
237
+ emitFallback("streaming_chat", "non_streaming_chat", streamParseErrors > 0
238
+ ? "stream_parse_error_no_content"
239
+ : streamParsedFrames > 0
240
+ ? "stream_no_text_content"
241
+ : "stream_no_parseable_frames");
242
+ }
243
+ else {
244
+ const sample = await readResponseText(streamRes, `${provider}:streaming_chat:error`);
245
+ emitFallback("streaming_chat", "non_streaming_chat", "stream_http_error", {
246
+ statusCode: streamRes.status,
247
+ sample_hex: sample.sample_hex,
248
+ sample_text: sample.sample_text,
249
+ });
250
+ }
251
+ }
252
+ catch (err) {
253
+ if (err instanceof Error && /abort/i.test(err.name))
254
+ throw err;
255
+ emitFallback("streaming_chat", "non_streaming_chat", formatErrorMessage(err));
256
+ }
257
+ // Attempt 2: non-streaming chat (JSON or direct text)
258
+ try {
259
+ emitProviderEvent({ event: "attempt", stage: "non_streaming_chat" });
260
+ const chatBody = {
261
+ model: resolvedModel,
262
+ messages: messagesToSend,
263
+ stream: false,
264
+ temperature: request.temperature,
265
+ top_p: request.topP,
266
+ };
267
+ const res = await fetch(`${config.baseUrl}/chat/completions`, {
268
+ method: "POST",
269
+ signal: controller.signal,
270
+ headers: {
271
+ "Content-Type": "application/json",
272
+ Accept: "application/json, text/plain;q=0.9, */*;q=0.1",
273
+ ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),
274
+ ...(config.headers ?? {}),
275
+ },
276
+ body: JSON.stringify(chatBody),
277
+ });
278
+ const sample = await readResponseText(res, `${provider}:non_streaming_chat`);
279
+ if (res.ok) {
280
+ const extracted = extractProviderBodyText(sample.text);
281
+ if (extracted.text.length > 0) {
282
+ yield { text: extracted.text, done: false };
283
+ yield {
284
+ text: "",
285
+ done: true,
286
+ promptTokens,
287
+ completionTokens,
288
+ };
289
+ doneSent = true;
290
+ emitProviderEvent({ event: "success", stage: "non_streaming_chat" });
291
+ return;
292
+ }
293
+ emitFallback("non_streaming_chat", "completions", "empty_non_streaming_response", {
294
+ statusCode: res.status,
295
+ sample_hex: sample.sample_hex,
296
+ sample_text: sample.sample_text,
297
+ });
298
+ }
299
+ else {
300
+ emitFallback("non_streaming_chat", "completions", "non_streaming_http_error", {
301
+ statusCode: res.status,
302
+ sample_hex: sample.sample_hex,
303
+ sample_text: sample.sample_text,
304
+ });
305
+ }
306
+ }
307
+ catch (err) {
308
+ if (err instanceof Error && /abort/i.test(err.name))
309
+ throw err;
310
+ emitFallback("non_streaming_chat", "completions", formatErrorMessage(err));
311
+ }
312
+ // Attempt 3: completions endpoint with `prompt` (legacy shape, JSON or direct text)
313
+ emitProviderEvent({ event: "attempt", stage: "completions" });
314
+ const prompt = messagesToPrompt || "";
315
+ const compBody = {
316
+ model: resolvedModel,
317
+ prompt,
318
+ temperature: request.temperature,
319
+ top_p: request.topP,
320
+ };
321
+ const compRes = await fetch(`${config.baseUrl}/completions`, {
322
+ method: "POST",
323
+ signal: controller.signal,
324
+ headers: {
325
+ "Content-Type": "application/json",
326
+ Accept: "application/json",
327
+ ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}),
328
+ ...(config.headers ?? {}),
329
+ },
330
+ body: JSON.stringify(compBody),
331
+ });
332
+ const compSample = await readResponseText(compRes, `${provider}:completions`);
333
+ if (!compRes.ok) {
334
+ throw new OpenAICompatibleError(`Provider '${provider}' API error: ${compRes.status} ${compRes.statusText}`, compRes.status, compSample.text);
335
+ }
336
+ const extracted = extractProviderBodyText(compSample.text);
337
+ const text = extracted.text;
338
+ if (text.length > 0)
339
+ yield { text, done: false };
340
+ yield { text: "", done: true, promptTokens, completionTokens };
341
+ emitProviderEvent({ event: "success", stage: "completions" });
342
+ return;
343
+ }
344
+ // default streaming path for other providers
49
345
  const body = {
50
346
  model: resolvedModel,
51
- messages: request.messages,
347
+ messages: sanitizeChatMessages(request.messages),
52
348
  stream: true,
53
349
  temperature: request.temperature,
54
350
  top_p: request.topP,
55
- stream_options: { include_usage: true },
56
351
  };
352
+ // llama.cpp rejects custom stream_options; only include for non-llama providers
353
+ if (provider !== 'llama.cpp') {
354
+ body.stream_options = { include_usage: true };
355
+ }
57
356
  const res = await fetch(`${config.baseUrl}/chat/completions`, {
58
357
  method: "POST",
59
358
  signal: controller.signal,
@@ -202,7 +501,8 @@ export class OpenAICompatibleClient {
202
501
  const { done, value } = await reader.read();
203
502
  if (done)
204
503
  break;
205
- buffer += decoder.decode(value, { stream: true });
504
+ captureRawBytes("openai-compatible:sse_chunk", value);
505
+ buffer += sanitizeTransportText(decoder.decode(value, { stream: true }));
206
506
  const lines = buffer.split("\n");
207
507
  buffer = lines.pop() ?? "";
208
508
  for (const line of lines) {
@@ -211,14 +511,14 @@ export class OpenAICompatibleClient {
211
511
  continue;
212
512
  if (!trimmed.startsWith("data:"))
213
513
  continue;
214
- const data = trimmed.slice(5).trim();
514
+ const data = sanitizeTransportText(trimmed.slice(5).trim());
215
515
  if (!data)
216
516
  continue;
217
517
  yield data;
218
518
  }
219
519
  }
220
520
  if (buffer.trim().startsWith("data:")) {
221
- const data = buffer.trim().slice(5).trim();
521
+ const data = sanitizeTransportText(buffer.trim().slice(5).trim());
222
522
  if (data)
223
523
  yield data;
224
524
  }
@@ -57,6 +57,7 @@ export declare function buildOpenAiCompatibleBaseUrl(baseUrl: string): string;
57
57
  export declare function inferProviderFromModel(model: string | undefined): string | undefined;
58
58
  export declare function isLocalLlmProvider(providerInput: string | undefined): providerInput is LocalLlmProvider;
59
59
  export declare function defaultModelForProvider(providerInput: string | undefined): string;
60
+ export declare function normalizeLlamaCppHfModelName(modelInput: string | undefined): string | undefined;
60
61
  export declare function providerEnvPrefix(providerInput: string | undefined): string;
61
62
  export declare function buildProviderDoctorCommands(providerInput: string | undefined, modelInput: string | undefined, baseUrlInput?: string): string[];
62
63
  export declare function parseJsoncLoose(raw: string): unknown;
@@ -72,22 +72,19 @@ export function inferProviderFromModel(model) {
72
72
  const value = model.trim().toLowerCase();
73
73
  if (!value)
74
74
  return undefined;
75
- if (value.startsWith("llama.cpp/") || value.startsWith("llamacpp/")) {
75
+ // explicit prefixes
76
+ if (value.startsWith("llama.cpp/") || value.startsWith("llamacpp/"))
76
77
  return "llama.cpp";
77
- }
78
- if (value.startsWith("ollama/") ||
79
- value.includes("llama") ||
80
- value.includes("qwen") ||
81
- value.includes("mistral") ||
82
- value.includes("deepseek") ||
83
- value.includes("phi") ||
84
- value.includes(":") ||
85
- value.includes("mixtral")) {
78
+ if (value.startsWith("ollama/"))
86
79
  return "ollama";
87
- }
88
- if (value.startsWith("copilot/")) {
80
+ if (value.startsWith("copilot/"))
89
81
  return "copilot";
90
- }
82
+ // file-like or gguf artifacts -> prefer llama.cpp
83
+ if (value.endsWith(".gguf") || value.includes(".gguf"))
84
+ return "llama.cpp";
85
+ if (value.endsWith(".bin"))
86
+ return "llama.cpp";
87
+ // common hosted provider hints
91
88
  if (value.includes("claude"))
92
89
  return "claude";
93
90
  if (value.includes("gemini"))
@@ -100,6 +97,11 @@ export function inferProviderFromModel(model) {
100
97
  value.startsWith("o5")) {
101
98
  return "codex";
102
99
  }
100
+ // model families often served via Ollama
101
+ if (value.includes("qwen") || value.includes("mistral") || value.includes("mixtral") || value.includes("deepseek") || value.includes("phi")) {
102
+ return "ollama";
103
+ }
104
+ // Ambiguous cases (e.g., hf.co/owner/name:ref) should not default to Ollama; allow runtime discovery to decide.
103
105
  return undefined;
104
106
  }
105
107
  export function isLocalLlmProvider(providerInput) {
@@ -114,6 +116,22 @@ export function defaultModelForProvider(providerInput) {
114
116
  return DEFAULT_LLAMA_CPP_MODEL;
115
117
  return DEFAULT_HOSTED_MODELS[provider] ?? DEFAULT_HOSTED_MODELS.codex;
116
118
  }
119
+ export function normalizeLlamaCppHfModelName(modelInput) {
120
+ const value = modelInput?.trim();
121
+ if (!value)
122
+ return undefined;
123
+ if (value.endsWith(".gguf"))
124
+ return value;
125
+ const normalized = value.replace(/^(?:https?:\/\/)?(?:hf\.co|huggingface\.co)\//i, "");
126
+ const match = normalized.match(/^([^/]+)\/([^:]+?)(?::([^:]+))?$/);
127
+ if (!match)
128
+ return value;
129
+ const repoName = match[2].replace(/\.gguf$/i, "").replace(/-GGUF$/i, "");
130
+ const quant = match[3]?.trim();
131
+ if (!quant)
132
+ return value;
133
+ return `${repoName}-${quant}.gguf`;
134
+ }
117
135
  export function providerEnvPrefix(providerInput) {
118
136
  const provider = normalizeProvider(providerInput);
119
137
  if (!provider)
@@ -124,46 +142,51 @@ export function providerEnvPrefix(providerInput) {
124
142
  }
125
143
  export function buildProviderDoctorCommands(providerInput, modelInput, baseUrlInput) {
126
144
  const provider = normalizeProvider(providerInput) ?? "ollama";
127
- const model = modelInput?.trim() || defaultModelForProvider(provider);
145
+ const model = modelInput?.trim();
128
146
  const baseUrl = normalizeLocalBaseUrl(baseUrlInput);
129
147
  if (provider === "ollama") {
148
+ const resolvedModel = model || defaultModelForProvider(provider);
130
149
  return [
131
150
  "ollama serve",
132
- `ollama pull ${model}`,
151
+ `ollama pull ${resolvedModel}`,
133
152
  ...(baseUrl ? [`curl -s ${baseUrl}/api/tags`] : []),
134
153
  baseUrl
135
- ? `ace doctor --llm ${provider} --model ${model} --base-url ${baseUrl}`
136
- : `ace doctor --llm ${provider} --model ${model} --scan`,
154
+ ? `ace doctor --llm ${provider} --model ${resolvedModel} --base-url ${baseUrl}`
155
+ : `ace doctor --llm ${provider} --model ${resolvedModel} --scan`,
137
156
  ];
138
157
  }
139
158
  if (provider === "llama.cpp") {
159
+ const resolvedModel = model || "<hf-model-or-gguf>";
140
160
  return [
141
161
  "# Start llama-server separately, for example:",
142
- "# llama-server -m /path/to/model.gguf --port 8080",
162
+ `# llama-server -hf ${resolvedModel} --port 8080`,
143
163
  ...(baseUrl ? [`curl -s ${buildOpenAiCompatibleBaseUrl(baseUrl)}/models`] : []),
144
164
  baseUrl
145
- ? `ace doctor --llm ${provider} --model ${model} --base-url ${baseUrl}`
146
- : `ace doctor --llm ${provider} --model ${model} --scan`,
165
+ ? `ace doctor --llm ${provider} --model ${resolvedModel} --base-url ${baseUrl}`
166
+ : `ace doctor --llm ${provider} --model ${resolvedModel} --scan`,
147
167
  ];
148
168
  }
149
169
  if (provider === "codex") {
170
+ const resolvedModel = model || defaultModelForProvider(provider);
150
171
  return [
151
172
  "export OPENAI_API_KEY=<token>",
152
173
  ...(baseUrl ? [`export OPENAI_BASE_URL=${baseUrl}`] : []),
153
- `ace doctor --llm ${provider} --model ${model}${baseUrl ? ` --base-url ${baseUrl}` : ""}`,
174
+ `ace doctor --llm ${provider} --model ${resolvedModel}${baseUrl ? ` --base-url ${baseUrl}` : ""}`,
154
175
  ];
155
176
  }
156
177
  if (provider === "copilot") {
178
+ const resolvedModel = model || defaultModelForProvider(provider);
157
179
  return [
158
180
  "gh auth login # or export GITHUB_TOKEN=<token>",
159
- `ace doctor --llm ${provider} --model ${model}`,
181
+ `ace doctor --llm ${provider} --model ${resolvedModel}`,
160
182
  ];
161
183
  }
162
184
  const prefix = providerEnvPrefix(provider);
185
+ const resolvedModel = model || defaultModelForProvider(provider);
163
186
  return [
164
187
  `export ${prefix}_BASE_URL=${baseUrl ?? "<openai-compatible-base-url>"}`,
165
188
  `export ${prefix}_API_KEY=<token>`,
166
- `ace doctor --llm ${provider} --model ${model}${baseUrl ? ` --base-url ${baseUrl}` : ""}`,
189
+ `ace doctor --llm ${provider} --model ${resolvedModel}${baseUrl ? ` --base-url ${baseUrl}` : ""}`,
167
190
  ];
168
191
  }
169
192
  function looksLikeModel(value) {
@@ -458,7 +481,10 @@ export function discoverProviderContext(options) {
458
481
  }
459
482
  }
460
483
  if (!model) {
461
- model = defaultModelForProvider(provider);
484
+ model = provider === "llama.cpp" ? "" : defaultModelForProvider(provider);
485
+ if (provider === "llama.cpp") {
486
+ notes.push("runtime_default=llama.cpp (model not set; run ace connect or ace doctor --scan)");
487
+ }
462
488
  }
463
489
  addModel(modelsByProvider, provider, model);
464
490
  const allProviders = sortProviders([
@@ -5,7 +5,7 @@ export declare const VERICIFY_PROCESS_POST_LOG_SCHEMA_NAME = "vericify-process-p
5
5
  export declare const VERICIFY_BRIDGE_SNAPSHOT_REL_PATH = "agent-state/vericify/ace-bridge.json";
6
6
  export declare const VERICIFY_BRIDGE_SNAPSHOT_SCHEMA_REL_PATH = "agent-state/MODULES/schemas/VERICIFY_BRIDGE_SNAPSHOT.schema.json";
7
7
  export declare const VERICIFY_BRIDGE_SNAPSHOT_SCHEMA_NAME = "vericify-bridge-snapshot@1.0.0";
8
- type VericifyProcessPostKind = "intent" | "progress" | "blocker" | "handoff_note" | "stale_ack" | "completion";
8
+ type VericifyProcessPostKind = "intent" | "progress" | "blocker" | "handoff_note" | "stale_ack" | "completion" | "plan_proposal" | "plan_quality_assessment";
9
9
  export interface VericifyProcessPost {
10
10
  process_post_id: string;
11
11
  run_id: string;
@@ -14,6 +14,7 @@ export interface VericifyProcessPost {
14
14
  agent_id: string;
15
15
  kind: VericifyProcessPostKind;
16
16
  summary: string;
17
+ blocker_category?: string;
17
18
  tool_refs: string[];
18
19
  evidence_refs: string[];
19
20
  checkpoint_ref?: string;
@@ -109,6 +110,7 @@ export declare function appendVericifyProcessPost(input: {
109
110
  agent_id: string;
110
111
  kind: VericifyProcessPostKind;
111
112
  summary: string;
113
+ blocker_category?: string;
112
114
  tool_refs?: string[];
113
115
  evidence_refs?: string[];
114
116
  checkpoint_ref?: string;
@@ -124,6 +126,7 @@ export declare function appendVericifyProcessPostSafe(input: {
124
126
  agent_id: string;
125
127
  kind: VericifyProcessPostKind;
126
128
  summary: string;
129
+ blocker_category?: string;
127
130
  tool_refs?: string[];
128
131
  evidence_refs?: string[];
129
132
  checkpoint_ref?: string;
@@ -50,6 +50,7 @@ function normalizePost(input) {
50
50
  agent_id: input.agent_id.trim(),
51
51
  kind: input.kind,
52
52
  summary: input.summary.trim(),
53
+ blocker_category: input.blocker_category?.trim() || undefined,
53
54
  tool_refs: [...new Set(input.tool_refs.map((entry) => entry.trim()).filter(Boolean))],
54
55
  evidence_refs: [
55
56
  ...new Set(input.evidence_refs.map((entry) => entry.trim()).filter(Boolean)),
@@ -354,6 +355,7 @@ export async function appendVericifyProcessPost(input) {
354
355
  agent_id: agentId,
355
356
  kind: input.kind,
356
357
  summary,
358
+ blocker_category: input.blocker_category,
357
359
  tool_refs: input.tool_refs ?? [],
358
360
  evidence_refs: input.evidence_refs ?? [],
359
361
  checkpoint_ref: input.checkpoint_ref,
@@ -375,6 +377,7 @@ export async function appendVericifyProcessPost(input) {
375
377
  metadata: {
376
378
  branch_id: post.branch_id,
377
379
  lane_id: post.lane_id,
380
+ blocker_category: post.blocker_category,
378
381
  checkpoint_ref: post.checkpoint_ref,
379
382
  tool_refs: post.tool_refs,
380
383
  evidence_refs: post.evidence_refs,