@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 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
- sessionMeta += '\n\nIMPORTANT: Use the tool_calls mechanism to invoke tools. Do NOT write JSON tool invocations in your message text.';
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 >= 4) {
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 not changed. Please proceed with your task using the information you already have.`,
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