From 75402cfe62f120674009ea1878cf499c53292bf3 Mon Sep 17 00:00:00 2001 From: Stefan Kuhn Date: Sat, 7 Mar 2026 00:35:03 +0000 Subject: [PATCH] fix(coding-lint): validate generated calls against a structured skill index Generated-code lint did not validate referenced `skills.*` / `world.*` calls correctly. `coder.js` extracted only method names such as `placeBlock`, while `SkillLibrary` exposed skill docs as strings keyed by fully qualified names such as `skills.placeBlock`. That meant lint validation depended on the shape of the prompt-doc data instead of on a real function index, so the check was unreliable and could misreport valid generated calls. Replace that with a structured namespace-aware function index in `SkillLibrary` and validate parsed calls against it in `coder.js`. Missing calls are now reported as fully qualified names, prompt doc selection stays unchanged, and the now-unused `getAllSkillDocs()` accessor is removed. --- src/agent/coder.js | 29 +++++++++++++++---------- src/agent/library/skill_library.js | 34 ++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/agent/coder.js b/src/agent/coder.js index 18a5f2618..36cdcabd6 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -111,21 +111,28 @@ export class Coder { async _lintCode(code) { let result = '#### CODE ERROR INFO ###\n'; - // Extract everything in the code between the beginning of 'skills./world.' and the '(' - const skillRegex = /(?:skills|world)\.(.*?)\(/g; - const skills = []; + // Extract namespace-qualified function calls like skills.placeBlock(...) + // and world.getNearestFreeSpace(...). Keeping namespace prevents + // collisions between similarly named functions. + const skillRegex = /\b(skills|world)\.([A-Za-z_$][\w$]*)\s*\(/g; + const referencedFunctions = []; let match; while ((match = skillRegex.exec(code)) !== null) { - skills.push(match[1]); + referencedFunctions.push({ + namespace: match[1], + name: match[2], + }); } - const allDocs = await this.agent.prompter.skill_libary.getAllSkillDocs(); + const availableFunctions = await this.agent.prompter.skill_libary.getAvailableFunctionsByNamespace(); // check function exists - const missingSkills = skills.filter(skill => !!allDocs[skill]); + const missingSkills = referencedFunctions + .filter(fn => !(availableFunctions[fn.namespace]?.has(fn.name))) + .map(fn => `${fn.namespace}.${fn.name}`); if (missingSkills.length > 0) { result += 'These functions do not exist.\n'; result += '### FUNCTIONS NOT FOUND ###\n'; result += missingSkills.join('\n'); - console.log(result) + console.log(result); return result; } @@ -192,7 +199,7 @@ export class Coder { const mainFn = compartment.evaluate(src); if (write_result) { - console.error('Error writing code execution file: ' + result); + console.error('Error writing code execution file: ' + write_result); return null; } return { func:{main: mainFn}, src_lint_copy: src_lint_copy }; @@ -200,11 +207,11 @@ export class Coder { _sanitizeCode(code) { code = code.trim(); - const remove_strs = ['Javascript', 'javascript', 'js'] + const remove_strs = ['Javascript', 'javascript', 'js']; for (let r of remove_strs) { if (code.startsWith(r)) { code = code.slice(r.length); - return code; + break; } } return code; @@ -222,4 +229,4 @@ export class Coder { }); }); } -} \ No newline at end of file +} diff --git a/src/agent/library/skill_library.js b/src/agent/library/skill_library.js index 4470586f1..fb6b8842e 100644 --- a/src/agent/library/skill_library.js +++ b/src/agent/library/skill_library.js @@ -8,11 +8,32 @@ export class SkillLibrary { this.embedding_model = embedding_model; this.skill_docs_embeddings = {}; this.skill_docs = null; - this.always_show_skills = ['skills.placeBlock', 'skills.wait', 'skills.breakBlockAt'] + this.available_functions_by_namespace = { + skills: new Set(), + world: new Set(), + }; + this.always_show_functions = [ + { namespace: 'skills', name: 'placeBlock' }, + { namespace: 'skills', name: 'wait' }, + { namespace: 'skills', name: 'breakBlockAt' }, + ]; } async initSkillLibrary() { const skillDocs = getSkillDocs(); this.skill_docs = skillDocs; + this.available_functions_by_namespace = { + skills: new Set(), + world: new Set(), + }; + for (const doc of skillDocs) { + const qualifiedName = doc.split('\n')[0]?.trim(); + const match = qualifiedName?.match(/^(skills|world)\.([A-Za-z_$][\w$]*)$/); + if (match) { + const namespace = match[1]; + const methodName = match[2]; + this.available_functions_by_namespace[namespace].add(methodName); + } + } if (this.embedding_model) { try { const embeddingPromises = skillDocs.map((doc) => { @@ -28,19 +49,20 @@ export class SkillLibrary { } } this.always_show_skills_docs = {}; - for (const skillName of this.always_show_skills) { - this.always_show_skills_docs[skillName] = this.skill_docs.find(doc => doc.includes(skillName)); + for (const func of this.always_show_functions) { + const qualifiedName = `${func.namespace}.${func.name}`; + this.always_show_skills_docs[qualifiedName] = this.skill_docs.find(doc => doc.startsWith(`${qualifiedName}\n`) || doc === qualifiedName); } } - async getAllSkillDocs() { - return this.skill_docs; + getAvailableFunctionsByNamespace() { + return this.available_functions_by_namespace; } async getRelevantSkillDocs(message, select_num) { if(!message) // use filler message if none is provided message = '(no message)'; - let skill_doc_similarities = []; + let skill_doc_similarities; if (select_num === -1) { skill_doc_similarities = Object.keys(this.skill_docs_embeddings)