@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
TypeScript
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