UNPKG

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