@zds-ai/cli 0.1.8 → 0.1.10
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/README.md +387 -34
- package/dist/agent/context-manager.d.ts +70 -0
- package/dist/agent/context-manager.js +138 -0
- package/dist/agent/context-manager.js.map +1 -0
- package/dist/agent/hook-manager.d.ts +194 -0
- package/dist/agent/hook-manager.js +676 -0
- package/dist/agent/hook-manager.js.map +1 -0
- package/dist/agent/llm-agent.d.ts +469 -100
- package/dist/agent/llm-agent.js +781 -1580
- package/dist/agent/llm-agent.js.map +1 -1
- package/dist/agent/message-processor.d.ts +103 -0
- package/dist/agent/message-processor.js +225 -0
- package/dist/agent/message-processor.js.map +1 -0
- package/dist/agent/prompt-variables.d.ts +103 -40
- package/dist/agent/prompt-variables.js +250 -113
- package/dist/agent/prompt-variables.js.map +1 -1
- package/dist/agent/session-manager.d.ts +75 -0
- package/dist/agent/session-manager.js +194 -0
- package/dist/agent/session-manager.js.map +1 -0
- package/dist/agent/tool-executor.d.ts +111 -0
- package/dist/agent/tool-executor.js +397 -0
- package/dist/agent/tool-executor.js.map +1 -0
- package/dist/bin/generate_image_sd.sh +19 -12
- package/dist/bin/joycaption.sh +37 -0
- package/dist/grok/client.d.ts +52 -0
- package/dist/grok/client.js +127 -19
- package/dist/grok/client.js.map +1 -1
- package/dist/grok/tools.js +42 -8
- package/dist/grok/tools.js.map +1 -1
- package/dist/hooks/use-input-handler.d.ts +1 -1
- package/dist/hooks/use-input-handler.js +100 -13
- package/dist/hooks/use-input-handler.js.map +1 -1
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/config.d.ts +1 -0
- package/dist/mcp/config.js +45 -7
- package/dist/mcp/config.js.map +1 -1
- package/dist/tools/character-tool.js +13 -1
- package/dist/tools/character-tool.js.map +1 -1
- package/dist/tools/image-tool.d.ts +11 -1
- package/dist/tools/image-tool.js +109 -2
- package/dist/tools/image-tool.js.map +1 -1
- package/dist/tools/introspect-tool.js +131 -30
- package/dist/tools/introspect-tool.js.map +1 -1
- package/dist/tools/morph-editor.d.ts +21 -9
- package/dist/tools/morph-editor.js +21 -9
- package/dist/tools/morph-editor.js.map +1 -1
- package/dist/ui/components/active-task-status.d.ts +1 -1
- package/dist/ui/components/api-key-input.d.ts +1 -1
- package/dist/ui/components/backend-status.d.ts +1 -1
- package/dist/ui/components/chat-history.d.ts +1 -1
- package/dist/ui/components/chat-interface.d.ts +1 -1
- package/dist/ui/components/chat-interface.js +1 -1
- package/dist/ui/components/chat-interface.js.map +1 -1
- package/dist/ui/components/context-status.d.ts +1 -1
- package/dist/ui/components/mood-status.d.ts +1 -1
- package/dist/ui/components/persona-status.d.ts +1 -1
- package/dist/utils/chat-history-manager.d.ts +12 -4
- package/dist/utils/chat-history-manager.js +26 -11
- package/dist/utils/chat-history-manager.js.map +1 -1
- package/dist/utils/hook-executor.d.ts +53 -2
- package/dist/utils/hook-executor.js +258 -36
- package/dist/utils/hook-executor.js.map +1 -1
- package/dist/utils/rephrase-handler.d.ts +1 -1
- package/dist/utils/settings-manager.d.ts +41 -11
- package/dist/utils/settings-manager.js +172 -40
- package/dist/utils/settings-manager.js.map +1 -1
- package/dist/utils/slash-commands.d.ts +3 -3
- package/dist/utils/slash-commands.js +11 -5
- package/dist/utils/slash-commands.js.map +1 -1
- package/dist/utils/startup-hook.js +9 -2
- package/dist/utils/startup-hook.js.map +1 -1
- package/package.json +10 -8
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { LLMClient } from "../grok/client.js";
|
|
2
|
+
import { getSettingsManager } from "../utils/settings-manager.js";
|
|
3
|
+
import { executeOperationHook, applyHookCommands } from "../utils/hook-executor.js";
|
|
4
|
+
import { getAllLLMTools } from "../grok/tools.js";
|
|
5
|
+
import { logApiError } from "../utils/error-logger.js";
|
|
6
|
+
import { Variable } from "./prompt-variables.js";
|
|
7
|
+
/**
|
|
8
|
+
* Manages hook execution for persona, mood, and task operations
|
|
9
|
+
*
|
|
10
|
+
* Handles:
|
|
11
|
+
* - Persona/mood/task hook execution with approval workflows
|
|
12
|
+
* - Backend and model switching with validation
|
|
13
|
+
* - Hook command processing and environment variable management
|
|
14
|
+
* - API testing for backend/model changes
|
|
15
|
+
* - System message generation for state changes
|
|
16
|
+
*/
|
|
17
|
+
export class HookManager {
|
|
18
|
+
deps;
|
|
19
|
+
constructor(deps) {
|
|
20
|
+
this.deps = deps;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Set agent persona with optional hook execution
|
|
24
|
+
* Executes persona hook if configured and processes backend/model changes
|
|
25
|
+
*
|
|
26
|
+
* @param persona New persona name
|
|
27
|
+
* @param color Optional display color
|
|
28
|
+
* @returns Success status and error message if failed
|
|
29
|
+
*/
|
|
30
|
+
async setPersona(persona, color) {
|
|
31
|
+
const settings = getSettingsManager();
|
|
32
|
+
const hookPath = settings.getPersonaHook();
|
|
33
|
+
const hookMandatory = settings.isPersonaHookMandatory();
|
|
34
|
+
if (!hookPath && hookMandatory) {
|
|
35
|
+
return { success: false, error: "Persona hook is mandatory but not configured" };
|
|
36
|
+
}
|
|
37
|
+
if (hookPath) {
|
|
38
|
+
const hookResult = await executeOperationHook(hookPath, "setPersona", {
|
|
39
|
+
persona_old: process.env.ZDS_AI_AGENT_PERSONA || "",
|
|
40
|
+
persona_new: persona,
|
|
41
|
+
persona_color: color || "white"
|
|
42
|
+
}, 30000, hookMandatory, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
|
|
43
|
+
if (!hookResult.approved) {
|
|
44
|
+
await this.processHookResult(hookResult);
|
|
45
|
+
return { success: false, error: hookResult.reason || "Hook rejected persona change" };
|
|
46
|
+
}
|
|
47
|
+
const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_PERSONA');
|
|
48
|
+
if (!result.success) {
|
|
49
|
+
return { success: false, error: "Persona change rejected due to failed model/backend test" };
|
|
50
|
+
}
|
|
51
|
+
if (result.transformedValue) {
|
|
52
|
+
persona = result.transformedValue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
process.env.ZDS_AI_AGENT_PERSONA = persona;
|
|
56
|
+
this.deps.setPersona(persona, color || "white");
|
|
57
|
+
this.deps.emit('personaChange', { persona, color: color || "white" });
|
|
58
|
+
return { success: true };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Set agent mood with optional hook execution
|
|
62
|
+
* Executes mood hook if configured and adds system message to chat
|
|
63
|
+
*
|
|
64
|
+
* @param mood New mood name
|
|
65
|
+
* @param color Optional display color
|
|
66
|
+
* @returns Success status and error message if failed
|
|
67
|
+
*/
|
|
68
|
+
async setMood(mood, color) {
|
|
69
|
+
const settings = getSettingsManager();
|
|
70
|
+
const hookPath = settings.getMoodHook();
|
|
71
|
+
const hookMandatory = settings.isMoodHookMandatory();
|
|
72
|
+
if (!hookPath && hookMandatory) {
|
|
73
|
+
return { success: false, error: "Mood hook is mandatory but not configured" };
|
|
74
|
+
}
|
|
75
|
+
if (hookPath) {
|
|
76
|
+
const hookResult = await executeOperationHook(hookPath, "setMood", {
|
|
77
|
+
mood_old: process.env.ZDS_AI_AGENT_MOOD || "",
|
|
78
|
+
mood_new: mood,
|
|
79
|
+
mood_color: color || "white"
|
|
80
|
+
}, 30000, hookMandatory, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
|
|
81
|
+
if (!hookResult.approved) {
|
|
82
|
+
await this.processHookResult(hookResult);
|
|
83
|
+
return { success: false, error: hookResult.reason || "Hook rejected mood change" };
|
|
84
|
+
}
|
|
85
|
+
const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_MOOD');
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
return { success: false, error: "Mood change rejected due to failed model/backend test" };
|
|
88
|
+
}
|
|
89
|
+
if (result.transformedValue) {
|
|
90
|
+
mood = result.transformedValue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
process.env.ZDS_AI_AGENT_MOOD = mood;
|
|
94
|
+
this.deps.setMood(mood, color || "white");
|
|
95
|
+
const oldMood = process.env.ZDS_AI_AGENT_MOOD_OLD || "";
|
|
96
|
+
const oldColor = process.env.ZDS_AI_AGENT_MOOD_COLOR_OLD || "white";
|
|
97
|
+
let systemContent;
|
|
98
|
+
if (oldMood) {
|
|
99
|
+
const oldColorStr = oldColor !== "white" ? ` (${oldColor})` : "";
|
|
100
|
+
const newColorStr = color && color !== "white" ? ` (${color})` : "";
|
|
101
|
+
systemContent = `Assistant changed the mood from "${oldMood}"${oldColorStr} to "${mood}"${newColorStr}`;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const colorStr = color && color !== "white" ? ` (${color})` : "";
|
|
105
|
+
systemContent = `Assistant set the mood to "${mood}"${colorStr}`;
|
|
106
|
+
}
|
|
107
|
+
this.deps.chatHistory.push({
|
|
108
|
+
type: 'system',
|
|
109
|
+
content: systemContent,
|
|
110
|
+
timestamp: new Date()
|
|
111
|
+
});
|
|
112
|
+
this.deps.emit('moodChange', { mood, color: color || "white" });
|
|
113
|
+
return { success: true };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Start a new active task with approval hook
|
|
117
|
+
* Prevents starting if another task is already active
|
|
118
|
+
*
|
|
119
|
+
* @param activeTask Task name
|
|
120
|
+
* @param action Task action/status
|
|
121
|
+
* @param color Optional display color
|
|
122
|
+
* @returns Success status and error message if failed
|
|
123
|
+
*/
|
|
124
|
+
async startActiveTask(activeTask, action, color) {
|
|
125
|
+
if (process.env.ZDS_AI_AGENT_ACTIVE_TASK) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: `Cannot start new task "${activeTask}". Active task "${process.env.ZDS_AI_AGENT_ACTIVE_TASK}" must be stopped first.`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const settings = getSettingsManager();
|
|
132
|
+
const hookPath = settings.getTaskApprovalHook();
|
|
133
|
+
if (hookPath) {
|
|
134
|
+
const hookResult = await executeOperationHook(hookPath, "startActiveTask", { activetask: activeTask, action, color: color || "white" }, 30000, false, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
|
|
135
|
+
await this.processHookResult(hookResult);
|
|
136
|
+
if (!hookResult.approved) {
|
|
137
|
+
return { success: false, error: hookResult.reason || "Hook rejected task start" };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
process.env.ZDS_AI_AGENT_ACTIVE_TASK = activeTask;
|
|
141
|
+
process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION = action;
|
|
142
|
+
this.deps.setActiveTask(activeTask, action, color || "white");
|
|
143
|
+
const colorStr = color && color !== "white" ? ` (${color})` : "";
|
|
144
|
+
this.deps.messages.push({
|
|
145
|
+
role: 'system',
|
|
146
|
+
content: `Assistant changed task status for "${activeTask}" to ${action}${colorStr}`
|
|
147
|
+
});
|
|
148
|
+
this.deps.emit('activeTaskChange', { activeTask, action, color: color || "white" });
|
|
149
|
+
return { success: true };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Transition active task status with approval hook
|
|
153
|
+
* Requires an active task to be running
|
|
154
|
+
*
|
|
155
|
+
* @param action New task action/status
|
|
156
|
+
* @param color Optional display color
|
|
157
|
+
* @returns Success status and error message if failed
|
|
158
|
+
*/
|
|
159
|
+
async transitionActiveTaskStatus(action, color) {
|
|
160
|
+
if (!process.env.ZDS_AI_AGENT_ACTIVE_TASK) {
|
|
161
|
+
return { success: false, error: "Cannot transition task status. No active task is currently running." };
|
|
162
|
+
}
|
|
163
|
+
const settings = getSettingsManager();
|
|
164
|
+
const hookPath = settings.getTaskApprovalHook();
|
|
165
|
+
if (hookPath) {
|
|
166
|
+
const hookResult = await executeOperationHook(hookPath, "transitionActiveTaskStatus", { action, color: color || "white" }, 30000, false, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
|
|
167
|
+
await this.processHookResult(hookResult);
|
|
168
|
+
if (!hookResult.approved) {
|
|
169
|
+
return { success: false, error: hookResult.reason || "Hook rejected task status transition" };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const oldAction = process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION || "";
|
|
173
|
+
process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION = action;
|
|
174
|
+
const colorStr = color && color !== "white" ? ` (${color})` : "";
|
|
175
|
+
this.deps.messages.push({
|
|
176
|
+
role: 'system',
|
|
177
|
+
content: `Assistant changed task status for "${process.env.ZDS_AI_AGENT_ACTIVE_TASK}" from ${oldAction} to ${action}${colorStr}`
|
|
178
|
+
});
|
|
179
|
+
this.deps.emit('activeTaskChange', {
|
|
180
|
+
activeTask: process.env.ZDS_AI_AGENT_ACTIVE_TASK,
|
|
181
|
+
action,
|
|
182
|
+
color: color || "white"
|
|
183
|
+
});
|
|
184
|
+
return { success: true };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Stop active task with approval hook and minimum delay
|
|
188
|
+
* Enforces 3-second minimum delay for task completion
|
|
189
|
+
*
|
|
190
|
+
* @param reason Reason for stopping task
|
|
191
|
+
* @param documentationFile Documentation file path
|
|
192
|
+
* @param color Optional display color
|
|
193
|
+
* @returns Success status and error message if failed
|
|
194
|
+
*/
|
|
195
|
+
async stopActiveTask(reason, documentationFile, color) {
|
|
196
|
+
if (!process.env.ZDS_AI_AGENT_ACTIVE_TASK) {
|
|
197
|
+
return { success: false, error: "Cannot stop task. No active task is currently running." };
|
|
198
|
+
}
|
|
199
|
+
const startTime = Date.now();
|
|
200
|
+
const settings = getSettingsManager();
|
|
201
|
+
const hookPath = settings.getTaskApprovalHook();
|
|
202
|
+
if (hookPath) {
|
|
203
|
+
const hookResult = await executeOperationHook(hookPath, "stopActiveTask", { reason, documentation_file: documentationFile, color: color || "white" }, 30000, false, this.deps.getCurrentTokenCount(), this.deps.getMaxContextSize());
|
|
204
|
+
await this.processHookResult(hookResult);
|
|
205
|
+
if (!hookResult.approved) {
|
|
206
|
+
return { success: false, error: hookResult.reason || "Hook rejected task stop" };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const elapsed = Date.now() - startTime;
|
|
210
|
+
const remainingDelay = Math.max(0, 3000 - elapsed);
|
|
211
|
+
if (remainingDelay > 0) {
|
|
212
|
+
await new Promise(resolve => setTimeout(resolve, remainingDelay));
|
|
213
|
+
}
|
|
214
|
+
const stoppedTask = process.env.ZDS_AI_AGENT_ACTIVE_TASK;
|
|
215
|
+
const stoppedAction = process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION || "";
|
|
216
|
+
delete process.env.ZDS_AI_AGENT_ACTIVE_TASK;
|
|
217
|
+
delete process.env.ZDS_AI_AGENT_ACTIVE_TASK_ACTION;
|
|
218
|
+
const colorStr = color && color !== "white" ? ` (${color})` : "";
|
|
219
|
+
this.deps.messages.push({
|
|
220
|
+
role: 'system',
|
|
221
|
+
content: `Assistant stopped task "${stoppedTask}" (was ${stoppedAction}) with reason: ${reason}${colorStr}`
|
|
222
|
+
});
|
|
223
|
+
this.deps.emit('activeTaskChange', { activeTask: "", action: "", color: "white" });
|
|
224
|
+
return { success: true };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Process hook result commands and apply environment changes
|
|
228
|
+
* Handles backend/model changes, environment variables, and system messages
|
|
229
|
+
*
|
|
230
|
+
* @param hookResult Hook execution result with commands
|
|
231
|
+
* @param envKey Optional environment key to extract transformed value
|
|
232
|
+
* @returns Success status and transformed value if applicable
|
|
233
|
+
*/
|
|
234
|
+
async processHookResult(hookResult, envKey) {
|
|
235
|
+
if (!hookResult.commands) {
|
|
236
|
+
return { success: true };
|
|
237
|
+
}
|
|
238
|
+
const results = applyHookCommands(hookResult.commands);
|
|
239
|
+
let transformedValue;
|
|
240
|
+
if (envKey && results.env[envKey]) {
|
|
241
|
+
transformedValue = results.env[envKey];
|
|
242
|
+
}
|
|
243
|
+
const success = await this.processHookCommands(results);
|
|
244
|
+
return { success, transformedValue };
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Process hook commands with backend/model testing
|
|
248
|
+
* Tests API connectivity before applying changes
|
|
249
|
+
*
|
|
250
|
+
* @param commands Processed hook commands
|
|
251
|
+
* @returns Success status of command processing
|
|
252
|
+
*/
|
|
253
|
+
async processHookCommands(commands) {
|
|
254
|
+
const { applyEnvVariables } = await import('../utils/hook-executor.js');
|
|
255
|
+
const hasBackendChange = commands.backend && commands.baseUrl && commands.apiKeyEnvVar;
|
|
256
|
+
const hasModelChange = commands.model;
|
|
257
|
+
// Apply immediate (non-conditional) commands right away
|
|
258
|
+
applyEnvVariables(commands.env);
|
|
259
|
+
const seenVars = new Set();
|
|
260
|
+
for (const { name, value } of commands.promptVars) {
|
|
261
|
+
if (seenVars.has(name)) {
|
|
262
|
+
Variable.append(name, value);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
Variable.set(name, value);
|
|
266
|
+
seenVars.add(name);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (commands.system) {
|
|
270
|
+
this.deps.chatHistory.push({
|
|
271
|
+
type: "system",
|
|
272
|
+
content: commands.system,
|
|
273
|
+
timestamp: new Date(),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// Check for CONDITIONAL commands without any CONDITION - this is an error
|
|
277
|
+
if (commands.conditionalResults && !hasBackendChange && !hasModelChange) {
|
|
278
|
+
const errorMsg = "Hook error: CONDITIONAL commands present but no CONDITION BACKEND or CONDITION MODEL specified. Conditional commands ignored.";
|
|
279
|
+
console.warn(errorMsg);
|
|
280
|
+
this.deps.chatHistory.push({
|
|
281
|
+
type: "system",
|
|
282
|
+
content: errorMsg,
|
|
283
|
+
timestamp: new Date(),
|
|
284
|
+
});
|
|
285
|
+
// Don't return false - allow processing to continue, just skip the conditional commands
|
|
286
|
+
}
|
|
287
|
+
// If there's a backend/model change, test it and apply conditional commands on success
|
|
288
|
+
if (hasBackendChange) {
|
|
289
|
+
const testResult = await this.testBackendModelChange(commands.backend, commands.baseUrl, commands.apiKeyEnvVar, commands.model);
|
|
290
|
+
if (!testResult.success) {
|
|
291
|
+
const parts = [];
|
|
292
|
+
if (commands.backend)
|
|
293
|
+
parts.push(`backend to "${commands.backend}"`);
|
|
294
|
+
if (commands.model)
|
|
295
|
+
parts.push(`model to "${commands.model}"`);
|
|
296
|
+
const errorMsg = `Failed to change ${parts.join(' and ')}: ${testResult.error}`;
|
|
297
|
+
this.deps.chatHistory.push({
|
|
298
|
+
type: "system",
|
|
299
|
+
content: errorMsg,
|
|
300
|
+
timestamp: new Date(),
|
|
301
|
+
});
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
// Apply conditional commands after successful test
|
|
305
|
+
if (commands.conditionalResults) {
|
|
306
|
+
applyEnvVariables(commands.conditionalResults.env);
|
|
307
|
+
const seenVars = new Set();
|
|
308
|
+
for (const { name, value } of commands.conditionalResults.promptVars) {
|
|
309
|
+
if (seenVars.has(name)) {
|
|
310
|
+
Variable.append(name, value);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
Variable.set(name, value);
|
|
314
|
+
seenVars.add(name);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (commands.conditionalResults.system) {
|
|
318
|
+
this.deps.chatHistory.push({
|
|
319
|
+
type: "system",
|
|
320
|
+
content: commands.conditionalResults.system,
|
|
321
|
+
timestamp: new Date(),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const parts = [];
|
|
326
|
+
if (commands.backend)
|
|
327
|
+
parts.push(`backend to "${commands.backend}"`);
|
|
328
|
+
if (commands.model)
|
|
329
|
+
parts.push(`model to "${commands.model}"`);
|
|
330
|
+
const successMsg = `Changed ${parts.join(' and ')}`;
|
|
331
|
+
this.deps.chatHistory.push({
|
|
332
|
+
type: "system",
|
|
333
|
+
content: successMsg,
|
|
334
|
+
timestamp: new Date(),
|
|
335
|
+
});
|
|
336
|
+
if (commands.backend) {
|
|
337
|
+
this.deps.emit('backendChange', { backend: commands.backend });
|
|
338
|
+
}
|
|
339
|
+
if (commands.model) {
|
|
340
|
+
this.deps.emit('modelChange', { model: commands.model });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else if (hasModelChange) {
|
|
344
|
+
const testResult = await this.testModel(commands.model);
|
|
345
|
+
if (!testResult.success) {
|
|
346
|
+
const errorMsg = `Failed to change model to "${commands.model}": ${testResult.error}`;
|
|
347
|
+
this.deps.chatHistory.push({
|
|
348
|
+
type: "system",
|
|
349
|
+
content: errorMsg,
|
|
350
|
+
timestamp: new Date(),
|
|
351
|
+
});
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
// Apply conditional commands after successful test
|
|
355
|
+
if (commands.conditionalResults) {
|
|
356
|
+
applyEnvVariables(commands.conditionalResults.env);
|
|
357
|
+
const seenVars = new Set();
|
|
358
|
+
for (const { name, value } of commands.conditionalResults.promptVars) {
|
|
359
|
+
if (seenVars.has(name)) {
|
|
360
|
+
Variable.append(name, value);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
Variable.set(name, value);
|
|
364
|
+
seenVars.add(name);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (commands.conditionalResults.system) {
|
|
368
|
+
this.deps.chatHistory.push({
|
|
369
|
+
type: "system",
|
|
370
|
+
content: commands.conditionalResults.system,
|
|
371
|
+
timestamp: new Date(),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const successMsg = `Model changed to "${commands.model}"`;
|
|
376
|
+
this.deps.chatHistory.push({
|
|
377
|
+
type: "system",
|
|
378
|
+
content: successMsg,
|
|
379
|
+
timestamp: new Date(),
|
|
380
|
+
});
|
|
381
|
+
this.deps.emit('modelChange', { model: commands.model });
|
|
382
|
+
}
|
|
383
|
+
// If no backend/model change, conditional commands are ignored (there's no condition to satisfy)
|
|
384
|
+
// Execute CALL commands after all other processing (fire-and-forget)
|
|
385
|
+
// Execute immediate CALLs
|
|
386
|
+
if (commands.calls.length > 0) {
|
|
387
|
+
this.executeCalls(commands.calls).catch(error => {
|
|
388
|
+
console.error("Error executing immediate CALL commands:", error);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
// Execute conditional CALLs only if backend/model test succeeded
|
|
392
|
+
if (commands.conditionalResults && commands.conditionalResults.calls.length > 0 && (hasBackendChange || hasModelChange)) {
|
|
393
|
+
this.executeCalls(commands.conditionalResults.calls).catch(error => {
|
|
394
|
+
console.error("Error executing conditional CALL commands:", error);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Render system message with current variable state
|
|
401
|
+
* Updates messages[0] with fresh system prompt from variables
|
|
402
|
+
*/
|
|
403
|
+
renderSystemMessage() {
|
|
404
|
+
this.deps.messages[0] = {
|
|
405
|
+
role: "system",
|
|
406
|
+
content: Variable.renderFull('SYSTEM')
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Test model change by making API call
|
|
411
|
+
* Validates model compatibility before switching
|
|
412
|
+
*
|
|
413
|
+
* @param newModel Model name to test
|
|
414
|
+
* @returns Success status and error message if failed
|
|
415
|
+
*/
|
|
416
|
+
async testModel(newModel) {
|
|
417
|
+
const previousModel = this.deps.getCurrentModel();
|
|
418
|
+
const previousTokenCounter = this.deps.getTokenCounter();
|
|
419
|
+
// Render system message with current variable state before testing
|
|
420
|
+
this.renderSystemMessage();
|
|
421
|
+
const testMessages = this.stripInProgressToolCalls(this.deps.messages);
|
|
422
|
+
const supportsTools = this.deps.getLLMClient().getSupportsTools();
|
|
423
|
+
const tools = supportsTools ? await getAllLLMTools() : [];
|
|
424
|
+
const requestPayload = {
|
|
425
|
+
model: newModel,
|
|
426
|
+
messages: testMessages,
|
|
427
|
+
tools: supportsTools && tools.length > 0 ? tools : undefined,
|
|
428
|
+
temperature: this.deps.temperature,
|
|
429
|
+
max_tokens: 10
|
|
430
|
+
};
|
|
431
|
+
try {
|
|
432
|
+
this.deps.getLLMClient().setModel(newModel);
|
|
433
|
+
const { createTokenCounter } = await import("../utils/token-counter.js");
|
|
434
|
+
this.deps.setTokenCounter(createTokenCounter(newModel));
|
|
435
|
+
const response = await this.deps.getLLMClient().chat(testMessages, tools, newModel, undefined, this.deps.temperature, undefined, 10);
|
|
436
|
+
if (!response || !response.choices || response.choices.length === 0) {
|
|
437
|
+
throw new Error("Invalid response from API");
|
|
438
|
+
}
|
|
439
|
+
previousTokenCounter.dispose();
|
|
440
|
+
return { success: true };
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
this.deps.getLLMClient().setModel(previousModel);
|
|
444
|
+
this.deps.getTokenCounter().dispose();
|
|
445
|
+
this.deps.setTokenCounter(previousTokenCounter);
|
|
446
|
+
const { message: logPaths } = await logApiError(requestPayload, error, { errorType: 'model-switch-test-failure', previousModel, newModel }, 'test-fail');
|
|
447
|
+
const errorMessage = error.message || "Unknown error during model test";
|
|
448
|
+
return {
|
|
449
|
+
success: false,
|
|
450
|
+
error: `Model test failed: ${errorMessage}\n${logPaths}`
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Test backend and model change by making API call
|
|
456
|
+
* Validates backend connectivity and model compatibility
|
|
457
|
+
*
|
|
458
|
+
* @param backend Backend name
|
|
459
|
+
* @param baseUrl API base URL
|
|
460
|
+
* @param apiKeyEnvVar Environment variable for API key
|
|
461
|
+
* @param model Optional model name
|
|
462
|
+
* @returns Success status and error message if failed
|
|
463
|
+
*/
|
|
464
|
+
async testBackendModelChange(backend, baseUrl, apiKeyEnvVar, model) {
|
|
465
|
+
const previousClient = this.deps.getLLMClient();
|
|
466
|
+
const previousTokenCounter = this.deps.getTokenCounter();
|
|
467
|
+
const previousApiKeyEnvVar = this.deps.apiKeyEnvVar;
|
|
468
|
+
const previousBackend = this.deps.getLLMClient().getBackendName();
|
|
469
|
+
const previousModel = this.deps.getCurrentModel();
|
|
470
|
+
let requestPayload;
|
|
471
|
+
let newModel;
|
|
472
|
+
let modelChanged = false;
|
|
473
|
+
try {
|
|
474
|
+
const apiKey = process.env[apiKeyEnvVar];
|
|
475
|
+
if (!apiKey) {
|
|
476
|
+
throw new Error(`API key not found in environment variable: ${apiKeyEnvVar}`);
|
|
477
|
+
}
|
|
478
|
+
newModel = model || this.deps.getCurrentModel();
|
|
479
|
+
modelChanged = newModel !== previousModel;
|
|
480
|
+
const newClient = new LLMClient(apiKey, newModel, baseUrl, backend);
|
|
481
|
+
this.deps.setLLMClient(newClient);
|
|
482
|
+
this.deps.setApiKeyEnvVar(apiKeyEnvVar);
|
|
483
|
+
// Update token counter only if model changed
|
|
484
|
+
if (modelChanged) {
|
|
485
|
+
const { createTokenCounter } = await import("../utils/token-counter.js");
|
|
486
|
+
this.deps.setTokenCounter(createTokenCounter(newModel));
|
|
487
|
+
}
|
|
488
|
+
const { loadMCPConfig } = await import("../mcp/config.js");
|
|
489
|
+
const { initializeMCPServers } = await import("../grok/tools.js");
|
|
490
|
+
try {
|
|
491
|
+
const config = loadMCPConfig();
|
|
492
|
+
if (config.servers.length > 0) {
|
|
493
|
+
await initializeMCPServers();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch (mcpError) {
|
|
497
|
+
console.warn("MCP reinitialization failed:", mcpError);
|
|
498
|
+
}
|
|
499
|
+
// Render system message with current variable state before testing
|
|
500
|
+
this.renderSystemMessage();
|
|
501
|
+
const testMessages = this.stripInProgressToolCalls(this.deps.messages);
|
|
502
|
+
const supportsTools = this.deps.getLLMClient().getSupportsTools();
|
|
503
|
+
const tools = supportsTools ? await getAllLLMTools() : [];
|
|
504
|
+
requestPayload = {
|
|
505
|
+
backend,
|
|
506
|
+
baseUrl,
|
|
507
|
+
model: newModel,
|
|
508
|
+
messages: testMessages,
|
|
509
|
+
tools: supportsTools && tools.length > 0 ? tools : undefined,
|
|
510
|
+
temperature: this.deps.temperature,
|
|
511
|
+
max_tokens: 10
|
|
512
|
+
};
|
|
513
|
+
const response = await this.deps.getLLMClient().chat(testMessages, tools, newModel, undefined, this.deps.temperature, undefined, 10);
|
|
514
|
+
if (!response || !response.choices || response.choices.length === 0) {
|
|
515
|
+
throw new Error("Invalid response from API");
|
|
516
|
+
}
|
|
517
|
+
// Dispose old token counter if model changed
|
|
518
|
+
if (modelChanged) {
|
|
519
|
+
previousTokenCounter.dispose();
|
|
520
|
+
}
|
|
521
|
+
return { success: true };
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
this.deps.setLLMClient(previousClient);
|
|
525
|
+
this.deps.setApiKeyEnvVar(previousApiKeyEnvVar);
|
|
526
|
+
// Restore token counter if we changed it
|
|
527
|
+
if (modelChanged) {
|
|
528
|
+
this.deps.getTokenCounter().dispose();
|
|
529
|
+
this.deps.setTokenCounter(previousTokenCounter);
|
|
530
|
+
}
|
|
531
|
+
let logPaths = '';
|
|
532
|
+
if (requestPayload) {
|
|
533
|
+
const result = await logApiError(requestPayload, error, {
|
|
534
|
+
errorType: 'backend-switch-test-failure',
|
|
535
|
+
previousBackend,
|
|
536
|
+
previousModel,
|
|
537
|
+
newBackend: backend,
|
|
538
|
+
newModel,
|
|
539
|
+
baseUrl,
|
|
540
|
+
apiKeyEnvVar
|
|
541
|
+
}, 'test-fail');
|
|
542
|
+
logPaths = result.message;
|
|
543
|
+
}
|
|
544
|
+
const errorMessage = error.message || "Unknown error during backend/model test";
|
|
545
|
+
return {
|
|
546
|
+
success: false,
|
|
547
|
+
error: logPaths ? `${errorMessage}\n${logPaths}` : errorMessage
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Strip in-progress tool calls from messages for API testing
|
|
553
|
+
* Removes incomplete tool call sequences to avoid API errors
|
|
554
|
+
*
|
|
555
|
+
* @param messages Message array to clean
|
|
556
|
+
* @returns Cleaned message array without incomplete tool calls
|
|
557
|
+
*/
|
|
558
|
+
stripInProgressToolCalls(messages) {
|
|
559
|
+
let lastAssistantIndex = -1;
|
|
560
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
561
|
+
if (messages[i].role === 'assistant') {
|
|
562
|
+
lastAssistantIndex = i;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (lastAssistantIndex === -1 || !messages[lastAssistantIndex].tool_calls) {
|
|
567
|
+
return messages;
|
|
568
|
+
}
|
|
569
|
+
const cleanedMessages = JSON.parse(JSON.stringify(messages));
|
|
570
|
+
const toolCallIds = new Set((cleanedMessages[lastAssistantIndex].tool_calls || []).map((tc) => tc.id));
|
|
571
|
+
delete cleanedMessages[lastAssistantIndex].tool_calls;
|
|
572
|
+
return cleanedMessages.filter((msg, idx) => {
|
|
573
|
+
if (idx <= lastAssistantIndex) {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
if (msg.role === 'tool' && toolCallIds.has(msg.tool_call_id)) {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
return true;
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Execute CALL commands asynchronously with recursion depth and duplicate tracking
|
|
584
|
+
* Fire-and-forget execution that processes hooks from called tools
|
|
585
|
+
*
|
|
586
|
+
* @param calls Array of CALL command strings
|
|
587
|
+
* @param context Call context for tracking recursion and duplicates
|
|
588
|
+
*/
|
|
589
|
+
async executeCalls(calls, context = { depth: 0, executedCalls: new Set() }) {
|
|
590
|
+
// Maximum recursion depth is 5
|
|
591
|
+
const MAX_DEPTH = 5;
|
|
592
|
+
if (context.depth >= MAX_DEPTH) {
|
|
593
|
+
console.warn(`CALL recursion depth limit (${MAX_DEPTH}) reached, skipping remaining calls`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
// Check if executeToolByName is available
|
|
597
|
+
if (!this.deps.executeToolByName) {
|
|
598
|
+
console.warn("CALL commands require executeToolByName dependency, skipping calls");
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
for (const callSpec of calls) {
|
|
602
|
+
// Parse "toolName arg1=val1 arg2=val2"
|
|
603
|
+
const parts = callSpec.trim().split(/\s+/);
|
|
604
|
+
if (parts.length === 0) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const toolName = parts[0];
|
|
608
|
+
const parameters = {};
|
|
609
|
+
// Parse parameters
|
|
610
|
+
for (let i = 1; i < parts.length; i++) {
|
|
611
|
+
const match = parts[i].match(/^([^=]+)=(.*)$/);
|
|
612
|
+
if (match) {
|
|
613
|
+
const [, key, value] = match;
|
|
614
|
+
// Try to parse as JSON, fall back to string
|
|
615
|
+
try {
|
|
616
|
+
parameters[key] = JSON.parse(value);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
parameters[key] = value;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
// Create signature for duplicate detection
|
|
624
|
+
const signature = `${toolName}:${JSON.stringify(parameters)}`;
|
|
625
|
+
if (context.executedCalls.has(signature)) {
|
|
626
|
+
console.warn(`Skipping duplicate CALL: ${signature}`);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
// Mark as executed
|
|
630
|
+
context.executedCalls.add(signature);
|
|
631
|
+
// Execute tool asynchronously (fire-and-forget)
|
|
632
|
+
this.executeCallAsync(toolName, parameters, context).catch(error => {
|
|
633
|
+
console.error(`Error executing CALL ${toolName}:`, error);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Execute a single CALL asynchronously with hook processing
|
|
639
|
+
* Runs tool hooks which may generate more CALL commands
|
|
640
|
+
*
|
|
641
|
+
* @param toolName Tool to execute
|
|
642
|
+
* @param parameters Tool parameters
|
|
643
|
+
* @param context Call context for tracking recursion
|
|
644
|
+
*/
|
|
645
|
+
async executeCallAsync(toolName, parameters, context) {
|
|
646
|
+
if (!this.deps.executeToolByName) {
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
// Execute the tool
|
|
651
|
+
const result = await this.deps.executeToolByName(toolName, parameters);
|
|
652
|
+
// Process any hook commands that were generated during tool execution
|
|
653
|
+
if (result.hookCommands && result.hookCommands.length > 0) {
|
|
654
|
+
const hookResults = applyHookCommands(result.hookCommands);
|
|
655
|
+
// Extract CALL commands from hook results (both immediate and conditional)
|
|
656
|
+
const recursiveCalls = [...hookResults.calls];
|
|
657
|
+
// Add conditional calls if present (they would have been validated by the tool's hooks)
|
|
658
|
+
if (hookResults.conditionalResults && hookResults.conditionalResults.calls.length > 0) {
|
|
659
|
+
recursiveCalls.push(...hookResults.conditionalResults.calls);
|
|
660
|
+
}
|
|
661
|
+
// Recursively execute CALL commands with incremented depth
|
|
662
|
+
if (recursiveCalls.length > 0) {
|
|
663
|
+
const nestedContext = {
|
|
664
|
+
depth: context.depth + 1,
|
|
665
|
+
executedCalls: context.executedCalls, // Share the same set to prevent duplicates across entire chain
|
|
666
|
+
};
|
|
667
|
+
await this.executeCalls(recursiveCalls, nestedContext);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
console.error(`Error in executeCallAsync for ${toolName}:`, error);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=hook-manager.js.map
|