@zds-ai/cli 0.1.2 → 0.1.4
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 +94 -42
- package/dist/agent/grok-agent.d.ts +19 -5
- package/dist/agent/grok-agent.js +256 -207
- package/dist/agent/grok-agent.js.map +1 -1
- package/dist/bin/generate_image_sd.sh +252 -0
- package/dist/bin/read_xlsx.py +308 -0
- package/dist/grok/client.d.ts +7 -0
- package/dist/grok/client.js +88 -7
- package/dist/grok/client.js.map +1 -1
- package/dist/grok/tools.js +12 -0
- package/dist/grok/tools.js.map +1 -1
- package/dist/hooks/use-input-handler.d.ts +2 -0
- package/dist/hooks/use-input-handler.js +76 -48
- package/dist/hooks/use-input-handler.js.map +1 -1
- package/dist/index.js +144 -21
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.js +3 -4
- package/dist/mcp/client.js.map +1 -1
- package/dist/tools/file-conversion-tool.js +2 -2
- package/dist/tools/file-conversion-tool.js.map +1 -1
- package/dist/tools/image-tool.d.ts +8 -2
- package/dist/tools/image-tool.js +65 -6
- package/dist/tools/image-tool.js.map +1 -1
- package/dist/tools/search.d.ts +1 -1
- package/dist/tools/search.js +41 -53
- package/dist/tools/search.js.map +1 -1
- package/dist/ui/components/chat-history.js +25 -12
- package/dist/ui/components/chat-history.js.map +1 -1
- package/dist/ui/components/chat-interface.js +16 -9
- package/dist/ui/components/chat-interface.js.map +1 -1
- package/dist/ui/components/rephrase-menu.d.ts +8 -0
- package/dist/ui/components/rephrase-menu.js +25 -0
- package/dist/ui/components/rephrase-menu.js.map +1 -0
- package/dist/utils/chat-history-manager.js +16 -6
- package/dist/utils/chat-history-manager.js.map +1 -1
- package/dist/utils/content-utils.d.ts +5 -0
- package/dist/utils/content-utils.js +15 -0
- package/dist/utils/content-utils.js.map +1 -0
- package/dist/utils/image-encoder.d.ts +35 -0
- package/dist/utils/image-encoder.js +133 -0
- package/dist/utils/image-encoder.js.map +1 -0
- package/dist/utils/rephrase-handler.d.ts +15 -0
- package/dist/utils/rephrase-handler.js +106 -0
- package/dist/utils/rephrase-handler.js.map +1 -0
- package/dist/utils/slash-commands.js +1 -23
- package/dist/utils/slash-commands.js.map +1 -1
- package/dist/utils/token-counter.js +17 -2
- package/dist/utils/token-counter.js.map +1 -1
- package/package.json +9 -6
package/dist/agent/grok-agent.js
CHANGED
|
@@ -3,12 +3,75 @@ import { getAllGrokTools, getMCPManager, initializeMCPServers, } from "../grok/t
|
|
|
3
3
|
import { loadMCPConfig } from "../mcp/config.js";
|
|
4
4
|
import { ChatHistoryManager } from "../utils/chat-history-manager.js";
|
|
5
5
|
import { logApiError } from "../utils/error-logger.js";
|
|
6
|
+
import { parseImagesFromMessage, hasImageReferences } from "../utils/image-encoder.js";
|
|
7
|
+
import { getTextContent } from "../utils/content-utils.js";
|
|
6
8
|
import fs from "fs";
|
|
7
9
|
import { TextEditorTool, MorphEditorTool, ZshTool, ConfirmationTool, SearchTool, EnvTool, IntrospectTool, ClearCacheTool, CharacterTool, TaskTool, InternetTool, ImageTool, FileConversionTool, RestartTool } from "../tools/index.js";
|
|
8
10
|
import { EventEmitter } from "events";
|
|
9
11
|
import { createTokenCounter } from "../utils/token-counter.js";
|
|
10
12
|
import { getSettingsManager } from "../utils/settings-manager.js";
|
|
11
13
|
import { executeOperationHook, executeToolApprovalHook, applyHookCommands } from "../utils/hook-executor.js";
|
|
14
|
+
// Interval (ms) between token count updates when streaming
|
|
15
|
+
const TOKEN_UPDATE_INTERVAL_MS = 250;
|
|
16
|
+
// Minimum delay (in ms) applied when stopping a task to ensure smooth UI/UX.
|
|
17
|
+
const MINIMUM_STOP_TASK_DELAY_MS = 3000;
|
|
18
|
+
// Maximum number of attempts to parse nested JSON strings in executeTool
|
|
19
|
+
const MAX_JSON_PARSE_ATTEMPTS = 5;
|
|
20
|
+
/**
|
|
21
|
+
* Threshold used to determine whether an AI response is "substantial" (in characters).
|
|
22
|
+
*/
|
|
23
|
+
const SUBSTANTIAL_RESPONSE_THRESHOLD = 50;
|
|
24
|
+
/**
|
|
25
|
+
* Extracts the first complete JSON object from a string.
|
|
26
|
+
* Handles duplicate/concatenated JSON objects (LLM bug) like: {"key":"val"}{"key":"val"}
|
|
27
|
+
* @param jsonString The string potentially containing concatenated JSON objects
|
|
28
|
+
* @returns The first complete JSON object, or the original string if no duplicates found
|
|
29
|
+
*/
|
|
30
|
+
function extractFirstJsonObject(jsonString) {
|
|
31
|
+
if (!jsonString.includes('}{'))
|
|
32
|
+
return jsonString;
|
|
33
|
+
try {
|
|
34
|
+
// Find the end of the first complete JSON object
|
|
35
|
+
let depth = 0;
|
|
36
|
+
let firstObjEnd = -1;
|
|
37
|
+
for (let i = 0; i < jsonString.length; i++) {
|
|
38
|
+
if (jsonString[i] === "{")
|
|
39
|
+
depth++;
|
|
40
|
+
if (jsonString[i] === "}") {
|
|
41
|
+
depth--;
|
|
42
|
+
if (depth === 0) {
|
|
43
|
+
firstObjEnd = i + 1;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (firstObjEnd > 0 && firstObjEnd < jsonString.length) {
|
|
49
|
+
// Extract and validate first object
|
|
50
|
+
const firstObj = jsonString.substring(0, firstObjEnd);
|
|
51
|
+
JSON.parse(firstObj); // Validate it's valid JSON
|
|
52
|
+
return firstObj;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// If extraction fails, return the original string
|
|
57
|
+
}
|
|
58
|
+
return jsonString;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Cleans up LLM-generated JSON argument strings for tool calls.
|
|
62
|
+
* Removes duplicate/concatenated JSON objects and trims.
|
|
63
|
+
* @param args The raw arguments string from the tool call
|
|
64
|
+
* @returns Cleaned and sanitized argument string
|
|
65
|
+
*/
|
|
66
|
+
function sanitizeToolArguments(args) {
|
|
67
|
+
let argsString = args?.trim() || "{}";
|
|
68
|
+
// Handle duplicate/concatenated JSON objects (LLM bug)
|
|
69
|
+
const extractedArgsString = extractFirstJsonObject(argsString);
|
|
70
|
+
if (extractedArgsString !== argsString) {
|
|
71
|
+
argsString = extractedArgsString;
|
|
72
|
+
}
|
|
73
|
+
return argsString;
|
|
74
|
+
}
|
|
12
75
|
export class GrokAgent extends EventEmitter {
|
|
13
76
|
grokClient;
|
|
14
77
|
textEditor;
|
|
@@ -44,7 +107,8 @@ export class GrokAgent extends EventEmitter {
|
|
|
44
107
|
activeTaskAction = "";
|
|
45
108
|
activeTaskColor = "white";
|
|
46
109
|
apiKeyEnvVar = "GROK_API_KEY";
|
|
47
|
-
|
|
110
|
+
pendingContextEditSession = null;
|
|
111
|
+
rephraseState = null;
|
|
48
112
|
constructor(apiKey, baseURL, model, maxToolRounds, debugLogFile, startupHookOutput, temperature, maxTokens) {
|
|
49
113
|
super();
|
|
50
114
|
const manager = getSettingsManager();
|
|
@@ -168,21 +232,24 @@ Current working directory: ${process.cwd()}`;
|
|
|
168
232
|
switch (entry.type) {
|
|
169
233
|
case "system":
|
|
170
234
|
// All system messages from chatHistory go into conversation (persona, mood, etc.)
|
|
235
|
+
// System messages must always be strings
|
|
171
236
|
historyMessages.push({
|
|
172
237
|
role: "system",
|
|
173
|
-
content: entry.content,
|
|
238
|
+
content: getTextContent(entry.content),
|
|
174
239
|
});
|
|
175
240
|
break;
|
|
176
241
|
case "user":
|
|
242
|
+
// User messages can have images (content arrays)
|
|
177
243
|
historyMessages.push({
|
|
178
244
|
role: "user",
|
|
179
|
-
content: entry.content,
|
|
245
|
+
content: entry.content || "",
|
|
180
246
|
});
|
|
181
247
|
break;
|
|
182
248
|
case "assistant":
|
|
249
|
+
// Assistant messages are always text (no images in responses)
|
|
183
250
|
const assistantMessage = {
|
|
184
251
|
role: "assistant",
|
|
185
|
-
content: entry.content || "", // Ensure content is never null/undefined
|
|
252
|
+
content: getTextContent(entry.content) || "", // Ensure content is never null/undefined
|
|
186
253
|
};
|
|
187
254
|
if (entry.tool_calls && entry.tool_calls.length > 0) {
|
|
188
255
|
// For assistant messages with tool calls, collect the tool results that correspond to them
|
|
@@ -283,6 +350,39 @@ Current working directory: ${process.cwd()}`;
|
|
|
283
350
|
return false;
|
|
284
351
|
}
|
|
285
352
|
async processUserMessage(message) {
|
|
353
|
+
// Detect rephrase commands
|
|
354
|
+
let isRephraseCommand = false;
|
|
355
|
+
let isSystemRephrase = false;
|
|
356
|
+
let messageToSend = message;
|
|
357
|
+
let messageType = "user";
|
|
358
|
+
if (message.startsWith("/system rephrase")) {
|
|
359
|
+
isRephraseCommand = true;
|
|
360
|
+
isSystemRephrase = true;
|
|
361
|
+
messageToSend = message.substring(8).trim(); // Strip "/system " (8 chars including space)
|
|
362
|
+
messageType = "system";
|
|
363
|
+
}
|
|
364
|
+
else if (message.startsWith("/rephrase")) {
|
|
365
|
+
isRephraseCommand = true;
|
|
366
|
+
messageToSend = message; // Keep full text including "/rephrase"
|
|
367
|
+
messageType = "user";
|
|
368
|
+
}
|
|
369
|
+
// If this is a rephrase command, find the last assistant message
|
|
370
|
+
if (isRephraseCommand) {
|
|
371
|
+
// Find index of last assistant message in chatHistory
|
|
372
|
+
let lastAssistantIndex = -1;
|
|
373
|
+
for (let i = this.chatHistory.length - 1; i >= 0; i--) {
|
|
374
|
+
if (this.chatHistory[i].type === "assistant") {
|
|
375
|
+
lastAssistantIndex = i;
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (lastAssistantIndex === -1) {
|
|
380
|
+
throw new Error("No previous assistant message to rephrase");
|
|
381
|
+
}
|
|
382
|
+
// Store rephrase state (will be updated with newResponseIndex after response)
|
|
383
|
+
// For now, just mark that we're in rephrase mode
|
|
384
|
+
this.setRephraseState(lastAssistantIndex, this.chatHistory.length, -1, messageType);
|
|
385
|
+
}
|
|
286
386
|
// Before adding the new user message, check if there are incomplete tool calls
|
|
287
387
|
// from a previous interrupted turn. This prevents malformed message sequences
|
|
288
388
|
// that cause Ollama 500 errors.
|
|
@@ -313,14 +413,40 @@ Current working directory: ${process.cwd()}`;
|
|
|
313
413
|
}
|
|
314
414
|
}
|
|
315
415
|
}
|
|
316
|
-
// Add user message to conversation
|
|
416
|
+
// Add user/system message to conversation
|
|
417
|
+
// Check for image references and parse if present
|
|
418
|
+
// Note: System messages can only have string content, so images are only supported for user messages
|
|
419
|
+
const supportsVision = this.grokClient.getSupportsVision();
|
|
420
|
+
let messageContent = messageToSend;
|
|
421
|
+
if (messageType === "user" && hasImageReferences(messageToSend) && supportsVision) {
|
|
422
|
+
// Parse images from message (only for user messages)
|
|
423
|
+
const parsed = parseImagesFromMessage(messageToSend);
|
|
424
|
+
if (parsed.images.length > 0) {
|
|
425
|
+
// Construct content array with text and images
|
|
426
|
+
messageContent = [
|
|
427
|
+
{ type: "text", text: parsed.text },
|
|
428
|
+
...parsed.images
|
|
429
|
+
];
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
// No valid images found (errors will be in parsed.text)
|
|
433
|
+
messageContent = parsed.text;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
317
436
|
const userEntry = {
|
|
318
|
-
type:
|
|
319
|
-
content:
|
|
437
|
+
type: messageType,
|
|
438
|
+
content: messageContent,
|
|
320
439
|
timestamp: new Date(),
|
|
321
440
|
};
|
|
322
441
|
this.chatHistory.push(userEntry);
|
|
323
|
-
|
|
442
|
+
// Push to messages array with proper typing based on role
|
|
443
|
+
if (messageType === "user") {
|
|
444
|
+
this.messages.push({ role: "user", content: messageContent });
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// System messages must have string content only
|
|
448
|
+
this.messages.push({ role: "system", content: typeof messageContent === "string" ? messageContent : messageToSend });
|
|
449
|
+
}
|
|
324
450
|
await this.emitContextChange();
|
|
325
451
|
const newEntries = [userEntry];
|
|
326
452
|
const maxToolRounds = this.maxToolRounds; // Prevent infinite loops
|
|
@@ -350,33 +476,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
350
476
|
// Clean up tool call arguments before adding to conversation history
|
|
351
477
|
// This prevents Ollama from rejecting malformed tool calls on subsequent API calls
|
|
352
478
|
const cleanedToolCalls = assistantMessage.tool_calls.map(toolCall => {
|
|
353
|
-
let argsString = toolCall.function.arguments
|
|
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
|
-
}
|
|
479
|
+
let argsString = sanitizeToolArguments(toolCall.function.arguments);
|
|
380
480
|
return {
|
|
381
481
|
...toolCall,
|
|
382
482
|
function: {
|
|
@@ -494,10 +594,11 @@ Current working directory: ${process.cwd()}`;
|
|
|
494
594
|
// Collect system messages that appeared after this assistant message
|
|
495
595
|
for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
|
|
496
596
|
const entry = this.chatHistory[i];
|
|
497
|
-
|
|
597
|
+
const content = getTextContent(entry.content);
|
|
598
|
+
if (entry.type === 'system' && content && content.trim()) {
|
|
498
599
|
this.messages.push({
|
|
499
600
|
role: 'system',
|
|
500
|
-
content:
|
|
601
|
+
content: content
|
|
501
602
|
});
|
|
502
603
|
}
|
|
503
604
|
// Stop if we hit another assistant or user message (next turn)
|
|
@@ -538,6 +639,11 @@ Current working directory: ${process.cwd()}`;
|
|
|
538
639
|
content: trimmedContent,
|
|
539
640
|
});
|
|
540
641
|
newEntries.push(responseEntry);
|
|
642
|
+
// Update rephrase state with the new response index
|
|
643
|
+
if (this.rephraseState && this.rephraseState.newResponseIndex === -1) {
|
|
644
|
+
const newResponseIndex = this.chatHistory.length - 1;
|
|
645
|
+
this.setRephraseState(this.rephraseState.originalAssistantMessageIndex, this.rephraseState.rephraseRequestIndex, newResponseIndex, this.rephraseState.messageType);
|
|
646
|
+
}
|
|
541
647
|
}
|
|
542
648
|
// TODO: HACK - This is a temporary fix to prevent duplicate responses.
|
|
543
649
|
// We need a proper way for the bot to signal task completion, such as:
|
|
@@ -546,8 +652,8 @@ Current working directory: ${process.cwd()}`;
|
|
|
546
652
|
// - A structured response format that explicitly marks completion
|
|
547
653
|
// For now, we break immediately after a substantial response to avoid
|
|
548
654
|
// the cascade of duplicate responses caused by "give it one more chance" logic.
|
|
549
|
-
// If the AI provided a substantial response (>
|
|
550
|
-
if (assistantMessage.content && assistantMessage.content.trim().length >
|
|
655
|
+
// If the AI provided a substantial response (>SUBSTANTIAL_RESPONSE_THRESHOLD chars), task is complete
|
|
656
|
+
if (assistantMessage.content && assistantMessage.content.trim().length > SUBSTANTIAL_RESPONSE_THRESHOLD) {
|
|
551
657
|
break; // Task complete - bot gave a full response
|
|
552
658
|
}
|
|
553
659
|
// Short/empty response, give AI another chance
|
|
@@ -693,6 +799,39 @@ Current working directory: ${process.cwd()}`;
|
|
|
693
799
|
async *processUserMessageStream(message) {
|
|
694
800
|
// Create new abort controller for this request
|
|
695
801
|
this.abortController = new AbortController();
|
|
802
|
+
// Detect rephrase commands
|
|
803
|
+
let isRephraseCommand = false;
|
|
804
|
+
let isSystemRephrase = false;
|
|
805
|
+
let messageToSend = message;
|
|
806
|
+
let messageType = "user";
|
|
807
|
+
if (message.startsWith("/system rephrase")) {
|
|
808
|
+
isRephraseCommand = true;
|
|
809
|
+
isSystemRephrase = true;
|
|
810
|
+
messageToSend = message.substring(8).trim(); // Strip "/system " (8 chars including space)
|
|
811
|
+
messageType = "system";
|
|
812
|
+
}
|
|
813
|
+
else if (message.startsWith("/rephrase")) {
|
|
814
|
+
isRephraseCommand = true;
|
|
815
|
+
messageToSend = message; // Keep full text including "/rephrase"
|
|
816
|
+
messageType = "user";
|
|
817
|
+
}
|
|
818
|
+
// If this is a rephrase command, find the last assistant message
|
|
819
|
+
if (isRephraseCommand) {
|
|
820
|
+
// Find index of last assistant message in chatHistory
|
|
821
|
+
let lastAssistantIndex = -1;
|
|
822
|
+
for (let i = this.chatHistory.length - 1; i >= 0; i--) {
|
|
823
|
+
if (this.chatHistory[i].type === "assistant") {
|
|
824
|
+
lastAssistantIndex = i;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (lastAssistantIndex === -1) {
|
|
829
|
+
throw new Error("No previous assistant message to rephrase");
|
|
830
|
+
}
|
|
831
|
+
// Store rephrase state (will be updated with newResponseIndex after response)
|
|
832
|
+
// For now, just mark that we're in rephrase mode
|
|
833
|
+
this.setRephraseState(lastAssistantIndex, this.chatHistory.length, -1, messageType);
|
|
834
|
+
}
|
|
696
835
|
// Before adding the new user message, check if there are incomplete tool calls
|
|
697
836
|
// from a previous interrupted turn. This prevents malformed message sequences
|
|
698
837
|
// that cause Ollama 500 errors.
|
|
@@ -723,14 +862,40 @@ Current working directory: ${process.cwd()}`;
|
|
|
723
862
|
}
|
|
724
863
|
}
|
|
725
864
|
}
|
|
726
|
-
// Add user message to both API conversation and chat history
|
|
865
|
+
// Add user/system message to both API conversation and chat history
|
|
866
|
+
// Check for image references and parse if present
|
|
867
|
+
// Note: System messages can only have string content, so images are only supported for user messages
|
|
868
|
+
const supportsVision = this.grokClient.getSupportsVision();
|
|
869
|
+
let messageContent = messageToSend;
|
|
870
|
+
if (messageType === "user" && hasImageReferences(messageToSend) && supportsVision) {
|
|
871
|
+
// Parse images from message (only for user messages)
|
|
872
|
+
const parsed = parseImagesFromMessage(messageToSend);
|
|
873
|
+
if (parsed.images.length > 0) {
|
|
874
|
+
// Construct content array with text and images
|
|
875
|
+
messageContent = [
|
|
876
|
+
{ type: "text", text: parsed.text },
|
|
877
|
+
...parsed.images
|
|
878
|
+
];
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
// No valid images found (errors will be in parsed.text)
|
|
882
|
+
messageContent = parsed.text;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
727
885
|
const userEntry = {
|
|
728
|
-
type:
|
|
729
|
-
content:
|
|
886
|
+
type: messageType,
|
|
887
|
+
content: messageContent,
|
|
730
888
|
timestamp: new Date(),
|
|
731
889
|
};
|
|
732
890
|
this.chatHistory.push(userEntry);
|
|
733
|
-
|
|
891
|
+
// Push to messages array with proper typing based on role
|
|
892
|
+
if (messageType === "user") {
|
|
893
|
+
this.messages.push({ role: "user", content: messageContent });
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
// System messages must have string content only
|
|
897
|
+
this.messages.push({ role: "system", content: typeof messageContent === "string" ? messageContent : messageToSend });
|
|
898
|
+
}
|
|
734
899
|
await this.emitContextChange();
|
|
735
900
|
// Yield user message so UI can display it immediately
|
|
736
901
|
yield {
|
|
@@ -847,7 +1012,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
847
1012
|
};
|
|
848
1013
|
// Emit token count update
|
|
849
1014
|
const now = Date.now();
|
|
850
|
-
if (now - lastTokenUpdate >
|
|
1015
|
+
if (now - lastTokenUpdate > TOKEN_UPDATE_INTERVAL_MS) {
|
|
851
1016
|
lastTokenUpdate = now;
|
|
852
1017
|
yield {
|
|
853
1018
|
type: "token_count",
|
|
@@ -875,33 +1040,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
875
1040
|
// Clean up tool call arguments before adding to conversation history
|
|
876
1041
|
// This prevents Ollama from rejecting malformed tool calls on subsequent API calls
|
|
877
1042
|
const cleanedToolCalls = accumulatedMessage.tool_calls?.map(toolCall => {
|
|
878
|
-
let argsString = toolCall.function.arguments
|
|
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
|
-
}
|
|
1043
|
+
let argsString = sanitizeToolArguments(toolCall.function.arguments);
|
|
905
1044
|
return {
|
|
906
1045
|
...toolCall,
|
|
907
1046
|
function: {
|
|
@@ -925,6 +1064,11 @@ Current working directory: ${process.cwd()}`;
|
|
|
925
1064
|
};
|
|
926
1065
|
this.chatHistory.push(assistantEntry);
|
|
927
1066
|
await this.emitContextChange();
|
|
1067
|
+
// Update rephrase state if this is a final response (no tool calls)
|
|
1068
|
+
if (this.rephraseState && this.rephraseState.newResponseIndex === -1 && (!accumulatedMessage.tool_calls || accumulatedMessage.tool_calls.length === 0)) {
|
|
1069
|
+
const newResponseIndex = this.chatHistory.length - 1;
|
|
1070
|
+
this.setRephraseState(this.rephraseState.originalAssistantMessageIndex, this.rephraseState.rephraseRequestIndex, newResponseIndex, this.rephraseState.messageType);
|
|
1071
|
+
}
|
|
928
1072
|
// Handle tool calls if present
|
|
929
1073
|
if (accumulatedMessage.tool_calls?.length > 0) {
|
|
930
1074
|
toolRounds++;
|
|
@@ -1039,10 +1183,11 @@ Current working directory: ${process.cwd()}`;
|
|
|
1039
1183
|
// Collect system messages that appeared after this assistant message
|
|
1040
1184
|
for (let i = assistantIndex + 1; i < this.chatHistory.length; i++) {
|
|
1041
1185
|
const entry = this.chatHistory[i];
|
|
1042
|
-
|
|
1186
|
+
const content = getTextContent(entry.content);
|
|
1187
|
+
if (entry.type === 'system' && content && content.trim()) {
|
|
1043
1188
|
this.messages.push({
|
|
1044
1189
|
role: 'system',
|
|
1045
|
-
content:
|
|
1190
|
+
content: content
|
|
1046
1191
|
});
|
|
1047
1192
|
}
|
|
1048
1193
|
// Stop if we hit another assistant or user message (next turn)
|
|
@@ -1100,7 +1245,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
1100
1245
|
this.chatHistory.push(errorEntry);
|
|
1101
1246
|
yield {
|
|
1102
1247
|
type: "content",
|
|
1103
|
-
content: errorEntry.content,
|
|
1248
|
+
content: getTextContent(errorEntry.content),
|
|
1104
1249
|
};
|
|
1105
1250
|
// Mark first message as processed even on error
|
|
1106
1251
|
this.firstMessageProcessed = true;
|
|
@@ -1178,41 +1323,16 @@ Current working directory: ${process.cwd()}`;
|
|
|
1178
1323
|
// Handle duplicate/concatenated JSON objects (LLM bug)
|
|
1179
1324
|
// Pattern: {"key":"val"}{"key":"val"}
|
|
1180
1325
|
let hadDuplicateJson = false;
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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
|
-
}
|
|
1326
|
+
const extractedArgsString = extractFirstJsonObject(argsString);
|
|
1327
|
+
if (extractedArgsString !== argsString) {
|
|
1328
|
+
hadDuplicateJson = true;
|
|
1329
|
+
argsString = extractedArgsString;
|
|
1210
1330
|
}
|
|
1211
1331
|
let args = JSON.parse(argsString);
|
|
1212
1332
|
// Handle multiple layers of JSON encoding (API bug)
|
|
1213
1333
|
// Keep parsing until we get an object, not a string
|
|
1214
1334
|
let parseCount = 0;
|
|
1215
|
-
while (typeof args === 'string' && parseCount <
|
|
1335
|
+
while (typeof args === 'string' && parseCount < MAX_JSON_PARSE_ATTEMPTS) {
|
|
1216
1336
|
parseCount++;
|
|
1217
1337
|
try {
|
|
1218
1338
|
args = JSON.parse(args);
|
|
@@ -1261,18 +1381,9 @@ Current working directory: ${process.cwd()}`;
|
|
|
1261
1381
|
// Validate tool arguments against schema
|
|
1262
1382
|
const validationError = await this.validateToolArguments(toolCall.function.name, args);
|
|
1263
1383
|
if (validationError) {
|
|
1264
|
-
//
|
|
1265
|
-
const
|
|
1266
|
-
console.warn(`[VALIDATION ERROR] ${
|
|
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
|
-
});
|
|
1384
|
+
// Validation failed - return error
|
|
1385
|
+
const errorMsg = `Tool call validation failed: ${validationError}. Please try again with correct parameters.`;
|
|
1386
|
+
console.warn(`[VALIDATION ERROR] ${errorMsg}`);
|
|
1276
1387
|
return {
|
|
1277
1388
|
success: false,
|
|
1278
1389
|
error: validationError
|
|
@@ -1388,6 +1499,8 @@ Current working directory: ${process.cwd()}`;
|
|
|
1388
1499
|
return await this.imageTool.captionImage(args.filename, args.prompt);
|
|
1389
1500
|
case "pngInfo":
|
|
1390
1501
|
return await this.imageTool.pngInfo(args.filename);
|
|
1502
|
+
case "listImageModels":
|
|
1503
|
+
return await this.imageTool.listImageModels();
|
|
1391
1504
|
case "readXlsx":
|
|
1392
1505
|
return await this.fileConversionTool.readXlsx(args.filename, args.sheetName, args.outputFormat, args.output);
|
|
1393
1506
|
case "listXlsxSheets":
|
|
@@ -1509,17 +1622,17 @@ Current working directory: ${process.cwd()}`;
|
|
|
1509
1622
|
const timestamp = entry.timestamp.toLocaleTimeString();
|
|
1510
1623
|
if (entry.type === 'user') {
|
|
1511
1624
|
lines.push(`(${msgNum}) ${userName} (user) - ${timestamp}`);
|
|
1512
|
-
lines.push(entry.content || "");
|
|
1625
|
+
lines.push(getTextContent(entry.content) || "");
|
|
1513
1626
|
lines.push("");
|
|
1514
1627
|
}
|
|
1515
1628
|
else if (entry.type === 'assistant') {
|
|
1516
1629
|
lines.push(`(${msgNum}) ${agentName} (assistant) - ${timestamp}`);
|
|
1517
|
-
lines.push(entry.content || "");
|
|
1630
|
+
lines.push(getTextContent(entry.content) || "");
|
|
1518
1631
|
lines.push("");
|
|
1519
1632
|
}
|
|
1520
1633
|
else if (entry.type === 'system') {
|
|
1521
1634
|
lines.push(`(${msgNum}) System (system) - ${timestamp}`);
|
|
1522
|
-
lines.push(entry.content || "");
|
|
1635
|
+
lines.push(getTextContent(entry.content) || "");
|
|
1523
1636
|
lines.push("");
|
|
1524
1637
|
}
|
|
1525
1638
|
else if (entry.type === 'tool_call') {
|
|
@@ -1534,7 +1647,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
1534
1647
|
const toolCall = entry.toolCall;
|
|
1535
1648
|
const toolName = toolCall?.function?.name || "unknown";
|
|
1536
1649
|
lines.push(`(${msgNum}) System (tool_result: ${toolName}) - ${timestamp}`);
|
|
1537
|
-
lines.push(entry.content || "");
|
|
1650
|
+
lines.push(getTextContent(entry.content) || "");
|
|
1538
1651
|
lines.push("");
|
|
1539
1652
|
}
|
|
1540
1653
|
});
|
|
@@ -1561,14 +1674,23 @@ Current working directory: ${process.cwd()}`;
|
|
|
1561
1674
|
getActiveTaskColor() {
|
|
1562
1675
|
return this.activeTaskColor;
|
|
1563
1676
|
}
|
|
1564
|
-
|
|
1565
|
-
this.
|
|
1677
|
+
setPendingContextEditSession(tmpJsonPath, contextFilePath) {
|
|
1678
|
+
this.pendingContextEditSession = { tmpJsonPath, contextFilePath };
|
|
1679
|
+
}
|
|
1680
|
+
getPendingContextEditSession() {
|
|
1681
|
+
return this.pendingContextEditSession;
|
|
1682
|
+
}
|
|
1683
|
+
clearPendingContextEditSession() {
|
|
1684
|
+
this.pendingContextEditSession = null;
|
|
1685
|
+
}
|
|
1686
|
+
setRephraseState(originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType) {
|
|
1687
|
+
this.rephraseState = { originalAssistantMessageIndex, rephraseRequestIndex, newResponseIndex, messageType };
|
|
1566
1688
|
}
|
|
1567
|
-
|
|
1568
|
-
return this.
|
|
1689
|
+
getRephraseState() {
|
|
1690
|
+
return this.rephraseState;
|
|
1569
1691
|
}
|
|
1570
|
-
|
|
1571
|
-
this.
|
|
1692
|
+
clearRephraseState() {
|
|
1693
|
+
this.rephraseState = null;
|
|
1572
1694
|
}
|
|
1573
1695
|
async setPersona(persona, color) {
|
|
1574
1696
|
// Execute hook if configured
|
|
@@ -1577,12 +1699,6 @@ Current working directory: ${process.cwd()}`;
|
|
|
1577
1699
|
const hookMandatory = settings.isPersonaHookMandatory();
|
|
1578
1700
|
if (!hookPath && hookMandatory) {
|
|
1579
1701
|
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
1702
|
return {
|
|
1587
1703
|
success: false,
|
|
1588
1704
|
error: reason
|
|
@@ -1600,24 +1716,13 @@ Current working directory: ${process.cwd()}`;
|
|
|
1600
1716
|
// Even in rejection, we process commands (might have MODEL change)
|
|
1601
1717
|
await this.processHookResult(hookResult);
|
|
1602
1718
|
// 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
1719
|
return {
|
|
1610
1720
|
success: false,
|
|
1611
1721
|
error: reason
|
|
1612
1722
|
};
|
|
1613
1723
|
}
|
|
1614
1724
|
if (hookResult.timedOut) {
|
|
1615
|
-
//
|
|
1616
|
-
this.chatHistory.push({
|
|
1617
|
-
type: 'system',
|
|
1618
|
-
content: `Persona hook timed out (auto-approved)`,
|
|
1619
|
-
timestamp: new Date()
|
|
1620
|
-
});
|
|
1725
|
+
// Hook timed out but was auto-approved
|
|
1621
1726
|
}
|
|
1622
1727
|
// Process hook commands (ENV, MODEL, SYSTEM)
|
|
1623
1728
|
const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_PERSONA');
|
|
@@ -1638,25 +1743,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
1638
1743
|
this.persona = persona;
|
|
1639
1744
|
this.personaColor = color || "white";
|
|
1640
1745
|
process.env.ZDS_AI_AGENT_PERSONA = persona;
|
|
1641
|
-
//
|
|
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
|
-
});
|
|
1746
|
+
// Persona hook generates success message - no need for redundant CLI message
|
|
1660
1747
|
this.emit('personaChange', {
|
|
1661
1748
|
persona: this.persona,
|
|
1662
1749
|
color: this.personaColor
|
|
@@ -1670,12 +1757,6 @@ Current working directory: ${process.cwd()}`;
|
|
|
1670
1757
|
const hookMandatory = settings.isMoodHookMandatory();
|
|
1671
1758
|
if (!hookPath && hookMandatory) {
|
|
1672
1759
|
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
1760
|
return {
|
|
1680
1761
|
success: false,
|
|
1681
1762
|
error: reason
|
|
@@ -1691,24 +1772,13 @@ Current working directory: ${process.cwd()}`;
|
|
|
1691
1772
|
const reason = hookResult.reason || "Hook rejected mood change";
|
|
1692
1773
|
// Process rejection commands (MODEL, SYSTEM)
|
|
1693
1774
|
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
1775
|
return {
|
|
1701
1776
|
success: false,
|
|
1702
1777
|
error: reason
|
|
1703
1778
|
};
|
|
1704
1779
|
}
|
|
1705
1780
|
if (hookResult.timedOut) {
|
|
1706
|
-
//
|
|
1707
|
-
this.chatHistory.push({
|
|
1708
|
-
type: 'system',
|
|
1709
|
-
content: `Mood hook timed out (auto-approved)`,
|
|
1710
|
-
timestamp: new Date()
|
|
1711
|
-
});
|
|
1781
|
+
// Hook timed out but was auto-approved
|
|
1712
1782
|
}
|
|
1713
1783
|
// Process hook commands (ENV, MODEL, SYSTEM)
|
|
1714
1784
|
const result = await this.processHookResult(hookResult, 'ZDS_AI_AGENT_MOOD');
|
|
@@ -1776,20 +1846,13 @@ Current working directory: ${process.cwd()}`;
|
|
|
1776
1846
|
await this.processHookResult(hookResult);
|
|
1777
1847
|
if (!hookResult.approved) {
|
|
1778
1848
|
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
1849
|
return {
|
|
1784
1850
|
success: false,
|
|
1785
1851
|
error: reason
|
|
1786
1852
|
};
|
|
1787
1853
|
}
|
|
1788
1854
|
if (hookResult.timedOut) {
|
|
1789
|
-
|
|
1790
|
-
role: 'system',
|
|
1791
|
-
content: `Task start hook timed out (auto-approved)`
|
|
1792
|
-
});
|
|
1855
|
+
// Hook timed out but was auto-approved
|
|
1793
1856
|
}
|
|
1794
1857
|
}
|
|
1795
1858
|
// Set the task
|
|
@@ -1831,20 +1894,13 @@ Current working directory: ${process.cwd()}`;
|
|
|
1831
1894
|
await this.processHookResult(hookResult);
|
|
1832
1895
|
if (!hookResult.approved) {
|
|
1833
1896
|
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
1897
|
return {
|
|
1839
1898
|
success: false,
|
|
1840
1899
|
error: reason
|
|
1841
1900
|
};
|
|
1842
1901
|
}
|
|
1843
1902
|
if (hookResult.timedOut) {
|
|
1844
|
-
|
|
1845
|
-
role: 'system',
|
|
1846
|
-
content: `Task transition hook timed out (auto-approved)`
|
|
1847
|
-
});
|
|
1903
|
+
// Hook timed out but was auto-approved
|
|
1848
1904
|
}
|
|
1849
1905
|
}
|
|
1850
1906
|
// Store old action for system message
|
|
@@ -1890,25 +1946,18 @@ Current working directory: ${process.cwd()}`;
|
|
|
1890
1946
|
await this.processHookResult(hookResult);
|
|
1891
1947
|
if (!hookResult.approved) {
|
|
1892
1948
|
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
1949
|
return {
|
|
1898
1950
|
success: false,
|
|
1899
1951
|
error: hookReason
|
|
1900
1952
|
};
|
|
1901
1953
|
}
|
|
1902
1954
|
if (hookResult.timedOut) {
|
|
1903
|
-
|
|
1904
|
-
role: 'system',
|
|
1905
|
-
content: `Task stop hook timed out (auto-approved)`
|
|
1906
|
-
});
|
|
1955
|
+
// Hook timed out but was auto-approved
|
|
1907
1956
|
}
|
|
1908
1957
|
}
|
|
1909
1958
|
// Calculate remaining time to meet 3-second minimum
|
|
1910
1959
|
const elapsed = Date.now() - startTime;
|
|
1911
|
-
const minimumDelay =
|
|
1960
|
+
const minimumDelay = MINIMUM_STOP_TASK_DELAY_MS;
|
|
1912
1961
|
const remainingDelay = Math.max(0, minimumDelay - elapsed);
|
|
1913
1962
|
// Wait for remaining time if needed
|
|
1914
1963
|
if (remainingDelay > 0) {
|
|
@@ -2410,7 +2459,7 @@ Current working directory: ${process.cwd()}`;
|
|
|
2410
2459
|
this.emit('modelChange', { model });
|
|
2411
2460
|
}
|
|
2412
2461
|
else {
|
|
2413
|
-
console.warn(
|
|
2462
|
+
console.warn("Failed to restore backend: API key not found in environment.");
|
|
2414
2463
|
}
|
|
2415
2464
|
}
|
|
2416
2465
|
catch (error) {
|