UNPKG

agents

Version:

A home for your AI agents

709 lines (708 loc) 31.7 kB
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js"; import { t as LifecycleCapability } from "./capability-B4WbF81e.js"; import { t as _classPrivateMethodInitSpec } from "./classPrivateMethodInitSpec-qMjJ6sHQ.js"; import { SqlError } from "./sql-error.js"; //#region src/streams/errors.ts /** * Error classes for the Streams capability. Each carries a stable `name` so * hosts and tests can classify failures without depending on message text. */ /** * Thrown when `open()` targets a terminal stream, or an append reaches a * stream that settled (or was deleted) after the writer was created. * * @experimental The API surface may change before stabilizing. */ var StreamClosedError = class extends Error { constructor(streamId, detail) { super(`Stream "${streamId}" is closed: ${detail}`); this.name = "StreamClosedError"; this.streamId = streamId; } }; /** * Thrown when `read()` targets a stream that was never opened (or was * deleted). `status()` returns `null` instead, for existence probes. * * @experimental The API surface may change before stabilizing. */ var StreamNotFoundError = class extends Error { constructor(streamId) { super(`Stream "${streamId}" does not exist. Open it before reading, or use status() to probe for existence.`); this.name = "StreamNotFoundError"; this.streamId = streamId; } }; /** * Thrown when a chunk or metadata value is not JSON-serializable or exceeds * the configured size limit. * * @experimental The API surface may change before stabilizing. */ var StreamSerializationError = class extends Error { constructor(context, detail) { super(`Cannot serialize ${context}: ${detail}`); this.name = "StreamSerializationError"; } }; //#endregion //#region src/streams/streams.ts /** * Durable incremental output for Lifecycle Objects. `Streams` owns the * `cf_agents_streams` and `cf_agents_stream_blocks` tables: an ordered, * durable chunk log per stream with a monotonic cursor, replay-then-tail * reads, and terminal status that doubles as recovery evidence for the * Tasks capability (which composes through checkpointed cursors, never * imports). * * Streams consumes only the standard capability services — storage and * events. It needs no alarm, so it also works on facets. Live fanout is * in-isolate: a Durable Object executes in one isolate at a time, so every * concurrent reader shares the producer's isolate; readers that outlive an * isolate replay from their cursor on reconnect. */ const STREAM_SCHEMA_VERSION_KEY = "cf_agents:streams_schema_version"; const CURRENT_STREAM_SCHEMA_VERSION = 2; /** * A block row grows by UPDATE until its body reaches this many characters, * then the next append opens a new block. Sized well under the 2 MiB row * limit so a 1 MiB chunk always fits in a fresh block, and small enough * that a replay page parses one block at a time. */ const BLOCK_MAX_CHARS = 256 * 1024; /** Rows read per page while folding one stream's v1 chunk rows into blocks. */ const LEGACY_FOLD_PAGE_ROWS = 500; /** Default ceiling for one serialized chunk (1 MiB). */ const DEFAULT_MAX_CHUNK_BYTES = 1048576; const MAX_STREAM_ID_LENGTH = 256; const READ_BATCH_SIZE = 100; const DEFAULT_LIST_LIMIT = 100; const utf8 = new TextEncoder(); var _maxChunkBytes = /* @__PURE__ */ new WeakMap(); var _wakeups = /* @__PURE__ */ new WeakMap(); var _legacyChunkTable = /* @__PURE__ */ new WeakMap(); var _Streams_brand = /* @__PURE__ */ new WeakSet(); var _deleteHooks = /* @__PURE__ */ new WeakMap(); /** * Durable incremental output for a Lifecycle Object. * * `open()` a stream, `append()` chunks (synchronous durable writes that wake * live readers), and settle it with `close()` or `error()`. `read()` replays * persisted chunks from a cursor and then tails live appends; `status()` * reports the state and cursor — the recovery evidence a Task's `recover` * callback consults after its producer was interrupted. * * @experimental The API surface may change before stabilizing. */ var Streams = class extends LifecycleCapability { constructor(options = {}) { super("streams"); _classPrivateMethodInitSpec(this, _Streams_brand); _classPrivateFieldInitSpec(this, _maxChunkBytes, void 0); _classPrivateFieldInitSpec(this, _wakeups, /* @__PURE__ */ new Map()); _classPrivateFieldInitSpec(this, _legacyChunkTable, false); _classPrivateFieldInitSpec(this, _deleteHooks, []); _classPrivateFieldSet2(_maxChunkBytes, this, options.maxChunkBytes ?? 1048576); } /** Migrate stream storage during Lifecycle startup. */ async onStart() { const storage = this.lifecycle.storage; const version = await storage.get(STREAM_SCHEMA_VERSION_KEY) ?? 0; if (version < CURRENT_STREAM_SCHEMA_VERSION) { _assertClassBrand(_Streams_brand, this, _ensureTables).call(this); await storage.put(STREAM_SCHEMA_VERSION_KEY, CURRENT_STREAM_SCHEMA_VERSION); } if (version >= 1) _classPrivateFieldSet2(_legacyChunkTable, this, _assertClassBrand(_Streams_brand, this, _hasLegacyChunkTable).call(this)); } /** * Open a stream for writing. Idempotent on the id: reopening a live stream * returns a writer positioned at its current cursor; reopening a terminal * stream throws {@link StreamClosedError}. */ async open(streamId, options = {}) { await this.lifecycle.ready(); _assertClassBrand(_Streams_brand, this, _validateStreamId).call(this, streamId); const existing = _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId); if (existing) { if (existing.state !== "streaming") throw new StreamClosedError(streamId, `already settled as ${existing.state}`); if (options.tag !== void 0 && options.tag !== (existing.tag ?? void 0)) throw new Error(`Stream "${streamId}" is already open with tag ${JSON.stringify(existing.tag)}; refusing reopen with tag ${JSON.stringify(options.tag)}`); return _assertClassBrand(_Streams_brand, this, _writer).call(this, streamId); } const metadataJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, options.metadata, `metadata for stream "${streamId}"`); const now = Date.now(); _assertClassBrand(_Streams_brand, this, _sql).bind(this)` INSERT INTO cf_agents_streams (stream_id, state, tag, metadata, chunk_count, created_at, updated_at) VALUES (${streamId}, 'streaming', ${options.tag ?? null}, ${metadataJson}, 0, ${now}, ${now}) `; _assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:opened", { streamId }); return _assertClassBrand(_Streams_brand, this, _writer).call(this, streamId); } /** * Replay persisted chunks from `from` (inclusive), then tail live appends * until the stream settles. Ends when the stream reaches a terminal state * and every durable chunk has been yielded; a read of an `errored` stream * still yields its chunks and then simply ends — consult {@link status} * for the terminal outcome. Aborting `options.signal` throws its reason. */ async *read(streamId, options = {}) { const signal = options.signal; for await (const batch of this.readBatches(streamId, options)) for (const item of batch) { if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("Read aborted"); yield item; } } /** * Batched form of {@link read}: yields non-empty arrays of consecutive * chunks instead of one chunk at a time. Replay yields up to * `options.batchSize` chunks per array; a live tail yields everything * that accumulated since the last wakeup as one array — so a consumer * paying per write (an SSE flush, an RPC hop, a history append) pays * once per backlog, not once per chunk. Same lifecycle as {@link read}: * ends when the stream settles and every durable chunk has been * yielded; aborting `options.signal` throws its reason. */ async *readBatches(streamId, options = {}) { await this.lifecycle.ready(); const signal = options.signal; const batchSize = Math.max(1, Math.floor(options.batchSize ?? READ_BATCH_SIZE)); let next = Math.max(0, options.from ?? 0); let signaledUpToDate = false; if (_assertClassBrand(_Streams_brand, this, _state).call(this, streamId) === void 0) throw new StreamNotFoundError(streamId); for (;;) { if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("Read aborted"); const rows = _assertClassBrand(_Streams_brand, this, _readChunks).call(this, streamId, next, batchSize); if (rows.length > 0) { next = rows[rows.length - 1].seq + 1; yield rows.map((row) => ({ seq: row.seq, chunk: JSON.parse(row.chunk) })); } if (rows.length === batchSize) continue; if (!signaledUpToDate) { signaledUpToDate = true; options.onUpToDate?.(); continue; } const state = _assertClassBrand(_Streams_brand, this, _state).call(this, streamId); if (state === void 0) return; if (state !== "streaming") { if (rows.length === 0) return; continue; } await _assertClassBrand(_Streams_brand, this, _waitForWakeup).call(this, streamId, signal); } } /** Read one stream's state and cursor, or null when it does not exist. */ async status(streamId) { await this.lifecycle.ready(); const row = _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId); return row ? _assertClassBrand(_Streams_brand, this, _rowToStatus).call(this, row) : null; } /** List streams, newest first. */ async list(options = {}) { await this.lifecycle.ready(); const states = Array.isArray(options.state) ? options.state : options.state !== void 0 ? [options.state] : []; let query = "SELECT * FROM cf_agents_streams WHERE 1 = 1"; const params = []; if (states.length > 0) { query += ` AND state IN (${states.map(() => "?").join(", ")})`; params.push(...states); } if (options.tag !== void 0) { query += " AND tag = ?"; params.push(options.tag); } query += " ORDER BY created_at DESC, stream_id DESC LIMIT ?"; params.push(options.limit ?? DEFAULT_LIST_LIMIT); let rows; try { rows = this.lifecycle.storage.sql.exec(query, ...params).toArray(); } catch (cause) { throw new SqlError(query, cause); } return rows.map((row) => _assertClassBrand(_Streams_brand, this, _rowToStatus).call(this, row)); } /** * Delete one terminal stream and its chunk log. * * @returns True when a terminal stream was deleted; false when none * exists. Throws on a live stream — settle it first. */ async delete(streamId) { await this.lifecycle.ready(); const row = _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId); if (!row) return false; if (row.state === "streaming") throw new Error(`Cannot delete live stream "${streamId}"; close() or error() it first`); _assertClassBrand(_Streams_brand, this, _deleteRows).call(this, streamId); _assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:deleted", { streamId }); return true; } /** * @internal Synchronous storage operations for same-isolate first-party * machinery — today the chat `ResumableStream` adapter, whose whole public * surface is synchronous and constructed before the Lifecycle starts. * Bypasses `lifecycle.ready()`: the caller owns startup ordering. The * invariant-bearing writes (append fence, settlement, wakeups, events) go * through the same private methods as the public API, so live readers and * diagnostics observe aperture writes exactly like capability writes. Will * break without notice; never use from application code. */ __DO_NOT_USE_WILL_BREAK__sync() { return { ensureTables: () => _assertClassBrand(_Streams_brand, this, _ensureTables).call(this), getStream: (streamId) => _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId), insertStream: (streamId, tag, metadata) => { _assertClassBrand(_Streams_brand, this, _validateStreamId).call(this, streamId); const metadataJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, metadata, `metadata for stream "${streamId}"`); const now = Date.now(); _assertClassBrand(_Streams_brand, this, _sql).bind(this)` INSERT INTO cf_agents_streams (stream_id, state, tag, metadata, chunk_count, created_at, updated_at) VALUES (${streamId}, 'streaming', ${tag}, ${metadataJson}, 0, ${now}, ${now}) `; _assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:opened", { streamId }); }, append: (streamId, chunk) => _assertClassBrand(_Streams_brand, this, _append).call(this, streamId, chunk), lastChunkAt: (streamId) => _assertClassBrand(_Streams_brand, this, _tail).call(this, streamId).lastChunkAt, cursor: (streamId) => _assertClassBrand(_Streams_brand, this, _tail).call(this, streamId).nextSeq, onDelete: (hook) => { _classPrivateFieldGet2(_deleteHooks, this).push(hook); return () => { const index = _classPrivateFieldGet2(_deleteHooks, this).indexOf(hook); if (index !== -1) _classPrivateFieldGet2(_deleteHooks, this).splice(index, 1); }; }, settle: (streamId, state, reason, options) => _assertClassBrand(_Streams_brand, this, _settle).call(this, streamId, state, reason, options), deleteUnchecked: (streamId) => { if (_assertClassBrand(_Streams_brand, this, _deleteRows).call(this, streamId) > 0) _assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:deleted", { streamId }); _assertClassBrand(_Streams_brand, this, _wake).call(this, streamId); }, deleteMany: (streamIds) => { for (const streamId of streamIds) { _assertClassBrand(_Streams_brand, this, _deleteRows).call(this, streamId); _assertClassBrand(_Streams_brand, this, _wake).call(this, streamId); } }, readChunks: (streamId, fromSeq, limit) => _assertClassBrand(_Streams_brand, this, _readChunks).call(this, streamId, fromSeq, limit), listRows: () => _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT * FROM cf_agents_streams ORDER BY created_at DESC, rowid DESC `, rowsByTag: (tag, state) => state ? _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT * FROM cf_agents_streams WHERE tag = ${tag} AND state = ${state} ORDER BY created_at DESC, rowid DESC ` : _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT * FROM cf_agents_streams WHERE tag = ${tag} ORDER BY created_at DESC, rowid DESC `, importStream: (row) => { _assertClassBrand(_Streams_brand, this, _validateStreamId).call(this, row.streamId); const metadataJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, row.metadata, `metadata for stream "${row.streamId}"`); _assertClassBrand(_Streams_brand, this, _sql).bind(this)` INSERT INTO cf_agents_streams (stream_id, state, tag, metadata, chunk_count, created_at, updated_at, closed_at) VALUES (${row.streamId}, ${row.state}, ${row.tag}, ${metadataJson}, ${row.chunkCount}, ${row.createdAt}, ${row.updatedAt}, ${row.closedAt}) `; }, importChunk: (streamId, chunk, createdAt) => { const chunkJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, chunk, `chunk for stream "${streamId}"`); if (chunkJson === null) return; _assertClassBrand(_Streams_brand, this, _writeChunk).call(this, streamId, chunkJson, createdAt); } }; } }; function _writer(streamId) { const capability = this; return { streamId, get cursor() { return _assertClassBrand(_Streams_brand, capability, _tail).call(capability, streamId).nextSeq; }, append: (chunk) => _assertClassBrand(_Streams_brand, this, _append).call(this, streamId, chunk), close: (options) => _assertClassBrand(_Streams_brand, this, _settle).call(this, streamId, "completed", null, options), error: (reason, options) => _assertClassBrand(_Streams_brand, this, _settle).call(this, streamId, "errored", reason ?? null, options) }; } function _append(streamId, chunk) { const chunkJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, chunk, `chunk for stream "${streamId}"`); if (chunkJson === null) throw new StreamSerializationError(`chunk for stream "${streamId}"`, "chunks must not be undefined"); const state = _assertClassBrand(_Streams_brand, this, _state).call(this, streamId); if (state !== "streaming") throw new StreamClosedError(streamId, state !== void 0 ? `already settled as ${state}` : "it was deleted"); const seq = _assertClassBrand(_Streams_brand, this, _writeChunk).call(this, streamId, chunkJson, Date.now()); _assertClassBrand(_Streams_brand, this, _wake).call(this, streamId); return seq; } /** * Append one serialized chunk to the stream's open block, or open a new * block when the current one is full. Either way it is one billed row: * an UPDATE that grows the block body, or the INSERT of the next block. * Blocks are what make cleanup cheap — a stream of thousands of chunks * is a handful of rows to delete at cutover, not thousands. */ function _writeChunk(streamId, chunkJson, at) { const tail = _assertClassBrand(_Streams_brand, this, _blockTail).call(this, streamId); const seq = tail?.seq_to ?? 0; if (tail && tail.len + chunkJson.length + 1 <= BLOCK_MAX_CHARS) _assertClassBrand(_Streams_brand, this, _sql).bind(this)` UPDATE cf_agents_stream_blocks SET body = body || ',' || ${chunkJson}, seq_to = ${seq + 1}, updated_at = ${at} WHERE stream_id = ${streamId} AND block = ${tail.block} `; else _assertClassBrand(_Streams_brand, this, _sql).bind(this)` INSERT INTO cf_agents_stream_blocks (stream_id, block, seq_from, seq_to, body, created_at, updated_at) VALUES (${streamId}, ${(tail?.block ?? -1) + 1}, ${seq}, ${seq + 1}, ${chunkJson}, ${at}, ${at}) `; return seq; } /** * One page of chunks from `fromSeq`, in seq order, parsing one block at * a time. A block body is the chunks' JSON texts joined by commas, so * `[${body}]` parses straight back into the chunk values. */ function _readChunks(streamId, fromSeq, limit) { _assertClassBrand(_Streams_brand, this, _foldLegacyChunks).call(this, streamId); const rows = []; let block = -1; while (rows.length < limit) { const next = _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT block, seq_from, seq_to, body, updated_at FROM cf_agents_stream_blocks WHERE stream_id = ${streamId} AND block > ${block} AND seq_to > ${fromSeq} ORDER BY block ASC LIMIT 1 `[0]; if (!next) break; block = next.block; const chunks = JSON.parse(`[${next.body}]`); for (let seq = Math.max(fromSeq, next.seq_from); seq < next.seq_to && rows.length < limit; seq++) rows.push({ stream_id: streamId, seq, chunk: JSON.stringify(chunks[seq - next.seq_from]), created_at: next.updated_at }); } return rows; } /** * Remove a stream's row and log. Deletion hooks see the row and its * cursor first, so an owner can account for the segments before they are * gone; this is the single point every delete path passes through. */ function _deleteRows(streamId) { if (_classPrivateFieldGet2(_deleteHooks, this).length > 0) { const row = _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId); if (row) { const cursor = _assertClassBrand(_Streams_brand, this, _tail).call(this, streamId).nextSeq; for (const hook of _classPrivateFieldGet2(_deleteHooks, this)) hook(row, cursor); } } if (_classPrivateFieldGet2(_legacyChunkTable, this)) { _assertClassBrand(_Streams_brand, this, _sql).bind(this)`DELETE FROM cf_agents_stream_chunks WHERE stream_id = ${streamId}`; _assertClassBrand(_Streams_brand, this, _dropLegacyChunkTableIfEmpty).call(this); } _assertClassBrand(_Streams_brand, this, _sql).bind(this)`DELETE FROM cf_agents_stream_blocks WHERE stream_id = ${streamId}`; return _assertClassBrand(_Streams_brand, this, _sqlWrite).call(this, "DELETE FROM cf_agents_streams WHERE stream_id = ?", [streamId]); } /** * @returns Whether this call transitioned the stream (a repeat, or a * deleted stream, is a no-op and returns false: the caller's `commit` * does not run and nothing is discarded). */ function _settle(streamId, state, reason, options) { if (!options?.commit && !options?.discard) { const settled = _assertClassBrand(_Streams_brand, this, _settleRow).call(this, streamId, state, reason); if (settled) _assertClassBrand(_Streams_brand, this, _emitSettled).call(this, streamId, state, reason); _assertClassBrand(_Streams_brand, this, _wake).call(this, streamId); return settled; } let settled = false; let deleted = false; const hadLegacy = _classPrivateFieldGet2(_legacyChunkTable, this); try { this.lifecycle.storage.transactionSync(() => { settled = _assertClassBrand(_Streams_brand, this, _settleRow).call(this, streamId, state, reason); if (!settled) return; options.commit?.(); if (options.discard) deleted = _assertClassBrand(_Streams_brand, this, _deleteRows).call(this, streamId) > 0; }); } finally { if (hadLegacy && !_classPrivateFieldGet2(_legacyChunkTable, this)) _classPrivateFieldSet2(_legacyChunkTable, this, _assertClassBrand(_Streams_brand, this, _hasLegacyChunkTable).call(this)); } if (settled) _assertClassBrand(_Streams_brand, this, _emitSettled).call(this, streamId, state, reason); if (deleted) _assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:deleted", { streamId }); _assertClassBrand(_Streams_brand, this, _wake).call(this, streamId); return settled; } /** * The one guarded UPDATE that ends a stream. Settlement is the moment * the stream row becomes exact at rest: the same write stamps the final * cursor, read from the chunk log's tail in the same synchronous block. * While the stream was live, appends wrote only the chunk log — the * row's chunk_count and updated_at were not maintained per append. * @returns Whether the row transitioned from `streaming`. */ function _settleRow(streamId, state, reason) { const finalCursor = _assertClassBrand(_Streams_brand, this, _tail).call(this, streamId).nextSeq; return _assertClassBrand(_Streams_brand, this, _sqlWrite).call(this, `UPDATE cf_agents_streams SET state = ?, error_message = ?, closed_at = ?, updated_at = ?, chunk_count = ? WHERE stream_id = ? AND state = 'streaming'`, [ state, reason, Date.now(), Date.now(), finalCursor, streamId ]) > 0; } function _emitSettled(streamId, state, reason) { _assertClassBrand(_Streams_brand, this, _emit).call(this, state === "completed" ? "stream:closed" : "stream:errored", { streamId, ...reason !== null ? { reason } : {} }); } function _wake(streamId) { const waiters = _classPrivateFieldGet2(_wakeups, this).get(streamId); if (!waiters) return; _classPrivateFieldGet2(_wakeups, this).delete(streamId); for (const wake of waiters) wake(); } function _waitForWakeup(streamId, signal) { return new Promise((resolve) => { const waiters = _classPrivateFieldGet2(_wakeups, this).get(streamId) ?? /* @__PURE__ */ new Set(); _classPrivateFieldGet2(_wakeups, this).set(streamId, waiters); const wake = () => { signal?.removeEventListener("abort", onAbort); resolve(); }; const onAbort = () => { waiters.delete(wake); if (waiters.size === 0 && _classPrivateFieldGet2(_wakeups, this).get(streamId) === waiters) _classPrivateFieldGet2(_wakeups, this).delete(streamId); resolve(); }; waiters.add(wake); signal?.addEventListener("abort", onAbort, { once: true }); }); } function _validateStreamId(streamId) { if (typeof streamId !== "string" || streamId.length === 0) throw new Error("Stream ids must be non-empty strings"); if (streamId.length > MAX_STREAM_ID_LENGTH) throw new Error(`Stream id exceeds ${MAX_STREAM_ID_LENGTH} characters`); } function _serialize(value, context) { if (value === void 0) return null; let json; try { json = JSON.stringify(value); } catch (error) { throw new StreamSerializationError(context, error instanceof Error ? error.message : String(error)); } if (json === void 0) throw new StreamSerializationError(context, `value of type ${typeof value} has no JSON representation`); const bytes = utf8.encode(json).byteLength; if (bytes > _classPrivateFieldGet2(_maxChunkBytes, this)) throw new StreamSerializationError(context, `serialized size ${bytes} bytes exceeds the ${_classPrivateFieldGet2(_maxChunkBytes, this)}-byte limit`); return json; } function _sql(strings, ...values) { const query = strings.reduce((result, part, index) => result + part + (index < values.length ? "?" : ""), ""); try { return [...this.lifecycle.storage.sql.exec(query, ...values)]; } catch (cause) { throw new SqlError(query, cause); } } function _sqlWrite(query, params) { try { return this.lifecycle.storage.sql.exec(query, ...params).rowsWritten; } catch (cause) { throw new SqlError(query, cause); } } function _getStream(streamId) { return _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT * FROM cf_agents_streams WHERE stream_id = ${streamId} `[0]; } /** * One stream's state alone — the narrow read for the append fence and the * reader loop's liveness checks, which need neither the metadata column * nor the (live-stale) counters of the full row. */ function _state(streamId) { return _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT state FROM cf_agents_streams WHERE stream_id = ${streamId} `[0]?.state; } /** * The chunk log's tail: the next append sequence and the newest chunk's * timestamp. One PK-served read (`ORDER BY seq DESC LIMIT 1`), and the * single derivation point for every cursor and per-append-liveness * consumer — while a stream is live, the chunk log is authoritative and * the stream row's `chunk_count`/`updated_at` are not maintained. */ function _tail(streamId) { const tail = _assertClassBrand(_Streams_brand, this, _blockTail).call(this, streamId); return tail ? { nextSeq: tail.seq_to, lastChunkAt: tail.updated_at } : { nextSeq: 0, lastChunkAt: null }; } /** The open block: one PK-served read (`ORDER BY block DESC LIMIT 1`). */ function _blockTail(streamId) { _assertClassBrand(_Streams_brand, this, _foldLegacyChunks).call(this, streamId); return _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT block, seq_to, updated_at, length(body) AS len FROM cf_agents_stream_blocks WHERE stream_id = ${streamId} ORDER BY block DESC LIMIT 1 `[0]; } /** * Schema v1 → v2 is lazy: the per-chunk `cf_agents_stream_chunks` rows of * ONE stream are folded into blocks the first time that stream is * touched (a tail read before an append or settle, a replay, a delete). * Startup never pays for the whole legacy log, so an object that let a * large log accumulate still boots within its memory budget; each fold * pages through its stream's rows and writes whole blocks, not one row * per chunk. The table is dropped once the last stream's rows are gone. */ function _foldLegacyChunks(streamId) { if (!_classPrivateFieldGet2(_legacyChunkTable, this)) return; this.lifecycle.storage.transactionSync(() => { const tail = _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT block, seq_to FROM cf_agents_stream_blocks WHERE stream_id = ${streamId} ORDER BY block DESC LIMIT 1 `[0]; let block = (tail?.block ?? -1) + 1; let seq = tail?.seq_to ?? 0; let body = ""; let seqFrom = seq; let createdAt = 0; let updatedAt = 0; const flush = () => { if (body === "") return; _assertClassBrand(_Streams_brand, this, _sql).bind(this)` INSERT INTO cf_agents_stream_blocks (stream_id, block, seq_from, seq_to, body, created_at, updated_at) VALUES (${streamId}, ${block}, ${seqFrom}, ${seq}, ${body}, ${createdAt}, ${updatedAt}) `; block += 1; body = ""; seqFrom = seq; }; let after = -1; for (;;) { const rows = _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT seq, chunk, created_at FROM cf_agents_stream_chunks WHERE stream_id = ${streamId} AND seq > ${after} ORDER BY seq ASC LIMIT ${LEGACY_FOLD_PAGE_ROWS} `; for (const row of rows) { if (body !== "" && body.length + row.chunk.length + 1 > BLOCK_MAX_CHARS) flush(); if (body === "") createdAt = row.created_at; body = body === "" ? row.chunk : `${body},${row.chunk}`; updatedAt = row.created_at; seq += 1; after = row.seq; } if (rows.length < LEGACY_FOLD_PAGE_ROWS) break; } flush(); if (after >= 0) _assertClassBrand(_Streams_brand, this, _sql).bind(this)`DELETE FROM cf_agents_stream_chunks WHERE stream_id = ${streamId}`; _assertClassBrand(_Streams_brand, this, _dropLegacyChunkTableIfEmpty).call(this); }); } function _hasLegacyChunkTable() { return _assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'cf_agents_stream_chunks' `.length > 0; } function _dropLegacyChunkTableIfEmpty() { if (_assertClassBrand(_Streams_brand, this, _sql).bind(this)` SELECT COUNT(*) AS n FROM (SELECT 1 FROM cf_agents_stream_chunks LIMIT 1) `[0].n > 0) return; _assertClassBrand(_Streams_brand, this, _sqlWrite).call(this, "DROP TABLE cf_agents_stream_chunks", []); _classPrivateFieldSet2(_legacyChunkTable, this, false); } function _rowToStatus(row) { let cursor = row.chunk_count; let updatedAt = row.updated_at; if (row.state === "streaming") { const tail = _assertClassBrand(_Streams_brand, this, _tail).call(this, row.stream_id); cursor = tail.nextSeq; if (tail.lastChunkAt !== null && tail.lastChunkAt > updatedAt) updatedAt = tail.lastChunkAt; } return { streamId: row.stream_id, state: row.state, cursor, ...row.tag !== null ? { tag: row.tag } : {}, ...row.metadata !== null ? { metadata: JSON.parse(row.metadata) } : {}, ...row.error_message !== null ? { error: row.error_message } : {}, createdAt: row.created_at, updatedAt, ...row.closed_at !== null ? { closedAt: row.closed_at } : {} }; } function _ensureTables() { const rawSql = (query) => { try { this.lifecycle.storage.sql.exec(query); } catch (cause) { throw new SqlError(query, cause); } }; rawSql(` CREATE TABLE IF NOT EXISTS cf_agents_streams ( stream_id TEXT PRIMARY KEY, state TEXT NOT NULL CHECK (state IN ( 'streaming', 'completed', 'errored' )), tag TEXT, metadata TEXT, error_message TEXT, chunk_count INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, closed_at INTEGER )`); rawSql(` CREATE INDEX IF NOT EXISTS idx_cf_agents_streams_tag ON cf_agents_streams(tag, created_at) `); rawSql(` CREATE TABLE IF NOT EXISTS cf_agents_stream_blocks ( stream_id TEXT NOT NULL, block INTEGER NOT NULL, seq_from INTEGER NOT NULL, seq_to INTEGER NOT NULL, body TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (stream_id, block) ) WITHOUT ROWID`); } function _emit(type, payload) { this.lifecycle.events.emit(type, payload); } //#endregion export { StreamSerializationError as a, StreamNotFoundError as i, Streams as n, StreamClosedError as r, DEFAULT_MAX_CHUNK_BYTES as t }; //# sourceMappingURL=streams-CCPRV6dt.js.map