agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
301 lines • 15.4 kB
TypeScript
/**
* artifacts/types — the claim-check shapes and the five-verb port.
*
* A tool stores its result under a REFERENCE and hands the model a claim
* ticket; the model routes tickets instead of hauling data. These are the
* shapes that make that sentence true: the ref (an opaque minted string),
* the metadata a consumer decides from (never the bytes), and the store
* port every backend implements.
*
* ── The constitution this port inherits (from `MemoryStore`) ────────────────
* 1. **Scope is always the first argument.** Every call takes an
* {@link ArtifactScope} — the same tenant/principal/conversation tuple
* memory scopes on — so stores enforce isolation at the boundary. A ref
* alone opens NOTHING; a wrong scope surfaces as "no data", never as a
* cross-tenant read.
* 2. **`get` returns `null` for missing OR expired.** Deliberate ambiguity:
* "no data" is the only actionable fact, and distinguishing the two would
* let a caller reason about another scope's contents.
* 3. **Reads return cursors, not unbounded arrays.** `list` pages.
* 4. **Methods return Promises uniformly**, so sync adapters can become
* async ones without breaking a caller.
*
* ── Five verbs, no more ─────────────────────────────────────────────────────
* `put · head · get · delete · list`. The moment this port grows `query()` or
* `transform()` we are building a database — compute belongs to the code leg,
* and that refusal is written here so a future round can be refused by
* citation.
*
* `head` earns its place because it IS the render-by-ref decision: a consumer
* picks what to do from `kind` and `bytes` without paying for the payload.
*
* ── …and two OPTIONAL ones (9.25.0) ─────────────────────────────────────────
* `putStream?` / `getStream?` are feature-detected members, not a sixth and
* seventh verb: a store that cannot move a payload without holding it whole
* LEAVES THEM ABSENT, and a consumer that calls one it did not detect gets a
* type error rather than a runtime surprise. An adapter must never fake a
* stream over bytes it already holds — a fake stream is an accepted-and-
* silently-wrong promise about memory. See {@link ArtifactStreamPutInput} for
* the two things streaming costs you (a stated `bytes`, and no digest).
*/
import type { MemoryIdentity } from '../memory/identity/types.js';
import type { DatasetTimeAxis } from '../time/axis.js';
/**
* The isolation tuple every artifact call presents — the SAME tuple memory
* scopes on (`{ tenant?, principal?, conversationId }`), under a name that
* says what it does here. One type, not a structural twin: two spellings of
* one scoping rule could disagree, and this one cannot.
*
* The framework composes it from the run's identity/session (an anonymous run
* scopes to its own runId; a session-bound run to its sessionId; an
* identity-carrying run to the caller's tenant/principal tuple). A tool never
* supplies it — `ctx.artifacts` is already bound.
*/
export type ArtifactScope = MemoryIdentity;
/**
* The ref the model speaks — an opaque MINTED string (`art_` + 22 random
* chars, ~26 total). Never content-addressed: the digest is metadata, never
* the key (content-as-key would collide two tenants' identical bytes into one
* object and could never name two generations of "the current dataset").
* The grammar has ONE owner: `naming.ts`.
*/
export type ArtifactRef = string;
/** Where an artifact came from — the join to the trace. Facts, never invented:
* absent fields mean the minting door genuinely did not have them. */
export interface ArtifactOrigin {
readonly runId?: string;
readonly toolCallId?: string;
}
/**
* The claim ticket's description — what a consumer needs to DECIDE, never the
* bytes. This is what `head` returns, what `list` rows are, and what every
* `artifacts.*` event carries (events never carry payloads).
*/
export interface ArtifactMeta {
readonly ref: ArtifactRef;
/** Consumer vocabulary — what this IS to whoever redeems it:
* `'dataset/rows'`, `'chart/spec'`, `'report/csv'`. Declared by the
* producer, never inferred. */
readonly kind: string;
/** MIME type of the payload: `'application/json'`, `'text/csv'`, … */
readonly mediaType: string;
/** Payload size in bytes (UTF-8 for text/JSON, byteLength for binary). */
readonly bytes: number;
/** The human name: `"Q3 sales by region"`. */
readonly label?: string;
/** `sha-256:<hex>` — integrity + idempotent re-put detection, computed at
* `put` when asked. Metadata, NEVER the key. Verified on `get`; a mismatch
* is a teaching refusal, never silent corruption. */
readonly digest?: string;
/** Unix ms when this artifact stops resolving — STATED at mint (from the
* store's ttl or the caller's own value, whichever is sooner), so consumers
* can reason about expiry instead of discovering it. Absent = no expiry. */
readonly expiresAt?: number;
/** The join to the causal record. */
readonly origin?: ArtifactOrigin;
/**
* Derivation FACTS — the refs this artifact was computed from. Validated at
* mint: naming a parent that does not resolve in the same scope is a
* refusal (a foreign key that cannot dangle at birth). Deliberately NOT a
* lineage-graph engine: walking parents is the consumer's fold over
* `head()`, and causation stays the trace's job.
*/
readonly parentRefs?: readonly ArtifactRef[];
/** The rows' TIME AXIS as the producer declared it — which column is time,
* how its values are written, and how each row summarises its interval.
* Validated at mint (a malformed one is refused, never repaired); absent =
* undeclared, and a consumer keeps its own heuristic. See `time/axis.ts`. */
readonly timeAxis?: DatasetTimeAxis;
/** Unix ms when the artifact was stored. */
readonly createdAt: number;
}
/** What `put` takes — everything on {@link ArtifactMeta} the CALLER owns.
* `ref`, `bytes`, `digest` and `createdAt` are the store's to stamp. */
export interface PutArtifactInput {
readonly kind: string;
readonly mediaType: string;
/**
* The payload. Strings and `Uint8Array` are stored byte-for-byte; any other
* value must be JSON-serializable (it is measured, digested and — in the
* durable adapters — persisted via JSON). A value JSON cannot carry is
* refused at `put` by name, never stored as an approximation.
*/
readonly data: unknown;
readonly label?: string;
/** Ask for an integrity digest, computed by the store at put. */
readonly digest?: 'sha-256';
/** Caller-stated expiry (unix ms). The store's own ttl may only TIGHTEN it. */
readonly expiresAt?: number;
readonly origin?: ArtifactOrigin;
readonly parentRefs?: readonly ArtifactRef[];
/** Declare the rows' time axis (see {@link ArtifactMeta.timeAxis}). */
readonly timeAxis?: DatasetTimeAxis;
}
/** Why an artifact left the store without its owner asking. */
export type ArtifactSweepReason = 'ttl' | 'max-bytes' | 'max-count';
/** One swept artifact — the fact a retention pass leaves behind. */
export interface SweptArtifact {
readonly ref: ArtifactRef;
readonly reason: ArtifactSweepReason;
readonly kind: string;
readonly bytes: number;
}
/**
* What `put` hands back: the ticket, plus everything retention swept to admit
* it. Sweeps ride the RESULT (collect during traversal, never post-process) so
* the capability layer can put each one on the record as it happens — a store
* that evicted silently would be a store that lies by omission.
*/
export interface ArtifactPutResult {
readonly meta: ArtifactMeta;
readonly swept: readonly SweptArtifact[];
}
/** What `get` returns when the ref resolves: the ticket and the payload. */
export interface ArtifactRecord {
readonly meta: ArtifactMeta;
readonly data: unknown;
}
/** Options for `list` — the cursor convention `MemoryStore.list` set. */
export interface ArtifactListOptions {
/** Continuation token from a previous page. Omit for the first page. */
readonly cursor?: string;
/** Maximum rows this page. Adapters may cap it lower. */
readonly limit?: number;
}
/** One page of tickets. Bytes never ride a listing. */
export interface ArtifactListResult {
readonly artifacts: readonly ArtifactMeta[];
/** Present iff more pages exist. */
readonly cursor?: string;
}
/**
* What a STREAMED put declares. Everything on {@link PutArtifactInput} the
* caller owns, minus the two things a store cannot honor without holding the
* payload whole:
*
* • **`bytes` is REQUIRED here** (it is stamped, not measured). Retention
* has to plan an eviction BEFORE the bytes arrive, and an object store
* has to declare a content length before it opens the upload — neither
* can wait for a stream to end. A store that can see the true count
* verifies it and refuses a mismatch by name rather than storing a meta
* that lies about its own payload.
* • **there is no `digest`.** A digest is computed over the whole canonical
* payload with the same primitive every adapter shares; a store that
* never holds the payload cannot produce one, and an incremental hash
* computed a second way would be a different promise wearing the same
* field name. Digest the source yourself, or use `put`.
*/
export interface ArtifactStreamPutInput {
readonly kind: string;
readonly mediaType: string;
/** The payload's exact byte length — stated by the producer, not measured. */
readonly bytes: number;
readonly label?: string;
/** Caller-stated expiry (unix ms). The store's own ttl may only TIGHTEN it. */
readonly expiresAt?: number;
readonly origin?: ArtifactOrigin;
readonly parentRefs?: readonly ArtifactRef[];
}
/** What `getStream` returns when the ref resolves: the ticket, and the
* payload's CANONICAL BYTES as a stream — the same bytes `meta.bytes` counts
* and a digest would cover, never a re-encoding.
*
* `meta.digest` (when the artifact carries one) describes these bytes but has
* NOT been checked against them: verification needs the whole payload, which
* is what this shape exists to avoid holding. It rides anyway so a caller who
* needs the guarantee can hash what it collected and compare — the loss is
* named, never silently traded. `get` remains the verifying read. */
export interface ArtifactStreamRecord {
readonly meta: ArtifactMeta;
readonly body: ReadableStream<Uint8Array>;
}
/**
* The port — five verbs, scope first, vendor-neutral, plus two optional
* streaming members. Adapters are the vendor layer; every backend
* (in-process, on-disk, embedded database, remote object storage) implements
* exactly this.
*/
export interface ArtifactStore {
/**
* Store a payload; mint and return the ticket. Validates the input (a
* malformed put is refused by name), validates `parentRefs` resolve in the
* SAME scope, measures, optionally digests, stamps `expiresAt` from the
* store's retention — and reports what retention swept to make room.
*/
put(scope: ArtifactScope, input: PutArtifactInput): Promise<ArtifactPutResult>;
/**
* The ticket without the payload — the render-by-ref decision. `null` for
* missing-or-expired (the deliberate ambiguity; both mean "no data").
*/
head(scope: ArtifactScope, ref: ArtifactRef): Promise<ArtifactMeta | null>;
/**
* The ticket and the payload. `null` for missing-or-expired. When the meta
* carries a `digest`, the payload is re-verified here — a mismatch throws
* {@link ArtifactIntegrityError}, never returns corrupt bytes as if whole.
*/
get(scope: ArtifactScope, ref: ArtifactRef): Promise<ArtifactRecord | null>;
/** Remove one artifact. No-op when it does not exist — deleting an absence
* is not an error, it is agreement. */
delete(scope: ArtifactScope, ref: ArtifactRef): Promise<void>;
/** Page through this scope's tickets, newest first. */
list(scope: ArtifactScope, options?: ArtifactListOptions): Promise<ArtifactListResult>;
/**
* OPTIONAL — store a payload the caller streams, without either side
* holding it whole. Absent on stores that cannot honor that promise; detect
* with `canPutArtifactStream(store)` before calling.
*
* The stream is consumed exactly once. Everything else is `put`'s law:
* `parentRefs` are proven first, retention plans against the DECLARED
* `bytes`, and the sweep rides the result.
*/
putStream?(scope: ArtifactScope, input: ArtifactStreamPutInput, body: ReadableStream<Uint8Array>): Promise<ArtifactPutResult>;
/**
* OPTIONAL — read a payload as a stream of its canonical bytes. `null` for
* missing-or-expired, exactly like `get`. Absent on stores that would have
* to read the payload whole to answer; detect with
* `canGetArtifactStream(store)`.
*
* A `digest` on the meta is NOT re-verified here — verification needs the
* whole payload, which is the thing this member exists to avoid. `get`
* remains the verifying read, and the difference is stated rather than
* silently traded.
*/
getStream?(scope: ArtifactScope, ref: ArtifactRef): Promise<ArtifactStreamRecord | null>;
}
/**
* A `put` named a parent that does not resolve in the same scope — a foreign
* key that would dangle at birth. Refused at mint, because a derivation fact
* that points at nothing is worse than no fact: every later consumer would
* inherit the lie.
*/
export declare class UnknownParentRefError extends Error {
readonly code: "ERR_UNKNOWN_PARENT_REF";
/** The parents that failed to resolve. */
readonly unresolved: readonly ArtifactRef[];
constructor(unresolved: readonly ArtifactRef[]);
}
/**
* A stored payload no longer matches the digest minted with it. Thrown by
* `get` instead of returning the bytes — corrupt data delivered as whole data
* is the accepted-and-silently-wrong failure, and the one thing a claim check
* must never do is honor a ticket with someone else's parcel.
*/
export declare class ArtifactIntegrityError extends Error {
readonly code: "ERR_ARTIFACT_INTEGRITY";
readonly ref: ArtifactRef;
/** The digest minted at put. */
readonly expected: string;
/** The digest of what the store actually holds now. */
readonly actual: string;
constructor(ref: ArtifactRef, expected: string, actual: string);
}
/**
* A `put` this store cannot honor as stated — a payload JSON cannot carry, a
* blank `kind`, a payload larger than the whole scope budget. Refused by name
* at the door: storing an approximation would be accepted-and-silently-wrong.
*/
export declare class InvalidArtifactError extends Error {
readonly code: "ERR_INVALID_ARTIFACT";
constructor(detail: string);
}
//# sourceMappingURL=types.d.ts.map