@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
88 lines • 3.87 kB
JavaScript
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* Watch the frames a model turn writes, to learn what the stream showed that the provider's result
* does not carry: the model's thinking (text and how long it took) and any pushed UI components.
*
* Derived from the FRAMES rather than asked of each provider, so every provider that streams
* `reasoning` gets it persisted without implementing anything. Thinking time is the sum of each burst
* of consecutive `reasoning` frames — from the first one to the next frame that is not reasoning (or
* the end of the turn) — which is what a reader watched as "thinking".
*
* Call it inside the step that runs the model, so the summary rides that step's journaled result and
* a replay reads the same numbers back instead of re-measuring a turn that is not happening. The same
* derivation as `observeTurnFrames` in `@dudousxd/nestjs-agent-core`, over typed frames instead of
* NDJSON bytes.
*/
export function observeTurnFrames(writer, now = Date.now) {
let reasoning = '';
let reasoningMs = 0;
let burstStartedAt;
const ui = new Map();
function closeBurst() {
if (burstStartedAt !== undefined) {
reasoningMs += Math.max(0, now() - burstStartedAt);
burstStartedAt = undefined;
}
}
function observe(frame) {
if (frame.t === 'event' && frame.event.kind === 'reasoning') {
burstStartedAt ??= now();
reasoning += frame.event.text;
return;
}
closeBurst();
// A preview of a call's streaming input is shown, never kept: the call's own push is.
if ((frame.t === 'component' && frame.partial === true) ||
(frame.t === 'event' && frame.event.kind === 'ui' && frame.event.partial === true)) {
return;
}
// A repeat id replaces the props but keeps the component where it first appeared, exactly as
// the client's data part does.
if (frame.t === 'event' && frame.event.kind === 'ui') {
const { kind: _kind, ...component } = frame.event;
ui.set(component.id, component);
}
else if (frame.t === 'component' && frame.id !== undefined) {
ui.set(frame.id, {
id: frame.id,
component: frame.name,
props: isRecord(frame.data) ? frame.data : { value: frame.data },
...(frame.toolCallId !== undefined ? { toolCallId: frame.toolCallId } : {}),
});
}
}
return {
writer: {
write(frame) {
// Observed before it is forwarded, so a burst closes at the moment the next frame was
// produced rather than whenever a slow sink got round to accepting it.
observe(frame);
return writer.write(frame);
},
end: () => writer.end(),
},
summary() {
closeBurst();
return {
...(reasoning.length > 0 ? { reasoning, reasoningMs: Math.round(reasoningMs) } : {}),
...(ui.size > 0 ? { ui: [...ui.values()] } : {}),
};
},
};
}
/** Fill what a provider did not report from what its frames showed. A provider's own value wins. */
export function withTurnFrames(result, frames) {
return {
...result,
...(result.reasoning === undefined && frames.reasoning !== undefined
? { reasoning: frames.reasoning }
: {}),
...(result.reasoningMs === undefined && frames.reasoningMs !== undefined
? { reasoningMs: frames.reasoningMs }
: {}),
...(result.ui === undefined && frames.ui !== undefined ? { ui: frames.ui } : {}),
};
}
//# sourceMappingURL=turn-frames.js.map