diff --git a/src/run.js b/src/run.js index f1c3bf5..6dd02c8 100644 --- a/src/run.js +++ b/src/run.js @@ -45,6 +45,25 @@ function makeExecutor(bundle) { }; } +/** + * Build a name -> rendered-arguments function for the bundle, used to log + * each call. Tools that do not provide a `render` fall back to a plain + * JSON rendering of the arguments. + */ +function makeRenderer(bundle) { + const byName = new Map(bundle.map((tool) => [tool.name, tool])); + + return function render(name, args) { + const tool = byName.get(name); + if (!tool || typeof tool.render !== 'function') return JSON.stringify(args || {}); + try { + return tool.render(args || {}); + } catch (err) { + return JSON.stringify(args || {}); + } + }; +} + /* ------------------------------------------------------------------ * * Tool-call extraction * ------------------------------------------------------------------ */ @@ -142,6 +161,7 @@ async function run({ parameters, })); const execute = makeExecutor(bundle); + const render = makeRenderer(bundle); const history = Array.isArray(messages) ? messages.slice() : []; @@ -188,10 +208,7 @@ async function run({ role: 'tool', content: call, }); - } - - if (calls.length) { - console.log('called: ' + calls.map(c => c.tool_name)); + process.stderr.write(`called: ${call.tool_name} ${render(call.tool_name, call.arguments)}\n`); } if (verify && verify()) { diff --git a/src/tools.js b/src/tools.js index 8957709..44feb59 100644 --- a/src/tools.js +++ b/src/tools.js @@ -149,6 +149,19 @@ function applyEdit(content, { search, replace = '', regex, global }) { return content.slice(0, at) + replace + content.slice(at + search.length); } +/** Collapse whitespace and clip to `max` characters for single-line output. */ +function oneLine(value, max = 60) { + const text = String(value ?? '').replace(/\s+/g, ' ').trim(); + return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text; +} + +/** Fallback argument rendering: values joined, JSON for non-strings. */ +function defaultRender(args) { + return Object.values(args ?? {}) + .map((value) => (typeof value === 'string' ? value : JSON.stringify(value))) + .join(' '); +} + /* ------------------------------------------------------------------ * * Tool base class * ------------------------------------------------------------------ */ @@ -181,6 +194,16 @@ export class Tool { return this.run(args ?? {}); } + /** + * One-line rendering of a call's arguments, used when logging the call. + * Subclasses override this when a more legible summary is possible. + */ + render(args) { + // returning all json for all calls bloats + return ''; + //return defaultRender(args); + } + /** Resolve `file` against the root, asserting the given permission. */ resolve(file, verb) { const abs = path.resolve(this.root, file); @@ -206,6 +229,11 @@ class ReadFile extends Tool { required: ['path'], }; + render({ path: file, lineStart, lineEnd }) { + const range = lineStart || lineEnd ? `:${lineStart ?? 1}-${lineEnd ?? 'eof'}` : ''; + return `${file}${range}`; + } + run({ path: file, lineStart, lineEnd }) { const abs = this.resolve(file, 'read'); @@ -236,6 +264,10 @@ class ListDir extends Tool { required: [], }; + render({ path: dir }) { + return dir || '.'; + } + run({ path: dir }) { const abs = this.resolve(dir || '.', 'read'); @@ -263,6 +295,10 @@ class FindFiles extends Tool { required: ['pattern'], }; + render({ pattern, path: dir }) { + return dir ? `${pattern} in ${dir}` : pattern; + } + run({ pattern, path: dir = '.', limit = 200 }) { const abs = this.resolve(dir, 'read'); const match = globToRegExp(pattern); @@ -307,6 +343,10 @@ class SearchFiles extends Tool { required: ['pattern'], }; + render({ pattern, path: dir = '.', glob = '**/*' }) { + return `${pattern} in ${dir} (${glob})`; + } + run({ pattern, path: dir = '.', @@ -390,6 +430,10 @@ class WriteFile extends Tool { required: ['path', 'content'], }; + render({ path: file }) { + return file; + } + run({ path: file, content }) { const abs = this.resolve(file, 'write'); fs.mkdirSync(path.dirname(abs), { recursive: true }); @@ -415,6 +459,10 @@ class UpdateFile extends Tool { required: ['path', 'search'], }; + render({ path: file, search }) { + return `${file}: ${oneLine(search)}`; + } + run({ path: file, search, replace = '', global, regex }) { const abs = this.resolve(file, 'write'); const content = fs.readFileSync(abs, 'utf8'); @@ -450,6 +498,10 @@ class MultiEdit extends Tool { required: ['path', 'edits'], }; + render({ path: file, edits }) { + return `${file} (${Array.isArray(edits) ? edits.length : 0} edits)`; + } + run({ path: file, edits }) { const abs = this.resolve(file, 'write'); const original = fs.readFileSync(abs, 'utf8'); @@ -504,12 +556,13 @@ function selectDefaults(selection) { * to the resolved scope so it only receives the call arguments. */ function wrap(tool, root) { - const { name, description, parameters, scope, handler } = tool; + const { name, description, parameters, scope, handler, render } = tool; return { name, description, parameters, handler: (args) => handler(args ?? {}, { root, scope }), + render: typeof render === 'function' ? render : defaultRender, }; }