UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

159 lines • 8.07 kB
import { randomUUID } from 'node:crypto'; import { validateActionProposalExpiryBatch, validateActionProposalWorkerClaim, } from '../action-proposal-discovery.js'; import { claimActionProposalOutcome } from '../action-proposal-outcome.js'; import { actionProposalCreationMatches, actionProposalScopeMatches, initialActionProposal, snapshotActionProposal, transitionActionProposalClaim, transitionActionProposalDecision, transitionActionProposalLease, transitionActionProposalSettlement, transitionActionProposalSupersession, validateActionProposalCreation, validateActionProposalListQuery, } from '../action-proposal-transitions.js'; /** Reference single-process implementation; durable adapters use the same transitions under CAS. */ export class InMemoryActionProposalStore { rows = new Map(); clock; constructor(options = {}) { this.clock = options.clock ?? Date.now; } async createActionProposal(input) { validateActionProposalCreation(input); const row = this.rows.get(input.id); if (row) { if (!actionProposalScopeMatches(row, input)) return { status: 'conflict' }; return { status: actionProposalCreationMatches(row, input) ? 'unchanged' : 'conflict', proposal: snapshotActionProposal(row), }; } const proposal = initialActionProposal(input, this.clock()); this.rows.set(input.id, proposal); return { status: 'created', proposal: snapshotActionProposal(proposal) }; } async getActionProposal(scope, id) { const row = this.rows.get(id); return row && actionProposalScopeMatches(row, scope) ? snapshotActionProposal(row) : null; } async listActionProposals(scope, query = {}) { const limit = validateActionProposalListQuery(query); return [...this.rows.values()] .filter((row) => actionProposalScopeMatches(row, scope) && (query.decision === undefined || row.decision === query.decision)) .filter((row) => query.after === undefined || row.createdAt > query.after.createdAt || (row.createdAt === query.after.createdAt && row.id > query.after.id)) .sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) .slice(0, limit) .map(snapshotActionProposal); } mutate(scope, id, transition) { const row = this.rows.get(id); if (!row || !actionProposalScopeMatches(row, scope)) return { status: 'not_found' }; const result = transition(row, this.clock()); if (result.proposal) this.rows.set(id, snapshotActionProposal(result.proposal)); return result; } async decideActionProposal(scope, id, command) { return this.mutate(scope, id, (row, now) => transitionActionProposalDecision(row, command, now)); } async claimActionProposal(scope, id, command) { return this.mutate(scope, id, (row, now) => transitionActionProposalClaim(row, command, now, randomUUID())); } async extendActionProposalLease(scope, id, command) { return this.mutate(scope, id, (row, now) => transitionActionProposalLease(row, command, now)); } async settleActionProposal(scope, id, command) { return this.mutate(scope, id, (row, now) => transitionActionProposalSettlement(row, command, now)); } async claimNextActionProposal(command) { const now = this.clock(); validateActionProposalWorkerClaim(command, now); const candidates = [...this.rows.values()] .filter((row) => row.decision === 'approved' && (row.execution?.status === 'queued' || (row.execution?.status === 'executing' && row.execution.lease !== null && now >= row.execution.lease.expiresAt))) .sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) .slice(0, 32); for (const candidate of candidates) { const claimed = await this.claimActionProposal(candidate, candidate.id, command); if (claimed.status === 'applied' && claimed.proposal) return claimed.proposal; } return null; } async expireActionProposals(command) { const now = this.clock(); validateActionProposalExpiryBatch(command, now); const candidates = [...this.rows.values()] .filter((row) => row.decision === 'pending' && row.expiresAt !== null && now >= row.expiresAt) .sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) .slice(0, command.limit); let expired = 0; for (const candidate of candidates) { const result = await this.decideActionProposal(candidate, candidate.id, { decision: 'expired', actorRef: 'system', via: 'expiry', }); if (result.status === 'applied') expired++; } return expired; } async claimNextActionProposalOutcome(command) { const now = this.clock(); validateActionProposalWorkerClaim(command, now); const candidates = [...this.rows.values()] .filter((row) => row.outcomeDelivery?.status === 'pending' && (!row.outcomeDelivery.lease || now >= row.outcomeDelivery.lease.expiresAt)) .sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) .slice(0, 32); for (const candidate of candidates) { const row = this.rows.get(candidate.id); if (!row) continue; const next = claimActionProposalOutcome(row, command, this.clock(), randomUUID()); if (!next?.outcome || !next.outcomeDelivery?.lease) continue; this.rows.set(next.id, next); return { outcome: snapshotActionProposal(next.outcome), lease: { outcomeId: next.outcome.id, token: next.outcomeDelivery.lease.token, generation: next.outcomeDelivery.generation, }, }; } return null; } async createReplacingActionProposal(input) { validateActionProposalCreation(input); const existing = this.rows.get(input.id); if (existing) return this.createActionProposal(input); const proposal = initialActionProposal(input, this.clock()); // Build every replacement snapshot before changing any row; no await exposes partial state. const replacements = []; if (input.replacementKey) for (const row of this.rows.values()) { if (row.decision !== 'pending' || !actionProposalScopeMatches(row, input) || row.toolName !== input.toolName || row.replacementKey !== input.replacementKey) continue; const result = transitionActionProposalSupersession(row, proposal, { replacementProposalId: proposal.id, actorRef: input.actorRef, via: 'supersession' }, this.clock()); if (result.proposal) replacements.push(result.proposal); } this.rows.set(proposal.id, proposal); for (const row of replacements) this.rows.set(row.id, row); return { status: 'created', proposal: snapshotActionProposal(proposal) }; } async supersedeActionProposal(scope, id, command) { const replacement = this.rows.get(command.replacementProposalId); if (!replacement || !actionProposalScopeMatches(replacement, scope)) return { status: 'not_found' }; return this.mutate(scope, id, (row, now) => transitionActionProposalSupersession(row, replacement, command, now)); } } //# sourceMappingURL=in-memory-action-proposal-store.js.map