@ekairos/events 1.22.67-beta.development.0 → 1.22.69-beta.development.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/dist/react.context-event-parts.d.ts +18 -0
- package/dist/react.context-event-parts.js +509 -0
- package/dist/react.d.ts +7 -42
- package/dist/react.js +4 -87
- package/dist/react.step-stream.d.ts +39 -0
- package/dist/react.step-stream.js +581 -0
- package/dist/react.types.d.ts +121 -0
- package/dist/react.types.js +2 -0
- package/dist/react.use-context.d.ts +7 -0
- package/dist/react.use-context.js +867 -0
- package/package.json +3 -2
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { INPUT_TEXT_ITEM_TYPE } from "./react.types";
|
|
4
|
+
import { buildContextStepViews, buildEventStepsIndex, buildLiveEventFromStepChunks, consumePersistedContextStepStream, extractPersistedContextTree, isUserEvent, } from "./react.step-stream";
|
|
5
|
+
import { getActionPartInfo, getReasoningState, normalizeContextEventParts, } from "./react.context-event-parts";
|
|
6
|
+
const DEFAULT_STREAM_CHUNK_DELAY_MS = process.env.NODE_ENV === "development" ? 80 : 0;
|
|
7
|
+
const STREAM_DEBUG_RAW_SAMPLE_LIMIT = 50;
|
|
8
|
+
function randomUuidV4() {
|
|
9
|
+
const anyCrypto = globalThis?.crypto;
|
|
10
|
+
if (anyCrypto?.randomUUID)
|
|
11
|
+
return anyCrypto.randomUUID();
|
|
12
|
+
if (anyCrypto?.getRandomValues) {
|
|
13
|
+
const bytes = new Uint8Array(16);
|
|
14
|
+
anyCrypto.getRandomValues(bytes);
|
|
15
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
16
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
17
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
|
|
18
|
+
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex
|
|
19
|
+
.slice(6, 8)
|
|
20
|
+
.join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
|
|
21
|
+
}
|
|
22
|
+
const s4 = () => Math.floor((1 + Math.random()) * 0x10000)
|
|
23
|
+
.toString(16)
|
|
24
|
+
.slice(1);
|
|
25
|
+
return `${s4()}${s4()}-${s4()}-4${s4().slice(1)}-${((8 + Math.random() * 4) |
|
|
26
|
+
0).toString(16)}${s4().slice(1)}-${s4()}${s4()}${s4()}`;
|
|
27
|
+
}
|
|
28
|
+
function asText(value) {
|
|
29
|
+
return typeof value === "string" ? value.trim() : "";
|
|
30
|
+
}
|
|
31
|
+
function asRecord(value) {
|
|
32
|
+
return value && typeof value === "object" ? value : null;
|
|
33
|
+
}
|
|
34
|
+
function normalizeContextStatus(value) {
|
|
35
|
+
const raw = asText(value);
|
|
36
|
+
if (raw === "closed")
|
|
37
|
+
return "closed";
|
|
38
|
+
if (raw === "streaming" || raw === "open_streaming")
|
|
39
|
+
return "open_streaming";
|
|
40
|
+
return "open_idle";
|
|
41
|
+
}
|
|
42
|
+
function firstLinkedRecord(value) {
|
|
43
|
+
if (Array.isArray(value))
|
|
44
|
+
return asRecord(value[0]);
|
|
45
|
+
return asRecord(value);
|
|
46
|
+
}
|
|
47
|
+
function toPublicContextFirstLevel(value) {
|
|
48
|
+
const record = asRecord(value);
|
|
49
|
+
const contextId = asText(record?.id);
|
|
50
|
+
if (!record || !contextId)
|
|
51
|
+
return null;
|
|
52
|
+
const currentExecutionRecord = firstLinkedRecord(record.currentExecution);
|
|
53
|
+
const currentExecutionId = asText(currentExecutionRecord?.id);
|
|
54
|
+
return {
|
|
55
|
+
id: contextId,
|
|
56
|
+
key: asText(record.key) || null,
|
|
57
|
+
name: asText(record.name) || null,
|
|
58
|
+
status: normalizeContextStatus(record.status),
|
|
59
|
+
content: record.content,
|
|
60
|
+
currentExecution: currentExecutionId
|
|
61
|
+
? {
|
|
62
|
+
id: currentExecutionId,
|
|
63
|
+
status: asText(currentExecutionRecord?.status) || null,
|
|
64
|
+
}
|
|
65
|
+
: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function messageToEphemeralEvent(message, ctx) {
|
|
69
|
+
return {
|
|
70
|
+
__contextId: ctx,
|
|
71
|
+
id: String(message.id),
|
|
72
|
+
type: INPUT_TEXT_ITEM_TYPE,
|
|
73
|
+
channel: "web",
|
|
74
|
+
createdAt: new Date().toISOString(),
|
|
75
|
+
content: { parts: Array.isArray(message.parts) ? message.parts : [] },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function partsToSendPayload(parts) {
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
id: randomUuidV4(),
|
|
82
|
+
role: "user",
|
|
83
|
+
parts: Array.isArray(parts) ? parts : [],
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
function mergeEvents(params) {
|
|
88
|
+
const byId = new Map();
|
|
89
|
+
for (const event of params.persisted) {
|
|
90
|
+
byId.set(String(event.id), event);
|
|
91
|
+
}
|
|
92
|
+
const merged = [...params.persisted];
|
|
93
|
+
for (const event of params.optimistic) {
|
|
94
|
+
const belongsToActive = String(event.__contextId) === String(params.currentContextId) ||
|
|
95
|
+
(event.__contextId == null && params.currentContextId != null);
|
|
96
|
+
if (!belongsToActive)
|
|
97
|
+
continue;
|
|
98
|
+
if (byId.has(String(event.id)))
|
|
99
|
+
continue;
|
|
100
|
+
merged.push(event);
|
|
101
|
+
}
|
|
102
|
+
return merged;
|
|
103
|
+
}
|
|
104
|
+
function stepStreamKey(step) {
|
|
105
|
+
if (step.streamId)
|
|
106
|
+
return `stream:${step.streamId}`;
|
|
107
|
+
if (step.streamClientId)
|
|
108
|
+
return `client:${step.streamClientId}`;
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
function nowIso() {
|
|
112
|
+
return new Date().toISOString();
|
|
113
|
+
}
|
|
114
|
+
function defaultStreamReaderInfo(step) {
|
|
115
|
+
const streamKey = stepStreamKey(step);
|
|
116
|
+
const hasStream = Boolean(streamKey);
|
|
117
|
+
const isRunning = step.status === "running";
|
|
118
|
+
const isFinished = Boolean(step.streamFinishedAt || step.stream?.done === true);
|
|
119
|
+
return {
|
|
120
|
+
status: !hasStream
|
|
121
|
+
? isRunning
|
|
122
|
+
? "missing_stream_identity"
|
|
123
|
+
: "no_stream_identity"
|
|
124
|
+
: isRunning
|
|
125
|
+
? "waiting_for_reader"
|
|
126
|
+
: isFinished
|
|
127
|
+
? "not_reading_finished_step"
|
|
128
|
+
: "not_reading_step",
|
|
129
|
+
streamKey: streamKey || null,
|
|
130
|
+
startedAt: null,
|
|
131
|
+
updatedAt: null,
|
|
132
|
+
completedAt: null,
|
|
133
|
+
attempts: 0,
|
|
134
|
+
chunkCount: 0,
|
|
135
|
+
byteOffset: 0,
|
|
136
|
+
streamByteOffset: 0,
|
|
137
|
+
lastChunkType: null,
|
|
138
|
+
lastSequence: null,
|
|
139
|
+
lastError: null,
|
|
140
|
+
reason: !hasStream
|
|
141
|
+
? "No streamId or streamClientId is present on event_steps."
|
|
142
|
+
: isRunning
|
|
143
|
+
? "Reader has not reported activity yet."
|
|
144
|
+
: isFinished
|
|
145
|
+
? "Step is finished and no replay reader is attached."
|
|
146
|
+
: "Step is not running.",
|
|
147
|
+
rawChunkSampleOffset: 0,
|
|
148
|
+
rawChunkSample: [],
|
|
149
|
+
rawLineSample: [],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function streamDebugSample(items) {
|
|
153
|
+
const offset = Math.max(0, items.length - STREAM_DEBUG_RAW_SAMPLE_LIMIT);
|
|
154
|
+
return {
|
|
155
|
+
offset,
|
|
156
|
+
sample: items.slice(offset),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function streamReaderRawDebug(active) {
|
|
160
|
+
const chunkSample = streamDebugSample(active.chunks);
|
|
161
|
+
const lineSample = streamDebugSample(active.rawLines);
|
|
162
|
+
return {
|
|
163
|
+
rawChunkSampleOffset: chunkSample.offset,
|
|
164
|
+
rawChunkSample: chunkSample.sample,
|
|
165
|
+
rawLineSample: lineSample.sample,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function abortAllStepReaders(readers) {
|
|
169
|
+
for (const active of readers.values()) {
|
|
170
|
+
active.abortController.abort();
|
|
171
|
+
}
|
|
172
|
+
readers.clear();
|
|
173
|
+
}
|
|
174
|
+
function isAbortLikeError(error, signal) {
|
|
175
|
+
if (signal.aborted)
|
|
176
|
+
return true;
|
|
177
|
+
const record = asRecord(error);
|
|
178
|
+
return (record?.name === "AbortError" ||
|
|
179
|
+
String(record?.message ?? "").toLowerCase().includes("abort"));
|
|
180
|
+
}
|
|
181
|
+
function sleep(ms, signal) {
|
|
182
|
+
return new Promise((resolve) => {
|
|
183
|
+
if (signal.aborted) {
|
|
184
|
+
resolve();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const timeout = globalThis.setTimeout(resolve, ms);
|
|
188
|
+
signal.addEventListener("abort", () => {
|
|
189
|
+
globalThis.clearTimeout(timeout);
|
|
190
|
+
resolve();
|
|
191
|
+
}, { once: true });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function normalizeStreamChunkDelayMs(value) {
|
|
195
|
+
const ms = typeof value === "number" ? value : Number(value);
|
|
196
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
197
|
+
return 0;
|
|
198
|
+
return Math.min(ms, 1000);
|
|
199
|
+
}
|
|
200
|
+
function normalizePartsForStep(parts) {
|
|
201
|
+
return normalizeContextEventParts(parts)
|
|
202
|
+
.map((part) => asRecord(part))
|
|
203
|
+
.filter((part) => Boolean(part));
|
|
204
|
+
}
|
|
205
|
+
function syncContextStepStreamReaders(params) {
|
|
206
|
+
const desired = new Map();
|
|
207
|
+
for (const step of params.steps) {
|
|
208
|
+
const streamKey = stepStreamKey(step);
|
|
209
|
+
if (step.status === "running" && !streamKey) {
|
|
210
|
+
params.setStreamReaderInfoByStepId((prev) => ({
|
|
211
|
+
...prev,
|
|
212
|
+
[step.stepId]: defaultStreamReaderInfo(step),
|
|
213
|
+
}));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (step.status !== "running")
|
|
217
|
+
continue;
|
|
218
|
+
if (!streamKey)
|
|
219
|
+
continue;
|
|
220
|
+
desired.set(step.stepId, { streamKey, step });
|
|
221
|
+
}
|
|
222
|
+
for (const [stepId, active] of params.readers.entries()) {
|
|
223
|
+
const next = desired.get(stepId);
|
|
224
|
+
if (!next || next.streamKey !== active.streamKey || active.db !== params.db) {
|
|
225
|
+
active.abortController.abort();
|
|
226
|
+
params.readers.delete(stepId);
|
|
227
|
+
params.setStreamReaderInfoByStepId((prev) => ({
|
|
228
|
+
...prev,
|
|
229
|
+
[stepId]: {
|
|
230
|
+
...(prev[stepId] ?? {
|
|
231
|
+
streamKey: active.streamKey,
|
|
232
|
+
startedAt: active.startedAt,
|
|
233
|
+
attempts: 0,
|
|
234
|
+
chunkCount: active.chunks.length,
|
|
235
|
+
byteOffset: active.byteOffset,
|
|
236
|
+
streamByteOffset: active.streamByteOffset,
|
|
237
|
+
}),
|
|
238
|
+
status: "aborted",
|
|
239
|
+
streamKey: active.streamKey,
|
|
240
|
+
updatedAt: nowIso(),
|
|
241
|
+
completedAt: nowIso(),
|
|
242
|
+
chunkCount: active.chunks.length,
|
|
243
|
+
byteOffset: active.byteOffset,
|
|
244
|
+
streamByteOffset: active.streamByteOffset,
|
|
245
|
+
reason: next
|
|
246
|
+
? "Stream key or db instance changed."
|
|
247
|
+
: "Step is no longer running; persisted parts are the source of truth.",
|
|
248
|
+
lastChunkType: prev[stepId]?.lastChunkType ?? null,
|
|
249
|
+
lastSequence: prev[stepId]?.lastSequence ?? null,
|
|
250
|
+
lastError: prev[stepId]?.lastError ?? null,
|
|
251
|
+
...streamReaderRawDebug(active),
|
|
252
|
+
},
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (const [stepId, next] of desired.entries()) {
|
|
257
|
+
if (params.readers.has(stepId))
|
|
258
|
+
continue;
|
|
259
|
+
const abortController = new AbortController();
|
|
260
|
+
const active = {
|
|
261
|
+
abortController,
|
|
262
|
+
db: params.db,
|
|
263
|
+
streamKey: next.streamKey,
|
|
264
|
+
chunks: [],
|
|
265
|
+
rawLines: [],
|
|
266
|
+
byteOffset: 0,
|
|
267
|
+
streamByteOffset: 0,
|
|
268
|
+
startedAt: nowIso(),
|
|
269
|
+
};
|
|
270
|
+
params.readers.set(stepId, active);
|
|
271
|
+
params.setStreamReaderInfoByStepId((prev) => ({
|
|
272
|
+
...prev,
|
|
273
|
+
[stepId]: {
|
|
274
|
+
status: "starting",
|
|
275
|
+
streamKey: next.streamKey,
|
|
276
|
+
startedAt: active.startedAt,
|
|
277
|
+
updatedAt: active.startedAt,
|
|
278
|
+
completedAt: null,
|
|
279
|
+
attempts: 0,
|
|
280
|
+
chunkCount: 0,
|
|
281
|
+
byteOffset: 0,
|
|
282
|
+
streamByteOffset: 0,
|
|
283
|
+
lastChunkType: null,
|
|
284
|
+
lastSequence: null,
|
|
285
|
+
lastError: null,
|
|
286
|
+
reason: "Reader scheduled for running step.",
|
|
287
|
+
rawChunkSampleOffset: 0,
|
|
288
|
+
rawChunkSample: [],
|
|
289
|
+
rawLineSample: [],
|
|
290
|
+
},
|
|
291
|
+
}));
|
|
292
|
+
void runContextStepStreamReader({
|
|
293
|
+
db: params.db,
|
|
294
|
+
step: next.step,
|
|
295
|
+
readers: params.readers,
|
|
296
|
+
active,
|
|
297
|
+
streamChunkDelayMs: params.streamChunkDelayMs,
|
|
298
|
+
setStreamReaderInfoByStepId: params.setStreamReaderInfoByStepId,
|
|
299
|
+
setLiveEventsByStepId: params.setLiveEventsByStepId,
|
|
300
|
+
setTurnSubstateKey: params.setTurnSubstateKey,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function runContextStepStreamReader(params) {
|
|
305
|
+
const { active, step } = params;
|
|
306
|
+
const signal = active.abortController.signal;
|
|
307
|
+
try {
|
|
308
|
+
for (let attempt = 0; attempt < 3 && !signal.aborted; attempt += 1) {
|
|
309
|
+
try {
|
|
310
|
+
params.setStreamReaderInfoByStepId((prev) => ({
|
|
311
|
+
...prev,
|
|
312
|
+
[step.stepId]: {
|
|
313
|
+
...(prev[step.stepId] ?? defaultStreamReaderInfo(step)),
|
|
314
|
+
status: "reading",
|
|
315
|
+
streamKey: active.streamKey,
|
|
316
|
+
startedAt: prev[step.stepId]?.startedAt ?? active.startedAt,
|
|
317
|
+
updatedAt: nowIso(),
|
|
318
|
+
attempts: attempt + 1,
|
|
319
|
+
chunkCount: active.chunks.length,
|
|
320
|
+
byteOffset: active.byteOffset,
|
|
321
|
+
streamByteOffset: active.streamByteOffset,
|
|
322
|
+
lastError: null,
|
|
323
|
+
reason: "createReadStream is active.",
|
|
324
|
+
...streamReaderRawDebug(active),
|
|
325
|
+
},
|
|
326
|
+
}));
|
|
327
|
+
await consumePersistedContextStepStream({
|
|
328
|
+
db: params.db,
|
|
329
|
+
signal,
|
|
330
|
+
byteOffset: active.byteOffset,
|
|
331
|
+
clientId: step.streamClientId,
|
|
332
|
+
streamId: step.streamId,
|
|
333
|
+
onByteOffset: (byteOffset) => {
|
|
334
|
+
active.byteOffset = byteOffset;
|
|
335
|
+
params.setStreamReaderInfoByStepId((prev) => ({
|
|
336
|
+
...prev,
|
|
337
|
+
[step.stepId]: {
|
|
338
|
+
...(prev[step.stepId] ?? defaultStreamReaderInfo(step)),
|
|
339
|
+
status: "reading",
|
|
340
|
+
streamKey: active.streamKey,
|
|
341
|
+
startedAt: prev[step.stepId]?.startedAt ?? active.startedAt,
|
|
342
|
+
updatedAt: nowIso(),
|
|
343
|
+
attempts: attempt + 1,
|
|
344
|
+
chunkCount: active.chunks.length,
|
|
345
|
+
byteOffset,
|
|
346
|
+
streamByteOffset: active.streamByteOffset,
|
|
347
|
+
reason: "Read stream byte offset advanced.",
|
|
348
|
+
...streamReaderRawDebug(active),
|
|
349
|
+
},
|
|
350
|
+
}));
|
|
351
|
+
},
|
|
352
|
+
onChunk: async (chunk, info) => {
|
|
353
|
+
const current = params.readers.get(step.stepId);
|
|
354
|
+
if (current !== active)
|
|
355
|
+
return;
|
|
356
|
+
active.byteOffset = info.parsedByteOffset;
|
|
357
|
+
active.streamByteOffset = info.streamByteOffset;
|
|
358
|
+
active.chunks.push(chunk);
|
|
359
|
+
active.rawLines.push(info.rawLine);
|
|
360
|
+
const chunkRecord = asRecord(chunk) ?? {};
|
|
361
|
+
const rawDebug = streamReaderRawDebug(active);
|
|
362
|
+
const liveEvent = buildLiveEventFromStepChunks({
|
|
363
|
+
eventId: `context-step-live:${step.stepId}`,
|
|
364
|
+
createdAt: step.streamStartedAt ||
|
|
365
|
+
step.streamFinishedAt ||
|
|
366
|
+
new Date().toISOString(),
|
|
367
|
+
chunks: active.chunks,
|
|
368
|
+
});
|
|
369
|
+
params.setLiveEventsByStepId((prev) => ({
|
|
370
|
+
...prev,
|
|
371
|
+
[step.stepId]: liveEvent,
|
|
372
|
+
}));
|
|
373
|
+
params.setStreamReaderInfoByStepId((prev) => ({
|
|
374
|
+
...prev,
|
|
375
|
+
[step.stepId]: {
|
|
376
|
+
...(prev[step.stepId] ?? defaultStreamReaderInfo(step)),
|
|
377
|
+
status: "reading",
|
|
378
|
+
streamKey: active.streamKey,
|
|
379
|
+
startedAt: prev[step.stepId]?.startedAt ?? active.startedAt,
|
|
380
|
+
updatedAt: nowIso(),
|
|
381
|
+
completedAt: null,
|
|
382
|
+
attempts: attempt + 1,
|
|
383
|
+
chunkCount: active.chunks.length,
|
|
384
|
+
byteOffset: info.parsedByteOffset,
|
|
385
|
+
streamByteOffset: info.streamByteOffset,
|
|
386
|
+
lastChunkType: asText(chunkRecord.chunkType) || null,
|
|
387
|
+
lastSequence: typeof chunkRecord.sequence === "number"
|
|
388
|
+
? chunkRecord.sequence
|
|
389
|
+
: Number.isFinite(Number(chunkRecord.sequence))
|
|
390
|
+
? Number(chunkRecord.sequence)
|
|
391
|
+
: null,
|
|
392
|
+
lastError: null,
|
|
393
|
+
reason: "Chunk consumed from resumable stream.",
|
|
394
|
+
...rawDebug,
|
|
395
|
+
},
|
|
396
|
+
}));
|
|
397
|
+
params.setTurnSubstateKey("actions");
|
|
398
|
+
await sleep(params.streamChunkDelayMs, signal);
|
|
399
|
+
},
|
|
400
|
+
onDone: async () => {
|
|
401
|
+
params.setStreamReaderInfoByStepId((prev) => ({
|
|
402
|
+
...prev,
|
|
403
|
+
[step.stepId]: {
|
|
404
|
+
...(prev[step.stepId] ?? defaultStreamReaderInfo(step)),
|
|
405
|
+
status: "completed",
|
|
406
|
+
streamKey: active.streamKey,
|
|
407
|
+
startedAt: prev[step.stepId]?.startedAt ?? active.startedAt,
|
|
408
|
+
updatedAt: nowIso(),
|
|
409
|
+
completedAt: nowIso(),
|
|
410
|
+
attempts: attempt + 1,
|
|
411
|
+
chunkCount: active.chunks.length,
|
|
412
|
+
byteOffset: active.byteOffset,
|
|
413
|
+
streamByteOffset: active.streamByteOffset,
|
|
414
|
+
reason: "Readable stream ended.",
|
|
415
|
+
...streamReaderRawDebug(active),
|
|
416
|
+
},
|
|
417
|
+
}));
|
|
418
|
+
params.setTurnSubstateKey((current) => current === "actions" ? null : current);
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
if (isAbortLikeError(error, signal))
|
|
425
|
+
return;
|
|
426
|
+
params.setStreamReaderInfoByStepId((prev) => ({
|
|
427
|
+
...prev,
|
|
428
|
+
[step.stepId]: {
|
|
429
|
+
...(prev[step.stepId] ?? defaultStreamReaderInfo(step)),
|
|
430
|
+
status: "failed",
|
|
431
|
+
streamKey: active.streamKey,
|
|
432
|
+
startedAt: prev[step.stepId]?.startedAt ?? active.startedAt,
|
|
433
|
+
updatedAt: nowIso(),
|
|
434
|
+
completedAt: null,
|
|
435
|
+
attempts: attempt + 1,
|
|
436
|
+
chunkCount: active.chunks.length,
|
|
437
|
+
byteOffset: active.byteOffset,
|
|
438
|
+
streamByteOffset: active.streamByteOffset,
|
|
439
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
440
|
+
reason: "createReadStream threw while reading.",
|
|
441
|
+
...streamReaderRawDebug(active),
|
|
442
|
+
},
|
|
443
|
+
}));
|
|
444
|
+
console.warn("[ekairos:context-stream] step stream read failed", {
|
|
445
|
+
stepId: step.stepId,
|
|
446
|
+
attempt: attempt + 1,
|
|
447
|
+
error,
|
|
448
|
+
});
|
|
449
|
+
await sleep(500 * (attempt + 1), signal);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
const current = params.readers.get(step.stepId);
|
|
455
|
+
if (current === active) {
|
|
456
|
+
params.readers.delete(step.stepId);
|
|
457
|
+
if (signal.aborted) {
|
|
458
|
+
params.setStreamReaderInfoByStepId((prev) => ({
|
|
459
|
+
...prev,
|
|
460
|
+
[step.stepId]: {
|
|
461
|
+
...(prev[step.stepId] ?? defaultStreamReaderInfo(step)),
|
|
462
|
+
status: "aborted",
|
|
463
|
+
streamKey: active.streamKey,
|
|
464
|
+
startedAt: prev[step.stepId]?.startedAt ?? active.startedAt,
|
|
465
|
+
updatedAt: nowIso(),
|
|
466
|
+
completedAt: nowIso(),
|
|
467
|
+
attempts: prev[step.stepId]?.attempts ?? 0,
|
|
468
|
+
chunkCount: active.chunks.length,
|
|
469
|
+
byteOffset: active.byteOffset,
|
|
470
|
+
streamByteOffset: active.streamByteOffset,
|
|
471
|
+
reason: "Reader abort signal was triggered.",
|
|
472
|
+
...streamReaderRawDebug(active),
|
|
473
|
+
},
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function getLiveStepParts(event) {
|
|
480
|
+
if (!event)
|
|
481
|
+
return [];
|
|
482
|
+
return normalizePartsForStep(Array.isArray(event.content?.parts) ? event.content.parts : []);
|
|
483
|
+
}
|
|
484
|
+
function stepPartGroupKey(part, index) {
|
|
485
|
+
const action = getActionPartInfo(part);
|
|
486
|
+
if (action)
|
|
487
|
+
return `action:${action.actionCallId}`;
|
|
488
|
+
const type = asText(part.type) || "part";
|
|
489
|
+
if (type === "reasoning")
|
|
490
|
+
return "reasoning";
|
|
491
|
+
if (type === "message")
|
|
492
|
+
return "message";
|
|
493
|
+
if (type === "source")
|
|
494
|
+
return "source";
|
|
495
|
+
return `${type}:${index}`;
|
|
496
|
+
}
|
|
497
|
+
function groupStepParts(parts) {
|
|
498
|
+
const groups = new Map();
|
|
499
|
+
parts.forEach((part, index) => {
|
|
500
|
+
const key = stepPartGroupKey(part, index);
|
|
501
|
+
const group = groups.get(key);
|
|
502
|
+
if (group) {
|
|
503
|
+
group.parts.push(part);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
groups.set(key, { key, index, parts: [part] });
|
|
507
|
+
});
|
|
508
|
+
return [...groups.values()].sort((a, b) => a.index - b.index);
|
|
509
|
+
}
|
|
510
|
+
export function mergeContextStepPartsForUI(params) {
|
|
511
|
+
const persistedParts = normalizePartsForStep(params.persistedParts);
|
|
512
|
+
const liveParts = normalizePartsForStep(params.liveParts);
|
|
513
|
+
if (params.stepStatus && params.stepStatus !== "running")
|
|
514
|
+
return persistedParts;
|
|
515
|
+
if (liveParts.length === 0)
|
|
516
|
+
return persistedParts;
|
|
517
|
+
if (persistedParts.length === 0)
|
|
518
|
+
return liveParts;
|
|
519
|
+
const persistedGroups = groupStepParts(persistedParts);
|
|
520
|
+
const liveGroups = groupStepParts(liveParts);
|
|
521
|
+
const mergedGroups = new Map();
|
|
522
|
+
for (const group of persistedGroups) {
|
|
523
|
+
mergedGroups.set(group.key, {
|
|
524
|
+
...group,
|
|
525
|
+
sortIndex: group.index * 2,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
for (const liveGroup of liveGroups) {
|
|
529
|
+
const persistedGroup = mergedGroups.get(liveGroup.key);
|
|
530
|
+
if (!persistedGroup) {
|
|
531
|
+
mergedGroups.set(liveGroup.key, {
|
|
532
|
+
...liveGroup,
|
|
533
|
+
sortIndex: liveGroup.index * 2 + 1,
|
|
534
|
+
});
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (partGroupHasTerminalPersistedPart(persistedGroup.parts))
|
|
538
|
+
continue;
|
|
539
|
+
mergedGroups.set(liveGroup.key, {
|
|
540
|
+
...liveGroup,
|
|
541
|
+
sortIndex: persistedGroup.sortIndex,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return [...mergedGroups.values()]
|
|
545
|
+
.sort((a, b) => a.sortIndex - b.sortIndex)
|
|
546
|
+
.flatMap((group) => group.parts);
|
|
547
|
+
}
|
|
548
|
+
function partGroupHasTerminalPersistedPart(parts) {
|
|
549
|
+
return parts.some((part) => {
|
|
550
|
+
const action = getActionPartInfo(part);
|
|
551
|
+
if (action)
|
|
552
|
+
return action.status === "completed" || action.status === "failed";
|
|
553
|
+
const type = asText(part.type);
|
|
554
|
+
if (type === "reasoning") {
|
|
555
|
+
const state = getReasoningState(part).toLowerCase();
|
|
556
|
+
return (state === "done" ||
|
|
557
|
+
state === "completed" ||
|
|
558
|
+
state === "complete" ||
|
|
559
|
+
state === "finished");
|
|
560
|
+
}
|
|
561
|
+
return true;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
function withLiveStepParts(params) {
|
|
565
|
+
if (!params.liveEvent)
|
|
566
|
+
return params.step;
|
|
567
|
+
if (params.step.status !== "running")
|
|
568
|
+
return params.step;
|
|
569
|
+
const liveParts = getLiveStepParts(params.liveEvent);
|
|
570
|
+
const mergedParts = mergeContextStepPartsForUI({
|
|
571
|
+
persistedParts: params.step.parts,
|
|
572
|
+
liveParts,
|
|
573
|
+
stepStatus: params.step.status,
|
|
574
|
+
});
|
|
575
|
+
return {
|
|
576
|
+
...params.step,
|
|
577
|
+
status: params.step.status === "running" && params.liveEvent.status !== "completed"
|
|
578
|
+
? "running"
|
|
579
|
+
: params.step.status,
|
|
580
|
+
parts: mergedParts,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function toPublicStep(step) {
|
|
584
|
+
return {
|
|
585
|
+
stepId: step.stepId,
|
|
586
|
+
executionId: step.executionId,
|
|
587
|
+
createdAt: step.createdAt,
|
|
588
|
+
updatedAt: step.updatedAt,
|
|
589
|
+
status: step.status,
|
|
590
|
+
iteration: step.iteration,
|
|
591
|
+
parts: step.parts,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const useDefaultState = (db, { contextId, contextKey }) => {
|
|
595
|
+
const contextRes = db.useQuery(contextId || contextKey
|
|
596
|
+
? {
|
|
597
|
+
event_contexts: {
|
|
598
|
+
$: {
|
|
599
|
+
where: contextId
|
|
600
|
+
? { id: contextId }
|
|
601
|
+
: { key: contextKey },
|
|
602
|
+
limit: 1,
|
|
603
|
+
},
|
|
604
|
+
items: {
|
|
605
|
+
$: { order: { createdAt: "asc" } },
|
|
606
|
+
},
|
|
607
|
+
currentExecution: {},
|
|
608
|
+
executions: {
|
|
609
|
+
$: { order: { createdAt: "desc" }, limit: 50 },
|
|
610
|
+
trigger: {},
|
|
611
|
+
reaction: {},
|
|
612
|
+
items: {
|
|
613
|
+
$: { order: { createdAt: "asc" } },
|
|
614
|
+
},
|
|
615
|
+
steps: {
|
|
616
|
+
$: { order: { createdAt: "asc" }, limit: 500 },
|
|
617
|
+
stream: {},
|
|
618
|
+
parts: {
|
|
619
|
+
$: { order: { idx: "asc" }, limit: 1000 },
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
}
|
|
625
|
+
: null);
|
|
626
|
+
const ctx = contextRes?.data?.event_contexts?.[0] ?? null;
|
|
627
|
+
const raw = (ctx?.items ?? []);
|
|
628
|
+
return {
|
|
629
|
+
context: ctx,
|
|
630
|
+
contextStatus: normalizeContextStatus(ctx?.status),
|
|
631
|
+
events: Array.isArray(raw) ? raw : [],
|
|
632
|
+
};
|
|
633
|
+
};
|
|
634
|
+
export function useContext(db, opts) {
|
|
635
|
+
const { apiUrl, initialContextId, contextKey, onContextUpdate, prepareAppendArgs, prepareRequestBody, streamChunkDelayMs = DEFAULT_STREAM_CHUNK_DELAY_MS, state: useStateImpl = useDefaultState, } = opts;
|
|
636
|
+
const normalizedStreamChunkDelayMs = normalizeStreamChunkDelayMs(streamChunkDelayMs);
|
|
637
|
+
const [contextId, setContextId] = useState(initialContextId || null);
|
|
638
|
+
const [turnSubstateKey, setTurnSubstateKey] = useState(null);
|
|
639
|
+
const [sendError, setSendError] = useState(null);
|
|
640
|
+
const [optimisticEvents, setOptimisticEvents] = useState([]);
|
|
641
|
+
const [pendingRequests, setPendingRequests] = useState(0);
|
|
642
|
+
const [liveEventsByStepId, setLiveEventsByStepId] = useState({});
|
|
643
|
+
const [streamReaderInfoByStepId, setStreamReaderInfoByStepId] = useState({});
|
|
644
|
+
const selectedContextIdRef = useRef(initialContextId || null);
|
|
645
|
+
const requestControllersRef = useRef(new Set());
|
|
646
|
+
const stepReadersRef = useRef(new Map());
|
|
647
|
+
useEffect(() => {
|
|
648
|
+
setContextId(initialContextId || null);
|
|
649
|
+
}, [initialContextId]);
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
selectedContextIdRef.current = contextId;
|
|
652
|
+
}, [contextId]);
|
|
653
|
+
const handleContextUpdate = useCallback((nextId) => {
|
|
654
|
+
setContextId(nextId);
|
|
655
|
+
onContextUpdate?.(nextId);
|
|
656
|
+
}, [onContextUpdate]);
|
|
657
|
+
const { context } = useStateImpl(db, {
|
|
658
|
+
contextId,
|
|
659
|
+
contextKey,
|
|
660
|
+
});
|
|
661
|
+
const publicContext = useMemo(() => toPublicContextFirstLevel(context), [context]);
|
|
662
|
+
useEffect(() => {
|
|
663
|
+
const nextId = asText(context?.id);
|
|
664
|
+
if (!nextId)
|
|
665
|
+
return;
|
|
666
|
+
if (nextId === contextId)
|
|
667
|
+
return;
|
|
668
|
+
handleContextUpdate(nextId);
|
|
669
|
+
}, [context, contextId, handleContextUpdate]);
|
|
670
|
+
const persistedTree = useMemo(() => extractPersistedContextTree(context), [context]);
|
|
671
|
+
const steps = useMemo(() => buildContextStepViews({
|
|
672
|
+
filteredSteps: persistedTree.filteredSteps,
|
|
673
|
+
persistedPartsByStep: persistedTree.persistedPartsByStep,
|
|
674
|
+
}), [persistedTree.filteredSteps, persistedTree.persistedPartsByStep]);
|
|
675
|
+
const stepsByEventId = useMemo(() => buildEventStepsIndex({
|
|
676
|
+
executions: persistedTree.persistedExecutions,
|
|
677
|
+
steps,
|
|
678
|
+
}), [persistedTree.persistedExecutions, steps]);
|
|
679
|
+
const activeExecutionId = useMemo(() => {
|
|
680
|
+
const currentExecutionId = asText(context?.currentExecution?.id);
|
|
681
|
+
if (currentExecutionId)
|
|
682
|
+
return currentExecutionId;
|
|
683
|
+
return steps.find((step) => step.status === "running")?.executionId ?? steps[0]?.executionId ?? null;
|
|
684
|
+
}, [context, steps]);
|
|
685
|
+
const persistedEvents = useMemo(() => {
|
|
686
|
+
return persistedTree.persistedEvents
|
|
687
|
+
.map((event) => {
|
|
688
|
+
const attachedSteps = stepsByEventId.get(String(event.id)) ?? [];
|
|
689
|
+
const renderedRuntimeSteps = attachedSteps.map((step) => {
|
|
690
|
+
const readerInfo = streamReaderInfoByStepId[step.stepId];
|
|
691
|
+
const liveEvent = liveEventsByStepId[step.stepId];
|
|
692
|
+
const shouldUseLiveStep = Boolean(liveEvent) && step.status === "running";
|
|
693
|
+
const renderedStep = shouldUseLiveStep
|
|
694
|
+
? withLiveStepParts({
|
|
695
|
+
step,
|
|
696
|
+
liveEvent,
|
|
697
|
+
})
|
|
698
|
+
: step;
|
|
699
|
+
return {
|
|
700
|
+
...renderedStep,
|
|
701
|
+
streamReader: readerInfo ?? defaultStreamReaderInfo(renderedStep),
|
|
702
|
+
};
|
|
703
|
+
});
|
|
704
|
+
const renderedSteps = renderedRuntimeSteps.map(toPublicStep);
|
|
705
|
+
const eventParts = attachedSteps.length > 0 && !isUserEvent(event)
|
|
706
|
+
? []
|
|
707
|
+
: normalizeContextEventParts(Array.isArray(event.content?.parts) ? event.content.parts : []);
|
|
708
|
+
const hasRunningStep = renderedRuntimeSteps.some((step) => step.status === "running");
|
|
709
|
+
return {
|
|
710
|
+
...event,
|
|
711
|
+
executionId: renderedSteps[0]?.executionId ?? null,
|
|
712
|
+
steps: renderedSteps,
|
|
713
|
+
content: {
|
|
714
|
+
...(event.content ?? {}),
|
|
715
|
+
parts: eventParts,
|
|
716
|
+
},
|
|
717
|
+
...(hasRunningStep && !isUserEvent(event)
|
|
718
|
+
? {
|
|
719
|
+
status: "pending",
|
|
720
|
+
}
|
|
721
|
+
: {}),
|
|
722
|
+
};
|
|
723
|
+
});
|
|
724
|
+
}, [
|
|
725
|
+
liveEventsByStepId,
|
|
726
|
+
persistedTree.persistedEvents,
|
|
727
|
+
stepsByEventId,
|
|
728
|
+
streamReaderInfoByStepId,
|
|
729
|
+
]);
|
|
730
|
+
useEffect(() => {
|
|
731
|
+
const persistedIds = new Set(persistedEvents.map((event) => String(event.id)));
|
|
732
|
+
setOptimisticEvents((current) => {
|
|
733
|
+
const next = current.filter((event) => !persistedIds.has(String(event.id)));
|
|
734
|
+
return next.length === current.length ? current : next;
|
|
735
|
+
});
|
|
736
|
+
}, [persistedEvents]);
|
|
737
|
+
useEffect(() => {
|
|
738
|
+
syncContextStepStreamReaders({
|
|
739
|
+
db,
|
|
740
|
+
steps,
|
|
741
|
+
streamChunkDelayMs: normalizedStreamChunkDelayMs,
|
|
742
|
+
readers: stepReadersRef.current,
|
|
743
|
+
setStreamReaderInfoByStepId,
|
|
744
|
+
setLiveEventsByStepId,
|
|
745
|
+
setTurnSubstateKey,
|
|
746
|
+
});
|
|
747
|
+
}, [db, normalizedStreamChunkDelayMs, steps]);
|
|
748
|
+
useEffect(() => {
|
|
749
|
+
return () => {
|
|
750
|
+
abortAllStepReaders(stepReadersRef.current);
|
|
751
|
+
};
|
|
752
|
+
}, []);
|
|
753
|
+
const effectiveContextStatus = publicContext?.status ?? "open_idle";
|
|
754
|
+
const stop = useCallback(() => {
|
|
755
|
+
for (const controller of requestControllersRef.current) {
|
|
756
|
+
controller.abort();
|
|
757
|
+
}
|
|
758
|
+
requestControllersRef.current.clear();
|
|
759
|
+
}, []);
|
|
760
|
+
const append = useCallback(async ({ parts, webSearch, reasoningLevel }) => {
|
|
761
|
+
const nextArgs = prepareAppendArgs
|
|
762
|
+
? await prepareAppendArgs({ parts, webSearch, reasoningLevel })
|
|
763
|
+
: { parts, webSearch, reasoningLevel };
|
|
764
|
+
const messages = partsToSendPayload(nextArgs.parts);
|
|
765
|
+
if (messages[0].parts.length === 0)
|
|
766
|
+
return;
|
|
767
|
+
const activeContextId = selectedContextIdRef.current || randomUuidV4();
|
|
768
|
+
if (!selectedContextIdRef.current) {
|
|
769
|
+
selectedContextIdRef.current = activeContextId;
|
|
770
|
+
handleContextUpdate(activeContextId);
|
|
771
|
+
}
|
|
772
|
+
const preparedRequestBody = prepareRequestBody
|
|
773
|
+
? await prepareRequestBody({
|
|
774
|
+
messages,
|
|
775
|
+
webSearch: nextArgs.webSearch,
|
|
776
|
+
reasoningLevel: nextArgs.reasoningLevel,
|
|
777
|
+
contextId: activeContextId,
|
|
778
|
+
})
|
|
779
|
+
: {
|
|
780
|
+
messages,
|
|
781
|
+
webSearch: Boolean(nextArgs.webSearch),
|
|
782
|
+
reasoningLevel: nextArgs.reasoningLevel ?? "low",
|
|
783
|
+
contextId: activeContextId,
|
|
784
|
+
};
|
|
785
|
+
const requestBody = {
|
|
786
|
+
...preparedRequestBody,
|
|
787
|
+
contextId: asText(preparedRequestBody.contextId) || activeContextId,
|
|
788
|
+
};
|
|
789
|
+
const optimistic = messageToEphemeralEvent(messages[0], activeContextId);
|
|
790
|
+
setOptimisticEvents((current) => [...current, optimistic]);
|
|
791
|
+
setSendError(null);
|
|
792
|
+
const abortController = new AbortController();
|
|
793
|
+
requestControllersRef.current.add(abortController);
|
|
794
|
+
setPendingRequests((current) => current + 1);
|
|
795
|
+
try {
|
|
796
|
+
const response = await fetch(apiUrl, {
|
|
797
|
+
method: "POST",
|
|
798
|
+
headers: {
|
|
799
|
+
"Content-Type": "application/json",
|
|
800
|
+
},
|
|
801
|
+
body: JSON.stringify(requestBody),
|
|
802
|
+
signal: abortController.signal,
|
|
803
|
+
});
|
|
804
|
+
if (!response.ok) {
|
|
805
|
+
const text = await response.text().catch(() => "");
|
|
806
|
+
throw new Error(text || `Request failed with status ${response.status}.`);
|
|
807
|
+
}
|
|
808
|
+
const responseText = await response.text();
|
|
809
|
+
const parsed = (() => {
|
|
810
|
+
if (!responseText.trim())
|
|
811
|
+
return null;
|
|
812
|
+
try {
|
|
813
|
+
return JSON.parse(responseText);
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
})();
|
|
819
|
+
const nextContextId = asText(parsed?.contextId);
|
|
820
|
+
if (nextContextId) {
|
|
821
|
+
handleContextUpdate(nextContextId);
|
|
822
|
+
}
|
|
823
|
+
const assistantEvent = asRecord(parsed?.assistantEvent);
|
|
824
|
+
if (assistantEvent?.id) {
|
|
825
|
+
setOptimisticEvents((current) => [
|
|
826
|
+
...current.filter((event) => String(event.id) !== String(assistantEvent.id)),
|
|
827
|
+
{
|
|
828
|
+
...assistantEvent,
|
|
829
|
+
__contextId: nextContextId || activeContextId,
|
|
830
|
+
},
|
|
831
|
+
]);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
setOptimisticEvents((current) => current.filter((event) => String(event.id) !== String(optimistic.id)));
|
|
836
|
+
setSendError(error instanceof Error ? error.message : "Request failed");
|
|
837
|
+
throw error;
|
|
838
|
+
}
|
|
839
|
+
finally {
|
|
840
|
+
requestControllersRef.current.delete(abortController);
|
|
841
|
+
setPendingRequests((current) => Math.max(0, current - 1));
|
|
842
|
+
}
|
|
843
|
+
}, [apiUrl, handleContextUpdate, prepareAppendArgs, prepareRequestBody]);
|
|
844
|
+
const mergedEvents = useMemo(() => mergeEvents({
|
|
845
|
+
persisted: persistedEvents,
|
|
846
|
+
optimistic: optimisticEvents,
|
|
847
|
+
currentContextId: contextId,
|
|
848
|
+
}), [contextId, optimisticEvents, persistedEvents]);
|
|
849
|
+
const sendStatus = sendError
|
|
850
|
+
? "error"
|
|
851
|
+
: pendingRequests > 0
|
|
852
|
+
? "submitting"
|
|
853
|
+
: "idle";
|
|
854
|
+
return {
|
|
855
|
+
apiUrl,
|
|
856
|
+
context: publicContext,
|
|
857
|
+
contextId,
|
|
858
|
+
contextStatus: effectiveContextStatus,
|
|
859
|
+
activeExecutionId,
|
|
860
|
+
turnSubstateKey,
|
|
861
|
+
events: mergedEvents,
|
|
862
|
+
sendStatus,
|
|
863
|
+
sendError,
|
|
864
|
+
stop,
|
|
865
|
+
append,
|
|
866
|
+
};
|
|
867
|
+
}
|