@loxia-labs/loxia-autopilot-one 1.0.1
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 +267 -0
- package/README.md +509 -0
- package/bin/cli.js +117 -0
- package/package.json +94 -0
- package/scripts/install-scanners.js +236 -0
- package/src/analyzers/CSSAnalyzer.js +297 -0
- package/src/analyzers/ConfigValidator.js +690 -0
- package/src/analyzers/ESLintAnalyzer.js +320 -0
- package/src/analyzers/JavaScriptAnalyzer.js +261 -0
- package/src/analyzers/PrettierFormatter.js +247 -0
- package/src/analyzers/PythonAnalyzer.js +266 -0
- package/src/analyzers/SecurityAnalyzer.js +729 -0
- package/src/analyzers/TypeScriptAnalyzer.js +247 -0
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
- package/src/analyzers/codeCloneDetector/detector.js +203 -0
- package/src/analyzers/codeCloneDetector/index.js +160 -0
- package/src/analyzers/codeCloneDetector/parser.js +199 -0
- package/src/analyzers/codeCloneDetector/reporter.js +148 -0
- package/src/analyzers/codeCloneDetector/scanner.js +59 -0
- package/src/core/agentPool.js +1474 -0
- package/src/core/agentScheduler.js +2147 -0
- package/src/core/contextManager.js +709 -0
- package/src/core/messageProcessor.js +732 -0
- package/src/core/orchestrator.js +548 -0
- package/src/core/stateManager.js +877 -0
- package/src/index.js +631 -0
- package/src/interfaces/cli.js +549 -0
- package/src/interfaces/webServer.js +2162 -0
- package/src/modules/fileExplorer/controller.js +280 -0
- package/src/modules/fileExplorer/index.js +37 -0
- package/src/modules/fileExplorer/middleware.js +92 -0
- package/src/modules/fileExplorer/routes.js +125 -0
- package/src/modules/fileExplorer/types.js +44 -0
- package/src/services/aiService.js +1232 -0
- package/src/services/apiKeyManager.js +164 -0
- package/src/services/benchmarkService.js +366 -0
- package/src/services/budgetService.js +539 -0
- package/src/services/contextInjectionService.js +247 -0
- package/src/services/conversationCompactionService.js +637 -0
- package/src/services/errorHandler.js +810 -0
- package/src/services/fileAttachmentService.js +544 -0
- package/src/services/modelRouterService.js +366 -0
- package/src/services/modelsService.js +322 -0
- package/src/services/qualityInspector.js +796 -0
- package/src/services/tokenCountingService.js +536 -0
- package/src/tools/agentCommunicationTool.js +1344 -0
- package/src/tools/agentDelayTool.js +485 -0
- package/src/tools/asyncToolManager.js +604 -0
- package/src/tools/baseTool.js +800 -0
- package/src/tools/browserTool.js +920 -0
- package/src/tools/cloneDetectionTool.js +621 -0
- package/src/tools/dependencyResolverTool.js +1215 -0
- package/src/tools/fileContentReplaceTool.js +875 -0
- package/src/tools/fileSystemTool.js +1107 -0
- package/src/tools/fileTreeTool.js +853 -0
- package/src/tools/imageTool.js +901 -0
- package/src/tools/importAnalyzerTool.js +1060 -0
- package/src/tools/jobDoneTool.js +248 -0
- package/src/tools/seekTool.js +956 -0
- package/src/tools/staticAnalysisTool.js +1778 -0
- package/src/tools/taskManagerTool.js +2873 -0
- package/src/tools/terminalTool.js +2304 -0
- package/src/tools/webTool.js +1430 -0
- package/src/types/agent.js +519 -0
- package/src/types/contextReference.js +972 -0
- package/src/types/conversation.js +730 -0
- package/src/types/toolCommand.js +747 -0
- package/src/utilities/attachmentValidator.js +292 -0
- package/src/utilities/configManager.js +582 -0
- package/src/utilities/constants.js +722 -0
- package/src/utilities/directoryAccessManager.js +535 -0
- package/src/utilities/fileProcessor.js +307 -0
- package/src/utilities/logger.js +436 -0
- package/src/utilities/tagParser.js +1246 -0
- package/src/utilities/toolConstants.js +317 -0
- package/web-ui/build/index.html +15 -0
- package/web-ui/build/logo.png +0 -0
- package/web-ui/build/logo2.png +0 -0
- package/web-ui/build/static/index-CjkkcnFA.js +344 -0
- package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TagParser - Utility for parsing XML-style tags and JSON tool commands
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Parse XML-style tool commands from agent messages
|
|
6
|
+
* - Extract parameters from tag-based format
|
|
7
|
+
* - Support for nested tags and attributes
|
|
8
|
+
* - JSON tool command parsing
|
|
9
|
+
* - Agent redirect parsing
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
AGENT_REDIRECT_ATTRIBUTES
|
|
14
|
+
} from './constants.js';
|
|
15
|
+
import {
|
|
16
|
+
TOOL_IDS,
|
|
17
|
+
COMMAND_FORMATS,
|
|
18
|
+
JSON_STRUCTURES,
|
|
19
|
+
identifyJsonStructure,
|
|
20
|
+
getToolIdFromAction,
|
|
21
|
+
isValidToolId
|
|
22
|
+
} from './toolConstants.js';
|
|
23
|
+
|
|
24
|
+
class TagParser {
|
|
25
|
+
constructor() {
|
|
26
|
+
// Tag parsing patterns
|
|
27
|
+
this.patterns = {
|
|
28
|
+
toolCommand: /<tool-command>(.*?)<\/tool-command>/gs,
|
|
29
|
+
tool: /<tool\s+([^>]*)>(.*?)<\/tool>/gs,
|
|
30
|
+
parameter: /<([^>\s]+)(?:\s+([^>]*))?>(((?!<\/\1>)[\s\S])*)<\/\1>/g,
|
|
31
|
+
attribute: /([\w-]+)=["']([^"']*)["']/g,
|
|
32
|
+
agentRedirect: /\[agent-redirect\s+([^\]]*)\](.*?)\[\/agent-redirect\]/gs,
|
|
33
|
+
jsonBlock: /```json\s*(\{[\s\S]*?\})\s*```/g,
|
|
34
|
+
// Also match plain JSON objects on their own line(s) as fallback
|
|
35
|
+
plainJson: /^(\{(?:[^{}]|(?:\{[^{}]*\}))*\})$/gm
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decode HTML entities that might be present in tool content
|
|
41
|
+
* @param {string} text - Text that might contain HTML entities
|
|
42
|
+
* @returns {string} Decoded text
|
|
43
|
+
*/
|
|
44
|
+
decodeHtmlEntities(text) {
|
|
45
|
+
const entityMap = {
|
|
46
|
+
'<': '<',
|
|
47
|
+
'>': '>',
|
|
48
|
+
'&': '&',
|
|
49
|
+
'"': '"',
|
|
50
|
+
''': "'",
|
|
51
|
+
'/': '/',
|
|
52
|
+
''': "'",
|
|
53
|
+
'/': '/'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return text.replace(/&(?:lt|gt|amp|quot|#x27|#x2F|#39|#47);/g, match => entityMap[match] || match);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract tool commands from message content
|
|
61
|
+
* @param {string} content - Message content to parse
|
|
62
|
+
* @returns {Array} Array of parsed tool commands
|
|
63
|
+
*/
|
|
64
|
+
extractToolCommands(content) {
|
|
65
|
+
const commands = [];
|
|
66
|
+
|
|
67
|
+
// Decode HTML entities that might be present in the content
|
|
68
|
+
const decodedContent = this.decodeHtmlEntities(content);
|
|
69
|
+
|
|
70
|
+
// PHASE 1: Extract explicit tool invocations (OUTER layer identification)
|
|
71
|
+
// These use explicit markers: <toolname>, [tool id=""], or ```json
|
|
72
|
+
const xmlCommands = this.extractXMLToolCommands(decodedContent);
|
|
73
|
+
commands.push(...xmlCommands);
|
|
74
|
+
|
|
75
|
+
// PHASE 2: Extract JSON code blocks (also OUTER layer, explicit)
|
|
76
|
+
// Extract these BEFORE sanitization so we don't lose them
|
|
77
|
+
const jsonCodeBlockCommands = this.extractJSONCodeBlocks(decodedContent);
|
|
78
|
+
commands.push(...jsonCodeBlockCommands);
|
|
79
|
+
|
|
80
|
+
// PHASE 3: Sanitize content by removing already-extracted invocations
|
|
81
|
+
// This prevents INNER payload from being misinterpreted as new tool invocations
|
|
82
|
+
const sanitizedContent = this.removeToolInvocationBlocks(decodedContent);
|
|
83
|
+
|
|
84
|
+
// PHASE 4: Extract plain JSON (fallback, only from sanitized content)
|
|
85
|
+
// This should rarely find anything if agents follow the ```json block convention
|
|
86
|
+
const plainJsonCommands = this.extractPlainJSON(sanitizedContent);
|
|
87
|
+
commands.push(...plainJsonCommands);
|
|
88
|
+
|
|
89
|
+
return commands;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove content inside tool invocation blocks to prevent false JSON detection
|
|
94
|
+
*
|
|
95
|
+
* ARCHITECTURAL PRINCIPLE (OUTER-INNER HIERARCHY):
|
|
96
|
+
* - OUTER layer (message level): Tool identification
|
|
97
|
+
* - INNER layer (payload level): Action/data for the tool
|
|
98
|
+
*
|
|
99
|
+
* Tool invocations appear at MESSAGE LEVEL using explicit markers:
|
|
100
|
+
* - XML: <toolname>...</toolname> or [tool id="..."]...[/tool]
|
|
101
|
+
* - JSON: ```json {"toolId": "..."} ```
|
|
102
|
+
*
|
|
103
|
+
* Content INSIDE these markers is PAYLOAD, not new tool invocations.
|
|
104
|
+
*
|
|
105
|
+
* This method removes:
|
|
106
|
+
* - Direct tool tags: <taskmanager>...</taskmanager>, <filesystem>...</filesystem>, etc.
|
|
107
|
+
* - Bracket notation: [tool id="filesystem"]...[/tool]
|
|
108
|
+
* - JSON code blocks: ```json ... ```
|
|
109
|
+
*
|
|
110
|
+
* This prevents:
|
|
111
|
+
* - File content like {"type": "image/png"} inside <write> tags from being detected
|
|
112
|
+
* - Duplicate extraction of the same JSON block
|
|
113
|
+
*
|
|
114
|
+
* @param {string} content - Content to sanitize
|
|
115
|
+
* @returns {string} Content with tool invocation blocks removed/replaced with placeholders
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
removeToolInvocationBlocks(content) {
|
|
119
|
+
let sanitized = content;
|
|
120
|
+
|
|
121
|
+
// Remove JSON code blocks first (to prevent duplicate extraction)
|
|
122
|
+
let jsonBlockPosition = 0;
|
|
123
|
+
while (jsonBlockPosition < sanitized.length) {
|
|
124
|
+
const startMarker = '```json';
|
|
125
|
+
const endMarker = '```';
|
|
126
|
+
|
|
127
|
+
const startIndex = sanitized.indexOf(startMarker, jsonBlockPosition);
|
|
128
|
+
if (startIndex === -1) break;
|
|
129
|
+
|
|
130
|
+
const contentStart = startIndex + startMarker.length;
|
|
131
|
+
const endIndex = sanitized.indexOf(endMarker, contentStart);
|
|
132
|
+
if (endIndex === -1) break;
|
|
133
|
+
|
|
134
|
+
// Replace JSON code block with placeholder
|
|
135
|
+
const before = sanitized.substring(0, startIndex);
|
|
136
|
+
const after = sanitized.substring(endIndex + endMarker.length);
|
|
137
|
+
sanitized = before + '[JSON_CODE_BLOCK_REMOVED]' + after;
|
|
138
|
+
|
|
139
|
+
jsonBlockPosition = startIndex + '[JSON_CODE_BLOCK_REMOVED]'.length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// List of known tool names (matches extractDirectToolTags)
|
|
143
|
+
const toolNames = [
|
|
144
|
+
'taskmanager', 'filesystem', 'terminal', 'browser',
|
|
145
|
+
'agentcommunication', 'jobdone', 'agentdelay', 'image-gen'
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
// Remove direct tool tags like <filesystem>...</filesystem>
|
|
149
|
+
for (const toolName of toolNames) {
|
|
150
|
+
const openTag = `<${toolName}>`;
|
|
151
|
+
const closeTag = `</${toolName}>`;
|
|
152
|
+
|
|
153
|
+
let position = 0;
|
|
154
|
+
while (position < sanitized.length) {
|
|
155
|
+
const openIndex = sanitized.indexOf(openTag, position);
|
|
156
|
+
if (openIndex === -1) break;
|
|
157
|
+
|
|
158
|
+
const closeIndex = sanitized.indexOf(closeTag, openIndex + openTag.length);
|
|
159
|
+
if (closeIndex === -1) break;
|
|
160
|
+
|
|
161
|
+
// Replace the entire tool block with a placeholder to preserve message structure
|
|
162
|
+
// This allows JSON outside tool blocks to still be detected
|
|
163
|
+
const before = sanitized.substring(0, openIndex);
|
|
164
|
+
const after = sanitized.substring(closeIndex + closeTag.length);
|
|
165
|
+
sanitized = before + `[XML_TOOL_REMOVED:${toolName}]` + after;
|
|
166
|
+
|
|
167
|
+
position = openIndex + `[XML_TOOL_REMOVED:${toolName}]`.length;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Remove bracket notation tool blocks: [tool id="..."]...[/tool]
|
|
172
|
+
// Use a simple loop to avoid regex complexity
|
|
173
|
+
let bracketPosition = 0;
|
|
174
|
+
while (bracketPosition < sanitized.length) {
|
|
175
|
+
const openMarker = '[tool id="';
|
|
176
|
+
const openIndex = sanitized.indexOf(openMarker, bracketPosition);
|
|
177
|
+
if (openIndex === -1) break;
|
|
178
|
+
|
|
179
|
+
// Find the closing quote to extract tool ID
|
|
180
|
+
const quoteStart = openIndex + openMarker.length;
|
|
181
|
+
const quoteEnd = sanitized.indexOf('"', quoteStart);
|
|
182
|
+
if (quoteEnd === -1) break;
|
|
183
|
+
|
|
184
|
+
const toolId = sanitized.substring(quoteStart, quoteEnd);
|
|
185
|
+
|
|
186
|
+
// Find the closing ] of the opening tag
|
|
187
|
+
const openTagEnd = sanitized.indexOf(']', quoteEnd);
|
|
188
|
+
if (openTagEnd === -1) break;
|
|
189
|
+
|
|
190
|
+
// Find the closing [/tool]
|
|
191
|
+
const closeMarker = '[/tool]';
|
|
192
|
+
const closeIndex = sanitized.indexOf(closeMarker, openTagEnd);
|
|
193
|
+
if (closeIndex === -1) break;
|
|
194
|
+
|
|
195
|
+
// Replace the bracket tool block with placeholder
|
|
196
|
+
const before = sanitized.substring(0, openIndex);
|
|
197
|
+
const after = sanitized.substring(closeIndex + closeMarker.length);
|
|
198
|
+
sanitized = before + `[BRACKET_TOOL_REMOVED:${toolId}]` + after;
|
|
199
|
+
|
|
200
|
+
bracketPosition = openIndex + `[BRACKET_TOOL_REMOVED:${toolId}]`.length;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return sanitized;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Extract XML-style tool commands
|
|
208
|
+
* @param {string} content - Content to parse
|
|
209
|
+
* @returns {Array} Array of parsed XML tool commands
|
|
210
|
+
*/
|
|
211
|
+
extractXMLToolCommands(content) {
|
|
212
|
+
const commands = [];
|
|
213
|
+
|
|
214
|
+
// Find all tool-command blocks
|
|
215
|
+
const toolCommandBlocks = this.matchAll(content, this.patterns.toolCommand);
|
|
216
|
+
|
|
217
|
+
for (const block of toolCommandBlocks) {
|
|
218
|
+
const blockContent = block.groups[1];
|
|
219
|
+
|
|
220
|
+
// Extract individual tool elements within the block
|
|
221
|
+
const tools = this.matchAll(blockContent, this.patterns.tool);
|
|
222
|
+
|
|
223
|
+
for (const tool of tools) {
|
|
224
|
+
const attributes = this.parseAttributes(tool.groups[1]);
|
|
225
|
+
const toolContent = tool.groups[2];
|
|
226
|
+
|
|
227
|
+
const command = {
|
|
228
|
+
type: COMMAND_FORMATS.XML,
|
|
229
|
+
toolId: attributes.id,
|
|
230
|
+
attributes,
|
|
231
|
+
parameters: this.parseXMLParameters(toolContent),
|
|
232
|
+
rawContent: tool.match.trim(),
|
|
233
|
+
blockContent: block.match.trim()
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
commands.push(command);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Extract direct tool tags (e.g., <taskmanager>, <filesystem>)
|
|
241
|
+
const directToolTags = this.extractDirectToolTags(content);
|
|
242
|
+
commands.push(...directToolTags);
|
|
243
|
+
|
|
244
|
+
// Extract bracket notation (e.g., [tool id="filesystem"]...[/tool])
|
|
245
|
+
const bracketNotationTools = this.extractBracketNotationTools(content);
|
|
246
|
+
commands.push(...bracketNotationTools);
|
|
247
|
+
|
|
248
|
+
return commands;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Extract direct tool tags like <taskmanager>, <filesystem>, etc.
|
|
253
|
+
* @param {string} content - Content to parse
|
|
254
|
+
* @returns {Array} Array of parsed direct tool commands
|
|
255
|
+
*/
|
|
256
|
+
extractDirectToolTags(content) {
|
|
257
|
+
const commands = [];
|
|
258
|
+
const toolNames = ['taskmanager', 'filesystem', 'terminal', 'browser', 'agentcommunication', 'jobdone', 'agentdelay', 'image-gen'];
|
|
259
|
+
|
|
260
|
+
for (const toolName of toolNames) {
|
|
261
|
+
const toolCommands = this.extractToolByName(content, toolName);
|
|
262
|
+
commands.push(...toolCommands);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return commands;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract bracket notation tool commands like [tool id="filesystem"]...[/tool]
|
|
270
|
+
* @param {string} content - Content to parse
|
|
271
|
+
* @returns {Array} Array of parsed bracket notation tool commands
|
|
272
|
+
*/
|
|
273
|
+
extractBracketNotationTools(content) {
|
|
274
|
+
const commands = [];
|
|
275
|
+
const openMarker = '[tool id="';
|
|
276
|
+
const closeMarker = '[/tool]';
|
|
277
|
+
|
|
278
|
+
let position = 0;
|
|
279
|
+
|
|
280
|
+
while (position < content.length) {
|
|
281
|
+
// Find opening marker
|
|
282
|
+
const openIndex = content.indexOf(openMarker, position);
|
|
283
|
+
if (openIndex === -1) break;
|
|
284
|
+
|
|
285
|
+
// Find the closing quote to extract tool ID
|
|
286
|
+
const quoteStart = openIndex + openMarker.length;
|
|
287
|
+
const quoteEnd = content.indexOf('"', quoteStart);
|
|
288
|
+
if (quoteEnd === -1) break;
|
|
289
|
+
|
|
290
|
+
const toolId = content.substring(quoteStart, quoteEnd);
|
|
291
|
+
|
|
292
|
+
// Find the closing ] of the opening tag
|
|
293
|
+
const openTagEnd = content.indexOf(']', quoteEnd);
|
|
294
|
+
if (openTagEnd === -1) break;
|
|
295
|
+
|
|
296
|
+
// Find the closing [/tool]
|
|
297
|
+
const closeIndex = content.indexOf(closeMarker, openTagEnd);
|
|
298
|
+
if (closeIndex === -1) break;
|
|
299
|
+
|
|
300
|
+
// Extract tool content between the tags
|
|
301
|
+
const toolContent = content.substring(openTagEnd + 1, closeIndex);
|
|
302
|
+
const fullMatch = content.substring(openIndex, closeIndex + closeMarker.length);
|
|
303
|
+
|
|
304
|
+
console.log(`TagParser DEBUG: extractBracketNotationTools - found ${toolId} tool`);
|
|
305
|
+
|
|
306
|
+
const command = {
|
|
307
|
+
type: COMMAND_FORMATS.XML,
|
|
308
|
+
toolId: toolId,
|
|
309
|
+
attributes: {},
|
|
310
|
+
parameters: this.parseXMLParameters(toolContent),
|
|
311
|
+
rawContent: fullMatch.trim(),
|
|
312
|
+
blockContent: fullMatch.trim()
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
commands.push(command);
|
|
316
|
+
position = closeIndex + closeMarker.length;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return commands;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Extract tool tags by name using string functions
|
|
324
|
+
* @param {string} content - Content to search
|
|
325
|
+
* @param {string} toolName - Tool name to extract (e.g., 'taskmanager')
|
|
326
|
+
* @returns {Array} Array of extracted tool commands
|
|
327
|
+
*/
|
|
328
|
+
extractToolByName(content, toolName) {
|
|
329
|
+
const commands = [];
|
|
330
|
+
const openTag = `<${toolName}>`;
|
|
331
|
+
const closeTag = `</${toolName}>`;
|
|
332
|
+
|
|
333
|
+
let position = 0;
|
|
334
|
+
|
|
335
|
+
while (position < content.length) {
|
|
336
|
+
const openTagStart = content.indexOf(openTag, position);
|
|
337
|
+
if (openTagStart === -1) break;
|
|
338
|
+
|
|
339
|
+
const openTagEnd = openTagStart + openTag.length;
|
|
340
|
+
const closeTagStart = content.indexOf(closeTag, openTagEnd);
|
|
341
|
+
|
|
342
|
+
if (closeTagStart === -1) {
|
|
343
|
+
position = openTagEnd;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const toolContent = content.substring(openTagEnd, closeTagStart);
|
|
348
|
+
const fullMatch = content.substring(openTagStart, closeTagStart + closeTag.length);
|
|
349
|
+
|
|
350
|
+
console.log(`TagParser DEBUG: extractToolByName - found ${toolName} tool`);
|
|
351
|
+
|
|
352
|
+
const command = {
|
|
353
|
+
type: COMMAND_FORMATS.XML,
|
|
354
|
+
toolId: toolName,
|
|
355
|
+
attributes: {},
|
|
356
|
+
parameters: this.parseXMLParameters(toolContent),
|
|
357
|
+
rawContent: fullMatch.trim(),
|
|
358
|
+
blockContent: fullMatch.trim()
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
commands.push(command);
|
|
362
|
+
position = closeTagStart + closeTag.length;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return commands;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Extract JSON-style tool commands
|
|
370
|
+
* @param {string} content - Content to parse
|
|
371
|
+
* @returns {Array} Array of parsed JSON tool commands
|
|
372
|
+
*/
|
|
373
|
+
extractJSONToolCommands(content) {
|
|
374
|
+
const commands = [];
|
|
375
|
+
|
|
376
|
+
// Method 1: Find JSON in markdown code blocks (preferred format)
|
|
377
|
+
commands.push(...this.extractJSONCodeBlocks(content));
|
|
378
|
+
|
|
379
|
+
// Method 2: Find plain JSON objects (fallback for agents that don't use code blocks)
|
|
380
|
+
commands.push(...this.extractPlainJSON(content));
|
|
381
|
+
|
|
382
|
+
return commands;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Extract JSON from markdown code blocks using string functions
|
|
387
|
+
* @private
|
|
388
|
+
*/
|
|
389
|
+
extractJSONCodeBlocks(content) {
|
|
390
|
+
const commands = [];
|
|
391
|
+
let searchIndex = 0;
|
|
392
|
+
|
|
393
|
+
while (true) {
|
|
394
|
+
// Find opening marker
|
|
395
|
+
const startMarker = '```json';
|
|
396
|
+
const endMarker = '```';
|
|
397
|
+
|
|
398
|
+
const startIndex = content.indexOf(startMarker, searchIndex);
|
|
399
|
+
if (startIndex === -1) break;
|
|
400
|
+
|
|
401
|
+
// Find closing marker
|
|
402
|
+
const contentStart = startIndex + startMarker.length;
|
|
403
|
+
const endIndex = content.indexOf(endMarker, contentStart);
|
|
404
|
+
if (endIndex === -1) break;
|
|
405
|
+
|
|
406
|
+
// Extract JSON content
|
|
407
|
+
const jsonString = content.substring(contentStart, endIndex).trim();
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const jsonData = JSON.parse(jsonString);
|
|
411
|
+
|
|
412
|
+
if (this.isToolCommandJSON(jsonData)) {
|
|
413
|
+
// ARCHITECTURAL PRINCIPLE: Use OUTER layer (toolId) for tool identification
|
|
414
|
+
const toolId = jsonData.toolId || jsonData.tool;
|
|
415
|
+
|
|
416
|
+
// Skip if no explicit outer tool identifier
|
|
417
|
+
if (!toolId || toolId === 'unknown') {
|
|
418
|
+
console.log('TagParser DEBUG: Skipping JSON block - missing explicit toolId');
|
|
419
|
+
searchIndex = endIndex + endMarker.length;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const command = {
|
|
424
|
+
type: COMMAND_FORMATS.JSON,
|
|
425
|
+
toolId: toolId,
|
|
426
|
+
parameters: jsonData.parameters || jsonData.actions || {},
|
|
427
|
+
actions: jsonData.actions,
|
|
428
|
+
rawContent: content.substring(startIndex, endIndex + endMarker.length),
|
|
429
|
+
jsonData
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
commands.push(command);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Handle toolCommands array format
|
|
436
|
+
if (jsonData.toolCommands && Array.isArray(jsonData.toolCommands)) {
|
|
437
|
+
for (const toolCommand of jsonData.toolCommands) {
|
|
438
|
+
if (this.isToolCommandJSON(toolCommand)) {
|
|
439
|
+
// ARCHITECTURAL PRINCIPLE: Use OUTER layer (toolId) for tool identification
|
|
440
|
+
const toolId = toolCommand.toolId || toolCommand.tool;
|
|
441
|
+
|
|
442
|
+
// Skip if no explicit outer tool identifier
|
|
443
|
+
if (!toolId || toolId === 'unknown') {
|
|
444
|
+
console.log('TagParser DEBUG: Skipping toolCommand - missing explicit toolId');
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const command = {
|
|
449
|
+
type: COMMAND_FORMATS.JSON,
|
|
450
|
+
toolId: toolId,
|
|
451
|
+
parameters: toolCommand.parameters || toolCommand.actions || {},
|
|
452
|
+
actions: toolCommand.actions,
|
|
453
|
+
rawContent: JSON.stringify(toolCommand, null, 2),
|
|
454
|
+
jsonData: toolCommand
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
commands.push(command);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
} catch (error) {
|
|
463
|
+
// Invalid JSON, skip this block
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
searchIndex = endIndex + endMarker.length;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return commands;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Extract plain JSON objects using string functions
|
|
474
|
+
* @private
|
|
475
|
+
*/
|
|
476
|
+
extractPlainJSON(content) {
|
|
477
|
+
const commands = [];
|
|
478
|
+
const lines = content.split('\n');
|
|
479
|
+
|
|
480
|
+
for (let i = 0; i < lines.length; i++) {
|
|
481
|
+
const line = lines[i].trim();
|
|
482
|
+
|
|
483
|
+
// Skip if line doesn't start with {
|
|
484
|
+
if (!line.startsWith('{')) continue;
|
|
485
|
+
|
|
486
|
+
// Try to find complete JSON object
|
|
487
|
+
let jsonString = '';
|
|
488
|
+
let braceCount = 0;
|
|
489
|
+
let foundComplete = false;
|
|
490
|
+
|
|
491
|
+
// Start from current line and look for complete JSON
|
|
492
|
+
for (let j = i; j < lines.length; j++) {
|
|
493
|
+
const currentLine = lines[j].trim();
|
|
494
|
+
jsonString += (j > i ? '\n' : '') + currentLine;
|
|
495
|
+
|
|
496
|
+
// Count braces to find complete object
|
|
497
|
+
for (const char of currentLine) {
|
|
498
|
+
if (char === '{') braceCount++;
|
|
499
|
+
if (char === '}') braceCount--;
|
|
500
|
+
if (braceCount === 0 && char === '}') {
|
|
501
|
+
foundComplete = true;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (foundComplete) break;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!foundComplete) continue;
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
const jsonData = JSON.parse(jsonString);
|
|
513
|
+
|
|
514
|
+
// ARCHITECTURAL PRINCIPLE: Use OUTER layer for tool identification
|
|
515
|
+
// Only parse INNER content (actions, type) if OUTER identifier missing
|
|
516
|
+
// This follows the layered invocation hierarchy:
|
|
517
|
+
// OUTER (message level): Tool identification via "toolId"
|
|
518
|
+
// INNER (payload level): Action specification via "type"
|
|
519
|
+
|
|
520
|
+
// Check if this JSON looks like a tool command
|
|
521
|
+
if (this.isToolCommandJSON(jsonData) ||
|
|
522
|
+
(jsonData.actions && Array.isArray(jsonData.actions)) ||
|
|
523
|
+
(jsonData.type && typeof jsonData.type === 'string')) {
|
|
524
|
+
|
|
525
|
+
// CRITICAL: Use OUTER identifier first, only infer from INNER as fallback
|
|
526
|
+
const toolId = jsonData.toolId || jsonData.tool || this.inferToolFromActions(jsonData);
|
|
527
|
+
|
|
528
|
+
// Skip if we couldn't determine a valid tool
|
|
529
|
+
if (!toolId || toolId === 'unknown') {
|
|
530
|
+
console.log('TagParser DEBUG: Skipping plain JSON - could not determine valid toolId');
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const command = {
|
|
535
|
+
type: COMMAND_FORMATS.JSON_PLAIN,
|
|
536
|
+
toolId: toolId,
|
|
537
|
+
parameters: jsonData.parameters || jsonData,
|
|
538
|
+
actions: jsonData.actions,
|
|
539
|
+
rawContent: jsonString,
|
|
540
|
+
jsonData,
|
|
541
|
+
warning: 'Plain JSON detected - should use ```json blocks'
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
commands.push(command);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
} catch (error) {
|
|
548
|
+
// Invalid JSON, skip
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return commands;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Infer tool ID from JSON structure
|
|
558
|
+
* Uses the constants-based approach for deterministic tool identification
|
|
559
|
+
* @private
|
|
560
|
+
*/
|
|
561
|
+
inferToolFromActions(jsonData) {
|
|
562
|
+
const structure = identifyJsonStructure(jsonData);
|
|
563
|
+
|
|
564
|
+
switch (structure) {
|
|
565
|
+
case JSON_STRUCTURES.STANDARD:
|
|
566
|
+
// Already has toolId
|
|
567
|
+
return jsonData.toolId || 'unknown';
|
|
568
|
+
|
|
569
|
+
case JSON_STRUCTURES.ACTIONS_ARRAY:
|
|
570
|
+
// Get tool from first action type
|
|
571
|
+
if (jsonData.actions && jsonData.actions.length > 0) {
|
|
572
|
+
const firstAction = jsonData.actions[0];
|
|
573
|
+
const toolId = getToolIdFromAction(firstAction.type);
|
|
574
|
+
return toolId || 'unknown';
|
|
575
|
+
}
|
|
576
|
+
break;
|
|
577
|
+
|
|
578
|
+
case JSON_STRUCTURES.DIRECT_ACTION:
|
|
579
|
+
// Get tool from type field
|
|
580
|
+
const toolId = getToolIdFromAction(jsonData.type);
|
|
581
|
+
return toolId || 'unknown';
|
|
582
|
+
|
|
583
|
+
case JSON_STRUCTURES.TOOL_COMMANDS:
|
|
584
|
+
// Get from first command
|
|
585
|
+
if (jsonData.toolCommands && jsonData.toolCommands.length > 0) {
|
|
586
|
+
return this.inferToolFromActions(jsonData.toolCommands[0]);
|
|
587
|
+
}
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return 'unknown';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Extract agent redirects from content
|
|
596
|
+
* @param {string} content - Content to parse
|
|
597
|
+
* @returns {Array} Array of parsed agent redirects
|
|
598
|
+
*/
|
|
599
|
+
extractAgentRedirects(content) {
|
|
600
|
+
const redirects = [];
|
|
601
|
+
|
|
602
|
+
const matches = this.matchAll(content, this.patterns.agentRedirect);
|
|
603
|
+
|
|
604
|
+
for (const match of matches) {
|
|
605
|
+
const attributeString = match.groups[1];
|
|
606
|
+
const messageContent = match.groups[2].trim();
|
|
607
|
+
|
|
608
|
+
const attributes = this.parseAttributes(attributeString);
|
|
609
|
+
|
|
610
|
+
const redirect = {
|
|
611
|
+
to: attributes.to,
|
|
612
|
+
content: messageContent,
|
|
613
|
+
urgent: attributes[AGENT_REDIRECT_ATTRIBUTES.URGENT] === 'true',
|
|
614
|
+
requiresResponse: attributes[AGENT_REDIRECT_ATTRIBUTES.REQUIRES_RESPONSE] === 'true',
|
|
615
|
+
context: attributes[AGENT_REDIRECT_ATTRIBUTES.CONTEXT],
|
|
616
|
+
rawMatch: match.match
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// Add any additional attributes
|
|
620
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
621
|
+
if (!['to', AGENT_REDIRECT_ATTRIBUTES.URGENT, AGENT_REDIRECT_ATTRIBUTES.REQUIRES_RESPONSE, AGENT_REDIRECT_ATTRIBUTES.CONTEXT].includes(key)) {
|
|
622
|
+
redirect[key] = value;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
redirects.push(redirect);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return redirects;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Parse XML parameters from tool content
|
|
634
|
+
* @param {string} content - Tool content to parse
|
|
635
|
+
* @returns {Object} Parsed parameters
|
|
636
|
+
*/
|
|
637
|
+
parseXMLParameters(content) {
|
|
638
|
+
const parameters = {};
|
|
639
|
+
|
|
640
|
+
console.log('TagParser DEBUG: parseXMLParameters - content length:', content.length);
|
|
641
|
+
|
|
642
|
+
let position = 0;
|
|
643
|
+
let foundCount = 0;
|
|
644
|
+
|
|
645
|
+
// Look for opening tags like <write>, <read>, etc.
|
|
646
|
+
while (position < content.length) {
|
|
647
|
+
const openTagStart = content.indexOf('<', position);
|
|
648
|
+
if (openTagStart === -1) break;
|
|
649
|
+
|
|
650
|
+
const openTagEnd = content.indexOf('>', openTagStart);
|
|
651
|
+
if (openTagEnd === -1) break;
|
|
652
|
+
|
|
653
|
+
// Extract the full opening tag
|
|
654
|
+
const openTag = content.substring(openTagStart, openTagEnd + 1);
|
|
655
|
+
|
|
656
|
+
// Parse tag name and attributes from the opening tag
|
|
657
|
+
const spaceIndex = openTag.indexOf(' ');
|
|
658
|
+
const tagName = spaceIndex > 0
|
|
659
|
+
? openTag.substring(1, spaceIndex)
|
|
660
|
+
: openTag.substring(1, openTag.length - 1);
|
|
661
|
+
|
|
662
|
+
const isValid = this.isValidXmlTagName(tagName);
|
|
663
|
+
|
|
664
|
+
// Skip malformed tags or content that looks like code
|
|
665
|
+
if (tagName.includes('/') || !tagName || !this.isValidXmlTagName(tagName)) {
|
|
666
|
+
position = openTagEnd + 1;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Check if this is a self-closing tag (ends with />)
|
|
671
|
+
const isSelfClosing = openTag.endsWith('/>');
|
|
672
|
+
|
|
673
|
+
let tagContent = '';
|
|
674
|
+
let closingTagStart = openTagEnd;
|
|
675
|
+
|
|
676
|
+
if (isSelfClosing) {
|
|
677
|
+
// Self-closing tag has no content
|
|
678
|
+
tagContent = '';
|
|
679
|
+
closingTagStart = openTagEnd; // Position right after the self-closing tag
|
|
680
|
+
} else {
|
|
681
|
+
// Look for the closing tag
|
|
682
|
+
const closingTag = `</${tagName}>`;
|
|
683
|
+
closingTagStart = content.indexOf(closingTag, openTagEnd + 1);
|
|
684
|
+
|
|
685
|
+
if (closingTagStart === -1) {
|
|
686
|
+
position = openTagEnd + 1;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Extract the content between tags
|
|
691
|
+
tagContent = content.substring(openTagEnd + 1, closingTagStart);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Extract attributes from the opening tag
|
|
695
|
+
let attributeString = spaceIndex > 0
|
|
696
|
+
? openTag.substring(spaceIndex + 1, openTag.length - 1).trim()
|
|
697
|
+
: '';
|
|
698
|
+
|
|
699
|
+
// For self-closing tags, remove the trailing '/' from attributes
|
|
700
|
+
if (isSelfClosing && attributeString.endsWith('/')) {
|
|
701
|
+
attributeString = attributeString.substring(0, attributeString.length - 1).trim();
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const attributes = this.parseAttributes(attributeString);
|
|
705
|
+
|
|
706
|
+
console.log('TagParser DEBUG: parseXMLParameters - found match:', {
|
|
707
|
+
paramName: tagName,
|
|
708
|
+
attributeString,
|
|
709
|
+
valueLength: tagContent.length,
|
|
710
|
+
valuePreview: tagContent.substring(0, 50) + (tagContent.length > 50 ? '...' : '')
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// CRITICAL FIX: Handle multiple tags with same name (e.g., multiple <write> tags)
|
|
714
|
+
// Convert to array if duplicate detected
|
|
715
|
+
const paramValue = {
|
|
716
|
+
value: tagContent.trim(),
|
|
717
|
+
attributes
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
if (parameters[tagName]) {
|
|
721
|
+
// Tag already exists - convert to array or append to existing array
|
|
722
|
+
if (Array.isArray(parameters[tagName])) {
|
|
723
|
+
// Already an array, append
|
|
724
|
+
parameters[tagName].push(paramValue);
|
|
725
|
+
console.log('TagParser DEBUG: appended to existing array for tag:', tagName, 'count:', parameters[tagName].length);
|
|
726
|
+
} else {
|
|
727
|
+
// First duplicate - convert to array
|
|
728
|
+
const existingValue = parameters[tagName];
|
|
729
|
+
parameters[tagName] = [existingValue, paramValue];
|
|
730
|
+
console.log('TagParser DEBUG: converted to array for duplicate tag:', tagName);
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
// First occurrence - store as single object
|
|
734
|
+
parameters[tagName] = paramValue;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// For convenience, also store direct access to value (deprecated, kept for backward compatibility)
|
|
738
|
+
// Only define if it doesn't exist (handles multiple tags with same name)
|
|
739
|
+
const valuePropertyName = tagName + '_value';
|
|
740
|
+
if (!Object.prototype.hasOwnProperty.call(parameters, valuePropertyName)) {
|
|
741
|
+
Object.defineProperty(parameters, valuePropertyName, {
|
|
742
|
+
value: tagContent.trim(),
|
|
743
|
+
enumerable: false
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
foundCount++;
|
|
748
|
+
|
|
749
|
+
// Update position based on whether it's self-closing or paired tags
|
|
750
|
+
if (isSelfClosing) {
|
|
751
|
+
position = openTagEnd + 1;
|
|
752
|
+
} else {
|
|
753
|
+
const closingTag = `</${tagName}>`;
|
|
754
|
+
position = closingTagStart + closingTag.length;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
console.log('TagParser DEBUG: parseXMLParameters - matches found:', foundCount);
|
|
759
|
+
console.log('TagParser DEBUG: parseXMLParameters - final parameters:', Object.keys(parameters));
|
|
760
|
+
return parameters;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Parse attributes from attribute string
|
|
765
|
+
* @param {string} attributeString - Attribute string to parse
|
|
766
|
+
* @returns {Object} Parsed attributes
|
|
767
|
+
*/
|
|
768
|
+
parseAttributes(attributeString) {
|
|
769
|
+
const attributes = {};
|
|
770
|
+
|
|
771
|
+
if (!attributeString) return attributes;
|
|
772
|
+
|
|
773
|
+
const attrMatches = this.matchAll(attributeString, this.patterns.attribute);
|
|
774
|
+
|
|
775
|
+
for (const match of attrMatches) {
|
|
776
|
+
const attrName = match.groups[0];
|
|
777
|
+
const attrValue = match.groups[1];
|
|
778
|
+
|
|
779
|
+
attributes[attrName] = attrValue;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return attributes;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Extract content from tags
|
|
787
|
+
* @param {string} content - Content to search
|
|
788
|
+
* @param {string} tagName - Tag name to extract
|
|
789
|
+
* @returns {Array} Array of extracted content strings
|
|
790
|
+
*/
|
|
791
|
+
static extractContent(content, tagName) {
|
|
792
|
+
const pattern = new RegExp(`<${tagName}[^>]*>([^<]*)<\\/${tagName}>`, 'g');
|
|
793
|
+
const matches = [];
|
|
794
|
+
let match;
|
|
795
|
+
|
|
796
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
797
|
+
matches.push(match[1].trim());
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return matches;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Extract tag with attributes
|
|
805
|
+
* @param {string} content - Content to search
|
|
806
|
+
* @param {string} tagName - Tag name to extract
|
|
807
|
+
* @returns {Array} Array of extracted tag objects with content and attributes
|
|
808
|
+
*/
|
|
809
|
+
static extractTagsWithAttributes(content, tagName) {
|
|
810
|
+
const pattern = new RegExp(`<${tagName}\\s*([^>]*)>([^<]*)<\\/${tagName}>`, 'g');
|
|
811
|
+
const tags = [];
|
|
812
|
+
let match;
|
|
813
|
+
|
|
814
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
815
|
+
const attributeString = match[1];
|
|
816
|
+
const tagContent = match[2].trim();
|
|
817
|
+
|
|
818
|
+
const parser = new TagParser();
|
|
819
|
+
const attributes = parser.parseAttributes(attributeString);
|
|
820
|
+
|
|
821
|
+
tags.push({
|
|
822
|
+
content: tagContent,
|
|
823
|
+
attributes,
|
|
824
|
+
rawMatch: match[0]
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return tags;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Check if JSON object represents a tool command
|
|
833
|
+
* @private
|
|
834
|
+
*/
|
|
835
|
+
isToolCommandJSON(obj) {
|
|
836
|
+
return obj &&
|
|
837
|
+
typeof obj === 'object' &&
|
|
838
|
+
(obj.toolId || obj.tool) &&
|
|
839
|
+
(obj.parameters || obj.actions);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Match all occurrences of a pattern
|
|
844
|
+
* @private
|
|
845
|
+
*/
|
|
846
|
+
matchAll(content, pattern) {
|
|
847
|
+
const matches = [];
|
|
848
|
+
let match;
|
|
849
|
+
|
|
850
|
+
// Reset pattern lastIndex to ensure clean matching
|
|
851
|
+
pattern.lastIndex = 0;
|
|
852
|
+
|
|
853
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
854
|
+
matches.push({
|
|
855
|
+
match: match[0],
|
|
856
|
+
groups: match.slice(1),
|
|
857
|
+
index: match.index
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return matches;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Validate tool command structure
|
|
866
|
+
* @param {Object} command - Tool command to validate
|
|
867
|
+
* @returns {Object} Validation result
|
|
868
|
+
*/
|
|
869
|
+
validateToolCommand(command) {
|
|
870
|
+
const errors = [];
|
|
871
|
+
|
|
872
|
+
if (!command.toolId) {
|
|
873
|
+
errors.push('Missing toolId');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (!command.parameters && !command.actions) {
|
|
877
|
+
errors.push('Missing parameters or actions');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (command.type === 'xml') {
|
|
881
|
+
if (!command.rawContent) {
|
|
882
|
+
errors.push('Missing rawContent for XML command');
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Validate XML parameter structure
|
|
886
|
+
if (command.parameters) {
|
|
887
|
+
for (const [paramName, paramData] of Object.entries(command.parameters)) {
|
|
888
|
+
if (typeof paramData !== 'object' || !('value' in paramData)) {
|
|
889
|
+
errors.push(`Invalid parameter structure for ${paramName}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (command.type === 'json') {
|
|
896
|
+
if (!command.jsonData) {
|
|
897
|
+
errors.push('Missing jsonData for JSON command');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
valid: errors.length === 0,
|
|
903
|
+
errors
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Normalize tool command to consistent format
|
|
909
|
+
* @param {Object} command - Tool command to normalize
|
|
910
|
+
* @returns {Object} Normalized command
|
|
911
|
+
*/
|
|
912
|
+
normalizeToolCommand(command) {
|
|
913
|
+
const normalized = {
|
|
914
|
+
toolId: command.toolId,
|
|
915
|
+
type: command.type,
|
|
916
|
+
parameters: {},
|
|
917
|
+
rawContent: command.rawContent
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
if (command.type === COMMAND_FORMATS.XML) {
|
|
921
|
+
console.log('TagParser DEBUG: normalizing XML command with parameters:', Object.keys(command.parameters || {}));
|
|
922
|
+
|
|
923
|
+
// SPECIAL CASE: AgentDelay tool expects flat object {duration, reason}, not actions array
|
|
924
|
+
if (command.toolId === 'agentdelay') {
|
|
925
|
+
const params = command.parameters || {};
|
|
926
|
+
|
|
927
|
+
// Extract duration and reason
|
|
928
|
+
if (params['pause-duration']) {
|
|
929
|
+
const durationStr = params['pause-duration'].value;
|
|
930
|
+
normalized.parameters.duration = parseInt(durationStr, 10);
|
|
931
|
+
}
|
|
932
|
+
if (params['reason']) {
|
|
933
|
+
normalized.parameters.reason = params['reason'].value.trim();
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
console.log('TagParser DEBUG: delay tool - flat parameters:', normalized.parameters);
|
|
937
|
+
return normalized;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// SPECIAL CASE: ImageGen tool has its own XML parser, pass raw content directly
|
|
941
|
+
if (command.toolId === 'image-gen') {
|
|
942
|
+
const params = command.parameters || {};
|
|
943
|
+
|
|
944
|
+
// Extract the inner XML content (without the <image-gen> wrapper)
|
|
945
|
+
// ImageTool.parseParameters() expects just the inner tags
|
|
946
|
+
let xmlContent = '';
|
|
947
|
+
for (const [key, paramData] of Object.entries(params)) {
|
|
948
|
+
xmlContent += `<${key}>${paramData.value}</${key}>\n`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Store the raw XML for ImageTool to parse
|
|
952
|
+
normalized.parameters = xmlContent.trim();
|
|
953
|
+
|
|
954
|
+
console.log('TagParser DEBUG: image-gen tool - passing raw XML to ImageTool:', xmlContent.substring(0, 100) + '...');
|
|
955
|
+
return normalized;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// NORMAL CASE: Convert XML parameters to actions array format for most tools
|
|
959
|
+
const actions = [];
|
|
960
|
+
|
|
961
|
+
for (const [key, paramData] of Object.entries(command.parameters || {})) {
|
|
962
|
+
console.log('TagParser DEBUG: processing XML parameter:', key, 'with data:', paramData);
|
|
963
|
+
|
|
964
|
+
// CRITICAL FIX: Handle both single object and array of objects (for duplicate tags)
|
|
965
|
+
const paramDataArray = Array.isArray(paramData) ? paramData : [paramData];
|
|
966
|
+
|
|
967
|
+
for (const singleParamData of paramDataArray) {
|
|
968
|
+
// Convert XML tags to action objects
|
|
969
|
+
const action = {
|
|
970
|
+
type: key, // e.g., "write", "read", "delete", "run-command"
|
|
971
|
+
...singleParamData.attributes // e.g., output-path, file-path, etc.
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// Add content if it exists
|
|
975
|
+
if (singleParamData.value && singleParamData.value.trim()) {
|
|
976
|
+
action.content = singleParamData.value.trim();
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
console.log('TagParser DEBUG: created action before normalization:', action);
|
|
980
|
+
|
|
981
|
+
// Convert attribute names to camelCase for consistency
|
|
982
|
+
const normalizedAction = {};
|
|
983
|
+
for (const [attrKey, attrValue] of Object.entries(action)) {
|
|
984
|
+
const normalizedKey = this._toCamelCase(attrKey);
|
|
985
|
+
normalizedAction[normalizedKey] = attrValue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// CRITICAL FIX: Map 'content' field to tool-specific field names based on action type
|
|
989
|
+
if (normalizedAction.content !== undefined) {
|
|
990
|
+
const contentFieldMapping = {
|
|
991
|
+
// Terminal tool mappings
|
|
992
|
+
'run-command': 'command',
|
|
993
|
+
'create-directory': 'directory',
|
|
994
|
+
'change-directory': 'directory',
|
|
995
|
+
'list-directory': 'directory',
|
|
996
|
+
'get-working-directory': null, // No content field needed
|
|
997
|
+
|
|
998
|
+
// FileSystem tool mappings
|
|
999
|
+
'write': null, // Uses content for file content (keep as-is)
|
|
1000
|
+
'read': 'filePath',
|
|
1001
|
+
'delete': 'filePath',
|
|
1002
|
+
'copy': null, // Uses attributes (source, destination)
|
|
1003
|
+
'move': null, // Uses attributes (source, destination)
|
|
1004
|
+
'append': 'filePath',
|
|
1005
|
+
'create-dir': 'directory',
|
|
1006
|
+
'list': 'directory',
|
|
1007
|
+
'exists': 'path',
|
|
1008
|
+
'stats': 'path',
|
|
1009
|
+
|
|
1010
|
+
// TaskManager tool mappings
|
|
1011
|
+
'action': null, // Keep as content
|
|
1012
|
+
'title': null,
|
|
1013
|
+
'description': null,
|
|
1014
|
+
'priority': null,
|
|
1015
|
+
'taskId': null,
|
|
1016
|
+
'status': null
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
const mappedFieldName = contentFieldMapping[normalizedAction.type];
|
|
1020
|
+
|
|
1021
|
+
if (mappedFieldName) {
|
|
1022
|
+
// Map content to the specific field name
|
|
1023
|
+
normalizedAction[mappedFieldName] = normalizedAction.content;
|
|
1024
|
+
delete normalizedAction.content;
|
|
1025
|
+
console.log(`TagParser DEBUG: mapped 'content' to '${mappedFieldName}' for action type '${normalizedAction.type}'`);
|
|
1026
|
+
} else if (mappedFieldName === null) {
|
|
1027
|
+
// Explicitly keep as content or remove if not needed
|
|
1028
|
+
console.log(`TagParser DEBUG: keeping action type '${normalizedAction.type}' as-is`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
console.log('TagParser DEBUG: final normalized action:', normalizedAction);
|
|
1033
|
+
actions.push(normalizedAction);
|
|
1034
|
+
} // End inner loop for paramDataArray
|
|
1035
|
+
|
|
1036
|
+
// Preserve original format for backward compatibility (use first value if array)
|
|
1037
|
+
const backwardCompatValue = Array.isArray(paramData) ? paramData[0] : paramData;
|
|
1038
|
+
normalized.parameters[key] = backwardCompatValue.value;
|
|
1039
|
+
if (backwardCompatValue.attributes && Object.keys(backwardCompatValue.attributes).length > 0) {
|
|
1040
|
+
normalized.parameters[`${key}_attributes`] = backwardCompatValue.attributes;
|
|
1041
|
+
}
|
|
1042
|
+
} // End outer loop for parameters
|
|
1043
|
+
|
|
1044
|
+
console.log('TagParser DEBUG: total actions created:', actions.length);
|
|
1045
|
+
|
|
1046
|
+
// Add actions array if we found any XML actions
|
|
1047
|
+
if (actions.length > 0) {
|
|
1048
|
+
normalized.parameters.actions = actions;
|
|
1049
|
+
console.log('TagParser DEBUG: added actions array to parameters');
|
|
1050
|
+
} else {
|
|
1051
|
+
console.log('TagParser DEBUG: WARNING - no actions created, this will cause "actions is not iterable" error');
|
|
1052
|
+
}
|
|
1053
|
+
} else if (command.type === COMMAND_FORMATS.JSON || command.type === COMMAND_FORMATS.JSON_PLAIN) {
|
|
1054
|
+
// JSON parameters are already in simple format
|
|
1055
|
+
normalized.parameters = { ...command.parameters };
|
|
1056
|
+
|
|
1057
|
+
// Handle actions array format (common in agentcommunication tool)
|
|
1058
|
+
if (command.actions && Array.isArray(command.actions)) {
|
|
1059
|
+
normalized.parameters.actions = command.actions;
|
|
1060
|
+
|
|
1061
|
+
// For agentcommunication tool, extract the action from the first item
|
|
1062
|
+
if (command.toolId === TOOL_IDS.AGENT_COMMUNICATION && command.actions.length > 0) {
|
|
1063
|
+
const firstAction = command.actions[0];
|
|
1064
|
+
normalized.parameters.action = firstAction.type || firstAction.action;
|
|
1065
|
+
// Spread the rest of the action properties
|
|
1066
|
+
Object.assign(normalized.parameters, firstAction);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return normalized;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Check if a string is a valid XML tag name for tool commands
|
|
1076
|
+
* @param {string} tagName - Tag name to validate
|
|
1077
|
+
* @returns {boolean} True if valid
|
|
1078
|
+
* @private
|
|
1079
|
+
*/
|
|
1080
|
+
isValidXmlTagName(tagName) {
|
|
1081
|
+
// Valid tool command tag names: write, read, delete, copy, move, etc.
|
|
1082
|
+
const validTagNames = [
|
|
1083
|
+
'write', 'read', 'delete', 'copy', 'move', 'append',
|
|
1084
|
+
'create-dir', 'list', 'exists', 'stats',
|
|
1085
|
+
'run-command', 'cd', 'pwd',
|
|
1086
|
+
// Terminal tool tags
|
|
1087
|
+
'create-directory', 'change-directory', 'list-directory', 'get-working-directory',
|
|
1088
|
+
// Agent delay tool tags
|
|
1089
|
+
'pause-duration', 'reason',
|
|
1090
|
+
// TaskManager tool tags
|
|
1091
|
+
'action', 'title', 'description', 'priority', 'taskId', 'status',
|
|
1092
|
+
'dependsOn', 'dependencyType', 'relatedTo', 'parentTaskId',
|
|
1093
|
+
'timeframe', 'templateId', 'stage', 'milestone', 'notes',
|
|
1094
|
+
// Browser tool tags
|
|
1095
|
+
'url', 'selector', 'text', 'screenshot', 'element',
|
|
1096
|
+
// Agent communication tags
|
|
1097
|
+
'recipient', 'message', 'conversation-id',
|
|
1098
|
+
// Image generation tool tags
|
|
1099
|
+
'prompt', 'output-path', 'size', 'quality', 'model', 'batch', 'image',
|
|
1100
|
+
// General parameter tags
|
|
1101
|
+
'command', 'path', 'content', 'name', 'value', 'type', 'target'
|
|
1102
|
+
];
|
|
1103
|
+
|
|
1104
|
+
// Must be a known tag name and contain only letters, numbers, and hyphens
|
|
1105
|
+
return validTagNames.includes(tagName) && this.containsOnlyValidChars(tagName);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Check if tag name contains only valid characters (letters, numbers, hyphens)
|
|
1110
|
+
* @param {string} tagName - Tag name to check
|
|
1111
|
+
* @returns {boolean} True if valid
|
|
1112
|
+
* @private
|
|
1113
|
+
*/
|
|
1114
|
+
containsOnlyValidChars(tagName) {
|
|
1115
|
+
if (!tagName || tagName.length === 0) return false;
|
|
1116
|
+
|
|
1117
|
+
// First character must be a letter
|
|
1118
|
+
const firstChar = tagName.charAt(0);
|
|
1119
|
+
const firstIsLetter = (firstChar >= 'a' && firstChar <= 'z') || (firstChar >= 'A' && firstChar <= 'Z');
|
|
1120
|
+
if (!firstIsLetter) {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Rest can be letters, numbers, or hyphens
|
|
1125
|
+
for (let i = 1; i < tagName.length; i++) {
|
|
1126
|
+
const char = tagName.charAt(i);
|
|
1127
|
+
const isLetter = (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z');
|
|
1128
|
+
const isNumber = char >= '0' && char <= '9';
|
|
1129
|
+
const isHyphen = char === '-';
|
|
1130
|
+
|
|
1131
|
+
if (!isLetter && !isNumber && !isHyphen) {
|
|
1132
|
+
return false;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Check if tag name is in the valid tag list (for debugging)
|
|
1141
|
+
* @param {string} tagName - Tag name to check
|
|
1142
|
+
* @returns {boolean} True if in list
|
|
1143
|
+
* @private
|
|
1144
|
+
*/
|
|
1145
|
+
isTagInValidList(tagName) {
|
|
1146
|
+
const validTagNames = [
|
|
1147
|
+
'write', 'read', 'delete', 'copy', 'move', 'append',
|
|
1148
|
+
'create-dir', 'list', 'exists', 'stats',
|
|
1149
|
+
'run-command', 'cd', 'pwd',
|
|
1150
|
+
// Terminal tool tags
|
|
1151
|
+
'create-directory', 'change-directory', 'list-directory', 'get-working-directory',
|
|
1152
|
+
// Agent delay tool tags
|
|
1153
|
+
'pause-duration', 'reason',
|
|
1154
|
+
// TaskManager tool tags
|
|
1155
|
+
'action', 'title', 'description', 'priority', 'taskId', 'status',
|
|
1156
|
+
'dependsOn', 'dependencyType', 'relatedTo', 'parentTaskId',
|
|
1157
|
+
'timeframe', 'templateId', 'stage', 'milestone', 'notes',
|
|
1158
|
+
// Browser tool tags
|
|
1159
|
+
'url', 'selector', 'text', 'screenshot', 'element',
|
|
1160
|
+
// Agent communication tags
|
|
1161
|
+
'recipient', 'message', 'conversation-id',
|
|
1162
|
+
// Image generation tool tags
|
|
1163
|
+
'prompt', 'output-path', 'size', 'quality', 'model', 'batch', 'image',
|
|
1164
|
+
// General parameter tags
|
|
1165
|
+
'command', 'path', 'content', 'name', 'value', 'type', 'target'
|
|
1166
|
+
];
|
|
1167
|
+
return validTagNames.includes(tagName);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Convert kebab-case or snake_case to camelCase
|
|
1172
|
+
* @private
|
|
1173
|
+
*/
|
|
1174
|
+
_toCamelCase(str) {
|
|
1175
|
+
return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase());
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Extract all content between tags, including nested tags
|
|
1180
|
+
* @param {string} content - Content to search
|
|
1181
|
+
* @param {string} startTag - Opening tag
|
|
1182
|
+
* @param {string} endTag - Closing tag
|
|
1183
|
+
* @returns {Array} Array of extracted content blocks
|
|
1184
|
+
*/
|
|
1185
|
+
static extractBetweenTags(content, startTag, endTag) {
|
|
1186
|
+
const blocks = [];
|
|
1187
|
+
let startIndex = 0;
|
|
1188
|
+
|
|
1189
|
+
while (true) {
|
|
1190
|
+
const start = content.indexOf(startTag, startIndex);
|
|
1191
|
+
if (start === -1) break;
|
|
1192
|
+
|
|
1193
|
+
const end = content.indexOf(endTag, start + startTag.length);
|
|
1194
|
+
if (end === -1) break;
|
|
1195
|
+
|
|
1196
|
+
const blockContent = content.substring(start + startTag.length, end);
|
|
1197
|
+
blocks.push({
|
|
1198
|
+
content: blockContent,
|
|
1199
|
+
fullMatch: content.substring(start, end + endTag.length),
|
|
1200
|
+
startIndex: start,
|
|
1201
|
+
endIndex: end + endTag.length
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
startIndex = end + endTag.length;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return blocks;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Clean content by removing all tool commands and agent redirects
|
|
1212
|
+
* @param {string} content - Content to clean
|
|
1213
|
+
* @returns {string} Cleaned content
|
|
1214
|
+
*/
|
|
1215
|
+
cleanContent(content) {
|
|
1216
|
+
let cleaned = content;
|
|
1217
|
+
|
|
1218
|
+
// Remove tool command blocks
|
|
1219
|
+
cleaned = cleaned.replace(this.patterns.toolCommand, '');
|
|
1220
|
+
|
|
1221
|
+
// Remove agent redirects
|
|
1222
|
+
cleaned = cleaned.replace(this.patterns.agentRedirect, '');
|
|
1223
|
+
|
|
1224
|
+
// Remove JSON tool command blocks
|
|
1225
|
+
cleaned = cleaned.replace(this.patterns.jsonBlock, (match, jsonContent) => {
|
|
1226
|
+
try {
|
|
1227
|
+
const jsonData = JSON.parse(jsonContent);
|
|
1228
|
+
if (this.isToolCommandJSON(jsonData) ||
|
|
1229
|
+
(jsonData.toolCommands && Array.isArray(jsonData.toolCommands))) {
|
|
1230
|
+
return '';
|
|
1231
|
+
}
|
|
1232
|
+
} catch {
|
|
1233
|
+
// Not a tool command JSON block, keep it
|
|
1234
|
+
}
|
|
1235
|
+
return match;
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
// Clean up excessive whitespace
|
|
1239
|
+
cleaned = cleaned.replace(/\n\s*\n\s*\n/g, '\n\n');
|
|
1240
|
+
cleaned = cleaned.trim();
|
|
1241
|
+
|
|
1242
|
+
return cleaned;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
export default TagParser;
|