@visorcraft/idlehands 2.2.12 → 2.2.14

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({
@@ -1994,6 +2059,7 @@ export async function createSession(opts) {
1994
2059
  let forceToollessRecoveryTurn = false;
1995
2060
  let toollessRecoveryUsed = false;
1996
2061
  const streamedToolCallPreviews = new Set();
2062
+ const streamedToolCallPreviewScores = new Map();
1997
2063
  // ── Security: credential leak detection + prompt injection guard ──
1998
2064
  const leakDetector = new LeakDetector();
1999
2065
  const promptGuard = new PromptGuard('warn');
@@ -2279,6 +2345,7 @@ export async function createSession(opts) {
2279
2345
  hookObj.onFirstDelta?.();
2280
2346
  };
2281
2347
  let resp;
2348
+ let streamFallbackDiag;
2282
2349
  try {
2283
2350
  try {
2284
2351
  // turns is 1-indexed (incremented at loop top), so first iteration = 1.
@@ -2314,6 +2381,24 @@ export async function createSession(opts) {
2314
2381
  if (cfg.verbose) {
2315
2382
  console.error(`[turn-debug] prompt_bytes=${promptBytesEstimate} tools=${toolsForTurn.length} tool_schema_bytes=${toolSchemaBytesEstimate} tool_schema_tokens~=${toolSchemaTokenEstimate}`);
2316
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
+ };
2317
2402
  // ── Response cache: check for cached response ──────────────
2318
2403
  // Only cache tool-less turns (final answers, explanations) since
2319
2404
  // tool-calling turns have side effects that shouldn't be replayed.
@@ -2365,9 +2450,6 @@ export async function createSession(opts) {
2365
2450
  ? delta.id
2366
2451
  : `stream_call_${delta.index}`;
2367
2452
  const previewKey = `${turns}:${id}:${name}`;
2368
- if (streamedToolCallPreviews.has(previewKey))
2369
- return;
2370
- streamedToolCallPreviews.add(previewKey);
2371
2453
  let parsedArgs = {};
2372
2454
  const rawArgs = typeof delta.argumentsSoFar === 'string' ? delta.argumentsSoFar.trim() : '';
2373
2455
  if (rawArgs) {
@@ -2380,7 +2462,17 @@ export async function createSession(opts) {
2380
2462
  catch {
2381
2463
  // partial JSON chunks are expected during streaming
2382
2464
  }
2465
+ if (!Object.keys(parsedArgs).length) {
2466
+ parsedArgs = extractPartialToolArgsPreview(name, rawArgs);
2467
+ }
2383
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));
2384
2476
  void emitToolCall(id, name, parsedArgs, 'planned');
2385
2477
  },
2386
2478
  };
@@ -2410,6 +2502,7 @@ export async function createSession(opts) {
2410
2502
  }
2411
2503
  },
2412
2504
  });
2505
+ noteStreamFallback('runtime-router', resp);
2413
2506
  }
2414
2507
  else {
2415
2508
  const isLikelyAuthError = (errMsg) => {
@@ -2457,6 +2550,7 @@ export async function createSession(opts) {
2457
2550
  }
2458
2551
  },
2459
2552
  });
2553
+ noteStreamFallback(target.name ?? 'default', resp);
2460
2554
  break;
2461
2555
  }
2462
2556
  catch (providerErr) {
@@ -2476,6 +2570,9 @@ export async function createSession(opts) {
2476
2570
  }
2477
2571
  }
2478
2572
  } // end if (!resp) — cache miss path
2573
+ if (streamFallbackDiag && lastTurnDebug) {
2574
+ lastTurnDebug.streamFallback = streamFallbackDiag;
2575
+ }
2479
2576
  // Successful response resets overflow recovery budget.
2480
2577
  overflowCompactionAttempts = 0;
2481
2578
  // ── Response cache: store cacheable responses ─────────────
@@ -3133,8 +3230,10 @@ export async function createSession(opts) {
3133
3230
  if (name === 'read_file' || name === 'read_files') {
3134
3231
  const filePath = typeof args.path === 'string' ? args.path : '';
3135
3232
  const searchTerm = typeof args.search === 'string' ? args.search : '';
3136
- // Fix 1: Hard cumulative budget — refuse reads past hard cap
3137
- if (cumulativeReadOnlyCalls > READ_BUDGET_HARD) {
3233
+ // Fix 1: Hard cumulative budget — refuse reads once hard cap is reached.
3234
+ // Count only actual executed read-only calls (not cache replays), so this check
3235
+ // blocks the next call exactly at the configured cap.
3236
+ if (cumulativeReadOnlyCalls >= READ_BUDGET_HARD) {
3138
3237
  await emitToolCall(callId, name, args);
3139
3238
  await emitToolResult({
3140
3239
  id: callId,
@@ -3539,6 +3638,13 @@ export async function createSession(opts) {
3539
3638
  toolCallId: callId,
3540
3639
  result: content,
3541
3640
  });
3641
+ // Count only actual read-only executions toward cumulative read budget.
3642
+ // Cached/replayed read observations should not consume budget.
3643
+ if (isReadOnlyToolDynamic(name) &&
3644
+ !reusedCachedReadTool &&
3645
+ !reusedCachedReadOnlyExec) {
3646
+ cumulativeReadOnlyCalls += 1;
3647
+ }
3542
3648
  // ── Per-file mutation spiral detection ──
3543
3649
  // Track edits to the same file. If the model keeps editing the same file
3544
3650
  // over and over, it's likely in an edit→break→read→edit corruption spiral.
@@ -3706,9 +3812,6 @@ export async function createSession(opts) {
3706
3812
  console.warn(`[guardrail] capped ${droppedCount} read-only tool calls (per-turn limit ${READ_ONLY_PER_TURN_CAP})`);
3707
3813
  }
3708
3814
  }
3709
- // Fix 1: Hard cumulative read budget — escalating enforcement
3710
- const readOnlyThisTurn = toolCallsArr.filter((tc) => isReadOnlyToolDynamic(tc.function.name));
3711
- cumulativeReadOnlyCalls += readOnlyThisTurn.length;
3712
3815
  if (harness.toolCalls.parallelCalls) {
3713
3816
  // Models that support parallel calls: read-only in parallel, mutations sequential
3714
3817
  const readonly = toolCallsArr.filter((tc) => isReadOnlyToolDynamic(tc.function.name));