@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
84 lines • 4.17 kB
JavaScript
/** What the model is told about a call whose turn died before the call was settled. */
export const UNFINISHED_TOOL_CALL = 'This tool call was never completed: the turn it belonged to ended unexpectedly before it was ' +
'settled. Do not assume it ran, and do not assume it did not — check the current state with a ' +
'read tool if one exists, tell the person plainly what is and is not confirmed, and do it again ' +
'only if they still want it.';
/** What is written on the row of a call whose run ended without settling it. */
export const RUN_ENDED_BEFORE_TOOL_CALL = 'the run ended before this tool call was settled';
const DECLINED = 'The person was asked to approve this action and declined it. Nothing ran and nothing changed.';
const EXPIRED = 'This action needed approval, and the request expired before anyone decided. Nothing ran.';
/** The ids of every tool call in `messages` that the message asking for it holds no result for. */
export function danglingToolCallIds(messages) {
const ids = [];
for (const message of messages) {
const calls = message.toolCalls ?? [];
if (message.role !== 'assistant' || calls.length === 0) {
continue;
}
const settled = new Set((message.toolResults ?? []).map((result) => result.id));
for (const call of calls) {
if (!settled.has(call.id)) {
ids.push(call.id);
}
}
}
return ids;
}
function resultFor(call, outcome) {
const base = { id: call.id, name: call.name };
switch (outcome?.status) {
case 'executed':
case 'auto_executed':
return { ...base, output: outcome.output ?? null };
case 'failed':
return { ...base, output: null, error: outcome.error ?? UNFINISHED_TOOL_CALL };
case 'rejected':
return { ...base, output: { rejected: true }, denied: true, error: DECLINED };
case 'expired':
return {
...base,
output: { rejected: true, expired: true },
denied: true,
expired: true,
error: EXPIRED,
};
default:
return { ...base, output: null, error: UNFINISHED_TOOL_CALL };
}
}
/**
* Give every tool call in a thread's history a result.
*
* A turn writes its results onto the assistant message once the step's LAST tool has settled. A turn
* that dies before that — its run failed, its worker was killed, an approval was never answered and
* the run was abandoned — leaves an assistant message that asks for tools and is answered by
* nothing. A provider refuses a prompt shaped like that (a tool call must be followed by its
* result), so every LATER turn on the thread fails too, with an error about the stream rather than
* about the history: one dead turn makes the whole conversation unusable.
*
* Here each such call is settled from what is actually known: the call's own row where the store
* can read it (`outcomes` — a tool that DID run hands the model its real output, so it is not run a
* second time), and otherwise a result saying the call was never completed. Pure: the same messages
* and outcomes always produce the same prompt, so a replay composes what the first attempt did.
* Messages with nothing dangling are returned as they are.
*/
export function settleDanglingToolCalls(messages, outcomes = []) {
const known = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
return messages.map((message) => {
const calls = message.toolCalls ?? [];
if (message.role !== 'assistant' || calls.length === 0) {
return message;
}
const results = message.toolResults ?? [];
const settled = new Set(results.map((result) => result.id));
const missing = calls.filter((call) => !settled.has(call.id));
if (missing.length === 0) {
return message;
}
return {
...message,
toolResults: [...results, ...missing.map((call) => resultFor(call, known.get(call.id)))],
};
});
}
//# sourceMappingURL=dangling-tool-calls.js.map