@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.
- package/CHANGELOG.md +16 -0
- package/README.md +502 -56
- package/assets/.agents/ACE/agent-qa/instructions.md +11 -0
- package/assets/agent-state/MODULES/schemas/RUNTIME_TOOL_SPEC_REGISTRY.schema.json +43 -0
- package/assets/agent-state/runtime-tool-specs.json +70 -2
- package/assets/instructions/ACE_Coder.instructions.md +13 -0
- package/assets/instructions/ACE_UI.instructions.md +11 -0
- package/dist/ace-context.js +70 -11
- package/dist/ace-internal-tools.d.ts +3 -1
- package/dist/ace-internal-tools.js +10 -2
- package/dist/agent-runtime/role-adapters.d.ts +18 -1
- package/dist/agent-runtime/role-adapters.js +49 -5
- package/dist/astgrep-index.d.ts +48 -0
- package/dist/astgrep-index.js +126 -1
- package/dist/cli.js +487 -17
- package/dist/discovery-runtime-wrappers.d.ts +108 -0
- package/dist/discovery-runtime-wrappers.js +615 -0
- package/dist/helpers/bootstrap.js +1 -1
- package/dist/helpers/constants.d.ts +4 -2
- package/dist/helpers/constants.js +8 -0
- package/dist/helpers/path-utils.d.ts +8 -1
- package/dist/helpers/path-utils.js +27 -8
- package/dist/helpers/store-resolution.js +7 -3
- package/dist/hermes/bridge-protocol.d.ts +41 -0
- package/dist/hermes/bridge-protocol.js +70 -0
- package/dist/hermes/launch-profile.d.ts +19 -0
- package/dist/hermes/launch-profile.js +81 -0
- package/dist/hermes/session-manager.d.ts +42 -0
- package/dist/hermes/session-manager.js +187 -0
- package/dist/job-scheduler.js +30 -4
- package/dist/json-sanitizer.d.ts +16 -0
- package/dist/json-sanitizer.js +26 -0
- package/dist/local-model-policy.d.ts +27 -0
- package/dist/local-model-policy.js +84 -0
- package/dist/local-model-runtime.d.ts +17 -0
- package/dist/local-model-runtime.js +77 -20
- package/dist/model-bridge.d.ts +6 -1
- package/dist/model-bridge.js +338 -21
- package/dist/orchestrator-supervisor.d.ts +42 -0
- package/dist/orchestrator-supervisor.js +110 -3
- package/dist/plan-proposal.d.ts +115 -0
- package/dist/plan-proposal.js +1073 -0
- package/dist/runtime-executor.d.ts +6 -1
- package/dist/runtime-executor.js +72 -5
- package/dist/runtime-tool-specs.d.ts +19 -1
- package/dist/runtime-tool-specs.js +67 -26
- package/dist/schemas.js +30 -1
- package/dist/server.d.ts +3 -0
- package/dist/server.js +73 -4
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +2 -0
- package/dist/store/bootstrap-store.d.ts +1 -0
- package/dist/store/bootstrap-store.js +8 -2
- package/dist/store/materializers/vericify-projector.js +3 -0
- package/dist/store/repositories/local-model-runtime-repository.d.ts +13 -1
- package/dist/store/repositories/local-model-runtime-repository.js +4 -1
- package/dist/store/repositories/vericify-repository.d.ts +1 -1
- package/dist/tools-agent.d.ts +20 -0
- package/dist/tools-agent.js +544 -29
- package/dist/tools-discovery.js +135 -0
- package/dist/tools-files.js +768 -66
- package/dist/tools-framework.js +80 -61
- package/dist/tools.d.ts +4 -1
- package/dist/tools.js +35 -13
- package/dist/tui/chat.d.ts +8 -0
- package/dist/tui/chat.js +74 -0
- package/dist/tui/index.d.ts +7 -0
- package/dist/tui/index.js +45 -2
- package/dist/tui/layout.d.ts +1 -0
- package/dist/tui/layout.js +4 -1
- package/dist/tui/ollama.d.ts +8 -1
- package/dist/tui/ollama.js +53 -12
- package/dist/tui/openai-compatible.d.ts +13 -0
- package/dist/tui/openai-compatible.js +305 -5
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +50 -24
- package/dist/vericify-bridge.d.ts +4 -1
- package/dist/vericify-bridge.js +3 -0
- package/package.json +2 -1
- package/scripts/hermes_bridge_worker.py +136 -0
package/dist/tui/ollama.js
CHANGED
|
@@ -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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
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 ${
|
|
151
|
+
`ollama pull ${resolvedModel}`,
|
|
133
152
|
...(baseUrl ? [`curl -s ${baseUrl}/api/tags`] : []),
|
|
134
153
|
baseUrl
|
|
135
|
-
? `ace doctor --llm ${provider} --model ${
|
|
136
|
-
: `ace doctor --llm ${provider} --model ${
|
|
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
|
-
|
|
162
|
+
`# llama-server -hf ${resolvedModel} --port 8080`,
|
|
143
163
|
...(baseUrl ? [`curl -s ${buildOpenAiCompatibleBaseUrl(baseUrl)}/models`] : []),
|
|
144
164
|
baseUrl
|
|
145
|
-
? `ace doctor --llm ${provider} --model ${
|
|
146
|
-
: `ace doctor --llm ${provider} --model ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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;
|
package/dist/vericify-bridge.js
CHANGED
|
@@ -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,
|