@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
137 lines • 5.58 kB
JavaScript
import { parsePartialJson } from './partial-json.js';
/** Default least time between two preview frames of one call ({@link ToolInputPreview.throttleMs}). */
export const DEFAULT_PREVIEW_THROTTLE_MS = 100;
/** The id a call's preview is shown under: the one its first `ctx.emitUi` push gets. */
export function previewUiId(toolCallId) {
return `${toolCallId}:ui:0`;
}
/** The frame that withdraws a preview: partial, with nothing to draw. */
export function withdrawPreviewFrame(shown) {
return {
t: 'component',
name: shown.component,
data: {},
id: shown.id,
toolCallId: shown.toolCallId,
partial: true,
};
}
/**
* Wrap the writer a model turn streams into so a tool that previews its input
* (`ToolHandler.previewInput`) gets `ui` frames drawn from the arguments while the model writes them.
*
* Per call: the streamed argument text (`tool-input-delta`) is accumulated, parsed as far as it goes
* (`parsePartialJson`) and handed to the preview; what it renders is written as a `partial` component
* frame — at most one every `throttleMs`, and only when it changed, so a long tree costs the stream
* (and every sink buffering it) a bounded number of snapshots rather than one per delta. Whatever the
* throttle held back is flushed when the arguments are complete (`tool-input-available`).
*
* Previews never fail the turn: a preview that throws is dropped for that call.
*/
export function previewToolInputs(inner, resolve, now = Date.now) {
const calls = new Map();
const shown = new Map();
async function emit(id, state, input, done) {
let rendered;
try {
const parsed = done && input !== undefined
? { value: input, complete: true, isOpen: () => false, pendingMember: () => undefined }
: parsePartialJson(state.text);
if (parsed === undefined)
return;
rendered = state.preview.render({
value: parsed.value,
done: done || parsed.complete,
isOpen: parsed.isOpen,
pendingMember: parsed.pendingMember,
});
}
catch {
rendered = null;
}
if (rendered === undefined)
return;
const uiId = previewUiId(id);
if (rendered === null) {
state.stopped = true;
const standing = shown.get(uiId);
if (standing !== undefined) {
shown.delete(uiId);
await inner.write(withdrawPreviewFrame(standing));
}
return;
}
const props = JSON.stringify(rendered.props);
if (props === state.lastProps && rendered.component === state.component)
return;
state.lastProps = props;
state.component = rendered.component;
state.lastAt = now();
shown.set(uiId, { id: uiId, component: rendered.component, toolCallId: id });
await inner.write({
t: 'component',
name: rendered.component,
data: JSON.parse(props),
id: uiId,
toolCallId: id,
...(rendered.version !== undefined ? { version: rendered.version } : {}),
partial: true,
});
}
return {
writer: {
async write(frame) {
await inner.write(frame);
if (frame.t !== 'event')
return;
const event = frame.event;
if (event.kind === 'tool-input-start') {
if (calls.has(event.id))
return;
let preview;
try {
preview = await resolve(event.name, event.id);
}
catch {
preview = undefined;
}
if (preview !== undefined) {
calls.set(event.id, {
preview,
text: '',
lastAt: undefined,
lastProps: undefined,
component: undefined,
stopped: false,
});
}
return;
}
if (event.kind === 'tool-input-delta') {
const state = calls.get(event.id);
if (state === undefined || state.stopped)
return;
state.text += event.delta;
const throttle = state.preview.throttleMs ?? DEFAULT_PREVIEW_THROTTLE_MS;
if (state.lastAt !== undefined && now() - state.lastAt < throttle)
return;
await emit(event.id, state, undefined, false);
return;
}
if (event.kind === 'tool-input-available') {
const state = calls.get(event.id);
if (state === undefined)
return;
calls.delete(event.id);
if (state.stopped)
return;
// The whole input, at once: what the throttle held back, and the closing brackets.
await emit(event.id, state, event.input, true);
}
},
end: () => inner.end(),
},
shown: () => [...shown.values()],
};
}
//# sourceMappingURL=tool-input-preview.js.map