agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
167 lines • 9.6 kB
TypeScript
/**
* artifacts/objectStore — the half every REMOTE OBJECT adapter shares.
*
* Two adapters put artifacts in somebody else's bucket. They speak different
* SDKs and dispatch different operations, and that is exactly where a vendor
* belongs — but the laws they follow must not fork, so the laws live here:
*
* 1. **The payload is the object body, and it is the CANONICAL BYTES** —
* the same bytes `meta.bytes` counts and a digest covers. Not a JSON
* envelope wrapping base64: a stored report should be downloadable with
* the vendor's own console and be the report. That choice is also what
* makes `getStream` possible at all — a body you have to parse is a body
* you cannot stream.
* 2. **The meta rides as ONE object-metadata entry** ({@link ARTIFACT_META_KEY}),
* an ASCII-safe JSON envelope. One key, one truth: a "helpful" second
* copy of `kind` in a sibling field is a second thing that can disagree.
* ASCII because user metadata travels as an HTTP header on at least one
* of these services — a label with an em-dash in it must not be
* mangled at the transport — and JSON-with-escapes rather than base64
* because an operator reading the console should be able to SEE what the
* ticket says.
* 3. **The metadata budget is checked at put and REFUSED by name.** Both
* services cap user metadata (the caps differ; each adapter states its
* own). A refusal that names the field to shorten beats a vendor's
* "MetadataTooLarge" at document 400, and beats silent truncation
* always.
* 4. **Missing means null; unreadable means an error.** A 404 is the
* port's deliberate "no data" ambiguity — but only when the call ASKED
* ABOUT ONE OBJECT. A write and a listing do not ask that question, so a
* 404 from one of them is the service saying something else entirely (the
* bucket does not exist, the endpoint is wrong); nobody downstream turns
* it into `null`, so letting it past the sanitizer hands the caller the
* SDK's own text — which contains the object key. That is what
* {@link NotFoundMeaning} makes each call site declare, and why the
* default is the safe one. Anything else — denied, throttled, a network
* failure — is NOT "no data" either, and answering `null` for it would
* report an empty scope over objects that exist. Each adapter maps its
* own SDK's missing-object shape; everything else raises through
* {@link objectSdkFailure}.
* 5. **Listings order by the SERVICE's own creation time**, which comes back
* free on a listing, and page by offset over that sorted list. The port
* promises newest-first ACROSS pages; a bucket's key order is
* lexicographic, so a cursor that was the service's own continuation
* token would page in the wrong order and call it paging.
*
* Nothing in this file imports an SDK, names a vendor, or knows a wire
* format. It is the part of an object adapter that can be unit-tested with
* nothing but strings.
*/
import type { PayloadShape } from './payload.js';
import { type ArtifactListOptions, type ArtifactListResult, type ArtifactMeta } from './types.js';
/** The single object-metadata key that carries the ticket. */
export declare const ARTIFACT_META_KEY = "af-artifact";
/**
* Encode a ticket for the object's metadata, refusing one that will not fit.
*
* @param meta the minted ticket.
* @param shape how the payload must be rebuilt from the body's bytes.
* @param budget the service's user-metadata cap, in bytes.
* @param adapter the factory name, for the refusal.
* @throws InvalidArtifactError when the encoded ticket exceeds the budget —
* naming the fields that are big enough to be the reason.
*/
export declare function encodeArtifactMetaValue(meta: ArtifactMeta, shape: PayloadShape, budget: number, adapter: string): string;
/**
* Read a ticket back off an object's metadata.
*
* `undefined` for anything this runtime must not interpret: no entry (not
* ours — a foreign object in a shared bucket is left alone), unparseable, a
* newer envelope version, or a ref that is not a minted ref. Refusing to
* interpret is the point: a half-read ticket would be a confident description
* of the wrong thing.
*/
export declare function decodeArtifactMetaValue(value: unknown): {
readonly meta: ArtifactMeta;
readonly shape: PayloadShape;
} | undefined;
/** Case-insensitive lookup in a metadata bag. At least one of these services
* lower-cases user-metadata keys in transit, so the key written and the key
* read back are not guaranteed to be the same string. */
export declare function readMetadataEntry(bag: Record<string, unknown> | undefined, key: string): unknown;
/** Has the calendar passed this ticket? The one expiry predicate both object
* adapters use, so "expired" cannot mean two things. */
export declare function isExpiredMeta(meta: ArtifactMeta, now: number): boolean;
/**
* Re-raise a failed SDK call WITHOUT its text (the `sdkFailure` law, applied
* to object storage).
*
* A cloud SDK reports transport and validation failures by echoing request
* detail into the message: the bucket, the KEY — which here contains the
* tenant, the principal and the conversation id — and sometimes a signed URL
* or a header the request carried. This library then hands that string to the
* LLM as a tool result, puts it on the commit log, and ships it to every sink
* attached. One echo and the scope tuple is in the conversation.
*
* So the text does not come through. What does is the part that is both safe
* and actionable: which operation failed, the exception's NAME, and the HTTP
* status. The original is deliberately NOT attached as `cause` — a cause
* travels into every serializer that walks own properties, which would undo
* all of this in one `JSON.stringify`.
*/
export declare function objectSdkFailure(adapter: string, operation: string, err: unknown): Error;
/**
* What a "not found" from ONE call is allowed to mean.
*
* The distinction is not academic: it decides whether an SDK error object
* leaves this library intact.
*
* • `'missing-object'` — the call named one object and asked whether it is
* there. Not-found is the port's deliberate "no data", so the SDK's own
* error travels back to the CALL SITE, which immediately converts it to
* `null`/`undefined`. It never leaves the adapter, so its text never reaches
* an event, a log, or a model.
*
* • `'not-an-answer'` — the call asked no such question (it wrote, or it
* listed). A not-found here means something the caller did not ask about is
* wrong — most often that the bucket itself is not there — and NOTHING
* downstream is waiting to convert it. It goes through
* {@link objectSdkFailure} like every other failure.
*
* This is the DEFAULT for exactly that reason: a call site that says nothing
* gets the sanitizer. Leaking has to be typed out on purpose, next to the
* `catch` that proves the error is caught.
*/
export type NotFoundMeaning = 'missing-object' | 'not-an-answer';
/**
* The one gate every object adapter runs its SDK failures through.
*
* Returns what to throw — the sanitized error for anything the caller is not
* standing by to interpret, the original ONLY for a genuine missing object at
* a call site that declared it asks about one, and our own refusals verbatim
* (an {@link InvalidArtifactError} is this library's sentence, already written
* for a human, and re-wrapping it would replace a teaching refusal with a
* transport complaint).
*
* Built once per adapter so the vendor's not-found detection is bound in one
* place: two call sites in the same adapter cannot end up disagreeing about
* what "missing" looks like.
*
* @param adapter the factory name, for the message.
* @param isMissingObject the adapter's own reading of its SDK's
* "no such object" — the one genuinely per-vendor part.
*/
export declare function objectFailurePolicy(adapter: string, isMissingObject: (err: unknown) => boolean): (operation: string, err: unknown, meaning: NotFoundMeaning) => unknown;
/** One row of a scope listing, before the tickets are paged. */
export interface ObjectListingRow {
readonly meta: ArtifactMeta;
/** The service's own creation time for the object (ms). The sort key —
* see law 5 in the module header. */
readonly serviceCreatedAt: number;
}
/**
* The port's paging law over a scope's rows: newest first, offset cursor.
*
* Sorted by the SERVICE's creation time (falling back to the ticket's own
* `createdAt` when a listing did not carry one), ties broken by ref so two
* objects written in the same millisecond still page deterministically.
*/
export declare function pageObjectListing(rows: readonly ObjectListingRow[], options: ArtifactListOptions | undefined): ArtifactListResult;
/** The offset a cursor names. Total: a cursor this store did not mint reads
* as "start at the beginning" rather than as an error — a caller cannot
* learn anything about another scope by guessing one. */
export declare function decodeOffsetCursor(cursor: string | undefined): number;
/** Validate a bucket name option at construction — the same shape of refusal
* every adapter here gives for a missing required option. */
export declare function assertBucketOption(adapter: string, option: string, value: unknown): string;
//# sourceMappingURL=objectStore.d.ts.map