/** * 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 : (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 (`:`, * 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 | 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; } /** A handle on one document. `firestore.d.ts` line 1436. */ export interface FirestoreDocumentReferenceLike { readonly id: string; get(): Promise; /** Resolves to a `WriteResult`, which this adapter does not read. */ delete(): Promise; } /** 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; set(documentRef: FirestoreDocumentReferenceLike, data: Record): FirestoreTransactionLike; } /** One connected database. `firestore.d.ts` line 554. */ export interface FirestoreLike { collection(collectionPath: string): FirestoreCollectionLike; runTransaction(updateFunction: (transaction: FirestoreTransactionLike) => Promise): Promise; /** Closes the client's gRPC channels. See {@link FirestoreSessions.close}. */ terminate(): Promise; } /** * 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): 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; /** * 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; } /** * 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