@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
105 lines • 4.62 kB
JavaScript
import { validateActionProposalWorkerClaim } from './action-proposal-discovery.js';
import { snapshotActionProposal } from './action-proposal-transitions.js';
export function attachActionProposalOutcome(row, now, ui = [], text) {
if (row.outcome)
return row;
const execution = row.execution;
const outcome = {
id: `${row.id}:outcome:1`,
proposalId: row.id,
outcomeVersion: 1,
tenantRef: row.tenantRef,
actorRef: row.actorRef,
threadId: row.threadId,
originRunId: row.originRunId,
originToolCallId: row.originToolCallId,
toolName: row.toolName,
decision: row.decision,
ui,
createdAt: now,
...(text !== undefined ? { text } : {}),
...(execution?.status === 'succeeded' || execution?.status === 'failed'
? { executionStatus: execution.status }
: {}),
...(execution && Object.hasOwn(execution, 'result') ? { result: execution.result } : {}),
...(execution?.error !== undefined
? { error: execution.error }
: row.decisionAudit?.reason !== undefined
? { error: row.decisionAudit.reason }
: {}),
};
return snapshotActionProposal({
...row,
outcome,
outcomeDelivery: { status: 'pending', generation: 0, lease: null },
});
}
export function claimActionProposalOutcome(row, command, now, token) {
validateActionProposalWorkerClaim(command, now);
const delivery = row.outcomeDelivery;
if (!row.outcome ||
!delivery ||
delivery.status !== 'pending' ||
(delivery.lease && now < delivery.lease.expiresAt))
return null;
const generation = delivery.generation + 1;
return snapshotActionProposal({
...row,
outcomeDelivery: {
...delivery,
generation,
lease: { token, generation, workerId: command.workerId, expiresAt: now + command.leaseMs },
},
});
}
export function actionProposalOutcomeFenceValid(row, command, now) {
if (!Number.isSafeInteger(now))
throw new RangeError('Invalid server clock');
if (typeof command.token !== 'string' ||
!command.token ||
!Number.isSafeInteger(command.generation) ||
command.generation < 1 ||
typeof command.outcomeId !== 'string' ||
!command.outcomeId)
throw new TypeError('Invalid outcome fence');
const delivery = row.outcomeDelivery;
return (row.outcome?.id === command.outcomeId &&
delivery?.status === 'pending' &&
delivery.generation === command.generation &&
delivery.lease?.token === command.token &&
delivery.lease.generation === command.generation &&
now < delivery.lease.expiresAt);
}
/**
* What happened, for the model (it reads the result from here) and the chat history. No proposal id:
* the structured outcome stored with the message carries it.
*/
function outcomeSummary(outcome) {
const tool = JSON.stringify(outcome.toolName);
if (outcome.executionStatus === 'succeeded')
return `Action ${tool} completed.${Object.hasOwn(outcome, 'result') ? ` Result: ${JSON.stringify(outcome.result)}` : ''}`;
if (outcome.executionStatus === 'failed')
return `Action ${tool} failed: ${JSON.stringify(outcome.error ?? 'Execution failed')}`;
return `Action ${tool} was ${outcome.decision} and was not executed.${outcome.error ? ` Reason: ${JSON.stringify(outcome.error)}` : ''}`;
}
export function actionProposalOutcomeScope(outcome) {
return { tenantRef: outcome.tenantRef, actorRef: outcome.actorRef, threadId: outcome.threadId };
}
/**
* Text a database column can hold: NUL and unpaired surrogates — which Postgres and SQLite refuse —
* are written as their JSON escapes, the way the JSON summary already carries them.
*/
function storableText(text) {
return text
.replaceAll(String.fromCharCode(0), '\\u0000')
.replace(/[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g, (char) => `\\u${char.charCodeAt(0).toString(16)}`);
}
/**
* The outcome message admitted to the thread. The tool's own presentation text (`present` /
* `emitUi`), when it has one, leads — what a person reads in the history; the summary follows.
*/
export function actionProposalOutcomeText(outcome) {
const text = outcome.text === undefined ? undefined : storableText(outcome.text).trim();
return text ? `${text}\n\n${outcomeSummary(outcome)}` : outcomeSummary(outcome);
}
//# sourceMappingURL=action-proposal-outcome.js.map