UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

128 lines • 4.83 kB
/* * `ctx.emitUi` plumbing, after `tool-ui.ts` in `@dudousxd/nestjs-agent-core`: a collector per tool * call that streams each push and remembers it, and the envelope that carries the pushes on the * tool step's journaled result — so a durable replay neither re-streams nor re-persists them. */ const TOOL_STEP_UI = '@@adonis-agent/tool-step-ui'; /** * What a tool step returns: the output alone when the tool pushed nothing (the bytes a run journaled * before `emitUi` existed), else an envelope carrying the pushes with it. */ export function wrapToolStepOutput(output, ui, text) { const reservedOutput = typeof output === 'object' && output !== null && (TOOL_STEP_UI in output || '@@adonis-agent/tool-preflight-denied' in output); if (ui.length === 0 && !text && !reservedOutput) { return output; } const wrapped = { [TOOL_STEP_UI]: 1, output, ui: [...ui], ...(text ? { text } : {}), }; return wrapped; } /** Read a tool step's result back — an envelope, or a bare output recorded without one. */ export function unwrapToolStepOutput(raw) { if (typeof raw === 'object' && raw !== null && raw[TOOL_STEP_UI] === 1) { const wrapped = raw; return { output: wrapped.output, ui: Array.isArray(wrapped.ui) ? wrapped.ui : [], ...(wrapped.text ? { text: wrapped.text } : {}), }; } return { output: raw, ui: [] }; } /** * An `emitUi` for a context with no conversation to push into (an MCP call, a handler invoked * directly): it accepts the push and answers an id, so a tool never has to branch on where it runs. */ export function createNoopEmitUi(scope = 'noop') { let next = 0; return async (_component, _props, options = {}) => { if (options.id !== undefined) { return { id: options.id }; } const id = `${scope}:ui:${next}`; next += 1; return { id }; }; } /** * The `emitUi` of one tool call. Ids default to `<toolCallId>:ui:<n>`; each push is written to the * run's stream as it happens (a component frame, which both envelopes carry) and kept for the step's * result. */ export function createUiCollector(toolCallId, writer) { const pushed = new Map(); let next = 0; const emit = async (component, props, options = {}) => { if (typeof component !== 'string' || component.length === 0) { throw new Error('emitUi: component must be a non-empty string'); } if (typeof props !== 'object' || props === null || Array.isArray(props)) { throw new Error('emitUi: props must be a JSON object'); } let id = options.id; if (id === undefined) { id = `${toolCallId}:ui:${next}`; next += 1; } const entry = { id, component, // Snapshot: the frame and the persisted value are what the tool pushed at THIS moment. props: JSON.parse(JSON.stringify(props)), ...(options.version !== undefined ? { version: options.version } : {}), toolCallId, ...(options.fallbackText !== undefined ? { fallbackText: options.fallbackText } : {}), ...(options.componentVersions !== undefined ? { componentVersions: JSON.parse(JSON.stringify(options.componentVersions)), } : {}), }; pushed.set(id, entry); await writer?.write({ t: 'component', name: component, data: entry.props, id, toolCallId, ...(entry.version !== undefined ? { version: entry.version } : {}), ...(entry.fallbackText !== undefined ? { fallbackText: entry.fallbackText } : {}), ...(entry.componentVersions !== undefined ? { componentVersions: entry.componentVersions } : {}), }); return { id }; }; return { emit, components: () => [...pushed.values()], restart: () => { next = 0; }, }; } /** Merge component lists: first-seen order, last props per id. */ export function mergeUi(...lists) { const merged = new Map(); for (const list of lists) { for (const component of list ?? []) { merged.set(component.id, component); } } return [...merged.values()]; } /** Preserve JSON snapshots, but make unsafe UTF-16 safe for human-text SQL columns. */ export function escapeUnsafeToolUiText(text) { return text .replaceAll('\u0000', '\\u0000') .replace(/[\uD800-\uDFFF]/gu, (unit) => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`); } //# sourceMappingURL=tool-ui.js.map