deepline 0.1.182 → 0.1.183

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.
Files changed (66) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +203 -6
  2. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +8 -1
  3. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +633 -107
  4. package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +3 -2
  5. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +1747 -1890
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +116 -0
  7. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +542 -78
  8. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +292 -11
  9. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/run-work-dispatcher.ts +519 -0
  10. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +2381 -0
  11. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +18 -5
  12. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/work-budget.ts +130 -0
  13. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/worker-platform-budget.ts +39 -0
  14. package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +2 -0
  15. package/dist/bundling-sources/sdk/src/client.ts +41 -0
  16. package/dist/bundling-sources/sdk/src/http.ts +23 -8
  17. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +56 -3
  18. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  19. package/dist/bundling-sources/sdk/src/types.ts +2 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +132 -10
  21. package/dist/bundling-sources/shared_libs/play-runtime/auth-scope-resolver.ts +92 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -4
  23. package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +8 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +101 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1605 -281
  26. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +105 -6
  27. package/dist/bundling-sources/shared_libs/play-runtime/dedup-backend.ts +0 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +1 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/durability-store.ts +4 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +116 -16
  31. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +186 -20
  32. package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +2 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +133 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +245 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +5 -5
  36. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +14 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +52 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +41 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +3 -2
  40. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +124 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +6 -2
  42. package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +48 -2
  43. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +158 -15
  44. package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +45 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +9 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2037 -262
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +42 -0
  48. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +183 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +88 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +8 -1
  51. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +103 -0
  52. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +19 -1
  53. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +343 -0
  54. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +43 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +437 -0
  57. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +83 -6
  58. package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +24 -17
  59. package/dist/cli/index.js +76 -17
  60. package/dist/cli/index.mjs +76 -17
  61. package/dist/index.d.mts +2 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.js +63 -9
  64. package/dist/index.mjs +63 -9
  65. package/dist/plays/bundle-play-file.mjs +2 -2
  66. package/package.json +1 -1
