UNPKG

@adonis-agora/agent

Version:

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

249 lines • 12 kB
import { type A2aActionPolicy, type A2aTurnOutcome, type A2aTurnService } from '../a2a/turn.js'; import type { Actor } from '../types.js'; import type { PoppyPrincipal } from './auth.js'; import { type PoppyEvent, type PoppyEventMessage, type PoppyResponder, type PoppySendBody, type PoppySender, type PoppyStatus } from './protocol.js'; import { type PoppyConversationRecord, type PoppyStore } from './store.js'; /** What the hooks are told about a conversation. */ export interface PoppyHookContext { conversationId: string; conversation: PoppyConversationRecord; /** Who the conversation's turns run as (its latest request); `null` before any. */ actor: Actor | null; } /** * What {@link PoppyHandoffHooks.request} decided: * - `'queued'` — someone will join: call {@link PoppyConversations.humanJoined} when they do, or * {@link PoppyConversations.handoffUnavailable} if nobody can. * - `'joined'` — a person took it now. * - `{ unavailable }` — nobody can; the text is what the Company Agent says (§7.9). */ export type PoppyHandoffDecision = 'queued' | 'joined' | { unavailable: string; }; /** * How the app brings a person in (§7.9). Without these, a handoff is answered by the Company Agent * saying no one is available — the conversation stays with the agent. */ export interface PoppyHandoffHooks { /** A person was asked for — by the Personal Agent (`requested`) or by the app (`company`). */ request(ctx: PoppyHookContext & { by: 'requested' | 'company'; }): PoppyHandoffDecision | Promise<PoppyHandoffDecision>; /** * A message from the Personal Agent while a person handles the conversation or is being found — * the agent does not answer those. Route it to your support tool. */ message?(ctx: PoppyHookContext & { message: PoppyEventMessage; }): void | Promise<void>; /** The conversation closed while a person was handling it or being found (§7.12). */ cancel?(ctx: PoppyHookContext): void | Promise<void>; } /** Notified when the Personal Agent opens or closes a Direct Conversation (§7.10). */ export interface PoppyDirectHooks { opened?(ctx: PoppyHookContext & { parentId: string; }): void | Promise<void>; closed?(ctx: PoppyHookContext & { parentId: string; }): void | Promise<void>; } /** What the Company Agent says on its own. */ export interface PoppyTexts { /** A handoff with no one to take it (§7.9). */ handoffUnavailable: string; /** A turn that failed before saying anything. */ failed: string; /** Asked in a Direct Conversation alongside an `authorization` event (§7.11 SHOULD). */ signIn: string; /** An action that ran — `{title}` is the app's confirmation title (default {@link formatPoppyAction}). */ actionSucceeded: string; /** An approved action that did not run. `{title}`, `{error}`. */ actionFailed: string; /** An action the policy refused. `{title}`. */ actionRejected: string; } export declare const DEFAULT_POPPY_TEXTS: PoppyTexts; /** An action of the turn, as the reply's `data.actions` carries it. */ export type PoppyActionOutcome = A2aTurnOutcome; /** * Words an action for the reply's text (§7: the message must make sense without `data`). Return * `null` to leave it out. Default {@link formatPoppyAction}. */ export type PoppyActionFormatter = (action: PoppyActionOutcome, texts: PoppyTexts) => string | null | undefined; /** * Default wording: the status line from `texts` with the confirmation title (a question like * "Log this dose?" loses its `?`), then the confirmation detail. */ export declare function formatPoppyAction(action: PoppyActionOutcome, texts: PoppyTexts): string; export interface PoppyConversationsOptions { store: PoppyStore; /** What runs a turn — `AgentService`, or an app's adapter (the A2A surface's contract). */ service: A2aTurnService & { drive?(runId: string): Promise<void>; }; /** The roles a tool declares, for matching the token's scopes. */ toolRoles: (toolName: string) => string[] | undefined; /** The registered agent that answers. Omitted → the agent provider's default. */ agentName?: string; /** What happens to `action` calls that park for approval. Default `'approve-delegated'`. */ actions?: A2aActionPolicy; /** Give up on a turn after this many ms. Default 120 000. */ timeoutMs?: number; /** Drive a durable run left pending, in this process (see `AgentService.drive`). Default `true`. */ drive?: boolean; /** Map a principal without an actor to the app's actor. Default: `poppy:<hash>`. */ actorFor?: (principal: PoppyPrincipal, defaults: Actor) => Actor | Promise<Actor>; /** Words each action of a reply. Default {@link formatPoppyAction}. */ formatAction?: PoppyActionFormatter; handoff?: PoppyHandoffHooks; direct?: PoppyDirectHooks; texts?: Partial<PoppyTexts>; /** Keep events this long, then prune them (a cursor there is `cursor_expired`). Default: forever. */ retentionMs?: number; /** How often a waiting read looks for events another replica appended. Default 1000 ms. */ pollMs?: number; /** Logs what fails outside a request (a turn, a hook). */ onError?: (error: unknown, context: string) => void; } /** What a read answers (§7.5). */ export interface PoppyReadResult { conversation_id: string; events: PoppyEvent[]; cursor: string | null; has_more: boolean; status: PoppyStatus; responder: PoppyResponder; } /** What a stream yields: a stored event, or a provisional piece of text (§7.6). */ export type PoppyStreamItem = { kind: 'event'; event: PoppyEvent; } | { kind: 'delta'; messageId: string; text: string; } | { kind: 'state'; status: PoppyStatus; }; /** The actor of a principal the resolver gave none: stable per (Personal Agent, its User). */ export declare function poppyActorId(clientId: string, userId: string): string; /** * Poppy conversations (§7) over the agent runtime: the state machine, the event log and the turns. * HTTP-free — `createPoppyHandler` puts the protocol's requests on it, and the app drives the * Company side through it (a person joining, their messages, `user_requested`). * * A conversation is an agent thread owned by the principal's actor. A user message is appended to * the event log, and the log is the turn queue: a pump per conversation (one replica at a time, * by lease) answers the messages after `turnSeq` in one turn, streams its text as `text-delta`, * then appends the reply as a `message` event. A replica that finds a conversation `working` with * no pump picks it up, so a reply survives the process that started it. * * @experimental Tracks Personal Agent Protocol Draft 0.1 (https://personalagentprotocol.org/docs/spec), * a spec still in development: this API WILL change as the spec evolves — possibly in breaking * ways, outside semver majors while it is a draft. */ export declare class PoppyConversations { #private; private readonly options; constructor(options: PoppyConversationsOptions); /** * The actor a principal's turns run as: the resolver's (`toActor`), else the app's `actorFor`, * else — signed in — the ACCOUNT, `{ id: accountId }`, the way a PACT delegation runs as the * account; signed out, `poppy:<hash>` (stable per Personal Agent and User, never an account). * It ALWAYS carries `personal_agent` (what puts it behind `personalAgentGate`) and one * `scope:<id>` role per account scope of a signed-in token — the roles a PACT delegation gives, * so a tool written for personal agents gates both surfaces alike. */ actorOf(principal: PoppyPrincipal): Promise<Actor>; /** `POST {endpoint}` — start a conversation (or a Direct Conversation) with its first message. */ start(principal: PoppyPrincipal, body: PoppySendBody): Promise<{ status: number; body: Record<string, unknown>; conversationId: string; }>; /** `POST {endpoint}/{id}/messages` — another message (§7.3). */ send(principal: PoppyPrincipal, conversationId: string, body: PoppySendBody): Promise<{ status: number; body: Record<string, unknown>; }>; /** `GET {endpoint}/{id}/events` without streaming — read (and wait for) events (§7.5). */ read(principal: PoppyPrincipal, conversationId: string, options: { cursor?: string; waitMs?: number; }): Promise<PoppyReadResult>; /** A read of a conversation already checked — also what a POST with `wait` answers (§7.3). */ readOwned(conversation: PoppyConversationRecord, options: { cursor?: string; waitMs?: number; }): Promise<PoppyReadResult>; /** Check a conversation is the principal's, for a caller that reads it afterwards. */ owned(principal: PoppyPrincipal, conversationId: string): Promise<PoppyConversationRecord>; /** * `GET {endpoint}/{id}/events` as SSE (§7.6): the events after `cursor`, then new ones as they * come, with `text-delta` pieces of a reply being written in this process. Ends when `signal` * aborts, or once the conversation is closed and every event was sent. */ stream(conversation: PoppyConversationRecord, cursor: string | undefined, signal: AbortSignal): AsyncGenerator<PoppyStreamItem>; /** `POST {endpoint}/{id}/handoff` — ask for a person at the Company (§7.9). */ requestHandoff(principal: PoppyPrincipal, conversationId: string): Promise<{ status: PoppyStatus; responder: PoppyResponder; }>; /** `POST {endpoint}/{id}/close` — close the conversation (§7.12). */ closeByAgent(principal: PoppyPrincipal, conversationId: string): Promise<{ status: PoppyStatus; responder: PoppyResponder; }>; /** Look a conversation up — for the app's own routing (a support tool, an admin page). */ get(conversationId: string): Promise<PoppyConversationRecord | null>; /** * Close a conversation (§7.12): its open Direct Conversation first, a pending handoff is * cancelled, and a `closed` state event added. Nothing it started is undone. Idempotent. */ close(conversationId: string): Promise<{ status: PoppyStatus; responder: PoppyResponder; }>; /** The Company Agent hands off to a person on its own (§7.9) — e.g. from one of your tools. */ handoff(conversationId: string): Promise<{ status: PoppyStatus; responder: PoppyResponder; }>; /** A person at the Company joined: `responder` becomes `human` (§7.7). */ humanJoined(conversationId: string): Promise<{ status: PoppyStatus; responder: PoppyResponder; }>; /** The person left: the Company Agent answers again. */ humanLeft(conversationId: string): Promise<{ status: PoppyStatus; responder: PoppyResponder; }>; /** * No one could take a queued handoff: the Company Agent says why, and the conversation goes back * to `idle` with the agent (§7.9). */ handoffUnavailable(conversationId: string, reason?: string): Promise<{ status: PoppyStatus; responder: PoppyResponder; }>; /** * Add a Company message — a person's (`sender: 'human'`, while they handle the conversation) or * an automated one. Answers the message's id. */ postMessage(conversationId: string, message: { text?: string; data?: Record<string, unknown>; sender: PoppySender; }): Promise<string>; /** * Ask to talk with the User directly (§7.10): a `user_requested` event. Only the Personal Agent * opens the Direct Conversation, once the User agrees. */ requestUser(conversationId: string, reason: string): Promise<void>; /** Stop pumping and waiting in this process (shutdown). */ shutdown(): void; } //# sourceMappingURL=conversations.d.ts.map