@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 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 ─────────────