@visorcraft/idlehands 1.0.3 → 1.0.5
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 +151 -5
- package/dist/agent.js.map +1 -1
- package/dist/tools.js +44 -20
- package/dist/tools.js.map +1 -1
- package/dist/upgrade.js +18 -18
- package/dist/upgrade.js.map +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -178,6 +178,19 @@ function formatDurationMs(ms) {
|
|
|
178
178
|
return '0.0s';
|
|
179
179
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
180
180
|
}
|
|
181
|
+
function looksLikePlanningNarration(text, finishReason) {
|
|
182
|
+
const s = String(text ?? '').trim().toLowerCase();
|
|
183
|
+
if (!s)
|
|
184
|
+
return false;
|
|
185
|
+
// Incomplete streamed answer: likely still needs another turn.
|
|
186
|
+
if (finishReason === 'length')
|
|
187
|
+
return true;
|
|
188
|
+
// Strong completion cues: treat as final answer.
|
|
189
|
+
if (/(^|\n)\s*(done|completed|finished|final answer|summary:)\b/.test(s))
|
|
190
|
+
return false;
|
|
191
|
+
// Typical "thinking out loud"/plan chatter that should continue with tools.
|
|
192
|
+
return /\b(let me|i(?:'|’)ll|i will|i'm going to|i am going to|next i(?:'|’)ll|first i(?:'|’)ll|i need to|i should|checking|reviewing|exploring|starting by)\b/.test(s);
|
|
193
|
+
}
|
|
181
194
|
function approxTokenCharCap(maxTokens) {
|
|
182
195
|
const safe = Math.max(64, Math.floor(maxTokens));
|
|
183
196
|
return safe * 4;
|
|
@@ -829,7 +842,7 @@ export async function createSession(opts) {
|
|
|
829
842
|
const harnessVaultMode = harness.defaults?.trifecta?.vaultMode || 'off';
|
|
830
843
|
const vaultMode = (cfg.trifecta?.vault?.mode || harnessVaultMode);
|
|
831
844
|
const vaultEnabled = cfg.trifecta?.enabled !== false && cfg.trifecta?.vault?.enabled !== false;
|
|
832
|
-
|
|
845
|
+
let activeVaultTools = vaultEnabled && vaultMode === 'active';
|
|
833
846
|
const lensEnabled = cfg.trifecta?.enabled !== false && cfg.trifecta?.lens?.enabled !== false;
|
|
834
847
|
const spawnTaskEnabled = opts.allowSpawnTask !== false && cfg.sub_agents?.enabled !== false;
|
|
835
848
|
const mcpServers = Array.isArray(cfg.mcp?.servers) ? cfg.mcp.servers : [];
|
|
@@ -903,8 +916,18 @@ export async function createSession(opts) {
|
|
|
903
916
|
}
|
|
904
917
|
if (vaultEnabled && !opts.runtime?.vault) {
|
|
905
918
|
await vault?.init().catch((e) => {
|
|
919
|
+
// If vault storage is unavailable (e.g., sandboxed FS / disk I/O),
|
|
920
|
+
// degrade gracefully by disabling active vault tools for this run.
|
|
921
|
+
activeVaultTools = false;
|
|
922
|
+
const msg = String(e?.message ?? e ?? 'unknown error');
|
|
923
|
+
const isDiskLike = /disk i\/o|sqlite|readonly|read-only|permission denied/i.test(msg);
|
|
906
924
|
if (!process.env.IDLEHANDS_QUIET_WARNINGS) {
|
|
907
|
-
|
|
925
|
+
if (isDiskLike) {
|
|
926
|
+
console.warn('[warn] vault disabled for this session (storage unavailable).');
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
console.warn(`[warn] vault init failed: ${msg}`);
|
|
930
|
+
}
|
|
908
931
|
}
|
|
909
932
|
});
|
|
910
933
|
}
|
|
@@ -1094,6 +1117,12 @@ export async function createSession(opts) {
|
|
|
1094
1117
|
if (!task) {
|
|
1095
1118
|
throw new Error('spawn_task: missing task');
|
|
1096
1119
|
}
|
|
1120
|
+
// Prevent using delegation to bypass package-install confirmation restrictions.
|
|
1121
|
+
const taskSafety = checkExecSafety(task);
|
|
1122
|
+
if (!cfg.no_confirm && taskSafety.tier === 'cautious' && taskSafety.reason === 'package install/remove') {
|
|
1123
|
+
throw new Error('spawn_task: blocked — package install/remove is restricted in the current approval mode. ' +
|
|
1124
|
+
'Do not delegate this to bypass confirmation requirements; ask the user to run with --no-confirm/--yolo instead.');
|
|
1125
|
+
}
|
|
1097
1126
|
const defaults = cfg.sub_agents ?? {};
|
|
1098
1127
|
const taskId = ++subTaskSeq;
|
|
1099
1128
|
const emitStatus = options?.emitStatus ?? (() => { });
|
|
@@ -1732,6 +1761,12 @@ export async function createSession(opts) {
|
|
|
1732
1761
|
const consecutiveCounts = new Map();
|
|
1733
1762
|
let lastPassiveVaultQuery = '';
|
|
1734
1763
|
let malformedCount = 0;
|
|
1764
|
+
let noProgressTurns = 0;
|
|
1765
|
+
const NO_PROGRESS_TURN_CAP = 3;
|
|
1766
|
+
let noToolTurns = 0;
|
|
1767
|
+
const NO_TOOL_REPROMPT_THRESHOLD = 2;
|
|
1768
|
+
let repromptUsed = false;
|
|
1769
|
+
let blockedPackageInstallAttempts = 0;
|
|
1735
1770
|
const maybeInjectVaultContext = async () => {
|
|
1736
1771
|
if (!vault || vaultMode !== 'passive')
|
|
1737
1772
|
return;
|
|
@@ -1948,7 +1983,19 @@ export async function createSession(opts) {
|
|
|
1948
1983
|
tgTokensPerSec: tgTps,
|
|
1949
1984
|
health: healthSnapshot,
|
|
1950
1985
|
};
|
|
1951
|
-
const
|
|
1986
|
+
const legacyChoice = resp?.role
|
|
1987
|
+
? {
|
|
1988
|
+
finish_reason: resp?.finish_reason ?? 'stop',
|
|
1989
|
+
message: {
|
|
1990
|
+
role: resp?.role ?? 'assistant',
|
|
1991
|
+
content: resp?.content ?? '',
|
|
1992
|
+
tool_calls: resp?.tool_calls,
|
|
1993
|
+
},
|
|
1994
|
+
}
|
|
1995
|
+
: undefined;
|
|
1996
|
+
const choice0 = resp.choices?.[0] ?? legacyChoice;
|
|
1997
|
+
const finishReason = choice0?.finish_reason ?? 'unknown';
|
|
1998
|
+
const msg = choice0?.message;
|
|
1952
1999
|
const content = msg?.content ?? '';
|
|
1953
2000
|
// Conditionally strip thinking blocks based on harness config (§4i).
|
|
1954
2001
|
// Non-reasoning models (thinking.strip === false) never emit <think> blocks,
|
|
@@ -1996,7 +2043,40 @@ export async function createSession(opts) {
|
|
|
1996
2043
|
}
|
|
1997
2044
|
}
|
|
1998
2045
|
}
|
|
2046
|
+
if (cfg.verbose) {
|
|
2047
|
+
console.warn(`[turn ${turns}] finish_reason=${finishReason} content_chars=${content.length} visible_chars=${visible.length} tool_calls=${toolCallsArr?.length ?? 0}`);
|
|
2048
|
+
}
|
|
2049
|
+
const narration = (visible || content || '').trim();
|
|
2050
|
+
if ((!toolCallsArr || !toolCallsArr.length) && narration.length === 0) {
|
|
2051
|
+
noProgressTurns += 1;
|
|
2052
|
+
if (cfg.verbose) {
|
|
2053
|
+
console.warn(`[loop] no-progress turn ${noProgressTurns}/${NO_PROGRESS_TURN_CAP} (empty response)`);
|
|
2054
|
+
}
|
|
2055
|
+
if (noProgressTurns >= NO_PROGRESS_TURN_CAP) {
|
|
2056
|
+
throw new Error(`no progress for ${NO_PROGRESS_TURN_CAP} consecutive turns (empty responses with no tool calls). ` +
|
|
2057
|
+
`Likely malformed/empty model output loop; stopping early.`);
|
|
2058
|
+
}
|
|
2059
|
+
messages.push({
|
|
2060
|
+
role: 'user',
|
|
2061
|
+
content: '[system] Your previous response was empty (no text, no tool calls). Continue by either calling a tool with valid JSON arguments or giving a final answer.',
|
|
2062
|
+
});
|
|
2063
|
+
await hookObj.onTurnEnd?.({
|
|
2064
|
+
turn: turns,
|
|
2065
|
+
toolCalls,
|
|
2066
|
+
promptTokens: cumulativeUsage.prompt,
|
|
2067
|
+
completionTokens: cumulativeUsage.completion,
|
|
2068
|
+
promptTokensTurn,
|
|
2069
|
+
completionTokensTurn,
|
|
2070
|
+
ttftMs,
|
|
2071
|
+
ttcMs,
|
|
2072
|
+
ppTps,
|
|
2073
|
+
tgTps,
|
|
2074
|
+
});
|
|
2075
|
+
continue;
|
|
2076
|
+
}
|
|
2077
|
+
noProgressTurns = 0;
|
|
1999
2078
|
if (toolCallsArr && toolCallsArr.length) {
|
|
2079
|
+
noToolTurns = 0;
|
|
2000
2080
|
// Deduplicate ghost tool calls: if llama-server's XML parser splits one
|
|
2001
2081
|
// tool call into two entries (one with full args, one empty/partial),
|
|
2002
2082
|
// drop the empty one. Only removes entries where a richer version of the
|
|
@@ -2282,6 +2362,9 @@ export async function createSession(opts) {
|
|
|
2282
2362
|
else if (builtInFn) {
|
|
2283
2363
|
const value = await builtInFn(ctx, args);
|
|
2284
2364
|
content = typeof value === 'string' ? value : JSON.stringify(value);
|
|
2365
|
+
if (name === 'exec') {
|
|
2366
|
+
blockedPackageInstallAttempts = 0;
|
|
2367
|
+
}
|
|
2285
2368
|
}
|
|
2286
2369
|
else if (isLspTool && lspManager) {
|
|
2287
2370
|
// LSP tool dispatch
|
|
@@ -2383,6 +2466,16 @@ export async function createSession(opts) {
|
|
|
2383
2466
|
if (e instanceof AgentLoopBreak)
|
|
2384
2467
|
throw e;
|
|
2385
2468
|
const msg = e?.message ?? String(e);
|
|
2469
|
+
// Fast-fail package-install bypass loops in non-yolo modes.
|
|
2470
|
+
// Applies to direct exec attempts and spawn_task delegation attempts.
|
|
2471
|
+
if ((tc.function.name === 'exec' || tc.function.name === 'spawn_task') &&
|
|
2472
|
+
/package install\/remove.*(?:blocked|restricted)|without --no-confirm\/--yolo/i.test(msg)) {
|
|
2473
|
+
blockedPackageInstallAttempts += 1;
|
|
2474
|
+
if (blockedPackageInstallAttempts >= 2) {
|
|
2475
|
+
throw new AgentLoopBreak(`${tc.function.name}: repeated blocked package-install attempts in current approval mode. ` +
|
|
2476
|
+
'Do not retry or delegate this. Continue with a zero-dependency path, or ask the user to restart with --no-confirm/--yolo.');
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2386
2479
|
// Hook: onToolResult for errors (Phase 8.5)
|
|
2387
2480
|
const callId = resolveCallId(tc);
|
|
2388
2481
|
hookObj.onToolResult?.({ id: callId, name: tc.function.name, success: false, summary: msg || 'unknown error', result: `ERROR: ${msg || 'unknown error'}` });
|
|
@@ -2483,8 +2576,61 @@ export async function createSession(opts) {
|
|
|
2483
2576
|
});
|
|
2484
2577
|
continue;
|
|
2485
2578
|
}
|
|
2579
|
+
const assistantText = visible || content || '';
|
|
2580
|
+
// Recovery fuse: if the model keeps narrating/planning without tool use,
|
|
2581
|
+
// nudge it once with the original task. Never resend more than once per ask().
|
|
2582
|
+
if (looksLikePlanningNarration(assistantText, finishReason)) {
|
|
2583
|
+
noToolTurns += 1;
|
|
2584
|
+
messages.push({ role: 'assistant', content: assistantText });
|
|
2585
|
+
if (noToolTurns >= NO_TOOL_REPROMPT_THRESHOLD) {
|
|
2586
|
+
if (!repromptUsed) {
|
|
2587
|
+
repromptUsed = true;
|
|
2588
|
+
noToolTurns = 0;
|
|
2589
|
+
const reminder = userContentToText(instruction).trim();
|
|
2590
|
+
const clippedReminder = reminder.length > 4000 ? `${reminder.slice(0, 4000)}\n[truncated]` : reminder;
|
|
2591
|
+
messages.push({
|
|
2592
|
+
role: 'user',
|
|
2593
|
+
content: `[system] You seem to be stuck narrating without using tools. Resume execution now.\n` +
|
|
2594
|
+
`Original task:\n${clippedReminder}\n\n` +
|
|
2595
|
+
`Call the needed tools directly. If everything is truly complete, provide the final answer.`
|
|
2596
|
+
});
|
|
2597
|
+
await hookObj.onTurnEnd?.({
|
|
2598
|
+
turn: turns,
|
|
2599
|
+
toolCalls,
|
|
2600
|
+
promptTokens: cumulativeUsage.prompt,
|
|
2601
|
+
completionTokens: cumulativeUsage.completion,
|
|
2602
|
+
promptTokensTurn,
|
|
2603
|
+
completionTokensTurn,
|
|
2604
|
+
ttftMs,
|
|
2605
|
+
ttcMs,
|
|
2606
|
+
ppTps,
|
|
2607
|
+
tgTps,
|
|
2608
|
+
});
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
throw new Error(`no-tool loop detected: model produced planning/narration without tool calls for ${NO_TOOL_REPROMPT_THRESHOLD} turns even after one recovery reprompt`);
|
|
2612
|
+
}
|
|
2613
|
+
messages.push({
|
|
2614
|
+
role: 'user',
|
|
2615
|
+
content: '[system] Continue executing the task. Use tools now (do not just narrate plans). If complete, give the final answer.'
|
|
2616
|
+
});
|
|
2617
|
+
await hookObj.onTurnEnd?.({
|
|
2618
|
+
turn: turns,
|
|
2619
|
+
toolCalls,
|
|
2620
|
+
promptTokens: cumulativeUsage.prompt,
|
|
2621
|
+
completionTokens: cumulativeUsage.completion,
|
|
2622
|
+
promptTokensTurn,
|
|
2623
|
+
completionTokensTurn,
|
|
2624
|
+
ttftMs,
|
|
2625
|
+
ttcMs,
|
|
2626
|
+
ppTps,
|
|
2627
|
+
tgTps,
|
|
2628
|
+
});
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
noToolTurns = 0;
|
|
2486
2632
|
// final assistant message
|
|
2487
|
-
messages.push({ role: 'assistant', content:
|
|
2633
|
+
messages.push({ role: 'assistant', content: assistantText });
|
|
2488
2634
|
await hookObj.onTurnEnd?.({
|
|
2489
2635
|
turn: turns,
|
|
2490
2636
|
toolCalls,
|
|
@@ -2497,7 +2643,7 @@ export async function createSession(opts) {
|
|
|
2497
2643
|
ppTps,
|
|
2498
2644
|
tgTps,
|
|
2499
2645
|
});
|
|
2500
|
-
return { text:
|
|
2646
|
+
return { text: assistantText, turns, toolCalls };
|
|
2501
2647
|
}
|
|
2502
2648
|
const reason = `max iterations exceeded (${maxIters})`;
|
|
2503
2649
|
throw new Error(reason);
|