175 lines
5.0 KiB
JavaScript
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;
|