Files
gral/src/run.js
T

175 lines
5.0 KiB
JavaScript

#!/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<any>} 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;