@visorcraft/idlehands 1.1.6 → 1.1.7
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 +130 -29
- package/dist/agent.js.map +1 -1
- package/dist/cli/args.js +4 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/session.js +110 -1
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/setup.js +22 -2
- package/dist/cli/setup.js.map +1 -1
- package/dist/client.js +51 -3
- package/dist/client.js.map +1 -1
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -1
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/loader.js +58 -0
- package/dist/hooks/loader.js.map +1 -0
- package/dist/hooks/manager.js +175 -0
- package/dist/hooks/manager.js.map +1 -0
- package/dist/hooks/plugins/example-console.js +24 -0
- package/dist/hooks/plugins/example-console.js.map +1 -0
- package/dist/hooks/scaffold.js +53 -0
- package/dist/hooks/scaffold.js.map +1 -0
- package/dist/hooks/types.js +8 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/tui/controller.js +328 -5
- package/dist/tui/controller.js.map +1 -1
- package/dist/tui/keymap.js +15 -0
- package/dist/tui/keymap.js.map +1 -1
- package/dist/tui/render.js +100 -1
- package/dist/tui/render.js.map +1 -1
- package/dist/tui/state.js +69 -1
- package/dist/tui/state.js.map +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -3,6 +3,7 @@ import { enforceContextBudget, stripThinking, estimateTokensFromMessages, estima
|
|
|
3
3
|
import * as tools from './tools.js';
|
|
4
4
|
import { selectHarness } from './harnesses.js';
|
|
5
5
|
import { BASE_MAX_TOKENS, deriveContextWindow, deriveGenerationParams, supportsVisionModel } from './model-customization.js';
|
|
6
|
+
import { HookManager, loadHookPlugins } from './hooks/index.js';
|
|
6
7
|
import { checkExecSafety, checkPathSafety } from './safety.js';
|
|
7
8
|
import { loadProjectContext } from './context.js';
|
|
8
9
|
import { loadGitContext, isGitDirty, stashWorkingTree } from './git.js';
|
|
@@ -989,6 +990,15 @@ export async function createSession(opts) {
|
|
|
989
990
|
if (typeof cfg.response_timeout === 'number' && cfg.response_timeout > 0) {
|
|
990
991
|
client.setResponseTimeout(cfg.response_timeout);
|
|
991
992
|
}
|
|
993
|
+
if (typeof client.setConnectionTimeout === 'function' && typeof cfg.connection_timeout === 'number' && cfg.connection_timeout > 0) {
|
|
994
|
+
client.setConnectionTimeout(cfg.connection_timeout);
|
|
995
|
+
}
|
|
996
|
+
if (typeof client.setInitialConnectionCheck === 'function' && typeof cfg.initial_connection_check === 'boolean') {
|
|
997
|
+
client.setInitialConnectionCheck(cfg.initial_connection_check);
|
|
998
|
+
}
|
|
999
|
+
if (typeof client.setInitialConnectionProbeTimeout === 'function' && typeof cfg.initial_connection_timeout === 'number' && cfg.initial_connection_timeout > 0) {
|
|
1000
|
+
client.setInitialConnectionProbeTimeout(cfg.initial_connection_timeout);
|
|
1001
|
+
}
|
|
992
1002
|
// Health check + model list (cheap, avoids wasting GPU on chat warmups if unreachable)
|
|
993
1003
|
let modelsList = normalizeModelsResponse(await client.models().catch(() => null));
|
|
994
1004
|
let model = cfg.model && cfg.model.trim().length
|
|
@@ -1004,6 +1014,44 @@ export async function createSession(opts) {
|
|
|
1004
1014
|
modelMeta,
|
|
1005
1015
|
});
|
|
1006
1016
|
let supportsVision = supportsVisionModel(model, modelMeta, harness);
|
|
1017
|
+
const sessionId = `session-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1018
|
+
const hookCfg = cfg.hooks ?? {};
|
|
1019
|
+
const hookManager = opts.runtime?.hookManager ?? new HookManager({
|
|
1020
|
+
enabled: hookCfg.enabled !== false,
|
|
1021
|
+
strict: hookCfg.strict === true,
|
|
1022
|
+
warnMs: hookCfg.warn_ms,
|
|
1023
|
+
allowedCapabilities: Array.isArray(hookCfg.allow_capabilities) ? hookCfg.allow_capabilities : undefined,
|
|
1024
|
+
context: () => ({
|
|
1025
|
+
sessionId,
|
|
1026
|
+
cwd: cfg.dir ?? process.cwd(),
|
|
1027
|
+
model,
|
|
1028
|
+
harness: harness.id,
|
|
1029
|
+
endpoint: cfg.endpoint,
|
|
1030
|
+
}),
|
|
1031
|
+
});
|
|
1032
|
+
const emitDetached = (promise, eventName) => {
|
|
1033
|
+
void promise.catch((error) => {
|
|
1034
|
+
if (!process.env.IDLEHANDS_QUIET_WARNINGS) {
|
|
1035
|
+
console.warn(`[hooks] async ${eventName} dispatch failed: ${error?.message ?? String(error)}`);
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
};
|
|
1039
|
+
if (!opts.runtime?.hookManager && hookManager.isEnabled()) {
|
|
1040
|
+
const loadedPlugins = await loadHookPlugins({
|
|
1041
|
+
pluginPaths: Array.isArray(hookCfg.plugin_paths) ? hookCfg.plugin_paths : [],
|
|
1042
|
+
cwd: cfg.dir ?? process.cwd(),
|
|
1043
|
+
strict: hookCfg.strict === true,
|
|
1044
|
+
});
|
|
1045
|
+
for (const loaded of loadedPlugins) {
|
|
1046
|
+
await hookManager.registerPlugin(loaded.plugin, loaded.path);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
await hookManager.emit('session_start', {
|
|
1050
|
+
model,
|
|
1051
|
+
harness: harness.id,
|
|
1052
|
+
endpoint: cfg.endpoint,
|
|
1053
|
+
cwd: cfg.dir ?? process.cwd(),
|
|
1054
|
+
});
|
|
1007
1055
|
if (!cfg.i_know_what_im_doing && contextWindow > 131072) {
|
|
1008
1056
|
console.warn('[warn] context_window is above 131072; this can increase memory usage and hurt throughput. Use --i-know-what-im-doing to proceed.');
|
|
1009
1057
|
}
|
|
@@ -1276,6 +1324,7 @@ export async function createSession(opts) {
|
|
|
1276
1324
|
];
|
|
1277
1325
|
sessionMetaPending = sessionMeta;
|
|
1278
1326
|
lastEditedPath = undefined;
|
|
1327
|
+
initialConnectionProbeDone = false;
|
|
1279
1328
|
mcpToolsLoaded = !mcpLazySchemaMode;
|
|
1280
1329
|
};
|
|
1281
1330
|
const restore = (next) => {
|
|
@@ -1298,6 +1347,7 @@ export async function createSession(opts) {
|
|
|
1298
1347
|
};
|
|
1299
1348
|
let reqCounter = 0;
|
|
1300
1349
|
let inFlight = null;
|
|
1350
|
+
let initialConnectionProbeDone = false;
|
|
1301
1351
|
let lastEditedPath;
|
|
1302
1352
|
// Plan mode state (Phase 8)
|
|
1303
1353
|
let planSteps = [];
|
|
@@ -1798,6 +1848,7 @@ export async function createSession(opts) {
|
|
|
1798
1848
|
return fresh.data.map((m) => m.id).filter(Boolean);
|
|
1799
1849
|
};
|
|
1800
1850
|
const setModel = (name) => {
|
|
1851
|
+
const previousModel = model;
|
|
1801
1852
|
model = name;
|
|
1802
1853
|
harness = selectHarness(model, cfg.harness && cfg.harness.trim() ? cfg.harness.trim() : undefined);
|
|
1803
1854
|
const nextMeta = modelsList?.data?.find((m) => m.id === model);
|
|
@@ -1815,6 +1866,11 @@ export async function createSession(opts) {
|
|
|
1815
1866
|
configuredTopP: cfg.top_p,
|
|
1816
1867
|
baseMaxTokens: BASE_MAX_TOKENS,
|
|
1817
1868
|
}));
|
|
1869
|
+
emitDetached(hookManager.emit('model_changed', {
|
|
1870
|
+
previousModel,
|
|
1871
|
+
nextModel: model,
|
|
1872
|
+
harness: harness.id,
|
|
1873
|
+
}), 'model_changed');
|
|
1818
1874
|
};
|
|
1819
1875
|
const setEndpoint = async (endpoint, modelName) => {
|
|
1820
1876
|
const normalized = endpoint.replace(/\/+$/, '');
|
|
@@ -2002,11 +2058,35 @@ export async function createSession(opts) {
|
|
|
2002
2058
|
const hookObj = typeof hooks === 'function' ? { onToken: hooks } : hooks ?? {};
|
|
2003
2059
|
let turns = 0;
|
|
2004
2060
|
let toolCalls = 0;
|
|
2061
|
+
const askId = `ask-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2062
|
+
const emitToolCall = async (call) => {
|
|
2063
|
+
hookObj.onToolCall?.(call);
|
|
2064
|
+
await hookManager.emit('tool_call', { askId, turn: turns, call });
|
|
2065
|
+
};
|
|
2066
|
+
const emitToolResult = async (result) => {
|
|
2067
|
+
await hookObj.onToolResult?.(result);
|
|
2068
|
+
await hookManager.emit('tool_result', { askId, turn: turns, result });
|
|
2069
|
+
};
|
|
2070
|
+
const emitTurnEnd = async (stats) => {
|
|
2071
|
+
await hookObj.onTurnEnd?.(stats);
|
|
2072
|
+
await hookManager.emit('turn_end', { askId, stats });
|
|
2073
|
+
};
|
|
2074
|
+
const finalizeAsk = async (text) => {
|
|
2075
|
+
await hookManager.emit('ask_end', { askId, text, turns, toolCalls });
|
|
2076
|
+
return { text, turns, toolCalls };
|
|
2077
|
+
};
|
|
2005
2078
|
const rawInstructionText = userContentToText(instruction).trim();
|
|
2079
|
+
await hookManager.emit('ask_start', { askId, instruction: rawInstructionText });
|
|
2006
2080
|
const projectDir = cfg.dir ?? process.cwd();
|
|
2007
2081
|
const reviewKeys = reviewArtifactKeys(projectDir);
|
|
2008
2082
|
const retrievalRequested = looksLikeReviewRetrievalRequest(rawInstructionText);
|
|
2009
2083
|
const shouldPersistReviewArtifact = looksLikeCodeReviewRequest(rawInstructionText) && !retrievalRequested;
|
|
2084
|
+
if (!retrievalRequested && cfg.initial_connection_check !== false && !initialConnectionProbeDone) {
|
|
2085
|
+
if (typeof client.probeConnection === 'function') {
|
|
2086
|
+
await client.probeConnection();
|
|
2087
|
+
initialConnectionProbeDone = true;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2010
2090
|
if (retrievalRequested) {
|
|
2011
2091
|
const latest = vault
|
|
2012
2092
|
? await vault.getLatestByKey(reviewKeys.latestKey, 'system').catch(() => null)
|
|
@@ -2023,37 +2103,37 @@ export async function createSession(opts) {
|
|
|
2023
2103
|
'Reply with "print stale review anyway" to override, or request a fresh review.';
|
|
2024
2104
|
messages.push({ role: 'assistant', content: blocked });
|
|
2025
2105
|
hookObj.onToken?.(blocked);
|
|
2026
|
-
await
|
|
2106
|
+
await emitTurnEnd({
|
|
2027
2107
|
turn: turns,
|
|
2028
2108
|
toolCalls,
|
|
2029
2109
|
promptTokens: cumulativeUsage.prompt,
|
|
2030
2110
|
completionTokens: cumulativeUsage.completion,
|
|
2031
2111
|
});
|
|
2032
|
-
return
|
|
2112
|
+
return await finalizeAsk(blocked);
|
|
2033
2113
|
}
|
|
2034
2114
|
const text = stale
|
|
2035
2115
|
? `${artifact.content}\n\n[artifact note] ${stale}`
|
|
2036
2116
|
: artifact.content;
|
|
2037
2117
|
messages.push({ role: 'assistant', content: text });
|
|
2038
2118
|
hookObj.onToken?.(text);
|
|
2039
|
-
await
|
|
2119
|
+
await emitTurnEnd({
|
|
2040
2120
|
turn: turns,
|
|
2041
2121
|
toolCalls,
|
|
2042
2122
|
promptTokens: cumulativeUsage.prompt,
|
|
2043
2123
|
completionTokens: cumulativeUsage.completion,
|
|
2044
2124
|
});
|
|
2045
|
-
return
|
|
2125
|
+
return await finalizeAsk(text);
|
|
2046
2126
|
}
|
|
2047
2127
|
const miss = 'No stored full code review found yet. Ask me to run a code review first, then I can replay it verbatim.';
|
|
2048
2128
|
messages.push({ role: 'assistant', content: miss });
|
|
2049
2129
|
hookObj.onToken?.(miss);
|
|
2050
|
-
await
|
|
2130
|
+
await emitTurnEnd({
|
|
2051
2131
|
turn: turns,
|
|
2052
2132
|
toolCalls,
|
|
2053
2133
|
promptTokens: cumulativeUsage.prompt,
|
|
2054
2134
|
completionTokens: cumulativeUsage.completion,
|
|
2055
2135
|
});
|
|
2056
|
-
return
|
|
2136
|
+
return await finalizeAsk(miss);
|
|
2057
2137
|
}
|
|
2058
2138
|
const persistReviewArtifact = async (finalText) => {
|
|
2059
2139
|
if (!vault || !shouldPersistReviewArtifact)
|
|
@@ -2193,6 +2273,7 @@ export async function createSession(opts) {
|
|
|
2193
2273
|
if (inFlight?.signal?.aborted)
|
|
2194
2274
|
break;
|
|
2195
2275
|
turns++;
|
|
2276
|
+
await hookManager.emit('turn_start', { askId, turn: turns });
|
|
2196
2277
|
const wallElapsed = (Date.now() - wallStart) / 1000;
|
|
2197
2278
|
if (wallElapsed > cfg.timeout) {
|
|
2198
2279
|
throw new Error(`session timeout exceeded (${cfg.timeout}s) after ${wallElapsed.toFixed(1)}s`);
|
|
@@ -2233,9 +2314,9 @@ export async function createSession(opts) {
|
|
|
2233
2314
|
const callerSignal = hookObj.signal;
|
|
2234
2315
|
const onCallerAbort = () => ac.abort();
|
|
2235
2316
|
callerSignal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
2236
|
-
// Per-request timeout: the lesser of response_timeout (default
|
|
2317
|
+
// Per-request timeout: the lesser of response_timeout (default 600s) or the remaining session wall time.
|
|
2237
2318
|
// This prevents a single slow request from consuming the entire session budget.
|
|
2238
|
-
const perReqCap = cfg.response_timeout && cfg.response_timeout > 0 ? cfg.response_timeout :
|
|
2319
|
+
const perReqCap = cfg.response_timeout && cfg.response_timeout > 0 ? cfg.response_timeout : 600;
|
|
2239
2320
|
const wallRemaining = Math.max(0, cfg.timeout - (Date.now() - wallStart) / 1000);
|
|
2240
2321
|
const reqTimeout = Math.min(perReqCap, Math.max(10, wallRemaining));
|
|
2241
2322
|
const timer = setTimeout(() => ac.abort(), reqTimeout * 1000);
|
|
@@ -2389,7 +2470,7 @@ export async function createSession(opts) {
|
|
|
2389
2470
|
role: 'user',
|
|
2390
2471
|
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.',
|
|
2391
2472
|
});
|
|
2392
|
-
await
|
|
2473
|
+
await emitTurnEnd({
|
|
2393
2474
|
turn: turns,
|
|
2394
2475
|
toolCalls,
|
|
2395
2476
|
promptTokens: cumulativeUsage.prompt,
|
|
@@ -2638,8 +2719,8 @@ export async function createSession(opts) {
|
|
|
2638
2719
|
const searchTerm = typeof args.search === 'string' ? args.search : '';
|
|
2639
2720
|
// Fix 1: Hard cumulative budget — refuse reads past hard cap
|
|
2640
2721
|
if (cumulativeReadOnlyCalls > READ_BUDGET_HARD) {
|
|
2641
|
-
|
|
2642
|
-
|
|
2722
|
+
await emitToolCall({ id: callId, name, args });
|
|
2723
|
+
await emitToolResult({ id: callId, name, success: false, summary: 'read budget exhausted', result: '' });
|
|
2643
2724
|
return { id: callId, content: `STOP: Read budget exhausted (${cumulativeReadOnlyCalls}/${READ_BUDGET_HARD} calls). Do NOT read more files. Use search_files or exec: grep -rn "pattern" path/ to find what you need.` };
|
|
2644
2725
|
}
|
|
2645
2726
|
// Fix 2: Directory scan detection — counts unique files per dir (re-reads are OK)
|
|
@@ -2654,8 +2735,8 @@ export async function createSession(opts) {
|
|
|
2654
2735
|
blockedDirs.add(parentDir);
|
|
2655
2736
|
}
|
|
2656
2737
|
if (blockedDirs.has(parentDir) && uniqueCount > 8) {
|
|
2657
|
-
|
|
2658
|
-
|
|
2738
|
+
await emitToolCall({ id: callId, name, args });
|
|
2739
|
+
await emitToolResult({ id: callId, name, success: false, summary: 'dir scan blocked', result: '' });
|
|
2659
2740
|
return { id: callId, content: `STOP: Directory scan detected — you've read ${uniqueCount} unique files from ${parentDir}/. Use search_files(pattern, '${parentDir}') or exec: grep -rn "pattern" ${parentDir}/ instead of reading files individually.` };
|
|
2660
2741
|
}
|
|
2661
2742
|
}
|
|
@@ -2666,8 +2747,8 @@ export async function createSession(opts) {
|
|
|
2666
2747
|
searchTermFiles.set(key, new Set());
|
|
2667
2748
|
searchTermFiles.get(key).add(filePath);
|
|
2668
2749
|
if (searchTermFiles.get(key).size >= 3) {
|
|
2669
|
-
|
|
2670
|
-
|
|
2750
|
+
await emitToolCall({ id: callId, name, args });
|
|
2751
|
+
await emitToolResult({ id: callId, name, success: false, summary: 'use search_files', result: '' });
|
|
2671
2752
|
return { id: callId, content: `STOP: You've searched ${searchTermFiles.get(key).size} files for "${searchTerm}" one at a time. This is what search_files does in one call. Use: search_files(pattern="${searchTerm}", path=".") or exec: grep -rn "${searchTerm}" .` };
|
|
2672
2753
|
}
|
|
2673
2754
|
}
|
|
@@ -2689,12 +2770,12 @@ export async function createSession(opts) {
|
|
|
2689
2770
|
// Notify via confirmProvider.showBlocked if available
|
|
2690
2771
|
opts.confirmProvider?.showBlocked?.({ tool: name, args, reason: `plan mode: ${summary}` });
|
|
2691
2772
|
// Hook: onToolCall + onToolResult for plan-blocked actions
|
|
2692
|
-
|
|
2693
|
-
|
|
2773
|
+
await emitToolCall({ id: callId, name, args });
|
|
2774
|
+
await emitToolResult({ id: callId, name, success: true, summary: `⏸ ${summary} (blocked)`, result: blockedMsg });
|
|
2694
2775
|
return { id: callId, content: blockedMsg };
|
|
2695
2776
|
}
|
|
2696
2777
|
// Hook: onToolCall (Phase 8.5)
|
|
2697
|
-
|
|
2778
|
+
await emitToolCall({ id: callId, name, args });
|
|
2698
2779
|
if (cfg.step_mode) {
|
|
2699
2780
|
const stepPrompt = `Step mode: execute ${name}(${JSON.stringify(args).slice(0, 200)}) ? [Y/n]`;
|
|
2700
2781
|
const ok = confirmBridge ? await confirmBridge(stepPrompt, { tool: name, args }) : true;
|
|
@@ -2815,7 +2896,7 @@ export async function createSession(opts) {
|
|
|
2815
2896
|
}
|
|
2816
2897
|
catch { }
|
|
2817
2898
|
}
|
|
2818
|
-
|
|
2899
|
+
await emitToolResult(resultEvent);
|
|
2819
2900
|
// Proactive LSP diagnostics after file mutations
|
|
2820
2901
|
if (lspManager?.hasServers() && lspCfg?.proactive_diagnostics !== false) {
|
|
2821
2902
|
if (FILE_MUTATION_TOOL_SET.has(name)) {
|
|
@@ -2843,7 +2924,7 @@ export async function createSession(opts) {
|
|
|
2843
2924
|
};
|
|
2844
2925
|
const results = [];
|
|
2845
2926
|
// Helper: catch tool errors but re-throw AgentLoopBreak (those must break the outer loop)
|
|
2846
|
-
const catchToolError = (e, tc) => {
|
|
2927
|
+
const catchToolError = async (e, tc) => {
|
|
2847
2928
|
if (e instanceof AgentLoopBreak)
|
|
2848
2929
|
throw e;
|
|
2849
2930
|
const msg = e?.message ?? String(e);
|
|
@@ -2877,7 +2958,7 @@ export async function createSession(opts) {
|
|
|
2877
2958
|
}
|
|
2878
2959
|
// Hook: onToolResult for errors (Phase 8.5)
|
|
2879
2960
|
const callId = resolveCallId(tc);
|
|
2880
|
-
|
|
2961
|
+
await emitToolResult({ id: callId, name: tc.function.name, success: false, summary: msg || 'unknown error', result: `ERROR: ${msg || 'unknown error'}` });
|
|
2881
2962
|
// Never return undefined error text; it makes bench failures impossible to debug.
|
|
2882
2963
|
return { id: callId, content: `ERROR: ${msg || 'unknown tool error'}` };
|
|
2883
2964
|
};
|
|
@@ -2916,7 +2997,7 @@ export async function createSession(opts) {
|
|
|
2916
2997
|
results.push(await runOne(tc));
|
|
2917
2998
|
}
|
|
2918
2999
|
catch (e) {
|
|
2919
|
-
results.push(catchToolError(e, tc));
|
|
3000
|
+
results.push(await catchToolError(e, tc));
|
|
2920
3001
|
}
|
|
2921
3002
|
}
|
|
2922
3003
|
}
|
|
@@ -2930,7 +3011,7 @@ export async function createSession(opts) {
|
|
|
2930
3011
|
results.push(await runOne(tc));
|
|
2931
3012
|
}
|
|
2932
3013
|
catch (e) {
|
|
2933
|
-
results.push(catchToolError(e, tc));
|
|
3014
|
+
results.push(await catchToolError(e, tc));
|
|
2934
3015
|
}
|
|
2935
3016
|
}
|
|
2936
3017
|
}
|
|
@@ -2972,7 +3053,7 @@ export async function createSession(opts) {
|
|
|
2972
3053
|
});
|
|
2973
3054
|
}
|
|
2974
3055
|
// Hook: onTurnEnd (Phase 8.5)
|
|
2975
|
-
await
|
|
3056
|
+
await emitTurnEnd({
|
|
2976
3057
|
turn: turns,
|
|
2977
3058
|
toolCalls,
|
|
2978
3059
|
promptTokens: cumulativeUsage.prompt,
|
|
@@ -3015,7 +3096,7 @@ export async function createSession(opts) {
|
|
|
3015
3096
|
`Original task:\n${clippedReminder}\n\n` +
|
|
3016
3097
|
`Call the needed tools directly. If everything is truly complete, provide the final answer.`
|
|
3017
3098
|
});
|
|
3018
|
-
await
|
|
3099
|
+
await emitTurnEnd({
|
|
3019
3100
|
turn: turns,
|
|
3020
3101
|
toolCalls,
|
|
3021
3102
|
promptTokens: cumulativeUsage.prompt,
|
|
@@ -3035,7 +3116,7 @@ export async function createSession(opts) {
|
|
|
3035
3116
|
role: 'user',
|
|
3036
3117
|
content: '[system] Continue executing the task. Use tools now (do not just narrate plans). If complete, give the final answer.'
|
|
3037
3118
|
});
|
|
3038
|
-
await
|
|
3119
|
+
await emitTurnEnd({
|
|
3039
3120
|
turn: turns,
|
|
3040
3121
|
toolCalls,
|
|
3041
3122
|
promptTokens: cumulativeUsage.prompt,
|
|
@@ -3053,7 +3134,7 @@ export async function createSession(opts) {
|
|
|
3053
3134
|
// final assistant message
|
|
3054
3135
|
messages.push({ role: 'assistant', content: assistantText });
|
|
3055
3136
|
await persistReviewArtifact(assistantText).catch(() => { });
|
|
3056
|
-
await
|
|
3137
|
+
await emitTurnEnd({
|
|
3057
3138
|
turn: turns,
|
|
3058
3139
|
toolCalls,
|
|
3059
3140
|
promptTokens: cumulativeUsage.prompt,
|
|
@@ -3065,7 +3146,7 @@ export async function createSession(opts) {
|
|
|
3065
3146
|
ppTps,
|
|
3066
3147
|
tgTps,
|
|
3067
3148
|
});
|
|
3068
|
-
return
|
|
3149
|
+
return await finalizeAsk(assistantText);
|
|
3069
3150
|
}
|
|
3070
3151
|
const reason = `max iterations exceeded (${maxIters})`;
|
|
3071
3152
|
const diag = lastSuccessfulTestRun
|
|
@@ -3091,6 +3172,12 @@ export async function createSession(opts) {
|
|
|
3091
3172
|
})();
|
|
3092
3173
|
const err = new Error(`BUG: threw undefined in agent.ask() (turn=${turns}). lastMsg=${lastMsg?.role ?? 'unknown'}:${lastMsgPreview}`);
|
|
3093
3174
|
await persistFailure(err, `ask turn ${turns}`);
|
|
3175
|
+
await hookManager.emit('ask_error', {
|
|
3176
|
+
askId,
|
|
3177
|
+
error: err.message,
|
|
3178
|
+
turns,
|
|
3179
|
+
toolCalls,
|
|
3180
|
+
});
|
|
3094
3181
|
throw err;
|
|
3095
3182
|
}
|
|
3096
3183
|
await persistFailure(e, `ask turn ${turns}`);
|
|
@@ -3100,8 +3187,21 @@ export async function createSession(opts) {
|
|
|
3100
3187
|
}
|
|
3101
3188
|
// Never rethrow undefined; normalize to Error for debuggability.
|
|
3102
3189
|
if (e === undefined) {
|
|
3103
|
-
|
|
3190
|
+
const normalized = new Error('BUG: threw undefined (normalized at ask() boundary)');
|
|
3191
|
+
await hookManager.emit('ask_error', {
|
|
3192
|
+
askId,
|
|
3193
|
+
error: normalized.message,
|
|
3194
|
+
turns,
|
|
3195
|
+
toolCalls,
|
|
3196
|
+
});
|
|
3197
|
+
throw normalized;
|
|
3104
3198
|
}
|
|
3199
|
+
await hookManager.emit('ask_error', {
|
|
3200
|
+
askId,
|
|
3201
|
+
error: e instanceof Error ? e.message : String(e),
|
|
3202
|
+
turns,
|
|
3203
|
+
toolCalls,
|
|
3204
|
+
});
|
|
3105
3205
|
throw e;
|
|
3106
3206
|
}
|
|
3107
3207
|
};
|
|
@@ -3148,6 +3248,7 @@ export async function createSession(opts) {
|
|
|
3148
3248
|
replay,
|
|
3149
3249
|
vault,
|
|
3150
3250
|
lens,
|
|
3251
|
+
hookManager,
|
|
3151
3252
|
get lastEditedPath() {
|
|
3152
3253
|
return lastEditedPath;
|
|
3153
3254
|
},
|