@xlaunch/llm 0.2.0-beta.1
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 +22 -0
- package/README.md +174 -0
- package/lib/index.js +2300 -0
- package/lib/invariant.js +84 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +547 -0
- package/lib/typert.remote-client.d.ts +26 -0
- package/lib/typert.remote-client.js +102 -0
- package/lib/types/adapter-failure.d.ts +14 -0
- package/lib/types/adapter-failure.js +105 -0
- package/lib/types/api-key.d.ts +28 -0
- package/lib/types/api-key.js +34 -0
- package/lib/types/assembler.d.ts +75 -0
- package/lib/types/assembler.js +191 -0
- package/lib/types/assistant-stream.d.ts +166 -0
- package/lib/types/assistant-stream.js +458 -0
- package/lib/types/attribution.d.ts +47 -0
- package/lib/types/attribution.js +46 -0
- package/lib/types/brand.d.ts +56 -0
- package/lib/types/brand.js +53 -0
- package/lib/types/call-config.d.ts +53 -0
- package/lib/types/call-config.js +46 -0
- package/lib/types/content.d.ts +130 -0
- package/lib/types/content.js +284 -0
- package/lib/types/error.d.ts +73 -0
- package/lib/types/error.js +145 -0
- package/lib/types/index.d.ts +408 -0
- package/lib/types/index.js +920 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/invariant.js +100 -0
- package/lib/types/message.d.ts +197 -0
- package/lib/types/message.js +82 -0
- package/lib/types/retry-policy.d.ts +66 -0
- package/lib/types/retry-policy.js +127 -0
- package/lib/types/types.d.ts +430 -0
- package/lib/types/types.js +7 -0
- package/package.json +81 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lossless compact representation of one model-stream attempt, plus record-level
|
|
3
|
+
* readers that answer common consumer questions without materializing members.
|
|
4
|
+
* Readers trust the static record type; expandAssistantStream is the validating
|
|
5
|
+
* path for records read at a durable boundary.
|
|
6
|
+
*/
|
|
7
|
+
import { BlockAssembler } from './assembler.ts';
|
|
8
|
+
import type { ToolCallId } from './brand.ts';
|
|
9
|
+
import type { StreamChunk } from './types.ts';
|
|
10
|
+
/** One model chunk paired with its original Session timestamp. */
|
|
11
|
+
export interface TimedStreamChunk {
|
|
12
|
+
readonly time: number;
|
|
13
|
+
readonly chunk: StreamChunk;
|
|
14
|
+
}
|
|
15
|
+
/** Lossless compact records embedded in durable Assistant attempt events. */
|
|
16
|
+
export type AssistantStreamRecord = {
|
|
17
|
+
readonly type: 'text-chunks';
|
|
18
|
+
readonly time0: number;
|
|
19
|
+
readonly index: number;
|
|
20
|
+
readonly dt: readonly number[];
|
|
21
|
+
readonly texts: readonly string[];
|
|
22
|
+
} | {
|
|
23
|
+
readonly type: 'reasoning-chunks';
|
|
24
|
+
readonly time0: number;
|
|
25
|
+
readonly index: number;
|
|
26
|
+
readonly dt: readonly number[];
|
|
27
|
+
readonly texts: readonly string[];
|
|
28
|
+
} | {
|
|
29
|
+
readonly type: 'tool-call-chunks';
|
|
30
|
+
readonly time0: number;
|
|
31
|
+
readonly index: number;
|
|
32
|
+
readonly dt: readonly number[];
|
|
33
|
+
readonly id: ToolCallId;
|
|
34
|
+
readonly name?: string;
|
|
35
|
+
readonly args: readonly string[];
|
|
36
|
+
} | {
|
|
37
|
+
readonly type: 'chunk';
|
|
38
|
+
readonly time: number;
|
|
39
|
+
readonly chunk: StreamChunk;
|
|
40
|
+
};
|
|
41
|
+
/** One packed delta run: every compact record except a raw `chunk`. */
|
|
42
|
+
export type AssistantStreamRun = Exclude<AssistantStreamRecord, {
|
|
43
|
+
type: 'chunk';
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Chunk types the accumulator never packs into runs, so every occurrence is a raw
|
|
47
|
+
* `chunk` record. Delta types are excluded because their packed members are not raw chunks.
|
|
48
|
+
*/
|
|
49
|
+
export type RawStreamChunkType = Exclude<StreamChunk['type'], 'text-delta' | 'reasoning-delta' | 'tool-call-delta'>;
|
|
50
|
+
/** Incrementally compacts one attempt without retaining a second raw-chunk list. */
|
|
51
|
+
export declare class AssistantStreamAccumulator {
|
|
52
|
+
private readonly records;
|
|
53
|
+
/**
|
|
54
|
+
* Add one timed chunk to the compact attempt stream.
|
|
55
|
+
* @param value - model chunk and its original Session timestamp.
|
|
56
|
+
* @returns a detached immutable copy for assembly and live publication.
|
|
57
|
+
*/
|
|
58
|
+
push(value: TimedStreamChunk): TimedStreamChunk;
|
|
59
|
+
/**
|
|
60
|
+
* Return the current compact attempt stream.
|
|
61
|
+
* @returns a detached immutable record list suitable for a durable event.
|
|
62
|
+
*/
|
|
63
|
+
snapshot(): readonly AssistantStreamRecord[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Expand compact records into the exact timed chunk sequence.
|
|
67
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
68
|
+
* @returns detached timed chunks with every original delta boundary preserved.
|
|
69
|
+
* @throws {TypeError} when a record or reconstructed timestamp is invalid.
|
|
70
|
+
*/
|
|
71
|
+
export declare function expandAssistantStream(stream: readonly AssistantStreamRecord[]): readonly TimedStreamChunk[];
|
|
72
|
+
/**
|
|
73
|
+
* Whether one chunk carries the model's first output token for latency measurement.
|
|
74
|
+
* @param chunk - any stream chunk.
|
|
75
|
+
* @returns true for a non-empty text, reasoning, or Tool-call arguments fragment and for
|
|
76
|
+
* every name-bearing Tool-call delta; false for block, usage, and finish chunks.
|
|
77
|
+
*/
|
|
78
|
+
export declare function isTokenDelta(chunk: StreamChunk): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Whether one chunk by itself contributes reader-visible transcript content.
|
|
81
|
+
* Text and reasoning count only with non-whitespace content, streamed as a delta or
|
|
82
|
+
* completed as a block; a block of any other kind counts at its start and its end,
|
|
83
|
+
* except a Tool call, which is protocol rather than content. Usage and finish never count.
|
|
84
|
+
* @param chunk - any stream chunk.
|
|
85
|
+
* @returns whether a transcript reader would see this chunk.
|
|
86
|
+
*/
|
|
87
|
+
export declare function isVisibleChunk(chunk: StreamChunk): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Whether one chunk carries non-whitespace text, as a text delta or a completed text block.
|
|
90
|
+
* Reasoning, Tool calls, and other block kinds never count.
|
|
91
|
+
* @param chunk - any stream chunk.
|
|
92
|
+
* @returns whether the chunk contributes visible text.
|
|
93
|
+
*/
|
|
94
|
+
export declare function chunkHasVisibleText(chunk: StreamChunk): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Time of the first member of one packed run that {@link isTokenDelta} accepts: a
|
|
97
|
+
* name-bearing Tool-call run starts at its first member, otherwise the first non-empty fragment.
|
|
98
|
+
* Stops scanning at that member.
|
|
99
|
+
* @param run - one packed delta run.
|
|
100
|
+
* @returns the member's reconstructed time, or undefined when no member qualifies.
|
|
101
|
+
*/
|
|
102
|
+
export declare function runFirstTokenTime(run: AssistantStreamRun): number | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Time of the first member of one packed run that {@link isVisibleChunk} accepts: the first
|
|
105
|
+
* non-whitespace text or reasoning fragment. A Tool-call run has none. Stops scanning at that member.
|
|
106
|
+
* @param run - one packed delta run.
|
|
107
|
+
* @returns the member's reconstructed time, or undefined when no member qualifies.
|
|
108
|
+
*/
|
|
109
|
+
export declare function runFirstVisibleTime(run: AssistantStreamRun): number | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Time of the first token in one compact stream per {@link isTokenDelta}, read from the
|
|
112
|
+
* records themselves and stopping at the first qualifying member.
|
|
113
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
114
|
+
* @returns the first token's time, or undefined when the stream carries no token.
|
|
115
|
+
*/
|
|
116
|
+
export declare function assistantStreamFirstTokenTime(stream: readonly AssistantStreamRecord[]): number | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* Whether one compact stream carries any reader-visible content per {@link isVisibleChunk},
|
|
119
|
+
* stopping at the first qualifying member.
|
|
120
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
121
|
+
* @returns whether a transcript reader would see anything from this stream.
|
|
122
|
+
*/
|
|
123
|
+
export declare function assistantStreamHasVisibleContent(stream: readonly AssistantStreamRecord[]): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Whether one compact stream carries non-whitespace text per {@link chunkHasVisibleText},
|
|
126
|
+
* stopping at the first qualifying member.
|
|
127
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
128
|
+
* @returns whether the stream contributes visible text.
|
|
129
|
+
*/
|
|
130
|
+
export declare function assistantStreamHasVisibleText(stream: readonly AssistantStreamRecord[]): boolean;
|
|
131
|
+
/**
|
|
132
|
+
* The last raw chunk of one never-packed type, scanning backwards and stopping at the first hit.
|
|
133
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
134
|
+
* @param type - chunk type that only appears as a raw record.
|
|
135
|
+
* @returns the stream's final chunk of that type, or undefined when it has none.
|
|
136
|
+
*/
|
|
137
|
+
export declare function lastAssistantStreamChunk<T extends RawStreamChunkType>(stream: readonly AssistantStreamRecord[], type: T): Extract<StreamChunk, {
|
|
138
|
+
type: T;
|
|
139
|
+
}> | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* Every raw chunk of one never-packed type, in stream order.
|
|
142
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
143
|
+
* @param type - chunk type that only appears as a raw record.
|
|
144
|
+
* @returns the matching chunks; empty when the stream has none.
|
|
145
|
+
*/
|
|
146
|
+
export declare function assistantStreamChunks<T extends RawStreamChunkType>(stream: readonly AssistantStreamRecord[], type: T): readonly Extract<StreamChunk, {
|
|
147
|
+
type: T;
|
|
148
|
+
}>[];
|
|
149
|
+
/**
|
|
150
|
+
* Every streamed text-delta fragment joined in stream order; reasoning and Tool-call fragments are excluded.
|
|
151
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
152
|
+
* @returns the joined text, empty when the stream carries no text delta.
|
|
153
|
+
*/
|
|
154
|
+
export declare function joinAssistantStreamText(stream: readonly AssistantStreamRecord[]): string;
|
|
155
|
+
/**
|
|
156
|
+
* Feed one compact stream into a {@link BlockAssembler} without materializing members.
|
|
157
|
+
* Each run contributes one delta carrying its joined fragments, which assembles the same
|
|
158
|
+
* blocks as the original per-member deltas because assembly only concatenates them;
|
|
159
|
+
* raw chunks are pushed as recorded. The records are trusted, not validated: validate a
|
|
160
|
+
* stream read at a durable boundary with {@link expandAssistantStream} first.
|
|
161
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
162
|
+
* @param assembler - assembler to feed; a fresh one by default.
|
|
163
|
+
* @returns the same assembler after every record was pushed.
|
|
164
|
+
*/
|
|
165
|
+
export declare function assembleAssistantStream(stream: readonly AssistantStreamRecord[], assembler?: BlockAssembler): BlockAssembler;
|
|
166
|
+
//# sourceMappingURL=assistant-stream.d.ts.map
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lossless compact representation of one model-stream attempt, plus record-level
|
|
3
|
+
* readers that answer common consumer questions without materializing members.
|
|
4
|
+
* Readers trust the static record type; expandAssistantStream is the validating
|
|
5
|
+
* path for records read at a durable boundary.
|
|
6
|
+
*/
|
|
7
|
+
import { assertNever, deepFreeze, snapshotJsonValue } from '@xlaunch/util-values';
|
|
8
|
+
import { BlockAssembler } from "./assembler.js";
|
|
9
|
+
function safeTime(value) {
|
|
10
|
+
if (!Number.isSafeInteger(value))
|
|
11
|
+
throw new TypeError(`Assistant stream time must be a safe integer, got ${String(value)}`);
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function safeIndex(value, label) {
|
|
15
|
+
if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) {
|
|
16
|
+
throw new TypeError(`${label} index must be a non-negative safe integer`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function snapshotChunk(chunk) {
|
|
21
|
+
const snapshot = snapshotJsonValue(chunk);
|
|
22
|
+
if (snapshot === undefined)
|
|
23
|
+
throw new TypeError('Assistant stream chunk must be losslessly JSON-serializable');
|
|
24
|
+
return snapshot;
|
|
25
|
+
}
|
|
26
|
+
function safeGap(previous, next) {
|
|
27
|
+
const gap = next - previous;
|
|
28
|
+
return Number.isSafeInteger(gap) && previous + gap === next ? gap : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Incrementally compacts one attempt without retaining a second raw-chunk list. */
|
|
31
|
+
export class AssistantStreamAccumulator {
|
|
32
|
+
records = [];
|
|
33
|
+
/**
|
|
34
|
+
* Add one timed chunk to the compact attempt stream.
|
|
35
|
+
* @param value - model chunk and its original Session timestamp.
|
|
36
|
+
* @returns a detached immutable copy for assembly and live publication.
|
|
37
|
+
*/
|
|
38
|
+
push(value) {
|
|
39
|
+
const time = safeTime(value.time);
|
|
40
|
+
const chunk = snapshotChunk(value.chunk);
|
|
41
|
+
const timed = deepFreeze({ time, chunk });
|
|
42
|
+
const previous = this.records.at(-1);
|
|
43
|
+
switch (chunk.type) {
|
|
44
|
+
case 'text-delta':
|
|
45
|
+
case 'reasoning-delta': {
|
|
46
|
+
safeIndex(chunk.index, chunk.type);
|
|
47
|
+
if (typeof chunk.text !== 'string')
|
|
48
|
+
throw new TypeError(`${chunk.type} text must be a string`);
|
|
49
|
+
const type = chunk.type === 'text-delta' ? 'text-chunks' : 'reasoning-chunks';
|
|
50
|
+
const gap = previous !== undefined && previous.type === type ? safeGap(previous.lastTime, time) : undefined;
|
|
51
|
+
if (previous !== undefined && previous.type === type && previous.index === chunk.index && gap !== undefined) {
|
|
52
|
+
previous.dt.push(gap);
|
|
53
|
+
previous.texts.push(chunk.text);
|
|
54
|
+
previous.lastTime = time;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
this.records.push({ type, time0: time, index: chunk.index, dt: [], texts: [chunk.text], lastTime: time });
|
|
58
|
+
}
|
|
59
|
+
return timed;
|
|
60
|
+
}
|
|
61
|
+
case 'tool-call-delta': {
|
|
62
|
+
safeIndex(chunk.index, chunk.type);
|
|
63
|
+
if (typeof chunk.id !== 'string')
|
|
64
|
+
throw new TypeError('tool-call-delta id must be a string');
|
|
65
|
+
if (Object.hasOwn(chunk, 'name') && typeof chunk.name !== 'string') {
|
|
66
|
+
throw new TypeError('tool-call-delta name must be a string');
|
|
67
|
+
}
|
|
68
|
+
if (typeof chunk.argumentsDelta !== 'string') {
|
|
69
|
+
throw new TypeError('tool-call-delta argumentsDelta must be a string');
|
|
70
|
+
}
|
|
71
|
+
if (chunk.id.length === 0 || chunk.name === '') {
|
|
72
|
+
this.records.push({ type: 'chunk', time, chunk });
|
|
73
|
+
return timed;
|
|
74
|
+
}
|
|
75
|
+
const gap = previous?.type === 'tool-call-chunks' ? safeGap(previous.lastTime, time) : undefined;
|
|
76
|
+
const sameName = previous?.type === 'tool-call-chunks'
|
|
77
|
+
&& Object.hasOwn(previous, 'name') === Object.hasOwn(chunk, 'name')
|
|
78
|
+
&& previous.name === chunk.name;
|
|
79
|
+
if (previous?.type === 'tool-call-chunks'
|
|
80
|
+
&& previous.index === chunk.index
|
|
81
|
+
&& previous.id === chunk.id
|
|
82
|
+
&& sameName
|
|
83
|
+
&& gap !== undefined) {
|
|
84
|
+
previous.dt.push(gap);
|
|
85
|
+
previous.args.push(chunk.argumentsDelta);
|
|
86
|
+
previous.lastTime = time;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.records.push({
|
|
90
|
+
type: 'tool-call-chunks',
|
|
91
|
+
time0: time,
|
|
92
|
+
index: chunk.index,
|
|
93
|
+
dt: [],
|
|
94
|
+
id: chunk.id,
|
|
95
|
+
...Object.hasOwn(chunk, 'name') ? { name: chunk.name } : {},
|
|
96
|
+
args: [chunk.argumentsDelta],
|
|
97
|
+
lastTime: time,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return timed;
|
|
101
|
+
}
|
|
102
|
+
case 'block-start':
|
|
103
|
+
case 'block-end':
|
|
104
|
+
case 'usage':
|
|
105
|
+
case 'finish':
|
|
106
|
+
this.records.push({ type: 'chunk', time, chunk });
|
|
107
|
+
return timed;
|
|
108
|
+
default:
|
|
109
|
+
return assertNever(chunk, 'AssistantStreamAccumulator.push');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Return the current compact attempt stream.
|
|
114
|
+
* @returns a detached immutable record list suitable for a durable event.
|
|
115
|
+
*/
|
|
116
|
+
snapshot() {
|
|
117
|
+
const records = this.records.map((record) => {
|
|
118
|
+
if (record.type === 'chunk')
|
|
119
|
+
return { ...record };
|
|
120
|
+
const { lastTime: _lastTime, ...durable } = record;
|
|
121
|
+
if (durable.type === 'tool-call-chunks') {
|
|
122
|
+
return { ...durable, dt: [...durable.dt], args: [...durable.args] };
|
|
123
|
+
}
|
|
124
|
+
return { ...durable, dt: [...durable.dt], texts: [...durable.texts] };
|
|
125
|
+
});
|
|
126
|
+
return deepFreeze(records);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Expand compact records into the exact timed chunk sequence.
|
|
131
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
132
|
+
* @returns detached timed chunks with every original delta boundary preserved.
|
|
133
|
+
* @throws {TypeError} when a record or reconstructed timestamp is invalid.
|
|
134
|
+
*/
|
|
135
|
+
export function expandAssistantStream(stream) {
|
|
136
|
+
const chunks = [];
|
|
137
|
+
for (const candidate of stream) {
|
|
138
|
+
const record = validateRecord(candidate);
|
|
139
|
+
if (record.type === 'chunk') {
|
|
140
|
+
chunks.push({ time: record.time, chunk: record.chunk });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const members = record.type === 'tool-call-chunks' ? record.args : record.texts;
|
|
144
|
+
let time = record.time0;
|
|
145
|
+
for (let index = 0; index < members.length; index += 1) {
|
|
146
|
+
if (index > 0)
|
|
147
|
+
time += record.dt[index - 1];
|
|
148
|
+
let chunk;
|
|
149
|
+
if (record.type === 'text-chunks') {
|
|
150
|
+
chunk = { type: 'text-delta', index: record.index, text: members[index] };
|
|
151
|
+
}
|
|
152
|
+
else if (record.type === 'reasoning-chunks') {
|
|
153
|
+
chunk = { type: 'reasoning-delta', index: record.index, text: members[index] };
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
chunk = {
|
|
157
|
+
type: 'tool-call-delta',
|
|
158
|
+
index: record.index,
|
|
159
|
+
id: record.id,
|
|
160
|
+
...Object.hasOwn(record, 'name') ? { name: record.name } : {},
|
|
161
|
+
argumentsDelta: members[index],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
chunks.push({ time, chunk });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return chunks;
|
|
168
|
+
}
|
|
169
|
+
function hasNonWhitespace(text) {
|
|
170
|
+
return /\S/.test(text);
|
|
171
|
+
}
|
|
172
|
+
function blockIsVisible(block) {
|
|
173
|
+
if (block.type === 'tool-call')
|
|
174
|
+
return false;
|
|
175
|
+
if (block.type === 'text' || block.type === 'reasoning')
|
|
176
|
+
return hasNonWhitespace(block.text);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Whether one chunk carries the model's first output token for latency measurement.
|
|
181
|
+
* @param chunk - any stream chunk.
|
|
182
|
+
* @returns true for a non-empty text, reasoning, or Tool-call arguments fragment and for
|
|
183
|
+
* every name-bearing Tool-call delta; false for block, usage, and finish chunks.
|
|
184
|
+
*/
|
|
185
|
+
export function isTokenDelta(chunk) {
|
|
186
|
+
switch (chunk.type) {
|
|
187
|
+
case 'text-delta':
|
|
188
|
+
case 'reasoning-delta':
|
|
189
|
+
return chunk.text !== '';
|
|
190
|
+
case 'tool-call-delta':
|
|
191
|
+
return chunk.argumentsDelta !== '' || chunk.name !== undefined;
|
|
192
|
+
default:
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Whether one chunk by itself contributes reader-visible transcript content.
|
|
198
|
+
* Text and reasoning count only with non-whitespace content, streamed as a delta or
|
|
199
|
+
* completed as a block; a block of any other kind counts at its start and its end,
|
|
200
|
+
* except a Tool call, which is protocol rather than content. Usage and finish never count.
|
|
201
|
+
* @param chunk - any stream chunk.
|
|
202
|
+
* @returns whether a transcript reader would see this chunk.
|
|
203
|
+
*/
|
|
204
|
+
export function isVisibleChunk(chunk) {
|
|
205
|
+
switch (chunk.type) {
|
|
206
|
+
case 'text-delta':
|
|
207
|
+
case 'reasoning-delta':
|
|
208
|
+
return hasNonWhitespace(chunk.text);
|
|
209
|
+
case 'block-start':
|
|
210
|
+
return chunk.blockType !== 'text' && chunk.blockType !== 'reasoning' && chunk.blockType !== 'tool-call';
|
|
211
|
+
case 'block-end':
|
|
212
|
+
return blockIsVisible(chunk.block);
|
|
213
|
+
default:
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Whether one chunk carries non-whitespace text, as a text delta or a completed text block.
|
|
219
|
+
* Reasoning, Tool calls, and other block kinds never count.
|
|
220
|
+
* @param chunk - any stream chunk.
|
|
221
|
+
* @returns whether the chunk contributes visible text.
|
|
222
|
+
*/
|
|
223
|
+
export function chunkHasVisibleText(chunk) {
|
|
224
|
+
if (chunk.type === 'text-delta')
|
|
225
|
+
return hasNonWhitespace(chunk.text);
|
|
226
|
+
return chunk.type === 'block-end' && chunk.block.type === 'text' && hasNonWhitespace(chunk.block.text);
|
|
227
|
+
}
|
|
228
|
+
function firstRunMemberTime(run, predicate) {
|
|
229
|
+
const fragments = run.type === 'tool-call-chunks' ? run.args : run.texts;
|
|
230
|
+
let time = run.time0;
|
|
231
|
+
for (let index = 0; index < fragments.length; index += 1) {
|
|
232
|
+
if (index > 0)
|
|
233
|
+
time += run.dt[index - 1];
|
|
234
|
+
if (predicate(fragments[index]))
|
|
235
|
+
return time;
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Time of the first member of one packed run that {@link isTokenDelta} accepts: a
|
|
241
|
+
* name-bearing Tool-call run starts at its first member, otherwise the first non-empty fragment.
|
|
242
|
+
* Stops scanning at that member.
|
|
243
|
+
* @param run - one packed delta run.
|
|
244
|
+
* @returns the member's reconstructed time, or undefined when no member qualifies.
|
|
245
|
+
*/
|
|
246
|
+
export function runFirstTokenTime(run) {
|
|
247
|
+
if (run.type === 'tool-call-chunks' && run.name !== undefined)
|
|
248
|
+
return run.time0;
|
|
249
|
+
return firstRunMemberTime(run, fragment => fragment !== '');
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Time of the first member of one packed run that {@link isVisibleChunk} accepts: the first
|
|
253
|
+
* non-whitespace text or reasoning fragment. A Tool-call run has none. Stops scanning at that member.
|
|
254
|
+
* @param run - one packed delta run.
|
|
255
|
+
* @returns the member's reconstructed time, or undefined when no member qualifies.
|
|
256
|
+
*/
|
|
257
|
+
export function runFirstVisibleTime(run) {
|
|
258
|
+
return run.type === 'tool-call-chunks' ? undefined : firstRunMemberTime(run, hasNonWhitespace);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Time of the first token in one compact stream per {@link isTokenDelta}, read from the
|
|
262
|
+
* records themselves and stopping at the first qualifying member.
|
|
263
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
264
|
+
* @returns the first token's time, or undefined when the stream carries no token.
|
|
265
|
+
*/
|
|
266
|
+
export function assistantStreamFirstTokenTime(stream) {
|
|
267
|
+
for (const record of stream) {
|
|
268
|
+
const time = record.type === 'chunk'
|
|
269
|
+
? (isTokenDelta(record.chunk) ? record.time : undefined)
|
|
270
|
+
: runFirstTokenTime(record);
|
|
271
|
+
if (time !== undefined)
|
|
272
|
+
return time;
|
|
273
|
+
}
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Whether one compact stream carries any reader-visible content per {@link isVisibleChunk},
|
|
278
|
+
* stopping at the first qualifying member.
|
|
279
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
280
|
+
* @returns whether a transcript reader would see anything from this stream.
|
|
281
|
+
*/
|
|
282
|
+
export function assistantStreamHasVisibleContent(stream) {
|
|
283
|
+
return stream.some(record => record.type === 'chunk'
|
|
284
|
+
? isVisibleChunk(record.chunk)
|
|
285
|
+
: runFirstVisibleTime(record) !== undefined);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Whether one compact stream carries non-whitespace text per {@link chunkHasVisibleText},
|
|
289
|
+
* stopping at the first qualifying member.
|
|
290
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
291
|
+
* @returns whether the stream contributes visible text.
|
|
292
|
+
*/
|
|
293
|
+
export function assistantStreamHasVisibleText(stream) {
|
|
294
|
+
return stream.some(record => record.type === 'text-chunks'
|
|
295
|
+
? record.texts.some(hasNonWhitespace)
|
|
296
|
+
: record.type === 'chunk' && chunkHasVisibleText(record.chunk));
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The last raw chunk of one never-packed type, scanning backwards and stopping at the first hit.
|
|
300
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
301
|
+
* @param type - chunk type that only appears as a raw record.
|
|
302
|
+
* @returns the stream's final chunk of that type, or undefined when it has none.
|
|
303
|
+
*/
|
|
304
|
+
export function lastAssistantStreamChunk(stream, type) {
|
|
305
|
+
for (let index = stream.length - 1; index >= 0; index -= 1) {
|
|
306
|
+
const record = stream[index];
|
|
307
|
+
if (record.type === 'chunk' && record.chunk.type === type)
|
|
308
|
+
return record.chunk;
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Every raw chunk of one never-packed type, in stream order.
|
|
314
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
315
|
+
* @param type - chunk type that only appears as a raw record.
|
|
316
|
+
* @returns the matching chunks; empty when the stream has none.
|
|
317
|
+
*/
|
|
318
|
+
export function assistantStreamChunks(stream, type) {
|
|
319
|
+
const chunks = [];
|
|
320
|
+
for (const record of stream) {
|
|
321
|
+
if (record.type === 'chunk' && record.chunk.type === type)
|
|
322
|
+
chunks.push(record.chunk);
|
|
323
|
+
}
|
|
324
|
+
return chunks;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Every streamed text-delta fragment joined in stream order; reasoning and Tool-call fragments are excluded.
|
|
328
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
329
|
+
* @returns the joined text, empty when the stream carries no text delta.
|
|
330
|
+
*/
|
|
331
|
+
export function joinAssistantStreamText(stream) {
|
|
332
|
+
const parts = [];
|
|
333
|
+
for (const record of stream) {
|
|
334
|
+
if (record.type === 'text-chunks')
|
|
335
|
+
parts.push(record.texts.join(''));
|
|
336
|
+
else if (record.type === 'chunk' && record.chunk.type === 'text-delta')
|
|
337
|
+
parts.push(record.chunk.text);
|
|
338
|
+
}
|
|
339
|
+
return parts.join('');
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Feed one compact stream into a {@link BlockAssembler} without materializing members.
|
|
343
|
+
* Each run contributes one delta carrying its joined fragments, which assembles the same
|
|
344
|
+
* blocks as the original per-member deltas because assembly only concatenates them;
|
|
345
|
+
* raw chunks are pushed as recorded. The records are trusted, not validated: validate a
|
|
346
|
+
* stream read at a durable boundary with {@link expandAssistantStream} first.
|
|
347
|
+
* @param stream - compact records from one durable Assistant settlement.
|
|
348
|
+
* @param assembler - assembler to feed; a fresh one by default.
|
|
349
|
+
* @returns the same assembler after every record was pushed.
|
|
350
|
+
*/
|
|
351
|
+
export function assembleAssistantStream(stream, assembler = new BlockAssembler()) {
|
|
352
|
+
for (const record of stream) {
|
|
353
|
+
switch (record.type) {
|
|
354
|
+
case 'chunk':
|
|
355
|
+
assembler.push(record.chunk);
|
|
356
|
+
break;
|
|
357
|
+
case 'text-chunks':
|
|
358
|
+
assembler.push({ type: 'text-delta', index: record.index, text: record.texts.join('') });
|
|
359
|
+
break;
|
|
360
|
+
case 'reasoning-chunks':
|
|
361
|
+
assembler.push({ type: 'reasoning-delta', index: record.index, text: record.texts.join('') });
|
|
362
|
+
break;
|
|
363
|
+
case 'tool-call-chunks':
|
|
364
|
+
assembler.push({
|
|
365
|
+
type: 'tool-call-delta',
|
|
366
|
+
index: record.index,
|
|
367
|
+
id: record.id,
|
|
368
|
+
...record.name === undefined ? {} : { name: record.name },
|
|
369
|
+
argumentsDelta: record.args.join(''),
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
default:
|
|
373
|
+
assertNever(record, 'assembleAssistantStream');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return assembler;
|
|
377
|
+
}
|
|
378
|
+
function validateRecord(value) {
|
|
379
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
380
|
+
throw new TypeError('Assistant stream record must be an object');
|
|
381
|
+
}
|
|
382
|
+
const record = value;
|
|
383
|
+
switch (record.type) {
|
|
384
|
+
case 'text-chunks':
|
|
385
|
+
case 'reasoning-chunks': {
|
|
386
|
+
exactKeys(record, ['type', 'time0', 'index', 'dt', 'texts'], record.type);
|
|
387
|
+
const texts = stringArray(record.texts, `${record.type} texts`);
|
|
388
|
+
if (texts.length === 0)
|
|
389
|
+
throw new TypeError(`${record.type} texts must be non-empty`);
|
|
390
|
+
validateRun(record, texts.length, record.type);
|
|
391
|
+
return record;
|
|
392
|
+
}
|
|
393
|
+
case 'tool-call-chunks': {
|
|
394
|
+
const keys = Object.hasOwn(record, 'name')
|
|
395
|
+
? ['type', 'time0', 'index', 'dt', 'id', 'name', 'args']
|
|
396
|
+
: ['type', 'time0', 'index', 'dt', 'id', 'args'];
|
|
397
|
+
exactKeys(record, keys, record.type);
|
|
398
|
+
const args = stringArray(record.args, 'tool-call-chunks args');
|
|
399
|
+
if (args.length === 0)
|
|
400
|
+
throw new TypeError('tool-call-chunks args must be non-empty');
|
|
401
|
+
if (typeof record.id !== 'string' || record.id.length === 0) {
|
|
402
|
+
throw new TypeError('tool-call-chunks id must be a non-empty string');
|
|
403
|
+
}
|
|
404
|
+
if (record.name !== undefined && (typeof record.name !== 'string' || record.name.length === 0)) {
|
|
405
|
+
throw new TypeError('tool-call-chunks name must be a non-empty string');
|
|
406
|
+
}
|
|
407
|
+
validateRun(record, args.length, record.type);
|
|
408
|
+
return record;
|
|
409
|
+
}
|
|
410
|
+
case 'chunk': {
|
|
411
|
+
exactKeys(record, ['type', 'time', 'chunk'], 'chunk');
|
|
412
|
+
const time = safeTime(record.time);
|
|
413
|
+
if (typeof record.chunk !== 'object'
|
|
414
|
+
|| record.chunk === null
|
|
415
|
+
|| Array.isArray(record.chunk)) {
|
|
416
|
+
throw new TypeError('Assistant stream raw chunk must be a lossless JSON object');
|
|
417
|
+
}
|
|
418
|
+
let chunk;
|
|
419
|
+
try {
|
|
420
|
+
chunk = snapshotChunk(record.chunk);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
throw new TypeError('Assistant stream raw chunk must be a lossless JSON object', { cause: error });
|
|
424
|
+
}
|
|
425
|
+
return deepFreeze({ type: 'chunk', time, chunk });
|
|
426
|
+
}
|
|
427
|
+
default:
|
|
428
|
+
throw new TypeError(`Unsupported Assistant stream record ${JSON.stringify(record.type)}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function validateRun(record, members, label) {
|
|
432
|
+
safeTime(record.time0);
|
|
433
|
+
safeIndex(record.index, label);
|
|
434
|
+
if (!Array.isArray(record.dt) || record.dt.some(value => !Number.isSafeInteger(value))) {
|
|
435
|
+
throw new TypeError(`${label} dt must contain safe integers`);
|
|
436
|
+
}
|
|
437
|
+
if (record.dt.length !== members - 1) {
|
|
438
|
+
throw new TypeError(`${label} dt length must be one less than its members`);
|
|
439
|
+
}
|
|
440
|
+
let time = record.time0;
|
|
441
|
+
for (const gap of record.dt) {
|
|
442
|
+
time += gap;
|
|
443
|
+
if (!Number.isSafeInteger(time))
|
|
444
|
+
throw new TypeError(`${label} member times must stay safe integers`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function stringArray(value, label) {
|
|
448
|
+
if (!Array.isArray(value) || value.some(member => typeof member !== 'string')) {
|
|
449
|
+
throw new TypeError(`${label} must be a string array`);
|
|
450
|
+
}
|
|
451
|
+
return value;
|
|
452
|
+
}
|
|
453
|
+
function exactKeys(record, keys, label) {
|
|
454
|
+
if (Object.keys(record).length !== keys.length || !keys.every(key => Object.hasOwn(record, key))) {
|
|
455
|
+
throw new TypeError(`${label} Assistant stream record must contain exactly ${keys.join(', ')}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
//# sourceMappingURL=assistant-stream.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
|
|
3
|
+
* adapters from drifting. See
|
|
4
|
+
* `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
|
|
5
|
+
*
|
|
6
|
+
* App-attribution vocabulary for provider requests.
|
|
7
|
+
* @module @xlaunch/llm/attribution
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Static public application identity sent to LLM providers.
|
|
11
|
+
*
|
|
12
|
+
* Every field is a public product fact, safe on every request: no secrets,
|
|
13
|
+
* local paths, session ids, prompt text, or per-user identifiers belong here,
|
|
14
|
+
* and nothing per-request may influence the values.
|
|
15
|
+
*/
|
|
16
|
+
export interface AppIdentity {
|
|
17
|
+
/** `User-Agent` product token (lowercase, hyphenated). */
|
|
18
|
+
product: string;
|
|
19
|
+
/** Product version; sourced from package metadata, never hand-copied. */
|
|
20
|
+
version: string;
|
|
21
|
+
/** Repository home URL of the app, used as the `User-Agent` comment. */
|
|
22
|
+
url: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The harness's own identity: the default every adapter sends. Deployments
|
|
26
|
+
* that need a white-label identity pass their own {@link AppIdentity} to
|
|
27
|
+
* {@link attributionHeaders} — omission falls back to this default; nothing
|
|
28
|
+
* can suppress attribution entirely.
|
|
29
|
+
*/
|
|
30
|
+
export declare const APP_IDENTITY: AppIdentity;
|
|
31
|
+
/**
|
|
32
|
+
* The standard `User-Agent` value: `product/version (+url)`. The
|
|
33
|
+
* parenthesized `+url` comment is the conventional self-identification form
|
|
34
|
+
* (RFC 9110 §10.1.5 product + comment syntax).
|
|
35
|
+
* @param identity - the identity to render; defaults to {@link APP_IDENTITY}.
|
|
36
|
+
* @returns the ready-to-send header value.
|
|
37
|
+
*/
|
|
38
|
+
export declare function userAgent(identity?: AppIdentity): string;
|
|
39
|
+
/**
|
|
40
|
+
* Build the attribution headers an adapter must send on every provider
|
|
41
|
+
* request. Header names are lowercase (HTTP field names are case-insensitive
|
|
42
|
+
* on the wire).
|
|
43
|
+
* @param identity - the identity to send; defaults to {@link APP_IDENTITY} — omission cannot suppress attribution.
|
|
44
|
+
* @returns headers to merge into the provider request (currently just `user-agent`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function attributionHeaders(identity?: AppIdentity): Record<string, string>;
|
|
47
|
+
//# sourceMappingURL=attribution.d.ts.map
|