@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
249 lines • 12 kB
TypeScript
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