agents 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-DE5zmCQ8.d.ts} +1038 -394
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +26 -26
- package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
- package/dist/agent-tools.d.ts +21 -21
- package/dist/browser/ai.js +4 -1
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
- package/dist/callable-decorator.d.ts +18 -0
- package/dist/callable-decorator.js +71 -0
- package/dist/callable-decorator.js.map +1 -0
- package/dist/capability-BjSKYpzg.js.map +1 -1
- package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
- package/dist/channel-Bnm4S7T2.d.ts +491 -0
- package/dist/channels/ai-sdk.d.ts +46 -0
- package/dist/channels/ai-sdk.js +120 -0
- package/dist/channels/ai-sdk.js.map +1 -0
- package/dist/channels/email.d.ts +95 -0
- package/dist/channels/email.js +323 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/index.d.ts +233 -0
- package/dist/channels/index.js +608 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/slack.d.ts +140 -0
- package/dist/channels/slack.js +614 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/tanstack-ai.d.ts +39 -0
- package/dist/channels/tanstack-ai.js +17 -0
- package/dist/channels/tanstack-ai.js.map +1 -0
- package/dist/channels/telegram.d.ts +106 -0
- package/dist/channels/telegram.js +427 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/voice.d.ts +45 -0
- package/dist/channels/voice.js +122 -0
- package/dist/channels/voice.js.map +1 -0
- package/dist/chat/index.d.ts +2325 -2019
- package/dist/chat/index.js +888 -518
- package/dist/chat/index.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +7 -7
- package/dist/chat-sdk/index.js +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/context/index.d.ts +216 -0
- package/dist/context/index.js +454 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{current-agent-CuMErtly.d.ts → current-agent-Da_C9a3b.d.ts} +90 -107
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
- package/dist/diagnostics-BzvaX2UT.js.map +1 -0
- package/dist/diagnostics-C4jcz3VK.js +360 -0
- package/dist/diagnostics-C4jcz3VK.js.map +1 -0
- package/dist/index-BB0kqhIz.d.ts +101 -0
- package/dist/index-BVVgDSdq.d.ts +1 -0
- package/dist/index-XDkuQ7zm.d.ts +89 -0
- package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
- package/dist/index.d.ts +91 -82
- package/dist/index.js +3 -2
- package/dist/ingress-BfetZbMO.js +83 -0
- package/dist/ingress-BfetZbMO.js.map +1 -0
- package/dist/internal-CYlgHl1l.js +59 -0
- package/dist/internal-CYlgHl1l.js.map +1 -0
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +35 -17
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle-CMRGjZdw.js +1299 -0
- package/dist/lifecycle-CMRGjZdw.js.map +1 -0
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +13 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +137 -0
- package/dist/routing/index.js +244 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sanitize-D9TujEK8.js +79 -0
- package/dist/sanitize-D9TujEK8.js.map +1 -0
- package/dist/scheduler-DD9NdYbF.js +665 -0
- package/dist/scheduler-DD9NdYbF.js.map +1 -0
- package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-Dwh85ZGl.d.ts} +21 -22
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +441 -0
- package/dist/sessions/index.js +2063 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/skills/index.d.ts +99 -0
- package/dist/skills/index.js +254 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/{src-5W6JNKVb.js → src-DlSHshb2.js} +1460 -1110
- package/dist/src-DlSHshb2.js.map +1 -0
- package/dist/streams/index.d.ts +120 -0
- package/dist/streams/index.js +107 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams-D6tJ0NN9.d.ts +370 -0
- package/dist/streams-DZKgAj9b.js +709 -0
- package/dist/streams-DZKgAj9b.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/surface-bZZJqBka.js +17 -0
- package/dist/surface-bZZJqBka.js.map +1 -0
- package/dist/tasks/index.d.ts +64 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks-BRJ5zgya.d.ts +517 -0
- package/dist/tasks-ylZgBjhj.js +1656 -0
- package/dist/tasks-ylZgBjhj.js.map +1 -0
- package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
- package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
- package/dist/text-stream-CpdiKrJB.js +272 -0
- package/dist/text-stream-CpdiKrJB.js.map +1 -0
- package/dist/tokens-nHAKcN6M.js +52 -0
- package/dist/tokens-nHAKcN6M.js.map +1 -0
- package/dist/tool-schema-CBjGPrsQ.js +31 -0
- package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
- package/dist/types-B7LojTe4.d.ts +202 -0
- package/dist/types-_Faxb570.d.ts +439 -0
- package/dist/voice/client.d.ts +226 -0
- package/dist/voice/client.js +932 -0
- package/dist/voice/client.js.map +1 -0
- package/dist/voice/errors.d.ts +43 -0
- package/dist/voice/errors.js +41 -0
- package/dist/voice/errors.js.map +1 -0
- package/dist/voice/index.d.ts +271 -0
- package/dist/voice/index.js +1812 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/react.d.ts +167 -0
- package/dist/voice/react.js +234 -0
- package/dist/voice/react.js.map +1 -0
- package/dist/voice/sfu.d.ts +71 -0
- package/dist/voice/sfu.js +157 -0
- package/dist/voice/sfu.js.map +1 -0
- package/dist/voice/text.d.ts +6 -0
- package/dist/voice/text.js +2 -0
- package/dist/voice/types.d.ts +58 -0
- package/dist/voice/types.js +18 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/workers-ai.d.ts +136 -0
- package/dist/voice/workers-ai.js +568 -0
- package/dist/voice/workers-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +192 -0
- package/dist/websockets/index.js +2 -0
- package/dist/websockets-DUfRHPRq.js +502 -0
- package/dist/websockets-DUfRHPRq.js.map +1 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/docs/agent-class.md +2 -2
- package/docs/agent-tools.md +2 -1
- package/docs/channels.md +323 -0
- package/docs/chat-agents.md +6 -13
- package/docs/context.md +131 -0
- package/docs/index.md +15 -12
- package/docs/lifecycle.md +102 -55
- package/docs/long-running-agents.md +2 -2
- package/docs/mcp-servers.md +5 -1
- package/docs/resumable-streaming.md +1 -1
- package/docs/routing.md +105 -0
- package/docs/sessions.md +237 -871
- package/docs/streams.md +213 -0
- package/docs/sub-agents.md +184 -124
- package/docs/tasks.md +246 -0
- package/docs/voice.md +745 -0
- package/package.json +115 -13
- package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
- package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
- package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
- package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
- package/dist/diagnostics-CaBjfz4J.js.map +0 -1
- package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
- package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
- package/dist/experimental/memory/session/index.d.ts +0 -671
- package/dist/experimental/memory/session/index.js +0 -2379
- package/dist/experimental/memory/session/index.js.map +0 -1
- package/dist/experimental/memory/utils/index.d.ts +0 -96
- package/dist/experimental/memory/utils/index.js +0 -79
- package/dist/experimental/memory/utils/index.js.map +0 -1
- package/dist/scheduler-CR9RHGos.js +0 -857
- package/dist/scheduler-CR9RHGos.js.map +0 -1
- package/dist/src-5W6JNKVb.js.map +0 -1
- package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
- package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js";
|
|
2
|
+
import { t as LifecycleCapability } from "./capability-BjSKYpzg.js";
|
|
3
|
+
import { t as _classPrivateMethodInitSpec } from "./classPrivateMethodInitSpec-qMjJ6sHQ.js";
|
|
4
|
+
import { SqlError } from "./sql-error.js";
|
|
5
|
+
//#region src/streams/errors.ts
|
|
6
|
+
/**
|
|
7
|
+
* Error classes for the Streams capability. Each carries a stable `name` so
|
|
8
|
+
* hosts and tests can classify failures without depending on message text.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Thrown when `open()` targets a terminal stream, or an append reaches a
|
|
12
|
+
* stream that settled (or was deleted) after the writer was created.
|
|
13
|
+
*
|
|
14
|
+
* @experimental The API surface may change before stabilizing.
|
|
15
|
+
*/
|
|
16
|
+
var StreamClosedError = class extends Error {
|
|
17
|
+
constructor(streamId, detail) {
|
|
18
|
+
super(`Stream "${streamId}" is closed: ${detail}`);
|
|
19
|
+
this.name = "StreamClosedError";
|
|
20
|
+
this.streamId = streamId;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when `read()` targets a stream that was never opened (or was
|
|
25
|
+
* deleted). `status()` returns `null` instead, for existence probes.
|
|
26
|
+
*
|
|
27
|
+
* @experimental The API surface may change before stabilizing.
|
|
28
|
+
*/
|
|
29
|
+
var StreamNotFoundError = class extends Error {
|
|
30
|
+
constructor(streamId) {
|
|
31
|
+
super(`Stream "${streamId}" does not exist. Open it before reading, or use status() to probe for existence.`);
|
|
32
|
+
this.name = "StreamNotFoundError";
|
|
33
|
+
this.streamId = streamId;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Thrown when a chunk or metadata value is not JSON-serializable or exceeds
|
|
38
|
+
* the configured size limit.
|
|
39
|
+
*
|
|
40
|
+
* @experimental The API surface may change before stabilizing.
|
|
41
|
+
*/
|
|
42
|
+
var StreamSerializationError = class extends Error {
|
|
43
|
+
constructor(context, detail) {
|
|
44
|
+
super(`Cannot serialize ${context}: ${detail}`);
|
|
45
|
+
this.name = "StreamSerializationError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/streams/streams.ts
|
|
50
|
+
/**
|
|
51
|
+
* Durable incremental output for Lifecycle Objects. `Streams` owns the
|
|
52
|
+
* `cf_agents_streams` and `cf_agents_stream_blocks` tables: an ordered,
|
|
53
|
+
* durable chunk log per stream with a monotonic cursor, replay-then-tail
|
|
54
|
+
* reads, and terminal status that doubles as recovery evidence for the
|
|
55
|
+
* Tasks capability (which composes through checkpointed cursors, never
|
|
56
|
+
* imports).
|
|
57
|
+
*
|
|
58
|
+
* Streams consumes only the standard capability services — storage and
|
|
59
|
+
* events. It needs no alarm, so it also works on facets. Live fanout is
|
|
60
|
+
* in-isolate: a Durable Object executes in one isolate at a time, so every
|
|
61
|
+
* concurrent reader shares the producer's isolate; readers that outlive an
|
|
62
|
+
* isolate replay from their cursor on reconnect.
|
|
63
|
+
*/
|
|
64
|
+
const STREAM_SCHEMA_VERSION_KEY = "cf_agents:streams_schema_version";
|
|
65
|
+
const CURRENT_STREAM_SCHEMA_VERSION = 2;
|
|
66
|
+
/**
|
|
67
|
+
* A block row grows by UPDATE until its body reaches this many characters,
|
|
68
|
+
* then the next append opens a new block. Sized well under the 2 MiB row
|
|
69
|
+
* limit so a 1 MiB chunk always fits in a fresh block, and small enough
|
|
70
|
+
* that a replay page parses one block at a time.
|
|
71
|
+
*/
|
|
72
|
+
const BLOCK_MAX_CHARS = 256 * 1024;
|
|
73
|
+
/** Rows read per page while folding one stream's v1 chunk rows into blocks. */
|
|
74
|
+
const LEGACY_FOLD_PAGE_ROWS = 500;
|
|
75
|
+
/** Default ceiling for one serialized chunk (1 MiB). */
|
|
76
|
+
const DEFAULT_MAX_CHUNK_BYTES = 1048576;
|
|
77
|
+
const MAX_STREAM_ID_LENGTH = 256;
|
|
78
|
+
const READ_BATCH_SIZE = 100;
|
|
79
|
+
const DEFAULT_LIST_LIMIT = 100;
|
|
80
|
+
const utf8 = new TextEncoder();
|
|
81
|
+
var _maxChunkBytes = /* @__PURE__ */ new WeakMap();
|
|
82
|
+
var _wakeups = /* @__PURE__ */ new WeakMap();
|
|
83
|
+
var _legacyChunkTable = /* @__PURE__ */ new WeakMap();
|
|
84
|
+
var _Streams_brand = /* @__PURE__ */ new WeakSet();
|
|
85
|
+
var _deleteHooks = /* @__PURE__ */ new WeakMap();
|
|
86
|
+
/**
|
|
87
|
+
* Durable incremental output for a Lifecycle Object.
|
|
88
|
+
*
|
|
89
|
+
* `open()` a stream, `append()` chunks (synchronous durable writes that wake
|
|
90
|
+
* live readers), and settle it with `close()` or `error()`. `read()` replays
|
|
91
|
+
* persisted chunks from a cursor and then tails live appends; `status()`
|
|
92
|
+
* reports the state and cursor — the recovery evidence a Task's `recover`
|
|
93
|
+
* callback consults after its producer was interrupted.
|
|
94
|
+
*
|
|
95
|
+
* @experimental The API surface may change before stabilizing.
|
|
96
|
+
*/
|
|
97
|
+
var Streams = class extends LifecycleCapability {
|
|
98
|
+
constructor(options = {}) {
|
|
99
|
+
super("streams");
|
|
100
|
+
_classPrivateMethodInitSpec(this, _Streams_brand);
|
|
101
|
+
_classPrivateFieldInitSpec(this, _maxChunkBytes, void 0);
|
|
102
|
+
_classPrivateFieldInitSpec(this, _wakeups, /* @__PURE__ */ new Map());
|
|
103
|
+
_classPrivateFieldInitSpec(this, _legacyChunkTable, false);
|
|
104
|
+
_classPrivateFieldInitSpec(this, _deleteHooks, []);
|
|
105
|
+
_classPrivateFieldSet2(_maxChunkBytes, this, options.maxChunkBytes ?? 1048576);
|
|
106
|
+
}
|
|
107
|
+
/** Migrate stream storage during Lifecycle startup. */
|
|
108
|
+
async onStart() {
|
|
109
|
+
const storage = this.lifecycle.storage;
|
|
110
|
+
const version = await storage.get(STREAM_SCHEMA_VERSION_KEY) ?? 0;
|
|
111
|
+
if (version < CURRENT_STREAM_SCHEMA_VERSION) {
|
|
112
|
+
_assertClassBrand(_Streams_brand, this, _ensureTables).call(this);
|
|
113
|
+
await storage.put(STREAM_SCHEMA_VERSION_KEY, CURRENT_STREAM_SCHEMA_VERSION);
|
|
114
|
+
}
|
|
115
|
+
if (version >= 1) _classPrivateFieldSet2(_legacyChunkTable, this, _assertClassBrand(_Streams_brand, this, _hasLegacyChunkTable).call(this));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Open a stream for writing. Idempotent on the id: reopening a live stream
|
|
119
|
+
* returns a writer positioned at its current cursor; reopening a terminal
|
|
120
|
+
* stream throws {@link StreamClosedError}.
|
|
121
|
+
*/
|
|
122
|
+
async open(streamId, options = {}) {
|
|
123
|
+
await this.lifecycle.ready();
|
|
124
|
+
_assertClassBrand(_Streams_brand, this, _validateStreamId).call(this, streamId);
|
|
125
|
+
const existing = _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId);
|
|
126
|
+
if (existing) {
|
|
127
|
+
if (existing.state !== "streaming") throw new StreamClosedError(streamId, `already settled as ${existing.state}`);
|
|
128
|
+
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)}`);
|
|
129
|
+
return _assertClassBrand(_Streams_brand, this, _writer).call(this, streamId);
|
|
130
|
+
}
|
|
131
|
+
const metadataJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, options.metadata, `metadata for stream "${streamId}"`);
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
_assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
134
|
+
INSERT INTO cf_agents_streams
|
|
135
|
+
(stream_id, state, tag, metadata, chunk_count, created_at, updated_at)
|
|
136
|
+
VALUES
|
|
137
|
+
(${streamId}, 'streaming', ${options.tag ?? null}, ${metadataJson}, 0, ${now}, ${now})
|
|
138
|
+
`;
|
|
139
|
+
_assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:opened", { streamId });
|
|
140
|
+
return _assertClassBrand(_Streams_brand, this, _writer).call(this, streamId);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Replay persisted chunks from `from` (inclusive), then tail live appends
|
|
144
|
+
* until the stream settles. Ends when the stream reaches a terminal state
|
|
145
|
+
* and every durable chunk has been yielded; a read of an `errored` stream
|
|
146
|
+
* still yields its chunks and then simply ends — consult {@link status}
|
|
147
|
+
* for the terminal outcome. Aborting `options.signal` throws its reason.
|
|
148
|
+
*/
|
|
149
|
+
async *read(streamId, options = {}) {
|
|
150
|
+
const signal = options.signal;
|
|
151
|
+
for await (const batch of this.readBatches(streamId, options)) for (const item of batch) {
|
|
152
|
+
if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("Read aborted");
|
|
153
|
+
yield item;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Batched form of {@link read}: yields non-empty arrays of consecutive
|
|
158
|
+
* chunks instead of one chunk at a time. Replay yields up to
|
|
159
|
+
* `options.batchSize` chunks per array; a live tail yields everything
|
|
160
|
+
* that accumulated since the last wakeup as one array — so a consumer
|
|
161
|
+
* paying per write (an SSE flush, an RPC hop, a history append) pays
|
|
162
|
+
* once per backlog, not once per chunk. Same lifecycle as {@link read}:
|
|
163
|
+
* ends when the stream settles and every durable chunk has been
|
|
164
|
+
* yielded; aborting `options.signal` throws its reason.
|
|
165
|
+
*/
|
|
166
|
+
async *readBatches(streamId, options = {}) {
|
|
167
|
+
await this.lifecycle.ready();
|
|
168
|
+
const signal = options.signal;
|
|
169
|
+
const batchSize = Math.max(1, Math.floor(options.batchSize ?? READ_BATCH_SIZE));
|
|
170
|
+
let next = Math.max(0, options.from ?? 0);
|
|
171
|
+
let signaledUpToDate = false;
|
|
172
|
+
if (_assertClassBrand(_Streams_brand, this, _state).call(this, streamId) === void 0) throw new StreamNotFoundError(streamId);
|
|
173
|
+
for (;;) {
|
|
174
|
+
if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("Read aborted");
|
|
175
|
+
const rows = _assertClassBrand(_Streams_brand, this, _readChunks).call(this, streamId, next, batchSize);
|
|
176
|
+
if (rows.length > 0) {
|
|
177
|
+
next = rows[rows.length - 1].seq + 1;
|
|
178
|
+
yield rows.map((row) => ({
|
|
179
|
+
seq: row.seq,
|
|
180
|
+
chunk: JSON.parse(row.chunk)
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
if (rows.length === batchSize) continue;
|
|
184
|
+
if (!signaledUpToDate) {
|
|
185
|
+
signaledUpToDate = true;
|
|
186
|
+
options.onUpToDate?.();
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const state = _assertClassBrand(_Streams_brand, this, _state).call(this, streamId);
|
|
190
|
+
if (state === void 0) return;
|
|
191
|
+
if (state !== "streaming") {
|
|
192
|
+
if (rows.length === 0) return;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
await _assertClassBrand(_Streams_brand, this, _waitForWakeup).call(this, streamId, signal);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Read one stream's state and cursor, or null when it does not exist. */
|
|
199
|
+
async status(streamId) {
|
|
200
|
+
await this.lifecycle.ready();
|
|
201
|
+
const row = _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId);
|
|
202
|
+
return row ? _assertClassBrand(_Streams_brand, this, _rowToStatus).call(this, row) : null;
|
|
203
|
+
}
|
|
204
|
+
/** List streams, newest first. */
|
|
205
|
+
async list(options = {}) {
|
|
206
|
+
await this.lifecycle.ready();
|
|
207
|
+
const states = Array.isArray(options.state) ? options.state : options.state !== void 0 ? [options.state] : [];
|
|
208
|
+
let query = "SELECT * FROM cf_agents_streams WHERE 1 = 1";
|
|
209
|
+
const params = [];
|
|
210
|
+
if (states.length > 0) {
|
|
211
|
+
query += ` AND state IN (${states.map(() => "?").join(", ")})`;
|
|
212
|
+
params.push(...states);
|
|
213
|
+
}
|
|
214
|
+
if (options.tag !== void 0) {
|
|
215
|
+
query += " AND tag = ?";
|
|
216
|
+
params.push(options.tag);
|
|
217
|
+
}
|
|
218
|
+
query += " ORDER BY created_at DESC, stream_id DESC LIMIT ?";
|
|
219
|
+
params.push(options.limit ?? DEFAULT_LIST_LIMIT);
|
|
220
|
+
let rows;
|
|
221
|
+
try {
|
|
222
|
+
rows = this.lifecycle.storage.sql.exec(query, ...params).toArray();
|
|
223
|
+
} catch (cause) {
|
|
224
|
+
throw new SqlError(query, cause);
|
|
225
|
+
}
|
|
226
|
+
return rows.map((row) => _assertClassBrand(_Streams_brand, this, _rowToStatus).call(this, row));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Delete one terminal stream and its chunk log.
|
|
230
|
+
*
|
|
231
|
+
* @returns True when a terminal stream was deleted; false when none
|
|
232
|
+
* exists. Throws on a live stream — settle it first.
|
|
233
|
+
*/
|
|
234
|
+
async delete(streamId) {
|
|
235
|
+
await this.lifecycle.ready();
|
|
236
|
+
const row = _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId);
|
|
237
|
+
if (!row) return false;
|
|
238
|
+
if (row.state === "streaming") throw new Error(`Cannot delete live stream "${streamId}"; close() or error() it first`);
|
|
239
|
+
_assertClassBrand(_Streams_brand, this, _deleteRows).call(this, streamId);
|
|
240
|
+
_assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:deleted", { streamId });
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* @internal Synchronous storage operations for same-isolate first-party
|
|
245
|
+
* machinery — today the chat `ResumableStream` adapter, whose whole public
|
|
246
|
+
* surface is synchronous and constructed before the Lifecycle starts.
|
|
247
|
+
* Bypasses `lifecycle.ready()`: the caller owns startup ordering. The
|
|
248
|
+
* invariant-bearing writes (append fence, settlement, wakeups, events) go
|
|
249
|
+
* through the same private methods as the public API, so live readers and
|
|
250
|
+
* diagnostics observe aperture writes exactly like capability writes. Will
|
|
251
|
+
* break without notice; never use from application code.
|
|
252
|
+
*/
|
|
253
|
+
__DO_NOT_USE_WILL_BREAK__sync() {
|
|
254
|
+
return {
|
|
255
|
+
ensureTables: () => _assertClassBrand(_Streams_brand, this, _ensureTables).call(this),
|
|
256
|
+
getStream: (streamId) => _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId),
|
|
257
|
+
insertStream: (streamId, tag, metadata) => {
|
|
258
|
+
_assertClassBrand(_Streams_brand, this, _validateStreamId).call(this, streamId);
|
|
259
|
+
const metadataJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, metadata, `metadata for stream "${streamId}"`);
|
|
260
|
+
const now = Date.now();
|
|
261
|
+
_assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
262
|
+
INSERT INTO cf_agents_streams
|
|
263
|
+
(stream_id, state, tag, metadata, chunk_count, created_at, updated_at)
|
|
264
|
+
VALUES
|
|
265
|
+
(${streamId}, 'streaming', ${tag}, ${metadataJson}, 0, ${now}, ${now})
|
|
266
|
+
`;
|
|
267
|
+
_assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:opened", { streamId });
|
|
268
|
+
},
|
|
269
|
+
append: (streamId, chunk) => _assertClassBrand(_Streams_brand, this, _append).call(this, streamId, chunk),
|
|
270
|
+
lastChunkAt: (streamId) => _assertClassBrand(_Streams_brand, this, _tail).call(this, streamId).lastChunkAt,
|
|
271
|
+
cursor: (streamId) => _assertClassBrand(_Streams_brand, this, _tail).call(this, streamId).nextSeq,
|
|
272
|
+
onDelete: (hook) => {
|
|
273
|
+
_classPrivateFieldGet2(_deleteHooks, this).push(hook);
|
|
274
|
+
return () => {
|
|
275
|
+
const index = _classPrivateFieldGet2(_deleteHooks, this).indexOf(hook);
|
|
276
|
+
if (index !== -1) _classPrivateFieldGet2(_deleteHooks, this).splice(index, 1);
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
settle: (streamId, state, reason, options) => _assertClassBrand(_Streams_brand, this, _settle).call(this, streamId, state, reason, options),
|
|
280
|
+
deleteUnchecked: (streamId) => {
|
|
281
|
+
if (_assertClassBrand(_Streams_brand, this, _deleteRows).call(this, streamId) > 0) _assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:deleted", { streamId });
|
|
282
|
+
_assertClassBrand(_Streams_brand, this, _wake).call(this, streamId);
|
|
283
|
+
},
|
|
284
|
+
deleteMany: (streamIds) => {
|
|
285
|
+
for (const streamId of streamIds) {
|
|
286
|
+
_assertClassBrand(_Streams_brand, this, _deleteRows).call(this, streamId);
|
|
287
|
+
_assertClassBrand(_Streams_brand, this, _wake).call(this, streamId);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
readChunks: (streamId, fromSeq, limit) => _assertClassBrand(_Streams_brand, this, _readChunks).call(this, streamId, fromSeq, limit),
|
|
291
|
+
listRows: () => _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
292
|
+
SELECT * FROM cf_agents_streams ORDER BY created_at DESC, rowid DESC
|
|
293
|
+
`,
|
|
294
|
+
rowsByTag: (tag, state) => state ? _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
295
|
+
SELECT * FROM cf_agents_streams
|
|
296
|
+
WHERE tag = ${tag} AND state = ${state}
|
|
297
|
+
ORDER BY created_at DESC, rowid DESC
|
|
298
|
+
` : _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
299
|
+
SELECT * FROM cf_agents_streams
|
|
300
|
+
WHERE tag = ${tag}
|
|
301
|
+
ORDER BY created_at DESC, rowid DESC
|
|
302
|
+
`,
|
|
303
|
+
importStream: (row) => {
|
|
304
|
+
_assertClassBrand(_Streams_brand, this, _validateStreamId).call(this, row.streamId);
|
|
305
|
+
const metadataJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, row.metadata, `metadata for stream "${row.streamId}"`);
|
|
306
|
+
_assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
307
|
+
INSERT INTO cf_agents_streams
|
|
308
|
+
(stream_id, state, tag, metadata, chunk_count,
|
|
309
|
+
created_at, updated_at, closed_at)
|
|
310
|
+
VALUES
|
|
311
|
+
(${row.streamId}, ${row.state}, ${row.tag}, ${metadataJson},
|
|
312
|
+
${row.chunkCount}, ${row.createdAt}, ${row.updatedAt},
|
|
313
|
+
${row.closedAt})
|
|
314
|
+
`;
|
|
315
|
+
},
|
|
316
|
+
importChunk: (streamId, chunk, createdAt) => {
|
|
317
|
+
const chunkJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, chunk, `chunk for stream "${streamId}"`);
|
|
318
|
+
if (chunkJson === null) return;
|
|
319
|
+
_assertClassBrand(_Streams_brand, this, _writeChunk).call(this, streamId, chunkJson, createdAt);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
function _writer(streamId) {
|
|
325
|
+
const capability = this;
|
|
326
|
+
return {
|
|
327
|
+
streamId,
|
|
328
|
+
get cursor() {
|
|
329
|
+
return _assertClassBrand(_Streams_brand, capability, _tail).call(capability, streamId).nextSeq;
|
|
330
|
+
},
|
|
331
|
+
append: (chunk) => _assertClassBrand(_Streams_brand, this, _append).call(this, streamId, chunk),
|
|
332
|
+
close: (options) => _assertClassBrand(_Streams_brand, this, _settle).call(this, streamId, "completed", null, options),
|
|
333
|
+
error: (reason, options) => _assertClassBrand(_Streams_brand, this, _settle).call(this, streamId, "errored", reason ?? null, options)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function _append(streamId, chunk) {
|
|
337
|
+
const chunkJson = _assertClassBrand(_Streams_brand, this, _serialize).call(this, chunk, `chunk for stream "${streamId}"`);
|
|
338
|
+
if (chunkJson === null) throw new StreamSerializationError(`chunk for stream "${streamId}"`, "chunks must not be undefined");
|
|
339
|
+
const state = _assertClassBrand(_Streams_brand, this, _state).call(this, streamId);
|
|
340
|
+
if (state !== "streaming") throw new StreamClosedError(streamId, state !== void 0 ? `already settled as ${state}` : "it was deleted");
|
|
341
|
+
const seq = _assertClassBrand(_Streams_brand, this, _writeChunk).call(this, streamId, chunkJson, Date.now());
|
|
342
|
+
_assertClassBrand(_Streams_brand, this, _wake).call(this, streamId);
|
|
343
|
+
return seq;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Append one serialized chunk to the stream's open block, or open a new
|
|
347
|
+
* block when the current one is full. Either way it is one billed row:
|
|
348
|
+
* an UPDATE that grows the block body, or the INSERT of the next block.
|
|
349
|
+
* Blocks are what make cleanup cheap — a stream of thousands of chunks
|
|
350
|
+
* is a handful of rows to delete at cutover, not thousands.
|
|
351
|
+
*/
|
|
352
|
+
function _writeChunk(streamId, chunkJson, at) {
|
|
353
|
+
const tail = _assertClassBrand(_Streams_brand, this, _blockTail).call(this, streamId);
|
|
354
|
+
const seq = tail?.seq_to ?? 0;
|
|
355
|
+
if (tail && tail.len + chunkJson.length + 1 <= BLOCK_MAX_CHARS) _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
356
|
+
UPDATE cf_agents_stream_blocks
|
|
357
|
+
SET body = body || ',' || ${chunkJson}, seq_to = ${seq + 1}, updated_at = ${at}
|
|
358
|
+
WHERE stream_id = ${streamId} AND block = ${tail.block}
|
|
359
|
+
`;
|
|
360
|
+
else _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
361
|
+
INSERT INTO cf_agents_stream_blocks
|
|
362
|
+
(stream_id, block, seq_from, seq_to, body, created_at, updated_at)
|
|
363
|
+
VALUES
|
|
364
|
+
(${streamId}, ${(tail?.block ?? -1) + 1}, ${seq}, ${seq + 1}, ${chunkJson}, ${at}, ${at})
|
|
365
|
+
`;
|
|
366
|
+
return seq;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* One page of chunks from `fromSeq`, in seq order, parsing one block at
|
|
370
|
+
* a time. A block body is the chunks' JSON texts joined by commas, so
|
|
371
|
+
* `[${body}]` parses straight back into the chunk values.
|
|
372
|
+
*/
|
|
373
|
+
function _readChunks(streamId, fromSeq, limit) {
|
|
374
|
+
_assertClassBrand(_Streams_brand, this, _foldLegacyChunks).call(this, streamId);
|
|
375
|
+
const rows = [];
|
|
376
|
+
let block = -1;
|
|
377
|
+
while (rows.length < limit) {
|
|
378
|
+
const next = _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
379
|
+
SELECT block, seq_from, seq_to, body, updated_at
|
|
380
|
+
FROM cf_agents_stream_blocks
|
|
381
|
+
WHERE stream_id = ${streamId} AND block > ${block} AND seq_to > ${fromSeq}
|
|
382
|
+
ORDER BY block ASC
|
|
383
|
+
LIMIT 1
|
|
384
|
+
`[0];
|
|
385
|
+
if (!next) break;
|
|
386
|
+
block = next.block;
|
|
387
|
+
const chunks = JSON.parse(`[${next.body}]`);
|
|
388
|
+
for (let seq = Math.max(fromSeq, next.seq_from); seq < next.seq_to && rows.length < limit; seq++) rows.push({
|
|
389
|
+
stream_id: streamId,
|
|
390
|
+
seq,
|
|
391
|
+
chunk: JSON.stringify(chunks[seq - next.seq_from]),
|
|
392
|
+
created_at: next.updated_at
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return rows;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Remove a stream's row and log. Deletion hooks see the row and its
|
|
399
|
+
* cursor first, so an owner can account for the segments before they are
|
|
400
|
+
* gone; this is the single point every delete path passes through.
|
|
401
|
+
*/
|
|
402
|
+
function _deleteRows(streamId) {
|
|
403
|
+
if (_classPrivateFieldGet2(_deleteHooks, this).length > 0) {
|
|
404
|
+
const row = _assertClassBrand(_Streams_brand, this, _getStream).call(this, streamId);
|
|
405
|
+
if (row) {
|
|
406
|
+
const cursor = _assertClassBrand(_Streams_brand, this, _tail).call(this, streamId).nextSeq;
|
|
407
|
+
for (const hook of _classPrivateFieldGet2(_deleteHooks, this)) hook(row, cursor);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (_classPrivateFieldGet2(_legacyChunkTable, this)) {
|
|
411
|
+
_assertClassBrand(_Streams_brand, this, _sql).bind(this)`DELETE FROM cf_agents_stream_chunks WHERE stream_id = ${streamId}`;
|
|
412
|
+
_assertClassBrand(_Streams_brand, this, _dropLegacyChunkTableIfEmpty).call(this);
|
|
413
|
+
}
|
|
414
|
+
_assertClassBrand(_Streams_brand, this, _sql).bind(this)`DELETE FROM cf_agents_stream_blocks WHERE stream_id = ${streamId}`;
|
|
415
|
+
return _assertClassBrand(_Streams_brand, this, _sqlWrite).call(this, "DELETE FROM cf_agents_streams WHERE stream_id = ?", [streamId]);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* @returns Whether this call transitioned the stream (a repeat, or a
|
|
419
|
+
* deleted stream, is a no-op and returns false: the caller's `commit`
|
|
420
|
+
* does not run and nothing is discarded).
|
|
421
|
+
*/
|
|
422
|
+
function _settle(streamId, state, reason, options) {
|
|
423
|
+
if (!options?.commit && !options?.discard) {
|
|
424
|
+
const settled = _assertClassBrand(_Streams_brand, this, _settleRow).call(this, streamId, state, reason);
|
|
425
|
+
if (settled) _assertClassBrand(_Streams_brand, this, _emitSettled).call(this, streamId, state, reason);
|
|
426
|
+
_assertClassBrand(_Streams_brand, this, _wake).call(this, streamId);
|
|
427
|
+
return settled;
|
|
428
|
+
}
|
|
429
|
+
let settled = false;
|
|
430
|
+
let deleted = false;
|
|
431
|
+
const hadLegacy = _classPrivateFieldGet2(_legacyChunkTable, this);
|
|
432
|
+
try {
|
|
433
|
+
this.lifecycle.storage.transactionSync(() => {
|
|
434
|
+
settled = _assertClassBrand(_Streams_brand, this, _settleRow).call(this, streamId, state, reason);
|
|
435
|
+
if (!settled) return;
|
|
436
|
+
options.commit?.();
|
|
437
|
+
if (options.discard) deleted = _assertClassBrand(_Streams_brand, this, _deleteRows).call(this, streamId) > 0;
|
|
438
|
+
});
|
|
439
|
+
} finally {
|
|
440
|
+
if (hadLegacy && !_classPrivateFieldGet2(_legacyChunkTable, this)) _classPrivateFieldSet2(_legacyChunkTable, this, _assertClassBrand(_Streams_brand, this, _hasLegacyChunkTable).call(this));
|
|
441
|
+
}
|
|
442
|
+
if (settled) _assertClassBrand(_Streams_brand, this, _emitSettled).call(this, streamId, state, reason);
|
|
443
|
+
if (deleted) _assertClassBrand(_Streams_brand, this, _emit).call(this, "stream:deleted", { streamId });
|
|
444
|
+
_assertClassBrand(_Streams_brand, this, _wake).call(this, streamId);
|
|
445
|
+
return settled;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* The one guarded UPDATE that ends a stream. Settlement is the moment
|
|
449
|
+
* the stream row becomes exact at rest: the same write stamps the final
|
|
450
|
+
* cursor, read from the chunk log's tail in the same synchronous block.
|
|
451
|
+
* While the stream was live, appends wrote only the chunk log — the
|
|
452
|
+
* row's chunk_count and updated_at were not maintained per append.
|
|
453
|
+
* @returns Whether the row transitioned from `streaming`.
|
|
454
|
+
*/
|
|
455
|
+
function _settleRow(streamId, state, reason) {
|
|
456
|
+
const finalCursor = _assertClassBrand(_Streams_brand, this, _tail).call(this, streamId).nextSeq;
|
|
457
|
+
return _assertClassBrand(_Streams_brand, this, _sqlWrite).call(this, `UPDATE cf_agents_streams
|
|
458
|
+
SET state = ?, error_message = ?, closed_at = ?, updated_at = ?,
|
|
459
|
+
chunk_count = ?
|
|
460
|
+
WHERE stream_id = ? AND state = 'streaming'`, [
|
|
461
|
+
state,
|
|
462
|
+
reason,
|
|
463
|
+
Date.now(),
|
|
464
|
+
Date.now(),
|
|
465
|
+
finalCursor,
|
|
466
|
+
streamId
|
|
467
|
+
]) > 0;
|
|
468
|
+
}
|
|
469
|
+
function _emitSettled(streamId, state, reason) {
|
|
470
|
+
_assertClassBrand(_Streams_brand, this, _emit).call(this, state === "completed" ? "stream:closed" : "stream:errored", {
|
|
471
|
+
streamId,
|
|
472
|
+
...reason !== null ? { reason } : {}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
function _wake(streamId) {
|
|
476
|
+
const waiters = _classPrivateFieldGet2(_wakeups, this).get(streamId);
|
|
477
|
+
if (!waiters) return;
|
|
478
|
+
_classPrivateFieldGet2(_wakeups, this).delete(streamId);
|
|
479
|
+
for (const wake of waiters) wake();
|
|
480
|
+
}
|
|
481
|
+
function _waitForWakeup(streamId, signal) {
|
|
482
|
+
return new Promise((resolve) => {
|
|
483
|
+
const waiters = _classPrivateFieldGet2(_wakeups, this).get(streamId) ?? /* @__PURE__ */ new Set();
|
|
484
|
+
_classPrivateFieldGet2(_wakeups, this).set(streamId, waiters);
|
|
485
|
+
const wake = () => {
|
|
486
|
+
signal?.removeEventListener("abort", onAbort);
|
|
487
|
+
resolve();
|
|
488
|
+
};
|
|
489
|
+
const onAbort = () => {
|
|
490
|
+
waiters.delete(wake);
|
|
491
|
+
if (waiters.size === 0 && _classPrivateFieldGet2(_wakeups, this).get(streamId) === waiters) _classPrivateFieldGet2(_wakeups, this).delete(streamId);
|
|
492
|
+
resolve();
|
|
493
|
+
};
|
|
494
|
+
waiters.add(wake);
|
|
495
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
function _validateStreamId(streamId) {
|
|
499
|
+
if (typeof streamId !== "string" || streamId.length === 0) throw new Error("Stream ids must be non-empty strings");
|
|
500
|
+
if (streamId.length > MAX_STREAM_ID_LENGTH) throw new Error(`Stream id exceeds ${MAX_STREAM_ID_LENGTH} characters`);
|
|
501
|
+
}
|
|
502
|
+
function _serialize(value, context) {
|
|
503
|
+
if (value === void 0) return null;
|
|
504
|
+
let json;
|
|
505
|
+
try {
|
|
506
|
+
json = JSON.stringify(value);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
throw new StreamSerializationError(context, error instanceof Error ? error.message : String(error));
|
|
509
|
+
}
|
|
510
|
+
if (json === void 0) throw new StreamSerializationError(context, `value of type ${typeof value} has no JSON representation`);
|
|
511
|
+
const bytes = utf8.encode(json).byteLength;
|
|
512
|
+
if (bytes > _classPrivateFieldGet2(_maxChunkBytes, this)) throw new StreamSerializationError(context, `serialized size ${bytes} bytes exceeds the ${_classPrivateFieldGet2(_maxChunkBytes, this)}-byte limit`);
|
|
513
|
+
return json;
|
|
514
|
+
}
|
|
515
|
+
function _sql(strings, ...values) {
|
|
516
|
+
const query = strings.reduce((result, part, index) => result + part + (index < values.length ? "?" : ""), "");
|
|
517
|
+
try {
|
|
518
|
+
return [...this.lifecycle.storage.sql.exec(query, ...values)];
|
|
519
|
+
} catch (cause) {
|
|
520
|
+
throw new SqlError(query, cause);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function _sqlWrite(query, params) {
|
|
524
|
+
try {
|
|
525
|
+
return this.lifecycle.storage.sql.exec(query, ...params).rowsWritten;
|
|
526
|
+
} catch (cause) {
|
|
527
|
+
throw new SqlError(query, cause);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function _getStream(streamId) {
|
|
531
|
+
return _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
532
|
+
SELECT * FROM cf_agents_streams WHERE stream_id = ${streamId}
|
|
533
|
+
`[0];
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* One stream's state alone — the narrow read for the append fence and the
|
|
537
|
+
* reader loop's liveness checks, which need neither the metadata column
|
|
538
|
+
* nor the (live-stale) counters of the full row.
|
|
539
|
+
*/
|
|
540
|
+
function _state(streamId) {
|
|
541
|
+
return _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
542
|
+
SELECT state FROM cf_agents_streams WHERE stream_id = ${streamId}
|
|
543
|
+
`[0]?.state;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* The chunk log's tail: the next append sequence and the newest chunk's
|
|
547
|
+
* timestamp. One PK-served read (`ORDER BY seq DESC LIMIT 1`), and the
|
|
548
|
+
* single derivation point for every cursor and per-append-liveness
|
|
549
|
+
* consumer — while a stream is live, the chunk log is authoritative and
|
|
550
|
+
* the stream row's `chunk_count`/`updated_at` are not maintained.
|
|
551
|
+
*/
|
|
552
|
+
function _tail(streamId) {
|
|
553
|
+
const tail = _assertClassBrand(_Streams_brand, this, _blockTail).call(this, streamId);
|
|
554
|
+
return tail ? {
|
|
555
|
+
nextSeq: tail.seq_to,
|
|
556
|
+
lastChunkAt: tail.updated_at
|
|
557
|
+
} : {
|
|
558
|
+
nextSeq: 0,
|
|
559
|
+
lastChunkAt: null
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/** The open block: one PK-served read (`ORDER BY block DESC LIMIT 1`). */
|
|
563
|
+
function _blockTail(streamId) {
|
|
564
|
+
_assertClassBrand(_Streams_brand, this, _foldLegacyChunks).call(this, streamId);
|
|
565
|
+
return _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
566
|
+
SELECT block, seq_to, updated_at, length(body) AS len
|
|
567
|
+
FROM cf_agents_stream_blocks
|
|
568
|
+
WHERE stream_id = ${streamId}
|
|
569
|
+
ORDER BY block DESC
|
|
570
|
+
LIMIT 1
|
|
571
|
+
`[0];
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Schema v1 → v2 is lazy: the per-chunk `cf_agents_stream_chunks` rows of
|
|
575
|
+
* ONE stream are folded into blocks the first time that stream is
|
|
576
|
+
* touched (a tail read before an append or settle, a replay, a delete).
|
|
577
|
+
* Startup never pays for the whole legacy log, so an object that let a
|
|
578
|
+
* large log accumulate still boots within its memory budget; each fold
|
|
579
|
+
* pages through its stream's rows and writes whole blocks, not one row
|
|
580
|
+
* per chunk. The table is dropped once the last stream's rows are gone.
|
|
581
|
+
*/
|
|
582
|
+
function _foldLegacyChunks(streamId) {
|
|
583
|
+
if (!_classPrivateFieldGet2(_legacyChunkTable, this)) return;
|
|
584
|
+
this.lifecycle.storage.transactionSync(() => {
|
|
585
|
+
const tail = _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
586
|
+
SELECT block, seq_to FROM cf_agents_stream_blocks
|
|
587
|
+
WHERE stream_id = ${streamId}
|
|
588
|
+
ORDER BY block DESC
|
|
589
|
+
LIMIT 1
|
|
590
|
+
`[0];
|
|
591
|
+
let block = (tail?.block ?? -1) + 1;
|
|
592
|
+
let seq = tail?.seq_to ?? 0;
|
|
593
|
+
let body = "";
|
|
594
|
+
let seqFrom = seq;
|
|
595
|
+
let createdAt = 0;
|
|
596
|
+
let updatedAt = 0;
|
|
597
|
+
const flush = () => {
|
|
598
|
+
if (body === "") return;
|
|
599
|
+
_assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
600
|
+
INSERT INTO cf_agents_stream_blocks
|
|
601
|
+
(stream_id, block, seq_from, seq_to, body, created_at, updated_at)
|
|
602
|
+
VALUES
|
|
603
|
+
(${streamId}, ${block}, ${seqFrom}, ${seq}, ${body}, ${createdAt}, ${updatedAt})
|
|
604
|
+
`;
|
|
605
|
+
block += 1;
|
|
606
|
+
body = "";
|
|
607
|
+
seqFrom = seq;
|
|
608
|
+
};
|
|
609
|
+
let after = -1;
|
|
610
|
+
for (;;) {
|
|
611
|
+
const rows = _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
612
|
+
SELECT seq, chunk, created_at FROM cf_agents_stream_chunks
|
|
613
|
+
WHERE stream_id = ${streamId} AND seq > ${after}
|
|
614
|
+
ORDER BY seq ASC
|
|
615
|
+
LIMIT ${LEGACY_FOLD_PAGE_ROWS}
|
|
616
|
+
`;
|
|
617
|
+
for (const row of rows) {
|
|
618
|
+
if (body !== "" && body.length + row.chunk.length + 1 > BLOCK_MAX_CHARS) flush();
|
|
619
|
+
if (body === "") createdAt = row.created_at;
|
|
620
|
+
body = body === "" ? row.chunk : `${body},${row.chunk}`;
|
|
621
|
+
updatedAt = row.created_at;
|
|
622
|
+
seq += 1;
|
|
623
|
+
after = row.seq;
|
|
624
|
+
}
|
|
625
|
+
if (rows.length < LEGACY_FOLD_PAGE_ROWS) break;
|
|
626
|
+
}
|
|
627
|
+
flush();
|
|
628
|
+
if (after >= 0) _assertClassBrand(_Streams_brand, this, _sql).bind(this)`DELETE FROM cf_agents_stream_chunks WHERE stream_id = ${streamId}`;
|
|
629
|
+
_assertClassBrand(_Streams_brand, this, _dropLegacyChunkTableIfEmpty).call(this);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
function _hasLegacyChunkTable() {
|
|
633
|
+
return _assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
634
|
+
SELECT name FROM sqlite_master
|
|
635
|
+
WHERE type = 'table' AND name = 'cf_agents_stream_chunks'
|
|
636
|
+
`.length > 0;
|
|
637
|
+
}
|
|
638
|
+
function _dropLegacyChunkTableIfEmpty() {
|
|
639
|
+
if (_assertClassBrand(_Streams_brand, this, _sql).bind(this)`
|
|
640
|
+
SELECT COUNT(*) AS n FROM (SELECT 1 FROM cf_agents_stream_chunks LIMIT 1)
|
|
641
|
+
`[0].n > 0) return;
|
|
642
|
+
_assertClassBrand(_Streams_brand, this, _sqlWrite).call(this, "DROP TABLE cf_agents_stream_chunks", []);
|
|
643
|
+
_classPrivateFieldSet2(_legacyChunkTable, this, false);
|
|
644
|
+
}
|
|
645
|
+
function _rowToStatus(row) {
|
|
646
|
+
let cursor = row.chunk_count;
|
|
647
|
+
let updatedAt = row.updated_at;
|
|
648
|
+
if (row.state === "streaming") {
|
|
649
|
+
const tail = _assertClassBrand(_Streams_brand, this, _tail).call(this, row.stream_id);
|
|
650
|
+
cursor = tail.nextSeq;
|
|
651
|
+
if (tail.lastChunkAt !== null && tail.lastChunkAt > updatedAt) updatedAt = tail.lastChunkAt;
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
streamId: row.stream_id,
|
|
655
|
+
state: row.state,
|
|
656
|
+
cursor,
|
|
657
|
+
...row.tag !== null ? { tag: row.tag } : {},
|
|
658
|
+
...row.metadata !== null ? { metadata: JSON.parse(row.metadata) } : {},
|
|
659
|
+
...row.error_message !== null ? { error: row.error_message } : {},
|
|
660
|
+
createdAt: row.created_at,
|
|
661
|
+
updatedAt,
|
|
662
|
+
...row.closed_at !== null ? { closedAt: row.closed_at } : {}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function _ensureTables() {
|
|
666
|
+
const rawSql = (query) => {
|
|
667
|
+
try {
|
|
668
|
+
this.lifecycle.storage.sql.exec(query);
|
|
669
|
+
} catch (cause) {
|
|
670
|
+
throw new SqlError(query, cause);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
rawSql(`
|
|
674
|
+
CREATE TABLE IF NOT EXISTS cf_agents_streams (
|
|
675
|
+
stream_id TEXT PRIMARY KEY,
|
|
676
|
+
state TEXT NOT NULL CHECK (state IN (
|
|
677
|
+
'streaming', 'completed', 'errored'
|
|
678
|
+
)),
|
|
679
|
+
tag TEXT,
|
|
680
|
+
metadata TEXT,
|
|
681
|
+
error_message TEXT,
|
|
682
|
+
chunk_count INTEGER NOT NULL DEFAULT 0,
|
|
683
|
+
created_at INTEGER NOT NULL,
|
|
684
|
+
updated_at INTEGER NOT NULL,
|
|
685
|
+
closed_at INTEGER
|
|
686
|
+
)`);
|
|
687
|
+
rawSql(`
|
|
688
|
+
CREATE INDEX IF NOT EXISTS idx_cf_agents_streams_tag
|
|
689
|
+
ON cf_agents_streams(tag, created_at)
|
|
690
|
+
`);
|
|
691
|
+
rawSql(`
|
|
692
|
+
CREATE TABLE IF NOT EXISTS cf_agents_stream_blocks (
|
|
693
|
+
stream_id TEXT NOT NULL,
|
|
694
|
+
block INTEGER NOT NULL,
|
|
695
|
+
seq_from INTEGER NOT NULL,
|
|
696
|
+
seq_to INTEGER NOT NULL,
|
|
697
|
+
body TEXT NOT NULL,
|
|
698
|
+
created_at INTEGER NOT NULL,
|
|
699
|
+
updated_at INTEGER NOT NULL,
|
|
700
|
+
PRIMARY KEY (stream_id, block)
|
|
701
|
+
) WITHOUT ROWID`);
|
|
702
|
+
}
|
|
703
|
+
function _emit(type, payload) {
|
|
704
|
+
this.lifecycle.events.emit(type, payload);
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
export { StreamSerializationError as a, StreamNotFoundError as i, Streams as n, StreamClosedError as r, DEFAULT_MAX_CHUNK_BYTES as t };
|
|
708
|
+
|
|
709
|
+
//# sourceMappingURL=streams-DZKgAj9b.js.map
|