@@ -0,0 +1,343 @@
1
+ export const PLAY_RUNTIME_TEST_FAULT_HEADER = 'x-deepline-test-fault';
2
+
3
+ export type RuntimeTestFaultName =
4
+ | 'receipt_complete_write_fail'
5
+ | 'receipt_fail_write_fail'
6
+ | 'worker_receipt_complete_write_fail';
7
+
8
+ export type RuntimeTestFaultRegistry = {
9
+ consume(name: RuntimeTestFaultName): boolean;
10
+ remaining(): Record<string, number>;
11
+ };
12
+
13
+ type ParsedRuntimeTestFaults =
14
+ | { ok: true; registry: RuntimeTestFaultRegistry | null }
15
+ | { ok: false; status: 400 | 403; error: string };
16
+
17
+ type RuntimeTestSeamContext = {
18
+ internalTokenHeader?: string | null;
19
+ syntheticRunHeader?: string | null;
20
+ };
21
+
22
+ type ParsedRuntimeTestFaultCounts =
23
+ | {
24
+ ok: true;
25
+ counts: Record<string, number> | null;
26
+ headerValue: string | null;
27
+ }
28
+ | { ok: false; status: 400 | 403; error: string };
29
+
30
+ type ValidatedRuntimeTestFaultHeader =
31
+ | { ok: true; headerValue: string | null }
32
+ | { ok: false; status: 400 | 403; error: string };
33
+
34
+ export type RuntimeTestPolicyOverrides = {
35
+ receiptLeaseTtlMs?: number;
36
+ sheetAttemptLeaseMs?: number;
37
+ heartbeatIntervalMs?: number;
38
+ workBudgetYieldLimits?: Partial<
39
+ Record<
40
+ | 'elapsed'
41
+ | 'subrequest'
42
+ | 'provider'
43
+ | 'tool'
44
+ | 'receipt'
45
+ | 'sheet'
46
+ | 'log'
47
+ | 'egress',
48
+ number
49
+ >
50
+ >;
51
+ batchGraceMs?: number;
52
+ };
53
+
54
+ type ValidatedRuntimeTestPolicyOverrides =
55
+ | { ok: true; overrides: RuntimeTestPolicyOverrides | null }
56
+ | { ok: false; status: 400 | 403; error: string };
57
+
58
+ const SUPPORTED_RUNTIME_TEST_FAULTS = new Set<RuntimeTestFaultName>([
59
+ 'receipt_complete_write_fail',
60
+ 'receipt_fail_write_fail',
61
+ 'worker_receipt_complete_write_fail',
62
+ ]);
63
+
64
+ const RUNTIME_TEST_POLICY_MS_FIELDS = new Set([
65
+ 'receiptLeaseTtlMs',
66
+ 'sheetAttemptLeaseMs',
67
+ 'heartbeatIntervalMs',
68
+ 'batchGraceMs',
69
+ ]);
70
+
71
+ const RUNTIME_TEST_POLICY_WORK_BUDGET_KEYS = new Set([
72
+ 'elapsed',
73
+ 'subrequest',
74
+ 'provider',
75
+ 'tool',
76
+ 'receipt',
77
+ 'sheet',
78
+ 'log',
79
+ 'egress',
80
+ ]);
81
+
82
+ const MAX_RUNTIME_TEST_POLICY_MS = 10 * 60_000;
83
+ const MAX_RUNTIME_TEST_POLICY_YIELD_LIMIT = 1_000_000;
84
+
85
+ function isProductionRuntime(): boolean {
86
+ const deeplineEnv = process.env.DEEPLINE_ENV?.trim().toLowerCase();
87
+ if (deeplineEnv === 'production' || deeplineEnv === 'prod') return true;
88
+ const vercelEnv = process.env.VERCEL_ENV?.trim().toLowerCase();
89
+ if (vercelEnv === 'production') return true;
90
+ if (vercelEnv === 'preview' || vercelEnv === 'development') return false;
91
+ return process.env.NODE_ENV === 'production';
92
+ }
93
+
94
+ function testRuntimeSeamsEnabled(): boolean {
95
+ return process.env.DEEPLINE_TEST_RUNTIME_SEAMS === '1';
96
+ }
97
+
98
+ function internalTokenAllowsRuntimeSeams(
99
+ context: RuntimeTestSeamContext | undefined,
100
+ ): boolean {
101
+ const deeplineEnv = process.env.DEEPLINE_ENV?.trim().toLowerCase();
102
+ if (deeplineEnv === 'prod') return false;
103
+ if (deeplineEnv === 'production') return false;
104
+ const expected = process.env.DEEPLINE_INTERNAL_TOKEN?.trim();
105
+ const actual = context?.internalTokenHeader?.trim();
106
+ return Boolean(expected && actual && expected === actual);
107
+ }
108
+
109
+ function runtimeTestSeamsAuthorized(
110
+ context: RuntimeTestSeamContext | undefined,
111
+ ): boolean {
112
+ return (
113
+ testRuntimeSeamsEnabled() ||
114
+ internalTokenAllowsRuntimeSeams(context) ||
115
+ Boolean(
116
+ context?.syntheticRunHeader?.trim() &&
117
+ context.syntheticRunHeader.trim() !== '0',
118
+ )
119
+ );
120
+ }
121
+
122
+ function parseRuntimeTestFaultHeader(
123
+ rawHeader: string,
124
+ ): Map<RuntimeTestFaultName, number> | { error: string } {
125
+ const faults = new Map<RuntimeTestFaultName, number>();
126
+ for (const rawPart of rawHeader.split(',')) {
127
+ const part = rawPart.trim();
128
+ if (!part) continue;
129
+ const [rawName, rawCount] = part.split(':');
130
+ const name = rawName?.trim() as RuntimeTestFaultName | undefined;
131
+ if (!name || !SUPPORTED_RUNTIME_TEST_FAULTS.has(name)) {
132
+ return { error: `Unsupported runtime test fault "${rawName ?? part}".` };
133
+ }
134
+ const count =
135
+ rawCount === undefined || rawCount.trim() === ''
136
+ ? 1
137
+ : Number.parseInt(rawCount.trim(), 10);
138
+ if (!Number.isInteger(count) || count < 1) {
139
+ return {
140
+ error: `Runtime test fault "${name}" count must be a positive integer.`,
141
+ };
142
+ }
143
+ faults.set(name, (faults.get(name) ?? 0) + count);
144
+ }
145
+ return faults;
146
+ }
147
+
148
+ function readPositiveIntegerField(input: {
149
+ value: unknown;
150
+ path: string;
151
+ max: number;
152
+ }): number | { error: string } {
153
+ if (
154
+ typeof input.value !== 'number' ||
155
+ !Number.isFinite(input.value) ||
156
+ !Number.isInteger(input.value) ||
157
+ input.value < 1 ||
158
+ input.value > input.max
159
+ ) {
160
+ return {
161
+ error: `${input.path} must be an integer between 1 and ${input.max}.`,
162
+ };
163
+ }
164
+ return input.value;
165
+ }
166
+
167
+ function parseRuntimeTestPolicyOverrides(
168
+ value: unknown,
169
+ ): RuntimeTestPolicyOverrides | { error: string } | null {
170
+ if (value === undefined || value === null) {
171
+ return null;
172
+ }
173
+ if (typeof value !== 'object' || Array.isArray(value)) {
174
+ return { error: 'testPolicyOverrides must be a JSON object.' };
175
+ }
176
+ const record = value as Record<string, unknown>;
177
+ const unknownKeys = Object.keys(record).filter(
178
+ (key) =>
179
+ !RUNTIME_TEST_POLICY_MS_FIELDS.has(key) &&
180
+ key !== 'workBudgetYieldLimits',
181
+ );
182
+ if (unknownKeys.length > 0) {
183
+ return {
184
+ error: `Unsupported testPolicyOverrides field "${unknownKeys[0]}".`,
185
+ };
186
+ }
187
+
188
+ const overrides: RuntimeTestPolicyOverrides = {};
189
+ for (const field of RUNTIME_TEST_POLICY_MS_FIELDS) {
190
+ if (!(field in record)) continue;
191
+ const parsed = readPositiveIntegerField({
192
+ value: record[field],
193
+ path: `testPolicyOverrides.${field}`,
194
+ max: MAX_RUNTIME_TEST_POLICY_MS,
195
+ });
196
+ if (typeof parsed !== 'number') return parsed;
197
+ (overrides as Record<string, number>)[field] = parsed;
198
+ }
199
+
200
+ if ('workBudgetYieldLimits' in record) {
201
+ const rawLimits = record.workBudgetYieldLimits;
202
+ if (
203
+ rawLimits === null ||
204
+ typeof rawLimits !== 'object' ||
205
+ Array.isArray(rawLimits)
206
+ ) {
207
+ return {
208
+ error: 'testPolicyOverrides.workBudgetYieldLimits must be an object.',
209
+ };
210
+ }
211
+ const limits: NonNullable<
212
+ RuntimeTestPolicyOverrides['workBudgetYieldLimits']
213
+ > = {};
214
+ for (const [key, rawLimit] of Object.entries(
215
+ rawLimits as Record<string, unknown>,
216
+ )) {
217
+ if (!RUNTIME_TEST_POLICY_WORK_BUDGET_KEYS.has(key)) {
218
+ return {
219
+ error: `Unsupported testPolicyOverrides.workBudgetYieldLimits field "${key}".`,
220
+ };
221
+ }
222
+ const parsed = readPositiveIntegerField({
223
+ value: rawLimit,
224
+ path: `testPolicyOverrides.workBudgetYieldLimits.${key}`,
225
+ max: MAX_RUNTIME_TEST_POLICY_YIELD_LIMIT,
226
+ });
227
+ if (typeof parsed !== 'number') return parsed;
228
+ limits[key as keyof typeof limits] = parsed;
229
+ }
230
+ if (Object.keys(limits).length > 0) {
231
+ overrides.workBudgetYieldLimits = limits;
232
+ }
233
+ }
234
+
235
+ return Object.keys(overrides).length > 0 ? overrides : null;
236
+ }
237
+
238
+ export function validateRuntimeTestPolicyOverrides(input: {
239
+ value: unknown;
240
+ internalTokenHeader?: string | null;
241
+ syntheticRunHeader?: string | null;
242
+ }): ValidatedRuntimeTestPolicyOverrides {
243
+ if (input.value === undefined || input.value === null) {
244
+ return { ok: true, overrides: null };
245
+ }
246
+ if (!runtimeTestSeamsAuthorized(input) || isProductionRuntime()) {
247
+ return {
248
+ ok: false,
249
+ status: 403,
250
+ error:
251
+ 'Runtime test policy overrides are disabled. Set DEEPLINE_TEST_RUNTIME_SEAMS=1 only in dev/preview/CI harnesses.',
252
+ };
253
+ }
254
+ const parsed = parseRuntimeTestPolicyOverrides(input.value);
255
+ if (parsed === null) {
256
+ return { ok: true, overrides: null };
257
+ }
258
+ if ('error' in parsed) {
259
+ return { ok: false, status: 400, error: parsed.error };
260
+ }
261
+ return { ok: true, overrides: parsed };
262
+ }
263
+
264
+ export function readRuntimeTestFaultRegistry(input: {
265
+ headerValue: string | null;
266
+ internalTokenHeader?: string | null;
267
+ syntheticRunHeader?: string | null;
268
+ }): ParsedRuntimeTestFaults {
269
+ const headerValue = input.headerValue?.trim();
270
+ if (!headerValue) {
271
+ return { ok: true, registry: null };
272
+ }
273
+ if (!runtimeTestSeamsAuthorized(input) || isProductionRuntime()) {
274
+ return {
275
+ ok: false,
276
+ status: 403,
277
+ error:
278
+ 'Runtime test seams are disabled. Set DEEPLINE_TEST_RUNTIME_SEAMS=1 only in dev/preview/CI harnesses.',
279
+ };
280
+ }
281
+ const parsed = parseRuntimeTestFaultHeader(headerValue);
282
+ if ('error' in parsed) {
283
+ return { ok: false, status: 400, error: parsed.error };
284
+ }
285
+ return {
286
+ ok: true,
287
+ registry: {
288
+ consume(name) {
289
+ const count = parsed.get(name) ?? 0;
290
+ if (count <= 0) return false;
291
+ if (count === 1) {
292
+ parsed.delete(name);
293
+ } else {
294
+ parsed.set(name, count - 1);
295
+ }
296
+ return true;
297
+ },
298
+ remaining() {
299
+ return Object.fromEntries(parsed.entries());
300
+ },
301
+ },
302
+ };
303
+ }
304
+
305
+ export function validateRuntimeTestFaultHeader(input: {
306
+ headerValue: string | null;
307
+ internalTokenHeader?: string | null;
308
+ syntheticRunHeader?: string | null;
309
+ }): ValidatedRuntimeTestFaultHeader {
310
+ const parsed = parseRuntimeTestFaultCounts(input);
311
+ if (parsed.ok === false) {
312
+ return { ok: false, status: parsed.status, error: parsed.error };
313
+ }
314
+ return { ok: true, headerValue: parsed.headerValue };
315
+ }
316
+
317
+ export function parseRuntimeTestFaultCounts(input: {
318
+ headerValue: string | null;
319
+ internalTokenHeader?: string | null;
320
+ syntheticRunHeader?: string | null;
321
+ }): ParsedRuntimeTestFaultCounts {
322
+ const headerValue = input.headerValue?.trim();
323
+ if (!headerValue) {
324
+ return { ok: true, counts: null, headerValue: null };
325
+ }
326
+ if (!runtimeTestSeamsAuthorized(input) || isProductionRuntime()) {
327
+ return {
328
+ ok: false,
329
+ status: 403,
330
+ error:
331
+ 'Runtime test seams are disabled. Set DEEPLINE_TEST_RUNTIME_SEAMS=1 only in dev/preview/CI harnesses.',
332
+ };
333
+ }
334
+ const parsed = parseRuntimeTestFaultHeader(headerValue);
335
+ if ('error' in parsed) {
336
+ return { ok: false, status: 400, error: parsed.error };
337
+ }
338
+ return {
339
+ ok: true,
340
+ counts: Object.fromEntries(parsed.entries()),
341
+ headerValue,
342
+ };
343
+ }
@@ -12,6 +12,17 @@ export const TOOL_EXECUTE_TRANSPORT_RETRY_DELAY_MS = 1_000;
12
12
  export const TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS = 1_000;
