@czap/astro 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/Satellite.d.ts +42 -0
- package/dist/Satellite.d.ts.map +1 -0
- package/dist/Satellite.js +55 -0
- package/dist/Satellite.js.map +1 -0
- package/dist/client-directives/gpu.d.ts +3 -0
- package/dist/client-directives/gpu.d.ts.map +1 -0
- package/dist/client-directives/gpu.js +5 -0
- package/dist/client-directives/gpu.js.map +1 -0
- package/dist/client-directives/llm.d.ts +3 -0
- package/dist/client-directives/llm.d.ts.map +1 -0
- package/dist/client-directives/llm.js +5 -0
- package/dist/client-directives/llm.js.map +1 -0
- package/dist/client-directives/satellite.d.ts +3 -0
- package/dist/client-directives/satellite.d.ts.map +1 -0
- package/dist/client-directives/satellite.js +5 -0
- package/dist/client-directives/satellite.js.map +1 -0
- package/dist/client-directives/stream.d.ts +3 -0
- package/dist/client-directives/stream.d.ts.map +1 -0
- package/dist/client-directives/stream.js +5 -0
- package/dist/client-directives/stream.js.map +1 -0
- package/dist/client-directives/wasm.d.ts +3 -0
- package/dist/client-directives/wasm.d.ts.map +1 -0
- package/dist/client-directives/wasm.js +6 -0
- package/dist/client-directives/wasm.js.map +1 -0
- package/dist/client-directives/worker.d.ts +3 -0
- package/dist/client-directives/worker.d.ts.map +1 -0
- package/dist/client-directives/worker.js +5 -0
- package/dist/client-directives/worker.js.map +1 -0
- package/dist/detect-upgrade.d.ts +16 -0
- package/dist/detect-upgrade.d.ts.map +1 -0
- package/dist/detect-upgrade.js +105 -0
- package/dist/detect-upgrade.js.map +1 -0
- package/dist/headers.d.ts +45 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.js +64 -0
- package/dist/headers.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.d.ts +76 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +240 -0
- package/dist/integration.js.map +1 -0
- package/dist/middleware.d.ts +69 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +75 -0
- package/dist/middleware.js.map +1 -0
- package/dist/quantize.d.ts +50 -0
- package/dist/quantize.d.ts.map +1 -0
- package/dist/quantize.js +122 -0
- package/dist/quantize.js.map +1 -0
- package/dist/runtime/boundary.d.ts +123 -0
- package/dist/runtime/boundary.d.ts.map +1 -0
- package/dist/runtime/boundary.js +164 -0
- package/dist/runtime/boundary.js.map +1 -0
- package/dist/runtime/globals.d.ts +32 -0
- package/dist/runtime/globals.d.ts.map +1 -0
- package/dist/runtime/globals.js +45 -0
- package/dist/runtime/globals.js.map +1 -0
- package/dist/runtime/gpu.d.ts +15 -0
- package/dist/runtime/gpu.d.ts.map +1 -0
- package/dist/runtime/gpu.js +266 -0
- package/dist/runtime/gpu.js.map +1 -0
- package/dist/runtime/index.d.ts +7 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/llm-receipt-tracker.d.ts +21 -0
- package/dist/runtime/llm-receipt-tracker.d.ts.map +1 -0
- package/dist/runtime/llm-receipt-tracker.js +60 -0
- package/dist/runtime/llm-receipt-tracker.js.map +1 -0
- package/dist/runtime/llm-render-pipeline.d.ts +89 -0
- package/dist/runtime/llm-render-pipeline.d.ts.map +1 -0
- package/dist/runtime/llm-render-pipeline.js +241 -0
- package/dist/runtime/llm-render-pipeline.js.map +1 -0
- package/dist/runtime/llm-session.d.ts +126 -0
- package/dist/runtime/llm-session.d.ts.map +1 -0
- package/dist/runtime/llm-session.js +385 -0
- package/dist/runtime/llm-session.js.map +1 -0
- package/dist/runtime/llm.d.ts +16 -0
- package/dist/runtime/llm.d.ts.map +1 -0
- package/dist/runtime/llm.js +273 -0
- package/dist/runtime/llm.js.map +1 -0
- package/dist/runtime/policy.d.ts +100 -0
- package/dist/runtime/policy.d.ts.map +1 -0
- package/dist/runtime/policy.js +147 -0
- package/dist/runtime/policy.js.map +1 -0
- package/dist/runtime/receipt-chain.d.ts +22 -0
- package/dist/runtime/receipt-chain.d.ts.map +1 -0
- package/dist/runtime/receipt-chain.js +80 -0
- package/dist/runtime/receipt-chain.js.map +1 -0
- package/dist/runtime/runtime-session.d.ts +34 -0
- package/dist/runtime/runtime-session.d.ts.map +1 -0
- package/dist/runtime/runtime-session.js +102 -0
- package/dist/runtime/runtime-session.js.map +1 -0
- package/dist/runtime/satellite.d.ts +13 -0
- package/dist/runtime/satellite.d.ts.map +1 -0
- package/dist/runtime/satellite.js +59 -0
- package/dist/runtime/satellite.js.map +1 -0
- package/dist/runtime/slots.d.ts +34 -0
- package/dist/runtime/slots.d.ts.map +1 -0
- package/dist/runtime/slots.js +108 -0
- package/dist/runtime/slots.js.map +1 -0
- package/dist/runtime/stream-session.d.ts +47 -0
- package/dist/runtime/stream-session.d.ts.map +1 -0
- package/dist/runtime/stream-session.js +82 -0
- package/dist/runtime/stream-session.js.map +1 -0
- package/dist/runtime/stream.d.ts +9 -0
- package/dist/runtime/stream.d.ts.map +1 -0
- package/dist/runtime/stream.js +308 -0
- package/dist/runtime/stream.js.map +1 -0
- package/dist/runtime/url-policy.d.ts +28 -0
- package/dist/runtime/url-policy.d.ts.map +1 -0
- package/dist/runtime/url-policy.js +87 -0
- package/dist/runtime/url-policy.js.map +1 -0
- package/dist/runtime/wasm.d.ts +20 -0
- package/dist/runtime/wasm.d.ts.map +1 -0
- package/dist/runtime/wasm.js +70 -0
- package/dist/runtime/wasm.js.map +1 -0
- package/dist/runtime/worker.d.ts +11 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +249 -0
- package/dist/runtime/worker.js.map +1 -0
- package/package.json +106 -0
- package/src/Satellite.astro +39 -0
- package/src/Satellite.ts +84 -0
- package/src/client-directives/gpu.ts +5 -0
- package/src/client-directives/llm.ts +5 -0
- package/src/client-directives/satellite.ts +5 -0
- package/src/client-directives/stream.ts +5 -0
- package/src/client-directives/wasm.ts +6 -0
- package/src/client-directives/worker.ts +5 -0
- package/src/detect-upgrade.ts +105 -0
- package/src/headers.ts +84 -0
- package/src/index.ts +30 -0
- package/src/integration.ts +309 -0
- package/src/middleware.ts +133 -0
- package/src/quantize.ts +173 -0
- package/src/runtime/boundary.ts +263 -0
- package/src/runtime/globals.ts +57 -0
- package/src/runtime/gpu.ts +291 -0
- package/src/runtime/index.ts +12 -0
- package/src/runtime/llm-receipt-tracker.ts +88 -0
- package/src/runtime/llm-render-pipeline.ts +366 -0
- package/src/runtime/llm-session.ts +548 -0
- package/src/runtime/llm.ts +344 -0
- package/src/runtime/policy.ts +229 -0
- package/src/runtime/receipt-chain.ts +106 -0
- package/src/runtime/runtime-session.ts +139 -0
- package/src/runtime/satellite.ts +80 -0
- package/src/runtime/slots.ts +136 -0
- package/src/runtime/stream-session.ts +125 -0
- package/src/runtime/stream.ts +407 -0
- package/src/runtime/url-policy.ts +107 -0
- package/src/runtime/wasm.ts +85 -0
- package/src/runtime/worker.ts +307 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import type { Receipt, UIFrame } from '@czap/core';
|
|
2
|
+
import {
|
|
3
|
+
LLMChunkNormalization,
|
|
4
|
+
createHtmlFragment,
|
|
5
|
+
type HtmlPolicy,
|
|
6
|
+
type LLMChunk,
|
|
7
|
+
type ToolCallAccumulator,
|
|
8
|
+
} from '@czap/web';
|
|
9
|
+
import { createLLMRenderPipeline, type LLMRenderPipeline } from './llm-render-pipeline.js';
|
|
10
|
+
import { createLLMReceiptTracker, type LLMReceiptTracker } from './llm-receipt-tracker.js';
|
|
11
|
+
import type { RuntimeSessionState } from './runtime-session.js';
|
|
12
|
+
|
|
13
|
+
type DeviceTier = 'none' | 'transitions' | 'animations' | 'physics' | 'compute';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Config accepted by {@link createLLMSession}. Drives a DOM-bound LLM
|
|
17
|
+
* session: `element` is the root the `client:llm` directive attaches
|
|
18
|
+
* to, `target` is where text is appended, and `mode` selects render
|
|
19
|
+
* strategy.
|
|
20
|
+
*/
|
|
21
|
+
export interface LLMSessionConfig {
|
|
22
|
+
/** Host element (directive root). Receives `czap:llm-*` events. */
|
|
23
|
+
readonly element: HTMLElement;
|
|
24
|
+
/** Text-sink element (typically a child of `element`). */
|
|
25
|
+
readonly target: HTMLElement;
|
|
26
|
+
/** Render mode label forwarded to the pipeline. */
|
|
27
|
+
readonly mode: string;
|
|
28
|
+
/** Device-tier getter used by the quality controller. */
|
|
29
|
+
readonly getDeviceTier: () => DeviceTier;
|
|
30
|
+
/** HTML trust policy governing text-sink writes. Defaults to `text`. */
|
|
31
|
+
readonly htmlPolicy?: HtmlPolicy;
|
|
32
|
+
/** Opt-in to `trusted-html` (pairs with `htmlPolicy`). */
|
|
33
|
+
readonly allowTrustedHtml?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Controller surface of an LLM session. Tracks runtime state, ingests
|
|
38
|
+
* chunks from a stream adapter, and releases resources on
|
|
39
|
+
* {@link LLMSessionShape.dispose}.
|
|
40
|
+
*/
|
|
41
|
+
export interface LLMSessionShape {
|
|
42
|
+
/** Current session state (`idle` / `active` / `reconnecting` / `disposed`). */
|
|
43
|
+
readonly state: RuntimeSessionState;
|
|
44
|
+
/** Transition from idle to active. */
|
|
45
|
+
activate(): void;
|
|
46
|
+
/** Enter the reconnecting state so incoming chunks are gated. */
|
|
47
|
+
beginReconnect(): void;
|
|
48
|
+
/** Consume one chunk; returns `done` on stream end. */
|
|
49
|
+
ingest(chunk: LLMChunk): 'continue' | 'done';
|
|
50
|
+
/** Replay receipts after a gap; returns the chosen strategy type. */
|
|
51
|
+
replayGap(): { readonly type: string };
|
|
52
|
+
/** Remember a server-emitted receipt envelope for later replay. */
|
|
53
|
+
rememberEnvelope(envelope: Receipt.Envelope): void;
|
|
54
|
+
/** Reset accumulated state; optionally re-bind the target element. */
|
|
55
|
+
reset(target?: HTMLElement): void;
|
|
56
|
+
/** Terminate the session and release pooled runtimes. */
|
|
57
|
+
dispose(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pluggable callbacks a session uses to render text/frames and emit
|
|
62
|
+
* observability events. Implementations either write to the DOM
|
|
63
|
+
* ({@link createDOMLLMSessionHost}) or funnel data into external
|
|
64
|
+
* observers ({@link createSupportLLMSessionHost}).
|
|
65
|
+
*/
|
|
66
|
+
export interface LLMSessionHost {
|
|
67
|
+
setTarget(target?: HTMLElement): void;
|
|
68
|
+
renderText(text: string, accumulated: string, mode: string): boolean;
|
|
69
|
+
renderFrame(frame: UIFrame, accumulated: string, mode: string): boolean;
|
|
70
|
+
emitToken(text: string, accumulated: string): void;
|
|
71
|
+
emitFrame(frame: UIFrame): void;
|
|
72
|
+
emitToolStart(name: string): void;
|
|
73
|
+
emitToolEnd(name: string, args: unknown): void;
|
|
74
|
+
emitDone(accumulated: string): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface SupportLLMSessionHostHandlers {
|
|
78
|
+
readonly onToken?: (detail: { readonly text: string; readonly accumulated: string }) => void;
|
|
79
|
+
readonly onTokenValue?: (text: string, accumulated: string) => void;
|
|
80
|
+
readonly onFrame?: (frame: UIFrame) => void;
|
|
81
|
+
readonly onToolStart?: (detail: { readonly name: string }) => void;
|
|
82
|
+
readonly onToolStartValue?: (name: string) => void;
|
|
83
|
+
readonly onToolEnd?: (detail: { readonly name: string; readonly args: unknown }) => void;
|
|
84
|
+
readonly onToolEndValue?: (name: string, args: unknown) => void;
|
|
85
|
+
readonly onDone?: (detail: { readonly accumulated: string }) => void;
|
|
86
|
+
readonly onDoneValue?: (accumulated: string) => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const noopSetTarget = (): void => {};
|
|
90
|
+
const noopEmitToken = (): void => {};
|
|
91
|
+
const noopEmitFrame = (): void => {};
|
|
92
|
+
const noopEmitToolStart = (): void => {};
|
|
93
|
+
const noopEmitToolEnd = (): void => {};
|
|
94
|
+
const noopEmitDone = (): void => {};
|
|
95
|
+
const alwaysRenderText = (): boolean => true;
|
|
96
|
+
const renderFrameTokensOnly = (frame: UIFrame): boolean => frame.tokens.length > 0;
|
|
97
|
+
|
|
98
|
+
function appendText(target: HTMLElement, text: string): void {
|
|
99
|
+
if (typeof target.append === 'function') {
|
|
100
|
+
target.append(text);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
target.appendChild(document.createTextNode(text));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function replaceWithText(target: HTMLElement, text: string): void {
|
|
108
|
+
target.textContent = text;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writeHtml(target: HTMLElement, html: string, htmlPolicy: HtmlPolicy, allowTrustedHtml: boolean): void {
|
|
112
|
+
// Route through createHtmlFragment so the assignment goes via the
|
|
113
|
+
// shared trust pipeline's Trusted-Types-aware `assignInnerHTML` helper
|
|
114
|
+
// rather than a raw `target.innerHTML = ...` (which throws under
|
|
115
|
+
// `require-trusted-types-for 'script'` enforcement). See SECURITY.md
|
|
116
|
+
// "CSP and Trusted Types" for the recipe.
|
|
117
|
+
const fragment = createHtmlFragment(html, {
|
|
118
|
+
policy: htmlPolicy,
|
|
119
|
+
allowTrustedHtml,
|
|
120
|
+
});
|
|
121
|
+
target.replaceChildren(fragment);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build an {@link LLMSessionHost} that writes text/frames directly to
|
|
126
|
+
* the DOM and dispatches `czap:llm-*` custom events on `element`.
|
|
127
|
+
* Default host used by {@link createLLMSession}.
|
|
128
|
+
*/
|
|
129
|
+
export function createDOMLLMSessionHost(
|
|
130
|
+
element: HTMLElement,
|
|
131
|
+
initialTarget: HTMLElement,
|
|
132
|
+
options?: { readonly htmlPolicy?: HtmlPolicy; readonly allowTrustedHtml?: boolean },
|
|
133
|
+
): LLMSessionHost {
|
|
134
|
+
let currentTarget = initialTarget;
|
|
135
|
+
const htmlPolicy = options?.htmlPolicy ?? 'text';
|
|
136
|
+
const allowTrustedHtml = options?.allowTrustedHtml ?? false;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
setTarget(target) {
|
|
140
|
+
if (target) {
|
|
141
|
+
currentTarget = target;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
renderText(text, accumulated, mode) {
|
|
146
|
+
if (htmlPolicy !== 'text') {
|
|
147
|
+
writeHtml(currentTarget, accumulated, htmlPolicy, allowTrustedHtml);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (mode === 'append') {
|
|
152
|
+
appendText(currentTarget, text);
|
|
153
|
+
} else {
|
|
154
|
+
replaceWithText(currentTarget, accumulated);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
renderFrame(frame, accumulated, mode) {
|
|
161
|
+
const text = frame.tokens.join('');
|
|
162
|
+
if (!text) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (htmlPolicy !== 'text') {
|
|
167
|
+
writeHtml(currentTarget, accumulated, htmlPolicy, allowTrustedHtml);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (mode === 'append') {
|
|
172
|
+
appendText(currentTarget, text);
|
|
173
|
+
} else {
|
|
174
|
+
replaceWithText(currentTarget, accumulated);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return true;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
emitToken(text, accumulated) {
|
|
181
|
+
element.dispatchEvent(
|
|
182
|
+
new CustomEvent('czap:llm-token', {
|
|
183
|
+
detail: { text, accumulated },
|
|
184
|
+
bubbles: true,
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
emitFrame(frame) {
|
|
190
|
+
element.dispatchEvent(
|
|
191
|
+
new CustomEvent('czap:llm-frame', {
|
|
192
|
+
detail: frame,
|
|
193
|
+
bubbles: true,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
emitToolStart(name) {
|
|
199
|
+
element.dispatchEvent(
|
|
200
|
+
new CustomEvent('czap:llm-tool-start', {
|
|
201
|
+
detail: { name },
|
|
202
|
+
bubbles: true,
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
emitToolEnd(name, args) {
|
|
208
|
+
element.dispatchEvent(
|
|
209
|
+
new CustomEvent('czap:llm-tool-end', {
|
|
210
|
+
detail: { name, args },
|
|
211
|
+
bubbles: true,
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
emitDone(accumulated) {
|
|
217
|
+
element.dispatchEvent(
|
|
218
|
+
new CustomEvent('czap:llm-done', {
|
|
219
|
+
detail: { accumulated },
|
|
220
|
+
bubbles: true,
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Build an {@link LLMSessionHost} that forwards events to the given
|
|
229
|
+
* handler callbacks instead of touching the DOM. Useful for tests,
|
|
230
|
+
* benchmarks, and any caller that wants to observe LLM output
|
|
231
|
+
* programmatically.
|
|
232
|
+
*/
|
|
233
|
+
export function createSupportLLMSessionHost(handlers?: SupportLLMSessionHostHandlers): LLMSessionHost {
|
|
234
|
+
const onToken = handlers?.onToken;
|
|
235
|
+
const onTokenValue = handlers?.onTokenValue;
|
|
236
|
+
const onFrame = handlers?.onFrame;
|
|
237
|
+
const onToolStart = handlers?.onToolStart;
|
|
238
|
+
const onToolStartValue = handlers?.onToolStartValue;
|
|
239
|
+
const onToolEnd = handlers?.onToolEnd;
|
|
240
|
+
const onToolEndValue = handlers?.onToolEndValue;
|
|
241
|
+
const onDone = handlers?.onDone;
|
|
242
|
+
const onDoneValue = handlers?.onDoneValue;
|
|
243
|
+
|
|
244
|
+
// The startup bench only needs the token boundary, so skip the generic
|
|
245
|
+
// support-host composition work when no other callbacks are present.
|
|
246
|
+
if (
|
|
247
|
+
onTokenValue &&
|
|
248
|
+
!onToken &&
|
|
249
|
+
!onFrame &&
|
|
250
|
+
!onToolStart &&
|
|
251
|
+
!onToolStartValue &&
|
|
252
|
+
!onToolEnd &&
|
|
253
|
+
!onToolEndValue &&
|
|
254
|
+
!onDone &&
|
|
255
|
+
!onDoneValue
|
|
256
|
+
) {
|
|
257
|
+
return {
|
|
258
|
+
setTarget: noopSetTarget,
|
|
259
|
+
renderText: alwaysRenderText,
|
|
260
|
+
renderFrame: renderFrameTokensOnly,
|
|
261
|
+
emitToken: onTokenValue,
|
|
262
|
+
emitFrame: noopEmitFrame,
|
|
263
|
+
emitToolStart: noopEmitToolStart,
|
|
264
|
+
emitToolEnd: noopEmitToolEnd,
|
|
265
|
+
emitDone: noopEmitDone,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const emitToken =
|
|
270
|
+
onTokenValue && onToken
|
|
271
|
+
? (text: string, accumulated: string): void => {
|
|
272
|
+
onTokenValue(text, accumulated);
|
|
273
|
+
onToken({ text, accumulated });
|
|
274
|
+
}
|
|
275
|
+
: onTokenValue
|
|
276
|
+
? onTokenValue
|
|
277
|
+
: onToken
|
|
278
|
+
? (text: string, accumulated: string): void => {
|
|
279
|
+
onToken({ text, accumulated });
|
|
280
|
+
}
|
|
281
|
+
: undefined;
|
|
282
|
+
const emitFrame = onFrame ?? noopEmitFrame;
|
|
283
|
+
const emitToolStart =
|
|
284
|
+
onToolStartValue && onToolStart
|
|
285
|
+
? (name: string): void => {
|
|
286
|
+
onToolStartValue(name);
|
|
287
|
+
onToolStart({ name });
|
|
288
|
+
}
|
|
289
|
+
: onToolStartValue
|
|
290
|
+
? onToolStartValue
|
|
291
|
+
: onToolStart
|
|
292
|
+
? (name: string): void => {
|
|
293
|
+
onToolStart({ name });
|
|
294
|
+
}
|
|
295
|
+
: noopEmitToolStart;
|
|
296
|
+
const emitToolEnd =
|
|
297
|
+
onToolEndValue && onToolEnd
|
|
298
|
+
? (name: string, args: unknown): void => {
|
|
299
|
+
onToolEndValue(name, args);
|
|
300
|
+
onToolEnd({ name, args });
|
|
301
|
+
}
|
|
302
|
+
: onToolEndValue
|
|
303
|
+
? onToolEndValue
|
|
304
|
+
: onToolEnd
|
|
305
|
+
? (name: string, args: unknown): void => {
|
|
306
|
+
onToolEnd({ name, args });
|
|
307
|
+
}
|
|
308
|
+
: noopEmitToolEnd;
|
|
309
|
+
const emitDone =
|
|
310
|
+
onDoneValue && onDone
|
|
311
|
+
? (accumulated: string): void => {
|
|
312
|
+
onDoneValue(accumulated);
|
|
313
|
+
onDone({ accumulated });
|
|
314
|
+
}
|
|
315
|
+
: onDoneValue
|
|
316
|
+
? onDoneValue
|
|
317
|
+
: onDone
|
|
318
|
+
? (accumulated: string): void => {
|
|
319
|
+
onDone({ accumulated });
|
|
320
|
+
}
|
|
321
|
+
: noopEmitDone;
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
setTarget: noopSetTarget,
|
|
325
|
+
renderText: alwaysRenderText,
|
|
326
|
+
renderFrame: renderFrameTokensOnly,
|
|
327
|
+
emitToken: emitToken ?? noopEmitToken,
|
|
328
|
+
emitFrame,
|
|
329
|
+
emitToolStart,
|
|
330
|
+
emitToolEnd,
|
|
331
|
+
emitDone,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Minimal {@link LLMSessionHost} that only surfaces the token
|
|
337
|
+
* boundary, skipping the branching composition in
|
|
338
|
+
* {@link createSupportLLMSessionHost}. Exposed for startup benchmarks
|
|
339
|
+
* that need the cheapest possible host.
|
|
340
|
+
*/
|
|
341
|
+
export function createSupportLLMTokenBoundaryHost(
|
|
342
|
+
onTokenValue: (text: string, accumulated: string) => void,
|
|
343
|
+
): LLMSessionHost {
|
|
344
|
+
return {
|
|
345
|
+
setTarget: noopSetTarget,
|
|
346
|
+
renderText: alwaysRenderText,
|
|
347
|
+
renderFrame: alwaysRenderText,
|
|
348
|
+
emitToken: onTokenValue,
|
|
349
|
+
emitFrame: noopEmitFrame,
|
|
350
|
+
emitToolStart: noopEmitToolStart,
|
|
351
|
+
emitToolEnd: noopEmitToolEnd,
|
|
352
|
+
emitDone: noopEmitDone,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
class LLMSessionController implements LLMSessionShape {
|
|
357
|
+
private runtimeState: RuntimeSessionState = 'idle';
|
|
358
|
+
private currentTarget: HTMLElement | undefined;
|
|
359
|
+
private toolCallBuffer: ToolCallAccumulator = null;
|
|
360
|
+
private readonly pipeline: LLMRenderPipeline;
|
|
361
|
+
private readonly receiptTracker: LLMReceiptTracker;
|
|
362
|
+
|
|
363
|
+
constructor(
|
|
364
|
+
private readonly config: Pick<LLMSessionConfig, 'mode' | 'getDeviceTier'> & { readonly target?: HTMLElement },
|
|
365
|
+
private readonly host: LLMSessionHost,
|
|
366
|
+
) {
|
|
367
|
+
this.currentTarget = config.target;
|
|
368
|
+
this.pipeline = createLLMRenderPipeline({ mode: config.mode, getDeviceTier: config.getDeviceTier });
|
|
369
|
+
this.receiptTracker = createLLMReceiptTracker();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
get state(): RuntimeSessionState {
|
|
373
|
+
return this.runtimeState;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
activate(): void {
|
|
377
|
+
if (!this.isDisposed()) {
|
|
378
|
+
this.runtimeState = 'active';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
beginReconnect(): void {
|
|
383
|
+
if (!this.isDisposed()) {
|
|
384
|
+
this.runtimeState = 'reconnecting';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
ingest(chunk: LLMChunk): 'continue' | 'done' {
|
|
389
|
+
if (this.isDisposed()) {
|
|
390
|
+
return 'done';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const recordFrame = (frame: UIFrame): void => this.receiptTracker.recordFrame(frame);
|
|
394
|
+
|
|
395
|
+
switch (chunk.type) {
|
|
396
|
+
case 'text': {
|
|
397
|
+
if (!chunk.content) {
|
|
398
|
+
return 'continue';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (
|
|
402
|
+
this.pipeline.shouldUseFastLane(
|
|
403
|
+
this.toolCallBuffer === null,
|
|
404
|
+
this.receiptTracker.receiptChain === null,
|
|
405
|
+
this.runtimeState,
|
|
406
|
+
) &&
|
|
407
|
+
this.pipeline.renderImmediateText(chunk.content, this.host)
|
|
408
|
+
) {
|
|
409
|
+
this.pipeline.setFastLanePrimed(true);
|
|
410
|
+
this.host.emitToken(chunk.content, this.pipeline.accumulated);
|
|
411
|
+
return 'continue';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.pipeline.pushText(chunk.content);
|
|
415
|
+
if (this.isDisposed()) {
|
|
416
|
+
this.pipeline.clearQueuedText();
|
|
417
|
+
return 'done';
|
|
418
|
+
}
|
|
419
|
+
if (!this.pipeline.canQueueRenderBurst()) {
|
|
420
|
+
this.pipeline.clearQueuedText();
|
|
421
|
+
return 'continue';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (this.pipeline.fastLanePrimed && !this.pipeline.llmRuntime) {
|
|
425
|
+
this.pipeline.promoteFastLane();
|
|
426
|
+
}
|
|
427
|
+
this.pipeline.enqueueFlush(this.host, recordFrame);
|
|
428
|
+
return 'continue';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
case 'tool-call-start': {
|
|
432
|
+
if (this.pipeline.flushQueued || this.pipeline.queuedTextFragments.length > 0) {
|
|
433
|
+
this.pipeline.flushPendingText(this.host, recordFrame);
|
|
434
|
+
}
|
|
435
|
+
const normalized = LLMChunkNormalization.normalize(chunk, this.toolCallBuffer);
|
|
436
|
+
this.toolCallBuffer = normalized.toolCallBuffer;
|
|
437
|
+
this.host.emitToolStart(normalized.chunk?.toolName ?? '');
|
|
438
|
+
return 'continue';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
case 'tool-call-delta': {
|
|
442
|
+
if (this.pipeline.flushQueued || this.pipeline.queuedTextFragments.length > 0) {
|
|
443
|
+
this.pipeline.flushPendingText(this.host, recordFrame);
|
|
444
|
+
}
|
|
445
|
+
const normalized = LLMChunkNormalization.normalize(chunk, this.toolCallBuffer);
|
|
446
|
+
this.toolCallBuffer = normalized.toolCallBuffer;
|
|
447
|
+
return 'continue';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
case 'tool-call-end': {
|
|
451
|
+
if (this.pipeline.flushQueued || this.pipeline.queuedTextFragments.length > 0) {
|
|
452
|
+
this.pipeline.flushPendingText(this.host, recordFrame);
|
|
453
|
+
}
|
|
454
|
+
if (!this.pipeline.llmRuntime) {
|
|
455
|
+
this.pipeline.promoteFastLane();
|
|
456
|
+
}
|
|
457
|
+
const normalized = LLMChunkNormalization.normalize(chunk, this.toolCallBuffer);
|
|
458
|
+
this.toolCallBuffer = normalized.toolCallBuffer;
|
|
459
|
+
const name = normalized.chunk?.toolName ?? '';
|
|
460
|
+
const args = normalized.chunk?.toolArgs;
|
|
461
|
+
this.host.emitToolEnd(name, args);
|
|
462
|
+
return 'continue';
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
case 'done':
|
|
466
|
+
this.pipeline.flushPendingText(this.host, recordFrame);
|
|
467
|
+
this.host.emitDone(this.pipeline.accumulated);
|
|
468
|
+
return 'done';
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
replayGap(): { readonly type: string } {
|
|
473
|
+
return this.receiptTracker.replayGap(this.pipeline, this.host);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
rememberEnvelope(envelope: Receipt.Envelope): void {
|
|
477
|
+
if (this.isDisposed()) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this.receiptTracker.rememberEnvelope(envelope);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
reset(target?: HTMLElement): void {
|
|
485
|
+
if (this.isDisposed()) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Preserve 'reconnecting' state so that chunks arriving between reset() and
|
|
490
|
+
// activate() cannot re-engage the fast lane prematurely. The caller must
|
|
491
|
+
// explicitly call activate() to advance past the reconnecting gate.
|
|
492
|
+
if (this.runtimeState !== 'reconnecting') {
|
|
493
|
+
this.runtimeState = 'idle';
|
|
494
|
+
}
|
|
495
|
+
this.resetSession(target);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
dispose(): void {
|
|
499
|
+
this.resetSession(this.currentTarget);
|
|
500
|
+
this.pipeline.releaseRuntime();
|
|
501
|
+
this.runtimeState = 'disposed';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private isDisposed(): boolean {
|
|
505
|
+
return this.runtimeState === 'disposed';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private resetSession(target = this.currentTarget): void {
|
|
509
|
+
this.currentTarget = target;
|
|
510
|
+
this.host.setTarget(target);
|
|
511
|
+
this.toolCallBuffer = null;
|
|
512
|
+
this.pipeline.resetPipelineState();
|
|
513
|
+
this.receiptTracker.reset();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Build an {@link LLMSessionShape} backed by a caller-supplied
|
|
519
|
+
* {@link LLMSessionHost}. Tests and bench harnesses prefer this
|
|
520
|
+
* variant over {@link createLLMSession} so they can observe output
|
|
521
|
+
* without a DOM.
|
|
522
|
+
*/
|
|
523
|
+
export function createLLMSessionWithHost(
|
|
524
|
+
config: Pick<LLMSessionConfig, 'mode' | 'getDeviceTier'> & { readonly target?: HTMLElement },
|
|
525
|
+
host: LLMSessionHost,
|
|
526
|
+
): LLMSessionShape {
|
|
527
|
+
return new LLMSessionController(config, host);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Default `client:llm` factory: builds a session wired to the DOM.
|
|
532
|
+
* Equivalent to composing {@link createDOMLLMSessionHost} with
|
|
533
|
+
* {@link createLLMSessionWithHost}.
|
|
534
|
+
*/
|
|
535
|
+
export function createLLMSession(config: LLMSessionConfig): LLMSessionShape {
|
|
536
|
+
const domHost = createDOMLLMSessionHost(config.element, config.target, {
|
|
537
|
+
htmlPolicy: config.htmlPolicy,
|
|
538
|
+
allowTrustedHtml: config.allowTrustedHtml,
|
|
539
|
+
});
|
|
540
|
+
return createLLMSessionWithHost(
|
|
541
|
+
{
|
|
542
|
+
mode: config.mode,
|
|
543
|
+
getDeviceTier: config.getDeviceTier,
|
|
544
|
+
target: config.target,
|
|
545
|
+
},
|
|
546
|
+
domHost,
|
|
547
|
+
);
|
|
548
|
+
}
|