UNPKG

@adonis-agora/agent

Version:

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

738 lines • 36.2 kB
import type { StandardSchemaV1 } from '@standard-schema/spec'; import type { ClaimedActionApproval } from './action-proposal-approval.js'; import type { AgentIntake } from './elicitation.js'; import type { SandboxClientConfig } from './genui/sandbox-kit.js'; import type { ActionProposalOutcome } from './spi/action-proposal-outcome-store.js'; import type { ActorResolver } from './spi/actor-resolver.js'; import type { ChatQueueState } from './spi/chat-queue.js'; import type { ToolPreflightResult } from './spi/tool.js'; import type { AgentUiComponent } from './stream-events.js'; import type { ToolConfirmation, ToolPresentation } from './tool-presentation.js'; import type { UiCapabilities } from './ui-capabilities.js'; /** Who is driving the turn. Roles + tenant come from the host app (nestjs-context/authz). */ export interface Actor { id: string; /** The caller's roles. Tool authorization is a set intersection against a tool's `roles`. */ roles?: string[]; tenantRef?: string; } export type ToolKind = 'read' | 'action' | 'agent' | 'ask' | 'skill' | 'memory'; /** * Declared shape of a tool. * - `read` auto-executes. * - `action` never auto-executes — requires HITL approval. * - `agent` delegates to another named agent (durable: a child workflow; inline: a nested loop), * handled at the loop level — NOT via a handler. Carries `targetAgent`. * - `ask` puts a structured question set to the user and parks. Carried by no {@link ToolSpec}: * `ask` has no handler and is never registered, it is offered to the model straight from * module config — so the branch that decides whether a call parks on a human can never * be settled by a process-local registry lookup. * - `skill` reads an authored procedure out of the catalog the turn's `skills:catalog` checkpoint * holds. Registered by nothing, for the same reason `ask` is not, and it spends a read's * checkpoints exactly — see `skills.ts`. * - `memory` writes one durable fact about the actor, authorized against the scopes the turn's * `memory:digest` checkpoint holds. Registered by nothing, and spends a read's * checkpoints exactly — see `memory.ts`. */ /** {@link ToolSpec.genui}: which genui tool this is, and what its telemetry may keep. */ export interface GenuiToolMarker { /** `tree` (`ui__render`), `sandbox` (`ui__sandbox`), `per-component` (`ui__show_*`), `show` (`ui__show`). */ mode: 'tree' | 'sandbox' | 'per-component' | 'show'; /** `false`: no telemetry for this tool. `samplePayloads: true` also stores the call's input (debugging only). */ telemetry?: false | { samplePayloads?: boolean; }; } export interface ToolSpec { name: string; kind: ToolKind; description: string; /** * For `kind: 'action'` — how long its approval (or proposal) stays open, in ms, over the * approval policy's `ttlMs`: an action whose card carries something with its own lifetime (a * sign-in link) lives as long as that, not as a Confirm button does. */ approvalTtlMs?: number; /** * How a person-facing surface talks about this tool (see {@link ToolPresentation}). Never shown * to the model; served to clients by `GET <path>/tools`. */ presentation?: ToolPresentation; replacementKey?: string | ((input: unknown, ctx: import('./spi/tool.js').AiToolCtx) => string | undefined | Promise<string | undefined>); /** * Input schema as a [Standard Schema](https://standardschema.dev) — validation-agnostic, so * Zod, Valibot, or ArkType all work. The loop validates input via `~standard.validate` before * running the handler, and providers convert it to the model's tool-parameter JSON schema. */ inputSchema: StandardSchemaV1; /** For `kind: 'agent'` — the name of the agent to delegate to. */ targetAgent?: string; /** * For `kind: 'agent'` — start the delegate and let the calling turn END instead of holding it open * until the delegate answers. The call's result is a receipt (`DetachedDelegationReceipt`), and the * answer arrives later as its own message in the same thread (see {@link AgentRunInput.deliverTo}). * * Authored per EDGE (`delegatesTo: [{ agent, detached: true }]`), never chosen by the model: a model * that can detach can detach the one thing the user is sitting there waiting for, and it cannot know * which that is. The person wiring `A -> B` does. * * Settled into the call's `persist:toolcall` checkpoint, so a replay reads the branch back rather * than re-deciding it against a registry that may have changed while the run was parked. */ detached?: boolean; /** * A successful call ENDS the turn: its effect is the answer (it pushed the UI the user reads), so * no further model call narrates it. A failed call does not end it — the model gets to recover. */ terminal?: boolean; /** * Marks a generative-UI tool (set by `genuiTools`): the loop records one telemetry row per call * (`AgentStore.recordGenuiCalls`) — tool, channel, mode, attempt, validity, error category, the * call's share of its step's tokens and cost, time to first and to complete frame. Metadata only: * the call's input is stored only with `telemetry.samplePayloads`. `telemetry: false` records * nothing. */ genui?: GenuiToolMarker; /** Roles allowed to invoke. Undefined → defaults applied by RolesPolicy (unrestricted unless `defaultRoles` is set). */ roles?: string[]; /** * Whether the tool exists in this deployment. `false` (or a predicate returning `false`) drops it * before the role filter, so it is never offered to the model and cannot be invoked. Undefined → * enabled. * * A predicate is re-evaluated every turn, so a flag flipped at runtime takes effect on the next * message with nothing re-registered. For availability that depends on a container-resolved * service, put `isEnabled()` on the tool class instead — a spec is data, a handler is a service. */ enabled?: boolean | (() => boolean | Promise<boolean>); /** * An authorization ability name (e.g. 'cache.purge'). Consumed by an ability-aware RolesPolicy * such as the `@adonis-agora/agent/authz` Bouncer adapter (`authzToolAuthorizer`), which denies * every tool that declares none. Apps that don't use authz ignore it and rely on `roles` instead — * both live on the same SPI, so neither is required. */ ability?: string; } /** What the model is told a tool looks like (no handler, no host types). */ export interface ToolDefinition { name: string; kind: ToolKind; description: string; inputSchema: StandardSchemaV1; } /** A tool call the model asked for during a turn. */ export interface ToolCallRequest { actionApproval?: ClaimedActionApproval; prepared?: { preparationInput: unknown; input: unknown; preflight: ToolPreflightResult; replacementKey?: string; }; /** Trusted domain preparation stamped by the runtime into the model checkpoint, never supplied by the model. */ preflight?: ToolPreflightResult | { status: 'failed'; error: string; }; id: string; name: string; input: unknown; /** * The tool's declared kind (`ToolSpec.kind`), stamped where the tool was OFFERED — inside the llm * checkpoint, by the process that built the definition list the model chose from. It travels with * the call from there, so the approval branch does not depend on which process replays the turn. * Undefined only for a call that predates the stamp, or one no registry could resolve * (defensively treated as `read` wherever a definite value is required). */ kind?: ToolKind; /** * The call this one ran inside — an inner call of a code-mode tool an engine reported (see * `AgentEngine`). Absent for a call the model made itself, which is every call the loop records. */ parentId?: string; } /** Result of running a tool. */ export interface ToolResult { id: string; name: string; output: unknown; error?: string; /** * A person declined this action, so the tool never ran. Set INSTEAD of a failure, and read by * every consumer that has to tell the two apart. `error` still carries what the MODEL is told, * because that is the channel a model reads a tool's outcome on; this flag is what everything * else reads. */ denied?: true; /** * The approval request lapsed before anyone decided, so the tool never ran. Always set together * with {@link denied}: an expiry IS a refusal to every consumer that only knows that much, and this * flag is for the ones that tell "nobody answered" from "someone said no". */ expired?: true; } export interface MessageUsage { /** * Total input (prompt) tokens for the turn — the whole input side, cached and uncached alike. * `cacheWriteTokens` + `cacheReadTokens` are subsets of this count, not additions to it, so * token totals and quota never change when a breakdown is present. */ inputTokens: number; /** Total output (completion) tokens for the turn; `reasoningTokens` is a subset of this. */ outputTokens: number; /** * How many of `inputTokens` were written to the prompt cache this turn (billed at a premium, * ~1.25× base input). Undefined when the provider doesn't report caching. Refines the cost * estimate only — priced by the pricing row's cache-write rate (falling back to the input rate). */ cacheWriteTokens?: number; /** * How many of `inputTokens` were served from the prompt cache this turn (billed at a discount, * ~0.1× base input). Undefined when the provider doesn't report caching. */ cacheReadTokens?: number; /** * How many of `outputTokens` the model spent on reasoning/thinking. Observability only — reasoning * tokens are billed at the output rate, so they don't change the cost estimate. Undefined for * non-reasoning models or providers that don't report it. */ reasoningTokens?: number; /** * This turn's USD cost: the provider's own reported figure when it has one (a gateway), else an * estimate from the bound `AgentPricingStore` (cached once per run — see `AgentLoopDeps.pricingStore`), * else `null` when no pricing store is bound or the model has no price row. Never `0` for an unpriced * model — a real $0 turn and "we don't know" must stay distinguishable. Undefined before the loop * folds cost (e.g. a plain usage struct that predates pricing). */ costUsd?: number | null; } /** * Para que serviu o consumo de uma linha do ledger. * * `embedding` é o gasto de RETRIEVAL: embedar a pergunta do usuário antes da busca vetorial. Ele * entrou depois dos outros, e a razão é a que importa — sem ele, um agente com retrieval em modo * inject gastava tokens em toda pergunta e nem o painel nem a COTA enxergavam. A cota soma o * ledger inteiro, sem filtrar propósito, então registrar aqui é o que faz o gasto de RAG passar a * contar contra o teto diário. */ export type UsagePurpose = 'chat' | 'title' | 'follow_ups' | 'summary' | 'embedding' | 'structured_output'; export interface QuotaState { usedTokens: number; limitTokens: number; withinLimit: boolean; } /** A human decision on a pending action tool call. */ export interface Decision { approved: boolean; reason?: string; /** Opaque ref of WHO decided, when it wasn't the run's own actor. */ executedByRef?: string; /** * Approve later calls of the SAME tool in the SAME thread without asking again. Read only on an * approval; the loop answers it through `AgentStore.rememberedApprovals`. */ remember?: boolean; /** * The surface the decision came through — `'web'`, `'slack'`, `'console'`, anything the caller * names. Provenance only: persisted with the call, never authorized against. */ decidedVia?: string; /** * Nobody decided before the request lapsed. Set by the RUNNER when the approval wait times out * (see `AgentLoopHooks.awaitApproval`'s `timeoutMs`), never by a person — the HTTP surface does not * accept it. Read as a denial the model is told expired. */ expired?: true; } export type MessageRole = 'user' | 'assistant' | 'system'; /** * A file a user attached to a message so a vision-capable model sees it natively (an image, a PDF). * The lib stays provider-agnostic: it passes {@link MessageAttachment.url} straight through as the * model's image/file part data — making that URL reachable by the provider (a presigned URL, a proxy, * or a `data:` URI) is the consumer's job. The lib never fetches bytes or talks to a store; that * upload-time work is the {@link import('./spi/attachment-staging.js').AttachmentStagingStore} seam. */ export interface MessageAttachment { /** Stable id of the stored media object in the consumer's media store. Provenance + replay key. */ mediaId: string; /** A URL the model provider can fetch the bytes from at turn time. */ url: string; /** MIME type — routes the part: `image/*` → image part, otherwise → file part. */ contentType: string; /** Original filename, for display and the file part's filename. */ name: string; } /** A neutral chat message exchanged with the model. */ export interface ModelMessage { role: MessageRole; content: string; toolCalls?: ToolCallRequest[]; toolResults?: ToolResult[]; /** User-message attachments (image/PDF), rendered as native model content parts by the adapter. */ attachments?: MessageAttachment[]; } export interface PageContext { kind?: string; [key: string]: unknown; } /** * Inputs a {@link PromptBuilder} may use to compose the effective system prompt for a turn. * `basePrompt` is the agent's own (already-resolved) base prompt, so a persona builder can wrap * or extend it rather than replace it. */ export interface PromptContext { actor: Actor; persona?: Persona; pageContext?: PageContext; uiCapabilities?: UiCapabilities; /** The channel the turn runs on (`web`, `whatsapp`, …) — see `turnChannel`. */ channel?: string; basePrompt: string; } /** * A dynamic system prompt. Return a string (optionally async) built from the turn's context — * e.g. injecting the actor, the current page, or a data-shape description. The loop resolves it * once per turn from stable inputs (actor/persona/pageContext), so it stays replay-safe. */ export type PromptBuilder = (ctx: PromptContext) => string | Promise<string>; /** * A named variant of ONE agent: its own prompt and, optionally, a narrower tool allow-list. The * caller picks one per send (`POST <path>/chat { persona }`); everything else — the agent's model, * its access rules, its handoffs, its history — stays the agent's. A variant that needs any of those * to differ is a different agent, not a persona. The same shape as `@dudousxd/nestjs-agent`'s. */ export interface Persona { /** Unique within its agent — what a send names and what a message records. */ id: string; /** What a picker shows. */ label: string; /** One line about what the persona is for, for a picker. */ description?: string; /** * The persona's prompt. A flat string STANDS IN FOR the agent's base prompt; a * {@link PromptBuilder} is handed that base as `ctx.basePrompt`, so it can wrap it instead. Omit → * the agent's base prompt, unchanged (which can itself read `ctx.persona`). */ systemPrompt?: string | PromptBuilder; /** * Only these tool names are offered — and only these may be invoked, handoffs included — under * this persona. Layered AFTER the agent's own allow-list, `enabled`, the roles policy and `canUse`: * it narrows, never widens. Omit → whatever the agent offers. */ allowedTools?: string[]; /** * Agent names this persona answers for: a send, a queued message, a thread's `defaultAgent` or an * in-flight run that names one of them runs as THIS agent under THIS persona. For an app that folds * separate agents into personas of one — what recorded the old agent name keeps resolving, with no * data migration. A name that is still a registered agent is never an alias. */ aliases?: string[]; } /** * A persona as a turn RESOLVED it, journaled in the `persona:resolve` checkpoint — so every replay of * the run uses this, and not whatever the persona's configuration says by the time it resumes. */ export interface TurnPersona { id: string; label: string; allowedTools?: string[]; /** The persona's prompt, resolved (with the base prompt it wraps). Absent → the base prompt. */ prompt?: string; } /** One persona as `GET <path>/agents` lists it — what a persona picker renders. */ export interface PersonaCatalogEntry { id: string; label: string; description?: string; } /** One agent as `GET <path>/agents` lists it — what an agent picker renders. */ export interface AgentCatalogEntry { name: string; description: string; /** Whether this is the agent a turn uses when the caller names none. Omitted when not the default. */ isDefault?: true; /** * The catalog model this agent always runs on, when the model catalog locks it to one — an agent * picker can say so before a chat starts. `GET <path>/models?agent=` reports the same lock as * `locked`. */ lockedModel?: string; /** The agent's personas, for a persona picker. Omitted when it declares none. */ personas?: PersonaCatalogEntry[]; /** The persona a send runs under when it names none. Omitted when the agent has no default. */ defaultPersona?: string; } /** * What the HOST knows about a send that the library does not model — see * {@link AgentRunInput.hostContext}. Plain JSON. */ export type AgentHostContext = { readonly [key: string]: unknown; }; /** Everything needed to run one agent turn. */ export interface AgentRunInput { threadId: string; actor: Actor; /** The latest user message text. */ userText: string; /** Files attached to the latest user message (image/PDF). Persisted with it and sent to the model. */ attachments?: MessageAttachment[]; /** * When the person sent {@link userText} (ISO 8601), stored with it as `StoredMessage.sentAt`. * Omitted → the message's own `createdAt` stands for it. */ sentAt?: string; /** * The persona of {@link agentName} this turn runs under. An id ({@link Persona.id}) is the durable * form: the service resolves it from the send, the thread's pin and the agent's default BEFORE the * run starts, and the loop looks its definition up once, in the `persona:resolve` checkpoint. A * whole {@link Persona} is applied as given, with no checkpoint — what a caller of `runAgentLoop` * may hand it, and what a run started by a release before 0.60 recorded in its input. Omitted → no * persona, and no checkpoint spent on one. */ persona?: string | Persona; pageContext?: PageContext; uiCapabilities?: UiCapabilities; /** * Answer the thread's last user message again instead of appending {@link userText} (ignored): * the loop drops every message after that user message — the answer being replaced — and runs * the turn on what is left. What `POST <path>/chat { threadId, regenerate: true }` starts. */ regenerate?: boolean; /** YYYY-MM-DD stamped by the runner so quota/day stays deterministic under durable replay. */ day?: string; /** Which named agent runs this turn. Omitted → the default/single agent. */ agentName?: string; /** * The run that started this one (a delegation's parent). Recorded with the run so a governance * surface can roll a delegation's cost up to the turn that asked for it; without it a child run is * a row with nothing pointing at it outside the durable engine's own journal. */ parentRunId?: string; /** * How many agent→agent delegations deep this run already is (0 for a top-level turn). The runner * increments it for each child run; the loop refuses to delegate past its depth ceiling. */ delegationDepth?: number; /** * The named agents already on this delegation chain, root first — what {@link delegationDepth} * counts, spelled out. The runner appends its own agent's name for each child it starts. * * A count can only say a chain is LONG. This says whether it is going in circles, and how often: * an agent that appears here is one the chain has already passed through, so a delegation back to * it is a cycle by inspection rather than by proxy. A run whose runner supplies none falls back to * the depth ceiling alone. */ delegationPath?: readonly string[]; /** * The model this turn runs on — a catalog id the service already checked (a per-send pick, else * the thread's pinned model). Handed to the provider as `ModelTurnArgs.model`, and the usage label * when the provider reports none. Omitted → the provider's default. */ model?: string; /** * Set on a DETACHED sub-agent run only: the thread and tool call it answers into when it finishes. * Its presence is also what makes a run detached from the inside — it owns its own stream (no * ancestor sink to forward into), so nothing else tells it apart from a top-level turn. */ deliverTo?: DetachedDelivery; /** * What the HOST knows about this turn that the library does not model — where it came from, where * its answer goes, on whose behalf (e.g. `{ source: 'slack', delivery: { channel, ts } }`). Plain * JSON: it travels through the queue and the durable journal untouched, and reaches the runner's * hooks (an engine's host callbacks) as it was sent. Never shown to the model or to clients, and * never inherited by a delegated run. */ hostContext?: AgentHostContext; } /** * Where a detached sub-agent run posts its answer: the thread that delegated it, and the `agent`-kind * tool call that started it. Carried on the child run's own {@link AgentRunInput}, because by the time * the child finishes the delegating turn is over and nothing else is holding the address. */ export interface DetachedDelivery { threadId: string; toolCallId: string; } /** * One `agent → agent` delegation edge, with the authorization the synthesized `ask_<target>` tool * carries. The object form exists because delegation goes through the same {@link ToolSpec} gate as * every other tool: a spec with neither `roles` nor `ability` is denied by both shipped authorizers, * so an orchestrator that must actually delegate has to say who may. * * ```ts * // Role-based (the default `DefaultToolAuthorizer`): * { name: 'orchestrator', delegatesTo: [{ agent: 'researcher', roles: ['ANALYST', 'ADMIN'] }] } * * // Ability-based (the `@adonis-agora/agent/authz` Bouncer adapter): * { name: 'orchestrator', delegatesTo: [{ agent: 'researcher', ability: 'agent.delegate' }] } * ``` */ export interface DelegateEdge { /** Name of the agent to delegate to — the `target` in `ask_<target>`. */ agent: string; /** * Run the delegate in the BACKGROUND: the synthesized tool is `start_<target>`, the calling turn * ends with a receipt instead of waiting, and the delegate's answer is posted into the same thread * as its own message (stamped with its `runId` / `agentName`) when it is ready. The chat stays free * meanwhile. Default `false` — the awaited `ask_<target>`. See {@link ToolSpec.detached}. */ detached?: boolean; /** * Roles allowed to invoke the synthesized delegate tool. Omitted → the `RolesPolicy` default * (`defaultRoles`; unrestricted unless configured, under {@link import('./authorizer.js').DefaultToolAuthorizer}). */ roles?: string[]; /** * Ability the synthesized delegate tool declares. REQUIRED under an ability-aware authorizer such * as `authzToolAuthorizer`, which denies every tool that declares none — omit it there and the * delegation is denied on every call. */ ability?: string; } /** * A named agent: its prompt, the tools it may use, and its personas. Definitions are registered in * `config/agent.ts` under `agents: [...]`; an orchestrator delegates to the others it names in * {@link AgentDefinition.delegatesTo}, which the factory turns into `ask_<target>` tools. Model, * store, sink and governance are shared from the module config unless overridden here. */ export interface AgentDefinition { name: string; /** One line about what the agent does, for a picker (`GET <path>/agents`). */ description?: string; /** Base prompt for this agent. A flat string, or a {@link PromptBuilder} resolved per turn. */ systemPrompt?: string | PromptBuilder; /** Allow-list of tool names this agent may use (subset of all registered tools). */ tools?: string[]; /** * Other agents this agent may delegate to. Each edge is auto-registered as an `agent`-kind tool * named `ask_<target>`, which the loop authorizes exactly like any other tool. * * A bare string declares the edge with no authorization annotation: under the default * {@link import('./authorizer.js').DefaultToolAuthorizer} it takes `defaultRoles` (unrestricted * unless configured), and under the `@adonis-agora/agent/authz` adapter a tool with no `ability` * is denied outright — so under authz a bare edge can never be called. Use the * {@link DelegateEdge} object form to declare the `roles` and/or `ability` the delegate tool * carries. */ delegatesTo?: (string | DelegateEdge)[]; /** Named variants of this agent — see {@link Persona}. Undefined → none. */ personas?: Persona[]; /** * The persona a send runs under when neither it nor its thread names one. Undefined → the persona * whose id is `'default'`, when the agent declares one; else none. */ defaultPersona?: string; modelId?: string; maxSteps?: number; /** * How many agent→agent delegations deep a chain starting at this agent may go. * Undefined → {@link import('./agent-loop.js').MAX_DELEGATION_DEPTH}. */ maxDelegationDepth?: number; /** * How many times one agent may appear on a single delegation chain. * Undefined → {@link import('./agent-loop.js').DEFAULT_MAX_AGENT_APPEARANCES}. */ maxAgentAppearances?: number; /** * Offer the model the built-in `ask` tool, so it can put a question set to the user when it judges * the scope is missing, and wait for the answers (`POST <path>/tool-call/answer`). Off by default. * See `AgentLoopDeps.ask`. * * The run PARKS on the person. Under the inline runner that wait lives in the process that started * the turn, so the answer has to reach the same replica; under the durable runner (`durable: true`) * it is a journaled signal any replica can deliver. */ ask?: boolean; /** * A question set this agent asks before it starts working — authored here, not by the model — on * a thread's first turn, or on every turn. See `AgentLoopDeps.intake`. Parks like {@link ask}. */ intake?: AgentIntake; /** * Per-agent {@link ActorResolver} override. When set, this agent resolves the request's actor with * its own resolver instead of the module-global `config.actorResolver` — e.g. an agent that reads * the caller from the HTTP body rather than the session. Falls back to the global resolver when * unset, so agents without one behave exactly as before. Same type as the global resolver. */ actorResolver?: ActorResolver; } export interface ThreadSummary { id: string; title: string; /** * The persona this thread's turns run under when a send names none — the last one a send on it * named, or `PATCH <path>/threads/:id { persona }`. `null` → none pinned (the agent's default * applies). The routes report a pin the thread's agent no longer declares as `null` too. */ persona: string | null; pinnedAt?: string; transient: boolean; createdAt: string; updatedAt: string; lastMessagePreview?: string; /** * The model pinned on this thread (`PATCH /threads/:id { model }`) — every turn without its own * `model` runs on it. `null` → the provider's default. Undefined for a store that does not persist it. */ model?: string | null; /** * The agent this thread's turns run as when a send names none (`PATCH /threads/:id * { defaultAgent }`). `null` → the configured default agent. Undefined for a store that does not * persist it. */ defaultAgent?: string | null; /** * The run streaming on this thread right now — the one a reloading client re-attaches to * (`GET <path>/chat/:runId/stream`) — or `null` when none is. Set when a turn starts, cleared when * it ends. */ activeRunId?: string | null; } export interface StoredMessage { actionProposalOutcome?: ActionProposalOutcome; id: string; role: MessageRole; content: string; toolCalls?: ToolCallRequest[]; toolResults?: ToolResult[]; /** Files the user attached to this message (image/PDF). Persisted with the message, replayed as-is. */ attachments?: MessageAttachment[]; followUps?: string[]; usage?: MessageUsage; /** The persona that was active when the message was written; absent when none was selected. */ persona?: string; /** * The run (turn) that wrote this message. Without it a reader can only guess which turn a message * belongs to by comparing timestamps against the run's `startedAt`, and that guess breaks the * moment a turn is regenerated: regeneration truncates the replaced answer and re-answers the * SURVIVING user message without appending a new one, so walking forward by time hands the older * run the replacement's text. Absent on a row written outside a run. */ runId?: string; /** * The agent that wrote this message, when it is not the turn's own: a detached sub-agent's answer * lands in the delegating thread as a message of its own, and this is how a reader tells "the * research agent finished" from the assistant's next reply. Absent on every other message. */ agentName?: string; /** * The model's thinking for this step, as it streamed (`reasoning` frames), so a reloaded thread * shows it where the live one did. Absent when the model produced none, or on a row written * before this was recorded. */ reasoning?: string; /** How long the model spent thinking in this step, in ms — what a "Thought for 4s" label reads. */ reasoningMs?: number; /** * Components pushed into this step (`ui` frames), in first-seen order with the last props for each * `id` — a reloaded thread replays them as `data-ui` parts. */ ui?: AgentUiComponent[]; /** * The approval record of every call on this message that was put to a person under an * `ApprovalPolicy` — who had to decide, until when, and how it settled. Read off the tool-call rows * by the store; absent when no call on the message asked for one. */ approvals?: ToolCallApproval[]; /** * The thread owner's rating of this message (`POST <path>/messages/:id/feedback`). Absent when * nobody rated it, or on a store that does not record feedback. */ feedback?: MessageFeedback; /** * When the person SENT this message (ISO 8601), when it differs from when it was stored: a text * channel's provider timestamp (a turn that waited in a queue is stored minutes later), the time a * queued send was queued, or one the send named (`ChatParams.sentAt`). Absent on every other * message — read {@link createdAt} then. What `messageTimestamps` shows the model. */ sentAt?: string; createdAt: string; } /** A thumbs-up/down on one message, with an optional free-text comment. */ export type MessageFeedbackValue = 'up' | 'down'; /** What {@link StoredMessage.feedback} holds. Not copied when a thread is forked. */ export interface MessageFeedback { value: MessageFeedbackValue; comment?: string; /** ISO-8601 instant the rating was last set. */ updatedAt: string; } /** * How one approval stands. `pending` → still parked; `approved` → someone said yes (or a remembered * approval did); `rejected` → someone said no; `expired` → nobody answered before `expiresAt`. */ export type ToolCallApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired'; /** The persisted approval metadata of one action tool call. See {@link StoredMessage.approvals}. */ export interface ToolCallApproval { proposalId?: string; target?: { kind: 'proposal'; proposalId: string; }; confirmation?: ToolConfirmation; toolCallId: string; /** Who may decide: `'requester'` (the thread's own actor) or a role name. */ approver: string; /** ISO-8601 instant the request lapses; absent → it never does. */ expiresAt?: string; status: ToolCallApprovalStatus; /** The decision asked for later calls of this tool in this thread to be approved automatically. */ remember?: boolean; /** Opaque ref of who decided. Absent while pending and on an expiry. */ decidedBy?: string; /** The surface the decision came through (`'web'`, `'slack'`, `'remembered'`, …). */ decidedVia?: string; /** What the person said when declining. */ reason?: string; } export interface ThreadDetail extends ThreadSummary { messages: StoredMessage[]; /** * Messages sent while a turn was running, waiting to run after it, and whether the queue is * draining. Present when the store supports a queue (`ChatQueueStore`); omitted otherwise. */ queue?: ChatQueueState; } export type ToolCallStatus = 'proposed' | 'auto_executed' | 'pending_approval' | 'executed' | 'rejected' | 'failed' /** An approval request lapsed before anyone decided; the tool never ran. */ | 'expired'; /** The attachment rules in force — what the upload route enforces (`GET <path>/config`). */ export interface AgentAttachmentConfig { /** An upload store is configured, so uploads work at all. */ enabled: boolean; /** How a client uploads (`null` when `enabled` is false). */ upload: 'multipart' | 'resumable' | null; maxBytes: number; allowedContentTypes: readonly string[]; /** How many attachments one message may name. */ maxPerMessage: number; } /** * `GET <path>/config` — server facts a client would otherwise repeat (the shape of * `@dudousxd/nestjs-agent`'s `AgentClientConfig`, which its React `useAgentConfig` reads). */ export interface AgentClientConfig { attachments: AgentAttachmentConfig; /** A model catalog is available, so `GET <path>/models` lists something to pick. */ models: { enabled: boolean; }; /** Sends are refused with `429` once `GET <path>/quota` reports `blocked`. */ quota: { enforced: boolean; }; /** No `actorResolver`: every browser is its own anonymous actor. */ identity: { anonymous: boolean; }; /** * Generative UI facts a renderer needs: the sandbox's theme, Tailwind runtime and kit bundle * (same-origin urls). Absent without a configured sandbox. */ genui?: { sandbox?: SandboxClientConfig; }; } //# sourceMappingURL=types.d.ts.map