/**
* hosting/types — the ports an agent needs to stand up and stay up.
*
* `AgentHost` is "something can call me". `ConversationHost` is "something can
* TALK to me" — a door that stays open, because `HostRequest → HostReply` is
* one exchange and some doors are not. `SessionLifecycle` is "the conversation
* outlives the request". All three are deliberately written in the vocabulary
* every transport and every store already has — an input, a reply, a frame, a
* session id, a stored blob — and in nothing else.
*
* **The rule these types are written under:** no runtime, product or protocol
* gets a field, a name or an assumption here. A port shaped around one
* provider's request envelope stops being a port and becomes that provider's
* SDK with extra steps, and every later adapter pays for it. If a decision only
* makes sense for one place you might deploy, it belongs in the adapter for
* that place, not in this file. `nodeHost` is the first adapter and it does not
* get special treatment either: its paths, its status codes and its JSON body
* shape all live in `nodeHost.ts`, and nothing in this file knows they exist.
*
* Pattern: Ports & adapters (hexagonal). Role: the port side, exclusively.
*/
///
import type { FlowchartCheckpoint } from 'footprintjs';
import type { Agent } from '../core/Agent.js';
import type { AskComponent } from '../core/askComponent.js';
import type { CheckInRequest } from '../core/checkin.js';
import type { MiddlewareAsk } from '../core/pause.js';
import type { AgentRunCheckpoint } from '../core/runCheckpoint.js';
import type { Unsubscribe } from '../events/dispatcher.js';
import type { ArtifactWireRequest, ArtifactWireResult } from './artifactWire.js';
import type { IdentityVerificationOptions } from './identityVerification.js';
import type { AdmissionPolicy } from './admission.js';
import type { IngressSink } from './ingressRecord.js';
import type { SessionSummary, SessionWireRequest, SessionWireResult } from './sessionWire.js';
export type { Unsubscribe };
/**
* Something a host can do BEYOND the baseline of "accept a request, deliver one
* reply". Read it from {@link AgentHost.capabilities} and branch on it — never
* assume it, and never infer it from the adapter's name.
*
* The union starts at exactly what a shipped adapter can honour today. A name
* is added when an adapter can actually keep the promise, never in anticipation
* of a transport that does not exist yet: a capability nobody implements is a
* promise the library cannot keep, and pre-minting one for an imagined future
* transport would bake that transport's assumptions in before it arrives.
*
* - `'streaming'` — the caller SEES `reply.emit(...)` pieces as they arrive.
* - `'conversation'` — this host can also carry a two-way channel that stays
* open ({@link ConversationHost.serveConversations}). It joined the union
* when two shipped adapters honoured it, not when it was imagined.
*
* Declared at CONSTRUCTION and static thereafter, which is a constraint worth
* knowing about: a capability whose truth depended on what happened to be
* installed at call time could not be declared here honestly, so an adapter
* that can only sometimes keep a promise does not make it.
*/
export type HostCapability = 'streaming' | 'conversation';
/**
* One inbound request, as the transport described it.
*/
export interface HostRequest {
/** What the caller is asking. */
readonly input: string;
/**
* A person's answer to an outstanding {@link PendingAsk} — and **the one
* thing that distinguishes a resume from a new message.**
*
* Present ⇒ this request answers the run that paused on this session. Absent
* ⇒ this request is a new message. That is the whole contract, and it is a
* FIELD rather than an inference on purpose: reading approval out of prose
* ("yes, go ahead") is a guess, and a guess is not something a consent gate
* may be built on.
*
* The port never interprets it. It is handed to `agent.resume(checkpoint,
* decision)` exactly as it arrived — a {@link CheckInRequest} or a middleware
* `ask` is answered with the shipped `checkInApproved()` / `checkInDeclined()`
* vocabulary; a plain `askHuman` pause is answered with whatever that tool's
* author documented. Typed `unknown` because the library does not get to
* decide what a tool asked for.
*/
readonly decision?: unknown;
/**
* The conversation this request CLAIMS to belong to — caller data, exactly as
* the transport declared it (a JSON field, a header, a path segment).
*
* It is **not identity** and must never be trusted as identity on its own:
* anyone who can reach the host can put any string here, including someone
* else's. Authenticate the caller by your own means, then check that the
* authenticated principal is allowed this session, before you serve it.
*/
readonly sessionId?: string;
/**
* WHO the transport says is calling — the end user's id, exactly as the
* transport declared it (9.12.0).
*
* A different fact from {@link sessionId} beside it, and the difference is
* the whole reason it is a second field: a session is a THREAD and this is a
* PERSON. One conversation belongs to one user; one user has many
* conversations; and an audit trail that reports the thread where the actor
* belongs names the wrong party in a way nobody can see from the outside.
*
* **Absent when absent.** No wire derives it from the session id, from the
* body, or from anything else — a request that carried no user is a request
* with no `userId`, and inventing one would be worse than reporting none.
*
* **How much it is worth is the transport's answer, not this port's.** A
* managed runtime whose front door authenticates the caller and forwards the
* result is a transport whose wire can fill this in, and its adapter does. A
* container you expose directly is not: a header there is a string anybody
* can send, and the generic JSON wire therefore reads none — a wire that
* promoted one to "who did this" would hand every caller the ability to sign
* somebody else's name. Which is which is decided in the adapter, by whoever
* knows what stands in front of it.
*/
readonly userId?: string;
/**
* An artifact operation this request carries INSTEAD of a message (9.23.0)
* — `head` (the claim ticket's metadata) or `get` (metadata + payload), for
* one ref.
*
* Its PRESENCE is the discriminant, exactly as {@link decision}'s is: a
* request carrying `artifact` redeems a ticket and never starts or resumes
* a run — `input` and `decision` do not ride it. A handler that serves
* artifacts answers with {@link HostReply.artifact}; a handler that does
* not should refuse it by name rather than treat it as a message, because a
* redemption silently answered by a model turn is a caller told nothing and
* billed anyway. `standingAgent` answers it: the ref is resolved against
* the serving agent's store under the requesting session's
* identity-composed scope — exactly the scope the run's own tools used.
*/
readonly artifact?: ArtifactWireRequest;
/**
* A session-history operation this request carries INSTEAD of a message
* (9.26.0) — `list` (the verified caller's own sessions) or `transcript`
* (one owned session's messages).
*
* Its PRESENCE is the discriminant, exactly as {@link artifact}'s is: a
* request carrying `session` reads history and never starts or resumes a
* run. Both ops REQUIRE a door that verifies identity
* ({@link StandingAgentBaseOptions.identity}) — reading "your" conversations
* from an unverified header is enumeration — and both are refused by name
* where the session store keeps no owner index.
*/
readonly session?: SessionWireRequest;
/**
* Transport headers with lower-cased names, as delivered. Present so a
* handler can map its own conventions (a correlation id, a tenant) without
* the port having to guess which ones matter.
*
* Since 9.26.0 this is also where a door that verifies identity reads the
* caller's credential from — `authorization: Bearer `, the one
* vocabulary every transport in this package normalizes onto. The port still
* does not interpret headers itself; it hands them to whoever was configured
* to.
*/
readonly headers?: Readonly>;
/** Aborted when the caller goes away. */
readonly signal?: AbortSignal;
}
/**
* The one reply a request gets. Exactly one of {@link HostReply.complete},
* {@link HostReply.awaiting}, {@link HostReply.artifact} or
* {@link HostReply.fail} ends it; a second call is ignored rather than allowed
* to corrupt the wire.
*
* Three terminals for a RUN, because a run has three ends and only three: it
* answered, it stopped to ask a person something, or it failed. Before
* `'flowchart-v1'` there was nowhere to keep a paused run, so the middle one
* was delivered through `fail` — an error standing in for unfinished work. It
* is a terminal of its own now, and a pause is never reported as a failure
* again. The fourth terminal ends a request that never was a run:
* {@link HostRequest.artifact} redeems a claim ticket, and `artifact(...)` is
* how the resolved ticket comes back.
*/
export interface HostReply {
/** Deliver the final answer and end the reply. */
complete(output: string): void;
/**
* End the reply with **unfinished work**: the run stopped to ask a person
* something, the paused run is stored, and a later request carrying
* {@link HostRequest.decision} continues it.
*
* This is not a failure and must not be reported as one. The agent did not
* break, no work was lost, and there is nothing to retry — there is a question
* outstanding. An adapter that maps this onto a 5xx, an error counter or a
* dead-letter queue is telling every dashboard it feeds something that is not
* true.
*
* Optional on the TYPE for the same reason {@link HostReply.emit} is: a
* minimal adapter need not implement it. Every shipped adapter does. When it
* is absent the composer still STORES the paused run — the store is not the
* transport's business — and ends the reply with a named refusal instead, so
* the pause is never lost merely because the wire could not describe it.
*/
awaiting?(pending: PendingAsk): void;
/**
* End the reply with a **resolved artifact** (9.23.0): the metadata for a
* `head`, metadata + payload for a `get`. The terminal a request carrying
* {@link HostRequest.artifact} ends through when the ref resolved; a ref
* that did not resolve ends through `fail` with the one indistinguishable
* not-found refusal.
*
* Optional on the TYPE for the same reason {@link HostReply.awaiting} is: a
* minimal adapter need not implement it, and every shipped adapter does.
* When it is absent the composer ends the reply with a named refusal
* (`ArtifactNotCarriedError`) instead — the resolution is not lost quietly
* merely because the wire could not describe it.
*/
artifact?(result: ArtifactWireResult): void;
/**
* End the reply with **resolved session history** (9.26.0): the caller's own
* sessions for a `list`, one owned session's messages for a `transcript`.
*
* The terminal a request carrying {@link HostRequest.session} ends through.
* A session the verified caller does not own does not end here — it ends
* through `fail` with the one indistinguishable not-found, because "exists
* but not yours" is an oracle for which ids are real.
*
* Optional on the TYPE for the same reason {@link artifact} is: a minimal
* adapter need not implement it, and every shipped adapter does. When it is
* absent the composer ends the reply with a named refusal
* (`SessionsNotCarriedError`) rather than improvising a body shape no client
* was written against.
*/
sessions?(result: SessionWireResult): void;
/**
* A piece of the answer, as it is produced.
*
* Optional on the TYPE so a minimal adapter need not implement it — every
* shipped adapter does. Whether the caller SEES the pieces as they arrive is
* the whole difference between hosts, and that is what `'streaming'` in
* {@link AgentHost.capabilities} reports. A host without it buffers what it is
* handed and the authoritative `complete(output)` is what the caller
* receives; the buffer is settled by the completion, never sent alongside it,
* because a chunk is a preview of the same text and delivering both would
* hand the caller the answer twice.
*
* Handler code is identical either way: emit freely, complete once.
*/
emit?(chunk: string): void;
/** End the reply with a failure. */
fail(error: Error): void;
}
/**
* What you hand {@link AgentHost.serve}. Throwing is treated exactly like
* calling `reply.fail(err)` — a handler that throws is a failed request, never
* a hung one.
*/
export type HostHandler = (request: HostRequest, reply: HostReply) => void | Promise;
/** A live host. */
export interface HostHandle {
/**
* Stop taking new requests, let the in-flight ones finish, then release the
* transport. Idempotent, so a shutdown hook and an explicit close can
* coexist. Requests arriving after it are refused with a
* {@link HostClosedError} naming the adapter.
*
* "Release the transport" is as strong as the port can be, because an
* adapter may be serving on a transport the CALLER owns rather than one it
* created. Releasing one of those means handing the routes back — never
* closing it — and a request arriving afterwards is the caller's to answer,
* not this host's to refuse.
*/
close(): Promise;
}
/**
* The port: something that can carry requests to one handler and carry its
* replies back.
*/
export interface AgentHost {
/**
* Which adapter this is. Every refusal names it, so an error tells you WHO
* refused rather than leaving you to guess which layer you are looking at.
*/
readonly name: string;
/** What this adapter can do beyond the baseline. Feature-detect; never assume. */
readonly capabilities: readonly HostCapability[];
/** Start serving. Resolves once the host is actually live. */
serve(handler: HostHandler): Promise;
}
/**
* A door that stays open: one session-scoped, two-way channel.
*
* The distinction this type exists for, in the words of the field report that
* bought it: **`HostRequest → HostReply` is one exchange, and this door is a
* conversation.** A request has one reply and then it is over. A conversation
* has neither side taking turns by rule, no reply count, and an end that either
* side can call.
*
* ── Frames are STRINGS here, deliberately ───────────────────────────────────
* What the frames MEAN is the consumer's contract, not this port's: one
* consumer pushes tool calls down the channel, another speaks a standardized
* agent↔UI protocol, a third exchanges long-running task updates. JSON is what
* all three happen to use and none of them agree on beyond that, so the port
* carries text and stays out of it. Binary is a capability question, deferred
* until a consumer needs it rather than guessed at now.
*
* ── What is NOT here ────────────────────────────────────────────────────────
* No authentication, no chunking, no heartbeat, no protocol framing. See
* {@link ConversationLimits} for why the last two are absences with a reason
* rather than gaps.
*/
export interface HostConversation {
/**
* The conversation this channel CLAIMS to belong to — caller data, exactly as
* the transport declared it, and **not identity**. The whole of
* {@link HostRequest.sessionId}'s warning applies here word for word: anyone
* who can reach the host can put any string here, including someone else's.
*
* A conversation and a request carrying the same string are the same
* session's, as far as this port is concerned. What that entitles either of
* them to is yours to decide, above the port.
*/
readonly sessionId?: string;
/**
* Transport headers with lower-cased names, as delivered — so a handler can
* map its own conventions without the port guessing which ones matter.
*
* This is also where an adapter puts credentials that its transport spells
* some other way: a bearer token a browser could only send as a subprotocol
* arrives here as an ordinary `authorization` header, because a port field
* spelled the way one vendor spells it is how a port stops being one.
*/
readonly headers?: Readonly>;
/**
* Host → far side. One frame, delivered whole.
*
* Refuses BY NAME rather than dropping quietly in two cases: a conversation
* that has ended (`ConversationClosedError`), and a frame past the ceiling the
* adapter declared (`FrameTooLargeError`). A dropped frame on a channel that
* looks open is the failure mode this port exists to make impossible.
*/
send(frame: string): void;
/**
* Far side → host. Returns an unsubscribe.
*
* Frames that arrive BEFORE the first subscriber are held and delivered to
* it, up to the bound the adapter declares
* ({@link ConversationLimits.maxPendingBytes}) — an `async` handler that
* awaits anything before subscribing would otherwise silently lose the far
* side's opening frame, which on a channel whose first frame is a greeting is
* every conversation.
*/
onFrame(cb: (frame: string) => void): Unsubscribe;
/**
* The end, delivered exactly once per subscriber — including to a subscriber
* that arrives after it already happened, which is answered immediately
* rather than never.
*/
onClose(cb: (reason: ConversationClose) => void): Unsubscribe;
/**
* End it politely: flush what is queued, tell the far side, then stop.
* Idempotent — the first call owns the ending and later ones do nothing.
*/
close(reason?: string): void;
}
/**
* How a conversation ended, in terms every transport can answer.
*
* `by` is the fact a consumer branches on and the reason there is no numeric
* code here: what all three of "a browser-parked channel", "a UI protocol" and
* "a long-running task exchange" need to know is whether the far side said
* goodbye, whether we did, or whether it broke — and a transport's own numbers
* answer that only if you already know that transport. Adapters render their
* own vocabulary (a close code, a timeout, a ceiling) into {@link reason}.
*/
export interface ConversationClose {
/**
* - `'far-side'` — they ended it.
* - `'host'` — we did, through {@link HostConversation.close}.
* - `'transport'` — nobody ended it; it broke or timed out.
*/
readonly by: 'far-side' | 'host' | 'transport';
/** What was said about it, when anything was. */
readonly reason?: string;
}
/**
* What you hand {@link ConversationHost.serveConversations} — called once per
* conversation, with that conversation.
*
* Throwing ends THAT conversation with a stated reason and never the host: one
* caller's bad frame is not an outage.
*/
export type ConversationHandler = (conversation: HostConversation) => void | Promise;
/**
* The ceilings a door imposes, **declared rather than discovered**.
*
* ── Why the port does not just handle them ──────────────────────────────────
* A transport that caps frame size or idles out must SAY so, and then get out
* of the way. Hiding a 32KB cap inside auto-chunking would be the adapter
* deciding a protocol question for every consumer at once — how a message is
* split, how the pieces are numbered, how the far side knows the last one has
* landed — and those answers differ per consumer. Same for liveness: a
* heartbeat is frames on somebody's protocol, and inventing them puts bytes on
* the wire that the consumer's parser never agreed to.
*
* So the port's job is to make the ceiling VISIBLE and let the layer above act:
* chunk above the port, heartbeat above the port.
*
* ── Enforced vs reported ────────────────────────────────────────────────────
* A door enforces what it IS and reports what it SITS BEHIND, and the doc on
* each field says which. Absent means "no ceiling this adapter knows of", never
* "no ceiling" — the runtime in front of you may have one it never told us
* about.
*/
export interface ConversationLimits {
/**
* Largest single frame this door carries, in bytes of UTF-8.
*
* **Enforced, both directions**, by the door that declares it: an inbound
* frame past it ends the conversation with a stated reason, and
* {@link HostConversation.send} past it refuses by name instead of
* truncating or silently splitting.
*
* A frame the transport delivered in pieces counts in TOTAL — the port's
* frame is the whole message, not the transport's packet, so fragmentation
* cannot be used to walk around the ceiling.
*/
readonly maxFrameBytes?: number;
/**
* How long the transport tolerates silence before it closes the channel.
*
* **Reported, not imposed.** The door declaring it usually is not the thing
* enforcing it — a runtime's front door idles a socket out long before the
* process inside notices — and a consumer that needs the channel to stay up
* sends its own heartbeat frames on its own protocol. Making that possible is
* the whole reason this number is written down.
*/
readonly idleMs?: number;
/**
* How much the door holds for you before the first
* {@link HostConversation.onFrame} subscriber exists, in bytes.
*
* **Enforced**, and a ceiling on the DOOR rather than on the transport: the
* pre-subscribe buffer that stops an `async` handler from losing the opening
* frame is a queue somebody else fills and this process pays for, so it gets
* a number and a stated overflow instead of growing until the host dies. Past
* it, the conversation ends with a reason naming this bound.
*
* Bounded in BYTES rather than in frames on purpose: a frame count would
* still admit `count × maxFrameBytes` of memory, which is the same unbounded
* queue with an extra step.
*/
readonly maxPendingBytes?: number;
}
/**
* The port: something that can carry conversations to one handler.
*
* It sits BESIDE {@link AgentHost} rather than inside it, because a transport
* that can carry a request cannot necessarily carry a conversation, and one
* that carries conversations need not answer requests at all. A host that does
* both implements both and declares `'conversation'` in
* {@link AgentHost.capabilities}.
*/
export interface ConversationHost {
/** Which adapter this is. Every refusal names it. */
readonly name: string;
/** What this adapter can do beyond the baseline. Feature-detect; never assume. */
readonly capabilities: readonly HostCapability[];
/**
* What this door caps, as declared facts. Absent means this adapter knows of
* no ceiling — never that there is none.
*/
readonly conversationLimits?: ConversationLimits;
/**
* Start taking conversations. Resolves once the door is actually open.
*
* The handle's `close()` ends every live conversation politely and then
* releases the door.
*/
serveConversations(handler: ConversationHandler): Promise;
}
/**
* A session packed for storage.
*
* `format` names WHAT is inside, so a reader that does not know the shape
* refuses BY NAME instead of restoring a session it cannot actually read.
* Formats are ADDED, never redefined: an old runtime meeting a new format says
* so and stops, which is the only safe thing it can do with a payload it cannot
* interpret. Two exist:
*
* • `'conversation-v1'` — a conversation and only a conversation. Every turn
* that ran to an answer stores this.
* • `'flowchart-v1'` — a run that stopped mid-flow to ask a person something:
* the engine's own checkpoint, the conversation as of the pause, and the
* outstanding ask. 7.14 shipped the version field for exactly this day, and
* said so; this is that day.
*
* The union is discriminated on `format`, so a reader that switches on it is
* exhaustive by construction and a third format tomorrow breaks the switch at
* compile time rather than at 3am.
*/
export type CheckpointEnvelope = ConversationEnvelope | PausedRunEnvelope;
/** A conversation packed for storage — what a turn that ANSWERED leaves behind. */
export interface ConversationEnvelope {
/** Names the shape of `data`. Unknown values are refused, never guessed at. */
readonly format: 'conversation-v1';
/** The conversation itself. */
readonly data: AgentRunCheckpoint;
/** Wall-clock when it was packed. Diagnostic. */
readonly savedAt: number;
}
/** A paused run packed for storage — what a turn that ASKED leaves behind. */
export interface PausedRunEnvelope {
/** Names the shape of `data`. Unknown values are refused, never guessed at. */
readonly format: 'flowchart-v1';
/** The paused run. */
readonly data: PausedRun;
/** Wall-clock when it was packed. Diagnostic. */
readonly savedAt: number;
}
/**
* A run that stopped to ask a person something, in the three pieces a session
* actually needs: what continues it, what it has said so far, and what it is
* waiting on.
*
* ── JSON, honestly ───────────────────────────────────────────────────────────
* A `FlowchartCheckpoint` is **JSON-safe to resume from, and not byte-identical
* through JSON.** `JSON.stringify` drops any property whose value is
* `undefined`, and a real paused agent run has a dozen of them. Every one
* measured sits in `executionTree` / `subflowResults` — the diagnostic halves
* the engine keeps for narrative and BTS. `sharedState`, which is the half
* `agent.resume()` actually reads, round-trips unchanged, because footprintjs's
* TypedScope already JSON-round-trips every object write on its way into
* committed state.
*
* So: store it anywhere that speaks JSON and resume works. Do not assert that
* what came back deep-equals what went in — `key: undefined` comes back as no
* key at all, and a test written to expect otherwise is testing `JSON`, not
* this library.
*/
export interface PausedRun {
/** The engine checkpoint — everything `agent.resume(checkpoint, decision)` needs. */
readonly checkpoint: FlowchartCheckpoint;
/**
* The conversation as of the pause, in the same shape every other turn stores.
*
* Kept alongside the checkpoint so a session that is waiting on a person is
* still a readable conversation: a support view can show what was said, and a
* runtime that cannot resume this run can still see the turn that led to the
* question.
*/
readonly conversation: AgentRunCheckpoint;
/** What the run is waiting on, as data. */
readonly pending: PendingAsk;
}
/**
* The question a paused run is waiting on — the part of a pause that is safe to
* hand to whoever asked.
*
* **It deliberately carries no checkpoint.** The engine checkpoint holds the
* entire shared state of the run: the system prompt, the whole conversation,
* every tool result. That belongs in the store, which the operator chose and
* controls, and not in a reply to whoever posted the request. The caller gets
* the question; the store gets the state.
*/
export interface PendingAsk {
/** The session holding the paused run — where the decision has to be sent back. */
readonly sessionId?: string;
/** The tool that asked, when the run recorded which one it was. */
readonly tool?: string;
/** The question in plain words, when the pause carried one. */
readonly question?: string;
/**
* Present ONLY when a tool declared `checkIn` — the typed ask plus its
* evidence pack (what the tool will do, what context the run read, which
* context drove the choice, the run so far). Answer with `checkInApproved()`
* / `checkInDeclined()`.
*/
readonly checkIn?: CheckInRequest;
/**
* Present ONLY when a `toolMiddleware` answered `ask` — the question and the
* middleware that put it. Answered with the same decision vocabulary a
* check-in uses, deliberately: a person approving is a person approving.
*/
readonly ask?: MiddlewareAsk;
/**
* Which REGISTERED screen component collects the answer (9.24.0) — the
* typed half of the question, lifted from whichever pause kind carried it
* so a screen has ONE place to look. Ids and props only, never markup: the
* registry lives in the frontend, and a screen that does not know the id
* falls back to the prose `question` exactly as before.
*
* `props` is the small inline half; `propsRef` is a claim ticket the screen
* redeems through the artifact wire (`head` then `get`) under the SAME
* session identity every other redemption presents — a 200-option picker
* rides the store, not this reply and not the checkpoint. The ref was
* validated to resolve when the ask was raised.
*
* The answer comes back through {@link HostRequest.decision} unchanged —
* the component changes how the question is asked, never what the answer
* is. A screen may render the decision as words; the words are display,
* the structured decision is the record.
*/
readonly component?: AskComponent;
/**
* Exactly what the tool passed to `askHuman()` / `pauseHere()`, uninterpreted.
* For a plain pause this is the whole of what the tool's author chose to say,
* and the library is not entitled to summarise it.
*/
readonly pauseData: unknown;
}
/**
* How often a run's progress is written to the session store — the trade
* between latency and how much a crash can cost you.
*
* - `'exit'` (default) — one write, when the run finishes. The behaviour every
* release before 7.19 had, spelled out rather than implied. A crash mid-run
* loses the whole turn.
* - `'async'` — a write is STARTED whenever the conversation changes and never
* waited on. The run never slows down; the store is behind by however much
* the newest un-landed write carried. At most one write is in flight and the
* newest snapshot supersedes any queued one, so what a crash leaves is always
* a PREFIX of the run, never a mixture.
* - `'sync'` — persist-then-proceed. The same trigger, but **iteration N's
* tools do not execute until iteration N-1's write has landed**, and the
* answer is not delivered until the last write has landed. You pay the
* store's latency once per iteration, knowingly, and in exchange the amount
* of work a crash can re-run has a number: **the current iteration, and
* nothing before it.**
*
* ── The bound, stated exactly ────────────────────────────────────────────────
* A commit boundary is a whole stage, and the agent dispatches ALL of one
* iteration's tool calls inside one stage body. So under `'sync'` a crash
* re-executes the tools of the iteration that was in flight — never an earlier
* one. That is the same idempotency requirement `resumeOnError` has always
* carried, now with a boundary instead of a warning: mutating tools must be
* idempotent, keyed on stable call content rather than `ctx.toolCallId`.
*/
export type DurabilityMode = 'exit' | 'async' | 'sync';
/**
* Why a session is being woken.
*
* - `'invoke'` — a request arrived for that session.
* - `'resume'` — that request carries a person's decision for a run which
* paused earlier.
* - `'artifact'` — that request redeems an artifact ref, and the session's
* stored identity is needed to compose the scope it resolves under. Fired
* only when the resolution actually reads the store (a request carrying a
* `userId`); a session-only resolution composes its scope from the request
* alone and wakes nothing.
* - `'transcript'` — a verified owner is READING that session's messages back
* (9.26.0). Nothing runs and nothing is written; the store is woken because
* it is about to be read from, which is the only promise this hook ever
* made.
*
* `'resume'` was absent until 7.19, `'artifact'` until 9.23 and `'transcript'`
* until 9.26, because nothing could produce them: naming reasons nothing fires
* would be an interface describing a system that does not exist. Something
* produces each of them now.
*/
export type WakeReason = 'invoke' | 'resume' | 'artifact' | 'transcript';
/**
* The port: where a conversation lives between requests.
*
* Deliberately two required methods. Anything a real store also wants — a TTL,
* a scan, a delete — is that store's own API, not a demand this port makes of
* every store that will ever implement it.
*
* ── Claiming the port ───────────────────────────────────────────────────────
* The two required methods are what TypeScript checks. What a store has to
* *behave* like is a battery every shipped store runs against and any store
* can: `sessionLifecycleConformance`, exported from this same subpath. It was
* written after a flaw in THIS interface's semantics turned up in four
* independent adapters at once — every one of them tested, and every one of
* them tested only against its own doubles, which is precisely why none of
* them could see it. Run it against your store; a case your store cannot
* satisfy is DECLARED by name with its reason, never skipped.
*/
export interface SessionLifecycle {
/** The stored conversation, or `undefined` for a session that has none yet. */
hydrate(sessionId: string): Promise;
/**
* Store the conversation for this session. Last write wins — with ONE
* exception, and it is the only thing this method ever refuses.
*
* **A store that keeps an owner index refuses a turn signed by somebody
* other than the established owner** (`SessionOwnershipConflictError`,
* 9.36.1), and refuses it whole: neither the index nor the ENVELOPE changes.
* Until 9.36.1 the write-once rule protected only the index, so such a write
* kept the first writer's `owner` and stored the second writer's entire
* conversation — leaving the index naming one person and the stored
* conversation naming another. The person the index named listed it, opened
* it, and read the other one's conversation.
*
* A turn carrying a LEANER identity is not refused. It claims nobody, so it
* contradicts nobody, and the established owner stands — see `listByUser`.
*
* The composer refuses a foreign turn at the door long before this, when a
* verifier is configured. This is the store keeping the same promise for
* every caller that holds it directly, and for a door with no verifier.
*/
persist(sessionId: string, envelope: CheckpointEnvelope): Promise;
/**
* Called once per served request, before `hydrate`, for stores that need to
* spin something up before they can answer. Errors from it fail the request —
* a store that could not wake cannot be read from either.
*/
onWake?(sessionId: string, reason: WakeReason): void | Promise;
/**
* OPTIONAL (9.26.0) — the sessions this user owns, newest first.
*
* Feature-detected, never assumed. The two required methods are a key/value
* map and most stores are exactly that; demanding a secondary index of every
* implementation that will ever exist would be this port breaking its own
* rule ("anything a real store also wants is that store's API, not a demand
* this port makes"). A store that leaves this absent makes
* `{ op: 'session-list' }` refuse BY NAME, naming the store's limitation —
* never answer "you have no sessions", which is an answer nobody could
* distinguish from the truth.
*
* **Ownership is derived, never declared.** `persist` takes no owner and
* gains none: a store fills its index from the stored envelope itself
* (`envelopeOwner`), which reads the `principal` on the conversation the
* composer put there. A store that let a caller state an owner would be a
* store where owning somebody's session is a matter of asking for it.
*
* **And established ONCE.** The first turn that signs for a conversation owns
* it; no later write moves that — not a leaner identity (which would erase
* it) and not a different one (which would transfer it). Every shipped store
* implements the index that way, and a custom one that let the last writer
* win would undo every ownership check made against it one turn later.
*
* The two halves of "no later write moves it" are NOT symmetrical, and
* 9.36.1 exists because they were treated as if they were:
*
* - a **leaner** turn is stored, and the owner is kept. It claims nobody, so
* it contradicts nobody. Refusing it would break a flow this contract
* blesses in the sentence above.
* - a **different** turn is REFUSED — index and envelope both. Keeping the
* index while storing the payload is not "not moving" the owner; it is
* moving the CONVERSATION out from under them, which is worse. Don't
* write this rule yourself: `resolveSessionOwner(sessionId, stored,
* incoming)` is the one implementation, and it is what every shipped store
* now calls.
*
* ── Which of the two says who owns a session ────────────────────────────────
* This index is the authority for LISTING and for the session-history ops:
* `ownerOf` is the question `{ op: 'session-transcript' }` asks, and nothing
* re-derives an answer behind it. An ordinary TURN does not ask it — it
* re-derives from the envelope it has just hydrated (`envelopeOwner`), which
* is the same derivation and one round-trip cheaper, and which is what keeps
* a store with NO index protected exactly as well as one with a fast one.
*
* Those two can never name different people: that is what the refusal above
* buys, and it is the only reason a turn is safe to answer from the envelope.
* They CAN differ in one direction — an index that names somebody over a
* conversation that names nobody, which is what a leaner turn leaves behind.
* That state only ever REFUSES: the owner can still list and read the
* session, and a turn on it is refused at any verifying door, for everybody.
* Stated because it is a real asymmetry and not a bug to be surprised by.
*
* A conversation that ran anonymously has no owner and appears in nobody's
* list. That is the honest consequence of deriving rather than inventing.
*/
listByUser?(userId: string, options?: SessionListOptions): Promise;
/**
* OPTIONAL (9.26.0) — who owns one session, or `undefined` for a session
* that does not exist OR names no owner.
*
* The deliberate ambiguity is the same one `ArtifactStore.get` makes:
* "missing" and "not yours" must be indistinguishable from the outside, and
* a store that answered them differently would hand a caller an oracle for
* which session ids are real.
*
* Implement it beside `listByUser` — a door that can list but not check
* ownership can hand somebody a list and then refuse to open any of it.
*
* This is the ownership INDEX, and it is not a second source of truth beside
* the identity embedded in the stored envelope: `persist` refuses any write
* that would make the two name different people. A store that answers this
* from something a caller supplied, rather than from what it derived, has
* built a permission system out of a request field.
*/
ownerOf?(sessionId: string): Promise;
/**
* OPTIONAL (9.42.0) — how conversations in this store stop existing.
*
* Feature-detected exactly like the two members above, and for the same
* reason: the two required methods are a key/value map, and demanding a
* retention mechanism of every implementation that will ever exist would be
* this port breaking its own rule ("anything a real store also wants is that
* store's API, not a demand this port makes"). A store that leaves this
* absent makes {@link sessionRetention} refuse BY NAME, naming the store's
* limitation and which shipped stores do implement it — never a silent
* no-op, which is the one failure mode a retention feature must not have,
* because from the outside "nothing was deleted" and "everything is fine"
* look identical until somebody asks how long you keep conversations.
*
* ── Why it is one member with two arms, and not one verb ────────────────────
* Because there are exactly two honest implementations of retention, and a
* port member only half the shipped stores could implement would not be a
* port member at all:
*
* - some stores **delete when you ask them to** — a `Map`, a table, a file.
* A sweep is one loop or one statement there, and the caller's cron
* decides when.
* - some stores **cannot honestly sweep, because the backend already
* expires rows on a policy the operator configured** out of band. Asking
* such a store to sweep would mean a query plus one delete per row, at an
* unbounded cost, duplicating a job the service does for free — and a
* store that reported "0 deleted" from a backend that deletes plenty
* would be lying by omission.
*
* So the answer is a DISCRIMINATED UNION and `deletedBy` is the field a
* consumer branches on: one feature check, then one exhaustive branch the
* compiler enforces. See {@link SessionRetention}.
*
* ── The shape that was deliberately NOT chosen ──────────────────────────────
* A stated expiry at persist time — `persist(id, envelope, { expiresAt })` —
* reads well and is the wrong shape twice over. It puts a demand on the one
* method every store must implement, so every store that will ever exist has
* to honour it; and it cannot be feature-detected, because `typeof
* store.persist === 'function'` is true whether or not the third argument is
* read. A store that ignored it would keep every conversation forever while
* its caller believed retention was configured. An optional member that is
* absent refuses; an optional ARGUMENT that is ignored does not.
*
* ── Why not the shape the ARTIFACT port already uses ────────────────────────
* That sibling port states an expiry at MINT (`ttlMs` → a stamped
* `expiresAt`) and sweeps a scope when a put next touches it. It is the right
* shape THERE because every artifact store in that family holds its own
* bytes, so every one of them can sweep. This family does not: two of its
* stores are managed services that expire rows on a policy nobody here
* controls, and one has no delete at all. A port copies its sibling's
* vocabulary only where the sibling's assumption also holds.
*
* ── What this is not ────────────────────────────────────────────────────────
* Not a wire op. No door in this package deletes conversations on request,
* and the composer never expires anything on its own: retention is an
* operator's job with an operator's blast radius, and a request that could
* delete somebody's history because it named a cutoff is not a feature.
*
* @example A cleanup job that works against any store that can answer
* const policy = sessionRetention(sessions);
* if (policy.deletedBy === 'this-store') {
* const { forgotten, more } = await policy.forgetOlderThan(Date.now() - THIRTY_DAYS);
* log(`forgot ${forgotten} conversations${more ? ', more remain' : ''}`);
* } else {
* log(`the backend expires these on '${policy.expiresOn}': ${policy.enableWith}`);
* }
*/
retention?(): SessionRetention;
}
/**
* How one store's conversations stop existing — the answer
* {@link SessionLifecycle.retention} gives, in the two shapes a store can
* honestly give it.
*
* `deletedBy` is the discriminant, and it is spelled as WHO does the deleting
* rather than as a mechanism, because that is the fact a caller acts on: one
* arm hands you something to call, the other hands you something to configure.
*/
export type SessionRetention = SessionSweep | SessionExpiryPolicy;
/**
* This store deletes, when you ask it to.
*
* The shape for a store that holds its own bytes — a map, a table, a file.
* Nothing sweeps on a timer of its own: a library that started an interval
* would be deciding your process's lifetime for you, and a sweep that runs
* without anybody asking is a deletion nobody can point at afterwards.
*/
export interface SessionSweep {
readonly deletedBy: 'this-store';
/**
* Forget every session whose stored `savedAt` is STRICTLY BEFORE `before`
* (epoch milliseconds), and say how many went.
*
* Strictly before, so that passing the same cutoff twice is stable and a
* session written exactly at the boundary survives — an off-by-one here
* deletes a conversation somebody is still holding.
*
* The clock is the ENVELOPE's `savedAt` — the same value the listing sorts
* on — and not the store's own wall clock. That makes a sweep a pure
* function of what is stored: the same cutoff over the same rows forgets the
* same conversations, in a test and at 3am.
*
* Bounded by `options.limit` ({@link DEFAULT_SWEEP_LIMIT} when absent), so a
* first sweep of a store that has been running for a year cannot become one
* unbounded transaction. `more` says whether another call has work to do.
*
* Deleting a session that is not there is not an error, and a sweep that
* matches nothing answers `{ forgotten: 0, more: false }` rather than
* refusing — a cleanup job runs on a schedule, and most of its runs have
* nothing to do.
*/
forgetOlderThan(before: number, options?: SessionSweepOptions): Promise;
}
/**
* The BACKEND deletes, on a policy an operator configured — this store only
* writes what that policy reads, and can say exactly what to turn on.
*
* There is nothing to call here on purpose. A method that did nothing but
* report "the backend handles it" would be a method somebody puts in a cron
* job, and a cron job that deletes nothing is worse than no cron job: it looks
* like retention is running.
*/
export interface SessionExpiryPolicy {
readonly deletedBy: 'the-backend';
/**
* Whether this store is actually stamping an expiry on every write.
*
* `false` means the mechanism exists and nothing is using it — the store was
* built without a retention setting, so the backend's policy has nothing to
* act on and conversations live until somebody deletes them. Reported rather
* than implied, because "there is a TTL field" and "things are expiring" are
* different facts and only one of them answers an auditor.
*/
readonly active: boolean;
/**
* What the backend's policy acts on — a stored field name, or the setting
* that starts the clock. Named so an operator can configure the policy
* against the right thing rather than guessing from a schema dump.
*/
readonly expiresOn: string;
/**
* What an operator does to turn the policy on, in one step they can act on
* — with this store's own collection, database or resource already filled
* in where the step needs one.
*
* A sentence rather than a code, for the same reason the refusals in this
* package print a whole command instead of naming a concept: the operator
* reading this is the person who can fix it in sixty seconds, and only if
* nobody makes them go and look it up first.
*/
readonly enableWith: string;
}
/** Options for {@link SessionSweep.forgetOlderThan}. */
export interface SessionSweepOptions {
/**
* The most sessions to forget in this call. Default
* {@link DEFAULT_SWEEP_LIMIT}; stores may cap it lower.
*
* A bound rather than "delete everything matching", because the first sweep
* after this feature is turned on is the biggest one that store will ever
* do, and it is the one most likely to hold a write lock long enough to be
* noticed by every request behind it.
*/
readonly limit?: number;
}
/** What one sweep did. */
export interface SessionSweepResult {
/** How many conversations this call forgot. */
readonly forgotten: number;
/**
* True when sessions older than the cutoff REMAIN — i.e. the limit was
* reached. A caller that loops until this is `false` drains the backlog in
* bounded steps; one that ignores it simply sweeps again next time.
*/
readonly more: boolean;
}
/**
* How many sessions one {@link SessionSweep.forgetOlderThan} forgets when the
* caller names no limit.
*
* Big enough that an ordinary nightly sweep finishes in one call, small enough
* that the first sweep of a neglected store is a series of short transactions
* rather than one long one.
*/
export declare const DEFAULT_SWEEP_LIMIT = 1000;
/** Paging for {@link SessionLifecycle.listByUser} — the cursor convention every
* listing in this package follows. */
export interface SessionListOptions {
/** Continuation token from a previous page. Omit for the first page. */
readonly cursor?: string;
/** Maximum rows this page. Stores may cap it lower. */
readonly limit?: number;
}
/** One page of a user's sessions. Never carries message content — a listing
* says WHICH conversations exist, and `session-transcript` says what is in
* one. */
export interface SessionListPage {
readonly sessions: readonly SessionSummary[];
/** Present iff more pages exist. */
readonly cursor?: string;
}
/**
* What to do when a request arrives for a session that already has a run in
* flight.
*
* - `'reject'` (default) — refuse it, naming the run that is already going.
* A user who double-submits gets one answer and one refusal, not two runs
* racing to write the same conversation.
* - `'enqueue'` — queue it. It starts after the active run has persisted, so
* the second turn sees the first turn's stored state rather than the state
* it was about to overwrite.
*
* This governs the SAME session only. A request for a different session is
* never refused — there is nothing wrong with it; it simply waits its turn.
*/
export type ConcurrentInvokePolicy = 'reject' | 'enqueue';
/**
* Everything {@link standingAgent} takes EXCEPT which agent answers — that is
* the one choice with two shapes, and it lives on
* {@link StandingAgentOptions}.
*
* Generic in the host's own handle type so composing does not cost you what
* the adapter told you. `nodeHost` hands back the URL it actually bound —
* which is the only way to find out when you asked for port `0` — and passing
* it through `standingAgent` keeps that, without the port having to know that
* "a URL" is a thing some adapters have.
*/
export interface StandingAgentBaseOptions| {
/** Where conversations live between requests. */
readonly sessions: SessionLifecycle;
/** What carries requests in. */
readonly host: AgentHost & {
serve(handler: HostHandler): Promise | ;
};
/** Default `'reject'`. See {@link ConcurrentInvokePolicy}. */
readonly onConcurrentInvoke?: ConcurrentInvokePolicy;
/**
* Verify WHO is calling, instead of believing a header (9.26.0).
*
* With this set, every request's `Authorization: Bearer ` is checked
* BEFORE the run's identity/scope is composed, and the proven user id is the
* one that reaches `EventMeta.principal`, `ctx.identity`, the memory
* namespace and the artifact scope. A request that NAMES a user it cannot
* prove is refused by name — never downgraded to anonymous, never served
* under the name it claimed.
*
* **It also decides whose sessions are whose.** With a verifier configured,
* EVERY door that opens a stored conversation asks one question first — the
* two session-history ops and an ordinary turn alike: does this session
* belong to the caller who proved who they are? A turn naming somebody
* else's session is refused with the same indistinguishable
* `SessionNotFoundError` a transcript gets, before a line of that
* conversation is hydrated into a model's context and before anything is
* written back. Ownership is the `principal` the first turn signed with, and
* no later turn moves it.
*
* The consequence to plan for: conversations stored BEFORE a door started
* verifying name no owner, so they cannot be continued at one that does. That
* is a loud refusal by design — the alternative is a door that hands old
* conversations to whoever names them first.
*
* Unset — the default — nothing changes: `HostRequest.userId` is read exactly
* as it has been since 9.12.0, and every release's behaviour before this one
* is byte-identical. Which is right depends on what stands in front of you,
* and that is a fact only the deployment knows.
*
* @example
* identity: { verify: jwksIdentity({
* jwksUrl: 'https://idp.example.com/.well-known/jwks.json',
* issuer: 'https://idp.example.com/',
* audience:'my-api',
* }).verify }
*/
readonly identity?: IdentityVerificationOptions;
/**
* Decide whether a request runs at all, BEFORE any model is called (9.26.0).
*
* Called once per turn with the verified caller, the session and what that
* caller has spent inside a rolling window; it answers `'allow'`,
* `{ queue: true }` (run it, but behind this session's other work) or
* `{ refuse: '' }`. `turnsPerHour({ limit })` is the shipped
* reference policy.
*
* Unset — the default — no accounting is kept, no listener is installed, and
* not one line of this runs.
*
* **The honest boundary, stated once:** accounting is PER PROCESS. Two
* containers each keep their own window, so a limit of 20 across three
* replicas is a limit of 60. Centralize by writing a policy that consults
* your own store — the `decide` seam is exactly where that goes.
*
* And the second one: a turn is counted when it is ADMITTED, so a request
* the lane then refuses as a concurrent run still counts against the
* window. Counting later would let a burst of simultaneous requests each be
* decided against a window none of them had joined.
*/
readonly admission?: AdmissionPolicy;
/**
* Where this door's ingress decisions go (9.32.0) — one record per request,
* including every request that never became a run.
*
* `auditExport()` is a record of RUNS, so a 401 from `identity.verify` and a
* 429 from `admission.decide` are not in it: they happen before an agent
* exists. Two independent review rounds named that gap. This is where the
* turned-away go — and where the SERVED are recorded too, because an absence
* is only readable against a census.
*
* **It does not join the audit hash chain.** Nothing here is hashed,
* sequenced or verifiable by `verifyAuditBundle`; it is a stream you chain
* into your own sink. Claiming otherwise would make this the very
* looks-like-evidence failure it exists to close.
*
* The record carries classes and identifiers only — never the token, never a
* header, never a claim set, never an error's message. See
* {@link IngressRecord} for the field-by-field contract and for the one thing
* it deliberately does not cover (a body the transport's own wire grammar
* refused before this composer ever saw it).
*
* Unset — the default — nothing is built, nothing is wrapped, and the reply
* the handler uses is the host's own object, to the byte.
*
* @example
* onIngressDecision: (record) => {
* if (record.outcome !== 'served') securityLog.write(record);
* }
*/
readonly onIngressDecision?: IngressSink;
/**
* How often a run's progress becomes crash-survivable. Default `'exit'` —
* one write when the run finishes, which is what every release before this
* one did. See {@link DurabilityMode} for what the other two buy and cost.
*
* Under `'exit'` nothing is attached to the agent at all: no observer, no
* per-commit work, no barrier. An agent served this way behaves and performs
* exactly as it did in 7.18.
*/
readonly durability?: DurabilityMode;
/**
* What `close()` does to the telemetry enabled on the agent (8.12.0).
* Default `'flush'`.
*
* - `'flush'` — drain every strategy the agent has enabled, and leave them
* running. This is the default because it is what everyone already
* believed happened: an exporter that batches otherwise loses whatever it
* had buffered when the server stops. Draining is safe on an agent this
* composer only BORROWED — it ships data and disables nothing.
* - `'flush-and-stop'` — drain, then release: timers cleared, clients
* closed, further events dropped. Say this only when the agent's life
* ends with the host, because stopping is terminal.
* - `'none'` — touch nothing, exactly as releases before 8.12.0 did.
*/
readonly shutdown?: 'flush' | 'flush-and-stop' | 'none';
/**
* Signals whose arrival should close this host (8.12.0). Off by default,
* and that is a deliberate refusal rather than an omission.
*
* **A library must not grab signals.** `process.on('SIGTERM', …)` is not
* observation: Node's default action for SIGTERM is to terminate, and
* ADDING any listener suppresses that default. A library that installs one
* behind your back can turn a container's graceful stop into a thirty-second
* wait for SIGKILL in an application that never asked for it. Handlers are
* process-global and cannot be arbitrated between libraries, ten runners
* would mean ten handlers and a `MaxListenersExceededWarning`, only Node has
* signals at all, and no library can know your exit policy.
*
* A composition root may, because it already owns the shape of the process
* — and this one asks first. When you pass signals here, this composer:
*
* 1. installs one listener per signal;
* 2. on arrival, runs the same `close()` you would have called — the host
* stops taking requests, in-flight runs finish, telemetry drains per
* {@link StandingAgentBaseOptions.shutdown};
* 3. removes its own listeners and RE-RAISES the signal, so the process
* dies exactly the way the platform expects rather than by an exit code
* this library invented.
*
* `close()` removes the listeners too, so a handle you close yourself leaves
* nothing installed.
*
* @example
* await standingAgent({ agent, sessions, host, shutdownOn: ['SIGTERM', 'SIGINT'] });
*/
readonly shutdownOn?: readonly NodeJS.Signals[];
}
/** How many sessions a `agentFactory` pool holds before it evicts the least
* recently used one. See {@link StandingAgentPoolOptions.maxActiveSessions}. */
export declare const DEFAULT_MAX_ACTIVE_SESSIONS = 100;
/**
* One shared agent, serving every session — the original shape, and still the
* right one for most deployments.
*
* An `Agent` instance holds per-run state on itself, so one instance can only
* be in one run at a time. That is why this shape SERIALIZES globally: not a
* tuning choice, a correctness requirement. See
* {@link StandingAgentPoolOptions.agentFactory} for the shape that runs
* sessions in parallel, and `standingAgent`'s own doc for the full comparison.
*/
export interface StandingAgentSharedOptions| extends StandingAgentBaseOptions | {
/**
* The agent that answers. ONE instance, shared by every session — which is
* why this shape runs one request at a time, globally (see
* {@link ConcurrentInvokePolicy} for the separate question of two turns of
* the SAME conversation).
*
* The composer only BORROWS it: `close()` drains its telemetry and leaves it
* usable, per {@link StandingAgentBaseOptions.shutdown}.
*/
readonly agent: Agent;
/** Refused beside `agent` — see {@link StandingAgentPoolOptions.agentFactory}. */
readonly agentFactory?: undefined;
/** Meaningless without a pool; refused rather than ignored. */
readonly maxActiveSessions?: undefined;
}
/**
* One agent PER ACTIVE SESSION, built on demand — sessions run in parallel
* (9.10.0).
*/
export interface StandingAgentPoolOptions | extends StandingAgentBaseOptions | {
/** Refused beside `agentFactory` — two spellings of one choice. */
readonly agent?: undefined;
/**
* Build an agent. Called once per session that arrives and is not already in
* the pool; the instance it returns serves that session and nobody else.
*
* ── What it buys ──────────────────────────────────────────────────────────
* **Sessions run at the same time.** Two people asking two questions are two
* instances and two runs; neither waits for the other. Within one session the
* turns still serialize — on that session's own instance — which is the same
* correctness rule the shared shape enforces globally, applied where it
* actually binds.
*
* ── What it costs ─────────────────────────────────────────────────────────
* An instance per active session: memory, and whatever your agent builds at
* construction (a provider client, tool wiring). The pool is bounded by
* {@link StandingAgentPoolOptions.maxActiveSessions} and evicts least
* recently used, which is invisible to the user because the CONVERSATION
* lives in the session store — an evicted session re-hydrates onto a fresh
* instance on its next request.
*
* ── The one law ───────────────────────────────────────────────────────────
* **Return a NEW agent every call.** A factory that hands back an instance it
* has already handed back is refused BY NAME on the spot: two sessions on one
* instance is the exact corruption the pool exists to prevent, and it would
* otherwise show up as one user's answer appearing in another user's
* conversation with nothing in the recording to say so. Build the agent
* INSIDE the factory; do not close over one.
*
* Instances the factory makes are the composer's, so it stops them — on
* eviction, and on `close()` unless
* {@link StandingAgentBaseOptions.shutdown} is `'none'`.
*
* @example
* await standingAgent({
* agentFactory: () => Agent.create({ provider, model }).system('…').build(),
* sessions: sqliteSessions({ file: './sessions.db' }),
* host: nodeHost({ port: 8080 }),
* maxActiveSessions: 200,
* });
*/
readonly agentFactory: () => Agent;
/**
* How many sessions hold an instance at once. Default
* {@link DEFAULT_MAX_ACTIVE_SESSIONS} (100). Must be a positive integer.
*
* When a new session arrives at a full pool, the least recently used session
* that is NOT running is retired: its tool sessions are closed with reason
* `'evicted'`, its agent is shut down, and its conversation stays in the
* session store. Nothing about that is visible to the person on the other
* end — their next message hydrates the same conversation onto a fresh
* instance.
*
* **A running session is never evicted.** If every session in the pool is
* busy, the pool grows past this number rather than tearing down a live run:
* the bound is on how many idle instances are RETAINED, and losing somebody's
* answer to a cache policy is not a trade this composer will make on your
* behalf. It comes back under the bound as soon as a run finishes.
*/
readonly maxActiveSessions?: number;
}
/**
* Options for {@link standingAgent} — pick ONE of the two agent shapes.
*
* - `{ agent }` — one instance shared by every session, serialized globally.
* See {@link StandingAgentSharedOptions}.
* - `{ agentFactory }` — one instance per active session, sessions in
* parallel. See {@link StandingAgentPoolOptions}.
*
* Passing both is refused by name at construction: they are two spellings of
* the same decision, and which one won would be invisible.
*/
export type StandingAgentOptions | = StandingAgentSharedOptions | | StandingAgentPoolOptions | ;
//# sourceMappingURL=types.d.ts.map | |