@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
615 lines • 32.4 kB
TypeScript
import type { ActionApprovalMode } from './action-proposal-receipt.js';
import { ActionProposalService } from './action-proposal-service.js';
import { type TextActionProposalVocabulary } from './action-proposal-text.js';
import type { AgentDepsFactory } from './agent-deps-factory.js';
import { type AttachmentLimits } from './attachment-limits.js';
import type { ChatQueueService } from './chat-queue-service.js';
import type { ActionProposalMutationResult } from './spi/action-proposal-store.js';
import type { AgentRunner } from './spi/agent-runner.js';
import type { AgentStore } from './spi/agent-store.js';
import { type ApprovalDecisionRef, type ToolCallApprovalState } from './spi/approval-policy.js';
import type { AttachmentRef, AttachmentStagingStore, StagedAttachment } from './spi/attachment-staging.js';
import type { BackgroundActorResolver } from './spi/background-actor-resolver.js';
import type { ChatQueueState } from './spi/chat-queue.js';
import { type ModelCatalog, type ModelCatalogView } from './spi/model-catalog.js';
import { type QuotaProvider, type QuotaReport } from './spi/quota-provider.js';
import type { StreamFrame } from './spi/token-stream-sink.js';
import { type LucidDatabaseLike } from './stores/lucid.js';
import type { ToolCatalogEntry } from './tool-presentation.js';
import type { Actor, AgentCatalogEntry, AgentHostContext, MessageAttachment, MessageFeedback, PageContext, Persona, PersonaCatalogEntry, ThreadDetail, ThreadSummary } from './types.js';
import type { UiCapabilities } from './ui-capabilities.js';
export interface ChatParams {
actor: Actor;
message: string;
threadId?: string;
agentName?: string;
/**
* Run this turn under one of the agent's personas, and pin it on the thread so later sends that
* name none keep it. Refused ({@link PersonaNotFoundError}, `400 persona_not_found`) unless the
* agent declares it. Omitted → the thread's pinned persona, else the agent's `defaultPersona`, else
* none.
*/
personaId?: string;
pageContext?: PageContext;
uiCapabilities?: UiCapabilities;
/**
* The channel this turn runs on — `web`, `mobile`, `whatsapp`… — over whatever its page context
* says. Stamped as `pageContext.channel`, which is what decides (`turnChannel`) which genui tools
* the turn is offered (`genui({ channels })`). Omitted → the page context's, else `web`.
*/
channel?: string;
/**
* Uploads to attach to this message, named by id (`POST <path>/attachments` answered it). The
* configured attachment store resolves each — the url the model fetches is never taken from here.
*/
attachments?: AttachmentRef[];
/**
* Run THIS turn on a catalog model instead of the thread's pinned one (or the provider default).
* Never stored on the thread — only `setThreadModel` (`PATCH <path>/threads/:id { model }`) pins.
* Refused ({@link ModelNotAllowedError}) unless the catalog lists it as available to this actor.
*/
model?: string;
/**
* Answer the thread's last user message again (the retry under the last answer): requires
* `threadId` ({@link RegenerateNeedsThreadError}), ignores `message`, stores no user message, drops
* the answer(s) after that user message and starts a new run.
*/
regenerate?: boolean;
/**
* When creating a thread (no `threadId`), start it transient — a scratch conversation left out of
* the thread list until {@link AgentService.promoteThread} keeps it. Ignored with a `threadId`.
*/
transient?: boolean;
/** What to do when the thread already has a turn running — see {@link ChatSendMode}. */
mode?: ChatSendMode;
/**
* When creating a thread (no `threadId`), ask the store to create it under this id — the id an
* AG-UI consumer named the conversation with. A store may ignore it (`CreateThreadInput.id`); the
* thread's real id is always the one on the result.
*/
newThreadId?: string;
/**
* The host's own facts about this send — where it came from, where its answer goes (see
* `AgentRunInput.hostContext`). Carried to the turn as sent, through the queue when it waits there;
* never shown to the model or to clients. Set by in-process callers, never from a request body.
*/
hostContext?: AgentHostContext;
/**
* When the person sent this message, when that is not "now": a channel's provider timestamp, a
* client that sends what it buffered offline. A `Date`, an ISO 8601 string or epoch milliseconds;
* stored with the message (`StoredMessage.sentAt`) and what `messageTimestamps` shows the model.
* Unreadable → ignored; in the future → now. Omitted → the message's `createdAt` stands for it
* (for a send that waits in the queue, the time it was queued).
*/
sentAt?: Date | string | number;
}
/** How {@link AgentService.send} treats one send. */
export interface ChatSendOptions {
/**
* Read a message that is a text decision ("yes", "no #ID") as one — decide the thread's pending
* proposal instead of starting a turn. Default `true`. `false` → always a turn: for a caller that
* decides by text itself, scoped to the proposals it showed (the text channels).
*/
textDecisions?: boolean;
}
/**
* What a send does when its thread already has a turn running:
* - `'auto'` (default) — run now when the thread is idle, else wait in the thread's queue.
* - `'queue'` — always wait in the queue, behind whatever is already waiting (it still starts at
* once when nothing is running and nothing is ahead of it).
* - `'interrupt'` — cancel the running turn and run this one next, ahead of the queue.
*/
export type ChatSendMode = 'auto' | 'queue' | 'interrupt';
/** A send that is waiting in its thread's queue instead of running. */
export interface QueuedSend {
threadId: string;
queued: true;
/** The queued message's id — also the run id it will start under. */
messageId: string;
/** 0-based place in the queue at the time it was queued (`0` → next). */
position: number;
queue: ChatQueueState;
/** The turn it started under, when it started straight away (an idle thread). */
runId?: string;
/** The run an interrupt cancelled to make room for it. */
interrupting?: string;
}
/** What {@link AgentService.send} did: started a turn, or queued the message. */
export interface ProposalDecisionSend {
threadId: string;
proposalDecision: ActionProposalMutationResult | {
status: 'ambiguous';
proposalIds: string[];
};
text: string;
queued?: undefined;
runId?: never;
}
export type ChatSendResult = ProposalDecisionSend | {
runId: string;
threadId: string;
queued?: undefined;
} | QueuedSend;
/**
* A queue request the service refuses — `status` and `code` are what the routes answer:
* `409 run_active` (a start-or-refuse {@link AgentService.chat}, or a regenerate, on a busy thread),
* `410` (a queued message that already started or was removed), `404` (no such queued message),
* `400` (a bad patch), `501` (the store cannot hold a queue).
*/
export declare class ChatQueueError extends Error {
readonly status: 400 | 404 | 409 | 410 | 501;
readonly code?: string | undefined;
constructor(status: 400 | 404 | 409 | 410 | 501, message: string, code?: string | undefined);
}
/** `regenerate: true` on a send without a `threadId` — there is no answer to regenerate. */
export declare class RegenerateNeedsThreadError extends Error {
constructor();
}
/**
* The framework-agnostic orchestration facade the `/agent` routes call: start a run, subscribe to
* its live token stream, deliver HITL decisions, and read/mutate threads. It owns no HTTP — the
* provider's route handlers resolve the actor and pipe the SSE, then delegate here.
*/
/** What the service needs beyond the runner, store and deps — all optional. */
export interface AgentServiceOptions {
/** Which models a caller may pick. Omitted → an empty catalog; naming a model is refused. */
models?: ModelCatalog;
/**
* The budget report behind `GET <path>/quota` and, when `gated`, the send gate. Omitted → a day
* window from the ledger, never gated.
*/
quota?: {
provider: QuotaProvider;
gated: boolean;
};
/** Where attachments live (`attachments` in `config/agent.ts`). Absent → a send naming one is refused. */
attachments?: AttachmentStagingStore;
/**
* The thread message queue — the same instance the runner drains with. Absent (or over a store
* that is not a `ChatQueueStore`) → a send on a busy thread starts a second, concurrent turn, as
* before the queue existed.
*/
queue?: ChatQueueService;
actionProposals?: ActionProposalService;
backgroundActorResolver?: BackgroundActorResolver;
}
/** A send's attachments were refused — `status` is what the chat route answers (`403`, `501`). */
/** A thread's `defaultAgent` named an agent this app does not register. */
export declare class UnknownAgentError extends Error {
readonly agentName: string;
constructor(agentName: string);
}
export declare class AttachmentRefusedError extends Error {
readonly status: 400 | 403 | 413 | 415 | 501;
constructor(status: 400 | 403 | 413 | 415 | 501, message: string);
}
/**
* The staged-attachment inventory cannot be answered here — no attachment store, a store without
* `list`, or an agent store without `referencedMediaIds`. `status` is what `GET <path>/attachments`
* answers. Never answered with an empty list instead: "no inventory" and "no files" are opposite
* facts, and a sweep that read the first as the second would delete files that are in use.
*/
export declare class AttachmentInventoryError extends Error {
readonly status: 501;
constructor(message: string);
}
export declare class AgentService {
private readonly runner;
private readonly store;
private readonly deps;
private readonly options;
constructor(runner: AgentRunner, store: AgentStore, deps: AgentDepsFactory, options?: AgentServiceOptions);
/**
* The models `actor` may pick for `agent` (`GET <path>/models`). An empty catalog when none is
* configured, so a picker simply has nothing to offer.
*/
/** Is a model catalog configured (so `GET <path>/models` lists something)? */
hasModelCatalog(): boolean;
listModels(actor: Actor, agent?: string): Promise<ModelCatalogView>;
/**
* `model` if the catalog offers it to this actor and agent right now, else a
* {@link ModelNotAllowedError} naming why. The one gate every model choice passes — a send's own,
* and a thread's pinned one when it is pinned and again at every turn that runs on it.
*/
assertModelAllowed(actor: Actor, agent: string, model: string): Promise<string>;
/**
* The model a turn runs on: the send's own (that turn only — never stored on the thread), else the
* thread's pinned one, else none. An agent the catalog locks to one model runs on it whatever was
* sent or pinned, and a send naming another model is refused.
*/
private resolveModel;
/**
* Pin (or with `null`, unpin) a catalog model on a thread — checked against the catalog for the
* thread's default agent. `false` when the store cannot persist it (no `updateThread`).
*/
setThreadModel(actor: Actor, threadId: string, model: string | null): Promise<boolean>;
/**
* Set a thread's default agent and/or pin its model (`PATCH <path>/threads/:id`). `null` clears
* either. The model is checked against the catalog for the agent the thread's next turn runs as —
* the one this patch sets, else the thread's own default, else the configured one. A
* `defaultAgent` that names no registered agent is refused ({@link UnknownAgentError}). `false`
* when the store cannot persist it (no `updateThread`). Ownership is the caller's to check.
*/
updateThreadSettings(actor: Actor, threadId: string, patch: {
defaultAgent?: string | null;
model?: string | null;
persona?: string | null;
}): Promise<boolean>;
/** A registered agent, or a name one of their personas took over (`Persona.aliases`). */
private isKnownAgent;
/**
* The persona this send runs under — see {@link AgentDepsFactory.resolvePersona}. The thread's
* pinned persona is read only when the send names none AND the agent has personas to pick from.
*/
private resolveSendPersona;
/** Pin `persona` on the thread when it is not already — a no-op on a store that cannot. */
private pinThreadPersona;
/**
* The agent a send runs as: the one it names, else the thread's own default agent (when the
* thread exists and has one), else the configured default.
*/
private resolveAgentName;
/**
* The registered agents, for a picker (`GET <path>/agents`). With a model catalog, an agent it
* locks to one model for this actor carries `lockedModel` — the same lock `GET <path>/models`
* reports as `locked`.
*/
listAgents(actor: Actor): Promise<AgentCatalogEntry[]>;
/** The actor's budget across windows (`GET <path>/quota`). */
quotaReport(actor: Actor): Promise<QuotaReport>;
/**
* Refuse a turn the actor's budget no longer covers — only when a budget was configured
* (`quota: { limits }` or a `QuotaProvider`): the default report is informational.
*/
private assertWithinQuota;
/** The queue service when the bound store can hold a queue, else `undefined`. */
private queueing;
/** The queue service, or a `501` naming what is missing. */
private requireQueue;
/**
* Start a turn — for in-process callers that need a run id back. A send that would have to wait
* (its thread already has a turn running) is refused with a `409 run_active`
* {@link ChatQueueError} instead; {@link send} is the form that queues it.
*/
chat(params: ChatParams): Promise<{
runId: string;
threadId: string;
}>;
/**
* Send a message: start a turn, or — when its thread already has one running — queue the message
* to run after it (see {@link ChatSendMode}). What `POST <path>/chat` calls.
*/
send(params: ChatParams, options?: ChatSendOptions): Promise<ChatSendResult>;
/** Start or queue the turn itself — what a send is once it is not a text decision. */
private sendTurn;
/**
* Start a turn the thread is already claimed for, under the claimed id. A runner that minted an
* id of its own anyway gets the thread re-pointed at it; one that fails to start frees the thread.
*/
private startClaimed;
/** Put a send in its thread's queue, then drain it if the thread turned out to be free. */
private enqueue;
/** Can this deployment queue messages (the store is a `ChatQueueStore` and a queue is wired)? */
queueSupported(): boolean;
/** `GET <path>/threads/:id/queue` — the thread's waiting messages and whether it drains. */
getQueue(threadId: string): Promise<ChatQueueState>;
/** The thread a queued message waits on — `null` when there is no such message. */
queuedMessageThread(messageId: string): Promise<string | null>;
/**
* `PATCH <path>/queue/:messageId` — change a waiting message's text and/or attachments, and/or
* move it to `position` in the queue. Answers the thread's queue.
*/
updateQueuedMessage(actor: Actor, messageId: string, patch: {
message?: string;
attachments?: AttachmentRef[] | null;
position?: number;
}): Promise<ChatQueueState>;
/**
* `POST <path>/queue/:messageId/interrupt` — run a waiting message NOW: it moves to the head of
* its queue as an interrupt, any pause is lifted, and the turn holding the thread is cancelled for
* it (the cancel settles into the queue, which starts it). With nothing running it starts at once.
*
* One request rather than "remove it, then send it again with `mode: 'interrupt'`", because a
* client cannot compose that safely: the message is lost if the second request fails, and runs
* twice if another tab's drain starts it in between. Here it never leaves the queue. Answers the
* queue, plus `interrupting` (the run that was cancelled) or `runId` (the run the message started
* under — its own id).
*/
interruptQueuedMessage(messageId: string): Promise<ChatQueueState & {
runId?: string;
interrupting?: string;
}>;
/** `DELETE <path>/queue/:messageId` — drop a waiting message. Answers the thread's queue. */
removeQueuedMessage(messageId: string): Promise<ChatQueueState>;
/** `DELETE <path>/threads/:id/queue` — drop every waiting message (and any pause). */
clearQueue(threadId: string): Promise<ChatQueueState>;
/**
* `POST <path>/threads/:id/queue/resume` — lift a pause and start the head when nothing is
* running. Answers the queue, and the run the head started under, if it did.
*/
resumeQueue(threadId: string): Promise<ChatQueueState & {
runId?: string;
}>;
subscribe(runId: string): AsyncIterable<StreamFrame>;
/**
* Rate a message: `'up'`/`'down'` with an optional comment, or `null` to clear the rating. Checks
* nothing about WHO — the route resolves the message's thread (`threadOfMessage`) and its owner
* first. Returns the stored rating, or an error the route maps to a status.
*/
setMessageFeedback(messageId: string, input: {
value: unknown;
comment?: unknown;
}): Promise<{
ok: true;
feedback: MessageFeedback | null;
} | {
ok: false;
status: 400 | 501;
error: string;
}>;
/** The thread a message belongs to (`AgentStore.threadOfMessage`); `undefined` when unsupported. */
threadOfMessage(messageId: string): Promise<string | null | undefined>;
/**
* Is anything streaming (or buffered) under this run — or about to? `true` when the sink cannot
* say.
*
* "About to": a run that holds its thread and has written nothing yet. It was admitted (a send, or
* a queue drain handing the thread to the next message) and its body has not reached the sink — on
* a durable runner, because no worker has picked it up. A client told that run started attaches
* right then; answering "nothing to resume" would make it give up on a turn that is a moment from
* streaming. The runner decides whether the holder is alive, as it does for admission: a claim
* left by a process that died is not a stream.
*/
hasStream(runId: string): Promise<boolean>;
/**
* The owning actor ref of a run (turn), or `null` if unknown — for per-actor route ownership
* checks. A run that holds a thread but has not started executing has no row of its own yet (see
* `AgentStore.threadHeldByRun`): its owner is the thread's.
*/
runOwner(runId: string): Promise<string | null>;
/** The owning actor ref of a thread, or `null` if unknown — for per-actor route ownership checks. */
/** The run a tool call belongs to (see `AgentStore.getToolCallRunId`); `null` when unknown. */
toolCallRun(toolCallId: string): Promise<string | null>;
threadOwner(threadId: string): Promise<string | null>;
/**
* Refuse a decision addressed at a run that is over. The runtime would take the signal and buffer
* it for a run that never comes back — the person's "yes" is accepted, the card says so, and
* nothing runs. Settles what the dead run left behind on the way out. A runner that cannot say
* whether a run is alive is taken at its word that it is.
*/
private assertRunWaiting;
/**
* A thread's independent proposals. Without them (the default blocking approvals) there are none,
* so the read answers an empty list rather than refusing: a client that lists by default (React's
* `useAgentChat`) needs no flag. Deciding one still answers `501`.
*/
listActionProposals(actor: Actor, threadId: string): Promise<import("./spi/action-proposal-store.js").ActionProposal[]>;
/** One page of {@link listActionProposals}: an empty, final page without independent proposals. */
listActionProposalsPage(actor: Actor, threadId: string, after?: import('./spi/action-proposal-store.js').ListActionProposals['after']): Promise<{
items: import("./spi/action-proposal-store.js").ActionProposal[];
next?: {
createdAt: number;
id: string;
};
}>;
decideActionProposal(actor: Actor, threadId: string, proposalId: string, decision: 'approved' | 'rejected', body: unknown, via?: string): Promise<ActionProposalMutationResult>;
/** `actionApprovalMode` in `config/agent.ts` — `'blocking'` unless it says `'independent'`. */
actionApprovalMode(): ActionApprovalMode;
/**
* The words a text decision is made of (`actionProposalText.vocabulary` over the English
* defaults) — what a surface tells a person to reply, or what it maps a button to.
*/
actionProposalVocabulary(): TextActionProposalVocabulary;
/** The configured chat reply to a proposal decision (see `actionProposalText.replies`). */
actionProposalReply(result: import('./spi/action-proposal-store.js').ActionProposalMutationResult, decision: 'approved' | 'rejected'): string;
handleTextDecision(actor: Actor, threadId: string, text: string): Promise<{
status: 'unmatched';
} | {
threadId: string;
proposalDecision: ActionProposalMutationResult;
text: string;
} | {
threadId: string;
proposalDecision: {
status: 'ambiguous';
proposalIds: string[];
};
text: string;
}>;
/**
* Approve a parked action call. `executedByRef` is who decided (the routes stamp the caller);
* `remember` approves later calls of the same tool in the same thread; `via` names the surface the
* decision came through (`'web'`, `'slack'`, …) — all persisted with the call.
*/
approve(runId: string, toolCallId: string, opts?: {
executedByRef?: string;
remember?: boolean;
via?: string;
}): Promise<void>;
reject(runId: string, toolCallId: string, reason?: string, opts?: {
executedByRef?: string;
via?: string;
}): Promise<void>;
/**
* A call's approval state (see `AgentStore.toolCallApproval`); `null` when unknown or unrecorded.
* Pass the call's `runId` when known: another run may have been handed the same call id.
*/
toolCallApproval(toolCallId: string, runId?: string): Promise<ToolCallApprovalState | null>;
/**
* May `actor` settle a call whose recorded approver is `approver` (never the requester — that is
* the ownership check)? The configured policy's `canDecide`, else "holds that role".
*/
mayDecide(actor: Actor, decision: ApprovalDecisionRef): Promise<boolean>;
/**
* Deliver a human's answers to a parked question set. An id the reply omits takes that question's
* own pre-picked default, resolved by the loop against the request it already holds — so "just
* pressed enter" and "picked exactly the defaults" persist identically, and a client that never
* rendered the defaults cannot submit a blank.
*
* Addressed at a tool call that is in fact waiting for an approve/reject, this is refused: see
* {@link import('./elicitation.js').HumanReplyMismatchError}. Answers say nothing about whether
* proposed work should go ahead, and the alternative is a rejection nobody made.
*/
/**
* Check a reply against the questions it answers before it is signalled: every submitted value a
* question's own rules accept (a typed input's type, bounds and pattern; a pick from the offered
* options), and every `required` question answered — by the reply, or by the defaults an omitted
* question falls back to. Returns the first problem as `answers["<id>"] <reason>`, or `null`.
* Needs the store's `toolCallInput`; without it the loop's own filtering is the only check.
*/
answerProblem(toolCallId: string, answers: Record<string, string[]>, runId?: string): Promise<string | null>;
answer(args: {
runId: string;
toolCallId: string;
answers: Record<string, string[]>;
answeredByRef?: string;
/** The surface the answer came through (`'web'`, `'slack'`, …) — an approval's `via`. */
answeredVia?: string;
}): Promise<void>;
/**
* The user declined to answer and told the agent to proceed on its own assumptions. It lands on
* the same values a confirmation would and persists as `rejected` rather than `executed`, because
* proceeding on an assumption someone declined to confirm is a different fact from proceeding on
* one they chose.
*/
skip(args: {
runId: string;
toolCallId: string;
answeredByRef?: string;
answeredVia?: string;
}): Promise<void>;
cancel(runId: string): Promise<void>;
/**
* Execute in this process what of `runId` still waits for a worker (see
* {@link AgentRunner.drive}) — for a caller that waits on the run from inside that worker's own
* work. A no-op when the runner runs turns where it starts them.
*/
drive(runId: string): Promise<void>;
/**
* The tools THIS actor can reach through an agent, with how a chat surface talks about each —
* the same list the model is offered (`ToolRegistry.visibleSpecs` behind `definitionsFor`, against
* the agent's allow-list and the roles policy). Built-in tools the loop serves itself (`ask`,
* `skill`, `remember`) are not registry tools and are not listed.
*/
toolCatalog(actor: Actor, agentName?: string): Promise<ToolCatalogEntry[]>;
/**
* `GET <path>/tools?agent=*`: the union of {@link toolCatalog} across the default agent and every
* registered one — each tool this actor reaches through ANY of them, once (first agent wins its
* entry), under the same gates.
*/
toolCatalogForAllAgents(actor: Actor): Promise<ToolCatalogEntry[]>;
/**
* The persona `id` of `agentName` (default: the default agent), else its default persona — the
* definition, for a caller that wants to read it. Unlike a send, an unknown `id` is not refused.
*/
resolvePersona(agentName?: string, id?: string): Persona | undefined;
/**
* The agent and persona a queued message that carries no persona starts as: an agent name a
* persona took over, then the thread's pin, then the agent's default — never refused. What the
* provider hands the queue as `resolveTarget`.
*/
queuedTarget(message: {
threadId: string;
agentName?: string;
}): Promise<{
agentName?: string;
persona?: string;
}>;
/** `agentName`'s personas (default: the default agent's) as a picker reads them. */
personaCatalog(agentName?: string): PersonaCatalogEntry[];
listThreads(actorRef: string): Promise<ThreadSummary[]>;
/**
* A thread as the routes report it: a persona pin that no registered agent declares — a thread
* created before pins existed holds whatever it was created with (`'default'`, most often), or the
* persona was removed since — reads as `null`, which is what it resolves to.
*/
private withPinnedPersona;
/**
* A thread as `actor` reads it back. Each attachment's url is re-minted by `mediaId` through the
* attachment store: the one persisted with the message was minted for THAT turn (a signed url that
* expires), so an old turn would otherwise show a dead link. One the store will not resolve for
* this actor keeps the url it was stored with.
*/
getThread(threadId: string, actor?: Actor): Promise<ThreadDetail | null>;
/**
* The size cap and content types an attachment is held to — what the attachment store declares,
* else the defaults (20 MiB; images, PDF, plain text, CSV). `null` when no attachment store is
* configured, so nothing can be attached.
*/
attachmentLimits(): AttachmentLimits | null;
/**
* Stage a file the server received itself (a channel's media message) for `actor`, held to the same
* limits as `POST <path>/attachments` — then name it in a send as `{ mediaId }`. Refused
* ({@link AttachmentRefusedError}) with `501` when no attachment store is configured, `415` for a
* content type outside the allowlist, `413` past the size cap.
*/
stageAttachment(actor: Actor, file: {
data: Buffer;
filename: string;
contentType: string;
}): Promise<MessageAttachment>;
/**
* The Lucid database the agent's store writes to, or `null` when the store is not Lucid — what
* shared state that should live next to the threads (a text channel's store) is built on.
*/
lucidDatabase(): LucidDatabaseLike | null;
/**
* Turn a send's `{ mediaId }` refs into attachments through the store, for this actor. Refused
* ({@link AttachmentRefusedError}) when no store is configured or one ref is not the actor's to use.
*/
private resolveAttachments;
/**
* Every file this actor has staged, newest first — across threads, including uploads that were
* never sent, which nothing else on this surface can see (`GET <path>/attachments`). Metadata only:
* a url is minted per request by `resolve`, so it can stay short-lived.
*
* Refused ({@link AttachmentInventoryError}) when the configured attachment store keeps no
* inventory (`AttachmentStagingStore.list`).
*/
listAttachments(actor: Actor, options?: {
limit?: number;
}): Promise<StagedAttachment[]>;
/**
* This actor's staged media that no live message — and no message waiting in a thread's queue —
* references, and that is old enough not to be an upload in flight: the candidate set for a sweep.
* Returns them; never deletes them. The bytes are the host's, and so is the decision:
*
* ```ts
* const service = await app.container.make(AgentService)
* for (const file of await service.collectableAttachments(actor, { olderThan: subDays(new Date(), 7) })) {
* await media.delete(file.mediaId)
* }
* ```
*
* `olderThan` is required and has no default: how long a composer may sit open with a file attached
* is the host's knowledge. The staging store is asked to apply it, and the result is filtered again
* here — a store that ignored the hint would otherwise hand back an upload someone is about to send.
*
* Both halves must be answerable or this refuses ({@link AttachmentInventoryError}): an
* unanswerable reference query means "cannot tell", and treating it as "nothing is referenced"
* would mark every attachment the actor ever sent as safe to delete.
*/
collectableAttachments(actor: Actor, options: {
olderThan: Date;
limit?: number;
}): Promise<StagedAttachment[]>;
/** The configured store's `list`, or a refusal — never an empty inventory in its place. */
private attachmentInventory;
renameThread(threadId: string, title: string): Promise<void>;
deleteThread(threadId: string): Promise<void>;
/**
* Make a transient thread a regular one (listed by {@link listThreads}). `false` when the store
* cannot — it has no `promoteThread`.
*/
promoteThread(threadId: string): Promise<boolean>;
/**
* Drop a message and everything after it — the "edit and resend" primitive: the client then sends
* a fresh turn on the shortened thread.
*/
truncateThreadFrom(threadId: string, messageId: string): Promise<void>;
forkThread(threadId: string, fromMessageId: string): Promise<ThreadSummary>;
quotaToday(actorRef: string): Promise<{
usedTokens: number;
}>;
}
//# sourceMappingURL=agent-service.d.ts.map