@visorcraft/idlehands 1.0.9 → 1.1.0
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 +19 -0
- package/dist/agent.js +85 -3
- package/dist/agent.js.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/bot.js +5 -5
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/runtime-cmds.js +134 -12
- package/dist/cli/runtime-cmds.js.map +1 -1
- package/dist/cli/setup.js +15 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/client.js +168 -0
- package/dist/client.js.map +1 -1
- package/dist/history.js +1 -1
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -1
- package/dist/spinner.js +8 -1
- package/dist/spinner.js.map +1 -1
- package/dist/tools.js +10 -2
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Idle Hands is built for people who want an agent that can actually ship work, no
|
|
|
13
13
|
|
|
14
14
|
- **TUI-first UX** for real daily use (streaming output, slash commands, approvals)
|
|
15
15
|
- **Runtime orchestration** (hosts/backends/models) for local + remote model stacks
|
|
16
|
+
- **Size-aware runtime probes** so very large GGUF/RPC models get sane startup timeouts by default
|
|
16
17
|
- **Safety + approvals** with explicit modes (`plan`, `reject`, `default`, `auto-edit`, `yolo`)
|
|
17
18
|
- **Headless mode** for CI and scripts (`json`, `stream-json`, `--fail-on-error`, `--diff-only`)
|
|
18
19
|
- **Bot frontends** (Telegram + Discord) with service management
|
|
@@ -158,6 +159,24 @@ If you use a dedicated `idlehands` account, install/manage the service while log
|
|
|
158
159
|
|
|
159
160
|
---
|
|
160
161
|
|
|
162
|
+
|
|
163
|
+
## Runtime probe defaults (size-aware)
|
|
164
|
+
|
|
165
|
+
When a model does not explicitly set probe timeout and probe interval, Idle Hands derives defaults from estimated model size on the target host.
|
|
166
|
+
|
|
167
|
+
Default tiers used by idlehands select:
|
|
168
|
+
|
|
169
|
+
| Model size (GiB) | probe timeout | probe interval |
|
|
170
|
+
|---:|---:|---:|
|
|
171
|
+
| <= 10 | 120s | 1000ms |
|
|
172
|
+
| <= 40 | 300s | 1200ms |
|
|
173
|
+
| <= 80 | 900s | 2000ms |
|
|
174
|
+
| <= 140 | 3600s | 5000ms |
|
|
175
|
+
| > 140 | 5400s | 5000ms |
|
|
176
|
+
|
|
177
|
+
Per-model override remains available in runtimes.json under models.launch.
|
|
178
|
+
Explicit per-model values always take precedence.
|
|
179
|
+
|
|
161
180
|
## Documentation map
|
|
162
181
|
|
|
163
182
|
- [Getting Started](https://visorcraft.github.io/IdleHands/guide/getting-started)
|
package/dist/agent.js
CHANGED
|
@@ -592,6 +592,14 @@ export function parseToolCallsFromContent(content) {
|
|
|
592
592
|
const xmlCalls = parseXmlToolCalls(trimmed);
|
|
593
593
|
if (xmlCalls?.length)
|
|
594
594
|
return xmlCalls;
|
|
595
|
+
// Case 5: Lightweight function-tag calls (seen in some Qwen content-mode outputs):
|
|
596
|
+
// <function=tool_name>
|
|
597
|
+
// {...json args...}
|
|
598
|
+
// </function>
|
|
599
|
+
// or single-line <function=tool_name>{...}</function>
|
|
600
|
+
const fnTagCalls = parseFunctionTagToolCalls(trimmed);
|
|
601
|
+
if (fnTagCalls?.length)
|
|
602
|
+
return fnTagCalls;
|
|
595
603
|
return null;
|
|
596
604
|
}
|
|
597
605
|
/**
|
|
@@ -1006,8 +1014,51 @@ export async function createSession(opts) {
|
|
|
1006
1014
|
sessionMeta += `\n\n[Sub-agents] spawn_task is available (isolated context, sequential queue, default max_iterations=${subMaxIter}).`;
|
|
1007
1015
|
}
|
|
1008
1016
|
// Harness-driven suffix: append to first user message (NOT system prompt — §9b KV cache rule)
|
|
1017
|
+
// Check if model needs content-mode tool calls (known incompatible templates)
|
|
1018
|
+
// This runs before harness checks so it works regardless of quirk flags.
|
|
1019
|
+
{
|
|
1020
|
+
const modelName = cfg.model ?? '';
|
|
1021
|
+
const { OpenAIClient: OAIClient } = await import('./client.js');
|
|
1022
|
+
if (!client.contentModeToolCalls && OAIClient.needsContentMode(modelName)) {
|
|
1023
|
+
client.contentModeToolCalls = true;
|
|
1024
|
+
client.recordKnownPatternMatch();
|
|
1025
|
+
if (cfg.verbose) {
|
|
1026
|
+
console.warn(`[info] Model "${modelName}" matched known content-mode pattern — using content-based tool calls`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1009
1030
|
if (harness.quirks.needsExplicitToolCallFormatReminder) {
|
|
1010
|
-
|
|
1031
|
+
if (client.contentModeToolCalls) {
|
|
1032
|
+
// In content mode, tell the model to use JSON tool calls in its output
|
|
1033
|
+
sessionMeta += '\n\nYou have access to the following tools. To call a tool, output a JSON block in your response like this:\n```json\n{"name": "tool_name", "arguments": {"param": "value"}}\n```\nAvailable tools:\n';
|
|
1034
|
+
const toolSchemas = getToolsSchema();
|
|
1035
|
+
for (const t of toolSchemas) {
|
|
1036
|
+
const fn = t.function;
|
|
1037
|
+
if (fn) {
|
|
1038
|
+
const params = fn.parameters?.properties
|
|
1039
|
+
? Object.entries(fn.parameters.properties).map(([k, v]) => `${k}: ${v.type ?? 'any'}`).join(', ')
|
|
1040
|
+
: '';
|
|
1041
|
+
sessionMeta += `- ${fn.name}(${params}): ${fn.description ?? ''}\n`;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
sessionMeta += '\nIMPORTANT: Output tool calls as JSON blocks in your message. Do NOT use the tool_calls API mechanism.\nIf you use XML/function tags (e.g. <function=name>), include a full JSON object of arguments between braces.';
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
sessionMeta += '\n\nIMPORTANT: Use the tool_calls mechanism to invoke tools. Do NOT write JSON tool invocations in your message text.';
|
|
1048
|
+
}
|
|
1049
|
+
// One-time tool-call template smoke test (first ask() call only, skip in content mode)
|
|
1050
|
+
if (!client.contentModeToolCalls && !client.__toolCallSmokeTested) {
|
|
1051
|
+
client.__toolCallSmokeTested = true;
|
|
1052
|
+
try {
|
|
1053
|
+
const smokeErr = await client.smokeTestToolCalls(cfg.model ?? 'default');
|
|
1054
|
+
if (smokeErr) {
|
|
1055
|
+
console.error(`\x1b[33m[warn] Tool-call smoke test failed: ${smokeErr}\x1b[0m`);
|
|
1056
|
+
console.error(`\x1b[33m This model/server may not support tool-call replay correctly.\x1b[0m`);
|
|
1057
|
+
console.error(`\x1b[33m Consider using a different model or updating llama.cpp.\x1b[0m`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
catch { }
|
|
1061
|
+
}
|
|
1011
1062
|
}
|
|
1012
1063
|
if (harness.systemPromptSuffix) {
|
|
1013
1064
|
sessionMeta += '\n\n' + harness.systemPromptSuffix;
|
|
@@ -2212,14 +2263,19 @@ export async function createSession(opts) {
|
|
|
2212
2263
|
consecutiveCounts.set(sig, 1);
|
|
2213
2264
|
}
|
|
2214
2265
|
const consec = consecutiveCounts.get(sig) ?? 1;
|
|
2215
|
-
if (consec >=
|
|
2266
|
+
if (consec >= 3) {
|
|
2216
2267
|
const args = sig.slice(toolName.length + 1);
|
|
2217
2268
|
const argsPreview = args.length > 220 ? args.slice(0, 220) + '…' : args;
|
|
2218
2269
|
messages.push({
|
|
2219
2270
|
role: 'user',
|
|
2220
|
-
content: `[System] You have read the same resource ${consec} consecutive times (${toolName} ${argsPreview}). The content has
|
|
2271
|
+
content: `[System] STOP READING: You have read the same resource ${consec} consecutive times (${toolName} ${argsPreview}). The content has NOT changed. You already have this data. Proceed immediately with your next action (write_file, edit_file, exec, etc.) — do NOT read this resource again.`,
|
|
2221
2272
|
});
|
|
2222
2273
|
}
|
|
2274
|
+
// Hard-break: after 6 consecutive identical reads, stop the session
|
|
2275
|
+
if (consec >= 6) {
|
|
2276
|
+
throw new Error(`tool ${toolName}: identical read repeated ${consec}x consecutively; breaking loop. ` +
|
|
2277
|
+
`The resource content has not changed between reads.`);
|
|
2278
|
+
}
|
|
2223
2279
|
continue;
|
|
2224
2280
|
}
|
|
2225
2281
|
// Default behavior for mutating/other tools: break on repeated identical signature.
|
|
@@ -2821,4 +2877,30 @@ async function autoPickModel(client, cached) {
|
|
|
2821
2877
|
clearTimeout(timer);
|
|
2822
2878
|
}
|
|
2823
2879
|
}
|
|
2880
|
+
function parseFunctionTagToolCalls(content) {
|
|
2881
|
+
const m = content.match(/<function=([\w.-]+)>([\s\S]*?)<\/function>/i);
|
|
2882
|
+
if (!m)
|
|
2883
|
+
return null;
|
|
2884
|
+
const name = m[1];
|
|
2885
|
+
const body = (m[2] ?? '').trim();
|
|
2886
|
+
// If body contains JSON object, use it as arguments; else empty object.
|
|
2887
|
+
let args = '{}';
|
|
2888
|
+
const jsonStart = body.indexOf('{');
|
|
2889
|
+
const jsonEnd = body.lastIndexOf('}');
|
|
2890
|
+
if (jsonStart !== -1 && jsonEnd > jsonStart) {
|
|
2891
|
+
const sub = body.slice(jsonStart, jsonEnd + 1);
|
|
2892
|
+
try {
|
|
2893
|
+
JSON.parse(sub);
|
|
2894
|
+
args = sub;
|
|
2895
|
+
}
|
|
2896
|
+
catch {
|
|
2897
|
+
// keep {}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
return [{
|
|
2901
|
+
id: 'call_0',
|
|
2902
|
+
type: 'function',
|
|
2903
|
+
function: { name, arguments: args }
|
|
2904
|
+
}];
|
|
2905
|
+
}
|
|
2824
2906
|
//# sourceMappingURL=agent.js.map
|