@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
JavaScript
/**
* 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