@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
176 lines (175 loc) • 7.61 kB
JavaScript
import { createCapability } from "./capabilities.js";
//#region src/activities/chat/middleware/run-store.ts
/**
* Run lifecycle types — the neutral home for what a "run" is.
*
* Shared by `@tanstack/ai-persistence` (which exposes a `runs` store through
* `withPersistence`) and `@tanstack/ai-sandbox` (whose run driver records run
* status). Living in core is what lets one `RunRecord` per run be shared by
* both, instead of each package keeping its own and disagreeing. Same rationale
* as `LockStore` (`packages/ai/src/locks.ts`), which is likewise a
* coordination primitive that core owns so that no consumer package has to.
*/
var TERMINAL = {
completed: true,
failed: true,
aborted: true
};
var ALL_STATUSES = {
running: true,
interrupted: true,
completed: true,
failed: true,
aborted: true
};
/**
* Whether `value` is a {@link RunStatus} — the guard a backend validates a row
* with at DESERIALIZATION.
*
* `RunStatus` is a compile-time claim about a storage column. A row arrives as
* JSON out of D1, a Durable Object, or Postgres, and nothing in the type system
* checked what that column actually held, so a `RunStore` implementation should
* run its row's `status` through this before handing the record on. The readers
* downstream act DESTRUCTIVELY on the answer — `@tanstack/ai-sandbox`'s journal
* sweep DELETES the journal of a run it believes terminal — so a row that lies
* about its status is not a display bug.
*/
function isRunStatus(value) {
return typeof value === "string" && Object.hasOwn(ALL_STATUSES, value);
}
/**
* Whether `status` means no further events will be appended. Narrows, so a
* caller inside the guard can pass `status` where a {@link TerminalRunStatus}
* is required without a cast.
*
* `Object.hasOwn`, never `in`: `in` walks the prototype chain, so a row whose
* `status` column held `'toString'` or `'constructor'` would be reported
* terminal. `status` is TYPED `RunStatus`, but every value reaching here comes
* off a user-implemented {@link RunStore} and the type is only a claim (see
* {@link isRunStatus}). A false `true` deletes a live run's journal
* (`@tanstack/ai-sandbox`'s journal sweep), fails its attach as `'terminal-run'`
* (`attach-preflight`), and refuses to drive it (`stream-to-response.ts`).
*/
function isTerminalRunStatus(status) {
return Object.hasOwn(TERMINAL, status);
}
/**
* Type a {@link RunStore} implementation inline: pass the object and get
* autocomplete plus contract checking with no separate annotation. Mirrors
* `defineLock` / `defineSandboxInstanceStore`.
*
* The generic return preserves the argument's own type, so an optional method
* the implementation actually provides stays known-present on the result
* instead of collapsing back to `| undefined` on the interface.
*/
function defineRunStore(store) {
return store;
}
/**
* Whether the current run can be DETACHED rather than destroyed when its client
* disconnects — `true` only when some middleware has both a {@link RunStore} and
* a durable event log wired (`withSandbox`'s `runs` + `durability.adapter`).
*
* Lives in core for the same reason `LockStore` does: it is a coordination fact
* that two consumer packages must agree on, and neither may depend on the other.
* `@tanstack/ai-sandbox` provides it; `@tanstack/ai-persistence` reads it to
* decide whether an abort is terminal (`'aborted'`) or a detach (write nothing).
* A persistence → sandbox import would be a layering inversion.
*
* Consumers read it with `{ optional: true }`: absent means "not detachable",
* which is every app that has not wired durability.
*
* Typed `true`, not `boolean`: ABSENCE is the negative, so a published `false`
* has no meaning — and a consumer that tests PRESENCE rather than the value
* would read one as "detachable". Narrowing the payload makes that
* unrepresentable instead of merely undocumented.
*/
var DetachableRunCapability = createCapability()("detachable-run");
/**
* Destructured accessors: `getDetachableRun(ctx, { optional: true })` /
* `provideDetachableRun(ctx, true)`.
*/
var [getDetachableRun, provideDetachableRun] = DetachableRunCapability;
/**
* Whether this run's teardown DID detach — the disconnect was survived, the
* agent is still working, and a later attach can take the run over.
*
* The past-tense counterpart of {@link DetachableRunCapability}, and the two must
* not be confused:
*
* - **detachABLE** is published at `setup`, and only says a disconnect *may* be
* survived (a `RunStore` and a durable log are wired).
* - **detachED** is published on the ABORT path, by the middleware that actually
* makes the call — `withSandbox`'s `onAbort`, which is the only actor that has
* resolved BOTH out-of-band cancel bands (`AbortInfo.cancelRequested` and
* `wasCancelRequested` on the record) and `detachOnDisconnect`. An explicit
* cancel, a non-detachable disconnect, an error, and a normal finish all leave
* it unpublished.
*
* Its consumer is the durable DELIVERY sink in `stream-to-response.ts`: a
* detached run's log must stay OPEN and un-terminalized so the takeover can
* continue it (see `wasRunDetached` in `../../../delivery-detach`). Reading it
* is safe and race-free only because a `for await` over the chat stream awaits
* the generator's `return()` — and therefore the whole `onAbort` chain — before
* the sink's own `finally` runs.
*
* Read with `{ optional: true }`: absent means "not detached", which is every
* other exit path and every app that has not wired durability.
*
* Typed `true`, not `boolean`, for the same reason as
* {@link DetachableRunCapability}: absence is the only negative, so publishing
* `false` must not be representable.
*/
var RunDetachedCapability = createCapability()("run-detached");
/**
* Destructured accessors: `getRunDetached(ctx, { optional: true })` /
* `provideRunDetached(ctx, true)`.
*/
var [getRunDetached, provideRunDetached] = RunDetachedCapability;
/** In-memory {@link RunStore}. Single process only. */
var InMemoryRunStore = class {
runs = /* @__PURE__ */ new Map();
createOrResume(input) {
const existing = this.runs.get(input.runId);
if (existing) return Promise.resolve(existing);
const record = {
runId: input.runId,
threadId: input.threadId,
status: input.status ?? "running",
startedAt: input.startedAt
};
this.runs.set(record.runId, record);
return Promise.resolve(record);
}
update(runId, patch) {
const existing = this.runs.get(runId);
if (existing) this.runs.set(runId, {
...existing,
...patch
});
return Promise.resolve();
}
get(runId) {
return Promise.resolve(this.runs.get(runId) ?? null);
}
listByThread(threadId) {
const matching = [...this.runs.values()].filter((run) => run.threadId === threadId).sort((a, b) => a.startedAt - b.startedAt);
return Promise.resolve(matching);
}
listReclaimable(opts) {
const cutoff = opts.now - opts.ttlMs;
const matching = [...this.runs.values()].filter((run) => run.status === "running" && run.detachedSince !== void 0 && run.detachedSince <= cutoff);
return Promise.resolve(matching);
}
findActiveRun(threadId) {
let active = null;
for (const run of this.runs.values()) {
if (run.threadId !== threadId || run.status !== "running") continue;
if (active === null || run.startedAt > active.startedAt) active = run;
}
return Promise.resolve(active);
}
};
//#endregion
export { DetachableRunCapability, InMemoryRunStore, RunDetachedCapability, defineRunStore, getDetachableRun, getRunDetached, isRunStatus, isTerminalRunStatus, provideDetachableRun, provideRunDetached };
//# sourceMappingURL=run-store.js.map