UNPKG

@adonis-agora/agent

Version:

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

120 lines • 5.6 kB
/** * Recognizes complete, explicit decision messages. This grants no authority: callers must * resolve the authenticated actor and use the normal scoped decision service with via=text. * Never feed model output here or consume an unmatched message as an approval. */ export function resolveTextActionProposalDecision(text, proposals, vocabulary) { const command = parseTextActionProposalCommand(text, vocabulary); if (command.status === 'unmatched') return command; const { decision, remember } = command; const pending = proposals.filter((proposal) => proposal.decision === 'pending'); const explicitId = command.proposalId; if (explicitId !== undefined) { if (!pending.some((proposal) => proposal.id === explicitId)) return { status: 'unmatched' }; return { status: 'decision', proposalId: explicitId, decision, remember }; } if (pending.length === 0) return { status: 'unmatched' }; if (pending.length > 1) return { status: 'ambiguous', proposalIds: pending.map(({ id }) => id) }; const candidate = pending[0]; if (!candidate) return { status: 'unmatched' }; return { status: 'decision', proposalId: candidate.id, decision, remember }; } /** English. Portuguese ships as {@link ptBrActionProposalText}; any other language via config. */ export const DEFAULT_TEXT_ACTION_PROPOSAL_VOCABULARY = { approve: ['yes', 'confirm', 'approve', 'approved', 'ok'], reject: ['no', 'cancel', 'reject', 'deny'], remember: ['always in this conversation'], }; export const DEFAULT_TEXT_ACTION_PROPOSAL_REPLIES = { approved: 'Proposal approved and queued to run.', rejected: 'Proposal rejected; nothing was run.', expired: 'The proposal expired; nothing was run.', unchanged: 'This proposal could not be changed; refresh the list to see where it stands.', ambiguous: (ids) => `Which proposal? Reply confirm #ID or cancel #ID: ${ids.join(', ')}`, tooMany: 'There are several proposals. Confirm or reject one with an explicit #ID.', }; /** The reply to a decision the store answered with `result`. */ export function textActionProposalReply(result, decision, replies = DEFAULT_TEXT_ACTION_PROPOSAL_REPLIES) { return result.status === 'expired' || result.proposal?.decision === 'expired' ? replies.expired : result.status === 'applied' || result.status === 'unchanged' ? decision === 'approved' ? replies.approved : replies.rejected : replies.unchanged; } const escapeRegExp = (word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const alternatives = (words) => words .map((word) => word.trim()) .filter((word) => word.length > 0) .map((word) => escapeRegExp(word).replace(/\s+/g, '\\s+')) .join('|'); const fold = (word) => word.trim().toLowerCase(); /** Parse exact consent syntax only. Authentication and target lookup remain the caller's responsibility. */ export function parseTextActionProposalCommand(text, vocabulary = DEFAULT_TEXT_ACTION_PROPOSAL_VOCABULARY) { if (/[\r\n]/u.test(text)) return { status: 'unmatched' }; const verbs = alternatives([...vocabulary.approve, ...vocabulary.reject]); if (verbs.length === 0) return { status: 'unmatched' }; const remember = alternatives(vocabulary.remember); const match = new RegExp(`^(${verbs})${remember.length > 0 ? `(?: (${remember}))?` : '()'}(?: #([^\\s]+?))?[.!]?$`, 'iu').exec(text.trim()); if (!match) return { status: 'unmatched' }; const verb = match[1] === undefined ? undefined : fold(match[1]); if (verb === undefined) return { status: 'unmatched' }; // A word on both lists is read as a refusal: never infer consent from an ambiguous word. const decision = vocabulary.reject.some((word) => fold(word) === verb) ? 'rejected' : 'approved'; const remembers = match[2] !== undefined && match[2] !== ''; if (remembers && decision !== 'approved') return { status: 'unmatched' }; return { status: 'command', decision, remember: remembers, ...(match[3] !== undefined ? { proposalId: match[3] } : {}), }; } /** * Brazilian Portuguese text decisions — `config/agent.ts`: `actionProposalText: ptBrActionProposalText`. * Commands in Portuguese (English ones still work), replies in Portuguese. */ export const ptBrActionProposalText = { vocabulary: { language: 'pt-BR', approve: [ 'sim', 'confirmo', 'confirmar', 'aprovar', 'aprovo', 'pode', ...DEFAULT_TEXT_ACTION_PROPOSAL_VOCABULARY.approve, ], reject: [ 'não', 'nao', 'cancelar', 'cancela', 'rejeitar', 'rejeito', ...DEFAULT_TEXT_ACTION_PROPOSAL_VOCABULARY.reject, ], remember: ['sempre nesta conversa', ...DEFAULT_TEXT_ACTION_PROPOSAL_VOCABULARY.remember], }, replies: { approved: 'Confirmado! Já estou cuidando disso.', rejected: 'Tudo bem, cancelado. Nada foi feito.', expired: 'Esse pedido expirou; nada foi feito.', unchanged: 'Não foi possível alterar esta proposta; atualize a lista para consultar seu estado.', ambiguous: (ids) => `Qual proposta? Responda confirmar #ID ou cancelar #ID: ${ids.join(', ')}`, tooMany: 'Há várias propostas. Confirme ou rejeite usando #ID explícito.', }, }; //# sourceMappingURL=action-proposal-text.js.map