@visorcraft/idlehands 2.2.11 → 2.2.13
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 +126 -2
- package/dist/agent.js.map +1 -1
- package/dist/bot/metrics-command.js +2 -0
- package/dist/bot/metrics-command.js.map +1 -1
- package/dist/client.js +58 -3
- package/dist/client.js.map +1 -1
- package/dist/progress/turn-progress.js +6 -0
- package/dist/progress/turn-progress.js.map +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -312,6 +312,71 @@ export async function createSession(opts) {
|
|
|
312
312
|
if (recentToolUsage.length > 60)
|
|
313
313
|
recentToolUsage.shift();
|
|
314
314
|
};
|
|
315
|
+
const extractPartialToolArgsPreview = (toolName, rawArgs) => {
|
|
316
|
+
const out = {};
|
|
317
|
+
const text = String(rawArgs ?? '');
|
|
318
|
+
if (!text.trim())
|
|
319
|
+
return out;
|
|
320
|
+
const pickString = (key) => {
|
|
321
|
+
const m = text.match(new RegExp(`"${key}"\\s*:\\s*"([^\\n\"]*)`));
|
|
322
|
+
return m?.[1];
|
|
323
|
+
};
|
|
324
|
+
const pickNumber = (key) => {
|
|
325
|
+
const m = text.match(new RegExp(`"${key}"\\s*:\\s*(-?\\d+)`));
|
|
326
|
+
if (!m)
|
|
327
|
+
return undefined;
|
|
328
|
+
const n = Number.parseInt(m[1], 10);
|
|
329
|
+
return Number.isFinite(n) ? n : undefined;
|
|
330
|
+
};
|
|
331
|
+
const pathLikeTools = new Set([
|
|
332
|
+
'read_file',
|
|
333
|
+
'write_file',
|
|
334
|
+
'edit_range',
|
|
335
|
+
'edit_file',
|
|
336
|
+
'insert_file',
|
|
337
|
+
'list_dir',
|
|
338
|
+
'lsp_diagnostics',
|
|
339
|
+
'lsp_symbols',
|
|
340
|
+
'lsp_hover',
|
|
341
|
+
'lsp_definition',
|
|
342
|
+
'lsp_references',
|
|
343
|
+
]);
|
|
344
|
+
if (pathLikeTools.has(toolName)) {
|
|
345
|
+
const path = pickString('path');
|
|
346
|
+
if (path)
|
|
347
|
+
out.path = path;
|
|
348
|
+
}
|
|
349
|
+
if (toolName === 'search_files') {
|
|
350
|
+
const pattern = pickString('pattern');
|
|
351
|
+
const path = pickString('path');
|
|
352
|
+
if (pattern)
|
|
353
|
+
out.pattern = pattern;
|
|
354
|
+
if (path)
|
|
355
|
+
out.path = path;
|
|
356
|
+
}
|
|
357
|
+
if (toolName === 'exec') {
|
|
358
|
+
const command = pickString('command');
|
|
359
|
+
const cwd = pickString('cwd');
|
|
360
|
+
if (command)
|
|
361
|
+
out.command = command;
|
|
362
|
+
if (cwd)
|
|
363
|
+
out.cwd = cwd;
|
|
364
|
+
}
|
|
365
|
+
if (toolName === 'vault_search') {
|
|
366
|
+
const query = pickString('query');
|
|
367
|
+
if (query)
|
|
368
|
+
out.query = query;
|
|
369
|
+
}
|
|
370
|
+
if (toolName === 'edit_range') {
|
|
371
|
+
const start = pickNumber('start_line');
|
|
372
|
+
const end = pickNumber('end_line');
|
|
373
|
+
if (start != null)
|
|
374
|
+
out.start_line = start;
|
|
375
|
+
if (end != null)
|
|
376
|
+
out.end_line = end;
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
};
|
|
315
380
|
const vault = vaultEnabled
|
|
316
381
|
? (opts.runtime?.vault ??
|
|
317
382
|
new VaultStore({
|
|
@@ -1676,10 +1741,10 @@ export async function createSession(opts) {
|
|
|
1676
1741
|
const hasOnToolResult = Boolean(hookObj.onToolResult);
|
|
1677
1742
|
const hasOnToolLoop = Boolean(hookObj.onToolLoop);
|
|
1678
1743
|
const hasOnTurnEnd = Boolean(hookObj.onTurnEnd);
|
|
1679
|
-
const emitToolCall = async (id, name, args) => {
|
|
1744
|
+
const emitToolCall = async (id, name, args, phase = 'executing') => {
|
|
1680
1745
|
if (!hasOnToolCall && !hooksEnabled)
|
|
1681
1746
|
return;
|
|
1682
|
-
const call = { id, name, args };
|
|
1747
|
+
const call = { id, name, args, phase };
|
|
1683
1748
|
if (hasOnToolCall)
|
|
1684
1749
|
hookObj.onToolCall?.(call);
|
|
1685
1750
|
if (hooksEnabled) {
|
|
@@ -1993,6 +2058,8 @@ export async function createSession(opts) {
|
|
|
1993
2058
|
const toolLoopWarningKeys = new Set();
|
|
1994
2059
|
let forceToollessRecoveryTurn = false;
|
|
1995
2060
|
let toollessRecoveryUsed = false;
|
|
2061
|
+
const streamedToolCallPreviews = new Set();
|
|
2062
|
+
const streamedToolCallPreviewScores = new Map();
|
|
1996
2063
|
// ── Security: credential leak detection + prompt injection guard ──
|
|
1997
2064
|
const leakDetector = new LeakDetector();
|
|
1998
2065
|
const promptGuard = new PromptGuard('warn');
|
|
@@ -2278,6 +2345,7 @@ export async function createSession(opts) {
|
|
|
2278
2345
|
hookObj.onFirstDelta?.();
|
|
2279
2346
|
};
|
|
2280
2347
|
let resp;
|
|
2348
|
+
let streamFallbackDiag;
|
|
2281
2349
|
try {
|
|
2282
2350
|
try {
|
|
2283
2351
|
// turns is 1-indexed (incremented at loop top), so first iteration = 1.
|
|
@@ -2313,6 +2381,24 @@ export async function createSession(opts) {
|
|
|
2313
2381
|
if (cfg.verbose) {
|
|
2314
2382
|
console.error(`[turn-debug] prompt_bytes=${promptBytesEstimate} tools=${toolsForTurn.length} tool_schema_bytes=${toolSchemaBytesEstimate} tool_schema_tokens~=${toolSchemaTokenEstimate}`);
|
|
2315
2383
|
}
|
|
2384
|
+
const noteStreamFallback = (providerName, response) => {
|
|
2385
|
+
const fallback = response?.meta?.stream_fallback;
|
|
2386
|
+
if (!fallback || typeof fallback !== 'object')
|
|
2387
|
+
return;
|
|
2388
|
+
const reason = String(fallback.reason ?? 'unknown');
|
|
2389
|
+
const attempt = Number(fallback.attempt ?? NaN);
|
|
2390
|
+
const status = Number(fallback.status ?? NaN);
|
|
2391
|
+
const detail = [
|
|
2392
|
+
Number.isFinite(attempt) ? `attempt=${attempt}` : null,
|
|
2393
|
+
Number.isFinite(status) ? `status=${status}` : null,
|
|
2394
|
+
]
|
|
2395
|
+
.filter(Boolean)
|
|
2396
|
+
.join(' ');
|
|
2397
|
+
streamFallbackDiag = `${providerName}:${reason}${detail ? ` (${detail})` : ''}`;
|
|
2398
|
+
if (cfg.verbose) {
|
|
2399
|
+
console.warn(`[routing] stream fallback provider=${providerName} reason=${reason}${detail ? ` ${detail}` : ''}`);
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2316
2402
|
// ── Response cache: check for cached response ──────────────
|
|
2317
2403
|
// Only cache tool-less turns (final answers, explanations) since
|
|
2318
2404
|
// tool-calling turns have side effects that shouldn't be replayed.
|
|
@@ -2356,6 +2442,39 @@ export async function createSession(opts) {
|
|
|
2356
2442
|
requestId: `r${reqCounter}`,
|
|
2357
2443
|
onToken: hookObj.onToken,
|
|
2358
2444
|
onFirstDelta,
|
|
2445
|
+
onToolCallDelta: (delta) => {
|
|
2446
|
+
const name = typeof delta?.name === 'string' ? delta.name : '';
|
|
2447
|
+
if (!name)
|
|
2448
|
+
return;
|
|
2449
|
+
const id = typeof delta?.id === 'string' && delta.id.trim().length
|
|
2450
|
+
? delta.id
|
|
2451
|
+
: `stream_call_${delta.index}`;
|
|
2452
|
+
const previewKey = `${turns}:${id}:${name}`;
|
|
2453
|
+
let parsedArgs = {};
|
|
2454
|
+
const rawArgs = typeof delta.argumentsSoFar === 'string' ? delta.argumentsSoFar.trim() : '';
|
|
2455
|
+
if (rawArgs) {
|
|
2456
|
+
try {
|
|
2457
|
+
const parsed = parseJsonArgs(rawArgs);
|
|
2458
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
2459
|
+
parsedArgs = parsed;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
catch {
|
|
2463
|
+
// partial JSON chunks are expected during streaming
|
|
2464
|
+
}
|
|
2465
|
+
if (!Object.keys(parsedArgs).length) {
|
|
2466
|
+
parsedArgs = extractPartialToolArgsPreview(name, rawArgs);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
const score = Object.keys(parsedArgs).length + (rawArgs ? 1 : 0);
|
|
2470
|
+
const prevScore = streamedToolCallPreviewScores.get(previewKey) ?? 0;
|
|
2471
|
+
const shouldEmit = !streamedToolCallPreviews.has(previewKey) || score > prevScore;
|
|
2472
|
+
if (!shouldEmit)
|
|
2473
|
+
return;
|
|
2474
|
+
streamedToolCallPreviews.add(previewKey);
|
|
2475
|
+
streamedToolCallPreviewScores.set(previewKey, Math.max(prevScore, score));
|
|
2476
|
+
void emitToolCall(id, name, parsedArgs, 'planned');
|
|
2477
|
+
},
|
|
2359
2478
|
};
|
|
2360
2479
|
if (primaryUsesRuntimeModel && primaryRoute?.model) {
|
|
2361
2480
|
// Runtime-native routing: lane model/fallbacks reference runtime model IDs.
|
|
@@ -2383,6 +2502,7 @@ export async function createSession(opts) {
|
|
|
2383
2502
|
}
|
|
2384
2503
|
},
|
|
2385
2504
|
});
|
|
2505
|
+
noteStreamFallback('runtime-router', resp);
|
|
2386
2506
|
}
|
|
2387
2507
|
else {
|
|
2388
2508
|
const isLikelyAuthError = (errMsg) => {
|
|
@@ -2430,6 +2550,7 @@ export async function createSession(opts) {
|
|
|
2430
2550
|
}
|
|
2431
2551
|
},
|
|
2432
2552
|
});
|
|
2553
|
+
noteStreamFallback(target.name ?? 'default', resp);
|
|
2433
2554
|
break;
|
|
2434
2555
|
}
|
|
2435
2556
|
catch (providerErr) {
|
|
@@ -2449,6 +2570,9 @@ export async function createSession(opts) {
|
|
|
2449
2570
|
}
|
|
2450
2571
|
}
|
|
2451
2572
|
} // end if (!resp) — cache miss path
|
|
2573
|
+
if (streamFallbackDiag && lastTurnDebug) {
|
|
2574
|
+
lastTurnDebug.streamFallback = streamFallbackDiag;
|
|
2575
|
+
}
|
|
2452
2576
|
// Successful response resets overflow recovery budget.
|
|
2453
2577
|
overflowCompactionAttempts = 0;
|
|
2454
2578
|
// ── Response cache: store cacheable responses ─────────────
|