@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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +101 -0
- package/README.zh.md +101 -0
- package/lib/index.js +1407 -0
- package/lib/invariant.js +84 -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 +56 -0
- package/lib/types/assembler.js +148 -0
- package/lib/types/attribution.d.ts +47 -0
- package/lib/types/attribution.js +46 -0
- package/lib/types/brand.d.ts +48 -0
- package/lib/types/brand.js +44 -0
- package/lib/types/call-config.d.ts +62 -0
- package/lib/types/call-config.js +86 -0
- package/lib/types/content.d.ts +12 -0
- package/lib/types/content.js +14 -0
- package/lib/types/error.d.ts +73 -0
- package/lib/types/error.js +145 -0
- package/lib/types/index.d.ts +341 -0
- package/lib/types/index.js +730 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/invariant.js +100 -0
- package/lib/types/message.d.ts +206 -0
- package/lib/types/message.js +100 -0
- package/lib/types/never.d.ts +16 -0
- package/lib/types/never.js +21 -0
- package/lib/types/retry-policy.d.ts +66 -0
- package/lib/types/retry-policy.js +123 -0
- package/lib/types/types.d.ts +349 -0
- package/lib/types/types.js +7 -0
- package/package.json +64 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation call configuration and freeze utilities. Provider routing,
|
|
3
|
+
* model, reasoning effort, and sampling values are request-header state that
|
|
4
|
+
* can affect cache reuse; request waterfalls replace them and the loop logs
|
|
5
|
+
* changed snapshots instead of allowing silent per-call drift.
|
|
6
|
+
* @module dsh-llm/call-config
|
|
7
|
+
*/
|
|
8
|
+
import type { GenerateOptions } from './types.ts';
|
|
9
|
+
import type { ReasoningEffortId } from './brand.ts';
|
|
10
|
+
/**
|
|
11
|
+
* Provider, model, reasoning effort, and sampling scalars of one conversation's
|
|
12
|
+
* requests. Every field maps 1:1 onto the same-named `GenerateOptions` field;
|
|
13
|
+
* the loop builds requests from the logged header rather than accepting these
|
|
14
|
+
* per call.
|
|
15
|
+
*/
|
|
16
|
+
export interface LlmCallConfig {
|
|
17
|
+
provider: string;
|
|
18
|
+
model: string;
|
|
19
|
+
reasoningEffort?: ReasoningEffortId;
|
|
20
|
+
temperature?: number;
|
|
21
|
+
maxTokens?: number;
|
|
22
|
+
stop?: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Effective config fields supplied by exact-model adapter resolution rather
|
|
26
|
+
* than by the caller's request proposal.
|
|
27
|
+
*/
|
|
28
|
+
export interface LlmCallConfigAdapterDefaults {
|
|
29
|
+
reasoningEffort?: true;
|
|
30
|
+
maxTokens?: true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Field-wise equality over {@link LlmCallConfig} — the comparison a caller
|
|
34
|
+
* runs to decide whether a proposed configuration is a real change (worth a
|
|
35
|
+
* logged header snapshot) or the held one restated.
|
|
36
|
+
* @param a - one configuration.
|
|
37
|
+
* @param b - the other.
|
|
38
|
+
* @returns whether every field (including the `stop` list, element-wise) matches.
|
|
39
|
+
*/
|
|
40
|
+
export declare function callConfigEquals(a: LlmCallConfig, b: LlmCallConfig): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Mark one exact request object as assembled by dsh-agent-loop.
|
|
43
|
+
* @param request - loop-owned request envelope before LLM dispatch.
|
|
44
|
+
* @returns the same request object marked as created by the process-local agent loop.
|
|
45
|
+
*/
|
|
46
|
+
export declare function markAgentLoopRequest<T extends GenerateOptions>(request: T): T;
|
|
47
|
+
/**
|
|
48
|
+
* Test whether the exact request object was assembled by dsh-agent-loop.
|
|
49
|
+
* @param request - request envelope observed at the LLM waterfall.
|
|
50
|
+
* @returns whether {@link markAgentLoopRequest} recorded this object.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isAgentLoopRequest(request: GenerateOptions): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Deep-freeze a value in place with an iterative traversal, guarding cycles,
|
|
55
|
+
* so later mutation throws without imposing a JavaScript call-stack depth cap.
|
|
56
|
+
* {@link AbortSignal} objects are deliberately skipped because they are the
|
|
57
|
+
* request's live cancellation channel and freezing them breaks abort.
|
|
58
|
+
* @param value - the value to freeze in place.
|
|
59
|
+
* @returns the same value, frozen.
|
|
60
|
+
*/
|
|
61
|
+
export declare function deepFreeze<T>(value: T): T;
|
|
62
|
+
//# sourceMappingURL=call-config.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation call configuration and freeze utilities. Provider routing,
|
|
3
|
+
* model, reasoning effort, and sampling values are request-header state that
|
|
4
|
+
* can affect cache reuse; request waterfalls replace them and the loop logs
|
|
5
|
+
* changed snapshots instead of allowing silent per-call drift.
|
|
6
|
+
* @module dsh-llm/call-config
|
|
7
|
+
*/
|
|
8
|
+
/** Process-local identities of request objects assembled by dsh-agent-loop. */
|
|
9
|
+
const AGENT_LOOP_REQUESTS = new WeakSet();
|
|
10
|
+
/**
|
|
11
|
+
* Field-wise equality over {@link LlmCallConfig} — the comparison a caller
|
|
12
|
+
* runs to decide whether a proposed configuration is a real change (worth a
|
|
13
|
+
* logged header snapshot) or the held one restated.
|
|
14
|
+
* @param a - one configuration.
|
|
15
|
+
* @param b - the other.
|
|
16
|
+
* @returns whether every field (including the `stop` list, element-wise) matches.
|
|
17
|
+
*/
|
|
18
|
+
export function callConfigEquals(a, b) {
|
|
19
|
+
if (a.provider !== b.provider
|
|
20
|
+
|| a.model !== b.model
|
|
21
|
+
|| a.reasoningEffort !== b.reasoningEffort
|
|
22
|
+
|| a.temperature !== b.temperature
|
|
23
|
+
|| a.maxTokens !== b.maxTokens)
|
|
24
|
+
return false;
|
|
25
|
+
if (a.stop === undefined || b.stop === undefined)
|
|
26
|
+
return a.stop === b.stop;
|
|
27
|
+
return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Mark one exact request object as assembled by dsh-agent-loop.
|
|
31
|
+
* @param request - loop-owned request envelope before LLM dispatch.
|
|
32
|
+
* @returns the same request object marked as created by the process-local agent loop.
|
|
33
|
+
*/
|
|
34
|
+
export function markAgentLoopRequest(request) {
|
|
35
|
+
AGENT_LOOP_REQUESTS.add(request);
|
|
36
|
+
return request;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Test whether the exact request object was assembled by dsh-agent-loop.
|
|
40
|
+
* @param request - request envelope observed at the LLM waterfall.
|
|
41
|
+
* @returns whether {@link markAgentLoopRequest} recorded this object.
|
|
42
|
+
*/
|
|
43
|
+
export function isAgentLoopRequest(request) {
|
|
44
|
+
return AGENT_LOOP_REQUESTS.has(request);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Deep-freeze a value in place with an iterative traversal, guarding cycles,
|
|
48
|
+
* so later mutation throws without imposing a JavaScript call-stack depth cap.
|
|
49
|
+
* {@link AbortSignal} objects are deliberately skipped because they are the
|
|
50
|
+
* request's live cancellation channel and freezing them breaks abort.
|
|
51
|
+
* @param value - the value to freeze in place.
|
|
52
|
+
* @returns the same value, frozen.
|
|
53
|
+
*/
|
|
54
|
+
export function deepFreeze(value) {
|
|
55
|
+
const seen = new WeakSet();
|
|
56
|
+
const pending = [{ kind: 'visit', node: value }];
|
|
57
|
+
while (pending.length > 0) {
|
|
58
|
+
const task = pending.pop();
|
|
59
|
+
/* v8 ignore next -- the loop condition guarantees one pending task. */
|
|
60
|
+
if (task === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
if (task.kind === 'property') {
|
|
63
|
+
pending.push({ kind: 'visit', node: task.source[task.key] });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const node = task.node;
|
|
67
|
+
if (node === null || typeof node !== 'object')
|
|
68
|
+
continue;
|
|
69
|
+
if (node instanceof AbortSignal)
|
|
70
|
+
continue;
|
|
71
|
+
if (seen.has(node))
|
|
72
|
+
continue;
|
|
73
|
+
seen.add(node);
|
|
74
|
+
Object.freeze(node);
|
|
75
|
+
const keys = Object.keys(node);
|
|
76
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
77
|
+
const key = keys[index];
|
|
78
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
79
|
+
if (key === undefined)
|
|
80
|
+
continue;
|
|
81
|
+
pending.push({ kind: 'property', source: node, key });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=call-config.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Content-block structure helpers. @module @stackstackstack/dsh-llm/content */
|
|
2
|
+
import type { ContentBlock } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* True when typed model content contains an image block, walking nested
|
|
5
|
+
* tool-result content. This is the one recursive image walk shared by every
|
|
6
|
+
* image policy (capability gating, text-only serialization, compaction
|
|
7
|
+
* survey), so a consumer cannot silently diverge on nesting depth.
|
|
8
|
+
* @param content - typed model content blocks.
|
|
9
|
+
* @returns whether any nested block is an image.
|
|
10
|
+
*/
|
|
11
|
+
export declare function contentHasImage(content: readonly ContentBlock[]): boolean;
|
|
12
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Content-block structure helpers. @module @stackstackstack/dsh-llm/content */
|
|
2
|
+
/**
|
|
3
|
+
* True when typed model content contains an image block, walking nested
|
|
4
|
+
* tool-result content. This is the one recursive image walk shared by every
|
|
5
|
+
* image policy (capability gating, text-only serialization, compaction
|
|
6
|
+
* survey), so a consumer cannot silently diverge on nesting depth.
|
|
7
|
+
* @param content - typed model content blocks.
|
|
8
|
+
* @returns whether any nested block is an image.
|
|
9
|
+
*/
|
|
10
|
+
export function contentHasImage(content) {
|
|
11
|
+
return content.some(block => block.type === 'image'
|
|
12
|
+
|| (block.type === 'tool-result' && contentHasImage(block.content)));
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness error base with a stable machine-routable code and chained cause.
|
|
3
|
+
* Package errors extend it so tool results and replay can retain failure class.
|
|
4
|
+
* @module @stackstackstack/dsh-llm/error
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Base class for all harness errors. Carries a `code` (stable, programmatic —
|
|
8
|
+
* e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
|
|
9
|
+
* human-readable `message`, and supports `cause` chaining via the standard
|
|
10
|
+
* `ErrorOptions`. `name` defaults to the subclass constructor name.
|
|
11
|
+
*/
|
|
12
|
+
export declare class HarnessError extends Error {
|
|
13
|
+
/** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
|
|
14
|
+
readonly code: string;
|
|
15
|
+
constructor(message: string, code: string, options?: ErrorOptions);
|
|
16
|
+
}
|
|
17
|
+
/** Canonical provider-neutral code for a model request rejected because its context window was exceeded. */
|
|
18
|
+
export declare const CONTEXT_WINDOW_EXCEEDED_CODE = "CONTEXT_WINDOW_EXCEEDED";
|
|
19
|
+
/** Canonical provider-neutral code for an exhausted account quota or balance. */
|
|
20
|
+
export declare const QUOTA_EXCEEDED_CODE = "QUOTA";
|
|
21
|
+
/**
|
|
22
|
+
* Canonical provider-neutral code for a response that completed normally but
|
|
23
|
+
* carried no content blocks at all. Providers occasionally emit a degenerate
|
|
24
|
+
* completion (a terminal stop with zero output); adapters classify it as this
|
|
25
|
+
* failure instead of yielding an empty assistant message, because an empty
|
|
26
|
+
* message silently ends the turn with nothing for the user or the loop to act
|
|
27
|
+
* on. The attempt produced nothing durable, so retry policy treats it as safe
|
|
28
|
+
* to repeat.
|
|
29
|
+
*/
|
|
30
|
+
export declare const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
|
|
31
|
+
/**
|
|
32
|
+
* Canonical provider-neutral code for a credential that was supplied but
|
|
33
|
+
* cannot be used — malformed rather than absent. Distinct from
|
|
34
|
+
* `MISSING_CREDENTIAL` because the fix differs: correct the stored value
|
|
35
|
+
* rather than supply one. Deliberately outside the default retryable set —
|
|
36
|
+
* a malformed credential fails identically on every attempt.
|
|
37
|
+
*/
|
|
38
|
+
export declare const INVALID_CREDENTIAL_CODE = "INVALID_CREDENTIAL";
|
|
39
|
+
/**
|
|
40
|
+
* Recognize the context-overflow wording used by OpenAI-compatible providers
|
|
41
|
+
* and library adapters. Adapters pass all available provider code, type, and
|
|
42
|
+
* message text so both thrown and in-band delivery styles share one classifier.
|
|
43
|
+
* @param detail - provider error code/type/message text joined into one string.
|
|
44
|
+
* @returns true when the detail identifies a request exceeding the model context window.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isContextWindowExceededError(detail: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Recognize provider wording that identifies an exhausted account quota rather
|
|
49
|
+
* than a transient request-rate limit.
|
|
50
|
+
* @param detail - provider error code/type/message text joined into one string.
|
|
51
|
+
* @returns true only for terminal quota, balance, credit, budget, or usage-limit wording.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isQuotaExceededError(detail: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Render a thrown value with its full `cause` chain and AggregateError
|
|
56
|
+
* members, so transport wrappers like undici's `TypeError: fetch failed`
|
|
57
|
+
* surface the underlying failure instead of masking it. Plain structured
|
|
58
|
+
* failures render their own data-backed `message`. Diagnostic-surface
|
|
59
|
+
* rendering only (messages, notices, logs) — never parse the result; route on
|
|
60
|
+
* {@link HarnessError.code}.
|
|
61
|
+
* @param value - the caught value (`unknown` in catch clauses).
|
|
62
|
+
* @returns the outermost message first, each cause appended with `: ` (skipped
|
|
63
|
+
* when it repeats the wrapper message verbatim), and AggregateError members
|
|
64
|
+
* bracketed and `; `-joined.
|
|
65
|
+
*/
|
|
66
|
+
export declare function errorChain(value: unknown): string;
|
|
67
|
+
/**
|
|
68
|
+
* Narrow an arbitrary thrown value to a HarnessError (for `instanceof` at runtime boundaries).
|
|
69
|
+
* @param value - the caught value (`unknown` in catch clauses).
|
|
70
|
+
* @returns true only for real instances; duck-typed or cross-realm errors do not narrow.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isHarnessError(value: unknown): value is HarnessError;
|
|
73
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness error base with a stable machine-routable code and chained cause.
|
|
3
|
+
* Package errors extend it so tool results and replay can retain failure class.
|
|
4
|
+
* @module @stackstackstack/dsh-llm/error
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Base class for all harness errors. Carries a `code` (stable, programmatic —
|
|
8
|
+
* e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
|
|
9
|
+
* human-readable `message`, and supports `cause` chaining via the standard
|
|
10
|
+
* `ErrorOptions`. `name` defaults to the subclass constructor name.
|
|
11
|
+
*/
|
|
12
|
+
export class HarnessError extends Error {
|
|
13
|
+
/** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
|
|
14
|
+
code;
|
|
15
|
+
constructor(message, code, options) {
|
|
16
|
+
super(message, options);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.name = new.target.name;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Canonical provider-neutral code for a model request rejected because its context window was exceeded. */
|
|
22
|
+
export const CONTEXT_WINDOW_EXCEEDED_CODE = 'CONTEXT_WINDOW_EXCEEDED';
|
|
23
|
+
/** Canonical provider-neutral code for an exhausted account quota or balance. */
|
|
24
|
+
export const QUOTA_EXCEEDED_CODE = 'QUOTA';
|
|
25
|
+
/**
|
|
26
|
+
* Canonical provider-neutral code for a response that completed normally but
|
|
27
|
+
* carried no content blocks at all. Providers occasionally emit a degenerate
|
|
28
|
+
* completion (a terminal stop with zero output); adapters classify it as this
|
|
29
|
+
* failure instead of yielding an empty assistant message, because an empty
|
|
30
|
+
* message silently ends the turn with nothing for the user or the loop to act
|
|
31
|
+
* on. The attempt produced nothing durable, so retry policy treats it as safe
|
|
32
|
+
* to repeat.
|
|
33
|
+
*/
|
|
34
|
+
export const EMPTY_RESPONSE_CODE = 'EMPTY_RESPONSE';
|
|
35
|
+
/**
|
|
36
|
+
* Canonical provider-neutral code for a credential that was supplied but
|
|
37
|
+
* cannot be used — malformed rather than absent. Distinct from
|
|
38
|
+
* `MISSING_CREDENTIAL` because the fix differs: correct the stored value
|
|
39
|
+
* rather than supply one. Deliberately outside the default retryable set —
|
|
40
|
+
* a malformed credential fails identically on every attempt.
|
|
41
|
+
*/
|
|
42
|
+
export const INVALID_CREDENTIAL_CODE = 'INVALID_CREDENTIAL';
|
|
43
|
+
/** Structured codes and plain phrases that explicitly name a context bound being exceeded. */
|
|
44
|
+
const STRUCTURED_CONTEXT_OVERFLOW = new RegExp(String.raw `(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]`
|
|
45
|
+
+ String.raw `(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, 'i');
|
|
46
|
+
/** Request-size wording that ties "too large" directly to model context capacity. */
|
|
47
|
+
const TOO_LARGE_FOR_CONTEXT = new RegExp(String.raw `\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?`
|
|
48
|
+
+ String.raw `too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?`
|
|
49
|
+
+ String.raw `(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, 'i');
|
|
50
|
+
/** "Exceeds" wording is safe only when its object is explicitly the model context. */
|
|
51
|
+
const EXCEEDS_MODEL_CONTEXT = new RegExp(String.raw `\b(?:input|prompt|request|messages?)\b.{0,40}`
|
|
52
|
+
+ String.raw `\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}`
|
|
53
|
+
+ String.raw `\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, 'i');
|
|
54
|
+
/**
|
|
55
|
+
* Recognize the context-overflow wording used by OpenAI-compatible providers
|
|
56
|
+
* and library adapters. Adapters pass all available provider code, type, and
|
|
57
|
+
* message text so both thrown and in-band delivery styles share one classifier.
|
|
58
|
+
* @param detail - provider error code/type/message text joined into one string.
|
|
59
|
+
* @returns true when the detail identifies a request exceeding the model context window.
|
|
60
|
+
*/
|
|
61
|
+
export function isContextWindowExceededError(detail) {
|
|
62
|
+
return STRUCTURED_CONTEXT_OVERFLOW.test(detail)
|
|
63
|
+
|| /\b(?:maximum|max)(?:\s+(?:allowed|supported))?\s+context\s+(?:length|window)\b/i.test(detail)
|
|
64
|
+
|| TOO_LARGE_FOR_CONTEXT.test(detail)
|
|
65
|
+
|| /\b(?:input|prompt|request)\s+(?:is\s+)?too\s+(?:long|large)\s+for\s+(?:this|the)\s+model\b/i.test(detail)
|
|
66
|
+
|| EXCEEDS_MODEL_CONTEXT.test(detail);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Recognize provider wording that identifies an exhausted account quota rather
|
|
70
|
+
* than a transient request-rate limit.
|
|
71
|
+
* @param detail - provider error code/type/message text joined into one string.
|
|
72
|
+
* @returns true only for terminal quota, balance, credit, budget, or usage-limit wording.
|
|
73
|
+
*/
|
|
74
|
+
export function isQuotaExceededError(detail) {
|
|
75
|
+
return /\binsufficient[\s_-]+(?:quota|balance|credits?)\b/i.test(detail)
|
|
76
|
+
|| /\b(?:quota|usage[\s_-]+limit)[\s_-]+(?:exceeded|exhausted|reached)\b/i.test(detail)
|
|
77
|
+
|| /\bexceed(?:ed|s)?[\s_-]+(?:(?:your|the)[\s_-]+)?(?:current[\s_-]+)?quota\b/i.test(detail)
|
|
78
|
+
|| /\b(?:balance|credits?)[\s_-]+(?:exhausted|depleted)\b/i.test(detail)
|
|
79
|
+
|| /\bout[\s_-]+of[\s_-]+(?:credits?|budget)\b/i.test(detail);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Render a thrown value with its full `cause` chain and AggregateError
|
|
83
|
+
* members, so transport wrappers like undici's `TypeError: fetch failed`
|
|
84
|
+
* surface the underlying failure instead of masking it. Plain structured
|
|
85
|
+
* failures render their own data-backed `message`. Diagnostic-surface
|
|
86
|
+
* rendering only (messages, notices, logs) — never parse the result; route on
|
|
87
|
+
* {@link HarnessError.code}.
|
|
88
|
+
* @param value - the caught value (`unknown` in catch clauses).
|
|
89
|
+
* @returns the outermost message first, each cause appended with `: ` (skipped
|
|
90
|
+
* when it repeats the wrapper message verbatim), and AggregateError members
|
|
91
|
+
* bracketed and `; `-joined.
|
|
92
|
+
*/
|
|
93
|
+
export function errorChain(value) {
|
|
94
|
+
// Tracks the active recursion path (entries removed on exit), so only true
|
|
95
|
+
// cycles are flagged and a diamond-shared cause still renders in full.
|
|
96
|
+
const path = new Set();
|
|
97
|
+
const render = (current) => {
|
|
98
|
+
if (path.has(current))
|
|
99
|
+
return '<circular cause>';
|
|
100
|
+
path.add(current);
|
|
101
|
+
try {
|
|
102
|
+
if (!(current instanceof Error)) {
|
|
103
|
+
if (typeof current === 'object' && current !== null) {
|
|
104
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, 'message');
|
|
105
|
+
if (descriptor !== undefined && 'value' in descriptor && typeof descriptor.value === 'string') {
|
|
106
|
+
return descriptor.value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return String(current);
|
|
110
|
+
}
|
|
111
|
+
const message = current.message === '' ? current.name : current.message;
|
|
112
|
+
const members = current instanceof AggregateError && current.errors.length > 0
|
|
113
|
+
? ` [${current.errors.map(render).join('; ')}]`
|
|
114
|
+
: '';
|
|
115
|
+
const causeText = current.cause === undefined || current.cause === null
|
|
116
|
+
? ''
|
|
117
|
+
: render(current.cause);
|
|
118
|
+
// Wrappers like `new HarnessError(String(value), code, { cause: value })`
|
|
119
|
+
// repeat their cause verbatim; rendering it again would only add noise.
|
|
120
|
+
const cause = causeText === '' || causeText === message ? '' : `: ${causeText}`;
|
|
121
|
+
return `${message}${members}${cause}`;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Only hostile coercion or hostile accessors (a throwing toString /
|
|
125
|
+
// Symbol.toPrimitive on a non-Error, or a throwing message/name/cause/
|
|
126
|
+
// errors getter on an Error subclass): this renderer feeds UI notices
|
|
127
|
+
// and logs, so nothing may escape. Inner frames catch their own throws,
|
|
128
|
+
// so only the hostile node collapses, not the whole chain.
|
|
129
|
+
return '<unrenderable value>';
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
path.delete(current);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
return render(value);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Narrow an arbitrary thrown value to a HarnessError (for `instanceof` at runtime boundaries).
|
|
139
|
+
* @param value - the caught value (`unknown` in catch clauses).
|
|
140
|
+
* @returns true only for real instances; duck-typed or cross-realm errors do not narrow.
|
|
141
|
+
*/
|
|
142
|
+
export function isHarnessError(value) {
|
|
143
|
+
return value instanceof HarnessError;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=error.js.map
|