@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
192 lines • 7.46 kB
JavaScript
import { getToolName } from 'ai';
import { fillTemplate, phraseFor } from './phrasing.js';
import { resolveResultView } from './result-view.js';
/** Whether the stream classified this call as an `action` (it parks for approval before it runs). */
export function isActionCall(part) {
const metadata = part.toolMetadata;
return (typeof metadata === 'object' &&
metadata !== null &&
metadata.toolKind === 'action');
}
export function toolCallState(part) {
const output = 'output' in part ? part.output : undefined;
if (output !== null &&
typeof output === 'object' &&
'proposalId' in output &&
'executed' in output &&
output.executed === false)
return {
status: 'awaiting-approval',
isSettled: false,
isFailed: false,
isDenied: false,
output,
error: null,
};
const errorText = 'errorText' in part ? part.errorText : undefined;
const outputError = output !== null && typeof output === 'object' && 'error' in output
? String(output.error)
: undefined;
const isDenied = part.state === 'output-denied';
const isFailed = part.state === 'output-error' || (outputError !== undefined && !isDenied);
const isSettled = part.state === 'output-available' || part.state === 'output-error' || isDenied;
const isAwaiting = part.state === 'approval-requested' || (part.state === 'input-available' && isActionCall(part));
const status = isDenied
? 'denied'
: isFailed
? 'failed'
: isSettled
? 'done'
: isAwaiting
? 'awaiting-approval'
: 'running';
return {
status,
isSettled,
isFailed,
isDenied,
output,
error: errorText ?? outputError ?? null,
};
}
/**
* Which failed calls the model went on to correct, by call id: a call that failed and was followed,
* in the same run, by a SUCCESS of the same tool. The operator wants to know a retry happened, not
* to read every attempt. A failed read is not corrected by a successful query.
*/
export function correctedCallIds(parts) {
const corrected = new Set();
const lastSuccessByTool = new Map();
parts.forEach((part, index) => {
if (part.state === 'output-available' && !toolCallState(part).isFailed) {
lastSuccessByTool.set(getToolName(part), index);
}
});
parts.forEach((part, index) => {
const successAt = lastSuccessByTool.get(getToolName(part));
if (successAt !== undefined && index < successAt && toolCallState(part).isFailed) {
corrected.add(part.toolCallId);
}
});
return corrected;
}
/**
* Which failed calls the model retried AT ONCE, by call id: a failure whose next tool call (in
* message order) is the same tool and has not failed — it succeeded, or is still running. A chain
* of failures that ends in a success is retried whole. What a person should not see: the model
* fixing its own malformed call (a `ui__render` it left the envelope out of) before the view it
* then drew. A failure the model moved on from, or that the retry failed too, stays.
*/
export function retriedCallIds(parts) {
const retried = new Set();
let next;
for (let index = parts.length - 1; index >= 0; index -= 1) {
const part = parts[index];
const name = getToolName(part);
const state = toolCallState(part);
if (state.isFailed) {
if (next?.name === name && next.ok) {
retried.add(part.toolCallId);
continue;
}
next = { name, ok: false };
}
else {
next = { name, ok: !state.isDenied };
}
}
return retried;
}
export function describeToolCall(part, catalog, options = {}) {
const presentation = catalog?.[getToolName(part)];
const state = toolCallState(part);
const input = part.input;
const confirm = presentation?.confirm;
return {
status: state.status,
phrase: phraseFor(presentation, input, state.isSettled, options.fallback),
label: presentation?.label ?? null,
icon: presentation?.icon ?? null,
tone: presentation?.tone ?? 'neutral',
detail: presentation?.detail ?? null,
confirm: options.confirmation !== undefined
? {
title: options.confirmation.title,
verb: options.confirmation.verb,
detail: options.confirmation.detail ?? null,
}
: confirm === undefined
? null
: {
title: fillTemplate(confirm.title, input),
verb: fillTemplate(confirm.verb, input),
detail: confirm.detail !== undefined ? fillTemplate(confirm.detail, input) : null,
},
result: state.status === 'done'
? resolveResultView(state.output, presentation?.result, {
...(options.inferResult !== undefined ? { infer: options.inferResult } : {}),
})
: null,
error: state.error,
presentation: presentation ?? null,
};
}
const STATUS_WEIGHT = {
running: 4,
queued: 4,
executing: 4,
succeeded: 0,
'awaiting-approval': 3,
failed: 2,
denied: 1,
done: 0,
};
function descendants(call) {
return call.children.flatMap((child) => [child, ...descendants(child)]);
}
function leaves(call) {
return call.children.length === 0 ? [call] : call.children.flatMap(leaves);
}
/**
* Fold a tool run into activity groups, in order of first appearance. Pass `block.roots` (the
* transcript's call tree) so nesting is honoured; a flat `block.calls` works too and simply has
* nothing nested.
*/
export function groupToolActivity(calls, options = {}) {
const expanded = options.expandNested === true ? calls.flatMap(leaves) : [...calls];
const corrected = options.hideCorrected === true
? correctedCallIds(expanded.map((call) => call.part))
: new Set();
const groups = new Map();
for (const call of expanded) {
if (corrected.has(call.toolCallId))
continue;
const presentation = options.catalog?.[call.name];
const key = options.keyOf?.(call, presentation) ?? presentation?.label ?? call.name;
const status = call.description.status;
const phrase = phraseFor(presentation, call.part.input, status !== 'running' && status !== 'awaiting-approval', options.fallback);
const inner = options.expandNested === true ? 0 : descendants(call).length;
const existing = groups.get(key);
if (existing === undefined) {
groups.set(key, {
key,
label: presentation?.label ?? null,
icon: presentation?.icon ?? null,
status,
count: 1,
phrase,
calls: [call],
innerCount: inner,
});
continue;
}
existing.count += 1;
existing.calls.push(call);
existing.phrase = phrase;
existing.innerCount += inner;
if (STATUS_WEIGHT[status] > STATUS_WEIGHT[existing.status])
existing.status = status;
}
return [...groups.values()];
}
//# sourceMappingURL=tool-activity.js.map