@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.
- package/LICENSE +118 -0
- package/README.md +121 -0
- package/dist/api/agent.d.ts +69 -0
- package/dist/api/agent.d.ts.map +1 -0
- package/dist/api/agent.js +1034 -0
- package/dist/api/agent.js.map +1 -0
- package/dist/api/analytics.d.ts +43 -0
- package/dist/api/analytics.d.ts.map +1 -0
- package/dist/api/analytics.js +163 -0
- package/dist/api/analytics.js.map +1 -0
- package/dist/api/http.d.ts +5 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +59 -0
- package/dist/api/http.js.map +1 -0
- package/dist/api/index.d.ts +12 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +17 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/taskTracking.d.ts +54 -0
- package/dist/api/taskTracking.d.ts.map +1 -0
- package/dist/api/taskTracking.js +208 -0
- package/dist/api/taskTracking.js.map +1 -0
- package/dist/api/types.d.ts +92 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/utils.d.ts +8 -0
- package/dist/api/utils.d.ts.map +1 -0
- package/dist/api/utils.js +54 -0
- package/dist/api/utils.js.map +1 -0
- package/dist/api/websocket.d.ts +74 -0
- package/dist/api/websocket.d.ts.map +1 -0
- package/dist/api/websocket.js +685 -0
- package/dist/api/websocket.js.map +1 -0
- package/dist/auth/index.d.ts +25 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +85 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/clients/index.d.ts +8 -0
- package/dist/clients/index.d.ts.map +1 -0
- package/dist/clients/index.js +7 -0
- package/dist/clients/index.js.map +1 -0
- package/dist/clients/info/InfoClient.d.ts +37 -0
- package/dist/clients/info/InfoClient.d.ts.map +1 -0
- package/dist/clients/info/InfoClient.js +69 -0
- package/dist/clients/info/InfoClient.js.map +1 -0
- package/dist/clients/info/index.d.ts +4 -0
- package/dist/clients/info/index.d.ts.map +1 -0
- package/dist/clients/info/index.js +2 -0
- package/dist/clients/info/index.js.map +1 -0
- package/dist/clients/info/types.d.ts +21 -0
- package/dist/clients/info/types.d.ts.map +1 -0
- package/dist/clients/info/types.js +2 -0
- package/dist/clients/info/types.js.map +1 -0
- package/dist/clients/sessions/SessionsClient.d.ts +34 -0
- package/dist/clients/sessions/SessionsClient.d.ts.map +1 -0
- package/dist/clients/sessions/SessionsClient.js +71 -0
- package/dist/clients/sessions/SessionsClient.js.map +1 -0
- package/dist/clients/sessions/index.d.ts +4 -0
- package/dist/clients/sessions/index.d.ts.map +1 -0
- package/dist/clients/sessions/index.js +2 -0
- package/dist/clients/sessions/index.js.map +1 -0
- package/dist/clients/sessions/types.d.ts +20 -0
- package/dist/clients/sessions/types.d.ts.map +1 -0
- package/dist/clients/sessions/types.js +2 -0
- package/dist/clients/sessions/types.js.map +1 -0
- package/dist/config/ConfigManager.d.ts +43 -0
- package/dist/config/ConfigManager.d.ts.map +1 -0
- package/dist/config/ConfigManager.js +472 -0
- package/dist/config/ConfigManager.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/urlConfig.d.ts +15 -0
- package/dist/config/urlConfig.d.ts.map +1 -0
- package/dist/config/urlConfig.js +75 -0
- package/dist/config/urlConfig.js.map +1 -0
- package/dist/constants/errors.d.ts +2 -0
- package/dist/constants/errors.d.ts.map +1 -0
- package/dist/constants/errors.js +2 -0
- package/dist/constants/errors.js.map +1 -0
- package/dist/constants/index.d.ts +7 -0
- package/dist/constants/index.d.ts.map +1 -0
- package/dist/constants/index.js +11 -0
- package/dist/constants/index.js.map +1 -0
- package/dist/constants/tools.d.ts +4 -0
- package/dist/constants/tools.d.ts.map +1 -0
- package/dist/constants/tools.js +4 -0
- package/dist/constants/tools.js.map +1 -0
- package/dist/constants/versions.d.ts +2 -0
- package/dist/constants/versions.d.ts.map +1 -0
- package/dist/constants/versions.js +2 -0
- package/dist/constants/versions.js.map +1 -0
- package/dist/context/buildUserContext.d.ts +18 -0
- package/dist/context/buildUserContext.d.ts.map +1 -0
- package/dist/context/buildUserContext.js +34 -0
- package/dist/context/buildUserContext.js.map +1 -0
- package/dist/context/index.d.ts +9 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +9 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/messageManager.d.ts +42 -0
- package/dist/context/messageManager.d.ts.map +1 -0
- package/dist/context/messageManager.js +322 -0
- package/dist/context/messageManager.js.map +1 -0
- package/dist/context/taskFocus.d.ts +2 -0
- package/dist/context/taskFocus.d.ts.map +1 -0
- package/dist/context/taskFocus.js +26 -0
- package/dist/context/taskFocus.js.map +1 -0
- package/dist/context/userInput.d.ts +3 -0
- package/dist/context/userInput.d.ts.map +1 -0
- package/dist/context/userInput.js +20 -0
- package/dist/context/userInput.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/MCPManager.d.ts +125 -0
- package/dist/mcp/MCPManager.d.ts.map +1 -0
- package/dist/mcp/MCPManager.js +616 -0
- package/dist/mcp/MCPManager.js.map +1 -0
- package/dist/mcp/approvedTools.d.ts +4 -0
- package/dist/mcp/approvedTools.d.ts.map +1 -0
- package/dist/mcp/approvedTools.js +19 -0
- package/dist/mcp/approvedTools.js.map +1 -0
- package/dist/mcp/baseServer.d.ts +75 -0
- package/dist/mcp/baseServer.d.ts.map +1 -0
- package/dist/mcp/baseServer.js +107 -0
- package/dist/mcp/baseServer.js.map +1 -0
- package/dist/mcp/builtinServers.d.ts +15 -0
- package/dist/mcp/builtinServers.d.ts.map +1 -0
- package/dist/mcp/builtinServers.js +155 -0
- package/dist/mcp/builtinServers.js.map +1 -0
- package/dist/mcp/dynamicBEServer.d.ts +20 -0
- package/dist/mcp/dynamicBEServer.d.ts.map +1 -0
- package/dist/mcp/dynamicBEServer.js +52 -0
- package/dist/mcp/dynamicBEServer.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +24 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/mcpInitialization.d.ts +2 -0
- package/dist/mcp/mcpInitialization.d.ts.map +1 -0
- package/dist/mcp/mcpInitialization.js +56 -0
- package/dist/mcp/mcpInitialization.js.map +1 -0
- package/dist/mcp/servers/filesystem.d.ts +75 -0
- package/dist/mcp/servers/filesystem.d.ts.map +1 -0
- package/dist/mcp/servers/filesystem.js +992 -0
- package/dist/mcp/servers/filesystem.js.map +1 -0
- package/dist/mcp/servers/gerrit.d.ts +19 -0
- package/dist/mcp/servers/gerrit.d.ts.map +1 -0
- package/dist/mcp/servers/gerrit.js +515 -0
- package/dist/mcp/servers/gerrit.js.map +1 -0
- package/dist/mcp/servers/git.d.ts +18 -0
- package/dist/mcp/servers/git.d.ts.map +1 -0
- package/dist/mcp/servers/git.js +441 -0
- package/dist/mcp/servers/git.js.map +1 -0
- package/dist/mcp/servers/ripgrep.d.ts +34 -0
- package/dist/mcp/servers/ripgrep.d.ts.map +1 -0
- package/dist/mcp/servers/ripgrep.js +517 -0
- package/dist/mcp/servers/ripgrep.js.map +1 -0
- package/dist/mcp/servers/shell.d.ts +20 -0
- package/dist/mcp/servers/shell.d.ts.map +1 -0
- package/dist/mcp/servers/shell.js +603 -0
- package/dist/mcp/servers/shell.js.map +1 -0
- package/dist/mcp/serversRegistry.d.ts +55 -0
- package/dist/mcp/serversRegistry.d.ts.map +1 -0
- package/dist/mcp/serversRegistry.js +410 -0
- package/dist/mcp/serversRegistry.js.map +1 -0
- package/dist/mcp/toolProcessor.d.ts +42 -0
- package/dist/mcp/toolProcessor.d.ts.map +1 -0
- package/dist/mcp/toolProcessor.js +200 -0
- package/dist/mcp/toolProcessor.js.map +1 -0
- package/dist/mcp/types.d.ts +29 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/parser/index.d.ts +72 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +967 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/types.d.ts +153 -0
- package/dist/parser/types.d.ts.map +1 -0
- package/dist/parser/types.js +6 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/parser/utils.d.ts +18 -0
- package/dist/parser/utils.d.ts.map +1 -0
- package/dist/parser/utils.js +64 -0
- package/dist/parser/utils.js.map +1 -0
- package/dist/sdk/QodoSDK.d.ts +152 -0
- package/dist/sdk/QodoSDK.d.ts.map +1 -0
- package/dist/sdk/QodoSDK.js +786 -0
- package/dist/sdk/QodoSDK.js.map +1 -0
- package/dist/sdk/bootstrap.d.ts +16 -0
- package/dist/sdk/bootstrap.d.ts.map +1 -0
- package/dist/sdk/bootstrap.js +21 -0
- package/dist/sdk/bootstrap.js.map +1 -0
- package/dist/sdk/builders.d.ts +54 -0
- package/dist/sdk/builders.d.ts.map +1 -0
- package/dist/sdk/builders.js +117 -0
- package/dist/sdk/builders.js.map +1 -0
- package/dist/sdk/defaults.d.ts +11 -0
- package/dist/sdk/defaults.d.ts.map +1 -0
- package/dist/sdk/defaults.js +39 -0
- package/dist/sdk/defaults.js.map +1 -0
- package/dist/sdk/discovery.d.ts +2 -0
- package/dist/sdk/discovery.d.ts.map +1 -0
- package/dist/sdk/discovery.js +25 -0
- package/dist/sdk/discovery.js.map +1 -0
- package/dist/sdk/events.d.ts +168 -0
- package/dist/sdk/events.d.ts.map +1 -0
- package/dist/sdk/events.js +52 -0
- package/dist/sdk/events.js.map +1 -0
- package/dist/sdk/index.d.ts +17 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +17 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/runner/AgentRunner.d.ts +22 -0
- package/dist/sdk/runner/AgentRunner.d.ts.map +1 -0
- package/dist/sdk/runner/AgentRunner.js +222 -0
- package/dist/sdk/runner/AgentRunner.js.map +1 -0
- package/dist/sdk/runner/finalize.d.ts +9 -0
- package/dist/sdk/runner/finalize.d.ts.map +1 -0
- package/dist/sdk/runner/finalize.js +115 -0
- package/dist/sdk/runner/finalize.js.map +1 -0
- package/dist/sdk/runner/formats.d.ts +7 -0
- package/dist/sdk/runner/formats.d.ts.map +1 -0
- package/dist/sdk/runner/formats.js +91 -0
- package/dist/sdk/runner/formats.js.map +1 -0
- package/dist/sdk/runner/index.d.ts +9 -0
- package/dist/sdk/runner/index.d.ts.map +1 -0
- package/dist/sdk/runner/index.js +9 -0
- package/dist/sdk/runner/index.js.map +1 -0
- package/dist/sdk/runner/progress.d.ts +3 -0
- package/dist/sdk/runner/progress.d.ts.map +1 -0
- package/dist/sdk/runner/progress.js +16 -0
- package/dist/sdk/runner/progress.js.map +1 -0
- package/dist/sdk/schemas.d.ts +50 -0
- package/dist/sdk/schemas.d.ts.map +1 -0
- package/dist/sdk/schemas.js +145 -0
- package/dist/sdk/schemas.js.map +1 -0
- package/dist/session/SessionContext.d.ts +86 -0
- package/dist/session/SessionContext.d.ts.map +1 -0
- package/dist/session/SessionContext.js +395 -0
- package/dist/session/SessionContext.js.map +1 -0
- package/dist/session/environment.d.ts +42 -0
- package/dist/session/environment.d.ts.map +1 -0
- package/dist/session/environment.js +27 -0
- package/dist/session/environment.js.map +1 -0
- package/dist/session/history.d.ts +3 -0
- package/dist/session/history.d.ts.map +1 -0
- package/dist/session/history.js +67 -0
- package/dist/session/history.js.map +1 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +9 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/serverData.d.ts +38 -0
- package/dist/session/serverData.d.ts.map +1 -0
- package/dist/session/serverData.js +241 -0
- package/dist/session/serverData.js.map +1 -0
- package/dist/tracking/Tracker.d.ts +55 -0
- package/dist/tracking/Tracker.d.ts.map +1 -0
- package/dist/tracking/Tracker.js +217 -0
- package/dist/tracking/Tracker.js.map +1 -0
- package/dist/tracking/index.d.ts +8 -0
- package/dist/tracking/index.d.ts.map +1 -0
- package/dist/tracking/index.js +8 -0
- package/dist/tracking/index.js.map +1 -0
- package/dist/tracking/schemas.d.ts +292 -0
- package/dist/tracking/schemas.d.ts.map +1 -0
- package/dist/tracking/schemas.js +91 -0
- package/dist/tracking/schemas.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/extractSetFlags.d.ts +6 -0
- package/dist/utils/extractSetFlags.d.ts.map +1 -0
- package/dist/utils/extractSetFlags.js +16 -0
- package/dist/utils/extractSetFlags.js.map +1 -0
- package/dist/utils/formatTimeAgo.d.ts +2 -0
- package/dist/utils/formatTimeAgo.d.ts.map +1 -0
- package/dist/utils/formatTimeAgo.js +20 -0
- package/dist/utils/formatTimeAgo.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/machineId.d.ts +14 -0
- package/dist/utils/machineId.d.ts.map +1 -0
- package/dist/utils/machineId.js +66 -0
- package/dist/utils/machineId.js.map +1 -0
- package/dist/utils/pathUtils.d.ts +22 -0
- package/dist/utils/pathUtils.d.ts.map +1 -0
- package/dist/utils/pathUtils.js +54 -0
- package/dist/utils/pathUtils.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- 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
|