diff options
Diffstat (limited to 'modules/core/module-aiassist.js')
| -rw-r--r-- | modules/core/module-aiassist.js | 60 |
1 files changed, 60 insertions, 0 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index fb4329f..a9b8fcc 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -1573,6 +1573,54 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re _pathArg(v) { return typeof v === 'string' ? v : (v == null ? '' : String(v)); } + // Single boundary coercion for tool arguments. The schemas already say + // which args are strings, but models send numbers/arrays/objects anyway + // and a handler's `a.field || ''` only catches FALSY values — so + // `.trim()`/`.split()`/`.replace()` on a truthy number or object threw a + // TypeError and the call was lost (one filed auto-report per tool). + // Coerce once here, per declared schema type, instead of patching ~26 + // handlers. Never throws: an unknown tool, bad defs or a non-object + // argument all fall back to the original value. + _coerceToolArgs(name, a) { + try { + if (!a || typeof a !== 'object' || Array.isArray(a)) return a; + var defs = this._toolDefs; + if (!defs) { defs = this._buildToolDefs(); this._toolDefs = defs; } + if (!Array.isArray(defs)) return a; + var props = null; + for (var i = 0; i < defs.length; i++) { + var fn = defs[i] && defs[i].function; + if (fn && fn.name === name) { + props = (fn.parameters && fn.parameters.properties) || null; + break; + } + } + if (!props) return a; + for (var p in props) { + if (!Object.prototype.hasOwnProperty.call(a, p)) continue; + var v = a[p]; + // Absent/null stays absent: _pathArg and the handlers' own + // defaults still own "no value given". + if (v === null || v === undefined) continue; + var type = props[p] && props[p].type; + if (type === 'string') { + if (typeof v === 'string') continue; + if (typeof v === 'number' || typeof v === 'boolean') { + a[p] = String(v); + } else if (typeof v === 'object') { + // Structured content must reach the file as readable + // JSON, never "[object Object]". + try { a[p] = JSON.stringify(v); } catch (_e) { a[p] = String(v); } + } + } else if (type === 'integer' || type === 'number') { + // Only convert unambiguous numeric strings; anything else + // is left for the handler to validate/report. + if (typeof v === 'string' && /^-?\d+(\.\d+)?$/.test(v)) a[p] = Number(v); + } + } + return a; + } catch (_e) { return a; } + } _updateDebugPanel() { if (!this._debugPanelEnabled) return; const panel = document.getElementById('debug-ai-panel'); @@ -2593,6 +2641,13 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re this.terminal.print(`🟡 GUARDRAIL WARNING: ${guardrail.description} (rule: ${guardrail.id})`, 'warning'); } a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); + // A body of "null" / "[1,2]" parses to a non-object, and every + // handler below reads a.field — normalise that once here. + if (!a || typeof a !== 'object' || Array.isArray(a)) a = {}; + // ONE schema-driven coercion for the whole switch: a model that + // sends a number/array/object for a schema-string field must not + // reach `.trim()`/`.split()` still holding that type. + a = this._coerceToolArgs(name, a); switch (name) { case 'list_files': { const originalPath = this._pathArg(a.path); @@ -3503,6 +3558,11 @@ max_chars: { type: 'integer', description: 'Max characters of page content to re let a = {}; try { a = typeof argsStr === 'object' ? argsStr : (argsStr ? JSON.parse(argsStr) : {}); + if (!a || typeof a !== 'object' || Array.isArray(a)) a = {}; + // Same single boundary as runTool: module tools take a string + // `name`/`code`/`file` too and are reachable directly and via + // runTool's `case 'view_module':` delegation. + a = this._coerceToolArgs(name, a); switch (name) { case 'view_module': { const modName = a.name || ''; |