whale-code 6.4.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 (319) hide show
  1. package/README.md +95 -0
  2. package/bin/swag-agent.js +9 -0
  3. package/bin/swagmanager-mcp.js +321 -0
  4. package/dist/cli/app.d.ts +26 -0
  5. package/dist/cli/app.js +64 -0
  6. package/dist/cli/chat/AgentSelector.d.ts +14 -0
  7. package/dist/cli/chat/AgentSelector.js +14 -0
  8. package/dist/cli/chat/ChatApp.d.ts +9 -0
  9. package/dist/cli/chat/ChatApp.js +267 -0
  10. package/dist/cli/chat/ChatInput.d.ts +39 -0
  11. package/dist/cli/chat/ChatInput.js +509 -0
  12. package/dist/cli/chat/MarkdownText.d.ts +10 -0
  13. package/dist/cli/chat/MarkdownText.js +20 -0
  14. package/dist/cli/chat/MessageList.d.ts +37 -0
  15. package/dist/cli/chat/MessageList.js +80 -0
  16. package/dist/cli/chat/ModelSelector.d.ts +20 -0
  17. package/dist/cli/chat/ModelSelector.js +73 -0
  18. package/dist/cli/chat/RewindViewer.d.ts +26 -0
  19. package/dist/cli/chat/RewindViewer.js +185 -0
  20. package/dist/cli/chat/StoreSelector.d.ts +14 -0
  21. package/dist/cli/chat/StoreSelector.js +24 -0
  22. package/dist/cli/chat/StreamingText.d.ts +12 -0
  23. package/dist/cli/chat/StreamingText.js +12 -0
  24. package/dist/cli/chat/SubagentPanel.d.ts +45 -0
  25. package/dist/cli/chat/SubagentPanel.js +110 -0
  26. package/dist/cli/chat/TeamPanel.d.ts +21 -0
  27. package/dist/cli/chat/TeamPanel.js +42 -0
  28. package/dist/cli/chat/ToolIndicator.d.ts +25 -0
  29. package/dist/cli/chat/ToolIndicator.js +436 -0
  30. package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
  31. package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
  32. package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
  33. package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
  34. package/dist/cli/commands/config-cmd.d.ts +10 -0
  35. package/dist/cli/commands/config-cmd.js +99 -0
  36. package/dist/cli/commands/doctor.d.ts +14 -0
  37. package/dist/cli/commands/doctor.js +172 -0
  38. package/dist/cli/commands/init.d.ts +16 -0
  39. package/dist/cli/commands/init.js +278 -0
  40. package/dist/cli/commands/mcp.d.ts +12 -0
  41. package/dist/cli/commands/mcp.js +162 -0
  42. package/dist/cli/login/LoginApp.d.ts +7 -0
  43. package/dist/cli/login/LoginApp.js +157 -0
  44. package/dist/cli/print-mode.d.ts +31 -0
  45. package/dist/cli/print-mode.js +202 -0
  46. package/dist/cli/serve-mode.d.ts +37 -0
  47. package/dist/cli/serve-mode.js +636 -0
  48. package/dist/cli/services/agent-definitions.d.ts +25 -0
  49. package/dist/cli/services/agent-definitions.js +91 -0
  50. package/dist/cli/services/agent-events.d.ts +178 -0
  51. package/dist/cli/services/agent-events.js +175 -0
  52. package/dist/cli/services/agent-loop.d.ts +90 -0
  53. package/dist/cli/services/agent-loop.js +762 -0
  54. package/dist/cli/services/agent-worker-base.d.ts +97 -0
  55. package/dist/cli/services/agent-worker-base.js +220 -0
  56. package/dist/cli/services/auth-service.d.ts +30 -0
  57. package/dist/cli/services/auth-service.js +160 -0
  58. package/dist/cli/services/background-processes.d.ts +126 -0
  59. package/dist/cli/services/background-processes.js +318 -0
  60. package/dist/cli/services/browser-auth.d.ts +24 -0
  61. package/dist/cli/services/browser-auth.js +180 -0
  62. package/dist/cli/services/claude-md-loader.d.ts +16 -0
  63. package/dist/cli/services/claude-md-loader.js +58 -0
  64. package/dist/cli/services/config-store.d.ts +47 -0
  65. package/dist/cli/services/config-store.js +79 -0
  66. package/dist/cli/services/debug-log.d.ts +10 -0
  67. package/dist/cli/services/debug-log.js +52 -0
  68. package/dist/cli/services/error-logger.d.ts +58 -0
  69. package/dist/cli/services/error-logger.js +269 -0
  70. package/dist/cli/services/file-history.d.ts +21 -0
  71. package/dist/cli/services/file-history.js +83 -0
  72. package/dist/cli/services/format-server-response.d.ts +16 -0
  73. package/dist/cli/services/format-server-response.js +440 -0
  74. package/dist/cli/services/git-context.d.ts +11 -0
  75. package/dist/cli/services/git-context.js +66 -0
  76. package/dist/cli/services/hooks.d.ts +85 -0
  77. package/dist/cli/services/hooks.js +258 -0
  78. package/dist/cli/services/interactive-tools.d.ts +125 -0
  79. package/dist/cli/services/interactive-tools.js +260 -0
  80. package/dist/cli/services/keybinding-manager.d.ts +52 -0
  81. package/dist/cli/services/keybinding-manager.js +115 -0
  82. package/dist/cli/services/local-tools.d.ts +22 -0
  83. package/dist/cli/services/local-tools.js +697 -0
  84. package/dist/cli/services/lsp-manager.d.ts +18 -0
  85. package/dist/cli/services/lsp-manager.js +717 -0
  86. package/dist/cli/services/mcp-client.d.ts +48 -0
  87. package/dist/cli/services/mcp-client.js +157 -0
  88. package/dist/cli/services/memory-manager.d.ts +16 -0
  89. package/dist/cli/services/memory-manager.js +57 -0
  90. package/dist/cli/services/model-manager.d.ts +18 -0
  91. package/dist/cli/services/model-manager.js +71 -0
  92. package/dist/cli/services/model-router.d.ts +26 -0
  93. package/dist/cli/services/model-router.js +149 -0
  94. package/dist/cli/services/permission-modes.d.ts +13 -0
  95. package/dist/cli/services/permission-modes.js +43 -0
  96. package/dist/cli/services/rewind.d.ts +84 -0
  97. package/dist/cli/services/rewind.js +194 -0
  98. package/dist/cli/services/ripgrep.d.ts +28 -0
  99. package/dist/cli/services/ripgrep.js +138 -0
  100. package/dist/cli/services/sandbox.d.ts +29 -0
  101. package/dist/cli/services/sandbox.js +97 -0
  102. package/dist/cli/services/server-tools.d.ts +61 -0
  103. package/dist/cli/services/server-tools.js +543 -0
  104. package/dist/cli/services/session-persistence.d.ts +23 -0
  105. package/dist/cli/services/session-persistence.js +99 -0
  106. package/dist/cli/services/subagent-worker.d.ts +19 -0
  107. package/dist/cli/services/subagent-worker.js +41 -0
  108. package/dist/cli/services/subagent.d.ts +47 -0
  109. package/dist/cli/services/subagent.js +647 -0
  110. package/dist/cli/services/system-prompt.d.ts +7 -0
  111. package/dist/cli/services/system-prompt.js +198 -0
  112. package/dist/cli/services/team-lead.d.ts +73 -0
  113. package/dist/cli/services/team-lead.js +512 -0
  114. package/dist/cli/services/team-state.d.ts +77 -0
  115. package/dist/cli/services/team-state.js +398 -0
  116. package/dist/cli/services/teammate.d.ts +31 -0
  117. package/dist/cli/services/teammate.js +689 -0
  118. package/dist/cli/services/telemetry.d.ts +61 -0
  119. package/dist/cli/services/telemetry.js +209 -0
  120. package/dist/cli/services/tools/agent-tools.d.ts +14 -0
  121. package/dist/cli/services/tools/agent-tools.js +347 -0
  122. package/dist/cli/services/tools/file-ops.d.ts +15 -0
  123. package/dist/cli/services/tools/file-ops.js +487 -0
  124. package/dist/cli/services/tools/search-tools.d.ts +8 -0
  125. package/dist/cli/services/tools/search-tools.js +186 -0
  126. package/dist/cli/services/tools/shell-exec.d.ts +10 -0
  127. package/dist/cli/services/tools/shell-exec.js +168 -0
  128. package/dist/cli/services/tools/task-manager.d.ts +28 -0
  129. package/dist/cli/services/tools/task-manager.js +209 -0
  130. package/dist/cli/services/tools/web-tools.d.ts +11 -0
  131. package/dist/cli/services/tools/web-tools.js +395 -0
  132. package/dist/cli/setup/SetupApp.d.ts +9 -0
  133. package/dist/cli/setup/SetupApp.js +191 -0
  134. package/dist/cli/shared/MatrixIntro.d.ts +4 -0
  135. package/dist/cli/shared/MatrixIntro.js +83 -0
  136. package/dist/cli/shared/Theme.d.ts +74 -0
  137. package/dist/cli/shared/Theme.js +127 -0
  138. package/dist/cli/shared/WhaleBanner.d.ts +10 -0
  139. package/dist/cli/shared/WhaleBanner.js +12 -0
  140. package/dist/cli/shared/markdown.d.ts +21 -0
  141. package/dist/cli/shared/markdown.js +756 -0
  142. package/dist/cli/status/StatusApp.d.ts +4 -0
  143. package/dist/cli/status/StatusApp.js +105 -0
  144. package/dist/cli/stores/StoreApp.d.ts +7 -0
  145. package/dist/cli/stores/StoreApp.js +81 -0
  146. package/dist/index.d.ts +15 -0
  147. package/dist/index.js +538 -0
  148. package/dist/local-agent/connection.d.ts +48 -0
  149. package/dist/local-agent/connection.js +332 -0
  150. package/dist/local-agent/discovery.d.ts +18 -0
  151. package/dist/local-agent/discovery.js +146 -0
  152. package/dist/local-agent/executor.d.ts +34 -0
  153. package/dist/local-agent/executor.js +241 -0
  154. package/dist/local-agent/index.d.ts +14 -0
  155. package/dist/local-agent/index.js +198 -0
  156. package/dist/node/adapters/base.d.ts +35 -0
  157. package/dist/node/adapters/base.js +10 -0
  158. package/dist/node/adapters/discord.d.ts +29 -0
  159. package/dist/node/adapters/discord.js +299 -0
  160. package/dist/node/adapters/email.d.ts +23 -0
  161. package/dist/node/adapters/email.js +218 -0
  162. package/dist/node/adapters/imessage.d.ts +17 -0
  163. package/dist/node/adapters/imessage.js +118 -0
  164. package/dist/node/adapters/slack.d.ts +26 -0
  165. package/dist/node/adapters/slack.js +259 -0
  166. package/dist/node/adapters/sms.d.ts +23 -0
  167. package/dist/node/adapters/sms.js +161 -0
  168. package/dist/node/adapters/telegram.d.ts +17 -0
  169. package/dist/node/adapters/telegram.js +101 -0
  170. package/dist/node/adapters/webchat.d.ts +27 -0
  171. package/dist/node/adapters/webchat.js +160 -0
  172. package/dist/node/adapters/whatsapp.d.ts +28 -0
  173. package/dist/node/adapters/whatsapp.js +230 -0
  174. package/dist/node/cli.d.ts +2 -0
  175. package/dist/node/cli.js +325 -0
  176. package/dist/node/config.d.ts +17 -0
  177. package/dist/node/config.js +31 -0
  178. package/dist/node/runtime.d.ts +50 -0
  179. package/dist/node/runtime.js +351 -0
  180. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
  181. package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
  182. package/dist/server/handlers/analytics.d.ts +17 -0
  183. package/dist/server/handlers/analytics.js +266 -0
  184. package/dist/server/handlers/api-keys.d.ts +6 -0
  185. package/dist/server/handlers/api-keys.js +221 -0
  186. package/dist/server/handlers/billing.d.ts +33 -0
  187. package/dist/server/handlers/billing.js +272 -0
  188. package/dist/server/handlers/browser.d.ts +10 -0
  189. package/dist/server/handlers/browser.js +517 -0
  190. package/dist/server/handlers/catalog.d.ts +99 -0
  191. package/dist/server/handlers/catalog.js +976 -0
  192. package/dist/server/handlers/comms.d.ts +254 -0
  193. package/dist/server/handlers/comms.js +588 -0
  194. package/dist/server/handlers/creations.d.ts +6 -0
  195. package/dist/server/handlers/creations.js +479 -0
  196. package/dist/server/handlers/crm.d.ts +89 -0
  197. package/dist/server/handlers/crm.js +538 -0
  198. package/dist/server/handlers/discovery.d.ts +6 -0
  199. package/dist/server/handlers/discovery.js +288 -0
  200. package/dist/server/handlers/embeddings.d.ts +92 -0
  201. package/dist/server/handlers/embeddings.js +197 -0
  202. package/dist/server/handlers/enrichment.d.ts +8 -0
  203. package/dist/server/handlers/enrichment.js +768 -0
  204. package/dist/server/handlers/image-gen.d.ts +6 -0
  205. package/dist/server/handlers/image-gen.js +409 -0
  206. package/dist/server/handlers/inventory.d.ts +319 -0
  207. package/dist/server/handlers/inventory.js +447 -0
  208. package/dist/server/handlers/kali.d.ts +10 -0
  209. package/dist/server/handlers/kali.js +210 -0
  210. package/dist/server/handlers/llm-providers.d.ts +6 -0
  211. package/dist/server/handlers/llm-providers.js +673 -0
  212. package/dist/server/handlers/local-agent.d.ts +6 -0
  213. package/dist/server/handlers/local-agent.js +118 -0
  214. package/dist/server/handlers/meta-ads.d.ts +111 -0
  215. package/dist/server/handlers/meta-ads.js +2279 -0
  216. package/dist/server/handlers/nodes.d.ts +33 -0
  217. package/dist/server/handlers/nodes.js +699 -0
  218. package/dist/server/handlers/operations.d.ts +138 -0
  219. package/dist/server/handlers/operations.js +131 -0
  220. package/dist/server/handlers/platform.d.ts +23 -0
  221. package/dist/server/handlers/platform.js +227 -0
  222. package/dist/server/handlers/supply-chain.d.ts +19 -0
  223. package/dist/server/handlers/supply-chain.js +327 -0
  224. package/dist/server/handlers/transcription.d.ts +17 -0
  225. package/dist/server/handlers/transcription.js +121 -0
  226. package/dist/server/handlers/video-gen.d.ts +6 -0
  227. package/dist/server/handlers/video-gen.js +466 -0
  228. package/dist/server/handlers/voice.d.ts +8 -0
  229. package/dist/server/handlers/voice.js +1146 -0
  230. package/dist/server/handlers/workflow-steps.d.ts +86 -0
  231. package/dist/server/handlers/workflow-steps.js +2349 -0
  232. package/dist/server/handlers/workflows.d.ts +7 -0
  233. package/dist/server/handlers/workflows.js +989 -0
  234. package/dist/server/index.d.ts +1 -0
  235. package/dist/server/index.js +2427 -0
  236. package/dist/server/lib/batch-client.d.ts +80 -0
  237. package/dist/server/lib/batch-client.js +467 -0
  238. package/dist/server/lib/code-worker-pool.d.ts +31 -0
  239. package/dist/server/lib/code-worker-pool.js +224 -0
  240. package/dist/server/lib/code-worker.d.ts +1 -0
  241. package/dist/server/lib/code-worker.js +188 -0
  242. package/dist/server/lib/compaction-service.d.ts +32 -0
  243. package/dist/server/lib/compaction-service.js +162 -0
  244. package/dist/server/lib/logger.d.ts +19 -0
  245. package/dist/server/lib/logger.js +46 -0
  246. package/dist/server/lib/otel.d.ts +38 -0
  247. package/dist/server/lib/otel.js +126 -0
  248. package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
  249. package/dist/server/lib/pg-rate-limiter.js +86 -0
  250. package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
  251. package/dist/server/lib/prompt-sanitizer.js +177 -0
  252. package/dist/server/lib/provider-capabilities.d.ts +85 -0
  253. package/dist/server/lib/provider-capabilities.js +190 -0
  254. package/dist/server/lib/provider-failover.d.ts +74 -0
  255. package/dist/server/lib/provider-failover.js +210 -0
  256. package/dist/server/lib/rate-limiter.d.ts +39 -0
  257. package/dist/server/lib/rate-limiter.js +147 -0
  258. package/dist/server/lib/server-agent-loop.d.ts +107 -0
  259. package/dist/server/lib/server-agent-loop.js +667 -0
  260. package/dist/server/lib/server-subagent.d.ts +78 -0
  261. package/dist/server/lib/server-subagent.js +203 -0
  262. package/dist/server/lib/session-checkpoint.d.ts +51 -0
  263. package/dist/server/lib/session-checkpoint.js +145 -0
  264. package/dist/server/lib/ssrf-guard.d.ts +13 -0
  265. package/dist/server/lib/ssrf-guard.js +240 -0
  266. package/dist/server/lib/supabase-client.d.ts +7 -0
  267. package/dist/server/lib/supabase-client.js +78 -0
  268. package/dist/server/lib/template-resolver.d.ts +31 -0
  269. package/dist/server/lib/template-resolver.js +215 -0
  270. package/dist/server/lib/utils.d.ts +16 -0
  271. package/dist/server/lib/utils.js +147 -0
  272. package/dist/server/local-agent-gateway.d.ts +82 -0
  273. package/dist/server/local-agent-gateway.js +426 -0
  274. package/dist/server/providers/anthropic.d.ts +20 -0
  275. package/dist/server/providers/anthropic.js +199 -0
  276. package/dist/server/providers/bedrock.d.ts +20 -0
  277. package/dist/server/providers/bedrock.js +194 -0
  278. package/dist/server/providers/gemini.d.ts +24 -0
  279. package/dist/server/providers/gemini.js +486 -0
  280. package/dist/server/providers/openai.d.ts +24 -0
  281. package/dist/server/providers/openai.js +522 -0
  282. package/dist/server/providers/registry.d.ts +32 -0
  283. package/dist/server/providers/registry.js +58 -0
  284. package/dist/server/providers/shared.d.ts +32 -0
  285. package/dist/server/providers/shared.js +124 -0
  286. package/dist/server/providers/types.d.ts +92 -0
  287. package/dist/server/providers/types.js +12 -0
  288. package/dist/server/proxy-handlers.d.ts +6 -0
  289. package/dist/server/proxy-handlers.js +89 -0
  290. package/dist/server/tool-router.d.ts +149 -0
  291. package/dist/server/tool-router.js +803 -0
  292. package/dist/server/validation.d.ts +24 -0
  293. package/dist/server/validation.js +301 -0
  294. package/dist/server/worker.d.ts +19 -0
  295. package/dist/server/worker.js +201 -0
  296. package/dist/setup.d.ts +8 -0
  297. package/dist/setup.js +181 -0
  298. package/dist/shared/agent-core.d.ts +157 -0
  299. package/dist/shared/agent-core.js +534 -0
  300. package/dist/shared/anthropic-types.d.ts +105 -0
  301. package/dist/shared/anthropic-types.js +7 -0
  302. package/dist/shared/api-client.d.ts +90 -0
  303. package/dist/shared/api-client.js +379 -0
  304. package/dist/shared/constants.d.ts +33 -0
  305. package/dist/shared/constants.js +80 -0
  306. package/dist/shared/sse-parser.d.ts +26 -0
  307. package/dist/shared/sse-parser.js +259 -0
  308. package/dist/shared/tool-dispatch.d.ts +52 -0
  309. package/dist/shared/tool-dispatch.js +191 -0
  310. package/dist/shared/types.d.ts +72 -0
  311. package/dist/shared/types.js +7 -0
  312. package/dist/updater.d.ts +25 -0
  313. package/dist/updater.js +140 -0
  314. package/dist/webchat/widget.d.ts +0 -0
  315. package/dist/webchat/widget.js +397 -0
  316. package/package.json +95 -0
  317. package/src/cli/services/builtin-skills/commit.md +19 -0
  318. package/src/cli/services/builtin-skills/review-pr.md +21 -0
  319. package/src/cli/services/builtin-skills/review.md +18 -0
