@qodo/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/LICENSE +118 -0
  2. package/README.md +121 -0
  3. package/dist/api/agent.d.ts +69 -0
  4. package/dist/api/agent.d.ts.map +1 -0
  5. package/dist/api/agent.js +1034 -0
  6. package/dist/api/agent.js.map +1 -0
  7. package/dist/api/analytics.d.ts +43 -0
  8. package/dist/api/analytics.d.ts.map +1 -0
  9. package/dist/api/analytics.js +163 -0
  10. package/dist/api/analytics.js.map +1 -0
  11. package/dist/api/http.d.ts +5 -0
  12. package/dist/api/http.d.ts.map +1 -0
  13. package/dist/api/http.js +59 -0
  14. package/dist/api/http.js.map +1 -0
  15. package/dist/api/index.d.ts +12 -0
  16. package/dist/api/index.d.ts.map +1 -0
  17. package/dist/api/index.js +17 -0
  18. package/dist/api/index.js.map +1 -0
  19. package/dist/api/taskTracking.d.ts +54 -0
  20. package/dist/api/taskTracking.d.ts.map +1 -0
  21. package/dist/api/taskTracking.js +208 -0
  22. package/dist/api/taskTracking.js.map +1 -0
  23. package/dist/api/types.d.ts +92 -0
  24. package/dist/api/types.d.ts.map +1 -0
  25. package/dist/api/types.js +2 -0
  26. package/dist/api/types.js.map +1 -0
  27. package/dist/api/utils.d.ts +8 -0
  28. package/dist/api/utils.d.ts.map +1 -0
  29. package/dist/api/utils.js +54 -0
  30. package/dist/api/utils.js.map +1 -0
  31. package/dist/api/websocket.d.ts +74 -0
  32. package/dist/api/websocket.d.ts.map +1 -0
  33. package/dist/api/websocket.js +685 -0
  34. package/dist/api/websocket.js.map +1 -0
  35. package/dist/auth/index.d.ts +25 -0
  36. package/dist/auth/index.d.ts.map +1 -0
  37. package/dist/auth/index.js +85 -0
  38. package/dist/auth/index.js.map +1 -0
  39. package/dist/clients/index.d.ts +8 -0
  40. package/dist/clients/index.d.ts.map +1 -0
  41. package/dist/clients/index.js +7 -0
  42. package/dist/clients/index.js.map +1 -0
  43. package/dist/clients/info/InfoClient.d.ts +37 -0
  44. package/dist/clients/info/InfoClient.d.ts.map +1 -0
  45. package/dist/clients/info/InfoClient.js +69 -0
  46. package/dist/clients/info/InfoClient.js.map +1 -0
  47. package/dist/clients/info/index.d.ts +4 -0
  48. package/dist/clients/info/index.d.ts.map +1 -0
  49. package/dist/clients/info/index.js +2 -0
  50. package/dist/clients/info/index.js.map +1 -0
  51. package/dist/clients/info/types.d.ts +21 -0
  52. package/dist/clients/info/types.d.ts.map +1 -0
  53. package/dist/clients/info/types.js +2 -0
  54. package/dist/clients/info/types.js.map +1 -0
  55. package/dist/clients/sessions/SessionsClient.d.ts +34 -0
  56. package/dist/clients/sessions/SessionsClient.d.ts.map +1 -0
  57. package/dist/clients/sessions/SessionsClient.js +71 -0
  58. package/dist/clients/sessions/SessionsClient.js.map +1 -0
  59. package/dist/clients/sessions/index.d.ts +4 -0
  60. package/dist/clients/sessions/index.d.ts.map +1 -0
  61. package/dist/clients/sessions/index.js +2 -0
  62. package/dist/clients/sessions/index.js.map +1 -0
  63. package/dist/clients/sessions/types.d.ts +20 -0
  64. package/dist/clients/sessions/types.d.ts.map +1 -0
  65. package/dist/clients/sessions/types.js +2 -0
  66. package/dist/clients/sessions/types.js.map +1 -0
  67. package/dist/config/ConfigManager.d.ts +43 -0
  68. package/dist/config/ConfigManager.d.ts.map +1 -0
  69. package/dist/config/ConfigManager.js +472 -0
  70. package/dist/config/ConfigManager.js.map +1 -0
  71. package/dist/config/index.d.ts +6 -0
  72. package/dist/config/index.d.ts.map +1 -0
  73. package/dist/config/index.js +7 -0
  74. package/dist/config/index.js.map +1 -0
  75. package/dist/config/urlConfig.d.ts +15 -0
  76. package/dist/config/urlConfig.d.ts.map +1 -0
  77. package/dist/config/urlConfig.js +75 -0
  78. package/dist/config/urlConfig.js.map +1 -0
  79. package/dist/constants/errors.d.ts +2 -0
  80. package/dist/constants/errors.d.ts.map +1 -0
  81. package/dist/constants/errors.js +2 -0
  82. package/dist/constants/errors.js.map +1 -0
  83. package/dist/constants/index.d.ts +7 -0
  84. package/dist/constants/index.d.ts.map +1 -0
  85. package/dist/constants/index.js +11 -0
  86. package/dist/constants/index.js.map +1 -0
  87. package/dist/constants/tools.d.ts +4 -0
  88. package/dist/constants/tools.d.ts.map +1 -0
  89. package/dist/constants/tools.js +4 -0
  90. package/dist/constants/tools.js.map +1 -0
  91. package/dist/constants/versions.d.ts +2 -0
  92. package/dist/constants/versions.d.ts.map +1 -0
  93. package/dist/constants/versions.js +2 -0
  94. package/dist/constants/versions.js.map +1 -0
  95. package/dist/context/buildUserContext.d.ts +18 -0
  96. package/dist/context/buildUserContext.d.ts.map +1 -0
  97. package/dist/context/buildUserContext.js +34 -0
  98. package/dist/context/buildUserContext.js.map +1 -0
  99. package/dist/context/index.d.ts +9 -0
  100. package/dist/context/index.d.ts.map +1 -0
  101. package/dist/context/index.js +9 -0
  102. package/dist/context/index.js.map +1 -0
  103. package/dist/context/messageManager.d.ts +42 -0
  104. package/dist/context/messageManager.d.ts.map +1 -0
  105. package/dist/context/messageManager.js +322 -0
  106. package/dist/context/messageManager.js.map +1 -0
  107. package/dist/context/taskFocus.d.ts +2 -0
  108. package/dist/context/taskFocus.d.ts.map +1 -0
  109. package/dist/context/taskFocus.js +26 -0
  110. package/dist/context/taskFocus.js.map +1 -0
  111. package/dist/context/userInput.d.ts +3 -0
  112. package/dist/context/userInput.d.ts.map +1 -0
  113. package/dist/context/userInput.js +20 -0
  114. package/dist/context/userInput.js.map +1 -0
  115. package/dist/index.d.ts +18 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +21 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/mcp/MCPManager.d.ts +125 -0
  120. package/dist/mcp/MCPManager.d.ts.map +1 -0
  121. package/dist/mcp/MCPManager.js +616 -0
  122. package/dist/mcp/MCPManager.js.map +1 -0
  123. package/dist/mcp/approvedTools.d.ts +4 -0
  124. package/dist/mcp/approvedTools.d.ts.map +1 -0
  125. package/dist/mcp/approvedTools.js +19 -0
  126. package/dist/mcp/approvedTools.js.map +1 -0
  127. package/dist/mcp/baseServer.d.ts +75 -0
  128. package/dist/mcp/baseServer.d.ts.map +1 -0
  129. package/dist/mcp/baseServer.js +107 -0
  130. package/dist/mcp/baseServer.js.map +1 -0
  131. package/dist/mcp/builtinServers.d.ts +15 -0
  132. package/dist/mcp/builtinServers.d.ts.map +1 -0
  133. package/dist/mcp/builtinServers.js +155 -0
  134. package/dist/mcp/builtinServers.js.map +1 -0
  135. package/dist/mcp/dynamicBEServer.d.ts +20 -0
  136. package/dist/mcp/dynamicBEServer.d.ts.map +1 -0
  137. package/dist/mcp/dynamicBEServer.js +52 -0
  138. package/dist/mcp/dynamicBEServer.js.map +1 -0
  139. package/dist/mcp/index.d.ts +19 -0
  140. package/dist/mcp/index.d.ts.map +1 -0
  141. package/dist/mcp/index.js +24 -0
  142. package/dist/mcp/index.js.map +1 -0
  143. package/dist/mcp/mcpInitialization.d.ts +2 -0
  144. package/dist/mcp/mcpInitialization.d.ts.map +1 -0
  145. package/dist/mcp/mcpInitialization.js +56 -0
  146. package/dist/mcp/mcpInitialization.js.map +1 -0
  147. package/dist/mcp/servers/filesystem.d.ts +75 -0
  148. package/dist/mcp/servers/filesystem.d.ts.map +1 -0
  149. package/dist/mcp/servers/filesystem.js +992 -0
  150. package/dist/mcp/servers/filesystem.js.map +1 -0
  151. package/dist/mcp/servers/gerrit.d.ts +19 -0
  152. package/dist/mcp/servers/gerrit.d.ts.map +1 -0
  153. package/dist/mcp/servers/gerrit.js +515 -0
  154. package/dist/mcp/servers/gerrit.js.map +1 -0
  155. package/dist/mcp/servers/git.d.ts +18 -0
  156. package/dist/mcp/servers/git.d.ts.map +1 -0
  157. package/dist/mcp/servers/git.js +441 -0
  158. package/dist/mcp/servers/git.js.map +1 -0
  159. package/dist/mcp/servers/ripgrep.d.ts +34 -0
  160. package/dist/mcp/servers/ripgrep.d.ts.map +1 -0
  161. package/dist/mcp/servers/ripgrep.js +517 -0
  162. package/dist/mcp/servers/ripgrep.js.map +1 -0
  163. package/dist/mcp/servers/shell.d.ts +20 -0
  164. package/dist/mcp/servers/shell.d.ts.map +1 -0
  165. package/dist/mcp/servers/shell.js +603 -0
  166. package/dist/mcp/servers/shell.js.map +1 -0
  167. package/dist/mcp/serversRegistry.d.ts +55 -0
  168. package/dist/mcp/serversRegistry.d.ts.map +1 -0
  169. package/dist/mcp/serversRegistry.js +410 -0
  170. package/dist/mcp/serversRegistry.js.map +1 -0
  171. package/dist/mcp/toolProcessor.d.ts +42 -0
  172. package/dist/mcp/toolProcessor.d.ts.map +1 -0
  173. package/dist/mcp/toolProcessor.js +200 -0
  174. package/dist/mcp/toolProcessor.js.map +1 -0
  175. package/dist/mcp/types.d.ts +29 -0
  176. package/dist/mcp/types.d.ts.map +1 -0
  177. package/dist/mcp/types.js +2 -0
  178. package/dist/mcp/types.js.map +1 -0
  179. package/dist/parser/index.d.ts +72 -0
  180. package/dist/parser/index.d.ts.map +1 -0
  181. package/dist/parser/index.js +967 -0
  182. package/dist/parser/index.js.map +1 -0
  183. package/dist/parser/types.d.ts +153 -0
  184. package/dist/parser/types.d.ts.map +1 -0
  185. package/dist/parser/types.js +6 -0
  186. package/dist/parser/types.js.map +1 -0
  187. package/dist/parser/utils.d.ts +18 -0
  188. package/dist/parser/utils.d.ts.map +1 -0
  189. package/dist/parser/utils.js +64 -0
  190. package/dist/parser/utils.js.map +1 -0
  191. package/dist/sdk/QodoSDK.d.ts +152 -0
  192. package/dist/sdk/QodoSDK.d.ts.map +1 -0
  193. package/dist/sdk/QodoSDK.js +786 -0
  194. package/dist/sdk/QodoSDK.js.map +1 -0
  195. package/dist/sdk/bootstrap.d.ts +16 -0
  196. package/dist/sdk/bootstrap.d.ts.map +1 -0
  197. package/dist/sdk/bootstrap.js +21 -0
  198. package/dist/sdk/bootstrap.js.map +1 -0
  199. package/dist/sdk/builders.d.ts +54 -0
  200. package/dist/sdk/builders.d.ts.map +1 -0
  201. package/dist/sdk/builders.js +117 -0
  202. package/dist/sdk/builders.js.map +1 -0
  203. package/dist/sdk/defaults.d.ts +11 -0
  204. package/dist/sdk/defaults.d.ts.map +1 -0
  205. package/dist/sdk/defaults.js +39 -0
  206. package/dist/sdk/defaults.js.map +1 -0
  207. package/dist/sdk/discovery.d.ts +2 -0
  208. package/dist/sdk/discovery.d.ts.map +1 -0
  209. package/dist/sdk/discovery.js +25 -0
  210. package/dist/sdk/discovery.js.map +1 -0
  211. package/dist/sdk/events.d.ts +168 -0
  212. package/dist/sdk/events.d.ts.map +1 -0
  213. package/dist/sdk/events.js +52 -0
  214. package/dist/sdk/events.js.map +1 -0
  215. package/dist/sdk/index.d.ts +17 -0
  216. package/dist/sdk/index.d.ts.map +1 -0
  217. package/dist/sdk/index.js +17 -0
  218. package/dist/sdk/index.js.map +1 -0
  219. package/dist/sdk/runner/AgentRunner.d.ts +22 -0
  220. package/dist/sdk/runner/AgentRunner.d.ts.map +1 -0
  221. package/dist/sdk/runner/AgentRunner.js +222 -0
  222. package/dist/sdk/runner/AgentRunner.js.map +1 -0
  223. package/dist/sdk/runner/finalize.d.ts +9 -0
  224. package/dist/sdk/runner/finalize.d.ts.map +1 -0
  225. package/dist/sdk/runner/finalize.js +115 -0
  226. package/dist/sdk/runner/finalize.js.map +1 -0
  227. package/dist/sdk/runner/formats.d.ts +7 -0
  228. package/dist/sdk/runner/formats.d.ts.map +1 -0
  229. package/dist/sdk/runner/formats.js +91 -0
  230. package/dist/sdk/runner/formats.js.map +1 -0
  231. package/dist/sdk/runner/index.d.ts +9 -0
  232. package/dist/sdk/runner/index.d.ts.map +1 -0
  233. package/dist/sdk/runner/index.js +9 -0
  234. package/dist/sdk/runner/index.js.map +1 -0
  235. package/dist/sdk/runner/progress.d.ts +3 -0
  236. package/dist/sdk/runner/progress.d.ts.map +1 -0
  237. package/dist/sdk/runner/progress.js +16 -0
  238. package/dist/sdk/runner/progress.js.map +1 -0
  239. package/dist/sdk/schemas.d.ts +50 -0
  240. package/dist/sdk/schemas.d.ts.map +1 -0
  241. package/dist/sdk/schemas.js +145 -0
  242. package/dist/sdk/schemas.js.map +1 -0
  243. package/dist/session/SessionContext.d.ts +86 -0
  244. package/dist/session/SessionContext.d.ts.map +1 -0
  245. package/dist/session/SessionContext.js +395 -0
  246. package/dist/session/SessionContext.js.map +1 -0
  247. package/dist/session/environment.d.ts +42 -0
  248. package/dist/session/environment.d.ts.map +1 -0
  249. package/dist/session/environment.js +27 -0
  250. package/dist/session/environment.js.map +1 -0
  251. package/dist/session/history.d.ts +3 -0
  252. package/dist/session/history.d.ts.map +1 -0
  253. package/dist/session/history.js +67 -0
  254. package/dist/session/history.js.map +1 -0
  255. package/dist/session/index.d.ts +10 -0
  256. package/dist/session/index.d.ts.map +1 -0
  257. package/dist/session/index.js +9 -0
  258. package/dist/session/index.js.map +1 -0
  259. package/dist/session/serverData.d.ts +38 -0
  260. package/dist/session/serverData.d.ts.map +1 -0
  261. package/dist/session/serverData.js +241 -0
  262. package/dist/session/serverData.js.map +1 -0
  263. package/dist/tracking/Tracker.d.ts +55 -0
  264. package/dist/tracking/Tracker.d.ts.map +1 -0
  265. package/dist/tracking/Tracker.js +217 -0
  266. package/dist/tracking/Tracker.js.map +1 -0
  267. package/dist/tracking/index.d.ts +8 -0
  268. package/dist/tracking/index.d.ts.map +1 -0
  269. package/dist/tracking/index.js +8 -0
  270. package/dist/tracking/index.js.map +1 -0
  271. package/dist/tracking/schemas.d.ts +292 -0
  272. package/dist/tracking/schemas.d.ts.map +1 -0
  273. package/dist/tracking/schemas.js +91 -0
  274. package/dist/tracking/schemas.js.map +1 -0
  275. package/dist/types.d.ts +4 -0
  276. package/dist/types.d.ts.map +1 -0
  277. package/dist/types.js +2 -0
  278. package/dist/types.js.map +1 -0
  279. package/dist/utils/extractSetFlags.d.ts +6 -0
  280. package/dist/utils/extractSetFlags.d.ts.map +1 -0
  281. package/dist/utils/extractSetFlags.js +16 -0
  282. package/dist/utils/extractSetFlags.js.map +1 -0
  283. package/dist/utils/formatTimeAgo.d.ts +2 -0
  284. package/dist/utils/formatTimeAgo.d.ts.map +1 -0
  285. package/dist/utils/formatTimeAgo.js +20 -0
  286. package/dist/utils/formatTimeAgo.js.map +1 -0
  287. package/dist/utils/index.d.ts +12 -0
  288. package/dist/utils/index.d.ts.map +1 -0
  289. package/dist/utils/index.js +12 -0
  290. package/dist/utils/index.js.map +1 -0
  291. package/dist/utils/machineId.d.ts +14 -0
  292. package/dist/utils/machineId.d.ts.map +1 -0
  293. package/dist/utils/machineId.js +66 -0
  294. package/dist/utils/machineId.js.map +1 -0
  295. package/dist/utils/pathUtils.d.ts +22 -0
  296. package/dist/utils/pathUtils.d.ts.map +1 -0
  297. package/dist/utils/pathUtils.js +54 -0
  298. package/dist/utils/pathUtils.js.map +1 -0
  299. package/dist/version.d.ts +2 -0
  300. package/dist/version.d.ts.map +1 -0
  301. package/dist/version.js +23 -0
  302. package/dist/version.js.map +1 -0
  303. package/package.json +93 -0
