@visorcraft/idlehands 1.3.2 → 1.3.4
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/README.md +28 -1
- package/dist/agent.js +36 -1
- package/dist/agent.js.map +1 -1
- package/dist/anton/controller.js +104 -4
- package/dist/anton/controller.js.map +1 -1
- package/dist/anton/prompt.js +7 -2
- package/dist/anton/prompt.js.map +1 -1
- package/dist/anton/reporter.js +35 -3
- package/dist/anton/reporter.js.map +1 -1
- package/dist/anton/session.js +2 -2
- package/dist/anton/session.js.map +1 -1
- package/dist/anton/verifier.js +86 -21
- package/dist/anton/verifier.js.map +1 -1
- package/dist/bot/auto-continue.js +24 -0
- package/dist/bot/auto-continue.js.map +1 -0
- package/dist/bot/commands.js +47 -1
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/discord.js +145 -9
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/telegram.js +92 -5
- package/dist/bot/telegram.js.map +1 -1
- package/dist/bot/ux/events.js +142 -0
- package/dist/bot/ux/events.js.map +1 -0
- package/dist/cli/commands/anton.js +12 -1
- package/dist/cli/commands/anton.js.map +1 -1
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -1
- package/dist/tools.js +52 -6
- package/dist/tools.js.map +1 -1
- package/dist/tui/controller.js +198 -1
- package/dist/tui/controller.js.map +1 -1
- package/dist/tui/render.js +45 -1
- package/dist/tui/render.js.map +1 -1
- package/dist/tui/state.js +53 -0
- package/dist/tui/state.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,10 +25,12 @@ Idle Hands is built for people who want an agent that can actually ship work, no
|
|
|
25
25
|
|
|
26
26
|
Trifecta is the integrated core of Idle Hands:
|
|
27
27
|
|
|
28
|
-
- **Vault** → persistent memory + notes (`/vault`, `/note`, `/notes`)
|
|
28
|
+
- **Vault** → persistent memory + notes (`/vault`, `/note`, `/notes`) + automatic turn action summaries
|
|
29
29
|
- **Replay** → file checkpoints and rewind/diff (`/checkpoints`, `/rewind`, `/diff`, `/undo`)
|
|
30
30
|
- **Lens** → structural compression/indexing for better context usage
|
|
31
31
|
|
|
32
|
+
Every tool-using turn is automatically summarized and persisted to the Vault, so the model can recall what it did — even after context compaction drops earlier messages. This is especially useful for local models with limited context windows.
|
|
33
|
+
|
|
32
34
|
Runtime controls:
|
|
33
35
|
|
|
34
36
|
```bash
|
|
@@ -222,6 +224,31 @@ idlehands health --scan-ports 8000-8100
|
|
|
222
224
|
idlehands health --scan-ports 8080,8081,9000
|
|
223
225
|
```
|
|
224
226
|
|
|
227
|
+
## Tool loop auto-continue
|
|
228
|
+
|
|
229
|
+
When the agent hits a critical tool loop (repeated identical tool calls with no progress), Idle Hands now automatically retries instead of stopping and waiting for a manual "Continue" message.
|
|
230
|
+
|
|
231
|
+
- **All surfaces**: TUI, Telegram bot, Discord bot, and Anton all support auto-continue
|
|
232
|
+
- **User notification**: each retry sends a visible notice with error details and attempt count
|
|
233
|
+
- **Configurable**: up to 5 retries by default (max 10), fully disableable
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"tool_loop_auto_continue": {
|
|
238
|
+
"enabled": true,
|
|
239
|
+
"max_retries": 5
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
On each retry the user sees:
|
|
245
|
+
> ⚠️ Tool loop detected: {error details}
|
|
246
|
+
> 🔄 Automatically continuing the task. (retry 1 of 5)
|
|
247
|
+
|
|
248
|
+
After exhausting all retries, the error surfaces normally. Anton handles this internally without orchestrator involvement.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
225
252
|
## Documentation map
|
|
226
253
|
|
|
227
254
|
- [Getting Started](https://visorcraft.github.io/IdleHands/guide/getting-started)
|
package/dist/agent.js
CHANGED
|
@@ -1879,6 +1879,26 @@ export async function createSession(opts) {
|
|
|
1879
1879
|
};
|
|
1880
1880
|
const finalizeAsk = async (text) => {
|
|
1881
1881
|
const finalText = ensureInformativeAssistantText(text, { toolCalls, turns });
|
|
1882
|
+
// Auto-persist turn action summary to Vault so the model can recall what it did.
|
|
1883
|
+
if (vault && toolCalls > 0) {
|
|
1884
|
+
try {
|
|
1885
|
+
const actions = [];
|
|
1886
|
+
for (const [callId, name] of toolNameByCallId) {
|
|
1887
|
+
const args = toolArgsByCallId.get(callId) ?? {};
|
|
1888
|
+
actions.push(planModeSummary(name, args));
|
|
1889
|
+
}
|
|
1890
|
+
if (actions.length) {
|
|
1891
|
+
const userPromptSnippet = rawInstructionText.length > 120
|
|
1892
|
+
? rawInstructionText.slice(0, 120) + '…'
|
|
1893
|
+
: rawInstructionText;
|
|
1894
|
+
const summary = `User asked: ${userPromptSnippet}\nActions (${actions.length} tool calls, ${turns} turns):\n${actions.map(a => `- ${a}`).join('\n')}\nResult: ${finalText.length > 200 ? finalText.slice(0, 200) + '…' : finalText}`;
|
|
1895
|
+
await vault.upsertNote(`turn_summary_${askId}`, summary, 'system');
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
catch {
|
|
1899
|
+
// best-effort — never block ask completion for summary persistence
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1882
1902
|
await hookManager.emit('ask_end', { askId, text: finalText, turns, toolCalls });
|
|
1883
1903
|
return { text: finalText, turns, toolCalls };
|
|
1884
1904
|
};
|
|
@@ -2192,6 +2212,7 @@ export async function createSession(opts) {
|
|
|
2192
2212
|
}
|
|
2193
2213
|
}
|
|
2194
2214
|
messages = compacted;
|
|
2215
|
+
let summaryUsed = false;
|
|
2195
2216
|
if (dropped.length) {
|
|
2196
2217
|
const droppedTokens = estimateTokensFromMessages(dropped);
|
|
2197
2218
|
if (cfg.compact_summary !== false && droppedTokens > 200) {
|
|
@@ -2210,6 +2231,7 @@ export async function createSession(opts) {
|
|
|
2210
2231
|
});
|
|
2211
2232
|
const summary = resp.choices?.[0]?.message?.content ?? '';
|
|
2212
2233
|
if (summary.trim()) {
|
|
2234
|
+
summaryUsed = true;
|
|
2213
2235
|
messages.push({
|
|
2214
2236
|
role: 'system',
|
|
2215
2237
|
content: `[Compacted ${dropped.length} messages (~${droppedTokens} tokens). Progress summary:]\n${summary.trim()}\n[Continue from where you left off. Do not repeat completed work.]`,
|
|
@@ -2230,10 +2252,19 @@ export async function createSession(opts) {
|
|
|
2230
2252
|
// Update token count AFTER injections so downstream reads are accurate
|
|
2231
2253
|
currentContextTokens = estimateTokensFromMessages(messages);
|
|
2232
2254
|
const afterTokens = estimateTokensFromMessages(compacted);
|
|
2255
|
+
const freedTokens = Math.max(0, beforeTokens - afterTokens);
|
|
2256
|
+
// Emit compaction event for callers (e.g. Anton controller → Discord)
|
|
2257
|
+
if (dropped.length) {
|
|
2258
|
+
try {
|
|
2259
|
+
await hookObj.onCompaction?.({ droppedMessages: dropped.length, freedTokens, summaryUsed });
|
|
2260
|
+
}
|
|
2261
|
+
catch { /* best effort */ }
|
|
2262
|
+
console.error(`[compaction] auto: dropped=${dropped.length} msgs, freed=~${freedTokens} tokens, summary=${summaryUsed}, remaining=${messages.length} msgs (~${currentContextTokens} tokens)`);
|
|
2263
|
+
}
|
|
2233
2264
|
return {
|
|
2234
2265
|
beforeMessages: beforeMsgs.length,
|
|
2235
2266
|
afterMessages: compacted.length,
|
|
2236
|
-
freedTokens
|
|
2267
|
+
freedTokens,
|
|
2237
2268
|
archivedToolMessages: dropped.filter((m) => m.role === 'tool').length,
|
|
2238
2269
|
droppedMessages: dropped.length,
|
|
2239
2270
|
dryRun: false,
|
|
@@ -2579,6 +2610,8 @@ export async function createSession(opts) {
|
|
|
2579
2610
|
const warningKey = `${warning.level}:${warning.detector}:${detected.signature}`;
|
|
2580
2611
|
if (!toolLoopWarningKeys.has(warningKey)) {
|
|
2581
2612
|
toolLoopWarningKeys.add(warningKey);
|
|
2613
|
+
const argsSnippet = JSON.stringify(args).slice(0, 300);
|
|
2614
|
+
console.error(`[tool-loop] ${warning.level}: ${warning.toolName} (${warning.detector}, count=${warning.count}) args=${argsSnippet}`);
|
|
2582
2615
|
await emitToolLoop({
|
|
2583
2616
|
level: warning.level,
|
|
2584
2617
|
detector: warning.detector,
|
|
@@ -2748,6 +2781,7 @@ export async function createSession(opts) {
|
|
|
2748
2781
|
lastTurnSigs = turnSigs;
|
|
2749
2782
|
if (shouldForceToollessRecovery) {
|
|
2750
2783
|
if (!toollessRecoveryUsed) {
|
|
2784
|
+
console.error(`[tool-loop] Disabling tools for one recovery turn (turn=${turns})`);
|
|
2751
2785
|
forceToollessRecoveryTurn = true;
|
|
2752
2786
|
toollessRecoveryUsed = true;
|
|
2753
2787
|
messages.push({
|
|
@@ -2768,6 +2802,7 @@ export async function createSession(opts) {
|
|
|
2768
2802
|
});
|
|
2769
2803
|
continue;
|
|
2770
2804
|
}
|
|
2805
|
+
console.error(`[tool-loop] Recovery failed — model resumed looping after tools-disabled turn (turn=${turns})`);
|
|
2771
2806
|
throw new AgentLoopBreak('critical tool-loop persisted after one tools-disabled recovery turn. Stopping to avoid infinite loop.');
|
|
2772
2807
|
}
|
|
2773
2808
|
const runOne = async (tc) => {
|