13
13
  export const TOOL_EXECUTE_RETRY_DELAY_MAX_MS = 5_000;
14
14
  export const TOOL_EXECUTE_BARE_RATE_LIMIT_BACKPRESSURE_MS = 60_000;
15
+ export const TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE = 'AUTH_SCOPE_CHANGED';
16
+
17
+ export class ToolExecuteAuthScopeChangedError extends Error {
18
+ readonly code = TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE;
19
+ readonly status = 409;
20
+
21
+ constructor(message = 'Tool execution auth scope changed.') {
22
+ super(message);
23
+ this.name = 'ToolExecuteAuthScopeChangedError';
24
+ }
25
+ }
15
26
 
16
27
  export type ToolExecuteHttpRetryDecision = {
17
28
  retryable: boolean;
@@ -42,6 +53,35 @@ export type ToolExecuteHttpFailureAttemptTracker = {
42
53
  }): number;
43
54
  };
44
55
 
56
+ function parseJsonObject(text: string): Record<string, unknown> | null {
57
+ if (!text.trim()) return null;
58
+ try {
59
+ const parsed = JSON.parse(text) as unknown;
60
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
61
+ ? (parsed as Record<string, unknown>)
62
+ : null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ export function parseToolExecuteAuthScopeChangedError(input: {
69
+ status: number;
70
+ bodyText: string;
71
+ }): ToolExecuteAuthScopeChangedError | null {
72
+ if (input.status !== 409) return null;
73
+ const body = parseJsonObject(input.bodyText);
74
+ const code = typeof body?.code === 'string' ? body.code.trim() : '';
75
+ if (code !== TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE) return null;
76
+ const message =
77
+ typeof body?.error === 'string' && body.error.trim()
78
+ ? body.error.trim()
79
+ : typeof body?.message === 'string' && body.message.trim()
80
+ ? body.message.trim()
81
+ : undefined;
82
+ return new ToolExecuteAuthScopeChangedError(message);
83
+ }
84
+
45
85
  function decideToolExecuteHttpRetry(input: {
46
86
  status: number;
47
87
  hardBillingFailure?: boolean;
@@ -164,6 +204,9 @@ export function classifyToolExecuteHttpFailure(input: {
164
204
  });
165
205
  const shouldRetry =
166
206
  retryDecision.retryable && input.attempt < retryDecision.attemptCap;
207
+ error.receiptFailureKind = retryDecision.retryable
208
+ ? 'repairable'
209
+ : 'terminal';
167
210
  const retryAfterMs = parseToolExecuteRetryAfterMs(
168
211
  input.retryAfterHeader,
169
212
  input.nowMs,
@@ -1,17 +1,22 @@
1
+ import type { WorkReceiptFailureKind } from './work-receipts';
2
+
1
3
  export class ToolHttpError extends Error {
2
4
  readonly billing: Record<string, unknown> | null;
3
5
  /** HTTP status of the failed tool-execute response (e.g. 429, 502). */
4
6
  readonly status: number;
7
+ receiptFailureKind: WorkReceiptFailureKind | null;
5
8
 
6
9
  constructor(
7
10
  message: string,
8
11
  billing: Record<string, unknown> | null,
9
12
  status: number,
13
+ receiptFailureKind: WorkReceiptFailureKind | null = null,
10
14
  ) {
11
15
  super(message);
12
16
  this.name = 'ToolHttpError';
13
17
  this.billing = billing;
14
18
  this.status = status;
19
+ this.receiptFailureKind = receiptFailureKind;
15
20
  }
16
21
  }
17
22
 
@@ -247,3 +252,9 @@ export function isHardBillingToolHttpError(error: unknown): boolean {
247
252
  export function isRateLimitToolHttpError(error: unknown): boolean {
248
253
  return error instanceof ToolHttpError && error.status === 429;
249
254
  }
255
+
256
+ export function getToolHttpErrorReceiptFailureKind(
257
+ error: unknown,
258
+ ): WorkReceiptFailureKind | null {
259
+ return error instanceof ToolHttpError ? error.receiptFailureKind : null;
260
+ }