@tangle-network/agent-app 0.43.29 → 0.43.31
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/chat-routes/index.d.ts +120 -3
- package/dist/chat-routes/index.js +278 -96
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/{chunk-ATRJULKZ.js → chunk-XYWCGFII.js} +17 -8
- package/dist/chunk-XYWCGFII.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/stream/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ATRJULKZ.js.map +0 -1
|
@@ -28,6 +28,16 @@ import '../contract-DYbTzEDf.js';
|
|
|
28
28
|
* Handlers are web-standard `Request → Response` (Workers, Node 18+, Deno) —
|
|
29
29
|
* no router import. Auth/access is one injected `authorize` seam, composable
|
|
30
30
|
* with `/app-auth` guards but not coupled to them.
|
|
31
|
+
*
|
|
32
|
+
* Five optional product seams let a complex turn-orchestrator compose the
|
|
33
|
+
* vertical instead of hand-rolling a generator — each omittable to the exact
|
|
34
|
+
* behavior above: `turnLock` (single-flight acquire/release around the turn),
|
|
35
|
+
* `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn`
|
|
36
|
+
* (observe + augment the producer input), `lifecycle` (deterministic
|
|
37
|
+
* start/complete/error telemetry), `heartbeat` (keepalive during silent
|
|
38
|
+
* producer waits), plus `onRawEvent` (the raw producer events, for telemetry).
|
|
39
|
+
* `handleChatTurn` stays the engine — the seams only wrap its input, its
|
|
40
|
+
* producer stream, and its settle.
|
|
31
41
|
*/
|
|
32
42
|
|
|
33
43
|
/** Usage receipt persisted onto the assistant message (the flattened
|
|
@@ -103,6 +113,86 @@ interface ChatTurnProduceArgs<TContext> {
|
|
|
103
113
|
turnStreamId: string;
|
|
104
114
|
priorMessages: PersistedChatMessageForTurn[];
|
|
105
115
|
}
|
|
116
|
+
/** One event as it crosses the route: the producer's own vocabulary, or an
|
|
117
|
+
* injected keepalive. Same shape the engine forwards verbatim. */
|
|
118
|
+
type ChatRouteEvent = {
|
|
119
|
+
type: string;
|
|
120
|
+
data?: Record<string, unknown>;
|
|
121
|
+
};
|
|
122
|
+
/** Keepalive emitted while the producer is quiet (long tool calls, first-token
|
|
123
|
+
* wait) so client watchdogs stay re-armed. One is emitted each time
|
|
124
|
+
* `intervalMs` elapses with no producer event; the window resets on every real
|
|
125
|
+
* event, so a chatty producer never triggers one. The product owns the event
|
|
126
|
+
* shape (`type` + `data`). Omit → no keepalives (today's behavior). */
|
|
127
|
+
interface ChatTurnHeartbeat {
|
|
128
|
+
intervalMs: number;
|
|
129
|
+
event(info: {
|
|
130
|
+
elapsedMs: number;
|
|
131
|
+
tick: number;
|
|
132
|
+
}): ChatRouteEvent;
|
|
133
|
+
}
|
|
134
|
+
/** Patch a `beforeTurn` hook returns to augment the producer's input. Omitted
|
|
135
|
+
* fields keep the route-assembled value; the product's `produce` still owns
|
|
136
|
+
* the system prompt. */
|
|
137
|
+
interface ChatTurnInputPatch {
|
|
138
|
+
prompt?: string | ChatTurnPartInput[];
|
|
139
|
+
priorMessages?: PersistedChatMessageForTurn[];
|
|
140
|
+
}
|
|
141
|
+
/** Pre-turn readiness verdict — proceed, or short-circuit with the product's
|
|
142
|
+
* own `Response` (e.g. a canned assistant reply asking for missing context).
|
|
143
|
+
* Distinct from `authorize`: this gates domain readiness, not access. */
|
|
144
|
+
type ChatTurnGateResult = {
|
|
145
|
+
proceed: true;
|
|
146
|
+
} | {
|
|
147
|
+
proceed: false;
|
|
148
|
+
response: Response;
|
|
149
|
+
};
|
|
150
|
+
/** Single-flight lock verdict — acquired (with an opaque handle passed back to
|
|
151
|
+
* `release`), or already held (short-circuit with the product's 409-style
|
|
152
|
+
* `Response`). */
|
|
153
|
+
type ChatTurnLockResult = {
|
|
154
|
+
acquired: true;
|
|
155
|
+
handle?: unknown;
|
|
156
|
+
} | {
|
|
157
|
+
acquired: false;
|
|
158
|
+
response: Response;
|
|
159
|
+
};
|
|
160
|
+
/** Async acquire/release wrapped around the turn. `acquire` runs before any
|
|
161
|
+
* side effect; `release` runs once when the turn settles — including on a
|
|
162
|
+
* short-circuit or a throw. */
|
|
163
|
+
interface ChatTurnLock<TContext> {
|
|
164
|
+
acquire(args: ChatTurnProduceArgs<TContext>): ChatTurnLockResult | Promise<ChatTurnLockResult>;
|
|
165
|
+
release(handle: unknown): void | Promise<void>;
|
|
166
|
+
}
|
|
167
|
+
interface ChatTurnLifecycleBase<TContext> {
|
|
168
|
+
identity: ChatTurnIdentity;
|
|
169
|
+
executionId: string;
|
|
170
|
+
turnStreamId: string;
|
|
171
|
+
context: TContext;
|
|
172
|
+
}
|
|
173
|
+
interface ChatTurnLifecycleStart<TContext> extends ChatTurnLifecycleBase<TContext> {
|
|
174
|
+
startedAt: number;
|
|
175
|
+
}
|
|
176
|
+
interface ChatTurnLifecycleComplete<TContext> extends ChatTurnLifecycleBase<TContext> {
|
|
177
|
+
finalText: string;
|
|
178
|
+
usage: ChatTurnUsage;
|
|
179
|
+
durationMs: number;
|
|
180
|
+
}
|
|
181
|
+
interface ChatTurnLifecycleError<TContext> extends ChatTurnLifecycleBase<TContext> {
|
|
182
|
+
error: unknown;
|
|
183
|
+
durationMs: number;
|
|
184
|
+
}
|
|
185
|
+
/** Deterministic run telemetry: `onTurnStart` fires before the producer runs;
|
|
186
|
+
* exactly one of `onTurnComplete` / `onTurnError` fires after the turn
|
|
187
|
+
* settles, always after `onTurnStart`. Failure is derived from the turn's own
|
|
188
|
+
* `error` / `session.run.failed` events (or a drain throw), not the engine's
|
|
189
|
+
* lifecycle envelope. Hook errors are swallowed — telemetry never fails a
|
|
190
|
+
* turn. */
|
|
191
|
+
interface ChatTurnLifecycle<TContext> {
|
|
192
|
+
onTurnStart?(info: ChatTurnLifecycleStart<TContext>): void | Promise<void>;
|
|
193
|
+
onTurnComplete?(info: ChatTurnLifecycleComplete<TContext>): void | Promise<void>;
|
|
194
|
+
onTurnError?(info: ChatTurnLifecycleError<TContext>): void | Promise<void>;
|
|
195
|
+
}
|
|
106
196
|
interface CreateChatTurnRoutesOptions<TContext = void> {
|
|
107
197
|
/** Names the product in `deriveExecutionId` so retries land on the same
|
|
108
198
|
* substrate execution. */
|
|
@@ -121,15 +211,42 @@ interface CreateChatTurnRoutesOptions<TContext = void> {
|
|
|
121
211
|
* wrapped in `createSandboxChatProducer`. Router/openai-compat lane: the
|
|
122
212
|
* product's own producer. May be async (box resolution). */
|
|
123
213
|
produce(args: ChatTurnProduceArgs<TContext>): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer>;
|
|
214
|
+
/** Single-flight lock acquired before any side effect and released once when
|
|
215
|
+
* the turn settles (including short-circuit/throw). Omit → no lock. */
|
|
216
|
+
turnLock?: ChatTurnLock<TContext>;
|
|
217
|
+
/** Pre-turn readiness gate that can short-circuit with a product `Response`
|
|
218
|
+
* before the producer runs (the user row is already persisted). Runs after
|
|
219
|
+
* `turnLock.acquire`, before `beforeTurn`. Omit → always proceed. */
|
|
220
|
+
contextGate?(args: ChatTurnProduceArgs<TContext>): ChatTurnGateResult | Promise<ChatTurnGateResult>;
|
|
221
|
+
/** Observe the assembled producer input and optionally augment it (rewrite
|
|
222
|
+
* the prompt / prior messages) before the producer runs. Omit → no change. */
|
|
223
|
+
beforeTurn?(args: ChatTurnProduceArgs<TContext>): ChatTurnInputPatch | void | Promise<ChatTurnInputPatch | void>;
|
|
224
|
+
/** Deterministic run telemetry (start / complete / error) with identity and
|
|
225
|
+
* timing. Omit → no telemetry. */
|
|
226
|
+
lifecycle?: ChatTurnLifecycle<TContext>;
|
|
227
|
+
/** Keepalive injected while the producer is quiet. Omit → no keepalives. */
|
|
228
|
+
heartbeat?: ChatTurnHeartbeat;
|
|
229
|
+
/** Observe each event the producer emits, before the engine frames it and
|
|
230
|
+
* before any heartbeat injection (the raw sidecar-producer events, for
|
|
231
|
+
* telemetry). Never alters the stream; errors are swallowed. Distinct from
|
|
232
|
+
* `onEvent`, which sees the engine-framed stream incl. lifecycle envelopes. */
|
|
233
|
+
onRawEvent?(event: ChatRouteEvent, context: TContext): void | Promise<void>;
|
|
124
234
|
/** Pre-persist transform of the final text (e.g. `/redact`'s `redactPII`).
|
|
125
235
|
* Live stream is never altered. */
|
|
126
236
|
transformFinalText?(text: string): string | Promise<string>;
|
|
127
|
-
/** Post-processing after a
|
|
128
|
-
*
|
|
237
|
+
/** Post-processing after a turn settles (billing, titles, audit). Fires with
|
|
238
|
+
* `failed:true` + `failureReason` when the turn carried a terminal error
|
|
239
|
+
* event (model 402 / rate-limit / server error) instead of a clean
|
|
240
|
+
* completion, so products skip the deduct and render an error row rather
|
|
241
|
+
* than billing an empty turn and marking it done. A turn that THROWS never
|
|
242
|
+
* reaches this hook (the engine skips it on a producer throw). Errors are
|
|
243
|
+
* swallowed by the engine — they never fail a streamed turn. */
|
|
129
244
|
onTurnComplete?(input: {
|
|
130
245
|
identity: ChatTurnIdentity;
|
|
131
246
|
finalText: string;
|
|
132
247
|
context: TContext;
|
|
248
|
+
failed: boolean;
|
|
249
|
+
failureReason?: string;
|
|
133
250
|
}): Promise<void>;
|
|
134
251
|
/** Per-event side channel (product broadcast). The turn-buffer tap is
|
|
135
252
|
* already wired; this runs in addition. */
|
|
@@ -273,4 +390,4 @@ declare function sanitizeUploadFilename(name: string): string;
|
|
|
273
390
|
declare function bytesToBase64(bytes: Uint8Array): string;
|
|
274
391
|
declare function createUploadRoute(options: CreateUploadRouteOptions): (request: Request) => Promise<Response>;
|
|
275
392
|
|
|
276
|
-
export { type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, bytesToBase64, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, sanitizeUploadFilename };
|
|
393
|
+
export { type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, bytesToBase64, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, sanitizeUploadFilename };
|
|
@@ -32,10 +32,16 @@ import {
|
|
|
32
32
|
normalizeToolEvent,
|
|
33
33
|
replayTurnEvents,
|
|
34
34
|
resolveChatTurn
|
|
35
|
-
} from "../chunk-
|
|
35
|
+
} from "../chunk-XYWCGFII.js";
|
|
36
36
|
|
|
37
37
|
// src/chat-routes/turn-routes.ts
|
|
38
38
|
import { deriveExecutionId, handleChatTurn } from "@tangle-network/agent-runtime";
|
|
39
|
+
function failureReasonOf(data) {
|
|
40
|
+
if (!data) return void 0;
|
|
41
|
+
const message = data.message ?? data.error ?? data.reason;
|
|
42
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
39
45
|
function errorResponse(err) {
|
|
40
46
|
return Response.json({ code: err.code, error: err.message }, { status: err.status });
|
|
41
47
|
}
|
|
@@ -66,6 +72,49 @@ function validateTurnBody(body, maxInlinePartBytes) {
|
|
|
66
72
|
function userPartsWithFiles(userParts, fileParts) {
|
|
67
73
|
return toChatMessageParts([...userParts, ...fileParts.map((part) => ({ ...part }))]);
|
|
68
74
|
}
|
|
75
|
+
async function* tapRawEvents(source, onRawEvent, log) {
|
|
76
|
+
for await (const event of source) {
|
|
77
|
+
try {
|
|
78
|
+
await onRawEvent(event);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
log("[chat-routes] onRawEvent failed", { error: err instanceof Error ? err.message : String(err) });
|
|
81
|
+
}
|
|
82
|
+
yield event;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function* withStreamHeartbeat(source, intervalMs, makeEvent) {
|
|
86
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
87
|
+
try {
|
|
88
|
+
let pending = iterator.next();
|
|
89
|
+
let windowStart = Date.now();
|
|
90
|
+
let tick = 0;
|
|
91
|
+
for (; ; ) {
|
|
92
|
+
let timer;
|
|
93
|
+
let winner;
|
|
94
|
+
try {
|
|
95
|
+
const heartbeat = new Promise((resolve) => {
|
|
96
|
+
timer = setTimeout(() => resolve("heartbeat"), intervalMs);
|
|
97
|
+
});
|
|
98
|
+
winner = await Promise.race([pending.then(() => "event"), heartbeat]);
|
|
99
|
+
} finally {
|
|
100
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
if (winner === "heartbeat") {
|
|
103
|
+
tick += 1;
|
|
104
|
+
yield makeEvent({ elapsedMs: Date.now() - windowStart, tick });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const result = await pending;
|
|
108
|
+
if (result.done) return;
|
|
109
|
+
yield result.value;
|
|
110
|
+
pending = iterator.next();
|
|
111
|
+
windowStart = Date.now();
|
|
112
|
+
tick = 0;
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
await iterator.return?.();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
69
118
|
function createChatTurnRoutes(options) {
|
|
70
119
|
const log = options.log ?? ((message, meta) => console.error(message, meta ?? ""));
|
|
71
120
|
async function turn(request, ctx) {
|
|
@@ -89,14 +138,6 @@ function createChatTurnRoutes(options) {
|
|
|
89
138
|
parts: m.parts ?? null
|
|
90
139
|
}));
|
|
91
140
|
const chatTurn = resolveChatTurn({ existingMessages, userContent: content, turnId });
|
|
92
|
-
if (chatTurn.shouldInsertUserMessage) {
|
|
93
|
-
await options.store.appendMessage({
|
|
94
|
-
threadId: payload.threadId,
|
|
95
|
-
role: "user",
|
|
96
|
-
content,
|
|
97
|
-
parts: userPartsWithFiles(chatTurn.userParts, fileParts)
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
141
|
const identity = {
|
|
101
142
|
tenantId,
|
|
102
143
|
sessionId: payload.threadId,
|
|
@@ -110,104 +151,245 @@ function createChatTurnRoutes(options) {
|
|
|
110
151
|
});
|
|
111
152
|
const turnStreamId = crypto.randomUUID();
|
|
112
153
|
const prompt = fileParts.length === 0 ? content : content ? [{ type: "text", text: content }, ...fileParts] : [...fileParts];
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
scopeId: payload.threadId,
|
|
117
|
-
coalesce: options.coalesceTurnEvents ?? coalesceDeltas
|
|
118
|
-
});
|
|
119
|
-
const turnMarker = { type: "turn", turnId: turnStreamId };
|
|
120
|
-
await tap.onEvent(turnMarker);
|
|
121
|
-
let producer;
|
|
122
|
-
let runFailed = false;
|
|
123
|
-
const result = handleChatTurn({
|
|
154
|
+
let produceArgs = {
|
|
155
|
+
request,
|
|
156
|
+
body: payload,
|
|
124
157
|
identity,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
finalText: () => producer?.finalText() ?? ""
|
|
145
|
-
}),
|
|
146
|
-
onEvent: async (event) => {
|
|
147
|
-
if (event.type === "session.run.failed" || event.type === "error") runFailed = true;
|
|
148
|
-
await tap.onEvent(event);
|
|
149
|
-
if (options.onEvent) await options.onEvent(event, context);
|
|
150
|
-
},
|
|
151
|
-
...options.transformFinalText ? { transformFinalText: options.transformFinalText } : {},
|
|
152
|
-
persistAssistantMessage: async ({ finalText }) => {
|
|
153
|
-
const parts = producer?.assistantParts ? toChatMessageParts(producer.assistantParts()) : void 0;
|
|
154
|
-
if (!finalText.trim() && (!parts || parts.length === 0)) return;
|
|
155
|
-
const usage = producer?.usage?.() ?? {};
|
|
156
|
-
await options.store.appendMessage({
|
|
157
|
-
threadId: payload.threadId,
|
|
158
|
-
role: "assistant",
|
|
159
|
-
content: finalText,
|
|
160
|
-
...parts && parts.length > 0 ? { parts } : {},
|
|
161
|
-
...producer?.model ? { model: producer.model } : {},
|
|
162
|
-
...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
|
|
163
|
-
...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
|
|
164
|
-
...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {},
|
|
165
|
-
...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
|
|
166
|
-
...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
|
|
167
|
-
...usage.costUsd !== void 0 ? { costUsd: usage.costUsd } : {}
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
...options.onTurnComplete ? {
|
|
171
|
-
onTurnComplete: ({ identity: turnIdentity, finalText }) => options.onTurnComplete({ identity: turnIdentity, finalText, context })
|
|
172
|
-
} : {},
|
|
173
|
-
...options.traceFlush ? { traceFlush: () => options.traceFlush(context) } : {}
|
|
158
|
+
context,
|
|
159
|
+
prompt,
|
|
160
|
+
executionId,
|
|
161
|
+
turnStreamId,
|
|
162
|
+
priorMessages: chatTurn.priorMessages
|
|
163
|
+
};
|
|
164
|
+
let lockAcquired = false;
|
|
165
|
+
let lockHandle;
|
|
166
|
+
let lockReleased = false;
|
|
167
|
+
const releaseLock = async () => {
|
|
168
|
+
if (!lockAcquired || lockReleased) return;
|
|
169
|
+
lockReleased = true;
|
|
170
|
+
try {
|
|
171
|
+
await options.turnLock.release(lockHandle);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
log("[chat-routes] turnLock.release failed", {
|
|
174
|
+
turnId: turnStreamId,
|
|
175
|
+
error: err instanceof Error ? err.message : String(err)
|
|
176
|
+
});
|
|
174
177
|
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
};
|
|
179
|
+
if (options.turnLock) {
|
|
180
|
+
const acquired = await options.turnLock.acquire(produceArgs);
|
|
181
|
+
if (!acquired.acquired) return acquired.response;
|
|
182
|
+
lockAcquired = true;
|
|
183
|
+
lockHandle = acquired.handle;
|
|
184
|
+
}
|
|
185
|
+
let producer;
|
|
186
|
+
let runFailed = false;
|
|
187
|
+
let lastFailureData;
|
|
188
|
+
let turnStartedAtMs = 0;
|
|
189
|
+
let turnStarted = false;
|
|
190
|
+
let lifecycleSettled = false;
|
|
191
|
+
const fireTerminalLifecycle = async (failed, terminalError) => {
|
|
192
|
+
if (lifecycleSettled) return;
|
|
193
|
+
lifecycleSettled = true;
|
|
194
|
+
const lifecycle = options.lifecycle;
|
|
195
|
+
if (!lifecycle) return;
|
|
196
|
+
const durationMs = Date.now() - turnStartedAtMs;
|
|
179
197
|
try {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
198
|
+
if (failed) {
|
|
199
|
+
await lifecycle.onTurnError?.({
|
|
200
|
+
identity,
|
|
201
|
+
executionId,
|
|
202
|
+
turnStreamId,
|
|
203
|
+
context,
|
|
204
|
+
durationMs,
|
|
205
|
+
error: terminalError ?? lastFailureData ?? new Error("chat turn failed")
|
|
206
|
+
});
|
|
207
|
+
} else {
|
|
208
|
+
await lifecycle.onTurnComplete?.({
|
|
209
|
+
identity,
|
|
210
|
+
executionId,
|
|
211
|
+
turnStreamId,
|
|
212
|
+
context,
|
|
213
|
+
durationMs,
|
|
214
|
+
finalText: producer?.finalText() ?? "",
|
|
215
|
+
usage: producer?.usage?.() ?? {}
|
|
216
|
+
});
|
|
183
217
|
}
|
|
184
|
-
await tap.done(runFailed ? "error" : "complete");
|
|
185
218
|
} catch (err) {
|
|
186
|
-
|
|
187
|
-
log("[chat-routes] turn drain failed", {
|
|
219
|
+
log("[chat-routes] lifecycle terminal hook failed", {
|
|
188
220
|
turnId: turnStreamId,
|
|
189
221
|
error: err instanceof Error ? err.message : String(err)
|
|
190
222
|
});
|
|
191
223
|
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
controller.close();
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
if (chatTurn.shouldInsertUserMessage) {
|
|
227
|
+
await options.store.appendMessage({
|
|
228
|
+
threadId: payload.threadId,
|
|
229
|
+
role: "user",
|
|
230
|
+
content,
|
|
231
|
+
parts: userPartsWithFiles(chatTurn.userParts, fileParts)
|
|
232
|
+
});
|
|
202
233
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
234
|
+
if (options.contextGate) {
|
|
235
|
+
const gate = await options.contextGate(produceArgs);
|
|
236
|
+
if (!gate.proceed) {
|
|
237
|
+
await releaseLock();
|
|
238
|
+
return gate.response;
|
|
239
|
+
}
|
|
209
240
|
}
|
|
210
|
-
|
|
241
|
+
if (options.beforeTurn) {
|
|
242
|
+
const patch = await options.beforeTurn(produceArgs);
|
|
243
|
+
if (patch) produceArgs = { ...produceArgs, ...patch };
|
|
244
|
+
}
|
|
245
|
+
const tap = createBufferedTurnTap({
|
|
246
|
+
store: options.turnStore,
|
|
247
|
+
turnId: turnStreamId,
|
|
248
|
+
scopeId: payload.threadId,
|
|
249
|
+
coalesce: options.coalesceTurnEvents ?? coalesceDeltas
|
|
250
|
+
});
|
|
251
|
+
const turnMarker = { type: "turn", turnId: turnStreamId };
|
|
252
|
+
await tap.onEvent(turnMarker);
|
|
253
|
+
turnStartedAtMs = Date.now();
|
|
254
|
+
turnStarted = true;
|
|
255
|
+
if (options.lifecycle?.onTurnStart) {
|
|
256
|
+
try {
|
|
257
|
+
await options.lifecycle.onTurnStart({
|
|
258
|
+
identity,
|
|
259
|
+
executionId,
|
|
260
|
+
turnStreamId,
|
|
261
|
+
context,
|
|
262
|
+
startedAt: turnStartedAtMs
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
log("[chat-routes] lifecycle.onTurnStart failed", {
|
|
266
|
+
turnId: turnStreamId,
|
|
267
|
+
error: err instanceof Error ? err.message : String(err)
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const result = handleChatTurn({
|
|
272
|
+
identity,
|
|
273
|
+
waitUntil: ctx?.waitUntil,
|
|
274
|
+
log,
|
|
275
|
+
hooks: {
|
|
276
|
+
// The engine wants a synchronous producer; box resolution is async —
|
|
277
|
+
// defer it into the generator's first pull.
|
|
278
|
+
produce: () => ({
|
|
279
|
+
stream: (async function* () {
|
|
280
|
+
producer = await options.produce(produceArgs);
|
|
281
|
+
let source = producer.stream;
|
|
282
|
+
if (options.onRawEvent) {
|
|
283
|
+
source = tapRawEvents(source, (event) => options.onRawEvent(event, context), log);
|
|
284
|
+
}
|
|
285
|
+
if (options.heartbeat) {
|
|
286
|
+
source = withStreamHeartbeat(source, options.heartbeat.intervalMs, options.heartbeat.event);
|
|
287
|
+
}
|
|
288
|
+
for await (const event of source) yield event;
|
|
289
|
+
})(),
|
|
290
|
+
finalText: () => producer?.finalText() ?? ""
|
|
291
|
+
}),
|
|
292
|
+
onEvent: async (event) => {
|
|
293
|
+
if (event.type === "session.run.failed" || event.type === "error") {
|
|
294
|
+
runFailed = true;
|
|
295
|
+
lastFailureData = event.data;
|
|
296
|
+
}
|
|
297
|
+
await tap.onEvent(event);
|
|
298
|
+
if (options.onEvent) await options.onEvent(event, context);
|
|
299
|
+
},
|
|
300
|
+
...options.transformFinalText ? { transformFinalText: options.transformFinalText } : {},
|
|
301
|
+
persistAssistantMessage: async ({ finalText }) => {
|
|
302
|
+
const rawParts = producer?.assistantParts ? producer.assistantParts() : void 0;
|
|
303
|
+
const projected = rawParts && options.transformFinalText ? await Promise.all(
|
|
304
|
+
rawParts.map(
|
|
305
|
+
async (part) => String(part.type ?? "") === "text" ? { ...part, text: await options.transformFinalText(String(part.text ?? "")) } : part
|
|
306
|
+
)
|
|
307
|
+
) : rawParts;
|
|
308
|
+
const parts = projected ? toChatMessageParts(projected) : void 0;
|
|
309
|
+
if (!finalText.trim() && (!parts || parts.length === 0)) return;
|
|
310
|
+
const usage = producer?.usage?.() ?? {};
|
|
311
|
+
await options.store.appendMessage({
|
|
312
|
+
threadId: payload.threadId,
|
|
313
|
+
role: "assistant",
|
|
314
|
+
content: finalText,
|
|
315
|
+
...parts && parts.length > 0 ? { parts } : {},
|
|
316
|
+
...producer?.model ? { model: producer.model } : {},
|
|
317
|
+
...usage.inputTokens !== void 0 ? { inputTokens: usage.inputTokens } : {},
|
|
318
|
+
...usage.outputTokens !== void 0 ? { outputTokens: usage.outputTokens } : {},
|
|
319
|
+
...usage.reasoningTokens !== void 0 ? { reasoningTokens: usage.reasoningTokens } : {},
|
|
320
|
+
...usage.cacheReadTokens !== void 0 ? { cacheReadTokens: usage.cacheReadTokens } : {},
|
|
321
|
+
...usage.cacheWriteTokens !== void 0 ? { cacheWriteTokens: usage.cacheWriteTokens } : {},
|
|
322
|
+
...usage.costUsd !== void 0 ? { costUsd: usage.costUsd } : {}
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
...options.onTurnComplete ? {
|
|
326
|
+
// Wired into the engine's completion hook, which fires only when
|
|
327
|
+
// the stream ended without throwing. A terminal error EVENT
|
|
328
|
+
// (not a throw) still lands here — so surface `runFailed` so the
|
|
329
|
+
// product skips billing an errored turn instead of marking it
|
|
330
|
+
// complete with empty text.
|
|
331
|
+
onTurnComplete: ({ identity: turnIdentity, finalText }) => options.onTurnComplete({
|
|
332
|
+
identity: turnIdentity,
|
|
333
|
+
finalText,
|
|
334
|
+
context,
|
|
335
|
+
failed: runFailed,
|
|
336
|
+
...runFailed ? { failureReason: failureReasonOf(lastFailureData) } : {}
|
|
337
|
+
})
|
|
338
|
+
} : {},
|
|
339
|
+
...options.traceFlush ? { traceFlush: () => options.traceFlush(context) } : {}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
const [clientBody, drainBody] = result.body.tee();
|
|
343
|
+
const drained = (async () => {
|
|
344
|
+
const reader = drainBody.getReader();
|
|
345
|
+
let drainError;
|
|
346
|
+
try {
|
|
347
|
+
for (; ; ) {
|
|
348
|
+
const { done } = await reader.read();
|
|
349
|
+
if (done) break;
|
|
350
|
+
}
|
|
351
|
+
} catch (err) {
|
|
352
|
+
drainError = err;
|
|
353
|
+
log("[chat-routes] turn drain failed", {
|
|
354
|
+
turnId: turnStreamId,
|
|
355
|
+
error: err instanceof Error ? err.message : String(err)
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const failed = runFailed || drainError !== void 0;
|
|
359
|
+
try {
|
|
360
|
+
await tap.done(failed ? "error" : "complete");
|
|
361
|
+
} catch (err) {
|
|
362
|
+
log("[chat-routes] turn buffer finalize failed", {
|
|
363
|
+
turnId: turnStreamId,
|
|
364
|
+
error: err instanceof Error ? err.message : String(err)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
await fireTerminalLifecycle(failed, drainError);
|
|
368
|
+
await releaseLock();
|
|
369
|
+
})();
|
|
370
|
+
if (ctx?.waitUntil) ctx.waitUntil(drained);
|
|
371
|
+
else void drained.catch(() => {
|
|
372
|
+
});
|
|
373
|
+
const encoder = new TextEncoder();
|
|
374
|
+
const marker = new ReadableStream({
|
|
375
|
+
start(controller) {
|
|
376
|
+
controller.enqueue(encoder.encode(`${JSON.stringify(turnMarker)}
|
|
377
|
+
`));
|
|
378
|
+
controller.close();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
const body = concatStreams([marker, clientBody]);
|
|
382
|
+
return new Response(body, {
|
|
383
|
+
headers: {
|
|
384
|
+
"Content-Type": result.contentType,
|
|
385
|
+
"Cache-Control": "no-cache"
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
} catch (err) {
|
|
389
|
+
if (turnStarted) await fireTerminalLifecycle(true, err);
|
|
390
|
+
await releaseLock();
|
|
391
|
+
throw err;
|
|
392
|
+
}
|
|
211
393
|
}
|
|
212
394
|
async function replay(request, params) {
|
|
213
395
|
const turnId = params.turnId?.trim();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/chat-routes/turn-routes.ts","../../src/chat-routes/sandbox-producer.ts","../../src/chat-routes/upload.ts"],"sourcesContent":["/**\n * `createChatTurnRoutes` — the assembled server chat vertical (issue #188\n * Phase 1). One factory composing the pieces every product re-wired by hand:\n *\n * body parse/validate → `/web` `parseJsonObjectBody` + `./wire`\n * turn identity → `/stream` `resolveChatTurn` + agent-runtime\n * `deriveExecutionId`\n * producer → injected seam (sandbox lane via\n * `createSandboxChatProducer`; router lane is the\n * product's own `ChatTurnProducer`)\n * turn engine → agent-runtime `handleChatTurn` (verbatim)\n * durability → `/stream` turn-buffer tap, wired BY DEFAULT\n * (tee + drain keeps the turn running after a\n * client drop; replay serves the buffered tail)\n * persistence → injected `/chat-store`-shaped store\n * (user row on send, assistant row on completion)\n * interactions answer → `/interactions` `createInteractionAnswerRoute`\n *\n * Handlers are web-standard `Request → Response` (Workers, Node 18+, Deno) —\n * no router import. Auth/access is one injected `authorize` seam, composable\n * with `/app-auth` guards but not coupled to them.\n */\n\nimport { deriveExecutionId, handleChatTurn } from '@tangle-network/agent-runtime'\nimport type { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime'\nimport { toChatMessageParts, type ChatMessagePart } from '../chat-store/parts'\nimport {\n createInteractionAnswerRoute,\n type InteractionAnswerRoute,\n type InteractionAnswerRouteOptions,\n} from '../interactions/route'\nimport {\n coalesceDeltas,\n createBufferedTurnTap,\n normalizeClientTurnId,\n replayTurnEvents,\n resolveChatTurn,\n type PersistedChatMessageForTurn,\n type TurnEventStore,\n} from '../stream/index'\nimport { parseJsonObjectBody } from '../web/index'\nimport {\n assertPromptPartsWithinCap,\n ChatTurnInputError,\n parseChatTurnParts,\n type ChatTurnFilePartInput,\n type ChatTurnPartInput,\n type ChatTurnRequestPayload,\n} from './wire'\n\n// ── seams ───────────────────────────────────────────────────────────────────\n\n/** Usage receipt persisted onto the assistant message (the flattened\n * `step-finish` shape `/chat-store`'s columns mirror). */\nexport interface ChatTurnUsage {\n inputTokens?: number\n outputTokens?: number\n reasoningTokens?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n costUsd?: number\n}\n\n/** What the route persists — a structural subset of `/chat-store`'s\n * `ChatStore`, so `createChatStore(db, tables)` satisfies it directly and a\n * product with its own persistence adapts without importing drizzle. */\nexport interface ChatTurnMessageStore {\n listMessages(threadId: string): Promise<Array<{\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts?: ChatMessagePart[] | null\n }>>\n appendMessage(input: {\n threadId: string\n role: 'user' | 'assistant'\n content: string\n parts?: ChatMessagePart[]\n model?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n }): Promise<unknown>\n}\n\n/** `ChatTurnProducer` plus the persisted projection the assembly reads after\n * drain. `createSandboxChatProducer` returns this; a router-lane producer\n * may omit the optional members (finalText persists as a single text part). */\nexport interface ChatTurnRouteProducer extends ChatTurnProducer {\n assistantParts?(): Array<Record<string, unknown>>\n usage?(): ChatTurnUsage\n model?: string\n}\n\nexport type ChatTurnAuthorization<TContext> =\n | { ok: true; tenantId: string; userId: string; context: TContext }\n | { ok: false; response: Response }\n\nexport interface ChatTurnAuthorizeArgs {\n request: Request\n intent: 'turn' | 'replay'\n /** Parsed, validated POST body (turn intent only). */\n body?: ChatTurnRequestPayload\n /** The buffered turn id being replayed (replay intent only). */\n turnId?: string\n}\n\nexport interface ChatTurnProduceArgs<TContext> {\n request: Request\n body: ChatTurnRequestPayload\n identity: ChatTurnIdentity\n context: TContext\n /** The message to send: plain text, or parts when the client attached\n * files (a text part is prepended from `content` when present). */\n prompt: string | ChatTurnPartInput[]\n /** Stable id for cross-process reconnect (`deriveExecutionId`). */\n executionId: string\n /** The turn-buffer id announced to the client for replay. */\n turnStreamId: string\n priorMessages: PersistedChatMessageForTurn[]\n}\n\nexport interface CreateChatTurnRoutesOptions<TContext = void> {\n /** Names the product in `deriveExecutionId` so retries land on the same\n * substrate execution. */\n projectId: string\n /** Authenticate + authorize the caller for a turn or a replay. The only\n * product-supplied access step: session auth, thread/workspace access,\n * seat/balance gates, rate limits all live here. */\n authorize(args: ChatTurnAuthorizeArgs): Promise<ChatTurnAuthorization<TContext>>\n /** Thread/message persistence (`/chat-store`'s store or a product adapter). */\n store: ChatTurnMessageStore\n /** Turn-event buffer (`createD1TurnEventStore(env.DB)` in production,\n * `createMemoryTurnEventStore()` in tests). Wired by default — every turn\n * is buffered and replayable. */\n turnStore: TurnEventStore\n /** Build the turn's event stream. Sandbox lane: `streamSandboxPrompt(...)`\n * wrapped in `createSandboxChatProducer`. Router/openai-compat lane: the\n * product's own producer. May be async (box resolution). */\n produce(args: ChatTurnProduceArgs<TContext>): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer>\n /** Pre-persist transform of the final text (e.g. `/redact`'s `redactPII`).\n * Live stream is never altered. */\n transformFinalText?(text: string): string | Promise<string>\n /** Post-processing after a successful turn (billing, titles, audit). Errors\n * are swallowed by the engine — they never fail a streamed turn. */\n onTurnComplete?(input: { identity: ChatTurnIdentity; finalText: string; context: TContext }): Promise<void>\n /** Per-event side channel (product broadcast). The turn-buffer tap is\n * already wired; this runs in addition. */\n onEvent?(event: { type: string; data?: Record<string, unknown> }, context: TContext): void | Promise<void>\n /** Trace flush handed to `waitUntil` (OTLP export). */\n traceFlush?(context: TContext): Promise<void>\n /** Compose the interaction-answer endpoints (`/interactions`). Omit when the\n * product has no sidecar ask channel. */\n interactions?: InteractionAnswerRouteOptions\n /** Byte budget for inline prompt parts. Default `INLINE_PARTS_MAX_BYTES`. */\n maxInlinePartBytes?: number\n /** Per-flush coalescer for the turn buffer. Default `coalesceDeltas` (this\n * assembly streams the client vocabulary's `{type:'text'|'reasoning',\n * text}` lines, which it merges). A producer streaming raw\n * `message.part.updated` events passes `coalesceChatStreamEvents`. */\n coalesceTurnEvents?: (events: unknown[]) => unknown[]\n replay?: { pollMs?: number; timeoutMs?: number }\n log?: (message: string, meta?: Record<string, unknown>) => void\n}\n\nexport interface ChatTurnRoutes {\n /** POST — run one turn, streaming NDJSON. First line is\n * `{type:'turn', turnId}` (the replay handle); the rest is the engine's\n * event protocol. Pass the platform's `waitUntil` so the turn keeps\n * running (and buffering) after a client disconnect. */\n turn(request: Request, ctx?: { waitUntil?(p: Promise<unknown>): void }): Promise<Response>\n /** GET — replay a buffered turn from `?fromSeq=` (0 = everything), then\n * follow it live until it completes. */\n replay(request: Request, params: { turnId: string }): Promise<Response>\n /** list/answer endpoints from `/interactions`; null when not configured. */\n interactions: InteractionAnswerRoute | null\n}\n\n// ── body validation ────────────────────────────────────────────────────────\n\nfunction errorResponse(err: ChatTurnInputError): Response {\n return Response.json({ code: err.code, error: err.message }, { status: err.status })\n}\n\ninterface ParsedTurnBody {\n payload: ChatTurnRequestPayload\n content: string\n fileParts: ChatTurnFilePartInput[]\n turnId: string | undefined\n}\n\nfunction validateTurnBody(body: Record<string, unknown>, maxInlinePartBytes: number | undefined): ParsedTurnBody {\n const threadId = typeof body.threadId === 'string' ? body.threadId.trim() : ''\n if (!threadId) throw new ChatTurnInputError('Missing threadId')\n const rawContent = body.content ?? body.message ?? ''\n if (typeof rawContent !== 'string') throw new ChatTurnInputError('content must be a string')\n const content = rawContent.trim()\n const fileParts = parseChatTurnParts(body.parts)\n if (!content && fileParts.length === 0) {\n throw new ChatTurnInputError('Missing content (send text, parts, or both)')\n }\n assertPromptPartsWithinCap(fileParts, maxInlinePartBytes)\n let turnId: string | undefined\n try {\n turnId = normalizeClientTurnId(body.turnId)\n } catch (err) {\n throw new ChatTurnInputError(err instanceof Error ? err.message : 'Invalid turnId')\n }\n return {\n payload: { ...body, threadId, content } as ChatTurnRequestPayload,\n content,\n fileParts,\n turnId,\n }\n}\n\n/** File parts persist onto the user message verbatim — the wire shape is the\n * persisted `ChatFilePart`/`ChatImagePart` vocabulary already. The typed\n * projection is `/chat-store`'s (same boundary as the assistant hop). */\nfunction userPartsWithFiles(\n userParts: Array<Record<string, unknown>>,\n fileParts: ChatTurnFilePartInput[],\n): ChatMessagePart[] {\n return toChatMessageParts([...userParts, ...fileParts.map((part) => ({ ...part }))])\n}\n\n// ── the factory ────────────────────────────────────────────────────────────\n\nexport function createChatTurnRoutes<TContext = void>(\n options: CreateChatTurnRoutesOptions<TContext>,\n): ChatTurnRoutes {\n const log = options.log ?? ((message, meta) => console.error(message, meta ?? ''))\n\n async function turn(request: Request, ctx?: { waitUntil?(p: Promise<unknown>): void }): Promise<Response> {\n const [rawBody, badBody] = await parseJsonObjectBody(request)\n if (badBody) return badBody\n\n let parsed: ParsedTurnBody\n try {\n parsed = validateTurnBody(rawBody, options.maxInlinePartBytes)\n } catch (err) {\n if (err instanceof ChatTurnInputError) return errorResponse(err)\n throw err\n }\n const { payload, content, fileParts, turnId } = parsed\n\n const auth = await options.authorize({ request, intent: 'turn', body: payload })\n if (!auth.ok) return auth.response\n const { tenantId, userId, context } = auth\n\n // Turn identity: reuse the just-persisted user row on a retry (same\n // turnId or identical trailing content) instead of double-inserting.\n const existingMessages = (await options.store.listMessages(payload.threadId)).map((m) => ({\n id: m.id,\n role: m.role,\n content: m.content,\n parts: (m.parts ?? null) as PersistedChatMessageForTurn['parts'],\n }))\n const chatTurn = resolveChatTurn({ existingMessages, userContent: content, turnId })\n\n if (chatTurn.shouldInsertUserMessage) {\n await options.store.appendMessage({\n threadId: payload.threadId,\n role: 'user',\n content,\n parts: userPartsWithFiles(chatTurn.userParts, fileParts),\n })\n }\n\n const identity: ChatTurnIdentity = {\n tenantId,\n sessionId: payload.threadId,\n userId,\n turnIndex: chatTurn.turnIndex,\n }\n const executionId = deriveExecutionId({\n projectId: options.projectId,\n sessionId: payload.threadId,\n turnIndex: chatTurn.turnIndex,\n })\n const turnStreamId = crypto.randomUUID()\n\n const prompt: string | ChatTurnPartInput[] =\n fileParts.length === 0\n ? content\n : content\n ? [{ type: 'text', text: content }, ...fileParts]\n : [...fileParts]\n\n // Durability tap: every engine event buffers (coalesced) so a dropped\n // client replays the tail. Live delivery rides the Response body, not the\n // tap, so `write` is intentionally absent.\n const tap = createBufferedTurnTap({\n store: options.turnStore,\n turnId: turnStreamId,\n scopeId: payload.threadId,\n coalesce: options.coalesceTurnEvents ?? coalesceDeltas,\n })\n const turnMarker = { type: 'turn', turnId: turnStreamId }\n await tap.onEvent(turnMarker)\n\n let producer: ChatTurnRouteProducer | undefined\n let runFailed = false\n\n const result = handleChatTurn({\n identity,\n waitUntil: ctx?.waitUntil,\n log,\n hooks: {\n // The engine wants a synchronous producer; box resolution is async —\n // defer it into the generator's first pull.\n produce: () => ({\n stream: (async function* () {\n producer = await options.produce({\n request,\n body: payload,\n identity,\n context,\n prompt,\n executionId,\n turnStreamId,\n priorMessages: chatTurn.priorMessages,\n })\n for await (const event of producer.stream) yield event\n })(),\n finalText: () => producer?.finalText() ?? '',\n }),\n onEvent: async (event) => {\n if (event.type === 'session.run.failed' || event.type === 'error') runFailed = true\n await tap.onEvent(event)\n if (options.onEvent) await options.onEvent(event, context)\n },\n ...(options.transformFinalText ? { transformFinalText: options.transformFinalText } : {}),\n persistAssistantMessage: async ({ finalText }) => {\n // The typed boundary: stream-normalizer records → stored vocabulary\n // (validating projection owned by /chat-store — no cast here).\n const parts = producer?.assistantParts ? toChatMessageParts(producer.assistantParts()) : undefined\n if (!finalText.trim() && (!parts || parts.length === 0)) return\n const usage = producer?.usage?.() ?? {}\n await options.store.appendMessage({\n threadId: payload.threadId,\n role: 'assistant',\n content: finalText,\n ...(parts && parts.length > 0 ? { parts } : {}),\n ...(producer?.model ? { model: producer.model } : {}),\n ...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),\n ...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),\n ...(usage.reasoningTokens !== undefined ? { reasoningTokens: usage.reasoningTokens } : {}),\n ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),\n ...(usage.cacheWriteTokens !== undefined ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),\n ...(usage.costUsd !== undefined ? { costUsd: usage.costUsd } : {}),\n })\n },\n ...(options.onTurnComplete\n ? {\n onTurnComplete: ({ identity: turnIdentity, finalText }: { identity: ChatTurnIdentity; finalText: string }) =>\n options.onTurnComplete!({ identity: turnIdentity, finalText, context }),\n }\n : {}),\n ...(options.traceFlush ? { traceFlush: () => options.traceFlush!(context) } : {}),\n },\n })\n\n // Tee: one branch to the live client, one drained under waitUntil so the\n // turn (and its buffering via onEvent) runs to completion after a client\n // drop — the engine body executes as it is pulled.\n const [clientBody, drainBody] = result.body.tee()\n const drained = (async () => {\n const reader = drainBody.getReader()\n try {\n for (;;) {\n const { done } = await reader.read()\n if (done) break\n }\n await tap.done(runFailed ? 'error' : 'complete')\n } catch (err) {\n await tap.done('error')\n log('[chat-routes] turn drain failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n })()\n if (ctx?.waitUntil) ctx.waitUntil(drained)\n else void drained.catch(() => {})\n\n // Announce the replay handle before the engine's first event.\n const encoder = new TextEncoder()\n const marker = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(encoder.encode(`${JSON.stringify(turnMarker)}\\n`))\n controller.close()\n },\n })\n const body = concatStreams([marker, clientBody])\n\n return new Response(body, {\n headers: {\n 'Content-Type': result.contentType,\n 'Cache-Control': 'no-cache',\n },\n })\n }\n\n async function replay(request: Request, params: { turnId: string }): Promise<Response> {\n const turnId = params.turnId?.trim()\n if (!turnId) return Response.json({ error: 'Missing turnId' }, { status: 400 })\n const auth = await options.authorize({ request, intent: 'replay', turnId })\n if (!auth.ok) return auth.response\n\n const fromSeqRaw = new URL(request.url).searchParams.get('fromSeq')\n const fromSeq = fromSeqRaw ? Math.max(0, Math.trunc(Number(fromSeqRaw)) || 0) : 0\n\n const encoder = new TextEncoder()\n const events = replayTurnEvents({\n store: options.turnStore,\n turnId,\n fromSeq,\n ...(options.replay?.pollMs !== undefined ? { pollMs: options.replay.pollMs } : {}),\n ...(options.replay?.timeoutMs !== undefined ? { timeoutMs: options.replay.timeoutMs } : {}),\n })\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n const { done, value } = await events.next()\n if (done) {\n controller.close()\n return\n }\n controller.enqueue(encoder.encode(`${value.event}\\n`))\n },\n cancel() {\n void events.return(undefined)\n },\n })\n return new Response(body, {\n headers: {\n 'Content-Type': 'application/x-ndjson',\n 'Cache-Control': 'no-cache',\n },\n })\n }\n\n return {\n turn,\n replay,\n interactions: options.interactions ? createInteractionAnswerRoute(options.interactions) : null,\n }\n}\n\n/** Sequential concat of byte streams (marker line, then the engine body). */\nfunction concatStreams(streams: ReadableStream<Uint8Array>[]): ReadableStream<Uint8Array> {\n let index = 0\n let reader: ReadableStreamDefaultReader<Uint8Array> | null = null\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n for (;;) {\n if (!reader) {\n const next = streams[index++]\n if (!next) {\n controller.close()\n return\n }\n reader = next.getReader()\n }\n const { done, value } = await reader.read()\n if (done) {\n reader = null\n continue\n }\n controller.enqueue(value)\n return\n }\n },\n async cancel(reason) {\n await reader?.cancel(reason)\n for (const stream of streams.slice(index)) await stream.cancel(reason)\n },\n })\n}\n","/**\n * Sandbox lane: bridge a raw sandbox event stream (`streamSandboxPrompt`) into\n * the `ChatTurnProducer` shape agent-runtime's `handleChatTurn` consumes AND\n * the client vocabulary `/web-react`'s `dispatchChatStreamLine` already parses\n * (`text` / `reasoning` / `tool_call` / `tool_result` / `usage` /\n * `interaction`). Legal and tax each hand-rolled this mapping differently;\n * this is that middle, composed from `/stream`'s normalizers — no new loop\n * logic, no SDK import (the event source is an injected `AsyncIterable`).\n *\n * Alongside the live mapping it accumulates the PERSISTED projection — the\n * `message.parts` rows `/chat-store` stores — via `normalizePersistedPart` /\n * `mergePersistedPart` / `finalizeAssistantParts`, plus the usage receipt from\n * `step-finish` parts. `createChatTurnRoutes` reads both after drain.\n */\n\nimport {\n isRenderableInteractionKind,\n parseInteractionRequest,\n} from '../interactions/contract'\nimport {\n asRecord,\n asString,\n finalizeAssistantParts,\n getPartKey,\n mergePersistedPart,\n normalizePersistedPart,\n normalizeToolEvent,\n type JsonRecord,\n type StreamEvent,\n} from '../stream/index'\nimport type { ChatTurnRouteProducer, ChatTurnUsage } from './turn-routes'\n\nexport interface SandboxChatProducerOptions {\n /** The raw sandbox event stream (e.g. `streamSandboxPrompt(...)`). */\n events: AsyncIterable<unknown>\n /** Recorded on the persisted assistant message. */\n model?: string\n /** Which ask kinds the product renders a card for. Anything else is\n * auto-declined (see `declineInteraction`) so the run never hangs in the\n * broker waiting on a card no client will show. Default: question/plan. */\n isRenderableInteraction?: (kind: string) => boolean\n /** Resolve a non-renderable ask (wire `respondToSessionInteraction` with the\n * session's sidecar connection). Without it, non-renderable asks are only\n * logged — the run stays blocked until the broker times out. */\n declineInteraction?: (id: string) => Promise<void>\n log?: (message: string, meta?: Record<string, unknown>) => void\n}\n\ninterface TextTracker {\n /** Full accumulated text per part key, to derive suffix deltas from\n * snapshot-only harness events. */\n seen: Map<string, string>\n}\n\n/** Delta to emit for one text/reasoning part update: prefer the harness's\n * explicit delta; otherwise diff the snapshot against what was already\n * emitted for that part (snapshot-only harnesses re-send the whole text). */\nfunction textDelta(tracker: TextTracker, key: string, part: JsonRecord, rawDelta: unknown): string {\n const explicit = typeof rawDelta === 'string' ? rawDelta : undefined\n const previous = tracker.seen.get(key) ?? ''\n if (explicit !== undefined) {\n tracker.seen.set(key, previous + explicit)\n return explicit\n }\n const snapshot = asString(part.text) ?? asString(part.content) ?? ''\n if (!snapshot) return ''\n if (snapshot.startsWith(previous)) {\n tracker.seen.set(key, snapshot)\n return snapshot.slice(previous.length)\n }\n // The snapshot replaced the text outright — emit it whole; the persisted\n // projection stays correct because finalText is authoritative at finalize.\n tracker.seen.set(key, snapshot)\n return snapshot\n}\n\nfunction usageFromStepFinish(part: JsonRecord, usage: ChatTurnUsage): void {\n const tokens = asRecord(part.tokens)\n if (tokens) {\n const cache = asRecord(tokens.cache)\n const add = (current: number | undefined, value: unknown): number | undefined => {\n const n = Number(value)\n if (!Number.isFinite(n)) return current\n return (current ?? 0) + n\n }\n usage.inputTokens = add(usage.inputTokens, tokens.input)\n usage.outputTokens = add(usage.outputTokens, tokens.output)\n usage.reasoningTokens = add(usage.reasoningTokens, tokens.reasoning)\n if (cache) {\n usage.cacheReadTokens = add(usage.cacheReadTokens, cache.read)\n usage.cacheWriteTokens = add(usage.cacheWriteTokens, cache.write)\n }\n }\n const cost = Number(part.cost)\n if (Number.isFinite(cost)) usage.costUsd = (usage.costUsd ?? 0) + cost\n}\n\nexport function createSandboxChatProducer(options: SandboxChatProducerOptions): ChatTurnRouteProducer {\n const log = options.log ?? ((message, meta) => console.error(message, meta ?? ''))\n const renderable = options.isRenderableInteraction ?? isRenderableInteractionKind\n\n let fullText = ''\n const partOrder: string[] = []\n const partMap = new Map<string, JsonRecord>()\n const tracker: TextTracker = { seen: new Map() }\n const usage: ChatTurnUsage = {}\n /** Tool ids already announced as `tool_call` / settled as `tool_result`. */\n const announcedTools = new Set<string>()\n const settledTools = new Set<string>()\n /** Id-less step boundaries: one occurrence per key, never merged. */\n let stepCounter = 0\n\n function recordPersistedPart(part: JsonRecord, delta: string | undefined, keyOverride?: string): void {\n const persisted = normalizePersistedPart(part)\n if (!persisted) return\n const key = keyOverride ?? getPartKey(persisted)\n if (!partMap.has(key)) partOrder.push(key)\n partMap.set(key, mergePersistedPart(partMap.get(key), persisted, delta))\n }\n\n async function* stream(): AsyncGenerator<StreamEvent, void, unknown> {\n for await (const raw of options.events) {\n const record = asRecord(raw)\n if (!record || typeof record.type !== 'string') continue\n // Fold bare tool_call/tool_result shapes into the canonical part event;\n // everything else keeps its original record (verbatim forwarding must\n // not strip fields outside `data`).\n const normalized = normalizeToolEvent({ type: record.type, data: asRecord(record.data) })\n const event = normalized.type === 'message.part.updated' ? normalized : (record as unknown as StreamEvent)\n\n if (event.type === 'message.part.updated') {\n const part = asRecord(event.data?.part)\n if (!part) continue\n const rawDelta = event.data?.delta\n const partType = String(part.type ?? '')\n\n if (partType === 'text' || partType === 'reasoning') {\n const key = getPartKey(part)\n const delta = textDelta(tracker, key, part, rawDelta)\n recordPersistedPart(part, delta || undefined)\n if (delta) {\n if (partType === 'text') fullText += delta\n yield { type: partType, text: delta } as StreamEvent & { text: string }\n }\n continue\n }\n\n if (partType === 'tool') {\n recordPersistedPart(part, undefined)\n const persisted = partMap.get(getPartKey(part))\n const state = asRecord(persisted?.state)\n const toolId = String(persisted?.id ?? '')\n const toolName = String(persisted?.tool ?? 'tool')\n if (toolId && !announcedTools.has(toolId)) {\n announcedTools.add(toolId)\n yield {\n type: 'tool_call',\n call: { toolCallId: toolId, toolName, args: asRecord(state?.input) ?? {} },\n } as StreamEvent\n }\n const status = String(state?.status ?? '')\n if (toolId && (status === 'completed' || status === 'error') && !settledTools.has(toolId)) {\n settledTools.add(toolId)\n yield {\n type: 'tool_result',\n toolCallId: toolId,\n toolName,\n outcome: {\n ok: status === 'completed',\n ...(state?.output !== undefined ? { result: state.output } : {}),\n ...(asString(state?.error) ? { message: asString(state?.error) } : {}),\n },\n } as StreamEvent\n }\n continue\n }\n\n if (partType === 'step-finish') {\n usageFromStepFinish(part, usage)\n // Persist the per-step receipt too (unique key per occurrence: the\n // parts have no id and two receipts must never merge into one).\n recordPersistedPart(part, undefined, `step-finish:#${stepCounter++}`)\n const promptTokens = usage.inputTokens ?? 0\n const completionTokens = usage.outputTokens ?? 0\n if (promptTokens || completionTokens) {\n yield { type: 'usage', usage: { promptTokens, completionTokens } } as StreamEvent\n }\n continue\n }\n\n if (partType === 'step-start') {\n recordPersistedPart(part, undefined, `step-start:#${stepCounter}`)\n continue\n }\n\n // Remaining storable kinds (file/image/subtask) have no live\n // vocabulary line; they persist so the transcript keeps them.\n recordPersistedPart(part, undefined)\n continue\n }\n\n if (event.type === 'interaction') {\n const parsed = parseInteractionRequest(asRecord(record.data))\n if (!parsed.succeeded) {\n log('[chat-routes] dropping malformed interaction event', { error: parsed.error })\n continue\n }\n if (renderable(parsed.value.kind)) {\n yield event\n continue\n }\n // Non-renderable ask: the run is blocked in the broker until someone\n // answers. Decline it so the turn proceeds instead of hanging.\n if (options.declineInteraction) {\n try {\n await options.declineInteraction(parsed.value.id)\n } catch (err) {\n log('[chat-routes] failed to auto-decline interaction', {\n id: parsed.value.id,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n } else {\n log('[chat-routes] non-renderable interaction with no declineInteraction wired', {\n id: parsed.value.id,\n kind: parsed.value.kind,\n })\n }\n continue\n }\n\n if (event.type === 'result') {\n const finalText = asString(event.data?.finalText)\n if (finalText) fullText = finalText\n const resultUsage = asRecord(event.data?.usage)\n if (resultUsage) {\n const input = Number(resultUsage.inputTokens)\n const output = Number(resultUsage.outputTokens)\n if (Number.isFinite(input)) usage.inputTokens = input\n if (Number.isFinite(output)) usage.outputTokens = output\n }\n continue\n }\n\n // Everything else (interaction.cancel, error, lifecycle) forwards\n // verbatim — the client parser ignores unknown types.\n yield event\n }\n }\n\n return {\n stream: stream(),\n finalText: () => fullText,\n assistantParts: () => finalizeAssistantParts(partOrder, partMap, fullText),\n usage: () => usage,\n ...(options.model ? { model: options.model } : {}),\n }\n}\n","/**\n * `createUploadRoute` — the multimodal middle. Accepts multipart file uploads\n * and returns `PromptInputPart`-shaped descriptors the client echoes back on\n * send (`ChatTurnRequestPayload.parts`):\n *\n * ≤ inlineMaxBytes (700 KiB default) → inline `data:` URI part — rides the\n * turn body directly, no sandbox round trip.\n * > inlineMaxBytes → written into the sandbox workspace (base64 through the\n * structural `write` seam — `box.fs` satisfies it) and referenced by\n * `path`. Mandatory two-step: the gateway caps request bodies at ~1 MiB,\n * so a large file can never ride the prompt POST.\n *\n * The sink is structural (no sandbox-SDK import); products pass `box.fs`.\n */\n\nimport type { ChatTurnFilePartInput } from './wire'\n\n/** 700 KiB: base64 inflates ~4/3, so an inline part stays comfortably under\n * the ~1 MiB gateway body cap alongside the JSON envelope. */\nexport const UPLOAD_INLINE_MAX_BYTES = 700 * 1024\n\n/** 8 MiB default ceiling per file — one base64 `write` call handles it. Raise\n * it only with a sink that can take the bigger single write. */\nexport const UPLOAD_MAX_FILE_BYTES = 8 * 1024 * 1024\n\n/** Structural match of the sandbox SDK's `box.fs` write surface (v0.10.5+:\n * `encoding: 'base64'` is the worker-safe binary path). */\nexport interface SandboxUploadSink {\n write(path: string, content: string, options?: { encoding?: 'utf8' | 'base64' }): Promise<unknown>\n}\n\nexport type UploadAuthorization =\n | {\n ok: true\n /** Where large files land. Absent/null: only inline uploads are\n * accepted and an over-inline-cap file is rejected with 413. */\n sink?: SandboxUploadSink | null\n /** Per-request override of the workspace directory large files go to. */\n uploadDir?: string\n }\n | { ok: false; response: Response }\n\nexport interface CreateUploadRouteOptions {\n /** Authenticate the caller and resolve the sandbox file sink (usually\n * `ensureWorkspaceSandbox(...)` → `box.fs`). */\n authorize(args: { request: Request }): Promise<UploadAuthorization>\n /** Inline-vs-sandbox threshold. Default {@link UPLOAD_INLINE_MAX_BYTES}. */\n inlineMaxBytes?: number\n /** Hard per-file cap. Default {@link UPLOAD_MAX_FILE_BYTES}. */\n maxFileBytes?: number\n /** Workspace directory for path-ref files. Default `'uploads'`. */\n uploadDir?: string\n}\n\n/** One uploaded file, ready for the composer chip and the turn body. */\nexport interface UploadedChatFile {\n id: string\n name: string\n size: number\n mediaType: string\n /** True when the part carries the bytes inline (`data:` URI). */\n inline: boolean\n /** Echo this back verbatim in `ChatTurnRequestPayload.parts`. */\n part: ChatTurnFilePartInput\n}\n\n/** Path-safe file name: basename only, conservative charset, length-capped. */\nexport function sanitizeUploadFilename(name: string): string {\n const base = name.split(/[\\\\/]/).pop() ?? 'file'\n const safe = base.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^\\.+/, '_')\n return (safe || 'file').slice(0, 120)\n}\n\nconst BASE64_CHUNK = 0x8000\n\nexport function bytesToBase64(bytes: Uint8Array): string {\n let binary = ''\n for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK))\n }\n return btoa(binary)\n}\n\nfunction uploadError(status: number, code: string, error: string): Response {\n return Response.json({ code, error }, { status })\n}\n\nexport function createUploadRoute(options: CreateUploadRouteOptions): (request: Request) => Promise<Response> {\n const inlineMaxBytes = options.inlineMaxBytes ?? UPLOAD_INLINE_MAX_BYTES\n const maxFileBytes = options.maxFileBytes ?? UPLOAD_MAX_FILE_BYTES\n\n return async function upload(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (!auth.ok) return auth.response\n const sink = auth.sink ?? null\n const uploadDir = (auth.uploadDir ?? options.uploadDir ?? 'uploads').replace(/\\/+$/, '')\n\n let form: FormData\n try {\n form = await request.formData()\n } catch {\n return uploadError(400, 'INVALID_UPLOAD', 'Expected a multipart/form-data body with file fields')\n }\n const files: File[] = []\n form.forEach((value) => {\n if (value instanceof File) files.push(value)\n })\n if (files.length === 0) {\n return uploadError(400, 'INVALID_UPLOAD', 'No files in the upload body')\n }\n\n const uploaded: UploadedChatFile[] = []\n for (const file of files) {\n const name = sanitizeUploadFilename(file.name)\n const mediaType = file.type || 'application/octet-stream'\n const partType: ChatTurnFilePartInput['type'] = mediaType.startsWith('image/') ? 'image' : 'file'\n\n if (file.size > maxFileBytes) {\n return uploadError(\n 413,\n 'FILE_TOO_LARGE',\n `${name} is ${file.size}B, over the ${maxFileBytes}B per-file cap`,\n )\n }\n\n const id = crypto.randomUUID()\n if (file.size <= inlineMaxBytes) {\n const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()))\n uploaded.push({\n id,\n name,\n size: file.size,\n mediaType,\n inline: true,\n part: {\n type: partType,\n filename: name,\n mediaType,\n url: `data:${mediaType};base64,${base64}`,\n },\n })\n continue\n }\n\n if (!sink) {\n return uploadError(\n 413,\n 'SANDBOX_REQUIRED',\n `${name} is ${file.size}B, over the ${inlineMaxBytes}B inline cap, and no sandbox is available to hold it`,\n )\n }\n const path = `${uploadDir}/${id}-${name}`\n const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()))\n await sink.write(path, base64, { encoding: 'base64' })\n uploaded.push({\n id,\n name,\n size: file.size,\n mediaType,\n inline: false,\n part: { type: partType, filename: name, mediaType, path },\n })\n }\n\n return Response.json({ files: uploaded })\n }\n}\n"],"mappings":"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","names":["base64"]}
|
|
1
|
+
{"version":3,"sources":["../../src/chat-routes/turn-routes.ts","../../src/chat-routes/sandbox-producer.ts","../../src/chat-routes/upload.ts"],"sourcesContent":["/**\n * `createChatTurnRoutes` — the assembled server chat vertical (issue #188\n * Phase 1). One factory composing the pieces every product re-wired by hand:\n *\n * body parse/validate → `/web` `parseJsonObjectBody` + `./wire`\n * turn identity → `/stream` `resolveChatTurn` + agent-runtime\n * `deriveExecutionId`\n * producer → injected seam (sandbox lane via\n * `createSandboxChatProducer`; router lane is the\n * product's own `ChatTurnProducer`)\n * turn engine → agent-runtime `handleChatTurn` (verbatim)\n * durability → `/stream` turn-buffer tap, wired BY DEFAULT\n * (tee + drain keeps the turn running after a\n * client drop; replay serves the buffered tail)\n * persistence → injected `/chat-store`-shaped store\n * (user row on send, assistant row on completion)\n * interactions answer → `/interactions` `createInteractionAnswerRoute`\n *\n * Handlers are web-standard `Request → Response` (Workers, Node 18+, Deno) —\n * no router import. Auth/access is one injected `authorize` seam, composable\n * with `/app-auth` guards but not coupled to them.\n *\n * Five optional product seams let a complex turn-orchestrator compose the\n * vertical instead of hand-rolling a generator — each omittable to the exact\n * behavior above: `turnLock` (single-flight acquire/release around the turn),\n * `contextGate` (pre-producer domain-readiness short-circuit), `beforeTurn`\n * (observe + augment the producer input), `lifecycle` (deterministic\n * start/complete/error telemetry), `heartbeat` (keepalive during silent\n * producer waits), plus `onRawEvent` (the raw producer events, for telemetry).\n * `handleChatTurn` stays the engine — the seams only wrap its input, its\n * producer stream, and its settle.\n */\n\nimport { deriveExecutionId, handleChatTurn } from '@tangle-network/agent-runtime'\nimport type { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime'\nimport { toChatMessageParts, type ChatMessagePart } from '../chat-store/parts'\nimport {\n createInteractionAnswerRoute,\n type InteractionAnswerRoute,\n type InteractionAnswerRouteOptions,\n} from '../interactions/route'\nimport {\n coalesceDeltas,\n createBufferedTurnTap,\n normalizeClientTurnId,\n replayTurnEvents,\n resolveChatTurn,\n type PersistedChatMessageForTurn,\n type TurnEventStore,\n} from '../stream/index'\nimport { parseJsonObjectBody } from '../web/index'\nimport {\n assertPromptPartsWithinCap,\n ChatTurnInputError,\n parseChatTurnParts,\n type ChatTurnFilePartInput,\n type ChatTurnPartInput,\n type ChatTurnRequestPayload,\n} from './wire'\n\n// ── seams ───────────────────────────────────────────────────────────────────\n\n/** Usage receipt persisted onto the assistant message (the flattened\n * `step-finish` shape `/chat-store`'s columns mirror). */\nexport interface ChatTurnUsage {\n inputTokens?: number\n outputTokens?: number\n reasoningTokens?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n costUsd?: number\n}\n\n/** What the route persists — a structural subset of `/chat-store`'s\n * `ChatStore`, so `createChatStore(db, tables)` satisfies it directly and a\n * product with its own persistence adapts without importing drizzle. */\nexport interface ChatTurnMessageStore {\n listMessages(threadId: string): Promise<Array<{\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts?: ChatMessagePart[] | null\n }>>\n appendMessage(input: {\n threadId: string\n role: 'user' | 'assistant'\n content: string\n parts?: ChatMessagePart[]\n model?: string | null\n inputTokens?: number | null\n outputTokens?: number | null\n reasoningTokens?: number | null\n cacheReadTokens?: number | null\n cacheWriteTokens?: number | null\n costUsd?: number | null\n }): Promise<unknown>\n}\n\n/** `ChatTurnProducer` plus the persisted projection the assembly reads after\n * drain. `createSandboxChatProducer` returns this; a router-lane producer\n * may omit the optional members (finalText persists as a single text part). */\nexport interface ChatTurnRouteProducer extends ChatTurnProducer {\n assistantParts?(): Array<Record<string, unknown>>\n usage?(): ChatTurnUsage\n model?: string\n}\n\nexport type ChatTurnAuthorization<TContext> =\n | { ok: true; tenantId: string; userId: string; context: TContext }\n | { ok: false; response: Response }\n\nexport interface ChatTurnAuthorizeArgs {\n request: Request\n intent: 'turn' | 'replay'\n /** Parsed, validated POST body (turn intent only). */\n body?: ChatTurnRequestPayload\n /** The buffered turn id being replayed (replay intent only). */\n turnId?: string\n}\n\nexport interface ChatTurnProduceArgs<TContext> {\n request: Request\n body: ChatTurnRequestPayload\n identity: ChatTurnIdentity\n context: TContext\n /** The message to send: plain text, or parts when the client attached\n * files (a text part is prepended from `content` when present). */\n prompt: string | ChatTurnPartInput[]\n /** Stable id for cross-process reconnect (`deriveExecutionId`). */\n executionId: string\n /** The turn-buffer id announced to the client for replay. */\n turnStreamId: string\n priorMessages: PersistedChatMessageForTurn[]\n}\n\n/** One event as it crosses the route: the producer's own vocabulary, or an\n * injected keepalive. Same shape the engine forwards verbatim. */\ntype ChatRouteEvent = { type: string; data?: Record<string, unknown> }\n\n/** Best-effort human-readable cause from a terminal `error` /\n * `session.run.failed` event's `data`. */\nfunction failureReasonOf(data: Record<string, unknown> | undefined): string | undefined {\n if (!data) return undefined\n const message = data.message ?? data.error ?? data.reason\n if (typeof message === 'string' && message.length > 0) return message\n return undefined\n}\n\n/** Keepalive emitted while the producer is quiet (long tool calls, first-token\n * wait) so client watchdogs stay re-armed. One is emitted each time\n * `intervalMs` elapses with no producer event; the window resets on every real\n * event, so a chatty producer never triggers one. The product owns the event\n * shape (`type` + `data`). Omit → no keepalives (today's behavior). */\nexport interface ChatTurnHeartbeat {\n intervalMs: number\n event(info: { elapsedMs: number; tick: number }): ChatRouteEvent\n}\n\n/** Patch a `beforeTurn` hook returns to augment the producer's input. Omitted\n * fields keep the route-assembled value; the product's `produce` still owns\n * the system prompt. */\nexport interface ChatTurnInputPatch {\n prompt?: string | ChatTurnPartInput[]\n priorMessages?: PersistedChatMessageForTurn[]\n}\n\n/** Pre-turn readiness verdict — proceed, or short-circuit with the product's\n * own `Response` (e.g. a canned assistant reply asking for missing context).\n * Distinct from `authorize`: this gates domain readiness, not access. */\nexport type ChatTurnGateResult =\n | { proceed: true }\n | { proceed: false; response: Response }\n\n/** Single-flight lock verdict — acquired (with an opaque handle passed back to\n * `release`), or already held (short-circuit with the product's 409-style\n * `Response`). */\nexport type ChatTurnLockResult =\n | { acquired: true; handle?: unknown }\n | { acquired: false; response: Response }\n\n/** Async acquire/release wrapped around the turn. `acquire` runs before any\n * side effect; `release` runs once when the turn settles — including on a\n * short-circuit or a throw. */\nexport interface ChatTurnLock<TContext> {\n acquire(args: ChatTurnProduceArgs<TContext>): ChatTurnLockResult | Promise<ChatTurnLockResult>\n release(handle: unknown): void | Promise<void>\n}\n\ninterface ChatTurnLifecycleBase<TContext> {\n identity: ChatTurnIdentity\n executionId: string\n turnStreamId: string\n context: TContext\n}\nexport interface ChatTurnLifecycleStart<TContext> extends ChatTurnLifecycleBase<TContext> {\n startedAt: number\n}\nexport interface ChatTurnLifecycleComplete<TContext> extends ChatTurnLifecycleBase<TContext> {\n finalText: string\n usage: ChatTurnUsage\n durationMs: number\n}\nexport interface ChatTurnLifecycleError<TContext> extends ChatTurnLifecycleBase<TContext> {\n error: unknown\n durationMs: number\n}\n\n/** Deterministic run telemetry: `onTurnStart` fires before the producer runs;\n * exactly one of `onTurnComplete` / `onTurnError` fires after the turn\n * settles, always after `onTurnStart`. Failure is derived from the turn's own\n * `error` / `session.run.failed` events (or a drain throw), not the engine's\n * lifecycle envelope. Hook errors are swallowed — telemetry never fails a\n * turn. */\nexport interface ChatTurnLifecycle<TContext> {\n onTurnStart?(info: ChatTurnLifecycleStart<TContext>): void | Promise<void>\n onTurnComplete?(info: ChatTurnLifecycleComplete<TContext>): void | Promise<void>\n onTurnError?(info: ChatTurnLifecycleError<TContext>): void | Promise<void>\n}\n\nexport interface CreateChatTurnRoutesOptions<TContext = void> {\n /** Names the product in `deriveExecutionId` so retries land on the same\n * substrate execution. */\n projectId: string\n /** Authenticate + authorize the caller for a turn or a replay. The only\n * product-supplied access step: session auth, thread/workspace access,\n * seat/balance gates, rate limits all live here. */\n authorize(args: ChatTurnAuthorizeArgs): Promise<ChatTurnAuthorization<TContext>>\n /** Thread/message persistence (`/chat-store`'s store or a product adapter). */\n store: ChatTurnMessageStore\n /** Turn-event buffer (`createD1TurnEventStore(env.DB)` in production,\n * `createMemoryTurnEventStore()` in tests). Wired by default — every turn\n * is buffered and replayable. */\n turnStore: TurnEventStore\n /** Build the turn's event stream. Sandbox lane: `streamSandboxPrompt(...)`\n * wrapped in `createSandboxChatProducer`. Router/openai-compat lane: the\n * product's own producer. May be async (box resolution). */\n produce(args: ChatTurnProduceArgs<TContext>): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer>\n /** Single-flight lock acquired before any side effect and released once when\n * the turn settles (including short-circuit/throw). Omit → no lock. */\n turnLock?: ChatTurnLock<TContext>\n /** Pre-turn readiness gate that can short-circuit with a product `Response`\n * before the producer runs (the user row is already persisted). Runs after\n * `turnLock.acquire`, before `beforeTurn`. Omit → always proceed. */\n contextGate?(args: ChatTurnProduceArgs<TContext>): ChatTurnGateResult | Promise<ChatTurnGateResult>\n /** Observe the assembled producer input and optionally augment it (rewrite\n * the prompt / prior messages) before the producer runs. Omit → no change. */\n beforeTurn?(args: ChatTurnProduceArgs<TContext>): ChatTurnInputPatch | void | Promise<ChatTurnInputPatch | void>\n /** Deterministic run telemetry (start / complete / error) with identity and\n * timing. Omit → no telemetry. */\n lifecycle?: ChatTurnLifecycle<TContext>\n /** Keepalive injected while the producer is quiet. Omit → no keepalives. */\n heartbeat?: ChatTurnHeartbeat\n /** Observe each event the producer emits, before the engine frames it and\n * before any heartbeat injection (the raw sidecar-producer events, for\n * telemetry). Never alters the stream; errors are swallowed. Distinct from\n * `onEvent`, which sees the engine-framed stream incl. lifecycle envelopes. */\n onRawEvent?(event: ChatRouteEvent, context: TContext): void | Promise<void>\n /** Pre-persist transform of the final text (e.g. `/redact`'s `redactPII`).\n * Live stream is never altered. */\n transformFinalText?(text: string): string | Promise<string>\n /** Post-processing after a turn settles (billing, titles, audit). Fires with\n * `failed:true` + `failureReason` when the turn carried a terminal error\n * event (model 402 / rate-limit / server error) instead of a clean\n * completion, so products skip the deduct and render an error row rather\n * than billing an empty turn and marking it done. A turn that THROWS never\n * reaches this hook (the engine skips it on a producer throw). Errors are\n * swallowed by the engine — they never fail a streamed turn. */\n onTurnComplete?(input: {\n identity: ChatTurnIdentity\n finalText: string\n context: TContext\n failed: boolean\n failureReason?: string\n }): Promise<void>\n /** Per-event side channel (product broadcast). The turn-buffer tap is\n * already wired; this runs in addition. */\n onEvent?(event: { type: string; data?: Record<string, unknown> }, context: TContext): void | Promise<void>\n /** Trace flush handed to `waitUntil` (OTLP export). */\n traceFlush?(context: TContext): Promise<void>\n /** Compose the interaction-answer endpoints (`/interactions`). Omit when the\n * product has no sidecar ask channel. */\n interactions?: InteractionAnswerRouteOptions\n /** Byte budget for inline prompt parts. Default `INLINE_PARTS_MAX_BYTES`. */\n maxInlinePartBytes?: number\n /** Per-flush coalescer for the turn buffer. Default `coalesceDeltas` (this\n * assembly streams the client vocabulary's `{type:'text'|'reasoning',\n * text}` lines, which it merges). A producer streaming raw\n * `message.part.updated` events passes `coalesceChatStreamEvents`. */\n coalesceTurnEvents?: (events: unknown[]) => unknown[]\n replay?: { pollMs?: number; timeoutMs?: number }\n log?: (message: string, meta?: Record<string, unknown>) => void\n}\n\nexport interface ChatTurnRoutes {\n /** POST — run one turn, streaming NDJSON. First line is\n * `{type:'turn', turnId}` (the replay handle); the rest is the engine's\n * event protocol. Pass the platform's `waitUntil` so the turn keeps\n * running (and buffering) after a client disconnect. */\n turn(request: Request, ctx?: { waitUntil?(p: Promise<unknown>): void }): Promise<Response>\n /** GET — replay a buffered turn from `?fromSeq=` (0 = everything), then\n * follow it live until it completes. */\n replay(request: Request, params: { turnId: string }): Promise<Response>\n /** list/answer endpoints from `/interactions`; null when not configured. */\n interactions: InteractionAnswerRoute | null\n}\n\n// ── body validation ────────────────────────────────────────────────────────\n\nfunction errorResponse(err: ChatTurnInputError): Response {\n return Response.json({ code: err.code, error: err.message }, { status: err.status })\n}\n\ninterface ParsedTurnBody {\n payload: ChatTurnRequestPayload\n content: string\n fileParts: ChatTurnFilePartInput[]\n turnId: string | undefined\n}\n\nfunction validateTurnBody(body: Record<string, unknown>, maxInlinePartBytes: number | undefined): ParsedTurnBody {\n const threadId = typeof body.threadId === 'string' ? body.threadId.trim() : ''\n if (!threadId) throw new ChatTurnInputError('Missing threadId')\n const rawContent = body.content ?? body.message ?? ''\n if (typeof rawContent !== 'string') throw new ChatTurnInputError('content must be a string')\n const content = rawContent.trim()\n const fileParts = parseChatTurnParts(body.parts)\n if (!content && fileParts.length === 0) {\n throw new ChatTurnInputError('Missing content (send text, parts, or both)')\n }\n assertPromptPartsWithinCap(fileParts, maxInlinePartBytes)\n let turnId: string | undefined\n try {\n turnId = normalizeClientTurnId(body.turnId)\n } catch (err) {\n throw new ChatTurnInputError(err instanceof Error ? err.message : 'Invalid turnId')\n }\n return {\n payload: { ...body, threadId, content } as ChatTurnRequestPayload,\n content,\n fileParts,\n turnId,\n }\n}\n\n/** File parts persist onto the user message verbatim — the wire shape is the\n * persisted `ChatFilePart`/`ChatImagePart` vocabulary already. The typed\n * projection is `/chat-store`'s (same boundary as the assistant hop). */\nfunction userPartsWithFiles(\n userParts: Array<Record<string, unknown>>,\n fileParts: ChatTurnFilePartInput[],\n): ChatMessagePart[] {\n return toChatMessageParts([...userParts, ...fileParts.map((part) => ({ ...part }))])\n}\n\n// ── producer-stream wrappers (heartbeat + raw tap) ───────────────────────────\n\n/** Fire `onRawEvent` for each producer event, before the engine frames it.\n * Best-effort — a telemetry throw is logged, never propagated. */\nasync function* tapRawEvents(\n source: AsyncIterable<ChatRouteEvent>,\n onRawEvent: (event: ChatRouteEvent) => void | Promise<void>,\n log: (message: string, meta?: Record<string, unknown>) => void,\n): AsyncGenerator<ChatRouteEvent, void, unknown> {\n for await (const event of source) {\n try {\n await onRawEvent(event)\n } catch (err) {\n log('[chat-routes] onRawEvent failed', { error: err instanceof Error ? err.message : String(err) })\n }\n yield event\n }\n}\n\n/** Inject a keepalive whenever `intervalMs` elapses with no source event. The\n * silent window (elapsed + tick) resets on every real event, so a producer\n * that keeps emitting never triggers a heartbeat. Closes the source on early\n * return, matching a `for await` over it. */\nasync function* withStreamHeartbeat(\n source: AsyncIterable<ChatRouteEvent>,\n intervalMs: number,\n makeEvent: (info: { elapsedMs: number; tick: number }) => ChatRouteEvent,\n): AsyncGenerator<ChatRouteEvent, void, unknown> {\n const iterator = source[Symbol.asyncIterator]()\n try {\n let pending = iterator.next()\n let windowStart = Date.now()\n let tick = 0\n for (;;) {\n let timer: ReturnType<typeof setTimeout> | undefined\n let winner: 'event' | 'heartbeat'\n try {\n const heartbeat = new Promise<'heartbeat'>((resolve) => {\n timer = setTimeout(() => resolve('heartbeat'), intervalMs)\n })\n winner = await Promise.race([pending.then(() => 'event' as const), heartbeat])\n } finally {\n // Clear the pending timer on EVERY exit — including a `pending`\n // rejection — so a rejected source never orphans a setTimeout that\n // keeps the runtime alive for up to `intervalMs`.\n if (timer !== undefined) clearTimeout(timer)\n }\n if (winner === 'heartbeat') {\n tick += 1\n yield makeEvent({ elapsedMs: Date.now() - windowStart, tick })\n continue\n }\n const result = await pending\n if (result.done) return\n yield result.value\n pending = iterator.next()\n windowStart = Date.now()\n tick = 0\n }\n } finally {\n await iterator.return?.()\n }\n}\n\n// ── the factory ────────────────────────────────────────────────────────────\n\nexport function createChatTurnRoutes<TContext = void>(\n options: CreateChatTurnRoutesOptions<TContext>,\n): ChatTurnRoutes {\n const log = options.log ?? ((message, meta) => console.error(message, meta ?? ''))\n\n async function turn(request: Request, ctx?: { waitUntil?(p: Promise<unknown>): void }): Promise<Response> {\n const [rawBody, badBody] = await parseJsonObjectBody(request)\n if (badBody) return badBody\n\n let parsed: ParsedTurnBody\n try {\n parsed = validateTurnBody(rawBody, options.maxInlinePartBytes)\n } catch (err) {\n if (err instanceof ChatTurnInputError) return errorResponse(err)\n throw err\n }\n const { payload, content, fileParts, turnId } = parsed\n\n const auth = await options.authorize({ request, intent: 'turn', body: payload })\n if (!auth.ok) return auth.response\n const { tenantId, userId, context } = auth\n\n // Turn identity: reuse the just-persisted user row on a retry (same\n // turnId or identical trailing content) instead of double-inserting.\n const existingMessages = (await options.store.listMessages(payload.threadId)).map((m) => ({\n id: m.id,\n role: m.role,\n content: m.content,\n parts: (m.parts ?? null) as PersistedChatMessageForTurn['parts'],\n }))\n const chatTurn = resolveChatTurn({ existingMessages, userContent: content, turnId })\n\n const identity: ChatTurnIdentity = {\n tenantId,\n sessionId: payload.threadId,\n userId,\n turnIndex: chatTurn.turnIndex,\n }\n const executionId = deriveExecutionId({\n projectId: options.projectId,\n sessionId: payload.threadId,\n turnIndex: chatTurn.turnIndex,\n })\n const turnStreamId = crypto.randomUUID()\n\n const prompt: string | ChatTurnPartInput[] =\n fileParts.length === 0\n ? content\n : content\n ? [{ type: 'text', text: content }, ...fileParts]\n : [...fileParts]\n\n // The producer input every pre-turn seam reads (and `beforeTurn` may\n // rewrite). Mutated in place before the producer's deferred first pull.\n let produceArgs: ChatTurnProduceArgs<TContext> = {\n request,\n body: payload,\n identity,\n context,\n prompt,\n executionId,\n turnStreamId,\n priorMessages: chatTurn.priorMessages,\n }\n\n // Single-flight lock: acquire before any side effect. `release` runs\n // exactly once — in the drain's `finally` on a normal turn, or right here\n // on a short-circuit / throw.\n let lockAcquired = false\n let lockHandle: unknown\n let lockReleased = false\n const releaseLock = async (): Promise<void> => {\n if (!lockAcquired || lockReleased) return\n lockReleased = true\n try {\n await options.turnLock!.release(lockHandle)\n } catch (err) {\n log('[chat-routes] turnLock.release failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n if (options.turnLock) {\n const acquired = await options.turnLock.acquire(produceArgs)\n if (!acquired.acquired) return acquired.response\n lockAcquired = true\n lockHandle = acquired.handle\n }\n\n // Turn state, hoisted so the pre-stream `catch` can settle the lifecycle\n // (fire `onTurnError`, close the span) even when a seam throws\n // synchronously before the drain — the drain would otherwise be the only\n // path that runs the terminal hook.\n let producer: ChatTurnRouteProducer | undefined\n let runFailed = false\n // Data of the event that marked the run failed — handed to `onTurnError`\n // when no drain throw supplies a richer cause.\n let lastFailureData: Record<string, unknown> | undefined\n let turnStartedAtMs = 0\n let turnStarted = false\n let lifecycleSettled = false\n\n // Exactly one terminal lifecycle hook, after the turn settles (idempotent).\n // Failure is this route's own verdict (`runFailed` from error/failed\n // events, or a drain/sync throw), not the engine's envelope.\n const fireTerminalLifecycle = async (failed: boolean, terminalError: unknown): Promise<void> => {\n if (lifecycleSettled) return\n lifecycleSettled = true\n const lifecycle = options.lifecycle\n if (!lifecycle) return\n const durationMs = Date.now() - turnStartedAtMs\n try {\n if (failed) {\n await lifecycle.onTurnError?.({\n identity, executionId, turnStreamId, context, durationMs,\n error: terminalError ?? lastFailureData ?? new Error('chat turn failed'),\n })\n } else {\n await lifecycle.onTurnComplete?.({\n identity, executionId, turnStreamId, context, durationMs,\n finalText: producer?.finalText() ?? '',\n usage: producer?.usage?.() ?? {},\n })\n }\n } catch (err) {\n log('[chat-routes] lifecycle terminal hook failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n try {\n if (chatTurn.shouldInsertUserMessage) {\n await options.store.appendMessage({\n threadId: payload.threadId,\n role: 'user',\n content,\n parts: userPartsWithFiles(chatTurn.userParts, fileParts),\n })\n }\n\n // Domain-readiness gate: may short-circuit with the product's own\n // response before the producer runs. The user row above is kept (a real\n // user turn); the gate's response is the assistant side of it.\n if (options.contextGate) {\n const gate = await options.contextGate(produceArgs)\n if (!gate.proceed) {\n await releaseLock()\n return gate.response\n }\n }\n\n // Observe + optionally augment the assembled producer input.\n if (options.beforeTurn) {\n const patch = await options.beforeTurn(produceArgs)\n if (patch) produceArgs = { ...produceArgs, ...patch }\n }\n\n // Durability tap: every engine event buffers (coalesced) so a dropped\n // client replays the tail. Live delivery rides the Response body, not the\n // tap, so `write` is intentionally absent.\n const tap = createBufferedTurnTap({\n store: options.turnStore,\n turnId: turnStreamId,\n scopeId: payload.threadId,\n coalesce: options.coalesceTurnEvents ?? coalesceDeltas,\n })\n const turnMarker = { type: 'turn', turnId: turnStreamId }\n await tap.onEvent(turnMarker)\n\n turnStartedAtMs = Date.now()\n turnStarted = true\n if (options.lifecycle?.onTurnStart) {\n try {\n await options.lifecycle.onTurnStart({\n identity, executionId, turnStreamId, context, startedAt: turnStartedAtMs,\n })\n } catch (err) {\n log('[chat-routes] lifecycle.onTurnStart failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n }\n\n const result = handleChatTurn({\n identity,\n waitUntil: ctx?.waitUntil,\n log,\n hooks: {\n // The engine wants a synchronous producer; box resolution is async —\n // defer it into the generator's first pull.\n produce: () => ({\n stream: (async function* () {\n producer = await options.produce(produceArgs)\n let source: AsyncIterable<ChatRouteEvent> = producer.stream\n if (options.onRawEvent) {\n source = tapRawEvents(source, (event) => options.onRawEvent!(event, context), log)\n }\n if (options.heartbeat) {\n source = withStreamHeartbeat(source, options.heartbeat.intervalMs, options.heartbeat.event)\n }\n for await (const event of source) yield event\n })(),\n finalText: () => producer?.finalText() ?? '',\n }),\n onEvent: async (event) => {\n if (event.type === 'session.run.failed' || event.type === 'error') {\n runFailed = true\n lastFailureData = event.data\n }\n await tap.onEvent(event)\n if (options.onEvent) await options.onEvent(event, context)\n },\n ...(options.transformFinalText ? { transformFinalText: options.transformFinalText } : {}),\n persistAssistantMessage: async ({ finalText }) => {\n // The typed boundary: stream-normalizer records → stored vocabulary\n // (validating projection owned by /chat-store — no cast here). The\n // scalar `finalText` arrives already transformed by the engine; the\n // producer's text PARTS are raw, so the same transform must run over\n // each text segment before persistence or a redaction (legal PII)\n // leaks at rest through message.parts.\n const rawParts = producer?.assistantParts ? producer.assistantParts() : undefined\n const projected =\n rawParts && options.transformFinalText\n ? await Promise.all(\n rawParts.map(async (part) =>\n String((part as { type?: unknown }).type ?? '') === 'text'\n ? { ...part, text: await options.transformFinalText!(String((part as { text?: unknown }).text ?? '')) }\n : part,\n ),\n )\n : rawParts\n const parts = projected ? toChatMessageParts(projected) : undefined\n if (!finalText.trim() && (!parts || parts.length === 0)) return\n const usage = producer?.usage?.() ?? {}\n await options.store.appendMessage({\n threadId: payload.threadId,\n role: 'assistant',\n content: finalText,\n ...(parts && parts.length > 0 ? { parts } : {}),\n ...(producer?.model ? { model: producer.model } : {}),\n ...(usage.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),\n ...(usage.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),\n ...(usage.reasoningTokens !== undefined ? { reasoningTokens: usage.reasoningTokens } : {}),\n ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),\n ...(usage.cacheWriteTokens !== undefined ? { cacheWriteTokens: usage.cacheWriteTokens } : {}),\n ...(usage.costUsd !== undefined ? { costUsd: usage.costUsd } : {}),\n })\n },\n ...(options.onTurnComplete\n ? {\n // Wired into the engine's completion hook, which fires only when\n // the stream ended without throwing. A terminal error EVENT\n // (not a throw) still lands here — so surface `runFailed` so the\n // product skips billing an errored turn instead of marking it\n // complete with empty text.\n onTurnComplete: ({ identity: turnIdentity, finalText }: { identity: ChatTurnIdentity; finalText: string }) =>\n options.onTurnComplete!({\n identity: turnIdentity,\n finalText,\n context,\n failed: runFailed,\n ...(runFailed ? { failureReason: failureReasonOf(lastFailureData) } : {}),\n }),\n }\n : {}),\n ...(options.traceFlush ? { traceFlush: () => options.traceFlush!(context) } : {}),\n },\n })\n\n // Tee: one branch to the live client, one drained under waitUntil so the\n // turn (and its buffering via onEvent) runs to completion after a client\n // drop — the engine body executes as it is pulled.\n const [clientBody, drainBody] = result.body.tee()\n const drained = (async () => {\n const reader = drainBody.getReader()\n let drainError: unknown\n try {\n for (;;) {\n const { done } = await reader.read()\n if (done) break\n }\n } catch (err) {\n drainError = err\n log('[chat-routes] turn drain failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n const failed = runFailed || drainError !== undefined\n try {\n await tap.done(failed ? 'error' : 'complete')\n } catch (err) {\n log('[chat-routes] turn buffer finalize failed', {\n turnId: turnStreamId,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n await fireTerminalLifecycle(failed, drainError)\n await releaseLock()\n })()\n if (ctx?.waitUntil) ctx.waitUntil(drained)\n else void drained.catch(() => {})\n\n // Announce the replay handle before the engine's first event.\n const encoder = new TextEncoder()\n const marker = new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(encoder.encode(`${JSON.stringify(turnMarker)}\\n`))\n controller.close()\n },\n })\n const body = concatStreams([marker, clientBody])\n\n return new Response(body, {\n headers: {\n 'Content-Type': result.contentType,\n 'Cache-Control': 'no-cache',\n },\n })\n } catch (err) {\n // A throw before the turn began streaming (user-insert, gate, beforeTurn,\n // lifecycle-start, tap setup, engine construction, tee). If the turn had\n // already started, settle the lifecycle with `onTurnError` (close the\n // span) — the drain never ran to do it. Then release the lock, propagate.\n if (turnStarted) await fireTerminalLifecycle(true, err)\n await releaseLock()\n throw err\n }\n }\n\n async function replay(request: Request, params: { turnId: string }): Promise<Response> {\n const turnId = params.turnId?.trim()\n if (!turnId) return Response.json({ error: 'Missing turnId' }, { status: 400 })\n const auth = await options.authorize({ request, intent: 'replay', turnId })\n if (!auth.ok) return auth.response\n\n const fromSeqRaw = new URL(request.url).searchParams.get('fromSeq')\n const fromSeq = fromSeqRaw ? Math.max(0, Math.trunc(Number(fromSeqRaw)) || 0) : 0\n\n const encoder = new TextEncoder()\n const events = replayTurnEvents({\n store: options.turnStore,\n turnId,\n fromSeq,\n ...(options.replay?.pollMs !== undefined ? { pollMs: options.replay.pollMs } : {}),\n ...(options.replay?.timeoutMs !== undefined ? { timeoutMs: options.replay.timeoutMs } : {}),\n })\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n const { done, value } = await events.next()\n if (done) {\n controller.close()\n return\n }\n controller.enqueue(encoder.encode(`${value.event}\\n`))\n },\n cancel() {\n void events.return(undefined)\n },\n })\n return new Response(body, {\n headers: {\n 'Content-Type': 'application/x-ndjson',\n 'Cache-Control': 'no-cache',\n },\n })\n }\n\n return {\n turn,\n replay,\n interactions: options.interactions ? createInteractionAnswerRoute(options.interactions) : null,\n }\n}\n\n/** Sequential concat of byte streams (marker line, then the engine body). */\nfunction concatStreams(streams: ReadableStream<Uint8Array>[]): ReadableStream<Uint8Array> {\n let index = 0\n let reader: ReadableStreamDefaultReader<Uint8Array> | null = null\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n for (;;) {\n if (!reader) {\n const next = streams[index++]\n if (!next) {\n controller.close()\n return\n }\n reader = next.getReader()\n }\n const { done, value } = await reader.read()\n if (done) {\n reader = null\n continue\n }\n controller.enqueue(value)\n return\n }\n },\n async cancel(reason) {\n await reader?.cancel(reason)\n for (const stream of streams.slice(index)) await stream.cancel(reason)\n },\n })\n}\n","/**\n * Sandbox lane: bridge a raw sandbox event stream (`streamSandboxPrompt`) into\n * the `ChatTurnProducer` shape agent-runtime's `handleChatTurn` consumes AND\n * the client vocabulary `/web-react`'s `dispatchChatStreamLine` already parses\n * (`text` / `reasoning` / `tool_call` / `tool_result` / `usage` /\n * `interaction`). Legal and tax each hand-rolled this mapping differently;\n * this is that middle, composed from `/stream`'s normalizers — no new loop\n * logic, no SDK import (the event source is an injected `AsyncIterable`).\n *\n * Alongside the live mapping it accumulates the PERSISTED projection — the\n * `message.parts` rows `/chat-store` stores — via `normalizePersistedPart` /\n * `mergePersistedPart` / `finalizeAssistantParts`, plus the usage receipt from\n * `step-finish` parts. `createChatTurnRoutes` reads both after drain.\n */\n\nimport {\n isRenderableInteractionKind,\n parseInteractionRequest,\n} from '../interactions/contract'\nimport {\n asRecord,\n asString,\n finalizeAssistantParts,\n getPartKey,\n mergePersistedPart,\n normalizePersistedPart,\n normalizeToolEvent,\n type JsonRecord,\n type StreamEvent,\n} from '../stream/index'\nimport type { ChatTurnRouteProducer, ChatTurnUsage } from './turn-routes'\n\nexport interface SandboxChatProducerOptions {\n /** The raw sandbox event stream (e.g. `streamSandboxPrompt(...)`). */\n events: AsyncIterable<unknown>\n /** Recorded on the persisted assistant message. */\n model?: string\n /** Which ask kinds the product renders a card for. Anything else is\n * auto-declined (see `declineInteraction`) so the run never hangs in the\n * broker waiting on a card no client will show. Default: question/plan. */\n isRenderableInteraction?: (kind: string) => boolean\n /** Resolve a non-renderable ask (wire `respondToSessionInteraction` with the\n * session's sidecar connection). Without it, non-renderable asks are only\n * logged — the run stays blocked until the broker times out. */\n declineInteraction?: (id: string) => Promise<void>\n log?: (message: string, meta?: Record<string, unknown>) => void\n}\n\ninterface TextTracker {\n /** Full accumulated text per part key, to derive suffix deltas from\n * snapshot-only harness events. */\n seen: Map<string, string>\n}\n\n/** Delta to emit for one text/reasoning part update: prefer the harness's\n * explicit delta; otherwise diff the snapshot against what was already\n * emitted for that part (snapshot-only harnesses re-send the whole text). */\nfunction textDelta(tracker: TextTracker, key: string, part: JsonRecord, rawDelta: unknown): string {\n const explicit = typeof rawDelta === 'string' ? rawDelta : undefined\n const previous = tracker.seen.get(key) ?? ''\n if (explicit !== undefined) {\n tracker.seen.set(key, previous + explicit)\n return explicit\n }\n const snapshot = asString(part.text) ?? asString(part.content) ?? ''\n if (!snapshot) return ''\n if (snapshot.startsWith(previous)) {\n tracker.seen.set(key, snapshot)\n return snapshot.slice(previous.length)\n }\n // The snapshot replaced the text outright — emit it whole; the persisted\n // projection stays correct because finalText is authoritative at finalize.\n tracker.seen.set(key, snapshot)\n return snapshot\n}\n\nfunction usageFromStepFinish(part: JsonRecord, usage: ChatTurnUsage): void {\n const tokens = asRecord(part.tokens)\n if (tokens) {\n const cache = asRecord(tokens.cache)\n const add = (current: number | undefined, value: unknown): number | undefined => {\n const n = Number(value)\n if (!Number.isFinite(n)) return current\n return (current ?? 0) + n\n }\n usage.inputTokens = add(usage.inputTokens, tokens.input)\n usage.outputTokens = add(usage.outputTokens, tokens.output)\n usage.reasoningTokens = add(usage.reasoningTokens, tokens.reasoning)\n if (cache) {\n usage.cacheReadTokens = add(usage.cacheReadTokens, cache.read)\n usage.cacheWriteTokens = add(usage.cacheWriteTokens, cache.write)\n }\n }\n const cost = Number(part.cost)\n if (Number.isFinite(cost)) usage.costUsd = (usage.costUsd ?? 0) + cost\n}\n\nexport function createSandboxChatProducer(options: SandboxChatProducerOptions): ChatTurnRouteProducer {\n const log = options.log ?? ((message, meta) => console.error(message, meta ?? ''))\n const renderable = options.isRenderableInteraction ?? isRenderableInteractionKind\n\n let fullText = ''\n const partOrder: string[] = []\n const partMap = new Map<string, JsonRecord>()\n const tracker: TextTracker = { seen: new Map() }\n const usage: ChatTurnUsage = {}\n /** Tool ids already announced as `tool_call` / settled as `tool_result`. */\n const announcedTools = new Set<string>()\n const settledTools = new Set<string>()\n /** Id-less step boundaries: one occurrence per key, never merged. */\n let stepCounter = 0\n\n function recordPersistedPart(part: JsonRecord, delta: string | undefined, keyOverride?: string): void {\n const persisted = normalizePersistedPart(part)\n if (!persisted) return\n const key = keyOverride ?? getPartKey(persisted)\n if (!partMap.has(key)) partOrder.push(key)\n partMap.set(key, mergePersistedPart(partMap.get(key), persisted, delta))\n }\n\n async function* stream(): AsyncGenerator<StreamEvent, void, unknown> {\n for await (const raw of options.events) {\n const record = asRecord(raw)\n if (!record || typeof record.type !== 'string') continue\n // Fold bare tool_call/tool_result shapes into the canonical part event;\n // everything else keeps its original record (verbatim forwarding must\n // not strip fields outside `data`).\n const normalized = normalizeToolEvent({ type: record.type, data: asRecord(record.data) })\n const event = normalized.type === 'message.part.updated' ? normalized : (record as unknown as StreamEvent)\n\n if (event.type === 'message.part.updated') {\n const part = asRecord(event.data?.part)\n if (!part) continue\n const rawDelta = event.data?.delta\n const partType = String(part.type ?? '')\n\n if (partType === 'text' || partType === 'reasoning') {\n const key = getPartKey(part)\n const delta = textDelta(tracker, key, part, rawDelta)\n recordPersistedPart(part, delta || undefined)\n if (delta) {\n if (partType === 'text') fullText += delta\n yield { type: partType, text: delta } as StreamEvent & { text: string }\n }\n continue\n }\n\n if (partType === 'tool') {\n recordPersistedPart(part, undefined)\n const persisted = partMap.get(getPartKey(part))\n const state = asRecord(persisted?.state)\n const toolId = String(persisted?.id ?? '')\n const toolName = String(persisted?.tool ?? 'tool')\n if (toolId && !announcedTools.has(toolId)) {\n announcedTools.add(toolId)\n yield {\n type: 'tool_call',\n call: { toolCallId: toolId, toolName, args: asRecord(state?.input) ?? {} },\n } as StreamEvent\n }\n const status = String(state?.status ?? '')\n if (toolId && (status === 'completed' || status === 'error') && !settledTools.has(toolId)) {\n settledTools.add(toolId)\n yield {\n type: 'tool_result',\n toolCallId: toolId,\n toolName,\n outcome: {\n ok: status === 'completed',\n ...(state?.output !== undefined ? { result: state.output } : {}),\n ...(asString(state?.error) ? { message: asString(state?.error) } : {}),\n },\n } as StreamEvent\n }\n continue\n }\n\n if (partType === 'step-finish') {\n usageFromStepFinish(part, usage)\n // Persist the per-step receipt too (unique key per occurrence: the\n // parts have no id and two receipts must never merge into one).\n recordPersistedPart(part, undefined, `step-finish:#${stepCounter++}`)\n const promptTokens = usage.inputTokens ?? 0\n const completionTokens = usage.outputTokens ?? 0\n if (promptTokens || completionTokens) {\n yield { type: 'usage', usage: { promptTokens, completionTokens } } as StreamEvent\n }\n continue\n }\n\n if (partType === 'step-start') {\n recordPersistedPart(part, undefined, `step-start:#${stepCounter}`)\n continue\n }\n\n // Remaining storable kinds (file/image/subtask) have no live\n // vocabulary line; they persist so the transcript keeps them.\n recordPersistedPart(part, undefined)\n continue\n }\n\n if (event.type === 'interaction') {\n const parsed = parseInteractionRequest(asRecord(record.data))\n if (!parsed.succeeded) {\n log('[chat-routes] dropping malformed interaction event', { error: parsed.error })\n continue\n }\n if (renderable(parsed.value.kind)) {\n yield event\n continue\n }\n // Non-renderable ask: the run is blocked in the broker until someone\n // answers. Decline it so the turn proceeds instead of hanging.\n if (options.declineInteraction) {\n try {\n await options.declineInteraction(parsed.value.id)\n } catch (err) {\n log('[chat-routes] failed to auto-decline interaction', {\n id: parsed.value.id,\n error: err instanceof Error ? err.message : String(err),\n })\n }\n } else {\n log('[chat-routes] non-renderable interaction with no declineInteraction wired', {\n id: parsed.value.id,\n kind: parsed.value.kind,\n })\n }\n continue\n }\n\n if (event.type === 'result') {\n const finalText = asString(event.data?.finalText)\n if (finalText) fullText = finalText\n const resultUsage = asRecord(event.data?.usage)\n if (resultUsage) {\n const input = Number(resultUsage.inputTokens)\n const output = Number(resultUsage.outputTokens)\n if (Number.isFinite(input)) usage.inputTokens = input\n if (Number.isFinite(output)) usage.outputTokens = output\n }\n continue\n }\n\n // Everything else (interaction.cancel, error, lifecycle) forwards\n // verbatim — the client parser ignores unknown types.\n yield event\n }\n }\n\n return {\n stream: stream(),\n finalText: () => fullText,\n assistantParts: () => finalizeAssistantParts(partOrder, partMap, fullText),\n usage: () => usage,\n ...(options.model ? { model: options.model } : {}),\n }\n}\n","/**\n * `createUploadRoute` — the multimodal middle. Accepts multipart file uploads\n * and returns `PromptInputPart`-shaped descriptors the client echoes back on\n * send (`ChatTurnRequestPayload.parts`):\n *\n * ≤ inlineMaxBytes (700 KiB default) → inline `data:` URI part — rides the\n * turn body directly, no sandbox round trip.\n * > inlineMaxBytes → written into the sandbox workspace (base64 through the\n * structural `write` seam — `box.fs` satisfies it) and referenced by\n * `path`. Mandatory two-step: the gateway caps request bodies at ~1 MiB,\n * so a large file can never ride the prompt POST.\n *\n * The sink is structural (no sandbox-SDK import); products pass `box.fs`.\n */\n\nimport type { ChatTurnFilePartInput } from './wire'\n\n/** 700 KiB: base64 inflates ~4/3, so an inline part stays comfortably under\n * the ~1 MiB gateway body cap alongside the JSON envelope. */\nexport const UPLOAD_INLINE_MAX_BYTES = 700 * 1024\n\n/** 8 MiB default ceiling per file — one base64 `write` call handles it. Raise\n * it only with a sink that can take the bigger single write. */\nexport const UPLOAD_MAX_FILE_BYTES = 8 * 1024 * 1024\n\n/** Structural match of the sandbox SDK's `box.fs` write surface (v0.10.5+:\n * `encoding: 'base64'` is the worker-safe binary path). */\nexport interface SandboxUploadSink {\n write(path: string, content: string, options?: { encoding?: 'utf8' | 'base64' }): Promise<unknown>\n}\n\nexport type UploadAuthorization =\n | {\n ok: true\n /** Where large files land. Absent/null: only inline uploads are\n * accepted and an over-inline-cap file is rejected with 413. */\n sink?: SandboxUploadSink | null\n /** Per-request override of the workspace directory large files go to. */\n uploadDir?: string\n }\n | { ok: false; response: Response }\n\nexport interface CreateUploadRouteOptions {\n /** Authenticate the caller and resolve the sandbox file sink (usually\n * `ensureWorkspaceSandbox(...)` → `box.fs`). */\n authorize(args: { request: Request }): Promise<UploadAuthorization>\n /** Inline-vs-sandbox threshold. Default {@link UPLOAD_INLINE_MAX_BYTES}. */\n inlineMaxBytes?: number\n /** Hard per-file cap. Default {@link UPLOAD_MAX_FILE_BYTES}. */\n maxFileBytes?: number\n /** Workspace directory for path-ref files. Default `'uploads'`. */\n uploadDir?: string\n}\n\n/** One uploaded file, ready for the composer chip and the turn body. */\nexport interface UploadedChatFile {\n id: string\n name: string\n size: number\n mediaType: string\n /** True when the part carries the bytes inline (`data:` URI). */\n inline: boolean\n /** Echo this back verbatim in `ChatTurnRequestPayload.parts`. */\n part: ChatTurnFilePartInput\n}\n\n/** Path-safe file name: basename only, conservative charset, length-capped. */\nexport function sanitizeUploadFilename(name: string): string {\n const base = name.split(/[\\\\/]/).pop() ?? 'file'\n const safe = base.replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^\\.+/, '_')\n return (safe || 'file').slice(0, 120)\n}\n\nconst BASE64_CHUNK = 0x8000\n\nexport function bytesToBase64(bytes: Uint8Array): string {\n let binary = ''\n for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK) {\n binary += String.fromCharCode(...bytes.subarray(offset, offset + BASE64_CHUNK))\n }\n return btoa(binary)\n}\n\nfunction uploadError(status: number, code: string, error: string): Response {\n return Response.json({ code, error }, { status })\n}\n\nexport function createUploadRoute(options: CreateUploadRouteOptions): (request: Request) => Promise<Response> {\n const inlineMaxBytes = options.inlineMaxBytes ?? UPLOAD_INLINE_MAX_BYTES\n const maxFileBytes = options.maxFileBytes ?? UPLOAD_MAX_FILE_BYTES\n\n return async function upload(request: Request): Promise<Response> {\n const auth = await options.authorize({ request })\n if (!auth.ok) return auth.response\n const sink = auth.sink ?? null\n const uploadDir = (auth.uploadDir ?? options.uploadDir ?? 'uploads').replace(/\\/+$/, '')\n\n let form: FormData\n try {\n form = await request.formData()\n } catch {\n return uploadError(400, 'INVALID_UPLOAD', 'Expected a multipart/form-data body with file fields')\n }\n const files: File[] = []\n form.forEach((value) => {\n if (value instanceof File) files.push(value)\n })\n if (files.length === 0) {\n return uploadError(400, 'INVALID_UPLOAD', 'No files in the upload body')\n }\n\n const uploaded: UploadedChatFile[] = []\n for (const file of files) {\n const name = sanitizeUploadFilename(file.name)\n const mediaType = file.type || 'application/octet-stream'\n const partType: ChatTurnFilePartInput['type'] = mediaType.startsWith('image/') ? 'image' : 'file'\n\n if (file.size > maxFileBytes) {\n return uploadError(\n 413,\n 'FILE_TOO_LARGE',\n `${name} is ${file.size}B, over the ${maxFileBytes}B per-file cap`,\n )\n }\n\n const id = crypto.randomUUID()\n if (file.size <= inlineMaxBytes) {\n const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()))\n uploaded.push({\n id,\n name,\n size: file.size,\n mediaType,\n inline: true,\n part: {\n type: partType,\n filename: name,\n mediaType,\n url: `data:${mediaType};base64,${base64}`,\n },\n })\n continue\n }\n\n if (!sink) {\n return uploadError(\n 413,\n 'SANDBOX_REQUIRED',\n `${name} is ${file.size}B, over the ${inlineMaxBytes}B inline cap, and no sandbox is available to hold it`,\n )\n }\n const path = `${uploadDir}/${id}-${name}`\n const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()))\n await sink.write(path, base64, { encoding: 'base64' })\n uploaded.push({\n id,\n name,\n size: file.size,\n mediaType,\n inline: false,\n part: { type: partType, filename: name, mediaType, path },\n })\n }\n\n return Response.json({ files: uploaded })\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAS,mBAAmB,sBAAsB;AA4GlD,SAAS,gBAAgB,MAA+D;AACtF,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,UAAU,KAAK,WAAW,KAAK,SAAS,KAAK;AACnD,MAAI,OAAO,YAAY,YAAY,QAAQ,SAAS,EAAG,QAAO;AAC9D,SAAO;AACT;AAkKA,SAAS,cAAc,KAAmC;AACxD,SAAO,SAAS,KAAK,EAAE,MAAM,IAAI,MAAM,OAAO,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AACrF;AASA,SAAS,iBAAiB,MAA+B,oBAAwD;AAC/G,QAAM,WAAW,OAAO,KAAK,aAAa,WAAW,KAAK,SAAS,KAAK,IAAI;AAC5E,MAAI,CAAC,SAAU,OAAM,IAAI,mBAAmB,kBAAkB;AAC9D,QAAM,aAAa,KAAK,WAAW,KAAK,WAAW;AACnD,MAAI,OAAO,eAAe,SAAU,OAAM,IAAI,mBAAmB,0BAA0B;AAC3F,QAAM,UAAU,WAAW,KAAK;AAChC,QAAM,YAAY,mBAAmB,KAAK,KAAK;AAC/C,MAAI,CAAC,WAAW,UAAU,WAAW,GAAG;AACtC,UAAM,IAAI,mBAAmB,6CAA6C;AAAA,EAC5E;AACA,6BAA2B,WAAW,kBAAkB;AACxD,MAAI;AACJ,MAAI;AACF,aAAS,sBAAsB,KAAK,MAAM;AAAA,EAC5C,SAAS,KAAK;AACZ,UAAM,IAAI,mBAAmB,eAAe,QAAQ,IAAI,UAAU,gBAAgB;AAAA,EACpF;AACA,SAAO;AAAA,IACL,SAAS,EAAE,GAAG,MAAM,UAAU,QAAQ;AAAA,IACtC;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKA,SAAS,mBACP,WACA,WACmB;AACnB,SAAO,mBAAmB,CAAC,GAAG,WAAW,GAAG,UAAU,IAAI,CAAC,UAAU,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;AACrF;AAMA,gBAAgB,aACd,QACA,YACA,KAC+C;AAC/C,mBAAiB,SAAS,QAAQ;AAChC,QAAI;AACF,YAAM,WAAW,KAAK;AAAA,IACxB,SAAS,KAAK;AACZ,UAAI,mCAAmC,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC;AAAA,IACpG;AACA,UAAM;AAAA,EACR;AACF;AAMA,gBAAgB,oBACd,QACA,YACA,WAC+C;AAC/C,QAAM,WAAW,OAAO,OAAO,aAAa,EAAE;AAC9C,MAAI;AACF,QAAI,UAAU,SAAS,KAAK;AAC5B,QAAI,cAAc,KAAK,IAAI;AAC3B,QAAI,OAAO;AACX,eAAS;AACP,UAAI;AACJ,UAAI;AACJ,UAAI;AACF,cAAM,YAAY,IAAI,QAAqB,CAAC,YAAY;AACtD,kBAAQ,WAAW,MAAM,QAAQ,WAAW,GAAG,UAAU;AAAA,QAC3D,CAAC;AACD,iBAAS,MAAM,QAAQ,KAAK,CAAC,QAAQ,KAAK,MAAM,OAAgB,GAAG,SAAS,CAAC;AAAA,MAC/E,UAAE;AAIA,YAAI,UAAU,OAAW,cAAa,KAAK;AAAA,MAC7C;AACA,UAAI,WAAW,aAAa;AAC1B,gBAAQ;AACR,cAAM,UAAU,EAAE,WAAW,KAAK,IAAI,IAAI,aAAa,KAAK,CAAC;AAC7D;AAAA,MACF;AACA,YAAM,SAAS,MAAM;AACrB,UAAI,OAAO,KAAM;AACjB,YAAM,OAAO;AACb,gBAAU,SAAS,KAAK;AACxB,oBAAc,KAAK,IAAI;AACvB,aAAO;AAAA,IACT;AAAA,EACF,UAAE;AACA,UAAM,SAAS,SAAS;AAAA,EAC1B;AACF;AAIO,SAAS,qBACd,SACgB;AAChB,QAAM,MAAM,QAAQ,QAAQ,CAAC,SAAS,SAAS,QAAQ,MAAM,SAAS,QAAQ,EAAE;AAEhF,iBAAe,KAAK,SAAkB,KAAoE;AACxG,UAAM,CAAC,SAAS,OAAO,IAAI,MAAM,oBAAoB,OAAO;AAC5D,QAAI,QAAS,QAAO;AAEpB,QAAI;AACJ,QAAI;AACF,eAAS,iBAAiB,SAAS,QAAQ,kBAAkB;AAAA,IAC/D,SAAS,KAAK;AACZ,UAAI,eAAe,mBAAoB,QAAO,cAAc,GAAG;AAC/D,YAAM;AAAA,IACR;AACA,UAAM,EAAE,SAAS,SAAS,WAAW,OAAO,IAAI;AAEhD,UAAM,OAAO,MAAM,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,MAAM,QAAQ,CAAC;AAC/E,QAAI,CAAC,KAAK,GAAI,QAAO,KAAK;AAC1B,UAAM,EAAE,UAAU,QAAQ,QAAQ,IAAI;AAItC,UAAM,oBAAoB,MAAM,QAAQ,MAAM,aAAa,QAAQ,QAAQ,GAAG,IAAI,CAAC,OAAO;AAAA,MACxF,IAAI,EAAE;AAAA,MACN,MAAM,EAAE;AAAA,MACR,SAAS,EAAE;AAAA,MACX,OAAQ,EAAE,SAAS;AAAA,IACrB,EAAE;AACF,UAAM,WAAW,gBAAgB,EAAE,kBAAkB,aAAa,SAAS,OAAO,CAAC;AAEnF,UAAM,WAA6B;AAAA,MACjC;AAAA,MACA,WAAW,QAAQ;AAAA,MACnB;AAAA,MACA,WAAW,SAAS;AAAA,IACtB;AACA,UAAM,cAAc,kBAAkB;AAAA,MACpC,WAAW,QAAQ;AAAA,MACnB,WAAW,QAAQ;AAAA,MACnB,WAAW,SAAS;AAAA,IACtB,CAAC;AACD,UAAM,eAAe,OAAO,WAAW;AAEvC,UAAM,SACJ,UAAU,WAAW,IACjB,UACA,UACE,CAAC,EAAE,MAAM,QAAQ,MAAM,QAAQ,GAAG,GAAG,SAAS,IAC9C,CAAC,GAAG,SAAS;AAIrB,QAAI,cAA6C;AAAA,MAC/C;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,eAAe,SAAS;AAAA,IAC1B;AAKA,QAAI,eAAe;AACnB,QAAI;AACJ,QAAI,eAAe;AACnB,UAAM,cAAc,YAA2B;AAC7C,UAAI,CAAC,gBAAgB,aAAc;AACnC,qBAAe;AACf,UAAI;AACF,cAAM,QAAQ,SAAU,QAAQ,UAAU;AAAA,MAC5C,SAAS,KAAK;AACZ,YAAI,yCAAyC;AAAA,UAC3C,QAAQ;AAAA,UACR,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,QACxD,CAAC;AAAA,MACH;AAAA,IACF;AACA,QAAI,QAAQ,UAAU;AACpB,YAAM,WAAW,MAAM,QAAQ,SAAS,QAAQ,WAAW;AAC3D,UAAI,CAAC,SAAS,SAAU,QAAO,SAAS;AACxC,qBAAe;AACf,mBAAa,SAAS;AAAA,IACxB;AAMA,QAAI;AACJ,QAAI,YAAY;AAGhB,QAAI;AACJ,QAAI,kBAAkB;AACtB,QAAI,cAAc;AAClB,QAAI,mBAAmB;AAKvB,UAAM,wBAAwB,OAAO,QAAiB,kBAA0C;AAC9F,UAAI,iBAAkB;AACtB,yBAAmB;AACnB,YAAM,YAAY,QAAQ;AAC1B,UAAI,CAAC,UAAW;AAChB,YAAM,aAAa,KAAK,IAAI,IAAI;AAChC,UAAI;AACF,YAAI,QAAQ;AACV,gBAAM,UAAU,cAAc;AAAA,YAC5B;AAAA,YAAU;AAAA,YAAa;AAAA,YAAc;AAAA,YAAS;AAAA,YAC9C,OAAO,iBAAiB,mBAAmB,IAAI,MAAM,kBAAkB;AAAA,UACzE,CAAC;AAAA,QACH,OAAO;AACL,gBAAM,UAAU,iBAAiB;AAAA,YAC/B;AAAA,YAAU;AAAA,YAAa;AAAA,YAAc;AAAA,YAAS;AAAA,YAC9C,WAAW,UAAU,UAAU,KAAK;AAAA,YACpC,OAAO,UAAU,QAAQ,KAAK,CAAC;AAAA,UACjC,CAAC;AAAA,QACH;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,gDAAgD;AAAA,UAClD,QAAQ;AAAA,UACR,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,QACxD,CAAC;AAAA,MACH;AAAA,IACF;AAEA,QAAI;AACF,UAAI,SAAS,yBAAyB;AACpC,cAAM,QAAQ,MAAM,cAAc;AAAA,UAChC,UAAU,QAAQ;AAAA,UAClB,MAAM;AAAA,UACN;AAAA,UACA,OAAO,mBAAmB,SAAS,WAAW,SAAS;AAAA,QACzD,CAAC;AAAA,MACH;AAKA,UAAI,QAAQ,aAAa;AACvB,cAAM,OAAO,MAAM,QAAQ,YAAY,WAAW;AAClD,YAAI,CAAC,KAAK,SAAS;AACjB,gBAAM,YAAY;AAClB,iBAAO,KAAK;AAAA,QACd;AAAA,MACF;AAGA,UAAI,QAAQ,YAAY;AACtB,cAAM,QAAQ,MAAM,QAAQ,WAAW,WAAW;AAClD,YAAI,MAAO,eAAc,EAAE,GAAG,aAAa,GAAG,MAAM;AAAA,MACtD;AAKA,YAAM,MAAM,sBAAsB;AAAA,QAChC,OAAO,QAAQ;AAAA,QACf,QAAQ;AAAA,QACR,SAAS,QAAQ;AAAA,QACjB,UAAU,QAAQ,sBAAsB;AAAA,MAC1C,CAAC;AACD,YAAM,aAAa,EAAE,MAAM,QAAQ,QAAQ,aAAa;AACxD,YAAM,IAAI,QAAQ,UAAU;AAE5B,wBAAkB,KAAK,IAAI;AAC3B,oBAAc;AACd,UAAI,QAAQ,WAAW,aAAa;AAClC,YAAI;AACF,gBAAM,QAAQ,UAAU,YAAY;AAAA,YAClC;AAAA,YAAU;AAAA,YAAa;AAAA,YAAc;AAAA,YAAS,WAAW;AAAA,UAC3D,CAAC;AAAA,QACH,SAAS,KAAK;AACZ,cAAI,8CAA8C;AAAA,YAChD,QAAQ;AAAA,YACR,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,UACxD,CAAC;AAAA,QACH;AAAA,MACF;AAEA,YAAM,SAAS,eAAe;AAAA,QAC5B;AAAA,QACA,WAAW,KAAK;AAAA,QAChB;AAAA,QACA,OAAO;AAAA;AAAA;AAAA,UAGL,SAAS,OAAO;AAAA,YACd,SAAS,mBAAmB;AAC1B,yBAAW,MAAM,QAAQ,QAAQ,WAAW;AAC5C,kBAAI,SAAwC,SAAS;AACrD,kBAAI,QAAQ,YAAY;AACtB,yBAAS,aAAa,QAAQ,CAAC,UAAU,QAAQ,WAAY,OAAO,OAAO,GAAG,GAAG;AAAA,cACnF;AACA,kBAAI,QAAQ,WAAW;AACrB,yBAAS,oBAAoB,QAAQ,QAAQ,UAAU,YAAY,QAAQ,UAAU,KAAK;AAAA,cAC5F;AACA,+BAAiB,SAAS,OAAQ,OAAM;AAAA,YAC1C,GAAG;AAAA,YACH,WAAW,MAAM,UAAU,UAAU,KAAK;AAAA,UAC5C;AAAA,UACA,SAAS,OAAO,UAAU;AACxB,gBAAI,MAAM,SAAS,wBAAwB,MAAM,SAAS,SAAS;AACjE,0BAAY;AACZ,gCAAkB,MAAM;AAAA,YAC1B;AACA,kBAAM,IAAI,QAAQ,KAAK;AACvB,gBAAI,QAAQ,QAAS,OAAM,QAAQ,QAAQ,OAAO,OAAO;AAAA,UAC3D;AAAA,UACA,GAAI,QAAQ,qBAAqB,EAAE,oBAAoB,QAAQ,mBAAmB,IAAI,CAAC;AAAA,UACvF,yBAAyB,OAAO,EAAE,UAAU,MAAM;AAOhD,kBAAM,WAAW,UAAU,iBAAiB,SAAS,eAAe,IAAI;AACxE,kBAAM,YACJ,YAAY,QAAQ,qBAChB,MAAM,QAAQ;AAAA,cACZ,SAAS;AAAA,gBAAI,OAAO,SAClB,OAAQ,KAA4B,QAAQ,EAAE,MAAM,SAChD,EAAE,GAAG,MAAM,MAAM,MAAM,QAAQ,mBAAoB,OAAQ,KAA4B,QAAQ,EAAE,CAAC,EAAE,IACpG;AAAA,cACN;AAAA,YACF,IACA;AACN,kBAAM,QAAQ,YAAY,mBAAmB,SAAS,IAAI;AAC1D,gBAAI,CAAC,UAAU,KAAK,MAAM,CAAC,SAAS,MAAM,WAAW,GAAI;AACzD,kBAAM,QAAQ,UAAU,QAAQ,KAAK,CAAC;AACtC,kBAAM,QAAQ,MAAM,cAAc;AAAA,cAChC,UAAU,QAAQ;AAAA,cAClB,MAAM;AAAA,cACN,SAAS;AAAA,cACT,GAAI,SAAS,MAAM,SAAS,IAAI,EAAE,MAAM,IAAI,CAAC;AAAA,cAC7C,GAAI,UAAU,QAAQ,EAAE,OAAO,SAAS,MAAM,IAAI,CAAC;AAAA,cACnD,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,YAAY,IAAI,CAAC;AAAA,cAC5E,GAAI,MAAM,iBAAiB,SAAY,EAAE,cAAc,MAAM,aAAa,IAAI,CAAC;AAAA,cAC/E,GAAI,MAAM,oBAAoB,SAAY,EAAE,iBAAiB,MAAM,gBAAgB,IAAI,CAAC;AAAA,cACxF,GAAI,MAAM,oBAAoB,SAAY,EAAE,iBAAiB,MAAM,gBAAgB,IAAI,CAAC;AAAA,cACxF,GAAI,MAAM,qBAAqB,SAAY,EAAE,kBAAkB,MAAM,iBAAiB,IAAI,CAAC;AAAA,cAC3F,GAAI,MAAM,YAAY,SAAY,EAAE,SAAS,MAAM,QAAQ,IAAI,CAAC;AAAA,YAClE,CAAC;AAAA,UACH;AAAA,UACA,GAAI,QAAQ,iBACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAME,gBAAgB,CAAC,EAAE,UAAU,cAAc,UAAU,MACnD,QAAQ,eAAgB;AAAA,cACtB,UAAU;AAAA,cACV;AAAA,cACA;AAAA,cACA,QAAQ;AAAA,cACR,GAAI,YAAY,EAAE,eAAe,gBAAgB,eAAe,EAAE,IAAI,CAAC;AAAA,YACzE,CAAC;AAAA,UACL,IACA,CAAC;AAAA,UACL,GAAI,QAAQ,aAAa,EAAE,YAAY,MAAM,QAAQ,WAAY,OAAO,EAAE,IAAI,CAAC;AAAA,QACjF;AAAA,MACF,CAAC;AAKD,YAAM,CAAC,YAAY,SAAS,IAAI,OAAO,KAAK,IAAI;AAChD,YAAM,WAAW,YAAY;AAC3B,cAAM,SAAS,UAAU,UAAU;AACnC,YAAI;AACJ,YAAI;AACF,qBAAS;AACP,kBAAM,EAAE,KAAK,IAAI,MAAM,OAAO,KAAK;AACnC,gBAAI,KAAM;AAAA,UACZ;AAAA,QACF,SAAS,KAAK;AACZ,uBAAa;AACb,cAAI,mCAAmC;AAAA,YACrC,QAAQ;AAAA,YACR,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,UACxD,CAAC;AAAA,QACH;AACA,cAAM,SAAS,aAAa,eAAe;AAC3C,YAAI;AACF,gBAAM,IAAI,KAAK,SAAS,UAAU,UAAU;AAAA,QAC9C,SAAS,KAAK;AACZ,cAAI,6CAA6C;AAAA,YAC/C,QAAQ;AAAA,YACR,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,UACxD,CAAC;AAAA,QACH;AACA,cAAM,sBAAsB,QAAQ,UAAU;AAC9C,cAAM,YAAY;AAAA,MACpB,GAAG;AACH,UAAI,KAAK,UAAW,KAAI,UAAU,OAAO;AAAA,UACpC,MAAK,QAAQ,MAAM,MAAM;AAAA,MAAC,CAAC;AAGhC,YAAM,UAAU,IAAI,YAAY;AAChC,YAAM,SAAS,IAAI,eAA2B;AAAA,QAC5C,MAAM,YAAY;AAChB,qBAAW,QAAQ,QAAQ,OAAO,GAAG,KAAK,UAAU,UAAU,CAAC;AAAA,CAAI,CAAC;AACpE,qBAAW,MAAM;AAAA,QACnB;AAAA,MACF,CAAC;AACD,YAAM,OAAO,cAAc,CAAC,QAAQ,UAAU,CAAC;AAE/C,aAAO,IAAI,SAAS,MAAM;AAAA,QACxB,SAAS;AAAA,UACP,gBAAgB,OAAO;AAAA,UACvB,iBAAiB;AAAA,QACnB;AAAA,MACF,CAAC;AAAA,IACH,SAAS,KAAK;AAKZ,UAAI,YAAa,OAAM,sBAAsB,MAAM,GAAG;AACtD,YAAM,YAAY;AAClB,YAAM;AAAA,IACR;AAAA,EACF;AAEA,iBAAe,OAAO,SAAkB,QAA+C;AACrF,UAAM,SAAS,OAAO,QAAQ,KAAK;AACnC,QAAI,CAAC,OAAQ,QAAO,SAAS,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAC9E,UAAM,OAAO,MAAM,QAAQ,UAAU,EAAE,SAAS,QAAQ,UAAU,OAAO,CAAC;AAC1E,QAAI,CAAC,KAAK,GAAI,QAAO,KAAK;AAE1B,UAAM,aAAa,IAAI,IAAI,QAAQ,GAAG,EAAE,aAAa,IAAI,SAAS;AAClE,UAAM,UAAU,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI;AAEhF,UAAM,UAAU,IAAI,YAAY;AAChC,UAAM,SAAS,iBAAiB;AAAA,MAC9B,OAAO,QAAQ;AAAA,MACf;AAAA,MACA;AAAA,MACA,GAAI,QAAQ,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,OAAO,OAAO,IAAI,CAAC;AAAA,MAChF,GAAI,QAAQ,QAAQ,cAAc,SAAY,EAAE,WAAW,QAAQ,OAAO,UAAU,IAAI,CAAC;AAAA,IAC3F,CAAC;AACD,UAAM,OAAO,IAAI,eAA2B;AAAA,MAC1C,MAAM,KAAK,YAAY;AACrB,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,YAAI,MAAM;AACR,qBAAW,MAAM;AACjB;AAAA,QACF;AACA,mBAAW,QAAQ,QAAQ,OAAO,GAAG,MAAM,KAAK;AAAA,CAAI,CAAC;AAAA,MACvD;AAAA,MACA,SAAS;AACP,aAAK,OAAO,OAAO,MAAS;AAAA,MAC9B;AAAA,IACF,CAAC;AACD,WAAO,IAAI,SAAS,MAAM;AAAA,MACxB,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,iBAAiB;AAAA,MACnB;AAAA,IACF,CAAC;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAc,QAAQ,eAAe,6BAA6B,QAAQ,YAAY,IAAI;AAAA,EAC5F;AACF;AAGA,SAAS,cAAc,SAAmE;AACxF,MAAI,QAAQ;AACZ,MAAI,SAAyD;AAC7D,SAAO,IAAI,eAA2B;AAAA,IACpC,MAAM,KAAK,YAAY;AACrB,iBAAS;AACP,YAAI,CAAC,QAAQ;AACX,gBAAM,OAAO,QAAQ,OAAO;AAC5B,cAAI,CAAC,MAAM;AACT,uBAAW,MAAM;AACjB;AAAA,UACF;AACA,mBAAS,KAAK,UAAU;AAAA,QAC1B;AACA,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,YAAI,MAAM;AACR,mBAAS;AACT;AAAA,QACF;AACA,mBAAW,QAAQ,KAAK;AACxB;AAAA,MACF;AAAA,IACF;AAAA,IACA,MAAM,OAAO,QAAQ;AACnB,YAAM,QAAQ,OAAO,MAAM;AAC3B,iBAAW,UAAU,QAAQ,MAAM,KAAK,EAAG,OAAM,OAAO,OAAO,MAAM;AAAA,IACvE;AAAA,EACF,CAAC;AACH;;;ACnwBA,SAAS,UAAU,SAAsB,KAAa,MAAkB,UAA2B;AACjG,QAAM,WAAW,OAAO,aAAa,WAAW,WAAW;AAC3D,QAAM,WAAW,QAAQ,KAAK,IAAI,GAAG,KAAK;AAC1C,MAAI,aAAa,QAAW;AAC1B,YAAQ,KAAK,IAAI,KAAK,WAAW,QAAQ;AACzC,WAAO;AAAA,EACT;AACA,QAAM,WAAW,SAAS,KAAK,IAAI,KAAK,SAAS,KAAK,OAAO,KAAK;AAClE,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,SAAS,WAAW,QAAQ,GAAG;AACjC,YAAQ,KAAK,IAAI,KAAK,QAAQ;AAC9B,WAAO,SAAS,MAAM,SAAS,MAAM;AAAA,EACvC;AAGA,UAAQ,KAAK,IAAI,KAAK,QAAQ;AAC9B,SAAO;AACT;AAEA,SAAS,oBAAoB,MAAkB,OAA4B;AACzE,QAAM,SAAS,SAAS,KAAK,MAAM;AACnC,MAAI,QAAQ;AACV,UAAM,QAAQ,SAAS,OAAO,KAAK;AACnC,UAAM,MAAM,CAAC,SAA6B,UAAuC;AAC/E,YAAM,IAAI,OAAO,KAAK;AACtB,UAAI,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AAChC,cAAQ,WAAW,KAAK;AAAA,IAC1B;AACA,UAAM,cAAc,IAAI,MAAM,aAAa,OAAO,KAAK;AACvD,UAAM,eAAe,IAAI,MAAM,cAAc,OAAO,MAAM;AAC1D,UAAM,kBAAkB,IAAI,MAAM,iBAAiB,OAAO,SAAS;AACnE,QAAI,OAAO;AACT,YAAM,kBAAkB,IAAI,MAAM,iBAAiB,MAAM,IAAI;AAC7D,YAAM,mBAAmB,IAAI,MAAM,kBAAkB,MAAM,KAAK;AAAA,IAClE;AAAA,EACF;AACA,QAAM,OAAO,OAAO,KAAK,IAAI;AAC7B,MAAI,OAAO,SAAS,IAAI,EAAG,OAAM,WAAW,MAAM,WAAW,KAAK;AACpE;AAEO,SAAS,0BAA0B,SAA4D;AACpG,QAAM,MAAM,QAAQ,QAAQ,CAAC,SAAS,SAAS,QAAQ,MAAM,SAAS,QAAQ,EAAE;AAChF,QAAM,aAAa,QAAQ,2BAA2B;AAEtD,MAAI,WAAW;AACf,QAAM,YAAsB,CAAC;AAC7B,QAAM,UAAU,oBAAI,IAAwB;AAC5C,QAAM,UAAuB,EAAE,MAAM,oBAAI,IAAI,EAAE;AAC/C,QAAM,QAAuB,CAAC;AAE9B,QAAM,iBAAiB,oBAAI,IAAY;AACvC,QAAM,eAAe,oBAAI,IAAY;AAErC,MAAI,cAAc;AAElB,WAAS,oBAAoB,MAAkB,OAA2B,aAA4B;AACpG,UAAM,YAAY,uBAAuB,IAAI;AAC7C,QAAI,CAAC,UAAW;AAChB,UAAM,MAAM,eAAe,WAAW,SAAS;AAC/C,QAAI,CAAC,QAAQ,IAAI,GAAG,EAAG,WAAU,KAAK,GAAG;AACzC,YAAQ,IAAI,KAAK,mBAAmB,QAAQ,IAAI,GAAG,GAAG,WAAW,KAAK,CAAC;AAAA,EACzE;AAEA,kBAAgB,SAAqD;AACnE,qBAAiB,OAAO,QAAQ,QAAQ;AACtC,YAAM,SAAS,SAAS,GAAG;AAC3B,UAAI,CAAC,UAAU,OAAO,OAAO,SAAS,SAAU;AAIhD,YAAM,aAAa,mBAAmB,EAAE,MAAM,OAAO,MAAM,MAAM,SAAS,OAAO,IAAI,EAAE,CAAC;AACxF,YAAM,QAAQ,WAAW,SAAS,yBAAyB,aAAc;AAEzE,UAAI,MAAM,SAAS,wBAAwB;AACzC,cAAM,OAAO,SAAS,MAAM,MAAM,IAAI;AACtC,YAAI,CAAC,KAAM;AACX,cAAM,WAAW,MAAM,MAAM;AAC7B,cAAM,WAAW,OAAO,KAAK,QAAQ,EAAE;AAEvC,YAAI,aAAa,UAAU,aAAa,aAAa;AACnD,gBAAM,MAAM,WAAW,IAAI;AAC3B,gBAAM,QAAQ,UAAU,SAAS,KAAK,MAAM,QAAQ;AACpD,8BAAoB,MAAM,SAAS,MAAS;AAC5C,cAAI,OAAO;AACT,gBAAI,aAAa,OAAQ,aAAY;AACrC,kBAAM,EAAE,MAAM,UAAU,MAAM,MAAM;AAAA,UACtC;AACA;AAAA,QACF;AAEA,YAAI,aAAa,QAAQ;AACvB,8BAAoB,MAAM,MAAS;AACnC,gBAAM,YAAY,QAAQ,IAAI,WAAW,IAAI,CAAC;AAC9C,gBAAM,QAAQ,SAAS,WAAW,KAAK;AACvC,gBAAM,SAAS,OAAO,WAAW,MAAM,EAAE;AACzC,gBAAM,WAAW,OAAO,WAAW,QAAQ,MAAM;AACjD,cAAI,UAAU,CAAC,eAAe,IAAI,MAAM,GAAG;AACzC,2BAAe,IAAI,MAAM;AACzB,kBAAM;AAAA,cACJ,MAAM;AAAA,cACN,MAAM,EAAE,YAAY,QAAQ,UAAU,MAAM,SAAS,OAAO,KAAK,KAAK,CAAC,EAAE;AAAA,YAC3E;AAAA,UACF;AACA,gBAAM,SAAS,OAAO,OAAO,UAAU,EAAE;AACzC,cAAI,WAAW,WAAW,eAAe,WAAW,YAAY,CAAC,aAAa,IAAI,MAAM,GAAG;AACzF,yBAAa,IAAI,MAAM;AACvB,kBAAM;AAAA,cACJ,MAAM;AAAA,cACN,YAAY;AAAA,cACZ;AAAA,cACA,SAAS;AAAA,gBACP,IAAI,WAAW;AAAA,gBACf,GAAI,OAAO,WAAW,SAAY,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;AAAA,gBAC9D,GAAI,SAAS,OAAO,KAAK,IAAI,EAAE,SAAS,SAAS,OAAO,KAAK,EAAE,IAAI,CAAC;AAAA,cACtE;AAAA,YACF;AAAA,UACF;AACA;AAAA,QACF;AAEA,YAAI,aAAa,eAAe;AAC9B,8BAAoB,MAAM,KAAK;AAG/B,8BAAoB,MAAM,QAAW,gBAAgB,aAAa,EAAE;AACpE,gBAAM,eAAe,MAAM,eAAe;AAC1C,gBAAM,mBAAmB,MAAM,gBAAgB;AAC/C,cAAI,gBAAgB,kBAAkB;AACpC,kBAAM,EAAE,MAAM,SAAS,OAAO,EAAE,cAAc,iBAAiB,EAAE;AAAA,UACnE;AACA;AAAA,QACF;AAEA,YAAI,aAAa,cAAc;AAC7B,8BAAoB,MAAM,QAAW,eAAe,WAAW,EAAE;AACjE;AAAA,QACF;AAIA,4BAAoB,MAAM,MAAS;AACnC;AAAA,MACF;AAEA,UAAI,MAAM,SAAS,eAAe;AAChC,cAAM,SAAS,wBAAwB,SAAS,OAAO,IAAI,CAAC;AAC5D,YAAI,CAAC,OAAO,WAAW;AACrB,cAAI,sDAAsD,EAAE,OAAO,OAAO,MAAM,CAAC;AACjF;AAAA,QACF;AACA,YAAI,WAAW,OAAO,MAAM,IAAI,GAAG;AACjC,gBAAM;AACN;AAAA,QACF;AAGA,YAAI,QAAQ,oBAAoB;AAC9B,cAAI;AACF,kBAAM,QAAQ,mBAAmB,OAAO,MAAM,EAAE;AAAA,UAClD,SAAS,KAAK;AACZ,gBAAI,oDAAoD;AAAA,cACtD,IAAI,OAAO,MAAM;AAAA,cACjB,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,YACxD,CAAC;AAAA,UACH;AAAA,QACF,OAAO;AACL,cAAI,6EAA6E;AAAA,YAC/E,IAAI,OAAO,MAAM;AAAA,YACjB,MAAM,OAAO,MAAM;AAAA,UACrB,CAAC;AAAA,QACH;AACA;AAAA,MACF;AAEA,UAAI,MAAM,SAAS,UAAU;AAC3B,cAAM,YAAY,SAAS,MAAM,MAAM,SAAS;AAChD,YAAI,UAAW,YAAW;AAC1B,cAAM,cAAc,SAAS,MAAM,MAAM,KAAK;AAC9C,YAAI,aAAa;AACf,gBAAM,QAAQ,OAAO,YAAY,WAAW;AAC5C,gBAAM,SAAS,OAAO,YAAY,YAAY;AAC9C,cAAI,OAAO,SAAS,KAAK,EAAG,OAAM,cAAc;AAChD,cAAI,OAAO,SAAS,MAAM,EAAG,OAAM,eAAe;AAAA,QACpD;AACA;AAAA,MACF;AAIA,YAAM;AAAA,IACR;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,WAAW,MAAM;AAAA,IACjB,gBAAgB,MAAM,uBAAuB,WAAW,SAAS,QAAQ;AAAA,IACzE,OAAO,MAAM;AAAA,IACb,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,EAClD;AACF;;;AC9OO,IAAM,0BAA0B,MAAM;AAItC,IAAM,wBAAwB,IAAI,OAAO;AA4CzC,SAAS,uBAAuB,MAAsB;AAC3D,QAAM,OAAO,KAAK,MAAM,OAAO,EAAE,IAAI,KAAK;AAC1C,QAAM,OAAO,KAAK,QAAQ,qBAAqB,GAAG,EAAE,QAAQ,QAAQ,GAAG;AACvE,UAAQ,QAAQ,QAAQ,MAAM,GAAG,GAAG;AACtC;AAEA,IAAM,eAAe;AAEd,SAAS,cAAc,OAA2B;AACvD,MAAI,SAAS;AACb,WAAS,SAAS,GAAG,SAAS,MAAM,QAAQ,UAAU,cAAc;AAClE,cAAU,OAAO,aAAa,GAAG,MAAM,SAAS,QAAQ,SAAS,YAAY,CAAC;AAAA,EAChF;AACA,SAAO,KAAK,MAAM;AACpB;AAEA,SAAS,YAAY,QAAgB,MAAc,OAAyB;AAC1E,SAAO,SAAS,KAAK,EAAE,MAAM,MAAM,GAAG,EAAE,OAAO,CAAC;AAClD;AAEO,SAAS,kBAAkB,SAA4E;AAC5G,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,eAAe,QAAQ,gBAAgB;AAE7C,SAAO,eAAe,OAAO,SAAqC;AAChE,UAAM,OAAO,MAAM,QAAQ,UAAU,EAAE,QAAQ,CAAC;AAChD,QAAI,CAAC,KAAK,GAAI,QAAO,KAAK;AAC1B,UAAM,OAAO,KAAK,QAAQ;AAC1B,UAAM,aAAa,KAAK,aAAa,QAAQ,aAAa,WAAW,QAAQ,QAAQ,EAAE;AAEvF,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,QAAQ,SAAS;AAAA,IAChC,QAAQ;AACN,aAAO,YAAY,KAAK,kBAAkB,sDAAsD;AAAA,IAClG;AACA,UAAM,QAAgB,CAAC;AACvB,SAAK,QAAQ,CAAC,UAAU;AACtB,UAAI,iBAAiB,KAAM,OAAM,KAAK,KAAK;AAAA,IAC7C,CAAC;AACD,QAAI,MAAM,WAAW,GAAG;AACtB,aAAO,YAAY,KAAK,kBAAkB,6BAA6B;AAAA,IACzE;AAEA,UAAM,WAA+B,CAAC;AACtC,eAAW,QAAQ,OAAO;AACxB,YAAM,OAAO,uBAAuB,KAAK,IAAI;AAC7C,YAAM,YAAY,KAAK,QAAQ;AAC/B,YAAM,WAA0C,UAAU,WAAW,QAAQ,IAAI,UAAU;AAE3F,UAAI,KAAK,OAAO,cAAc;AAC5B,eAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,GAAG,IAAI,OAAO,KAAK,IAAI,eAAe,YAAY;AAAA,QACpD;AAAA,MACF;AAEA,YAAM,KAAK,OAAO,WAAW;AAC7B,UAAI,KAAK,QAAQ,gBAAgB;AAC/B,cAAMA,UAAS,cAAc,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC,CAAC;AACrE,iBAAS,KAAK;AAAA,UACZ;AAAA,UACA;AAAA,UACA,MAAM,KAAK;AAAA,UACX;AAAA,UACA,QAAQ;AAAA,UACR,MAAM;AAAA,YACJ,MAAM;AAAA,YACN,UAAU;AAAA,YACV;AAAA,YACA,KAAK,QAAQ,SAAS,WAAWA,OAAM;AAAA,UACzC;AAAA,QACF,CAAC;AACD;AAAA,MACF;AAEA,UAAI,CAAC,MAAM;AACT,eAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA,GAAG,IAAI,OAAO,KAAK,IAAI,eAAe,cAAc;AAAA,QACtD;AAAA,MACF;AACA,YAAM,OAAO,GAAG,SAAS,IAAI,EAAE,IAAI,IAAI;AACvC,YAAM,SAAS,cAAc,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC,CAAC;AACrE,YAAM,KAAK,MAAM,MAAM,QAAQ,EAAE,UAAU,SAAS,CAAC;AACrD,eAAS,KAAK;AAAA,QACZ;AAAA,QACA;AAAA,QACA,MAAM,KAAK;AAAA,QACX;AAAA,QACA,QAAQ;AAAA,QACR,MAAM,EAAE,MAAM,UAAU,UAAU,MAAM,WAAW,KAAK;AAAA,MAC1D,CAAC;AAAA,IACH;AAEA,WAAO,SAAS,KAAK,EAAE,OAAO,SAAS,CAAC;AAAA,EAC1C;AACF;","names":["base64"]}
|
|
@@ -149,6 +149,13 @@ function getPartKey(part) {
|
|
|
149
149
|
const lane = type && type !== "unknown" ? type : "text";
|
|
150
150
|
return `${lane}:${String(part.id ?? part.partId ?? part.index ?? "current")}`;
|
|
151
151
|
}
|
|
152
|
+
function overlayDefined(base, patch) {
|
|
153
|
+
const out = { ...base };
|
|
154
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
155
|
+
if (value !== void 0) out[key] = value;
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
152
159
|
function mergePersistedPart(existing, incoming, delta) {
|
|
153
160
|
const type = String(incoming.type ?? "");
|
|
154
161
|
if (!existing) {
|
|
@@ -178,14 +185,16 @@ function mergePersistedPart(existing, incoming, delta) {
|
|
|
178
185
|
};
|
|
179
186
|
}
|
|
180
187
|
if (type === "tool" && String(existing.type ?? "") === "tool") {
|
|
188
|
+
const existingState = asRecord(existing.state) ?? {};
|
|
189
|
+
const incomingState = asRecord(incoming.state) ?? {};
|
|
190
|
+
const mergedState = overlayDefined(existingState, incomingState);
|
|
191
|
+
const existingStatus = String(existingState.status ?? "");
|
|
192
|
+
if ((existingStatus === "completed" || existingStatus === "error") && String(incomingState.status ?? "") === "running") {
|
|
193
|
+
mergedState.status = existingStatus;
|
|
194
|
+
}
|
|
181
195
|
return {
|
|
182
|
-
...existing,
|
|
183
|
-
|
|
184
|
-
state: {
|
|
185
|
-
...asRecord(existing.state) ?? {},
|
|
186
|
-
...asRecord(incoming.state) ?? {},
|
|
187
|
-
time: asRecord(incoming.state)?.time ?? asRecord(existing.state)?.time
|
|
188
|
-
}
|
|
196
|
+
...overlayDefined(existing, incoming),
|
|
197
|
+
state: mergedState
|
|
189
198
|
};
|
|
190
199
|
}
|
|
191
200
|
return incoming;
|
|
@@ -515,4 +524,4 @@ export {
|
|
|
515
524
|
createD1TurnEventStore,
|
|
516
525
|
createMemoryTurnEventStore
|
|
517
526
|
};
|
|
518
|
-
//# sourceMappingURL=chunk-
|
|
527
|
+
//# sourceMappingURL=chunk-XYWCGFII.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stream/stream-normalizer.ts","../src/stream/turn-identity.ts","../src/stream/turn-buffer.ts"],"sourcesContent":["export type JsonRecord = Record<string, unknown>\n\nexport interface StreamEvent {\n type: string\n data?: JsonRecord\n}\n\nexport function asRecord(value: unknown): JsonRecord | undefined {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? value as JsonRecord\n : undefined\n}\n\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nexport function resolveToolId(part: JsonRecord): string {\n return String(\n part.id ??\n part.callID ??\n part.callId ??\n part.toolUseId ??\n part.toolCallId ??\n part.tool ??\n part.name ??\n `tool-${Date.now()}`,\n )\n}\n\nexport function resolveToolName(part: JsonRecord): string {\n return String(part.tool ?? part.name ?? 'tool')\n}\n\nexport function normalizeTime(value: unknown): JsonRecord | undefined {\n const record = asRecord(value)\n if (!record) return undefined\n\n const start = Number(record.start ?? record.startedAt ?? record.started_at)\n const end = Number(record.end ?? record.completedAt ?? record.completed_at)\n if (!Number.isFinite(start) && !Number.isFinite(end)) return undefined\n\n return {\n start: Number.isFinite(start) ? start : undefined,\n end: Number.isFinite(end) ? end : undefined,\n }\n}\n\nexport function normalizeToolEvent(event: StreamEvent): StreamEvent {\n if (event.type === 'tool_call' || event.type === 'tool.call') {\n const data = event.data ?? {}\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n input: data.arguments ?? data.input,\n status: 'running',\n },\n },\n }\n }\n\n if (event.type === 'tool_result' || event.type === 'tool.result') {\n const data = event.data ?? {}\n const error = asString(data.error)\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n output: data.output,\n error,\n status: error ? 'error' : 'completed',\n },\n },\n }\n }\n\n return event\n}\n\nexport function normalizePersistedPart(rawPart: JsonRecord): JsonRecord | null {\n const type = String(rawPart.type ?? '')\n\n if (type === 'text') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'text',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n // id: per-segment identity from the harness; absent on legacy parts,\n // which collapse to a single keyed segment. Never invented here.\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'reasoning') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'reasoning',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n time: normalizeTime(rawPart.time),\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'file' || type === 'image') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type,\n ...(id ? { id } : {}),\n ...(asString(rawPart.filename) ? { filename: asString(rawPart.filename) } : {}),\n ...(asString(rawPart.mediaType) ? { mediaType: asString(rawPart.mediaType) } : {}),\n ...(asString(rawPart.url) ? { url: asString(rawPart.url) } : {}),\n ...(asString(rawPart.path) ? { path: asString(rawPart.path) } : {}),\n ...(type === 'file' && asString(rawPart.content) ? { content: asString(rawPart.content) } : {}),\n }\n }\n\n if (type === 'step-start') {\n return { type: 'step-start' }\n }\n\n // The harness's per-step usage receipt. Dropping it here silently loses the\n // turn's token/cost accounting from the persisted transcript.\n if (type === 'step-finish') {\n const tokens = asRecord(rawPart.tokens)\n const cost = Number(rawPart.cost)\n return {\n type: 'step-finish',\n ...(asString(rawPart.reason) ? { reason: asString(rawPart.reason) } : {}),\n ...(tokens ? { tokens } : {}),\n ...(Number.isFinite(cost) ? { cost } : {}),\n }\n }\n\n if (type === 'subtask') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'subtask',\n prompt: asString(rawPart.prompt) ?? '',\n description: asString(rawPart.description) ?? '',\n agent: asString(rawPart.agent) ?? '',\n ...(id ? { id } : {}),\n }\n }\n\n // System-authored persisted parts (`/interactions` codecs) pass through\n // verbatim when a producer routes them here — they are already projections.\n if (type === 'interaction' || type === 'notice') {\n return rawPart\n }\n\n if (type === 'tool') {\n const state = asRecord(rawPart.state)\n const output = state?.output ?? rawPart.output\n const error = asString(state?.error ?? rawPart.error)\n const terminalError =\n state?.status === 'error' ||\n state?.status === 'failed' ||\n rawPart.status === 'error' ||\n rawPart.status === 'failed' ||\n Boolean(error)\n const status =\n state?.status === 'completed' || rawPart.status === 'completed'\n ? 'completed'\n : terminalError\n ? 'error'\n : output !== undefined\n ? 'completed'\n : 'running'\n\n return {\n type: 'tool',\n id: resolveToolId(rawPart),\n tool: resolveToolName(rawPart),\n callID:\n rawPart.callID != null || rawPart.callId != null\n ? String(rawPart.callID ?? rawPart.callId)\n : undefined,\n state: {\n status,\n input: state?.input ?? rawPart.input,\n output,\n error,\n metadata: asRecord(state?.metadata) ?? asRecord(rawPart.metadata),\n time: normalizeTime(state?.time ?? rawPart.time),\n },\n }\n }\n\n return null\n}\n\nexport function getPartKey(part: JsonRecord): string {\n const type = String(part.type ?? 'unknown')\n if (type === 'tool') {\n return `tool:${resolveToolId(part)}`\n }\n\n // Keyed by the part's OWN type so distinct kinds never merge into each\n // other. Untyped parts fall back to the text lane (legacy shape).\n const lane = type && type !== 'unknown' ? type : 'text'\n return `${lane}:${String(part.id ?? part.partId ?? part.index ?? 'current')}`\n}\n\n/** Shallow overlay that skips `undefined` incoming values, so a later partial\n * update never erases a field an earlier one captured. */\nfunction overlayDefined(base: JsonRecord, patch: JsonRecord): JsonRecord {\n const out: JsonRecord = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\nexport function mergePersistedPart(existing: JsonRecord | undefined, incoming: JsonRecord, delta?: string): JsonRecord {\n const type = String(incoming.type ?? '')\n if (!existing) {\n if (type === 'text' && delta) {\n return { type: 'text', text: delta }\n }\n return incoming\n }\n\n if (type === 'text' && String(existing.type ?? '') === 'text') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n // An empty snapshot never erases accumulated text (matches reasoning).\n text: delta ? `${existingText}${delta}` : incomingText || existingText,\n }\n }\n\n if (type === 'reasoning' && String(existing.type ?? '') === 'reasoning') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n text: delta && incomingText === existingText ? `${existingText}${delta}` : incomingText || existingText,\n time: incoming.time ?? existing.time,\n }\n }\n\n if (type === 'tool' && String(existing.type ?? '') === 'tool') {\n const existingState = asRecord(existing.state) ?? {}\n const incomingState = asRecord(incoming.state) ?? {}\n // Overlay only DEFINED incoming fields: a normalized tool part always\n // carries `output`/`error` keys (undefined when not captured), so a plain\n // spread would clobber a completed tool's output with a later empty update.\n const mergedState = overlayDefined(existingState, incomingState)\n // A partial update with no captured status/output/error normalizes to\n // `running`; never let it downgrade a tool that already settled.\n const existingStatus = String(existingState.status ?? '')\n if (\n (existingStatus === 'completed' || existingStatus === 'error') &&\n String(incomingState.status ?? '') === 'running'\n ) {\n mergedState.status = existingStatus\n }\n return {\n ...overlayDefined(existing, incoming),\n state: mergedState,\n }\n }\n\n return incoming\n}\n\nexport function finalizeAssistantParts(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n const parts = partOrder\n .map((key) => partMap.get(key))\n .filter((part): part is JsonRecord => Boolean(part))\n\n const textParts = parts.filter((part) => String(part.type ?? '') === 'text')\n\n if (textParts.length === 0) {\n if (finalText.trim()) {\n parts.push({ type: 'text', text: finalText })\n }\n return parts\n }\n\n // Id-less text parts form a single logical stream — the final text is\n // authoritative for it.\n if (!textParts.some((part) => asString(part.id))) {\n return parts.map((part) => {\n if (String(part.type ?? '') !== 'text') return part\n return {\n ...part,\n text: finalText || String(part.text ?? ''),\n }\n })\n }\n\n // Per-id text segments: invariant is concat(text parts) === persisted final\n // text, so segment boundaries survive without duplicating the answer into\n // every segment.\n const joined = textParts.map((part) => String(part.text ?? '')).join('')\n if (finalText === joined || finalText.trimEnd() === joined.trimEnd()) {\n return parts\n }\n\n if (finalText.startsWith(joined)) {\n // Final text extends the streamed segments (e.g. a failure diagnostic\n // appended after the stream) — persist the remainder as a trailing\n // id-less segment.\n return [...parts, { type: 'text', text: finalText.slice(joined.length) }]\n }\n\n // Final text replaced the streamed text outright. Keep non-text chronology;\n // collapse text to one authoritative segment at the last text position.\n const lastTextPart = textParts[textParts.length - 1]\n return parts\n .filter((part) => String(part.type ?? '') !== 'text' || part === lastTextPart)\n .map((part) => (part === lastTextPart ? { ...part, text: finalText } : part))\n}\n\nexport function encodeEvent(encoder: TextEncoder, event: StreamEvent): Uint8Array {\n return encoder.encode(`${JSON.stringify(event)}\\n`)\n}\n","import type { JsonRecord } from './stream-normalizer'\n\nexport interface PersistedChatMessageForTurn {\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts: Array<Record<string, unknown>> | null\n}\n\nexport interface ResolvedChatTurn {\n turnIndex: number\n shouldInsertUserMessage: boolean\n priorMessages: PersistedChatMessageForTurn[]\n userParts: JsonRecord[]\n}\n\nexport function normalizeClientTurnId(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value !== 'string') throw new Error('turnId must be a string')\n const trimmed = value.trim()\n if (!trimmed) throw new Error('turnId must not be blank')\n if (trimmed.length > 160) throw new Error('turnId is too long')\n if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {\n throw new Error('turnId contains unsupported characters')\n }\n return trimmed\n}\n\nexport function buildUserTextParts(text: string, turnId: string | undefined): JsonRecord[] {\n const part: JsonRecord = { type: 'text', text }\n if (turnId) part.turnId = turnId\n return [part]\n}\n\nexport function messageHasTurnId(message: PersistedChatMessageForTurn, turnId: string): boolean {\n for (const part of message.parts ?? []) {\n if (part && typeof part === 'object' && String(part.turnId ?? '') === turnId) {\n return true\n }\n }\n return false\n}\n\nexport function resolveChatTurn(input: {\n existingMessages: PersistedChatMessageForTurn[]\n userContent: string\n turnId?: string\n}): ResolvedChatTurn {\n const { existingMessages, userContent, turnId } = input\n const reusableIndex = findReusableUserMessageIndex(existingMessages, userContent, turnId)\n if (reusableIndex >= 0) {\n return {\n turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),\n shouldInsertUserMessage: false,\n priorMessages: existingMessages.slice(0, reusableIndex),\n userParts: buildUserTextParts(userContent, turnId),\n }\n }\n\n return {\n turnIndex: countUserMessages(existingMessages),\n shouldInsertUserMessage: true,\n priorMessages: existingMessages,\n userParts: buildUserTextParts(userContent, turnId),\n }\n}\n\nfunction findReusableUserMessageIndex(\n messages: PersistedChatMessageForTurn[],\n userContent: string,\n turnId: string | undefined,\n): number {\n if (turnId) {\n for (let index = messages.length - 1; index >= 0; index -= 1) {\n const message = messages[index]\n if (message?.role === 'user' && messageHasTurnId(message, turnId)) return index\n }\n }\n\n const latest = messages.at(-1)\n if (latest?.role === 'user' && latest.content === userContent) {\n return messages.length - 1\n }\n\n return -1\n}\n\nfunction countUserMessages(messages: PersistedChatMessageForTurn[]): number {\n return messages.filter((message) => message.role === 'user').length\n}\n","/**\n * Resumable chat turns — the router-path answer to \"streams resume on\n * disconnect\" (issue #27). A turn's loop events are teed into a store as they\n * stream; the turn keeps running under `ctx.waitUntil` when the client drops;\n * a reconnecting client replays the buffered tail by sequence number and\n * keeps following until the turn completes.\n *\n * POST /chat/stream → pumpBufferedTurn(...) + live NDJSON\n * GET /chat/stream/:turnId → replayTurnEvents({ fromSeq }) → NDJSON\n *\n * Storage is a structural seam ({@link TurnEventStore}); a D1 implementation\n * ships here because that's what Cloudflare products have (KV is unsuitable:\n * eventually consistent cross-isolate). Per-token deltas would mean hundreds\n * of rows per turn, so consecutive text/reasoning deltas are coalesced within\n * a flush window before they are persisted — replay yields slightly chunkier\n * deltas with identical concatenation.\n */\n\nexport type TurnStatus = 'running' | 'complete' | 'error'\n\nexport interface BufferedTurnEvent {\n seq: number\n /** The serialized event line (JSON string, no trailing newline). */\n event: string\n}\n\nexport interface TurnEventStore {\n append(turnId: string, events: BufferedTurnEvent[]): Promise<void>\n read(turnId: string, fromSeq: number): Promise<BufferedTurnEvent[]>\n /** Record turn lifecycle. `scopeId` (a thread/session id) is optional and lets\n * {@link TurnEventStore.listRunning} rediscover this turn after a client reload\n * loses the turnId; stores that don't track scope ignore it. */\n setStatus(turnId: string, status: TurnStatus, scopeId?: string): Promise<void>\n getStatus(turnId: string): Promise<TurnStatus | null>\n /** Running turnIds for a scope, newest first — so a reloaded client (clientRunId\n * lost) can find and resume the in-flight turn. Optional: a store records it\n * only if `setStatus` was given a `scopeId`. */\n listRunning?(scopeId: string): Promise<string[]>\n}\n\n// ── coalescing ────────────────────────────────────────────────────────────\n\ntype AnyRecord = Record<string, unknown>\n\nfunction deltaTypeOf(ev: unknown): 'text' | 'reasoning' | null {\n const e = ev as AnyRecord | null\n if (!e || typeof e !== 'object') return null\n const inner = (e.kind === 'event' ? (e.event as AnyRecord | undefined) : e) as AnyRecord | undefined\n if (!inner || typeof inner !== 'object') return null\n if ((inner.type === 'text' || inner.type === 'reasoning') && typeof inner.text === 'string') {\n return inner.type\n }\n return null\n}\n\n/** Merge consecutive text/reasoning deltas of the same type into one event.\n * Concatenation-preserving: replaying the coalesced stream produces the same\n * accumulated text as the original. */\nexport function coalesceDeltas(events: unknown[]): unknown[] {\n const out: unknown[] = []\n for (const ev of events) {\n const type = deltaTypeOf(ev)\n const prev = out[out.length - 1]\n if (type && prev && deltaTypeOf(prev) === type) {\n const read = (x: unknown): AnyRecord =>\n ((x as AnyRecord).kind === 'event' ? (x as AnyRecord).event : x) as AnyRecord\n const merged = JSON.parse(JSON.stringify(prev)) as AnyRecord\n read(merged).text = String(read(prev).text) + String(read(ev).text)\n out[out.length - 1] = merged\n continue\n }\n out.push(ev)\n }\n return out\n}\n\nfunction asPartUpdate(ev: unknown): { partId: unknown; delta: unknown } | null {\n const e = ev as AnyRecord | null\n if (!e || typeof e !== 'object' || e.type !== 'message.part.updated') return null\n const data = e.data as AnyRecord | undefined\n if (!data || typeof data !== 'object') return null\n const part = data.part as AnyRecord | undefined\n const partId = part?.id ?? data.partId ?? part?.partId ?? null\n return { partId, delta: data.delta }\n}\n\n/**\n * Coalesce consecutive `message.part.updated` deltas for the SAME part into one\n * event. agent-runtime products stream `ChatStreamEvent` NDJSON\n * (`{type:'message.part.updated', data:{part, delta}}`); pumped through the\n * buffer with the default tool-loop coalescer, every per-token delta persists as\n * its own row because that coalescer never recognizes the shape. Pass this as\n * {@link PumpBufferedTurnOptions.coalesce} instead.\n *\n * Concatenation-preserving for BOTH consumer styles: the merged event keeps the\n * LATEST event's `data.part` (already the cumulative accumulation) and sets\n * `data.delta` to the concatenation of the merged deltas, so a client that\n * appends `delta` and one that reads the cumulative `part` both reconstruct the\n * identical final text.\n */\nexport function coalesceChatStreamEvents(events: unknown[]): unknown[] {\n const out: unknown[] = []\n for (const ev of events) {\n const cur = asPartUpdate(ev)\n const prevEv = out[out.length - 1]\n const prev = prevEv ? asPartUpdate(prevEv) : null\n if (cur && prev && cur.partId != null && cur.partId === prev.partId) {\n // Base the merged row on the latest event (its `part` is the most complete\n // accumulation); carry forward the summed delta.\n const merged = JSON.parse(JSON.stringify(ev)) as AnyRecord\n ;(merged.data as AnyRecord).delta = String(prev.delta ?? '') + String(cur.delta ?? '')\n out[out.length - 1] = merged\n continue\n }\n out.push(ev)\n }\n return out\n}\n\n// ── buffering core (the tap) ────────────────────────────────────────────────\n\nexport interface BufferedTurnOptions {\n store: TurnEventStore\n turnId: string\n /** Deliver one serialized line to the live client. Throwing here (client\n * disconnected) does NOT stop buffering — events keep persisting. */\n write?: (line: string) => Promise<void> | void\n /** Flush buffered events to the store at most this often. Default 400ms. */\n flushIntervalMs?: number\n /** Per-flush coalescer. Default {@link coalesceDeltas} (tool-loop text/reasoning\n * deltas). agent-runtime products streaming `ChatStreamEvent` pass\n * {@link coalesceChatStreamEvents} so per-token deltas don't each persist as a\n * row. Must be concatenation-preserving. */\n coalesce?: (events: unknown[]) => unknown[]\n /** Optional scope (thread/session id) recorded with the turn status, so\n * {@link TurnEventStore.listRunning} can find this turn after a reload. */\n scopeId?: string\n}\n\n/** A push-driven buffer for a turn whose producer the caller does NOT own. */\nexport interface BufferedTurnTap {\n /** Buffer one event: persist (coalesced, on the flush window) + best-effort\n * live-deliver. Wire to a push source's per-event hook (e.g. agent-runtime\n * `handleChatTurn`'s `hooks.onEvent`). Marks the turn 'running' on first call. */\n onEvent(raw: unknown): Promise<void>\n /** Settle the turn: final flush + set status. Call after the producer resolves\n * ('complete') or rejects ('error'). 'error' flushes what was produced first. */\n done(status?: Extract<TurnStatus, 'complete' | 'error'>): Promise<void>\n}\n\n/**\n * The buffering core. Sequence-numbers every event, delivers it to `write`\n * (best-effort — a disconnected client never stops buffering), and flushes to\n * the store in coalesced batches. Drives both transports:\n *\n * • {@link pumpBufferedTurn} — when you OWN an `AsyncIterable` producer.\n * • this tap (`onEvent`/`done`) — when the producer owns iteration and only\n * hands you a push callback (agent-runtime `handleChatTurn`'s `hooks.onEvent`\n * + the finished body). Durability stays here in the shell; the engine needs\n * no `TurnEventStore` seam.\n */\nexport function createBufferedTurnTap(opts: BufferedTurnOptions): BufferedTurnTap {\n const flushIntervalMs = opts.flushIntervalMs ?? 400\n const coalesce = opts.coalesce ?? coalesceDeltas\n const startedAt = Date.now()\n let seq = 0\n let clientGone = false\n let pending: unknown[] = []\n let lastFlush = Date.now()\n let started = false\n\n async function flush(): Promise<void> {\n if (pending.length === 0) return\n const batch = coalesce(pending)\n pending = []\n const rows = batch.map((ev) => ({ seq: ++seq, event: JSON.stringify(ev) }))\n await opts.store.append(opts.turnId, rows)\n lastFlush = Date.now()\n }\n\n async function ensureStarted(): Promise<void> {\n if (started) return\n started = true\n await opts.store.setStatus(opts.turnId, 'running', opts.scopeId)\n }\n\n return {\n async onEvent(raw) {\n await ensureStarted()\n // Stamp ms-since-turn-start so any stored turn is replayable AND traceable\n // (see ../trace) from the same buffered rows.\n const ev = raw && typeof raw === 'object' ? { ...(raw as Record<string, unknown>), _t: Date.now() - startedAt } : raw\n pending.push(ev)\n if (!clientGone && opts.write) {\n try {\n // Live delivery carries a provisional ordering hint, not the persisted\n // seq (coalescing changes seq assignment); clients resume with the\n // seqs from replay, or 0 for \"everything\".\n await opts.write(JSON.stringify(ev))\n } catch {\n clientGone = true\n }\n }\n if (Date.now() - lastFlush >= flushIntervalMs) await flush()\n },\n async done(status = 'complete') {\n await ensureStarted()\n if (status === 'error') {\n await flush().catch(() => {})\n await opts.store.setStatus(opts.turnId, 'error', opts.scopeId).catch(() => {})\n return\n }\n await flush()\n await opts.store.setStatus(opts.turnId, 'complete', opts.scopeId)\n },\n }\n}\n\n// ── pump (producer side) ──────────────────────────────────────────────────\n\nexport interface PumpBufferedTurnOptions extends BufferedTurnOptions {\n source: AsyncIterable<unknown>\n}\n\n/**\n * Drive a turn to completion regardless of the live client, when you OWN the\n * producer as an `AsyncIterable`. A thin driver over {@link createBufferedTurnTap}.\n * Returns a promise that resolves when the turn finishes — hand it to\n * `ctx.waitUntil` so a disconnect can't kill the turn. Never rejects on\n * client-write failure; a source error marks the turn 'error' (after flushing\n * what was produced) and rethrows.\n */\nexport async function pumpBufferedTurn(opts: PumpBufferedTurnOptions): Promise<void> {\n const tap = createBufferedTurnTap(opts)\n try {\n for await (const raw of opts.source) await tap.onEvent(raw)\n await tap.done('complete')\n } catch (err) {\n await tap.done('error')\n throw err\n }\n}\n\n// ── replay (consumer side) ────────────────────────────────────────────────\n\nexport interface ReplayTurnEventsOptions {\n store: TurnEventStore\n turnId: string\n /** Replay strictly after this sequence number (0 = from the beginning). */\n fromSeq?: number\n /** Poll cadence while the turn is still running. Default 500ms. */\n pollMs?: number\n /** Give up following a 'running' turn after this long. Default 120s. */\n timeoutMs?: number\n}\n\n/**\n * Yield buffered events after `fromSeq`, then keep polling while the turn is\n * still 'running' until it completes, errors, or times out. Terminates with a\n * final `{seq: -1, event: '{\"type\":\"turn_status\",...}'}` marker so clients\n * know why the replay ended.\n */\nexport async function* replayTurnEvents(opts: ReplayTurnEventsOptions): AsyncGenerator<BufferedTurnEvent> {\n const pollMs = opts.pollMs ?? 500\n const timeoutMs = opts.timeoutMs ?? 120_000\n let cursor = opts.fromSeq ?? 0\n const deadline = Date.now() + timeoutMs\n\n for (;;) {\n const batch = await opts.store.read(opts.turnId, cursor)\n for (const row of batch) {\n cursor = Math.max(cursor, row.seq)\n yield row\n }\n const status = await opts.store.getStatus(opts.turnId)\n if (status !== 'running') {\n yield { seq: -1, event: JSON.stringify({ type: 'turn_status', status: status ?? 'unknown' }) }\n return\n }\n if (Date.now() >= deadline) {\n yield { seq: -1, event: JSON.stringify({ type: 'turn_status', status: 'timeout' }) }\n return\n }\n await new Promise((r) => setTimeout(r, pollMs))\n }\n}\n\n// ── D1 store ──────────────────────────────────────────────────────────────\n\n/** Minimal structural D1 contract (Cloudflare `D1Database` satisfies it). */\nexport interface D1LikeForTurns {\n prepare(sql: string): {\n bind(...values: unknown[]): {\n run(): Promise<unknown>\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>\n first<T = Record<string, unknown>>(): Promise<T | null>\n }\n }\n}\n\n/** Schema for the D1 store — append to the product's migrations. */\nexport const TURN_EVENTS_MIGRATION_SQL = `\nCREATE TABLE IF NOT EXISTS turn_events (\n turnId TEXT NOT NULL,\n seq INTEGER NOT NULL,\n event TEXT NOT NULL,\n PRIMARY KEY (turnId, seq)\n);\nCREATE TABLE IF NOT EXISTS turn_status (\n turnId TEXT PRIMARY KEY,\n status TEXT NOT NULL,\n scopeId TEXT,\n updatedAt TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_turn_status_scope ON turn_status (scopeId, status);\n`\n\n/** For deployments whose `turn_status` table predates `scopeId`/`listRunning` —\n * run once to add the column (the CREATE above already includes it for new\n * deployments). SQLite ignores a duplicate-add error if already applied. */\nexport const TURN_STATUS_SCOPE_MIGRATION_SQL = `ALTER TABLE turn_status ADD COLUMN scopeId TEXT;`\n\nexport function createD1TurnEventStore(db: D1LikeForTurns): TurnEventStore {\n return {\n async append(turnId, events) {\n if (!events.length) return\n // One multi-row insert per flush window keeps write volume bounded.\n const placeholders = events.map(() => '(?, ?, ?)').join(', ')\n const values = events.flatMap((e) => [turnId, e.seq, e.event])\n await db.prepare(`INSERT OR IGNORE INTO turn_events (turnId, seq, event) VALUES ${placeholders}`).bind(...values).run()\n },\n async read(turnId, fromSeq) {\n const { results } = await db\n .prepare('SELECT seq, event FROM turn_events WHERE turnId = ? AND seq > ? ORDER BY seq ASC')\n .bind(turnId, fromSeq)\n .all<{ seq: number; event: string }>()\n return results\n },\n async setStatus(turnId, status, scopeId) {\n // COALESCE preserves a scopeId set on the initial 'running' write when a\n // later 'complete'/'error' write passes none.\n await db\n .prepare(\n 'INSERT INTO turn_status (turnId, status, scopeId, updatedAt) VALUES (?, ?, ?, ?) ON CONFLICT(turnId) DO UPDATE SET status = excluded.status, scopeId = COALESCE(excluded.scopeId, turn_status.scopeId), updatedAt = excluded.updatedAt',\n )\n .bind(turnId, status, scopeId ?? null, new Date().toISOString())\n .run()\n },\n async getStatus(turnId) {\n const row = await db.prepare('SELECT status FROM turn_status WHERE turnId = ?').bind(turnId).first<{ status: TurnStatus }>()\n return row?.status ?? null\n },\n async listRunning(scopeId) {\n const { results } = await db\n .prepare(\"SELECT turnId FROM turn_status WHERE scopeId = ? AND status = 'running' ORDER BY updatedAt DESC\")\n .bind(scopeId)\n .all<{ turnId: string }>()\n return results.map((r) => r.turnId)\n },\n }\n}\n\n/** In-memory store for tests and keyless local dev. */\nexport function createMemoryTurnEventStore(): TurnEventStore {\n const events = new Map<string, BufferedTurnEvent[]>()\n const status = new Map<string, TurnStatus>()\n const scopes = new Map<string, string>()\n const order: string[] = []\n return {\n async append(turnId, rows) {\n const list = events.get(turnId) ?? []\n list.push(...rows)\n events.set(turnId, list)\n },\n async read(turnId, fromSeq) {\n return (events.get(turnId) ?? []).filter((e) => e.seq > fromSeq)\n },\n async setStatus(turnId, s, scopeId) {\n status.set(turnId, s)\n if (scopeId) scopes.set(turnId, scopeId)\n if (!order.includes(turnId)) order.push(turnId)\n },\n async getStatus(turnId) {\n return status.get(turnId) ?? null\n },\n async listRunning(scopeId) {\n // Newest first, mirroring the D1 store's `ORDER BY updatedAt DESC`.\n return [...order].reverse().filter((t) => status.get(t) === 'running' && scopes.get(t) === scopeId)\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
package/dist/stream/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.31",
|
|
4
4
|
"packageManager": "pnpm@10.33.4",
|
|
5
5
|
"description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
|
|
6
6
|
"keywords": [
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/stream/stream-normalizer.ts","../src/stream/turn-identity.ts","../src/stream/turn-buffer.ts"],"sourcesContent":["export type JsonRecord = Record<string, unknown>\n\nexport interface StreamEvent {\n type: string\n data?: JsonRecord\n}\n\nexport function asRecord(value: unknown): JsonRecord | undefined {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? value as JsonRecord\n : undefined\n}\n\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nexport function resolveToolId(part: JsonRecord): string {\n return String(\n part.id ??\n part.callID ??\n part.callId ??\n part.toolUseId ??\n part.toolCallId ??\n part.tool ??\n part.name ??\n `tool-${Date.now()}`,\n )\n}\n\nexport function resolveToolName(part: JsonRecord): string {\n return String(part.tool ?? part.name ?? 'tool')\n}\n\nexport function normalizeTime(value: unknown): JsonRecord | undefined {\n const record = asRecord(value)\n if (!record) return undefined\n\n const start = Number(record.start ?? record.startedAt ?? record.started_at)\n const end = Number(record.end ?? record.completedAt ?? record.completed_at)\n if (!Number.isFinite(start) && !Number.isFinite(end)) return undefined\n\n return {\n start: Number.isFinite(start) ? start : undefined,\n end: Number.isFinite(end) ? end : undefined,\n }\n}\n\nexport function normalizeToolEvent(event: StreamEvent): StreamEvent {\n if (event.type === 'tool_call' || event.type === 'tool.call') {\n const data = event.data ?? {}\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n input: data.arguments ?? data.input,\n status: 'running',\n },\n },\n }\n }\n\n if (event.type === 'tool_result' || event.type === 'tool.result') {\n const data = event.data ?? {}\n const error = asString(data.error)\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n output: data.output,\n error,\n status: error ? 'error' : 'completed',\n },\n },\n }\n }\n\n return event\n}\n\nexport function normalizePersistedPart(rawPart: JsonRecord): JsonRecord | null {\n const type = String(rawPart.type ?? '')\n\n if (type === 'text') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'text',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n // id: per-segment identity from the harness; absent on legacy parts,\n // which collapse to a single keyed segment. Never invented here.\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'reasoning') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'reasoning',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n time: normalizeTime(rawPart.time),\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'file' || type === 'image') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type,\n ...(id ? { id } : {}),\n ...(asString(rawPart.filename) ? { filename: asString(rawPart.filename) } : {}),\n ...(asString(rawPart.mediaType) ? { mediaType: asString(rawPart.mediaType) } : {}),\n ...(asString(rawPart.url) ? { url: asString(rawPart.url) } : {}),\n ...(asString(rawPart.path) ? { path: asString(rawPart.path) } : {}),\n ...(type === 'file' && asString(rawPart.content) ? { content: asString(rawPart.content) } : {}),\n }\n }\n\n if (type === 'step-start') {\n return { type: 'step-start' }\n }\n\n // The harness's per-step usage receipt. Dropping it here silently loses the\n // turn's token/cost accounting from the persisted transcript.\n if (type === 'step-finish') {\n const tokens = asRecord(rawPart.tokens)\n const cost = Number(rawPart.cost)\n return {\n type: 'step-finish',\n ...(asString(rawPart.reason) ? { reason: asString(rawPart.reason) } : {}),\n ...(tokens ? { tokens } : {}),\n ...(Number.isFinite(cost) ? { cost } : {}),\n }\n }\n\n if (type === 'subtask') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'subtask',\n prompt: asString(rawPart.prompt) ?? '',\n description: asString(rawPart.description) ?? '',\n agent: asString(rawPart.agent) ?? '',\n ...(id ? { id } : {}),\n }\n }\n\n // System-authored persisted parts (`/interactions` codecs) pass through\n // verbatim when a producer routes them here — they are already projections.\n if (type === 'interaction' || type === 'notice') {\n return rawPart\n }\n\n if (type === 'tool') {\n const state = asRecord(rawPart.state)\n const output = state?.output ?? rawPart.output\n const error = asString(state?.error ?? rawPart.error)\n const terminalError =\n state?.status === 'error' ||\n state?.status === 'failed' ||\n rawPart.status === 'error' ||\n rawPart.status === 'failed' ||\n Boolean(error)\n const status =\n state?.status === 'completed' || rawPart.status === 'completed'\n ? 'completed'\n : terminalError\n ? 'error'\n : output !== undefined\n ? 'completed'\n : 'running'\n\n return {\n type: 'tool',\n id: resolveToolId(rawPart),\n tool: resolveToolName(rawPart),\n callID:\n rawPart.callID != null || rawPart.callId != null\n ? String(rawPart.callID ?? rawPart.callId)\n : undefined,\n state: {\n status,\n input: state?.input ?? rawPart.input,\n output,\n error,\n metadata: asRecord(state?.metadata) ?? asRecord(rawPart.metadata),\n time: normalizeTime(state?.time ?? rawPart.time),\n },\n }\n }\n\n return null\n}\n\nexport function getPartKey(part: JsonRecord): string {\n const type = String(part.type ?? 'unknown')\n if (type === 'tool') {\n return `tool:${resolveToolId(part)}`\n }\n\n // Keyed by the part's OWN type so distinct kinds never merge into each\n // other. Untyped parts fall back to the text lane (legacy shape).\n const lane = type && type !== 'unknown' ? type : 'text'\n return `${lane}:${String(part.id ?? part.partId ?? part.index ?? 'current')}`\n}\n\nexport function mergePersistedPart(existing: JsonRecord | undefined, incoming: JsonRecord, delta?: string): JsonRecord {\n const type = String(incoming.type ?? '')\n if (!existing) {\n if (type === 'text' && delta) {\n return { type: 'text', text: delta }\n }\n return incoming\n }\n\n if (type === 'text' && String(existing.type ?? '') === 'text') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n // An empty snapshot never erases accumulated text (matches reasoning).\n text: delta ? `${existingText}${delta}` : incomingText || existingText,\n }\n }\n\n if (type === 'reasoning' && String(existing.type ?? '') === 'reasoning') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n text: delta && incomingText === existingText ? `${existingText}${delta}` : incomingText || existingText,\n time: incoming.time ?? existing.time,\n }\n }\n\n if (type === 'tool' && String(existing.type ?? '') === 'tool') {\n return {\n ...existing,\n ...incoming,\n state: {\n ...(asRecord(existing.state) ?? {}),\n ...(asRecord(incoming.state) ?? {}),\n time: asRecord(incoming.state)?.time ?? asRecord(existing.state)?.time,\n },\n }\n }\n\n return incoming\n}\n\nexport function finalizeAssistantParts(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n const parts = partOrder\n .map((key) => partMap.get(key))\n .filter((part): part is JsonRecord => Boolean(part))\n\n const textParts = parts.filter((part) => String(part.type ?? '') === 'text')\n\n if (textParts.length === 0) {\n if (finalText.trim()) {\n parts.push({ type: 'text', text: finalText })\n }\n return parts\n }\n\n // Id-less text parts form a single logical stream — the final text is\n // authoritative for it.\n if (!textParts.some((part) => asString(part.id))) {\n return parts.map((part) => {\n if (String(part.type ?? '') !== 'text') return part\n return {\n ...part,\n text: finalText || String(part.text ?? ''),\n }\n })\n }\n\n // Per-id text segments: invariant is concat(text parts) === persisted final\n // text, so segment boundaries survive without duplicating the answer into\n // every segment.\n const joined = textParts.map((part) => String(part.text ?? '')).join('')\n if (finalText === joined || finalText.trimEnd() === joined.trimEnd()) {\n return parts\n }\n\n if (finalText.startsWith(joined)) {\n // Final text extends the streamed segments (e.g. a failure diagnostic\n // appended after the stream) — persist the remainder as a trailing\n // id-less segment.\n return [...parts, { type: 'text', text: finalText.slice(joined.length) }]\n }\n\n // Final text replaced the streamed text outright. Keep non-text chronology;\n // collapse text to one authoritative segment at the last text position.\n const lastTextPart = textParts[textParts.length - 1]\n return parts\n .filter((part) => String(part.type ?? '') !== 'text' || part === lastTextPart)\n .map((part) => (part === lastTextPart ? { ...part, text: finalText } : part))\n}\n\nexport function encodeEvent(encoder: TextEncoder, event: StreamEvent): Uint8Array {\n return encoder.encode(`${JSON.stringify(event)}\\n`)\n}\n","import type { JsonRecord } from './stream-normalizer'\n\nexport interface PersistedChatMessageForTurn {\n id: string\n role: 'user' | 'assistant' | 'system' | 'tool'\n content: string\n parts: Array<Record<string, unknown>> | null\n}\n\nexport interface ResolvedChatTurn {\n turnIndex: number\n shouldInsertUserMessage: boolean\n priorMessages: PersistedChatMessageForTurn[]\n userParts: JsonRecord[]\n}\n\nexport function normalizeClientTurnId(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value !== 'string') throw new Error('turnId must be a string')\n const trimmed = value.trim()\n if (!trimmed) throw new Error('turnId must not be blank')\n if (trimmed.length > 160) throw new Error('turnId is too long')\n if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {\n throw new Error('turnId contains unsupported characters')\n }\n return trimmed\n}\n\nexport function buildUserTextParts(text: string, turnId: string | undefined): JsonRecord[] {\n const part: JsonRecord = { type: 'text', text }\n if (turnId) part.turnId = turnId\n return [part]\n}\n\nexport function messageHasTurnId(message: PersistedChatMessageForTurn, turnId: string): boolean {\n for (const part of message.parts ?? []) {\n if (part && typeof part === 'object' && String(part.turnId ?? '') === turnId) {\n return true\n }\n }\n return false\n}\n\nexport function resolveChatTurn(input: {\n existingMessages: PersistedChatMessageForTurn[]\n userContent: string\n turnId?: string\n}): ResolvedChatTurn {\n const { existingMessages, userContent, turnId } = input\n const reusableIndex = findReusableUserMessageIndex(existingMessages, userContent, turnId)\n if (reusableIndex >= 0) {\n return {\n turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),\n shouldInsertUserMessage: false,\n priorMessages: existingMessages.slice(0, reusableIndex),\n userParts: buildUserTextParts(userContent, turnId),\n }\n }\n\n return {\n turnIndex: countUserMessages(existingMessages),\n shouldInsertUserMessage: true,\n priorMessages: existingMessages,\n userParts: buildUserTextParts(userContent, turnId),\n }\n}\n\nfunction findReusableUserMessageIndex(\n messages: PersistedChatMessageForTurn[],\n userContent: string,\n turnId: string | undefined,\n): number {\n if (turnId) {\n for (let index = messages.length - 1; index >= 0; index -= 1) {\n const message = messages[index]\n if (message?.role === 'user' && messageHasTurnId(message, turnId)) return index\n }\n }\n\n const latest = messages.at(-1)\n if (latest?.role === 'user' && latest.content === userContent) {\n return messages.length - 1\n }\n\n return -1\n}\n\nfunction countUserMessages(messages: PersistedChatMessageForTurn[]): number {\n return messages.filter((message) => message.role === 'user').length\n}\n","/**\n * Resumable chat turns — the router-path answer to \"streams resume on\n * disconnect\" (issue #27). A turn's loop events are teed into a store as they\n * stream; the turn keeps running under `ctx.waitUntil` when the client drops;\n * a reconnecting client replays the buffered tail by sequence number and\n * keeps following until the turn completes.\n *\n * POST /chat/stream → pumpBufferedTurn(...) + live NDJSON\n * GET /chat/stream/:turnId → replayTurnEvents({ fromSeq }) → NDJSON\n *\n * Storage is a structural seam ({@link TurnEventStore}); a D1 implementation\n * ships here because that's what Cloudflare products have (KV is unsuitable:\n * eventually consistent cross-isolate). Per-token deltas would mean hundreds\n * of rows per turn, so consecutive text/reasoning deltas are coalesced within\n * a flush window before they are persisted — replay yields slightly chunkier\n * deltas with identical concatenation.\n */\n\nexport type TurnStatus = 'running' | 'complete' | 'error'\n\nexport interface BufferedTurnEvent {\n seq: number\n /** The serialized event line (JSON string, no trailing newline). */\n event: string\n}\n\nexport interface TurnEventStore {\n append(turnId: string, events: BufferedTurnEvent[]): Promise<void>\n read(turnId: string, fromSeq: number): Promise<BufferedTurnEvent[]>\n /** Record turn lifecycle. `scopeId` (a thread/session id) is optional and lets\n * {@link TurnEventStore.listRunning} rediscover this turn after a client reload\n * loses the turnId; stores that don't track scope ignore it. */\n setStatus(turnId: string, status: TurnStatus, scopeId?: string): Promise<void>\n getStatus(turnId: string): Promise<TurnStatus | null>\n /** Running turnIds for a scope, newest first — so a reloaded client (clientRunId\n * lost) can find and resume the in-flight turn. Optional: a store records it\n * only if `setStatus` was given a `scopeId`. */\n listRunning?(scopeId: string): Promise<string[]>\n}\n\n// ── coalescing ────────────────────────────────────────────────────────────\n\ntype AnyRecord = Record<string, unknown>\n\nfunction deltaTypeOf(ev: unknown): 'text' | 'reasoning' | null {\n const e = ev as AnyRecord | null\n if (!e || typeof e !== 'object') return null\n const inner = (e.kind === 'event' ? (e.event as AnyRecord | undefined) : e) as AnyRecord | undefined\n if (!inner || typeof inner !== 'object') return null\n if ((inner.type === 'text' || inner.type === 'reasoning') && typeof inner.text === 'string') {\n return inner.type\n }\n return null\n}\n\n/** Merge consecutive text/reasoning deltas of the same type into one event.\n * Concatenation-preserving: replaying the coalesced stream produces the same\n * accumulated text as the original. */\nexport function coalesceDeltas(events: unknown[]): unknown[] {\n const out: unknown[] = []\n for (const ev of events) {\n const type = deltaTypeOf(ev)\n const prev = out[out.length - 1]\n if (type && prev && deltaTypeOf(prev) === type) {\n const read = (x: unknown): AnyRecord =>\n ((x as AnyRecord).kind === 'event' ? (x as AnyRecord).event : x) as AnyRecord\n const merged = JSON.parse(JSON.stringify(prev)) as AnyRecord\n read(merged).text = String(read(prev).text) + String(read(ev).text)\n out[out.length - 1] = merged\n continue\n }\n out.push(ev)\n }\n return out\n}\n\nfunction asPartUpdate(ev: unknown): { partId: unknown; delta: unknown } | null {\n const e = ev as AnyRecord | null\n if (!e || typeof e !== 'object' || e.type !== 'message.part.updated') return null\n const data = e.data as AnyRecord | undefined\n if (!data || typeof data !== 'object') return null\n const part = data.part as AnyRecord | undefined\n const partId = part?.id ?? data.partId ?? part?.partId ?? null\n return { partId, delta: data.delta }\n}\n\n/**\n * Coalesce consecutive `message.part.updated` deltas for the SAME part into one\n * event. agent-runtime products stream `ChatStreamEvent` NDJSON\n * (`{type:'message.part.updated', data:{part, delta}}`); pumped through the\n * buffer with the default tool-loop coalescer, every per-token delta persists as\n * its own row because that coalescer never recognizes the shape. Pass this as\n * {@link PumpBufferedTurnOptions.coalesce} instead.\n *\n * Concatenation-preserving for BOTH consumer styles: the merged event keeps the\n * LATEST event's `data.part` (already the cumulative accumulation) and sets\n * `data.delta` to the concatenation of the merged deltas, so a client that\n * appends `delta` and one that reads the cumulative `part` both reconstruct the\n * identical final text.\n */\nexport function coalesceChatStreamEvents(events: unknown[]): unknown[] {\n const out: unknown[] = []\n for (const ev of events) {\n const cur = asPartUpdate(ev)\n const prevEv = out[out.length - 1]\n const prev = prevEv ? asPartUpdate(prevEv) : null\n if (cur && prev && cur.partId != null && cur.partId === prev.partId) {\n // Base the merged row on the latest event (its `part` is the most complete\n // accumulation); carry forward the summed delta.\n const merged = JSON.parse(JSON.stringify(ev)) as AnyRecord\n ;(merged.data as AnyRecord).delta = String(prev.delta ?? '') + String(cur.delta ?? '')\n out[out.length - 1] = merged\n continue\n }\n out.push(ev)\n }\n return out\n}\n\n// ── buffering core (the tap) ────────────────────────────────────────────────\n\nexport interface BufferedTurnOptions {\n store: TurnEventStore\n turnId: string\n /** Deliver one serialized line to the live client. Throwing here (client\n * disconnected) does NOT stop buffering — events keep persisting. */\n write?: (line: string) => Promise<void> | void\n /** Flush buffered events to the store at most this often. Default 400ms. */\n flushIntervalMs?: number\n /** Per-flush coalescer. Default {@link coalesceDeltas} (tool-loop text/reasoning\n * deltas). agent-runtime products streaming `ChatStreamEvent` pass\n * {@link coalesceChatStreamEvents} so per-token deltas don't each persist as a\n * row. Must be concatenation-preserving. */\n coalesce?: (events: unknown[]) => unknown[]\n /** Optional scope (thread/session id) recorded with the turn status, so\n * {@link TurnEventStore.listRunning} can find this turn after a reload. */\n scopeId?: string\n}\n\n/** A push-driven buffer for a turn whose producer the caller does NOT own. */\nexport interface BufferedTurnTap {\n /** Buffer one event: persist (coalesced, on the flush window) + best-effort\n * live-deliver. Wire to a push source's per-event hook (e.g. agent-runtime\n * `handleChatTurn`'s `hooks.onEvent`). Marks the turn 'running' on first call. */\n onEvent(raw: unknown): Promise<void>\n /** Settle the turn: final flush + set status. Call after the producer resolves\n * ('complete') or rejects ('error'). 'error' flushes what was produced first. */\n done(status?: Extract<TurnStatus, 'complete' | 'error'>): Promise<void>\n}\n\n/**\n * The buffering core. Sequence-numbers every event, delivers it to `write`\n * (best-effort — a disconnected client never stops buffering), and flushes to\n * the store in coalesced batches. Drives both transports:\n *\n * • {@link pumpBufferedTurn} — when you OWN an `AsyncIterable` producer.\n * • this tap (`onEvent`/`done`) — when the producer owns iteration and only\n * hands you a push callback (agent-runtime `handleChatTurn`'s `hooks.onEvent`\n * + the finished body). Durability stays here in the shell; the engine needs\n * no `TurnEventStore` seam.\n */\nexport function createBufferedTurnTap(opts: BufferedTurnOptions): BufferedTurnTap {\n const flushIntervalMs = opts.flushIntervalMs ?? 400\n const coalesce = opts.coalesce ?? coalesceDeltas\n const startedAt = Date.now()\n let seq = 0\n let clientGone = false\n let pending: unknown[] = []\n let lastFlush = Date.now()\n let started = false\n\n async function flush(): Promise<void> {\n if (pending.length === 0) return\n const batch = coalesce(pending)\n pending = []\n const rows = batch.map((ev) => ({ seq: ++seq, event: JSON.stringify(ev) }))\n await opts.store.append(opts.turnId, rows)\n lastFlush = Date.now()\n }\n\n async function ensureStarted(): Promise<void> {\n if (started) return\n started = true\n await opts.store.setStatus(opts.turnId, 'running', opts.scopeId)\n }\n\n return {\n async onEvent(raw) {\n await ensureStarted()\n // Stamp ms-since-turn-start so any stored turn is replayable AND traceable\n // (see ../trace) from the same buffered rows.\n const ev = raw && typeof raw === 'object' ? { ...(raw as Record<string, unknown>), _t: Date.now() - startedAt } : raw\n pending.push(ev)\n if (!clientGone && opts.write) {\n try {\n // Live delivery carries a provisional ordering hint, not the persisted\n // seq (coalescing changes seq assignment); clients resume with the\n // seqs from replay, or 0 for \"everything\".\n await opts.write(JSON.stringify(ev))\n } catch {\n clientGone = true\n }\n }\n if (Date.now() - lastFlush >= flushIntervalMs) await flush()\n },\n async done(status = 'complete') {\n await ensureStarted()\n if (status === 'error') {\n await flush().catch(() => {})\n await opts.store.setStatus(opts.turnId, 'error', opts.scopeId).catch(() => {})\n return\n }\n await flush()\n await opts.store.setStatus(opts.turnId, 'complete', opts.scopeId)\n },\n }\n}\n\n// ── pump (producer side) ──────────────────────────────────────────────────\n\nexport interface PumpBufferedTurnOptions extends BufferedTurnOptions {\n source: AsyncIterable<unknown>\n}\n\n/**\n * Drive a turn to completion regardless of the live client, when you OWN the\n * producer as an `AsyncIterable`. A thin driver over {@link createBufferedTurnTap}.\n * Returns a promise that resolves when the turn finishes — hand it to\n * `ctx.waitUntil` so a disconnect can't kill the turn. Never rejects on\n * client-write failure; a source error marks the turn 'error' (after flushing\n * what was produced) and rethrows.\n */\nexport async function pumpBufferedTurn(opts: PumpBufferedTurnOptions): Promise<void> {\n const tap = createBufferedTurnTap(opts)\n try {\n for await (const raw of opts.source) await tap.onEvent(raw)\n await tap.done('complete')\n } catch (err) {\n await tap.done('error')\n throw err\n }\n}\n\n// ── replay (consumer side) ────────────────────────────────────────────────\n\nexport interface ReplayTurnEventsOptions {\n store: TurnEventStore\n turnId: string\n /** Replay strictly after this sequence number (0 = from the beginning). */\n fromSeq?: number\n /** Poll cadence while the turn is still running. Default 500ms. */\n pollMs?: number\n /** Give up following a 'running' turn after this long. Default 120s. */\n timeoutMs?: number\n}\n\n/**\n * Yield buffered events after `fromSeq`, then keep polling while the turn is\n * still 'running' until it completes, errors, or times out. Terminates with a\n * final `{seq: -1, event: '{\"type\":\"turn_status\",...}'}` marker so clients\n * know why the replay ended.\n */\nexport async function* replayTurnEvents(opts: ReplayTurnEventsOptions): AsyncGenerator<BufferedTurnEvent> {\n const pollMs = opts.pollMs ?? 500\n const timeoutMs = opts.timeoutMs ?? 120_000\n let cursor = opts.fromSeq ?? 0\n const deadline = Date.now() + timeoutMs\n\n for (;;) {\n const batch = await opts.store.read(opts.turnId, cursor)\n for (const row of batch) {\n cursor = Math.max(cursor, row.seq)\n yield row\n }\n const status = await opts.store.getStatus(opts.turnId)\n if (status !== 'running') {\n yield { seq: -1, event: JSON.stringify({ type: 'turn_status', status: status ?? 'unknown' }) }\n return\n }\n if (Date.now() >= deadline) {\n yield { seq: -1, event: JSON.stringify({ type: 'turn_status', status: 'timeout' }) }\n return\n }\n await new Promise((r) => setTimeout(r, pollMs))\n }\n}\n\n// ── D1 store ──────────────────────────────────────────────────────────────\n\n/** Minimal structural D1 contract (Cloudflare `D1Database` satisfies it). */\nexport interface D1LikeForTurns {\n prepare(sql: string): {\n bind(...values: unknown[]): {\n run(): Promise<unknown>\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>\n first<T = Record<string, unknown>>(): Promise<T | null>\n }\n }\n}\n\n/** Schema for the D1 store — append to the product's migrations. */\nexport const TURN_EVENTS_MIGRATION_SQL = `\nCREATE TABLE IF NOT EXISTS turn_events (\n turnId TEXT NOT NULL,\n seq INTEGER NOT NULL,\n event TEXT NOT NULL,\n PRIMARY KEY (turnId, seq)\n);\nCREATE TABLE IF NOT EXISTS turn_status (\n turnId TEXT PRIMARY KEY,\n status TEXT NOT NULL,\n scopeId TEXT,\n updatedAt TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS idx_turn_status_scope ON turn_status (scopeId, status);\n`\n\n/** For deployments whose `turn_status` table predates `scopeId`/`listRunning` —\n * run once to add the column (the CREATE above already includes it for new\n * deployments). SQLite ignores a duplicate-add error if already applied. */\nexport const TURN_STATUS_SCOPE_MIGRATION_SQL = `ALTER TABLE turn_status ADD COLUMN scopeId TEXT;`\n\nexport function createD1TurnEventStore(db: D1LikeForTurns): TurnEventStore {\n return {\n async append(turnId, events) {\n if (!events.length) return\n // One multi-row insert per flush window keeps write volume bounded.\n const placeholders = events.map(() => '(?, ?, ?)').join(', ')\n const values = events.flatMap((e) => [turnId, e.seq, e.event])\n await db.prepare(`INSERT OR IGNORE INTO turn_events (turnId, seq, event) VALUES ${placeholders}`).bind(...values).run()\n },\n async read(turnId, fromSeq) {\n const { results } = await db\n .prepare('SELECT seq, event FROM turn_events WHERE turnId = ? AND seq > ? ORDER BY seq ASC')\n .bind(turnId, fromSeq)\n .all<{ seq: number; event: string }>()\n return results\n },\n async setStatus(turnId, status, scopeId) {\n // COALESCE preserves a scopeId set on the initial 'running' write when a\n // later 'complete'/'error' write passes none.\n await db\n .prepare(\n 'INSERT INTO turn_status (turnId, status, scopeId, updatedAt) VALUES (?, ?, ?, ?) ON CONFLICT(turnId) DO UPDATE SET status = excluded.status, scopeId = COALESCE(excluded.scopeId, turn_status.scopeId), updatedAt = excluded.updatedAt',\n )\n .bind(turnId, status, scopeId ?? null, new Date().toISOString())\n .run()\n },\n async getStatus(turnId) {\n const row = await db.prepare('SELECT status FROM turn_status WHERE turnId = ?').bind(turnId).first<{ status: TurnStatus }>()\n return row?.status ?? null\n },\n async listRunning(scopeId) {\n const { results } = await db\n .prepare(\"SELECT turnId FROM turn_status WHERE scopeId = ? AND status = 'running' ORDER BY updatedAt DESC\")\n .bind(scopeId)\n .all<{ turnId: string }>()\n return results.map((r) => r.turnId)\n },\n }\n}\n\n/** In-memory store for tests and keyless local dev. */\nexport function createMemoryTurnEventStore(): TurnEventStore {\n const events = new Map<string, BufferedTurnEvent[]>()\n const status = new Map<string, TurnStatus>()\n const scopes = new Map<string, string>()\n const order: string[] = []\n return {\n async append(turnId, rows) {\n const list = events.get(turnId) ?? []\n list.push(...rows)\n events.set(turnId, list)\n },\n async read(turnId, fromSeq) {\n return (events.get(turnId) ?? []).filter((e) => e.seq > fromSeq)\n },\n async setStatus(turnId, s, scopeId) {\n status.set(turnId, s)\n if (scopeId) scopes.set(turnId, scopeId)\n if (!order.includes(turnId)) order.push(turnId)\n },\n async getStatus(turnId) {\n return status.get(turnId) ?? null\n },\n async listRunning(scopeId) {\n // Newest first, mirroring the D1 store's `ORDER BY updatedAt DESC`.\n return [...order].reverse().filter((t) => status.get(t) === 'running' && scopes.get(t) === scopeId)\n },\n }\n}\n"],"mappings":"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","names":[]}
|