agents
Version:
A home for your AI agents
709 lines (708 loc) • 31.7 kB
JavaScript
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