@visorcraft/idlehands 1.4.5 → 2.0.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/dist/agent/constants.js +12 -0
- package/dist/agent/constants.js.map +1 -0
- package/dist/agent/errors.js +8 -0
- package/dist/agent/errors.js.map +1 -0
- package/dist/agent/exec-helpers.js +105 -0
- package/dist/agent/exec-helpers.js.map +1 -0
- package/dist/agent/model-pick.js +21 -0
- package/dist/agent/model-pick.js.map +1 -0
- package/dist/agent/session-utils.js +63 -0
- package/dist/agent/session-utils.js.map +1 -0
- package/dist/agent/subagent-context.js +78 -0
- package/dist/agent/subagent-context.js.map +1 -0
- package/dist/agent/tool-loop-guard.js.map +1 -1
- package/dist/agent/tool-policy.js +54 -0
- package/dist/agent/tool-policy.js.map +1 -0
- package/dist/agent/tools-schema.js +281 -0
- package/dist/agent/tools-schema.js.map +1 -0
- package/dist/agent.js +136 -630
- package/dist/agent.js.map +1 -1
- package/dist/anton/controller.js +42 -139
- package/dist/anton/controller.js.map +1 -1
- package/dist/anton/lint-baseline.js +64 -0
- package/dist/anton/lint-baseline.js.map +1 -0
- package/dist/anton/preflight.js.map +1 -1
- package/dist/anton/prompt.js +71 -71
- package/dist/anton/reporter.js.map +1 -1
- package/dist/anton/runtime-ready.js +120 -0
- package/dist/anton/runtime-ready.js.map +1 -0
- package/dist/anton/session.js +8 -6
- package/dist/anton/session.js.map +1 -1
- package/dist/anton/verifier-utils.js +148 -0
- package/dist/anton/verifier-utils.js.map +1 -0
- package/dist/anton/verifier.js +26 -227
- package/dist/anton/verifier.js.map +1 -1
- package/dist/bot/anton-auto-pin.js +12 -0
- package/dist/bot/anton-auto-pin.js.map +1 -0
- package/dist/bot/anton-commands.js +137 -0
- package/dist/bot/anton-commands.js.map +1 -0
- package/dist/bot/anton-run.js +144 -0
- package/dist/bot/anton-run.js.map +1 -0
- package/dist/bot/anton-status-format.js +18 -0
- package/dist/bot/anton-status-format.js.map +1 -0
- package/dist/bot/basic-commands.js +114 -0
- package/dist/bot/basic-commands.js.map +1 -0
- package/dist/bot/command-format.js.map +1 -1
- package/dist/bot/command-logic.js +8 -728
- package/dist/bot/command-logic.js.map +1 -1
- package/dist/bot/commands.js +18 -1
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/discord-anton-autopin.js +29 -0
- package/dist/bot/discord-anton-autopin.js.map +1 -0
- package/dist/bot/discord-anton.js +45 -0
- package/dist/bot/discord-anton.js.map +1 -0
- package/dist/bot/discord-commands.js +20 -52
- package/dist/bot/discord-commands.js.map +1 -1
- package/dist/bot/discord-result.js +9 -0
- package/dist/bot/discord-result.js.map +1 -0
- package/dist/bot/discord-routing.js.map +1 -1
- package/dist/bot/discord.js +42 -12
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/escalation-commands.js +145 -0
- package/dist/bot/escalation-commands.js.map +1 -0
- package/dist/bot/escalation.js.map +1 -1
- package/dist/bot/git-status-command.js +28 -0
- package/dist/bot/git-status-command.js.map +1 -0
- package/dist/bot/model-endpoint.js +25 -0
- package/dist/bot/model-endpoint.js.map +1 -0
- package/dist/bot/session-history.js +61 -0
- package/dist/bot/session-history.js.map +1 -0
- package/dist/bot/session-settings.js +89 -0
- package/dist/bot/session-settings.js.map +1 -0
- package/dist/bot/telegram-commands.js +15 -7
- package/dist/bot/telegram-commands.js.map +1 -1
- package/dist/bot/telegram.js +13 -28
- package/dist/bot/telegram.js.map +1 -1
- package/dist/cli/agent-turn.js +8 -2
- package/dist/cli/agent-turn.js.map +1 -1
- package/dist/cli/commands/anton.js +8 -3
- package/dist/cli/commands/anton.js.map +1 -1
- package/dist/cli/commands/model.js +1 -3
- package/dist/cli/commands/model.js.map +1 -1
- package/dist/cli/commands/project.js +1 -1
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/secrets.js +1 -1
- package/dist/cli/commands/secrets.js.map +1 -1
- package/dist/cli/commands/session.js +22 -12
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/guided-onboarding.js +20 -0
- package/dist/cli/guided-onboarding.js.map +1 -0
- package/dist/cli/runtime-cmds.js +8 -133
- package/dist/cli/runtime-cmds.js.map +1 -1
- package/dist/cli/runtime-common.js +35 -0
- package/dist/cli/runtime-common.js.map +1 -0
- package/dist/cli/runtime-detect.js +12 -0
- package/dist/cli/runtime-detect.js.map +1 -0
- package/dist/cli/runtime-host-command.js +7 -0
- package/dist/cli/runtime-host-command.js.map +1 -0
- package/dist/cli/runtime-probe-defaults.js +63 -0
- package/dist/cli/runtime-probe-defaults.js.map +1 -0
- package/dist/cli/runtime-scan-ports.js +30 -0
- package/dist/cli/runtime-scan-ports.js.map +1 -0
- package/dist/cli/setup-bot-step.js +51 -0
- package/dist/cli/setup-bot-step.js.map +1 -0
- package/dist/cli/setup-runtime-forms.js +214 -0
- package/dist/cli/setup-runtime-forms.js.map +1 -0
- package/dist/cli/setup-style.js +8 -0
- package/dist/cli/setup-style.js.map +1 -0
- package/dist/cli/setup-ui.js +146 -0
- package/dist/cli/setup-ui.js.map +1 -0
- package/dist/cli/setup.js +11 -449
- package/dist/cli/setup.js.map +1 -1
- package/dist/client/error-utils.js +37 -0
- package/dist/client/error-utils.js.map +1 -0
- package/dist/client/pressure.js +77 -0
- package/dist/client/pressure.js.map +1 -0
- package/dist/client.js +24 -122
- package/dist/client.js.map +1 -1
- package/dist/config.js +34 -17
- package/dist/config.js.map +1 -1
- package/dist/git.js +8 -2
- package/dist/git.js.map +1 -1
- package/dist/hooks/types.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/progress/message-edit-scheduler.js.map +1 -1
- package/dist/progress/turn-progress.js.map +1 -1
- package/dist/runtime/executor.js +4 -1
- package/dist/runtime/executor.js.map +1 -1
- package/dist/runtime/health.js.map +1 -1
- package/dist/runtime/host-runner.js.map +1 -1
- package/dist/safety.js +3 -2
- package/dist/safety.js.map +1 -1
- package/dist/shared/config-utils.js.map +1 -1
- package/dist/tools/exec-core.js +252 -0
- package/dist/tools/exec-core.js.map +1 -0
- package/dist/tools/exec-pty.js +89 -0
- package/dist/tools/exec-pty.js.map +1 -0
- package/dist/tools/exec-utils.js +94 -0
- package/dist/tools/exec-utils.js.map +1 -0
- package/dist/tools/file-discovery.js +144 -0
- package/dist/tools/file-discovery.js.map +1 -0
- package/dist/tools/file-mutations.js +326 -0
- package/dist/tools/file-mutations.js.map +1 -0
- package/dist/tools/file-read.js +133 -0
- package/dist/tools/file-read.js.map +1 -0
- package/dist/tools/patch-apply.js +168 -0
- package/dist/tools/patch-apply.js.map +1 -0
- package/dist/tools/path-safety.js.map +1 -1
- package/dist/tools/replay-utils.js +25 -0
- package/dist/tools/replay-utils.js.map +1 -0
- package/dist/tools/search-utils.js +55 -0
- package/dist/tools/search-utils.js.map +1 -0
- package/dist/tools/sys-notes.js +34 -0
- package/dist/tools/sys-notes.js.map +1 -0
- package/dist/tools/text-utils.js +164 -0
- package/dist/tools/text-utils.js.map +1 -0
- package/dist/tools/undo.js +1 -1
- package/dist/tools/undo.js.map +1 -1
- package/dist/tools/vault-tools.js +36 -0
- package/dist/tools/vault-tools.js.map +1 -0
- package/dist/tools.js +19 -1460
- package/dist/tools.js.map +1 -1
- package/dist/tui/controller.js +5 -2
- package/dist/tui/controller.js.map +1 -1
- package/dist/tui/render.js.map +1 -1
- package/dist/utils.js +2 -2
- package/dist/utils.js.map +1 -1
- package/dist/vault.js +1 -1
- package/dist/vault.js.map +1 -1
- package/dist/watchdog.js +1 -3
- package/dist/watchdog.js.map +1 -1
- package/package.json +2 -1
package/dist/agent.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { DEFAULT_SUB_AGENT_RESULT_TOKEN_CAP, DEFAULT_SUB_AGENT_SYSTEM_PROMPT, MCP_TOOLS_REQUEST_TOKEN, } from './agent/constants.js';
|
|
4
|
+
import { AgentLoopBreak } from './agent/errors.js';
|
|
5
|
+
import { execRcShouldSignalFailure, looksLikeReadOnlyExecCommand, readOnlyExecCacheable, withCachedExecObservationHint, withReplayedExecHint, } from './agent/exec-helpers.js';
|
|
6
|
+
import { generateMinimalDiff, toolResultSummary, execCommandFromSig, formatDurationMs, looksLikePlanningNarration, capTextByApproxTokens, sanitizePathsInMessage, digestToolResult, } from './agent/formatting.js';
|
|
7
|
+
import { autoPickModel } from './agent/model-pick.js';
|
|
4
8
|
import { reviewArtifactKeys, looksLikeCodeReviewRequest, looksLikeReviewRetrievalRequest, retrievalAllowsStaleArtifact, parseReviewArtifactStalePolicy, parseReviewArtifact, reviewArtifactStaleReason, gitHead, normalizeModelsResponse, } from './agent/review-artifact.js';
|
|
9
|
+
import { capApprovalMode, ensureInformativeAssistantText, isContextWindowExceededError, makeAbortController, userContentToText, userDisallowsDelegation, } from './agent/session-utils.js';
|
|
10
|
+
import { buildSubAgentContextBlock, extractLensBody } from './agent/subagent-context.js';
|
|
5
11
|
import { parseToolCallsFromContent, getMissingRequiredParams, getArgValidationIssues, stripMarkdownFences, parseJsonArgs, } from './agent/tool-calls.js';
|
|
6
12
|
import { ToolLoopGuard } from './agent/tool-loop-guard.js';
|
|
13
|
+
import { isLspTool, isMutationTool, isReadOnlyTool, planModeSummary } from './agent/tool-policy.js';
|
|
14
|
+
import { buildToolsSchema } from './agent/tools-schema.js';
|
|
7
15
|
import { OpenAIClient } from './client.js';
|
|
8
16
|
import { loadProjectContext } from './context.js';
|
|
9
17
|
import { loadGitContext, isGitDirty, stashWorkingTree } from './git.js';
|
|
@@ -18,148 +26,12 @@ import { BASE_MAX_TOKENS, deriveContextWindow, deriveGenerationParams, supportsV
|
|
|
18
26
|
import { ReplayStore } from './replay.js';
|
|
19
27
|
import { checkExecSafety, checkPathSafety } from './safety.js';
|
|
20
28
|
import { normalizeApprovalMode } from './shared/config-utils.js';
|
|
21
|
-
import {
|
|
29
|
+
import { collectSnapshot } from './sys/context.js';
|
|
22
30
|
import { ToolError, ValidationError } from './tools/tool-error.js';
|
|
23
31
|
import * as tools from './tools.js';
|
|
24
32
|
import { stateDir, timestampedId } from './utils.js';
|
|
25
33
|
import { VaultStore } from './vault.js';
|
|
26
34
|
export { parseToolCallsFromContent };
|
|
27
|
-
function makeAbortController() {
|
|
28
|
-
// Node 24: AbortController is global.
|
|
29
|
-
return new AbortController();
|
|
30
|
-
}
|
|
31
|
-
const CACHED_EXEC_OBSERVATION_HINT = '[idlehands hint] Reused cached output for repeated read-only exec call (unchanged observation).';
|
|
32
|
-
function looksLikeReadOnlyExecCommand(command) {
|
|
33
|
-
// Strip leading `cd <path> &&` / `cd <path>;` prefixes — cd is read-only
|
|
34
|
-
// navigation, the actual command that matters comes after.
|
|
35
|
-
let cmd = String(command || '')
|
|
36
|
-
.trim()
|
|
37
|
-
.toLowerCase();
|
|
38
|
-
if (!cmd)
|
|
39
|
-
return false;
|
|
40
|
-
cmd = cmd.replace(/^(\s*cd\s+[^;&|]+\s*(?:&&|;)\s*)+/i, '').trim();
|
|
41
|
-
if (!cmd)
|
|
42
|
-
return false;
|
|
43
|
-
// Shell redirects are likely writes.
|
|
44
|
-
if (/(^|\s)(?:>>?|<<?)\s*/.test(cmd))
|
|
45
|
-
return false;
|
|
46
|
-
// Obvious mutators.
|
|
47
|
-
if (/\b(?:rm|mv|cp|touch|mkdir|rmdir|chmod|chown|truncate|dd)\b/.test(cmd))
|
|
48
|
-
return false;
|
|
49
|
-
if (/\b(?:sed|perl)\b[^\n]*\s-i\b/.test(cmd))
|
|
50
|
-
return false;
|
|
51
|
-
if (/\btee\b/.test(cmd))
|
|
52
|
-
return false;
|
|
53
|
-
// Git: allow common read-only subcommands, block mutating verbs.
|
|
54
|
-
if (/\bgit\b/.test(cmd)) {
|
|
55
|
-
if (/\bgit\b[^\n|;&]*\b(?:add|am|apply|bisect|checkout|switch|clean|clone|commit|fetch|merge|pull|push|rebase|reset|revert|stash)\b/.test(cmd)) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
if (/\bgit\b[^\n|;&]*\b(?:log|show|status|diff|rev-parse|branch(?:\s+--list)?|tag(?:\s+--list)?|ls-files|grep)\b/.test(cmd)) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (/^\s*(?:grep|rg|ag|ack|find|ls|cat|head|tail|wc|stat)\b/.test(cmd))
|
|
63
|
-
return true;
|
|
64
|
-
if (/\|\s*(?:grep|rg|ag|ack)\b/.test(cmd))
|
|
65
|
-
return true;
|
|
66
|
-
// Additional read-only commands: file info, path lookup, system/user info
|
|
67
|
-
if (/^\s*(?:file|which|type|uname|env|printenv|id|whoami|pwd)\b/.test(cmd))
|
|
68
|
-
return true;
|
|
69
|
-
// Git read-only subcommands that aren't covered above
|
|
70
|
-
if (/\bgit\b[^\n|;&]*\b(?:blame|remote|config\s+--(?:get|list|global|local|system))\b/.test(cmd))
|
|
71
|
-
return true;
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
function execRcShouldSignalFailure(command) {
|
|
75
|
-
const cmd = String(command || '').toLowerCase();
|
|
76
|
-
if (!cmd)
|
|
77
|
-
return false;
|
|
78
|
-
// Common checks where non-zero usually means real failure.
|
|
79
|
-
if (/\b(?:npm|pnpm|yarn)\s+(?:run\s+)?(?:test|build|lint|typecheck|check)\b/.test(cmd))
|
|
80
|
-
return true;
|
|
81
|
-
if (/\bnode\s+--test\b/.test(cmd))
|
|
82
|
-
return true;
|
|
83
|
-
if (/\b(?:pytest|go\s+test|cargo\s+test|ctest|mvn\s+test|gradle\s+test)\b/.test(cmd))
|
|
84
|
-
return true;
|
|
85
|
-
if (/\b(?:cargo\s+build|go\s+build|tsc\b)\b/.test(cmd))
|
|
86
|
-
return true;
|
|
87
|
-
// Grep/rg no-match rc=1 should not be treated as failure.
|
|
88
|
-
if (/^\s*(?:rg|grep|ag|ack)\b/.test(cmd))
|
|
89
|
-
return false;
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
function withCachedExecObservationHint(content) {
|
|
93
|
-
if (!content)
|
|
94
|
-
return content;
|
|
95
|
-
try {
|
|
96
|
-
const parsed = JSON.parse(content);
|
|
97
|
-
const out = typeof parsed?.out === 'string' ? parsed.out : '';
|
|
98
|
-
if (out.includes(CACHED_EXEC_OBSERVATION_HINT))
|
|
99
|
-
return content;
|
|
100
|
-
parsed.out = out ? `${out}\n${CACHED_EXEC_OBSERVATION_HINT}` : CACHED_EXEC_OBSERVATION_HINT;
|
|
101
|
-
parsed.cached_observation = true;
|
|
102
|
-
return JSON.stringify(parsed);
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
if (content.includes(CACHED_EXEC_OBSERVATION_HINT))
|
|
106
|
-
return content;
|
|
107
|
-
return `${content}\n${CACHED_EXEC_OBSERVATION_HINT}`;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const REPLAYED_EXEC_HINT = '[idlehands hint] You already ran this exact command. This is the replayed result from your previous execution. Do NOT re-run it — use the output below to continue your task.';
|
|
111
|
-
function withReplayedExecHint(content) {
|
|
112
|
-
if (!content)
|
|
113
|
-
return content;
|
|
114
|
-
try {
|
|
115
|
-
const parsed = JSON.parse(content);
|
|
116
|
-
const out = typeof parsed?.out === 'string' ? parsed.out : '';
|
|
117
|
-
if (out.includes(REPLAYED_EXEC_HINT))
|
|
118
|
-
return content;
|
|
119
|
-
parsed.out = out ? `${REPLAYED_EXEC_HINT}\n${out}` : REPLAYED_EXEC_HINT;
|
|
120
|
-
parsed.replayed = true;
|
|
121
|
-
return JSON.stringify(parsed);
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
if (content.includes(REPLAYED_EXEC_HINT))
|
|
125
|
-
return content;
|
|
126
|
-
return `${REPLAYED_EXEC_HINT}\n${content}`;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function readOnlyExecCacheable(content) {
|
|
130
|
-
try {
|
|
131
|
-
const parsed = JSON.parse(content);
|
|
132
|
-
const rc = Number(parsed?.rc ?? NaN);
|
|
133
|
-
return Number.isFinite(rc) && rc === 0;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
function ensureInformativeAssistantText(text, ctx) {
|
|
140
|
-
if (String(text ?? '').trim())
|
|
141
|
-
return text;
|
|
142
|
-
if (ctx.toolCalls > 0) {
|
|
143
|
-
return 'I completed the requested tool work, but I have no user-visible response text yet. Ask me to summarize what was done.';
|
|
144
|
-
}
|
|
145
|
-
return `I have no user-visible response text for this turn (turn=${ctx.turns}). Please try again or rephrase your request.`;
|
|
146
|
-
}
|
|
147
|
-
function isContextWindowExceededError(err) {
|
|
148
|
-
const status = Number(err?.status ?? NaN);
|
|
149
|
-
const msg = String(err?.message ?? err ?? '');
|
|
150
|
-
if (status === 413)
|
|
151
|
-
return true;
|
|
152
|
-
if (!msg)
|
|
153
|
-
return false;
|
|
154
|
-
return /(exceeds?\s+the\s+available\s+context\s+size|exceed_context|context\s+size|context\s+window|maximum\s+context\s+length|too\s+many\s+tokens|request\s*\(\d+\s*tokens\))/i.test(msg);
|
|
155
|
-
}
|
|
156
|
-
/** Errors that should break the outer agent loop, not be caught by per-tool handlers */
|
|
157
|
-
class AgentLoopBreak extends Error {
|
|
158
|
-
constructor(message) {
|
|
159
|
-
super(message);
|
|
160
|
-
this.name = 'AgentLoopBreak';
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
35
|
const SYSTEM_PROMPT = `You are a coding agent with filesystem and shell access. Execute the user's request using the provided tools.
|
|
164
36
|
|
|
165
37
|
Rules:
|
|
@@ -186,429 +58,6 @@ Rules:
|
|
|
186
58
|
Tool call format:
|
|
187
59
|
- Use tool_calls. Do not write JSON tool invocations in your message text.
|
|
188
60
|
`;
|
|
189
|
-
const MCP_TOOLS_REQUEST_TOKEN = '[[MCP_TOOLS_REQUEST]]';
|
|
190
|
-
const DEFAULT_SUB_AGENT_SYSTEM_PROMPT = `You are a focused coding sub-agent. Execute only the delegated task.
|
|
191
|
-
- Work in the current directory. Use relative paths for all file operations.
|
|
192
|
-
- Read the target file before editing. You need the exact text for search/replace.
|
|
193
|
-
- Keep tool usage tight and efficient.
|
|
194
|
-
- Prefer surgical edits over rewrites.
|
|
195
|
-
- Do NOT create files outside the working directory unless explicitly requested.
|
|
196
|
-
- When running commands in a subdirectory, use exec's cwd parameter — NOT "cd /path && cmd".
|
|
197
|
-
- Run verification commands when relevant.
|
|
198
|
-
- Return a concise outcome summary.`;
|
|
199
|
-
const DEFAULT_SUB_AGENT_RESULT_TOKEN_CAP = 4000;
|
|
200
|
-
const LSP_TOOL_NAMES = [
|
|
201
|
-
'lsp_diagnostics',
|
|
202
|
-
'lsp_symbols',
|
|
203
|
-
'lsp_hover',
|
|
204
|
-
'lsp_definition',
|
|
205
|
-
'lsp_references',
|
|
206
|
-
];
|
|
207
|
-
const LSP_TOOL_NAME_SET = new Set(LSP_TOOL_NAMES);
|
|
208
|
-
const FILE_MUTATION_TOOL_SET = new Set([
|
|
209
|
-
'edit_file',
|
|
210
|
-
'edit_range',
|
|
211
|
-
'apply_patch',
|
|
212
|
-
'write_file',
|
|
213
|
-
'insert_file',
|
|
214
|
-
]);
|
|
215
|
-
/** Approval mode permissiveness ranking (lower = more restrictive). */
|
|
216
|
-
const APPROVAL_MODE_RANK = {
|
|
217
|
-
plan: 0,
|
|
218
|
-
reject: 1,
|
|
219
|
-
default: 2,
|
|
220
|
-
'auto-edit': 3,
|
|
221
|
-
yolo: 4,
|
|
222
|
-
};
|
|
223
|
-
/**
|
|
224
|
-
* Cap a sub-agent's approval mode at the parent's level.
|
|
225
|
-
* Sub-agents cannot escalate beyond the parent's approval mode.
|
|
226
|
-
*/
|
|
227
|
-
function capApprovalMode(requested, parentMode) {
|
|
228
|
-
return APPROVAL_MODE_RANK[requested] <= APPROVAL_MODE_RANK[parentMode] ? requested : parentMode;
|
|
229
|
-
}
|
|
230
|
-
async function buildSubAgentContextBlock(cwd, rawFiles) {
|
|
231
|
-
const values = Array.isArray(rawFiles) ? rawFiles : [];
|
|
232
|
-
const files = values
|
|
233
|
-
.map((v) => (typeof v === 'string' ? v.trim() : ''))
|
|
234
|
-
.filter(Boolean)
|
|
235
|
-
.slice(0, 12);
|
|
236
|
-
if (!files.length)
|
|
237
|
-
return { block: '', included: [], skipped: [] };
|
|
238
|
-
const MAX_TOTAL_CHARS = 24_000;
|
|
239
|
-
const MAX_PER_FILE_CHARS = 4_000;
|
|
240
|
-
let total = 0;
|
|
241
|
-
const parts = [];
|
|
242
|
-
const included = [];
|
|
243
|
-
const skipped = [];
|
|
244
|
-
for (const rel of files) {
|
|
245
|
-
const abs = path.resolve(cwd, rel);
|
|
246
|
-
const relFromCwd = path.relative(cwd, abs);
|
|
247
|
-
if (relFromCwd.startsWith('..') || path.isAbsolute(relFromCwd)) {
|
|
248
|
-
skipped.push(`${rel} (outside cwd)`);
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
let stat;
|
|
252
|
-
try {
|
|
253
|
-
stat = await fs.stat(abs);
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
skipped.push(`${rel} (missing)`);
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
if (!stat?.isFile()) {
|
|
260
|
-
skipped.push(`${rel} (not a file)`);
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
const buf = await fs.readFile(abs).catch(() => null);
|
|
264
|
-
if (!buf) {
|
|
265
|
-
skipped.push(`${rel} (unreadable)`);
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (isLikelyBinaryBuffer(buf)) {
|
|
269
|
-
skipped.push(`${rel} (binary)`);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const raw = buf.toString('utf8');
|
|
273
|
-
const body = raw.length > MAX_PER_FILE_CHARS
|
|
274
|
-
? `${raw.slice(0, MAX_PER_FILE_CHARS)}\n[truncated: ${raw.length} chars total]`
|
|
275
|
-
: raw;
|
|
276
|
-
const section = `[file:${rel}]\n${body}\n[/file:${rel}]`;
|
|
277
|
-
if (total + section.length > MAX_TOTAL_CHARS) {
|
|
278
|
-
skipped.push(`${rel} (context budget reached)`);
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
parts.push(section);
|
|
282
|
-
included.push(rel);
|
|
283
|
-
total += section.length;
|
|
284
|
-
}
|
|
285
|
-
return { block: parts.join('\n\n'), included, skipped };
|
|
286
|
-
}
|
|
287
|
-
function extractLensBody(projection) {
|
|
288
|
-
const lines = String(projection ?? '').split(/\r?\n/);
|
|
289
|
-
if (!lines.length)
|
|
290
|
-
return '';
|
|
291
|
-
let start = 0;
|
|
292
|
-
if (lines[0].startsWith('# '))
|
|
293
|
-
start = 1;
|
|
294
|
-
if (lines[start]?.startsWith('# lens:'))
|
|
295
|
-
start += 1;
|
|
296
|
-
return lines
|
|
297
|
-
.slice(start)
|
|
298
|
-
.filter((line) => line.trim().length > 0)
|
|
299
|
-
.slice(0, 40)
|
|
300
|
-
.join('\n');
|
|
301
|
-
}
|
|
302
|
-
function buildToolsSchema(opts) {
|
|
303
|
-
const obj = (properties, required = []) => ({
|
|
304
|
-
type: 'object',
|
|
305
|
-
additionalProperties: false,
|
|
306
|
-
properties,
|
|
307
|
-
required,
|
|
308
|
-
});
|
|
309
|
-
const str = () => ({ type: 'string' });
|
|
310
|
-
const bool = () => ({ type: 'boolean' });
|
|
311
|
-
const int = (min, max) => ({
|
|
312
|
-
type: 'integer',
|
|
313
|
-
...(min !== undefined && { minimum: min }),
|
|
314
|
-
...(max !== undefined && { maximum: max }),
|
|
315
|
-
});
|
|
316
|
-
const schemas = [
|
|
317
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
318
|
-
// Token-safe reads (require limit; allow plain output without per-line numbers)
|
|
319
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
320
|
-
{
|
|
321
|
-
type: 'function',
|
|
322
|
-
function: {
|
|
323
|
-
name: 'read_file',
|
|
324
|
-
description: 'Read a bounded slice of a file. Never repeat an identical call consecutively; reuse the prior result.',
|
|
325
|
-
parameters: obj({
|
|
326
|
-
path: str(),
|
|
327
|
-
offset: int(1, 1_000_000),
|
|
328
|
-
limit: int(1, 240),
|
|
329
|
-
search: str(),
|
|
330
|
-
context: int(0, 80),
|
|
331
|
-
format: { type: 'string', enum: ['plain', 'numbered', 'sparse'] },
|
|
332
|
-
max_bytes: int(256, 20_000),
|
|
333
|
-
}, ['path', 'limit']),
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
{
|
|
337
|
-
type: 'function',
|
|
338
|
-
function: {
|
|
339
|
-
name: 'read_files',
|
|
340
|
-
description: 'Batch read bounded file slices. Never repeat an identical call consecutively; reuse the prior result.',
|
|
341
|
-
parameters: obj({
|
|
342
|
-
requests: {
|
|
343
|
-
type: 'array',
|
|
344
|
-
items: obj({
|
|
345
|
-
path: str(),
|
|
346
|
-
offset: int(1, 1_000_000),
|
|
347
|
-
limit: int(1, 240),
|
|
348
|
-
search: str(),
|
|
349
|
-
context: int(0, 80),
|
|
350
|
-
format: { type: 'string', enum: ['plain', 'numbered', 'sparse'] },
|
|
351
|
-
max_bytes: int(256, 20_000),
|
|
352
|
-
}, ['path', 'limit']),
|
|
353
|
-
},
|
|
354
|
-
}, ['requests']),
|
|
355
|
-
},
|
|
356
|
-
},
|
|
357
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
358
|
-
// Writes/edits
|
|
359
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
360
|
-
{
|
|
361
|
-
type: 'function',
|
|
362
|
-
function: {
|
|
363
|
-
name: 'write_file',
|
|
364
|
-
description: 'Write file (atomic, backup). Existing non-empty files require overwrite=true (or force=true).',
|
|
365
|
-
parameters: obj({ path: str(), content: str(), overwrite: bool(), force: bool() }, [
|
|
366
|
-
'path',
|
|
367
|
-
'content',
|
|
368
|
-
]),
|
|
369
|
-
},
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
type: 'function',
|
|
373
|
-
function: {
|
|
374
|
-
name: 'apply_patch',
|
|
375
|
-
description: 'Apply unified diff patch (multi-file).\n\nUSAGE EXAMPLE:\n apply_patch({\n patch: "--- a/src/file.ts\\n+++ b/src/file.ts\\n@@ -1,5 +1,5 @@\\n-old text\\n+new text\\n",\n files: ["src/file.ts"]\n })\n\nThe patch must be valid unified diff text. Tool-call arguments must be valid JSON. Use strip=1 if paths include directory prefixes.\nFiles listed must match the paths in the diff.',
|
|
376
|
-
parameters: obj({
|
|
377
|
-
patch: str(),
|
|
378
|
-
files: { type: 'array', items: str() },
|
|
379
|
-
strip: int(0, 5),
|
|
380
|
-
}, ['patch', 'files']),
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
type: 'function',
|
|
385
|
-
function: {
|
|
386
|
-
name: 'edit_range',
|
|
387
|
-
description: 'Replace a line range in a file.\n\nUSAGE EXAMPLE:\n edit_range({\n path: "src/file.ts",\n start_line: 10,\n end_line: 15,\n replacement: "new content\\nmore content"\n })\n\n- start_line and end_line are 1-indexed (first line is 1, not 0)\n- To delete lines, set replacement to empty string ""\n- To insert at a position, set start_line and end_line to the same value\n- Tool-call arguments must be valid JSON (double quotes, no trailing commas/comments)\n- The replacement text replaces the entire range inclusive',
|
|
388
|
-
parameters: obj({
|
|
389
|
-
path: str(),
|
|
390
|
-
start_line: int(1),
|
|
391
|
-
end_line: int(1),
|
|
392
|
-
replacement: str(),
|
|
393
|
-
}, ['path', 'start_line', 'end_line', 'replacement']),
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
type: 'function',
|
|
398
|
-
function: {
|
|
399
|
-
name: 'edit_file',
|
|
400
|
-
description: 'Legacy exact replace (requires old_text). Prefer apply_patch/edit_range.',
|
|
401
|
-
parameters: obj({ path: str(), old_text: str(), new_text: str(), replace_all: bool() }, [
|
|
402
|
-
'path',
|
|
403
|
-
'old_text',
|
|
404
|
-
'new_text',
|
|
405
|
-
]),
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
{
|
|
409
|
-
type: 'function',
|
|
410
|
-
function: {
|
|
411
|
-
name: 'insert_file',
|
|
412
|
-
description: 'Insert text at line (0=prepend, -1=append).',
|
|
413
|
-
parameters: obj({ path: str(), line: int(), text: str() }, ['path', 'line', 'text']),
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
417
|
-
// Bounded listings/search (expose existing caps)
|
|
418
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
419
|
-
{
|
|
420
|
-
type: 'function',
|
|
421
|
-
function: {
|
|
422
|
-
name: 'list_dir',
|
|
423
|
-
description: 'List directory entries. Never repeat an identical call consecutively for the same path/options; reuse the prior result.',
|
|
424
|
-
parameters: obj({ path: str(), recursive: bool(), max_entries: int(1, 500) }, ['path']),
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
type: 'function',
|
|
429
|
-
function: {
|
|
430
|
-
name: 'search_files',
|
|
431
|
-
description: 'Search regex in files.',
|
|
432
|
-
parameters: obj({ pattern: str(), path: str(), include: str(), max_results: int(1, 100) }, [
|
|
433
|
-
'pattern',
|
|
434
|
-
'path',
|
|
435
|
-
]),
|
|
436
|
-
},
|
|
437
|
-
},
|
|
438
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
439
|
-
// Exec (minified schema)
|
|
440
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
441
|
-
{
|
|
442
|
-
type: 'function',
|
|
443
|
-
function: {
|
|
444
|
-
name: 'exec',
|
|
445
|
-
description: 'Run bash -c; returns JSON rc/out/err.',
|
|
446
|
-
parameters: obj({ command: str(), cwd: str(), timeout: int(1, 120) }, ['command']),
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
];
|
|
450
|
-
if (opts?.allowSpawnTask !== false) {
|
|
451
|
-
schemas.push({
|
|
452
|
-
type: 'function',
|
|
453
|
-
function: {
|
|
454
|
-
name: 'spawn_task',
|
|
455
|
-
description: 'Run a sub-agent task (no parent history).',
|
|
456
|
-
parameters: obj({
|
|
457
|
-
task: str(),
|
|
458
|
-
context_files: { type: 'array', items: str() },
|
|
459
|
-
model: str(),
|
|
460
|
-
endpoint: str(),
|
|
461
|
-
max_iterations: int(),
|
|
462
|
-
max_tokens: int(),
|
|
463
|
-
timeout_sec: int(),
|
|
464
|
-
system_prompt: str(),
|
|
465
|
-
approval_mode: {
|
|
466
|
-
type: 'string',
|
|
467
|
-
enum: ['plan', 'reject', 'default', 'auto-edit', 'yolo'],
|
|
468
|
-
},
|
|
469
|
-
}, ['task']),
|
|
470
|
-
},
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
if (opts?.activeVaultTools) {
|
|
474
|
-
schemas.push({
|
|
475
|
-
type: 'function',
|
|
476
|
-
function: {
|
|
477
|
-
name: 'vault_search',
|
|
478
|
-
description: 'Search vault.',
|
|
479
|
-
parameters: obj({ query: str(), limit: int() }, ['query']),
|
|
480
|
-
},
|
|
481
|
-
}, {
|
|
482
|
-
type: 'function',
|
|
483
|
-
function: {
|
|
484
|
-
name: 'vault_note',
|
|
485
|
-
description: 'Write vault note.',
|
|
486
|
-
parameters: obj({ key: str(), value: str() }, ['key', 'value']),
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
else if (opts?.passiveVault) {
|
|
491
|
-
// In passive mode, expose vault_search (read-only) so the model can recover
|
|
492
|
-
// compacted context on demand, but don't expose vault_note (write).
|
|
493
|
-
schemas.push({
|
|
494
|
-
type: 'function',
|
|
495
|
-
function: {
|
|
496
|
-
name: 'vault_search',
|
|
497
|
-
description: 'Search vault memory for earlier context that was compacted away. Use sparingly — only when you need to recall specific details from earlier in the conversation.',
|
|
498
|
-
parameters: obj({ query: str(), limit: int() }, ['query']),
|
|
499
|
-
},
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
// Phase 9: sys_context tool is only available in sys mode.
|
|
503
|
-
if (opts?.sysMode) {
|
|
504
|
-
schemas.push(SYS_CONTEXT_SCHEMA);
|
|
505
|
-
}
|
|
506
|
-
if (opts?.lspTools) {
|
|
507
|
-
schemas.push({
|
|
508
|
-
type: 'function',
|
|
509
|
-
function: {
|
|
510
|
-
name: 'lsp_diagnostics',
|
|
511
|
-
description: 'Get LSP diagnostics (errors/warnings) for file or project.',
|
|
512
|
-
parameters: obj({ path: str(), severity: int() }, []),
|
|
513
|
-
},
|
|
514
|
-
}, {
|
|
515
|
-
type: 'function',
|
|
516
|
-
function: {
|
|
517
|
-
name: 'lsp_symbols',
|
|
518
|
-
description: 'List symbols (functions, classes, vars) in a file.',
|
|
519
|
-
parameters: obj({ path: str() }, ['path']),
|
|
520
|
-
},
|
|
521
|
-
}, {
|
|
522
|
-
type: 'function',
|
|
523
|
-
function: {
|
|
524
|
-
name: 'lsp_hover',
|
|
525
|
-
description: 'Get type/docs for symbol at position.',
|
|
526
|
-
parameters: obj({ path: str(), line: int(), character: int() }, [
|
|
527
|
-
'path',
|
|
528
|
-
'line',
|
|
529
|
-
'character',
|
|
530
|
-
]),
|
|
531
|
-
},
|
|
532
|
-
}, {
|
|
533
|
-
type: 'function',
|
|
534
|
-
function: {
|
|
535
|
-
name: 'lsp_definition',
|
|
536
|
-
description: 'Go to definition of symbol at position.',
|
|
537
|
-
parameters: obj({ path: str(), line: int(), character: int() }, [
|
|
538
|
-
'path',
|
|
539
|
-
'line',
|
|
540
|
-
'character',
|
|
541
|
-
]),
|
|
542
|
-
},
|
|
543
|
-
}, {
|
|
544
|
-
type: 'function',
|
|
545
|
-
function: {
|
|
546
|
-
name: 'lsp_references',
|
|
547
|
-
description: 'Find all references to symbol at position.',
|
|
548
|
-
parameters: obj({ path: str(), line: int(), character: int(), max_results: int() }, [
|
|
549
|
-
'path',
|
|
550
|
-
'line',
|
|
551
|
-
'character',
|
|
552
|
-
]),
|
|
553
|
-
},
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
if (opts?.mcpTools?.length) {
|
|
557
|
-
schemas.push(...opts.mcpTools);
|
|
558
|
-
}
|
|
559
|
-
return schemas;
|
|
560
|
-
}
|
|
561
|
-
function isReadOnlyTool(name) {
|
|
562
|
-
return (name === 'read_file' ||
|
|
563
|
-
name === 'read_files' ||
|
|
564
|
-
name === 'list_dir' ||
|
|
565
|
-
name === 'search_files' ||
|
|
566
|
-
name === 'vault_search' ||
|
|
567
|
-
name === 'sys_context');
|
|
568
|
-
}
|
|
569
|
-
/** Human-readable summary of what a blocked tool call would do. */
|
|
570
|
-
function planModeSummary(name, args) {
|
|
571
|
-
switch (name) {
|
|
572
|
-
case 'write_file':
|
|
573
|
-
return `write ${args.path ?? 'unknown'} (${typeof args.content === 'string' ? args.content.split('\n').length : '?'} lines)`;
|
|
574
|
-
case 'apply_patch':
|
|
575
|
-
return `apply patch to ${Array.isArray(args.files) ? args.files.length : '?'} file(s)`;
|
|
576
|
-
case 'edit_range':
|
|
577
|
-
return `edit ${args.path ?? 'unknown'} lines ${args.start_line ?? '?'}-${args.end_line ?? '?'}`;
|
|
578
|
-
case 'edit_file':
|
|
579
|
-
return `edit ${args.path ?? 'unknown'} (replace ${typeof args.old_text === 'string' ? args.old_text.split('\n').length : '?'} lines)`;
|
|
580
|
-
case 'insert_file':
|
|
581
|
-
return `insert into ${args.path ?? 'unknown'} at line ${args.line ?? '?'}`;
|
|
582
|
-
case 'exec':
|
|
583
|
-
return `run: ${typeof args.command === 'string' ? args.command.slice(0, 80) : 'unknown'}`;
|
|
584
|
-
case 'spawn_task':
|
|
585
|
-
return `spawn sub-agent task: ${typeof args.task === 'string' ? args.task.slice(0, 80) : 'unknown'}`;
|
|
586
|
-
case 'vault_note':
|
|
587
|
-
return `vault note: ${args.key ?? 'unknown'}`;
|
|
588
|
-
default:
|
|
589
|
-
return `${name}(${Object.keys(args).join(', ')})`;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
function userContentToText(content) {
|
|
593
|
-
if (typeof content === 'string')
|
|
594
|
-
return content;
|
|
595
|
-
return content
|
|
596
|
-
.filter((p) => p.type === 'text')
|
|
597
|
-
.map((p) => p.text)
|
|
598
|
-
.join('\n')
|
|
599
|
-
.trim();
|
|
600
|
-
}
|
|
601
|
-
function userDisallowsDelegation(content) {
|
|
602
|
-
const text = userContentToText(content).toLowerCase();
|
|
603
|
-
if (!text)
|
|
604
|
-
return false;
|
|
605
|
-
const mentionsDelegation = /\b(?:spawn[_\-\s]?task|sub[\-\s]?agents?|delegate|delegation)\b/.test(text);
|
|
606
|
-
if (!mentionsDelegation)
|
|
607
|
-
return false;
|
|
608
|
-
const negationNearDelegation = /\b(?:do not|don't|dont|no|without|avoid|skip|never)\b[^\n.]{0,90}\b(?:spawn[_\-\s]?task|sub[\-\s]?agents?|delegate|delegation)\b/.test(text) ||
|
|
609
|
-
/\b(?:spawn[_\-\s]?task|sub[\-\s]?agents?|delegate|delegation)\b[^\n.]{0,50}\b(?:do not|don't|dont|not allowed|forbidden|no)\b/.test(text);
|
|
610
|
-
return negationNearDelegation;
|
|
611
|
-
}
|
|
612
61
|
export async function createSession(opts) {
|
|
613
62
|
const cfg = opts.config;
|
|
614
63
|
const projectDir = cfg.dir ?? process.cwd();
|
|
@@ -1313,7 +762,7 @@ export async function createSession(opts) {
|
|
|
1313
762
|
else if (step.tool === 'spawn_task') {
|
|
1314
763
|
content = await runSpawnTaskCore(step.args, { signal: inFlight?.signal });
|
|
1315
764
|
}
|
|
1316
|
-
else if (
|
|
765
|
+
else if (isLspTool(step.tool) && lspManager) {
|
|
1317
766
|
content = await dispatchLspTool(step.tool, step.args);
|
|
1318
767
|
}
|
|
1319
768
|
else if (mcpManager?.hasTool(step.tool)) {
|
|
@@ -1968,10 +1417,39 @@ export async function createSession(opts) {
|
|
|
1968
1417
|
const hookObj = typeof hooks === 'function' ? { onToken: hooks } : (hooks ?? {});
|
|
1969
1418
|
let turns = 0;
|
|
1970
1419
|
let toolCalls = 0;
|
|
1420
|
+
const tokenEstimateCache = new WeakMap();
|
|
1421
|
+
const estimateTokensCached = (msgs) => {
|
|
1422
|
+
const key = msgs;
|
|
1423
|
+
const cached = tokenEstimateCache.get(key);
|
|
1424
|
+
if (cached !== undefined)
|
|
1425
|
+
return cached;
|
|
1426
|
+
const v = estimateTokensFromMessages(msgs);
|
|
1427
|
+
tokenEstimateCache.set(key, v);
|
|
1428
|
+
return v;
|
|
1429
|
+
};
|
|
1430
|
+
const perfEnabled = process.env.IDLEHANDS_PERF_TRACE === '1';
|
|
1431
|
+
const perf = {
|
|
1432
|
+
modelMs: 0,
|
|
1433
|
+
ttftMsSum: 0,
|
|
1434
|
+
ttftSamples: 0,
|
|
1435
|
+
compactions: 0,
|
|
1436
|
+
compactMs: 0,
|
|
1437
|
+
};
|
|
1971
1438
|
const askId = `ask-${timestampedId()}`;
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1439
|
+
const hooksEnabled = hookManager.isEnabled();
|
|
1440
|
+
const hasOnToolCall = Boolean(hookObj.onToolCall);
|
|
1441
|
+
const hasOnToolResult = Boolean(hookObj.onToolResult);
|
|
1442
|
+
const hasOnToolLoop = Boolean(hookObj.onToolLoop);
|
|
1443
|
+
const hasOnTurnEnd = Boolean(hookObj.onTurnEnd);
|
|
1444
|
+
const emitToolCall = async (id, name, args) => {
|
|
1445
|
+
if (!hasOnToolCall && !hooksEnabled)
|
|
1446
|
+
return;
|
|
1447
|
+
const call = { id, name, args };
|
|
1448
|
+
if (hasOnToolCall)
|
|
1449
|
+
hookObj.onToolCall?.(call);
|
|
1450
|
+
if (hooksEnabled) {
|
|
1451
|
+
await hookManager.emit('tool_call', { askId, turn: turns, call });
|
|
1452
|
+
}
|
|
1975
1453
|
};
|
|
1976
1454
|
const emitToolStream = (stream) => {
|
|
1977
1455
|
try {
|
|
@@ -1980,29 +1458,46 @@ export async function createSession(opts) {
|
|
|
1980
1458
|
catch {
|
|
1981
1459
|
// best effort
|
|
1982
1460
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1461
|
+
if (hooksEnabled) {
|
|
1462
|
+
try {
|
|
1463
|
+
void hookManager.emit('tool_stream', { askId, turn: turns, stream });
|
|
1464
|
+
}
|
|
1465
|
+
catch {
|
|
1466
|
+
// best effort
|
|
1467
|
+
}
|
|
1988
1468
|
}
|
|
1989
1469
|
};
|
|
1990
1470
|
const isReadOnlyToolDynamic = (toolName) => {
|
|
1991
1471
|
return (isReadOnlyTool(toolName) ||
|
|
1992
|
-
|
|
1472
|
+
isLspTool(toolName) ||
|
|
1993
1473
|
Boolean(mcpManager?.isToolReadOnly(toolName)));
|
|
1994
1474
|
};
|
|
1995
1475
|
const emitToolResult = async (result) => {
|
|
1996
|
-
|
|
1997
|
-
|
|
1476
|
+
if (!hasOnToolResult && !hooksEnabled)
|
|
1477
|
+
return;
|
|
1478
|
+
if (hasOnToolResult)
|
|
1479
|
+
await hookObj.onToolResult?.(result);
|
|
1480
|
+
if (hooksEnabled) {
|
|
1481
|
+
await hookManager.emit('tool_result', { askId, turn: turns, result });
|
|
1482
|
+
}
|
|
1998
1483
|
};
|
|
1999
1484
|
const emitToolLoop = async (loop) => {
|
|
2000
|
-
|
|
2001
|
-
|
|
1485
|
+
if (!hasOnToolLoop && !hooksEnabled)
|
|
1486
|
+
return;
|
|
1487
|
+
if (hasOnToolLoop)
|
|
1488
|
+
await hookObj.onToolLoop?.(loop);
|
|
1489
|
+
if (hooksEnabled) {
|
|
1490
|
+
await hookManager.emit('tool_loop', { askId, turn: turns, loop });
|
|
1491
|
+
}
|
|
2002
1492
|
};
|
|
2003
1493
|
const emitTurnEnd = async (stats) => {
|
|
2004
|
-
|
|
2005
|
-
|
|
1494
|
+
if (!hasOnTurnEnd && !hooksEnabled)
|
|
1495
|
+
return;
|
|
1496
|
+
if (hasOnTurnEnd)
|
|
1497
|
+
await hookObj.onTurnEnd?.(stats);
|
|
1498
|
+
if (hooksEnabled) {
|
|
1499
|
+
await hookManager.emit('turn_end', { askId, stats });
|
|
1500
|
+
}
|
|
2006
1501
|
};
|
|
2007
1502
|
const finalizeAsk = async (text) => {
|
|
2008
1503
|
const finalText = ensureInformativeAssistantText(text, { toolCalls, turns });
|
|
@@ -2034,13 +1529,20 @@ export async function createSession(opts) {
|
|
|
2034
1529
|
// best-effort — never block ask completion for summary persistence
|
|
2035
1530
|
}
|
|
2036
1531
|
}
|
|
2037
|
-
|
|
1532
|
+
if (hooksEnabled)
|
|
1533
|
+
await hookManager.emit('ask_end', { askId, text: finalText, turns, toolCalls });
|
|
1534
|
+
if (perfEnabled) {
|
|
1535
|
+
const wallMs = Date.now() - wallStart;
|
|
1536
|
+
const avgTtft = perf.ttftSamples > 0 ? Math.round(perf.ttftMsSum / perf.ttftSamples) : 0;
|
|
1537
|
+
console.error(`[perf] ask=${askId} turns=${turns} toolCalls=${toolCalls} wallMs=${wallMs} modelMs=${perf.modelMs} compactMs=${perf.compactMs} avgTTFTms=${avgTtft} compactions=${perf.compactions}`);
|
|
1538
|
+
}
|
|
2038
1539
|
return { text: finalText, turns, toolCalls };
|
|
2039
1540
|
};
|
|
2040
1541
|
const rawInstructionText = userContentToText(instruction).trim();
|
|
2041
1542
|
lastAskInstructionText = rawInstructionText;
|
|
2042
1543
|
lastCompactionReminderObjective = '';
|
|
2043
|
-
|
|
1544
|
+
if (hooksEnabled)
|
|
1545
|
+
await hookManager.emit('ask_start', { askId, instruction: rawInstructionText });
|
|
2044
1546
|
const reviewKeys = reviewArtifactKeys(projectDir);
|
|
2045
1547
|
const retrievalRequested = looksLikeReviewRetrievalRequest(rawInstructionText);
|
|
2046
1548
|
const shouldPersistReviewArtifact = looksLikeCodeReviewRequest(rawInstructionText) && !retrievalRequested;
|
|
@@ -2313,15 +1815,17 @@ export async function createSession(opts) {
|
|
|
2313
1815
|
if (inFlight?.signal?.aborted)
|
|
2314
1816
|
break;
|
|
2315
1817
|
turns++;
|
|
2316
|
-
|
|
1818
|
+
if (hooksEnabled)
|
|
1819
|
+
await hookManager.emit('turn_start', { askId, turn: turns });
|
|
2317
1820
|
const wallElapsed = (Date.now() - wallStart) / 1000;
|
|
2318
1821
|
if (wallElapsed > cfg.timeout) {
|
|
2319
1822
|
throw new Error(`session timeout exceeded (${cfg.timeout}s) after ${wallElapsed.toFixed(1)}s`);
|
|
2320
1823
|
}
|
|
2321
1824
|
await maybeAutoDetectModelChange();
|
|
1825
|
+
const compactionStartMs = Date.now();
|
|
2322
1826
|
await runCompactionWithLock('auto context-budget compaction', async () => {
|
|
2323
1827
|
const beforeMsgs = messages;
|
|
2324
|
-
const beforeTokens =
|
|
1828
|
+
const beforeTokens = estimateTokensCached(beforeMsgs);
|
|
2325
1829
|
const compacted = enforceContextBudget({
|
|
2326
1830
|
messages: beforeMsgs,
|
|
2327
1831
|
contextWindow,
|
|
@@ -2330,8 +1834,15 @@ export async function createSession(opts) {
|
|
|
2330
1834
|
compactAt: cfg.compact_at ?? 0.8,
|
|
2331
1835
|
toolSchemaTokens: estimateToolSchemaTokens(getToolsSchema()),
|
|
2332
1836
|
});
|
|
2333
|
-
|
|
2334
|
-
|
|
1837
|
+
let dropped;
|
|
1838
|
+
if (compacted.length === beforeMsgs.length) {
|
|
1839
|
+
// Fast path: no drops expected when lengths match.
|
|
1840
|
+
dropped = [];
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
const compactedByRefs = new Set(compacted);
|
|
1844
|
+
dropped = beforeMsgs.filter((m) => !compactedByRefs.has(m));
|
|
1845
|
+
}
|
|
2335
1846
|
if (dropped.length && vault) {
|
|
2336
1847
|
try {
|
|
2337
1848
|
// Store the original/current user prompt before compaction so it survives context loss.
|
|
@@ -2366,7 +1877,10 @@ export async function createSession(opts) {
|
|
|
2366
1877
|
messages = compacted;
|
|
2367
1878
|
let summaryUsed = false;
|
|
2368
1879
|
if (dropped.length) {
|
|
2369
|
-
|
|
1880
|
+
let droppedTokens = 0;
|
|
1881
|
+
if (cfg.compact_summary !== false) {
|
|
1882
|
+
droppedTokens = estimateTokensCached(dropped);
|
|
1883
|
+
}
|
|
2370
1884
|
if (cfg.compact_summary !== false && droppedTokens > 200) {
|
|
2371
1885
|
try {
|
|
2372
1886
|
const summaryContent = buildCompactionSummaryPrompt(dropped);
|
|
@@ -2414,11 +1928,12 @@ export async function createSession(opts) {
|
|
|
2414
1928
|
}
|
|
2415
1929
|
}
|
|
2416
1930
|
// Update token count AFTER injections so downstream reads are accurate
|
|
2417
|
-
currentContextTokens =
|
|
2418
|
-
const afterTokens =
|
|
1931
|
+
currentContextTokens = estimateTokensCached(messages);
|
|
1932
|
+
const afterTokens = estimateTokensCached(compacted);
|
|
2419
1933
|
const freedTokens = Math.max(0, beforeTokens - afterTokens);
|
|
2420
1934
|
// Emit compaction event for callers (e.g. Anton controller → Discord)
|
|
2421
1935
|
if (dropped.length) {
|
|
1936
|
+
perf.compactions++;
|
|
2422
1937
|
try {
|
|
2423
1938
|
await hookObj.onCompaction?.({
|
|
2424
1939
|
droppedMessages: dropped.length,
|
|
@@ -2440,6 +1955,7 @@ export async function createSession(opts) {
|
|
|
2440
1955
|
dryRun: false,
|
|
2441
1956
|
};
|
|
2442
1957
|
});
|
|
1958
|
+
perf.compactMs += Date.now() - compactionStartMs;
|
|
2443
1959
|
const ac = makeAbortController();
|
|
2444
1960
|
inFlight = ac;
|
|
2445
1961
|
// If caller provided an AbortSignal (bench iteration timeout, etc), propagate it.
|
|
@@ -2511,6 +2027,11 @@ export async function createSession(opts) {
|
|
|
2511
2027
|
inFlight = null;
|
|
2512
2028
|
}
|
|
2513
2029
|
const ttcMs = Date.now() - turnStartMs;
|
|
2030
|
+
perf.modelMs += ttcMs;
|
|
2031
|
+
if (ttftMs !== undefined) {
|
|
2032
|
+
perf.ttftMsSum += ttftMs;
|
|
2033
|
+
perf.ttftSamples++;
|
|
2034
|
+
}
|
|
2514
2035
|
const promptTokensTurn = resp.usage?.prompt_tokens ?? 0;
|
|
2515
2036
|
const completionTokensTurn = resp.usage?.completion_tokens ?? 0;
|
|
2516
2037
|
// Track server-reported usage when available
|
|
@@ -2743,7 +2264,7 @@ export async function createSession(opts) {
|
|
|
2743
2264
|
},
|
|
2744
2265
|
});
|
|
2745
2266
|
// Tool-call argument parsing and validation logic
|
|
2746
|
-
const fileMutationsInTurn = toolCallsArr.filter((tc) =>
|
|
2267
|
+
const fileMutationsInTurn = toolCallsArr.filter((tc) => isMutationTool(tc.function?.name)).length;
|
|
2747
2268
|
if (fileMutationsInTurn >= 3 && isGitDirty(ctx.cwd)) {
|
|
2748
2269
|
const shouldStash = confirmBridge
|
|
2749
2270
|
? await confirmBridge(`Working tree is dirty and the agent plans ${fileMutationsInTurn} file edits. Stash current changes first? [Y/n]`, { tool: 'git_stash', args: { fileMutationsInTurn } })
|
|
@@ -3014,10 +2535,10 @@ export async function createSession(opts) {
|
|
|
3014
2535
|
]);
|
|
3015
2536
|
}
|
|
3016
2537
|
const builtInFn = tools[name];
|
|
3017
|
-
const
|
|
2538
|
+
const lspToolCall = isLspTool(name);
|
|
3018
2539
|
const isSpawnTask = name === 'spawn_task';
|
|
3019
2540
|
const hasMcpTool = mcpManager?.hasTool(name) === true;
|
|
3020
|
-
if (!builtInFn && !
|
|
2541
|
+
if (!builtInFn && !lspToolCall && !hasMcpTool && !isSpawnTask)
|
|
3021
2542
|
throw new Error(`unknown tool: ${name}`);
|
|
3022
2543
|
// Keep parsed args by call-id so we can digest/archive tool outputs with context.
|
|
3023
2544
|
toolArgsByCallId.set(callId, args && typeof args === 'object' && !Array.isArray(args) ? args : {});
|
|
@@ -3051,7 +2572,7 @@ export async function createSession(opts) {
|
|
|
3051
2572
|
throw new Error(`exec: ${reason} — command: ${args.command}`);
|
|
3052
2573
|
}
|
|
3053
2574
|
}
|
|
3054
|
-
if (
|
|
2575
|
+
if (isMutationTool(name) && typeof args.path === 'string') {
|
|
3055
2576
|
const absPath = args.path.startsWith('/')
|
|
3056
2577
|
? args.path
|
|
3057
2578
|
: path.resolve(projectDir, args.path);
|
|
@@ -3075,7 +2596,7 @@ export async function createSession(opts) {
|
|
|
3075
2596
|
const searchTerm = typeof args.search === 'string' ? args.search : '';
|
|
3076
2597
|
// Fix 1: Hard cumulative budget — refuse reads past hard cap
|
|
3077
2598
|
if (cumulativeReadOnlyCalls > READ_BUDGET_HARD) {
|
|
3078
|
-
await emitToolCall(
|
|
2599
|
+
await emitToolCall(callId, name, args);
|
|
3079
2600
|
await emitToolResult({
|
|
3080
2601
|
id: callId,
|
|
3081
2602
|
name,
|
|
@@ -3102,7 +2623,7 @@ export async function createSession(opts) {
|
|
|
3102
2623
|
blockedDirs.add(parentDir);
|
|
3103
2624
|
}
|
|
3104
2625
|
if (blockedDirs.has(parentDir) && uniqueCount > 8) {
|
|
3105
|
-
await emitToolCall(
|
|
2626
|
+
await emitToolCall(callId, name, args);
|
|
3106
2627
|
await emitToolResult({
|
|
3107
2628
|
id: callId,
|
|
3108
2629
|
name,
|
|
@@ -3123,7 +2644,7 @@ export async function createSession(opts) {
|
|
|
3123
2644
|
searchTermFiles.set(key, new Set());
|
|
3124
2645
|
searchTermFiles.get(key).add(filePath);
|
|
3125
2646
|
if (searchTermFiles.get(key).size >= 3) {
|
|
3126
|
-
await emitToolCall(
|
|
2647
|
+
await emitToolCall(callId, name, args);
|
|
3127
2648
|
await emitToolResult({
|
|
3128
2649
|
id: callId,
|
|
3129
2650
|
name,
|
|
@@ -3159,7 +2680,7 @@ export async function createSession(opts) {
|
|
|
3159
2680
|
reason: `plan mode: ${summary}`,
|
|
3160
2681
|
});
|
|
3161
2682
|
// Hook: onToolCall + onToolResult for plan-blocked actions
|
|
3162
|
-
await emitToolCall(
|
|
2683
|
+
await emitToolCall(callId, name, args);
|
|
3163
2684
|
await emitToolResult({
|
|
3164
2685
|
id: callId,
|
|
3165
2686
|
name,
|
|
@@ -3170,7 +2691,7 @@ export async function createSession(opts) {
|
|
|
3170
2691
|
return { id: callId, content: blockedMsg };
|
|
3171
2692
|
}
|
|
3172
2693
|
// Hook: onToolCall (Phase 8.5)
|
|
3173
|
-
await emitToolCall(
|
|
2694
|
+
await emitToolCall(callId, name, args);
|
|
3174
2695
|
if (cfg.step_mode) {
|
|
3175
2696
|
const stepPrompt = `Step mode: execute ${name}(${JSON.stringify(args).slice(0, 200)}) ? [Y/n]`;
|
|
3176
2697
|
const ok = confirmBridge
|
|
@@ -3259,7 +2780,7 @@ export async function createSession(opts) {
|
|
|
3259
2780
|
}
|
|
3260
2781
|
}
|
|
3261
2782
|
}
|
|
3262
|
-
else if (isLspTool && lspManager) {
|
|
2783
|
+
else if (isLspTool(name) && lspManager) {
|
|
3263
2784
|
// LSP tool dispatch
|
|
3264
2785
|
content = await dispatchLspTool(name, args);
|
|
3265
2786
|
}
|
|
@@ -3325,7 +2846,7 @@ export async function createSession(opts) {
|
|
|
3325
2846
|
if (lines.length > 0)
|
|
3326
2847
|
resultEvent.searchMatches = lines.slice(0, 20);
|
|
3327
2848
|
}
|
|
3328
|
-
else if (
|
|
2849
|
+
else if (isMutationTool(name) && replay) {
|
|
3329
2850
|
// Grab the most recent checkpoint for a diff preview
|
|
3330
2851
|
try {
|
|
3331
2852
|
const cps = await replay.list(1);
|
|
@@ -3348,7 +2869,7 @@ export async function createSession(opts) {
|
|
|
3348
2869
|
await emitToolResult(resultEvent);
|
|
3349
2870
|
// Proactive LSP diagnostics after file mutations
|
|
3350
2871
|
if (lspManager?.hasServers() && lspCfg?.proactive_diagnostics !== false) {
|
|
3351
|
-
if (
|
|
2872
|
+
if (isMutationTool(name)) {
|
|
3352
2873
|
const mutatedPath = typeof args.path === 'string' ? args.path : '';
|
|
3353
2874
|
if (mutatedPath) {
|
|
3354
2875
|
try {
|
|
@@ -3380,7 +2901,7 @@ export async function createSession(opts) {
|
|
|
3380
2901
|
// ── Per-file mutation spiral detection ──
|
|
3381
2902
|
// Track edits to the same file. If the model keeps editing the same file
|
|
3382
2903
|
// over and over, it's likely in an edit→break→read→edit corruption spiral.
|
|
3383
|
-
if (
|
|
2904
|
+
if (isMutationTool(name) && toolSuccess && typeof args.path === 'string') {
|
|
3384
2905
|
const absPath = args.path.startsWith('/')
|
|
3385
2906
|
? args.path
|
|
3386
2907
|
: path.resolve(projectDir, args.path);
|
|
@@ -3550,7 +3071,7 @@ export async function createSession(opts) {
|
|
|
3550
3071
|
}
|
|
3551
3072
|
catch (e) {
|
|
3552
3073
|
results.push(await catchToolError(e, tc));
|
|
3553
|
-
if (
|
|
3074
|
+
if (isMutationTool(tc.function.name)) {
|
|
3554
3075
|
// Fail-fast: after mutating tool failure, stop the remaining batch.
|
|
3555
3076
|
break;
|
|
3556
3077
|
}
|
|
@@ -3568,7 +3089,7 @@ export async function createSession(opts) {
|
|
|
3568
3089
|
}
|
|
3569
3090
|
catch (e) {
|
|
3570
3091
|
results.push(await catchToolError(e, tc));
|
|
3571
|
-
if (
|
|
3092
|
+
if (isMutationTool(tc.function.name)) {
|
|
3572
3093
|
// Fail-fast: after mutating tool failure, stop the remaining batch.
|
|
3573
3094
|
break;
|
|
3574
3095
|
}
|
|
@@ -3797,12 +3318,13 @@ export async function createSession(opts) {
|
|
|
3797
3318
|
})();
|
|
3798
3319
|
const err = new Error(`BUG: threw undefined in agent.ask() (turn=${turns}). lastMsg=${lastMsg?.role ?? 'unknown'}:${lastMsgPreview}`);
|
|
3799
3320
|
await persistFailure(err, `ask turn ${turns}`);
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3321
|
+
if (hooksEnabled)
|
|
3322
|
+
await hookManager.emit('ask_error', {
|
|
3323
|
+
askId,
|
|
3324
|
+
error: err.message,
|
|
3325
|
+
turns,
|
|
3326
|
+
toolCalls,
|
|
3327
|
+
});
|
|
3806
3328
|
throw err;
|
|
3807
3329
|
}
|
|
3808
3330
|
await persistFailure(e, `ask turn ${turns}`);
|
|
@@ -3813,12 +3335,13 @@ export async function createSession(opts) {
|
|
|
3813
3335
|
// Never rethrow undefined; normalize to Error for debuggability.
|
|
3814
3336
|
if (e === undefined) {
|
|
3815
3337
|
const normalized = new Error('BUG: threw undefined (normalized at ask() boundary)');
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3338
|
+
if (hooksEnabled)
|
|
3339
|
+
await hookManager.emit('ask_error', {
|
|
3340
|
+
askId,
|
|
3341
|
+
error: normalized.message,
|
|
3342
|
+
turns,
|
|
3343
|
+
toolCalls,
|
|
3344
|
+
});
|
|
3822
3345
|
throw normalized;
|
|
3823
3346
|
}
|
|
3824
3347
|
await hookManager.emit('ask_error', {
|
|
@@ -3918,21 +3441,4 @@ export async function runAgent(opts) {
|
|
|
3918
3441
|
});
|
|
3919
3442
|
return session.ask(opts.instruction, opts.onToken);
|
|
3920
3443
|
}
|
|
3921
|
-
async function autoPickModel(client, cached) {
|
|
3922
|
-
const ac = makeAbortController();
|
|
3923
|
-
const timer = setTimeout(() => ac.abort(), 3000);
|
|
3924
|
-
try {
|
|
3925
|
-
const models = cached ?? normalizeModelsResponse(await client.models(ac.signal));
|
|
3926
|
-
const q = models.data.find((m) => /qwen/i.test(m.id));
|
|
3927
|
-
if (q)
|
|
3928
|
-
return q.id;
|
|
3929
|
-
const first = models.data[0]?.id;
|
|
3930
|
-
if (!first)
|
|
3931
|
-
throw new Error('No models found on server. Check your endpoint and that a model is loaded.');
|
|
3932
|
-
return first;
|
|
3933
|
-
}
|
|
3934
|
-
finally {
|
|
3935
|
-
clearTimeout(timer);
|
|
3936
|
-
}
|
|
3937
|
-
}
|
|
3938
3444
|
//# sourceMappingURL=agent.js.map
|