micro-models-agent 0.51.1 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands.js +162 -38
- package/dist/cli/completer.js +5 -5
- package/dist/cli/main.js +42 -54
- package/dist/cli/repl-commands.js +138 -38
- package/dist/cli/repl.js +175 -89
- package/dist/cli/run-result.js +11 -0
- package/dist/cli/security-commands.js +6 -6
- package/dist/cli/setup.js +21 -15
- package/dist/config/config.js +54 -27
- package/dist/config/defaults.js +17 -0
- package/dist/config/domains.js +179 -0
- package/dist/config/index.js +2 -1
- package/dist/config/security.js +28 -8
- package/dist/core/agent.js +162 -30
- package/dist/core/bootstrap.js +94 -17
- package/dist/core/crash-handler.js +51 -0
- package/dist/core/environment.js +199 -0
- package/dist/core/session-logger.js +60 -6
- package/dist/core/version.js +2 -0
- package/dist/i18n/en.json +120 -39
- package/dist/i18n/ru.json +91 -10
- package/dist/llm/openai-compat.js +191 -53
- package/dist/llm/orchestrator.js +5 -3
- package/dist/logger/app-logger.js +50 -4
- package/dist/main.js +1288 -635
- package/dist/modules/browser/session.js +4 -0
- package/dist/modules/certification/cli.js +58 -19
- package/dist/modules/certification/loader.js +2 -1
- package/dist/modules/certification/manifest.js +22 -14
- package/dist/modules/certification/runner.js +91 -5
- package/dist/modules/certification/scenarios.js +290 -7
- package/dist/modules/context/fact-extractor.js +6 -0
- package/dist/modules/context/manager.js +19 -2
- package/dist/modules/execution/audit-runners.js +61 -7
- package/dist/modules/execution/execution-plugin.js +219 -60
- package/dist/modules/execution/module.js +207 -18
- package/dist/modules/execution/moe-executor.js +33 -20
- package/dist/modules/execution/plan-store.js +39 -0
- package/dist/modules/execution/plan-tool.js +188 -19
- package/dist/modules/execution/planner.js +27 -23
- package/dist/modules/execution/stuck-detector.js +244 -8
- package/dist/modules/execution/tracker.js +8 -6
- package/dist/modules/execution/verifier.js +15 -2
- package/dist/modules/hallucination/detector.js +4 -0
- package/dist/modules/hallucination/factual.js +45 -5
- package/dist/modules/indexer/module.js +1 -0
- package/dist/modules/lsp/client.js +123 -12
- package/dist/modules/lsp/index.js +1 -1
- package/dist/modules/lsp/module.js +30 -2
- package/dist/modules/lsp/probe.js +11 -1
- package/dist/modules/lsp/startup-check.js +5 -2
- package/dist/modules/plugins/builtin/lint-on-write.js +144 -41
- package/dist/modules/plugins/manager.js +57 -13
- package/dist/modules/pricing/index.js +61 -0
- package/dist/modules/pricing/prices.js +129 -0
- package/dist/modules/providers/create.js +22 -0
- package/dist/modules/providers/fallback.js +79 -0
- package/dist/modules/providers/health.js +46 -0
- package/dist/modules/providers/index.js +5 -0
- package/dist/modules/providers/manager.js +161 -0
- package/dist/modules/providers/presets.js +128 -0
- package/dist/modules/providers/registry.js +22 -0
- package/dist/modules/providers/types.js +1 -0
- package/dist/modules/registry.js +1 -0
- package/dist/modules/security/command-validator.js +14 -0
- package/dist/modules/security/encryption.js +6 -6
- package/dist/modules/security/network-validator.js +17 -0
- package/dist/modules/security/path-validator.js +22 -26
- package/dist/modules/session/store.js +10 -10
- package/dist/tools/approve.js +1 -0
- package/dist/tools/attach-image.js +12 -0
- package/dist/tools/bash.js +27 -4
- package/dist/tools/browser.js +1 -0
- package/dist/tools/chunk-query.js +1 -0
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/download-file.js +1 -0
- package/dist/tools/edit-file.js +2 -1
- package/dist/tools/enable-tools.js +1 -0
- package/dist/tools/executor.js +17 -7
- package/dist/tools/file-info.js +1 -0
- package/dist/tools/glob-tool.js +1 -0
- package/dist/tools/grep-tool.js +54 -13
- package/dist/tools/list-dir.js +1 -0
- package/dist/tools/load-skill.js +1 -0
- package/dist/tools/mcp-call.js +1 -0
- package/dist/tools/move-file.js +1 -0
- package/dist/tools/path-utils.js +51 -1
- package/dist/tools/pipeline-run.js +1 -0
- package/dist/tools/process-kill.js +11 -0
- package/dist/tools/process-list.js +1 -0
- package/dist/tools/process-log.js +9 -0
- package/dist/tools/question.js +1 -0
- package/dist/tools/read-file.js +94 -6
- package/dist/tools/recall.js +1 -0
- package/dist/tools/remember.js +1 -0
- package/dist/tools/scope-check.js +7 -5
- package/dist/tools/search-history.js +1 -0
- package/dist/tools/subagent.js +4 -4
- package/dist/tools/web-browse.js +1 -0
- package/dist/tools/web-fetch.js +27 -6
- package/dist/tools/web-search.js +70 -43
- package/dist/tools/write-file.js +1 -0
- package/dist/ui/line-editor.js +142 -23
- package/dist/ui/line-math.js +8 -4
- package/dist/ui/renderer.js +57 -7
- package/package.json +50 -48
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TokenCounter } from "./token-counter";
|
|
2
2
|
import { t } from "../i18n/index";
|
|
3
3
|
import { createRateLimiter } from "../modules/security/rate-limiter";
|
|
4
|
+
const REQUEST_TIMEOUT_MS = 120000;
|
|
4
5
|
function buildRequestBody(opts) {
|
|
5
6
|
const body = {
|
|
6
7
|
model: opts.model,
|
|
@@ -41,6 +42,8 @@ export class OpenAICompatProvider {
|
|
|
41
42
|
maxRetries: 3,
|
|
42
43
|
baseDelay: 1000,
|
|
43
44
|
maxDelay: 30000,
|
|
45
|
+
maxStreamRetries: 2,
|
|
46
|
+
noDataTimeoutMs: 180000,
|
|
44
47
|
};
|
|
45
48
|
this.rateLimiter = createRateLimiter(config.rateLimits);
|
|
46
49
|
}
|
|
@@ -54,15 +57,11 @@ export class OpenAICompatProvider {
|
|
|
54
57
|
const streamResult = this.doStream(messages, tools, signal, options);
|
|
55
58
|
let hasToolCall = false;
|
|
56
59
|
let hasText = false;
|
|
57
|
-
let reasoningAcc = "";
|
|
58
60
|
for await (const chunk of streamResult) {
|
|
59
61
|
if (chunk.type === "tool_call")
|
|
60
62
|
hasToolCall = true;
|
|
61
63
|
if (chunk.type === "text")
|
|
62
64
|
hasText = true;
|
|
63
|
-
if (chunk.type === "reasoning" && chunk.content) {
|
|
64
|
-
reasoningAcc += chunk.content;
|
|
65
|
-
}
|
|
66
65
|
yield chunk;
|
|
67
66
|
}
|
|
68
67
|
if (!hasToolCall && !hasText) {
|
|
@@ -73,63 +72,114 @@ export class OpenAICompatProvider {
|
|
|
73
72
|
}
|
|
74
73
|
}
|
|
75
74
|
async *doStream(messages, tools, signal, options) {
|
|
75
|
+
const { baseDelay, maxDelay, maxStreamRetries, noDataTimeoutMs } = this.retryConfig;
|
|
76
|
+
const streamRetries = maxStreamRetries ?? 2;
|
|
77
|
+
const idleTimeoutMs = noDataTimeoutMs ?? 180000;
|
|
78
|
+
for (let attempt = 0;; attempt++) {
|
|
79
|
+
let emitted = false;
|
|
80
|
+
const onEmit = () => {
|
|
81
|
+
emitted = true;
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
const sawDone = yield* this.streamOnce(messages, tools, signal, options, onEmit, idleTimeoutMs);
|
|
85
|
+
// Clean [DONE] or any partial content → deliver what we have.
|
|
86
|
+
if (sawDone || emitted)
|
|
87
|
+
return;
|
|
88
|
+
// Stream closed before [DONE] with zero content → retryable truncation.
|
|
89
|
+
if (attempt >= streamRetries)
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err?.name === "AbortError" || err?.llmTerminal || signal?.aborted)
|
|
94
|
+
throw err;
|
|
95
|
+
// Content was already delivered — a re-stream would duplicate chunks.
|
|
96
|
+
if (emitted || attempt >= streamRetries)
|
|
97
|
+
throw err;
|
|
98
|
+
// Connection dropped before any content → retry the whole request.
|
|
99
|
+
}
|
|
100
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
101
|
+
const jitter = Math.random() * baseDelay * 0.1;
|
|
102
|
+
await this.sleep(delay + jitter, signal);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async *streamOnce(messages, tools, signal, options, onEmit, idleTimeoutMs) {
|
|
106
|
+
const maxTokens = options?.maxTokens ?? this.config.maxCompletionTokens ?? 4096;
|
|
76
107
|
const body = buildRequestBody({
|
|
77
108
|
model: this.model,
|
|
78
109
|
messages,
|
|
79
110
|
tools,
|
|
80
111
|
stream: true,
|
|
81
|
-
maxTokens
|
|
112
|
+
maxTokens,
|
|
82
113
|
reasoningEffort: options?.reasoningEffort,
|
|
83
114
|
});
|
|
84
|
-
const headers =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
115
|
+
const { headers, abortSignal, cleanup, isTimeout, flagTimeout, controller } = this.buildRequestSetup(signal);
|
|
116
|
+
let response;
|
|
117
|
+
try {
|
|
118
|
+
response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers,
|
|
121
|
+
body: JSON.stringify(body),
|
|
122
|
+
signal: abortSignal,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
// Fetch-level failures are already HTTP-retried inside fetchWithRetry —
|
|
127
|
+
// don't double-retry them at the stream level.
|
|
128
|
+
if (err?.name === "AbortError")
|
|
129
|
+
throw err;
|
|
130
|
+
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
131
|
+
wrapped.llmTerminal = true;
|
|
132
|
+
throw wrapped;
|
|
89
133
|
}
|
|
90
|
-
const controller = new AbortController();
|
|
91
|
-
const totalTimeoutMs = 120000;
|
|
92
|
-
const timeoutId = setTimeout(() => controller.abort(), totalTimeoutMs);
|
|
93
|
-
const abortSignal = (() => {
|
|
94
|
-
if (!signal)
|
|
95
|
-
return controller.signal;
|
|
96
|
-
try {
|
|
97
|
-
return AbortSignal.any([controller.signal, signal]);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
signal.addEventListener("abort", () => controller.abort(), {
|
|
101
|
-
once: true,
|
|
102
|
-
});
|
|
103
|
-
return controller.signal;
|
|
104
|
-
}
|
|
105
|
-
})();
|
|
106
|
-
const response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers,
|
|
109
|
-
body: JSON.stringify(body),
|
|
110
|
-
signal: abortSignal,
|
|
111
|
-
});
|
|
112
134
|
if (!response.ok) {
|
|
113
|
-
|
|
135
|
+
cleanup();
|
|
114
136
|
const errorText = await response.text();
|
|
115
|
-
|
|
137
|
+
const err = new Error(t("error.llm_api", {
|
|
116
138
|
status: response.status,
|
|
117
139
|
statusText: response.statusText,
|
|
118
140
|
errorText,
|
|
119
141
|
}));
|
|
142
|
+
err.llmTerminal = true;
|
|
143
|
+
throw err;
|
|
120
144
|
}
|
|
121
145
|
const reader = response.body?.getReader();
|
|
122
146
|
if (!reader) {
|
|
123
|
-
|
|
147
|
+
cleanup();
|
|
124
148
|
throw new Error(t("error.no_response_body"));
|
|
125
149
|
}
|
|
126
150
|
const decoder = new TextDecoder();
|
|
127
151
|
let buffer = "";
|
|
128
152
|
const toolCallAccs = new Map();
|
|
153
|
+
// A tool_call started accumulating — the server committed to a response.
|
|
154
|
+
// If the stream stalls after this, the provider most likely buffers SSE
|
|
155
|
+
// instead of streaming argument deltas (seen with LM Studio).
|
|
156
|
+
let sawToolCallStart = false;
|
|
129
157
|
let usage;
|
|
158
|
+
let sawDone = false;
|
|
159
|
+
let lastFinishReason;
|
|
160
|
+
let sawText = false;
|
|
161
|
+
const readIdle = () => new Promise((resolve, reject) => {
|
|
162
|
+
const idleTimer = setTimeout(() => {
|
|
163
|
+
flagTimeout();
|
|
164
|
+
controller.abort();
|
|
165
|
+
}, idleTimeoutMs);
|
|
166
|
+
reader.read().then((result) => {
|
|
167
|
+
clearTimeout(idleTimer);
|
|
168
|
+
if (isTimeout())
|
|
169
|
+
reject(new Error(t(sawToolCallStart ? "error.llm_stream_idle_toolcall" : "error.llm_stream_idle", { timeout: idleTimeoutMs })));
|
|
170
|
+
else
|
|
171
|
+
resolve(result);
|
|
172
|
+
}, (err) => {
|
|
173
|
+
clearTimeout(idleTimer);
|
|
174
|
+
if (isTimeout())
|
|
175
|
+
reject(new Error(t(sawToolCallStart ? "error.llm_stream_idle_toolcall" : "error.llm_stream_idle", { timeout: idleTimeoutMs })));
|
|
176
|
+
else
|
|
177
|
+
reject(err);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
130
180
|
try {
|
|
131
181
|
while (true) {
|
|
132
|
-
const { done, value } = await
|
|
182
|
+
const { done, value } = await readIdle();
|
|
133
183
|
if (done)
|
|
134
184
|
break;
|
|
135
185
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -140,8 +190,10 @@ export class OpenAICompatProvider {
|
|
|
140
190
|
if (!trimmed || !trimmed.startsWith("data: "))
|
|
141
191
|
continue;
|
|
142
192
|
const data = trimmed.slice(6);
|
|
143
|
-
if (data === "[DONE]")
|
|
193
|
+
if (data === "[DONE]") {
|
|
194
|
+
sawDone = true;
|
|
144
195
|
continue;
|
|
196
|
+
}
|
|
145
197
|
try {
|
|
146
198
|
const parsed = JSON.parse(data);
|
|
147
199
|
const choice = parsed.choices?.[0];
|
|
@@ -158,10 +210,14 @@ export class OpenAICompatProvider {
|
|
|
158
210
|
}
|
|
159
211
|
const delta = choice.delta || {};
|
|
160
212
|
const finishReason = choice.finish_reason;
|
|
213
|
+
if (finishReason)
|
|
214
|
+
lastFinishReason = finishReason;
|
|
161
215
|
if (delta.reasoning_content) {
|
|
216
|
+
onEmit();
|
|
162
217
|
yield { type: "reasoning", content: delta.reasoning_content };
|
|
163
218
|
}
|
|
164
219
|
if (delta.tool_calls) {
|
|
220
|
+
sawToolCallStart = true;
|
|
165
221
|
for (const tc of delta.tool_calls) {
|
|
166
222
|
const idx = tc.index ?? 0;
|
|
167
223
|
if (!toolCallAccs.has(idx)) {
|
|
@@ -178,11 +234,14 @@ export class OpenAICompatProvider {
|
|
|
178
234
|
}
|
|
179
235
|
}
|
|
180
236
|
if (delta.content) {
|
|
237
|
+
onEmit();
|
|
238
|
+
sawText = true;
|
|
181
239
|
yield { type: "text", content: delta.content };
|
|
182
240
|
}
|
|
183
241
|
if (finishReason === "tool_calls" && toolCallAccs.size > 0) {
|
|
184
242
|
for (const [, acc] of toolCallAccs) {
|
|
185
243
|
if (acc.name) {
|
|
244
|
+
onEmit();
|
|
186
245
|
yield {
|
|
187
246
|
type: "tool_call",
|
|
188
247
|
toolCall: {
|
|
@@ -202,13 +261,71 @@ export class OpenAICompatProvider {
|
|
|
202
261
|
}
|
|
203
262
|
}
|
|
204
263
|
if (usage) {
|
|
264
|
+
onEmit();
|
|
205
265
|
yield { type: "done", usage };
|
|
206
266
|
}
|
|
267
|
+
// finish_reason "length": the completion hit the token limit. An
|
|
268
|
+
// unfinished tool_call would otherwise be silently swallowed (tool_calls
|
|
269
|
+
// are only yielded on finish_reason "tool_calls") and surface as an
|
|
270
|
+
// empty response → blind hallucination retries. Surface the real cause.
|
|
271
|
+
if (lastFinishReason === "length") {
|
|
272
|
+
const truncatedToolCall = sawToolCallStart && toolCallAccs.size > 0;
|
|
273
|
+
if (truncatedToolCall || !sawText) {
|
|
274
|
+
const err = new Error(t(truncatedToolCall ? "error.llm_truncated_toolcall" : "error.llm_truncated", {
|
|
275
|
+
tokens: maxTokens,
|
|
276
|
+
}));
|
|
277
|
+
err.llmTerminal = true;
|
|
278
|
+
// The provider is fine — the model just overran the completion
|
|
279
|
+
// limit. The agent loop feeds this back so the model can adapt
|
|
280
|
+
// (split the output) instead of the session dying.
|
|
281
|
+
err.recoverableLlm = true;
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
207
285
|
}
|
|
208
286
|
finally {
|
|
209
|
-
|
|
287
|
+
cleanup();
|
|
210
288
|
reader.releaseLock();
|
|
211
289
|
}
|
|
290
|
+
return sawDone;
|
|
291
|
+
}
|
|
292
|
+
/** Shared request setup: auth headers + timeout/abort controller wiring. */
|
|
293
|
+
buildRequestSetup(signal) {
|
|
294
|
+
const headers = {
|
|
295
|
+
"Content-Type": "application/json",
|
|
296
|
+
};
|
|
297
|
+
if (this.config.apiKey && this.config.apiKey !== "not-needed") {
|
|
298
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
299
|
+
}
|
|
300
|
+
const controller = new AbortController();
|
|
301
|
+
let timedOut = false;
|
|
302
|
+
const timeoutId = setTimeout(() => {
|
|
303
|
+
timedOut = true;
|
|
304
|
+
controller.abort();
|
|
305
|
+
}, REQUEST_TIMEOUT_MS);
|
|
306
|
+
const abortSignal = (() => {
|
|
307
|
+
if (!signal)
|
|
308
|
+
return controller.signal;
|
|
309
|
+
try {
|
|
310
|
+
return AbortSignal.any([controller.signal, signal]);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
signal.addEventListener("abort", () => controller.abort(), {
|
|
314
|
+
once: true,
|
|
315
|
+
});
|
|
316
|
+
return controller.signal;
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
return {
|
|
320
|
+
headers,
|
|
321
|
+
abortSignal,
|
|
322
|
+
cleanup: () => clearTimeout(timeoutId),
|
|
323
|
+
isTimeout: () => timedOut,
|
|
324
|
+
flagTimeout: () => {
|
|
325
|
+
timedOut = true;
|
|
326
|
+
},
|
|
327
|
+
controller,
|
|
328
|
+
};
|
|
212
329
|
}
|
|
213
330
|
async doNonStreaming(messages, tools, signal, options) {
|
|
214
331
|
const body = buildRequestBody({
|
|
@@ -219,18 +336,13 @@ export class OpenAICompatProvider {
|
|
|
219
336
|
maxTokens: options?.maxTokens ?? this.config.maxCompletionTokens ?? 4096,
|
|
220
337
|
reasoningEffort: options?.reasoningEffort,
|
|
221
338
|
});
|
|
222
|
-
const headers =
|
|
223
|
-
"Content-Type": "application/json",
|
|
224
|
-
};
|
|
225
|
-
if (this.config.apiKey && this.config.apiKey !== "not-needed") {
|
|
226
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
227
|
-
}
|
|
339
|
+
const { headers, abortSignal, cleanup, isTimeout } = this.buildRequestSetup(signal);
|
|
228
340
|
try {
|
|
229
341
|
const response = await this.fetchWithRetry(`${this.config.baseUrl}/chat/completions`, {
|
|
230
342
|
method: "POST",
|
|
231
343
|
headers,
|
|
232
344
|
body: JSON.stringify(body),
|
|
233
|
-
signal,
|
|
345
|
+
signal: abortSignal,
|
|
234
346
|
});
|
|
235
347
|
if (!response.ok) {
|
|
236
348
|
const errorText = await response.text();
|
|
@@ -279,8 +391,14 @@ export class OpenAICompatProvider {
|
|
|
279
391
|
return chunks;
|
|
280
392
|
}
|
|
281
393
|
catch (err) {
|
|
394
|
+
if (isTimeout() && err?.name === "AbortError") {
|
|
395
|
+
throw new Error(t("error.llm_timeout", { timeout: REQUEST_TIMEOUT_MS }));
|
|
396
|
+
}
|
|
282
397
|
throw err instanceof Error ? err : new Error(String(err));
|
|
283
398
|
}
|
|
399
|
+
finally {
|
|
400
|
+
cleanup();
|
|
401
|
+
}
|
|
284
402
|
}
|
|
285
403
|
countTokens(text) {
|
|
286
404
|
return this.tokenCounter.count(text);
|
|
@@ -299,7 +417,9 @@ export class OpenAICompatProvider {
|
|
|
299
417
|
headers,
|
|
300
418
|
});
|
|
301
419
|
if (!response.ok) {
|
|
302
|
-
|
|
420
|
+
// Surface the failure: health probes must distinguish a dead
|
|
421
|
+
// provider from one that serves an empty model list.
|
|
422
|
+
throw new Error(`HTTP ${response.status} from ${url}`);
|
|
303
423
|
}
|
|
304
424
|
const data = (await response.json());
|
|
305
425
|
const models = (data.data || data || [])
|
|
@@ -307,33 +427,51 @@ export class OpenAICompatProvider {
|
|
|
307
427
|
.filter(Boolean);
|
|
308
428
|
return models;
|
|
309
429
|
}
|
|
310
|
-
catch {
|
|
311
|
-
|
|
430
|
+
catch (err) {
|
|
431
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
312
432
|
}
|
|
313
433
|
}
|
|
314
434
|
async fetchWithRetry(url, init) {
|
|
315
435
|
const { maxRetries, baseDelay, maxDelay } = this.retryConfig;
|
|
316
|
-
let
|
|
436
|
+
let lastStatus = 0;
|
|
437
|
+
let lastRetryAfter = 0;
|
|
317
438
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
318
439
|
try {
|
|
319
440
|
const response = await fetch(url, init);
|
|
320
441
|
if (!this.isRetryable(response.status))
|
|
321
442
|
return response;
|
|
322
|
-
|
|
443
|
+
lastStatus = response.status;
|
|
444
|
+
const retryAfter = Number(response.headers.get("retry-after") ?? 0);
|
|
445
|
+
lastRetryAfter = retryAfter > 0 ? retryAfter * 1000 : 0;
|
|
323
446
|
}
|
|
324
447
|
catch (err) {
|
|
325
448
|
if (err.name === "AbortError") {
|
|
326
449
|
throw err;
|
|
327
450
|
}
|
|
328
|
-
|
|
451
|
+
if (attempt === maxRetries)
|
|
452
|
+
throw err;
|
|
329
453
|
}
|
|
330
454
|
if (attempt < maxRetries) {
|
|
331
|
-
|
|
455
|
+
// Prefer the provider's Retry-After window (saturating at maxDelay);
|
|
456
|
+
// otherwise exponential backoff. 429s from free tiers need a real wait.
|
|
457
|
+
const delay = lastRetryAfter > 0
|
|
458
|
+
? Math.min(lastRetryAfter, maxDelay)
|
|
459
|
+
: Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
332
460
|
const jitter = Math.random() * baseDelay * 0.1;
|
|
333
461
|
await this.sleep(delay + jitter, init.signal ?? undefined);
|
|
334
462
|
}
|
|
335
463
|
}
|
|
336
|
-
|
|
464
|
+
if (lastStatus === 429) {
|
|
465
|
+
const e429 = new Error(t("error.llm_429", {
|
|
466
|
+
model: this.model,
|
|
467
|
+
baseUrl: this.config.baseUrl,
|
|
468
|
+
}));
|
|
469
|
+
e429.llmStatus = lastStatus;
|
|
470
|
+
throw e429;
|
|
471
|
+
}
|
|
472
|
+
const eRetry = new Error(t("error.llm_retries"));
|
|
473
|
+
eRetry.llmStatus = lastStatus;
|
|
474
|
+
throw eRetry;
|
|
337
475
|
}
|
|
338
476
|
isRetryable(status) {
|
|
339
477
|
return status === 429 || status >= 500;
|
package/dist/llm/orchestrator.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createProvider } from "../modules/providers/create";
|
|
2
2
|
import { jsonrepair } from "jsonrepair";
|
|
3
3
|
const PLAN_SYSTEM_PROMPT = `You are a planning assistant for an agent system with multiple expert sub-agents.
|
|
4
4
|
Break down the user's task into subtasks that can be executed by different expert agents.
|
|
@@ -56,10 +56,11 @@ export class OrchestratorClient {
|
|
|
56
56
|
this.config = config;
|
|
57
57
|
if (config.model) {
|
|
58
58
|
if (config.provider) {
|
|
59
|
-
this.provider =
|
|
59
|
+
this.provider = createProvider(config.provider.type ?? "openai-compat", {
|
|
60
60
|
model: config.model,
|
|
61
61
|
baseUrl: config.provider.baseUrl || "http://localhost:1234/v1",
|
|
62
62
|
apiKey: config.provider.apiKey,
|
|
63
|
+
contextWindow: 32768,
|
|
63
64
|
retry: config.retry,
|
|
64
65
|
});
|
|
65
66
|
}
|
|
@@ -67,9 +68,10 @@ export class OrchestratorClient {
|
|
|
67
68
|
this.provider = defaultProvider;
|
|
68
69
|
}
|
|
69
70
|
else {
|
|
70
|
-
this.provider =
|
|
71
|
+
this.provider = createProvider("openai-compat", {
|
|
71
72
|
model: config.model,
|
|
72
73
|
baseUrl: "http://localhost:1234/v1",
|
|
74
|
+
contextWindow: 32768,
|
|
73
75
|
retry: config.retry,
|
|
74
76
|
});
|
|
75
77
|
}
|
|
@@ -59,19 +59,19 @@ export class Logger {
|
|
|
59
59
|
}
|
|
60
60
|
// --- Tagged helpers (legacy v1 API) ---
|
|
61
61
|
logLLMRequest(model, messagesCount, promptPreview, caller) {
|
|
62
|
-
this.fileLog.logLLMRequest(model, messagesCount, promptPreview, caller);
|
|
62
|
+
this.fileLog.logLLMRequest(model, messagesCount, sanitizeLogMessage(promptPreview), caller);
|
|
63
63
|
}
|
|
64
64
|
logLLMResponse(model, responseLength, genTimeMs, error, caller) {
|
|
65
65
|
this.fileLog.logLLMResponse(model, responseLength, genTimeMs, error, caller);
|
|
66
66
|
}
|
|
67
67
|
logToolCall(tool, preview, result) {
|
|
68
|
-
this.fileLog.logToolCall(tool, preview, result);
|
|
68
|
+
this.fileLog.logToolCall(tool, sanitizeLogMessage(preview), result !== undefined ? sanitizeLogMessage(result) : undefined);
|
|
69
69
|
}
|
|
70
70
|
logToolOutput(tool, output, exitCode) {
|
|
71
|
-
this.fileLog.logToolOutput(tool, output, exitCode);
|
|
71
|
+
this.fileLog.logToolOutput(tool, sanitizeLogMessage(output), exitCode);
|
|
72
72
|
}
|
|
73
73
|
logREPL(tag, content) {
|
|
74
|
-
this.fileLog.logREPL(tag, content);
|
|
74
|
+
this.fileLog.logREPL(tag, sanitizeLogMessage(content));
|
|
75
75
|
}
|
|
76
76
|
child(prefix) {
|
|
77
77
|
const childLogger = new Logger(this.level, this.prefix ? `${this.prefix}:${prefix}` : prefix);
|
|
@@ -140,4 +140,50 @@ export class Logger {
|
|
|
140
140
|
}
|
|
141
141
|
return sanitized;
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Write a structured record to app.jsonl only (no console / .log output).
|
|
145
|
+
* Used for machine-readable reports (environment, etc.) that would be too
|
|
146
|
+
* noisy on the terminal but are valuable for cross-device diagnosis.
|
|
147
|
+
*/
|
|
148
|
+
logStructured(type, data) {
|
|
149
|
+
const logTarget = this.sessionDir ?? this.logDir;
|
|
150
|
+
if (!logTarget)
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
appendFileSync(join(logTarget, "app.jsonl"), JSON.stringify({
|
|
154
|
+
level: "info",
|
|
155
|
+
ts: new Date().toISOString(),
|
|
156
|
+
type,
|
|
157
|
+
meta: this.sanitizeMeta(data),
|
|
158
|
+
}) + "\n", "utf-8");
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
/* file logging is best-effort */
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Write a log entry to app.jsonl only — no console output, no .log file.
|
|
166
|
+
* Use this for noisy subsystems (LSP, probes) whose warnings should be
|
|
167
|
+
* diagnosable from session files but not pollute the user's terminal.
|
|
168
|
+
*/
|
|
169
|
+
logSilent(level, msg, meta) {
|
|
170
|
+
const logTarget = this.sessionDir ?? this.logDir;
|
|
171
|
+
if (!logTarget)
|
|
172
|
+
return;
|
|
173
|
+
const ts = new Date().toISOString();
|
|
174
|
+
const sanitizedMsg = sanitizeLogMessage(msg);
|
|
175
|
+
const sanitizedMeta = meta ? this.sanitizeMeta(meta) : undefined;
|
|
176
|
+
try {
|
|
177
|
+
appendFileSync(join(logTarget, "app.jsonl"), JSON.stringify({
|
|
178
|
+
level,
|
|
179
|
+
ts,
|
|
180
|
+
prefix: this.prefix,
|
|
181
|
+
msg: sanitizedMsg,
|
|
182
|
+
meta: sanitizedMeta ?? null,
|
|
183
|
+
}) + "\n", "utf-8");
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
/* file logging is best-effort */
|
|
187
|
+
}
|
|
188
|
+
}
|
|
143
189
|
}
|