UNPKG

@adonis-agora/agent

Version:

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

207 lines • 9.77 kB
import type { LucidClientLike, LucidRawRunner } from '../stores/lucid.js'; import type { Actor } from '../types.js'; import type { PoppyContext, PoppyEvent, PoppyEventBody, PoppyResponder, PoppyStatus } from './protocol.js'; /** * A Poppy conversation as this surface keeps it. It belongs to a Personal Agent (`clientId`) and * one of its Users (`userId`); once it used an account here, to that account too (`accountRef`, * §7.2). The agent side of it is an agent thread (`threadId`) — a Direct Conversation shares its * parent's thread, which is how the Company Agent knows what was said in either (§7.10). */ export interface PoppyConversationRecord { id: string; clientId: string; userId: string; accountRef: string | null; agentName: string; threadId: string | null; /** Set on a Direct Conversation: the conversation it was opened from. */ parentId: string | null; /** On a parent: its open Direct Conversation. */ openDirectId: string | null; status: PoppyStatus; responder: PoppyResponder; /** The User's context as last shared — fields a message leaves out keep their value (§7.4). */ context: PoppyContext; /** The run whose reply is being written, while there is one. */ activeRunId: string | null; /** * The position of the last message event a turn has taken up. The event log IS the turn queue: * user messages after it are what the next turn answers — on whichever replica picks it up. */ turnSeq: number; /** Who the latest request came as — what the next turn runs as. */ grant: PoppyGrant | null; /** Events at or below this sequence number were pruned: a cursor there is `cursor_expired`. */ prunedSeq: number; createdAt: number; updatedAt: number; } /** The identity a conversation's turns run as: that of its latest request (§7.11). */ export interface PoppyGrant { actor: Actor; /** The token's account scopes; empty when signed out. */ scopes: string[]; signedIn: boolean; } export type PoppyConversationPatch = Partial<Pick<PoppyConversationRecord, 'accountRef' | 'threadId' | 'openDirectId' | 'status' | 'responder' | 'context' | 'activeRunId' | 'turnSeq' | 'grant' | 'prunedSeq'>>; /** An event with its position in the conversation. */ export interface StoredPoppyEvent { seq: number; event: PoppyEvent; } /** What an accepted message keeps for its retries (§7.3). */ export interface PoppyMessageReceipt { fingerprint: string; conversationId: string; /** The HTTP status and body the first delivery got. */ response: { status: number; body: Record<string, unknown>; }; } export type PoppyMessageClaim = { status: 'claimed'; } | ({ status: 'existing'; } & PoppyMessageReceipt); /** * Where conversations, their events and message receipts live. Events are append-only and read by * position, so a cursor replays exactly what came after it, on any replica. * * @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 interface PoppyStore { createConversation(record: PoppyConversationRecord): Promise<void>; getConversation(id: string): Promise<PoppyConversationRecord | null>; updateConversation(id: string, patch: PoppyConversationPatch): Promise<void>; /** * Append an event, numbered after the conversation's last. With `key`, at most one event per key * is ever appended: a second append answers the first event with `created: false` — what keeps a * re-read turn from writing its reply twice. */ appendEvent(conversationId: string, body: PoppyEventBody, options?: { key?: string; }): Promise<{ event: StoredPoppyEvent; created: boolean; }>; /** Events after `afterSeq`, in order, at most `limit`. */ listEvents(conversationId: string, afterSeq: number, limit: number): Promise<StoredPoppyEvent[]>; /** The position of an event, `null` when the conversation has no such event (any more). */ findEventSeq(conversationId: string, eventId: string): Promise<number | null>; /** Drop the events created before `before` (epoch ms), keeping at least the last one. */ pruneEvents(conversationId: string, before: number): Promise<void>; /** Claim `messageId` for `owner` — atomically, so two deliveries of one message are one. */ claimMessage(owner: string, messageId: string, receipt: PoppyMessageReceipt): Promise<PoppyMessageClaim>; /** Drop a claim whose message could not be taken, so its retry is processed afresh. */ releaseMessage(owner: string, messageId: string): Promise<void>; /** * Take (or renew) the right to write the reply of the conversation's active run for `ttlMs` — * `false` while another holder's lease is live. What lets any replica pick up a reply whose * writer died, without two writing it at once. */ leaseReader(conversationId: string, holder: string, ttlMs: number): Promise<boolean>; releaseReader(conversationId: string, holder: string): Promise<void>; } /** `evt_<seq in base 36>_<random>`: unique, and its position survives the event being pruned. */ export declare function newEventId(seq: number): string; /** The position an event id was minted at, or `null` for an id this surface never minted. */ export declare function eventIdSeq(id: string): number | null; /** Process-local {@link PoppyStore} — tests and single-instance dev. */ export declare class InMemoryPoppyStore implements PoppyStore { #private; createConversation(record: PoppyConversationRecord): Promise<void>; getConversation(id: string): Promise<PoppyConversationRecord | null>; updateConversation(id: string, patch: PoppyConversationPatch): Promise<void>; appendEvent(conversationId: string, body: PoppyEventBody, options?: { key?: string; }): Promise<{ event: { seq: number; event: PoppyEvent; }; created: boolean; }>; listEvents(conversationId: string, afterSeq: number, limit: number): Promise<{ seq: number; event: PoppyEvent; }[]>; findEventSeq(conversationId: string, eventId: string): Promise<number | null>; pruneEvents(conversationId: string, before: number): Promise<void>; claimMessage(owner: string, messageId: string, receipt: PoppyMessageReceipt): Promise<{ fingerprint: string; conversationId: string; /** The HTTP status and body the first delivery got. */ response: { status: number; body: Record<string, unknown>; }; status: 'existing'; } | { status: 'claimed'; }>; releaseMessage(owner: string, messageId: string): Promise<void>; leaseReader(conversationId: string, holder: string, ttlMs: number): Promise<boolean>; releaseReader(conversationId: string, holder: string): Promise<void>; } export declare const POPPY_TABLES: { readonly conversations: 'agent_poppy_conversation'; readonly events: 'agent_poppy_event'; readonly messages: 'agent_poppy_message'; }; /** * `CREATE TABLE IF NOT EXISTS` DDL for the Poppy tables — the agent tables' portable dialect * (quoted identifiers, epoch-ms `BIGINT`, `TEXT` JSON; `forDialect` adapts it to MySQL). An event's * primary key is its position, so two appends racing for one position cannot both win; a message's * is its (owner, id), the idempotency lock of §7.3. */ export declare function poppyTableStatements(): string[]; /** Create the Poppy tables if missing. Idempotent. */ export declare function ensurePoppyTables(db: LucidRawRunner): Promise<void>; export interface LucidPoppyStoreOptions { /** Create the tables on first use (`CREATE TABLE IF NOT EXISTS`). Default `true`. */ autoCreateTables?: boolean; } /** {@link PoppyStore} over Lucid (any of SQLite / Postgres / MySQL). */ export declare class LucidPoppyStore implements PoppyStore { #private; private readonly db; constructor(db: LucidClientLike & LucidRawRunner, options?: LucidPoppyStoreOptions); /** Provision the tables (once). A failure is retried on the next call. */ ready(): Promise<void>; createConversation(record: PoppyConversationRecord): Promise<void>; getConversation(id: string): Promise<PoppyConversationRecord | null>; updateConversation(id: string, patch: PoppyConversationPatch): Promise<void>; appendEvent(conversationId: string, body: PoppyEventBody, options?: { key?: string; }): Promise<{ event: { seq: number; event: PoppyEvent; }; created: boolean; }>; listEvents(conversationId: string, afterSeq: number, limit: number): Promise<{ seq: number; event: PoppyEvent; }[]>; findEventSeq(conversationId: string, eventId: string): Promise<number | null>; pruneEvents(conversationId: string, before: number): Promise<void>; claimMessage(owner: string, messageId: string, receipt: PoppyMessageReceipt): Promise<{ status: 'claimed'; fingerprint?: never; conversationId?: never; response?: never; } | { status: 'existing'; fingerprint: string; conversationId: string; response: PoppyMessageReceipt['response']; }>; releaseMessage(owner: string, messageId: string): Promise<void>; leaseReader(conversationId: string, holder: string, ttlMs: number): Promise<boolean>; releaseReader(conversationId: string, holder: string): Promise<void>; } //# sourceMappingURL=store.d.ts.map