@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,26 @@
|
|
|
1
|
+
/* Generated by @xlaunch/typert-generator from the Host FaceModel — do not edit. */
|
|
2
|
+
import type {
|
|
3
|
+
RemoteResult,
|
|
4
|
+
TypertRemoteContribution,
|
|
5
|
+
} from '@xlaunch/typert-protocol'
|
|
6
|
+
import type { LlmConfigurableProvider, LlmDiscoveredModel, LlmModelDiscoveryRequest, LlmProviderInfo } from '@xlaunch/llm/types'
|
|
7
|
+
|
|
8
|
+
declare module '@xlaunch/typert-protocol' {
|
|
9
|
+
interface TypertRemoteNamespace$6c6c6d {
|
|
10
|
+
discoverModels: (settingsNs: string, request: LlmModelDiscoveryRequest, signal?: AbortSignal) => Promise<RemoteResult<LlmDiscoveredModel[]>>
|
|
11
|
+
listConfigurableProviders: () => Promise<RemoteResult<LlmConfigurableProvider[]>>
|
|
12
|
+
listProviders: () => Promise<RemoteResult<LlmProviderInfo[]>>
|
|
13
|
+
}
|
|
14
|
+
interface TypertRemoteMap {
|
|
15
|
+
'llm/discoverModels': (settingsNs: string, request: LlmModelDiscoveryRequest, signal?: AbortSignal) => Promise<RemoteResult<LlmDiscoveredModel[]>>
|
|
16
|
+
'llm/listConfigurableProviders': () => Promise<RemoteResult<LlmConfigurableProvider[]>>
|
|
17
|
+
'llm/listProviders': () => Promise<RemoteResult<LlmProviderInfo[]>>
|
|
18
|
+
}
|
|
19
|
+
interface TypertRemoteNamespaceMap {
|
|
20
|
+
'llm': TypertRemoteNamespace$6c6c6d
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export declare const TYPERT_REMOTE: TypertRemoteContribution
|
|
25
|
+
export default TYPERT_REMOTE
|
|
26
|
+
//# sourceMappingURL=typert.remote-client.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/* Generated by @xlaunch/typert-generator from the Host FaceModel — do not edit. */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
const _xlaunch_llm_llm_discoverModels_parameter_0$schema = z.string()
|
|
5
|
+
const _xlaunch_llm_llm_discoverModels_parameter_1$schema = z.object({
|
|
6
|
+
'provider': z.string().optional(),
|
|
7
|
+
'baseURL': z.string().optional(),
|
|
8
|
+
'api': z.string().optional(),
|
|
9
|
+
'apiKey': z.string().optional(),
|
|
10
|
+
})
|
|
11
|
+
const _xlaunch_llm_llm_discoverModels_result$schema = z.array(z.object({
|
|
12
|
+
'id': z.string(),
|
|
13
|
+
'name': z.string().optional(),
|
|
14
|
+
'contextWindow': z.number().optional(),
|
|
15
|
+
'maxTokens': z.number().optional(),
|
|
16
|
+
}))
|
|
17
|
+
const _xlaunch_llm_llm_listConfigurableProviders_result$schema = z.array(z.object({
|
|
18
|
+
'provider': z.string(),
|
|
19
|
+
'displayName': z.string(),
|
|
20
|
+
'settingsNs': z.string(),
|
|
21
|
+
'settingsPath': z.array(z.string()),
|
|
22
|
+
'declared': z.boolean().optional(),
|
|
23
|
+
}))
|
|
24
|
+
const _xlaunch_llm_llm_listProviders_result$schema = z.array(z.object({
|
|
25
|
+
'id': z.string(),
|
|
26
|
+
'name': z.string(),
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
export const TYPERT_REMOTE = {
|
|
30
|
+
package: '@xlaunch/llm',
|
|
31
|
+
descriptors: [
|
|
32
|
+
{
|
|
33
|
+
id: '@xlaunch/llm#llm/discoverModels',
|
|
34
|
+
service: 'llm',
|
|
35
|
+
namespace: 'llm',
|
|
36
|
+
method: 'discoverModels',
|
|
37
|
+
implementation: 'remoteDiscoverModels',
|
|
38
|
+
invocation: { kind: 'direct' },
|
|
39
|
+
parameters: [
|
|
40
|
+
{
|
|
41
|
+
name: 'settingsNs',
|
|
42
|
+
wire: 'settingsNs',
|
|
43
|
+
source: 'json',
|
|
44
|
+
codec: {
|
|
45
|
+
mode: 'strict',
|
|
46
|
+
typeSymbol: '@xlaunch/llm#llm/discoverModels:settingsNs',
|
|
47
|
+
schema: _xlaunch_llm_llm_discoverModels_parameter_0$schema,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'request',
|
|
52
|
+
wire: 'request',
|
|
53
|
+
source: 'json',
|
|
54
|
+
codec: {
|
|
55
|
+
mode: 'strict',
|
|
56
|
+
typeSymbol: '@xlaunch/llm/types#LlmModelDiscoveryRequest',
|
|
57
|
+
schema: _xlaunch_llm_llm_discoverModels_parameter_1$schema,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
cancellation: { parameter: 'signal' },
|
|
62
|
+
result: {
|
|
63
|
+
mode: 'strict',
|
|
64
|
+
typeSymbol: '@xlaunch/llm#llm/discoverModels:result',
|
|
65
|
+
schema: _xlaunch_llm_llm_discoverModels_result$schema,
|
|
66
|
+
},
|
|
67
|
+
sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":625,"column":9},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: '@xlaunch/llm#llm/listConfigurableProviders',
|
|
71
|
+
service: 'llm',
|
|
72
|
+
namespace: 'llm',
|
|
73
|
+
method: 'listConfigurableProviders',
|
|
74
|
+
invocation: { kind: 'direct' },
|
|
75
|
+
parameters: [
|
|
76
|
+
],
|
|
77
|
+
result: {
|
|
78
|
+
mode: 'strict',
|
|
79
|
+
typeSymbol: '@xlaunch/llm#llm/listConfigurableProviders:result',
|
|
80
|
+
schema: _xlaunch_llm_llm_listConfigurableProviders_result$schema,
|
|
81
|
+
},
|
|
82
|
+
sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":538,"column":3},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: '@xlaunch/llm#llm/listProviders',
|
|
86
|
+
service: 'llm',
|
|
87
|
+
namespace: 'llm',
|
|
88
|
+
method: 'listProviders',
|
|
89
|
+
invocation: { kind: 'direct' },
|
|
90
|
+
parameters: [
|
|
91
|
+
],
|
|
92
|
+
result: {
|
|
93
|
+
mode: 'strict',
|
|
94
|
+
typeSymbol: '@xlaunch/llm#llm/listProviders:result',
|
|
95
|
+
schema: _xlaunch_llm_llm_listProviders_result$schema,
|
|
96
|
+
},
|
|
97
|
+
sourceLocation: {"file":"packages/llm/llm/src/index.ts","line":466,"column":3},
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default TYPERT_REMOTE
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalization for values thrown by a final LLM adapter boundary.
|
|
3
|
+
*
|
|
4
|
+
* @module @xlaunch/llm/adapter-failure
|
|
5
|
+
*/
|
|
6
|
+
import type { LlmFailure } from './types.ts';
|
|
7
|
+
/**
|
|
8
|
+
* Detach serializable provider facts from a value thrown by an adapter.
|
|
9
|
+
* @param value - arbitrary value thrown during adapter dispatch or iteration.
|
|
10
|
+
* @returns immutable provider-neutral facts suitable for a terminal finish chunk.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeLlmFailure(value: unknown): LlmFailure;
|
|
14
|
+
//# sourceMappingURL=adapter-failure.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalization for values thrown by a final LLM adapter boundary.
|
|
3
|
+
*
|
|
4
|
+
* @module @xlaunch/llm/adapter-failure
|
|
5
|
+
*/
|
|
6
|
+
import { HarnessError } from "./error.js";
|
|
7
|
+
/**
|
|
8
|
+
* Detach serializable provider facts from a value thrown by an adapter.
|
|
9
|
+
* @param value - arbitrary value thrown during adapter dispatch or iteration.
|
|
10
|
+
* @returns immutable provider-neutral facts suitable for a terminal finish chunk.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function normalizeLlmFailure(value) {
|
|
14
|
+
const error = value instanceof Error
|
|
15
|
+
? value
|
|
16
|
+
: new HarnessError(thrownMessage(value), 'UNKNOWN', { cause: value });
|
|
17
|
+
// Cross-package copies preserve own data but not class identity. Trust the
|
|
18
|
+
// carried facts only when both own properties agree after validation.
|
|
19
|
+
const carried = ownFailureSnapshot(error);
|
|
20
|
+
if (carried !== undefined && carried.code === ownErrorCode(error))
|
|
21
|
+
return carried;
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
message: errorMessage(error),
|
|
24
|
+
code: harnessErrorCode(error),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** Render a non-Error throw without letting hostile coercion escape normalization. */
|
|
28
|
+
function thrownMessage(value) {
|
|
29
|
+
try {
|
|
30
|
+
const message = String(value);
|
|
31
|
+
return message.length > 0 ? message : 'LLM adapter failed';
|
|
32
|
+
}
|
|
33
|
+
catch (_hostileThrownValue) {
|
|
34
|
+
return 'LLM adapter failed';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Read a foreign error's own data-backed `code` without invoking accessors. */
|
|
38
|
+
function ownErrorCode(error) {
|
|
39
|
+
try {
|
|
40
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, 'code');
|
|
41
|
+
return descriptor !== undefined && 'value' in descriptor ? descriptor.value : undefined;
|
|
42
|
+
}
|
|
43
|
+
catch (_sdkPropertyTrap) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Snapshot an own data property without invoking an SDK-defined accessor. */
|
|
48
|
+
function ownFailureSnapshot(error) {
|
|
49
|
+
try {
|
|
50
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, 'failure');
|
|
51
|
+
return descriptor !== undefined && 'value' in descriptor
|
|
52
|
+
? failureSnapshot(descriptor.value)
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
catch (_sdkPropertyTrap) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Validate and detach an arbitrary serializable failure payload. */
|
|
60
|
+
function failureSnapshot(value) {
|
|
61
|
+
if (typeof value !== 'object' || value === null)
|
|
62
|
+
return undefined;
|
|
63
|
+
try {
|
|
64
|
+
const candidate = value;
|
|
65
|
+
const message = candidate.message;
|
|
66
|
+
const code = candidate.code;
|
|
67
|
+
const status = candidate.status;
|
|
68
|
+
const providerRetryAfterMs = candidate.providerRetryAfterMs;
|
|
69
|
+
const requestId = candidate.requestId;
|
|
70
|
+
if (typeof message !== 'string' || message.length === 0
|
|
71
|
+
|| typeof code !== 'string' || code.length === 0
|
|
72
|
+
|| (status !== undefined && (!Number.isInteger(status) || status < 100 || status > 599))
|
|
73
|
+
|| (providerRetryAfterMs !== undefined
|
|
74
|
+
&& (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0))
|
|
75
|
+
|| (requestId !== undefined && (typeof requestId !== 'string' || requestId.length === 0)))
|
|
76
|
+
return undefined;
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
message,
|
|
79
|
+
code,
|
|
80
|
+
...status === undefined ? {} : { status },
|
|
81
|
+
...providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs },
|
|
82
|
+
...requestId === undefined ? {} : { requestId },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (_sdkFailureGetter) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Read an SDK error message without letting an accessor replace the primary failure. */
|
|
90
|
+
function errorMessage(error) {
|
|
91
|
+
try {
|
|
92
|
+
const message = error.message;
|
|
93
|
+
if (typeof message === 'string' && message.length > 0)
|
|
94
|
+
return message;
|
|
95
|
+
}
|
|
96
|
+
catch (_sdkMessageGetter) {
|
|
97
|
+
// The fallback below preserves a serializable failure beside the original Error.
|
|
98
|
+
}
|
|
99
|
+
return 'LLM adapter failed';
|
|
100
|
+
}
|
|
101
|
+
/** Trust only Harness-owned codes; third-party SDK codes are not our taxonomy. */
|
|
102
|
+
function harnessErrorCode(error) {
|
|
103
|
+
return error instanceof HarnessError ? error.code : 'UNKNOWN';
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=adapter-failure.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one definition of a well-formed provider API key, shared by every
|
|
3
|
+
* adapter that puts one in an HTTP header.
|
|
4
|
+
* @module @xlaunch/llm/api-key
|
|
5
|
+
*/
|
|
6
|
+
/** Why a supplied API key cannot be used. */
|
|
7
|
+
export type ApiKeyRejection = 'empty' | 'illegalCharacters';
|
|
8
|
+
/** The verdict on one supplied API key. */
|
|
9
|
+
export type ApiKeyCheck = {
|
|
10
|
+
readonly ok: true;
|
|
11
|
+
readonly value: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
readonly reason: ApiKeyRejection;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Judge one *supplied* API key, trimming surrounding whitespace first.
|
|
18
|
+
*
|
|
19
|
+
* Trimming is silent because a padded key has one unambiguous reading; every
|
|
20
|
+
* other defect is reported. Absence is a configuration state this function
|
|
21
|
+
* never sees — a profile naming no credential authenticates through the
|
|
22
|
+
* provider's own ambient discovery or OAuth — so callers decide whether a
|
|
23
|
+
* value was supplied before asking.
|
|
24
|
+
* @param raw - the key exactly as configured, stored, or typed.
|
|
25
|
+
* @returns the trimmed key, or why it cannot be used.
|
|
26
|
+
*/
|
|
27
|
+
export declare function normalizeApiKey(raw: string): ApiKeyCheck;
|
|
28
|
+
//# sourceMappingURL=api-key.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one definition of a well-formed provider API key, shared by every
|
|
3
|
+
* adapter that puts one in an HTTP header.
|
|
4
|
+
* @module @xlaunch/llm/api-key
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Characters an HTTP header value carries verbatim and every known provider
|
|
8
|
+
* key uses: printable ASCII, space excluded. A key outside this set cannot
|
|
9
|
+
* reach any provider — `fetch` refuses to build the header — so this is a
|
|
10
|
+
* transport invariant rather than one provider's policy. Latin-1 is excluded
|
|
11
|
+
* deliberately: a header could carry it, but no provider issues it, and
|
|
12
|
+
* admitting it trades a local explained refusal for an opaque 401.
|
|
13
|
+
*/
|
|
14
|
+
const LEGAL_API_KEY = /^[\x21-\x7E]+$/;
|
|
15
|
+
/**
|
|
16
|
+
* Judge one *supplied* API key, trimming surrounding whitespace first.
|
|
17
|
+
*
|
|
18
|
+
* Trimming is silent because a padded key has one unambiguous reading; every
|
|
19
|
+
* other defect is reported. Absence is a configuration state this function
|
|
20
|
+
* never sees — a profile naming no credential authenticates through the
|
|
21
|
+
* provider's own ambient discovery or OAuth — so callers decide whether a
|
|
22
|
+
* value was supplied before asking.
|
|
23
|
+
* @param raw - the key exactly as configured, stored, or typed.
|
|
24
|
+
* @returns the trimmed key, or why it cannot be used.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeApiKey(raw) {
|
|
27
|
+
const value = raw.trim();
|
|
28
|
+
if (value.length === 0)
|
|
29
|
+
return { ok: false, reason: 'empty' };
|
|
30
|
+
if (!LEGAL_API_KEY.test(value))
|
|
31
|
+
return { ok: false, reason: 'illegalCharacters' };
|
|
32
|
+
return { ok: true, value };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=api-key.js.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental chunk-to-message assembler. This is the single canonical assembly
|
|
3
|
+
* algorithm used by the agent loop to build an assistant message from a chunk
|
|
4
|
+
* stream while logging the raw chunks for replay fidelity.
|
|
5
|
+
*
|
|
6
|
+
* @module @xlaunch/llm/assembler
|
|
7
|
+
*/
|
|
8
|
+
import type { Message, MessageSource } from './message.ts';
|
|
9
|
+
import type { ContentBlock, FinishReason, ReplayEnvelope, StreamChunk, TokenUsage } from './types.ts';
|
|
10
|
+
/**
|
|
11
|
+
* Incrementally assembles raw {@link StreamChunk}s into complete
|
|
12
|
+
* {@link ContentBlock}s and a final assistant {@link Message}.
|
|
13
|
+
*
|
|
14
|
+
* The agent loop feeds it while logging raw chunks for replay fidelity, then
|
|
15
|
+
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends,
|
|
16
|
+
* or `interruptedBlocks()` when cancellation cut the stream short.
|
|
17
|
+
*
|
|
18
|
+
* Tolerant of delta-only protocols (no block-start/end); deltas arriving for
|
|
19
|
+
* an index already closed by `block-end` are ignored (malformed stream) so a
|
|
20
|
+
* misbehaving adapter cannot grow memory or corrupt a completed block.
|
|
21
|
+
*/
|
|
22
|
+
export declare class BlockAssembler {
|
|
23
|
+
private partials;
|
|
24
|
+
private order;
|
|
25
|
+
private _usage;
|
|
26
|
+
private _finish;
|
|
27
|
+
private _replayState;
|
|
28
|
+
/**
|
|
29
|
+
* Feed one chunk into the assembly state.
|
|
30
|
+
* @param chunk - the next raw chunk, in stream order.
|
|
31
|
+
*/
|
|
32
|
+
push(chunk: StreamChunk): void;
|
|
33
|
+
private ensure;
|
|
34
|
+
private assemble;
|
|
35
|
+
/** Invariant accessor: every index in `order` has a partial. */
|
|
36
|
+
private mustGet;
|
|
37
|
+
/**
|
|
38
|
+
* The one shared keep/drop decision over all seen blocks: max-token
|
|
39
|
+
* truncation drops tool calls that cannot be executed safely. Emitted blocks
|
|
40
|
+
* and replay metadata both derive from this result, so they cannot disagree.
|
|
41
|
+
*/
|
|
42
|
+
private assembled;
|
|
43
|
+
/**
|
|
44
|
+
* Assemble all blocks seen so far, in stream order.
|
|
45
|
+
* @returns one block per seen index, except that max-token truncation drops
|
|
46
|
+
* tool calls that cannot be executed safely; an open block assembles from
|
|
47
|
+
* its accumulated deltas (an unknown block type never closed by `block-end` throws).
|
|
48
|
+
*/
|
|
49
|
+
blocks(): ContentBlock[];
|
|
50
|
+
/**
|
|
51
|
+
* Assemble the prefix an interrupted stream can safely finalize: closed and
|
|
52
|
+
* open text/reasoning blocks with non-whitespace content, in stream order.
|
|
53
|
+
* Tool calls are omitted because interruption precedes dispatch; retaining
|
|
54
|
+
* one would require a fabricated result. Open unknown blocks are also omitted.
|
|
55
|
+
* @returns the kept blocks; empty when nothing streamed before the interruption.
|
|
56
|
+
*/
|
|
57
|
+
interruptedBlocks(): ContentBlock[];
|
|
58
|
+
/** Usage from the `usage` chunk; undefined until one arrives. */
|
|
59
|
+
get usage(): TokenUsage | undefined;
|
|
60
|
+
/** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
|
|
61
|
+
get finish(): FinishReason;
|
|
62
|
+
/**
|
|
63
|
+
* Replay metadata from the terminal finish chunk, if any, with per-block
|
|
64
|
+
* entries pruned in step with {@link blocks}. Undefined when the envelope's
|
|
65
|
+
* entries do not align with the emitted blocks.
|
|
66
|
+
*/
|
|
67
|
+
get replayState(): ReplayEnvelope | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* The assembled assistant message.
|
|
70
|
+
* @param source - producer attribution for the assembled message.
|
|
71
|
+
* @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules).
|
|
72
|
+
*/
|
|
73
|
+
message(source?: MessageSource): Message;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=assembler.d.ts.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental chunk-to-message assembler. This is the single canonical assembly
|
|
3
|
+
* algorithm used by the agent loop to build an assistant message from a chunk
|
|
4
|
+
* stream while logging the raw chunks for replay fidelity.
|
|
5
|
+
*
|
|
6
|
+
* @module @xlaunch/llm/assembler
|
|
7
|
+
*/
|
|
8
|
+
import { brandString } from '@xlaunch/brand';
|
|
9
|
+
import { assertNever } from '@xlaunch/util-values';
|
|
10
|
+
import { createMessage } from "./message.js";
|
|
11
|
+
/**
|
|
12
|
+
* Incrementally assembles raw {@link StreamChunk}s into complete
|
|
13
|
+
* {@link ContentBlock}s and a final assistant {@link Message}.
|
|
14
|
+
*
|
|
15
|
+
* The agent loop feeds it while logging raw chunks for replay fidelity, then
|
|
16
|
+
* reads `blocks()` / `message()` / `usage` / `finish` once the stream ends,
|
|
17
|
+
* or `interruptedBlocks()` when cancellation cut the stream short.
|
|
18
|
+
*
|
|
19
|
+
* Tolerant of delta-only protocols (no block-start/end); deltas arriving for
|
|
20
|
+
* an index already closed by `block-end` are ignored (malformed stream) so a
|
|
21
|
+
* misbehaving adapter cannot grow memory or corrupt a completed block.
|
|
22
|
+
*/
|
|
23
|
+
export class BlockAssembler {
|
|
24
|
+
partials = new Map();
|
|
25
|
+
order = [];
|
|
26
|
+
_usage;
|
|
27
|
+
_finish;
|
|
28
|
+
_replayState;
|
|
29
|
+
/**
|
|
30
|
+
* Feed one chunk into the assembly state.
|
|
31
|
+
* @param chunk - the next raw chunk, in stream order.
|
|
32
|
+
*/
|
|
33
|
+
push(chunk) {
|
|
34
|
+
switch (chunk.type) {
|
|
35
|
+
case 'block-start': {
|
|
36
|
+
if (!this.partials.has(chunk.index)) {
|
|
37
|
+
this.order.push(chunk.index);
|
|
38
|
+
this.partials.set(chunk.index, {
|
|
39
|
+
blockType: chunk.blockType,
|
|
40
|
+
text: '',
|
|
41
|
+
toolCallArguments: '',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
case 'text-delta':
|
|
47
|
+
case 'reasoning-delta': {
|
|
48
|
+
const partial = this.ensure(chunk.index, chunk.type === 'text-delta' ? 'text' : 'reasoning');
|
|
49
|
+
if (partial.block)
|
|
50
|
+
return; // closed by block-end; ignore stragglers
|
|
51
|
+
partial.text += chunk.text;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
case 'tool-call-delta': {
|
|
55
|
+
const partial = this.ensure(chunk.index, 'tool-call');
|
|
56
|
+
if (partial.block)
|
|
57
|
+
return; // closed by block-end; ignore stragglers
|
|
58
|
+
partial.toolCallId = chunk.id;
|
|
59
|
+
if (chunk.name)
|
|
60
|
+
partial.toolCallName = chunk.name;
|
|
61
|
+
partial.toolCallArguments += chunk.argumentsDelta;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
case 'block-end': {
|
|
65
|
+
const partial = this.ensure(chunk.index, chunk.block.type);
|
|
66
|
+
// First close wins; ignoring re-close stragglers keeps streamed output
|
|
67
|
+
// and the final assembled block in agreement.
|
|
68
|
+
if (partial.block)
|
|
69
|
+
return;
|
|
70
|
+
partial.block = chunk.block;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case 'usage': {
|
|
74
|
+
this._usage = chunk.usage;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
case 'finish': {
|
|
78
|
+
this._finish = chunk.reason;
|
|
79
|
+
this._replayState = chunk.replayState;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
default: return assertNever(chunk, 'BlockAssembler.push');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
ensure(index, blockType) {
|
|
86
|
+
let partial = this.partials.get(index);
|
|
87
|
+
if (!partial) {
|
|
88
|
+
partial = { blockType, text: '', toolCallArguments: '' };
|
|
89
|
+
this.partials.set(index, partial);
|
|
90
|
+
this.order.push(index);
|
|
91
|
+
}
|
|
92
|
+
return partial;
|
|
93
|
+
}
|
|
94
|
+
assemble(partial, index) {
|
|
95
|
+
if (partial.block)
|
|
96
|
+
return partial.block;
|
|
97
|
+
switch (partial.blockType) {
|
|
98
|
+
case 'text': return { type: 'text', text: partial.text };
|
|
99
|
+
case 'reasoning': return { type: 'reasoning', text: partial.text };
|
|
100
|
+
case 'tool-call': return {
|
|
101
|
+
type: 'tool-call',
|
|
102
|
+
id: partial.toolCallId ?? brandString(`call-${index}`),
|
|
103
|
+
name: partial.toolCallName ?? '',
|
|
104
|
+
arguments: partial.toolCallArguments,
|
|
105
|
+
};
|
|
106
|
+
default: throw new Error(`cannot assemble incomplete block of type "${partial.blockType}"`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Invariant accessor: every index in `order` has a partial. */
|
|
110
|
+
mustGet(index) {
|
|
111
|
+
const partial = this.partials.get(index);
|
|
112
|
+
if (!partial)
|
|
113
|
+
throw new Error(`BlockAssembler invariant violated: no partial for index ${index}`);
|
|
114
|
+
return partial;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The one shared keep/drop decision over all seen blocks: max-token
|
|
118
|
+
* truncation drops tool calls that cannot be executed safely. Emitted blocks
|
|
119
|
+
* and replay metadata both derive from this result, so they cannot disagree.
|
|
120
|
+
*/
|
|
121
|
+
assembled() {
|
|
122
|
+
const all = this.order.map(index => this.assemble(this.mustGet(index), index));
|
|
123
|
+
const kept = this.finish.kind === 'max-tokens'
|
|
124
|
+
? all.map(block => block.type !== 'tool-call')
|
|
125
|
+
: undefined;
|
|
126
|
+
const blocks = kept === undefined ? all : all.filter((_, position) => kept[position]);
|
|
127
|
+
const envelope = this._replayState;
|
|
128
|
+
if (envelope?.blocks === undefined)
|
|
129
|
+
return { blocks, replay: envelope };
|
|
130
|
+
if (envelope.blocks.length !== all.length)
|
|
131
|
+
return { blocks, replay: undefined };
|
|
132
|
+
return {
|
|
133
|
+
blocks,
|
|
134
|
+
replay: kept === undefined || blocks.length === all.length
|
|
135
|
+
? envelope
|
|
136
|
+
: { response: envelope.response, blocks: envelope.blocks.filter((_, position) => kept[position]) },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Assemble all blocks seen so far, in stream order.
|
|
141
|
+
* @returns one block per seen index, except that max-token truncation drops
|
|
142
|
+
* tool calls that cannot be executed safely; an open block assembles from
|
|
143
|
+
* its accumulated deltas (an unknown block type never closed by `block-end` throws).
|
|
144
|
+
*/
|
|
145
|
+
blocks() {
|
|
146
|
+
return this.assembled().blocks;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Assemble the prefix an interrupted stream can safely finalize: closed and
|
|
150
|
+
* open text/reasoning blocks with non-whitespace content, in stream order.
|
|
151
|
+
* Tool calls are omitted because interruption precedes dispatch; retaining
|
|
152
|
+
* one would require a fabricated result. Open unknown blocks are also omitted.
|
|
153
|
+
* @returns the kept blocks; empty when nothing streamed before the interruption.
|
|
154
|
+
*/
|
|
155
|
+
interruptedBlocks() {
|
|
156
|
+
return this.order
|
|
157
|
+
.map((index) => {
|
|
158
|
+
const partial = this.mustGet(index);
|
|
159
|
+
const type = partial.block?.type ?? partial.blockType;
|
|
160
|
+
if (type !== 'text' && type !== 'reasoning')
|
|
161
|
+
return undefined;
|
|
162
|
+
return this.assemble(partial, index);
|
|
163
|
+
})
|
|
164
|
+
.filter((block) => (block?.type === 'text' || block?.type === 'reasoning') && block.text.trim() !== '');
|
|
165
|
+
}
|
|
166
|
+
/** Usage from the `usage` chunk; undefined until one arrives. */
|
|
167
|
+
get usage() {
|
|
168
|
+
return this._usage;
|
|
169
|
+
}
|
|
170
|
+
/** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
|
|
171
|
+
get finish() {
|
|
172
|
+
return this._finish ?? { kind: 'stop' };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Replay metadata from the terminal finish chunk, if any, with per-block
|
|
176
|
+
* entries pruned in step with {@link blocks}. Undefined when the envelope's
|
|
177
|
+
* entries do not align with the emitted blocks.
|
|
178
|
+
*/
|
|
179
|
+
get replayState() {
|
|
180
|
+
return this.assembled().replay;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The assembled assistant message.
|
|
184
|
+
* @param source - producer attribution for the assembled message.
|
|
185
|
+
* @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules).
|
|
186
|
+
*/
|
|
187
|
+
message(source = { kind: 'plugin', plugin: 'xlaunch-llm/assembler' }) {
|
|
188
|
+
return createMessage({ role: 'assistant', content: this.blocks(), source });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=assembler.js.map
|