@visorcraft/idlehands 1.3.7 → 1.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +516 -259
- package/dist/agent.js.map +1 -1
- package/dist/anton/controller.js +57 -15
- package/dist/anton/controller.js.map +1 -1
- package/dist/anton/verifier.js +73 -2
- package/dist/anton/verifier.js.map +1 -1
- package/dist/bot/commands.js +24 -2
- package/dist/bot/commands.js.map +1 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { generateMinimalDiff, toolResultSummary, execCommandFromSig, formatDurationMs, looksLikePlanningNarration, capTextByApproxTokens, isLikelyBinaryBuffer, sanitizePathsInMessage, digestToolResult, } from './agent/formatting.js';
|
|
4
|
+
import { reviewArtifactKeys, looksLikeCodeReviewRequest, looksLikeReviewRetrievalRequest, retrievalAllowsStaleArtifact, parseReviewArtifactStalePolicy, parseReviewArtifact, reviewArtifactStaleReason, gitHead, normalizeModelsResponse, } from './agent/review-artifact.js';
|
|
5
|
+
import { parseToolCallsFromContent, getMissingRequiredParams, getArgValidationIssues, stripMarkdownFences, parseJsonArgs, } from './agent/tool-calls.js';
|
|
6
|
+
import { ToolLoopGuard } from './agent/tool-loop-guard.js';
|
|
2
7
|
import { OpenAIClient } from './client.js';
|
|
3
|
-
import { enforceContextBudget, stripThinking, estimateTokensFromMessages, estimateToolSchemaTokens } from './history.js';
|
|
4
|
-
import * as tools from './tools.js';
|
|
5
|
-
import { selectHarness } from './harnesses.js';
|
|
6
|
-
import { BASE_MAX_TOKENS, deriveContextWindow, deriveGenerationParams, supportsVisionModel } from './model-customization.js';
|
|
7
|
-
import { HookManager, loadHookPlugins } from './hooks/index.js';
|
|
8
|
-
import { checkExecSafety, checkPathSafety } from './safety.js';
|
|
9
8
|
import { loadProjectContext } from './context.js';
|
|
10
9
|
import { loadGitContext, isGitDirty, stashWorkingTree } from './git.js';
|
|
10
|
+
import { selectHarness } from './harnesses.js';
|
|
11
|
+
import { enforceContextBudget, stripThinking, estimateTokensFromMessages, estimateToolSchemaTokens, } from './history.js';
|
|
12
|
+
import { HookManager, loadHookPlugins } from './hooks/index.js';
|
|
11
13
|
import { projectIndexKeys, parseIndexMeta, isFreshIndex, indexSummaryLine } from './indexer.js';
|
|
12
|
-
import { ReplayStore } from './replay.js';
|
|
13
|
-
import { VaultStore } from './vault.js';
|
|
14
14
|
import { LensStore } from './lens.js';
|
|
15
|
-
import { SYS_CONTEXT_SCHEMA, collectSnapshot } from './sys/context.js';
|
|
16
|
-
import { MCPManager } from './mcp.js';
|
|
17
15
|
import { LspManager, detectInstalledLspServers } from './lsp.js';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
16
|
+
import { MCPManager } from './mcp.js';
|
|
17
|
+
import { BASE_MAX_TOKENS, deriveContextWindow, deriveGenerationParams, supportsVisionModel, } from './model-customization.js';
|
|
18
|
+
import { ReplayStore } from './replay.js';
|
|
19
|
+
import { checkExecSafety, checkPathSafety } from './safety.js';
|
|
20
|
+
import { normalizeApprovalMode } from './shared/config-utils.js';
|
|
21
|
+
import { SYS_CONTEXT_SCHEMA, collectSnapshot } from './sys/context.js';
|
|
20
22
|
import { ToolError, ValidationError } from './tools/tool-error.js';
|
|
21
|
-
import
|
|
22
|
-
export { parseToolCallsFromContent };
|
|
23
|
-
import { reviewArtifactKeys, looksLikeCodeReviewRequest, looksLikeReviewRetrievalRequest, retrievalAllowsStaleArtifact, parseReviewArtifactStalePolicy, parseReviewArtifact, reviewArtifactStaleReason, gitHead, normalizeModelsResponse, } from './agent/review-artifact.js';
|
|
24
|
-
import fs from 'node:fs/promises';
|
|
25
|
-
import path from 'node:path';
|
|
23
|
+
import * as tools from './tools.js';
|
|
26
24
|
import { stateDir, timestampedId } from './utils.js';
|
|
25
|
+
import { VaultStore } from './vault.js';
|
|
26
|
+
export { parseToolCallsFromContent };
|
|
27
27
|
function makeAbortController() {
|
|
28
28
|
// Node 24: AbortController is global.
|
|
29
29
|
return new AbortController();
|
|
@@ -32,7 +32,9 @@ const CACHED_EXEC_OBSERVATION_HINT = '[idlehands hint] Reused cached output for
|
|
|
32
32
|
function looksLikeReadOnlyExecCommand(command) {
|
|
33
33
|
// Strip leading `cd <path> &&` / `cd <path>;` prefixes — cd is read-only
|
|
34
34
|
// navigation, the actual command that matters comes after.
|
|
35
|
-
let cmd = String(command || '')
|
|
35
|
+
let cmd = String(command || '')
|
|
36
|
+
.trim()
|
|
37
|
+
.toLowerCase();
|
|
36
38
|
if (!cmd)
|
|
37
39
|
return false;
|
|
38
40
|
cmd = cmd.replace(/^(\s*cd\s+[^;&|]+\s*(?:&&|;)\s*)+/i, '').trim();
|
|
@@ -158,48 +160,66 @@ class AgentLoopBreak extends Error {
|
|
|
158
160
|
this.name = 'AgentLoopBreak';
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
|
-
const SYSTEM_PROMPT = `You are a coding agent with filesystem and shell access. Execute the user's request using the provided tools.
|
|
162
|
-
|
|
163
|
-
Rules:
|
|
164
|
-
- Work in the current directory. Use relative paths for all file operations.
|
|
165
|
-
- Do the work directly. Do NOT use spawn_task to delegate the user's primary request — only use it for genuinely independent subtasks that benefit from parallel execution.
|
|
166
|
-
- 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.
|
|
167
|
-
- Read the target file before editing. You need the exact text for search/replace.
|
|
168
|
-
- Use read_file with search=... to jump to relevant code; avoid reading whole files.
|
|
169
|
-
- Never call read_file/read_files/list_dir twice in a row with identical arguments (same path/options). Reuse the previous result instead.
|
|
170
|
-
- Prefer apply_patch or edit_range for code edits (token-efficient). Use edit_file only when exact old_text replacement is necessary.
|
|
171
|
-
- Tool-call arguments MUST be strict JSON (double-quoted keys/strings, no comments, no trailing commas).
|
|
172
|
-
- edit_range example: {"path":"src/foo.ts","start_line":10,"end_line":14,"replacement":"line A\nline B"}
|
|
173
|
-
- apply_patch example: {"patch":"--- a/src/foo.ts\n+++ b/src/foo.ts\n@@ -10,2 +10,2 @@\n-old\n+new","files":["src/foo.ts"]}
|
|
174
|
-
- write_file is for new files or explicit full rewrites only. Existing non-empty files require overwrite=true/force=true.
|
|
175
|
-
- Use insert_file for insertions (prepend/append/line).
|
|
176
|
-
- Use exec to run commands, tests, builds; check results before reporting success.
|
|
177
|
-
- 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.
|
|
178
|
-
- Batch work: read all files you need, then apply all edits, then verify.
|
|
179
|
-
- Be concise. Report what you changed and why.
|
|
180
|
-
- Do NOT read every file in a directory. Use search_files or exec with grep to locate relevant code first, then read only the files that match.
|
|
181
|
-
- If search_files returns 0 matches, try a broader pattern or use: exec grep -rn "keyword" path/
|
|
182
|
-
- Anton (the autonomous task runner) is ONLY activated when the user explicitly invokes /anton. Never self-activate as Anton or start processing task files on your own.
|
|
183
|
-
|
|
184
|
-
Tool call format:
|
|
185
|
-
- Use tool_calls. Do not write JSON tool invocations in your message text.
|
|
163
|
+
const SYSTEM_PROMPT = `You are a coding agent with filesystem and shell access. Execute the user's request using the provided tools.
|
|
164
|
+
|
|
165
|
+
Rules:
|
|
166
|
+
- Work in the current directory. Use relative paths for all file operations.
|
|
167
|
+
- Do the work directly. Do NOT use spawn_task to delegate the user's primary request — only use it for genuinely independent subtasks that benefit from parallel execution.
|
|
168
|
+
- 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.
|
|
169
|
+
- Read the target file before editing. You need the exact text for search/replace.
|
|
170
|
+
- Use read_file with search=... to jump to relevant code; avoid reading whole files.
|
|
171
|
+
- Never call read_file/read_files/list_dir twice in a row with identical arguments (same path/options). Reuse the previous result instead.
|
|
172
|
+
- Prefer apply_patch or edit_range for code edits (token-efficient). Use edit_file only when exact old_text replacement is necessary.
|
|
173
|
+
- Tool-call arguments MUST be strict JSON (double-quoted keys/strings, no comments, no trailing commas).
|
|
174
|
+
- edit_range example: {"path":"src/foo.ts","start_line":10,"end_line":14,"replacement":"line A\nline B"}
|
|
175
|
+
- apply_patch example: {"patch":"--- a/src/foo.ts\n+++ b/src/foo.ts\n@@ -10,2 +10,2 @@\n-old\n+new","files":["src/foo.ts"]}
|
|
176
|
+
- write_file is for new files or explicit full rewrites only. Existing non-empty files require overwrite=true/force=true.
|
|
177
|
+
- Use insert_file for insertions (prepend/append/line).
|
|
178
|
+
- Use exec to run commands, tests, builds; check results before reporting success.
|
|
179
|
+
- 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.
|
|
180
|
+
- Batch work: read all files you need, then apply all edits, then verify.
|
|
181
|
+
- Be concise. Report what you changed and why.
|
|
182
|
+
- Do NOT read every file in a directory. Use search_files or exec with grep to locate relevant code first, then read only the files that match.
|
|
183
|
+
- If search_files returns 0 matches, try a broader pattern or use: exec grep -rn "keyword" path/
|
|
184
|
+
- Anton (the autonomous task runner) is ONLY activated when the user explicitly invokes /anton. Never self-activate as Anton or start processing task files on your own.
|
|
185
|
+
|
|
186
|
+
Tool call format:
|
|
187
|
+
- Use tool_calls. Do not write JSON tool invocations in your message text.
|
|
186
188
|
`;
|
|
187
189
|
const MCP_TOOLS_REQUEST_TOKEN = '[[MCP_TOOLS_REQUEST]]';
|
|
188
|
-
const DEFAULT_SUB_AGENT_SYSTEM_PROMPT = `You are a focused coding sub-agent. Execute only the delegated task.
|
|
189
|
-
- Work in the current directory. Use relative paths for all file operations.
|
|
190
|
-
- Read the target file before editing. You need the exact text for search/replace.
|
|
191
|
-
- Keep tool usage tight and efficient.
|
|
192
|
-
- Prefer surgical edits over rewrites.
|
|
193
|
-
- Do NOT create files outside the working directory unless explicitly requested.
|
|
194
|
-
- When running commands in a subdirectory, use exec's cwd parameter — NOT "cd /path && cmd".
|
|
195
|
-
- Run verification commands when relevant.
|
|
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.
|
|
196
198
|
- Return a concise outcome summary.`;
|
|
197
199
|
const DEFAULT_SUB_AGENT_RESULT_TOKEN_CAP = 4000;
|
|
198
|
-
const LSP_TOOL_NAMES = [
|
|
200
|
+
const LSP_TOOL_NAMES = [
|
|
201
|
+
'lsp_diagnostics',
|
|
202
|
+
'lsp_symbols',
|
|
203
|
+
'lsp_hover',
|
|
204
|
+
'lsp_definition',
|
|
205
|
+
'lsp_references',
|
|
206
|
+
];
|
|
199
207
|
const LSP_TOOL_NAME_SET = new Set(LSP_TOOL_NAMES);
|
|
200
|
-
const FILE_MUTATION_TOOL_SET = new Set([
|
|
208
|
+
const FILE_MUTATION_TOOL_SET = new Set([
|
|
209
|
+
'edit_file',
|
|
210
|
+
'edit_range',
|
|
211
|
+
'apply_patch',
|
|
212
|
+
'write_file',
|
|
213
|
+
'insert_file',
|
|
214
|
+
]);
|
|
201
215
|
/** Approval mode permissiveness ranking (lower = more restrictive). */
|
|
202
|
-
const APPROVAL_MODE_RANK = {
|
|
216
|
+
const APPROVAL_MODE_RANK = {
|
|
217
|
+
plan: 0,
|
|
218
|
+
reject: 1,
|
|
219
|
+
default: 2,
|
|
220
|
+
'auto-edit': 3,
|
|
221
|
+
yolo: 4,
|
|
222
|
+
};
|
|
203
223
|
/**
|
|
204
224
|
* Cap a sub-agent's approval mode at the parent's level.
|
|
205
225
|
* Sub-agents cannot escalate beyond the parent's approval mode.
|
|
@@ -284,11 +304,15 @@ function buildToolsSchema(opts) {
|
|
|
284
304
|
type: 'object',
|
|
285
305
|
additionalProperties: false,
|
|
286
306
|
properties,
|
|
287
|
-
required
|
|
307
|
+
required,
|
|
288
308
|
});
|
|
289
309
|
const str = () => ({ type: 'string' });
|
|
290
310
|
const bool = () => ({ type: 'boolean' });
|
|
291
|
-
const int = (min, max) => ({
|
|
311
|
+
const int = (min, max) => ({
|
|
312
|
+
type: 'integer',
|
|
313
|
+
...(min !== undefined && { minimum: min }),
|
|
314
|
+
...(max !== undefined && { maximum: max }),
|
|
315
|
+
});
|
|
292
316
|
const schemas = [
|
|
293
317
|
// ────────────────────────────────────────────────────────────────────────────
|
|
294
318
|
// Token-safe reads (require limit; allow plain output without per-line numbers)
|
|
@@ -338,7 +362,10 @@ function buildToolsSchema(opts) {
|
|
|
338
362
|
function: {
|
|
339
363
|
name: 'write_file',
|
|
340
364
|
description: 'Write file (atomic, backup). Existing non-empty files require overwrite=true (or force=true).',
|
|
341
|
-
parameters: obj({ path: str(), content: str(), overwrite: bool(), force: bool() }, [
|
|
365
|
+
parameters: obj({ path: str(), content: str(), overwrite: bool(), force: bool() }, [
|
|
366
|
+
'path',
|
|
367
|
+
'content',
|
|
368
|
+
]),
|
|
342
369
|
},
|
|
343
370
|
},
|
|
344
371
|
{
|
|
@@ -371,7 +398,11 @@ function buildToolsSchema(opts) {
|
|
|
371
398
|
function: {
|
|
372
399
|
name: 'edit_file',
|
|
373
400
|
description: 'Legacy exact replace (requires old_text). Prefer apply_patch/edit_range.',
|
|
374
|
-
parameters: obj({ path: str(), old_text: str(), new_text: str(), replace_all: bool() }, [
|
|
401
|
+
parameters: obj({ path: str(), old_text: str(), new_text: str(), replace_all: bool() }, [
|
|
402
|
+
'path',
|
|
403
|
+
'old_text',
|
|
404
|
+
'new_text',
|
|
405
|
+
]),
|
|
375
406
|
},
|
|
376
407
|
},
|
|
377
408
|
{
|
|
@@ -398,7 +429,10 @@ function buildToolsSchema(opts) {
|
|
|
398
429
|
function: {
|
|
399
430
|
name: 'search_files',
|
|
400
431
|
description: 'Search regex in files.',
|
|
401
|
-
parameters: obj({ pattern: str(), path: str(), include: str(), max_results: int(1, 100) }, [
|
|
432
|
+
parameters: obj({ pattern: str(), path: str(), include: str(), max_results: int(1, 100) }, [
|
|
433
|
+
'pattern',
|
|
434
|
+
'path',
|
|
435
|
+
]),
|
|
402
436
|
},
|
|
403
437
|
},
|
|
404
438
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -428,18 +462,42 @@ function buildToolsSchema(opts) {
|
|
|
428
462
|
max_tokens: int(),
|
|
429
463
|
timeout_sec: int(),
|
|
430
464
|
system_prompt: str(),
|
|
431
|
-
approval_mode: {
|
|
465
|
+
approval_mode: {
|
|
466
|
+
type: 'string',
|
|
467
|
+
enum: ['plan', 'reject', 'default', 'auto-edit', 'yolo'],
|
|
468
|
+
},
|
|
432
469
|
}, ['task']),
|
|
433
470
|
},
|
|
434
471
|
});
|
|
435
472
|
}
|
|
436
473
|
if (opts?.activeVaultTools) {
|
|
437
|
-
schemas.push({
|
|
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
|
+
});
|
|
438
489
|
}
|
|
439
490
|
else if (opts?.passiveVault) {
|
|
440
491
|
// In passive mode, expose vault_search (read-only) so the model can recover
|
|
441
492
|
// compacted context on demand, but don't expose vault_note (write).
|
|
442
|
-
schemas.push({
|
|
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
|
+
});
|
|
443
501
|
}
|
|
444
502
|
// Phase 9: sys_context tool is only available in sys mode.
|
|
445
503
|
if (opts?.sysMode) {
|
|
@@ -451,36 +509,48 @@ function buildToolsSchema(opts) {
|
|
|
451
509
|
function: {
|
|
452
510
|
name: 'lsp_diagnostics',
|
|
453
511
|
description: 'Get LSP diagnostics (errors/warnings) for file or project.',
|
|
454
|
-
parameters: obj({ path: str(), severity: int() }, [])
|
|
455
|
-
}
|
|
512
|
+
parameters: obj({ path: str(), severity: int() }, []),
|
|
513
|
+
},
|
|
456
514
|
}, {
|
|
457
515
|
type: 'function',
|
|
458
516
|
function: {
|
|
459
517
|
name: 'lsp_symbols',
|
|
460
518
|
description: 'List symbols (functions, classes, vars) in a file.',
|
|
461
|
-
parameters: obj({ path: str() }, ['path'])
|
|
462
|
-
}
|
|
519
|
+
parameters: obj({ path: str() }, ['path']),
|
|
520
|
+
},
|
|
463
521
|
}, {
|
|
464
522
|
type: 'function',
|
|
465
523
|
function: {
|
|
466
524
|
name: 'lsp_hover',
|
|
467
525
|
description: 'Get type/docs for symbol at position.',
|
|
468
|
-
parameters: obj({ path: str(), line: int(), character: int() }, [
|
|
469
|
-
|
|
526
|
+
parameters: obj({ path: str(), line: int(), character: int() }, [
|
|
527
|
+
'path',
|
|
528
|
+
'line',
|
|
529
|
+
'character',
|
|
530
|
+
]),
|
|
531
|
+
},
|
|
470
532
|
}, {
|
|
471
533
|
type: 'function',
|
|
472
534
|
function: {
|
|
473
535
|
name: 'lsp_definition',
|
|
474
536
|
description: 'Go to definition of symbol at position.',
|
|
475
|
-
parameters: obj({ path: str(), line: int(), character: int() }, [
|
|
476
|
-
|
|
537
|
+
parameters: obj({ path: str(), line: int(), character: int() }, [
|
|
538
|
+
'path',
|
|
539
|
+
'line',
|
|
540
|
+
'character',
|
|
541
|
+
]),
|
|
542
|
+
},
|
|
477
543
|
}, {
|
|
478
544
|
type: 'function',
|
|
479
545
|
function: {
|
|
480
546
|
name: 'lsp_references',
|
|
481
547
|
description: 'Find all references to symbol at position.',
|
|
482
|
-
parameters: obj({ path: str(), line: int(), character: int(), max_results: int() }, [
|
|
483
|
-
|
|
548
|
+
parameters: obj({ path: str(), line: int(), character: int(), max_results: int() }, [
|
|
549
|
+
'path',
|
|
550
|
+
'line',
|
|
551
|
+
'character',
|
|
552
|
+
]),
|
|
553
|
+
},
|
|
484
554
|
});
|
|
485
555
|
}
|
|
486
556
|
if (opts?.mcpTools?.length) {
|
|
@@ -489,7 +559,12 @@ function buildToolsSchema(opts) {
|
|
|
489
559
|
return schemas;
|
|
490
560
|
}
|
|
491
561
|
function isReadOnlyTool(name) {
|
|
492
|
-
return name === 'read_file' ||
|
|
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');
|
|
493
568
|
}
|
|
494
569
|
/** Human-readable summary of what a blocked tool call would do. */
|
|
495
570
|
function planModeSummary(name, args) {
|
|
@@ -544,20 +619,23 @@ export async function createSession(opts) {
|
|
|
544
619
|
if (typeof cfg.response_timeout === 'number' && cfg.response_timeout > 0) {
|
|
545
620
|
client.setResponseTimeout(cfg.response_timeout);
|
|
546
621
|
}
|
|
547
|
-
if (typeof client.setConnectionTimeout === 'function' &&
|
|
622
|
+
if (typeof client.setConnectionTimeout === 'function' &&
|
|
623
|
+
typeof cfg.connection_timeout === 'number' &&
|
|
624
|
+
cfg.connection_timeout > 0) {
|
|
548
625
|
client.setConnectionTimeout(cfg.connection_timeout);
|
|
549
626
|
}
|
|
550
|
-
if (typeof client.setInitialConnectionCheck === 'function' &&
|
|
627
|
+
if (typeof client.setInitialConnectionCheck === 'function' &&
|
|
628
|
+
typeof cfg.initial_connection_check === 'boolean') {
|
|
551
629
|
client.setInitialConnectionCheck(cfg.initial_connection_check);
|
|
552
630
|
}
|
|
553
|
-
if (typeof client.setInitialConnectionProbeTimeout === 'function' &&
|
|
631
|
+
if (typeof client.setInitialConnectionProbeTimeout === 'function' &&
|
|
632
|
+
typeof cfg.initial_connection_timeout === 'number' &&
|
|
633
|
+
cfg.initial_connection_timeout > 0) {
|
|
554
634
|
client.setInitialConnectionProbeTimeout(cfg.initial_connection_timeout);
|
|
555
635
|
}
|
|
556
636
|
// Health check + model list (cheap, avoids wasting GPU on chat warmups if unreachable)
|
|
557
637
|
let modelsList = normalizeModelsResponse(await client.models().catch(() => null));
|
|
558
|
-
let model = cfg.model && cfg.model.trim().length
|
|
559
|
-
? cfg.model
|
|
560
|
-
: await autoPickModel(client, modelsList);
|
|
638
|
+
let model = cfg.model && cfg.model.trim().length ? cfg.model : await autoPickModel(client, modelsList);
|
|
561
639
|
let harness = selectHarness(model, cfg.harness && cfg.harness.trim() ? cfg.harness.trim() : undefined);
|
|
562
640
|
// Try to derive context window from /v1/models (if provided by server).
|
|
563
641
|
const explicitContextWindow = cfg.context_window != null;
|
|
@@ -570,19 +648,22 @@ export async function createSession(opts) {
|
|
|
570
648
|
let supportsVision = supportsVisionModel(model, modelMeta, harness);
|
|
571
649
|
const sessionId = `session-${timestampedId()}`;
|
|
572
650
|
const hookCfg = cfg.hooks ?? {};
|
|
573
|
-
const hookManager = opts.runtime?.hookManager ??
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
651
|
+
const hookManager = opts.runtime?.hookManager ??
|
|
652
|
+
new HookManager({
|
|
653
|
+
enabled: hookCfg.enabled !== false,
|
|
654
|
+
strict: hookCfg.strict === true,
|
|
655
|
+
warnMs: hookCfg.warn_ms,
|
|
656
|
+
allowedCapabilities: Array.isArray(hookCfg.allow_capabilities)
|
|
657
|
+
? hookCfg.allow_capabilities
|
|
658
|
+
: undefined,
|
|
659
|
+
context: () => ({
|
|
660
|
+
sessionId,
|
|
661
|
+
cwd: projectDir,
|
|
662
|
+
model,
|
|
663
|
+
harness: harness.id,
|
|
664
|
+
endpoint: cfg.endpoint,
|
|
665
|
+
}),
|
|
666
|
+
});
|
|
586
667
|
const emitDetached = (promise, eventName) => {
|
|
587
668
|
void promise.catch((error) => {
|
|
588
669
|
if (!process.env.IDLEHANDS_QUIET_WARNINGS) {
|
|
@@ -628,16 +709,33 @@ export async function createSession(opts) {
|
|
|
628
709
|
const lensEnabled = cfg.trifecta?.enabled !== false && cfg.trifecta?.lens?.enabled !== false;
|
|
629
710
|
const spawnTaskEnabled = opts.allowSpawnTask !== false && cfg.sub_agents?.enabled !== false;
|
|
630
711
|
const mcpServers = Array.isArray(cfg.mcp?.servers) ? cfg.mcp.servers : [];
|
|
631
|
-
const mcpEnabledTools = Array.isArray(cfg.mcp?.enabled_tools)
|
|
712
|
+
const mcpEnabledTools = Array.isArray(cfg.mcp?.enabled_tools)
|
|
713
|
+
? cfg.mcp?.enabled_tools
|
|
714
|
+
: undefined;
|
|
632
715
|
const mcpToolBudget = Number.isFinite(cfg.mcp_tool_budget)
|
|
633
716
|
? Number(cfg.mcp_tool_budget)
|
|
634
|
-
:
|
|
717
|
+
: Number.isFinite(cfg.mcp?.tool_budget)
|
|
718
|
+
? Number(cfg.mcp?.tool_budget)
|
|
719
|
+
: 1000;
|
|
635
720
|
const mcpCallTimeoutSec = Number.isFinite(cfg.mcp_call_timeout_sec)
|
|
636
721
|
? Number(cfg.mcp_call_timeout_sec)
|
|
637
|
-
:
|
|
722
|
+
: Number.isFinite(cfg.mcp?.call_timeout_sec)
|
|
723
|
+
? Number(cfg.mcp?.call_timeout_sec)
|
|
724
|
+
: 30;
|
|
638
725
|
const builtInToolNames = [
|
|
639
|
-
'read_file',
|
|
640
|
-
'
|
|
726
|
+
'read_file',
|
|
727
|
+
'read_files',
|
|
728
|
+
'write_file',
|
|
729
|
+
'apply_patch',
|
|
730
|
+
'edit_range',
|
|
731
|
+
'edit_file',
|
|
732
|
+
'insert_file',
|
|
733
|
+
'list_dir',
|
|
734
|
+
'search_files',
|
|
735
|
+
'exec',
|
|
736
|
+
'vault_search',
|
|
737
|
+
'vault_note',
|
|
738
|
+
'sys_context',
|
|
641
739
|
...(spawnTaskEnabled ? ['spawn_task'] : []),
|
|
642
740
|
];
|
|
643
741
|
const mcpManager = mcpServers.length
|
|
@@ -693,9 +791,11 @@ export async function createSession(opts) {
|
|
|
693
791
|
allowSpawnTask: spawnTaskEnabled,
|
|
694
792
|
});
|
|
695
793
|
const vault = vaultEnabled
|
|
696
|
-
? (opts.runtime?.vault ??
|
|
697
|
-
|
|
698
|
-
|
|
794
|
+
? (opts.runtime?.vault ??
|
|
795
|
+
new VaultStore({
|
|
796
|
+
immutableReviewArtifactsPerProject: cfg?.trifecta?.vault
|
|
797
|
+
?.immutable_review_artifacts_per_project,
|
|
798
|
+
}))
|
|
699
799
|
: undefined;
|
|
700
800
|
if (vault) {
|
|
701
801
|
// Scope vault entries by project directory to prevent cross-project context leaks
|
|
@@ -764,12 +864,15 @@ export async function createSession(opts) {
|
|
|
764
864
|
const lspServers = lspManager.listServers();
|
|
765
865
|
const running = lspServers.filter((s) => s.running).length;
|
|
766
866
|
sessionMeta += `\n\n[LSP] ${running} language server(s) active: ${lspServers.map((s) => `${s.language} (${s.command})`).join(', ')}.`;
|
|
767
|
-
sessionMeta +=
|
|
867
|
+
sessionMeta +=
|
|
868
|
+
'\n[LSP] Use lsp_diagnostics, lsp_symbols, lsp_hover, lsp_definition, lsp_references tools for semantic code intelligence.';
|
|
768
869
|
if (lensEnabled) {
|
|
769
|
-
sessionMeta +=
|
|
870
|
+
sessionMeta +=
|
|
871
|
+
'\n[LSP+Lens] lsp_symbols combines semantic symbol data with structural Lens context when available.';
|
|
770
872
|
}
|
|
771
873
|
if (lspCfg?.proactive_diagnostics !== false) {
|
|
772
|
-
sessionMeta +=
|
|
874
|
+
sessionMeta +=
|
|
875
|
+
'\n[LSP] Proactive diagnostics enabled: errors will be reported automatically after file edits.';
|
|
773
876
|
}
|
|
774
877
|
}
|
|
775
878
|
if (mcpManager) {
|
|
@@ -809,21 +912,26 @@ export async function createSession(opts) {
|
|
|
809
912
|
if (harness.quirks.needsExplicitToolCallFormatReminder) {
|
|
810
913
|
if (client.contentModeToolCalls) {
|
|
811
914
|
// In content mode, tell the model to use JSON tool calls in its output
|
|
812
|
-
sessionMeta +=
|
|
915
|
+
sessionMeta +=
|
|
916
|
+
'\n\nYou have access to the following tools. To call a tool, output a JSON block in your response like this:\n```json\n{"name": "tool_name", "arguments": {"param": "value"}}\n```\nAvailable tools:\n';
|
|
813
917
|
const toolSchemas = getToolsSchema();
|
|
814
918
|
for (const t of toolSchemas) {
|
|
815
919
|
const fn = t.function;
|
|
816
920
|
if (fn) {
|
|
817
921
|
const params = fn.parameters?.properties
|
|
818
|
-
? Object.entries(fn.parameters.properties)
|
|
922
|
+
? Object.entries(fn.parameters.properties)
|
|
923
|
+
.map(([k, v]) => `${k}: ${v.type ?? 'any'}`)
|
|
924
|
+
.join(', ')
|
|
819
925
|
: '';
|
|
820
926
|
sessionMeta += `- ${fn.name}(${params}): ${fn.description ?? ''}\n`;
|
|
821
927
|
}
|
|
822
928
|
}
|
|
823
|
-
sessionMeta +=
|
|
929
|
+
sessionMeta +=
|
|
930
|
+
'\nIMPORTANT: Output tool calls as JSON blocks in your message. Do NOT use the tool_calls API mechanism.\nIf you use XML/function tags (e.g. <function=name>), include a full JSON object of arguments between braces.';
|
|
824
931
|
}
|
|
825
932
|
else {
|
|
826
|
-
sessionMeta +=
|
|
933
|
+
sessionMeta +=
|
|
934
|
+
'\n\nIMPORTANT: Use the tool_calls mechanism to invoke tools. Do NOT write JSON tool invocations in your message text.';
|
|
827
935
|
}
|
|
828
936
|
// One-time tool-call template smoke test (first ask() call only, skip in content mode)
|
|
829
937
|
if (!client.contentModeToolCalls && !client.__toolCallSmokeTested) {
|
|
@@ -859,9 +967,7 @@ export async function createSession(opts) {
|
|
|
859
967
|
}
|
|
860
968
|
return p;
|
|
861
969
|
};
|
|
862
|
-
let messages = [
|
|
863
|
-
{ role: 'system', content: buildEffectiveSystemPrompt() }
|
|
864
|
-
];
|
|
970
|
+
let messages = [{ role: 'system', content: buildEffectiveSystemPrompt() }];
|
|
865
971
|
let sessionMetaPending = sessionMeta;
|
|
866
972
|
const setSystemPrompt = (prompt) => {
|
|
867
973
|
const next = String(prompt ?? '').trim();
|
|
@@ -883,9 +989,7 @@ export async function createSession(opts) {
|
|
|
883
989
|
};
|
|
884
990
|
const reset = () => {
|
|
885
991
|
const effective = buildEffectiveSystemPrompt();
|
|
886
|
-
messages = [
|
|
887
|
-
{ role: 'system', content: effective }
|
|
888
|
-
];
|
|
992
|
+
messages = [{ role: 'system', content: effective }];
|
|
889
993
|
sessionMetaPending = sessionMeta;
|
|
890
994
|
lastEditedPath = undefined;
|
|
891
995
|
initialConnectionProbeDone = false;
|
|
@@ -962,7 +1066,9 @@ export async function createSession(opts) {
|
|
|
962
1066
|
}
|
|
963
1067
|
// Prevent using delegation to bypass package-install confirmation restrictions.
|
|
964
1068
|
const taskSafety = checkExecSafety(task);
|
|
965
|
-
if (!cfg.no_confirm &&
|
|
1069
|
+
if (!cfg.no_confirm &&
|
|
1070
|
+
taskSafety.tier === 'cautious' &&
|
|
1071
|
+
taskSafety.reason === 'package install/remove') {
|
|
966
1072
|
throw new Error('spawn_task: blocked — package install/remove is restricted in the current approval mode. ' +
|
|
967
1073
|
'Do not delegate this to bypass confirmation requirements; ask the user to run with --no-confirm/--yolo instead.');
|
|
968
1074
|
}
|
|
@@ -971,39 +1077,43 @@ export async function createSession(opts) {
|
|
|
971
1077
|
const emitStatus = options?.emitStatus ?? (() => { });
|
|
972
1078
|
const maxIterations = Number.isFinite(args?.max_iterations)
|
|
973
1079
|
? Math.max(1, Math.floor(Number(args.max_iterations)))
|
|
974
|
-
:
|
|
1080
|
+
: Number.isFinite(defaults.max_iterations)
|
|
975
1081
|
? Math.max(1, Math.floor(Number(defaults.max_iterations)))
|
|
976
|
-
: 50
|
|
1082
|
+
: 50;
|
|
977
1083
|
const timeoutSec = Number.isFinite(args?.timeout_sec)
|
|
978
1084
|
? Math.max(1, Math.floor(Number(args.timeout_sec)))
|
|
979
|
-
:
|
|
1085
|
+
: Number.isFinite(defaults.timeout_sec)
|
|
980
1086
|
? Math.max(1, Math.floor(Number(defaults.timeout_sec)))
|
|
981
|
-
: Math.max(60, cfg.timeout)
|
|
1087
|
+
: Math.max(60, cfg.timeout);
|
|
982
1088
|
const subMaxTokens = Number.isFinite(args?.max_tokens)
|
|
983
1089
|
? Math.max(128, Math.floor(Number(args.max_tokens)))
|
|
984
|
-
:
|
|
1090
|
+
: Number.isFinite(defaults.max_tokens)
|
|
985
1091
|
? Math.max(128, Math.floor(Number(defaults.max_tokens)))
|
|
986
|
-
: maxTokens
|
|
1092
|
+
: maxTokens;
|
|
987
1093
|
const resultTokenCap = Number.isFinite(defaults.result_token_cap)
|
|
988
1094
|
? Math.max(256, Math.floor(Number(defaults.result_token_cap)))
|
|
989
1095
|
: DEFAULT_SUB_AGENT_RESULT_TOKEN_CAP;
|
|
990
1096
|
const parentApproval = cfg.approval_mode ?? 'default';
|
|
991
|
-
const rawApproval = normalizeApprovalMode(args?.approval_mode)
|
|
992
|
-
|
|
993
|
-
|
|
1097
|
+
const rawApproval = normalizeApprovalMode(args?.approval_mode) ??
|
|
1098
|
+
normalizeApprovalMode(defaults.approval_mode) ??
|
|
1099
|
+
parentApproval;
|
|
994
1100
|
// Sub-agents cannot escalate beyond the parent's approval mode.
|
|
995
1101
|
const approvalMode = capApprovalMode(rawApproval, parentApproval);
|
|
996
1102
|
const requestedModel = typeof args?.model === 'string' && args.model.trim()
|
|
997
1103
|
? args.model.trim()
|
|
998
|
-
:
|
|
1104
|
+
: typeof defaults.model === 'string' && defaults.model.trim()
|
|
1105
|
+
? defaults.model.trim()
|
|
1106
|
+
: model;
|
|
999
1107
|
const requestedEndpoint = typeof args?.endpoint === 'string' && args.endpoint.trim()
|
|
1000
1108
|
? args.endpoint.trim()
|
|
1001
|
-
:
|
|
1109
|
+
: typeof defaults.endpoint === 'string' && defaults.endpoint.trim()
|
|
1110
|
+
? defaults.endpoint.trim()
|
|
1111
|
+
: cfg.endpoint;
|
|
1002
1112
|
const requestedSystemPrompt = typeof args?.system_prompt === 'string' && args.system_prompt.trim()
|
|
1003
1113
|
? args.system_prompt.trim()
|
|
1004
|
-
:
|
|
1114
|
+
: typeof defaults.system_prompt === 'string' && defaults.system_prompt.trim()
|
|
1005
1115
|
? defaults.system_prompt.trim()
|
|
1006
|
-
: DEFAULT_SUB_AGENT_SYSTEM_PROMPT
|
|
1116
|
+
: DEFAULT_SUB_AGENT_SYSTEM_PROMPT;
|
|
1007
1117
|
const cwd = projectDir;
|
|
1008
1118
|
const ctxFiles = await buildSubAgentContextBlock(cwd, args?.context_files);
|
|
1009
1119
|
let delegatedInstruction = task;
|
|
@@ -1108,7 +1218,9 @@ export async function createSession(opts) {
|
|
|
1108
1218
|
`turns: ${subTurns}`,
|
|
1109
1219
|
`tool_calls: ${subToolCalls}`,
|
|
1110
1220
|
`files_changed: ${filesChanged.length ? filesChanged.join(', ') : 'none'}`,
|
|
1111
|
-
capped.truncated
|
|
1221
|
+
capped.truncated
|
|
1222
|
+
? `[sub-agent] summarized result capped to ~${resultTokenCap} tokens`
|
|
1223
|
+
: `[sub-agent] summarized result within cap`,
|
|
1112
1224
|
`result:\n${capped.text}`,
|
|
1113
1225
|
].join('\n');
|
|
1114
1226
|
});
|
|
@@ -1117,7 +1229,10 @@ export async function createSession(opts) {
|
|
|
1117
1229
|
const buildToolCtx = (overrides) => {
|
|
1118
1230
|
const defaultConfirmBridge = opts.confirmProvider
|
|
1119
1231
|
? async (prompt) => opts.confirmProvider.confirm({
|
|
1120
|
-
tool: '',
|
|
1232
|
+
tool: '',
|
|
1233
|
+
args: {},
|
|
1234
|
+
summary: prompt,
|
|
1235
|
+
mode: cfg.approval_mode,
|
|
1121
1236
|
})
|
|
1122
1237
|
: opts.confirm;
|
|
1123
1238
|
return {
|
|
@@ -1136,7 +1251,10 @@ export async function createSession(opts) {
|
|
|
1136
1251
|
vault,
|
|
1137
1252
|
lens,
|
|
1138
1253
|
signal: overrides?.signal ?? inFlight?.signal,
|
|
1139
|
-
onMutation: overrides?.onMutation ??
|
|
1254
|
+
onMutation: overrides?.onMutation ??
|
|
1255
|
+
((absPath) => {
|
|
1256
|
+
lastEditedPath = absPath;
|
|
1257
|
+
}),
|
|
1140
1258
|
};
|
|
1141
1259
|
};
|
|
1142
1260
|
const buildLspLensSymbolOutput = async (filePathRaw) => {
|
|
@@ -1178,8 +1296,8 @@ export async function createSession(opts) {
|
|
|
1178
1296
|
if (!planSteps.length)
|
|
1179
1297
|
return ['No plan steps to execute.'];
|
|
1180
1298
|
const toExec = index != null
|
|
1181
|
-
? planSteps.filter(s => s.index === index && s.blocked && !s.executed)
|
|
1182
|
-
: planSteps.filter(s => s.blocked && !s.executed);
|
|
1299
|
+
? planSteps.filter((s) => s.index === index && s.blocked && !s.executed)
|
|
1300
|
+
: planSteps.filter((s) => s.blocked && !s.executed);
|
|
1183
1301
|
if (!toExec.length)
|
|
1184
1302
|
return ['No pending blocked steps to execute.'];
|
|
1185
1303
|
const ctx = buildToolCtx();
|
|
@@ -1271,9 +1389,7 @@ export async function createSession(opts) {
|
|
|
1271
1389
|
const toolCalls = m.tool_calls;
|
|
1272
1390
|
if (toolCalls?.length) {
|
|
1273
1391
|
for (const tc of toolCalls) {
|
|
1274
|
-
const args = typeof tc.function?.arguments === 'string'
|
|
1275
|
-
? tc.function.arguments.slice(0, 200)
|
|
1276
|
-
: '';
|
|
1392
|
+
const args = typeof tc.function?.arguments === 'string' ? tc.function.arguments.slice(0, 200) : '';
|
|
1277
1393
|
parts.push(`[tool_call: ${tc.function?.name}(${args})]`);
|
|
1278
1394
|
}
|
|
1279
1395
|
}
|
|
@@ -1325,16 +1441,16 @@ export async function createSession(opts) {
|
|
|
1325
1441
|
const hits = await vault.search(query, 4);
|
|
1326
1442
|
if (!hits.length)
|
|
1327
1443
|
return;
|
|
1328
|
-
const lines = hits.map((r) => `${r.updatedAt} ${r.kind} ${r.key ?? r.tool ?? r.id} ${String(r.value ?? r.snippet ?? '')
|
|
1444
|
+
const lines = hits.map((r) => `${r.updatedAt} ${r.kind} ${r.key ?? r.tool ?? r.id} ${String(r.value ?? r.snippet ?? '')
|
|
1445
|
+
.replace(/\s+/g, ' ')
|
|
1446
|
+
.slice(0, 180)}`);
|
|
1329
1447
|
if (!lines.length)
|
|
1330
1448
|
return;
|
|
1331
1449
|
lastVaultInjectionQuery = query;
|
|
1332
|
-
const vaultContextHeader = vaultMode === 'passive'
|
|
1333
|
-
? '[Trifecta Vault (passive)]'
|
|
1334
|
-
: '[Vault context after compaction]';
|
|
1450
|
+
const vaultContextHeader = vaultMode === 'passive' ? '[Trifecta Vault (passive)]' : '[Vault context after compaction]';
|
|
1335
1451
|
messages.push({
|
|
1336
1452
|
role: 'user',
|
|
1337
|
-
content: `${vaultContextHeader} Relevant entries for "${query}":\n${lines.join('\n')}
|
|
1453
|
+
content: `${vaultContextHeader} Relevant entries for "${query}":\n${lines.join('\n')}`,
|
|
1338
1454
|
});
|
|
1339
1455
|
};
|
|
1340
1456
|
let compactionLockTail = Promise.resolve();
|
|
@@ -1402,9 +1518,11 @@ export async function createSession(opts) {
|
|
|
1402
1518
|
}
|
|
1403
1519
|
};
|
|
1404
1520
|
const compactHistory = async (opts) => {
|
|
1405
|
-
const reason = opts?.reason
|
|
1406
|
-
|
|
1407
|
-
|
|
1521
|
+
const reason = opts?.reason ??
|
|
1522
|
+
(opts?.hard
|
|
1523
|
+
? 'manual hard compaction'
|
|
1524
|
+
: opts?.force
|
|
1525
|
+
? 'manual force compaction'
|
|
1408
1526
|
: 'manual compaction');
|
|
1409
1527
|
return await runCompactionWithLock(reason, async () => {
|
|
1410
1528
|
const beforeMessages = messages.length;
|
|
@@ -1430,8 +1548,12 @@ export async function createSession(opts) {
|
|
|
1430
1548
|
let dropped = messages.filter((m) => !compactedByRefs.has(m));
|
|
1431
1549
|
if (opts?.topic) {
|
|
1432
1550
|
const topic = opts.topic.toLowerCase();
|
|
1433
|
-
dropped = dropped.filter((m) => !userContentToText(m.content ?? '')
|
|
1434
|
-
|
|
1551
|
+
dropped = dropped.filter((m) => !userContentToText(m.content ?? '')
|
|
1552
|
+
.toLowerCase()
|
|
1553
|
+
.includes(topic));
|
|
1554
|
+
const keepFromTopic = messages.filter((m) => userContentToText(m.content ?? '')
|
|
1555
|
+
.toLowerCase()
|
|
1556
|
+
.includes(topic));
|
|
1435
1557
|
compacted = [...compacted, ...keepFromTopic.filter((m) => !compactedByRefs.has(m))];
|
|
1436
1558
|
}
|
|
1437
1559
|
const archivedToolMessages = dropped.filter((m) => m.role === 'tool').length;
|
|
@@ -1447,7 +1569,10 @@ export async function createSession(opts) {
|
|
|
1447
1569
|
const m = messages[i];
|
|
1448
1570
|
if (m.role === 'user') {
|
|
1449
1571
|
const text = userContentToText((m.content ?? '')).trim();
|
|
1450
|
-
if (text &&
|
|
1572
|
+
if (text &&
|
|
1573
|
+
!text.startsWith('[Trifecta Vault') &&
|
|
1574
|
+
!text.startsWith('[Vault context') &&
|
|
1575
|
+
text.length > 20) {
|
|
1451
1576
|
userPromptToPreserve = text;
|
|
1452
1577
|
break;
|
|
1453
1578
|
}
|
|
@@ -1467,7 +1592,10 @@ export async function createSession(opts) {
|
|
|
1467
1592
|
// Update current context token count after compaction
|
|
1468
1593
|
currentContextTokens = estimateTokensFromMessages(compacted);
|
|
1469
1594
|
if (dropped.length) {
|
|
1470
|
-
messages.push({
|
|
1595
|
+
messages.push({
|
|
1596
|
+
role: 'system',
|
|
1597
|
+
content: buildCompactionSystemNote('manual', dropped.length),
|
|
1598
|
+
});
|
|
1471
1599
|
await injectVaultContext().catch(() => { });
|
|
1472
1600
|
if (opts?.reason || opts?.force) {
|
|
1473
1601
|
injectCompactionReminder(opts?.reason ?? 'history compaction');
|
|
@@ -1495,7 +1623,10 @@ export async function createSession(opts) {
|
|
|
1495
1623
|
let lastTurnMetrics;
|
|
1496
1624
|
let lastServerHealth;
|
|
1497
1625
|
let lastToolLoopStats = {
|
|
1498
|
-
totalHistory: 0,
|
|
1626
|
+
totalHistory: 0,
|
|
1627
|
+
signatures: [],
|
|
1628
|
+
outcomes: [],
|
|
1629
|
+
telemetry: {
|
|
1499
1630
|
callsRegistered: 0,
|
|
1500
1631
|
dedupedReplays: 0,
|
|
1501
1632
|
readCacheLookups: 0,
|
|
@@ -1505,7 +1636,7 @@ export async function createSession(opts) {
|
|
|
1505
1636
|
recoveryRecommended: 0,
|
|
1506
1637
|
readCacheHitRate: 0,
|
|
1507
1638
|
dedupeRate: 0,
|
|
1508
|
-
}
|
|
1639
|
+
},
|
|
1509
1640
|
};
|
|
1510
1641
|
let lastModelsProbeMs = 0;
|
|
1511
1642
|
const capturesDir = path.join(stateDir(), 'captures');
|
|
@@ -1667,7 +1798,8 @@ export async function createSession(opts) {
|
|
|
1667
1798
|
modelsList = normalizeModelsResponse(await client.models());
|
|
1668
1799
|
const chosen = modelName?.trim()
|
|
1669
1800
|
? modelName.trim()
|
|
1670
|
-
: (modelsList.data.find((m) => m.id === model)?.id ??
|
|
1801
|
+
: (modelsList.data.find((m) => m.id === model)?.id ??
|
|
1802
|
+
(await autoPickModel(client, modelsList)));
|
|
1671
1803
|
setModel(chosen);
|
|
1672
1804
|
};
|
|
1673
1805
|
const captureOn = async (filePath) => {
|
|
@@ -1685,9 +1817,7 @@ export async function createSession(opts) {
|
|
|
1685
1817
|
if (!lastCaptureRecord) {
|
|
1686
1818
|
throw new Error('No captured request/response pair is available yet.');
|
|
1687
1819
|
}
|
|
1688
|
-
const target = filePath?.trim()
|
|
1689
|
-
? path.resolve(filePath)
|
|
1690
|
-
: (capturePath || defaultCapturePath());
|
|
1820
|
+
const target = filePath?.trim() ? path.resolve(filePath) : capturePath || defaultCapturePath();
|
|
1691
1821
|
await appendCaptureRecord(lastCaptureRecord, target);
|
|
1692
1822
|
return target;
|
|
1693
1823
|
};
|
|
@@ -1741,7 +1871,7 @@ export async function createSession(opts) {
|
|
|
1741
1871
|
const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * q)));
|
|
1742
1872
|
return sorted[idx];
|
|
1743
1873
|
};
|
|
1744
|
-
const avg = (arr) =>
|
|
1874
|
+
const avg = (arr) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : undefined;
|
|
1745
1875
|
return {
|
|
1746
1876
|
turns: turnDurationsMs.length,
|
|
1747
1877
|
totalTokens,
|
|
@@ -1810,7 +1940,7 @@ export async function createSession(opts) {
|
|
|
1810
1940
|
setModel(nextModel);
|
|
1811
1941
|
messages.push({
|
|
1812
1942
|
role: 'system',
|
|
1813
|
-
content: '[system] Model changed mid-session. Previous context may not transfer perfectly.'
|
|
1943
|
+
content: '[system] Model changed mid-session. Previous context may not transfer perfectly.',
|
|
1814
1944
|
});
|
|
1815
1945
|
console.warn(`[model] Server model changed: ${previousModel} → ${nextModel} - switching harness to ${harness.id}`);
|
|
1816
1946
|
};
|
|
@@ -1835,7 +1965,7 @@ export async function createSession(opts) {
|
|
|
1835
1965
|
sessionMetaPending = null;
|
|
1836
1966
|
}
|
|
1837
1967
|
messages.push({ role: 'user', content: userContent });
|
|
1838
|
-
const hookObj = typeof hooks === 'function' ? { onToken: hooks } : hooks ?? {};
|
|
1968
|
+
const hookObj = typeof hooks === 'function' ? { onToken: hooks } : (hooks ?? {});
|
|
1839
1969
|
let turns = 0;
|
|
1840
1970
|
let toolCalls = 0;
|
|
1841
1971
|
const askId = `ask-${timestampedId()}`;
|
|
@@ -1858,7 +1988,9 @@ export async function createSession(opts) {
|
|
|
1858
1988
|
}
|
|
1859
1989
|
};
|
|
1860
1990
|
const isReadOnlyToolDynamic = (toolName) => {
|
|
1861
|
-
return isReadOnlyTool(toolName) ||
|
|
1991
|
+
return (isReadOnlyTool(toolName) ||
|
|
1992
|
+
LSP_TOOL_NAME_SET.has(toolName) ||
|
|
1993
|
+
Boolean(mcpManager?.isToolReadOnly(toolName)));
|
|
1862
1994
|
};
|
|
1863
1995
|
const emitToolResult = async (result) => {
|
|
1864
1996
|
await hookObj.onToolResult?.(result);
|
|
@@ -1886,16 +2018,15 @@ export async function createSession(opts) {
|
|
|
1886
2018
|
// Cap action list to prevent vault bloat on long sessions
|
|
1887
2019
|
const MAX_SUMMARY_ACTIONS = 30;
|
|
1888
2020
|
const cappedActions = actions.length > MAX_SUMMARY_ACTIONS
|
|
1889
|
-
? [
|
|
2021
|
+
? [
|
|
2022
|
+
...actions.slice(0, MAX_SUMMARY_ACTIONS),
|
|
2023
|
+
`... and ${actions.length - MAX_SUMMARY_ACTIONS} more`,
|
|
2024
|
+
]
|
|
1890
2025
|
: actions;
|
|
1891
2026
|
const userPrompt = lastAskInstructionText || '(unknown)';
|
|
1892
|
-
const userPromptSnippet = userPrompt.length > 120
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
const resultSnippet = finalText.length > 200
|
|
1896
|
-
? finalText.slice(0, 200) + '…'
|
|
1897
|
-
: finalText;
|
|
1898
|
-
const summary = `User asked: ${userPromptSnippet}\nActions (${actions.length} tool calls, ${turns} turns):\n${cappedActions.map(a => `- ${a}`).join('\n')}\nResult: ${resultSnippet}`;
|
|
2027
|
+
const userPromptSnippet = userPrompt.length > 120 ? userPrompt.slice(0, 120) + '…' : userPrompt;
|
|
2028
|
+
const resultSnippet = finalText.length > 200 ? finalText.slice(0, 200) + '…' : finalText;
|
|
2029
|
+
const summary = `User asked: ${userPromptSnippet}\nActions (${actions.length} tool calls, ${turns} turns):\n${cappedActions.map((a) => `- ${a}`).join('\n')}\nResult: ${resultSnippet}`;
|
|
1899
2030
|
await vault.upsertNote(`turn_summary_${askId}`, summary, 'system');
|
|
1900
2031
|
}
|
|
1901
2032
|
}
|
|
@@ -1913,7 +2044,9 @@ export async function createSession(opts) {
|
|
|
1913
2044
|
const reviewKeys = reviewArtifactKeys(projectDir);
|
|
1914
2045
|
const retrievalRequested = looksLikeReviewRetrievalRequest(rawInstructionText);
|
|
1915
2046
|
const shouldPersistReviewArtifact = looksLikeCodeReviewRequest(rawInstructionText) && !retrievalRequested;
|
|
1916
|
-
if (!retrievalRequested &&
|
|
2047
|
+
if (!retrievalRequested &&
|
|
2048
|
+
cfg.initial_connection_check !== false &&
|
|
2049
|
+
!initialConnectionProbeDone) {
|
|
1917
2050
|
if (typeof client.probeConnection === 'function') {
|
|
1918
2051
|
await client.probeConnection();
|
|
1919
2052
|
initialConnectionProbeDone = true;
|
|
@@ -1924,9 +2057,7 @@ export async function createSession(opts) {
|
|
|
1924
2057
|
? await vault.getLatestByKey(reviewKeys.latestKey, 'system').catch(() => null)
|
|
1925
2058
|
: null;
|
|
1926
2059
|
const parsedArtifact = latest?.value ? parseReviewArtifact(latest.value) : null;
|
|
1927
|
-
const artifact = parsedArtifact && parsedArtifact.projectId === reviewKeys.projectId
|
|
1928
|
-
? parsedArtifact
|
|
1929
|
-
: null;
|
|
2060
|
+
const artifact = parsedArtifact && parsedArtifact.projectId === reviewKeys.projectId ? parsedArtifact : null;
|
|
1930
2061
|
if (artifact?.content?.trim()) {
|
|
1931
2062
|
const stale = reviewArtifactStaleReason(artifact, projectDir);
|
|
1932
2063
|
const stalePolicy = parseReviewArtifactStalePolicy(cfg?.trifecta?.vault?.stale_policy);
|
|
@@ -1943,9 +2074,7 @@ export async function createSession(opts) {
|
|
|
1943
2074
|
});
|
|
1944
2075
|
return await finalizeAsk(blocked);
|
|
1945
2076
|
}
|
|
1946
|
-
const text = stale
|
|
1947
|
-
? `${artifact.content}\n\n[artifact note] ${stale}`
|
|
1948
|
-
: artifact.content;
|
|
2077
|
+
const text = stale ? `${artifact.content}\n\n[artifact note] ${stale}` : artifact.content;
|
|
1949
2078
|
messages.push({ role: 'assistant', content: text });
|
|
1950
2079
|
hookObj.onToken?.(text);
|
|
1951
2080
|
await emitTurnEnd({
|
|
@@ -2046,7 +2175,7 @@ export async function createSession(opts) {
|
|
|
2046
2175
|
// pressure can replay the cached result instead of re-executing.
|
|
2047
2176
|
const lastExecResultBySig = new Map();
|
|
2048
2177
|
// Cache successful read_file/read_files/list_dir results by signature + mtime for invalidation.
|
|
2049
|
-
const
|
|
2178
|
+
const _readFileCacheBySig = new Map();
|
|
2050
2179
|
const READ_FILE_CACHE_TOOLS = new Set(['read_file', 'read_files', 'list_dir']);
|
|
2051
2180
|
const toolLoopGuard = new ToolLoopGuard({
|
|
2052
2181
|
enabled: cfg.tool_loop_detection?.enabled,
|
|
@@ -2079,6 +2208,8 @@ export async function createSession(opts) {
|
|
|
2079
2208
|
let toollessRecoveryUsed = false;
|
|
2080
2209
|
// Prevent repeating the same "stop rerunning" reminder every turn.
|
|
2081
2210
|
const readOnlyExecHintedSigs = new Set();
|
|
2211
|
+
// Tool loop recovery: poisoned results and selective tool suppression.
|
|
2212
|
+
const suppressedTools = new Set();
|
|
2082
2213
|
// Keep a lightweight breadcrumb for diagnostics on partial failures.
|
|
2083
2214
|
let lastSuccessfulTestRun = null;
|
|
2084
2215
|
// One-time nudge to prevent post-success churn after green test runs.
|
|
@@ -2106,7 +2237,11 @@ export async function createSession(opts) {
|
|
|
2106
2237
|
const compactToolMessageForHistory = async (toolCallId, rawContent) => {
|
|
2107
2238
|
const toolName = toolNameByCallId.get(toolCallId) ?? 'tool';
|
|
2108
2239
|
const toolArgs = toolArgsByCallId.get(toolCallId) ?? {};
|
|
2109
|
-
const rawMsg = {
|
|
2240
|
+
const rawMsg = {
|
|
2241
|
+
role: 'tool',
|
|
2242
|
+
tool_call_id: toolCallId,
|
|
2243
|
+
content: rawContent,
|
|
2244
|
+
};
|
|
2110
2245
|
// Persist full-fidelity output immediately so live context can stay small.
|
|
2111
2246
|
if (vault && typeof vault.archiveToolResult === 'function') {
|
|
2112
2247
|
try {
|
|
@@ -2120,7 +2255,9 @@ export async function createSession(opts) {
|
|
|
2120
2255
|
if (lens) {
|
|
2121
2256
|
try {
|
|
2122
2257
|
const lensCompact = await lens.summarizeToolOutput(rawContent, toolName, typeof toolArgs.path === 'string' ? String(toolArgs.path) : undefined);
|
|
2123
|
-
if (typeof lensCompact === 'string' &&
|
|
2258
|
+
if (typeof lensCompact === 'string' &&
|
|
2259
|
+
lensCompact.length &&
|
|
2260
|
+
lensCompact.length < compact.length) {
|
|
2124
2261
|
compact = lensCompact;
|
|
2125
2262
|
}
|
|
2126
2263
|
}
|
|
@@ -2205,7 +2342,10 @@ export async function createSession(opts) {
|
|
|
2205
2342
|
if (m.role === 'user') {
|
|
2206
2343
|
const text = userContentToText((m.content ?? '')).trim();
|
|
2207
2344
|
// Skip vault injection messages and short prompts
|
|
2208
|
-
if (text &&
|
|
2345
|
+
if (text &&
|
|
2346
|
+
!text.startsWith('[Trifecta Vault') &&
|
|
2347
|
+
!text.startsWith('[Vault context') &&
|
|
2348
|
+
text.length > 20) {
|
|
2209
2349
|
userPromptToPreserve = text;
|
|
2210
2350
|
break;
|
|
2211
2351
|
}
|
|
@@ -2234,7 +2374,10 @@ export async function createSession(opts) {
|
|
|
2234
2374
|
const resp = await client.chat({
|
|
2235
2375
|
model,
|
|
2236
2376
|
messages: [
|
|
2237
|
-
{
|
|
2377
|
+
{
|
|
2378
|
+
role: 'system',
|
|
2379
|
+
content: 'Summarize this agent session progress concisely. List: files read, key findings, decisions made, current approach. Be terse.',
|
|
2380
|
+
},
|
|
2238
2381
|
{ role: 'user', content: summaryContent },
|
|
2239
2382
|
],
|
|
2240
2383
|
max_tokens: summaryMaxTokens,
|
|
@@ -2250,15 +2393,24 @@ export async function createSession(opts) {
|
|
|
2250
2393
|
});
|
|
2251
2394
|
}
|
|
2252
2395
|
else {
|
|
2253
|
-
messages.push({
|
|
2396
|
+
messages.push({
|
|
2397
|
+
role: 'system',
|
|
2398
|
+
content: buildCompactionSystemNote('auto', dropped.length),
|
|
2399
|
+
});
|
|
2254
2400
|
}
|
|
2255
2401
|
}
|
|
2256
2402
|
catch {
|
|
2257
|
-
messages.push({
|
|
2403
|
+
messages.push({
|
|
2404
|
+
role: 'system',
|
|
2405
|
+
content: buildCompactionSystemNote('auto', dropped.length),
|
|
2406
|
+
});
|
|
2258
2407
|
}
|
|
2259
2408
|
}
|
|
2260
2409
|
else {
|
|
2261
|
-
messages.push({
|
|
2410
|
+
messages.push({
|
|
2411
|
+
role: 'system',
|
|
2412
|
+
content: buildCompactionSystemNote('auto', dropped.length),
|
|
2413
|
+
});
|
|
2262
2414
|
}
|
|
2263
2415
|
}
|
|
2264
2416
|
// Update token count AFTER injections so downstream reads are accurate
|
|
@@ -2268,9 +2420,15 @@ export async function createSession(opts) {
|
|
|
2268
2420
|
// Emit compaction event for callers (e.g. Anton controller → Discord)
|
|
2269
2421
|
if (dropped.length) {
|
|
2270
2422
|
try {
|
|
2271
|
-
await hookObj.onCompaction?.({
|
|
2423
|
+
await hookObj.onCompaction?.({
|
|
2424
|
+
droppedMessages: dropped.length,
|
|
2425
|
+
freedTokens,
|
|
2426
|
+
summaryUsed,
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
catch {
|
|
2430
|
+
/* best effort */
|
|
2272
2431
|
}
|
|
2273
|
-
catch { /* best effort */ }
|
|
2274
2432
|
console.error(`[compaction] auto: dropped=${dropped.length} msgs, freed=~${freedTokens} tokens, summary=${summaryUsed}, remaining=${messages.length} msgs (~${currentContextTokens} tokens)`);
|
|
2275
2433
|
}
|
|
2276
2434
|
return {
|
|
@@ -2306,8 +2464,10 @@ export async function createSession(opts) {
|
|
|
2306
2464
|
let resp;
|
|
2307
2465
|
try {
|
|
2308
2466
|
try {
|
|
2309
|
-
const toolsForTurn = (cfg.no_tools || forceToollessRecoveryTurn)
|
|
2310
|
-
|
|
2467
|
+
const toolsForTurn = (cfg.no_tools || forceToollessRecoveryTurn)
|
|
2468
|
+
? []
|
|
2469
|
+
: getToolsSchema().filter(t => !suppressedTools.has(t.function.name));
|
|
2470
|
+
const toolChoiceForTurn = cfg.no_tools || forceToollessRecoveryTurn ? 'none' : 'auto';
|
|
2311
2471
|
resp = await client.chatStream({
|
|
2312
2472
|
model,
|
|
2313
2473
|
messages,
|
|
@@ -2326,7 +2486,8 @@ export async function createSession(opts) {
|
|
|
2326
2486
|
overflowCompactionAttempts = 0;
|
|
2327
2487
|
}
|
|
2328
2488
|
catch (e) {
|
|
2329
|
-
if (isContextWindowExceededError(e) &&
|
|
2489
|
+
if (isContextWindowExceededError(e) &&
|
|
2490
|
+
overflowCompactionAttempts < MAX_OVERFLOW_COMPACTION_ATTEMPTS) {
|
|
2330
2491
|
overflowCompactionAttempts++;
|
|
2331
2492
|
const useHardCompaction = overflowCompactionAttempts > 1;
|
|
2332
2493
|
const compacted = await compactHistory({
|
|
@@ -2364,9 +2525,7 @@ export async function createSession(opts) {
|
|
|
2364
2525
|
? promptTokensTurn / (ttftMs / 1000)
|
|
2365
2526
|
: undefined;
|
|
2366
2527
|
const genWindowMs = Math.max(1, ttcMs - (ttftMs ?? 0));
|
|
2367
|
-
const tgTps = completionTokensTurn > 0
|
|
2368
|
-
? completionTokensTurn / (genWindowMs / 1000)
|
|
2369
|
-
: undefined;
|
|
2528
|
+
const tgTps = completionTokensTurn > 0 ? completionTokensTurn / (genWindowMs / 1000) : undefined;
|
|
2370
2529
|
if (ttcMs > 0)
|
|
2371
2530
|
turnDurationsMs.push(ttcMs);
|
|
2372
2531
|
if (ttftMs != null && ttftMs > 0)
|
|
@@ -2413,7 +2572,9 @@ export async function createSession(opts) {
|
|
|
2413
2572
|
// Conditionally strip thinking blocks based on harness config (§4i).
|
|
2414
2573
|
// Non-reasoning models (thinking.strip === false) never emit <think> blocks,
|
|
2415
2574
|
// so stripping is a no-op — but we skip the regex work entirely.
|
|
2416
|
-
const st = harness.thinking.strip
|
|
2575
|
+
const st = harness.thinking.strip
|
|
2576
|
+
? stripThinking(content)
|
|
2577
|
+
: { visible: content, thinking: '' };
|
|
2417
2578
|
// Strip XML tool-call tag fragments that leak into visible narration
|
|
2418
2579
|
// when llama-server partially parses Qwen/Hermes XML tool calls.
|
|
2419
2580
|
const visible = st.visible
|
|
@@ -2431,7 +2592,9 @@ export async function createSession(opts) {
|
|
|
2431
2592
|
// For models with unreliable tool_calls arrays, validate entries and
|
|
2432
2593
|
// fall through to content parsing if they look malformed (§4i).
|
|
2433
2594
|
if (toolCallsArr?.length && !harness.toolCalls.reliableToolCallsArray) {
|
|
2434
|
-
const hasValid = toolCallsArr.some(tc => tc.function?.name &&
|
|
2595
|
+
const hasValid = toolCallsArr.some((tc) => tc.function?.name &&
|
|
2596
|
+
typeof tc.function.name === 'string' &&
|
|
2597
|
+
tc.function.name.length > 0);
|
|
2435
2598
|
if (!hasValid) {
|
|
2436
2599
|
if (cfg.verbose) {
|
|
2437
2600
|
console.warn(`[harness] tool_calls array present but no valid entries (reliableToolCallsArray=false), trying content fallback`);
|
|
@@ -2519,7 +2682,7 @@ export async function createSession(opts) {
|
|
|
2519
2682
|
const deduped = [];
|
|
2520
2683
|
for (const [, group] of byName) {
|
|
2521
2684
|
if (group.length > 1) {
|
|
2522
|
-
const maxArgs = Math.max(...group.map(g => g.argCount));
|
|
2685
|
+
const maxArgs = Math.max(...group.map((g) => g.argCount));
|
|
2523
2686
|
// Drop entries with strictly fewer args than the richest (ghost duplicates).
|
|
2524
2687
|
// Keep ALL entries that have the max arg count (genuine parallel calls).
|
|
2525
2688
|
for (const g of group) {
|
|
@@ -2552,15 +2715,22 @@ export async function createSession(opts) {
|
|
|
2552
2715
|
const compactAssistantToolCallText = assistantToolCallText.length > 900
|
|
2553
2716
|
? `${assistantToolCallText.slice(0, 900)}\n[history-compacted: assistant narration truncated before tool execution]`
|
|
2554
2717
|
: assistantToolCallText;
|
|
2555
|
-
messages.push({
|
|
2718
|
+
messages.push({
|
|
2719
|
+
role: 'assistant',
|
|
2720
|
+
content: compactAssistantToolCallText,
|
|
2721
|
+
tool_calls: originalToolCallsArr,
|
|
2722
|
+
});
|
|
2556
2723
|
// sigCounts is scoped to the entire ask() run (see above)
|
|
2557
2724
|
// Bridge ConfirmationProvider → legacy confirm callback for tools.
|
|
2558
2725
|
// If a ConfirmationProvider is given, wrap it; otherwise fall back to raw callback.
|
|
2559
2726
|
// The bridge accepts an optional context object for rich confirm data.
|
|
2560
2727
|
const confirmBridge = opts.confirmProvider
|
|
2561
2728
|
? async (prompt, bridgeCtx) => opts.confirmProvider.confirm({
|
|
2562
|
-
tool: bridgeCtx?.tool ?? '',
|
|
2563
|
-
|
|
2729
|
+
tool: bridgeCtx?.tool ?? '',
|
|
2730
|
+
args: bridgeCtx?.args ?? {},
|
|
2731
|
+
summary: prompt,
|
|
2732
|
+
diff: bridgeCtx?.diff,
|
|
2733
|
+
mode: cfg.approval_mode,
|
|
2564
2734
|
})
|
|
2565
2735
|
: opts.confirm;
|
|
2566
2736
|
const ctx = buildToolCtx({
|
|
@@ -2569,6 +2739,7 @@ export async function createSession(opts) {
|
|
|
2569
2739
|
onMutation: (absPath) => {
|
|
2570
2740
|
lastEditedPath = absPath;
|
|
2571
2741
|
mutationVersion++;
|
|
2742
|
+
suppressedTools.clear(); // file changed, re-enable all tools
|
|
2572
2743
|
},
|
|
2573
2744
|
});
|
|
2574
2745
|
// Tool-call argument parsing and validation logic
|
|
@@ -2611,6 +2782,8 @@ export async function createSession(opts) {
|
|
|
2611
2782
|
const replayExecSigs = new Set();
|
|
2612
2783
|
// Repeated read_file/read_files/list_dir calls can be served from cache.
|
|
2613
2784
|
const repeatedReadFileSigs = new Set();
|
|
2785
|
+
// Poisoned tool sigs: at consec >= 3, don't execute — return error instead.
|
|
2786
|
+
const poisonedToolSigs = new Set();
|
|
2614
2787
|
let shouldForceToollessRecovery = false;
|
|
2615
2788
|
const criticalLoopSigs = new Set();
|
|
2616
2789
|
for (const tc of toolCallsArr) {
|
|
@@ -2689,9 +2862,10 @@ export async function createSession(opts) {
|
|
|
2689
2862
|
}
|
|
2690
2863
|
if (count >= loopThreshold) {
|
|
2691
2864
|
const sigArgs = sigMetaBySig.get(sig)?.args ?? {};
|
|
2692
|
-
const command = typeof sigArgs?.command === 'string'
|
|
2693
|
-
|
|
2694
|
-
|
|
2865
|
+
const command = typeof sigArgs?.command === 'string'
|
|
2866
|
+
? String(sigArgs.command)
|
|
2867
|
+
: '';
|
|
2868
|
+
const canReuseReadOnlyObservation = looksLikeReadOnlyExecCommand(command) && execObservationCacheBySig.has(sig);
|
|
2695
2869
|
if (canReuseReadOnlyObservation) {
|
|
2696
2870
|
repeatedReadOnlyExecSigs.add(sig);
|
|
2697
2871
|
if (!readOnlyExecHintedSigs.has(sig)) {
|
|
@@ -2701,7 +2875,9 @@ export async function createSession(opts) {
|
|
|
2701
2875
|
continue;
|
|
2702
2876
|
}
|
|
2703
2877
|
const argsPreviewRaw = JSON.stringify(sigArgs);
|
|
2704
|
-
const argsPreview = argsPreviewRaw.length > 220
|
|
2878
|
+
const argsPreview = argsPreviewRaw.length > 220
|
|
2879
|
+
? argsPreviewRaw.slice(0, 220) + '…'
|
|
2880
|
+
: argsPreviewRaw;
|
|
2705
2881
|
throw new Error(`tool ${toolName}: identical call repeated ${loopThreshold}x across turns; breaking loop. ` +
|
|
2706
2882
|
`args=${argsPreview}`);
|
|
2707
2883
|
}
|
|
@@ -2743,21 +2919,17 @@ export async function createSession(opts) {
|
|
|
2743
2919
|
}
|
|
2744
2920
|
}
|
|
2745
2921
|
}
|
|
2746
|
-
// At
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
else if (toolName === 'read_files')
|
|
2753
|
-
resourceType = 'files';
|
|
2754
|
-
else if (toolName === 'list_dir')
|
|
2755
|
-
resourceType = 'directory';
|
|
2756
|
-
messages.push({
|
|
2757
|
-
role: 'system',
|
|
2758
|
-
content: `CRITICAL: ${resourceType} unchanged. Move on NOW.`,
|
|
2759
|
-
});
|
|
2922
|
+
// At consec >= 3: poison the result (don't execute, return error).
|
|
2923
|
+
// At consec >= 4: also suppress the tool from the schema entirely.
|
|
2924
|
+
if (consec >= 3) {
|
|
2925
|
+
poisonedToolSigs.add(sig);
|
|
2926
|
+
if (consec >= 4) {
|
|
2927
|
+
suppressedTools.add(toolName);
|
|
2760
2928
|
}
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2931
|
+
// At 2x, serve from cache if available
|
|
2932
|
+
if (consec >= 2 && isReadFileTool) {
|
|
2761
2933
|
const argsForSig = sigMetaBySig.get(sig)?.args ?? {};
|
|
2762
2934
|
const replay = await toolLoopGuard.getReadCacheReplay(toolName, argsForSig, ctx.cwd);
|
|
2763
2935
|
if (replay) {
|
|
@@ -2836,7 +3008,9 @@ export async function createSession(opts) {
|
|
|
2836
3008
|
throw new ToolError('invalid_args', `tool ${name}: arguments not valid JSON`, false, 'Return a valid JSON object for function.arguments.', { raw: rawArgs.slice(0, 200) });
|
|
2837
3009
|
}
|
|
2838
3010
|
if (args == null || typeof args !== 'object' || Array.isArray(args)) {
|
|
2839
|
-
throw new ValidationError([
|
|
3011
|
+
throw new ValidationError([
|
|
3012
|
+
{ field: 'arguments', message: 'must be a JSON object', value: args },
|
|
3013
|
+
]);
|
|
2840
3014
|
}
|
|
2841
3015
|
const builtInFn = tools[name];
|
|
2842
3016
|
const isLspTool = LSP_TOOL_NAME_SET.has(name);
|
|
@@ -2854,7 +3028,11 @@ export async function createSession(opts) {
|
|
|
2854
3028
|
if (builtInFn || isSpawnTask) {
|
|
2855
3029
|
const missing = getMissingRequiredParams(name, args);
|
|
2856
3030
|
if (missing.length) {
|
|
2857
|
-
throw new ValidationError(missing.map((m) => ({
|
|
3031
|
+
throw new ValidationError(missing.map((m) => ({
|
|
3032
|
+
field: m,
|
|
3033
|
+
message: 'required parameter is missing',
|
|
3034
|
+
value: undefined,
|
|
3035
|
+
})));
|
|
2858
3036
|
}
|
|
2859
3037
|
const argIssues = getArgValidationIssues(name, args);
|
|
2860
3038
|
if (argIssues.length) {
|
|
@@ -2873,7 +3051,9 @@ export async function createSession(opts) {
|
|
|
2873
3051
|
}
|
|
2874
3052
|
}
|
|
2875
3053
|
if (FILE_MUTATION_TOOL_SET.has(name) && typeof args.path === 'string') {
|
|
2876
|
-
const absPath = args.path.startsWith('/')
|
|
3054
|
+
const absPath = args.path.startsWith('/')
|
|
3055
|
+
? args.path
|
|
3056
|
+
: path.resolve(projectDir, args.path);
|
|
2877
3057
|
// ── Pre-dispatch: block edits to files in a mutation spiral ──
|
|
2878
3058
|
if (fileMutationBlocked.has(absPath)) {
|
|
2879
3059
|
const basename = path.basename(absPath);
|
|
@@ -2895,12 +3075,23 @@ export async function createSession(opts) {
|
|
|
2895
3075
|
// Fix 1: Hard cumulative budget — refuse reads past hard cap
|
|
2896
3076
|
if (cumulativeReadOnlyCalls > READ_BUDGET_HARD) {
|
|
2897
3077
|
await emitToolCall({ id: callId, name, args });
|
|
2898
|
-
await emitToolResult({
|
|
2899
|
-
|
|
3078
|
+
await emitToolResult({
|
|
3079
|
+
id: callId,
|
|
3080
|
+
name,
|
|
3081
|
+
success: false,
|
|
3082
|
+
summary: 'read budget exhausted',
|
|
3083
|
+
result: '',
|
|
3084
|
+
});
|
|
3085
|
+
return {
|
|
3086
|
+
id: callId,
|
|
3087
|
+
content: `STOP: Read budget exhausted (${cumulativeReadOnlyCalls}/${READ_BUDGET_HARD} calls). Do NOT read more files. Use search_files or exec: grep -rn "pattern" path/ to find what you need.`,
|
|
3088
|
+
};
|
|
2900
3089
|
}
|
|
2901
3090
|
// Fix 2: Directory scan detection — counts unique files per dir (re-reads are OK)
|
|
2902
3091
|
if (filePath) {
|
|
2903
|
-
const absFilePath = filePath.startsWith('/')
|
|
3092
|
+
const absFilePath = filePath.startsWith('/')
|
|
3093
|
+
? filePath
|
|
3094
|
+
: path.resolve(projectDir, filePath);
|
|
2904
3095
|
const parentDir = path.dirname(absFilePath);
|
|
2905
3096
|
if (!readDirFiles.has(parentDir))
|
|
2906
3097
|
readDirFiles.set(parentDir, new Set());
|
|
@@ -2911,8 +3102,17 @@ export async function createSession(opts) {
|
|
|
2911
3102
|
}
|
|
2912
3103
|
if (blockedDirs.has(parentDir) && uniqueCount > 8) {
|
|
2913
3104
|
await emitToolCall({ id: callId, name, args });
|
|
2914
|
-
await emitToolResult({
|
|
2915
|
-
|
|
3105
|
+
await emitToolResult({
|
|
3106
|
+
id: callId,
|
|
3107
|
+
name,
|
|
3108
|
+
success: false,
|
|
3109
|
+
summary: 'dir scan blocked',
|
|
3110
|
+
result: '',
|
|
3111
|
+
});
|
|
3112
|
+
return {
|
|
3113
|
+
id: callId,
|
|
3114
|
+
content: `STOP: Directory scan detected — you've read ${uniqueCount} unique files from ${parentDir}/. Use search_files(pattern, '${parentDir}') or exec: grep -rn "pattern" ${parentDir}/ instead of reading files individually.`,
|
|
3115
|
+
};
|
|
2916
3116
|
}
|
|
2917
3117
|
}
|
|
2918
3118
|
// Fix 3: Same-search-term detection
|
|
@@ -2923,8 +3123,17 @@ export async function createSession(opts) {
|
|
|
2923
3123
|
searchTermFiles.get(key).add(filePath);
|
|
2924
3124
|
if (searchTermFiles.get(key).size >= 3) {
|
|
2925
3125
|
await emitToolCall({ id: callId, name, args });
|
|
2926
|
-
await emitToolResult({
|
|
2927
|
-
|
|
3126
|
+
await emitToolResult({
|
|
3127
|
+
id: callId,
|
|
3128
|
+
name,
|
|
3129
|
+
success: false,
|
|
3130
|
+
summary: 'use search_files',
|
|
3131
|
+
result: '',
|
|
3132
|
+
});
|
|
3133
|
+
return {
|
|
3134
|
+
id: callId,
|
|
3135
|
+
content: `STOP: You've searched ${searchTermFiles.get(key).size} files for "${searchTerm}" one at a time. This is what search_files does in one call. Use: search_files(pattern="${searchTerm}", path=".") or exec: grep -rn "${searchTerm}" .`,
|
|
3136
|
+
};
|
|
2928
3137
|
}
|
|
2929
3138
|
}
|
|
2930
3139
|
}
|
|
@@ -2943,22 +3152,40 @@ export async function createSession(opts) {
|
|
|
2943
3152
|
planSteps.push(step);
|
|
2944
3153
|
const blockedMsg = `[blocked: approval_mode=plan] Would ${summary}`;
|
|
2945
3154
|
// Notify via confirmProvider.showBlocked if available
|
|
2946
|
-
opts.confirmProvider?.showBlocked?.({
|
|
3155
|
+
opts.confirmProvider?.showBlocked?.({
|
|
3156
|
+
tool: name,
|
|
3157
|
+
args,
|
|
3158
|
+
reason: `plan mode: ${summary}`,
|
|
3159
|
+
});
|
|
2947
3160
|
// Hook: onToolCall + onToolResult for plan-blocked actions
|
|
2948
3161
|
await emitToolCall({ id: callId, name, args });
|
|
2949
|
-
await emitToolResult({
|
|
3162
|
+
await emitToolResult({
|
|
3163
|
+
id: callId,
|
|
3164
|
+
name,
|
|
3165
|
+
success: true,
|
|
3166
|
+
summary: `⏸ ${summary} (blocked)`,
|
|
3167
|
+
result: blockedMsg,
|
|
3168
|
+
});
|
|
2950
3169
|
return { id: callId, content: blockedMsg };
|
|
2951
3170
|
}
|
|
2952
3171
|
// Hook: onToolCall (Phase 8.5)
|
|
2953
3172
|
await emitToolCall({ id: callId, name, args });
|
|
2954
3173
|
if (cfg.step_mode) {
|
|
2955
3174
|
const stepPrompt = `Step mode: execute ${name}(${JSON.stringify(args).slice(0, 200)}) ? [Y/n]`;
|
|
2956
|
-
const ok = confirmBridge
|
|
3175
|
+
const ok = confirmBridge
|
|
3176
|
+
? await confirmBridge(stepPrompt, { tool: name, args })
|
|
3177
|
+
: true;
|
|
2957
3178
|
if (!ok) {
|
|
2958
3179
|
return { id: callId, content: '[skipped by user: step mode]' };
|
|
2959
3180
|
}
|
|
2960
3181
|
}
|
|
2961
3182
|
const sig = toolLoopGuard.computeSignature(name, args && typeof args === 'object' && !Array.isArray(args) ? args : {});
|
|
3183
|
+
// Poisoned tool call: don't execute, return error-like result.
|
|
3184
|
+
if (poisonedToolSigs.has(sig)) {
|
|
3185
|
+
const consec = consecutiveCounts.get(sig) ?? 3;
|
|
3186
|
+
const poisonMsg = `Error: This exact ${name} call has been repeated ${consec} times with identical arguments and results. The tool is temporarily disabled. Use the information you already have, or try a different approach.`;
|
|
3187
|
+
return { id: callId, content: poisonMsg };
|
|
3188
|
+
}
|
|
2962
3189
|
let content = '';
|
|
2963
3190
|
let reusedCachedReadOnlyExec = false;
|
|
2964
3191
|
let reusedCachedReadTool = false;
|
|
@@ -2997,7 +3224,9 @@ export async function createSession(opts) {
|
|
|
2997
3224
|
};
|
|
2998
3225
|
const value = await builtInFn(callCtx, args);
|
|
2999
3226
|
content = typeof value === 'string' ? value : JSON.stringify(value);
|
|
3000
|
-
if (READ_FILE_CACHE_TOOLS.has(name) &&
|
|
3227
|
+
if (READ_FILE_CACHE_TOOLS.has(name) &&
|
|
3228
|
+
typeof content === 'string' &&
|
|
3229
|
+
!content.startsWith('ERROR:')) {
|
|
3001
3230
|
const baseCwd = typeof args?.cwd === 'string' ? String(args.cwd) : ctx.cwd;
|
|
3002
3231
|
await toolLoopGuard.storeReadCache(name, args, baseCwd, content);
|
|
3003
3232
|
}
|
|
@@ -3040,7 +3269,9 @@ export async function createSession(opts) {
|
|
|
3040
3269
|
const mcpReadOnly = isReadOnlyToolDynamic(name);
|
|
3041
3270
|
if (!cfg.step_mode && !ctx.noConfirm && !mcpReadOnly) {
|
|
3042
3271
|
const prompt = `Execute MCP tool '${name}'? [Y/n]`;
|
|
3043
|
-
const ok = confirmBridge
|
|
3272
|
+
const ok = confirmBridge
|
|
3273
|
+
? await confirmBridge(prompt, { tool: name, args })
|
|
3274
|
+
: true;
|
|
3044
3275
|
if (!ok) {
|
|
3045
3276
|
return { id: callId, content: '[skipped by user: approval]' };
|
|
3046
3277
|
}
|
|
@@ -3064,7 +3295,13 @@ export async function createSession(opts) {
|
|
|
3064
3295
|
let summary = reusedCachedReadOnlyExec
|
|
3065
3296
|
? 'cached read-only exec observation (unchanged)'
|
|
3066
3297
|
: toolResultSummary(name, args, content, true);
|
|
3067
|
-
const resultEvent = {
|
|
3298
|
+
const resultEvent = {
|
|
3299
|
+
id: callId,
|
|
3300
|
+
name,
|
|
3301
|
+
success: true,
|
|
3302
|
+
summary,
|
|
3303
|
+
result: content,
|
|
3304
|
+
};
|
|
3068
3305
|
// Phase 7: populate rich display fields
|
|
3069
3306
|
if (name === 'exec') {
|
|
3070
3307
|
try {
|
|
@@ -3114,14 +3351,18 @@ export async function createSession(opts) {
|
|
|
3114
3351
|
const mutatedPath = typeof args.path === 'string' ? args.path : '';
|
|
3115
3352
|
if (mutatedPath) {
|
|
3116
3353
|
try {
|
|
3117
|
-
const absPath = mutatedPath.startsWith('/')
|
|
3354
|
+
const absPath = mutatedPath.startsWith('/')
|
|
3355
|
+
? mutatedPath
|
|
3356
|
+
: path.join(projectDir, mutatedPath);
|
|
3118
3357
|
const fileText = await fs.readFile(absPath, 'utf8');
|
|
3119
3358
|
await lspManager.ensureOpen(absPath, fileText);
|
|
3120
3359
|
await lspManager.notifyDidSave(absPath, fileText);
|
|
3121
3360
|
// Small delay so the server can process diagnostics
|
|
3122
3361
|
await new Promise((r) => setTimeout(r, 200));
|
|
3123
3362
|
const diags = await lspManager.getDiagnostics(absPath);
|
|
3124
|
-
if (diags &&
|
|
3363
|
+
if (diags &&
|
|
3364
|
+
!diags.startsWith('No diagnostics') &&
|
|
3365
|
+
!diags.startsWith('[lsp] no language')) {
|
|
3125
3366
|
content += `\n\n[lsp] Diagnostics after edit:\n${diags}`;
|
|
3126
3367
|
}
|
|
3127
3368
|
}
|
|
@@ -3139,7 +3380,9 @@ export async function createSession(opts) {
|
|
|
3139
3380
|
// Track edits to the same file. If the model keeps editing the same file
|
|
3140
3381
|
// over and over, it's likely in an edit→break→read→edit corruption spiral.
|
|
3141
3382
|
if (FILE_MUTATION_TOOL_SET.has(name) && toolSuccess && typeof args.path === 'string') {
|
|
3142
|
-
const absPath = args.path.startsWith('/')
|
|
3383
|
+
const absPath = args.path.startsWith('/')
|
|
3384
|
+
? args.path
|
|
3385
|
+
: path.resolve(projectDir, args.path);
|
|
3143
3386
|
const basename = path.basename(absPath);
|
|
3144
3387
|
// write_file = full rewrite. If the model is rewriting the file completely
|
|
3145
3388
|
// after being warned, give it a fresh chance (reset counter to 1).
|
|
@@ -3154,15 +3397,18 @@ export async function createSession(opts) {
|
|
|
3154
3397
|
if (count >= FILE_MUTATION_BLOCK_THRESHOLD) {
|
|
3155
3398
|
// Mark for pre-dispatch blocking on subsequent edits
|
|
3156
3399
|
fileMutationBlocked.add(absPath);
|
|
3157
|
-
content +=
|
|
3158
|
-
|
|
3400
|
+
content +=
|
|
3401
|
+
`\n\n⚠️ BLOCKED: You have edited ${basename} ${count} times. Further edits to this file are now blocked. ` +
|
|
3402
|
+
`Restore it with: exec { "command": "git checkout -- ${args.path}" }, then make ONE complete edit.`;
|
|
3159
3403
|
}
|
|
3160
|
-
else if (count >= FILE_MUTATION_WARN_THRESHOLD &&
|
|
3404
|
+
else if (count >= FILE_MUTATION_WARN_THRESHOLD &&
|
|
3405
|
+
!fileMutationWarned.has(absPath)) {
|
|
3161
3406
|
fileMutationWarned.add(absPath);
|
|
3162
|
-
content +=
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3407
|
+
content +=
|
|
3408
|
+
`\n\n⚠️ WARNING: You have edited ${basename} ${count} times. ` +
|
|
3409
|
+
'If the file is broken, STOP making incremental fixes. ' +
|
|
3410
|
+
`Restore it with: exec { "command": "git checkout -- ${args.path}" }, ` +
|
|
3411
|
+
'read the original file carefully, then make ONE complete edit. Do NOT continue patching.';
|
|
3166
3412
|
}
|
|
3167
3413
|
}
|
|
3168
3414
|
}
|
|
@@ -3203,9 +3449,9 @@ export async function createSession(opts) {
|
|
|
3203
3449
|
// Fast-fail repeated blocked command loops with accurate reason labeling.
|
|
3204
3450
|
// Applies to direct exec attempts and spawn_task delegation attempts.
|
|
3205
3451
|
if (tc.function.name === 'exec' || tc.function.name === 'spawn_task') {
|
|
3206
|
-
const blockedMatch = msg.match(/^exec:\s*blocked\s*\(([^)]+)\)\s*without --no-confirm\/--yolo:\s*(.*)$/i)
|
|
3207
|
-
|
|
3208
|
-
|
|
3452
|
+
const blockedMatch = msg.match(/^exec:\s*blocked\s*\(([^)]+)\)\s*without --no-confirm\/--yolo:\s*(.*)$/i) ||
|
|
3453
|
+
msg.match(/^(spawn_task):\s*blocked\s*—\s*(.*)$/i) ||
|
|
3454
|
+
msg.match(/^exec:\s*blocked\s+(background command\b[^.]*)\./i);
|
|
3209
3455
|
if (blockedMatch) {
|
|
3210
3456
|
const reason = (blockedMatch[1] || blockedMatch[2] || 'blocked command').trim();
|
|
3211
3457
|
let parsedArgs = {};
|
|
@@ -3217,8 +3463,8 @@ export async function createSession(opts) {
|
|
|
3217
3463
|
? String(parsedArgs?.command ?? '')
|
|
3218
3464
|
: String(parsedArgs?.task ?? '');
|
|
3219
3465
|
const normalizedReason = reason.toLowerCase();
|
|
3220
|
-
const aggregateByReason = normalizedReason.includes('package install/remove')
|
|
3221
|
-
|
|
3466
|
+
const aggregateByReason = normalizedReason.includes('package install/remove') ||
|
|
3467
|
+
normalizedReason.includes('background command');
|
|
3222
3468
|
const sig = aggregateByReason
|
|
3223
3469
|
? `${tc.function.name}|${reason}`
|
|
3224
3470
|
: `${tc.function.name}|${reason}|${cmd}`;
|
|
@@ -3279,7 +3525,7 @@ export async function createSession(opts) {
|
|
|
3279
3525
|
const callId = resolveCallId(tc);
|
|
3280
3526
|
results.push({
|
|
3281
3527
|
id: callId,
|
|
3282
|
-
content: `STOP: Per-turn read limit (${READ_ONLY_PER_TURN_CAP}). Use search_files or exec with grep instead of reading files one by one
|
|
3528
|
+
content: `STOP: Per-turn read limit (${READ_ONLY_PER_TURN_CAP}). Use search_files or exec with grep instead of reading files one by one.`,
|
|
3283
3529
|
});
|
|
3284
3530
|
}
|
|
3285
3531
|
if (cfg.verbose) {
|
|
@@ -3293,8 +3539,7 @@ export async function createSession(opts) {
|
|
|
3293
3539
|
// Models that support parallel calls: read-only in parallel, mutations sequential
|
|
3294
3540
|
const readonly = toolCallsArr.filter((tc) => isReadOnlyToolDynamic(tc.function.name));
|
|
3295
3541
|
const others = toolCallsArr.filter((tc) => !isReadOnlyToolDynamic(tc.function.name));
|
|
3296
|
-
const ro = await Promise.all(readonly.map((tc) => runOne(tc)
|
|
3297
|
-
.catch((e) => catchToolError(e, tc))));
|
|
3542
|
+
const ro = await Promise.all(readonly.map((tc) => runOne(tc).catch((e) => catchToolError(e, tc))));
|
|
3298
3543
|
results.push(...ro);
|
|
3299
3544
|
for (const tc of others) {
|
|
3300
3545
|
if (ac.signal.aborted)
|
|
@@ -3352,7 +3597,7 @@ export async function createSession(opts) {
|
|
|
3352
3597
|
if (readOnlyExecTurnHints.length) {
|
|
3353
3598
|
const previews = readOnlyExecTurnHints
|
|
3354
3599
|
.slice(0, 2)
|
|
3355
|
-
.map((cmd) => cmd.length > 140 ? `${cmd.slice(0, 140)}…` : cmd)
|
|
3600
|
+
.map((cmd) => (cmd.length > 140 ? `${cmd.slice(0, 140)}…` : cmd))
|
|
3356
3601
|
.join(' | ');
|
|
3357
3602
|
messages.push({
|
|
3358
3603
|
role: 'user',
|
|
@@ -3393,7 +3638,9 @@ export async function createSession(opts) {
|
|
|
3393
3638
|
}
|
|
3394
3639
|
// ── Escalating cumulative read budget (§ anti-scan guardrails) ──
|
|
3395
3640
|
// Warn zone: append warnings to each read result when approaching the hard cap
|
|
3396
|
-
if (!readBudgetWarned &&
|
|
3641
|
+
if (!readBudgetWarned &&
|
|
3642
|
+
cumulativeReadOnlyCalls > READ_BUDGET_WARN &&
|
|
3643
|
+
cumulativeReadOnlyCalls <= READ_BUDGET_HARD) {
|
|
3397
3644
|
readBudgetWarned = true;
|
|
3398
3645
|
messages.push({
|
|
3399
3646
|
role: 'user',
|
|
@@ -3433,7 +3680,7 @@ export async function createSession(opts) {
|
|
|
3433
3680
|
messages.push({ role: 'assistant', content: visible || content || '' });
|
|
3434
3681
|
messages.push({
|
|
3435
3682
|
role: 'user',
|
|
3436
|
-
content: '[system] MCP tools are now enabled for this task. Continue and call tools as needed.'
|
|
3683
|
+
content: '[system] MCP tools are now enabled for this task. Continue and call tools as needed.',
|
|
3437
3684
|
});
|
|
3438
3685
|
continue;
|
|
3439
3686
|
}
|
|
@@ -3452,7 +3699,7 @@ export async function createSession(opts) {
|
|
|
3452
3699
|
const clippedReminder = reminder.length > 1600 ? `${reminder.slice(0, 1600)}\n[truncated]` : reminder;
|
|
3453
3700
|
messages.push({
|
|
3454
3701
|
role: 'user',
|
|
3455
|
-
content: `[system] Stuck narrating. Resume with tools.\nTask:\n${clippedReminder}
|
|
3702
|
+
content: `[system] Stuck narrating. Resume with tools.\nTask:\n${clippedReminder}`,
|
|
3456
3703
|
});
|
|
3457
3704
|
await emitTurnEnd({
|
|
3458
3705
|
turn: turns,
|
|
@@ -3474,7 +3721,7 @@ export async function createSession(opts) {
|
|
|
3474
3721
|
noToolNudgeUsed = true;
|
|
3475
3722
|
messages.push({
|
|
3476
3723
|
role: 'user',
|
|
3477
|
-
content: '[system] Use tools now or give final answer.'
|
|
3724
|
+
content: '[system] Use tools now or give final answer.',
|
|
3478
3725
|
});
|
|
3479
3726
|
await emitTurnEnd({
|
|
3480
3727
|
turn: turns,
|
|
@@ -3584,11 +3831,21 @@ export async function createSession(opts) {
|
|
|
3584
3831
|
};
|
|
3585
3832
|
// expose via getters so setModel() / reset() don't break references
|
|
3586
3833
|
return {
|
|
3587
|
-
get model() {
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
get
|
|
3591
|
-
|
|
3834
|
+
get model() {
|
|
3835
|
+
return model;
|
|
3836
|
+
},
|
|
3837
|
+
get harness() {
|
|
3838
|
+
return harness.id;
|
|
3839
|
+
},
|
|
3840
|
+
get endpoint() {
|
|
3841
|
+
return cfg.endpoint;
|
|
3842
|
+
},
|
|
3843
|
+
get contextWindow() {
|
|
3844
|
+
return contextWindow;
|
|
3845
|
+
},
|
|
3846
|
+
get supportsVision() {
|
|
3847
|
+
return supportsVision;
|
|
3848
|
+
},
|
|
3592
3849
|
get messages() {
|
|
3593
3850
|
return messages;
|
|
3594
3851
|
},
|
|
@@ -3647,7 +3904,7 @@ export async function createSession(opts) {
|
|
|
3647
3904
|
},
|
|
3648
3905
|
executePlanStep,
|
|
3649
3906
|
clearPlan,
|
|
3650
|
-
compactHistory
|
|
3907
|
+
compactHistory,
|
|
3651
3908
|
};
|
|
3652
3909
|
}
|
|
3653
3910
|
export async function runAgent(opts) {
|
|
@@ -3656,7 +3913,7 @@ export async function runAgent(opts) {
|
|
|
3656
3913
|
apiKey: opts.apiKey,
|
|
3657
3914
|
confirm: opts.confirm,
|
|
3658
3915
|
confirmProvider: opts.confirmProvider,
|
|
3659
|
-
runtime: opts.runtime
|
|
3916
|
+
runtime: opts.runtime,
|
|
3660
3917
|
});
|
|
3661
3918
|
return session.ask(opts.instruction, opts.onToken);
|
|
3662
3919
|
}
|