fixo-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +530 -0
- package/dist/agent/agent-client.d.ts +108 -0
- package/dist/agent/agent-client.d.ts.map +1 -0
- package/dist/agent/agent-client.js +1247 -0
- package/dist/agent/agent-client.js.map +1 -0
- package/dist/agent/agent-pool.d.ts +20 -0
- package/dist/agent/agent-pool.d.ts.map +1 -0
- package/dist/agent/agent-pool.js +217 -0
- package/dist/agent/agent-pool.js.map +1 -0
- package/dist/agent/background-awareness.d.ts +55 -0
- package/dist/agent/background-awareness.d.ts.map +1 -0
- package/dist/agent/background-awareness.js +104 -0
- package/dist/agent/background-awareness.js.map +1 -0
- package/dist/agent/command-parser.d.ts +33 -0
- package/dist/agent/command-parser.d.ts.map +1 -0
- package/dist/agent/command-parser.js +120 -0
- package/dist/agent/command-parser.js.map +1 -0
- package/dist/agent/context-budget.d.ts +91 -0
- package/dist/agent/context-budget.d.ts.map +1 -0
- package/dist/agent/context-budget.js +219 -0
- package/dist/agent/context-budget.js.map +1 -0
- package/dist/agent/conversation.d.ts +190 -0
- package/dist/agent/conversation.d.ts.map +1 -0
- package/dist/agent/conversation.js +547 -0
- package/dist/agent/conversation.js.map +1 -0
- package/dist/agent/hooks.d.ts +72 -0
- package/dist/agent/hooks.d.ts.map +1 -0
- package/dist/agent/hooks.js +214 -0
- package/dist/agent/hooks.js.map +1 -0
- package/dist/agent/mcp-bridge.d.ts +13 -0
- package/dist/agent/mcp-bridge.d.ts.map +1 -0
- package/dist/agent/mcp-bridge.js +86 -0
- package/dist/agent/mcp-bridge.js.map +1 -0
- package/dist/agent/mcp-client.d.ts +24 -0
- package/dist/agent/mcp-client.d.ts.map +1 -0
- package/dist/agent/mcp-client.js +146 -0
- package/dist/agent/mcp-client.js.map +1 -0
- package/dist/agent/mcp-manager.d.ts +13 -0
- package/dist/agent/mcp-manager.d.ts.map +1 -0
- package/dist/agent/mcp-manager.js +84 -0
- package/dist/agent/mcp-manager.js.map +1 -0
- package/dist/agent/mcp-registry.d.ts +45 -0
- package/dist/agent/mcp-registry.d.ts.map +1 -0
- package/dist/agent/mcp-registry.js +98 -0
- package/dist/agent/mcp-registry.js.map +1 -0
- package/dist/agent/orchestrator.d.ts +14 -0
- package/dist/agent/orchestrator.d.ts.map +1 -0
- package/dist/agent/orchestrator.js +118 -0
- package/dist/agent/orchestrator.js.map +1 -0
- package/dist/agent/parser-adapter.d.ts +120 -0
- package/dist/agent/parser-adapter.d.ts.map +1 -0
- package/dist/agent/parser-adapter.js +265 -0
- package/dist/agent/parser-adapter.js.map +1 -0
- package/dist/agent/parsers/imports.d.ts +11 -0
- package/dist/agent/parsers/imports.d.ts.map +1 -0
- package/dist/agent/parsers/imports.js +94 -0
- package/dist/agent/parsers/imports.js.map +1 -0
- package/dist/agent/parsers/shell.d.ts +23 -0
- package/dist/agent/parsers/shell.d.ts.map +1 -0
- package/dist/agent/parsers/shell.js +200 -0
- package/dist/agent/parsers/shell.js.map +1 -0
- package/dist/agent/parsers/symbols.d.ts +17 -0
- package/dist/agent/parsers/symbols.d.ts.map +1 -0
- package/dist/agent/parsers/symbols.js +103 -0
- package/dist/agent/parsers/symbols.js.map +1 -0
- package/dist/agent/permissions.d.ts +65 -0
- package/dist/agent/permissions.d.ts.map +1 -0
- package/dist/agent/permissions.js +219 -0
- package/dist/agent/permissions.js.map +1 -0
- package/dist/agent/predictive-gate.d.ts +69 -0
- package/dist/agent/predictive-gate.d.ts.map +1 -0
- package/dist/agent/predictive-gate.js +128 -0
- package/dist/agent/predictive-gate.js.map +1 -0
- package/dist/agent/provider-cooldown.d.ts +144 -0
- package/dist/agent/provider-cooldown.d.ts.map +1 -0
- package/dist/agent/provider-cooldown.js +300 -0
- package/dist/agent/provider-cooldown.js.map +1 -0
- package/dist/agent/providers-manager.d.ts +109 -0
- package/dist/agent/providers-manager.d.ts.map +1 -0
- package/dist/agent/providers-manager.js +464 -0
- package/dist/agent/providers-manager.js.map +1 -0
- package/dist/agent/repo-map.d.ts +6 -0
- package/dist/agent/repo-map.d.ts.map +1 -0
- package/dist/agent/repo-map.js +221 -0
- package/dist/agent/repo-map.js.map +1 -0
- package/dist/agent/retry.d.ts +103 -0
- package/dist/agent/retry.d.ts.map +1 -0
- package/dist/agent/retry.js +276 -0
- package/dist/agent/retry.js.map +1 -0
- package/dist/agent/search/index.d.ts +61 -0
- package/dist/agent/search/index.d.ts.map +1 -0
- package/dist/agent/search/index.js +314 -0
- package/dist/agent/search/index.js.map +1 -0
- package/dist/agent/single-agent.d.ts +76 -0
- package/dist/agent/single-agent.d.ts.map +1 -0
- package/dist/agent/single-agent.js +697 -0
- package/dist/agent/single-agent.js.map +1 -0
- package/dist/agent/skills.d.ts +22 -0
- package/dist/agent/skills.d.ts.map +1 -0
- package/dist/agent/skills.js +139 -0
- package/dist/agent/skills.js.map +1 -0
- package/dist/agent/stream-glue.d.ts +85 -0
- package/dist/agent/stream-glue.d.ts.map +1 -0
- package/dist/agent/stream-glue.js +120 -0
- package/dist/agent/stream-glue.js.map +1 -0
- package/dist/agent/subagent.d.ts +72 -0
- package/dist/agent/subagent.d.ts.map +1 -0
- package/dist/agent/subagent.js +193 -0
- package/dist/agent/subagent.js.map +1 -0
- package/dist/agent/telemetry.d.ts +192 -0
- package/dist/agent/telemetry.d.ts.map +1 -0
- package/dist/agent/telemetry.js +400 -0
- package/dist/agent/telemetry.js.map +1 -0
- package/dist/agent/tokenizer.d.ts +42 -0
- package/dist/agent/tokenizer.d.ts.map +1 -0
- package/dist/agent/tokenizer.js +107 -0
- package/dist/agent/tokenizer.js.map +1 -0
- package/dist/agent/tool-executor.d.ts +289 -0
- package/dist/agent/tool-executor.d.ts.map +1 -0
- package/dist/agent/tool-executor.js +2519 -0
- package/dist/agent/tool-executor.js.map +1 -0
- package/dist/agent/web-impl.d.ts +2 -0
- package/dist/agent/web-impl.d.ts.map +1 -0
- package/dist/agent/web-impl.js +34 -0
- package/dist/agent/web-impl.js.map +1 -0
- package/dist/agent/web.d.ts +8 -0
- package/dist/agent/web.d.ts.map +1 -0
- package/dist/agent/web.js +8 -0
- package/dist/agent/web.js.map +1 -0
- package/dist/agent/worker-agent.d.ts +27 -0
- package/dist/agent/worker-agent.d.ts.map +1 -0
- package/dist/agent/worker-agent.js +503 -0
- package/dist/agent/worker-agent.js.map +1 -0
- package/dist/config.d.ts +162 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +138 -0
- package/dist/config.js.map +1 -0
- package/dist/context/fixo-md-watcher.d.ts +42 -0
- package/dist/context/fixo-md-watcher.d.ts.map +1 -0
- package/dist/context/fixo-md-watcher.js +126 -0
- package/dist/context/fixo-md-watcher.js.map +1 -0
- package/dist/context/fixo-md.d.ts +50 -0
- package/dist/context/fixo-md.d.ts.map +1 -0
- package/dist/context/fixo-md.js +118 -0
- package/dist/context/fixo-md.js.map +1 -0
- package/dist/context/todo.d.ts +65 -0
- package/dist/context/todo.d.ts.map +1 -0
- package/dist/context/todo.js +194 -0
- package/dist/context/todo.js.map +1 -0
- package/dist/git/git-manager.d.ts +33 -0
- package/dist/git/git-manager.d.ts.map +1 -0
- package/dist/git/git-manager.js +293 -0
- package/dist/git/git-manager.js.map +1 -0
- package/dist/git/git-ops.d.ts +10 -0
- package/dist/git/git-ops.d.ts.map +1 -0
- package/dist/git/git-ops.js +131 -0
- package/dist/git/git-ops.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +352 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer.d.ts +30 -0
- package/dist/indexer.d.ts.map +1 -0
- package/dist/indexer.js +273 -0
- package/dist/indexer.js.map +1 -0
- package/dist/lsp/lsp-client.d.ts +24 -0
- package/dist/lsp/lsp-client.d.ts.map +1 -0
- package/dist/lsp/lsp-client.js +205 -0
- package/dist/lsp/lsp-client.js.map +1 -0
- package/dist/lsp/lsp-manager.d.ts +17 -0
- package/dist/lsp/lsp-manager.d.ts.map +1 -0
- package/dist/lsp/lsp-manager.js +154 -0
- package/dist/lsp/lsp-manager.js.map +1 -0
- package/dist/lsp/lsp-pre-save.d.ts +137 -0
- package/dist/lsp/lsp-pre-save.d.ts.map +1 -0
- package/dist/lsp/lsp-pre-save.js +245 -0
- package/dist/lsp/lsp-pre-save.js.map +1 -0
- package/dist/lsp/syntax-fallback.d.ts +83 -0
- package/dist/lsp/syntax-fallback.d.ts.map +1 -0
- package/dist/lsp/syntax-fallback.js +275 -0
- package/dist/lsp/syntax-fallback.js.map +1 -0
- package/dist/model-outcomes.d.ts +12 -0
- package/dist/model-outcomes.d.ts.map +1 -0
- package/dist/model-outcomes.js +46 -0
- package/dist/model-outcomes.js.map +1 -0
- package/dist/planner.d.ts +32 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +163 -0
- package/dist/planner.js.map +1 -0
- package/dist/project-memory.d.ts +29 -0
- package/dist/project-memory.d.ts.map +1 -0
- package/dist/project-memory.js +349 -0
- package/dist/project-memory.js.map +1 -0
- package/dist/review.d.ts +2 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +61 -0
- package/dist/review.js.map +1 -0
- package/dist/runtime/background-jobs.d.ts +97 -0
- package/dist/runtime/background-jobs.d.ts.map +1 -0
- package/dist/runtime/background-jobs.js +331 -0
- package/dist/runtime/background-jobs.js.map +1 -0
- package/dist/runtime/credential-vault.d.ts +124 -0
- package/dist/runtime/credential-vault.d.ts.map +1 -0
- package/dist/runtime/credential-vault.js +184 -0
- package/dist/runtime/credential-vault.js.map +1 -0
- package/dist/runtime/loop-trap.d.ts +197 -0
- package/dist/runtime/loop-trap.d.ts.map +1 -0
- package/dist/runtime/loop-trap.js +420 -0
- package/dist/runtime/loop-trap.js.map +1 -0
- package/dist/runtime/policy.d.ts +15 -0
- package/dist/runtime/policy.d.ts.map +1 -0
- package/dist/runtime/policy.js +60 -0
- package/dist/runtime/policy.js.map +1 -0
- package/dist/runtime/redaction.d.ts +66 -0
- package/dist/runtime/redaction.d.ts.map +1 -0
- package/dist/runtime/redaction.js +155 -0
- package/dist/runtime/redaction.js.map +1 -0
- package/dist/runtime/session-snapshots.d.ts +76 -0
- package/dist/runtime/session-snapshots.d.ts.map +1 -0
- package/dist/runtime/session-snapshots.js +166 -0
- package/dist/runtime/session-snapshots.js.map +1 -0
- package/dist/runtime/staging.d.ts +205 -0
- package/dist/runtime/staging.d.ts.map +1 -0
- package/dist/runtime/staging.js +526 -0
- package/dist/runtime/staging.js.map +1 -0
- package/dist/runtime/task-session.d.ts +95 -0
- package/dist/runtime/task-session.d.ts.map +1 -0
- package/dist/runtime/task-session.js +263 -0
- package/dist/runtime/task-session.js.map +1 -0
- package/dist/runtime/worktree.d.ts +55 -0
- package/dist/runtime/worktree.d.ts.map +1 -0
- package/dist/runtime/worktree.js +175 -0
- package/dist/runtime/worktree.js.map +1 -0
- package/dist/setup-wizard.d.ts +8 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +73 -0
- package/dist/setup-wizard.js.map +1 -0
- package/dist/shared/content.d.ts +43 -0
- package/dist/shared/content.d.ts.map +1 -0
- package/dist/shared/content.js +61 -0
- package/dist/shared/content.js.map +1 -0
- package/dist/shared/types.d.ts +217 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +3 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/test-runner.d.ts +5 -0
- package/dist/test-runner.d.ts.map +1 -0
- package/dist/test-runner.js +42 -0
- package/dist/test-runner.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/ascii.d.ts +23 -0
- package/dist/ui/ascii.d.ts.map +1 -0
- package/dist/ui/ascii.js +45 -0
- package/dist/ui/ascii.js.map +1 -0
- package/dist/ui/colors.d.ts +111 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +166 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/ui/image-attach.d.ts +27 -0
- package/dist/ui/image-attach.d.ts.map +1 -0
- package/dist/ui/image-attach.js +100 -0
- package/dist/ui/image-attach.js.map +1 -0
- package/dist/ui/index.d.ts +18 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +18 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/markdown-stream.d.ts +91 -0
- package/dist/ui/markdown-stream.d.ts.map +1 -0
- package/dist/ui/markdown-stream.js +524 -0
- package/dist/ui/markdown-stream.js.map +1 -0
- package/dist/ui/plan-renderer.d.ts +36 -0
- package/dist/ui/plan-renderer.d.ts.map +1 -0
- package/dist/ui/plan-renderer.js +79 -0
- package/dist/ui/plan-renderer.js.map +1 -0
- package/dist/ui/prompt.d.ts +11 -0
- package/dist/ui/prompt.d.ts.map +1 -0
- package/dist/ui/prompt.js +1960 -0
- package/dist/ui/prompt.js.map +1 -0
- package/dist/ui/render-primitives.d.ts +117 -0
- package/dist/ui/render-primitives.d.ts.map +1 -0
- package/dist/ui/render-primitives.js +322 -0
- package/dist/ui/render-primitives.js.map +1 -0
- package/dist/ui/render.d.ts +133 -0
- package/dist/ui/render.d.ts.map +1 -0
- package/dist/ui/render.js +547 -0
- package/dist/ui/render.js.map +1 -0
- package/dist/ui/session-header.d.ts +30 -0
- package/dist/ui/session-header.d.ts.map +1 -0
- package/dist/ui/session-header.js +74 -0
- package/dist/ui/session-header.js.map +1 -0
- package/dist/workspace-guard.d.ts +68 -0
- package/dist/workspace-guard.d.ts.map +1 -0
- package/dist/workspace-guard.js +168 -0
- package/dist/workspace-guard.js.map +1 -0
- package/dist/workspace-lock.d.ts +27 -0
- package/dist/workspace-lock.d.ts.map +1 -0
- package/dist/workspace-lock.js +95 -0
- package/dist/workspace-lock.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { AgentClient } from './agent-client.js';
|
|
2
|
+
import { ConversationManager } from './conversation.js';
|
|
3
|
+
import { getActiveTools, executeTool, classifyExecutionRole } from './tool-executor.js';
|
|
4
|
+
import { isTrivialQuery } from '../planner.js';
|
|
5
|
+
import { buildRepoMap } from './repo-map.js';
|
|
6
|
+
import { loadConfig } from '../config.js';
|
|
7
|
+
import { recordTelemetry, telemetry } from './telemetry.js';
|
|
8
|
+
import { buildProjectInstructionsBlock, recordFixoMdLoad, } from '../context/fixo-md.js';
|
|
9
|
+
import { loadTodoList, summariseTodoList, } from '../context/todo.js';
|
|
10
|
+
import { C } from '../ui/colors.js';
|
|
11
|
+
import { MarkdownStreamRenderer, renderMarkdown } from '../ui/markdown-stream.js';
|
|
12
|
+
import { SemanticLoopDetector, SemanticLoopAbortedError, toSafetyAlertDirective, } from '../runtime/loop-trap.js';
|
|
13
|
+
import { dashboard } from '../ui/render.js';
|
|
14
|
+
import * as p from '@clack/prompts';
|
|
15
|
+
export const promptsWrapper = {
|
|
16
|
+
select: p.select,
|
|
17
|
+
confirm: p.confirm,
|
|
18
|
+
spinner: p.spinner,
|
|
19
|
+
isCancel: p.isCancel,
|
|
20
|
+
};
|
|
21
|
+
import { TaskSession } from '../runtime/task-session.js';
|
|
22
|
+
import { BackgroundAwareness } from './background-awareness.js';
|
|
23
|
+
import { FixoMdWatcher } from '../context/fixo-md-watcher.js';
|
|
24
|
+
/* ──────────────────────── Constants ──────────────────────── */
|
|
25
|
+
const MAX_TOOL_CALLS = 25;
|
|
26
|
+
const MAX_TOOL_RESULT_LENGTH = 30_000;
|
|
27
|
+
const colors = {
|
|
28
|
+
reset: C.RESET,
|
|
29
|
+
bold: C.BOLD,
|
|
30
|
+
dim: C.SNOW4,
|
|
31
|
+
green: C.GREEN,
|
|
32
|
+
yellow: C.YELLOW,
|
|
33
|
+
cyan: C.BLUE,
|
|
34
|
+
red: C.RED,
|
|
35
|
+
gray: C.SNOW3,
|
|
36
|
+
magenta: C.PURPLE,
|
|
37
|
+
};
|
|
38
|
+
export function evaluateInputIntent(task) {
|
|
39
|
+
const cleanTask = task.toLowerCase().trim();
|
|
40
|
+
// Strong mutation indicators override any chat keywords (e.g. "refactor the list component")
|
|
41
|
+
const mutationKeywords = [
|
|
42
|
+
/\bcreate\b/, /\bwrite\b/, /\bfix\b/, /\brefactor\b/, /\bupdate\b/,
|
|
43
|
+
/\bdelete\b/, /\badd\b/, /\bimplement\b/, /\bmodify\b/, /\bchange\b/, /\bmake\b/
|
|
44
|
+
];
|
|
45
|
+
if (mutationKeywords.some(pattern => pattern.test(cleanTask))) {
|
|
46
|
+
return 'MUTATION';
|
|
47
|
+
}
|
|
48
|
+
// Codebase or file reference queries must have tools enabled
|
|
49
|
+
const codebaseKeywords = [
|
|
50
|
+
/\bcodebase\b/, /\brepo\b/, /\brepository\b/, /\bvulnerab\w*\b/, /\bfile\b/,
|
|
51
|
+
/\bfolder\b/, /\bdirectory\b/, /\bpath\b/, /\btest\b/, /\berror\b/,
|
|
52
|
+
/\bwarning\b/, /\bbug\b/, /\bissue\b/, /\bcompile\b/, /\bbuild\b/
|
|
53
|
+
];
|
|
54
|
+
const fileRefPattern = /\b[\w./-]+\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|php|css|scss|json|md|yml|yaml|toml|sh|bash|txt|html|vue|svelte)\b/i;
|
|
55
|
+
if (codebaseKeywords.some(pattern => pattern.test(cleanTask)) || fileRefPattern.test(cleanTask)) {
|
|
56
|
+
return 'MUTATION';
|
|
57
|
+
}
|
|
58
|
+
const chatKeywords = [
|
|
59
|
+
/\bguide\b/, /\bexplain\b/, /\bwhy\b/, /\bhow to\b/, /\blist\b/,
|
|
60
|
+
/\breview\b/, /\btell me\b/, /\bwhat is\b/, /\bsuggest\b/, /\bwhat are\b/
|
|
61
|
+
];
|
|
62
|
+
if (chatKeywords.some(pattern => pattern.test(cleanTask))) {
|
|
63
|
+
return 'CHAT_ONLY';
|
|
64
|
+
}
|
|
65
|
+
return 'MUTATION';
|
|
66
|
+
}
|
|
67
|
+
/* ──────────────────────── Permission helpers ──────────────────────── */
|
|
68
|
+
function formatPermissionPrompt(name, args) {
|
|
69
|
+
switch (name) {
|
|
70
|
+
case 'write_file':
|
|
71
|
+
return `Allow write to ${colors.cyan}${colors.bold}${args.path || 'unknown path'}${colors.reset}?`;
|
|
72
|
+
case 'run_command':
|
|
73
|
+
return `Allow command execution: ${colors.yellow}${colors.bold}${args.command || 'unknown command'}${colors.reset}?`;
|
|
74
|
+
case 'apply_patch':
|
|
75
|
+
return `Allow apply_patch (unified diff, ${(args.patch ?? '').length} chars)?`;
|
|
76
|
+
case 'replace_range':
|
|
77
|
+
return `Allow replace_range on ${colors.cyan}${args.path}${colors.reset} lines ${args.startLine}..${args.endLine}?`;
|
|
78
|
+
case 'insert_after':
|
|
79
|
+
return `Allow insert_after on ${colors.cyan}${args.path}${colors.reset}?`;
|
|
80
|
+
case 'rename_file':
|
|
81
|
+
return `Allow rename ${colors.cyan}${args.from}${colors.reset} → ${colors.cyan}${args.to}${colors.reset}?`;
|
|
82
|
+
case 'delete_file':
|
|
83
|
+
return `Allow ${colors.red}delete${colors.reset} ${colors.cyan}${args.path}${colors.reset}?`;
|
|
84
|
+
case 'create_branch':
|
|
85
|
+
return `Allow create git branch "${args.branchName}"?`;
|
|
86
|
+
case 'commit_changes':
|
|
87
|
+
return `Allow git commit: "${(args.message ?? '').slice(0, 80)}"?`;
|
|
88
|
+
case 'push_branch':
|
|
89
|
+
return `Allow git push to ${args.remote || 'origin'}?`;
|
|
90
|
+
case 'create_pull_request':
|
|
91
|
+
return `Allow create pull request (base: ${args.baseBranch || 'main'})?`;
|
|
92
|
+
default:
|
|
93
|
+
return `Allow ${name}?`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/* ──────────────────────── System Prompt ──────────────────────── */
|
|
97
|
+
/**
|
|
98
|
+
* Build the `content` for the next user message. When the caller
|
|
99
|
+
* supplied `pendingAttachments` (today: images queued via the
|
|
100
|
+
* `/image` slash command), the content is a typed block array
|
|
101
|
+
* with the task text first and the attachments after. Otherwise
|
|
102
|
+
* the historical plain-string shape is preserved so providers
|
|
103
|
+
* without vision support stay on the simple wire format.
|
|
104
|
+
*/
|
|
105
|
+
function buildUserContent(context) {
|
|
106
|
+
const attachments = context.pendingAttachments;
|
|
107
|
+
if (!attachments || attachments.length === 0) {
|
|
108
|
+
return context.task;
|
|
109
|
+
}
|
|
110
|
+
const blocks = [{ type: 'text', text: context.task }];
|
|
111
|
+
for (const a of attachments)
|
|
112
|
+
blocks.push(a);
|
|
113
|
+
return blocks;
|
|
114
|
+
}
|
|
115
|
+
function buildSystemPrompt(repoMap, context, enableTools = true) {
|
|
116
|
+
const parts = [];
|
|
117
|
+
if (enableTools) {
|
|
118
|
+
parts.push(`You are FixO CLI, an autonomous AI coding agent. You help developers by reading, writing, and modifying code files in their workspace.`, ``, `## Capabilities`, `You have access to these tools:`, `- **read_file(path)** — Read a file's contents`, `- **write_file(path, content)** — Create or overwrite a file`, `- **run_command(command)** — Execute a shell command (npm test, git status, etc.)`, `- **search_code(query)** — Search for patterns in the codebase`, `- **list_dir(path)** — List directory contents`, ``, `## Guidelines`, `1. ALWAYS read existing files before modifying them to understand current code.`, `2. For new files, write complete contents — never use placeholders like "// ... rest of the file". For edits to existing files, follow the Editing Discipline below.`, `3. After making changes, run the verification command if one is configured.`, `4. Keep your text responses concise. Focus on what you did and why.`, `5. If the task is ambiguous, ask a clarifying question instead of guessing.`, `6. Preserve existing code comments and formatting unless asked to change them.`, ``, `## Editing Discipline`, `Pick the narrowest tool that fits the change. Rewriting a file you only need to tweak burns tokens, defeats the LSP pre-save granularity, and risks clobbering concurrent edits.`, `- **Single-region edit on an existing file** (one symbol, one block, one line) → use \`str_replace\`. It is surgical and atomic. By default it errors when the snippet is non-unique — narrow the snippet, don't disable the check.`, `- **Multi-region or hunked edit on an existing file** (several non-adjacent changes, or a diff you already have) → use \`apply_patch\` with a unified diff. One tool call, all hunks atomic.`, `- **New file** OR **full rewrite** where the prior content is genuinely irrelevant → use \`write_file\`. This is the only sanctioned use of \`write_file\` on an existing path.`, `Never use \`write_file\` to "edit" an existing file by rewriting it whole. If the diff is small enough to describe, it is small enough for \`str_replace\` or \`apply_patch\`.`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
parts.push(`You are FixO CLI, a friendly AI coding assistant. You help developers by answering questions, explaining code, and discussing software engineering concepts.`, ``, `## Guidelines`, `1. Provide clear, detailed, and accurate explanations.`, `2. Keep your responses focused and helpful.`, `3. If you refer to code structure, do so conceptually as you currently do not have active tool access to modify code.`);
|
|
122
|
+
}
|
|
123
|
+
parts.push(``, `## Workspace`, `Working directory: ${context.cwd}`);
|
|
124
|
+
// Add pinned files info
|
|
125
|
+
if (context.selectedFiles.length > 0) {
|
|
126
|
+
parts.push(`Pinned files: ${context.selectedFiles.join(', ')}`);
|
|
127
|
+
}
|
|
128
|
+
// Add verification command
|
|
129
|
+
if (context.checkCommand) {
|
|
130
|
+
parts.push(`Verification command: \`${context.checkCommand}\``);
|
|
131
|
+
}
|
|
132
|
+
// Add project-specific system prompt
|
|
133
|
+
if (context.systemPromptOverride) {
|
|
134
|
+
parts.push(``, `## Project Instructions`, context.systemPromptOverride);
|
|
135
|
+
}
|
|
136
|
+
// Add FIXO.md block (project-local instructions from the
|
|
137
|
+
// configured lookup chain). Telemetry is emitted in a
|
|
138
|
+
// microtask so the system-prompt build remains sync.
|
|
139
|
+
const { block: fixoBlock, result: fixoResult } = buildProjectInstructionsBlock(context.cwd);
|
|
140
|
+
if (fixoBlock.length > 0) {
|
|
141
|
+
parts.push(fixoBlock);
|
|
142
|
+
void recordFixoMdLoad(fixoResult);
|
|
143
|
+
}
|
|
144
|
+
// Add repo map
|
|
145
|
+
parts.push(``, repoMap);
|
|
146
|
+
// Append a one-line todo summary so the LLM always knows
|
|
147
|
+
// what the current plan is without having to call
|
|
148
|
+
// todo_read on every turn.
|
|
149
|
+
const todoSummary = summariseTodoList(loadTodoList(context.cwd));
|
|
150
|
+
if (todoSummary.length > 0) {
|
|
151
|
+
parts.push(``, `## Todo`, todoSummary);
|
|
152
|
+
}
|
|
153
|
+
return parts.join('\n');
|
|
154
|
+
}
|
|
155
|
+
/* ──────────────────────── SingleAgent ──────────────────────── */
|
|
156
|
+
export class SingleAgent {
|
|
157
|
+
client;
|
|
158
|
+
verbose;
|
|
159
|
+
allowAll = false;
|
|
160
|
+
constructor(verbose = false) {
|
|
161
|
+
const config = loadConfig();
|
|
162
|
+
this.client = new AgentClient(config.freellmapi_api_key || '', config.apiUrl, verbose);
|
|
163
|
+
this.verbose = verbose;
|
|
164
|
+
}
|
|
165
|
+
/** Expose the underlying client for direct API calls (e.g. compaction). */
|
|
166
|
+
getClient() {
|
|
167
|
+
return this.client;
|
|
168
|
+
}
|
|
169
|
+
async runStreaming(context, conversation, rl) {
|
|
170
|
+
const startTime = Date.now();
|
|
171
|
+
const totalUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
172
|
+
let toolCallCount = 0;
|
|
173
|
+
const modifiedFiles = [];
|
|
174
|
+
let resolvedModel = context.model;
|
|
175
|
+
// Set model context limit for accurate overflow detection
|
|
176
|
+
conversation.setContextLimit(context.model);
|
|
177
|
+
// ──── Trivial query → stream directly ────
|
|
178
|
+
if (isTrivialQuery(context.task)) {
|
|
179
|
+
const trivialSystem = `You are FixO CLI, a friendly AI coding assistant. Respond briefly and helpfully.`;
|
|
180
|
+
// Auto-compact if context is getting large
|
|
181
|
+
await this.autoCompactIfNeeded(conversation, trivialSystem, context.task, context.model);
|
|
182
|
+
// Pillar 4 — proactive budget enforcement
|
|
183
|
+
await this.enforceContextBudget(conversation, trivialSystem, context.task, context.model);
|
|
184
|
+
const messages = [
|
|
185
|
+
{ role: 'system', content: trivialSystem },
|
|
186
|
+
...conversation.getMessages(),
|
|
187
|
+
{ role: 'user', content: buildUserContent(context) },
|
|
188
|
+
];
|
|
189
|
+
const streamRes = await this.streamResponse(messages, context.model, totalUsage);
|
|
190
|
+
const fullResponse = streamRes.responseText;
|
|
191
|
+
conversation.addTurn(context.task, fullResponse);
|
|
192
|
+
return {
|
|
193
|
+
success: true,
|
|
194
|
+
response: fullResponse,
|
|
195
|
+
modifiedFiles: [],
|
|
196
|
+
tokensUsed: totalUsage,
|
|
197
|
+
toolCallCount: 0,
|
|
198
|
+
durationMs: Date.now() - startTime,
|
|
199
|
+
model: streamRes.resolvedModel,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const intent = evaluateInputIntent(context.task);
|
|
203
|
+
if (intent === 'CHAT_ONLY') {
|
|
204
|
+
return await this.executePureChatStream(context.task, conversation, context);
|
|
205
|
+
}
|
|
206
|
+
// ──── Complex task → tool loop ────
|
|
207
|
+
const repoMap = buildRepoMap(context.cwd);
|
|
208
|
+
const systemPrompt = buildSystemPrompt(repoMap, context);
|
|
209
|
+
// Auto-compact before building messages if context is near limit
|
|
210
|
+
await this.autoCompactIfNeeded(conversation, systemPrompt, context.task, context.model);
|
|
211
|
+
// Pillar 4 — proactive budget enforcement
|
|
212
|
+
await this.enforceContextBudget(conversation, systemPrompt, context.task, context.model);
|
|
213
|
+
const messages = [
|
|
214
|
+
{ role: 'system', content: systemPrompt },
|
|
215
|
+
...conversation.getMessages(),
|
|
216
|
+
{ role: 'user', content: buildUserContent(context) },
|
|
217
|
+
];
|
|
218
|
+
/**
|
|
219
|
+
* Helper to inject a safety directive into the system message at the
|
|
220
|
+
* head of the messages array. The directive is prepended (rather than
|
|
221
|
+
* appended) so the LLM sees it before the conversation history,
|
|
222
|
+
* which maximises the chance it changes its strategy on the next
|
|
223
|
+
* turn. The base system prompt is preserved untouched.
|
|
224
|
+
*/
|
|
225
|
+
const injectSafetyDirective = (directive) => {
|
|
226
|
+
if (messages.length === 0 || messages[0]?.role !== 'system') {
|
|
227
|
+
messages.unshift({ role: 'system', content: directive });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const first = messages[0];
|
|
231
|
+
messages[0] = {
|
|
232
|
+
role: 'system',
|
|
233
|
+
content: `${directive}\n\n${first.content}`,
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
const taskSession = new TaskSession({
|
|
237
|
+
cwd: context.cwd,
|
|
238
|
+
task: context.task,
|
|
239
|
+
model: context.model,
|
|
240
|
+
policy: context.policy,
|
|
241
|
+
});
|
|
242
|
+
// Pillar 2 — auto-collect any expired staged writes at the
|
|
243
|
+
// start of every run. Stale staged writes from previous
|
|
244
|
+
// sessions are quarantined to a single TTL-bounded folder
|
|
245
|
+
// and removed here. Safe to run on every run start.
|
|
246
|
+
try {
|
|
247
|
+
const { AtomicStagingManager } = await import('../runtime/staging.js');
|
|
248
|
+
AtomicStagingManager.garbageCollectAll(context.cwd);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// Staging is best-effort cleanup; never block the run.
|
|
252
|
+
}
|
|
253
|
+
// Pillar 5 / Protection 2 — classify the task and gate
|
|
254
|
+
// mutation tools. Read-only / review / analysis tasks run
|
|
255
|
+
// without write_file, apply_patch, etc. visible to the LLM.
|
|
256
|
+
const role = classifyExecutionRole(context.task);
|
|
257
|
+
const activeTools = getActiveTools(role === 'READ_ONLY' ? 'READ_ONLY' : context.mode);
|
|
258
|
+
if (role === 'READ_ONLY') {
|
|
259
|
+
console.log(`${colors.dim}🛡 Read-only role — mutation tools hidden.${colors.reset}`);
|
|
260
|
+
}
|
|
261
|
+
const safety = loadConfig().preferences.safety;
|
|
262
|
+
// Pillar 2 — semantic loop detector. Tracks per-file frequency so
|
|
263
|
+
// an LLM which varies its search arguments but keeps hammering
|
|
264
|
+
// the same file still trips. The composite LoopTrapDetector is
|
|
265
|
+
// still wired in (callers may pass safety.loopTrap) so the two
|
|
266
|
+
// detectors run in parallel; the semantic one covers the most
|
|
267
|
+
// common accidental "stare at one file" failure mode.
|
|
268
|
+
const semanticLoopDetector = new SemanticLoopDetector(safety.semanticLoopTrap);
|
|
269
|
+
let pendingSafetyDirective = null;
|
|
270
|
+
// Pillar 5 — per-turn background-job awareness. The LLM
|
|
271
|
+
// routinely forgets jobs it spawned earlier; we counter that by
|
|
272
|
+
// injecting a compact `[Background Jobs]` directive at the head
|
|
273
|
+
// of each chat() call. New terminal statuses are announced
|
|
274
|
+
// exactly once; still-running jobs are reminded every turn.
|
|
275
|
+
const backgroundAwareness = new BackgroundAwareness(context.cwd);
|
|
276
|
+
// Phase 4 — FIXO.md per-turn re-injection. The watcher captures
|
|
277
|
+
// the on-disk fingerprint at run start so the first check is a
|
|
278
|
+
// no-op (file already baked into the system prompt). Any
|
|
279
|
+
// mid-run create/update/delete surfaces as a [Project
|
|
280
|
+
// Instructions] directive on the next chat().
|
|
281
|
+
const fixoMdWatcher = new FixoMdWatcher(context.cwd);
|
|
282
|
+
console.log(`\n${colors.cyan}${colors.bold}🤖 Agent working...${colors.reset}`);
|
|
283
|
+
try {
|
|
284
|
+
while (toolCallCount < MAX_TOOL_CALLS) {
|
|
285
|
+
// Background-job awareness: surface newly-finished and
|
|
286
|
+
// still-running jobs as a directive before each chat() call.
|
|
287
|
+
// Skipped on the first iteration because no async tools have
|
|
288
|
+
// run yet — saves tokens when the user's task doesn't
|
|
289
|
+
// involve background jobs at all.
|
|
290
|
+
if (toolCallCount > 0) {
|
|
291
|
+
const bgSnap = backgroundAwareness.snapshot();
|
|
292
|
+
const bgDirective = backgroundAwareness.formatDirective(bgSnap);
|
|
293
|
+
if (bgDirective) {
|
|
294
|
+
injectSafetyDirective(bgDirective);
|
|
295
|
+
backgroundAwareness.markAnnounced(bgSnap);
|
|
296
|
+
}
|
|
297
|
+
// FIXO.md mid-run change detection. Stats the active path
|
|
298
|
+
// and only injects when the on-disk fingerprint differs
|
|
299
|
+
// from what was baked into the system prompt. Skipped on
|
|
300
|
+
// iter 0 for the same reason as the job-awareness check.
|
|
301
|
+
const fixoMdWatch = fixoMdWatcher.check();
|
|
302
|
+
const fixoDirective = fixoMdWatcher.formatDirective(fixoMdWatch);
|
|
303
|
+
if (fixoDirective) {
|
|
304
|
+
injectSafetyDirective(fixoDirective);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const spinner = promptsWrapper.spinner();
|
|
308
|
+
spinner.start(`🤖 Agent thinking (turn ${toolCallCount + 1})...`);
|
|
309
|
+
dashboard.emit({
|
|
310
|
+
type: 'turn-start',
|
|
311
|
+
turnIndex: toolCallCount + 1,
|
|
312
|
+
task: context.task,
|
|
313
|
+
});
|
|
314
|
+
let result;
|
|
315
|
+
try {
|
|
316
|
+
result = await this.client.chat(messages, context.model, {
|
|
317
|
+
tools: activeTools,
|
|
318
|
+
tool_choice: 'auto',
|
|
319
|
+
});
|
|
320
|
+
resolvedModel = result.model;
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
// Handle context overflow — auto-compact and retry once
|
|
324
|
+
if (ConversationManager.isContextOverflowError(err)) {
|
|
325
|
+
spinner.stop('🔄 Context overflow detected');
|
|
326
|
+
console.log(`${colors.yellow}🔄 Context window full — auto-compacting...${colors.reset}`);
|
|
327
|
+
const compacted = await conversation.compact(this.client, context.model);
|
|
328
|
+
if (compacted) {
|
|
329
|
+
const info = conversation.getLastCompactionInfo();
|
|
330
|
+
console.log(`${colors.green}✓ Compacted: ${info?.messagesBefore ?? '?'} messages → summary + ${conversation.getMessageCount()} recent. ~${((info?.tokensFreed ?? 0) / 1000).toFixed(0)}k tokens freed.${colors.reset}`);
|
|
331
|
+
// Rebuild messages with compacted history
|
|
332
|
+
messages.length = 0;
|
|
333
|
+
messages.push({ role: 'system', content: systemPrompt }, ...conversation.getMessages(), { role: 'user', content: buildUserContent(context) });
|
|
334
|
+
continue; // Retry the LLM call
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
spinner.stop('🤖 Thought completed');
|
|
341
|
+
dashboard.emit({
|
|
342
|
+
type: 'status',
|
|
343
|
+
message: `Turn ${toolCallCount + 1} complete`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
totalUsage.prompt_tokens += result.usage.prompt_tokens;
|
|
347
|
+
totalUsage.completion_tokens += result.usage.completion_tokens;
|
|
348
|
+
totalUsage.total_tokens += result.usage.total_tokens;
|
|
349
|
+
// No tool calls → stream final response
|
|
350
|
+
if (!result.tool_calls || result.tool_calls.length === 0) {
|
|
351
|
+
const response = result.content ?? '';
|
|
352
|
+
// Print the response (already received in non-streaming mode)
|
|
353
|
+
if (response) {
|
|
354
|
+
renderMarkdown(response);
|
|
355
|
+
}
|
|
356
|
+
conversation.addTurn(context.task, response);
|
|
357
|
+
taskSession.finish('success', response);
|
|
358
|
+
return {
|
|
359
|
+
success: true,
|
|
360
|
+
response,
|
|
361
|
+
modifiedFiles,
|
|
362
|
+
tokensUsed: totalUsage,
|
|
363
|
+
toolCallCount,
|
|
364
|
+
durationMs: Date.now() - startTime,
|
|
365
|
+
model: resolvedModel,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// Execute tool calls (same as non-streaming)
|
|
369
|
+
const assistantMsg = {
|
|
370
|
+
role: 'assistant',
|
|
371
|
+
content: result.content,
|
|
372
|
+
tool_calls: result.tool_calls,
|
|
373
|
+
};
|
|
374
|
+
messages.push(assistantMsg);
|
|
375
|
+
if (result.content) {
|
|
376
|
+
console.log(`${colors.dim}${result.content}${colors.reset}`);
|
|
377
|
+
}
|
|
378
|
+
for (const toolCall of result.tool_calls) {
|
|
379
|
+
let parsedArgs;
|
|
380
|
+
try {
|
|
381
|
+
parsedArgs = JSON.parse(toolCall.function.arguments);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
parsedArgs = { error: 'Failed to parse tool arguments' };
|
|
385
|
+
}
|
|
386
|
+
// Pillar 2 — semantic loop detection. Records the tool
|
|
387
|
+
// call *before* execution so even a permission-denied
|
|
388
|
+
// tool still counts as a hit on the file. The verdict is
|
|
389
|
+
// inspected *after* execution so a warn can be staged as
|
|
390
|
+
// a system-prompt directive on the *next* LLM call.
|
|
391
|
+
if (semanticLoopDetector.preference.enabled) {
|
|
392
|
+
const verdict = semanticLoopDetector.record(toolCallCount, toolCall.function.name, parsedArgs, context.cwd);
|
|
393
|
+
if (verdict.state === 'warn') {
|
|
394
|
+
pendingSafetyDirective = toSafetyAlertDirective(verdict);
|
|
395
|
+
console.log(`${colors.yellow}⚠ Semantic loop warning: ${verdict.target} ` +
|
|
396
|
+
`accessed ${verdict.count}× in the last ${verdict.windowSize} turns.${colors.reset}`);
|
|
397
|
+
}
|
|
398
|
+
else if (verdict.state === 'hard-abort') {
|
|
399
|
+
// Rollback any staged writes from this run before
|
|
400
|
+
// throwing, so a runaway agent doesn't leave a
|
|
401
|
+
// half-edited workspace behind.
|
|
402
|
+
try {
|
|
403
|
+
const { AtomicStagingManager } = await import('../runtime/staging.js');
|
|
404
|
+
AtomicStagingManager.rollbackAll(context.cwd, taskSession.id);
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// best-effort; never mask the abort error
|
|
408
|
+
}
|
|
409
|
+
throw new SemanticLoopAbortedError(verdict.target, verdict.count, verdict.windowSize);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Apply any staged directive at the *start* of the next
|
|
413
|
+
// LLM call, not after the current iteration's tools have
|
|
414
|
+
// run. This keeps the conversation aligned with the model
|
|
415
|
+
// that produced the warning.
|
|
416
|
+
if (pendingSafetyDirective) {
|
|
417
|
+
injectSafetyDirective(pendingSafetyDirective);
|
|
418
|
+
pendingSafetyDirective = null;
|
|
419
|
+
}
|
|
420
|
+
const allowed = await this.askPermission(toolCall.function.name, parsedArgs, rl, context.yes);
|
|
421
|
+
let event;
|
|
422
|
+
if (!allowed) {
|
|
423
|
+
console.log(` ${colors.red}✗ Permission denied for ${toolCall.function.name}${colors.reset}`);
|
|
424
|
+
dashboard.emit({
|
|
425
|
+
type: 'tool-finish',
|
|
426
|
+
tool: toolCall.function.name,
|
|
427
|
+
target: parsedArgs.path ?? parsedArgs.from ?? '',
|
|
428
|
+
state: 'failed',
|
|
429
|
+
durationMs: 0,
|
|
430
|
+
});
|
|
431
|
+
event = {
|
|
432
|
+
tool: toolCall.function.name,
|
|
433
|
+
args: parsedArgs,
|
|
434
|
+
result: `Error: User denied permission to execute ${toolCall.function.name}.`,
|
|
435
|
+
isWrite: false,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
const toolStart = Date.now();
|
|
440
|
+
dashboard.emit({
|
|
441
|
+
type: 'tool-start',
|
|
442
|
+
tool: toolCall.function.name,
|
|
443
|
+
target: parsedArgs.path ?? parsedArgs.from ?? '',
|
|
444
|
+
turnIndex: toolCallCount + 1,
|
|
445
|
+
});
|
|
446
|
+
event = await executeTool(toolCall.function.name, parsedArgs, context.cwd, this.verbose, {
|
|
447
|
+
session: taskSession,
|
|
448
|
+
policy: context.policy,
|
|
449
|
+
allowWithoutPrompt: context.yes,
|
|
450
|
+
safety,
|
|
451
|
+
});
|
|
452
|
+
dashboard.emit({
|
|
453
|
+
type: 'tool-finish',
|
|
454
|
+
tool: toolCall.function.name,
|
|
455
|
+
target: parsedArgs.path ?? parsedArgs.from ?? '',
|
|
456
|
+
state: event.result.startsWith('Error:') ? 'failed' : 'completed',
|
|
457
|
+
durationMs: Date.now() - toolStart,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (event.isWrite && event.affectedPath) {
|
|
461
|
+
if (!modifiedFiles.includes(event.affectedPath)) {
|
|
462
|
+
modifiedFiles.push(event.affectedPath);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
let toolResult = event.result;
|
|
466
|
+
if (toolResult.length > MAX_TOOL_RESULT_LENGTH) {
|
|
467
|
+
toolResult =
|
|
468
|
+
toolResult.slice(0, MAX_TOOL_RESULT_LENGTH) +
|
|
469
|
+
`\n\n... (truncated, ${toolResult.length} total characters)`;
|
|
470
|
+
}
|
|
471
|
+
messages.push({
|
|
472
|
+
role: 'tool',
|
|
473
|
+
tool_call_id: toolCall.id,
|
|
474
|
+
content: toolResult,
|
|
475
|
+
});
|
|
476
|
+
toolCallCount++;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
console.log(`${colors.yellow}⚠ Tool call limit reached (${MAX_TOOL_CALLS}).${colors.reset}`);
|
|
480
|
+
conversation.addTurn(context.task, `Task processed with ${toolCallCount} tool calls.`);
|
|
481
|
+
const limitResponse = `Completed with ${toolCallCount} tool calls (limit reached).`;
|
|
482
|
+
taskSession.finish('success', limitResponse);
|
|
483
|
+
return {
|
|
484
|
+
success: true,
|
|
485
|
+
response: limitResponse,
|
|
486
|
+
modifiedFiles,
|
|
487
|
+
tokensUsed: totalUsage,
|
|
488
|
+
toolCallCount,
|
|
489
|
+
durationMs: Date.now() - startTime,
|
|
490
|
+
model: resolvedModel,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
495
|
+
taskSession.finish('error', errorMsg);
|
|
496
|
+
throw error;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Ask the user for permission to execute a tool.
|
|
501
|
+
* Prompts for every state-mutating tool: write_file,
|
|
502
|
+
* run_command, apply_patch, replace_range, insert_after,
|
|
503
|
+
* rename_file, delete_file, create_branch, commit_changes,
|
|
504
|
+
* push_branch, create_pull_request. Read-only tools (read_file,
|
|
505
|
+
* search_code, list_dir, extract_symbols, extract_imports)
|
|
506
|
+
* are auto-allowed.
|
|
507
|
+
*/
|
|
508
|
+
async askPermission(name, args, rl, allowWithoutPrompt) {
|
|
509
|
+
const MUTATING_TOOLS = new Set([
|
|
510
|
+
'write_file',
|
|
511
|
+
'run_command',
|
|
512
|
+
'apply_patch',
|
|
513
|
+
'replace_range',
|
|
514
|
+
'insert_after',
|
|
515
|
+
'rename_file',
|
|
516
|
+
'delete_file',
|
|
517
|
+
'create_branch',
|
|
518
|
+
'commit_changes',
|
|
519
|
+
'push_branch',
|
|
520
|
+
'create_pull_request',
|
|
521
|
+
]);
|
|
522
|
+
if (!MUTATING_TOOLS.has(name)) {
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
if (allowWithoutPrompt || this.allowAll) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
if (rl)
|
|
529
|
+
rl.pause();
|
|
530
|
+
try {
|
|
531
|
+
const message = formatPermissionPrompt(name, args);
|
|
532
|
+
const choice = await promptsWrapper.select({
|
|
533
|
+
message,
|
|
534
|
+
options: [
|
|
535
|
+
{ value: 'yes', label: 'Yes, allow' },
|
|
536
|
+
{ value: 'no', label: 'No, deny' },
|
|
537
|
+
{ value: 'all', label: 'Yes to all (trust session)' },
|
|
538
|
+
],
|
|
539
|
+
initialValue: 'yes',
|
|
540
|
+
});
|
|
541
|
+
if (promptsWrapper.isCancel(choice) || choice === 'no') {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
if (choice === 'all') {
|
|
545
|
+
this.allowAll = true;
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
return choice === 'yes';
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
if (rl)
|
|
552
|
+
rl.resume();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Stream a text-only response to the terminal.
|
|
557
|
+
*
|
|
558
|
+
* Selects the resumable streaming path when `preferences.resilience.
|
|
559
|
+
* streamResume === 'auto'` (the default). Set it to `'never'` to
|
|
560
|
+
* fall back to the legacy non-resumable path — useful for tests
|
|
561
|
+
* that want to observe raw stream cuts.
|
|
562
|
+
*/
|
|
563
|
+
async streamResponse(messages, model, usage) {
|
|
564
|
+
let fullText = '';
|
|
565
|
+
let resolvedModel = model;
|
|
566
|
+
const policy = loadConfig().preferences.resilience?.streamResume ?? 'auto';
|
|
567
|
+
const maxResumeAttempts = loadConfig().preferences.resilience?.maxResumeAttempts ?? 3;
|
|
568
|
+
const stream = policy === 'auto'
|
|
569
|
+
? this.client.chatStreamWithResume(messages, model, {}, maxResumeAttempts)
|
|
570
|
+
: this.client.chatStream(messages, model);
|
|
571
|
+
const renderer = new MarkdownStreamRenderer();
|
|
572
|
+
for await (const chunk of stream) {
|
|
573
|
+
if (chunk.type === 'content' && chunk.content) {
|
|
574
|
+
renderer.write(chunk.content);
|
|
575
|
+
fullText += chunk.content;
|
|
576
|
+
}
|
|
577
|
+
if (chunk.type === 'done') {
|
|
578
|
+
if (chunk.usage) {
|
|
579
|
+
usage.prompt_tokens += chunk.usage.prompt_tokens;
|
|
580
|
+
usage.completion_tokens += chunk.usage.completion_tokens;
|
|
581
|
+
usage.total_tokens += chunk.usage.total_tokens;
|
|
582
|
+
}
|
|
583
|
+
if (chunk.model) {
|
|
584
|
+
resolvedModel = chunk.model;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (fullText) {
|
|
589
|
+
if (!fullText.endsWith('\n'))
|
|
590
|
+
renderer.write('\n');
|
|
591
|
+
renderer.flush();
|
|
592
|
+
}
|
|
593
|
+
return { responseText: fullText, resolvedModel };
|
|
594
|
+
}
|
|
595
|
+
async executePureChatStream(task, conversation, context) {
|
|
596
|
+
const startTime = Date.now();
|
|
597
|
+
const totalUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
598
|
+
const repoMap = buildRepoMap(context.cwd);
|
|
599
|
+
const systemPrompt = buildSystemPrompt(repoMap, context, false);
|
|
600
|
+
// Auto-compact before chat if context is near limit
|
|
601
|
+
await this.autoCompactIfNeeded(conversation, systemPrompt, task, context.model);
|
|
602
|
+
// Pillar 4 — proactive budget enforcement
|
|
603
|
+
await this.enforceContextBudget(conversation, systemPrompt, task, context.model);
|
|
604
|
+
const messages = [
|
|
605
|
+
{ role: 'system', content: systemPrompt },
|
|
606
|
+
...conversation.getMessages(),
|
|
607
|
+
{ role: 'user', content: task },
|
|
608
|
+
];
|
|
609
|
+
const streamRes = await this.streamResponse(messages, context.model, totalUsage);
|
|
610
|
+
const fullResponse = streamRes.responseText;
|
|
611
|
+
conversation.addTurn(task, fullResponse);
|
|
612
|
+
return {
|
|
613
|
+
success: true,
|
|
614
|
+
response: fullResponse,
|
|
615
|
+
modifiedFiles: [],
|
|
616
|
+
tokensUsed: totalUsage,
|
|
617
|
+
toolCallCount: 0,
|
|
618
|
+
durationMs: Date.now() - startTime,
|
|
619
|
+
model: streamRes.resolvedModel,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Auto-compact the conversation if the next request would approach the context limit.
|
|
624
|
+
* This is the core of the auto-context-management system.
|
|
625
|
+
*/
|
|
626
|
+
async autoCompactIfNeeded(conversation, systemPrompt, userMessage, model) {
|
|
627
|
+
if (!conversation.shouldCompact(systemPrompt, userMessage)) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const estimatedTokens = conversation.estimateNextRequestTokens(systemPrompt, userMessage);
|
|
631
|
+
const limit = conversation.getContextLimit();
|
|
632
|
+
console.log(`\n${colors.yellow}🔄 Context approaching limit (${(estimatedTokens / 1000).toFixed(0)}k / ${(limit / 1000).toFixed(0)}k tokens) — auto-compacting...${colors.reset}`);
|
|
633
|
+
const success = await conversation.compact(this.client, model);
|
|
634
|
+
if (success) {
|
|
635
|
+
const info = conversation.getLastCompactionInfo();
|
|
636
|
+
const newEstimate = conversation.estimateNextRequestTokens(systemPrompt, userMessage);
|
|
637
|
+
console.log(`${colors.green}✓ Compacted: ${info?.messagesBefore ?? '?'} messages → summary + ${conversation.getMessageCount()} recent messages. ` +
|
|
638
|
+
`~${((info?.tokensFreed ?? 0) / 1000).toFixed(0)}k tokens freed (${(newEstimate / 1000).toFixed(0)}k / ${(limit / 1000).toFixed(0)}k now).${colors.reset}`);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
console.log(`${colors.dim}[Context] Could not compact further. Proceeding with current context.${colors.reset}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Pillar 4 — proactive context-budget enforcement.
|
|
646
|
+
*
|
|
647
|
+
* Runs the {@link ContextBudgetEnforcer} against the conversation
|
|
648
|
+
* history right before the LLM call. Honours the kill-switch in
|
|
649
|
+
* `preferences.resilience.contextBudget`:
|
|
650
|
+
*
|
|
651
|
+
* - `never` — no-op, returns immediately.
|
|
652
|
+
* - `truncate` — runs the enforcer; if it asks for compaction,
|
|
653
|
+
* we skip the LLM call (the next request will
|
|
654
|
+
* likely 413) and let the caller see a smaller
|
|
655
|
+
* prompt.
|
|
656
|
+
* - `auto` — runs the enforcer; if it asks for compaction,
|
|
657
|
+
* we additionally call `ConversationManager.compact`
|
|
658
|
+
* to summarise the oldest turns via the LLM.
|
|
659
|
+
*
|
|
660
|
+
* Returns a short report so callers can log what happened.
|
|
661
|
+
*/
|
|
662
|
+
async enforceContextBudget(conversation, systemPrompt, userMessage, model) {
|
|
663
|
+
const config = loadConfig();
|
|
664
|
+
const policy = config.preferences.resilience?.contextBudget ?? 'auto';
|
|
665
|
+
if (policy === 'never') {
|
|
666
|
+
return { trimmed: false, compacted: false, tokensAfter: 0 };
|
|
667
|
+
}
|
|
668
|
+
const limit = conversation.getContextLimit();
|
|
669
|
+
const ratio = config.preferences.resilience?.contextBudgetRatio ?? 0.8;
|
|
670
|
+
const maxTokens = Math.max(1, Math.floor(limit * ratio));
|
|
671
|
+
const { trimmed, report } = conversation.enforceBudget(maxTokens, model);
|
|
672
|
+
if (!trimmed) {
|
|
673
|
+
return { trimmed: false, compacted: false, tokensAfter: report.tokensAfter };
|
|
674
|
+
}
|
|
675
|
+
console.log(`${colors.dim}[ContextBudget] ${report.tokensAfter} tokens after ` +
|
|
676
|
+
`${report.actions.join(' → ')} (was ${report.tokensBefore}).${colors.reset}`);
|
|
677
|
+
recordTelemetry(telemetry.contextBudget({
|
|
678
|
+
tokensBefore: report.tokensBefore,
|
|
679
|
+
tokensAfter: report.tokensAfter,
|
|
680
|
+
actions: [...report.actions],
|
|
681
|
+
markedForCompaction: report.markForCompaction,
|
|
682
|
+
}));
|
|
683
|
+
if (report.markForCompaction && policy === 'auto') {
|
|
684
|
+
// Defer to the existing auto-compaction path which produces a
|
|
685
|
+
// structured LLM-generated summary.
|
|
686
|
+
await this.autoCompactIfNeeded(conversation, systemPrompt, userMessage, model);
|
|
687
|
+
const reEstimated = conversation.estimateNextRequestTokens(systemPrompt, userMessage);
|
|
688
|
+
return { trimmed: true, compacted: true, tokensAfter: reEstimated };
|
|
689
|
+
}
|
|
690
|
+
return { trimmed: true, compacted: false, tokensAfter: report.tokensAfter };
|
|
691
|
+
}
|
|
692
|
+
/** Proxy health check passthrough. */
|
|
693
|
+
async ping() {
|
|
694
|
+
return this.client.ping();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
//# sourceMappingURL=single-agent.js.map
|