coder-agent 2.2.2 → 2.3.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.
- package/dist/agent.js +226 -138
- package/dist/index.js +29 -11
- package/dist/tools.js +13 -6
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -236,10 +236,26 @@ function extractTextToolCalls(content) {
|
|
|
236
236
|
}
|
|
237
237
|
return calls;
|
|
238
238
|
}
|
|
239
|
-
// ─── Gemini API client
|
|
240
|
-
async function
|
|
239
|
+
// ─── Gemini API client with Auto-Rotation Fallback ────────────────────────────
|
|
240
|
+
async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initialDelayMs = 1500, signal) {
|
|
241
|
+
const rotationList = [
|
|
242
|
+
"gemini-2.5-flash",
|
|
243
|
+
"gemini-2.5-pro",
|
|
244
|
+
"gemini-2.0-flash",
|
|
245
|
+
"gemini-2.0-pro-exp"
|
|
246
|
+
];
|
|
247
|
+
let currentModel = params.model;
|
|
248
|
+
let modelIndex = rotationList.indexOf(currentModel);
|
|
249
|
+
if (modelIndex === -1) {
|
|
250
|
+
modelIndex = 0; // Default if not in list
|
|
251
|
+
}
|
|
241
252
|
let attempts = 0;
|
|
242
253
|
while (attempts < maxRetries) {
|
|
254
|
+
if (signal?.aborted) {
|
|
255
|
+
const abortErr = new Error("The user aborted a request.");
|
|
256
|
+
abortErr.name = "AbortError";
|
|
257
|
+
throw abortErr;
|
|
258
|
+
}
|
|
243
259
|
try {
|
|
244
260
|
const res = await fetch("https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", {
|
|
245
261
|
method: "POST",
|
|
@@ -247,7 +263,8 @@ async function callGeminiAPI(apiKey, params, maxRetries = 3, initialDelayMs = 15
|
|
|
247
263
|
"Content-Type": "application/json",
|
|
248
264
|
"Authorization": `Bearer ${apiKey}`
|
|
249
265
|
},
|
|
250
|
-
body: JSON.stringify(params)
|
|
266
|
+
body: JSON.stringify({ ...params, model: currentModel }),
|
|
267
|
+
signal
|
|
251
268
|
});
|
|
252
269
|
if (!res.ok) {
|
|
253
270
|
const errText = await res.text();
|
|
@@ -255,20 +272,50 @@ async function callGeminiAPI(apiKey, params, maxRetries = 3, initialDelayMs = 15
|
|
|
255
272
|
err.status = res.status;
|
|
256
273
|
throw err;
|
|
257
274
|
}
|
|
258
|
-
|
|
275
|
+
const data = await res.json();
|
|
276
|
+
return { data, modelUsed: currentModel };
|
|
259
277
|
}
|
|
260
278
|
catch (err) {
|
|
279
|
+
if (signal?.aborted || err?.name === "AbortError") {
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
261
282
|
attempts++;
|
|
262
283
|
const status = err?.status;
|
|
263
284
|
const isRetryableError = status === 429 || status === 503 || (status >= 500 && status < 600) || !status;
|
|
264
|
-
if (isRetryableError
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
285
|
+
if (isRetryableError) {
|
|
286
|
+
// Rotate model immediately if rate limit or service unavailable occurred
|
|
287
|
+
if ((status === 429 || status === 503) && modelIndex + 1 < rotationList.length) {
|
|
288
|
+
modelIndex++;
|
|
289
|
+
const nextModel = rotationList[modelIndex];
|
|
290
|
+
stopSpinner();
|
|
291
|
+
console.log(chalk.hex('#ff9f0a')('⚠') + ' ' + chalk.gray(`Rate limited on ${currentModel}. Rotating to ${nextModel}`));
|
|
292
|
+
startSpinner("thinking...");
|
|
293
|
+
currentModel = nextModel;
|
|
294
|
+
attempts = 0; // reset retry counter for the fresh model
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
// Otherwise do standard delay retry on same model
|
|
298
|
+
if (attempts < maxRetries) {
|
|
299
|
+
const delay = initialDelayMs * Math.pow(2, attempts - 1);
|
|
300
|
+
await new Promise((resolve, reject) => {
|
|
301
|
+
const timer = setTimeout(resolve, delay);
|
|
302
|
+
if (signal) {
|
|
303
|
+
const onAbort = () => {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
const abortErr = new Error("The user aborted a request.");
|
|
306
|
+
abortErr.name = "AbortError";
|
|
307
|
+
reject(abortErr);
|
|
308
|
+
};
|
|
309
|
+
signal.addEventListener("abort", onAbort);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
268
314
|
}
|
|
269
315
|
throw err;
|
|
270
316
|
}
|
|
271
317
|
}
|
|
318
|
+
throw new Error("Request failed after all fallback rotations.");
|
|
272
319
|
}
|
|
273
320
|
// ─── Agent Class ─────────────────────────────────────────────────────────────
|
|
274
321
|
export class Agent {
|
|
@@ -294,154 +341,195 @@ export class Agent {
|
|
|
294
341
|
setModel(model) {
|
|
295
342
|
this.model = model;
|
|
296
343
|
}
|
|
297
|
-
async chat(userMessage) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
344
|
+
async chat(userMessage, signal) {
|
|
345
|
+
try {
|
|
346
|
+
if (signal?.aborted) {
|
|
347
|
+
const abortErr = new Error("The user aborted a request.");
|
|
348
|
+
abortErr.name = "AbortError";
|
|
349
|
+
throw abortErr;
|
|
350
|
+
}
|
|
351
|
+
await this.memory.init(this.memoryScope, "coder");
|
|
352
|
+
if (signal?.aborted) {
|
|
353
|
+
const abortErr = new Error("The user aborted a request.");
|
|
354
|
+
abortErr.name = "AbortError";
|
|
355
|
+
throw abortErr;
|
|
356
|
+
}
|
|
357
|
+
// ── Phase 1: Input & Enriched Context Pre-Parsing ──────────────────────
|
|
358
|
+
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
359
|
+
startSpinner("thinking...");
|
|
360
|
+
const diagnostics = extractDiagnostics(userMessage);
|
|
361
|
+
let enrichedPrompt = userMessage;
|
|
362
|
+
if (diagnostics.length > 0) {
|
|
363
|
+
updateSpinner("resolving files & context...");
|
|
364
|
+
const contexts = [];
|
|
365
|
+
for (const diag of diagnostics) {
|
|
366
|
+
if (signal?.aborted) {
|
|
367
|
+
const abortErr = new Error("The user aborted a request.");
|
|
368
|
+
abortErr.name = "AbortError";
|
|
369
|
+
throw abortErr;
|
|
370
|
+
}
|
|
371
|
+
const filePath = normalizeFilePath(diag.resource);
|
|
320
372
|
try {
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const lines = localContent.split(/\r?\n/);
|
|
373
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
374
|
+
const lines = content.split(/\r?\n/);
|
|
324
375
|
const startLine = Math.max(1, diag.startLineNumber - 10);
|
|
325
376
|
const endLine = Math.min(lines.length, (diag.endLineNumber || diag.startLineNumber) + 10);
|
|
326
377
|
const slice = lines.slice(startLine - 1, endLine);
|
|
327
378
|
const numberedLines = slice.map((line, idx) => `${startLine + idx}: ${line}`);
|
|
328
|
-
contexts.push(`File contents of
|
|
379
|
+
contexts.push(`File contents of ${filePath} (around line ${diag.startLineNumber}):\n` + numberedLines.join("\n"));
|
|
329
380
|
}
|
|
330
|
-
catch {
|
|
331
|
-
|
|
381
|
+
catch (err) {
|
|
382
|
+
try {
|
|
383
|
+
const relativePath = path.relative("/", filePath).replace(/^[a-zA-Z]:/, "").replace(/^\\+|^[//]+/, "");
|
|
384
|
+
const localContent = await fs.readFile(relativePath, "utf-8");
|
|
385
|
+
const lines = localContent.split(/\r?\n/);
|
|
386
|
+
const startLine = Math.max(1, diag.startLineNumber - 10);
|
|
387
|
+
const endLine = Math.min(lines.length, (diag.endLineNumber || diag.startLineNumber) + 10);
|
|
388
|
+
const slice = lines.slice(startLine - 1, endLine);
|
|
389
|
+
const numberedLines = slice.map((line, idx) => `${startLine + idx}: ${line}`);
|
|
390
|
+
contexts.push(`File contents of resolved path ${relativePath} (around line ${diag.startLineNumber}):\n` + numberedLines.join("\n"));
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
contexts.push(`[File Context: failed to read path ${filePath} - ${err.message}]`);
|
|
394
|
+
}
|
|
332
395
|
}
|
|
333
396
|
}
|
|
397
|
+
enrichedPrompt += "\n\n=== Enriched Code Context (Auto-Parsed) ===\n" + contexts.join("\n\n") + "\n===========================================";
|
|
334
398
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
let args = {};
|
|
390
|
-
try {
|
|
391
|
-
args = JSON.parse(toolCall.function.arguments);
|
|
399
|
+
this.memory.add({ role: "user", content: enrichedPrompt });
|
|
400
|
+
let iterations = 0;
|
|
401
|
+
const MAX_ITERATIONS = 12;
|
|
402
|
+
const modifiedFiles = new Set();
|
|
403
|
+
let cleanContent = "";
|
|
404
|
+
while (iterations < MAX_ITERATIONS) {
|
|
405
|
+
if (signal?.aborted) {
|
|
406
|
+
const abortErr = new Error("The user aborted a request.");
|
|
407
|
+
abortErr.name = "AbortError";
|
|
408
|
+
throw abortErr;
|
|
409
|
+
}
|
|
410
|
+
iterations++;
|
|
411
|
+
updateSpinner("thinking...");
|
|
412
|
+
const responseObj = await callGeminiAPIWithRotation(this.apiKey, {
|
|
413
|
+
model: this.model,
|
|
414
|
+
messages: this.memory.getAll(),
|
|
415
|
+
tools: TOOL_DEFINITIONS,
|
|
416
|
+
tool_choice: "auto",
|
|
417
|
+
temperature: 0.2,
|
|
418
|
+
}, 3, 1500, signal);
|
|
419
|
+
stopSpinner();
|
|
420
|
+
if (signal?.aborted) {
|
|
421
|
+
const abortErr = new Error("The user aborted a request.");
|
|
422
|
+
abortErr.name = "AbortError";
|
|
423
|
+
throw abortErr;
|
|
424
|
+
}
|
|
425
|
+
this.model = responseObj.modelUsed; // Persist successful model rotation
|
|
426
|
+
const response = responseObj.data;
|
|
427
|
+
const choice = response.choices[0];
|
|
428
|
+
const msg = choice.message;
|
|
429
|
+
// Extract text-based tool calls if native tool_calls is empty
|
|
430
|
+
let toolCalls = msg.tool_calls || [];
|
|
431
|
+
const extracted = extractTextToolCalls(msg.content ?? "");
|
|
432
|
+
if (toolCalls.length === 0 && extracted.length > 0) {
|
|
433
|
+
toolCalls = extracted.map(e => ({
|
|
434
|
+
id: e.id,
|
|
435
|
+
type: "function",
|
|
436
|
+
function: {
|
|
437
|
+
name: e.name,
|
|
438
|
+
arguments: JSON.stringify(e.args)
|
|
439
|
+
}
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
// Save assistant message to memory
|
|
443
|
+
const assistantMsg = {
|
|
444
|
+
role: "assistant",
|
|
445
|
+
content: msg.content ?? "",
|
|
446
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
447
|
+
};
|
|
448
|
+
this.memory.add(assistantMsg);
|
|
449
|
+
// Clean up text tool calls from the conversational output content
|
|
450
|
+
cleanContent = msg.content ?? "";
|
|
451
|
+
for (const call of extracted) {
|
|
452
|
+
cleanContent = cleanContent.replace(call.rawText, "");
|
|
392
453
|
}
|
|
393
|
-
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
454
|
+
cleanContent = cleanContent.trim();
|
|
455
|
+
// ── No tool calls → final answer ─────────────────────────────────────
|
|
456
|
+
if (toolCalls.length === 0) {
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
// ── Phase 2: Tool Execution ───────────────────────────────────────────
|
|
460
|
+
const statusLines = [];
|
|
461
|
+
for (const toolCall of toolCalls) {
|
|
462
|
+
if (signal?.aborted) {
|
|
463
|
+
const abortErr = new Error("The user aborted a request.");
|
|
464
|
+
abortErr.name = "AbortError";
|
|
465
|
+
throw abortErr;
|
|
466
|
+
}
|
|
467
|
+
const name = toolCall.function.name;
|
|
468
|
+
let args = {};
|
|
469
|
+
try {
|
|
470
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
471
|
+
}
|
|
472
|
+
catch { }
|
|
473
|
+
// Render tool execution card
|
|
474
|
+
const badge = getToolBadge(name);
|
|
475
|
+
const target = getToolTargetDescription(name, args);
|
|
476
|
+
const details = getToolDetailsDescription(name, args);
|
|
477
|
+
console.log(` ${badge} ${chalk.gray(target)}`);
|
|
478
|
+
console.log(chalk.dim(` ${details}`));
|
|
479
|
+
// Track created or modified files
|
|
480
|
+
if (name === "write_file" || name === "patch_file") {
|
|
481
|
+
if (args.file_path) {
|
|
482
|
+
modifiedFiles.add(path.normalize(args.file_path));
|
|
483
|
+
}
|
|
404
484
|
}
|
|
485
|
+
const result = await dispatchTool(name, args, signal);
|
|
486
|
+
if (signal?.aborted) {
|
|
487
|
+
const abortErr = new Error("The user aborted a request.");
|
|
488
|
+
abortErr.name = "AbortError";
|
|
489
|
+
throw abortErr;
|
|
490
|
+
}
|
|
491
|
+
// Generate outcome result status line
|
|
492
|
+
const isError = result.startsWith("ERROR:");
|
|
493
|
+
if (isError) {
|
|
494
|
+
statusLines.push(`${chalk.hex('#ff453a')('✕')} ${chalk.gray(result.slice(6).trim())}`);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
statusLines.push(`${chalk.hex('#30d158')('✓')} ${chalk.gray(getToolSuccessSummary(name, args, result))}`);
|
|
498
|
+
}
|
|
499
|
+
this.memory.add({
|
|
500
|
+
role: "tool",
|
|
501
|
+
content: result,
|
|
502
|
+
tool_call_id: toolCall.id,
|
|
503
|
+
name,
|
|
504
|
+
});
|
|
405
505
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
statusLines.push(`${chalk.hex('#ff453a')('✕')} ${chalk.gray(result.slice(6).trim())}`);
|
|
506
|
+
// Stream results status lines
|
|
507
|
+
console.log("");
|
|
508
|
+
for (const line of statusLines) {
|
|
509
|
+
console.log(line);
|
|
411
510
|
}
|
|
412
|
-
|
|
413
|
-
|
|
511
|
+
if (iterations < MAX_ITERATIONS) {
|
|
512
|
+
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
513
|
+
startSpinner("thinking...");
|
|
414
514
|
}
|
|
415
|
-
this.memory.add({
|
|
416
|
-
role: "tool",
|
|
417
|
-
content: result,
|
|
418
|
-
tool_call_id: toolCall.id,
|
|
419
|
-
name,
|
|
420
|
-
});
|
|
421
515
|
}
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
console.log(line);
|
|
516
|
+
// Output modified files metadata line if applicable
|
|
517
|
+
if (modifiedFiles.size > 0) {
|
|
518
|
+
console.log(chalk.dim('─') + ' ' + chalk.dim(`${modifiedFiles.size} file(s) modified`));
|
|
426
519
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
520
|
+
// ── Phase 3: Final Agent Response ───────────────────────────────────────
|
|
521
|
+
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
522
|
+
if (cleanContent) {
|
|
523
|
+
console.log(formatResponseText(cleanContent));
|
|
524
|
+
console.log(""); // one trailing blank line
|
|
525
|
+
}
|
|
526
|
+
if (iterations >= MAX_ITERATIONS) {
|
|
527
|
+
console.log(chalk.hex('#ff453a')('✕ error'));
|
|
528
|
+
console.log(chalk.dim(' Max tool iterations reached.'));
|
|
430
529
|
}
|
|
431
530
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
console.log(chalk.dim('─') + ' ' + chalk.dim(`${modifiedFiles.size} file(s) modified`));
|
|
435
|
-
}
|
|
436
|
-
// ── Phase 3: Final Agent Response ───────────────────────────────────────
|
|
437
|
-
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
438
|
-
if (cleanContent) {
|
|
439
|
-
console.log(formatResponseText(cleanContent));
|
|
440
|
-
console.log(""); // one trailing blank line
|
|
441
|
-
}
|
|
442
|
-
if (iterations >= MAX_ITERATIONS) {
|
|
443
|
-
console.log(chalk.hex('#ff453a')('✕ error'));
|
|
444
|
-
console.log(chalk.dim(' Max tool iterations reached.'));
|
|
531
|
+
finally {
|
|
532
|
+
stopSpinner();
|
|
445
533
|
}
|
|
446
534
|
}
|
|
447
535
|
}
|
package/dist/index.js
CHANGED
|
@@ -174,6 +174,7 @@ async function main() {
|
|
|
174
174
|
});
|
|
175
175
|
let inputBuffer = "";
|
|
176
176
|
let pasteTimeout = null;
|
|
177
|
+
let currentAbortController = null;
|
|
177
178
|
rl.setPrompt(chalk.hex('#0a84ff')('›') + ' ');
|
|
178
179
|
rl.prompt();
|
|
179
180
|
rl.on("line", (line) => {
|
|
@@ -245,29 +246,46 @@ async function main() {
|
|
|
245
246
|
rl.prompt();
|
|
246
247
|
return;
|
|
247
248
|
}
|
|
249
|
+
currentAbortController = new AbortController();
|
|
248
250
|
try {
|
|
249
|
-
await agent.chat(trimmed);
|
|
251
|
+
await agent.chat(trimmed, currentAbortController.signal);
|
|
250
252
|
}
|
|
251
253
|
catch (err) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
console.log(chalk.dim(" Invalid API key. Check your configuration."));
|
|
255
|
-
}
|
|
256
|
-
else if (err?.status === 429) {
|
|
257
|
-
console.log(chalk.dim(" Rate limit exceeded on Gemini API."));
|
|
254
|
+
if (err?.name === "AbortError" || currentAbortController.signal.aborted) {
|
|
255
|
+
console.log(chalk.hex('#ff453a')('\n✕ cancelled'));
|
|
258
256
|
}
|
|
259
257
|
else {
|
|
260
|
-
console.log(chalk.
|
|
258
|
+
console.log(chalk.hex('#ff453a')('✕ error'));
|
|
259
|
+
if (err?.status === 401) {
|
|
260
|
+
console.log(chalk.dim(" Invalid API key. Check your configuration."));
|
|
261
|
+
}
|
|
262
|
+
else if (err?.status === 429) {
|
|
263
|
+
console.log(chalk.dim(" Rate limit exceeded on Gemini API."));
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
console.log(chalk.dim(` ${err.message}`));
|
|
267
|
+
}
|
|
261
268
|
}
|
|
262
269
|
}
|
|
270
|
+
finally {
|
|
271
|
+
currentAbortController = null;
|
|
272
|
+
}
|
|
263
273
|
rl.resume();
|
|
264
274
|
rl.prompt();
|
|
265
275
|
}, 80);
|
|
266
276
|
});
|
|
267
277
|
// Handle Ctrl+C gracefully
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
278
|
+
const sigintHandler = () => {
|
|
279
|
+
if (currentAbortController) {
|
|
280
|
+
currentAbortController.abort();
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
rl.close();
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
process.on("SIGINT", sigintHandler);
|
|
288
|
+
rl.on("SIGINT", sigintHandler);
|
|
271
289
|
}
|
|
272
290
|
main().catch((err) => {
|
|
273
291
|
console.error(chalk.hex('#ff453a')('✕ error'));
|
package/dist/tools.js
CHANGED
|
@@ -181,7 +181,7 @@ export async function list_directory({ dir_path = "." }) {
|
|
|
181
181
|
return `ERROR: ${e.message}`;
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
-
export async function run_shell({ command, cwd }) {
|
|
184
|
+
export async function run_shell({ command, cwd }, signal) {
|
|
185
185
|
try {
|
|
186
186
|
let targetCwd = process.cwd();
|
|
187
187
|
if (cwd) {
|
|
@@ -200,18 +200,22 @@ export async function run_shell({ command, cwd }) {
|
|
|
200
200
|
const { stdout, stderr } = await execAsync(command, {
|
|
201
201
|
cwd: targetCwd,
|
|
202
202
|
timeout: 30_000,
|
|
203
|
+
signal,
|
|
203
204
|
});
|
|
204
205
|
const out = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n--- stderr ---\n");
|
|
205
206
|
return out || "(no output)";
|
|
206
207
|
}
|
|
207
208
|
catch (e) {
|
|
209
|
+
if (e.name === "AbortError" || signal?.aborted) {
|
|
210
|
+
throw e;
|
|
211
|
+
}
|
|
208
212
|
return `EXIT ${e.code ?? "?"}: ${e.stderr || e.message}`;
|
|
209
213
|
}
|
|
210
214
|
}
|
|
211
|
-
export async function web_search({ query }) {
|
|
215
|
+
export async function web_search({ query }, signal) {
|
|
212
216
|
try {
|
|
213
217
|
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
|
|
214
|
-
const res = await fetch(url);
|
|
218
|
+
const res = await fetch(url, { signal });
|
|
215
219
|
const data = (await res.json());
|
|
216
220
|
const parts = [];
|
|
217
221
|
if (data.AbstractText)
|
|
@@ -229,6 +233,9 @@ export async function web_search({ query }) {
|
|
|
229
233
|
return parts.length ? parts.join("\n\n") : `No instant answer found for: "${query}". Try rephrasing.`;
|
|
230
234
|
}
|
|
231
235
|
catch (e) {
|
|
236
|
+
if (e.name === "AbortError" || signal?.aborted) {
|
|
237
|
+
throw e;
|
|
238
|
+
}
|
|
232
239
|
return `Search error: ${e.message}`;
|
|
233
240
|
}
|
|
234
241
|
}
|
|
@@ -341,13 +348,13 @@ export async function patch_file({ file_path, target_code, replacement_code }) {
|
|
|
341
348
|
}
|
|
342
349
|
}
|
|
343
350
|
// ─── Dispatcher ──────────────────────────────────────────────────────────────
|
|
344
|
-
export async function dispatchTool(name, args) {
|
|
351
|
+
export async function dispatchTool(name, args, signal) {
|
|
345
352
|
switch (name) {
|
|
346
353
|
case "read_file": return read_file(args);
|
|
347
354
|
case "write_file": return write_file(args);
|
|
348
355
|
case "list_directory": return list_directory(args);
|
|
349
|
-
case "run_shell": return run_shell(args);
|
|
350
|
-
case "web_search": return web_search(args);
|
|
356
|
+
case "run_shell": return run_shell(args, signal);
|
|
357
|
+
case "web_search": return web_search(args, signal);
|
|
351
358
|
case "find_files": return find_files(args);
|
|
352
359
|
case "read_file_lines": return read_file_lines(args);
|
|
353
360
|
case "search_grep": return search_grep(args);
|