UNPKG

@adonis-agora/agent

Version:

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

109 lines • 5.37 kB
/** * A connect finished: decide the `connect` call the turn parked on, so the conversation resumes * with the account connected — a scoped proposal decision under `actionApprovalMode: 'independent'`, * an approval of the parked call otherwise. A call already decided (the person tapped first) is left. */ export async function resumeAfterConnect(service, pending, /** * For a sign-in that finished after its card expired: the connection is stored all the same, and * the conversation is continued with a new turn ({@link PoppyConnectionsTexts.continueAfterConnect}) * instead of nothing happening. Without it, an expired card is left alone. */ late) { if (!pending.actor || !pending.threadId || !pending.toolCallId) return; const actor = pending.actor; const threadId = pending.threadId; if (service.actionApprovalMode() === 'independent') { const proposals = (await service.listActionProposals(actor, threadId)); const mine = proposals.filter((p) => p.originToolCallId === pending.toolCallId); const open = mine.find((p) => p.decision === 'pending'); if (open) { const decided = await service .decideActionProposal(actor, threadId, open.id, 'approved', {}, 'poppy-connect') .then((r) => r.status !== 'expired', () => false); if (decided) return; } // Approved already (the person tapped first) or turned down: nothing to add. if (mine.some((p) => p.decision === 'approved' || p.decision === 'rejected')) return; // Expired (or gone) before the sign-in finished: the account is connected anyway — go on. await continueLate(service, late, actor, threadId, pending, mine[0]?.executionContext); return; } if (!pending.runId) return; const state = await service.toolCallApproval(pending.toolCallId, pending.runId); if (state === null || state.status === 'pending_approval') { await service.approve(pending.runId, pending.toolCallId, { executedByRef: actor.id, via: 'poppy-connect', }); return; } if (state.status === 'expired') await continueLate(service, late, actor, threadId, pending); } async function continueLate(service, late, actor, threadId, pending, context) { if (!late || !service.send) return; const sent = await service.send({ actor, threadId, message: late.connections.texts.continueAfterConnect.replace(/\{company\}/g, pending.organization), ...(context?.agentName !== undefined ? { agentName: context.agentName } : {}), ...(context?.pageContext !== undefined ? { pageContext: context.pageContext } : {}), }, { textDecisions: false }); const runId = sent.runId; const address = context?.pageContext?.channel; if (runId !== undefined && late.follow && address !== null && typeof address === 'object') { await late.follow(address, { runId, threadId, actor }); } } /** * Independent approvals: deciding the `connect` proposal runs it, and its outcome is written to the * thread — but nothing asks the model to go on. Once that outcome is admitted (the * action-proposal worker's `onAdmitted`), this starts the next turn on the thread, in the person's * voice ({@link PoppyConnectionsTexts.continueAfterConnect}), so the model proceeds with what was * asked before the sign-in. A text channel's conversation gets that turn too (`follow`). * * Only for a connect this app's connect page completed (its pending connect is `connected`), and * only once: the outcome is admitted once. */ export async function continueAfterConnect(service, connections, outcome, options = {}) { if (outcome.toolName !== `${options.prefix ?? 'poppy_'}connect`) return null; if (outcome.executionStatus !== 'succeeded' || outcome.decision !== 'approved') return null; const result = outcome.result; if (result?.connected !== true) return null; const pending = await connections.pendingFor(outcome.originRunId, outcome.originToolCallId); if (pending === null || pending.status !== 'connected' || pending.actor === null) return null; const actor = pending.actor; const proposal = (await service.listActionProposals(actor, outcome.threadId)).find((p) => p.id === outcome.proposalId); const context = proposal?.executionContext; const company = typeof result.organization === 'string' ? result.organization : pending.organization; const sent = await service.send({ actor, threadId: outcome.threadId, message: connections.texts.continueAfterConnect.replace(/\{company\}/g, company), ...(context?.agentName !== undefined ? { agentName: context.agentName } : {}), ...(context?.pageContext !== undefined ? { pageContext: context.pageContext } : {}), }, { textDecisions: false }); const runId = sent.runId; if (runId === undefined) return null; const address = context?.pageContext?.channel; if (options.follow && address !== null && typeof address === 'object') { await options.follow(address, { runId, threadId: outcome.threadId, actor }); } return { runId }; } //# sourceMappingURL=resume.js.map