UNPKG

@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
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