@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
93 lines • 3.69 kB
JavaScript
/**
* A {@link RunSampleSource} over what the agent already persisted, read through
* {@link AgentGovernanceQueries} alone. Works against any adapter that implements it (Lucid, the
* in-memory one) — this module adds no tables of its own on the read side.
*
* The prompt and the answer come off `runDetail`, whose messages are the ones stamped with this
* run's own `run_id`. There is no transcript join and no time-window heuristic: a run that wrote no
* message has empty text, and no other run's answer can ever be handed to it.
*
* One seam remains, and it is the read-model's rather than this class's: a run whose messages were
* written before message-level run stamping reads as empty text. A rule-based scorer over tool
* outcomes still works on it; a judge over the answer has nothing to read.
*/
export class GovernanceRunSampleSource {
queries;
constructor(queries) {
this.queries = queries;
}
async listRuns(query) {
const filter = {
...(query.actorRef !== undefined ? { actor: query.actorRef } : {}),
...(query.agentName !== undefined ? { agent: query.agentName } : {}),
...(query.status !== undefined ? { status: query.status } : {}),
...(query.fromDay !== undefined ? { from: query.fromDay } : {}),
...(query.toDay !== undefined ? { to: query.toDay } : {}),
};
// The read-model clamps a page to 200, so a batch bigger than that has to walk the cursor rather
// than ask for the whole thing and quietly get a fifth of it.
const runs = [];
let cursor = null;
while (runs.length < query.limit) {
const page = await this.queries.listRuns({
...filter,
first: query.limit - runs.length,
...(cursor !== null ? { after: cursor } : {}),
});
for (const row of page.items) {
const detail = await this.queries.runDetail(row.runId);
if (detail !== null) {
runs.push(toScorableRun(detail));
}
}
cursor = page.nextCursor;
if (cursor === null || page.items.length === 0) {
break;
}
}
return runs;
}
async getRun(runId) {
const detail = await this.queries.runDetail(runId);
return detail === null ? null : toScorableRun(detail);
}
}
/**
* Flatten a run's trace into the unit a scorer judges. A turn can persist several assistant messages
* (one per model step); the LAST is the answer and the earlier ones are the steps that called tools,
* so both sides reduce by taking the last of their role.
*/
function toScorableRun(detail) {
const { run } = detail;
let input = '';
let output = '';
for (const message of detail.messages) {
if (message.role === 'user') {
input = message.content;
}
else if (message.role === 'assistant') {
output = message.content;
}
}
return {
runId: run.runId,
threadId: run.threadId,
actorRef: run.actorRef,
agentName: run.agentName,
status: run.status,
input,
output,
toolCalls: detail.toolCalls.map((call) => ({
toolCallId: call.toolCallId,
toolName: call.toolName,
toolType: call.toolType,
status: call.status,
executionMs: call.executionMs,
error: call.error,
})),
durationMs: run.durationMs,
error: run.error,
startedAt: run.startedAt,
};
}
//# sourceMappingURL=governance-sample-source.js.map