@visorcraft/idlehands 1.1.7 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/agent/formatting.js +251 -0
- package/dist/agent/formatting.js.map +1 -0
- package/dist/agent/review-artifact.js +147 -0
- package/dist/agent/review-artifact.js.map +1 -0
- package/dist/agent/tool-calls.js +226 -0
- package/dist/agent/tool-calls.js.map +1 -0
- package/dist/agent.js +186 -668
- package/dist/agent.js.map +1 -1
- package/dist/anton/controller.js +1 -1
- package/dist/anton/controller.js.map +1 -1
- package/dist/anton/lock.js +0 -3
- package/dist/anton/lock.js.map +1 -1
- package/dist/anton/parser.js +0 -1
- package/dist/anton/parser.js.map +1 -1
- package/dist/anton/reporter.js +1 -1
- package/dist/anton/reporter.js.map +1 -1
- package/dist/bot/commands.js +3 -2
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/confirm-telegram.js +2 -1
- package/dist/bot/confirm-telegram.js.map +1 -1
- package/dist/bot/discord-routing.js +179 -0
- package/dist/bot/discord-routing.js.map +1 -0
- package/dist/bot/discord-streaming.js +171 -0
- package/dist/bot/discord-streaming.js.map +1 -0
- package/dist/bot/discord.js +25 -221
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/format.js +2 -25
- package/dist/bot/format.js.map +1 -1
- package/dist/bot/telegram.js +56 -12
- package/dist/bot/telegram.js.map +1 -1
- package/dist/cli/build-repl-context.js.map +1 -1
- package/dist/cli/command-registry.js +2 -1
- package/dist/cli/command-registry.js.map +1 -1
- package/dist/cli/command-utils.js +27 -0
- package/dist/cli/command-utils.js.map +1 -0
- package/dist/cli/commands/anton.js +3 -2
- package/dist/cli/commands/anton.js.map +1 -1
- package/dist/cli/commands/model.js +8 -7
- package/dist/cli/commands/model.js.map +1 -1
- package/dist/cli/commands/project.js +5 -4
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/session.js +9 -8
- package/dist/cli/commands/session.js.map +1 -1
- package/dist/cli/commands/tools.js +4 -3
- package/dist/cli/commands/tools.js.map +1 -1
- package/dist/cli/input.js +2 -1
- package/dist/cli/input.js.map +1 -1
- package/dist/cli/repl-dispatch.js +85 -0
- package/dist/cli/repl-dispatch.js.map +1 -0
- package/dist/cli/runtime-cmds.js +7 -7
- package/dist/cli/runtime-cmds.js.map +1 -1
- package/dist/cli/service.js +0 -14
- package/dist/cli/service.js.map +1 -1
- package/dist/cli/setup.js +3 -3
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/watch.js +2 -1
- package/dist/cli/watch.js.map +1 -1
- package/dist/client.js +0 -1
- package/dist/client.js.map +1 -1
- package/dist/context.js +101 -10
- package/dist/context.js.map +1 -1
- package/dist/harnesses.js +1 -1
- package/dist/harnesses.js.map +1 -1
- package/dist/hooks/manager.js +5 -0
- package/dist/hooks/manager.js.map +1 -1
- package/dist/index.js +13 -64
- package/dist/index.js.map +1 -1
- package/dist/progress/agent-hooks.js +37 -0
- package/dist/progress/agent-hooks.js.map +1 -0
- package/dist/progress/ir.js +7 -0
- package/dist/progress/ir.js.map +1 -0
- package/dist/progress/progress-message-renderer.js +63 -0
- package/dist/progress/progress-message-renderer.js.map +1 -0
- package/dist/progress/serialize-discord.js +60 -0
- package/dist/progress/serialize-discord.js.map +1 -0
- package/dist/progress/serialize-telegram.js +55 -0
- package/dist/progress/serialize-telegram.js.map +1 -0
- package/dist/progress/serialize-tui.js +39 -0
- package/dist/progress/serialize-tui.js.map +1 -0
- package/dist/progress/tool-summary.js +58 -0
- package/dist/progress/tool-summary.js.map +1 -0
- package/dist/progress/tool-tail.js +48 -0
- package/dist/progress/tool-tail.js.map +1 -0
- package/dist/progress/turn-progress.js +215 -0
- package/dist/progress/turn-progress.js.map +1 -0
- package/dist/replay.js +2 -5
- package/dist/replay.js.map +1 -1
- package/dist/safety.js +0 -1
- package/dist/safety.js.map +1 -1
- package/dist/spinner.js +8 -0
- package/dist/spinner.js.map +1 -1
- package/dist/tools.js +422 -29
- package/dist/tools.js.map +1 -1
- package/dist/tui/branch-picker.js.map +1 -1
- package/dist/tui/command-handler.js.map +1 -1
- package/dist/tui/controller.js +89 -28
- package/dist/tui/controller.js.map +1 -1
- package/dist/tui/render.js +15 -2
- package/dist/tui/render.js.map +1 -1
- package/dist/tui/state.js +13 -0
- package/dist/tui/state.js.map +1 -1
- package/dist/upgrade.js.map +1 -1
- package/dist/utils.js +17 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -14,123 +14,18 @@ import { LensStore } from './lens.js';
|
|
|
14
14
|
import { SYS_CONTEXT_SCHEMA, collectSnapshot } from './sys/context.js';
|
|
15
15
|
import { MCPManager } from './mcp.js';
|
|
16
16
|
import { LspManager, detectInstalledLspServers } from './lsp.js';
|
|
17
|
+
import { generateMinimalDiff, toolResultSummary, execCommandFromSig, formatDurationMs, looksLikePlanningNarration, capTextByApproxTokens, isLikelyBinaryBuffer, sanitizePathsInMessage, digestToolResult, } from './agent/formatting.js';
|
|
18
|
+
import { parseToolCallsFromContent, getMissingRequiredParams, stripMarkdownFences } from './agent/tool-calls.js';
|
|
19
|
+
export { parseToolCallsFromContent };
|
|
20
|
+
import { reviewArtifactKeys, looksLikeCodeReviewRequest, looksLikeReviewRetrievalRequest, retrievalAllowsStaleArtifact, parseReviewArtifactStalePolicy, parseReviewArtifact, reviewArtifactStaleReason, gitHead, normalizeModelsResponse, } from './agent/review-artifact.js';
|
|
17
21
|
import fs from 'node:fs/promises';
|
|
18
22
|
import path from 'node:path';
|
|
19
|
-
import {
|
|
20
|
-
import { stateDir, BASH_PATH as BASH } from './utils.js';
|
|
23
|
+
import { stateDir, timestampedId } from './utils.js';
|
|
21
24
|
function makeAbortController() {
|
|
22
25
|
// Node 24: AbortController is global.
|
|
23
26
|
return new AbortController();
|
|
24
27
|
}
|
|
25
|
-
/** Generate a minimal unified diff for Phase 7 rich display (max 20 lines, truncated). */
|
|
26
|
-
function generateMinimalDiff(before, after, filePath) {
|
|
27
|
-
const bLines = before.split('\n');
|
|
28
|
-
const aLines = after.split('\n');
|
|
29
|
-
const out = [];
|
|
30
|
-
out.push(`--- a/${filePath}`);
|
|
31
|
-
out.push(`+++ b/${filePath}`);
|
|
32
|
-
// Simple line-by-line diff (find changed region)
|
|
33
|
-
let diffStart = 0;
|
|
34
|
-
while (diffStart < bLines.length && diffStart < aLines.length && bLines[diffStart] === aLines[diffStart])
|
|
35
|
-
diffStart++;
|
|
36
|
-
let bEnd = bLines.length - 1;
|
|
37
|
-
let aEnd = aLines.length - 1;
|
|
38
|
-
while (bEnd > diffStart && aEnd > diffStart && bLines[bEnd] === aLines[aEnd]) {
|
|
39
|
-
bEnd--;
|
|
40
|
-
aEnd--;
|
|
41
|
-
}
|
|
42
|
-
const contextBefore = Math.max(0, diffStart - 2);
|
|
43
|
-
const contextAfter = Math.min(Math.max(bLines.length, aLines.length) - 1, Math.max(bEnd, aEnd) + 2);
|
|
44
|
-
const bEndContext = Math.min(bLines.length - 1, contextAfter);
|
|
45
|
-
const aEndContext = Math.min(aLines.length - 1, contextAfter);
|
|
46
|
-
out.push(`@@ -${contextBefore + 1},${bEndContext - contextBefore + 1} +${contextBefore + 1},${aEndContext - contextBefore + 1} @@`);
|
|
47
|
-
let lineCount = 0;
|
|
48
|
-
const MAX_LINES = 20;
|
|
49
|
-
// Context before change
|
|
50
|
-
for (let i = contextBefore; i < diffStart && lineCount < MAX_LINES; i++) {
|
|
51
|
-
out.push(` ${bLines[i]}`);
|
|
52
|
-
lineCount++;
|
|
53
|
-
}
|
|
54
|
-
// Removed lines
|
|
55
|
-
for (let i = diffStart; i <= bEnd && i < bLines.length && lineCount < MAX_LINES; i++) {
|
|
56
|
-
out.push(`-${bLines[i]}`);
|
|
57
|
-
lineCount++;
|
|
58
|
-
}
|
|
59
|
-
// Added lines
|
|
60
|
-
for (let i = diffStart; i <= aEnd && i < aLines.length && lineCount < MAX_LINES; i++) {
|
|
61
|
-
out.push(`+${aLines[i]}`);
|
|
62
|
-
lineCount++;
|
|
63
|
-
}
|
|
64
|
-
// Context after change
|
|
65
|
-
const afterStart = Math.max(bEnd, aEnd) + 1;
|
|
66
|
-
for (let i = afterStart; i <= contextAfter && i < Math.max(bLines.length, aLines.length) && lineCount < MAX_LINES; i++) {
|
|
67
|
-
const line = i < aLines.length ? aLines[i] : bLines[i] ?? '';
|
|
68
|
-
out.push(` ${line}`);
|
|
69
|
-
lineCount++;
|
|
70
|
-
}
|
|
71
|
-
const totalChanges = (bEnd - diffStart + 1) + (aEnd - diffStart + 1);
|
|
72
|
-
if (lineCount >= MAX_LINES && totalChanges > MAX_LINES) {
|
|
73
|
-
out.push(`[+${totalChanges - MAX_LINES} more lines]`);
|
|
74
|
-
}
|
|
75
|
-
return out.join('\n');
|
|
76
|
-
}
|
|
77
|
-
/** Generate a one-line summary of a tool result for hooks/display. */
|
|
78
|
-
function toolResultSummary(name, args, content, success) {
|
|
79
|
-
if (!success)
|
|
80
|
-
return content.slice(0, 120);
|
|
81
|
-
switch (name) {
|
|
82
|
-
case 'read_file':
|
|
83
|
-
case 'read_files': {
|
|
84
|
-
const lines = content.split('\n').length;
|
|
85
|
-
return `${lines} lines read`;
|
|
86
|
-
}
|
|
87
|
-
case 'write_file':
|
|
88
|
-
return `wrote ${args.path || 'file'}`;
|
|
89
|
-
case 'edit_file':
|
|
90
|
-
return content.startsWith('ERROR') ? content.slice(0, 120) : `applied edit`;
|
|
91
|
-
case 'insert_file':
|
|
92
|
-
return `inserted at line ${args.line ?? '?'}`;
|
|
93
|
-
case 'exec': {
|
|
94
|
-
try {
|
|
95
|
-
const r = JSON.parse(content);
|
|
96
|
-
const lines = (r.out || '').split('\n').filter(Boolean).length;
|
|
97
|
-
return `rc=${r.rc}, ${lines} lines`;
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return content.slice(0, 80);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
case 'list_dir': {
|
|
104
|
-
const entries = content.split('\n').filter(Boolean).length;
|
|
105
|
-
return `${entries} entries`;
|
|
106
|
-
}
|
|
107
|
-
case 'search_files': {
|
|
108
|
-
const matches = (content.match(/^\d+:/gm) || []).length;
|
|
109
|
-
return `${matches} matches`;
|
|
110
|
-
}
|
|
111
|
-
case 'spawn_task': {
|
|
112
|
-
const line = content.split(/\r?\n/).find((l) => l.includes('status='));
|
|
113
|
-
return line ? line.trim() : 'sub-agent task finished';
|
|
114
|
-
}
|
|
115
|
-
case 'vault_search':
|
|
116
|
-
return `vault results`;
|
|
117
|
-
default:
|
|
118
|
-
return content.slice(0, 80);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
28
|
const CACHED_EXEC_OBSERVATION_HINT = '[idlehands hint] Reused cached output for repeated read-only exec call (unchanged observation).';
|
|
122
|
-
function execCommandFromSig(sig) {
|
|
123
|
-
if (!sig.startsWith('exec:'))
|
|
124
|
-
return '';
|
|
125
|
-
const raw = sig.slice('exec:'.length);
|
|
126
|
-
try {
|
|
127
|
-
const parsed = JSON.parse(raw);
|
|
128
|
-
return typeof parsed?.command === 'string' ? parsed.command : '';
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
return '';
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
29
|
function looksLikeReadOnlyExecCommand(command) {
|
|
135
30
|
const cmd = String(command || '').trim().toLowerCase();
|
|
136
31
|
if (!cmd)
|
|
@@ -203,7 +98,7 @@ Rules:
|
|
|
203
98
|
- Never use spawn_task to bypass confirmation/safety restrictions (for example blocked package installs). If a command is blocked, adapt the plan or ask the user for approval mode changes.
|
|
204
99
|
- Read the target file before editing. You need the exact text for search/replace.
|
|
205
100
|
- Use read_file with search=... to jump to relevant code; avoid reading whole files.
|
|
206
|
-
-
|
|
101
|
+
- Prefer apply_patch or edit_range for code edits (token-efficient). Use edit_file only when exact old_text replacement is necessary.
|
|
207
102
|
- Use insert_file for insertions (prepend/append/line).
|
|
208
103
|
- Use exec to run commands, tests, builds; check results before reporting success.
|
|
209
104
|
- When running commands in a subdirectory, use exec's cwd parameter — NOT "cd /path && cmd". Each exec call is a fresh shell; cd does not persist.
|
|
@@ -230,7 +125,7 @@ const DEFAULT_SUB_AGENT_RESULT_TOKEN_CAP = 4000;
|
|
|
230
125
|
const APPROVAL_MODE_SET = new Set(['plan', 'reject', 'default', 'auto-edit', 'yolo']);
|
|
231
126
|
const LSP_TOOL_NAMES = ['lsp_diagnostics', 'lsp_symbols', 'lsp_hover', 'lsp_definition', 'lsp_references'];
|
|
232
127
|
const LSP_TOOL_NAME_SET = new Set(LSP_TOOL_NAMES);
|
|
233
|
-
const FILE_MUTATION_TOOL_SET = new Set(['edit_file', 'write_file', 'insert_file']);
|
|
128
|
+
const FILE_MUTATION_TOOL_SET = new Set(['edit_file', 'edit_range', 'apply_patch', 'write_file', 'insert_file']);
|
|
234
129
|
function normalizeApprovalMode(value) {
|
|
235
130
|
if (typeof value !== 'string')
|
|
236
131
|
return undefined;
|
|
@@ -246,66 +141,6 @@ const APPROVAL_MODE_RANK = { plan: 0, reject: 1, default: 2, 'auto-edit': 3, yol
|
|
|
246
141
|
function capApprovalMode(requested, parentMode) {
|
|
247
142
|
return APPROVAL_MODE_RANK[requested] <= APPROVAL_MODE_RANK[parentMode] ? requested : parentMode;
|
|
248
143
|
}
|
|
249
|
-
function formatDurationMs(ms) {
|
|
250
|
-
if (!Number.isFinite(ms) || ms <= 0)
|
|
251
|
-
return '0.0s';
|
|
252
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
253
|
-
}
|
|
254
|
-
function looksLikePlanningNarration(text, finishReason) {
|
|
255
|
-
const s = String(text ?? '').trim().toLowerCase();
|
|
256
|
-
if (!s)
|
|
257
|
-
return false;
|
|
258
|
-
// Incomplete streamed answer: likely still needs another turn.
|
|
259
|
-
if (finishReason === 'length')
|
|
260
|
-
return true;
|
|
261
|
-
// Strong completion cues: treat as final answer.
|
|
262
|
-
if (/(^|\n)\s*(done|completed|finished|final answer|summary:)\b/.test(s))
|
|
263
|
-
return false;
|
|
264
|
-
// Typical "thinking out loud"/plan chatter that should continue with tools.
|
|
265
|
-
return /\b(let me|i(?:'|’)ll|i will|i'm going to|i am going to|next i(?:'|’)ll|first i(?:'|’)ll|i need to|i should|checking|reviewing|exploring|starting by)\b/.test(s);
|
|
266
|
-
}
|
|
267
|
-
function approxTokenCharCap(maxTokens) {
|
|
268
|
-
const safe = Math.max(64, Math.floor(maxTokens));
|
|
269
|
-
return safe * 4;
|
|
270
|
-
}
|
|
271
|
-
function capTextByApproxTokens(text, maxTokens) {
|
|
272
|
-
const raw = String(text ?? '');
|
|
273
|
-
const maxChars = approxTokenCharCap(maxTokens);
|
|
274
|
-
if (raw.length <= maxChars)
|
|
275
|
-
return { text: raw, truncated: false };
|
|
276
|
-
const clipped = raw.slice(0, maxChars);
|
|
277
|
-
return {
|
|
278
|
-
text: `${clipped}\n\n[sub-agent] result truncated to ~${maxTokens} tokens (${raw.length} chars original)`,
|
|
279
|
-
truncated: true,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
function isLikelyBinaryBuffer(buf) {
|
|
283
|
-
const n = Math.min(buf.length, 512);
|
|
284
|
-
for (let i = 0; i < n; i++) {
|
|
285
|
-
if (buf[i] === 0)
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Strip absolute paths from a message to prevent cross-project leaks in vault.
|
|
292
|
-
* Paths within cwd are replaced with relative equivalents; other absolute paths
|
|
293
|
-
* are replaced with just the basename.
|
|
294
|
-
*/
|
|
295
|
-
function sanitizePathsInMessage(message, cwd) {
|
|
296
|
-
const normCwd = cwd.replace(/\/+$/, '');
|
|
297
|
-
// Match absolute Unix paths (at least 2 segments)
|
|
298
|
-
return message.replace(/\/(?:home|tmp|var|usr|opt|etc|root)\/[^\s"',;)\]}>]+/g, (match) => {
|
|
299
|
-
const normMatch = match.replace(/\/+$/, '');
|
|
300
|
-
if (normMatch.startsWith(normCwd + '/')) {
|
|
301
|
-
// Within cwd — make relative
|
|
302
|
-
return normMatch.slice(normCwd.length + 1);
|
|
303
|
-
}
|
|
304
|
-
// Outside cwd — strip to basename
|
|
305
|
-
const base = path.basename(normMatch);
|
|
306
|
-
return base || match;
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
144
|
async function buildSubAgentContextBlock(cwd, rawFiles) {
|
|
310
145
|
const values = Array.isArray(rawFiles) ? rawFiles : [];
|
|
311
146
|
const files = values
|
|
@@ -385,155 +220,155 @@ function buildToolsSchema(opts) {
|
|
|
385
220
|
properties,
|
|
386
221
|
required
|
|
387
222
|
});
|
|
223
|
+
const str = () => ({ type: 'string' });
|
|
224
|
+
const bool = () => ({ type: 'boolean' });
|
|
225
|
+
const int = (min, max) => ({ type: 'integer', ...(min !== undefined && { minimum: min }), ...(max !== undefined && { maximum: max }) });
|
|
388
226
|
const schemas = [
|
|
227
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
228
|
+
// Token-safe reads (require limit; allow plain output without per-line numbers)
|
|
229
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
389
230
|
{
|
|
390
231
|
type: 'function',
|
|
391
232
|
function: {
|
|
392
233
|
name: 'read_file',
|
|
393
|
-
description: 'Read
|
|
234
|
+
description: 'Read a bounded slice of a file.',
|
|
394
235
|
parameters: obj({
|
|
395
|
-
path:
|
|
396
|
-
offset:
|
|
397
|
-
limit:
|
|
398
|
-
search:
|
|
399
|
-
context:
|
|
400
|
-
|
|
401
|
-
|
|
236
|
+
path: str(),
|
|
237
|
+
offset: int(1, 1_000_000),
|
|
238
|
+
limit: int(1, 240),
|
|
239
|
+
search: str(),
|
|
240
|
+
context: int(0, 80),
|
|
241
|
+
format: { type: 'string', enum: ['plain', 'numbered', 'sparse'] },
|
|
242
|
+
max_bytes: int(256, 20_000),
|
|
243
|
+
}, ['path', 'limit']),
|
|
244
|
+
},
|
|
402
245
|
},
|
|
403
246
|
{
|
|
404
247
|
type: 'function',
|
|
405
248
|
function: {
|
|
406
249
|
name: 'read_files',
|
|
407
|
-
description: 'Batch read
|
|
250
|
+
description: 'Batch read bounded file slices.',
|
|
408
251
|
parameters: obj({
|
|
409
252
|
requests: {
|
|
410
253
|
type: 'array',
|
|
411
254
|
items: obj({
|
|
412
|
-
path:
|
|
413
|
-
offset:
|
|
414
|
-
limit:
|
|
415
|
-
search:
|
|
416
|
-
context:
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
255
|
+
path: str(),
|
|
256
|
+
offset: int(1, 1_000_000),
|
|
257
|
+
limit: int(1, 240),
|
|
258
|
+
search: str(),
|
|
259
|
+
context: int(0, 80),
|
|
260
|
+
format: { type: 'string', enum: ['plain', 'numbered', 'sparse'] },
|
|
261
|
+
max_bytes: int(256, 20_000),
|
|
262
|
+
}, ['path', 'limit']),
|
|
263
|
+
},
|
|
264
|
+
}, ['requests']),
|
|
265
|
+
},
|
|
421
266
|
},
|
|
267
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
268
|
+
// Writes/edits
|
|
269
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
422
270
|
{
|
|
423
271
|
type: 'function',
|
|
424
272
|
function: {
|
|
425
273
|
name: 'write_file',
|
|
426
|
-
description: 'Write
|
|
427
|
-
parameters: obj({ path:
|
|
428
|
-
}
|
|
274
|
+
description: 'Write file (atomic, backup).',
|
|
275
|
+
parameters: obj({ path: str(), content: str() }, ['path', 'content']),
|
|
276
|
+
},
|
|
429
277
|
},
|
|
430
278
|
{
|
|
431
279
|
type: 'function',
|
|
432
280
|
function: {
|
|
433
|
-
name: '
|
|
434
|
-
description: '
|
|
281
|
+
name: 'apply_patch',
|
|
282
|
+
description: 'Apply unified diff patch (multi-file).',
|
|
435
283
|
parameters: obj({
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
284
|
+
patch: str(),
|
|
285
|
+
files: { type: 'array', items: str() },
|
|
286
|
+
strip: int(0, 5),
|
|
287
|
+
}, ['patch', 'files']),
|
|
288
|
+
},
|
|
442
289
|
},
|
|
443
290
|
{
|
|
444
291
|
type: 'function',
|
|
445
292
|
function: {
|
|
446
|
-
name: '
|
|
447
|
-
description: '
|
|
293
|
+
name: 'edit_range',
|
|
294
|
+
description: 'Replace a line range in a file.',
|
|
448
295
|
parameters: obj({
|
|
449
|
-
path:
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
296
|
+
path: str(),
|
|
297
|
+
start_line: int(1),
|
|
298
|
+
end_line: int(1),
|
|
299
|
+
replacement: str(),
|
|
300
|
+
}, ['path', 'start_line', 'end_line', 'replacement']),
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
type: 'function',
|
|
305
|
+
function: {
|
|
306
|
+
name: 'edit_file',
|
|
307
|
+
description: 'Legacy exact replace (requires old_text). Prefer apply_patch/edit_range.',
|
|
308
|
+
parameters: obj({ path: str(), old_text: str(), new_text: str(), replace_all: bool() }, ['path', 'old_text', 'new_text']),
|
|
309
|
+
},
|
|
454
310
|
},
|
|
311
|
+
{
|
|
312
|
+
type: 'function',
|
|
313
|
+
function: {
|
|
314
|
+
name: 'insert_file',
|
|
315
|
+
description: 'Insert text at line (0=prepend, -1=append).',
|
|
316
|
+
parameters: obj({ path: str(), line: int(), text: str() }, ['path', 'line', 'text']),
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
320
|
+
// Bounded listings/search (expose existing caps)
|
|
321
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
455
322
|
{
|
|
456
323
|
type: 'function',
|
|
457
324
|
function: {
|
|
458
325
|
name: 'list_dir',
|
|
459
|
-
description: 'List directory
|
|
460
|
-
parameters: obj({
|
|
461
|
-
|
|
462
|
-
recursive: { type: 'boolean' },
|
|
463
|
-
}, ['path'])
|
|
464
|
-
}
|
|
326
|
+
description: 'List directory entries.',
|
|
327
|
+
parameters: obj({ path: str(), recursive: bool(), max_entries: int(1, 500) }, ['path']),
|
|
328
|
+
},
|
|
465
329
|
},
|
|
466
330
|
{
|
|
467
331
|
type: 'function',
|
|
468
332
|
function: {
|
|
469
333
|
name: 'search_files',
|
|
470
|
-
description: 'Search
|
|
471
|
-
parameters: obj({
|
|
472
|
-
|
|
473
|
-
path: { type: 'string' },
|
|
474
|
-
include: { type: 'string' },
|
|
475
|
-
}, ['pattern', 'path'])
|
|
476
|
-
}
|
|
334
|
+
description: 'Search regex in files.',
|
|
335
|
+
parameters: obj({ pattern: str(), path: str(), include: str(), max_results: int(1, 100) }, ['pattern', 'path']),
|
|
336
|
+
},
|
|
477
337
|
},
|
|
338
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
339
|
+
// Exec (minified schema)
|
|
340
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
478
341
|
{
|
|
479
342
|
type: 'function',
|
|
480
343
|
function: {
|
|
481
344
|
name: 'exec',
|
|
482
|
-
description: 'Run
|
|
483
|
-
parameters: obj({
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
timeout: { type: 'integer', description: 'Timeout in seconds (default: 30, max: 120). Use 60-120 for npm install, builds, or test suites.' }
|
|
487
|
-
}, ['command'])
|
|
488
|
-
}
|
|
489
|
-
}
|
|
345
|
+
description: 'Run bash -c; returns JSON rc/out/err.',
|
|
346
|
+
parameters: obj({ command: str(), cwd: str(), timeout: int(1, 120) }, ['command']),
|
|
347
|
+
},
|
|
348
|
+
},
|
|
490
349
|
];
|
|
491
350
|
if (opts?.allowSpawnTask !== false) {
|
|
492
351
|
schemas.push({
|
|
493
352
|
type: 'function',
|
|
494
353
|
function: {
|
|
495
354
|
name: 'spawn_task',
|
|
496
|
-
description: '
|
|
355
|
+
description: 'Run a sub-agent task (no parent history).',
|
|
497
356
|
parameters: obj({
|
|
498
|
-
task:
|
|
499
|
-
context_files: {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
max_iterations: { type: 'integer', description: 'Optional max turn cap for the sub-agent' },
|
|
507
|
-
max_tokens: { type: 'integer', description: 'Optional max completion tokens for the sub-agent' },
|
|
508
|
-
timeout_sec: { type: 'integer', description: 'Optional timeout for this sub-agent run (seconds)' },
|
|
509
|
-
system_prompt: { type: 'string', description: 'Optional sub-agent system prompt override for this task' },
|
|
357
|
+
task: str(),
|
|
358
|
+
context_files: { type: 'array', items: str() },
|
|
359
|
+
model: str(),
|
|
360
|
+
endpoint: str(),
|
|
361
|
+
max_iterations: int(),
|
|
362
|
+
max_tokens: int(),
|
|
363
|
+
timeout_sec: int(),
|
|
364
|
+
system_prompt: str(),
|
|
510
365
|
approval_mode: { type: 'string', enum: ['plan', 'reject', 'default', 'auto-edit', 'yolo'] },
|
|
511
|
-
}, ['task'])
|
|
512
|
-
}
|
|
366
|
+
}, ['task']),
|
|
367
|
+
},
|
|
513
368
|
});
|
|
514
369
|
}
|
|
515
370
|
if (opts?.activeVaultTools) {
|
|
516
|
-
schemas.push({
|
|
517
|
-
type: 'function',
|
|
518
|
-
function: {
|
|
519
|
-
name: 'vault_search',
|
|
520
|
-
description: 'Search vault entries (notes and previous tool outputs) to reuse prior high-signal findings.',
|
|
521
|
-
parameters: obj({
|
|
522
|
-
query: { type: 'string' },
|
|
523
|
-
limit: { type: 'integer' }
|
|
524
|
-
}, ['query'])
|
|
525
|
-
}
|
|
526
|
-
}, {
|
|
527
|
-
type: 'function',
|
|
528
|
-
function: {
|
|
529
|
-
name: 'vault_note',
|
|
530
|
-
description: 'Persist a concise, high-signal note into the Trifecta vault.',
|
|
531
|
-
parameters: obj({
|
|
532
|
-
key: { type: 'string' },
|
|
533
|
-
value: { type: 'string' }
|
|
534
|
-
}, ['key', 'value'])
|
|
535
|
-
}
|
|
536
|
-
});
|
|
371
|
+
schemas.push({ type: 'function', function: { name: 'vault_search', description: 'Search vault.', parameters: obj({ query: str(), limit: int() }, ['query']) } }, { type: 'function', function: { name: 'vault_note', description: 'Write vault note.', parameters: obj({ key: str(), value: str() }, ['key', 'value']) } });
|
|
537
372
|
}
|
|
538
373
|
// Phase 9: sys_context tool is only available in sys mode.
|
|
539
374
|
if (opts?.sysMode) {
|
|
@@ -544,54 +379,36 @@ function buildToolsSchema(opts) {
|
|
|
544
379
|
type: 'function',
|
|
545
380
|
function: {
|
|
546
381
|
name: 'lsp_diagnostics',
|
|
547
|
-
description: 'Get
|
|
548
|
-
parameters: obj({
|
|
549
|
-
path: { type: 'string', description: 'File path (omit for project-wide diagnostics)' },
|
|
550
|
-
severity: { type: 'integer', description: '1=Error, 2=Warning, 3=Info, 4=Hint (default: config threshold)' },
|
|
551
|
-
}, [])
|
|
382
|
+
description: 'Get LSP diagnostics (errors/warnings) for file or project.',
|
|
383
|
+
parameters: obj({ path: str(), severity: int() }, [])
|
|
552
384
|
}
|
|
553
385
|
}, {
|
|
554
386
|
type: 'function',
|
|
555
387
|
function: {
|
|
556
388
|
name: 'lsp_symbols',
|
|
557
|
-
description: 'List
|
|
558
|
-
parameters: obj({
|
|
559
|
-
path: { type: 'string' },
|
|
560
|
-
}, ['path'])
|
|
389
|
+
description: 'List symbols (functions, classes, vars) in a file.',
|
|
390
|
+
parameters: obj({ path: str() }, ['path'])
|
|
561
391
|
}
|
|
562
392
|
}, {
|
|
563
393
|
type: 'function',
|
|
564
394
|
function: {
|
|
565
395
|
name: 'lsp_hover',
|
|
566
|
-
description: 'Get type
|
|
567
|
-
parameters: obj({
|
|
568
|
-
path: { type: 'string' },
|
|
569
|
-
line: { type: 'integer' },
|
|
570
|
-
character: { type: 'integer' },
|
|
571
|
-
}, ['path', 'line', 'character'])
|
|
396
|
+
description: 'Get type/docs for symbol at position.',
|
|
397
|
+
parameters: obj({ path: str(), line: int(), character: int() }, ['path', 'line', 'character'])
|
|
572
398
|
}
|
|
573
399
|
}, {
|
|
574
400
|
type: 'function',
|
|
575
401
|
function: {
|
|
576
402
|
name: 'lsp_definition',
|
|
577
|
-
description: 'Go to definition of
|
|
578
|
-
parameters: obj({
|
|
579
|
-
path: { type: 'string' },
|
|
580
|
-
line: { type: 'integer' },
|
|
581
|
-
character: { type: 'integer' },
|
|
582
|
-
}, ['path', 'line', 'character'])
|
|
403
|
+
description: 'Go to definition of symbol at position.',
|
|
404
|
+
parameters: obj({ path: str(), line: int(), character: int() }, ['path', 'line', 'character'])
|
|
583
405
|
}
|
|
584
406
|
}, {
|
|
585
407
|
type: 'function',
|
|
586
408
|
function: {
|
|
587
409
|
name: 'lsp_references',
|
|
588
|
-
description: 'Find all references to
|
|
589
|
-
parameters: obj({
|
|
590
|
-
path: { type: 'string' },
|
|
591
|
-
line: { type: 'integer' },
|
|
592
|
-
character: { type: 'integer' },
|
|
593
|
-
max_results: { type: 'integer', description: 'Cap results (default 50)' },
|
|
594
|
-
}, ['path', 'line', 'character'])
|
|
410
|
+
description: 'Find all references to symbol at position.',
|
|
411
|
+
parameters: obj({ path: str(), line: int(), character: int(), max_results: int() }, ['path', 'line', 'character'])
|
|
595
412
|
}
|
|
596
413
|
});
|
|
597
414
|
}
|
|
@@ -600,203 +417,6 @@ function buildToolsSchema(opts) {
|
|
|
600
417
|
}
|
|
601
418
|
return schemas;
|
|
602
419
|
}
|
|
603
|
-
/** @internal Exported for testing. Parses tool calls from model content when tool_calls array is empty. */
|
|
604
|
-
export function parseToolCallsFromContent(content) {
|
|
605
|
-
// Fallback parser: if model printed JSON tool_calls in content.
|
|
606
|
-
const trimmed = content.trim();
|
|
607
|
-
const tryParse = (s) => {
|
|
608
|
-
try {
|
|
609
|
-
return JSON.parse(s);
|
|
610
|
-
}
|
|
611
|
-
catch {
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
};
|
|
615
|
-
// Case 1: whole content is JSON
|
|
616
|
-
const whole = tryParse(trimmed);
|
|
617
|
-
if (whole?.tool_calls && Array.isArray(whole.tool_calls))
|
|
618
|
-
return whole.tool_calls;
|
|
619
|
-
if (whole?.name && whole?.arguments) {
|
|
620
|
-
return [
|
|
621
|
-
{
|
|
622
|
-
id: 'call_0',
|
|
623
|
-
type: 'function',
|
|
624
|
-
function: { name: String(whole.name), arguments: JSON.stringify(whole.arguments) }
|
|
625
|
-
}
|
|
626
|
-
];
|
|
627
|
-
}
|
|
628
|
-
// Case 2: raw JSON array of tool calls (model writes [{name, arguments}, ...])
|
|
629
|
-
const arrStart = trimmed.indexOf('[');
|
|
630
|
-
const arrEnd = trimmed.lastIndexOf(']');
|
|
631
|
-
if (arrStart !== -1 && arrEnd !== -1 && arrEnd > arrStart) {
|
|
632
|
-
const arrSub = tryParse(trimmed.slice(arrStart, arrEnd + 1));
|
|
633
|
-
if (Array.isArray(arrSub) && arrSub.length > 0 && arrSub[0]?.name) {
|
|
634
|
-
return arrSub.map((item, i) => ({
|
|
635
|
-
id: `call_${i}`,
|
|
636
|
-
type: 'function',
|
|
637
|
-
function: {
|
|
638
|
-
name: String(item.name),
|
|
639
|
-
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {})
|
|
640
|
-
}
|
|
641
|
-
}));
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
// Case 3: find a JSON object substring (handles tool_calls wrapper OR single tool-call)
|
|
645
|
-
const start = trimmed.indexOf('{');
|
|
646
|
-
const end = trimmed.lastIndexOf('}');
|
|
647
|
-
if (start !== -1 && end !== -1 && end > start) {
|
|
648
|
-
const sub = tryParse(trimmed.slice(start, end + 1));
|
|
649
|
-
if (sub?.tool_calls && Array.isArray(sub.tool_calls))
|
|
650
|
-
return sub.tool_calls;
|
|
651
|
-
if (sub?.name && sub?.arguments) {
|
|
652
|
-
return [
|
|
653
|
-
{
|
|
654
|
-
id: 'call_0',
|
|
655
|
-
type: 'function',
|
|
656
|
-
function: { name: String(sub.name), arguments: typeof sub.arguments === 'string' ? sub.arguments : JSON.stringify(sub.arguments) }
|
|
657
|
-
}
|
|
658
|
-
];
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
// Case 4: XML tool calls — used by Qwen, Hermes, and other models whose chat
|
|
662
|
-
// templates emit <tool_call><function=name><parameter=key>value</parameter></function></tool_call>.
|
|
663
|
-
// When llama-server's XML→JSON conversion fails (common with large write_file content),
|
|
664
|
-
// the raw XML leaks into the content field. This recovers it.
|
|
665
|
-
const xmlCalls = parseXmlToolCalls(trimmed);
|
|
666
|
-
if (xmlCalls?.length)
|
|
667
|
-
return xmlCalls;
|
|
668
|
-
// Case 5: Lightweight function-tag calls (seen in some Qwen content-mode outputs):
|
|
669
|
-
// <function=tool_name>
|
|
670
|
-
// {...json args...}
|
|
671
|
-
// </function>
|
|
672
|
-
// or single-line <function=tool_name>{...}</function>
|
|
673
|
-
const fnTagCalls = parseFunctionTagToolCalls(trimmed);
|
|
674
|
-
if (fnTagCalls?.length)
|
|
675
|
-
return fnTagCalls;
|
|
676
|
-
return null;
|
|
677
|
-
}
|
|
678
|
-
/**
|
|
679
|
-
* Parse XML-style tool calls from content.
|
|
680
|
-
* Format: <tool_call><function=name><parameter=key>value</parameter>...</function></tool_call>
|
|
681
|
-
* Handles multiple tool call blocks and arbitrary parameter names/values.
|
|
682
|
-
*/
|
|
683
|
-
function parseXmlToolCalls(content) {
|
|
684
|
-
// Quick bailout: no point parsing if there's no <tool_call> marker
|
|
685
|
-
if (!content.includes('<tool_call>'))
|
|
686
|
-
return null;
|
|
687
|
-
const calls = [];
|
|
688
|
-
// Match each <tool_call>...</tool_call> block.
|
|
689
|
-
// Using a manual scan instead of a single greedy regex to handle nested angle brackets
|
|
690
|
-
// in parameter values (e.g. TypeScript generics, JSX, comparison operators).
|
|
691
|
-
let searchFrom = 0;
|
|
692
|
-
while (searchFrom < content.length) {
|
|
693
|
-
const blockStart = content.indexOf('<tool_call>', searchFrom);
|
|
694
|
-
if (blockStart === -1)
|
|
695
|
-
break;
|
|
696
|
-
const blockEnd = content.indexOf('</tool_call>', blockStart);
|
|
697
|
-
if (blockEnd === -1)
|
|
698
|
-
break; // Truncated — can't recover partial tool calls
|
|
699
|
-
const block = content.slice(blockStart + '<tool_call>'.length, blockEnd);
|
|
700
|
-
searchFrom = blockEnd + '</tool_call>'.length;
|
|
701
|
-
// Extract function name: <function=name>...</function>
|
|
702
|
-
const fnMatch = block.match(/<function=(\w[\w.-]*)>/);
|
|
703
|
-
if (!fnMatch)
|
|
704
|
-
continue;
|
|
705
|
-
const fnName = fnMatch[1];
|
|
706
|
-
const fnStart = block.indexOf(fnMatch[0]) + fnMatch[0].length;
|
|
707
|
-
const fnEnd = block.lastIndexOf('</function>');
|
|
708
|
-
const fnBody = fnEnd !== -1 ? block.slice(fnStart, fnEnd) : block.slice(fnStart);
|
|
709
|
-
// Extract parameters: <parameter=key>value</parameter>
|
|
710
|
-
// Uses bracket-matching (depth counting) so that parameter values containing
|
|
711
|
-
// literal <parameter=...>...</parameter> (e.g. writing XML files) are handled
|
|
712
|
-
// correctly instead of being truncated at the inner close tag.
|
|
713
|
-
const args = {};
|
|
714
|
-
const openRe = /<parameter=(\w[\w.-]*)>/g;
|
|
715
|
-
const closeTag = '</parameter>';
|
|
716
|
-
let paramMatch;
|
|
717
|
-
while ((paramMatch = openRe.exec(fnBody)) !== null) {
|
|
718
|
-
const paramName = paramMatch[1];
|
|
719
|
-
const valueStart = paramMatch.index + paramMatch[0].length;
|
|
720
|
-
// Bracket-match: find the </parameter> that balances this open tag.
|
|
721
|
-
// Depth starts at 1; nested <parameter=...> increments, </parameter> decrements.
|
|
722
|
-
let depth = 1;
|
|
723
|
-
let scanPos = valueStart;
|
|
724
|
-
let closeIdx = -1;
|
|
725
|
-
while (scanPos < fnBody.length && depth > 0) {
|
|
726
|
-
const nextOpen = fnBody.indexOf('<parameter=', scanPos);
|
|
727
|
-
const nextClose = fnBody.indexOf(closeTag, scanPos);
|
|
728
|
-
if (nextClose === -1)
|
|
729
|
-
break; // No more close tags — truncated
|
|
730
|
-
if (nextOpen !== -1 && nextOpen < nextClose) {
|
|
731
|
-
// An open tag comes before the next close — increase depth
|
|
732
|
-
depth++;
|
|
733
|
-
scanPos = nextOpen + 1; // advance past '<' to avoid re-matching
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
// Close tag comes first — decrease depth
|
|
737
|
-
depth--;
|
|
738
|
-
if (depth === 0) {
|
|
739
|
-
closeIdx = nextClose;
|
|
740
|
-
}
|
|
741
|
-
scanPos = nextClose + closeTag.length;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
if (closeIdx === -1) {
|
|
745
|
-
// No matching close tag — take rest of body as value (truncated output)
|
|
746
|
-
args[paramName] = fnBody.slice(valueStart).trim();
|
|
747
|
-
break;
|
|
748
|
-
}
|
|
749
|
-
// Trim exactly the template-added leading/trailing newline, preserve internal whitespace
|
|
750
|
-
let value = fnBody.slice(valueStart, closeIdx);
|
|
751
|
-
if (value.startsWith('\n'))
|
|
752
|
-
value = value.slice(1);
|
|
753
|
-
if (value.endsWith('\n'))
|
|
754
|
-
value = value.slice(0, -1);
|
|
755
|
-
args[paramName] = value;
|
|
756
|
-
// Advance the regex past the close tag so the next openRe.exec starts after it
|
|
757
|
-
openRe.lastIndex = closeIdx + closeTag.length;
|
|
758
|
-
}
|
|
759
|
-
if (fnName && Object.keys(args).length > 0) {
|
|
760
|
-
calls.push({
|
|
761
|
-
id: `call_xml_${calls.length}`,
|
|
762
|
-
type: 'function',
|
|
763
|
-
function: {
|
|
764
|
-
name: fnName,
|
|
765
|
-
arguments: JSON.stringify(args)
|
|
766
|
-
}
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
return calls.length > 0 ? calls : null;
|
|
771
|
-
}
|
|
772
|
-
/** Check for missing required params by tool name — universal pre-dispatch validation */
|
|
773
|
-
function getMissingRequiredParams(toolName, args) {
|
|
774
|
-
const required = {
|
|
775
|
-
read_file: ['path'],
|
|
776
|
-
read_files: ['requests'],
|
|
777
|
-
write_file: ['path', 'content'],
|
|
778
|
-
edit_file: ['path', 'old_text', 'new_text'],
|
|
779
|
-
insert_file: ['path', 'line', 'text'],
|
|
780
|
-
list_dir: ['path'],
|
|
781
|
-
search_files: ['pattern', 'path'],
|
|
782
|
-
exec: ['command'],
|
|
783
|
-
spawn_task: ['task'],
|
|
784
|
-
sys_context: [],
|
|
785
|
-
vault_search: ['query'],
|
|
786
|
-
vault_note: ['key', 'value']
|
|
787
|
-
};
|
|
788
|
-
const req = required[toolName];
|
|
789
|
-
if (!req)
|
|
790
|
-
return [];
|
|
791
|
-
return req.filter(p => args[p] === undefined || args[p] === null);
|
|
792
|
-
}
|
|
793
|
-
/** Strip markdown code fences (```json ... ```) from tool argument strings */
|
|
794
|
-
function stripMarkdownFences(s) {
|
|
795
|
-
const trimmed = s.trim();
|
|
796
|
-
// Match ```json\n...\n``` or ```\n...\n```
|
|
797
|
-
const m = /^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/.exec(trimmed);
|
|
798
|
-
return m ? m[1] : s;
|
|
799
|
-
}
|
|
800
420
|
function isReadOnlyTool(name) {
|
|
801
421
|
return name === 'read_file' || name === 'read_files' || name === 'list_dir' || name === 'search_files' || name === 'vault_search' || name === 'sys_context';
|
|
802
422
|
}
|
|
@@ -805,6 +425,10 @@ function planModeSummary(name, args) {
|
|
|
805
425
|
switch (name) {
|
|
806
426
|
case 'write_file':
|
|
807
427
|
return `write ${args.path ?? 'unknown'} (${typeof args.content === 'string' ? args.content.split('\n').length : '?'} lines)`;
|
|
428
|
+
case 'apply_patch':
|
|
429
|
+
return `apply patch to ${Array.isArray(args.files) ? args.files.length : '?'} file(s)`;
|
|
430
|
+
case 'edit_range':
|
|
431
|
+
return `edit ${args.path ?? 'unknown'} lines ${args.start_line ?? '?'}-${args.end_line ?? '?'}`;
|
|
808
432
|
case 'edit_file':
|
|
809
433
|
return `edit ${args.path ?? 'unknown'} (replace ${typeof args.old_text === 'string' ? args.old_text.split('\n').length : '?'} lines)`;
|
|
810
434
|
case 'insert_file':
|
|
@@ -839,148 +463,6 @@ function userDisallowsDelegation(content) {
|
|
|
839
463
|
/\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);
|
|
840
464
|
return negationNearDelegation;
|
|
841
465
|
}
|
|
842
|
-
function reviewArtifactKeys(projectDir) {
|
|
843
|
-
const { projectId } = projectIndexKeys(projectDir);
|
|
844
|
-
return {
|
|
845
|
-
projectId,
|
|
846
|
-
latestKey: `artifact:review:latest:${projectId}`,
|
|
847
|
-
byIdPrefix: `artifact:review:item:${projectId}:`,
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
function looksLikeCodeReviewRequest(text) {
|
|
851
|
-
const t = text.toLowerCase();
|
|
852
|
-
if (!t.trim())
|
|
853
|
-
return false;
|
|
854
|
-
if (/^\s*\/review\b/.test(t))
|
|
855
|
-
return true;
|
|
856
|
-
if (/\b(?:code\s+review|security\s+review|review\s+the\s+(?:code|diff|changes|repo|repository|pr)|audit\s+the\s+code)\b/.test(t))
|
|
857
|
-
return true;
|
|
858
|
-
return /\breview\b/.test(t) && /\b(?:code|repo|repository|diff|changes|pull\s*request|pr)\b/.test(t);
|
|
859
|
-
}
|
|
860
|
-
function looksLikeReviewRetrievalRequest(text) {
|
|
861
|
-
const t = text.toLowerCase();
|
|
862
|
-
if (!t.trim())
|
|
863
|
-
return false;
|
|
864
|
-
if (/^\s*\/review\s+(?:print|show|replay|latest|last|full)\b/.test(t))
|
|
865
|
-
return true;
|
|
866
|
-
if (!/\breview\b/.test(t))
|
|
867
|
-
return false;
|
|
868
|
-
if (/\bprint\s+stale\s+review\s+anyway\b/.test(t))
|
|
869
|
-
return true;
|
|
870
|
-
if (/\b(?:print|show|display|repeat|paste|send|output|give)\b[^\n.]{0,80}\breview\b[^\n.]{0,40}\b(?:again|back)\b/.test(t))
|
|
871
|
-
return true;
|
|
872
|
-
if (/\b(?:print|show|display|repeat|paste|send|output|give)\b[^\n.]{0,80}\b(?:full|entire|complete|whole)\b[^\n.]{0,80}\breview\b/.test(t))
|
|
873
|
-
return true;
|
|
874
|
-
if (/\b(?:full|entire|complete|whole)\b[^\n.]{0,30}\bcode\s+review\b/.test(t) && /\b(?:print|show|display|repeat|paste|send|output|give)\b/.test(t))
|
|
875
|
-
return true;
|
|
876
|
-
if (/\b(?:print|show|display|repeat|paste|send|output|give)\b[^\n.]{0,80}\b(?:last|latest|previous)\b[^\n.]{0,40}\breview\b/.test(t))
|
|
877
|
-
return true;
|
|
878
|
-
return false;
|
|
879
|
-
}
|
|
880
|
-
function retrievalAllowsStaleArtifact(text) {
|
|
881
|
-
const t = text.toLowerCase();
|
|
882
|
-
if (!t.trim())
|
|
883
|
-
return false;
|
|
884
|
-
if (/\bprint\s+stale\s+review\s+anyway\b/.test(t))
|
|
885
|
-
return true;
|
|
886
|
-
if (/\b(?:force|override|ignore)\b[^\n.]{0,80}\b(?:stale|old|previous)\b[^\n.]{0,80}\breview\b/.test(t))
|
|
887
|
-
return true;
|
|
888
|
-
if (/\b(?:stale|old|previous)\b[^\n.]{0,80}\breview\b[^\n.]{0,80}\b(?:anyway|still|force|override|ignore)\b/.test(t))
|
|
889
|
-
return true;
|
|
890
|
-
return false;
|
|
891
|
-
}
|
|
892
|
-
function parseReviewArtifactStalePolicy(raw) {
|
|
893
|
-
const v = typeof raw === 'string' ? raw.toLowerCase().trim() : '';
|
|
894
|
-
if (v === 'block')
|
|
895
|
-
return 'block';
|
|
896
|
-
return 'warn';
|
|
897
|
-
}
|
|
898
|
-
function parseReviewArtifact(raw) {
|
|
899
|
-
try {
|
|
900
|
-
const parsed = JSON.parse(raw);
|
|
901
|
-
if (!parsed || typeof parsed !== 'object')
|
|
902
|
-
return null;
|
|
903
|
-
if (parsed.kind !== 'code_review')
|
|
904
|
-
return null;
|
|
905
|
-
if (typeof parsed.id !== 'string' || !parsed.id)
|
|
906
|
-
return null;
|
|
907
|
-
if (typeof parsed.createdAt !== 'string' || !parsed.createdAt)
|
|
908
|
-
return null;
|
|
909
|
-
if (typeof parsed.model !== 'string')
|
|
910
|
-
return null;
|
|
911
|
-
if (typeof parsed.projectId !== 'string' || !parsed.projectId)
|
|
912
|
-
return null;
|
|
913
|
-
if (typeof parsed.projectDir !== 'string' || !parsed.projectDir)
|
|
914
|
-
return null;
|
|
915
|
-
if (typeof parsed.prompt !== 'string')
|
|
916
|
-
return null;
|
|
917
|
-
if (typeof parsed.content !== 'string')
|
|
918
|
-
return null;
|
|
919
|
-
return parsed;
|
|
920
|
-
}
|
|
921
|
-
catch {
|
|
922
|
-
return null;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
function gitHead(cwd) {
|
|
926
|
-
const inside = spawnSync(BASH, ['-lc', 'git rev-parse --is-inside-work-tree'], {
|
|
927
|
-
cwd,
|
|
928
|
-
encoding: 'utf8',
|
|
929
|
-
timeout: 1000,
|
|
930
|
-
});
|
|
931
|
-
if (inside.status !== 0 || !String(inside.stdout || '').trim().startsWith('true'))
|
|
932
|
-
return undefined;
|
|
933
|
-
const head = spawnSync(BASH, ['-lc', 'git rev-parse HEAD'], {
|
|
934
|
-
cwd,
|
|
935
|
-
encoding: 'utf8',
|
|
936
|
-
timeout: 1000,
|
|
937
|
-
});
|
|
938
|
-
if (head.status !== 0)
|
|
939
|
-
return undefined;
|
|
940
|
-
const sha = String(head.stdout || '').trim();
|
|
941
|
-
return sha || undefined;
|
|
942
|
-
}
|
|
943
|
-
function shortSha(sha) {
|
|
944
|
-
if (!sha)
|
|
945
|
-
return 'unknown';
|
|
946
|
-
return sha.slice(0, 8);
|
|
947
|
-
}
|
|
948
|
-
function reviewArtifactStaleReason(artifact, cwd) {
|
|
949
|
-
const currentHead = gitHead(cwd);
|
|
950
|
-
const currentDirty = isGitDirty(cwd);
|
|
951
|
-
if (artifact.gitHead && currentHead && artifact.gitHead !== currentHead) {
|
|
952
|
-
return `Stored review was generated at commit ${shortSha(artifact.gitHead)}; repository is now at ${shortSha(currentHead)}.`;
|
|
953
|
-
}
|
|
954
|
-
if (artifact.gitDirty === false && currentDirty) {
|
|
955
|
-
return 'Stored review was generated on a clean tree; working tree now has uncommitted changes.';
|
|
956
|
-
}
|
|
957
|
-
return '';
|
|
958
|
-
}
|
|
959
|
-
function normalizeModelsResponse(raw) {
|
|
960
|
-
if (Array.isArray(raw)) {
|
|
961
|
-
return {
|
|
962
|
-
data: raw
|
|
963
|
-
.map((m) => {
|
|
964
|
-
if (!m)
|
|
965
|
-
return null;
|
|
966
|
-
if (typeof m === 'string')
|
|
967
|
-
return { id: m };
|
|
968
|
-
if (typeof m.id === 'string' && m.id)
|
|
969
|
-
return m;
|
|
970
|
-
return null;
|
|
971
|
-
})
|
|
972
|
-
.filter(Boolean)
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
if (raw && Array.isArray(raw.data)) {
|
|
976
|
-
return {
|
|
977
|
-
data: raw.data
|
|
978
|
-
.map((m) => (m && typeof m.id === 'string' && m.id ? m : null))
|
|
979
|
-
.filter(Boolean)
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
return { data: [] };
|
|
983
|
-
}
|
|
984
466
|
export async function createSession(opts) {
|
|
985
467
|
const cfg = opts.config;
|
|
986
468
|
let client = opts.runtime?.client ?? new OpenAIClient(cfg.endpoint, opts.apiKey, cfg.verbose);
|
|
@@ -1014,7 +496,7 @@ export async function createSession(opts) {
|
|
|
1014
496
|
modelMeta,
|
|
1015
497
|
});
|
|
1016
498
|
let supportsVision = supportsVisionModel(model, modelMeta, harness);
|
|
1017
|
-
const sessionId = `session-${
|
|
499
|
+
const sessionId = `session-${timestampedId()}`;
|
|
1018
500
|
const hookCfg = cfg.hooks ?? {};
|
|
1019
501
|
const hookManager = opts.runtime?.hookManager ?? new HookManager({
|
|
1020
502
|
enabled: hookCfg.enabled !== false,
|
|
@@ -1082,7 +564,7 @@ export async function createSession(opts) {
|
|
|
1082
564
|
? Number(cfg.mcp_call_timeout_sec)
|
|
1083
565
|
: (Number.isFinite(cfg.mcp?.call_timeout_sec) ? Number(cfg.mcp?.call_timeout_sec) : 30);
|
|
1084
566
|
const builtInToolNames = [
|
|
1085
|
-
'read_file', 'read_files', 'write_file', 'edit_file', 'insert_file',
|
|
567
|
+
'read_file', 'read_files', 'write_file', 'apply_patch', 'edit_range', 'edit_file', 'insert_file',
|
|
1086
568
|
'list_dir', 'search_files', 'exec', 'vault_search', 'vault_note', 'sys_context',
|
|
1087
569
|
...(spawnTaskEnabled ? ['spawn_task'] : []),
|
|
1088
570
|
];
|
|
@@ -2058,11 +1540,25 @@ export async function createSession(opts) {
|
|
|
2058
1540
|
const hookObj = typeof hooks === 'function' ? { onToken: hooks } : hooks ?? {};
|
|
2059
1541
|
let turns = 0;
|
|
2060
1542
|
let toolCalls = 0;
|
|
2061
|
-
const askId = `ask-${
|
|
1543
|
+
const askId = `ask-${timestampedId()}`;
|
|
2062
1544
|
const emitToolCall = async (call) => {
|
|
2063
1545
|
hookObj.onToolCall?.(call);
|
|
2064
1546
|
await hookManager.emit('tool_call', { askId, turn: turns, call });
|
|
2065
1547
|
};
|
|
1548
|
+
const emitToolStream = (stream) => {
|
|
1549
|
+
try {
|
|
1550
|
+
void hookObj.onToolStream?.(stream);
|
|
1551
|
+
}
|
|
1552
|
+
catch {
|
|
1553
|
+
// best effort
|
|
1554
|
+
}
|
|
1555
|
+
try {
|
|
1556
|
+
void hookManager.emit('tool_stream', { askId, turn: turns, stream });
|
|
1557
|
+
}
|
|
1558
|
+
catch {
|
|
1559
|
+
// best effort
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
2066
1562
|
const emitToolResult = async (result) => {
|
|
2067
1563
|
await hookObj.onToolResult?.(result);
|
|
2068
1564
|
await hookManager.emit('tool_result', { askId, turn: turns, result });
|
|
@@ -2142,7 +1638,7 @@ export async function createSession(opts) {
|
|
|
2142
1638
|
if (!clean)
|
|
2143
1639
|
return;
|
|
2144
1640
|
const createdAt = new Date().toISOString();
|
|
2145
|
-
const id = `review-${
|
|
1641
|
+
const id = `review-${timestampedId()}`;
|
|
2146
1642
|
const artifact = {
|
|
2147
1643
|
id,
|
|
2148
1644
|
kind: 'code_review',
|
|
@@ -2178,6 +1674,7 @@ export async function createSession(opts) {
|
|
|
2178
1674
|
// identical tool call signature counts across this ask() run
|
|
2179
1675
|
const sigCounts = new Map();
|
|
2180
1676
|
const toolNameByCallId = new Map();
|
|
1677
|
+
const toolArgsByCallId = new Map();
|
|
2181
1678
|
// Loop-break helper state: bump mutationVersion whenever a tool mutates files.
|
|
2182
1679
|
// We also record the mutationVersion at which a given signature was last seen.
|
|
2183
1680
|
let mutationVersion = 0;
|
|
@@ -2219,6 +1716,42 @@ export async function createSession(opts) {
|
|
|
2219
1716
|
}
|
|
2220
1717
|
return msg;
|
|
2221
1718
|
};
|
|
1719
|
+
const compactToolMessageForHistory = async (toolCallId, rawContent) => {
|
|
1720
|
+
const toolName = toolNameByCallId.get(toolCallId) ?? 'tool';
|
|
1721
|
+
const toolArgs = toolArgsByCallId.get(toolCallId) ?? {};
|
|
1722
|
+
const rawMsg = { role: 'tool', tool_call_id: toolCallId, content: rawContent };
|
|
1723
|
+
// Persist full-fidelity output immediately so live context can stay small.
|
|
1724
|
+
if (vault && typeof vault.archiveToolResult === 'function') {
|
|
1725
|
+
try {
|
|
1726
|
+
await vault.archiveToolResult(rawMsg, toolName);
|
|
1727
|
+
}
|
|
1728
|
+
catch (e) {
|
|
1729
|
+
console.warn(`[warn] vault archive failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
let compact = rawContent;
|
|
1733
|
+
if (lens) {
|
|
1734
|
+
try {
|
|
1735
|
+
const lensCompact = await lens.summarizeToolOutput(rawContent, toolName, typeof toolArgs.path === 'string' ? String(toolArgs.path) : undefined);
|
|
1736
|
+
if (typeof lensCompact === 'string' && lensCompact.length && lensCompact.length < compact.length) {
|
|
1737
|
+
compact = lensCompact;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
catch {
|
|
1741
|
+
// ignore lens failures; fallback to raw
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
const success = !String(rawContent).startsWith('ERROR:');
|
|
1745
|
+
const digested = digestToolResult(toolName, toolArgs, compact, success);
|
|
1746
|
+
if (digested !== rawContent) {
|
|
1747
|
+
return {
|
|
1748
|
+
role: 'tool',
|
|
1749
|
+
tool_call_id: toolCallId,
|
|
1750
|
+
content: `${digested}\n[full output archived in vault: tool=${toolName}, call_id=${toolCallId}]`,
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
return rawMsg;
|
|
1754
|
+
};
|
|
2222
1755
|
const persistFailure = async (error, contextLine) => {
|
|
2223
1756
|
if (!vault)
|
|
2224
1757
|
return;
|
|
@@ -2280,7 +1813,6 @@ export async function createSession(opts) {
|
|
|
2280
1813
|
}
|
|
2281
1814
|
await maybeAutoDetectModelChange();
|
|
2282
1815
|
const beforeMsgs = messages;
|
|
2283
|
-
const beforeTokens = estimateTokensFromMessages(beforeMsgs);
|
|
2284
1816
|
const compacted = enforceContextBudget({
|
|
2285
1817
|
messages: beforeMsgs,
|
|
2286
1818
|
contextWindow,
|
|
@@ -2289,7 +1821,6 @@ export async function createSession(opts) {
|
|
|
2289
1821
|
compactAt: cfg.compact_at ?? 0.8,
|
|
2290
1822
|
toolSchemaTokens: estimateToolSchemaTokens(getToolsSchema()),
|
|
2291
1823
|
});
|
|
2292
|
-
const compactedDropped = beforeMsgs.length > compacted.length || estimateTokensFromMessages(compacted) < beforeTokens;
|
|
2293
1824
|
const compactedByRefs = new Set(compacted);
|
|
2294
1825
|
const dropped = beforeMsgs.filter((m) => !compactedByRefs.has(m));
|
|
2295
1826
|
if (dropped.length && vault) {
|
|
@@ -2532,7 +2063,11 @@ export async function createSession(opts) {
|
|
|
2532
2063
|
if (visible && hookObj.onToken)
|
|
2533
2064
|
hookObj.onToken('\n');
|
|
2534
2065
|
toolCalls += toolCallsArr.length;
|
|
2535
|
-
|
|
2066
|
+
const assistantToolCallText = visible || '';
|
|
2067
|
+
const compactAssistantToolCallText = assistantToolCallText.length > 900
|
|
2068
|
+
? `${assistantToolCallText.slice(0, 900)}\n[history-compacted: assistant narration truncated before tool execution]`
|
|
2069
|
+
: assistantToolCallText;
|
|
2070
|
+
messages.push({ role: 'assistant', content: compactAssistantToolCallText, tool_calls: toolCallsArr });
|
|
2536
2071
|
// sigCounts is scoped to the entire ask() run (see above)
|
|
2537
2072
|
// Bridge ConfirmationProvider → legacy confirm callback for tools.
|
|
2538
2073
|
// If a ConfirmationProvider is given, wrap it; otherwise fall back to raw callback.
|
|
@@ -2655,7 +2190,7 @@ export async function createSession(opts) {
|
|
|
2655
2190
|
`Hint: you repeated the same tool call ${loopThreshold} times with identical arguments. ` +
|
|
2656
2191
|
`If the call succeeded, move on to the next step. ` +
|
|
2657
2192
|
`If it failed, check that all required parameters are present and correct. ` +
|
|
2658
|
-
`For write_file/edit_file, ensure
|
|
2193
|
+
`For write_file/edit_file/apply_patch/edit_range, ensure required args are present (content/old_text/new_text/patch/files/start_line/end_line/replacement).`);
|
|
2659
2194
|
}
|
|
2660
2195
|
}
|
|
2661
2196
|
// Update consecutive tracking: save this turn's signatures for next turn comparison.
|
|
@@ -2684,6 +2219,8 @@ export async function createSession(opts) {
|
|
|
2684
2219
|
const hasMcpTool = mcpManager?.hasTool(name) === true;
|
|
2685
2220
|
if (!builtInFn && !isLspTool && !hasMcpTool && !isSpawnTask)
|
|
2686
2221
|
throw new Error(`unknown tool: ${name}`);
|
|
2222
|
+
// Keep parsed args by call-id so we can digest/archive tool outputs with context.
|
|
2223
|
+
toolArgsByCallId.set(callId, args && typeof args === 'object' && !Array.isArray(args) ? args : {});
|
|
2687
2224
|
// Pre-dispatch check for missing required params.
|
|
2688
2225
|
// Universal: catches omitted params early with a clear, instructive error
|
|
2689
2226
|
// before the tool itself throws a less helpful message.
|
|
@@ -2798,7 +2335,13 @@ export async function createSession(opts) {
|
|
|
2798
2335
|
content = await runSpawnTask(args);
|
|
2799
2336
|
}
|
|
2800
2337
|
else if (builtInFn) {
|
|
2801
|
-
const
|
|
2338
|
+
const callCtx = {
|
|
2339
|
+
...ctx,
|
|
2340
|
+
toolCallId: callId,
|
|
2341
|
+
toolName: name,
|
|
2342
|
+
onToolStream: emitToolStream,
|
|
2343
|
+
};
|
|
2344
|
+
const value = await builtInFn(callCtx, args);
|
|
2802
2345
|
content = typeof value === 'string' ? value : JSON.stringify(value);
|
|
2803
2346
|
if (name === 'exec') {
|
|
2804
2347
|
// Successful exec clears blocked-loop counters.
|
|
@@ -3019,7 +2562,8 @@ export async function createSession(opts) {
|
|
|
3019
2562
|
if (ac.signal.aborted)
|
|
3020
2563
|
break;
|
|
3021
2564
|
for (const r of results) {
|
|
3022
|
-
|
|
2565
|
+
const compactToolMsg = await compactToolMessageForHistory(r.id, r.content);
|
|
2566
|
+
messages.push(compactToolMsg);
|
|
3023
2567
|
}
|
|
3024
2568
|
if (readOnlyExecTurnHints.length) {
|
|
3025
2569
|
const previews = readOnlyExecTurnHints
|
|
@@ -3293,30 +2837,4 @@ async function autoPickModel(client, cached) {
|
|
|
3293
2837
|
clearTimeout(timer);
|
|
3294
2838
|
}
|
|
3295
2839
|
}
|
|
3296
|
-
function parseFunctionTagToolCalls(content) {
|
|
3297
|
-
const m = content.match(/<function=([\w.-]+)>([\s\S]*?)<\/function>/i);
|
|
3298
|
-
if (!m)
|
|
3299
|
-
return null;
|
|
3300
|
-
const name = m[1];
|
|
3301
|
-
const body = (m[2] ?? '').trim();
|
|
3302
|
-
// If body contains JSON object, use it as arguments; else empty object.
|
|
3303
|
-
let args = '{}';
|
|
3304
|
-
const jsonStart = body.indexOf('{');
|
|
3305
|
-
const jsonEnd = body.lastIndexOf('}');
|
|
3306
|
-
if (jsonStart !== -1 && jsonEnd > jsonStart) {
|
|
3307
|
-
const sub = body.slice(jsonStart, jsonEnd + 1);
|
|
3308
|
-
try {
|
|
3309
|
-
JSON.parse(sub);
|
|
3310
|
-
args = sub;
|
|
3311
|
-
}
|
|
3312
|
-
catch {
|
|
3313
|
-
// keep {}
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
return [{
|
|
3317
|
-
id: 'call_0',
|
|
3318
|
-
type: 'function',
|
|
3319
|
-
function: { name, arguments: args }
|
|
3320
|
-
}];
|
|
3321
|
-
}
|
|
3322
2840
|
//# sourceMappingURL=agent.js.map
|