@@ -0,0 +1,762 @@
1
+ /**
2
+ * Agent Loop — local-first agentic CLI with server tool support
3
+ *
4
+ * LLM calls proxy through the `agent-proxy` edge function (server holds API key).
5
+ * User authenticates via Supabase JWT. Local tools execute on the client.
6
+ * Server tools execute via direct import of executeTool() (same codebase).
7
+ *
8
+ * Fallback: if proxy is unavailable and ANTHROPIC_API_KEY is set, calls directly.
9
+ *
10
+ * This file is the thin orchestrator + re-export facade. Domain logic lives in:
11
+ * - memory-manager.ts (loadMemory, addMemory, removeMemory, listMemories)
12
+ * - git-context.ts (gatherGitContext, refreshGitContext)
13
+ * - claude-md-loader.ts (loadClaudeMd, reloadClaudeMd)
14
+ * - permission-modes.ts (PermissionMode, set/get/isAllowed)
15
+ * - model-manager.ts (setModel, getModel, getModelShortName, estimateCostUsd)
16
+ * - session-persistence.ts (SessionMeta, save/load/list/find sessions)
17
+ * - system-prompt.ts (buildSystemPrompt)
18
+ */
19
+ import { LOCAL_TOOL_DEFINITIONS, executeLocalTool, isLocalTool, } from "./local-tools.js";
20
+ import { INTERACTIVE_TOOL_DEFINITIONS, executeInteractiveTool, } from "./interactive-tools.js";
21
+ import { loadConfig, resolveConfig, getProxyUrl } from "./config-store.js";
22
+ import { getValidToken, refreshSession } from "./auth-service.js";
23
+ import { isServerTool, loadServerToolDefinitions, executeServerTool, getServerStatus, setServerToolContext, } from "./server-tools.js";
24
+ import { nextTurn, createTurnContext, logSpan, generateSpanId, } from "./telemetry.js";
25
+ import { captureError, addBreadcrumb } from "./error-logger.js";
26
+ import { setGlobalEmitter, clearGlobalEmitter, } from "./agent-events.js";
27
+ import { mcpClientManager } from "./mcp-client.js";
28
+ import { loadHooks, runBeforeToolHook, runAfterToolHook, runSessionHook } from "./hooks.js";
29
+ import { LoopDetector, COMPACTION_TRIGGER_TOKENS, COMPACTION_TOTAL_BUDGET, getCompactionConfig } from "../../shared/agent-core.js";
30
+ import { parseSSEStream, processStreamWithCallbacks, collectStreamResult } from "../../shared/sse-parser.js";
31
+ import { callServerProxy, callTranscribe, buildAPIRequest, buildSystemBlocks, prepareWithCaching, trimGeminiContext, trimOpenAIContext, requestProviderCompaction } from "../../shared/api-client.js";
32
+ import { getProvider, MODELS } from "../../shared/constants.js";
33
+ import { dispatchTools, buildAssistantContent } from "../../shared/tool-dispatch.js";
34
+ // ── Extracted modules (re-exported below for backward compatibility) ──
35
+ import { loadMemory, addMemory, removeMemory, listMemories } from "./memory-manager.js";
36
+ import { refreshGitContext, resetGitContext } from "./git-context.js";
37
+ import { loadClaudeMd, reloadClaudeMd, resetClaudeMdCache } from "./claude-md-loader.js";
38
+ import { setPermissionMode, getPermissionMode, isToolAllowedByPermission } from "./permission-modes.js";
39
+ import { setModel, setModelById, getModel, getModelShortName, estimateCostUsd } from "./model-manager.js";
40
+ import { saveSession, loadSession, listSessions, findLatestSessionForCwd } from "./session-persistence.js";
41
+ import { buildSystemPrompt } from "./system-prompt.js";
42
+ // ============================================================================
43
+ // RE-EXPORTS — all consumers keep importing from agent-loop.ts
44
+ // ============================================================================
45
+ // Memory
46
+ export { loadMemory, addMemory, removeMemory, listMemories };
47
+ // Git context
48
+ export { refreshGitContext };
49
+ // CLAUDE.md
50
+ export { loadClaudeMd, reloadClaudeMd };
51
+ // Permission modes
52
+ export { setPermissionMode, getPermissionMode, isToolAllowedByPermission };
53
+ // Model management
54
+ export { setModel, getModel, getModelShortName, estimateCostUsd };
55
+ // Session persistence
56
+ export { saveSession, loadSession, listSessions, findLatestSessionForCwd };
57
+ // Server status (pass-through)
58
+ export { getServerStatus };
59
+ // MCP client manager
60
+ export { mcpClientManager };
61
+ // Re-export background process listing for /tasks command
62
+ export { listProcesses, listBackgroundAgents } from "./background-processes.js";
63
+ // Re-export event emitter for ChatApp
64
+ export { AgentEventEmitter } from "./agent-events.js";
65
+ // ============================================================================
66
+ // SESSION STATE
67
+ // ============================================================================
68
+ // CLI-only: Session-wide token tracking (actual counts from API responses).
69
+ // Reset via resetSessionState() when starting a new conversation.
70
+ let sessionInputTokens = 0;
71
+ let sessionOutputTokens = 0;
72
+ export function getSessionTokens() {
73
+ return { input: sessionInputTokens, output: sessionOutputTokens };
74
+ }
75
+ /**
76
+ * Reset all CLI-only session state. Call when starting a new conversation
77
+ * (e.g., /clear command, new print-mode run) to prevent stale token counts,
78
+ * loop detector state, and caches from leaking across sessions.
79
+ *
80
+ * Does NOT reset activeModel or permissionMode — those are user preferences
81
+ * that persist intentionally until explicitly changed.
82
+ */
83
+ export function resetSessionState() {
84
+ sessionInputTokens = 0;
85
+ sessionOutputTokens = 0;
86
+ sessionLoopDetector = null;
87
+ resetGitContext();
88
+ resetClaudeMdCache();
89
+ }
90
+ /** CLI-only: loop detector — persists session error state across turns (reset by resetSessionState) */
91
+ let sessionLoopDetector = null;
92
+ const MAX_TURNS = 200; // Match Claude Code — effectively unlimited within a session
93
+ // ============================================================================
94
+ // SHELL OUTPUT SUMMARIZATION
95
+ // ============================================================================
96
+ const SHELL_SUMMARIZE_LINE_THRESHOLD = 800;
97
+ const SHELL_SUMMARIZE_SIZE_THRESHOLD = 200_000; // 200KB — only summarize truly huge outputs
98
+ const SHELL_SUMMARIZE_MAX_INPUT = 300_000; // 300KB max to summarizer
99
+ const SHELL_SUMMARIZE_ORIGINAL_PREVIEW_LINES = 20;
100
+ /**
101
+ * Check if shell output should be summarized based on line count or size.
102
+ */
103
+ function shouldSummarizeShellOutput(output) {
104
+ if (output.length > SHELL_SUMMARIZE_SIZE_THRESHOLD)
105
+ return true;
106
+ const lineCount = output.split("\n").length;
107
+ return lineCount > SHELL_SUMMARIZE_LINE_THRESHOLD;
108
+ }
109
+ /**
110
+ * Summarize long shell output using Haiku via server proxy.
111
+ * Returns summarized output or original if summarization fails.
112
+ */
113
+ async function summarizeShellOutput(output, proxyUrl, token, storeId) {
114
+ const lineCount = output.split("\n").length;
115
+ const truncatedForSummary = output.length > SHELL_SUMMARIZE_MAX_INPUT
116
+ ? output.slice(0, SHELL_SUMMARIZE_MAX_INPUT) + "\n... (truncated for summarization)"
117
+ : output;
118
+ try {
119
+ const summaryConfig = buildAPIRequest({
120
+ model: MODELS.HAIKU,
121
+ contextProfile: "subagent",
122
+ });
123
+ const stream = await callServerProxy({
124
+ proxyUrl,
125
+ token,
126
+ model: MODELS.HAIKU,
127
+ system: [{ type: "text", text: "You are a concise technical summarizer. Summarize shell/command output preserving key information, errors, warnings, file paths, and actionable items. Be brief but thorough." }],
128
+ messages: [{ role: "user", content: `Summarize this shell output concisely, preserving key information, errors, and actionable items:\n\n${truncatedForSummary}` }],
129
+ tools: [],
130
+ apiConfig: summaryConfig,
131
+ storeId,
132
+ timeoutMs: 15_000,
133
+ });
134
+ const result = await collectStreamResult(parseSSEStream(stream));
135
+ const summary = result.text.trim();
136
+ if (!summary)
137
+ return output; // Summarization failed, return original
138
+ // Build first N lines preview
139
+ const originalLines = output.split("\n");
140
+ const preview = originalLines.slice(0, SHELL_SUMMARIZE_ORIGINAL_PREVIEW_LINES).join("\n");
141
+ return `[Summarized from ${lineCount} lines]\n\n${summary}\n\n[First ${SHELL_SUMMARIZE_ORIGINAL_PREVIEW_LINES} lines of original output]\n${preview}`;
142
+ }
143
+ catch {
144
+ // Summarization failed silently — return original output
145
+ return output;
146
+ }
147
+ }
148
+ /**
149
+ * Post-process tool results to summarize long bash output.
150
+ * Only affects bash tool results that exceed size/line thresholds.
151
+ */
152
+ async function summarizeLongToolResults(toolResults, toolNames, proxyUrl, token, shellSummarization, storeId) {
153
+ if (!shellSummarization)
154
+ return toolResults;
155
+ const tasks = toolResults.map(async (result) => {
156
+ // Only summarize bash tool string results
157
+ const toolName = toolNames.get(result.tool_use_id);
158
+ if (toolName !== "bash" || typeof result.content !== "string")
159
+ return result;
160
+ // Check thresholds
161
+ if (!shouldSummarizeShellOutput(result.content))
162
+ return result;
163
+ const summarized = await summarizeShellOutput(result.content, proxyUrl, token, storeId);
164
+ return { ...result, content: summarized };
165
+ });
166
+ return Promise.all(tasks);
167
+ }
168
+ // ============================================================================
169
+ // TOOL DEFINITIONS
170
+ // ============================================================================
171
+ async function getTools(allowedTools, disallowedTools) {
172
+ const localTools = LOCAL_TOOL_DEFINITIONS.map((t) => ({
173
+ name: t.name,
174
+ description: t.description,
175
+ input_schema: t.input_schema,
176
+ }));
177
+ // Add interactive tools (ask_user_question, enter_plan_mode, exit_plan_mode)
178
+ const interactiveTools = INTERACTIVE_TOOL_DEFINITIONS.map((t) => ({
179
+ name: t.name,
180
+ description: t.description,
181
+ input_schema: t.input_schema,
182
+ }));
183
+ localTools.push(...interactiveTools);
184
+ let serverTools = [];
185
+ try {
186
+ serverTools = await loadServerToolDefinitions();
187
+ }
188
+ catch {
189
+ // Server tools silently unavailable
190
+ }
191
+ // Deduplicate: local tools take priority over server tools with the same name
192
+ const localNames = new Set(localTools.map(t => t.name));
193
+ const uniqueServerTools = serverTools.filter(t => !localNames.has(t.name));
194
+ // MCP tools from external servers
195
+ const mcpTools = mcpClientManager.getTools();
196
+ let allTools = [...localTools, ...uniqueServerTools, ...mcpTools];
197
+ // Apply tool filtering
198
+ if (allowedTools && allowedTools.length > 0) {
199
+ const allowed = new Set(allowedTools);
200
+ allTools = allTools.filter(t => allowed.has(t.name));
201
+ }
202
+ if (disallowedTools && disallowedTools.length > 0) {
203
+ const disallowed = new Set(disallowedTools);
204
+ allTools = allTools.filter(t => !disallowed.has(t.name));
205
+ }
206
+ return {
207
+ tools: allTools,
208
+ serverToolCount: uniqueServerTools.length,
209
+ };
210
+ }
211
+ /** Exposed for /status command */
212
+ export async function getServerToolCount() {
213
+ try {
214
+ const defs = await loadServerToolDefinitions();
215
+ return defs.length;
216
+ }
217
+ catch {
218
+ return 0;
219
+ }
220
+ }
221
+ // ============================================================================
222
+ // MAIN LOOP
223
+ // ============================================================================
224
+ export async function runAgentLoop(opts) {
225
+ const { message, conversationHistory, callbacks, abortSignal, emitter } = opts;
226
+ if (opts.model)
227
+ setModel(opts.model);
228
+ // Set global emitter for subagents to use
229
+ if (emitter) {
230
+ setGlobalEmitter(emitter);
231
+ }
232
+ const effectiveMaxTurns = opts.maxTurns || MAX_TURNS;
233
+ // Load hooks from project and user config
234
+ const hooksCwd = opts.cwd || process.cwd();
235
+ const hooks = loadHooks(hooksCwd);
236
+ // Fire SessionStart hook (non-blocking)
237
+ if (hooks.length > 0) {
238
+ runSessionHook(hooks, "SessionStart", { session_id: `turn-${Date.now()}` }).catch(() => { });
239
+ }
240
+ // Shell summarization config (default: true)
241
+ const shellSummarization = opts.shellSummarization !== false;
242
+ const { tools, serverToolCount } = await getTools(opts.allowedTools, opts.disallowedTools);
243
+ const systemPrompt = await buildSystemPrompt(serverToolCount > 0, opts.effort);
244
+ // Build user content — text-only string or content blocks array with images
245
+ let userContent;
246
+ if (opts.images && opts.images.length > 0) {
247
+ const blocks = [];
248
+ for (const img of opts.images) {
249
+ blocks.push({
250
+ type: "image",
251
+ source: {
252
+ type: "base64",
253
+ media_type: img.mediaType,
254
+ data: img.base64,
255
+ },
256
+ });
257
+ }
258
+ blocks.push({ type: "text", text: message || "(see attached images)" });
259
+ userContent = blocks;
260
+ }
261
+ else {
262
+ userContent = message;
263
+ }
264
+ const messages = [
265
+ ...conversationHistory,
266
+ { role: "user", content: userContent },
267
+ ];
268
+ // Session-level loop detector: persists failed strategies across turns.
269
+ // Created once per conversation, reset only when user starts a new conversation.
270
+ if (!sessionLoopDetector || conversationHistory.length === 0) {
271
+ sessionLoopDetector = new LoopDetector();
272
+ }
273
+ const loopDetector = sessionLoopDetector;
274
+ loopDetector.resetTurn();
275
+ let totalIn = 0;
276
+ let totalOut = 0;
277
+ let totalCacheCreation = 0;
278
+ let totalCacheRead = 0;
279
+ let totalThinking = 0;
280
+ let allAssistantText = [];
281
+ let prevIterationInputTokens = 0; // Tracks actual context size from last API call
282
+ // Telemetry: one turn per user message (not per API call)
283
+ const sessionStart = Date.now();
284
+ const { storeId } = resolveConfig();
285
+ const turnNum = nextTurn(); // ONCE per user message
286
+ const turnCtx = createTurnContext({ model: getModel(), turnNumber: turnNum });
287
+ addBreadcrumb("agent", `Turn ${turnNum}: ${message.slice(0, 100)}`, "info");
288
+ // Set server tool context so tool calls carry trace/user identity to Fly.io server
289
+ setServerToolContext({
290
+ traceId: turnCtx.traceId,
291
+ conversationId: turnCtx.conversationId,
292
+ userId: turnCtx.userId,
293
+ userEmail: turnCtx.userEmail,
294
+ source: "whale-code",
295
+ });
296
+ logSpan({
297
+ action: "chat.user_message",
298
+ durationMs: 0,
299
+ context: turnCtx,
300
+ storeId: storeId || undefined,
301
+ details: {
302
+ message: message,
303
+ conversation_history_length: conversationHistory.length,
304
+ },
305
+ });
306
+ let sessionCostUsd = 0;
307
+ let compactionCount = 0;
308
+ const activeModel = getModel();
309
+ // Tool executor — routes to interactive, local, server, or MCP tools.
310
+ // Wraps execution with before/after hooks when hooks are loaded.
311
+ const INTERACTIVE_TOOL_NAMES = new Set(INTERACTIVE_TOOL_DEFINITIONS.map(t => t.name));
312
+ const toolExecutor = async (name, input) => {
313
+ if (!name) {
314
+ return { success: false, output: "Tool call missing name — skipping." };
315
+ }
316
+ // Permission mode enforcement
317
+ if (!isToolAllowedByPermission(name)) {
318
+ return { success: false, output: `Tool "${name}" blocked by ${getPermissionMode()} mode. Switch modes with /mode.` };
319
+ }
320
+ // BeforeTool hook — may block or modify input
321
+ let effectiveInput = input;
322
+ if (hooks.length > 0) {
323
+ const hookResult = await runBeforeToolHook(hooks, name, input);
324
+ if (!hookResult.allow) {
325
+ return { success: false, output: hookResult.message || "Blocked by hook" };
326
+ }
327
+ if (hookResult.modifiedInput) {
328
+ effectiveInput = hookResult.modifiedInput;
329
+ }
330
+ }
331
+ let result;
332
+ if (INTERACTIVE_TOOL_NAMES.has(name)) {
333
+ result = await executeInteractiveTool(name, effectiveInput);
334
+ }
335
+ else if (isLocalTool(name)) {
336
+ result = await executeLocalTool(name, effectiveInput);
337
+ }
338
+ else if (isServerTool(name)) {
339
+ result = await executeServerTool(name, effectiveInput, emitter);
340
+ }
341
+ else if (mcpClientManager.isMcpTool(name)) {
342
+ result = await mcpClientManager.callTool(name, effectiveInput);
343
+ }
344
+ else {
345
+ result = { success: false, output: `Unknown tool: ${name}` };
346
+ }
347
+ // AfterTool hook — may modify output
348
+ if (hooks.length > 0) {
349
+ const afterResult = await runAfterToolHook(hooks, name, result.output, result.success);
350
+ if (afterResult.modifiedOutput !== undefined) {
351
+ result = { ...result, output: afterResult.modifiedOutput };
352
+ }
353
+ }
354
+ return result;
355
+ };
356
+ // Deprecated: Anthropic context_management handles limits via clear_tool_uses + compaction.
357
+ // tool-dispatch.ts uses SAFETY_MAX_CHARS (500K) — this value is ignored.
358
+ const maxResultChars = undefined;
359
+ try {
360
+ for (let iteration = 0; iteration < effectiveMaxTurns; iteration++) {
361
+ if (abortSignal?.aborted) {
362
+ logSpan({ action: "chat.cancelled", durationMs: Date.now() - sessionStart, context: turnCtx, storeId: storeId || undefined, details: { iteration, reason: "user_abort" } });
363
+ callbacks.onError("Cancelled", messages);
364
+ return;
365
+ }
366
+ // Budget enforcement
367
+ if (opts.maxBudgetUsd && sessionCostUsd >= opts.maxBudgetUsd) {
368
+ logSpan({ action: "chat.budget_exceeded", durationMs: Date.now() - sessionStart, context: turnCtx, storeId: storeId || undefined, severity: "warn", details: { session_cost_usd: sessionCostUsd, max_budget_usd: opts.maxBudgetUsd, iteration } });
369
+ callbacks.onError(`Budget exceeded: $${sessionCostUsd.toFixed(4)} >= $${opts.maxBudgetUsd}`, messages);
370
+ return;
371
+ }
372
+ const apiStart = Date.now();
373
+ const apiSpanId = generateSpanId();
374
+ const apiRowId = crypto.randomUUID(); // UUID for this span's row — children reference via parent_id
375
+ const costContext = `Session cost: $${sessionCostUsd.toFixed(2)}${opts.maxBudgetUsd ? ` | Budget remaining: $${(opts.maxBudgetUsd - sessionCostUsd).toFixed(2)}` : ""}`;
376
+ // Build API request config
377
+ const currentModel = getModel();
378
+ const apiConfig = buildAPIRequest({
379
+ model: currentModel,
380
+ contextProfile: "main",
381
+ thinkingEnabled: opts.thinking,
382
+ });
383
+ // Prepare with prompt caching
384
+ let { tools: cachedTools, messages: cachedMessages } = prepareWithCaching(tools, messages);
385
+ const system = buildSystemBlocks(systemPrompt, costContext);
386
+ // Client-side context trimming for non-Anthropic providers.
387
+ // Uses prevIterationInputTokens (actual context size from last API call) — NOT
388
+ // cumulative sessionInputTokens, which grows quadratically and would trigger too early.
389
+ const provider = getProvider(currentModel);
390
+ if (provider === "gemini" || provider === "openai") {
391
+ const preTrimMessages = cachedMessages;
392
+ if (provider === "gemini") {
393
+ // Emergency fallback only — compaction fires at 700K first; trim at 950K catches failures
394
+ cachedMessages = trimGeminiContext(cachedMessages, prevIterationInputTokens);
395
+ }
396
+ else {
397
+ // Emergency fallback only — compaction fires at 120K first; trim at 190K catches failures
398
+ // GPT-4o: 128K context → 96K threshold (no compaction headroom)
399
+ const threshold = currentModel === "gpt-4o" ? 96_000 : 190_000;
400
+ cachedMessages = trimOpenAIContext(cachedMessages, prevIterationInputTokens, threshold);
401
+ }
402
+ // Notify UI when trimming actually occurred (trim returns same ref if no-op)
403
+ if (cachedMessages !== preTrimMessages) {
404
+ // Count tool results before/after to report meaningful numbers
405
+ const countToolResults = (msgs) => msgs.reduce((sum, m) => sum + (Array.isArray(m.content)
406
+ ? m.content.filter(b => b.type === "tool_result" && b.content !== "[trimmed]").length
407
+ : 0), 0);
408
+ const activeBefore = countToolResults(preTrimMessages);
409
+ const activeAfter = countToolResults(cachedMessages);
410
+ const estimatedSaved = Math.round(prevIterationInputTokens * ((activeBefore - activeAfter) / Math.max(activeBefore, 1)));
411
+ callbacks.onAutoCompact?.(activeBefore, activeAfter, estimatedSaved);
412
+ emitter?.emitCompact(activeBefore, activeAfter, estimatedSaved);
413
+ }
414
+ }
415
+ // Get auth token
416
+ const token = await getValidToken();
417
+ if (!token) {
418
+ throw new Error("Not logged in. Run: whale login");
419
+ }
420
+ // Call server proxy with built-in retry
421
+ const originalModel = currentModel;
422
+ const stream = await callServerProxy({
423
+ proxyUrl: getProxyUrl(),
424
+ token,
425
+ model: currentModel,
426
+ system,
427
+ messages: cachedMessages,
428
+ tools: cachedTools,
429
+ apiConfig,
430
+ signal: abortSignal,
431
+ fallbackModel: opts.fallbackModel,
432
+ storeId: storeId || undefined,
433
+ onFallback: (from, to) => {
434
+ setModel(to);
435
+ logSpan({ action: "claude_api_fallback", durationMs: 0, context: { ...turnCtx, spanId: apiSpanId }, storeId: storeId || undefined, details: { from_model: from, to_model: to } });
436
+ },
437
+ onRetry: (attempt, max, err) => {
438
+ const msg = `\n\x1b[33m\u21BB Retrying (${attempt}/${max})... ${err.slice(0, 80)}\x1b[0m\n`;
439
+ if (emitter) {
440
+ emitter.emitText(msg);
441
+ }
442
+ else {
443
+ callbacks.onText(msg);
444
+ }
445
+ },
446
+ onTokenRefresh: async () => {
447
+ const result = await refreshSession();
448
+ return result.success ? result.config.access_token : null;
449
+ },
450
+ });
451
+ // Process stream events with UI callbacks
452
+ const result = await processStreamWithCallbacks(parseSSEStream(stream, abortSignal), {
453
+ onText: (text) => {
454
+ if (emitter) {
455
+ emitter.emitText(text);
456
+ }
457
+ else {
458
+ callbacks.onText(text);
459
+ }
460
+ },
461
+ onToolStart: (name, input) => {
462
+ // NOTE: Do NOT call callbacks.onToolStart here — dispatchTools.onStart
463
+ // fires it once per tool at execution time. Calling it here too would
464
+ // send duplicate tool_start events (stale spinners in WhaleChat).
465
+ if (input) {
466
+ // Tool block complete — emit structured start for CLI TUI only
467
+ emitter?.emitToolStart("", name);
468
+ }
469
+ },
470
+ }, abortSignal);
471
+ // Flush buffered text
472
+ emitter?.flushText();
473
+ // Restore original model after transient fallback
474
+ if (getModel() !== originalModel && opts.fallbackModel) {
475
+ setModelById(originalModel);
476
+ }
477
+ // Update session token tracking
478
+ sessionInputTokens += result.usage.inputTokens;
479
+ sessionOutputTokens += result.usage.outputTokens;
480
+ prevIterationInputTokens = result.usage.inputTokens; // Actual context size for next trim check
481
+ // Emit usage with model + cost context for all providers
482
+ if (emitter && (result.usage.inputTokens > 0 || result.usage.outputTokens > 0)) {
483
+ const iterCost = estimateCostUsd(result.usage.inputTokens, result.usage.outputTokens, currentModel, result.thinkingTokens, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
484
+ emitter.emitUsage(result.usage.inputTokens, result.usage.outputTokens, currentModel, iterCost, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
485
+ }
486
+ totalIn += result.usage.inputTokens;
487
+ totalOut += result.usage.outputTokens;
488
+ totalCacheCreation += result.usage.cacheCreationTokens;
489
+ totalCacheRead += result.usage.cacheReadTokens;
490
+ totalThinking += result.thinkingTokens;
491
+ sessionCostUsd += estimateCostUsd(result.usage.inputTokens, result.usage.outputTokens, currentModel, result.thinkingTokens, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
492
+ // Server-side context management notification
493
+ if (result.contextManagementApplied) {
494
+ callbacks.onAutoCompact?.(messages.length, messages.length, 0);
495
+ emitter?.emitCompact(messages.length, messages.length, 0);
496
+ logSpan({ action: "chat.api_compaction", durationMs: Date.now() - apiStart, context: turnCtx, storeId: storeId || undefined, details: { type: "server_side", has_compaction_content: result.compactionContent !== null, iteration } });
497
+ }
498
+ if (result.text)
499
+ allAssistantText.push(result.text);
500
+ // Telemetry: API call span
501
+ const iterCostUsd = estimateCostUsd(result.usage.inputTokens, result.usage.outputTokens, currentModel, result.thinkingTokens, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
502
+ logSpan({
503
+ action: "claude_api_request",
504
+ durationMs: Date.now() - apiStart,
505
+ context: { ...turnCtx, spanId: apiSpanId, rowId: apiRowId, inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens, totalCost: iterCostUsd, model: currentModel },
506
+ storeId: storeId || undefined,
507
+ details: {
508
+ "gen_ai.request.model": currentModel,
509
+ "gen_ai.usage.input_tokens": result.usage.inputTokens,
510
+ "gen_ai.usage.output_tokens": result.usage.outputTokens,
511
+ "gen_ai.usage.cache_creation_tokens": result.usage.cacheCreationTokens,
512
+ "gen_ai.usage.cache_read_tokens": result.usage.cacheReadTokens,
513
+ "gen_ai.usage.cost": iterCostUsd,
514
+ stop_reason: result.stopReason === "compaction" ? "compaction" : result.toolUseBlocks.length > 0 ? "tool_use" : "end_turn",
515
+ iteration,
516
+ tool_count: result.toolUseBlocks.length,
517
+ tool_names: result.toolUseBlocks.map(t => t.name),
518
+ },
519
+ });
520
+ // Compaction handling — API paused after generating summary.
521
+ // Preserve last 2 messages for continuity, then resume.
522
+ if (result.stopReason === "compaction" && result.compactionContent) {
523
+ compactionCount++;
524
+ logSpan({ action: "chat.compaction_pause", durationMs: Date.now() - apiStart, context: turnCtx, storeId: storeId || undefined, details: { compaction_count: compactionCount, messages_before: messages.length } });
525
+ // Budget enforcement
526
+ if (compactionCount * COMPACTION_TRIGGER_TOKENS >= COMPACTION_TOTAL_BUDGET) {
527
+ const budgetMsg = "\n[Context budget reached — wrapping up.]";
528
+ if (emitter) {
529
+ emitter.emitText(budgetMsg);
530
+ }
531
+ else {
532
+ callbacks.onText(budgetMsg);
533
+ }
534
+ const compactedMessages = [
535
+ { role: "assistant", content: [{ type: "compaction", content: result.compactionContent }] },
536
+ { role: "user", content: [{ type: "text", text: "You have reached the context budget. Please wrap up your current work and provide a final summary of what was accomplished and what remains." }] },
537
+ ];
538
+ messages.length = 0;
539
+ messages.push(...compactedMessages);
540
+ continue;
541
+ }
542
+ // Normal compaction: preserve last 2 messages for continuity
543
+ const preserved = messages.slice(-2);
544
+ const compactedMessages = [
545
+ { role: "assistant", content: [{ type: "compaction", content: result.compactionContent }] },
546
+ ...preserved,
547
+ ];
548
+ messages.length = 0;
549
+ messages.push(...compactedMessages);
550
+ iteration--; // Don't count compaction as an iteration
551
+ continue;
552
+ }
553
+ // No tool calls — check if we should continue or stop
554
+ if (result.toolUseBlocks.length === 0) {
555
+ // If model hit max_tokens, it was truncated mid-response — continue so it can finish
556
+ if (result.stopReason === "max_tokens") {
557
+ const truncatedText = result.text || "";
558
+ const assistantContent = buildAssistantContent({
559
+ text: truncatedText,
560
+ toolUseBlocks: [],
561
+ thinkingBlocks: result.thinkingBlocks,
562
+ compactionContent: result.compactionContent,
563
+ });
564
+ messages.push({ role: "assistant", content: assistantContent });
565
+ messages.push({ role: "user", content: [{ type: "text", text: "[Your response was truncated due to length. Please continue where you left off.]" }] });
566
+ continue;
567
+ }
568
+ break;
569
+ }
570
+ // Execute tools via shared dispatch
571
+ const { results: toolResults, bailOut, bailMessage } = await dispatchTools(result.toolUseBlocks, toolExecutor, {
572
+ loopDetector,
573
+ maxConcurrent: 7,
574
+ maxResultChars,
575
+ onStart: (name, input) => {
576
+ callbacks.onToolStart(name, input);
577
+ },
578
+ onResult: (name, success, output, durationMs) => {
579
+ callbacks.onToolResult(name, success, output, undefined, durationMs);
580
+ logSpan({
581
+ action: `tool.${name}`,
582
+ durationMs,
583
+ context: { ...turnCtx, spanId: generateSpanId(), parentSpanId: apiSpanId, parentId: apiRowId },
584
+ storeId: storeId || undefined,
585
+ error: success ? undefined : output,
586
+ details: {
587
+ tool_input: {},
588
+ tool_result: truncateResult(output, 2000),
589
+ error_type: success ? undefined : classifyToolError(output),
590
+ iteration,
591
+ },
592
+ });
593
+ },
594
+ signal: abortSignal,
595
+ transcribeAudio: storeId
596
+ ? async (base64, mediaType) => callTranscribe({
597
+ proxyUrl: getProxyUrl(),
598
+ token,
599
+ storeId: storeId,
600
+ audioBase64: base64,
601
+ mediaType,
602
+ })
603
+ : undefined,
604
+ });
605
+ if (bailOut) {
606
+ logSpan({ action: "chat.bail_out", durationMs: Date.now() - sessionStart, context: turnCtx, storeId: storeId || undefined, severity: "warn", details: { ...loopDetector.getSessionStats(), message: bailMessage, iteration } });
607
+ }
608
+ // Shell output summarization — summarize long bash output to save context window
609
+ const toolNameMap = new Map(result.toolUseBlocks.map(t => [t.id, t.name]));
610
+ const finalToolResults = await summarizeLongToolResults(toolResults, toolNameMap, getProxyUrl(), token, shellSummarization, storeId || undefined);
611
+ // Build assistant content and append to conversation
612
+ const assistantContent = buildAssistantContent({
613
+ text: result.text,
614
+ toolUseBlocks: result.toolUseBlocks,
615
+ thinkingBlocks: result.thinkingBlocks,
616
+ compactionContent: result.compactionContent,
617
+ });
618
+ messages.push({ role: "assistant", content: assistantContent });
619
+ messages.push({ role: "user", content: finalToolResults });
620
+ // Non-native compaction for OpenAI/Gemini — fires after tool results appended
621
+ const compactionCfg = getCompactionConfig(currentModel);
622
+ if (!compactionCfg.isNative && result.usage.inputTokens >= compactionCfg.triggerTokens) {
623
+ compactionCount++;
624
+ if (compactionCount * compactionCfg.triggerTokens >= compactionCfg.totalBudget) {
625
+ // Budget exhaustion — force wrap-up (same as native compaction budget logic)
626
+ const budgetMsg = "\n[Context budget reached — wrapping up.]";
627
+ if (emitter) {
628
+ emitter.emitText(budgetMsg);
629
+ }
630
+ else {
631
+ callbacks.onText(budgetMsg);
632
+ }
633
+ const summary = await requestProviderCompaction({
634
+ proxyUrl: getProxyUrl(),
635
+ token,
636
+ messages: messages,
637
+ systemPrompt,
638
+ });
639
+ const compactedMessages = [
640
+ ...(summary
641
+ ? [{ role: "assistant", content: [{ type: "compaction", content: summary }] }]
642
+ : []),
643
+ { role: "user", content: [{ type: "text", text: "You have reached the context budget. Please wrap up your current work and provide a final summary of what was accomplished and what remains." }] },
644
+ ];
645
+ messages.length = 0;
646
+ messages.push(...compactedMessages);
647
+ continue;
648
+ }
649
+ // Normal compaction — summarize and preserve last 2 messages
650
+ const summary = await requestProviderCompaction({
651
+ proxyUrl: getProxyUrl(),
652
+ token,
653
+ messages: messages,
654
+ systemPrompt,
655
+ });
656
+ if (summary) {
657
+ const preserved = messages.slice(-2);
658
+ const compactedMessages = [
659
+ { role: "assistant", content: [{ type: "compaction", content: summary }] },
660
+ ...preserved,
661
+ ];
662
+ messages.length = 0;
663
+ messages.push(...compactedMessages);
664
+ iteration--; // Don't count compaction as an iteration
665
+ callbacks.onAutoCompact?.(messages.length + preserved.length, messages.length, Math.round(result.usage.inputTokens * 0.7));
666
+ emitter?.emitCompact(messages.length + preserved.length, messages.length, Math.round(result.usage.inputTokens * 0.7));
667
+ logSpan({ action: "chat.provider_compaction", durationMs: 0, context: turnCtx, storeId: storeId || undefined, details: { compaction_count: compactionCount, provider, model: currentModel, input_tokens: result.usage.inputTokens, trigger_tokens: compactionCfg.triggerTokens } });
668
+ }
669
+ }
670
+ }
671
+ // Telemetry: session summary
672
+ logSpan({
673
+ action: "chat.session_complete",
674
+ durationMs: Date.now() - sessionStart,
675
+ context: { ...turnCtx, inputTokens: totalIn, outputTokens: totalOut, model: activeModel },
676
+ storeId: storeId || undefined,
677
+ details: {
678
+ input_tokens: totalIn, output_tokens: totalOut, total_tokens: totalIn + totalOut,
679
+ cache_creation_tokens: totalCacheCreation, cache_read_tokens: totalCacheRead,
680
+ session_input_tokens: sessionInputTokens, session_output_tokens: sessionOutputTokens,
681
+ model: activeModel,
682
+ },
683
+ });
684
+ const turnCostUsd = estimateCostUsd(totalIn, totalOut, activeModel, totalThinking, totalCacheRead, totalCacheCreation);
685
+ callbacks.onUsage(totalIn, totalOut, totalThinking, activeModel, turnCostUsd, totalCacheRead, totalCacheCreation);
686
+ // Fire SessionEnd hook (non-blocking)
687
+ if (hooks.length > 0) {
688
+ runSessionHook(hooks, "SessionEnd", { session_id: `turn-${sessionStart}` }).catch(() => { });
689
+ }
690
+ const finalText = allAssistantText.length > 0 ? allAssistantText[allAssistantText.length - 1] : "";
691
+ emitter?.emitDone(finalText, messages);
692
+ if (emitter)
693
+ clearGlobalEmitter();
694
+ callbacks.onDone(messages);
695
+ }
696
+ catch (err) {
697
+ const errorMsg = abortSignal?.aborted || err?.message === "Cancelled"
698
+ ? "Cancelled"
699
+ : String(err?.message || err);
700
+ logSpan({
701
+ action: errorMsg === "Cancelled" ? "chat.cancelled" : "chat.fatal_error",
702
+ durationMs: Date.now() - sessionStart,
703
+ context: { ...turnCtx, inputTokens: totalIn, outputTokens: totalOut, model: activeModel },
704
+ storeId: storeId || undefined,
705
+ severity: errorMsg === "Cancelled" ? "info" : "error",
706
+ error: errorMsg === "Cancelled" ? undefined : errorMsg,
707
+ details: { input_tokens: totalIn, output_tokens: totalOut, session_cost_usd: sessionCostUsd, model: activeModel },
708
+ });
709
+ // Capture to error_events (not just audit_logs) for non-cancellations
710
+ if (errorMsg !== "Cancelled") {
711
+ captureError({
712
+ error: err instanceof Error ? err : undefined,
713
+ errorType: "AgentLoopError",
714
+ errorMessage: errorMsg,
715
+ severity: "error",
716
+ traceId: turnCtx.traceId,
717
+ spanId: turnCtx.spanId,
718
+ storeId: storeId || undefined,
719
+ tags: { model: activeModel, turn: String(turnNum) },
720
+ });
721
+ }
722
+ emitter?.emitError(errorMsg);
723
+ if (emitter)
724
+ clearGlobalEmitter();
725
+ callbacks.onError(errorMsg, messages);
726
+ }
727
+ }
728
+ // ============================================================================
729
+ // TELEMETRY HELPERS
730
+ // ============================================================================
731
+ export function truncateResult(output, maxLen) {
732
+ if (output.length <= maxLen)
733
+ return output;
734
+ return output.slice(0, maxLen) + `... (${output.length} chars total)`;
735
+ }
736
+ export function classifyToolError(output) {
737
+ const lower = output.toLowerCase();
738
+ if (lower.includes("timed out") || lower.includes("timeout"))
739
+ return "timeout";
740
+ if (lower.includes("permission denied") || lower.includes("eacces"))
741
+ return "permission";
742
+ if (lower.includes("not found") || lower.includes("no such file"))
743
+ return "not_found";
744
+ if (lower.includes("command not found") || lower.includes("exit code 127"))
745
+ return "command_not_found";
746
+ if (lower.includes("import") && lower.includes("error"))
747
+ return "import_error";
748
+ if (lower.includes("syntax") || lower.includes("parse"))
749
+ return "syntax_error";
750
+ if (lower.includes("externally-managed"))
751
+ return "env_managed";
752
+ return "unknown";
753
+ }
754
+ // Convenience: check if user can use the agent (logged in OR has API key)
755
+ export function canUseAgent() {
756
+ const config = loadConfig();
757
+ const hasToken = !!(config.access_token && config.refresh_token);
758
+ const hasApiKey = !!(process.env.ANTHROPIC_API_KEY || config.anthropic_api_key);
759
+ if (hasToken || hasApiKey)
760
+ return { ready: true };
761
+ return { ready: false, reason: "Run `whale login` to authenticate." };
762
+ }