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.
Files changed (303) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +530 -0
  3. package/dist/agent/agent-client.d.ts +108 -0
  4. package/dist/agent/agent-client.d.ts.map +1 -0
  5. package/dist/agent/agent-client.js +1247 -0
  6. package/dist/agent/agent-client.js.map +1 -0
  7. package/dist/agent/agent-pool.d.ts +20 -0
  8. package/dist/agent/agent-pool.d.ts.map +1 -0
  9. package/dist/agent/agent-pool.js +217 -0
  10. package/dist/agent/agent-pool.js.map +1 -0
  11. package/dist/agent/background-awareness.d.ts +55 -0
  12. package/dist/agent/background-awareness.d.ts.map +1 -0
  13. package/dist/agent/background-awareness.js +104 -0
  14. package/dist/agent/background-awareness.js.map +1 -0
  15. package/dist/agent/command-parser.d.ts +33 -0
  16. package/dist/agent/command-parser.d.ts.map +1 -0
  17. package/dist/agent/command-parser.js +120 -0
  18. package/dist/agent/command-parser.js.map +1 -0
  19. package/dist/agent/context-budget.d.ts +91 -0
  20. package/dist/agent/context-budget.d.ts.map +1 -0
  21. package/dist/agent/context-budget.js +219 -0
  22. package/dist/agent/context-budget.js.map +1 -0
  23. package/dist/agent/conversation.d.ts +190 -0
  24. package/dist/agent/conversation.d.ts.map +1 -0
  25. package/dist/agent/conversation.js +547 -0
  26. package/dist/agent/conversation.js.map +1 -0
  27. package/dist/agent/hooks.d.ts +72 -0
  28. package/dist/agent/hooks.d.ts.map +1 -0
  29. package/dist/agent/hooks.js +214 -0
  30. package/dist/agent/hooks.js.map +1 -0
  31. package/dist/agent/mcp-bridge.d.ts +13 -0
  32. package/dist/agent/mcp-bridge.d.ts.map +1 -0
  33. package/dist/agent/mcp-bridge.js +86 -0
  34. package/dist/agent/mcp-bridge.js.map +1 -0
  35. package/dist/agent/mcp-client.d.ts +24 -0
  36. package/dist/agent/mcp-client.d.ts.map +1 -0
  37. package/dist/agent/mcp-client.js +146 -0
  38. package/dist/agent/mcp-client.js.map +1 -0
  39. package/dist/agent/mcp-manager.d.ts +13 -0
  40. package/dist/agent/mcp-manager.d.ts.map +1 -0
  41. package/dist/agent/mcp-manager.js +84 -0
  42. package/dist/agent/mcp-manager.js.map +1 -0
  43. package/dist/agent/mcp-registry.d.ts +45 -0
  44. package/dist/agent/mcp-registry.d.ts.map +1 -0
  45. package/dist/agent/mcp-registry.js +98 -0
  46. package/dist/agent/mcp-registry.js.map +1 -0
  47. package/dist/agent/orchestrator.d.ts +14 -0
  48. package/dist/agent/orchestrator.d.ts.map +1 -0
  49. package/dist/agent/orchestrator.js +118 -0
  50. package/dist/agent/orchestrator.js.map +1 -0
  51. package/dist/agent/parser-adapter.d.ts +120 -0
  52. package/dist/agent/parser-adapter.d.ts.map +1 -0
  53. package/dist/agent/parser-adapter.js +265 -0
  54. package/dist/agent/parser-adapter.js.map +1 -0
  55. package/dist/agent/parsers/imports.d.ts +11 -0
  56. package/dist/agent/parsers/imports.d.ts.map +1 -0
  57. package/dist/agent/parsers/imports.js +94 -0
  58. package/dist/agent/parsers/imports.js.map +1 -0
  59. package/dist/agent/parsers/shell.d.ts +23 -0
  60. package/dist/agent/parsers/shell.d.ts.map +1 -0
  61. package/dist/agent/parsers/shell.js +200 -0
  62. package/dist/agent/parsers/shell.js.map +1 -0
  63. package/dist/agent/parsers/symbols.d.ts +17 -0
  64. package/dist/agent/parsers/symbols.d.ts.map +1 -0
  65. package/dist/agent/parsers/symbols.js +103 -0
  66. package/dist/agent/parsers/symbols.js.map +1 -0
  67. package/dist/agent/permissions.d.ts +65 -0
  68. package/dist/agent/permissions.d.ts.map +1 -0
  69. package/dist/agent/permissions.js +219 -0
  70. package/dist/agent/permissions.js.map +1 -0
  71. package/dist/agent/predictive-gate.d.ts +69 -0
  72. package/dist/agent/predictive-gate.d.ts.map +1 -0
  73. package/dist/agent/predictive-gate.js +128 -0
  74. package/dist/agent/predictive-gate.js.map +1 -0
  75. package/dist/agent/provider-cooldown.d.ts +144 -0
  76. package/dist/agent/provider-cooldown.d.ts.map +1 -0
  77. package/dist/agent/provider-cooldown.js +300 -0
  78. package/dist/agent/provider-cooldown.js.map +1 -0
  79. package/dist/agent/providers-manager.d.ts +109 -0
  80. package/dist/agent/providers-manager.d.ts.map +1 -0
  81. package/dist/agent/providers-manager.js +464 -0
  82. package/dist/agent/providers-manager.js.map +1 -0
  83. package/dist/agent/repo-map.d.ts +6 -0
  84. package/dist/agent/repo-map.d.ts.map +1 -0
  85. package/dist/agent/repo-map.js +221 -0
  86. package/dist/agent/repo-map.js.map +1 -0
  87. package/dist/agent/retry.d.ts +103 -0
  88. package/dist/agent/retry.d.ts.map +1 -0
  89. package/dist/agent/retry.js +276 -0
  90. package/dist/agent/retry.js.map +1 -0
  91. package/dist/agent/search/index.d.ts +61 -0
  92. package/dist/agent/search/index.d.ts.map +1 -0
  93. package/dist/agent/search/index.js +314 -0
  94. package/dist/agent/search/index.js.map +1 -0
  95. package/dist/agent/single-agent.d.ts +76 -0
  96. package/dist/agent/single-agent.d.ts.map +1 -0
  97. package/dist/agent/single-agent.js +697 -0
  98. package/dist/agent/single-agent.js.map +1 -0
  99. package/dist/agent/skills.d.ts +22 -0
  100. package/dist/agent/skills.d.ts.map +1 -0
  101. package/dist/agent/skills.js +139 -0
  102. package/dist/agent/skills.js.map +1 -0
  103. package/dist/agent/stream-glue.d.ts +85 -0
  104. package/dist/agent/stream-glue.d.ts.map +1 -0
  105. package/dist/agent/stream-glue.js +120 -0
  106. package/dist/agent/stream-glue.js.map +1 -0
  107. package/dist/agent/subagent.d.ts +72 -0
  108. package/dist/agent/subagent.d.ts.map +1 -0
  109. package/dist/agent/subagent.js +193 -0
  110. package/dist/agent/subagent.js.map +1 -0
  111. package/dist/agent/telemetry.d.ts +192 -0
  112. package/dist/agent/telemetry.d.ts.map +1 -0
  113. package/dist/agent/telemetry.js +400 -0
  114. package/dist/agent/telemetry.js.map +1 -0
  115. package/dist/agent/tokenizer.d.ts +42 -0
  116. package/dist/agent/tokenizer.d.ts.map +1 -0
  117. package/dist/agent/tokenizer.js +107 -0
  118. package/dist/agent/tokenizer.js.map +1 -0
  119. package/dist/agent/tool-executor.d.ts +289 -0
  120. package/dist/agent/tool-executor.d.ts.map +1 -0
  121. package/dist/agent/tool-executor.js +2519 -0
  122. package/dist/agent/tool-executor.js.map +1 -0
  123. package/dist/agent/web-impl.d.ts +2 -0
  124. package/dist/agent/web-impl.d.ts.map +1 -0
  125. package/dist/agent/web-impl.js +34 -0
  126. package/dist/agent/web-impl.js.map +1 -0
  127. package/dist/agent/web.d.ts +8 -0
  128. package/dist/agent/web.d.ts.map +1 -0
  129. package/dist/agent/web.js +8 -0
  130. package/dist/agent/web.js.map +1 -0
  131. package/dist/agent/worker-agent.d.ts +27 -0
  132. package/dist/agent/worker-agent.d.ts.map +1 -0
  133. package/dist/agent/worker-agent.js +503 -0
  134. package/dist/agent/worker-agent.js.map +1 -0
  135. package/dist/config.d.ts +162 -0
  136. package/dist/config.d.ts.map +1 -0
  137. package/dist/config.js +138 -0
  138. package/dist/config.js.map +1 -0
  139. package/dist/context/fixo-md-watcher.d.ts +42 -0
  140. package/dist/context/fixo-md-watcher.d.ts.map +1 -0
  141. package/dist/context/fixo-md-watcher.js +126 -0
  142. package/dist/context/fixo-md-watcher.js.map +1 -0
  143. package/dist/context/fixo-md.d.ts +50 -0
  144. package/dist/context/fixo-md.d.ts.map +1 -0
  145. package/dist/context/fixo-md.js +118 -0
  146. package/dist/context/fixo-md.js.map +1 -0
  147. package/dist/context/todo.d.ts +65 -0
  148. package/dist/context/todo.d.ts.map +1 -0
  149. package/dist/context/todo.js +194 -0
  150. package/dist/context/todo.js.map +1 -0
  151. package/dist/git/git-manager.d.ts +33 -0
  152. package/dist/git/git-manager.d.ts.map +1 -0
  153. package/dist/git/git-manager.js +293 -0
  154. package/dist/git/git-manager.js.map +1 -0
  155. package/dist/git/git-ops.d.ts +10 -0
  156. package/dist/git/git-ops.d.ts.map +1 -0
  157. package/dist/git/git-ops.js +131 -0
  158. package/dist/git/git-ops.js.map +1 -0
  159. package/dist/index.d.ts +3 -0
  160. package/dist/index.d.ts.map +1 -0
  161. package/dist/index.js +352 -0
  162. package/dist/index.js.map +1 -0
  163. package/dist/indexer.d.ts +30 -0
  164. package/dist/indexer.d.ts.map +1 -0
  165. package/dist/indexer.js +273 -0
  166. package/dist/indexer.js.map +1 -0
  167. package/dist/lsp/lsp-client.d.ts +24 -0
  168. package/dist/lsp/lsp-client.d.ts.map +1 -0
  169. package/dist/lsp/lsp-client.js +205 -0
  170. package/dist/lsp/lsp-client.js.map +1 -0
  171. package/dist/lsp/lsp-manager.d.ts +17 -0
  172. package/dist/lsp/lsp-manager.d.ts.map +1 -0
  173. package/dist/lsp/lsp-manager.js +154 -0
  174. package/dist/lsp/lsp-manager.js.map +1 -0
  175. package/dist/lsp/lsp-pre-save.d.ts +137 -0
  176. package/dist/lsp/lsp-pre-save.d.ts.map +1 -0
  177. package/dist/lsp/lsp-pre-save.js +245 -0
  178. package/dist/lsp/lsp-pre-save.js.map +1 -0
  179. package/dist/lsp/syntax-fallback.d.ts +83 -0
  180. package/dist/lsp/syntax-fallback.d.ts.map +1 -0
  181. package/dist/lsp/syntax-fallback.js +275 -0
  182. package/dist/lsp/syntax-fallback.js.map +1 -0
  183. package/dist/model-outcomes.d.ts +12 -0
  184. package/dist/model-outcomes.d.ts.map +1 -0
  185. package/dist/model-outcomes.js +46 -0
  186. package/dist/model-outcomes.js.map +1 -0
  187. package/dist/planner.d.ts +32 -0
  188. package/dist/planner.d.ts.map +1 -0
  189. package/dist/planner.js +163 -0
  190. package/dist/planner.js.map +1 -0
  191. package/dist/project-memory.d.ts +29 -0
  192. package/dist/project-memory.d.ts.map +1 -0
  193. package/dist/project-memory.js +349 -0
  194. package/dist/project-memory.js.map +1 -0
  195. package/dist/review.d.ts +2 -0
  196. package/dist/review.d.ts.map +1 -0
  197. package/dist/review.js +61 -0
  198. package/dist/review.js.map +1 -0
  199. package/dist/runtime/background-jobs.d.ts +97 -0
  200. package/dist/runtime/background-jobs.d.ts.map +1 -0
  201. package/dist/runtime/background-jobs.js +331 -0
  202. package/dist/runtime/background-jobs.js.map +1 -0
  203. package/dist/runtime/credential-vault.d.ts +124 -0
  204. package/dist/runtime/credential-vault.d.ts.map +1 -0
  205. package/dist/runtime/credential-vault.js +184 -0
  206. package/dist/runtime/credential-vault.js.map +1 -0
  207. package/dist/runtime/loop-trap.d.ts +197 -0
  208. package/dist/runtime/loop-trap.d.ts.map +1 -0
  209. package/dist/runtime/loop-trap.js +420 -0
  210. package/dist/runtime/loop-trap.js.map +1 -0
  211. package/dist/runtime/policy.d.ts +15 -0
  212. package/dist/runtime/policy.d.ts.map +1 -0
  213. package/dist/runtime/policy.js +60 -0
  214. package/dist/runtime/policy.js.map +1 -0
  215. package/dist/runtime/redaction.d.ts +66 -0
  216. package/dist/runtime/redaction.d.ts.map +1 -0
  217. package/dist/runtime/redaction.js +155 -0
  218. package/dist/runtime/redaction.js.map +1 -0
  219. package/dist/runtime/session-snapshots.d.ts +76 -0
  220. package/dist/runtime/session-snapshots.d.ts.map +1 -0
  221. package/dist/runtime/session-snapshots.js +166 -0
  222. package/dist/runtime/session-snapshots.js.map +1 -0
  223. package/dist/runtime/staging.d.ts +205 -0
  224. package/dist/runtime/staging.d.ts.map +1 -0
  225. package/dist/runtime/staging.js +526 -0
  226. package/dist/runtime/staging.js.map +1 -0
  227. package/dist/runtime/task-session.d.ts +95 -0
  228. package/dist/runtime/task-session.d.ts.map +1 -0
  229. package/dist/runtime/task-session.js +263 -0
  230. package/dist/runtime/task-session.js.map +1 -0
  231. package/dist/runtime/worktree.d.ts +55 -0
  232. package/dist/runtime/worktree.d.ts.map +1 -0
  233. package/dist/runtime/worktree.js +175 -0
  234. package/dist/runtime/worktree.js.map +1 -0
  235. package/dist/setup-wizard.d.ts +8 -0
  236. package/dist/setup-wizard.d.ts.map +1 -0
  237. package/dist/setup-wizard.js +73 -0
  238. package/dist/setup-wizard.js.map +1 -0
  239. package/dist/shared/content.d.ts +43 -0
  240. package/dist/shared/content.d.ts.map +1 -0
  241. package/dist/shared/content.js +61 -0
  242. package/dist/shared/content.js.map +1 -0
  243. package/dist/shared/types.d.ts +217 -0
  244. package/dist/shared/types.d.ts.map +1 -0
  245. package/dist/shared/types.js +3 -0
  246. package/dist/shared/types.js.map +1 -0
  247. package/dist/test-runner.d.ts +5 -0
  248. package/dist/test-runner.d.ts.map +1 -0
  249. package/dist/test-runner.js +42 -0
  250. package/dist/test-runner.js.map +1 -0
  251. package/dist/types.d.ts +85 -0
  252. package/dist/types.d.ts.map +1 -0
  253. package/dist/types.js +2 -0
  254. package/dist/types.js.map +1 -0
  255. package/dist/ui/ascii.d.ts +23 -0
  256. package/dist/ui/ascii.d.ts.map +1 -0
  257. package/dist/ui/ascii.js +45 -0
  258. package/dist/ui/ascii.js.map +1 -0
  259. package/dist/ui/colors.d.ts +111 -0
  260. package/dist/ui/colors.d.ts.map +1 -0
  261. package/dist/ui/colors.js +166 -0
  262. package/dist/ui/colors.js.map +1 -0
  263. package/dist/ui/image-attach.d.ts +27 -0
  264. package/dist/ui/image-attach.d.ts.map +1 -0
  265. package/dist/ui/image-attach.js +100 -0
  266. package/dist/ui/image-attach.js.map +1 -0
  267. package/dist/ui/index.d.ts +18 -0
  268. package/dist/ui/index.d.ts.map +1 -0
  269. package/dist/ui/index.js +18 -0
  270. package/dist/ui/index.js.map +1 -0
  271. package/dist/ui/markdown-stream.d.ts +91 -0
  272. package/dist/ui/markdown-stream.d.ts.map +1 -0
  273. package/dist/ui/markdown-stream.js +524 -0
  274. package/dist/ui/markdown-stream.js.map +1 -0
  275. package/dist/ui/plan-renderer.d.ts +36 -0
  276. package/dist/ui/plan-renderer.d.ts.map +1 -0
  277. package/dist/ui/plan-renderer.js +79 -0
  278. package/dist/ui/plan-renderer.js.map +1 -0
  279. package/dist/ui/prompt.d.ts +11 -0
  280. package/dist/ui/prompt.d.ts.map +1 -0
  281. package/dist/ui/prompt.js +1960 -0
  282. package/dist/ui/prompt.js.map +1 -0
  283. package/dist/ui/render-primitives.d.ts +117 -0
  284. package/dist/ui/render-primitives.d.ts.map +1 -0
  285. package/dist/ui/render-primitives.js +322 -0
  286. package/dist/ui/render-primitives.js.map +1 -0
  287. package/dist/ui/render.d.ts +133 -0
  288. package/dist/ui/render.d.ts.map +1 -0
  289. package/dist/ui/render.js +547 -0
  290. package/dist/ui/render.js.map +1 -0
  291. package/dist/ui/session-header.d.ts +30 -0
  292. package/dist/ui/session-header.d.ts.map +1 -0
  293. package/dist/ui/session-header.js +74 -0
  294. package/dist/ui/session-header.js.map +1 -0
  295. package/dist/workspace-guard.d.ts +68 -0
  296. package/dist/workspace-guard.d.ts.map +1 -0
  297. package/dist/workspace-guard.js +168 -0
  298. package/dist/workspace-guard.js.map +1 -0
  299. package/dist/workspace-lock.d.ts +27 -0
  300. package/dist/workspace-lock.d.ts.map +1 -0
  301. package/dist/workspace-lock.js +95 -0
  302. package/dist/workspace-lock.js.map +1 -0
  303. 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