UNPKG

@adonis-agora/agent

Version:

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

169 lines • 9.02 kB
import type { AgentRunner } from './spi/agent-runner.js'; import type { AgentStore } from './spi/agent-store.js'; import type { AttachmentStagingStore } from './spi/attachment-staging.js'; import { type ChatQueueState, type ChatQueueStore, type QueuedMessage, type QueuePauseReason } from './spi/chat-queue.js'; import type { QuotaProvider } from './spi/quota-provider.js'; import type { TokenStreamSink } from './spi/token-stream-sink.js'; import type { AgentStreamEvent } from './stream-events.js'; import type { Actor, AgentRunInput, Persona } from './types.js'; /** How the run that held a thread ended. */ export type QueueSettleOutcome = 'completed' | 'failed' | 'cancelled'; /** The next queued message a settling run handed the thread to — the caller starts it. */ export interface QueuedTurn { /** The run id the thread is already claimed for: the queued message's own id. */ runId: string; input: AgentRunInput; messageId: string; } /** * What {@link ChatQueueService.plan} decided. Plain data, so a durable workflow can journal it in a * `localStep` and start `next` itself (`ctx.startChild`) — replay-safe, and never twice. */ export interface QueuePlan { next?: QueuedTurn; /** The `queue` frame to write into the settling run's stream, before its terminal. */ frame?: AgentStreamEvent; } /** Starts a run under a pre-claimed id — a runner's own `start`, bound. */ export type QueuedTurnStarter = (input: AgentRunInput, runId: string) => Promise<unknown>; /** What {@link ChatQueueService} needs beyond the store and the sink — all optional. */ export interface ChatQueueServiceOptions { /** The budget a queued message's actor is checked against as it starts. Omitted → not checked. */ quota?: QuotaProvider; /** Re-mints a queued message's attachment urls as it starts (they may have expired waiting). */ attachments?: AttachmentStagingStore; /** * The agent and persona a queued message that carries NO persona starts as — one queued before * personas were persisted, or by a host that set none: an agent name a persona took over, then the * thread's pin, then the agent's default, exactly as a send resolves them. Never refused: nobody is * waiting on a 400. A message that carries its persona starts under it as queued. Omitted → such a * message starts as it was queued. */ resolveTarget?: (message: QueuedMessage) => Promise<{ agentName?: string; persona?: string; }>; /** * @deprecated Use {@link resolveTarget}. The persona a turn of `agentName` runs with, consulted * only for a message that carries none when `resolveTarget` is not given. */ personaFor?: (agentName: string | undefined) => Persona | undefined; } /** * The thread-level message queue: messages a person sends while a turn is running wait here, and * the next one starts as soon as the running turn settles. A port of `@dudousxd/nestjs-agent`'s * `ChatQueueService`; the wire is its `docs/stream-protocol.md`, *Message queue*. * * Everything that decides WHICH run holds a thread goes through the store's compare-and-set * admission (`claimActiveStream` / `releaseActiveStream`), so two processes that both try to start * the next turn — a settling run on a worker and a send on a web pod — cannot both win. The next * run's id is the queued message's own id, which makes every step here idempotent: a retried claim * for the same message succeeds again, and a durable start under the same id is a no-op. * * Policy, per outcome of the run that held the thread: * - `completed` → the head starts. * - `failed` → the queue pauses (`run_failed`), keeping every message. * - `cancelled` → the queue pauses (`cancelled`), unless the head was queued by an interrupt — the * cancel was made for it, so it starts. * A paused queue starts nothing until it is resumed. A head whose actor is over quota pauses it * (`quota_exceeded`) instead of starting. */ export declare class ChatQueueService { private readonly store; private readonly sink; private readonly options; /** * Runs this process claimed a thread for and has not handed to a runner yet. A holder in here is * alive whatever the runner says — it simply has not registered the run yet. */ private readonly starting; constructor(store: AgentStore, sink: TokenStreamSink, options?: ChatQueueServiceOptions); /** Whether the bound store can hold a queue. Without one, a busy thread is not queued. */ get supported(): boolean; /** The bound store as a queue store; throws when it is not one (check {@link supported} first). */ queueStore(): AgentStore & ChatQueueStore; state(threadId: string): Promise<ChatQueueState>; /** * Whether `holder` is still running. A run this process is about to start is; otherwise the * runner decides, and a runner that cannot tell says yes. */ isAlive(holder: string, runner: AgentRunner): Promise<boolean>; /** * The run holding `threadId`, or `null` when it is free — a holder the runner reports dead counts * as free, and is returned as `stale` so the caller can claim over it. */ holder(threadId: string, runner: AgentRunner): Promise<{ live: string | null; stale: string | null; }>; /** * Decide what follows a TOP-LEVEL run that is settling on `threadId`, and move the thread there: * hand it to the next queued message (claimed and popped here, started by the caller), or pause * the queue, or release the thread. Call it BEFORE the run's terminal frame, so a client reading * the stream learns what comes next from the `queue` frame, and so the thread is already free * when the client sees the run end. * * A no-op returning `{}` on a store without a queue. */ plan(args: { threadId: string; /** The run that is settling; `null` when the thread is idle (a kick). */ runId: string | null; outcome: QueueSettleOutcome; error?: string; }): Promise<QueuePlan>; /** * {@link plan}, then start what it picked with `start`. What an in-process settle (the inline * runner, a durable cancel issued outside a workflow) calls; answers the frame to write. */ handoff(args: { threadId: string; runId: string; outcome: QueueSettleOutcome; error?: string; }, start: QueuedTurnStarter): Promise<AgentStreamEvent | undefined>; /** * Start the head of an IDLE thread's queue — after a send queued behind a run that turned out to * be gone, after a resume, after a stale holder. Does nothing while a live run holds the thread * (its settle drains) or while the queue is paused. Answers the started run's id, if any. */ kick(threadId: string, runner: AgentRunner): Promise<string | undefined>; /** * Start a turn the thread is already claimed for. On failure the message goes back to the head of * the queue, the queue pauses (`start_failed`), and the claim is released — nothing is lost and * nothing is left holding the thread. */ launch(threadId: string, next: QueuedTurn, start: QueuedTurnStarter): Promise<boolean>; /** Put a turn that never started back at the head of its queue, paused, and free the thread. */ restore(threadId: string, next: QueuedTurn, reason: QueuePauseReason, message: string): Promise<void>; /** A `queue` frame with the thread's current state. */ pausedFrame(threadId: string): Promise<AgentStreamEvent>; /** * Tell whoever is watching the thread that its queue changed: a `queue` frame into the stream of * the run holding it. Best-effort — a thread nobody is streaming has no one to tell, and the next * read of the thread carries the queue anyway. */ publish(threadId: string): Promise<ChatQueueState>; /** Write one agent-protocol event into `runId`'s stream. */ write(runId: string, frame: AgentStreamEvent): Promise<void>; /** Why `actor` may not start a turn now (the quota message), or `null`. */ quotaBlock(actor: Actor): Promise<string | null>; /** * The turn a queued message starts: everything it was queued with, today's date, and its * attachments' urls minted afresh — the ones stored at enqueue time may have expired while it * waited. An attachment the store no longer resolves keeps its stored url. */ private inputFor; /** The agent and persona a queued message starts as — see {@link ChatQueueServiceOptions.resolveTarget}. */ private targetFor; private freshAttachments; } /** * A thread's own turn — not a sub-agent's run on a subthread, which neither holds its thread's * queue nor answers to it. */ export declare function isThreadTurn(input: { sinkRunId?: unknown; parentRunId?: unknown; }): boolean; //# sourceMappingURL=chat-queue-service.d.ts.map