@stackstackstack/dsh-llm 0.1.5

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.
@@ -0,0 +1,84 @@
1
+ //#region lib/types/invariant.js
2
+ /** Package-owned LLM stream-protocol invariants. @module @stackstackstack/dsh-llm/invariant */
3
+ const PACKAGE_NAME = "@stackstackstack/dsh-llm";
4
+ /** Cordis companion plugin name. */
5
+ const name = "llm-invariant";
6
+ /** Service required before the companion can reserve package ownership. */
7
+ const inject = ["invariants"];
8
+ /** Require one chunk index to be a non-negative safe integer. */
9
+ function validateIndex(index, fail) {
10
+ if (!Number.isSafeInteger(index) || index < 0) fail(`LLM stream block index must be a non-negative safe integer, got ${index}`);
11
+ }
12
+ /** Require a delta to address an open block of its matching type. */
13
+ function validateDelta(open, index, expected, fail) {
14
+ validateIndex(index, fail);
15
+ const actual = open.get(index);
16
+ if (actual !== expected) fail(`${expected} delta at index ${index} requires an open ${expected} block, got ${String(actual)}`);
17
+ }
18
+ /** Wrap one provider stream and enforce its grammar as chunks are consumed. */
19
+ async function* validateStream(source, fail) {
20
+ const open = /* @__PURE__ */ new Map();
21
+ let usageSeen = false;
22
+ let finished = false;
23
+ for await (const chunk of source) {
24
+ if (finished) fail(`LLM stream emitted ${chunk.type} after terminal finish`);
25
+ switch (chunk.type) {
26
+ case "block-start":
27
+ validateIndex(chunk.index, fail);
28
+ if (open.has(chunk.index)) fail(`LLM stream repeated block-start index ${chunk.index}`);
29
+ open.set(chunk.index, chunk.blockType);
30
+ break;
31
+ case "text-delta":
32
+ validateDelta(open, chunk.index, "text", fail);
33
+ break;
34
+ case "reasoning-delta":
35
+ validateDelta(open, chunk.index, "reasoning", fail);
36
+ break;
37
+ case "tool-call-delta":
38
+ validateDelta(open, chunk.index, "tool-call", fail);
39
+ break;
40
+ case "block-end": {
41
+ validateIndex(chunk.index, fail);
42
+ const blockType = open.get(chunk.index);
43
+ if (blockType === void 0) fail(`LLM stream block-end index ${chunk.index} has no open block`);
44
+ if (chunk.block.type !== blockType) fail(`LLM stream block-end index ${chunk.index} closes ${chunk.block.type}, expected ${blockType}`);
45
+ open.delete(chunk.index);
46
+ break;
47
+ }
48
+ case "usage":
49
+ if (usageSeen) fail("LLM stream emitted usage more than once");
50
+ usageSeen = true;
51
+ break;
52
+ case "finish":
53
+ if (open.size > 0 && chunk.reason.kind !== "error" && chunk.reason.kind !== "aborted") fail(`LLM stream finished with ${open.size} open block(s)`);
54
+ finished = true;
55
+ break;
56
+ }
57
+ yield chunk;
58
+ }
59
+ if (!finished) fail("LLM stream ended without a terminal finish chunk");
60
+ }
61
+ /** Install validation around every provider stream. */
62
+ const install = (ctx, fail) => {
63
+ ctx.on("llm/stream", (_options, next) => validateStream(next(), fail), {
64
+ global: true,
65
+ prepend: true
66
+ });
67
+ ctx.on("llm/adapters-updated", () => {
68
+ const llm = ctx.get("llm");
69
+ if (llm === void 0) return;
70
+ for (const provider of llm.listProviders()) try {
71
+ llm.providerRetryPolicy(provider.id);
72
+ } catch {
73
+ fail(`llm/adapters-updated fired while provider "${provider.id}" has no readable registration`);
74
+ }
75
+ }, { global: true });
76
+ };
77
+ /**
78
+ * Register the LLM invariant companion.
79
+ * @param ctx - Cordis context carrying the invariant service.
80
+ * @returns the installed registration's disposer after setup succeeds.
81
+ */
82
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
83
+ //#endregion
84
+ export { apply, inject, name };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalization for values thrown by a final LLM adapter boundary.
3
+ *
4
+ * @module @stackstackstack/dsh-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 @stackstackstack/dsh-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 @stackstackstack/dsh-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 @stackstackstack/dsh-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,56 @@
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 @stackstackstack/dsh-llm/assembler
7
+ */
8
+ import type { Message, MessageSource } from './message.ts';
9
+ import type { ContentBlock, FinishReason, 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
+ *
17
+ * Tolerant of delta-only protocols (no block-start/end); deltas arriving for
18
+ * an index already closed by `block-end` are ignored (malformed stream) so a
19
+ * misbehaving adapter cannot grow memory or corrupt a completed block.
20
+ */
21
+ export declare class BlockAssembler {
22
+ private partials;
23
+ private order;
24
+ private _usage;
25
+ private _finish;
26
+ private _replayState;
27
+ /**
28
+ * Feed one chunk into the assembly state.
29
+ * @param chunk - the next raw chunk, in stream order.
30
+ */
31
+ push(chunk: StreamChunk): void;
32
+ private ensure;
33
+ private assemble;
34
+ /** Invariant accessor: every index in `order` has a partial. */
35
+ private mustGet;
36
+ /**
37
+ * Assemble all blocks seen so far, in stream order.
38
+ * @returns one block per seen index, except that max-token truncation drops
39
+ * tool calls that cannot be executed safely; an open block assembles from
40
+ * its accumulated deltas (an unknown block type never closed by `block-end` throws).
41
+ */
42
+ blocks(): ContentBlock[];
43
+ /** Usage from the `usage` chunk; undefined until one arrives. */
44
+ get usage(): TokenUsage | undefined;
45
+ /** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
46
+ get finish(): FinishReason;
47
+ /** Adapter-private replay state from the terminal finish chunk, if any. */
48
+ get replayState(): unknown;
49
+ /**
50
+ * The assembled assistant message.
51
+ * @param source - producer attribution for the assembled message.
52
+ * @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules).
53
+ */
54
+ message(source?: MessageSource): Message;
55
+ }
56
+ //# sourceMappingURL=assembler.d.ts.map
@@ -0,0 +1,148 @@
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 @stackstackstack/dsh-llm/assembler
7
+ */
8
+ import { CallId } from "./brand.js";
9
+ import { assertNever } from "./never.js";
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
+ *
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 class BlockAssembler {
23
+ partials = new Map();
24
+ order = [];
25
+ _usage;
26
+ _finish;
27
+ _replayState = undefined;
28
+ /**
29
+ * Feed one chunk into the assembly state.
30
+ * @param chunk - the next raw chunk, in stream order.
31
+ */
32
+ push(chunk) {
33
+ switch (chunk.type) {
34
+ case 'block-start': {
35
+ if (!this.partials.has(chunk.index)) {
36
+ this.order.push(chunk.index);
37
+ this.partials.set(chunk.index, {
38
+ blockType: chunk.blockType,
39
+ text: '',
40
+ toolCallArguments: '',
41
+ });
42
+ }
43
+ return;
44
+ }
45
+ case 'text-delta':
46
+ case 'reasoning-delta': {
47
+ const partial = this.ensure(chunk.index, chunk.type === 'text-delta' ? 'text' : 'reasoning');
48
+ if (partial.block)
49
+ return; // closed by block-end; ignore stragglers
50
+ partial.text += chunk.text;
51
+ return;
52
+ }
53
+ case 'tool-call-delta': {
54
+ const partial = this.ensure(chunk.index, 'tool-call');
55
+ if (partial.block)
56
+ return; // closed by block-end; ignore stragglers
57
+ partial.toolCallId = chunk.id;
58
+ if (chunk.name)
59
+ partial.toolCallName = chunk.name;
60
+ partial.toolCallArguments += chunk.argumentsDelta;
61
+ return;
62
+ }
63
+ case 'block-end': {
64
+ const partial = this.ensure(chunk.index, chunk.block.type);
65
+ // First close wins; ignoring re-close stragglers keeps streamed output
66
+ // and the final assembled block in agreement.
67
+ if (partial.block)
68
+ return;
69
+ partial.block = chunk.block;
70
+ return;
71
+ }
72
+ case 'usage': {
73
+ this._usage = chunk.usage;
74
+ return;
75
+ }
76
+ case 'finish': {
77
+ this._finish = chunk.reason;
78
+ this._replayState = chunk.replayState;
79
+ return;
80
+ }
81
+ default: return assertNever(chunk, 'BlockAssembler.push');
82
+ }
83
+ }
84
+ ensure(index, blockType) {
85
+ let partial = this.partials.get(index);
86
+ if (!partial) {
87
+ partial = { blockType, text: '', toolCallArguments: '' };
88
+ this.partials.set(index, partial);
89
+ this.order.push(index);
90
+ }
91
+ return partial;
92
+ }
93
+ assemble(partial, index) {
94
+ if (partial.block)
95
+ return partial.block;
96
+ switch (partial.blockType) {
97
+ case 'text': return { type: 'text', text: partial.text };
98
+ case 'reasoning': return { type: 'reasoning', text: partial.text };
99
+ case 'tool-call': return {
100
+ type: 'tool-call',
101
+ id: partial.toolCallId ?? CallId(`call-${index}`),
102
+ name: partial.toolCallName ?? '',
103
+ arguments: partial.toolCallArguments,
104
+ };
105
+ default: throw new Error(`cannot assemble incomplete block of type "${partial.blockType}"`);
106
+ }
107
+ }
108
+ /** Invariant accessor: every index in `order` has a partial. */
109
+ mustGet(index) {
110
+ const partial = this.partials.get(index);
111
+ if (!partial)
112
+ throw new Error(`BlockAssembler invariant violated: no partial for index ${index}`);
113
+ return partial;
114
+ }
115
+ /**
116
+ * Assemble all blocks seen so far, in stream order.
117
+ * @returns one block per seen index, except that max-token truncation drops
118
+ * tool calls that cannot be executed safely; an open block assembles from
119
+ * its accumulated deltas (an unknown block type never closed by `block-end` throws).
120
+ */
121
+ blocks() {
122
+ const blocks = this.order.map(index => this.assemble(this.mustGet(index), index));
123
+ return this.finish.kind === 'max-tokens'
124
+ ? blocks.filter(block => block.type !== 'tool-call')
125
+ : blocks;
126
+ }
127
+ /** Usage from the `usage` chunk; undefined until one arrives. */
128
+ get usage() {
129
+ return this._usage;
130
+ }
131
+ /** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
132
+ get finish() {
133
+ return this._finish ?? { kind: 'stop' };
134
+ }
135
+ /** Adapter-private replay state from the terminal finish chunk, if any. */
136
+ get replayState() {
137
+ return this._replayState;
138
+ }
139
+ /**
140
+ * The assembled assistant message.
141
+ * @param source - producer attribution for the assembled message.
142
+ * @returns a frozen assistant-role message over `blocks()` (same open-block assembly rules).
143
+ */
144
+ message(source = { kind: 'plugin', plugin: 'dsh-llm/assembler' }) {
145
+ return createMessage({ role: 'assistant', content: this.blocks(), source });
146
+ }
147
+ }
148
+ //# sourceMappingURL=assembler.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 @stackstackstack/dsh-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
@@ -0,0 +1,46 @@
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 @stackstackstack/dsh-llm/attribution
8
+ */
9
+ import { createRequire } from 'node:module';
10
+ // The package's own manifest is the single source of the version so the
11
+ // User-Agent cannot drift from what is published (`./package.json` is an
12
+ // export of this package; the relative path resolves from both `src/` and
13
+ // the bundled `lib/`).
14
+ const { version } = createRequire(import.meta.url)('../package.json');
15
+ /**
16
+ * The harness's own identity: the default every adapter sends. Deployments
17
+ * that need a white-label identity pass their own {@link AppIdentity} to
18
+ * {@link attributionHeaders} — omission falls back to this default; nothing
19
+ * can suppress attribution entirely.
20
+ */
21
+ export const APP_IDENTITY = {
22
+ product: 'deepseek-harness',
23
+ version,
24
+ url: 'https://github.com/deepseek-ai/deepseek-harness',
25
+ };
26
+ /**
27
+ * The standard `User-Agent` value: `product/version (+url)`. The
28
+ * parenthesized `+url` comment is the conventional self-identification form
29
+ * (RFC 9110 §10.1.5 product + comment syntax).
30
+ * @param identity - the identity to render; defaults to {@link APP_IDENTITY}.
31
+ * @returns the ready-to-send header value.
32
+ */
33
+ export function userAgent(identity = APP_IDENTITY) {
34
+ return `${identity.product}/${identity.version} (+${identity.url})`;
35
+ }
36
+ /**
37
+ * Build the attribution headers an adapter must send on every provider
38
+ * request. Header names are lowercase (HTTP field names are case-insensitive
39
+ * on the wire).
40
+ * @param identity - the identity to send; defaults to {@link APP_IDENTITY} — omission cannot suppress attribution.
41
+ * @returns headers to merge into the provider request (currently just `user-agent`).
42
+ */
43
+ export function attributionHeaders(identity = APP_IDENTITY) {
44
+ return { 'user-agent': userAgent(identity) };
45
+ }
46
+ //# sourceMappingURL=attribution.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * dsh-llm's owned branded ids: tool-call correlation and provider request
3
+ * diagnostics.
4
+ *
5
+ * The `Branded<B>` primitive itself lives in `@stackstackstack/dsh-brand` (a
6
+ * zero-dependency type-only package) so every owner of a cross-boundary id can
7
+ * brand it without depending on dsh-llm; see that package's README for the
8
+ * nominal-typing policy.
9
+ *
10
+ * @module @stackstackstack/dsh-llm/brand
11
+ */
12
+ import type { Branded } from '@stackstackstack/dsh-brand';
13
+ /** Stable identity carried by one message across inbox, log, and model-request boundaries. */
14
+ export type MessageId = Branded<'MessageId'>;
15
+ /**
16
+ * Brand a message identifier.
17
+ * @param id - the opaque message identifier.
18
+ * @returns the same string, branded; no validation is performed.
19
+ */
20
+ export declare function MessageId(id: string): MessageId;
21
+ /**
22
+ * Correlates a model-issued tool call with its result. Provider-issued for
23
+ * real adapters; synthesized by mocks/assembler fallbacks.
24
+ */
25
+ export type CallId = Branded<'CallId'>;
26
+ /**
27
+ * Brand a string as a {@link CallId}.
28
+ * @param id - the provider-issued (or synthesized) call id.
29
+ * @returns the same string, branded; no validation is performed.
30
+ */
31
+ export declare function CallId(id: string): CallId;
32
+ /** Provider-issued request identifier retained for diagnostics across package boundaries. */
33
+ export type ProviderRequestId = Branded<'ProviderRequestId'>;
34
+ /**
35
+ * Brand a provider-issued request identifier.
36
+ * @param id - the opaque provider-issued string.
37
+ * @returns the same string, branded; no validation is performed.
38
+ */
39
+ export declare function ProviderRequestId(id: string): ProviderRequestId;
40
+ /** Adapter-owned identifier for one model's selectable reasoning effort. */
41
+ export type ReasoningEffortId = Branded<'ReasoningEffortId'>;
42
+ /**
43
+ * Brand an adapter-owned reasoning-effort identifier.
44
+ * @param id - the opaque identifier exposed by one model capability.
45
+ * @returns the same string, branded; no validation is performed.
46
+ */
47
+ export declare function ReasoningEffortId(id: string): ReasoningEffortId;
48
+ //# sourceMappingURL=brand.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * dsh-llm's owned branded ids: tool-call correlation and provider request
3
+ * diagnostics.
4
+ *
5
+ * The `Branded<B>` primitive itself lives in `@stackstackstack/dsh-brand` (a
6
+ * zero-dependency type-only package) so every owner of a cross-boundary id can
7
+ * brand it without depending on dsh-llm; see that package's README for the
8
+ * nominal-typing policy.
9
+ *
10
+ * @module @stackstackstack/dsh-llm/brand
11
+ */
12
+ /**
13
+ * Brand a message identifier.
14
+ * @param id - the opaque message identifier.
15
+ * @returns the same string, branded; no validation is performed.
16
+ */
17
+ export function MessageId(id) {
18
+ return id;
19
+ }
20
+ /**
21
+ * Brand a string as a {@link CallId}.
22
+ * @param id - the provider-issued (or synthesized) call id.
23
+ * @returns the same string, branded; no validation is performed.
24
+ */
25
+ export function CallId(id) {
26
+ return id;
27
+ }
28
+ /**
29
+ * Brand a provider-issued request identifier.
30
+ * @param id - the opaque provider-issued string.
31
+ * @returns the same string, branded; no validation is performed.
32
+ */
33
+ export function ProviderRequestId(id) {
34
+ return id;
35
+ }
36
+ /**
37
+ * Brand an adapter-owned reasoning-effort identifier.
38
+ * @param id - the opaque identifier exposed by one model capability.
39
+ * @returns the same string, branded; no validation is performed.
40
+ */
41
+ export function ReasoningEffortId(id) {
42
+ return id;
43
+ }
44
+ //# sourceMappingURL=brand.js.map