#!/usr/bin/env node 'use strict'; /* * run.js — the harness behind `Gral.run`. * * Given a prompt for a target agent it: * 1. Resolves the `read` / `write` permission scopes. * 2. Assembles the tool specs from `tools` + `defaultTools`. * 3. Runs the prompt/tool-call loop: send the prompt, execute any tool * calls locally, feed the results back, repeat until the agent * returns a plain response. * * Permission scopes and tool definitions live in 'tools.js'. */ import { resolveScope, buildTools } from './tools.js'; const MAX_ITERATIONS = 20; /* ------------------------------------------------------------------ * * Tool executor * ------------------------------------------------------------------ */ /** * Build a name -> execution function for the bundle. Unknown tools and * handler failures are reported as tool-result strings rather than thrown, * so the loop can hand them back to the agent. */ function makeExecutor(bundle) { const byName = new Map(bundle.map((tool) => [tool.name, tool])); return function execute(name, args) { const tool = byName.get(name); if (!tool) return `Error: unknown tool '${name}'`; if (typeof tool.handler !== 'function') { return `Error: tool '${name}' has no handler`; } try { return tool.handler(args || {}); } catch (err) { console.error(err); return `Error: ${err.message}`; } }; } /* ------------------------------------------------------------------ * * Tool-call extraction * ------------------------------------------------------------------ */ /** * Normalize the tool calls returned by the prompt action. Accepts either * a flat `{id, name, arguments}` shape or a nested `{id, function:{...}}` * shape, and parses string `arguments` as JSON. */ function extractToolCalls(res) { const raw = res && (res.toolCalls || res.tool_calls); if (!Array.isArray(raw)) return []; return raw.map((call) => { const fn = call.function || call; let args = call.arguments !== undefined ? call.arguments : fn.arguments; if (typeof args === 'string') { try { args = JSON.parse(args); } catch (err) { args = {}; } } return { id: call.id, name: fn.name, args: args || {} }; }); } /* ------------------------------------------------------------------ * * Run * ------------------------------------------------------------------ */ /** * The harness behind `Gral.run`. Sends a prompt to a target agent with a * tool bundle restricted by the supplied arguments, then loops: while the * agent returns tool calls, execute them locally and send the results * back until a plain response is produced. * * @param {object} options * @param {string} options.agentId Target agent for the prompt. * @param {string} [options.instruction] Instruction for the agent. * @param {string} [options.input] Main input for the agent. * @param {object[]} [options.messages] Prior messages for the agent. * @param {?boolean|string[]} [options.read] Readable files/dirs (null = all). * @param {?boolean|string[]} [options.write] Writable files/dirs (null = all). * @param {object[]} [options.tools] Extra tools to pass the agent. * @param {?boolean|string[]} [options.defaultTools] Default tool selection. * @returns {Promise} The agent's final response. */ async function run({ agentId, instruction, input, messages, read = null, write = null, tools = null, defaultTools = true, verify = null, } = {}) { const Gral = global.Gral; const root = Gral && Gral.path ? Gral.path : process.cwd(); if (!agentId) throw new Error('run: agentId is required'); if (!Gral || !Gral.horde) throw new Error('run: horde connection unavailable'); const bundle = buildTools({ root, read: resolveScope(read, root), write: resolveScope(write, root), tools, defaultTools, }); const specs = bundle.map(({ name, description, parameters }) => ({ type: 'function', function: { name, description, parameters, } })); const execute = makeExecutor(bundle); const history = Array.isArray(messages) ? messages.slice() : []; for (let step = 0; step < MAX_ITERATIONS; step++) { const res = await Gral.horde.sendAction('send-agent-prompt', { agentId, instruction, input, options: { messages: history, tools: specs, }, }); const calls = res.toolCalls || []; if (calls.length === 0) return res.response; if (res.response) { console.log(res.response); history.push({ role: 'assistant', content: res.response, }); } for (const call of calls) { call.result = execute(call.tool_name, call.arguments); history.push({ role: 'tool', content: call, }); } if (calls.length) { console.log('called: ' + calls.map(c => c.tool_name)); } if (verify && verify()) { return null; } } throw new Error(`run: exceeded ${MAX_ITERATIONS} tool-call iterations`); } export default run;