@visorcraft/idlehands 2.2.3 → 2.2.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.
Files changed (44) hide show
  1. package/dist/agent.js +241 -14
  2. package/dist/agent.js.map +1 -1
  3. package/dist/anton/verifier-utils.js +75 -3
  4. package/dist/anton/verifier-utils.js.map +1 -1
  5. package/dist/anton/verifier.js +2 -1
  6. package/dist/anton/verifier.js.map +1 -1
  7. package/dist/bot/anton-run.js +1 -0
  8. package/dist/bot/anton-run.js.map +1 -1
  9. package/dist/bot/basic-commands.js +2 -1
  10. package/dist/bot/basic-commands.js.map +1 -1
  11. package/dist/bot/command-logic.js +1 -1
  12. package/dist/bot/command-logic.js.map +1 -1
  13. package/dist/bot/commands.js +79 -1
  14. package/dist/bot/commands.js.map +1 -1
  15. package/dist/bot/discord-commands.js +121 -18
  16. package/dist/bot/discord-commands.js.map +1 -1
  17. package/dist/bot/discord.js +86 -6
  18. package/dist/bot/discord.js.map +1 -1
  19. package/dist/bot/runtime-model-picker.js +77 -0
  20. package/dist/bot/runtime-model-picker.js.map +1 -0
  21. package/dist/bot/session-settings.js +28 -30
  22. package/dist/bot/session-settings.js.map +1 -1
  23. package/dist/bot/telegram-commands.js +165 -36
  24. package/dist/bot/telegram-commands.js.map +1 -1
  25. package/dist/bot/telegram.js +6 -1
  26. package/dist/bot/telegram.js.map +1 -1
  27. package/dist/bot/ux/events.js.map +1 -1
  28. package/dist/bot/ux/progress-to-events.js +11 -0
  29. package/dist/bot/ux/progress-to-events.js.map +1 -1
  30. package/dist/cli/commands/anton.js +3 -0
  31. package/dist/cli/commands/anton.js.map +1 -1
  32. package/dist/cli/commands/editing.js +26 -0
  33. package/dist/cli/commands/editing.js.map +1 -1
  34. package/dist/cli/commands/session.js +1 -1
  35. package/dist/cli/commands/session.js.map +1 -1
  36. package/dist/config.js +134 -0
  37. package/dist/config.js.map +1 -1
  38. package/dist/routing/mode.js +32 -0
  39. package/dist/routing/mode.js.map +1 -0
  40. package/dist/routing/turn-router.js +128 -0
  41. package/dist/routing/turn-router.js.map +1 -0
  42. package/dist/runtime/template-check.js +18 -0
  43. package/dist/runtime/template-check.js.map +1 -1
  44. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -14,6 +14,7 @@ import { buildDefaultSystemPrompt } from './agent/prompt-builder.js';
14
14
  import { LeakDetector } from './security/leak-detector.js';
15
15
  import { PromptGuard } from './security/prompt-guard.js';
16
16
  import { ResponseCache } from './agent/response-cache.js';
17
+ import { resilientCall } from './agent/resilient-provider.js';
17
18
  import { ToolLoopGuard } from './agent/tool-loop-guard.js';
18
19
  import { isLspTool, isMutationTool, isReadOnlyTool, planModeSummary } from './agent/tool-policy.js';
19
20
  import { buildToolsSchema } from './agent/tools-schema.js';
@@ -31,6 +32,7 @@ import { MCPManager } from './mcp.js';
31
32
  import { BASE_MAX_TOKENS, deriveContextWindow, deriveGenerationParams, supportsVisionModel, } from './model-customization.js';
32
33
  import { ReplayStore } from './replay.js';
33
34
  import { checkExecSafety, checkPathSafety } from './safety.js';
35
+ import { decideTurnRoute } from './routing/turn-router.js';
34
36
  import { normalizeApprovalMode } from './shared/config-utils.js';
35
37
  import { collectSnapshot } from './sys/context.js';
36
38
  import { ToolError, ValidationError } from './tools/tool-error.js';