@@ -0,0 +1,1034 @@
1
+ import { v4 as uuid } from "uuid";
2
+ import { EventEmitter } from "events";
3
+ import { MCPManager } from "../mcp/index.js";
4
+ import { SERVER_ERROR_MESSAGE } from "../constants/errors.js";
5
+ import { getUserData, getCurrentGitSha1 } from "./utils.js";
6
+ import { EndNode, UserResponse } from "../constants/tools.js";
7
+ import { SessionContext } from "../session/index.js";
8
+ import { TaskTracker } from "./taskTracking.js";
9
+ import { toolProcessorManager } from "../mcp/index.js";
10
+ import process from "node:process";
11
+ import { ServerData } from "../session/index.js";
12
+ import { httpRequest } from "./http.js";
13
+ import { WebSocketClient, ConnectionState } from "./websocket.js";
14
+ import { getCurrentEnvironment } from "../session/index.js";
15
+ // Enhanced configuration with robust defaults
16
+ const CONFIG = {
17
+ REQUEST_TIMEOUT: 210000,
18
+ DELIMITER: "\n\n",
19
+ RECONNECT_ATTEMPTS: 5, // Increased from 3
20
+ INITIAL_RECONNECT_DELAY: 1000,
21
+ MAX_RECONNECT_DELAY: 30000, // Maximum 30 seconds
22
+ RECONNECT_BACKOFF_FACTOR: 1.5, // Exponential backoff multiplier
23
+ HEARTBEAT_INTERVAL: 30000,
24
+ HEARTBEAT_TIMEOUT: 60000, // 2x heartbeat interval
25
+ CONNECTION_TIMEOUT: 15000, // Increased from 10s
26
+ MESSAGE_QUEUE_MAX_SIZE: 1000,
27
+ MAX_PENDING_TOOLS: 50,
28
+ CIRCUIT_BREAKER_THRESHOLD: 5, // Failures before opening circuit
29
+ CIRCUIT_BREAKER_TIMEOUT: 60000, // 1 minute before trying again
30
+ IDLE_TIMEOUT: 60000, // 1 minute idle timeout before disconnecting
31
+ SUMMARIZATION_PREFETCH_TIMEOUT: 60000, // 60 seconds for summarization prefetch
32
+ };
33
+ export class AgentAPI extends EventEmitter {
34
+ sessionContext;
35
+ wsClient;
36
+ shouldDebugLog() {
37
+ const env = getCurrentEnvironment();
38
+ if (env?.sdkMode) {
39
+ return !!env.sdkDebug || process.env.QODO_DEBUG === 'true';
40
+ }
41
+ return this.sessionContext?.isDebugMode?.() || process.env.QODO_DEBUG === 'true';
42
+ }
43
+ debug(...args) {
44
+ if (this.shouldDebugLog()) {
45
+ // eslint-disable-next-line no-console
46
+ console.debug(...args);
47
+ }
48
+ }
49
+ currentSession;
50
+ pendingToolRequests = new Map();
51
+ // Tracks tool executions that are currently running (mainly relevant for auto-approved tools).
52
+ // Without this, the backend "Ready" signal can be misinterpreted as end-of-turn and we may
53
+ // emit forceStop early, which causes the CLI loading spinner to disappear while tools are still running.
54
+ inFlightToolCalls = new Set();
55
+ latestSessionId = "";
56
+ lastToolId = "";
57
+ mcpManager;
58
+ taskTracker;
59
+ pendingResumeError = null;
60
+ hotStartInProgress = false;
61
+ userHasControl = false; // Track if user has control (editor is shown)
62
+ // IPC bridge for sub-process orchestration
63
+ ipcBridge = null;
64
+ constructor(sessionContext) {
65
+ super();
66
+ this.sessionContext = sessionContext;
67
+ this.sessionContext = sessionContext || SessionContext.getInstance();
68
+ // MCPManager may not be initialized when MCP is disabled via config (tools = []).
69
+ // In that case, gracefully operate without tools by leaving mcpManager undefined.
70
+ try {
71
+ this.mcpManager = MCPManager.getInstance();
72
+ }
73
+ catch {
74
+ this.mcpManager = undefined;
75
+ }
76
+ this.taskTracker = new TaskTracker(this.sessionContext, this.mcpManager);
77
+ // Initialize WebSocket client
78
+ this.wsClient = new WebSocketClient();
79
+ this.setupWebSocketHandlers();
80
+ // Don't pre-fetch session ID - use the session ID provided by operations
81
+ // Listen for network connectivity changes if available
82
+ this.setupNetworkListeners();
83
+ // In subprocess mode, set up IPC bridge for tool approvals and inherited approvals
84
+ if (process.env.QODO_SUBPROCESS_MODE === 'true') {
85
+ try {
86
+ // Dynamic import without await (constructor cannot be async)
87
+ // @ts-ignore
88
+ import('../orchestrator/subprocessIpcBridge.js').then((mod) => {
89
+ try {
90
+ this.ipcBridge = new mod.SubprocessIpcBridge(this.mcpManager);
91
+ }
92
+ catch { }
93
+ }).catch(() => { });
94
+ }
95
+ catch { }
96
+ }
97
+ }
98
+ async ensureConnected(sessionId, requestId) {
99
+ try {
100
+ let finalSessionId = sessionId;
101
+ // Only fetch a session ID from server if none is provided
102
+ if (!finalSessionId) {
103
+ if (!this.latestSessionId) {
104
+ const serverData = ServerData.getInstance();
105
+ this.latestSessionId = await serverData.fetchNewSessionId();
106
+ this.debug(`Fetched new session ID from server: ${this.latestSessionId}`);
107
+ }
108
+ finalSessionId = this.latestSessionId;
109
+ }
110
+ else {
111
+ // Update stored session ID with the provided one
112
+ this.latestSessionId = sessionId;
113
+ }
114
+ await this.wsClient.connect(finalSessionId, requestId);
115
+ this.debug('WebSocket connected for session:', finalSessionId);
116
+ }
117
+ catch (error) {
118
+ this.debug('WebSocket connection failed:', error);
119
+ throw error;
120
+ }
121
+ }
122
+ setupWebSocketHandlers() {
123
+ // Handle incoming messages
124
+ this.wsClient.on('message', (message) => {
125
+ this.handleWebSocketMessage(message);
126
+ });
127
+ // Handle connection state changes
128
+ this.wsClient.on('stateChanged', (newState) => {
129
+ this.debug('WebSocket state changed to:', newState);
130
+ this.emit('connectionStateChanged', newState);
131
+ });
132
+ this.wsClient.on('disconnected', (info) => {
133
+ this.debug('WebSocket disconnected:', info.reason);
134
+ // Handle pending operations
135
+ if (info.reason !== 'idle' && info.reason !== 'normal') {
136
+ this.handleUnexpectedDisconnection();
137
+ }
138
+ });
139
+ this.wsClient.on('error', (error) => {
140
+ this.debug('WebSocket error:', error);
141
+ this.handleWebSocketError(error);
142
+ });
143
+ // Handle Ready protocol events
144
+ this.wsClient.on('readyReceived', async (checkpointId, previousReadyState) => {
145
+ this.debug('[AgentAPI] Server ready for next message', checkpointId ? `| Checkpoint: ${checkpointId.substring(0, 8)}` : '');
146
+ // IMPORTANT:
147
+ // Some backend flows may not emit an explicit EndNode/forceStop message.
148
+ // Historically we treated the WebSocket "Ready" signal as an end-of-turn indicator.
149
+ // However, during tool-use the backend may send "Ready" to signal it is ready to receive
150
+ // tool answers (IDERetrievalAnswer) rather than the assistant being done producing output.
151
+ //
152
+ // To avoid prematurely finalizing runs (especially with YOLO/always where tools are auto-approved),
153
+ // we apply a stricter heuristic:
154
+ // - only finalize when Ready arrives after a UserQuery (MESSAGE_SENT)
155
+ // - AND there are no approval prompts pending
156
+ // - AND there are no tool executions currently in-flight (auto-approved tools)
157
+ // - AND the WebSocket client has observed at least one non-Ready response since the UserQuery
158
+ // (otherwise this Ready can be part of an intermediate tool handshake)
159
+ try {
160
+ const hasResponses = this.wsClient.hasReceivedResponsesSinceLastMessage?.() === true;
161
+ if (this.currentSession?.waitingForResponse &&
162
+ this.pendingToolRequests.size === 0 &&
163
+ this.inFlightToolCalls.size === 0 &&
164
+ previousReadyState === 'MESSAGE_SENT' &&
165
+ hasResponses) {
166
+ this.currentSession.waitingForResponse = false;
167
+ await this.responseCallback({ forceStop: true });
168
+ }
169
+ }
170
+ catch { }
171
+ });
172
+ this.wsClient.on('checkpointRecovery', (info) => {
173
+ this.debug('[AgentAPI] Checkpoint recovery initiated', `| Reason: ${info.reason}`, info.checkpoint ? `| Checkpoint: ${info.checkpoint.substring(0, 8)}` : '');
174
+ });
175
+ this.wsClient.on('readyStateChanged', (info) => {
176
+ if (this.sessionContext?.isDebugMode()) {
177
+ this.debug('[AgentAPI] Ready state changed:', info);
178
+ }
179
+ });
180
+ }
181
+ setupNetworkListeners() {
182
+ // Node.js doesn't have built-in network change detection
183
+ // This could be enhanced with platform-specific implementations
184
+ if (typeof window !== 'undefined') {
185
+ window.addEventListener('online', () => this.handleNetworkOnline());
186
+ window.addEventListener('offline', () => this.handleNetworkOffline());
187
+ }
188
+ }
189
+ handleNetworkOnline() {
190
+ this.debug('Network connectivity restored');
191
+ if (this.wsClient.getState() === ConnectionState.FAILED && this.currentSession) {
192
+ // WebSocket client will handle reconnection automatically
193
+ this.debug('Network restored, WebSocket client will auto-reconnect');
194
+ }
195
+ }
196
+ handleNetworkOffline() {
197
+ this.debug('Network connectivity lost');
198
+ // WebSocket client will detect and handle the disconnection
199
+ }
200
+ getConnectionState() {
201
+ return this.wsClient.getState();
202
+ }
203
+ onConnectionStateChange(callback) {
204
+ this.on('connectionStateChanged', callback);
205
+ return () => this.off('connectionStateChanged', callback);
206
+ }
207
+ handleWebSocketError(error) {
208
+ // Handle WebSocket errors
209
+ this.debug('WebSocket error handled:', error);
210
+ // The WebSocketClient will handle reconnection, we just need to handle the business logic
211
+ if (this.currentSession?.waitingForResponse) {
212
+ this.handleError(error);
213
+ }
214
+ }
215
+ async handleUnexpectedDisconnection() {
216
+ try {
217
+ // If a tool approval is pending, keep the approval prompt active and do nothing here
218
+ if (this.pendingToolRequests.size > 0) {
219
+ return;
220
+ }
221
+ if (this.currentSession?.waitingForResponse) {
222
+ this.currentSession.waitingForResponse = false;
223
+ // Finish task when returning control due to WebSocket close
224
+ await this.taskTracker.trackTaskFinish(false);
225
+ await this.responseCallback({ forceStop: true });
226
+ }
227
+ }
228
+ catch (e) {
229
+ // Best-effort; ignore errors here
230
+ }
231
+ }
232
+ setUserHasControl(hasControl) {
233
+ const previousControl = this.userHasControl;
234
+ this.userHasControl = hasControl;
235
+ if (hasControl && !previousControl) {
236
+ // User is getting control back - finish the current task only if there's an active tracking
237
+ if (this.taskTracker.hasActiveTracking()) {
238
+ this.taskTracker.trackTaskFinish(false).catch(error => {
239
+ this.debug('Error finishing task:', error);
240
+ });
241
+ }
242
+ // Keep WebSocket alive when user has control
243
+ this.wsClient.keepAlive();
244
+ }
245
+ }
246
+ resetActivityTimer() {
247
+ // Public method to keep the connection alive when user is active
248
+ this.wsClient.keepAlive();
249
+ }
250
+ async toolApproval(identifier, approved) {
251
+ const pendingToolData = this.pendingToolRequests.get(identifier);
252
+ if (!pendingToolData) {
253
+ return;
254
+ }
255
+ // Check pending tool limits
256
+ if (this.pendingToolRequests.size > CONFIG.MAX_PENDING_TOOLS) {
257
+ this.debug(`Too many pending tools (${this.pendingToolRequests.size}), cleaning up oldest`);
258
+ this.cleanupOldestPendingTools();
259
+ }
260
+ this.currentSession = pendingToolData.agentSession;
261
+ this.currentSession.abortController = new AbortController();
262
+ if (approved) {
263
+ await this.callTool(pendingToolData.toolData);
264
+ }
265
+ else {
266
+ await this.handleToolDecline(pendingToolData.toolData);
267
+ }
268
+ this.pendingToolRequests.delete(identifier);
269
+ }
270
+ cleanupOldestPendingTools() {
271
+ // Remove oldest pending tools if we exceed the limit
272
+ const entries = Array.from(this.pendingToolRequests.entries());
273
+ const toRemove = entries.slice(0, entries.length - CONFIG.MAX_PENDING_TOOLS + 10); // Remove 10 extra
274
+ for (const [id] of toRemove) {
275
+ this.pendingToolRequests.delete(id);
276
+ }
277
+ }
278
+ async tryPrefetchSummarization(sessionIds, taskFocus) {
279
+ const summaries = {};
280
+ try {
281
+ if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
282
+ return summaries;
283
+ }
284
+ await Promise.allSettled(sessionIds.map(async (sid) => {
285
+ try {
286
+ const res = await httpRequest({
287
+ method: "POST",
288
+ url: "v2/agentic/get-session-summarization",
289
+ data: { session_id: sid, request_id: uuid(), agent_type: "cli", ...(taskFocus ? { task_focus: taskFocus } : {}) },
290
+ timeout: CONFIG.SUMMARIZATION_PREFETCH_TIMEOUT * 2
291
+ });
292
+ const summary = typeof res?.summary === 'string' ? res.summary.trim() : '';
293
+ if (summary) {
294
+ summaries[sid] = summary;
295
+ }
296
+ }
297
+ catch (e) {
298
+ if (this.sessionContext?.isDebugMode()) {
299
+ this.debug('Summarization prefetch failed for', sid, e instanceof Error ? e.message : e);
300
+ }
301
+ }
302
+ }));
303
+ return summaries;
304
+ }
305
+ catch (e) {
306
+ if (this.sessionContext?.isDebugMode()) {
307
+ this.debug('Summarization prefetch failed:', e instanceof Error ? e.message : e);
308
+ }
309
+ return summaries;
310
+ }
311
+ }
312
+ async resumeTask(agentSession) {
313
+ try {
314
+ this.latestSessionId = agentSession.sessionId;
315
+ await this.initializeTask(agentSession);
316
+ // Ensure connected with correct session ID
317
+ await this.ensureConnected(agentSession.sessionId, agentSession.requestId);
318
+ await this.sendUserQuery();
319
+ }
320
+ catch (error) {
321
+ await this.handleError(error);
322
+ }
323
+ }
324
+ async startTask(agentSession) {
325
+ try {
326
+ // Reset idle timer when starting a new task
327
+ this.resetActivityTimer();
328
+ this.latestSessionId = agentSession.sessionId;
329
+ await this.initializeTask(agentSession);
330
+ // Ensure connected with correct session ID
331
+ await this.ensureConnected(agentSession.sessionId, agentSession.requestId);
332
+ // Send the task request
333
+ await this.sendUserQuery();
334
+ }
335
+ catch (error) {
336
+ await this.handleError(error);
337
+ }
338
+ }
339
+ async startHotTask(agentSession) {
340
+ try {
341
+ this.debug("Starting hot task with session:", agentSession.sessionId);
342
+ // Reset idle timer when starting a hot task
343
+ this.resetActivityTimer();
344
+ await this.initializeTask(agentSession);
345
+ // Ensure connected (will update session ID if needed)
346
+ await this.ensureConnected(agentSession.sessionId, agentSession.requestId);
347
+ this.hotStartInProgress = true;
348
+ this.debug('Hot task initialized, waiting for user input');
349
+ }
350
+ catch (error) {
351
+ await this.handleError(error);
352
+ }
353
+ }
354
+ async sendHotStartUserRequest(userRequest, images) {
355
+ if (this.wsClient.getState() !== ConnectionState.CONNECTED) {
356
+ // Try to connect if not connected
357
+ await this.ensureConnected();
358
+ }
359
+ try {
360
+ // Reset idle timer when sending user request
361
+ this.resetActivityTimer();
362
+ this.setUserHasControl(false);
363
+ // Update current session with the user request
364
+ if (this.currentSession) {
365
+ this.currentSession.abortController = new AbortController();
366
+ this.currentSession.data = {
367
+ ...this.currentSession.data,
368
+ user_request: userRequest,
369
+ answer: undefined,
370
+ ...(images && images.length > 0 && { images })
371
+ };
372
+ this.latestSessionId = this.currentSession.sessionId;
373
+ }
374
+ // Mark hot start as complete and send the full request data
375
+ this.hotStartInProgress = false;
376
+ await this.sendUserQuery();
377
+ }
378
+ catch (error) {
379
+ console.error('Error sending hot start user request:', error);
380
+ await this.handleError(error);
381
+ }
382
+ }
383
+ async finishCurrentTask(isError = false) {
384
+ await this.taskTracker.trackTaskFinish(isError);
385
+ }
386
+ collectBaseData() {
387
+ // Get project root paths from session context
388
+ const projectRootPaths = this.sessionContext.getProjectRootPaths();
389
+ const command = this.currentSession?.command || this.sessionContext.getCommand();
390
+ const execCwd = this.sessionContext?.getExecutionCwd?.();
391
+ const baseData = {
392
+ projects_root_path: projectRootPaths,
393
+ cwd: execCwd || (Array.isArray(projectRootPaths) && projectRootPaths.length > 0 ? projectRootPaths[0] : process.cwd()),
394
+ };
395
+ const prev = this.sessionContext.getPreviousSessionsSummarization();
396
+ if (prev && prev.length > 0) {
397
+ const sections = prev.map((item, i) => {
398
+ const sessionId = typeof item === 'string' ? String(i + 1) : item.sessionId;
399
+ const summary = typeof item === 'string' ? item : item.summary;
400
+ return `### Session ${sessionId}\n${(summary || '').trim()}`;
401
+ });
402
+ baseData.previous_sessions_summarization = sections.join('\n\n---\n\n');
403
+ }
404
+ if (command.instructions || this.sessionContext.getGeneralInstructions()) {
405
+ baseData.instructions = command.instructions || this.sessionContext.getGeneralInstructions();
406
+ }
407
+ if (this.sessionContext.getSystemPrompt()) {
408
+ baseData.system_prompt = this.sessionContext.getSystemPrompt();
409
+ }
410
+ return baseData;
411
+ }
412
+ async initializeTask(agentSession) {
413
+ this.currentSession = agentSession;
414
+ this.currentSession.abortController = new AbortController();
415
+ }
416
+ cancelCurrentSession() {
417
+ this.requestCancellation();
418
+ }
419
+ requestCancellation() {
420
+ this.debug('requestCancellation called - aborting session and disconnecting WebSocket');
421
+ // Use AbortController for atomic cancellation state management
422
+ if (this.currentSession?.abortController && !this.currentSession?.abortController.signal.aborted) {
423
+ this.currentSession?.abortController.abort();
424
+ this.debug('AbortController signal sent');
425
+ }
426
+ // Disconnect and reconnect WebSocket to stop any ongoing communication
427
+ this.disconnectAndReconnect();
428
+ }
429
+ disconnectAndReconnect() {
430
+ try {
431
+ this.debug('Disconnecting and reconnecting WebSocket due to cancellation');
432
+ this.wsClient.disconnect();
433
+ // Reconnect will happen automatically when the next request is made
434
+ }
435
+ catch (error) {
436
+ this.debug('Error during disconnect/reconnect:', error);
437
+ }
438
+ }
439
+ async checkCancellation() {
440
+ // Use AbortController signal for atomic cancellation state checking
441
+ if (this.currentSession?.abortController?.signal.aborted) {
442
+ await this.handleCancellation();
443
+ return true;
444
+ }
445
+ return false;
446
+ }
447
+ async handleCancellation() {
448
+ // Finish task tracking before sending stop signal
449
+ await this.taskTracker.trackTaskFinish(true);
450
+ // Send stop signal
451
+ await this.responseCallback({
452
+ forceStop: true
453
+ });
454
+ // WebSocket stays connected for potential new requests
455
+ }
456
+ async sendUserQuery() {
457
+ if (!this.currentSession)
458
+ return;
459
+ const data = this.currentSession.data || {};
460
+ // Track task start when actually sending user request to backend
461
+ if (this.taskTracker.shouldTrackUserRequest(data)) {
462
+ // Initialize tracking for this user turn
463
+ this.taskTracker.initializeTaskTracking(this.currentSession);
464
+ }
465
+ // Prepare base data first so we can merge/inject instructions properly
466
+ const baseData = this.collectBaseData();
467
+ const command = this.currentSession.command || this.sessionContext.getCommand();
468
+ if (command?.output_schema) {
469
+ data.output_schema = command.output_schema;
470
+ }
471
+ const flags = this.sessionContext.getFlags();
472
+ if (flags?.max_iterations) {
473
+ data.max_iterations = flags.max_iterations;
474
+ }
475
+ if (this.sessionContext.getExecutionStrategy()) {
476
+ data.execution_strategy = this.sessionContext.getExecutionStrategy();
477
+ }
478
+ if (this.sessionContext.getQodoMd()) {
479
+ data.qodomd = this.sessionContext.getQodoMd();
480
+ }
481
+ data.custom_model = this.sessionContext.getModel();
482
+ // Inject special instruction for subprocess user input when enabled
483
+ try {
484
+ if (process.env.QODO_SUBPROCESS_MODE === 'true') {
485
+ const inject = [
486
+ '## IMPORTANT NOTE ##',
487
+ 'You are running in CI, which means you are not allowed, and cannot ask the user any questions, and you must not wait for user input. You must complete the task without any user interaction.',
488
+ "Try to make educated guesses based on the information you have, and use the tools available to you to gather more information.",
489
+ "If you cannot complete the task, you must return an error message that explains why you cannot complete the task. Do that as a last resort, after trying all the tools available to you.",
490
+ "## END IMPORTANT NOTE ##",
491
+ ].join('\n');
492
+ const currentInstructions = data.instructions ?? baseData.instructions ?? '';
493
+ data.instructions = currentInstructions
494
+ ? `${currentInstructions}\n\n${inject}`
495
+ : inject;
496
+ }
497
+ }
498
+ catch { }
499
+ const enabledToolsMap = this.mcpManager?.getEnabledTools(this.sessionContext.getAvailableTools(), this.sessionContext.getIgnoreTools());
500
+ const requestData = {
501
+ agent_type: "cli",
502
+ session_id: this.currentSession.sessionId,
503
+ user_data: getUserData(),
504
+ git_sha1: getCurrentGitSha1(),
505
+ tools: this.mcpManager && enabledToolsMap ? Object.fromEntries(enabledToolsMap) : {},
506
+ permissions: this.sessionContext.getPermissions(),
507
+ ...baseData,
508
+ ...data,
509
+ };
510
+ // Debug snapshot for UserQuery tools
511
+ try {
512
+ const perServerCounts = Object.fromEntries(Object.entries(requestData.tools || {}).map(([srv, arr]) => [srv, Array.isArray(arr) ? arr.length : 0]));
513
+ const totalTools = Object.values(requestData.tools || {}).reduce((acc, v) => acc + (Array.isArray(v) ? v.length : 0), 0);
514
+ this.debug('[AgentAPI] UserQuery tools snapshot:', { servers: Object.keys(requestData.tools || {}), perServerCounts, totalTools });
515
+ }
516
+ catch (e) {
517
+ this.debug('[AgentAPI] Failed to log UserQuery tools snapshot:', e instanceof Error ? e.message : e);
518
+ }
519
+ this.currentSession.waitingForResponse = true;
520
+ this.currentSession.lastPacketTimestamp = Date.now();
521
+ // Send via WebSocket client with new protocol format
522
+ try {
523
+ await this.wsClient.sendMessage('UserQuery', requestData);
524
+ }
525
+ catch (error) {
526
+ console.error('Error sending UserQuery message:', error);
527
+ await this.handleError(error);
528
+ }
529
+ }
530
+ async handleWebSocketMessage(message) {
531
+ try {
532
+ await this.processMessage(message);
533
+ }
534
+ catch (error) {
535
+ console.error('Error processing WebSocket message:', error);
536
+ await this.handleError(error);
537
+ }
538
+ }
539
+ async processMessage(message) {
540
+ if (await this.checkCancellation()) {
541
+ return;
542
+ }
543
+ // Update last packet timestamp
544
+ if (this.currentSession) {
545
+ this.currentSession.lastPacketTimestamp = Date.now();
546
+ }
547
+ try {
548
+ const data = JSON.parse(message);
549
+ // Handle backend error envelope: { error: string, message: string }
550
+ if (data && typeof data === 'object' && 'error' in data && 'message' in data) {
551
+ try {
552
+ await this.responseCallback({ error: String(data.message) });
553
+ // Only force return to input if no tool approvals are pending
554
+ if (this.pendingToolRequests.size === 0) {
555
+ await this.taskTracker.trackTaskFinish(true);
556
+ await this.responseCallback({ forceStop: true });
557
+ }
558
+ }
559
+ catch (e) {
560
+ console.error('Error handling backend error message:', e);
561
+ }
562
+ return;
563
+ }
564
+ this.wsClient.startReadyTimer();
565
+ await this.handleTaskResponse(data);
566
+ }
567
+ catch (error) {
568
+ // Don't process errors if we have a pending resume error
569
+ if (this.pendingResumeError) {
570
+ return;
571
+ }
572
+ throw new Error("Failed to process message");
573
+ }
574
+ }
575
+ async handleTaskResponseError(response) {
576
+ if (response.type === "error" && this.currentSession) {
577
+ // Extract error message from either response.message or response.data.tool_args.content
578
+ let errorMessage;
579
+ if (response.message) {
580
+ errorMessage = response.message;
581
+ }
582
+ else if (response.data && typeof response.data === 'object') {
583
+ const data = response.data;
584
+ if (data.tool === "UserResponse" && data.tool_args?.content) {
585
+ errorMessage = data.tool_args.content;
586
+ }
587
+ }
588
+ if (errorMessage) {
589
+ // Check if this is a "resume" error that should be retried
590
+ if (errorMessage.toLowerCase().includes("resume")) {
591
+ // Store the error to be thrown after stream completes
592
+ this.pendingResumeError = new Error(errorMessage);
593
+ return true;
594
+ }
595
+ if (response.error === "Timeout" && this.hotStartInProgress) {
596
+ this.hotStartInProgress = false;
597
+ this.latestSessionId = "";
598
+ return true;
599
+ }
600
+ // Send the error message
601
+ await this.responseCallback({
602
+ error: errorMessage,
603
+ });
604
+ // Track task finish with error before sending stop signal
605
+ await this.taskTracker.trackTaskFinish(true);
606
+ // Send stop signal
607
+ await this.responseCallback({
608
+ forceStop: true,
609
+ });
610
+ return true;
611
+ }
612
+ }
613
+ return false;
614
+ }
615
+ async handleTaskResponse(response) {
616
+ // Check for cancellation at the start of processing each response
617
+ this.debug("Processing task response:", response);
618
+ if (await this.checkCancellation()) {
619
+ return;
620
+ }
621
+ if (await this.handleTaskResponseError(response)) {
622
+ return;
623
+ }
624
+ if (!this.isValidTaskResponse(response)) {
625
+ return;
626
+ }
627
+ const toolData = response.data;
628
+ // Ensure identifier is always set
629
+ if (!toolData.identifier) {
630
+ if (toolData.tool === UserResponse && this.lastToolId) {
631
+ toolData.identifier = this.lastToolId;
632
+ }
633
+ else {
634
+ const newId = uuid();
635
+ toolData.identifier = newId;
636
+ if (toolData.tool === UserResponse) {
637
+ this.lastToolId = newId;
638
+ }
639
+ }
640
+ }
641
+ else if (toolData.tool !== UserResponse) {
642
+ this.lastToolId = "";
643
+ }
644
+ switch (toolData.tool) {
645
+ case UserResponse:
646
+ await this.responseCallback({ toolData });
647
+ break;
648
+ case EndNode:
649
+ await this.handleEndNode();
650
+ break;
651
+ default:
652
+ await this.handleToolExecution(toolData);
653
+ }
654
+ }
655
+ isValidTaskResponse(response) {
656
+ return Boolean(response?.session_id && response?.data && response.data?.tool);
657
+ }
658
+ async responseCallback(response) {
659
+ if (!this.currentSession) {
660
+ return;
661
+ }
662
+ try {
663
+ this.currentSession.userEngagementCallback(response);
664
+ }
665
+ catch (error) {
666
+ console.error("Error in responseCallback:", error);
667
+ }
668
+ }
669
+ async handleEndNode() {
670
+ // Track task finish before sending stop signal
671
+ await this.taskTracker.trackTaskFinish(false);
672
+ // Notify UI layer to finalize any accumulated responses
673
+ await this.responseCallback({
674
+ forceStop: true
675
+ });
676
+ // In subprocess mode, proactively inform parent about completion with the last AI message via IPC bridge
677
+ if (process.env.QODO_SUBPROCESS_MODE === 'true') {
678
+ try {
679
+ // Dynamically import to avoid any potential circular deps during construction
680
+ // @ts-ignore
681
+ const { MessageManager } = await import('../context/messageManager.js');
682
+ const mm = MessageManager.getInstance();
683
+ // Prefer structured output if present
684
+ const lastStructured = mm.getLastAIStructuredOutputMessage();
685
+ const lastPlain = mm.getLastAIMessage();
686
+ const chosen = lastStructured || lastPlain;
687
+ let text;
688
+ if (chosen) {
689
+ const content = chosen.content;
690
+ text = typeof content === 'string' ? content : JSON.stringify(content);
691
+ }
692
+ try {
693
+ this.ipcBridge?.notifyCompletion(true, text || '', undefined, this.currentSession?.sessionId || this.latestSessionId || undefined);
694
+ }
695
+ catch { }
696
+ }
697
+ catch { }
698
+ // Ensure WebSocket is cleaned up before exiting the subprocess
699
+ try {
700
+ this.wsClient.cleanup();
701
+ }
702
+ catch { }
703
+ // Terminate the subprocess to reliably trigger parent's child.on('exit')
704
+ process.exit(0);
705
+ }
706
+ // Don't automatically start idle timer here - it will be started when user gets control
707
+ // This allows the connection to stay open for potential new requests
708
+ }
709
+ async handleToolExecution(toolData) {
710
+ // Check for cancellation before executing tools
711
+ if (await this.checkCancellation()) {
712
+ return;
713
+ }
714
+ if (!toolData.server_name) {
715
+ return;
716
+ }
717
+ // Ensure identifier is set for all tool executions
718
+ toolData.identifier ??= uuid();
719
+ toolData.session_id ??= this.currentSession?.sessionId || "";
720
+ if (toolData.tool_result) {
721
+ await this.processToolResponse(toolData, toolData.tool_result);
722
+ return;
723
+ }
724
+ const { tool_reasoning, ...restArgs } = toolData.tool_args;
725
+ toolData.tool_args = restArgs;
726
+ toolData.tool_reasoning = tool_reasoning ? tool_reasoning : "";
727
+ try {
728
+ // If MCP is disabled entirely, decline tool execution immediately with a friendly message.
729
+ if (!this.mcpManager) {
730
+ await this.handleToolDecline(toolData, 'This agent was configured with `tools = []`, so no MCP tools are available.');
731
+ return;
732
+ }
733
+ const isAutoApprovedTool = this.mcpManager.isAutoApprovedTool(toolData.server_name, toolData.tool, toolData.tool_args);
734
+ // Dry run tools if configured
735
+ const processedToolData = await toolProcessorManager.preProcessTool(toolData, this.sessionContext);
736
+ await this.processToolReasoning(processedToolData, isAutoApprovedTool);
737
+ if (isAutoApprovedTool) {
738
+ // In subprocess mode, also surface non-approval tool calls to parent for UI visibility
739
+ try {
740
+ this.ipcBridge?.notifyToolCall(processedToolData);
741
+ }
742
+ catch { }
743
+ await this.callTool(processedToolData);
744
+ }
745
+ else {
746
+ // In subprocess mode, forward approval to orchestrator via IPC, otherwise use normal pending flow
747
+ if (process.env.QODO_SUBPROCESS_MODE === 'true' && typeof process.send === 'function') {
748
+ const approved = await (this.ipcBridge?.requestApproval(processedToolData) ?? Promise.resolve(false));
749
+ if (approved) {
750
+ await this.callTool(processedToolData);
751
+ }
752
+ else {
753
+ await this.handleToolDecline(processedToolData);
754
+ }
755
+ }
756
+ else {
757
+ this.setUserHasControl(true);
758
+ this.pendingToolRequests.set(processedToolData.identifier, {
759
+ agentSession: this.currentSession,
760
+ toolData: processedToolData,
761
+ });
762
+ }
763
+ }
764
+ }
765
+ catch (error) {
766
+ console.error("Error in tool execution:", error);
767
+ }
768
+ }
769
+ async handleToolDecline(toolData, reason = 'User declined tool execution') {
770
+ const answer = {
771
+ isError: true,
772
+ content: [{ type: "text", text: reason }],
773
+ };
774
+ if (this.currentSession) {
775
+ await this.processToolResponse(toolData, answer);
776
+ // Send tool response via WebSocket
777
+ await this.sendToolResponse(toolData, answer);
778
+ }
779
+ }
780
+ async callTool(toolData) {
781
+ // Check for cancellation before calling tool
782
+ if (await this.checkCancellation()) {
783
+ return;
784
+ }
785
+ const toolCallId = toolData.identifier ?? uuid();
786
+ toolData.identifier = toolCallId;
787
+ this.inFlightToolCalls.add(toolCallId);
788
+ try {
789
+ this.debug("Calling tool:", toolData.tool, "on server:", toolData.server_name);
790
+ // Track the tool execution
791
+ this.taskTracker.trackToolExecution(toolData.server_name, toolData.tool);
792
+ // If shell_execute or ripgrep_search without cwd, default to session execution CWD if configured.
793
+ // Also, for git tools, default repo_path to execution CWD when not provided. This gives
794
+ // in-process SDK sessions (which set an explicit cwd) consistent behavior across built-ins.
795
+ let patchedArgs = toolData.tool_args;
796
+ try {
797
+ const execCwd = this.sessionContext?.getExecutionCwd?.();
798
+ if (execCwd) {
799
+ const hasCwd = patchedArgs && typeof patchedArgs === 'object' && 'cwd' in patchedArgs && patchedArgs.cwd;
800
+ const isShell = toolData.server_name === 'shell' && toolData.tool === 'shell_execute';
801
+ const isRipgrep = toolData.server_name === 'ripgrep' && toolData.tool === 'ripgrep_search';
802
+ if (!hasCwd && (isShell || isRipgrep)) {
803
+ patchedArgs = { ...(patchedArgs || {}), cwd: execCwd };
804
+ }
805
+ // Git tools expect a repo_path; when omitted, treat the execution CWD as the repo root.
806
+ const needsRepoPath = toolData.server_name === 'git'
807
+ && (!patchedArgs || typeof patchedArgs.repo_path !== 'string' || !patchedArgs.repo_path);
808
+ if (needsRepoPath) {
809
+ patchedArgs = { ...(patchedArgs || {}), repo_path: execCwd };
810
+ }
811
+ }
812
+ }
813
+ catch { }
814
+ if (!this.mcpManager) {
815
+ // Should not be reachable because we guard earlier, but keep a safe fallback
816
+ // that gracefully declines the tool call instead of throwing.
817
+ await this.handleToolDecline(toolData, 'This agent was configured with `tools = []`, so no MCP tools are available.');
818
+ return;
819
+ }
820
+ const result = await this.mcpManager.callTool(toolData.server_name, toolData.tool, patchedArgs);
821
+ this.debug("Tool execution result:", result?.isError ? "Error" : "Success");
822
+ // Track code blocks for filesystem modifications
823
+ if (toolData.server_name.toLowerCase() === "filesystem" && result && !result.isError) {
824
+ this.taskTracker.trackFileSystemChanges(toolData.tool, toolData.tool_args, result);
825
+ }
826
+ if (result && this.currentSession) {
827
+ try {
828
+ const response = result
829
+ ? result
830
+ : { isError: true, content: [{ type: "text", text: "Unknown error occurred" }] };
831
+ if (await this.checkCancellation()) {
832
+ return;
833
+ }
834
+ // Post-process the tool result
835
+ const processedResponse = await toolProcessorManager.postProcessTool(toolData, response);
836
+ await this.processToolResponse(toolData, processedResponse);
837
+ // Also forward tool result to parent orchestrator in subprocess mode (for UI rendering)
838
+ try {
839
+ this.ipcBridge?.notifyToolResult(toolData, processedResponse);
840
+ }
841
+ catch { }
842
+ // Send tool response via WebSocket
843
+ await this.sendToolResponse(toolData, processedResponse);
844
+ }
845
+ catch (error) {
846
+ const errorResponse = {
847
+ isError: true,
848
+ content: [{ type: "text", text: "Unknown error occurred" }],
849
+ };
850
+ await this.processToolResponse(toolData, errorResponse);
851
+ await this.sendToolResponse(toolData, errorResponse);
852
+ }
853
+ }
854
+ }
855
+ catch (error) {
856
+ this.debug("Error in tool execution:", error);
857
+ const errorResponse = {
858
+ isError: true,
859
+ content: [{ type: "text", text: error instanceof Error ? error.message : "Unknown error occurred" }],
860
+ };
861
+ await this.processToolResponse(toolData, errorResponse);
862
+ // Also forward error tool result to parent orchestrator in subprocess mode
863
+ try {
864
+ this.ipcBridge?.notifyToolResult(toolData, errorResponse);
865
+ }
866
+ catch { }
867
+ await this.sendToolResponse(toolData, errorResponse);
868
+ }
869
+ finally {
870
+ this.inFlightToolCalls.delete(toolCallId);
871
+ }
872
+ }
873
+ async sendToolResponse(toolData, response) {
874
+ try {
875
+ if (!this.currentSession) {
876
+ console.error('No current session available for tool response');
877
+ return;
878
+ }
879
+ this.setUserHasControl(false);
880
+ // Create IDERetrievalAnswer format
881
+ const ideAnswer = {
882
+ tool: toolData.tool,
883
+ tool_id: toolData.identifier,
884
+ answer: response,
885
+ tools: this.convertToolsForIDE(),
886
+ user_data: getUserData(),
887
+ agent_type: "cli"
888
+ };
889
+ // Debug snapshot for IDERetrievalAnswer tools
890
+ try {
891
+ const toolsAny = ideAnswer.tools;
892
+ if (Array.isArray(toolsAny?.IDETool)) {
893
+ const arr = toolsAny.IDETool;
894
+ const sample = arr.slice(0, 3).map((t) => t?.name || t?.tool || '[unknown]');
895
+ this.debug('[AgentAPI] IDERetrievalAnswer tools snapshot (array form):', { count: arr.length, sample });
896
+ }
897
+ else if (toolsAny && typeof toolsAny === 'object') {
898
+ const perServerCounts = Object.fromEntries(Object.entries(toolsAny).map(([srv, arr]) => [srv, Array.isArray(arr) ? arr.length : 0]));
899
+ const totalTools = Object.values(toolsAny).reduce((acc, v) => acc + (Array.isArray(v) ? v.length : 0), 0);
900
+ this.debug('[AgentAPI] IDERetrievalAnswer tools snapshot (map form):', { servers: Object.keys(toolsAny), perServerCounts, totalTools });
901
+ }
902
+ else {
903
+ this.debug('[AgentAPI] IDERetrievalAnswer tools snapshot: tools is empty or invalid');
904
+ }
905
+ }
906
+ catch (e) {
907
+ this.debug('[AgentAPI] Failed to log IDERetrievalAnswer tools snapshot:', e instanceof Error ? e.message : e);
908
+ }
909
+ // Send via WebSocket client with new protocol format
910
+ await this.wsClient.sendMessage('IDERetrievalAnswer', ideAnswer);
911
+ this.debug('Tool response sent via WebSocket client');
912
+ }
913
+ catch (error) {
914
+ console.error('Error sending tool response:', error);
915
+ // Only handle error if we're not already in an error state
916
+ if (this.currentSession?.waitingForResponse) {
917
+ await this.handleError(error);
918
+ }
919
+ }
920
+ }
921
+ convertToolsForIDE() {
922
+ // For IDERetrievalAnswer the backend expects a ToolType-keyed dict.
923
+ // Use 'IDETool' with a flat array of tools.
924
+ const enabledTools = this.mcpManager?.getEnabledTools(this.sessionContext.getAvailableTools(), this.sessionContext.getIgnoreTools());
925
+ // Debug: inspect enabled tools map content before conversion
926
+ try {
927
+ const isMap = enabledTools instanceof Map;
928
+ const serverCount = isMap ? enabledTools.size : 0;
929
+ const serverNames = isMap ? Array.from(enabledTools.keys()) : [];
930
+ const perServerCounts = isMap ? Object.fromEntries(Array.from(enabledTools.entries()).map(([k, v]) => [k, Array.isArray(v) ? v.length : 0])) : {};
931
+ const flattenedCount = isMap ? Array.from(enabledTools.values()).flat().length : 0;
932
+ this.debug('[AgentAPI] convertToolsForIDE (to IDETool array):', { isMap, serverCount, serverNames, perServerCounts, flattenedCount });
933
+ }
934
+ catch (e) {
935
+ this.debug('[AgentAPI] convertToolsForIDE: failed to log enabled tools map:', e instanceof Error ? e.message : e);
936
+ }
937
+ const ideTools = enabledTools ? Array.from(enabledTools.values()).flat() : [];
938
+ return { IDETool: ideTools };
939
+ }
940
+ async processToolReasoning(toolData, isAutoApprovedTool) {
941
+ if (!this.currentSession) {
942
+ return;
943
+ }
944
+ await this.responseCallback({
945
+ toolData: {
946
+ ...toolData,
947
+ ...(!isAutoApprovedTool ? { pending_approval: true } : {}),
948
+ }
949
+ });
950
+ }
951
+ async processToolResponse(toolData, toolResult) {
952
+ await this.responseCallback({
953
+ toolData: { ...toolData, tool_result: toolResult },
954
+ });
955
+ }
956
+ async handleError(error) {
957
+ if (this.currentSession) {
958
+ this.currentSession.waitingForResponse = false;
959
+ }
960
+ let message = error instanceof Error ? error.message : "Unknown error occurred";
961
+ // Handle different types of cancellation/abort errors
962
+ if (error instanceof Error) {
963
+ if (error.name === "AbortError" || error.name === "CanceledError") {
964
+ // Check if this was a user-initiated cancellation vs timeout
965
+ if (this.currentSession?.abortController?.signal.aborted) {
966
+ return;
967
+ }
968
+ else {
969
+ message = "Request Timeout";
970
+ }
971
+ }
972
+ }
973
+ await this.responseCallback({
974
+ error: message || SERVER_ERROR_MESSAGE,
975
+ });
976
+ // Finish task tracking before terminating session
977
+ await this.taskTracker.trackTaskFinish(true);
978
+ // Terminate session after error
979
+ await this.responseCallback({
980
+ forceStop: true,
981
+ });
982
+ // In subprocess mode, proactively inform parent about failure and exit via IPC bridge
983
+ if (process.env.QODO_SUBPROCESS_MODE === 'true') {
984
+ try {
985
+ // Dynamically import to avoid early init issues
986
+ // @ts-ignore
987
+ const { MessageManager } = await import('../context/messageManager.js');
988
+ const mm = MessageManager.getInstance();
989
+ const lastStructured = mm.getLastAIStructuredOutputMessage();
990
+ const lastPlain = mm.getLastAIMessage();
991
+ const chosen = lastStructured || lastPlain;
992
+ let text;
993
+ if (chosen) {
994
+ const content = chosen.content;
995
+ text = typeof content === 'string' ? content : JSON.stringify(content);
996
+ }
997
+ try {
998
+ this.ipcBridge?.notifyCompletion(false, text || '', message || '', this.currentSession?.sessionId || this.latestSessionId || undefined);
999
+ }
1000
+ catch { }
1001
+ }
1002
+ catch { }
1003
+ try {
1004
+ this.wsClient.cleanup();
1005
+ }
1006
+ catch { }
1007
+ process.exit(1);
1008
+ }
1009
+ }
1010
+ // Enhanced cleanup method with comprehensive resource management
1011
+ cleanupConnections() {
1012
+ this.wsClient.cleanup();
1013
+ }
1014
+ // Complete cleanup method
1015
+ cleanup() {
1016
+ try {
1017
+ this.cleanupConnections();
1018
+ }
1019
+ catch { }
1020
+ this.pendingToolRequests.clear();
1021
+ this.currentSession = undefined;
1022
+ this.latestSessionId = "";
1023
+ try {
1024
+ this.ipcBridge?.dispose?.();
1025
+ }
1026
+ catch { }
1027
+ this.ipcBridge = null;
1028
+ }
1029
+ // Get the task tracker instance
1030
+ getTaskTracker() {
1031
+ return this.taskTracker;
1032
+ }
1033
+ }
1034
+ //# sourceMappingURL=agent.js.map