aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-aiassist.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-05-30 06:00:47 +0200
committerAgando <roduccomm@gmail.com>2026-05-30 06:00:47 +0200
commite3fb2d434c4f7317944b7c77781599528d766a39 (patch)
tree8f9f9c94b19d46f42f8086adb6b68ee07dfce5c6 /modules/core/module-aiassist.js
parentd570f86177c514ae888fb63fc0bb0da7dd4d5097 (diff)
auto: 2026-05-30_06:00 changes
Diffstat (limited to 'modules/core/module-aiassist.js')
-rw-r--r--modules/core/module-aiassist.js113
1 files changed, 16 insertions, 97 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 602ae3e..f6fadb5 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -543,42 +543,6 @@ RULES:
}
return null;
}
- _parseToolBlocksFromText(text) {
- if (!text) return [];
- const toolCalls = [];
- // Match ```tool name(args) or ```tool\nname(args) blocks
- const blockRegex = /```tool(?:\s*\n)?\s*(\w+)\s*({[^}]*})?\s*```/gi;
- let match;
- while ((match = blockRegex.exec(text)) !== null) {
- const name = match[1];
- let args = {};
- try {
- if (match[2]) args = JSON.parse(match[2]);
- } catch (e) {
- // If JSON parse fails, treat as raw string
- args = match[2] ? { path: match[2].replace(/[{}]/g,'').trim() } : {};
- }
- toolCalls.push({
- id: 'text_' + Date.now() + '_' + toolCalls.length,
- type: 'function',
- function: { name, arguments: args }
- });
- }
- // Also handle single-line format: ```tool get_status{}
- const simpleRegex = /```tool\s+(\w+)\s*\{([^}]*)\}\s*```/gi;
- while ((match = simpleRegex.exec(text)) !== null) {
- // Avoid duplicates — skip if already caught by blockRegex
- const name = match[1];
- if (!toolCalls.find(tc => tc.function.name === name)) {
- toolCalls.push({
- id: 'text_' + Date.now() + '_' + toolCalls.length,
- type: 'function',
- function: { name, arguments: {} }
- });
- }
- }
- return toolCalls;
- }
_trackToolFailure(toolName, error) {
try {
const key = 'webx11_tool_failures';
@@ -611,67 +575,7 @@ RULES:
if (this._cancelled) return { finalAnswer: null, toolTurns };
if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; }
if (res.message && !res.tool_calls) {
- let text = res.message.content || '';
- // Fallback: parse ```tool blocks from text when model writes them instead of native tool_calls
- let parsedToolCalls = this._parseToolBlocksFromText(text);
- if (parsedToolCalls.length > 0) {
- this.terminal.print(`🔧 Parsed ${parsedToolCalls.length} tool call(s) from text block — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info');
- // Extract any non-tool text as pending assistant content
- const cleanText = text.replace(/```tool[\s\S]*?```/g, '').trim();
- if (cleanText) messages.push({ role: 'assistant', content: cleanText });
- messages.push({ role: 'assistant', content: '(calling tool via text block)' });
- let pendingText = cleanText || null;
- const destructiveTools = parsedToolCalls.filter(tc => this.isDestructive(tc.function.name));
- const safeTools = parsedToolCalls.filter(tc => !this.isDestructive(tc.function.name));
- const safeResults = await Promise.all(safeTools.map(async (tc) => {
- const fn = tc.function;
- const t0 = Date.now();
- const toolResult = await this.runTool(fn.name, fn.arguments);
- const t1 = Math.round((Date.now() - t0) / 1000);
- return { fn, toolResult, t1 };
- }));
- for (const { fn, toolResult, t1 } of safeResults) {
- this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
- const obs = this._summarizeObservation(toolResult, 150);
- const err = this._isToolError(toolResult);
- if (err) {
- this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`);
- this._trackToolFailure(fn.name, err);
- this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${err}`, 'warning');
- } else {
- this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
- this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
- }
- messages.push({ role: 'tool', content: toolResult });
- }
- if (destructiveTools.length > 0) {
- const approved = await this.confirmActionMulti(destructiveTools);
- if (approved) {
- for (const tc of destructiveTools) {
- const fn = tc.function;
- const t0 = Date.now();
- this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
- const toolResult = await this.runTool(fn.name, fn.arguments);
- const t1 = Math.round((Date.now() - t0) / 1000);
- const dObs = this._summarizeObservation(toolResult, 150);
- const dErr = this._isToolError(toolResult);
- if (dErr) {
- this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${dErr} (${t1}s)`);
- this._trackToolFailure(fn.name, dErr);
- this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${dErr}`, 'warning');
- } else {
- this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
- this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info');
- }
- messages.push({ role: 'tool', content: toolResult });
- }
- } else {
- messages.push({ role: 'tool', content: `Destructive actions (${destructiveTools.map(tc => tc.function.name).join(', ')}) denied.` });
- }
- }
- if (pendingText) messages.push({ role: 'assistant', content: pendingText });
- continue;
- }
+ const text = res.message.content || '';
this.terminal.printAIResponse(text || '(no response)');
if (text) this.terminal.ollamaManager.fireResponseHooks(text);
this.terminal.print(`💬 AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info');
@@ -734,6 +638,21 @@ RULES:
});
}
}
+// Validator: check safe tool results for consistency
+ if (safeResults.length > 0 && pendingText) {
+ const om = this.terminal.ollamaManager;
+ if (om && om.validatorEnabled && om.validatorModel) {
+ let anyInvalid = false;
+ for (const { fn, toolResult } of safeResults) {
+ const valid = await om.validateToolResult(fn.name, fn.arguments, toolResult, pendingText || '');
+ if (!valid && !this._cancelled) {
+ anyInvalid = true;
+ this.terminal.print('🔍 Validator: ' + fn.name + ' result — AI note may not match (logged)', 'info');
+ // Just log it — don't force retry (cloud models get 400 on retry)
+ }
+ }
+ }
+ }
if (pendingText) {
messages.push({ role: 'assistant', content: pendingText });
}