agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
623 lines • 33.9 kB
TypeScript
/**
* firestoreSessions — conversations in Firestore, so a fleet shares them and
* nobody runs a database.
*
* The ladder this rung sits on is already in the package. `memorySessions()`
* loses everything on restart and says so. `sqliteSessions()` survives a restart
* on ONE machine and says so. `agentEngineSessions()` is a fleet store, but only
* for people who already own a Vertex reasoning engine. Firestore is the row for
* everyone else on this column: a serverless document database with no instance
* to size, no connection pool to tune, and a free tier — the plainest "many
* containers, one conversation" answer Google has.
*
* ── The shape, said plainly ─────────────────────────────────────────────────
* One collection. One document per session. Seven fields:
*
* sessionId · format · savedAt · envelope · owner · messageCount · expiresAt
*
* That is the SQLite table, moved. It is deliberately the same shape, because
* the two stores implement the same port under the same laws, and a reader who
* has understood one should not have to learn a second model to audit the other.
*
* The envelope rides as a JSON **string**, not as a nested map, and that is a
* decision rather than laziness. A `CheckpointEnvelope` carries arbitrary
* conversation JSON: keys chosen by a model's tool call, values that may be
* `undefined`, arrays inside arrays. Firestore refuses all three — a field name
* may not contain a dot, a tilde, a star, a slash, a bracket or a backtick;
* `undefined` throws unless the client was
* built with `ignoreUndefinedProperties`, and a directly nested array is not a
* representable value. Serialising once at the edge makes every one of those a
* non-event, at the cost of not being able to query INSIDE a conversation —
* which no caller of this port has ever asked to do.
*
* ── Why the document id is a hash ───────────────────────────────────────────
* A `sessionId` in this library is OPAQUE. It may be a UUID, an upstream
* gateway's correlation id, a path-shaped tenant key, or a unicode string a
* person typed. Firestore document names have rules: no `/`, not `.` or `..`,
* not matching `__.*__`, and at most 1500 bytes. A session id that broke any of
* them would fail at the wire on the one turn it mattered — or worse, two ids
* that differ only past a truncation point would silently become ONE
* conversation.
*
* So the document name is `sha256(domain + NUL + sessionId)` in hex — 64
* characters, always legal, injective for every input anybody will ever have.
* The raw id is stored in the `sessionId` FIELD, so a listing can hand it back
* and a console reader can still see whose document they are looking at.
*
* **This is not encryption, and it is important not to read it as any.** The
* conversation itself is stored in the clear; the hash is an addressing scheme,
* not a confidentiality control. Anyone who can read the collection can read
* every conversation in it, and the `sessionId` field beside the hash spells out
* the id the hash was made from. Two things it DOES cost an operator, stated
* because they are discovered at the worst moment otherwise:
*
* • you cannot look a session up in the Firestore console by typing its raw
* id — you have to query `sessionId == '…'`, or hash it yourself;
* • a document name carries no information a human can sort or scan by.
*
* Encryption at rest is Google's (always on, and configurable with CMEK).
* Access control is IAM's. Neither is this adapter's, and neither is implied by
* the hash.
*
* ── The composite index, and the error you get without it ───────────────────
* `listByUser` runs one server-side query — an equality on `owner`, ordered by
* `savedAt` descending, paged with a real Firestore cursor. Firestore's
* automatic single-field indexes do NOT serve that shape: an equality filter on
* one field ordered by another needs a COMPOSITE index, and until it exists the
* query fails with gRPC status 9, `FAILED_PRECONDITION`.
*
* The index, exactly:
*
* collection group : <your collection> (default: agentfootprint_sessions)
* fields : owner Ascending
* savedAt Descending
* __name__ Descending
*
* `__name__` is Firestore's document-name field. It is the tiebreaker this
* adapter orders by explicitly (see {@link FirestoreSessions.listByUser}), and
* an index's trailing `__name__` takes the direction of the last ordered field —
* so a console-generated index for `owner ASC, savedAt DESC` is the right one.
*
* gcloud firestore indexes composite create \
* --collection-group=agentfootprint_sessions \
* --field-config=field-path=owner,order=ascending \
* --field-config=field-path=savedAt,order=descending \
* --database='(default)'
*
* `--database` is spelled out rather than left to gcloud's default, and it must
* match the `database` this store was built with. gcloud assumes `(default)` when
* the flag is absent, so an operator on a NAMED database who follows a command
* without it creates the index somewhere else and gets the identical failure
* back, with nothing to suggest why.
*
* When the index is missing this adapter raises {@link FirestoreIndexMissingError},
* which prints that same line with your collection and database already filled
* in, and names those fields rather than restating the service's message — see
* {@link firestoreFailure} for why no Google text is ever echoed here.
*
* ── Retention: a SECOND timestamp, not a converted one (9.42.0) ─────────────
* A native Firestore TTL policy deletes a document when a field of type
* **Timestamp** is in the past. `savedAt` is a NUMBER — epoch milliseconds —
* so until this release an operator could not point a policy at this
* collection at all without a library change. The fix is a second field,
* `expiresAt`, written as a `Date` (the client stores one as a Timestamp) on
* every persist, holding `savedAt + expireAfterMs`. Give the store no
* `expireAfterMs` and the field is written as `null`, which a TTL policy
* ignores, and nothing about this release changes.
*
* **`savedAt` was NOT converted, and that is the load-bearing decision.** It is
* the ordering key of the composite index, the field the listing sorts by, and
* the first half of every cursor this store has ever minted (`<savedAt>:<docId>`,
* parsed with `parseFloat` and handed back to `startAfter` as a number).
* Converting it would have meant a new index, a new cursor grammar, and every
* pagination token issued by an earlier release becoming unreadable — but the
* decisive one is quieter: **Firestore's value ordering sorts by TYPE first.**
* Every number sorts before every timestamp. A collection holding old
* number-`savedAt` documents beside new timestamp-`savedAt` ones would list
* them in two blocks, all of one type then all of the other, with a person's
* conversations silently out of order by the type of the field rather than by
* when they were written. A second field costs one small write per turn and
* has no such shadow.
*
* So the migration consequence, plainly:
*
* • **Every document already stored stays readable and listable.** Nothing
* about `savedAt`, the index, the cursor or the query changed, and a
* document written by 9.33–9.41 is read by this release unchanged.
* • **Old documents do not start expiring.** They carry no `expiresAt`, and
* a TTL policy ignores a document whose field is missing. A conversation
* that is still being used gains one on its next turn; a conversation
* nobody ever writes to again keeps living. If those matter, delete them
* by hand once (a query on `savedAt` plus `forget`, or a `gcloud`
* bulk-delete) — this adapter will not walk your collection behind your
* back.
* • **The policy is still yours to create.** `retention().enableWith` prints
* the exact command with your collection and database in it.
*
* ── The ceiling, since a store should name its own ──────────────────────────
* A Firestore document is capped at 1 MiB. A conversation whose stored envelope
* approaches that is refused BY NAME before the write
* ({@link EnvelopeTooLargeError}) rather than being sent and rejected as an
* opaque `INVALID_ARGUMENT`. Nothing is ever truncated: half a conversation
* stored as if it were whole is the failure this whole file exists to avoid.
* Compaction (the builder's `.compaction({ … })`, or `.window()`) is the
* answer, and the refusal says so. Named from the builder deliberately: there
* is no `agent.compact()` to call, and a doc comment ships into the emitted
* `.d.ts`, so a method named here that does not exist is a method somebody
* types on hover.
*
* ── The laws it inherits rather than re-implements ──────────────────────────
* `checkEnvelope` runs on the way OUT and on the way IN, so an envelope whose
* `format` this runtime does not know is refused by name, and a session that is
* PRESENT but unreadable is refused by name too. Only a session that was never
* written hydrates as `undefined`. A conversation that exists and cannot be read
* must never be answered with a fresh start — from the outside that is
* indistinguishable from a brand-new user.
*
* Ownership is DERIVED from the stored envelope and established ONCE. See
* `persist` below: SQLite gets that from `COALESCE(sessions.owner,
* excluded.owner)`, Firestore has no such thing, and a `set({ merge: true })`
* would let the last writer win — which is exactly the bug, not a workaround
* for it. So the write is a transaction.
*
* ── How much of this is verified ────────────────────────────────────────────
* **Field-validated except the ownership refusal (2026-08).** An independent
* field trial ran THIS adapter against a real Firestore and exercised seven of
* its eight areas live: the round trip, the server-side indexed listing with
* its document-name tiebreak and real cursor, the missing-index refusal, the
* hashed document name, the size ceiling, ownership derived from the envelope,
* and `forget`.
*
* The eighth is named because under-claiming and over-claiming are the same
* defect: **the refusal of a turn signed by somebody else** — the
* `resolveSessionOwner` conflict this file's `persist` raises out of the
* transaction — was NOT exercised, because it did not exist when the trial
* ran. It is held by tests here, including the contested-write case of the
* shared conformance battery, and by nothing in the field. Two writers racing
* for one fresh session against a real Firestore, with the real client's
* transaction retry underneath, is exactly the shape a double models by
* assumption; the sibling Vertex adapter's equivalent was found in the field
* and not in a test suite.
*
* This section said "NOT field-validated. Nothing here has been run against a
* live Firestore" for two releases after that stopped being true. Under-claiming
* is the safe direction and it is still the same defect class the trials keep
* reporting: a status that does not track the evidence is a status nobody can
* use, whichever way it is wrong.
*
* The pin is the `firestoreSessions` row of `GOOGLE_SURFACE_PINS` in
* `test/adapters/google/googlePin.ts`, asserted by
* `test/adapters/google/google-surface-pin.test.ts`. Its 18 members were
* hand-verified against a real `@google-cloud/firestore` 9.0.0 install in a
* scratch project OUTSIDE this repository: seventeen read off
* `types/firestore.d.ts` before a line of this file was written, and
* `DocumentSnapshot.id` verified afterwards, when a review found the row had
* pinned `DocumentReference.id` — real, but a member this adapter never reads —
* in place of the one the cursor actually reads.
*
* That package is deliberately NOT installed here. It depends on
* `@opentelemetry/api`, so installing it hoists that package to the repository
* root and disarms `test/observability-providers/otel.test.ts`, which proves
* `otelObservability()` refuses BY NAME when `@opentelemetry/api` is absent.
* The consequence has to be said plainly: **the reality assertion — "every
* pinned member really exists on the real package" — SKIPS in this repository.**
* It runs in full for anyone who installs `@google-cloud/firestore` locally.
*
* So what is machine-checked in CI is the SHAPE pin, not the reality pin: this
* adapter dispatches exactly the members the row names and no others, every run,
* everywhere. That the row spells those members the way Google does is held by a
* hand check against a real install, not by a test that runs here.
*
* The DESIGN was informed, before any of that, by an EARLIER and separate trial
* of a DIFFERENT Firestore
* session adapter, which ran against a real Firestore and passed eight ownership
* and history checks — and whose own report named the defect this adapter does
* not reproduce: that adapter read every document for one owner, sorted them in
* the client, and applied an offset cursor. That works until one person has a
* lot of conversations, and then it costs a full read of all of them per page.
* What the trial proves is that the ownership and history SEMANTICS survive a
* real service; it proves nothing about this file's query, cursor, transaction
* or index, because that adapter had none of them.
*/
import type { SessionExpiryPolicy, SessionLifecycle } from '../../hosting/types.js';
/** One document as it came back. `firestore.d.ts` line 1605. */
export interface FirestoreDocumentSnapshotLike {
/** A PROPERTY, not a method — `readonly exists: boolean`. */
readonly exists: boolean;
readonly id: string;
/** `undefined` when the document does not exist. */
data(): Record<string, unknown> | undefined;
}
/** The answer to a query. `firestore.d.ts` line 2163. */
export interface FirestoreQuerySnapshotLike {
/**
* An ARRAY, already materialised. Not an async iterable, not a stream — the
* SDK has `Query.stream()` for that and this adapter does not use it. A
* double that hands back an async iterable here would be testing a client
* nobody ships.
*/
readonly docs: readonly FirestoreDocumentSnapshotLike[];
}
/** The query builder. Every method is SYNCHRONOUS and returns a NEW query
* (the SDK documents the immutability explicitly); only `get` is async.
* `firestore.d.ts` line 1714. */
export interface FirestoreQueryLike {
where(fieldPath: string, opStr: string, value: unknown): FirestoreQueryLike;
orderBy(fieldPath: string | unknown, directionStr?: 'asc' | 'desc'): FirestoreQueryLike;
startAfter(...fieldValues: unknown[]): FirestoreQueryLike;
limit(limit: number): FirestoreQueryLike;
get(): Promise<FirestoreQuerySnapshotLike>;
}
/** A handle on one document. `firestore.d.ts` line 1436. */
export interface FirestoreDocumentReferenceLike {
readonly id: string;
get(): Promise<FirestoreDocumentSnapshotLike>;
/** Resolves to a `WriteResult`, which this adapter does not read. */
delete(): Promise<unknown>;
}
/** A collection is a Query that can also mint document handles.
* `firestore.d.ts` line 2305. */
export interface FirestoreCollectionLike extends FirestoreQueryLike {
doc(documentPath: string): FirestoreDocumentReferenceLike;
}
/**
* The transaction handle. `firestore.d.ts` line 801.
*
* The asymmetry is the thing to get right in a double: `get` is ASYNC and
* answers a snapshot, while `set` is SYNCHRONOUS and answers the transaction
* itself for chaining. A double whose `set` returned a promise would let an
* adapter that forgot to sequence its writes pass a test it should fail.
*/
export interface FirestoreTransactionLike {
get(documentRef: FirestoreDocumentReferenceLike): Promise<FirestoreDocumentSnapshotLike>;
set(documentRef: FirestoreDocumentReferenceLike, data: Record<string, unknown>): FirestoreTransactionLike;
}
/** One connected database. `firestore.d.ts` line 554. */
export interface FirestoreLike {
collection(collectionPath: string): FirestoreCollectionLike;
runTransaction<T>(updateFunction: (transaction: FirestoreTransactionLike) => Promise<T>): Promise<T>;
/** Closes the client's gRPC channels. See {@link FirestoreSessions.close}. */
terminate(): Promise<void>;
}
/**
* The two exports this adapter needs from the package.
*
* `FieldPath.documentId()` is a STATIC that returns a sentinel; it is how a
* query orders by document name, and there is no string spelling of it that the
* client will accept in `orderBy`.
*/
export interface FirestoreConstructorLike {
new (settings?: Record<string, unknown>): FirestoreLike;
}
/** The module's shape, as this adapter loads it. */
export interface FirestoreSdkModule {
readonly Firestore: FirestoreConstructorLike;
readonly FieldPath: {
documentId(): unknown;
};
}
/** Options for {@link firestoreSessions}. */
export interface FirestoreSessionsOptions {
/**
* The Google Cloud project. Omit and the client reads it from the ambient
* environment the same way every Google client does (`GCLOUD_PROJECT`, or the
* Application Default Credentials).
*/
readonly project?: string;
/**
* Which Firestore database in that project. Omit for `'(default)'`.
*
* Worth setting deliberately: a project can hold several databases, and
* "conversations went to the wrong one" looks exactly like "conversations
* were lost".
*/
readonly database?: string;
/**
* The collection sessions live in. Default
* {@link DEFAULT_SESSION_COLLECTION}.
*
* A top-level collection name, not a path — this adapter does not nest
* sessions under another document, because a store that required a parent
* would be making a decision about your data model that the port never asked
* for.
*/
readonly collection?: string;
/**
* A Firestore client you already built.
*
* Most applications that reach for this adapter already have one, and two
* clients in one process means two sets of gRPC channels for no benefit. When
* you pass one, this store never terminates it — see
* {@link FirestoreSessions.close}.
*
* Passing this together with `project` or `database` is refused rather than
* silently ignored: those settings belong to whoever CONSTRUCTED the client,
* and accepting them here would let a caller believe they had switched
* databases.
*
* It does NOT remove the need for the package to be installed:
* `FieldPath.documentId()` is a static on the module, and there is no string
* spelling of `__name__` the client accepts in `orderBy`. In practice that
* costs nothing — anyone holding a client already has the package — but it is
* stated rather than discovered, because "I passed my own client, why is it
* still loading the module?" is a fair question to have answered here.
*/
readonly firestore?: FirestoreLike;
/**
* How long after its last turn a conversation should expire, in
* milliseconds. Omit and nothing expires (9.42.0).
*
* Setting it makes every write stamp {@link EXPIRES_AT_FIELD} —
* `savedAt + expireAfterMs`, as a timestamp — which is the field a **native
* Firestore TTL policy** reads. The policy itself is yours to enable, once,
* with the command {@link FirestoreSessions.retention} hands you; this store
* writes what it reads and never deletes a document on a clock of its own.
* Both halves are needed and neither is silent about the other: with no
* policy the field is inert data, and with no `expireAfterMs` the policy has
* nothing to act on.
*
* **The clock is the conversation's own `savedAt`, not this process's.** So
* the expiry is IDLE time: every turn pushes it out, and a conversation dies
* `expireAfterMs` after somebody last spoke, which is what a session
* retention rule almost always means. It also makes the stamp a pure
* function of the envelope — the same conversation stamps the same instant
* on any machine, whatever its clock says.
*
* **It does not reach backwards.** Documents written before this was
* configured carry no expiry field, and a TTL policy ignores a document
* whose field is missing — so old conversations expire only once they are
* written again. See the module header for what to do about the ones that
* never will be.
*
* Named `expireAfterMs` rather than `ttlMs` deliberately, even though the
* artifact stores in this package spell their dial the second way: theirs is
* stamped once at mint and measured from creation, and this one is measured
* from the LAST turn and moves on every write. One word for two clocks would
* be the cheaper name and the more expensive mistake.
*/
readonly expireAfterMs?: number;
/**
* @internal Test seam only — the `@google-cloud/firestore` module, injected.
* Lets the suite exercise every path (including the refusals) without a
* credential or a network. Not public API, and not a place to plug in another
* Firestore driver.
*/
readonly _sdk?: FirestoreSdkModule;
}
/** Where sessions live when the caller names no collection. */
export declare const DEFAULT_SESSION_COLLECTION = "agentfootprint_sessions";
/**
* The document field a native TTL policy is configured against (9.42.0).
*
* A SECOND field beside `savedAt`, never a change to it — see the module
* header for why converting the ordering key would have re-sorted every
* conversation already in the collection.
*
* Exported because an operator has to type this name into the console or the
* `gcloud` line, and a field name that lives only inside a string literal is a
* field name somebody will mistype at 2am.
*/
export declare const EXPIRES_AT_FIELD = "expiresAt";
/**
* The one step that turns the TTL policy on, with this store's own collection
* and database already in it.
*
* One spelling, shared by the retention answer and the documentation that
* quotes it, for the same reason the missing-index refusal prints the whole
* `gcloud` line: the person reading it can fix this in sixty seconds, and only
* if nobody makes them go and look up the flags first.
*
* `--database` is always printed, including for `(default)`, where gcloud
* would have assumed it — a project may hold several databases, and a policy
* created on the wrong one leaves conversations living forever with nothing to
* suggest why. Single-quoted, because `(default)` is bare parentheses and a
* syntax error in every shell this will be pasted into.
*/
export declare function ttlPolicyCommand(collection: string, database?: string): string;
/**
* Firestore's hard ceiling on one document, in bytes. Not ours — the service's.
*
* @see https://cloud.google.com/firestore/quotas
*/
export declare const FIRESTORE_MAX_DOCUMENT_BYTES = 1048576;
/**
* The largest stored envelope this adapter will attempt, in bytes.
*
* Below the real ceiling by a margin, because the document also carries five
* other fields, their NAMES, and the document's own path — all of which count
* toward Firestore's total. Refusing a little early with a sentence that says
* what to do beats sending a 1,048,570-byte envelope and getting back an
* `INVALID_ARGUMENT` that names nothing.
*/
export declare const FIRESTORE_MAX_ENVELOPE_BYTES: number;
/**
* A session store in Firestore.
*
* A {@link SessionLifecycle} plus the three things a real store owns beyond the
* port — forgetting, closing, and telling you where it is writing — because the
* port deliberately asks for two methods and leaves the rest to whoever
* implements it.
*/
export interface FirestoreSessions extends SessionLifecycle {
/** The collection these sessions live in. Useful in an incident. */
readonly collection: string;
/** Forget one session. A session that was never there is not an error. */
forget(sessionId: string): Promise<void>;
/**
* How conversations here stop existing: **the backend does it**, on a native
* TTL policy an operator enables once, against {@link EXPIRES_AT_FIELD}
* (9.42.0).
*
* Narrowed from the port's optional member to a required one, and to the one
* arm this store can be: a caller holding a `FirestoreSessions` needs no
* feature check and no branch, and gets a compile error rather than a
* runtime surprise if they reach for a sweep that does not exist here.
*
* `active` says whether this store was built with `expireAfterMs` and is
* therefore stamping anything; `enableWith` is the exact command, with this
* store's collection and database already filled in.
*/
retention(): SessionExpiryPolicy;
/**
* The document name one session id maps to — the sha-256 above.
*
* Exposed because the mapping is one-way and an operator in an incident needs
* it: this is the string to paste into the Firestore console to find one
* conversation. It is a pure function, it touches nothing, and it works on a
* closed store.
*/
documentIdFor(sessionId: string): string;
/**
* Stop using this store, and release the client's gRPC channels.
*
* **Async, unlike the other stores' `close()`, and that is not an
* inconsistency for its own sake.** Terminating a Firestore client closes
* channels; a Node process holding an open channel does not exit. A `void`
* return here would be a promise this adapter could not keep, so the shape
* says what actually happens and a shutdown hook can await it.
*
* Idempotent, and **final** — reading or writing afterwards refuses by name
* rather than quietly reconnecting, because a store that reopened behind you
* would hide a shutdown-ordering bug instead of surfacing it.
*
* A client you passed in yourself is NOT terminated: this store did not open
* it and does not get to decide when the rest of your application stops using
* it. Nothing is torn down on Google's side either — the documents outlive the
* process, which is the entire reason to use a managed store.
*/
close(): Promise<void>;
}
/**
* Raised when the query `listByUser` needs has no composite index yet.
*
* Its own class rather than a generic failure, because this is the ONE
* Firestore error an operator can fix in sixty seconds — and the only way they
* will know that is if the message says which index, on which collection, in
* which DATABASE, in which order. See the module header for the `gcloud` line.
*
* The database is named and the `--database` flag is always printed, including
* for `(default)`, where gcloud would have assumed it anyway. That is deliberate:
* a project may hold several Firestore databases, and an operator on a
* non-default one who follows a command with no `--database` creates the index on
* `(default)` and gets this identical error back. A refusal that teaches the
* wrong fix is worse than a bare failure, and one always-present flag costs
* nothing to be right.
*
* The service's own message carries a one-click creation link and is
* deliberately NOT echoed: it restates the failing query, and the failing query
* contains a user id. See {@link firestoreFailure}.
*/
export declare class FirestoreIndexMissingError extends Error {
readonly code: "ERR_FIRESTORE_INDEX_MISSING";
/** The collection whose index is missing. */
readonly collection: string;
/**
* The database it lives in, or `undefined` when this store did not build the
* client and therefore cannot know — see the message for what to do then.
*/
readonly database: string | undefined;
constructor(collection: string, database?: string);
}
/**
* Raised when a conversation is too big to be one Firestore document.
*
* Refused BEFORE the write, so the failure names the conversation and the fix
* rather than arriving as an opaque `INVALID_ARGUMENT` from the wire. Nothing is
* truncated on the way past: a conversation half-stored as if it were whole is
* the exact failure this file's other laws exist to prevent.
*/
export declare class EnvelopeTooLargeError extends Error {
readonly code: "ERR_ENVELOPE_TOO_LARGE";
/** The session that could not be stored. */
readonly sessionId: string;
/** How big its serialized envelope was. */
readonly bytes: number;
constructor(sessionId: string, bytes: number);
}
/**
* Conversations in Firestore — a fleet-shared session store with no instance to
* run.
*
* **Status: field-validated except the ownership refusal (2026-08).** An
* independent field trial ran this adapter against a real Firestore and
* exercised seven of its eight areas live — round trip, indexed and cursored
* listing, the missing-index refusal, the hashed document name, the size
* ceiling, derived ownership and `forget`. The eighth, the refusal of a turn
* signed by somebody ELSE, was added after the trial and is held by tests
* here and by nothing in the field. Every SDK member it calls was read off a
* real install of `@google-cloud/firestore` 9.0.0 and hand-verified there; the
* test that re-checks those names against the real package SKIPS in this
* repository, because the package is deliberately not installed here. What
* runs in CI is the dispatch pin. See the module header for the full account.
*
* @throws FirestoreIndexMissingError from `listByUser` until the composite index
* exists — see the module header for the exact index.
* @throws EnvelopeTooLargeError from `persist` for a conversation above
* {@link FIRESTORE_MAX_ENVELOPE_BYTES}. Never truncated.
*
* @example Conversations that expire 30 days after their last turn
* const sessions = firestoreSessions({
* project: 'my-project',
* expireAfterMs: 30 * 24 * 60 * 60 * 1000,
* });
* // Then, ONCE, as an operator — the store prints the exact command:
* console.log(sessions.retention().enableWith);
*
* @example A standing agent whose conversations are shared across instances
* import { standingAgent, nodeHost } from 'agentfootprint/hosting';
* import { firestoreSessions } from 'agentfootprint/hosting';
*
* const sessions = firestoreSessions({ project: 'my-project' });
* const handle = await standingAgent({
* agentFactory: () => buildAgent(),
* host: nodeHost({ port: 8080 }),
* sessions,
* });
* process.on('SIGTERM', () => void handle.close().then(() => sessions.close()));
*
* @example Reusing the Firestore client the application already has
* const sessions = firestoreSessions({ firestore: db, collection: 'chat_sessions' });
* // close() will NOT terminate `db` — this store did not open it.
*/
export declare function firestoreSessions(options?: FirestoreSessionsOptions): FirestoreSessions;
/**
* The document name for one session id — `sha256(domain ‖ NUL ‖ id)` in hex.
*
* A module-level pure function rather than a closure, so the same mapping is
* available to the store, to a test, and to an operator who needs it in a REPL.
* The NUL separator is what stops `domain + "a" + "bc"` and `domain + "ab" + "c"`
* from being the same input; a session id may legally contain anything else.
*
* See the module header for why this is an ADDRESSING scheme and not, in any
* sense, encryption.
*/
export declare function documentIdFor(sessionId: string): string;
/**
* The gRPC status of a failed call, as a NAME, wherever the client put it.
*
* Two spellings are accepted because two layers report it differently: the gax
* layer sets a numeric `code`, and some wrappers carry the name as a string. A
* classifier that read only one of them would quietly stop classifying the day
* the client is upgraded.
*/
export declare function grpcStatusOf(err: unknown): string | undefined;
/** Is this the service saying "that query has no index"? */
export declare function isFailedPrecondition(err: unknown): boolean;
/**
* Re-raise a failed Firestore call **without its text** — the same law the AWS
* and Vertex columns follow, re-aimed at a gRPC error.
*
* What comes through is the part that is both safe and actionable: which
* operation failed, which collection it was on, and the gRPC status name. What
* does not is the SDK's message, because a Firestore error restates the failing
* request — a document path, a filter value, a field — and those carry a user id
* and a whole conversation's state. An error thrown from an adapter reaches the
* model as a tool result AND rides the event stream to every sink attached to
* the agent.
*
* No credential is ever named. **The original is deliberately not attached as
* `cause`** — a cause travels with the error into every serializer that walks
* own properties, which would undo all of this in one `JSON.stringify`.
*/
export declare function firestoreFailure(operation: string, collection: string, err: unknown): Error;
//# sourceMappingURL=firestoreSessions.d.ts.map