@build-astron-co/nimbus 0.2.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 (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. package/tsconfig.json +24 -0
@@ -0,0 +1,717 @@
1
+ /**
2
+ * Core Agentic Loop
3
+ *
4
+ * Implements the autonomous agent loop:
5
+ * 1. Build context (system prompt + history + tools)
6
+ * 2. Send to LLM with tools enabled
7
+ * 3. Stream text response
8
+ * 4. If tool_use: check permissions → execute → collect results
9
+ * 5. Append messages → loop back to LLM
10
+ * 6. Exit when LLM returns end_turn (no more tool calls)
11
+ *
12
+ * This is the heart of the Nimbus agent. Every user message enters
13
+ * {@link runAgentLoop}, which orchestrates a multi-turn conversation with
14
+ * the LLM, executing tools on its behalf until it signals completion by
15
+ * returning a response with no further tool calls.
16
+ *
17
+ * @module agent/loop
18
+ */
19
+
20
+ import type { LLMRouter } from '../llm/router';
21
+ import type {
22
+ LLMMessage,
23
+ ToolCall,
24
+ ToolCompletionRequest,
25
+ ToolDefinition as LLMToolDefinition,
26
+ } from '../llm/types';
27
+ import {
28
+ toOpenAITool,
29
+ type ToolDefinition,
30
+ type ToolResult,
31
+ type ToolRegistry,
32
+ } from '../tools/schemas/types';
33
+ import { buildSystemPrompt, type AgentMode } from './system-prompt';
34
+ import type { ContextManager, CompactionResult } from './context-manager';
35
+ import { runCompaction } from './compaction-agent';
36
+ import type { LSPManager } from '../lsp/manager';
37
+ import { SnapshotManager } from '../snapshots/manager';
38
+ import { calculateCost } from '../llm/cost-calculator';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Public Types
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /** Options for running the agent loop. */
45
+ export interface AgentLoopOptions {
46
+ /** The LLM router instance. */
47
+ router: LLMRouter;
48
+
49
+ /** Tool registry with available tools. */
50
+ toolRegistry: ToolRegistry;
51
+
52
+ /** Agent mode (plan/build/deploy). */
53
+ mode: AgentMode;
54
+
55
+ /** Maximum number of LLM turns before stopping (default: 50). */
56
+ maxTurns?: number;
57
+
58
+ /** Model to use (e.g. `'anthropic/claude-sonnet-4-20250514'`). */
59
+ model?: string;
60
+
61
+ /** Current working directory. */
62
+ cwd?: string;
63
+
64
+ /** Custom NIMBUS.md content injected into the system prompt. */
65
+ nimbusInstructions?: string;
66
+
67
+ /** Callback for streaming text output. */
68
+ onText?: (text: string) => void;
69
+
70
+ /** Callback when a tool call starts. */
71
+ onToolCallStart?: (toolCall: ToolCallInfo) => void;
72
+
73
+ /** Callback when a tool call completes. */
74
+ onToolCallEnd?: (toolCall: ToolCallInfo, result: ToolResult) => void;
75
+
76
+ /**
77
+ * Callback to check permission before tool execution.
78
+ * If omitted, all tools are executed without prompting.
79
+ */
80
+ checkPermission?: (tool: ToolDefinition, input: unknown) => Promise<PermissionDecision>;
81
+
82
+ /** AbortSignal for cancellation (Ctrl+C). */
83
+ signal?: AbortSignal;
84
+
85
+ /** Session ID for persistence (reserved for future use). */
86
+ sessionId?: string;
87
+
88
+ /** Optional context manager for auto-compact. When provided, the loop
89
+ * checks context usage after each tool-call turn and triggers
90
+ * compaction if the threshold is exceeded. */
91
+ contextManager?: ContextManager;
92
+
93
+ /** Callback fired when auto-compact is triggered. Receives the
94
+ * compaction result with token savings information. */
95
+ onCompact?: (result: CompactionResult) => void;
96
+
97
+ /** Optional LSP manager for post-edit diagnostics. When provided,
98
+ * the loop queries the language server after file-editing tools
99
+ * and appends any diagnostics to the tool result so the LLM can
100
+ * self-correct type errors and other issues. */
101
+ lspManager?: LSPManager;
102
+
103
+ /** Optional snapshot manager for auto-capture before file-editing tools.
104
+ * When provided, a snapshot is captured before each file-modifying tool
105
+ * call so users can undo/redo changes. */
106
+ snapshotManager?: SnapshotManager;
107
+
108
+ /** Callback fired after each LLM turn with accumulated usage and cost.
109
+ * Allows the TUI to update cost/token display in real-time during
110
+ * multi-turn agent loops, not just at the end. */
111
+ onUsage?: (usage: AgentLoopUsage, costUSD: number) => void;
112
+ }
113
+
114
+ /** Information about a tool call in progress. */
115
+ export interface ToolCallInfo {
116
+ /** Provider-assigned unique ID for this tool call. */
117
+ id: string;
118
+
119
+ /** Tool name as it appears in the registry. */
120
+ name: string;
121
+
122
+ /** Parsed input arguments. */
123
+ input: unknown;
124
+ }
125
+
126
+ /**
127
+ * Result of a permission check.
128
+ *
129
+ * - `allow` -- proceed with execution.
130
+ * - `deny` -- skip this invocation and report denial to the LLM.
131
+ * - `block` -- skip and report that the tool is permanently blocked.
132
+ */
133
+ export type PermissionDecision = 'allow' | 'deny' | 'block';
134
+
135
+ /** Aggregate token usage across all LLM turns. */
136
+ export interface AgentLoopUsage {
137
+ /** Total prompt (input) tokens consumed. */
138
+ promptTokens: number;
139
+
140
+ /** Total completion (output) tokens consumed. */
141
+ completionTokens: number;
142
+
143
+ /** Sum of prompt + completion tokens. */
144
+ totalTokens: number;
145
+ }
146
+
147
+ /** Result of running the agent loop. */
148
+ export interface AgentLoopResult {
149
+ /** The conversation messages after the loop completes. */
150
+ messages: LLMMessage[];
151
+
152
+ /** Number of LLM turns taken. */
153
+ turns: number;
154
+
155
+ /** Whether the loop was interrupted via the AbortSignal. */
156
+ interrupted: boolean;
157
+
158
+ /** Total token usage across all turns. */
159
+ usage: AgentLoopUsage;
160
+
161
+ /** Total estimated cost in USD. */
162
+ totalCost: number;
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Constants
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /** Default model when none is specified. */
170
+ const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';
171
+
172
+ /** Default max output tokens per LLM call. */
173
+ const DEFAULT_MAX_TOKENS = 8192;
174
+
175
+ /** Default maximum number of agent turns. */
176
+ const DEFAULT_MAX_TURNS = 50;
177
+
178
+ /** Maximum characters of tool output to include in conversation history.
179
+ * Anything beyond this is truncated to prevent context window overflow. */
180
+ const MAX_TOOL_OUTPUT_CHARS = 100_000;
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Main Entry Point
184
+ // ---------------------------------------------------------------------------
185
+
186
+ /**
187
+ * Run the agentic loop.
188
+ *
189
+ * Takes a user message and existing conversation history, then runs
190
+ * the LLM in a loop until it stops requesting tool calls.
191
+ *
192
+ * The loop terminates when any of the following conditions are met:
193
+ * - The LLM returns a response with no tool calls (natural end).
194
+ * - The maximum number of turns is reached.
195
+ * - The AbortSignal fires (e.g. user presses Ctrl+C).
196
+ * - An unrecoverable LLM API error occurs.
197
+ *
198
+ * @param userMessage - The new user message to process.
199
+ * @param history - Prior conversation messages (may be empty for a fresh session).
200
+ * @param options - Configuration for the loop.
201
+ * @returns The final conversation state, turn count, usage, and cost.
202
+ */
203
+ export async function runAgentLoop(
204
+ userMessage: string,
205
+ history: LLMMessage[],
206
+ options: AgentLoopOptions
207
+ ): Promise<AgentLoopResult> {
208
+ const {
209
+ router,
210
+ toolRegistry,
211
+ mode,
212
+ maxTurns = DEFAULT_MAX_TURNS,
213
+ model,
214
+ cwd,
215
+ nimbusInstructions,
216
+ onText,
217
+ onToolCallStart,
218
+ onToolCallEnd,
219
+ checkPermission,
220
+ signal,
221
+ } = options;
222
+
223
+ // -----------------------------------------------------------------------
224
+ // 1. Prepare tools and system prompt
225
+ // -----------------------------------------------------------------------
226
+
227
+ const tools = getToolsForMode(toolRegistry.getAll(), mode);
228
+
229
+ const systemPrompt = buildSystemPrompt({
230
+ mode,
231
+ tools,
232
+ nimbusInstructions,
233
+ cwd,
234
+ });
235
+
236
+ // Convert agentic ToolDefinitions to the LLM-level format expected by
237
+ // the router's routeWithTools() method (OpenAI function-calling shape).
238
+ const llmTools: LLMToolDefinition[] = tools.map(toOpenAITool);
239
+
240
+ // -----------------------------------------------------------------------
241
+ // 2. Initialize conversation state
242
+ // -----------------------------------------------------------------------
243
+
244
+ const messages: LLMMessage[] = [...history, { role: 'user', content: userMessage }];
245
+
246
+ let turns = 0;
247
+ let interrupted = false;
248
+ const totalUsage: AgentLoopUsage = {
249
+ promptTokens: 0,
250
+ completionTokens: 0,
251
+ totalTokens: 0,
252
+ };
253
+ let totalCost = 0;
254
+
255
+ // -----------------------------------------------------------------------
256
+ // 3. Main agent loop
257
+ // -----------------------------------------------------------------------
258
+
259
+ while (turns < maxTurns) {
260
+ // Check for cancellation before each turn
261
+ if (signal?.aborted) {
262
+ interrupted = true;
263
+ break;
264
+ }
265
+
266
+ turns++;
267
+
268
+ try {
269
+ // Build the completion request with tool definitions
270
+ const request: ToolCompletionRequest = {
271
+ messages: [{ role: 'system', content: systemPrompt }, ...messages],
272
+ model: model ?? DEFAULT_MODEL,
273
+ tools: llmTools,
274
+ maxTokens: DEFAULT_MAX_TOKENS,
275
+ };
276
+
277
+ // Stream text tokens incrementally via routeStreamWithTools.
278
+ // Tokens are forwarded to onText as they arrive; tool calls
279
+ // are accumulated from the final chunk.
280
+ let responseContent = '';
281
+ let responseToolCalls: ToolCall[] | undefined;
282
+ let responseUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
283
+
284
+ for await (const chunk of router.routeStreamWithTools(request)) {
285
+ if (chunk.content) {
286
+ responseContent += chunk.content;
287
+ if (onText) {
288
+ onText(chunk.content);
289
+ }
290
+ }
291
+ if (chunk.toolCallStart && onText) {
292
+ // Show early feedback when the LLM starts composing a tool call
293
+ onText(`\n[Preparing tool: ${chunk.toolCallStart.name}...]\n`);
294
+ }
295
+ if (chunk.toolCalls) {
296
+ responseToolCalls = chunk.toolCalls;
297
+ }
298
+ if (chunk.usage) {
299
+ responseUsage = chunk.usage;
300
+ }
301
+ }
302
+
303
+ // Accumulate usage and cost
304
+ totalUsage.promptTokens += responseUsage.promptTokens;
305
+ totalUsage.completionTokens += responseUsage.completionTokens;
306
+ totalUsage.totalTokens += responseUsage.totalTokens;
307
+
308
+ // Estimate cost for this turn
309
+ const resolvedModel = model ?? DEFAULT_MODEL;
310
+ const providerName = resolvedModel.includes('/') ? resolvedModel.split('/')[0] : 'anthropic';
311
+ const modelName = resolvedModel.includes('/')
312
+ ? resolvedModel.split('/').slice(1).join('/')
313
+ : resolvedModel;
314
+ const turnCost = calculateCost(
315
+ providerName,
316
+ modelName,
317
+ responseUsage.promptTokens,
318
+ responseUsage.completionTokens
319
+ );
320
+ totalCost += turnCost.costUSD;
321
+
322
+ // Notify caller of accumulated usage/cost after each turn
323
+ if (options.onUsage) {
324
+ options.onUsage(totalUsage, totalCost);
325
+ }
326
+
327
+ // -----------------------------------------------------------------
328
+ // No tool calls → the LLM is done
329
+ // -----------------------------------------------------------------
330
+ if (!responseToolCalls || responseToolCalls.length === 0) {
331
+ messages.push({
332
+ role: 'assistant',
333
+ content: responseContent,
334
+ });
335
+ break;
336
+ }
337
+
338
+ // -----------------------------------------------------------------
339
+ // Tool calls present → execute each one
340
+ // -----------------------------------------------------------------
341
+
342
+ // Append the assistant message that contains the tool calls
343
+ messages.push({
344
+ role: 'assistant',
345
+ content: responseContent,
346
+ toolCalls: responseToolCalls,
347
+ });
348
+
349
+ // Process tool calls sequentially (order may matter for side effects)
350
+ for (const toolCall of responseToolCalls) {
351
+ // Check for cancellation between tool calls
352
+ if (signal?.aborted) {
353
+ interrupted = true;
354
+ break;
355
+ }
356
+
357
+ const result = await executeToolCall(
358
+ toolCall,
359
+ toolRegistry,
360
+ onToolCallStart,
361
+ onToolCallEnd,
362
+ checkPermission,
363
+ options.lspManager,
364
+ options.snapshotManager,
365
+ options.sessionId,
366
+ signal
367
+ );
368
+
369
+ // Append each tool result as a separate message so the LLM can
370
+ // match it to the corresponding tool_use block by toolCallId.
371
+ let toolContent = result.isError ? `Error: ${result.error}` : result.output;
372
+
373
+ // Truncate excessively large tool outputs to prevent context overflow
374
+ if (toolContent.length > MAX_TOOL_OUTPUT_CHARS) {
375
+ const truncatedLength = toolContent.length;
376
+ toolContent = `${toolContent.slice(0, MAX_TOOL_OUTPUT_CHARS)}\n\n... [Output truncated: ${truncatedLength.toLocaleString()} chars total, showing first ${MAX_TOOL_OUTPUT_CHARS.toLocaleString()}]`;
377
+ }
378
+
379
+ messages.push({
380
+ role: 'tool',
381
+ toolCallId: toolCall.id,
382
+ name: toolCall.function.name,
383
+ content: toolContent,
384
+ });
385
+ }
386
+
387
+ // If we broke out of the tool-call loop due to cancellation, exit
388
+ // the main loop as well.
389
+ if (interrupted) {
390
+ break;
391
+ }
392
+
393
+ // -----------------------------------------------------------------
394
+ // Auto-compact check
395
+ // -----------------------------------------------------------------
396
+ // After tool results are appended, check whether the conversation
397
+ // has grown past the context window threshold. If so, summarize
398
+ // older messages to free up space for future turns.
399
+ if (options.contextManager) {
400
+ const toolTokens = llmTools.reduce(
401
+ (sum, t) => sum + Math.ceil(JSON.stringify(t).length / 4),
402
+ 0
403
+ );
404
+ if (options.contextManager.shouldCompact(systemPrompt, messages, toolTokens)) {
405
+ try {
406
+ const compactResult = await runCompaction(messages, options.contextManager, { router });
407
+ // Replace messages with the compacted version
408
+ messages.length = 0;
409
+ messages.push(...compactResult.messages);
410
+ if (options.onCompact) {
411
+ options.onCompact(compactResult.result);
412
+ }
413
+ } catch (compactErr) {
414
+ // Compaction failed — notify user visibly and continue with original messages
415
+ const compactErrMsg =
416
+ compactErr instanceof Error ? compactErr.message : String(compactErr);
417
+ if (onText) {
418
+ onText(
419
+ `\n[Warning: Auto-compaction failed: ${compactErrMsg}. Context may exceed budget on the next turn.]\n`
420
+ );
421
+ }
422
+ }
423
+ }
424
+ }
425
+ } catch (error: unknown) {
426
+ // LLM API error — report to the caller and break
427
+ const msg = error instanceof Error ? error.message : String(error);
428
+ if (onText) {
429
+ onText(`\n[Error: ${msg}]\n`);
430
+ }
431
+ messages.push({
432
+ role: 'assistant',
433
+ content: `I encountered an error: ${msg}`,
434
+ });
435
+ break;
436
+ }
437
+ }
438
+
439
+ // -----------------------------------------------------------------------
440
+ // 4. Post-loop bookkeeping
441
+ // -----------------------------------------------------------------------
442
+
443
+ if (turns >= maxTurns && !interrupted) {
444
+ if (onText) {
445
+ onText(`\n[Agent reached maximum turns limit (${maxTurns}). Stopping.]\n`);
446
+ }
447
+ }
448
+
449
+ return {
450
+ messages,
451
+ turns,
452
+ interrupted,
453
+ usage: totalUsage,
454
+ totalCost,
455
+ };
456
+ }
457
+
458
+ // ---------------------------------------------------------------------------
459
+ // Tool Execution
460
+ // ---------------------------------------------------------------------------
461
+
462
+ /** Tools that modify files and should trigger LSP diagnostics. */
463
+ const FILE_EDITING_TOOLS = new Set(['edit_file', 'multi_edit', 'write_file']);
464
+
465
+ /**
466
+ * Extract the file path from a tool call's parsed arguments.
467
+ *
468
+ * File-editing tools all have a `path` parameter that identifies
469
+ * the target file. Returns `null` for non-file tools.
470
+ */
471
+ function extractFilePath(toolName: string, input: unknown): string | null {
472
+ if (!FILE_EDITING_TOOLS.has(toolName)) {
473
+ return null;
474
+ }
475
+ if (input && typeof input === 'object' && 'path' in input) {
476
+ return (input as { path: string }).path;
477
+ }
478
+ return null;
479
+ }
480
+
481
+ /**
482
+ * Execute a single tool call.
483
+ *
484
+ * Handles:
485
+ * - Looking up the tool in the registry.
486
+ * - Parsing the JSON arguments string from the LLM response.
487
+ * - Validating input against the Zod schema.
488
+ * - Checking permissions via the caller-supplied callback.
489
+ * - Invoking the tool and returning the result.
490
+ * - Notifying start/end callbacks.
491
+ * - Querying the LSP for diagnostics after file edits.
492
+ *
493
+ * @param toolCall - The raw tool call from the LLM response.
494
+ * @param registry - The tool registry to look up the tool definition.
495
+ * @param onStart - Optional callback fired before execution.
496
+ * @param onEnd - Optional callback fired after execution (or error).
497
+ * @param checkPermission - Optional permission gate.
498
+ * @param lspManager - Optional LSP manager for post-edit diagnostics.
499
+ * @returns The tool result (always succeeds; errors are captured inside the result).
500
+ */
501
+ async function executeToolCall(
502
+ toolCall: ToolCall,
503
+ registry: ToolRegistry,
504
+ onStart?: (info: ToolCallInfo) => void,
505
+ onEnd?: (info: ToolCallInfo, result: ToolResult) => void,
506
+ checkPermission?: (tool: ToolDefinition, input: unknown) => Promise<PermissionDecision>,
507
+ lspManager?: LSPManager,
508
+ snapshotManager?: SnapshotManager,
509
+ sessionId?: string,
510
+ signal?: AbortSignal
511
+ ): Promise<ToolResult> {
512
+ const toolName = toolCall.function.name;
513
+
514
+ // Parse the JSON arguments string from the LLM
515
+ let parsedArgs: unknown;
516
+ try {
517
+ parsedArgs = JSON.parse(toolCall.function.arguments);
518
+ } catch {
519
+ const result: ToolResult = {
520
+ output: '',
521
+ error: `Failed to parse tool arguments as JSON for '${toolName}': ${toolCall.function.arguments}`,
522
+ isError: true,
523
+ };
524
+ return result;
525
+ }
526
+
527
+ const callInfo: ToolCallInfo = {
528
+ id: toolCall.id,
529
+ name: toolName,
530
+ input: parsedArgs,
531
+ };
532
+
533
+ // Look up the tool definition
534
+ const tool = registry.get(toolName);
535
+ if (!tool) {
536
+ const result: ToolResult = {
537
+ output: '',
538
+ error: `Unknown tool: ${toolName}`,
539
+ isError: true,
540
+ };
541
+ if (onEnd) {
542
+ onEnd(callInfo, result);
543
+ }
544
+ return result;
545
+ }
546
+
547
+ // Notify start
548
+ if (onStart) {
549
+ onStart(callInfo);
550
+ }
551
+
552
+ // Permission check
553
+ if (checkPermission) {
554
+ const decision = await checkPermission(tool, parsedArgs);
555
+ if (decision === 'deny' || decision === 'block') {
556
+ const result: ToolResult = {
557
+ output: '',
558
+ error:
559
+ decision === 'block'
560
+ ? `Tool '${toolName}' is blocked by permission policy.`
561
+ : `User denied permission for tool '${toolName}'.`,
562
+ isError: true,
563
+ };
564
+ if (onEnd) {
565
+ onEnd(callInfo, result);
566
+ }
567
+ return result;
568
+ }
569
+ }
570
+
571
+ // Capture snapshot before file-modifying tools for undo/redo support
572
+ if (
573
+ snapshotManager &&
574
+ SnapshotManager.shouldSnapshot(toolName, parsedArgs as Record<string, unknown>)
575
+ ) {
576
+ try {
577
+ await snapshotManager.captureSnapshot({
578
+ sessionId: sessionId || 'default',
579
+ messageId: toolCall.id,
580
+ toolCallId: toolCall.id,
581
+ description: `${toolName}: ${extractFilePath(toolName, parsedArgs) || '(bash command)'}`,
582
+ });
583
+ } catch {
584
+ // Snapshot failure should never block the tool call
585
+ }
586
+ }
587
+
588
+ // Validate input against the tool's Zod schema and execute
589
+ let result: ToolResult;
590
+ try {
591
+ const validatedInput = tool.inputSchema.parse(parsedArgs);
592
+
593
+ // Thread AbortSignal into bash tool for Ctrl+C child process killing
594
+ if (signal && toolName === 'bash' && validatedInput && typeof validatedInput === 'object') {
595
+ (validatedInput as Record<string, unknown>)._signal = signal;
596
+ }
597
+
598
+ result = await tool.execute(validatedInput);
599
+ } catch (error: unknown) {
600
+ const msg = error instanceof Error ? error.message : String(error);
601
+ result = {
602
+ output: '',
603
+ error: `Tool execution failed: ${msg}`,
604
+ isError: true,
605
+ };
606
+ }
607
+
608
+ // -----------------------------------------------------------------------
609
+ // LSP diagnostics injection
610
+ // -----------------------------------------------------------------------
611
+ // After a successful file edit, notify the language server and collect
612
+ // any diagnostics (type errors, lint issues). If errors exist they are
613
+ // appended to the tool output so the LLM sees them on its next turn
614
+ // and can self-correct.
615
+ if (lspManager && !result.isError) {
616
+ const filePath = extractFilePath(toolName, parsedArgs);
617
+ if (filePath) {
618
+ try {
619
+ await lspManager.touchFile(filePath);
620
+ const diagnostics = await lspManager.getDiagnostics(filePath);
621
+ if (diagnostics.length > 0) {
622
+ const formatted = lspManager.formatDiagnosticsForAgent(diagnostics);
623
+ if (formatted) {
624
+ result = {
625
+ ...result,
626
+ output: result.output ? `${result.output}\n\n${formatted}` : formatted,
627
+ };
628
+ }
629
+ }
630
+ } catch (lspErr) {
631
+ // LSP errors should never block the agent loop.
632
+ // Append a note to the tool result so the LLM (and user) can see it.
633
+ const lspErrMsg = lspErr instanceof Error ? lspErr.message : String(lspErr);
634
+ result = {
635
+ ...result,
636
+ output: result.output
637
+ ? `${result.output}\n\n[Note: LSP diagnostics unavailable: ${lspErrMsg}]`
638
+ : `[Note: LSP diagnostics unavailable: ${lspErrMsg}]`,
639
+ };
640
+ }
641
+ }
642
+ }
643
+
644
+ // Notify end
645
+ if (onEnd) {
646
+ onEnd(callInfo, result);
647
+ }
648
+
649
+ return result;
650
+ }
651
+
652
+ // ---------------------------------------------------------------------------
653
+ // Mode-Based Tool Filtering
654
+ // ---------------------------------------------------------------------------
655
+
656
+ /**
657
+ * Set of tool names allowed in `plan` mode.
658
+ *
659
+ * Plan mode is strictly read-only: the agent can inspect files, search
660
+ * the codebase, read tasks, estimate costs, and detect drift -- but it
661
+ * cannot write files, run commands, or mutate infrastructure.
662
+ */
663
+ const PLAN_MODE_TOOLS = new Set([
664
+ 'read_file',
665
+ 'glob',
666
+ 'grep',
667
+ 'list_dir',
668
+ 'webfetch',
669
+ 'todo_read',
670
+ 'todo_write',
671
+ 'task',
672
+ 'cost_estimate',
673
+ 'drift_detect',
674
+ 'cloud_discover',
675
+ ]);
676
+
677
+ /**
678
+ * Set of tool names blocked in `build` mode.
679
+ *
680
+ * Build mode allows reads and writes (file edits, code generation) but
681
+ * blocks infrastructure-mutating operations that could affect live
682
+ * environments. The permission engine provides fine-grained control on
683
+ * top of this coarse filter.
684
+ */
685
+ const BUILD_MODE_BLOCKED_TOOLS = new Set(['terraform', 'kubectl', 'helm']);
686
+
687
+ /**
688
+ * Filter tools based on the current agent mode.
689
+ *
690
+ * - **plan**: Only read-only tools + cost/drift analysis.
691
+ * - **build**: All tools except infrastructure mutation commands.
692
+ * - **deploy**: All tools are available.
693
+ *
694
+ * @param allTools - Every tool registered in the system.
695
+ * @param mode - The active agent mode.
696
+ * @returns The subset of tools available in the given mode.
697
+ */
698
+ export function getToolsForMode(allTools: ToolDefinition[], mode: AgentMode): ToolDefinition[] {
699
+ switch (mode) {
700
+ case 'plan':
701
+ return allTools.filter(t => PLAN_MODE_TOOLS.has(t.name));
702
+
703
+ case 'build':
704
+ return allTools.filter(t => !BUILD_MODE_BLOCKED_TOOLS.has(t.name));
705
+
706
+ case 'deploy':
707
+ // All tools available
708
+ return allTools;
709
+
710
+ default: {
711
+ // Exhaustive check -- if a new mode is added this becomes a compile
712
+ // error (assuming AgentMode is a union type).
713
+ const _exhaustive: never = mode;
714
+ return allTools;
715
+ }
716
+ }
717
+ }