@zds-ai/cli 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 +21 -0
- package/README.md +497 -0
- package/dist/agent/grok-agent.d.ts +250 -0
- package/dist/agent/grok-agent.js +2480 -0
- package/dist/agent/grok-agent.js.map +1 -0
- package/dist/agent/index.d.ts +14 -0
- package/dist/agent/index.js +136 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +239 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/grok/client.d.ts +55 -0
- package/dist/grok/client.js +276 -0
- package/dist/grok/client.js.map +1 -0
- package/dist/grok/tools.d.ts +8 -0
- package/dist/grok/tools.js +878 -0
- package/dist/grok/tools.js.map +1 -0
- package/dist/hooks/use-enhanced-input.d.ts +38 -0
- package/dist/hooks/use-enhanced-input.js +228 -0
- package/dist/hooks/use-enhanced-input.js.map +1 -0
- package/dist/hooks/use-input-handler.d.ts +36 -0
- package/dist/hooks/use-input-handler.js +1099 -0
- package/dist/hooks/use-input-handler.js.map +1 -0
- package/dist/hooks/use-input-history.d.ts +9 -0
- package/dist/hooks/use-input-history.js +61 -0
- package/dist/hooks/use-input-history.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +869 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +41 -0
- package/dist/mcp/client.js +224 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/config.d.ts +13 -0
- package/dist/mcp/config.js +56 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/transports.d.ts +53 -0
- package/dist/mcp/transports.js +256 -0
- package/dist/mcp/transports.js.map +1 -0
- package/dist/tools/character-tool.d.ts +27 -0
- package/dist/tools/character-tool.js +194 -0
- package/dist/tools/character-tool.js.map +1 -0
- package/dist/tools/clear-cache-tool.d.ts +14 -0
- package/dist/tools/clear-cache-tool.js +82 -0
- package/dist/tools/clear-cache-tool.js.map +1 -0
- package/dist/tools/confirmation-tool.d.ts +16 -0
- package/dist/tools/confirmation-tool.js +72 -0
- package/dist/tools/confirmation-tool.js.map +1 -0
- package/dist/tools/env-tool.d.ts +17 -0
- package/dist/tools/env-tool.js +89 -0
- package/dist/tools/env-tool.js.map +1 -0
- package/dist/tools/file-conversion-tool.d.ts +16 -0
- package/dist/tools/file-conversion-tool.js +181 -0
- package/dist/tools/file-conversion-tool.js.map +1 -0
- package/dist/tools/image-tool.d.ts +22 -0
- package/dist/tools/image-tool.js +268 -0
- package/dist/tools/image-tool.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/internet-tool.d.ts +11 -0
- package/dist/tools/internet-tool.js +108 -0
- package/dist/tools/internet-tool.js.map +1 -0
- package/dist/tools/introspect-tool.d.ts +11 -0
- package/dist/tools/introspect-tool.js +243 -0
- package/dist/tools/introspect-tool.js.map +1 -0
- package/dist/tools/morph-editor.d.ts +38 -0
- package/dist/tools/morph-editor.js +318 -0
- package/dist/tools/morph-editor.js.map +1 -0
- package/dist/tools/restart-tool.d.ts +7 -0
- package/dist/tools/restart-tool.js +24 -0
- package/dist/tools/restart-tool.js.map +1 -0
- package/dist/tools/search.d.ts +71 -0
- package/dist/tools/search.js +340 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/task-tool.d.ts +19 -0
- package/dist/tools/task-tool.js +115 -0
- package/dist/tools/task-tool.js.map +1 -0
- package/dist/tools/text-editor.d.ts +35 -0
- package/dist/tools/text-editor.js +669 -0
- package/dist/tools/text-editor.js.map +1 -0
- package/dist/tools/tool-discovery.d.ts +20 -0
- package/dist/tools/tool-discovery.js +45 -0
- package/dist/tools/tool-discovery.js.map +1 -0
- package/dist/tools/zsh.d.ts +13 -0
- package/dist/tools/zsh.js +168 -0
- package/dist/tools/zsh.js.map +1 -0
- package/dist/types/index.d.ts +31 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/app.d.ts +7 -0
- package/dist/ui/app.js +99 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/components/active-task-status.d.ts +7 -0
- package/dist/ui/components/active-task-status.js +37 -0
- package/dist/ui/components/active-task-status.js.map +1 -0
- package/dist/ui/components/api-key-input.d.ts +7 -0
- package/dist/ui/components/api-key-input.js +80 -0
- package/dist/ui/components/api-key-input.js.map +1 -0
- package/dist/ui/components/backend-status.d.ts +7 -0
- package/dist/ui/components/backend-status.js +85 -0
- package/dist/ui/components/backend-status.js.map +1 -0
- package/dist/ui/components/chat-history.d.ts +8 -0
- package/dist/ui/components/chat-history.js +187 -0
- package/dist/ui/components/chat-history.js.map +1 -0
- package/dist/ui/components/chat-input.d.ts +9 -0
- package/dist/ui/components/chat-input.js +63 -0
- package/dist/ui/components/chat-input.js.map +1 -0
- package/dist/ui/components/chat-interface.d.ts +9 -0
- package/dist/ui/components/chat-interface.js +389 -0
- package/dist/ui/components/chat-interface.js.map +1 -0
- package/dist/ui/components/command-suggestions.d.ts +17 -0
- package/dist/ui/components/command-suggestions.js +22 -0
- package/dist/ui/components/command-suggestions.js.map +1 -0
- package/dist/ui/components/confirmation-dialog.d.ts +11 -0
- package/dist/ui/components/confirmation-dialog.js +105 -0
- package/dist/ui/components/confirmation-dialog.js.map +1 -0
- package/dist/ui/components/context-status.d.ts +7 -0
- package/dist/ui/components/context-status.js +36 -0
- package/dist/ui/components/context-status.js.map +1 -0
- package/dist/ui/components/diff-renderer.d.ts +13 -0
- package/dist/ui/components/diff-renderer.js +206 -0
- package/dist/ui/components/diff-renderer.js.map +1 -0
- package/dist/ui/components/loading-spinner.d.ts +8 -0
- package/dist/ui/components/loading-spinner.js +64 -0
- package/dist/ui/components/loading-spinner.js.map +1 -0
- package/dist/ui/components/mcp-status.d.ts +5 -0
- package/dist/ui/components/mcp-status.js +57 -0
- package/dist/ui/components/mcp-status.js.map +1 -0
- package/dist/ui/components/model-selection.d.ts +12 -0
- package/dist/ui/components/model-selection.js +17 -0
- package/dist/ui/components/model-selection.js.map +1 -0
- package/dist/ui/components/mood-status.d.ts +7 -0
- package/dist/ui/components/mood-status.js +34 -0
- package/dist/ui/components/mood-status.js.map +1 -0
- package/dist/ui/components/persona-status.d.ts +7 -0
- package/dist/ui/components/persona-status.js +34 -0
- package/dist/ui/components/persona-status.js.map +1 -0
- package/dist/ui/shared/max-sized-box.d.ts +8 -0
- package/dist/ui/shared/max-sized-box.js +6 -0
- package/dist/ui/shared/max-sized-box.js.map +1 -0
- package/dist/ui/utils/code-colorizer.d.ts +2 -0
- package/dist/ui/utils/code-colorizer.js +7 -0
- package/dist/ui/utils/code-colorizer.js.map +1 -0
- package/dist/ui/utils/colors.d.ts +14 -0
- package/dist/ui/utils/colors.js +15 -0
- package/dist/ui/utils/colors.js.map +1 -0
- package/dist/ui/utils/markdown-renderer.d.ts +4 -0
- package/dist/ui/utils/markdown-renderer.js +40 -0
- package/dist/ui/utils/markdown-renderer.js.map +1 -0
- package/dist/utils/auth-helper.d.ts +63 -0
- package/dist/utils/auth-helper.js +129 -0
- package/dist/utils/auth-helper.js.map +1 -0
- package/dist/utils/chat-history-manager-sqlite.d.ts +92 -0
- package/dist/utils/chat-history-manager-sqlite.js +334 -0
- package/dist/utils/chat-history-manager-sqlite.js.map +1 -0
- package/dist/utils/chat-history-manager.d.ts +87 -0
- package/dist/utils/chat-history-manager.js +273 -0
- package/dist/utils/chat-history-manager.js.map +1 -0
- package/dist/utils/chat-history-manager.json-backup.d.ts +69 -0
- package/dist/utils/chat-history-manager.json-backup.js +215 -0
- package/dist/utils/chat-history-manager.json-backup.js.map +1 -0
- package/dist/utils/confirmation-service.d.ts +46 -0
- package/dist/utils/confirmation-service.js +165 -0
- package/dist/utils/confirmation-service.js.map +1 -0
- package/dist/utils/custom-instructions.d.ts +1 -0
- package/dist/utils/custom-instructions.js +30 -0
- package/dist/utils/custom-instructions.js.map +1 -0
- package/dist/utils/database-connection.d.ts +27 -0
- package/dist/utils/database-connection.js +81 -0
- package/dist/utils/database-connection.js.map +1 -0
- package/dist/utils/database-schema.d.ts +17 -0
- package/dist/utils/database-schema.js +93 -0
- package/dist/utils/database-schema.js.map +1 -0
- package/dist/utils/error-logger.d.ts +13 -0
- package/dist/utils/error-logger.js +56 -0
- package/dist/utils/error-logger.js.map +1 -0
- package/dist/utils/hook-executor.d.ts +59 -0
- package/dist/utils/hook-executor.js +351 -0
- package/dist/utils/hook-executor.js.map +1 -0
- package/dist/utils/model-config.d.ts +28 -0
- package/dist/utils/model-config.js +42 -0
- package/dist/utils/model-config.js.map +1 -0
- package/dist/utils/path-utils.d.ts +4 -0
- package/dist/utils/path-utils.js +12 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/dist/utils/settings-manager.d.ts +169 -0
- package/dist/utils/settings-manager.js +403 -0
- package/dist/utils/settings-manager.js.map +1 -0
- package/dist/utils/settings.d.ts +1 -0
- package/dist/utils/settings.js +4 -0
- package/dist/utils/settings.js.map +1 -0
- package/dist/utils/slash-commands.d.ts +25 -0
- package/dist/utils/slash-commands.js +454 -0
- package/dist/utils/slash-commands.js.map +1 -0
- package/dist/utils/startup-hook.d.ts +13 -0
- package/dist/utils/startup-hook.js +44 -0
- package/dist/utils/startup-hook.js.map +1 -0
- package/dist/utils/text-utils.d.ts +80 -0
- package/dist/utils/text-utils.js +182 -0
- package/dist/utils/text-utils.js.map +1 -0
- package/dist/utils/token-counter.d.ts +33 -0
- package/dist/utils/token-counter.js +78 -0
- package/dist/utils/token-counter.js.map +1 -0
- package/package.json +102 -0
|
@@ -0,0 +1,2480 @@
|
|
|
1
|
+
import { GrokClient } from "../grok/client.js";
|
|
2
|
+
import { getAllGrokTools, getMCPManager, initializeMCPServers, } from "../grok/tools.js";
|
|
3
|
+
import { loadMCPConfig } from "../mcp/config.js";
|
|
4
|
+
import { ChatHistoryManager } from "../utils/chat-history-manager.js";
|
|
5
|
+
import { logApiError } from "../utils/error-logger.js";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { TextEditorTool, MorphEditorTool, ZshTool, ConfirmationTool, SearchTool, EnvTool, IntrospectTool, ClearCacheTool, CharacterTool, TaskTool, InternetTool, ImageTool, FileConversionTool, RestartTool } from "../tools/index.js";
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import { createTokenCounter } from "../utils/token-counter.js";
|
|
10
|
+
import { getSettingsManager } from "../utils/settings-manager.js";
|
|
11
|
+
import { executeOperationHook, executeToolApprovalHook, applyHookCommands } from "../utils/hook-executor.js";
|
|
12
|
+
export class GrokAgent extends EventEmitter {
|
|
13
|
+
grokClient;
|
|
14
|
+
textEditor;
|
|
15
|
+
morphEditor;
|
|
16
|
+
zsh;
|
|
17
|
+
confirmationTool;
|
|
18
|
+
search;
|
|
19
|
+
env;
|
|
20
|
+
introspect;
|
|
21
|
+
clearCacheTool;
|
|
22
|
+
characterTool;
|
|
23
|
+
taskTool;
|
|
24
|
+
internetTool;
|
|
25
|
+
imageTool;
|
|
26
|
+
fileConversionTool;
|
|
27
|
+
restartTool;
|
|
28
|
+
chatHistory = [];
|
|
29
|
+
messages = [];
|
|
30
|
+
tokenCounter;
|
|
31
|
+
abortController = null;
|
|
32
|
+
mcpInitialized = false;
|
|
33
|
+
maxToolRounds;
|
|
34
|
+
temperature;
|
|
35
|
+
maxTokens;
|
|
36
|
+
firstMessageProcessed = false;
|
|
37
|
+
contextWarningAt80 = false;
|
|
38
|
+
contextWarningAt90 = false;
|
|
39
|
+
persona = "";
|
|
40
|
+
personaColor = "white";
|
|
41
|
+
mood = "";
|
|
42
|
+
moodColor = "white";
|
|
43
|
+
activeTask = "";
|
|
44
|
+
activeTaskAction = "";
|
|
45
|
+
activeTaskColor = "white";
|
|
46
|
+
apiKeyEnvVar = "GROK_API_KEY";
|
|
47
|
+
pendingContextEdit = null;
|
|
48
|
+
constructor(apiKey, baseURL, model, maxToolRounds, debugLogFile, startupHookOutput, temperature, maxTokens) {
|
|
49
|
+
super();
|
|
50
|
+
const manager = getSettingsManager();
|
|
51
|
+
const savedModel = manager.getCurrentModel();
|
|
52
|
+
const modelToUse = model || savedModel || "grok-code-fast-1";
|
|
53
|
+
this.maxToolRounds = maxToolRounds || 400;
|
|
54
|
+
this.temperature = temperature ?? manager.getTemperature();
|
|
55
|
+
this.maxTokens = maxTokens ?? manager.getMaxTokens();
|
|
56
|
+
// Get display name from environment (set by zai/helpers)
|
|
57
|
+
const displayName = process.env.GROK_BACKEND_DISPLAY_NAME;
|
|
58
|
+
this.grokClient = new GrokClient(apiKey, modelToUse, baseURL, displayName);
|
|
59
|
+
// Set apiKeyEnvVar based on backend name
|
|
60
|
+
const backendName = this.grokClient.getBackendName().toUpperCase();
|
|
61
|
+
this.apiKeyEnvVar = `${backendName}_API_KEY`;
|
|
62
|
+
this.textEditor = new TextEditorTool();
|
|
63
|
+
this.morphEditor = process.env.MORPH_API_KEY ? new MorphEditorTool() : null;
|
|
64
|
+
this.zsh = new ZshTool();
|
|
65
|
+
this.confirmationTool = new ConfirmationTool();
|
|
66
|
+
this.search = new SearchTool();
|
|
67
|
+
this.env = new EnvTool();
|
|
68
|
+
this.introspect = new IntrospectTool();
|
|
69
|
+
this.clearCacheTool = new ClearCacheTool();
|
|
70
|
+
this.restartTool = new RestartTool();
|
|
71
|
+
this.characterTool = new CharacterTool();
|
|
72
|
+
this.taskTool = new TaskTool();
|
|
73
|
+
this.internetTool = new InternetTool();
|
|
74
|
+
this.imageTool = new ImageTool();
|
|
75
|
+
this.fileConversionTool = new FileConversionTool();
|
|
76
|
+
this.textEditor.setAgent(this); // Give text editor access to agent for context awareness
|
|
77
|
+
this.introspect.setAgent(this); // Give introspect access to agent for tool class info
|
|
78
|
+
this.clearCacheTool.setAgent(this); // Give clearCache access to agent
|
|
79
|
+
this.characterTool.setAgent(this); // Give character tool access to agent
|
|
80
|
+
this.taskTool.setAgent(this); // Give task tool access to agent
|
|
81
|
+
this.internetTool.setAgent(this); // Give internet tool access to agent
|
|
82
|
+
this.imageTool.setAgent(this); // Give image tool access to agent
|
|
83
|
+
this.zsh.setAgent(this); // Give zsh tool access to agent for CWD tracking
|
|
84
|
+
this.tokenCounter = createTokenCounter(modelToUse);
|
|
85
|
+
// Initialize MCP servers if configured
|
|
86
|
+
this.initializeMCP(debugLogFile);
|
|
87
|
+
// System message will be set after async initialization
|
|
88
|
+
this.messages.push({
|
|
89
|
+
role: "system",
|
|
90
|
+
content: "Initializing...", // Temporary, will be replaced in initialize()
|
|
91
|
+
});
|
|
92
|
+
// Note: THE system prompt is NOT added to chatHistory
|
|
93
|
+
// Only conversational system messages go in chatHistory
|
|
94
|
+
// Store startup hook output for later use
|
|
95
|
+
this.startupHookOutput = startupHookOutput;
|
|
96
|
+
}
|
|
97
|
+
startupHookOutput;
|
|
98
|
+
systemPrompt = "Initializing..."; // THE system prompt (always at messages[0])
|
|
99
|
+
/**
|
|
100
|
+
* Initialize the agent with dynamic system prompt
|
|
101
|
+
* Must be called after construction
|
|
102
|
+
*/
|
|
103
|
+
async initialize() {
|
|
104
|
+
// Build system message
|
|
105
|
+
await this.buildSystemMessage();
|
|
106
|
+
// Execute instance hook on every startup (fresh or not)
|
|
107
|
+
const settings = getSettingsManager();
|
|
108
|
+
const instanceHookPath = settings.getInstanceHook();
|
|
109
|
+
if (instanceHookPath) {
|
|
110
|
+
const hookResult = await executeOperationHook(instanceHookPath, "instance", {}, 30000, false, // Instance hook is not mandatory
|
|
111
|
+
this.getCurrentTokenCount(), this.getMaxContextSize());
|
|
112
|
+
if (hookResult.approved && hookResult.commands && hookResult.commands.length > 0) {
|
|
113
|
+
// Apply hook commands (ENV, TOOL_RESULT, MODEL, SYSTEM)
|
|
114
|
+
await this.processHookResult(hookResult);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build/rebuild the system message with current tool availability
|
|
120
|
+
* Updates this.systemPrompt which is always used for messages[0]
|
|
121
|
+
*/
|
|
122
|
+
async buildSystemMessage() {
|
|
123
|
+
// Add startup hook output if provided
|
|
124
|
+
const startupHookSection = this.startupHookOutput
|
|
125
|
+
? `${this.startupHookOutput}\n`
|
|
126
|
+
: "";
|
|
127
|
+
// Generate dynamic tool list using introspect tool
|
|
128
|
+
const toolsResult = await this.introspect.introspect("tools");
|
|
129
|
+
const toolsSection = toolsResult.success ? toolsResult.output : "Tools: Unknown";
|
|
130
|
+
// Build THE system prompt
|
|
131
|
+
this.systemPrompt = `${startupHookSection}
|
|
132
|
+
${toolsSection}
|
|
133
|
+
|
|
134
|
+
Current working directory: ${process.cwd()}`;
|
|
135
|
+
// Update messages[0] with the system prompt
|
|
136
|
+
this.messages[0] = {
|
|
137
|
+
role: "system",
|
|
138
|
+
content: this.systemPrompt,
|
|
139
|
+
};
|
|
140
|
+
// Note: chatHistory no longer contains THE system prompt
|
|
141
|
+
// Only conversational system messages (persona, mood, etc.) go in chatHistory
|
|
142
|
+
}
|
|
143
|
+
async loadInitialHistory(history, systemPrompt) {
|
|
144
|
+
// Load chatHistory (no system messages in new architecture)
|
|
145
|
+
this.chatHistory = history;
|
|
146
|
+
// Set system prompt if provided, otherwise generate one
|
|
147
|
+
if (systemPrompt) {
|
|
148
|
+
this.setSystemPrompt(systemPrompt);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await this.buildSystemMessage();
|
|
152
|
+
}
|
|
153
|
+
// Instance hook now runs in initialize() for both fresh and existing sessions
|
|
154
|
+
// Convert history to messages format for API calls
|
|
155
|
+
const historyMessages = [];
|
|
156
|
+
// Track which tool_call_ids we've seen in assistant messages
|
|
157
|
+
const seenToolCallIds = new Set();
|
|
158
|
+
// First pass: collect all tool_call_ids from assistant messages
|
|
159
|
+
for (const entry of history) {
|
|
160
|
+
if (entry.type === "assistant" && entry.tool_calls) {
|
|
161
|
+
entry.tool_calls.forEach(tc => seenToolCallIds.add(tc.id));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Second pass: build history messages, only including tool_results that have matching tool_calls
|
|
165
|
+
const toolResultMessages = [];
|
|
166
|
+
const toolCallIdToMessage = new Map();
|
|
167
|
+
for (const entry of history) {
|
|
168
|
+
switch (entry.type) {
|
|
169
|
+
case "system":
|
|
170
|
+
// All system messages from chatHistory go into conversation (persona, mood, etc.)
|
|
171
|
+
historyMessages.push({
|
|
172
|
+
role: "system",
|
|
173
|
+
content: entry.content,
|
|
174
|
+
});
|
|
175
|
+
break;
|
|
176
|
+
case "user":
|
|
177
|
+
historyMessages.push({
|
|
178
|
+
role: "user",
|
|
179
|
+
content: entry.content,
|
|
180
|
+
});
|
|
181
|
+
break;
|
|
182
|
+
case "assistant":
|
|
183
|
+
const assistantMessage = {
|
|
184
|
+
role: "assistant",
|
|
185
|
+
content: entry.content || "", // Ensure content is never null/undefined
|
|
186
|
+
};
|
|
187
|
+
if (entry.tool_calls && entry.tool_calls.length > 0) {
|
|
188
|
+
// For assistant messages with tool calls, collect the tool results that correspond to them
|
|
189
|
+
const correspondingToolResults = [];
|
|
190
|
+
const toolCallsWithResults = [];
|
|
191
|
+
entry.tool_calls.forEach(tc => {
|
|
192
|
+
// Find the tool_result entry for this tool_call
|
|
193
|
+
const toolResultEntry = history.find(h => h.type === "tool_result" && h.toolCall?.id === tc.id);
|
|
194
|
+
if (toolResultEntry) {
|
|
195
|
+
// Only include this tool_call if we have its result
|
|
196
|
+
toolCallsWithResults.push(tc);
|
|
197
|
+
correspondingToolResults.push({
|
|
198
|
+
role: "tool",
|
|
199
|
+
content: toolResultEntry.toolResult?.output || toolResultEntry.toolResult?.error || "",
|
|
200
|
+
tool_call_id: tc.id,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
// Only add tool_calls if we have at least one with a result
|
|
205
|
+
if (toolCallsWithResults.length > 0) {
|
|
206
|
+
assistantMessage.tool_calls = toolCallsWithResults;
|
|
207
|
+
// Add assistant message
|
|
208
|
+
historyMessages.push(assistantMessage);
|
|
209
|
+
// Add corresponding tool results immediately after
|
|
210
|
+
historyMessages.push(...correspondingToolResults);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// No tool results found, just add the assistant message without tool_calls
|
|
214
|
+
historyMessages.push(assistantMessage);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
historyMessages.push(assistantMessage);
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
case "tool_result":
|
|
222
|
+
// Skip tool_result entries here - they're handled when processing assistant messages with tool_calls
|
|
223
|
+
break;
|
|
224
|
+
// Skip tool_call entries as they are included with assistant
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Insert history messages after the system message
|
|
228
|
+
this.messages.splice(1, 0, ...historyMessages);
|
|
229
|
+
// Update token count in system message
|
|
230
|
+
const currentTokens = this.tokenCounter.countTokens(this.messages.map(m => typeof m.content === 'string' ? m.content : '').join(''));
|
|
231
|
+
if (this.messages.length > 0 && this.messages[0].role === 'system' && typeof this.messages[0].content === 'string') {
|
|
232
|
+
this.messages[0].content = this.messages[0].content.replace(/Current conversation token usage: .*/, `Current conversation token usage: ${currentTokens}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async initializeMCP(debugLogFile) {
|
|
236
|
+
// Initialize MCP in the background without blocking
|
|
237
|
+
Promise.resolve().then(async () => {
|
|
238
|
+
try {
|
|
239
|
+
const config = loadMCPConfig();
|
|
240
|
+
if (config.servers.length > 0) {
|
|
241
|
+
await initializeMCPServers(debugLogFile);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
console.warn("MCP initialization failed:", error);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
this.mcpInitialized = true;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
isGrokModel() {
|
|
253
|
+
const currentModel = this.grokClient.getCurrentModel();
|
|
254
|
+
return currentModel.toLowerCase().includes("grok");
|
|
255
|
+
}
|
|
256
|
+
// Heuristic: enable web search only when likely needed
|
|
257
|
+
shouldUseSearchFor(message) {
|
|
258
|
+
const q = message.toLowerCase();
|
|
259
|
+
const keywords = [
|
|
260
|
+
"today",
|
|
261
|
+
"latest",
|
|
262
|
+
"news",
|
|
263
|
+
"trending",
|
|
264
|
+
"breaking",
|
|
265
|
+
"current",
|
|
266
|
+
"now",
|
|
267
|
+
"recent",
|
|
268
|
+
"x.com",
|
|
269
|
+
"twitter",
|
|
270
|
+
"tweet",
|
|
271
|
+
"what happened",
|
|
272
|
+
"as of",
|
|
273
|
+
"update on",
|
|
274
|
+
"release notes",
|
|
275
|
+
"changelog",
|
|
276
|
+
"price",
|
|
277
|
+
];
|
|
278
|
+
if (keywords.some((k) => q.includes(k)))
|
|
279
|
+
return true;
|
|
280
|
+
// crude date pattern (e.g., 2024/2025) may imply recency
|
|
281
|
+
if (/(20\d{2})/.test(q))
|
|
282
|
+
return true;
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
async processUserMessage(message) {
|
|
286
|
+
// Before adding the new user message, check if there are incomplete tool calls
|
|
287
|
+
// from a previous interrupted turn. This prevents malformed message sequences
|
|
288
|
+
// that cause Ollama 500 errors.
|
|
289
|
+
const lastMessage = this.messages[this.messages.length - 1];
|
|
290
|
+
if (lastMessage?.role === "assistant" && lastMessage.tool_calls) {
|
|
291
|
+
// Find tool_call_ids that don't have corresponding tool result messages
|
|
292
|
+
const toolCallIds = new Set(lastMessage.tool_calls.map((tc) => tc.id));
|
|
293
|
+
const completedToolCallIds = new Set();
|
|
294
|
+
// Check which tool calls have results
|
|
295
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
296
|
+
const msg = this.messages[i];
|
|
297
|
+
if (msg.role === "tool" && msg.tool_call_id) {
|
|
298
|
+
completedToolCallIds.add(msg.tool_call_id);
|
|
299
|
+
}
|
|
300
|
+
// Stop when we hit the assistant message with tool_calls
|
|
301
|
+
if (this.messages[i] === lastMessage)
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
// Add cancelled results for any incomplete tool calls
|
|
305
|
+
for (const toolCallId of toolCallIds) {
|
|
306
|
+
if (!completedToolCallIds.has(toolCallId)) {
|
|
307
|
+
console.error(`Adding cancelled result for incomplete tool call: ${toolCallId}`);
|
|
308
|
+
this.messages.push({
|
|
309
|
+
role: "tool",
|
|
310
|
+
content: "[Cancelled by user]",
|
|
311
|
+
tool_call_id: toolCallId,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Add user message to conversation
|
|
317
|
+
const userEntry = {
|
|
318
|
+
type: "user",
|
|
319
|
+
content: message,
|
|
320
|
+
timestamp: new Date(),
|
|
321
|
+
};
|
|
322
|
+
this.chatHistory.push(userEntry);
|
|
323
|
+
this.messages.push({ role: "user", content: message });
|
|
324
|
+
await this.emitContextChange();
|
|
325
|
+
const newEntries = [userEntry];
|
|
326
|
+
const maxToolRounds = this.maxToolRounds; // Prevent infinite loops
|
|
327
|
+
let toolRounds = 0;
|
|
328
|
+
let consecutiveNonToolResponses = 0;
|
|
329
|
+
try {
|
|
330
|
+
// Always fetch tools fresh - getAllGrokTools() handles lazy refresh internally
|
|
331
|
+
const supportsTools = this.grokClient.getSupportsTools();
|
|
332
|
+
let currentResponse = await this.grokClient.chat(this.messages, supportsTools ? await getAllGrokTools() : [], undefined, this.isGrokModel() && this.shouldUseSearchFor(message)
|
|
333
|
+
? { search_parameters: { mode: "auto" } }
|
|
334
|
+
: { search_parameters: { mode: "off" } }, this.temperature, this.abortController?.signal, this.maxTokens);
|
|
335
|
+
// Parse XML tool calls from response if present
|
|
336
|
+
if (currentResponse.choices?.[0]?.message) {
|
|
337
|
+
currentResponse.choices[0].message = this.parseXMLToolCalls(currentResponse.choices[0].message);
|
|
338
|
+
}
|
|
339
|
+
// Agent loop - continue until no more tool calls or max rounds reached
|
|
340
|
+
while (toolRounds < maxToolRounds) {
|
|
341
|
+
const assistantMessage = currentResponse.choices?.[0]?.message;
|
|
342
|
+
if (!assistantMessage) {
|
|
343
|
+
throw new Error("No response from Grok");
|
|
344
|
+
}
|
|
345
|
+
// Handle tool calls
|
|
346
|
+
if (assistantMessage.tool_calls &&
|
|
347
|
+
assistantMessage.tool_calls.length > 0) {
|
|
348
|
+
toolRounds++;
|
|
349
|
+
consecutiveNonToolResponses = 0; // Reset counter when AI makes tool calls
|
|
350
|
+
// Clean up tool call arguments before adding to conversation history
|
|
351
|
+
// This prevents Ollama from rejecting malformed tool calls on subsequent API calls
|
|
352
|
+
const cleanedToolCalls = assistantMessage.tool_calls.map(toolCall => {
|
|
353
|
+
let argsString = toolCall.function.arguments?.trim() || "{}";
|
|
354
|
+
// Handle duplicate/concatenated JSON objects (LLM bug)
|
|
355
|
+
if (argsString.includes('}{')) {
|
|
356
|
+
try {
|
|
357
|
+
let depth = 0;
|
|
358
|
+
let firstObjEnd = -1;
|
|
359
|
+
for (let i = 0; i < argsString.length; i++) {
|
|
360
|
+
if (argsString[i] === '{')
|
|
361
|
+
depth++;
|
|
362
|
+
if (argsString[i] === '}') {
|
|
363
|
+
depth--;
|
|
364
|
+
if (depth === 0) {
|
|
365
|
+
firstObjEnd = i + 1;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (firstObjEnd > 0 && firstObjEnd < argsString.length) {
|
|
371
|
+
const firstObj = argsString.substring(0, firstObjEnd);
|
|
372
|
+
JSON.parse(firstObj); // Validate
|
|
373
|
+
argsString = firstObj; // Use cleaned version
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
// Keep original if cleaning fails
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
...toolCall,
|
|
382
|
+
function: {
|
|
383
|
+
...toolCall.function,
|
|
384
|
+
arguments: argsString
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
// Add assistant message to conversation
|
|
389
|
+
this.messages.push({
|
|
390
|
+
role: "assistant",
|
|
391
|
+
content: assistantMessage.content || "(Calling tools to perform this request)",
|
|
392
|
+
tool_calls: cleanedToolCalls,
|
|
393
|
+
});
|
|
394
|
+
// Add assistant message to chat history
|
|
395
|
+
const assistantToolCallEntry = {
|
|
396
|
+
type: "assistant",
|
|
397
|
+
content: assistantMessage.content || "(Calling tools to perform this request)",
|
|
398
|
+
timestamp: new Date(),
|
|
399
|
+
tool_calls: assistantMessage.tool_calls,
|
|
400
|
+
};
|
|
401
|
+
this.chatHistory.push(assistantToolCallEntry);
|
|
402
|
+
newEntries.push(assistantToolCallEntry);
|
|
403
|
+
await this.emitContextChange();
|
|
404
|
+
// Create initial tool call entries to show tools are being executed
|
|
405
|
+
// Use cleanedToolCalls to preserve arguments in chatHistory
|
|
406
|
+
cleanedToolCalls.forEach((toolCall) => {
|
|
407
|
+
const toolCallEntry = {
|
|
408
|
+
type: "tool_call",
|
|
409
|
+
content: "Executing...",
|
|
410
|
+
timestamp: new Date(),
|
|
411
|
+
toolCall: toolCall,
|
|
412
|
+
};
|
|
413
|
+
this.chatHistory.push(toolCallEntry);
|
|
414
|
+
newEntries.push(toolCallEntry);
|
|
415
|
+
});
|
|
416
|
+
// Execute tool calls and update the entries
|
|
417
|
+
let toolIndex = 0;
|
|
418
|
+
const completedToolCallIds = new Set();
|
|
419
|
+
try {
|
|
420
|
+
for (const toolCall of cleanedToolCalls) {
|
|
421
|
+
// Check for cancellation before executing each tool
|
|
422
|
+
if (this.abortController?.signal.aborted) {
|
|
423
|
+
console.error(`Tool execution cancelled after ${toolIndex}/${cleanedToolCalls.length} tools`);
|
|
424
|
+
// Add cancelled responses for remaining uncompleted tools
|
|
425
|
+
for (let i = toolIndex; i < cleanedToolCalls.length; i++) {
|
|
426
|
+
const remainingToolCall = cleanedToolCalls[i];
|
|
427
|
+
this.messages.push({
|
|
428
|
+
role: "tool",
|
|
429
|
+
content: "[Cancelled by user]",
|
|
430
|
+
tool_call_id: remainingToolCall.id,
|
|
431
|
+
});
|
|
432
|
+
completedToolCallIds.add(remainingToolCall.id);
|
|
433
|
+
}
|
|
434
|
+
throw new Error("Operation cancelled by user");
|
|
435
|
+
}
|
|
436
|
+
const result = await this.executeTool(toolCall);
|
|
437
|
+
// Update the existing tool_call entry with the result
|
|
438
|
+
const entryIndex = this.chatHistory.findIndex((entry) => entry.type === "tool_call" && entry.toolCall?.id === toolCall.id);
|
|
439
|
+
if (entryIndex !== -1) {
|
|
440
|
+
const updatedEntry = {
|
|
441
|
+
...this.chatHistory[entryIndex],
|
|
442
|
+
type: "tool_result",
|
|
443
|
+
content: result.success
|
|
444
|
+
? result.output || "Success"
|
|
445
|
+
: result.error || "Error occurred",
|
|
446
|
+
toolResult: result,
|
|
447
|
+
};
|
|
448
|
+
this.chatHistory[entryIndex] = updatedEntry;
|
|
449
|
+
// Also update in newEntries for return value
|
|
450
|
+
const newEntryIndex = newEntries.findIndex((entry) => entry.type === "tool_call" &&
|
|
451
|
+
entry.toolCall?.id === toolCall.id);
|
|
452
|
+
if (newEntryIndex !== -1) {
|
|
453
|
+
newEntries[newEntryIndex] = updatedEntry;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Add tool result to messages with proper format (needed for AI context)
|
|
457
|
+
this.messages.push({
|
|
458
|
+
role: "tool",
|
|
459
|
+
content: result.success
|
|
460
|
+
? result.output || "Success"
|
|
461
|
+
: result.error || "Error",
|
|
462
|
+
tool_call_id: toolCall.id,
|
|
463
|
+
});
|
|
464
|
+
completedToolCallIds.add(toolCall.id);
|
|
465
|
+
await this.emitContextChange();
|
|
466
|
+
toolIndex++;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
finally {
|
|
470
|
+
// Ensure ALL tool calls in this.messages have results, even if we crashed/errored
|
|
471
|
+
for (const toolCall of cleanedToolCalls) {
|
|
472
|
+
if (!completedToolCallIds.has(toolCall.id)) {
|
|
473
|
+
this.messages.push({
|
|
474
|
+
role: "tool",
|
|
475
|
+
content: "[Error: Tool execution interrupted]",
|
|
476
|
+
tool_call_id: toolCall.id,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// After all tool results are added, add any system messages from this tool round
|
|
482
|
+
// System messages are added to chatHistory during tool execution (for display)
|
|
483
|
+
// Now we add them to this.messages in the same order (after all tool results)
|
|
484
|
+
// Find the most recent assistant message with tool_calls in chatHistory (search backwards)
|
|
485
|
+
let assistantIndex = -1;
|
|
486
|
+
for (let i = this.chatHistory.length - 1; i >= 0; i--) {
|
|
487
|
+
const entry = this.chatHistory[i];
|
|
488
|
+
if (entry.type === "assistant" && entry.tool_calls && entry.tool_calls.length > 0) {
|
|
489
|
+
assistantIndex = i;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (assistantIndex !== -1) {
|
|
494
|
+
// Collect system messages that appeared after this assistant message
|
|
495
|
+
for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
|
|
496
|
+
const entry = this.chatHistory[i];
|
|
497
|
+
if (entry.type === 'system' && entry.content && entry.content.trim()) {
|
|
498
|
+
this.messages.push({
|
|
499
|
+
role: 'system',
|
|
500
|
+
content: entry.content
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
// Stop if we hit another assistant or user message (next turn)
|
|
504
|
+
if (entry.type === 'assistant' || entry.type === 'user') {
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Get next response - this might contain more tool calls
|
|
510
|
+
// Debug logging to diagnose tool_call/tool_result mismatch
|
|
511
|
+
const debugLogPath = ChatHistoryManager.getDebugLogPath();
|
|
512
|
+
const timestamp = new Date().toISOString();
|
|
513
|
+
fs.appendFileSync(debugLogPath, `\n${timestamp} - [DEBUG] Messages before API call (${this.messages.length} messages):\n`);
|
|
514
|
+
this.messages.forEach((msg, idx) => {
|
|
515
|
+
const msgSummary = { idx, role: msg.role };
|
|
516
|
+
if (msg.tool_calls)
|
|
517
|
+
msgSummary.tool_calls = msg.tool_calls.map((tc) => tc.id);
|
|
518
|
+
if (msg.tool_call_id)
|
|
519
|
+
msgSummary.tool_call_id = msg.tool_call_id;
|
|
520
|
+
fs.appendFileSync(debugLogPath, ` ${JSON.stringify(msgSummary)}\n`);
|
|
521
|
+
});
|
|
522
|
+
currentResponse = await this.grokClient.chat(this.messages, supportsTools ? await getAllGrokTools() : [], undefined, this.isGrokModel() && this.shouldUseSearchFor(message)
|
|
523
|
+
? { search_parameters: { mode: "auto" } }
|
|
524
|
+
: { search_parameters: { mode: "off" } }, this.temperature, this.abortController?.signal, this.maxTokens);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
// No tool calls in this response - only add it if there's actual content
|
|
528
|
+
const trimmedContent = assistantMessage.content?.trim();
|
|
529
|
+
if (trimmedContent) {
|
|
530
|
+
const responseEntry = {
|
|
531
|
+
type: "assistant",
|
|
532
|
+
content: trimmedContent,
|
|
533
|
+
timestamp: new Date(),
|
|
534
|
+
};
|
|
535
|
+
this.chatHistory.push(responseEntry);
|
|
536
|
+
this.messages.push({
|
|
537
|
+
role: "assistant",
|
|
538
|
+
content: trimmedContent,
|
|
539
|
+
});
|
|
540
|
+
newEntries.push(responseEntry);
|
|
541
|
+
}
|
|
542
|
+
// TODO: HACK - This is a temporary fix to prevent duplicate responses.
|
|
543
|
+
// We need a proper way for the bot to signal task completion, such as:
|
|
544
|
+
// - A special tool call like "taskComplete()"
|
|
545
|
+
// - A finish_reason indicator in the API response
|
|
546
|
+
// - A structured response format that explicitly marks completion
|
|
547
|
+
// For now, we break immediately after a substantial response to avoid
|
|
548
|
+
// the cascade of duplicate responses caused by "give it one more chance" logic.
|
|
549
|
+
// If the AI provided a substantial response (>50 chars), task is complete
|
|
550
|
+
if (assistantMessage.content && assistantMessage.content.trim().length > 50) {
|
|
551
|
+
break; // Task complete - bot gave a full response
|
|
552
|
+
}
|
|
553
|
+
// Short/empty response, give AI another chance
|
|
554
|
+
currentResponse = await this.grokClient.chat(this.messages, supportsTools ? await getAllGrokTools() : [], undefined, this.isGrokModel() && this.shouldUseSearchFor(message)
|
|
555
|
+
? { search_parameters: { mode: "auto" } }
|
|
556
|
+
: { search_parameters: { mode: "off" } }, this.temperature, this.abortController?.signal, this.maxTokens);
|
|
557
|
+
// Parse XML tool calls from followup response if present
|
|
558
|
+
if (currentResponse.choices?.[0]?.message) {
|
|
559
|
+
currentResponse.choices[0].message = this.parseXMLToolCalls(currentResponse.choices[0].message);
|
|
560
|
+
}
|
|
561
|
+
const followupMessage = currentResponse.choices?.[0]?.message;
|
|
562
|
+
if (!followupMessage?.tool_calls || followupMessage.tool_calls.length === 0) {
|
|
563
|
+
break; // AI doesn't want to continue
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (toolRounds >= maxToolRounds) {
|
|
568
|
+
const warningEntry = {
|
|
569
|
+
type: "assistant",
|
|
570
|
+
content: "Maximum tool execution rounds reached. Stopping to prevent infinite loops.",
|
|
571
|
+
timestamp: new Date(),
|
|
572
|
+
};
|
|
573
|
+
this.chatHistory.push(warningEntry);
|
|
574
|
+
newEntries.push(warningEntry);
|
|
575
|
+
}
|
|
576
|
+
// Mark first message as processed so subsequent messages use cached tools
|
|
577
|
+
this.firstMessageProcessed = true;
|
|
578
|
+
// Check if tool support changed during first message processing
|
|
579
|
+
// If model doesn't support tools, regenerate system message without tool list
|
|
580
|
+
const supportsToolsAfter = this.grokClient.getSupportsTools();
|
|
581
|
+
if (!supportsToolsAfter && supportsTools) {
|
|
582
|
+
// Tool support was disabled during first message - regenerate system message
|
|
583
|
+
await this.buildSystemMessage();
|
|
584
|
+
}
|
|
585
|
+
return newEntries;
|
|
586
|
+
}
|
|
587
|
+
catch (error) {
|
|
588
|
+
const errorEntry = {
|
|
589
|
+
type: "assistant",
|
|
590
|
+
content: `Sorry, I encountered an error: ${error.message}`,
|
|
591
|
+
timestamp: new Date(),
|
|
592
|
+
};
|
|
593
|
+
this.chatHistory.push(errorEntry);
|
|
594
|
+
// Mark first message as processed even on error
|
|
595
|
+
this.firstMessageProcessed = true;
|
|
596
|
+
return [userEntry, errorEntry];
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Parse XML-formatted tool calls from message content (x.ai format)
|
|
601
|
+
* Converts <xai:function_call> elements to standard GrokToolCall format
|
|
602
|
+
*/
|
|
603
|
+
parseXMLToolCalls(message) {
|
|
604
|
+
if (!message.content || typeof message.content !== 'string') {
|
|
605
|
+
return message;
|
|
606
|
+
}
|
|
607
|
+
const content = message.content;
|
|
608
|
+
const xmlToolCallRegex = /<xai:function_call\s+name="([^"]+)">([\s\S]*?)<\/xai:function_call>/g;
|
|
609
|
+
const matches = Array.from(content.matchAll(xmlToolCallRegex));
|
|
610
|
+
if (matches.length === 0) {
|
|
611
|
+
return message;
|
|
612
|
+
}
|
|
613
|
+
// Parse each XML tool call
|
|
614
|
+
const toolCalls = [];
|
|
615
|
+
let cleanedContent = content;
|
|
616
|
+
for (const match of matches) {
|
|
617
|
+
const functionName = match[1];
|
|
618
|
+
const paramsXML = match[2];
|
|
619
|
+
// Parse parameters
|
|
620
|
+
const paramRegex = /<parameter\s+name="([^"]+)">([^<]*)<\/parameter>/g;
|
|
621
|
+
const paramMatches = Array.from(paramsXML.matchAll(paramRegex));
|
|
622
|
+
const args = {};
|
|
623
|
+
for (const paramMatch of paramMatches) {
|
|
624
|
+
args[paramMatch[1]] = paramMatch[2];
|
|
625
|
+
}
|
|
626
|
+
// Generate a unique ID for this tool call
|
|
627
|
+
const toolCallId = `call_xml_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
628
|
+
toolCalls.push({
|
|
629
|
+
id: toolCallId,
|
|
630
|
+
type: "function",
|
|
631
|
+
function: {
|
|
632
|
+
name: functionName,
|
|
633
|
+
arguments: JSON.stringify(args)
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
// Remove this XML block from content
|
|
637
|
+
cleanedContent = cleanedContent.replace(match[0], '');
|
|
638
|
+
}
|
|
639
|
+
// Trim any extra whitespace
|
|
640
|
+
cleanedContent = cleanedContent.trim();
|
|
641
|
+
// Return modified message with tool_calls and cleaned content
|
|
642
|
+
return {
|
|
643
|
+
...message,
|
|
644
|
+
content: cleanedContent || null,
|
|
645
|
+
tool_calls: [...(message.tool_calls || []), ...toolCalls]
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
messageReducer(previous, item) {
|
|
649
|
+
const reduce = (acc, delta) => {
|
|
650
|
+
// Ensure acc is always an object before spreading (handles null/undefined)
|
|
651
|
+
acc = { ...(acc || {}) };
|
|
652
|
+
for (const [key, value] of Object.entries(delta)) {
|
|
653
|
+
// Skip null values in delta (Venice sends tool_calls: null which breaks Object.entries)
|
|
654
|
+
if (value === null)
|
|
655
|
+
continue;
|
|
656
|
+
if (acc[key] === undefined || acc[key] === null) {
|
|
657
|
+
acc[key] = value;
|
|
658
|
+
// Clean up index properties from tool calls
|
|
659
|
+
if (Array.isArray(acc[key])) {
|
|
660
|
+
for (const arr of acc[key]) {
|
|
661
|
+
delete arr.index;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else if (typeof acc[key] === "string" && typeof value === "string") {
|
|
666
|
+
// Don't concatenate certain properties that should remain separate
|
|
667
|
+
const nonConcatenableProps = ['id', 'type', 'name'];
|
|
668
|
+
if (nonConcatenableProps.includes(key)) {
|
|
669
|
+
// For non-concatenable properties, keep the new value
|
|
670
|
+
acc[key] = value;
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
// For content, arguments, and other text properties, concatenate
|
|
674
|
+
acc[key] += value;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
else if (Array.isArray(acc[key]) && Array.isArray(value)) {
|
|
678
|
+
const accArray = acc[key];
|
|
679
|
+
for (let i = 0; i < value.length; i++) {
|
|
680
|
+
if (!accArray[i])
|
|
681
|
+
accArray[i] = {};
|
|
682
|
+
accArray[i] = reduce(accArray[i], value[i]);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
else if (typeof acc[key] === "object" && typeof value === "object") {
|
|
686
|
+
acc[key] = reduce(acc[key], value);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return acc;
|
|
690
|
+
};
|
|
691
|
+
return reduce(previous, item.choices?.[0]?.delta || {});
|
|
692
|
+
}
|
|
693
|
+
async *processUserMessageStream(message) {
|
|
694
|
+
// Create new abort controller for this request
|
|
695
|
+
this.abortController = new AbortController();
|
|
696
|
+
// Before adding the new user message, check if there are incomplete tool calls
|
|
697
|
+
// from a previous interrupted turn. This prevents malformed message sequences
|
|
698
|
+
// that cause Ollama 500 errors.
|
|
699
|
+
const lastMessage = this.messages[this.messages.length - 1];
|
|
700
|
+
if (lastMessage?.role === "assistant" && lastMessage.tool_calls) {
|
|
701
|
+
// Find tool_call_ids that don't have corresponding tool result messages
|
|
702
|
+
const toolCallIds = new Set(lastMessage.tool_calls.map((tc) => tc.id));
|
|
703
|
+
const completedToolCallIds = new Set();
|
|
704
|
+
// Check which tool calls have results
|
|
705
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
706
|
+
const msg = this.messages[i];
|
|
707
|
+
if (msg.role === "tool" && msg.tool_call_id) {
|
|
708
|
+
completedToolCallIds.add(msg.tool_call_id);
|
|
709
|
+
}
|
|
710
|
+
// Stop when we hit the assistant message with tool_calls
|
|
711
|
+
if (this.messages[i] === lastMessage)
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
// Add cancelled results for any incomplete tool calls
|
|
715
|
+
for (const toolCallId of toolCallIds) {
|
|
716
|
+
if (!completedToolCallIds.has(toolCallId)) {
|
|
717
|
+
console.error(`Adding cancelled result for incomplete tool call: ${toolCallId}`);
|
|
718
|
+
this.messages.push({
|
|
719
|
+
role: "tool",
|
|
720
|
+
content: "[Cancelled by user]",
|
|
721
|
+
tool_call_id: toolCallId,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Add user message to both API conversation and chat history
|
|
727
|
+
const userEntry = {
|
|
728
|
+
type: "user",
|
|
729
|
+
content: message,
|
|
730
|
+
timestamp: new Date(),
|
|
731
|
+
};
|
|
732
|
+
this.chatHistory.push(userEntry);
|
|
733
|
+
this.messages.push({ role: "user", content: message });
|
|
734
|
+
await this.emitContextChange();
|
|
735
|
+
// Yield user message so UI can display it immediately
|
|
736
|
+
yield {
|
|
737
|
+
type: "user_message",
|
|
738
|
+
userEntry: userEntry,
|
|
739
|
+
};
|
|
740
|
+
// Calculate input tokens
|
|
741
|
+
let inputTokens = this.tokenCounter.countMessageTokens(this.messages);
|
|
742
|
+
yield {
|
|
743
|
+
type: "token_count",
|
|
744
|
+
tokenCount: inputTokens,
|
|
745
|
+
};
|
|
746
|
+
const maxToolRounds = this.maxToolRounds; // Prevent infinite loops
|
|
747
|
+
let toolRounds = 0;
|
|
748
|
+
let totalOutputTokens = 0;
|
|
749
|
+
let lastTokenUpdate = 0;
|
|
750
|
+
let consecutiveNonToolResponses = 0;
|
|
751
|
+
try {
|
|
752
|
+
// Always fetch tools fresh - getAllGrokTools() handles lazy refresh internally
|
|
753
|
+
const supportsTools = this.grokClient.getSupportsTools();
|
|
754
|
+
// Agent loop - continue until no more tool calls or max rounds reached
|
|
755
|
+
while (toolRounds < maxToolRounds) {
|
|
756
|
+
// Check if operation was cancelled
|
|
757
|
+
if (this.abortController?.signal.aborted) {
|
|
758
|
+
yield {
|
|
759
|
+
type: "content",
|
|
760
|
+
content: "\n\n[Operation cancelled by user]",
|
|
761
|
+
};
|
|
762
|
+
yield { type: "done" };
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
// Update system message with current token count
|
|
766
|
+
if (this.messages.length > 0 && this.messages[0].role === 'system' && typeof this.messages[0].content === 'string') {
|
|
767
|
+
this.messages[0].content = this.messages[0].content.replace(/Current conversation token usage: .*/, `Current conversation token usage: ${inputTokens}`);
|
|
768
|
+
}
|
|
769
|
+
// Stream response and accumulate
|
|
770
|
+
const stream = this.grokClient.chatStream(this.messages, supportsTools ? await getAllGrokTools() : [], undefined, this.isGrokModel() && this.shouldUseSearchFor(message)
|
|
771
|
+
? { search_parameters: { mode: "auto" } }
|
|
772
|
+
: { search_parameters: { mode: "off" } }, this.temperature, this.abortController?.signal, this.maxTokens);
|
|
773
|
+
let accumulatedMessage = {};
|
|
774
|
+
let accumulatedContent = "";
|
|
775
|
+
let tool_calls_yielded = false;
|
|
776
|
+
let streamFinished = false;
|
|
777
|
+
let insideThinkTag = false;
|
|
778
|
+
try {
|
|
779
|
+
for await (const chunk of stream) {
|
|
780
|
+
// Check for cancellation in the streaming loop
|
|
781
|
+
if (this.abortController?.signal.aborted) {
|
|
782
|
+
yield {
|
|
783
|
+
type: "content",
|
|
784
|
+
content: "\n\n[Operation cancelled by user]",
|
|
785
|
+
};
|
|
786
|
+
yield { type: "done" };
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (!chunk.choices?.[0])
|
|
790
|
+
continue;
|
|
791
|
+
// Check if stream is finished (Venice sends garbage after this)
|
|
792
|
+
if (chunk.choices?.[0]?.finish_reason === "stop" || chunk.choices?.[0]?.finish_reason === "tool_calls") {
|
|
793
|
+
streamFinished = true;
|
|
794
|
+
}
|
|
795
|
+
// Accumulate the message using reducer
|
|
796
|
+
accumulatedMessage = this.messageReducer(accumulatedMessage, chunk);
|
|
797
|
+
// Check for tool calls - yield when we have complete tool calls with function names
|
|
798
|
+
if (!tool_calls_yielded && accumulatedMessage.tool_calls?.length > 0) {
|
|
799
|
+
// Check if we have at least one complete tool call with a function name
|
|
800
|
+
const hasCompleteTool = accumulatedMessage.tool_calls.some((tc) => tc.function?.name);
|
|
801
|
+
if (hasCompleteTool) {
|
|
802
|
+
yield {
|
|
803
|
+
type: "tool_calls",
|
|
804
|
+
tool_calls: accumulatedMessage.tool_calls,
|
|
805
|
+
};
|
|
806
|
+
tool_calls_yielded = true;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
// Stream content as it comes (but ignore content after stream is finished to avoid Venice garbage)
|
|
810
|
+
if (chunk.choices[0].delta?.content && !streamFinished) {
|
|
811
|
+
let deltaContent = chunk.choices[0].delta.content;
|
|
812
|
+
// Handle thinking tags that may span multiple chunks
|
|
813
|
+
// First, remove complete <think>...</think> blocks within this chunk
|
|
814
|
+
deltaContent = deltaContent.replace(/<think>[\s\S]*?<\/think>/g, '');
|
|
815
|
+
// Check for opening <think> tag
|
|
816
|
+
if (deltaContent.includes('<think>')) {
|
|
817
|
+
insideThinkTag = true;
|
|
818
|
+
// Remove everything from <think> onwards in this chunk
|
|
819
|
+
deltaContent = deltaContent.substring(0, deltaContent.indexOf('<think>'));
|
|
820
|
+
}
|
|
821
|
+
// If we're inside a think tag, remove everything up to and including </think>
|
|
822
|
+
if (insideThinkTag) {
|
|
823
|
+
if (deltaContent.includes('</think>')) {
|
|
824
|
+
// Found closing tag - remove everything up to and including it
|
|
825
|
+
const closeIndex = deltaContent.indexOf('</think>');
|
|
826
|
+
deltaContent = deltaContent.substring(closeIndex + 8); // 8 = length of '</think>'
|
|
827
|
+
insideThinkTag = false;
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
// Still inside think block - remove entire chunk
|
|
831
|
+
deltaContent = '';
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// Skip completely empty chunks after filtering (but keep spaces!)
|
|
835
|
+
if (deltaContent === '')
|
|
836
|
+
continue;
|
|
837
|
+
accumulatedContent += deltaContent;
|
|
838
|
+
// Update token count in real-time including accumulated content and any tool calls
|
|
839
|
+
const currentOutputTokens = this.tokenCounter.estimateStreamingTokens(accumulatedContent) +
|
|
840
|
+
(accumulatedMessage.tool_calls
|
|
841
|
+
? this.tokenCounter.countTokens(JSON.stringify(accumulatedMessage.tool_calls))
|
|
842
|
+
: 0);
|
|
843
|
+
totalOutputTokens = currentOutputTokens;
|
|
844
|
+
yield {
|
|
845
|
+
type: "content",
|
|
846
|
+
content: deltaContent,
|
|
847
|
+
};
|
|
848
|
+
// Emit token count update
|
|
849
|
+
const now = Date.now();
|
|
850
|
+
if (now - lastTokenUpdate > 250) {
|
|
851
|
+
lastTokenUpdate = now;
|
|
852
|
+
yield {
|
|
853
|
+
type: "token_count",
|
|
854
|
+
tokenCount: inputTokens + totalOutputTokens,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
catch (streamError) {
|
|
861
|
+
// Check if stream was aborted
|
|
862
|
+
if (this.abortController?.signal.aborted || streamError.name === 'AbortError' || streamError.code === 'ABORT_ERR') {
|
|
863
|
+
yield {
|
|
864
|
+
type: "content",
|
|
865
|
+
content: "\n\n[Operation cancelled by user]",
|
|
866
|
+
};
|
|
867
|
+
yield { type: "done" };
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
// Re-throw other errors to be caught by outer catch
|
|
871
|
+
throw streamError;
|
|
872
|
+
}
|
|
873
|
+
// Parse XML tool calls from accumulated message if present
|
|
874
|
+
accumulatedMessage = this.parseXMLToolCalls(accumulatedMessage);
|
|
875
|
+
// Clean up tool call arguments before adding to conversation history
|
|
876
|
+
// This prevents Ollama from rejecting malformed tool calls on subsequent API calls
|
|
877
|
+
const cleanedToolCalls = accumulatedMessage.tool_calls?.map(toolCall => {
|
|
878
|
+
let argsString = toolCall.function.arguments?.trim() || "{}";
|
|
879
|
+
// Handle duplicate/concatenated JSON objects (LLM bug)
|
|
880
|
+
if (argsString.includes('}{')) {
|
|
881
|
+
try {
|
|
882
|
+
let depth = 0;
|
|
883
|
+
let firstObjEnd = -1;
|
|
884
|
+
for (let i = 0; i < argsString.length; i++) {
|
|
885
|
+
if (argsString[i] === '{')
|
|
886
|
+
depth++;
|
|
887
|
+
if (argsString[i] === '}') {
|
|
888
|
+
depth--;
|
|
889
|
+
if (depth === 0) {
|
|
890
|
+
firstObjEnd = i + 1;
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (firstObjEnd > 0 && firstObjEnd < argsString.length) {
|
|
896
|
+
const firstObj = argsString.substring(0, firstObjEnd);
|
|
897
|
+
JSON.parse(firstObj); // Validate
|
|
898
|
+
argsString = firstObj; // Use cleaned version
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
catch (e) {
|
|
902
|
+
// Keep original if cleaning fails
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
...toolCall,
|
|
907
|
+
function: {
|
|
908
|
+
...toolCall.function,
|
|
909
|
+
arguments: argsString
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
});
|
|
913
|
+
// Add accumulated message to conversation for API context
|
|
914
|
+
this.messages.push({
|
|
915
|
+
role: "assistant",
|
|
916
|
+
content: accumulatedMessage.content || "(Calling tools to perform this request)",
|
|
917
|
+
tool_calls: cleanedToolCalls,
|
|
918
|
+
});
|
|
919
|
+
// Add assistant message to chat history
|
|
920
|
+
const assistantEntry = {
|
|
921
|
+
type: "assistant",
|
|
922
|
+
content: accumulatedMessage.content || "(Calling tools to perform this request)",
|
|
923
|
+
timestamp: new Date(),
|
|
924
|
+
tool_calls: accumulatedMessage.tool_calls,
|
|
925
|
+
};
|
|
926
|
+
this.chatHistory.push(assistantEntry);
|
|
927
|
+
await this.emitContextChange();
|
|
928
|
+
// Handle tool calls if present
|
|
929
|
+
if (accumulatedMessage.tool_calls?.length > 0) {
|
|
930
|
+
toolRounds++;
|
|
931
|
+
// Only yield tool_calls if we haven't already yielded them during streaming
|
|
932
|
+
if (!tool_calls_yielded) {
|
|
933
|
+
yield {
|
|
934
|
+
type: "tool_calls",
|
|
935
|
+
tool_calls: accumulatedMessage.tool_calls,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
// Add tool_call entries to chatHistory so they persist through UI sync
|
|
939
|
+
// Use cleanedToolCalls to preserve arguments in chatHistory
|
|
940
|
+
cleanedToolCalls.forEach((toolCall) => {
|
|
941
|
+
const toolCallEntry = {
|
|
942
|
+
type: "tool_call",
|
|
943
|
+
content: "Executing...",
|
|
944
|
+
timestamp: new Date(),
|
|
945
|
+
toolCall: toolCall,
|
|
946
|
+
};
|
|
947
|
+
this.chatHistory.push(toolCallEntry);
|
|
948
|
+
});
|
|
949
|
+
// Execute tools
|
|
950
|
+
let toolIndex = 0;
|
|
951
|
+
const completedToolCallIds = new Set();
|
|
952
|
+
try {
|
|
953
|
+
for (const toolCall of cleanedToolCalls) {
|
|
954
|
+
// Check for cancellation before executing each tool
|
|
955
|
+
if (this.abortController?.signal.aborted) {
|
|
956
|
+
console.error(`Tool execution cancelled after ${toolIndex}/${cleanedToolCalls.length} tools`);
|
|
957
|
+
// Add cancelled responses for remaining uncompleted tools
|
|
958
|
+
for (let i = toolIndex; i < cleanedToolCalls.length; i++) {
|
|
959
|
+
const remainingToolCall = cleanedToolCalls[i];
|
|
960
|
+
this.messages.push({
|
|
961
|
+
role: "tool",
|
|
962
|
+
content: "[Cancelled by user]",
|
|
963
|
+
tool_call_id: remainingToolCall.id,
|
|
964
|
+
});
|
|
965
|
+
completedToolCallIds.add(remainingToolCall.id);
|
|
966
|
+
}
|
|
967
|
+
yield {
|
|
968
|
+
type: "content",
|
|
969
|
+
content: "\n\n[Operation cancelled by user]",
|
|
970
|
+
};
|
|
971
|
+
yield { type: "done" };
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
// Capture chatHistory length before tool execution to detect new system messages
|
|
975
|
+
const chatHistoryLengthBefore = this.chatHistory.length;
|
|
976
|
+
const result = await this.executeTool(toolCall);
|
|
977
|
+
// Collect any new system messages added during tool execution (from hooks)
|
|
978
|
+
const newSystemMessages = [];
|
|
979
|
+
for (let i = chatHistoryLengthBefore; i < this.chatHistory.length; i++) {
|
|
980
|
+
if (this.chatHistory[i].type === "system") {
|
|
981
|
+
newSystemMessages.push(this.chatHistory[i]);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
yield {
|
|
985
|
+
type: "tool_result",
|
|
986
|
+
toolCall,
|
|
987
|
+
toolResult: result,
|
|
988
|
+
systemMessages: newSystemMessages.length > 0 ? newSystemMessages : undefined,
|
|
989
|
+
};
|
|
990
|
+
// Update the tool_call entry in chatHistory to tool_result
|
|
991
|
+
const entryIndex = this.chatHistory.findIndex((entry) => entry.type === "tool_call" && entry.toolCall?.id === toolCall.id);
|
|
992
|
+
if (entryIndex !== -1) {
|
|
993
|
+
this.chatHistory[entryIndex] = {
|
|
994
|
+
...this.chatHistory[entryIndex],
|
|
995
|
+
type: "tool_result",
|
|
996
|
+
content: result.success
|
|
997
|
+
? (result.output?.trim() || "Success")
|
|
998
|
+
: (result.error?.trim() || "Error occurred"),
|
|
999
|
+
toolResult: result,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
// Add tool result with proper format (needed for AI context)
|
|
1003
|
+
this.messages.push({
|
|
1004
|
+
role: "tool",
|
|
1005
|
+
content: result.success
|
|
1006
|
+
? result.output || "Success"
|
|
1007
|
+
: result.error || "Error",
|
|
1008
|
+
tool_call_id: toolCall.id,
|
|
1009
|
+
});
|
|
1010
|
+
completedToolCallIds.add(toolCall.id);
|
|
1011
|
+
toolIndex++;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
finally {
|
|
1015
|
+
// Ensure ALL tool calls in this.messages have results, even if we crashed/errored
|
|
1016
|
+
for (const toolCall of cleanedToolCalls) {
|
|
1017
|
+
if (!completedToolCallIds.has(toolCall.id)) {
|
|
1018
|
+
this.messages.push({
|
|
1019
|
+
role: "tool",
|
|
1020
|
+
content: "[Error: Tool execution interrupted]",
|
|
1021
|
+
tool_call_id: toolCall.id,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
// After all tool results are added, add any system messages from this tool round
|
|
1027
|
+
// System messages are added to chatHistory during tool execution (for display)
|
|
1028
|
+
// Now we add them to this.messages in the same order (after all tool results)
|
|
1029
|
+
// Find the most recent assistant message with tool_calls in chatHistory (search backwards)
|
|
1030
|
+
let assistantIndex = -1;
|
|
1031
|
+
for (let i = this.chatHistory.length - 1; i >= 0; i--) {
|
|
1032
|
+
const entry = this.chatHistory[i];
|
|
1033
|
+
if (entry.type === "assistant" && entry.tool_calls && entry.tool_calls.length > 0) {
|
|
1034
|
+
assistantIndex = i;
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (assistantIndex !== -1) {
|
|
1039
|
+
// Collect system messages that appeared after this assistant message
|
|
1040
|
+
for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
|
|
1041
|
+
const entry = this.chatHistory[i];
|
|
1042
|
+
if (entry.type === 'system' && entry.content && entry.content.trim()) {
|
|
1043
|
+
this.messages.push({
|
|
1044
|
+
role: 'system',
|
|
1045
|
+
content: entry.content
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
// Stop if we hit another assistant or user message (next turn)
|
|
1049
|
+
if (entry.type === 'assistant' || entry.type === 'user') {
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// Update token count after processing all tool calls to include tool results
|
|
1055
|
+
inputTokens = this.tokenCounter.countMessageTokens(this.messages);
|
|
1056
|
+
// Final token update after tools processed
|
|
1057
|
+
yield {
|
|
1058
|
+
type: "token_count",
|
|
1059
|
+
tokenCount: inputTokens + totalOutputTokens,
|
|
1060
|
+
};
|
|
1061
|
+
// Continue the loop to get the next response (which might have more tool calls)
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
// No tool calls, we're done
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (toolRounds >= maxToolRounds) {
|
|
1069
|
+
yield {
|
|
1070
|
+
type: "content",
|
|
1071
|
+
content: "\n\nMaximum tool execution rounds reached. Stopping to prevent infinite loops.",
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
// Mark first message as processed so subsequent messages use cached tools
|
|
1075
|
+
this.firstMessageProcessed = true;
|
|
1076
|
+
// Check if tool support changed during first message processing
|
|
1077
|
+
// If model doesn't support tools, regenerate system message without tool list
|
|
1078
|
+
const supportsToolsAfter = this.grokClient.getSupportsTools();
|
|
1079
|
+
if (!supportsToolsAfter && supportsTools) {
|
|
1080
|
+
// Tool support was disabled during first message - regenerate system message
|
|
1081
|
+
await this.buildSystemMessage();
|
|
1082
|
+
}
|
|
1083
|
+
yield { type: "done" };
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
// Check if this was a cancellation (check both abort signal and error name)
|
|
1087
|
+
if (this.abortController?.signal.aborted || error.name === 'AbortError' || error.code === 'ABORT_ERR') {
|
|
1088
|
+
yield {
|
|
1089
|
+
type: "content",
|
|
1090
|
+
content: "\n\n[Operation cancelled by user]",
|
|
1091
|
+
};
|
|
1092
|
+
yield { type: "done" };
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const errorEntry = {
|
|
1096
|
+
type: "assistant",
|
|
1097
|
+
content: `Sorry, I encountered an error: ${error.message}`,
|
|
1098
|
+
timestamp: new Date(),
|
|
1099
|
+
};
|
|
1100
|
+
this.chatHistory.push(errorEntry);
|
|
1101
|
+
yield {
|
|
1102
|
+
type: "content",
|
|
1103
|
+
content: errorEntry.content,
|
|
1104
|
+
};
|
|
1105
|
+
// Mark first message as processed even on error
|
|
1106
|
+
this.firstMessageProcessed = true;
|
|
1107
|
+
yield { type: "done" };
|
|
1108
|
+
}
|
|
1109
|
+
finally {
|
|
1110
|
+
// Clean up abort controller
|
|
1111
|
+
this.abortController = null;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Apply default parameter values for tools
|
|
1116
|
+
* This ensures the approval hook sees the same parameters that will be used during execution
|
|
1117
|
+
*/
|
|
1118
|
+
applyToolParameterDefaults(toolName, params) {
|
|
1119
|
+
// Handle null/undefined params (can happen if API sends "null" as arguments string)
|
|
1120
|
+
const result = { ...(params || {}) };
|
|
1121
|
+
switch (toolName) {
|
|
1122
|
+
case "listFiles":
|
|
1123
|
+
// dirname defaults to current directory
|
|
1124
|
+
if (!result.dirname) {
|
|
1125
|
+
result.dirname = ".";
|
|
1126
|
+
}
|
|
1127
|
+
break;
|
|
1128
|
+
// Add other tools with defaults here as needed
|
|
1129
|
+
}
|
|
1130
|
+
return result;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Validate tool arguments against the tool's schema
|
|
1134
|
+
* Returns null if valid, or an error message if invalid
|
|
1135
|
+
*/
|
|
1136
|
+
async validateToolArguments(toolName, args) {
|
|
1137
|
+
try {
|
|
1138
|
+
// Get all tools (including MCP tools)
|
|
1139
|
+
const supportsTools = this.grokClient.getSupportsTools();
|
|
1140
|
+
const allTools = supportsTools ? await getAllGrokTools() : [];
|
|
1141
|
+
// Find the tool schema
|
|
1142
|
+
const toolSchema = allTools.find(t => t.function.name === toolName);
|
|
1143
|
+
if (!toolSchema) {
|
|
1144
|
+
return `Unknown tool: ${toolName}`;
|
|
1145
|
+
}
|
|
1146
|
+
const schema = toolSchema.function.parameters;
|
|
1147
|
+
const properties = schema.properties || {};
|
|
1148
|
+
const required = schema.required || [];
|
|
1149
|
+
// Check if tool accepts no parameters
|
|
1150
|
+
const acceptsNoParams = Object.keys(properties).length === 0;
|
|
1151
|
+
const hasArgs = args && typeof args === 'object' && Object.keys(args).length > 0;
|
|
1152
|
+
if (acceptsNoParams && hasArgs) {
|
|
1153
|
+
return `Tool ${toolName} accepts no parameters, but received: ${JSON.stringify(args)}`;
|
|
1154
|
+
}
|
|
1155
|
+
// Check for unknown parameters
|
|
1156
|
+
for (const argKey of Object.keys(args || {})) {
|
|
1157
|
+
if (!properties[argKey]) {
|
|
1158
|
+
return `Tool ${toolName} does not accept parameter '${argKey}'. Valid parameters: ${Object.keys(properties).join(', ') || 'none'}`;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
// Check for missing required parameters
|
|
1162
|
+
for (const requiredParam of required) {
|
|
1163
|
+
if (!(requiredParam in (args || {}))) {
|
|
1164
|
+
return `Tool ${toolName} missing required parameter '${requiredParam}'`;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return null; // Valid
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
console.error(`Error validating tool arguments for ${toolName}:`, error);
|
|
1171
|
+
return null; // Allow execution if validation itself fails
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
async executeTool(toolCall) {
|
|
1175
|
+
try {
|
|
1176
|
+
// Parse arguments - handle empty string as empty object for parameter-less tools
|
|
1177
|
+
let argsString = toolCall.function.arguments?.trim() || "{}";
|
|
1178
|
+
// Handle duplicate/concatenated JSON objects (LLM bug)
|
|
1179
|
+
// Pattern: {"key":"val"}{"key":"val"}
|
|
1180
|
+
let hadDuplicateJson = false;
|
|
1181
|
+
if (argsString.includes('}{')) {
|
|
1182
|
+
try {
|
|
1183
|
+
// Find the end of the first complete JSON object
|
|
1184
|
+
let depth = 0;
|
|
1185
|
+
let firstObjEnd = -1;
|
|
1186
|
+
for (let i = 0; i < argsString.length; i++) {
|
|
1187
|
+
if (argsString[i] === '{')
|
|
1188
|
+
depth++;
|
|
1189
|
+
if (argsString[i] === '}') {
|
|
1190
|
+
depth--;
|
|
1191
|
+
if (depth === 0) {
|
|
1192
|
+
firstObjEnd = i + 1;
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (firstObjEnd > 0 && firstObjEnd < argsString.length) {
|
|
1198
|
+
// Extract and validate first object
|
|
1199
|
+
const firstObj = argsString.substring(0, firstObjEnd);
|
|
1200
|
+
JSON.parse(firstObj); // Validate it's valid JSON
|
|
1201
|
+
// Use only the first object
|
|
1202
|
+
hadDuplicateJson = true;
|
|
1203
|
+
argsString = firstObj;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
catch (e) {
|
|
1207
|
+
// If extraction fails, continue with original string
|
|
1208
|
+
// The error will be caught by the main JSON.parse below
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
let args = JSON.parse(argsString);
|
|
1212
|
+
// Handle multiple layers of JSON encoding (API bug)
|
|
1213
|
+
// Keep parsing until we get an object, not a string
|
|
1214
|
+
let parseCount = 0;
|
|
1215
|
+
while (typeof args === 'string' && parseCount < 5) {
|
|
1216
|
+
parseCount++;
|
|
1217
|
+
try {
|
|
1218
|
+
args = JSON.parse(args);
|
|
1219
|
+
}
|
|
1220
|
+
catch (e) {
|
|
1221
|
+
// If parse fails, the string isn't valid JSON - stop trying
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
// Log if we had to fix encoding
|
|
1226
|
+
if (parseCount > 0) {
|
|
1227
|
+
const bugMsg = `[BUG] Tool ${toolCall.function.name} had ${parseCount} extra layer(s) of JSON encoding`;
|
|
1228
|
+
console.warn(bugMsg);
|
|
1229
|
+
const systemMsg = `Warning: Tool arguments for ${toolCall.function.name} had ${parseCount} extra encoding layer(s) - this is an API bug`;
|
|
1230
|
+
this.messages.push({
|
|
1231
|
+
role: 'system',
|
|
1232
|
+
content: systemMsg
|
|
1233
|
+
});
|
|
1234
|
+
this.chatHistory.push({
|
|
1235
|
+
type: 'system',
|
|
1236
|
+
content: systemMsg,
|
|
1237
|
+
timestamp: new Date()
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
// Log if we had to fix duplicate JSON
|
|
1241
|
+
if (hadDuplicateJson) {
|
|
1242
|
+
const bugMsg = `[BUG] Tool ${toolCall.function.name} had duplicate/concatenated JSON objects`;
|
|
1243
|
+
console.warn(bugMsg);
|
|
1244
|
+
const systemMsg = `Warning: Tool arguments for ${toolCall.function.name} had duplicate JSON objects (used first object only) - this is an LLM bug`;
|
|
1245
|
+
this.messages.push({
|
|
1246
|
+
role: 'system',
|
|
1247
|
+
content: systemMsg
|
|
1248
|
+
});
|
|
1249
|
+
this.chatHistory.push({
|
|
1250
|
+
type: 'system',
|
|
1251
|
+
content: systemMsg,
|
|
1252
|
+
timestamp: new Date()
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
// Ensure args is always an object (API might send null)
|
|
1256
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
1257
|
+
args = {};
|
|
1258
|
+
}
|
|
1259
|
+
// Apply parameter defaults before validation and execution
|
|
1260
|
+
args = this.applyToolParameterDefaults(toolCall.function.name, args);
|
|
1261
|
+
// Validate tool arguments against schema
|
|
1262
|
+
const validationError = await this.validateToolArguments(toolCall.function.name, args);
|
|
1263
|
+
if (validationError) {
|
|
1264
|
+
// Add system message explaining the validation error
|
|
1265
|
+
const systemMsg = `Tool call validation failed: ${validationError}. Please try again with correct parameters.`;
|
|
1266
|
+
console.warn(`[VALIDATION ERROR] ${systemMsg}`);
|
|
1267
|
+
this.messages.push({
|
|
1268
|
+
role: 'system',
|
|
1269
|
+
content: systemMsg
|
|
1270
|
+
});
|
|
1271
|
+
this.chatHistory.push({
|
|
1272
|
+
type: 'system',
|
|
1273
|
+
content: systemMsg,
|
|
1274
|
+
timestamp: new Date()
|
|
1275
|
+
});
|
|
1276
|
+
return {
|
|
1277
|
+
success: false,
|
|
1278
|
+
error: validationError
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
// Task tools (startActiveTask, transitionActiveTaskStatus, stopActiveTask) have their own
|
|
1282
|
+
// dedicated task approval hook, so skip the general tool approval hook for them
|
|
1283
|
+
const isTaskTool = ['startActiveTask', 'transitionActiveTaskStatus', 'stopActiveTask'].includes(toolCall.function.name);
|
|
1284
|
+
// Check tool approval hook if configured (skip for task tools)
|
|
1285
|
+
const settings = getSettingsManager();
|
|
1286
|
+
const toolApprovalHook = settings.getToolApprovalHook();
|
|
1287
|
+
if (toolApprovalHook && !isTaskTool) {
|
|
1288
|
+
const approvalResult = await executeToolApprovalHook(toolApprovalHook, toolCall.function.name, args, 30000, // 30 second timeout
|
|
1289
|
+
this.getCurrentTokenCount(), this.getMaxContextSize());
|
|
1290
|
+
if (!approvalResult.approved) {
|
|
1291
|
+
const reason = approvalResult.reason || "Tool execution denied by approval hook";
|
|
1292
|
+
// Process rejection commands (MODEL, SYSTEM, BACKEND, etc.)
|
|
1293
|
+
await this.processHookResult(approvalResult);
|
|
1294
|
+
return {
|
|
1295
|
+
success: false,
|
|
1296
|
+
error: `Tool execution blocked: ${reason}`,
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
if (approvalResult.timedOut) {
|
|
1300
|
+
// Log timeout for debugging (don't block)
|
|
1301
|
+
console.warn(`Tool approval hook timed out for ${toolCall.function.name} (auto-approved)`);
|
|
1302
|
+
}
|
|
1303
|
+
// Process hook commands (ENV, TOOL_RESULT, MODEL, SYSTEM, BACKEND, etc.)
|
|
1304
|
+
// TOOL_RESULT is for tool return values, not used by approval hook
|
|
1305
|
+
// ENV variables can affect tool behavior if tools read from process.env
|
|
1306
|
+
await this.processHookResult(approvalResult);
|
|
1307
|
+
}
|
|
1308
|
+
switch (toolCall.function.name) {
|
|
1309
|
+
case "viewFile":
|
|
1310
|
+
{
|
|
1311
|
+
let range;
|
|
1312
|
+
range = args.start_line && args.end_line
|
|
1313
|
+
? [args.start_line, args.end_line]
|
|
1314
|
+
: undefined;
|
|
1315
|
+
return await this.textEditor.viewFile(args.filename, range);
|
|
1316
|
+
}
|
|
1317
|
+
case "createNewFile":
|
|
1318
|
+
return await this.textEditor.createNewFile(args.filename, args.content);
|
|
1319
|
+
case "strReplace":
|
|
1320
|
+
return await this.textEditor.strReplace(args.filename, args.old_str, args.new_str, args.replace_all);
|
|
1321
|
+
case "editFile":
|
|
1322
|
+
if (!this.morphEditor) {
|
|
1323
|
+
return {
|
|
1324
|
+
success: false,
|
|
1325
|
+
error: "Morph Fast Apply not available. Please set MORPH_API_KEY environment variable to use this feature.",
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
return await this.morphEditor.editFile(args.filename, args.instructions, args.code_edit);
|
|
1329
|
+
case "execute":
|
|
1330
|
+
return await this.zsh.execute(args.command);
|
|
1331
|
+
case "listFiles":
|
|
1332
|
+
return await this.zsh.listFiles(args.dirname);
|
|
1333
|
+
case "universalSearch":
|
|
1334
|
+
return await this.search.universalSearch(args.query, {
|
|
1335
|
+
searchType: args.search_type,
|
|
1336
|
+
includePattern: args.include_pattern,
|
|
1337
|
+
excludePattern: args.exclude_pattern,
|
|
1338
|
+
caseSensitive: args.case_sensitive,
|
|
1339
|
+
wholeWord: args.whole_word,
|
|
1340
|
+
regex: args.regex,
|
|
1341
|
+
maxResults: args.max_results,
|
|
1342
|
+
fileTypes: args.file_types,
|
|
1343
|
+
includeHidden: args.include_hidden,
|
|
1344
|
+
});
|
|
1345
|
+
case "getEnv":
|
|
1346
|
+
return await this.env.getEnv(args.variable);
|
|
1347
|
+
case "getAllEnv":
|
|
1348
|
+
return await this.env.getAllEnv();
|
|
1349
|
+
case "searchEnv":
|
|
1350
|
+
return await this.env.searchEnv(args.pattern);
|
|
1351
|
+
case "introspect":
|
|
1352
|
+
return await this.introspect.introspect(args.target);
|
|
1353
|
+
case "clearCache":
|
|
1354
|
+
return await this.clearCacheTool.clearCache(args.confirmationCode);
|
|
1355
|
+
case "restart":
|
|
1356
|
+
return await this.restartTool.restart();
|
|
1357
|
+
case "setPersona":
|
|
1358
|
+
return await this.characterTool.setPersona(args.persona, args.color);
|
|
1359
|
+
case "setMood":
|
|
1360
|
+
return await this.characterTool.setMood(args.mood, args.color);
|
|
1361
|
+
case "getPersona":
|
|
1362
|
+
return await this.characterTool.getPersona();
|
|
1363
|
+
case "getMood":
|
|
1364
|
+
return await this.characterTool.getMood();
|
|
1365
|
+
case "getAvailablePersonas":
|
|
1366
|
+
return await this.characterTool.getAvailablePersonas();
|
|
1367
|
+
case "startActiveTask":
|
|
1368
|
+
return await this.taskTool.startActiveTask(args.activeTask, args.action, args.color);
|
|
1369
|
+
case "transitionActiveTaskStatus":
|
|
1370
|
+
return await this.taskTool.transitionActiveTaskStatus(args.action, args.color);
|
|
1371
|
+
case "stopActiveTask":
|
|
1372
|
+
return await this.taskTool.stopActiveTask(args.reason, args.documentationFile, args.color);
|
|
1373
|
+
case "insertLines":
|
|
1374
|
+
return await this.textEditor.insertLines(args.filename, args.insert_line, args.new_str);
|
|
1375
|
+
case "replaceLines":
|
|
1376
|
+
return await this.textEditor.replaceLines(args.filename, args.start_line, args.end_line, args.new_str);
|
|
1377
|
+
case "undoEdit":
|
|
1378
|
+
return await this.textEditor.undoEdit();
|
|
1379
|
+
case "chdir":
|
|
1380
|
+
return this.zsh.chdir(args.dirname);
|
|
1381
|
+
case "pwdir":
|
|
1382
|
+
return this.zsh.pwdir();
|
|
1383
|
+
case "downloadFile":
|
|
1384
|
+
return await this.internetTool.downloadFile(args.url);
|
|
1385
|
+
case "generateImage":
|
|
1386
|
+
return await this.imageTool.generateImage(args.prompt, args.negativePrompt, args.width, args.height, args.model, args.sampler, args.configScale, args.numSteps, args.nsfw, args.name, args.move, args.seed);
|
|
1387
|
+
case "captionImage":
|
|
1388
|
+
return await this.imageTool.captionImage(args.filename, args.prompt);
|
|
1389
|
+
case "pngInfo":
|
|
1390
|
+
return await this.imageTool.pngInfo(args.filename);
|
|
1391
|
+
case "readXlsx":
|
|
1392
|
+
return await this.fileConversionTool.readXlsx(args.filename, args.sheetName, args.outputFormat, args.output);
|
|
1393
|
+
case "listXlsxSheets":
|
|
1394
|
+
return await this.fileConversionTool.listXlsxSheets(args.filename);
|
|
1395
|
+
default:
|
|
1396
|
+
// Check if this is an MCP tool
|
|
1397
|
+
if (toolCall.function.name.startsWith("mcp__")) {
|
|
1398
|
+
return await this.executeMCPTool(toolCall.function.name, args);
|
|
1399
|
+
}
|
|
1400
|
+
return {
|
|
1401
|
+
success: false,
|
|
1402
|
+
error: `Unknown tool: ${toolCall.function.name}`,
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
catch (error) {
|
|
1407
|
+
return {
|
|
1408
|
+
success: false,
|
|
1409
|
+
error: `Tool execution error: ${error.message}`,
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
async executeMCPTool(toolName, args) {
|
|
1414
|
+
try {
|
|
1415
|
+
const mcpManager = getMCPManager();
|
|
1416
|
+
const result = await mcpManager.callTool(toolName, args);
|
|
1417
|
+
if (result.isError) {
|
|
1418
|
+
return {
|
|
1419
|
+
success: false,
|
|
1420
|
+
error: result.content[0]?.text || "MCP tool error",
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
// Extract content from result
|
|
1424
|
+
const output = result.content
|
|
1425
|
+
.map((item) => {
|
|
1426
|
+
if (item.type === "text") {
|
|
1427
|
+
return item.text;
|
|
1428
|
+
}
|
|
1429
|
+
else if (item.type === "resource") {
|
|
1430
|
+
return `Resource: ${item.resource?.uri || "Unknown"}`;
|
|
1431
|
+
}
|
|
1432
|
+
return String(item);
|
|
1433
|
+
})
|
|
1434
|
+
.join("\n");
|
|
1435
|
+
// After successful MCP tool execution, invalidate cache for that server
|
|
1436
|
+
// Next call to getAllGrokTools() will lazy-refresh this server
|
|
1437
|
+
const serverNameMatch = toolName.match(/^mcp__(.+?)__/);
|
|
1438
|
+
if (serverNameMatch) {
|
|
1439
|
+
const serverName = serverNameMatch[1];
|
|
1440
|
+
mcpManager.invalidateCache(serverName);
|
|
1441
|
+
}
|
|
1442
|
+
return {
|
|
1443
|
+
success: true,
|
|
1444
|
+
output: output || "Success",
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
catch (error) {
|
|
1448
|
+
return {
|
|
1449
|
+
success: false,
|
|
1450
|
+
error: `MCP tool execution error: ${error.message}`,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
getChatHistory() {
|
|
1455
|
+
return [...this.chatHistory];
|
|
1456
|
+
}
|
|
1457
|
+
setChatHistory(history) {
|
|
1458
|
+
this.chatHistory = [...history];
|
|
1459
|
+
}
|
|
1460
|
+
getSystemPrompt() {
|
|
1461
|
+
return this.systemPrompt;
|
|
1462
|
+
}
|
|
1463
|
+
setSystemPrompt(prompt) {
|
|
1464
|
+
this.systemPrompt = prompt;
|
|
1465
|
+
this.messages[0] = {
|
|
1466
|
+
role: "system",
|
|
1467
|
+
content: prompt,
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
getMessages() {
|
|
1471
|
+
return [...this.messages];
|
|
1472
|
+
}
|
|
1473
|
+
getCurrentTokenCount() {
|
|
1474
|
+
return this.tokenCounter.countMessageTokens(this.messages);
|
|
1475
|
+
}
|
|
1476
|
+
getMaxContextSize() {
|
|
1477
|
+
// TODO: Make this model-specific when different models have different context windows
|
|
1478
|
+
// For now, return the standard Grok context window size
|
|
1479
|
+
return 128000;
|
|
1480
|
+
}
|
|
1481
|
+
getContextUsagePercent() {
|
|
1482
|
+
const current = this.getCurrentTokenCount();
|
|
1483
|
+
const max = this.getMaxContextSize();
|
|
1484
|
+
return (current / max) * 100;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Convert context messages to markdown format for viewing
|
|
1488
|
+
* Format: (N) Name (role) - timestamp
|
|
1489
|
+
*/
|
|
1490
|
+
async convertContextToMarkdown() {
|
|
1491
|
+
const lines = [];
|
|
1492
|
+
// Header
|
|
1493
|
+
const { ChatHistoryManager } = await import("../utils/chat-history-manager.js");
|
|
1494
|
+
const historyManager = ChatHistoryManager.getInstance();
|
|
1495
|
+
const contextFilePath = historyManager.getContextFilePath();
|
|
1496
|
+
lines.push("# Conversation Context");
|
|
1497
|
+
lines.push(`Context File: ${contextFilePath}`);
|
|
1498
|
+
lines.push(`Session: ${process.env.ZDS_AI_AGENT_SESSION || "N/A"}`);
|
|
1499
|
+
lines.push(`Tokens: ${this.getCurrentTokenCount()} / ${this.getMaxContextSize()} (${this.getContextUsagePercent().toFixed(1)}%)`);
|
|
1500
|
+
lines.push("");
|
|
1501
|
+
lines.push("---");
|
|
1502
|
+
lines.push("");
|
|
1503
|
+
// Get agent name from environment or default
|
|
1504
|
+
const agentName = process.env.ZDS_AI_AGENT_BOT_NAME || "Assistant";
|
|
1505
|
+
const userName = process.env.ZDS_AI_AGENT_MESSAGE_AUTHOR || "User";
|
|
1506
|
+
// Process messages
|
|
1507
|
+
this.chatHistory.forEach((entry, index) => {
|
|
1508
|
+
const msgNum = index + 1;
|
|
1509
|
+
const timestamp = entry.timestamp.toLocaleTimeString();
|
|
1510
|
+
if (entry.type === 'user') {
|
|
1511
|
+
lines.push(`(${msgNum}) ${userName} (user) - ${timestamp}`);
|
|
1512
|
+
lines.push(entry.content || "");
|
|
1513
|
+
lines.push("");
|
|
1514
|
+
}
|
|
1515
|
+
else if (entry.type === 'assistant') {
|
|
1516
|
+
lines.push(`(${msgNum}) ${agentName} (assistant) - ${timestamp}`);
|
|
1517
|
+
lines.push(entry.content || "");
|
|
1518
|
+
lines.push("");
|
|
1519
|
+
}
|
|
1520
|
+
else if (entry.type === 'system') {
|
|
1521
|
+
lines.push(`(${msgNum}) System (system) - ${timestamp}`);
|
|
1522
|
+
lines.push(entry.content || "");
|
|
1523
|
+
lines.push("");
|
|
1524
|
+
}
|
|
1525
|
+
else if (entry.type === 'tool_call') {
|
|
1526
|
+
const toolCall = entry.toolCall;
|
|
1527
|
+
const toolName = toolCall?.function?.name || "unknown";
|
|
1528
|
+
const params = toolCall?.function?.arguments ? JSON.parse(toolCall.function.arguments) : {};
|
|
1529
|
+
lines.push(`(${msgNum}) ${agentName} (tool_call: ${toolName}) - ${timestamp}`);
|
|
1530
|
+
lines.push(`Parameters: ${JSON.stringify(params, null, 2)}`);
|
|
1531
|
+
lines.push("");
|
|
1532
|
+
}
|
|
1533
|
+
else if (entry.type === 'tool_result') {
|
|
1534
|
+
const toolCall = entry.toolCall;
|
|
1535
|
+
const toolName = toolCall?.function?.name || "unknown";
|
|
1536
|
+
lines.push(`(${msgNum}) System (tool_result: ${toolName}) - ${timestamp}`);
|
|
1537
|
+
lines.push(entry.content || "");
|
|
1538
|
+
lines.push("");
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
return lines.join("\n");
|
|
1542
|
+
}
|
|
1543
|
+
getPersona() {
|
|
1544
|
+
return this.persona;
|
|
1545
|
+
}
|
|
1546
|
+
getPersonaColor() {
|
|
1547
|
+
return this.personaColor;
|
|
1548
|
+
}
|
|
1549
|
+
getMood() {
|
|
1550
|
+
return this.mood;
|
|
1551
|
+
}
|
|
1552
|
+
getMoodColor() {
|
|
1553
|
+
return this.moodColor;
|
|
1554
|
+
}
|
|
1555
|
+
getActiveTask() {
|
|
1556
|
+
return this.activeTask;
|
|
1557
|
+
}
|
|
1558
|
+
getActiveTaskAction() {
|
|
1559
|
+
return this.activeTaskAction;
|
|
1560
|
+
}
|
|
1561
|
+
getActiveTaskColor() {
|
|
1562
|
+
return this.activeTaskColor;
|
|
1563
|
+
}
|
|
1564
|
+
setPendingContextEdit(tmpJsonPath, contextFilePath) {
|
|
1565
|
+
this.pendingContextEdit = { tmpJsonPath, contextFilePath };
|
|
1566
|
+
}
|
|
1567
|
+
getPendingContextEdit() {
|
|
1568
|
+
return this.pendingContextEdit;
|
|
1569
|
+
}
|
|
1570
|
+
clearPendingContextEdit() {
|
|
1571
|
+
this.pendingContextEdit = null;
|
|
1572
|
+
}
|
|
1573
|
+
async setPersona(persona, color) {
|
|
1574
|
+
// Execute hook if configured
|
|
1575
|
+
const settings = getSettingsManager();
|
|
1576
|
+
const hookPath = settings.getPersonaHook();
|
|
1577
|
+
const hookMandatory = settings.isPersonaHookMandatory();
|
|
1578
|
+
if (!hookPath && hookMandatory) {
|
|
1579
|
+
const reason = "Persona hook is mandatory but not configured";
|
|
1580
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
1581
|
+
this.chatHistory.push({
|
|
1582
|
+
type: 'system',
|
|
1583
|
+
content: `Failed to change persona to "${persona}": ${reason}`,
|
|
1584
|
+
timestamp: new Date()
|
|
1585
|
+
});
|
|
1586
|
+
return {
|
|
1587
|
+
success: false,
|
|
1588
|
+
error: reason
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
if (hookPath) {
|
|
1592
|
+
const hookResult = await executeOperationHook(hookPath, "setPersona", {
|
|
1593
|
+
persona_old: this.persona || "",
|
|
1594
|
+
persona_new: persona,
|
|
1595
|
+
persona_color: color || "white"
|
|
1596
|
+
}, 30000, hookMandatory, this.getCurrentTokenCount(), this.getMaxContextSize());
|
|
1597
|
+
if (!hookResult.approved) {
|
|
1598
|
+
const reason = hookResult.reason || "Hook rejected persona change";
|
|
1599
|
+
// Process rejection commands (MODEL, SYSTEM)
|
|
1600
|
+
// Even in rejection, we process commands (might have MODEL change)
|
|
1601
|
+
await this.processHookResult(hookResult);
|
|
1602
|
+
// Note: We ignore the return value here since we're already rejecting the persona
|
|
1603
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
1604
|
+
this.chatHistory.push({
|
|
1605
|
+
type: 'system',
|
|
1606
|
+
content: `Failed to change persona to "${persona}": ${reason}`,
|
|
1607
|
+
timestamp: new Date()
|
|
1608
|
+
});
|
|
1609
|
+
return {
|
|
1610
|
+
success: false,
|
|
1611
|
+
error: reason
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
if (hookResult.timedOut) {
|
|
1615
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
1616
|
+
this.chatHistory.push({
|
|
1617
|
+
type: 'system',
|
|
1618
|
+
content: `Persona hook timed out (auto-approved)`,
|
|
1619
|
+
timestamp: new Date()
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
// Process hook commands (ENV, MODEL, SYSTEM)
|
|
1623
|
+
const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_PERSONA');
|
|
1624
|
+
if (!result.success) {
|
|
1625
|
+
// Model/backend test failed - don't apply persona change
|
|
1626
|
+
return {
|
|
1627
|
+
success: false,
|
|
1628
|
+
error: "Persona change rejected due to failed model/backend test"
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
// Apply persona transformation if present
|
|
1632
|
+
if (result.transformedValue) {
|
|
1633
|
+
persona = result.transformedValue;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
const oldPersona = this.persona;
|
|
1637
|
+
const oldColor = this.personaColor;
|
|
1638
|
+
this.persona = persona;
|
|
1639
|
+
this.personaColor = color || "white";
|
|
1640
|
+
process.env.ZDS_AI_AGENT_PERSONA = persona;
|
|
1641
|
+
// Add system message for recordkeeping
|
|
1642
|
+
let systemContent;
|
|
1643
|
+
if (oldPersona) {
|
|
1644
|
+
const oldColorStr = oldColor && oldColor !== "white" ? ` (${oldColor})` : "";
|
|
1645
|
+
const newColorStr = this.personaColor && this.personaColor !== "white" ? ` (${this.personaColor})` : "";
|
|
1646
|
+
systemContent = `Assistant changed the persona from "${oldPersona}"${oldColorStr} to "${this.persona}"${newColorStr}`;
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
const colorStr = this.personaColor && this.personaColor !== "white" ? ` (${this.personaColor})` : "";
|
|
1650
|
+
systemContent = `Assistant set the persona to "${this.persona}"${colorStr}`;
|
|
1651
|
+
}
|
|
1652
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
1653
|
+
// System messages added during tool execution create invalid message sequences
|
|
1654
|
+
// because they get inserted between tool_calls and tool_results
|
|
1655
|
+
this.chatHistory.push({
|
|
1656
|
+
type: 'system',
|
|
1657
|
+
content: systemContent,
|
|
1658
|
+
timestamp: new Date()
|
|
1659
|
+
});
|
|
1660
|
+
this.emit('personaChange', {
|
|
1661
|
+
persona: this.persona,
|
|
1662
|
+
color: this.personaColor
|
|
1663
|
+
});
|
|
1664
|
+
return { success: true };
|
|
1665
|
+
}
|
|
1666
|
+
async setMood(mood, color) {
|
|
1667
|
+
// Execute hook if configured
|
|
1668
|
+
const settings = getSettingsManager();
|
|
1669
|
+
const hookPath = settings.getMoodHook();
|
|
1670
|
+
const hookMandatory = settings.isMoodHookMandatory();
|
|
1671
|
+
if (!hookPath && hookMandatory) {
|
|
1672
|
+
const reason = "Mood hook is mandatory but not configured";
|
|
1673
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
1674
|
+
this.chatHistory.push({
|
|
1675
|
+
type: 'system',
|
|
1676
|
+
content: `Failed to change mood to "${mood}": ${reason}`,
|
|
1677
|
+
timestamp: new Date()
|
|
1678
|
+
});
|
|
1679
|
+
return {
|
|
1680
|
+
success: false,
|
|
1681
|
+
error: reason
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
if (hookPath) {
|
|
1685
|
+
const hookResult = await executeOperationHook(hookPath, "setMood", {
|
|
1686
|
+
mood_old: this.mood || "",
|
|
1687
|
+
mood_new: mood,
|
|
1688
|
+
mood_color: color || "white"
|
|
1689
|
+
}, 30000, hookMandatory, this.getCurrentTokenCount(), this.getMaxContextSize());
|
|
1690
|
+
if (!hookResult.approved) {
|
|
1691
|
+
const reason = hookResult.reason || "Hook rejected mood change";
|
|
1692
|
+
// Process rejection commands (MODEL, SYSTEM)
|
|
1693
|
+
await this.processHookResult(hookResult);
|
|
1694
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
1695
|
+
this.chatHistory.push({
|
|
1696
|
+
type: 'system',
|
|
1697
|
+
content: `Failed to change mood to "${mood}": ${reason}`,
|
|
1698
|
+
timestamp: new Date()
|
|
1699
|
+
});
|
|
1700
|
+
return {
|
|
1701
|
+
success: false,
|
|
1702
|
+
error: reason
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
if (hookResult.timedOut) {
|
|
1706
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
1707
|
+
this.chatHistory.push({
|
|
1708
|
+
type: 'system',
|
|
1709
|
+
content: `Mood hook timed out (auto-approved)`,
|
|
1710
|
+
timestamp: new Date()
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
// Process hook commands (ENV, MODEL, SYSTEM)
|
|
1714
|
+
const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_MOOD');
|
|
1715
|
+
if (!result.success) {
|
|
1716
|
+
// Model/backend test failed - don't apply mood change
|
|
1717
|
+
return {
|
|
1718
|
+
success: false,
|
|
1719
|
+
error: "Mood change rejected due to failed model/backend test"
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
// Apply mood transformation if present
|
|
1723
|
+
if (result.transformedValue) {
|
|
1724
|
+
mood = result.transformedValue;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const oldMood = this.mood;
|
|
1728
|
+
const oldColor = this.moodColor;
|
|
1729
|
+
this.mood = mood;
|
|
1730
|
+
this.moodColor = color || "white";
|
|
1731
|
+
process.env.ZDS_AI_AGENT_MOOD = mood;
|
|
1732
|
+
// Add system message for recordkeeping
|
|
1733
|
+
let systemContent;
|
|
1734
|
+
if (oldMood) {
|
|
1735
|
+
const oldColorStr = oldColor && oldColor !== "white" ? ` (${oldColor})` : "";
|
|
1736
|
+
const newColorStr = this.moodColor && this.moodColor !== "white" ? ` (${this.moodColor})` : "";
|
|
1737
|
+
systemContent = `Assistant changed the mood from "${oldMood}"${oldColorStr} to "${this.mood}"${newColorStr}`;
|
|
1738
|
+
}
|
|
1739
|
+
else {
|
|
1740
|
+
const colorStr = this.moodColor && this.moodColor !== "white" ? ` (${this.moodColor})` : "";
|
|
1741
|
+
systemContent = `Assistant set the mood to "${this.mood}"${colorStr}`;
|
|
1742
|
+
}
|
|
1743
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
1744
|
+
// System messages added during tool execution create invalid message sequences
|
|
1745
|
+
// because they get inserted between tool_calls and tool_results
|
|
1746
|
+
this.chatHistory.push({
|
|
1747
|
+
type: 'system',
|
|
1748
|
+
content: systemContent,
|
|
1749
|
+
timestamp: new Date()
|
|
1750
|
+
});
|
|
1751
|
+
this.emit('moodChange', {
|
|
1752
|
+
mood: this.mood,
|
|
1753
|
+
color: this.moodColor
|
|
1754
|
+
});
|
|
1755
|
+
return { success: true };
|
|
1756
|
+
}
|
|
1757
|
+
async startActiveTask(activeTask, action, color) {
|
|
1758
|
+
// Cannot start new task if one already exists
|
|
1759
|
+
if (this.activeTask) {
|
|
1760
|
+
return {
|
|
1761
|
+
success: false,
|
|
1762
|
+
error: `Cannot start new task "${activeTask}". Active task "${this.activeTask}" must be stopped first.`
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
// Execute hook if configured
|
|
1766
|
+
const settings = getSettingsManager();
|
|
1767
|
+
const hookPath = settings.getTaskApprovalHook();
|
|
1768
|
+
if (hookPath) {
|
|
1769
|
+
const hookResult = await executeOperationHook(hookPath, "startActiveTask", {
|
|
1770
|
+
activetask: activeTask,
|
|
1771
|
+
action: action,
|
|
1772
|
+
color: color || "white"
|
|
1773
|
+
}, 30000, false, // Task hook is not mandatory
|
|
1774
|
+
this.getCurrentTokenCount(), this.getMaxContextSize());
|
|
1775
|
+
// Process hook commands (MODEL, SYSTEM, ENV, BACKEND, etc.) for both approval and rejection
|
|
1776
|
+
await this.processHookResult(hookResult);
|
|
1777
|
+
if (!hookResult.approved) {
|
|
1778
|
+
const reason = hookResult.reason || "Hook rejected task start";
|
|
1779
|
+
this.messages.push({
|
|
1780
|
+
role: 'system',
|
|
1781
|
+
content: `Failed to start task "${activeTask}": ${reason}`
|
|
1782
|
+
});
|
|
1783
|
+
return {
|
|
1784
|
+
success: false,
|
|
1785
|
+
error: reason
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
if (hookResult.timedOut) {
|
|
1789
|
+
this.messages.push({
|
|
1790
|
+
role: 'system',
|
|
1791
|
+
content: `Task start hook timed out (auto-approved)`
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
// Set the task
|
|
1796
|
+
this.activeTask = activeTask;
|
|
1797
|
+
this.activeTaskAction = action;
|
|
1798
|
+
this.activeTaskColor = color || "white";
|
|
1799
|
+
// Add system message
|
|
1800
|
+
const colorStr = this.activeTaskColor && this.activeTaskColor !== "white" ? ` (${this.activeTaskColor})` : "";
|
|
1801
|
+
this.messages.push({
|
|
1802
|
+
role: 'system',
|
|
1803
|
+
content: `Assistant changed task status for "${this.activeTask}" to ${this.activeTaskAction}${colorStr}`
|
|
1804
|
+
});
|
|
1805
|
+
// Emit event
|
|
1806
|
+
this.emit('activeTaskChange', {
|
|
1807
|
+
activeTask: this.activeTask,
|
|
1808
|
+
action: this.activeTaskAction,
|
|
1809
|
+
color: this.activeTaskColor
|
|
1810
|
+
});
|
|
1811
|
+
return { success: true };
|
|
1812
|
+
}
|
|
1813
|
+
async transitionActiveTaskStatus(action, color) {
|
|
1814
|
+
// Cannot transition if no active task
|
|
1815
|
+
if (!this.activeTask) {
|
|
1816
|
+
return {
|
|
1817
|
+
success: false,
|
|
1818
|
+
error: "Cannot transition task status. No active task is currently running."
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
// Execute hook if configured
|
|
1822
|
+
const settings = getSettingsManager();
|
|
1823
|
+
const hookPath = settings.getTaskApprovalHook();
|
|
1824
|
+
if (hookPath) {
|
|
1825
|
+
const hookResult = await executeOperationHook(hookPath, "transitionActiveTaskStatus", {
|
|
1826
|
+
action: action,
|
|
1827
|
+
color: color || "white"
|
|
1828
|
+
}, 30000, false, // Task hook is not mandatory
|
|
1829
|
+
this.getCurrentTokenCount(), this.getMaxContextSize());
|
|
1830
|
+
// Process hook commands (MODEL, SYSTEM, ENV, BACKEND, etc.) for both approval and rejection
|
|
1831
|
+
await this.processHookResult(hookResult);
|
|
1832
|
+
if (!hookResult.approved) {
|
|
1833
|
+
const reason = hookResult.reason || "Hook rejected task status transition";
|
|
1834
|
+
this.messages.push({
|
|
1835
|
+
role: 'system',
|
|
1836
|
+
content: `Failed to transition task "${this.activeTask}" from ${this.activeTaskAction} to ${action}: ${reason}`
|
|
1837
|
+
});
|
|
1838
|
+
return {
|
|
1839
|
+
success: false,
|
|
1840
|
+
error: reason
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
if (hookResult.timedOut) {
|
|
1844
|
+
this.messages.push({
|
|
1845
|
+
role: 'system',
|
|
1846
|
+
content: `Task transition hook timed out (auto-approved)`
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
// Store old action for system message
|
|
1851
|
+
const oldAction = this.activeTaskAction;
|
|
1852
|
+
// Update the action and color
|
|
1853
|
+
this.activeTaskAction = action;
|
|
1854
|
+
this.activeTaskColor = color || this.activeTaskColor;
|
|
1855
|
+
// Add system message
|
|
1856
|
+
const colorStr = this.activeTaskColor && this.activeTaskColor !== "white" ? ` (${this.activeTaskColor})` : "";
|
|
1857
|
+
this.messages.push({
|
|
1858
|
+
role: 'system',
|
|
1859
|
+
content: `Assistant changed task status for "${this.activeTask}" from ${oldAction} to ${this.activeTaskAction}${colorStr}`
|
|
1860
|
+
});
|
|
1861
|
+
// Emit event
|
|
1862
|
+
this.emit('activeTaskChange', {
|
|
1863
|
+
activeTask: this.activeTask,
|
|
1864
|
+
action: this.activeTaskAction,
|
|
1865
|
+
color: this.activeTaskColor
|
|
1866
|
+
});
|
|
1867
|
+
return { success: true };
|
|
1868
|
+
}
|
|
1869
|
+
async stopActiveTask(reason, documentationFile, color) {
|
|
1870
|
+
// Cannot stop if no active task
|
|
1871
|
+
if (!this.activeTask) {
|
|
1872
|
+
return {
|
|
1873
|
+
success: false,
|
|
1874
|
+
error: "Cannot stop task. No active task is currently running."
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
// Record the start time for 3-second minimum
|
|
1878
|
+
const startTime = Date.now();
|
|
1879
|
+
// Execute hook if configured
|
|
1880
|
+
const settings = getSettingsManager();
|
|
1881
|
+
const hookPath = settings.getTaskApprovalHook();
|
|
1882
|
+
if (hookPath) {
|
|
1883
|
+
const hookResult = await executeOperationHook(hookPath, "stopActiveTask", {
|
|
1884
|
+
reason: reason,
|
|
1885
|
+
documentation_file: documentationFile,
|
|
1886
|
+
color: color || "white"
|
|
1887
|
+
}, 30000, false, // Task hook is not mandatory
|
|
1888
|
+
this.getCurrentTokenCount(), this.getMaxContextSize());
|
|
1889
|
+
// Process hook commands (MODEL, SYSTEM, ENV, BACKEND, etc.) for both approval and rejection
|
|
1890
|
+
await this.processHookResult(hookResult);
|
|
1891
|
+
if (!hookResult.approved) {
|
|
1892
|
+
const hookReason = hookResult.reason || "Hook rejected task stop";
|
|
1893
|
+
this.messages.push({
|
|
1894
|
+
role: 'system',
|
|
1895
|
+
content: `Failed to stop task "${this.activeTask}": ${hookReason}`
|
|
1896
|
+
});
|
|
1897
|
+
return {
|
|
1898
|
+
success: false,
|
|
1899
|
+
error: hookReason
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
if (hookResult.timedOut) {
|
|
1903
|
+
this.messages.push({
|
|
1904
|
+
role: 'system',
|
|
1905
|
+
content: `Task stop hook timed out (auto-approved)`
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
// Calculate remaining time to meet 3-second minimum
|
|
1910
|
+
const elapsed = Date.now() - startTime;
|
|
1911
|
+
const minimumDelay = 3000;
|
|
1912
|
+
const remainingDelay = Math.max(0, minimumDelay - elapsed);
|
|
1913
|
+
// Wait for remaining time if needed
|
|
1914
|
+
if (remainingDelay > 0) {
|
|
1915
|
+
await new Promise(resolve => setTimeout(resolve, remainingDelay));
|
|
1916
|
+
}
|
|
1917
|
+
// Store task info for system message before clearing
|
|
1918
|
+
const stoppedTask = this.activeTask;
|
|
1919
|
+
const stoppedAction = this.activeTaskAction;
|
|
1920
|
+
// Clear the task
|
|
1921
|
+
this.activeTask = "";
|
|
1922
|
+
this.activeTaskAction = "";
|
|
1923
|
+
this.activeTaskColor = "white";
|
|
1924
|
+
// Add system message
|
|
1925
|
+
const colorStr = color && color !== "white" ? ` (${color})` : "";
|
|
1926
|
+
this.messages.push({
|
|
1927
|
+
role: 'system',
|
|
1928
|
+
content: `Assistant stopped task "${stoppedTask}" (was ${stoppedAction}) with reason: ${reason}${colorStr}`
|
|
1929
|
+
});
|
|
1930
|
+
// Emit event to clear widget
|
|
1931
|
+
this.emit('activeTaskChange', {
|
|
1932
|
+
activeTask: "",
|
|
1933
|
+
action: "",
|
|
1934
|
+
color: "white"
|
|
1935
|
+
});
|
|
1936
|
+
return { success: true };
|
|
1937
|
+
}
|
|
1938
|
+
async emitContextChange() {
|
|
1939
|
+
const percent = this.getContextUsagePercent();
|
|
1940
|
+
this.emit('contextChange', {
|
|
1941
|
+
current: this.getCurrentTokenCount(),
|
|
1942
|
+
max: this.getMaxContextSize(),
|
|
1943
|
+
percent
|
|
1944
|
+
});
|
|
1945
|
+
// Add system warnings based on context usage (may auto-clear at 100%)
|
|
1946
|
+
await this.addContextWarningIfNeeded(percent);
|
|
1947
|
+
}
|
|
1948
|
+
async addContextWarningIfNeeded(percent) {
|
|
1949
|
+
let warning = null;
|
|
1950
|
+
const roundedPercent = Math.round(percent);
|
|
1951
|
+
if (percent >= 100) {
|
|
1952
|
+
// Auto-clear at 100%+ to prevent exceeding context limits
|
|
1953
|
+
warning = `CONTEXT LIMIT REACHED: You are at ${roundedPercent}% context capacity! Automatically clearing cache to prevent context overflow...`;
|
|
1954
|
+
this.messages.push({
|
|
1955
|
+
role: 'system',
|
|
1956
|
+
content: warning
|
|
1957
|
+
});
|
|
1958
|
+
// Perform automatic cache clear
|
|
1959
|
+
await this.clearCache();
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
if (percent >= 95) {
|
|
1963
|
+
// Very stern warning at 95%+ (every time)
|
|
1964
|
+
warning = `CRITICAL CONTEXT WARNING: You are at ${roundedPercent}% context capacity! You MUST immediately save any notes and lessons learned, then run the 'clearCache' tool to reset the conversation context. The conversation will fail if you do not take action now.`;
|
|
1965
|
+
}
|
|
1966
|
+
else if (percent >= 90 && !this.contextWarningAt90) {
|
|
1967
|
+
// Dire warning at 90% (one time only)
|
|
1968
|
+
this.contextWarningAt90 = true;
|
|
1969
|
+
warning = `URGENT CONTEXT WARNING: You are at ${roundedPercent}% context capacity! Perform your final tasks or responses and prepare to be reset.`;
|
|
1970
|
+
}
|
|
1971
|
+
else if (percent >= 80 && !this.contextWarningAt80) {
|
|
1972
|
+
// Initial warning at 80% (one time only)
|
|
1973
|
+
this.contextWarningAt80 = true;
|
|
1974
|
+
warning = `Context Warning: You are at ${roundedPercent}% context capacity! You are approaching the limit. Be concise and avoid lengthy outputs.`;
|
|
1975
|
+
}
|
|
1976
|
+
if (warning) {
|
|
1977
|
+
// Add as a system message
|
|
1978
|
+
this.messages.push({
|
|
1979
|
+
role: 'system',
|
|
1980
|
+
content: warning
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
async executeCommand(command, skipConfirmation = false) {
|
|
1985
|
+
return await this.zsh.execute(command, 30000, skipConfirmation);
|
|
1986
|
+
}
|
|
1987
|
+
getCurrentModel() {
|
|
1988
|
+
return this.grokClient.getCurrentModel();
|
|
1989
|
+
}
|
|
1990
|
+
setModel(model) {
|
|
1991
|
+
this.grokClient.setModel(model);
|
|
1992
|
+
// Update token counter for new model
|
|
1993
|
+
this.tokenCounter.dispose();
|
|
1994
|
+
this.tokenCounter = createTokenCounter(model);
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Strip in-progress tool calls from messages for backend/model testing
|
|
1998
|
+
* Removes tool_calls from the last assistant message and any corresponding tool results
|
|
1999
|
+
* @returns Cleaned copy of messages array, or original if no stripping needed
|
|
2000
|
+
*/
|
|
2001
|
+
static stripInProgressToolCalls(messages) {
|
|
2002
|
+
// Find the last assistant message
|
|
2003
|
+
let lastAssistantIndex = -1;
|
|
2004
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2005
|
+
if (messages[i].role === 'assistant') {
|
|
2006
|
+
lastAssistantIndex = i;
|
|
2007
|
+
break;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
// If no assistant message or it has no tool_calls, return original
|
|
2011
|
+
if (lastAssistantIndex === -1 || !messages[lastAssistantIndex].tool_calls) {
|
|
2012
|
+
return messages;
|
|
2013
|
+
}
|
|
2014
|
+
// Create deep copy to avoid modifying original
|
|
2015
|
+
const cleanedMessages = JSON.parse(JSON.stringify(messages));
|
|
2016
|
+
// Collect tool_call_ids from the last assistant message
|
|
2017
|
+
const toolCallIds = new Set((cleanedMessages[lastAssistantIndex].tool_calls || []).map((tc) => tc.id));
|
|
2018
|
+
// Remove tool_calls from the last assistant message
|
|
2019
|
+
delete cleanedMessages[lastAssistantIndex].tool_calls;
|
|
2020
|
+
// Remove any tool result messages that correspond to those tool_call_ids
|
|
2021
|
+
// (in case some finished but not all)
|
|
2022
|
+
return cleanedMessages.filter((msg, idx) => {
|
|
2023
|
+
if (idx <= lastAssistantIndex) {
|
|
2024
|
+
return true; // Keep all messages before and including the assistant message
|
|
2025
|
+
}
|
|
2026
|
+
if (msg.role === 'tool' && toolCallIds.has(msg.tool_call_id)) {
|
|
2027
|
+
return false; // Remove tool results for the in-progress tool calls
|
|
2028
|
+
}
|
|
2029
|
+
return true;
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Test a model change by making a test API call with current conversation context
|
|
2034
|
+
* Rolls back to previous model if test fails
|
|
2035
|
+
* @param newModel Model to test
|
|
2036
|
+
* @returns Promise with success status and optional error message
|
|
2037
|
+
*/
|
|
2038
|
+
async testModel(newModel) {
|
|
2039
|
+
const previousModel = this.getCurrentModel();
|
|
2040
|
+
const previousTokenCounter = this.tokenCounter;
|
|
2041
|
+
// Strip in-progress tool calls to avoid sending incomplete assistant messages
|
|
2042
|
+
const testMessages = GrokAgent.stripInProgressToolCalls(this.messages);
|
|
2043
|
+
// Build request payload for logging
|
|
2044
|
+
const supportsTools = this.grokClient.getSupportsTools();
|
|
2045
|
+
const tools = supportsTools ? await getAllGrokTools() : [];
|
|
2046
|
+
const requestPayload = {
|
|
2047
|
+
model: newModel,
|
|
2048
|
+
messages: testMessages,
|
|
2049
|
+
tools: supportsTools && tools.length > 0 ? tools : undefined,
|
|
2050
|
+
temperature: this.temperature,
|
|
2051
|
+
max_tokens: 10
|
|
2052
|
+
};
|
|
2053
|
+
try {
|
|
2054
|
+
// Temporarily set the new model
|
|
2055
|
+
this.grokClient.setModel(newModel);
|
|
2056
|
+
this.tokenCounter = createTokenCounter(newModel);
|
|
2057
|
+
// Test with actual conversation context to verify the model can handle it
|
|
2058
|
+
// This catches issues like ollama models that fail to parse tool calls
|
|
2059
|
+
const response = await this.grokClient.chat(testMessages, tools, newModel, undefined, this.temperature, undefined, 10);
|
|
2060
|
+
// Check if response is valid
|
|
2061
|
+
if (!response || !response.choices || response.choices.length === 0) {
|
|
2062
|
+
throw new Error("Invalid response from API");
|
|
2063
|
+
}
|
|
2064
|
+
// Test succeeded - keep the new model
|
|
2065
|
+
previousTokenCounter.dispose();
|
|
2066
|
+
return { success: true };
|
|
2067
|
+
}
|
|
2068
|
+
catch (error) {
|
|
2069
|
+
// Test failed - roll back to previous model
|
|
2070
|
+
this.grokClient.setModel(previousModel);
|
|
2071
|
+
this.tokenCounter.dispose();
|
|
2072
|
+
this.tokenCounter = previousTokenCounter;
|
|
2073
|
+
// Log test failure with full request/response for debugging
|
|
2074
|
+
const { message: logPaths } = await logApiError(requestPayload, error, { errorType: 'model-switch-test-failure', previousModel, newModel }, 'test-fail');
|
|
2075
|
+
const errorMessage = error.message || "Unknown error during model test";
|
|
2076
|
+
return {
|
|
2077
|
+
success: false,
|
|
2078
|
+
error: `Model test failed: ${errorMessage}\n${logPaths}`
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Test backend/baseUrl/model changes by making a test API call with current conversation context
|
|
2084
|
+
* Rolls back all changes if test fails
|
|
2085
|
+
* @param backend Backend display name
|
|
2086
|
+
* @param baseUrl Base URL for API calls
|
|
2087
|
+
* @param apiKeyEnvVar Name of environment variable containing API key
|
|
2088
|
+
* @param model Model to use (optional, uses current model if not specified)
|
|
2089
|
+
* @returns Promise with success status and optional error message
|
|
2090
|
+
*/
|
|
2091
|
+
async testBackendModelChange(backend, baseUrl, apiKeyEnvVar, model) {
|
|
2092
|
+
const previousClient = this.grokClient;
|
|
2093
|
+
const previousApiKeyEnvVar = this.apiKeyEnvVar;
|
|
2094
|
+
const previousBackend = this.grokClient.getBackendName();
|
|
2095
|
+
const previousModel = this.getCurrentModel();
|
|
2096
|
+
let requestPayload;
|
|
2097
|
+
let newModel;
|
|
2098
|
+
try {
|
|
2099
|
+
// Get API key from environment
|
|
2100
|
+
const apiKey = process.env[apiKeyEnvVar];
|
|
2101
|
+
if (!apiKey) {
|
|
2102
|
+
throw new Error(`API key not found in environment variable: ${apiKeyEnvVar}`);
|
|
2103
|
+
}
|
|
2104
|
+
// Use current model if not specified
|
|
2105
|
+
newModel = model || this.getCurrentModel();
|
|
2106
|
+
// Create new client with new configuration
|
|
2107
|
+
this.grokClient = new GrokClient(apiKey, newModel, baseUrl, backend);
|
|
2108
|
+
// Store the API key env var name for session persistence
|
|
2109
|
+
this.apiKeyEnvVar = apiKeyEnvVar;
|
|
2110
|
+
// Reinitialize MCP servers since we're switching to a new backend/model
|
|
2111
|
+
try {
|
|
2112
|
+
const config = loadMCPConfig();
|
|
2113
|
+
if (config.servers.length > 0) {
|
|
2114
|
+
await initializeMCPServers();
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
catch (mcpError) {
|
|
2118
|
+
console.warn("MCP reinitialization failed:", mcpError);
|
|
2119
|
+
}
|
|
2120
|
+
// Strip in-progress tool calls to avoid sending incomplete assistant messages
|
|
2121
|
+
const testMessages = GrokAgent.stripInProgressToolCalls(this.messages);
|
|
2122
|
+
// Build request payload for logging
|
|
2123
|
+
const supportsTools = this.grokClient.getSupportsTools();
|
|
2124
|
+
const tools = supportsTools ? await getAllGrokTools() : [];
|
|
2125
|
+
requestPayload = {
|
|
2126
|
+
backend,
|
|
2127
|
+
baseUrl,
|
|
2128
|
+
model: newModel,
|
|
2129
|
+
messages: testMessages,
|
|
2130
|
+
tools: supportsTools && tools.length > 0 ? tools : undefined,
|
|
2131
|
+
temperature: this.temperature,
|
|
2132
|
+
max_tokens: 10
|
|
2133
|
+
};
|
|
2134
|
+
// Test with actual conversation context to verify the backend/model can handle it
|
|
2135
|
+
// This catches issues like ollama models that fail to parse tool calls
|
|
2136
|
+
const response = await this.grokClient.chat(testMessages, tools, newModel, undefined, this.temperature, undefined, 10);
|
|
2137
|
+
// Check if response is valid
|
|
2138
|
+
if (!response || !response.choices || response.choices.length === 0) {
|
|
2139
|
+
throw new Error("Invalid response from API");
|
|
2140
|
+
}
|
|
2141
|
+
// Test succeeded - new client is now active
|
|
2142
|
+
return { success: true };
|
|
2143
|
+
}
|
|
2144
|
+
catch (error) {
|
|
2145
|
+
// Test failed - restore previous client and API key env var
|
|
2146
|
+
this.grokClient = previousClient;
|
|
2147
|
+
this.apiKeyEnvVar = previousApiKeyEnvVar;
|
|
2148
|
+
// Log test failure with full request/response for debugging (if we got far enough to build the payload)
|
|
2149
|
+
let logPaths = '';
|
|
2150
|
+
if (requestPayload) {
|
|
2151
|
+
const result = await logApiError(requestPayload, error, {
|
|
2152
|
+
errorType: 'backend-switch-test-failure',
|
|
2153
|
+
previousBackend,
|
|
2154
|
+
previousModel,
|
|
2155
|
+
newBackend: backend,
|
|
2156
|
+
newModel,
|
|
2157
|
+
baseUrl,
|
|
2158
|
+
apiKeyEnvVar
|
|
2159
|
+
}, 'test-fail');
|
|
2160
|
+
logPaths = result.message;
|
|
2161
|
+
}
|
|
2162
|
+
const errorMessage = error.message || "Unknown error during backend/model test";
|
|
2163
|
+
return {
|
|
2164
|
+
success: false,
|
|
2165
|
+
error: logPaths ? `${errorMessage}\n${logPaths}` : errorMessage
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Process hook result including commands and transformations
|
|
2171
|
+
* Handles ENV transformations, model/backend testing, and error messaging
|
|
2172
|
+
* @param hookResult Hook execution result
|
|
2173
|
+
* @param envKey Optional ENV key to check for transformation (e.g., ZDS_AI_AGENT_PERSONA)
|
|
2174
|
+
* @returns Object with success status and transformed value (if any)
|
|
2175
|
+
*/
|
|
2176
|
+
async processHookResult(hookResult, envKey) {
|
|
2177
|
+
if (!hookResult.commands) {
|
|
2178
|
+
return { success: true };
|
|
2179
|
+
}
|
|
2180
|
+
const results = applyHookCommands(hookResult.commands);
|
|
2181
|
+
// Check for transformation via ENV if key provided
|
|
2182
|
+
let transformedValue;
|
|
2183
|
+
if (envKey && results.env[envKey]) {
|
|
2184
|
+
transformedValue = results.env[envKey];
|
|
2185
|
+
}
|
|
2186
|
+
// Process commands (test model/backend, apply ENV vars, add SYSTEM messages)
|
|
2187
|
+
const success = await this.processHookCommands(results);
|
|
2188
|
+
return { success, transformedValue };
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Process hook commands (MODEL, BACKEND, BASE_URL, SYSTEM, ENV)
|
|
2192
|
+
* Handles model/backend testing and error messaging
|
|
2193
|
+
* @param commands Hook commands from applyHookCommands()
|
|
2194
|
+
*/
|
|
2195
|
+
async processHookCommands(commands) {
|
|
2196
|
+
// Import the helper function
|
|
2197
|
+
const { applyEnvVariables } = await import('../utils/hook-executor.js');
|
|
2198
|
+
// Check if backend or model change is requested
|
|
2199
|
+
const hasBackendChange = commands.backend && commands.baseUrl && commands.apiKeyEnvVar;
|
|
2200
|
+
const hasModelChange = commands.model;
|
|
2201
|
+
// Test backend/model changes FIRST before applying anything
|
|
2202
|
+
if (hasBackendChange) {
|
|
2203
|
+
// Backend change - test backend/baseUrl/model together
|
|
2204
|
+
const testResult = await this.testBackendModelChange(commands.backend, commands.baseUrl, commands.apiKeyEnvVar, commands.model);
|
|
2205
|
+
if (!testResult.success) {
|
|
2206
|
+
// Test failed - don't apply ANYTHING
|
|
2207
|
+
const parts = [];
|
|
2208
|
+
if (commands.backend)
|
|
2209
|
+
parts.push(`backend to "${commands.backend}"`);
|
|
2210
|
+
if (commands.model)
|
|
2211
|
+
parts.push(`model to "${commands.model}"`);
|
|
2212
|
+
const errorMsg = `Failed to change ${parts.join(' and ')}: ${testResult.error}`;
|
|
2213
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
2214
|
+
this.chatHistory.push({
|
|
2215
|
+
type: "system",
|
|
2216
|
+
content: errorMsg,
|
|
2217
|
+
timestamp: new Date(),
|
|
2218
|
+
});
|
|
2219
|
+
return false; // Signal failure - caller should not apply other changes
|
|
2220
|
+
}
|
|
2221
|
+
// Test succeeded - apply ENV variables and add success message
|
|
2222
|
+
applyEnvVariables(commands.env);
|
|
2223
|
+
const parts = [];
|
|
2224
|
+
if (commands.backend)
|
|
2225
|
+
parts.push(`backend to "${commands.backend}"`);
|
|
2226
|
+
if (commands.model)
|
|
2227
|
+
parts.push(`model to "${commands.model}"`);
|
|
2228
|
+
const successMsg = `Changed ${parts.join(' and ')}`;
|
|
2229
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
2230
|
+
this.chatHistory.push({
|
|
2231
|
+
type: "system",
|
|
2232
|
+
content: successMsg,
|
|
2233
|
+
timestamp: new Date(),
|
|
2234
|
+
});
|
|
2235
|
+
// Emit events for UI updates
|
|
2236
|
+
if (commands.backend) {
|
|
2237
|
+
this.emit('backendChange', {
|
|
2238
|
+
backend: commands.backend
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
if (commands.model) {
|
|
2242
|
+
this.emit('modelChange', {
|
|
2243
|
+
model: commands.model
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
else if (hasModelChange) {
|
|
2248
|
+
// Model-only change
|
|
2249
|
+
const testResult = await this.testModel(commands.model);
|
|
2250
|
+
if (!testResult.success) {
|
|
2251
|
+
// Test failed - don't apply ANYTHING
|
|
2252
|
+
const errorMsg = `Failed to change model to "${commands.model}": ${testResult.error}`;
|
|
2253
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
2254
|
+
this.chatHistory.push({
|
|
2255
|
+
type: "system",
|
|
2256
|
+
content: errorMsg,
|
|
2257
|
+
timestamp: new Date(),
|
|
2258
|
+
});
|
|
2259
|
+
return false; // Signal failure - caller should not apply other changes
|
|
2260
|
+
}
|
|
2261
|
+
// Test succeeded - apply ENV variables and add success message
|
|
2262
|
+
applyEnvVariables(commands.env);
|
|
2263
|
+
const successMsg = `Model changed to "${commands.model}"`;
|
|
2264
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
2265
|
+
this.chatHistory.push({
|
|
2266
|
+
type: "system",
|
|
2267
|
+
content: successMsg,
|
|
2268
|
+
timestamp: new Date(),
|
|
2269
|
+
});
|
|
2270
|
+
// Emit modelChange event for UI updates
|
|
2271
|
+
this.emit('modelChange', {
|
|
2272
|
+
model: commands.model
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
else {
|
|
2276
|
+
// No model or backend change - just apply ENV variables
|
|
2277
|
+
applyEnvVariables(commands.env);
|
|
2278
|
+
}
|
|
2279
|
+
// Add SYSTEM message if present
|
|
2280
|
+
if (commands.system) {
|
|
2281
|
+
// Note: Don't add to this.messages during tool execution - only chatHistory
|
|
2282
|
+
this.chatHistory.push({
|
|
2283
|
+
type: "system",
|
|
2284
|
+
content: commands.system,
|
|
2285
|
+
timestamp: new Date(),
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
return true; // Signal success - caller can apply other changes
|
|
2289
|
+
}
|
|
2290
|
+
getBackend() {
|
|
2291
|
+
// Just return the backend name from the client (no detection)
|
|
2292
|
+
return this.grokClient.getBackendName();
|
|
2293
|
+
}
|
|
2294
|
+
abortCurrentOperation() {
|
|
2295
|
+
if (this.abortController) {
|
|
2296
|
+
this.abortController.abort();
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
async clearCache() {
|
|
2300
|
+
const { ChatHistoryManager } = await import("../utils/chat-history-manager.js");
|
|
2301
|
+
const { executeStartupHook } = await import("../utils/startup-hook.js");
|
|
2302
|
+
const { executeOperationHook, applyHookCommands } = await import("../utils/hook-executor.js");
|
|
2303
|
+
const historyManager = ChatHistoryManager.getInstance();
|
|
2304
|
+
// Backup current context to timestamped files
|
|
2305
|
+
historyManager.backupHistory();
|
|
2306
|
+
// Clear the context
|
|
2307
|
+
this.chatHistory = [];
|
|
2308
|
+
this.messages = [];
|
|
2309
|
+
this.contextWarningAt80 = false;
|
|
2310
|
+
this.contextWarningAt90 = false;
|
|
2311
|
+
this.firstMessageProcessed = false;
|
|
2312
|
+
// Add temporary system message (will be replaced by initialize())
|
|
2313
|
+
this.messages.push({
|
|
2314
|
+
role: "system",
|
|
2315
|
+
content: "Initializing...",
|
|
2316
|
+
});
|
|
2317
|
+
this.chatHistory.push({
|
|
2318
|
+
type: "system",
|
|
2319
|
+
content: "Initializing...",
|
|
2320
|
+
timestamp: new Date(),
|
|
2321
|
+
});
|
|
2322
|
+
try {
|
|
2323
|
+
// Re-execute startup hook to get fresh output
|
|
2324
|
+
this.startupHookOutput = await executeStartupHook();
|
|
2325
|
+
// Reinitialize with system message and startup hook
|
|
2326
|
+
// Instance hook runs automatically at end of initialize()
|
|
2327
|
+
await this.initialize();
|
|
2328
|
+
}
|
|
2329
|
+
catch (error) {
|
|
2330
|
+
console.error("Error during initialize() in clearCache():", error);
|
|
2331
|
+
// Continue anyway - we still want to save the cleared state
|
|
2332
|
+
}
|
|
2333
|
+
// Save the cleared state FIRST before emitting (in case emit causes exit)
|
|
2334
|
+
const sessionState = this.getSessionState();
|
|
2335
|
+
historyManager.saveContext(this.systemPrompt, this.chatHistory, sessionState);
|
|
2336
|
+
historyManager.saveMessages(this.messages);
|
|
2337
|
+
// Emit context change WITHOUT calling addContextWarningIfNeeded (to avoid recursive clearCache)
|
|
2338
|
+
const percent = this.getContextUsagePercent();
|
|
2339
|
+
this.emit('contextChange', {
|
|
2340
|
+
current: this.getCurrentTokenCount(),
|
|
2341
|
+
max: this.getMaxContextSize(),
|
|
2342
|
+
percent
|
|
2343
|
+
});
|
|
2344
|
+
// Note: Intentionally NOT calling addContextWarningIfNeeded here to prevent recursion
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Get current session state for persistence
|
|
2348
|
+
*/
|
|
2349
|
+
getSessionState() {
|
|
2350
|
+
return {
|
|
2351
|
+
session: process.env.ZDS_AI_AGENT_SESSION || "",
|
|
2352
|
+
persona: this.persona,
|
|
2353
|
+
personaColor: this.personaColor,
|
|
2354
|
+
mood: this.mood,
|
|
2355
|
+
moodColor: this.moodColor,
|
|
2356
|
+
activeTask: this.activeTask,
|
|
2357
|
+
activeTaskAction: this.activeTaskAction,
|
|
2358
|
+
activeTaskColor: this.activeTaskColor,
|
|
2359
|
+
cwd: process.cwd(),
|
|
2360
|
+
contextCurrent: this.getCurrentTokenCount(),
|
|
2361
|
+
contextMax: this.getMaxContextSize(),
|
|
2362
|
+
backend: this.grokClient.getBackendName(),
|
|
2363
|
+
baseUrl: this.grokClient.getBaseURL(),
|
|
2364
|
+
apiKeyEnvVar: this.apiKeyEnvVar,
|
|
2365
|
+
model: this.getCurrentModel(),
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
/**
|
|
2369
|
+
* Restore session state from persistence
|
|
2370
|
+
*/
|
|
2371
|
+
async restoreSessionState(state) {
|
|
2372
|
+
// Restore session ID
|
|
2373
|
+
if (state.session) {
|
|
2374
|
+
process.env.ZDS_AI_AGENT_SESSION = state.session;
|
|
2375
|
+
}
|
|
2376
|
+
// Restore cwd early (hooks may need correct working directory)
|
|
2377
|
+
if (state.cwd) {
|
|
2378
|
+
try {
|
|
2379
|
+
process.chdir(state.cwd);
|
|
2380
|
+
}
|
|
2381
|
+
catch (error) {
|
|
2382
|
+
console.warn(`Failed to restore working directory to ${state.cwd}:`, error);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
// Restore backend/baseUrl/apiKeyEnvVar/model if present (creates initial client)
|
|
2386
|
+
if (state.backend && state.baseUrl && state.apiKeyEnvVar) {
|
|
2387
|
+
try {
|
|
2388
|
+
// Get API key from environment
|
|
2389
|
+
const apiKey = process.env[state.apiKeyEnvVar];
|
|
2390
|
+
if (apiKey) {
|
|
2391
|
+
// Create new client with restored configuration
|
|
2392
|
+
const model = state.model || this.getCurrentModel();
|
|
2393
|
+
this.grokClient = new GrokClient(apiKey, model, state.baseUrl, state.backend);
|
|
2394
|
+
this.apiKeyEnvVar = state.apiKeyEnvVar;
|
|
2395
|
+
// Reinitialize MCP servers when restoring session
|
|
2396
|
+
try {
|
|
2397
|
+
const config = loadMCPConfig();
|
|
2398
|
+
if (config.servers.length > 0) {
|
|
2399
|
+
await initializeMCPServers();
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
catch (mcpError) {
|
|
2403
|
+
console.warn("MCP reinitialization failed:", mcpError);
|
|
2404
|
+
}
|
|
2405
|
+
// Dispose old token counter and create new one for the restored model
|
|
2406
|
+
this.tokenCounter.dispose();
|
|
2407
|
+
this.tokenCounter = createTokenCounter(model);
|
|
2408
|
+
// Emit events for UI updates
|
|
2409
|
+
this.emit('backendChange', { backend: state.backend });
|
|
2410
|
+
this.emit('modelChange', { model });
|
|
2411
|
+
}
|
|
2412
|
+
else {
|
|
2413
|
+
console.warn(`Failed to restore backend: API key not found in environment variable ${state.apiKeyEnvVar}`);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
catch (error) {
|
|
2417
|
+
console.warn(`Failed to restore backend configuration:`, error);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
// Restore persona (hook may change backend/model and sets env vars)
|
|
2421
|
+
if (state.persona) {
|
|
2422
|
+
try {
|
|
2423
|
+
await this.setPersona(state.persona, state.personaColor);
|
|
2424
|
+
}
|
|
2425
|
+
catch (error) {
|
|
2426
|
+
console.warn(`Failed to restore persona "${state.persona}":`, error);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
// Restore mood (hook sets env vars)
|
|
2430
|
+
if (state.mood) {
|
|
2431
|
+
try {
|
|
2432
|
+
await this.setMood(state.mood, state.moodColor);
|
|
2433
|
+
}
|
|
2434
|
+
catch (error) {
|
|
2435
|
+
console.warn(`Failed to restore mood "${state.mood}":`, error);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
// Restore active task (hook sets env vars)
|
|
2439
|
+
if (state.activeTask) {
|
|
2440
|
+
try {
|
|
2441
|
+
await this.startActiveTask(state.activeTask, state.activeTaskAction, state.activeTaskColor);
|
|
2442
|
+
}
|
|
2443
|
+
catch (error) {
|
|
2444
|
+
console.warn(`Failed to restore active task "${state.activeTask}":`, error);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Get all tool instances and their class names for display purposes
|
|
2450
|
+
*/
|
|
2451
|
+
getToolClassInfo() {
|
|
2452
|
+
const toolInstances = this.getToolInstances();
|
|
2453
|
+
return toolInstances.map(({ instance, className }) => ({
|
|
2454
|
+
className,
|
|
2455
|
+
methods: instance.getHandledToolNames ? instance.getHandledToolNames() : []
|
|
2456
|
+
}));
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Get all tool instances via reflection
|
|
2460
|
+
*/
|
|
2461
|
+
getToolInstances() {
|
|
2462
|
+
const instances = [];
|
|
2463
|
+
// Use reflection to find all tool instance properties
|
|
2464
|
+
const propertyNames = Object.getOwnPropertyNames(this);
|
|
2465
|
+
for (const propName of propertyNames) {
|
|
2466
|
+
const propValue = this[propName];
|
|
2467
|
+
// Check if this property is a tool instance (has getHandledToolNames method)
|
|
2468
|
+
if (propValue &&
|
|
2469
|
+
typeof propValue === 'object' &&
|
|
2470
|
+
typeof propValue.getHandledToolNames === 'function') {
|
|
2471
|
+
instances.push({
|
|
2472
|
+
instance: propValue,
|
|
2473
|
+
className: propValue.constructor.name
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
return instances;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
//# sourceMappingURL=grok-agent.js.map
|