@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
JavaScript
/*
* `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