@@ -1078,6 +1080,9 @@ export async function createSession(opts) {
1078
1080
  let captureEnabled = false;
1079
1081
  let capturePath;
1080
1082
  let lastCaptureRecord = null;
1083
+ const routedClients = new Map();
1084
+ const probedEndpoints = new Set();
1085
+ const normalizeEndpoint = (endpoint) => endpoint.trim().replace(/\/+$/, '');
1081
1086
  const defaultCapturePath = () => {
1082
1087
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1083
1088
  return path.join(capturesDir, `${stamp}.jsonl`);
@@ -1086,18 +1091,135 @@ export async function createSession(opts) {
1086
1091
  await fs.mkdir(path.dirname(outPath), { recursive: true });
1087
1092
  await fs.appendFile(outPath, JSON.stringify(record) + '\n', 'utf8');
1088
1093
  };
1089
- const wireCaptureHook = () => {
1090
- if (typeof client.setExchangeHook !== 'function')
1094
+ const applyClientRuntimeOptions = (target) => {
1095
+ if (typeof target.setVerbose === 'function') {
1096
+ target.setVerbose(cfg.verbose);
1097
+ }
1098
+ if (typeof cfg.response_timeout === 'number' && cfg.response_timeout > 0) {
1099
+ target.setResponseTimeout(cfg.response_timeout);
1100
+ }
1101
+ if (typeof target.setConnectionTimeout === 'function' &&
1102
+ typeof cfg.connection_timeout === 'number' &&
1103
+ cfg.connection_timeout > 0) {
1104
+ target.setConnectionTimeout(cfg.connection_timeout);
1105
+ }
1106
+ if (typeof target.setInitialConnectionCheck === 'function' &&
1107
+ typeof cfg.initial_connection_check === 'boolean') {
1108
+ target.setInitialConnectionCheck(cfg.initial_connection_check);
1109
+ }
1110
+ if (typeof target.setInitialConnectionProbeTimeout === 'function' &&
1111
+ typeof cfg.initial_connection_timeout === 'number' &&
1112
+ cfg.initial_connection_timeout > 0) {
1113
+ target.setInitialConnectionProbeTimeout(cfg.initial_connection_timeout);
1114
+ }
1115
+ };
1116
+ const attachCaptureHook = (target) => {
1117
+ if (typeof target.setExchangeHook !== 'function')
1091
1118
  return;
1092
- client.setExchangeHook(async (record) => {
1119
+ target.setExchangeHook(async (record) => {
1093
1120
  lastCaptureRecord = record;
1094
1121
  if (!captureEnabled)
1095
1122
  return;
1096
- const target = capturePath || defaultCapturePath();
1097
- capturePath = target;
1098
- await appendCaptureRecord(record, target);
1123
+ const outFile = capturePath || defaultCapturePath();
1124
+ capturePath = outFile;
1125
+ await appendCaptureRecord(record, outFile);
1099
1126
  });
1100
1127
  };
1128
+ const getClientForEndpoint = (endpoint) => {
1129
+ if (!endpoint)
1130
+ return client;
1131
+ const normalized = normalizeEndpoint(endpoint);
1132
+ if (!normalized || normalized === normalizeEndpoint(cfg.endpoint))
1133
+ return client;
1134
+ const existing = routedClients.get(normalized);
1135
+ if (existing)
1136
+ return existing;
1137
+ const routed = new OpenAIClient(normalized, opts.apiKey, cfg.verbose);
1138
+ applyClientRuntimeOptions(routed);
1139
+ attachCaptureHook(routed);
1140
+ routedClients.set(normalized, routed);
1141
+ return routed;
1142
+ };
1143
+ let runtimeRoutingModules = null;
1144
+ let runtimeRoutingUnavailable = false;
1145
+ let runtimeModelIdsCache = null;
1146
+ const loadRuntimeRoutingModules = async () => {
1147
+ if (runtimeRoutingUnavailable)
1148
+ return null;
1149
+ if (runtimeRoutingModules)
1150
+ return runtimeRoutingModules;
1151
+ try {
1152
+ const [planner, executor, store] = await Promise.all([
1153
+ import('./runtime/planner.js'),
1154
+ import('./runtime/executor.js'),
1155
+ import('./runtime/store.js'),
1156
+ ]);
1157
+ runtimeRoutingModules = { planner, executor, store };
1158
+ return runtimeRoutingModules;
1159
+ }
1160
+ catch {
1161
+ runtimeRoutingUnavailable = true;
1162
+ return null;
1163
+ }
1164
+ };
1165
+ const loadRuntimeModelIds = async () => {
1166
+ if (runtimeModelIdsCache)
1167
+ return runtimeModelIdsCache;
1168
+ const mods = await loadRuntimeRoutingModules();
1169
+ if (!mods) {
1170
+ runtimeModelIdsCache = new Set();
1171
+ return runtimeModelIdsCache;
1172
+ }
1173
+ try {
1174
+ const runtimes = await mods.store.loadRuntimes();
1175
+ runtimeModelIdsCache = new Set(runtimes.models.filter((m) => m.enabled !== false).map((m) => m.id));
1176
+ return runtimeModelIdsCache;
1177
+ }
1178
+ catch {
1179
+ runtimeModelIdsCache = new Set();
1180
+ return runtimeModelIdsCache;
1181
+ }
1182
+ };
1183
+ const ensureRuntimeModelActive = async (runtimeModelId) => {
1184
+ const mods = await loadRuntimeRoutingModules();
1185
+ if (!mods)
1186
+ throw new Error('Runtime routing is unavailable in this build/environment');
1187
+ const runtimes = await mods.store.loadRuntimes();
1188
+ runtimeModelIdsCache = new Set(runtimes.models.filter((m) => m.enabled !== false).map((m) => m.id));
1189
+ const modelExists = runtimes.models.some((m) => m.enabled !== false && m.id === runtimeModelId);
1190
+ if (!modelExists) {
1191
+ throw new Error(`Runtime model not found or disabled: ${runtimeModelId}`);
1192
+ }
1193
+ let active = await mods.executor.loadActiveRuntime();
1194
+ if (active?.healthy && active.modelId === runtimeModelId && active.endpoint) {
1195
+ if (normalizeEndpoint(active.endpoint) !== normalizeEndpoint(cfg.endpoint)) {
1196
+ await setEndpoint(active.endpoint);
1197
+ }
1198
+ return;
1199
+ }
1200
+ const planResult = mods.planner.plan({ modelId: runtimeModelId, mode: 'live' }, runtimes, active);
1201
+ if (!planResult.ok) {
1202
+ throw new Error(`Runtime switch plan failed for ${runtimeModelId}: ${planResult.reason}`);
1203
+ }
1204
+ if (!planResult.reuse) {
1205
+ const execResult = await mods.executor.execute(planResult, {
1206
+ confirm: async () => false,
1207
+ });
1208
+ if (!execResult.ok) {
1209
+ throw new Error(`Runtime switch failed for ${runtimeModelId}: ${execResult.error ?? 'unknown error'}`);
1210
+ }
1211
+ }
1212
+ active = await mods.executor.loadActiveRuntime();
1213
+ if (!active?.endpoint || active.healthy !== true) {
1214
+ throw new Error(`Runtime did not become healthy for ${runtimeModelId}`);
1215
+ }
1216
+ if (normalizeEndpoint(active.endpoint) !== normalizeEndpoint(cfg.endpoint)) {
1217
+ await setEndpoint(active.endpoint);
1218
+ }
1219
+ };
1220
+ const wireCaptureHook = () => {
1221
+ attachCaptureHook(client);
1222
+ };
1101
1223
  wireCaptureHook();
1102
1224
  const replayEnabled = cfg.trifecta?.enabled !== false && cfg.trifecta?.replay?.enabled !== false;
1103
1225
  const replay = replayEnabled ? (opts.runtime?.replay ?? new ReplayStore()) : undefined;
@@ -1228,9 +1350,9 @@ export async function createSession(opts) {
1228
1350
  else {
1229
1351
  client = new OpenAIClient(normalized, opts.apiKey, cfg.verbose);
1230
1352
  }
1231
- if (typeof client.setVerbose === 'function') {
1232
- client.setVerbose(cfg.verbose);
1233
- }
1353
+ applyClientRuntimeOptions(client);
1354
+ routedClients.clear();
1355
+ probedEndpoints.clear();
1234
1356
  wireCaptureHook();
1235
1357
  modelsList = normalizeModelsResponse(await client.models());
1236
1358
  const chosen = modelName?.trim()
@@ -1288,6 +1410,7 @@ export async function createSession(opts) {
1288
1410
  const close = async () => {
1289
1411
  await mcpManager?.close().catch(() => { });
1290
1412
  await lspManager?.close().catch(() => { });
1413
+ routedClients.clear();
1291
1414
  vault?.close();
1292
1415
  lens?.close();
1293
1416
  };
@@ -1575,6 +1698,9 @@ export async function createSession(opts) {
1575
1698
  if (typeof client.probeConnection === 'function') {
1576
1699
  await client.probeConnection();
1577
1700
  initialConnectionProbeDone = true;
1701
+ if (typeof client.getEndpoint === 'function') {
1702
+ probedEndpoints.add(normalizeEndpoint(client.getEndpoint()));
1703
+ }
1578
1704
  }
1579
1705
  }
1580
1706
  if (retrievalRequested) {
@@ -1621,6 +1747,38 @@ export async function createSession(opts) {
1621
1747
  });
1622
1748
  return await finalizeAsk(miss);
1623
1749
  }
1750
+ const turnRoute = decideTurnRoute(cfg, rawInstructionText, model);
1751
+ const primaryRoute = turnRoute.providerTargets[0];
1752
+ const runtimeModelIds = await loadRuntimeModelIds();
1753
+ const routeRuntimeFallbackModels = (primaryRoute?.fallbackModels ?? []).filter((m) => runtimeModelIds.has(m));
1754
+ const routeApiFallbackModels = (primaryRoute?.fallbackModels ?? []).filter((m) => !runtimeModelIds.has(m));
1755
+ const primaryUsesRuntimeModel = !!primaryRoute?.model && runtimeModelIds.has(primaryRoute.model);
1756
+ // Non-runtime route models can be selected directly in-session.
1757
+ if (!primaryUsesRuntimeModel && primaryRoute?.model && primaryRoute.model !== model) {
1758
+ setModel(primaryRoute.model);
1759
+ }
1760
+ if (cfg.verbose) {
1761
+ const routeParts = [
1762
+ `requested=${turnRoute.requestedMode}`,
1763
+ `selected=${turnRoute.selectedMode}`,
1764
+ `source=${turnRoute.selectedModeSource}`,
1765
+ `hint=${turnRoute.classificationHint ?? 'none'}`,
1766
+ `provider=${primaryRoute?.name ?? 'default'}`,
1767
+ `model=${primaryRoute?.model ?? model}`,
1768
+ ];
1769
+ if (turnRoute.heuristicDecision)
1770
+ routeParts.push(`heuristic=${turnRoute.heuristicDecision}`);
1771
+ if (primaryUsesRuntimeModel) {
1772
+ const runtimeChain = [primaryRoute?.model, ...routeRuntimeFallbackModels]
1773
+ .filter(Boolean)
1774
+ .join(' -> ');
1775
+ routeParts.push(`runtime_chain=${runtimeChain || 'none'}`);
1776
+ }
1777
+ else if (routeApiFallbackModels.length) {
1778
+ routeParts.push(`api_fallbacks=${routeApiFallbackModels.join(',')}`);
1779
+ }
1780
+ console.error(`[routing] ${routeParts.join(' ')}`);
1781
+ }
1624
1782
  const persistReviewArtifact = async (finalText) => {
1625
1783
  if (!vault || !shouldPersistReviewArtifact)
1626
1784
  return;
@@ -2045,8 +2203,7 @@ export async function createSession(opts) {
2045
2203
  }
2046
2204
  }
2047
2205
  if (!resp) {
2048
- resp = await client.chatStream({
2049
- model,
2206
+ const chatOptsBase = {
2050
2207
  messages,
2051
2208
  tools: toolsForTurn,
2052
2209
  tool_choice: toolChoiceForTurn,
@@ -2055,9 +2212,10 @@ export async function createSession(opts) {
2055
2212
  max_tokens: maxTokens,
2056
2213
  extra: {
2057
2214
  cache_prompt: cfg.cache_prompt ?? true,
2058
- // Speculative decoding: draft model params for llama-server
2059
2215
  ...(cfg.draft_model ? { draft_model: cfg.draft_model } : {}),
2060
- ...(cfg.draft_n ? { speculative: { n: cfg.draft_n, p_min: cfg.draft_p_min ?? 0.5 } } : {}),
2216
+ ...(cfg.draft_n
2217
+ ? { speculative: { n: cfg.draft_n, p_min: cfg.draft_p_min ?? 0.5 } }
2218
+ : {}),
2061
2219
  ...(frequencyPenalty && { frequency_penalty: frequencyPenalty }),
2062
2220
  ...(presencePenalty && { presence_penalty: presencePenalty }),
2063
2221
  },
@@ -2065,7 +2223,73 @@ export async function createSession(opts) {
2065
2223
  requestId: `r${reqCounter}`,
2066
2224
  onToken: hookObj.onToken,
2067
2225
  onFirstDelta,
2068
- });
2226
+ };
2227
+ if (primaryUsesRuntimeModel && primaryRoute?.model) {
2228
+ // Runtime-native routing: lane model/fallbacks reference runtime model IDs.
2229
+ const runtimePrimaryModel = primaryRoute.model;
2230
+ const runtimeFallbackMap = {};
2231
+ if (routeRuntimeFallbackModels.length > 0) {
2232
+ runtimeFallbackMap[runtimePrimaryModel] = routeRuntimeFallbackModels;
2233
+ }
2234
+ resp = await resilientCall([
2235
+ {
2236
+ name: 'runtime-router',
2237
+ execute: async (runtimeModelId) => {
2238
+ await ensureRuntimeModelActive(runtimeModelId);
2239
+ const runtimeClient = getClientForEndpoint();
2240
+ const runtimeModel = model;
2241
+ return runtimeClient.chatStream({ ...chatOptsBase, model: runtimeModel });
2242
+ },
2243
+ },
2244
+ ], runtimePrimaryModel, {
2245
+ maxRetries: 0,
2246
+ modelFallbacks: runtimeFallbackMap,
2247
+ onRetry: (info) => {
2248
+ if (cfg.verbose) {
2249
+ console.error(`[routing] runtime-fallback: model=${info.model} attempt=${info.attempt}/${info.maxAttempts} reason=${info.reason}`);
2250
+ }
2251
+ },
2252
+ });
2253
+ }
2254
+ else {
2255
+ const routeEndpoint = primaryRoute?.endpoint;
2256
+ const activeClient = getClientForEndpoint(routeEndpoint);
2257
+ const endpointKey = routeEndpoint ? normalizeEndpoint(routeEndpoint) : undefined;
2258
+ if (endpointKey && !probedEndpoints.has(endpointKey)) {
2259
+ if (typeof activeClient.probeConnection === 'function') {
2260
+ try {
2261
+ await activeClient.probeConnection();
2262
+ }
2263
+ catch {
2264
+ // best-effort: if probe fails we still try the call
2265
+ }
2266
+ probedEndpoints.add(endpointKey);
2267
+ }
2268
+ }
2269
+ const routeModel = primaryRoute?.model ?? model;
2270
+ if (routeApiFallbackModels.length > 0) {
2271
+ const modelFallbackMap = {
2272
+ [routeModel]: routeApiFallbackModels,
2273
+ };
2274
+ resp = await resilientCall([
2275
+ {
2276
+ name: primaryRoute?.name ?? 'default',
2277
+ execute: (m) => activeClient.chatStream({ ...chatOptsBase, model: m }),
2278
+ },
2279
+ ], routeModel, {
2280
+ maxRetries: 1,
2281
+ modelFallbacks: modelFallbackMap,
2282
+ onRetry: (info) => {
2283
+ if (cfg.verbose) {
2284
+ console.error(`[routing] retry: provider=${info.provider} model=${info.model} attempt=${info.attempt}/${info.maxAttempts} reason=${info.reason}`);
2285
+ }
2286
+ },
2287
+ });
2288
+ }
2289
+ else {
2290
+ resp = await activeClient.chatStream({ ...chatOptsBase, model: routeModel });
2291
+ }
2292
+ }
2069
2293
  } // end if (!resp) — cache miss path
2070
2294
  // Successful response resets overflow recovery budget.
2071
2295
  overflowCompactionAttempts = 0;
@@ -3533,6 +3757,9 @@ export async function createSession(opts) {
3533
3757
  refreshServerHealth,
3534
3758
  getPerfSummary,
3535
3759
  getToolLoopStats: () => lastToolLoopStats,
3760
+ get lastAskInstructionText() {
3761
+ return lastAskInstructionText;
3762
+ },
3536
3763
  captureOn,
3537
3764
  captureOff,
3538
3765
  captureLast,