deepline 0.1.301 → 0.1.302

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 (35) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +8 -0
  2. package/dist/bundling-sources/sdk/src/errors.ts +57 -15
  3. package/dist/bundling-sources/sdk/src/http.ts +82 -5
  4. package/dist/bundling-sources/sdk/src/index.ts +4 -0
  5. package/dist/bundling-sources/sdk/src/play.ts +29 -1
  6. package/dist/bundling-sources/sdk/src/release.ts +5 -3
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +120 -14
  8. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +10 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +59 -11
  10. package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +3 -3
  11. package/dist/bundling-sources/shared_libs/play-runtime/execution-plan.ts +0 -15
  12. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +3 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +3 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +136 -36
  15. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +17 -12
  16. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +210 -5
  17. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +4 -0
  18. package/dist/bundling-sources/shared_libs/plays/artifact-contract-version.ts +6 -0
  19. package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +4 -0
  20. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +159 -12
  21. package/dist/bundling-sources/shared_libs/plays/contracts.ts +69 -1
  22. package/dist/bundling-sources/shared_libs/tool-execution-error.ts +428 -0
  23. package/dist/cli/index.js +501 -179
  24. package/dist/cli/index.mjs +501 -179
  25. package/dist/index.d.mts +39 -18
  26. package/dist/index.d.ts +39 -18
  27. package/dist/index.js +394 -59
  28. package/dist/index.mjs +391 -59
  29. package/dist/plays/bundle-play-file.d.mts +4 -2
  30. package/dist/plays/bundle-play-file.d.ts +4 -2
  31. package/dist/plays/bundle-play-file.mjs +116 -19
  32. package/dist/{compiler-manifest-BjopNIqx.d.mts → tool-execution-error-9mH4i-mJ.d.mts} +79 -1
  33. package/dist/{compiler-manifest-BjopNIqx.d.ts → tool-execution-error-9mH4i-mJ.d.ts} +79 -1
  34. package/package.json +1 -1
  35. package/dist/bundling-sources/shared_libs/play-runtime/completed-receipt-cache.ts +0 -166
@@ -3,6 +3,7 @@ import {
3
3
  normalizeToolHttpErrorMessage,
4
4
  type ToolHttpError,
5
5
  } from './tool-http-errors';
6
+ import type { ToolExecutionErrorSchemaVersion } from '../tool-execution-error';
6
7
 
7
8
  export const TOOL_EXECUTE_TRANSIENT_HTTP_MAX_ATTEMPTS = 2;
8
9
  // A provider may need to finish the original request before it can replay the
@@ -296,6 +297,7 @@ export function classifyToolExecuteHttpFailure(input: {
296
297
  status: number;
297
298
  attempt: number;
298
299
  bodyText: string;
300
+ schemaVersion?: ToolExecutionErrorSchemaVersion;
299
301
  retryAfterHeader?: string | null;
300
302
  transientHttpRetrySafe?: boolean;
301
303
  providerLatencyMs?: number;
@@ -318,12 +320,20 @@ export function classifyToolExecuteHttpFailure(input: {
318
320
  hasRetryAfterHeader,
319
321
  transientHttpRetrySafe,
320
322
  });
323
+ const retryAfterMs = parseToolExecuteRetryAfterMs(
324
+ input.retryAfterHeader,
325
+ input.nowMs,
326
+ );
321
327
  const error = normalizeToolHttpErrorMessage({
322
328
  toolId: input.toolId,
323
329
  status: input.status,
324
330
  attempt: input.attempt,
325
331
  maxAttempts: initialRetryDecision.attemptCap,
326
332
  bodyText: input.bodyText,
333
+ schemaVersion: input.schemaVersion,
334
+ retryable: initialRetryDecision.retryable,
335
+ retryAfterMs:
336
+ input.status === 429 && hasRetryAfterHeader ? retryAfterMs : null,
327
337
  });
328
338
  if (
329
339
  typeof input.providerLatencyMs === 'number' &&
@@ -355,10 +365,6 @@ export function classifyToolExecuteHttpFailure(input: {
355
365
  ) {
356
366
  error.receiptFailureKind = 'repairable';
357
367
  }
358
- const retryAfterMs = parseToolExecuteRetryAfterMs(
359
- input.retryAfterHeader,
360
- input.nowMs,
361
- );
362
368
  const retryDelayMs =
363
369
  input.status === 429
364
370
  ? Math.min(
@@ -369,14 +375,14 @@ export function classifyToolExecuteHttpFailure(input: {
369
375
  ),
370
376
  )
371
377
  : retryDecision.reason === 'idempotency_in_progress' &&
372
- !hasRetryAfterHeader
373
- ? idempotencyInProgressRetryDelayMs(input.attempt)
374
- : retryDecision.reason === 'gateway_invocation_in_progress' &&
375
378
  !hasRetryAfterHeader
376
379
  ? idempotencyInProgressRetryDelayMs(input.attempt)
377
- : retryAfterMs > 0
378
- ? Math.min(TOOL_EXECUTE_RETRY_DELAY_MAX_MS, retryAfterMs)
379
- : TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS;
380
+ : retryDecision.reason === 'gateway_invocation_in_progress' &&
381
+ !hasRetryAfterHeader
382
+ ? idempotencyInProgressRetryDelayMs(input.attempt)
383
+ : retryAfterMs > 0
384
+ ? Math.min(TOOL_EXECUTE_RETRY_DELAY_MAX_MS, retryAfterMs)
385
+ : TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS;
380
386
  return {
381
387
  ...retryDecision,
382
388
  error,
@@ -395,7 +401,6 @@ export function classifyToolExecuteHttpFailure(input: {
395
401
  : TOOL_EXECUTE_BARE_RATE_LIMIT_BACKPRESSURE_MS
396
402
  : null,
397
403
  chargeRetryBudget:
398
- shouldRetry &&
399
- retryDecision.reason !== 'gateway_invocation_in_progress',
404
+ shouldRetry && retryDecision.reason !== 'gateway_invocation_in_progress',
400
405
  };
401
406
  }
@@ -1,5 +1,50 @@
1
1
  import type { WorkReceiptFailureKind } from './work-receipts';
2
+ import {
3
+ LEGACY_TOOL_EXECUTION_ERROR_SCHEMA_VERSION,
4
+ ToolExecutionError,
5
+ deserializeToolExecutionFailure,
6
+ isProviderTransientFailure,
7
+ normalizeToolExecutionCategory,
8
+ normalizeToolExecutionOrigin,
9
+ type ToolExecutionErrorSchemaVersion,
10
+ type ToolExecutionErrorOptions,
11
+ } from '../tool-execution-error';
2
12
 
13
+ const TOOL_HTTP_ERROR_BRAND = Symbol.for('deepline.tool-http-error.v1');
14
+
15
+ type ToolHttpErrorConstructorInput = {
16
+ message: string;
17
+ billing: Record<string, unknown> | null;
18
+ status: number;
19
+ receiptFailureKind: WorkReceiptFailureKind;
20
+ options: Partial<ToolExecutionErrorOptions>;
21
+ };
22
+
23
+ function applyToolHttpErrorFields(
24
+ error: {
25
+ name: string;
26
+ billing: Record<string, unknown> | null;
27
+ status: number;
28
+ receiptFailureKind: WorkReceiptFailureKind;
29
+ },
30
+ input: ToolHttpErrorConstructorInput,
31
+ ): void {
32
+ error.name = 'ToolHttpError';
33
+ error.billing = input.billing;
34
+ error.status = input.status;
35
+ error.receiptFailureKind = input.receiptFailureKind;
36
+ Object.defineProperty(error, TOOL_HTTP_ERROR_BRAND, {
37
+ configurable: false,
38
+ enumerable: false,
39
+ writable: false,
40
+ value: true,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Historical play-runtime HTTP error. Existing artifacts must keep this exact
46
+ * Error prototype; typed errors are an explicit artifact-contract opt-in.
47
+ */
3
48
  export class ToolHttpError extends Error {
4
49
  readonly billing: Record<string, unknown> | null;
5
50
  /** HTTP status of the failed tool-execute response (e.g. 429, 502). */
@@ -11,15 +56,89 @@ export class ToolHttpError extends Error {
11
56
  billing: Record<string, unknown> | null,
12
57
  status: number,
13
58
  receiptFailureKind: WorkReceiptFailureKind = 'terminal',
59
+ _options: Partial<ToolExecutionErrorOptions> = {},
14
60
  ) {
15
61
  super(message);
16
- this.name = 'ToolHttpError';
17
62
  this.billing = billing;
18
63
  this.status = status;
19
64
  this.receiptFailureKind = receiptFailureKind;
65
+ applyToolHttpErrorFields(this, {
66
+ message,
67
+ billing,
68
+ status,
69
+ receiptFailureKind,
70
+ options: _options,
71
+ });
72
+ }
73
+
74
+ static [Symbol.hasInstance](value: unknown): boolean {
75
+ return Boolean(
76
+ value &&
77
+ typeof value === 'object' &&
78
+ (value as Record<symbol, unknown>)[TOOL_HTTP_ERROR_BRAND] === true,
79
+ );
20
80
  }
21
81
  }
22
82
 
83
+ class StructuredToolHttpError extends ToolExecutionError {
84
+ readonly billing: Record<string, unknown> | null;
85
+ readonly status: number;
86
+ receiptFailureKind: WorkReceiptFailureKind;
87
+
88
+ constructor(input: ToolHttpErrorConstructorInput) {
89
+ super(input.message, {
90
+ toolId: input.options.toolId ?? 'unknown_tool',
91
+ provider: input.options.provider ?? null,
92
+ operation: input.options.operation ?? null,
93
+ code: input.options.code ?? null,
94
+ origin: input.options.origin ?? 'unknown',
95
+ category: input.options.category ?? 'unknown',
96
+ retryable: input.options.retryable === true,
97
+ statusCode:
98
+ input.options.statusCode === undefined
99
+ ? input.status
100
+ : input.options.statusCode,
101
+ requestId: input.options.requestId ?? null,
102
+ retryAfterMs: input.options.retryAfterMs ?? null,
103
+ networkKind: input.options.networkKind ?? null,
104
+ networkScope: input.options.networkScope ?? null,
105
+ });
106
+ this.billing = input.billing;
107
+ this.status = input.status;
108
+ this.receiptFailureKind = input.receiptFailureKind;
109
+ applyToolHttpErrorFields(this, input);
110
+ this.name = isProviderTransientFailure(this)
111
+ ? 'ProviderTransientError'
112
+ : 'ToolExecutionError';
113
+ }
114
+ }
115
+
116
+ export function createToolHttpError(
117
+ schemaVersion: ToolExecutionErrorSchemaVersion,
118
+ message: string,
119
+ billing: Record<string, unknown> | null,
120
+ status: number,
121
+ receiptFailureKind: WorkReceiptFailureKind = 'terminal',
122
+ options: Partial<ToolExecutionErrorOptions> = {},
123
+ ): ToolHttpError {
124
+ if (schemaVersion === LEGACY_TOOL_EXECUTION_ERROR_SCHEMA_VERSION) {
125
+ return new ToolHttpError(
126
+ message,
127
+ billing,
128
+ status,
129
+ receiptFailureKind,
130
+ options,
131
+ );
132
+ }
133
+ return new StructuredToolHttpError({
134
+ message,
135
+ billing,
136
+ status,
137
+ receiptFailureKind,
138
+ options,
139
+ }) as ToolHttpError;
140
+ }
141
+
23
142
  function formatCreditAmount(value: unknown): string {
24
143
  if (typeof value !== 'number' || !Number.isFinite(value)) {
25
144
  return String(value ?? '-');
@@ -222,7 +341,12 @@ export function normalizeToolHttpErrorMessage(input: {
222
341
  attempt: number;
223
342
  maxAttempts: number;
224
343
  bodyText: string;
344
+ retryable?: boolean;
345
+ retryAfterMs?: number | null;
346
+ schemaVersion?: ToolExecutionErrorSchemaVersion;
225
347
  }): ToolHttpError {
348
+ const schemaVersion =
349
+ input.schemaVersion ?? LEGACY_TOOL_EXECUTION_ERROR_SCHEMA_VERSION;
226
350
  let parsed: Record<string, unknown> | null = null;
227
351
  try {
228
352
  const candidate = JSON.parse(input.bodyText);
@@ -230,9 +354,64 @@ export function normalizeToolHttpErrorMessage(input: {
230
354
  } catch {
231
355
  parsed = null;
232
356
  }
357
+ const serializedFailure = parsed?.tool_error;
358
+ const hydratedFailure = deserializeToolExecutionFailure(
359
+ '',
360
+ serializedFailure,
361
+ schemaVersion,
362
+ );
363
+ const provider =
364
+ getStringField(parsed, 'provider') ??
365
+ hydratedFailure?.provider ??
366
+ input.toolId.split(/[._:./-]+/)[0]?.trim() ??
367
+ null;
368
+ const operation =
369
+ getStringField(parsed, 'operation') ??
370
+ hydratedFailure?.operation ??
371
+ input.toolId;
372
+ const code = getStringField(parsed, 'code') ?? hydratedFailure?.code ?? null;
373
+ const rawOrigin =
374
+ getStringField(parsed, 'failure_origin') ?? hydratedFailure?.origin;
375
+ const rawCategory =
376
+ getStringField(parsed, 'error_category') ?? hydratedFailure?.category;
377
+ const origin = normalizeToolExecutionOrigin(rawOrigin, {
378
+ provider,
379
+ operation,
380
+ });
381
+ const category = normalizeToolExecutionCategory(rawCategory);
382
+ const trustworthy = origin !== 'unknown' && category !== 'unknown';
383
+ const publicOptions: Partial<ToolExecutionErrorOptions> = {
384
+ toolId: input.toolId,
385
+ provider,
386
+ operation,
387
+ code,
388
+ origin,
389
+ category,
390
+ retryable: trustworthy
391
+ ? (hydratedFailure?.retryable ?? input.retryable === true)
392
+ : false,
393
+ statusCode: input.status,
394
+ requestId:
395
+ getStringField(parsed, 'request_id') ??
396
+ getStringField(parsed, 'requestId') ??
397
+ hydratedFailure?.requestId ??
398
+ null,
399
+ retryAfterMs: hydratedFailure?.retryAfterMs ?? input.retryAfterMs ?? null,
400
+ networkKind:
401
+ hydratedFailure?.networkKind ??
402
+ (category === 'network' && code?.toUpperCase().includes('TIMEOUT')
403
+ ? 'timeout'
404
+ : null),
405
+ networkScope:
406
+ hydratedFailure?.networkScope ??
407
+ (origin === 'provider' && category === 'network'
408
+ ? 'deepline_to_provider'
409
+ : null),
410
+ };
233
411
  const billing = getObjectField(parsed, 'billing');
234
412
  if (isInsufficientCreditsBilling(billing)) {
235
- return new ToolHttpError(
413
+ return createToolHttpError(
414
+ schemaVersion,
236
415
  `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatInsufficientCreditsMessage(
237
416
  {
238
417
  billing,
@@ -241,6 +420,14 @@ export function normalizeToolHttpErrorMessage(input: {
241
420
  )}`,
242
421
  billing,
243
422
  input.status,
423
+ 'terminal',
424
+ {
425
+ ...publicOptions,
426
+ origin: 'deepline',
427
+ category: 'billing',
428
+ code: publicOptions.code ?? 'INSUFFICIENT_CREDITS',
429
+ retryable: false,
430
+ },
244
431
  );
245
432
  }
246
433
  const hardBillingPayload = isHardBillingFailurePayload(billing)
@@ -249,7 +436,8 @@ export function normalizeToolHttpErrorMessage(input: {
249
436
  ? normalizeHardBillingPayload(parsed)
250
437
  : null;
251
438
  if (hardBillingPayload) {
252
- return new ToolHttpError(
439
+ return createToolHttpError(
440
+ schemaVersion,
253
441
  formatHardBillingFailureMessage({
254
442
  billing: hardBillingPayload,
255
443
  toolId: input.toolId,
@@ -259,9 +447,17 @@ export function normalizeToolHttpErrorMessage(input: {
259
447
  }),
260
448
  hardBillingPayload,
261
449
  input.status,
450
+ 'terminal',
451
+ {
452
+ ...publicOptions,
453
+ origin: 'deepline',
454
+ category: 'billing',
455
+ retryable: false,
456
+ },
262
457
  );
263
458
  }
264
- return new ToolHttpError(
459
+ return createToolHttpError(
460
+ schemaVersion,
265
461
  `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatPublicToolErrorPayload(
266
462
  {
267
463
  parsed,
@@ -271,6 +467,7 @@ export function normalizeToolHttpErrorMessage(input: {
271
467
  billing,
272
468
  input.status,
273
469
  receiptFailureKindForToolErrorPayload(parsed),
470
+ publicOptions,
274
471
  );
275
472
  }
276
473
 
@@ -281,10 +478,18 @@ export function extractErrorBilling(
281
478
  }
282
479
 
283
480
  export function isHardBillingToolHttpError(error: unknown): boolean {
284
- return (
481
+ if (
285
482
  error instanceof ToolHttpError &&
286
483
  (isInsufficientCreditsBilling(error.billing) ||
287
484
  isHardBillingFailurePayload(error.billing))
485
+ ) {
486
+ return true;
487
+ }
488
+ return (
489
+ error instanceof ToolExecutionError &&
490
+ error.origin === 'deepline' &&
491
+ error.category === 'billing' &&
492
+ error.code !== 'BILLING_UNAVAILABLE'
288
493
  );
289
494
  }
290
495
 
@@ -1,4 +1,5 @@
1
1
  import { isReusableWorkReceiptState } from './work-receipt-state-machine';
2
+ import type { ToolExecutionFailureV1 } from '../tool-execution-error';
2
3
 
3
4
  export type WorkReceiptFailureKind = 'terminal' | 'repairable';
4
5
 
@@ -37,6 +38,7 @@ export type WorkReceipt = {
37
38
  status: WorkReceiptStatus;
38
39
  output?: unknown;
39
40
  error?: string | null;
41
+ errorPayload?: ToolExecutionFailureV1 | null;
40
42
  failureKind?: WorkReceiptFailureKind | null;
41
43
  runId?: string | null;
42
44
  leaseId?: string | null;
@@ -89,6 +91,7 @@ export type WorkReceiptBatchFailCommand = {
89
91
  runId: string;
90
92
  key: string;
91
93
  error: string;
94
+ errorPayload?: ToolExecutionFailureV1 | null;
92
95
  failureKind?: WorkReceiptFailureKind | null;
93
96
  leaseId?: string | null;
94
97
  runAttempt?: number | null;
@@ -161,6 +164,7 @@ export type WorkReceiptStore = {
161
164
  failReceipt(
162
165
  input: WorkReceiptCommand & {
163
166
  error: string;
167
+ errorPayload?: ToolExecutionFailureV1 | null;
164
168
  failureKind?: WorkReceiptFailureKind | null;
165
169
  },
166
170
  ): Promise<WorkReceipt | null>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Immutable Play artifact ABI version shared by bundling and SDK release
3
+ * policy. Keep this module dependency-free: shared Play code must not import
4
+ * the SDK release manifest or create a package cycle.
5
+ */
6
+ export const CURRENT_PLAY_ARTIFACT_CONTRACT_VERSION = 2 as const;
@@ -1,3 +1,5 @@
1
+ import type { ToolExecutionErrorSchemaVersion } from '../tool-execution-error';
2
+
1
3
  export type PlayPackageImport = {
2
4
  name: string;
3
5
  version: string | null;
@@ -21,6 +23,8 @@ export type PlayArtifactCompatibility = {
21
23
  minRunnerVersion: number;
22
24
  runtimeFeatures: PlayRuntimeFeature[];
23
25
  runtimeBackend?: string | null;
26
+ /** Missing preserves the legacy schema used before this field existed. */
27
+ toolErrorSchemaVersion?: ToolExecutionErrorSchemaVersion;
24
28
  };
25
29
 
26
30
  /** The only executable Play artifact contract. */
@@ -28,10 +28,13 @@ import type {
28
28
  PlayRuntimeFeature,
29
29
  } from '../artifact-types';
30
30
  import { buildPlayContractCompatibility } from '../contracts';
31
+ import type { ToolExecutionErrorSchemaVersion } from '../../tool-execution-error';
31
32
  import { validatePlaySourceFilesHaveNoInlineSecrets } from '../secret-guardrails';
32
33
  import { MAX_PLAY_BUNDLE_BYTES } from './limits';
33
34
 
34
- const PLAY_BUNDLE_CACHE_VERSION = 25;
35
+ // The authored tool-error schema is part of the artifact bytes. Do not reuse a
36
+ // local bundle cached before compatibility selection entered graph analysis.
37
+ const PLAY_BUNDLE_CACHE_VERSION = 27;
35
38
  const PLAY_ARTIFACT_CACHE_DIR = join(
36
39
  tmpdir(),
37
40
  `deepline-play-artifacts-v${PLAY_BUNDLE_CACHE_VERSION}`,
@@ -152,6 +155,7 @@ type SourceGraphAnalysis = {
152
155
  importPolicy: PlayImportPolicy;
153
156
  playName: string | null;
154
157
  playDescription: string | null;
158
+ toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion | null;
155
159
  importedPlayDependencies: ImportedPlayDependency[];
156
160
  };
157
161
 
@@ -501,6 +505,8 @@ type PlayMetadataExtractionContext = {
501
505
  type ExtractedPlayMetadata = {
502
506
  name: string | null;
503
507
  description: string | null;
508
+ toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion | null;
509
+ toolErrorSchemaVersionUnknown: boolean;
504
510
  };
505
511
 
506
512
  const TypeScriptParser = Parser.extend(
@@ -795,9 +801,13 @@ function staticStringFromExpression(
795
801
  }
796
802
 
797
803
  function propertyNameFromKey(property: AstNode): string | null {
798
- if (property.computed) return null;
799
804
  const key = isAstNode(property.key) ? property.key : null;
800
805
  if (!key) return null;
806
+ if (property.computed) {
807
+ return key.type === 'Literal' && typeof key.value === 'string'
808
+ ? key.value
809
+ : null;
810
+ }
801
811
  if (key.type === 'Identifier' && typeof key.name === 'string') {
802
812
  return key.name;
803
813
  }
@@ -826,6 +836,109 @@ function objectExpressionFromNode(
826
836
  );
827
837
  }
828
838
 
839
+ type StaticPropertyResolution =
840
+ | { kind: 'absent' }
841
+ | { kind: 'found'; value: AstNode }
842
+ | { kind: 'unknown' };
843
+
844
+ function resolveStaticProperty(
845
+ node: AstNode | null | undefined,
846
+ propertyName: string,
847
+ context: PlayMetadataExtractionContext,
848
+ ancestors = new Set<AstNode>(),
849
+ ): StaticPropertyResolution {
850
+ if (!node) return { kind: 'absent' };
851
+ const object = objectExpressionFromNode(node, context);
852
+ if (!object) return { kind: 'unknown' };
853
+ if (ancestors.has(object)) return { kind: 'unknown' };
854
+ const nextAncestors = new Set(ancestors).add(object);
855
+
856
+ let resolution: StaticPropertyResolution = { kind: 'absent' };
857
+ for (const property of astArray(object.properties)) {
858
+ if (property.type === 'SpreadElement') {
859
+ const spreadResolution = resolveStaticProperty(
860
+ isAstNode(property.argument) ? property.argument : null,
861
+ propertyName,
862
+ context,
863
+ nextAncestors,
864
+ );
865
+ if (spreadResolution.kind !== 'absent') {
866
+ resolution = spreadResolution;
867
+ }
868
+ continue;
869
+ }
870
+ if (property.type !== 'Property') continue;
871
+ const resolvedPropertyName = propertyNameFromKey(property);
872
+ if (resolvedPropertyName !== propertyName) {
873
+ if (property.computed && resolvedPropertyName === null) {
874
+ resolution = { kind: 'unknown' };
875
+ }
876
+ continue;
877
+ }
878
+ const propertyValue = property.shorthand ? property.key : property.value;
879
+ resolution = isAstNode(propertyValue)
880
+ ? { kind: 'found', value: propertyValue }
881
+ : { kind: 'unknown' };
882
+ }
883
+ return resolution;
884
+ }
885
+
886
+ function staticNumberFromExpression(
887
+ node: AstNode | null | undefined,
888
+ context: PlayMetadataExtractionContext,
889
+ seen = new Set<string>(),
890
+ ): number | null {
891
+ const expression = unwrapStaticExpression(node);
892
+ if (!expression) return null;
893
+ if (expression.type === 'Literal' && typeof expression.value === 'number') {
894
+ return expression.value;
895
+ }
896
+
897
+ const identifier = getIdentifierName(expression);
898
+ if (!identifier || seen.has(identifier)) return null;
899
+ seen.add(identifier);
900
+ return staticNumberFromExpression(
901
+ context.declarations.get(identifier),
902
+ context,
903
+ seen,
904
+ );
905
+ }
906
+
907
+ function toolErrorSchemaVersionFromOptions(
908
+ node: AstNode | null | undefined,
909
+ context: PlayMetadataExtractionContext,
910
+ ): ToolExecutionErrorSchemaVersion | null | undefined {
911
+ const directCompatibility = resolveStaticProperty(
912
+ node,
913
+ 'compatibility',
914
+ context,
915
+ );
916
+ if (directCompatibility.kind === 'unknown') return null;
917
+
918
+ const bindings = resolveStaticProperty(node, 'bindings', context);
919
+ if (bindings.kind === 'unknown') return null;
920
+ const bindingCompatibility =
921
+ bindings.kind === 'found'
922
+ ? resolveStaticProperty(bindings.value, 'compatibility', context)
923
+ : ({ kind: 'absent' } satisfies StaticPropertyResolution);
924
+ if (bindingCompatibility.kind === 'unknown') return null;
925
+
926
+ const compatibility =
927
+ directCompatibility.kind === 'found'
928
+ ? directCompatibility
929
+ : bindingCompatibility;
930
+ if (compatibility.kind === 'absent') return undefined;
931
+ const schema = resolveStaticProperty(
932
+ compatibility.value,
933
+ 'toolErrorSchemaVersion',
934
+ context,
935
+ );
936
+ if (schema.kind !== 'found') return null;
937
+ const schemaVersion = staticNumberFromExpression(schema.value, context);
938
+ if (schemaVersion === 0 || schemaVersion === 1) return schemaVersion;
939
+ return null;
940
+ }
941
+
829
942
  function stringPropertyFromObjectExpression(
830
943
  node: AstNode | null | undefined,
831
944
  propertyName: string,
@@ -895,12 +1008,16 @@ function playMetadataFromDefinePlayCall(
895
1008
 
896
1009
  const args = astArray(expression.arguments);
897
1010
  const firstArg = args[0] ?? null;
1011
+ const isObjectForm =
1012
+ args.length === 1 || objectExpressionFromNode(firstArg, context) !== null;
898
1013
  const objectName = stringPropertyFromObjectExpression(
899
1014
  firstArg,
900
1015
  'id',
901
1016
  context,
902
1017
  );
903
- const name = objectName ?? staticStringFromExpression(firstArg, context);
1018
+ const name = isObjectForm
1019
+ ? objectName
1020
+ : staticStringFromExpression(firstArg, context);
904
1021
 
905
1022
  let description =
906
1023
  stringPropertyFromObjectExpression(firstArg, 'description', context) ??
@@ -913,8 +1030,27 @@ function playMetadataFromDefinePlayCall(
913
1030
  );
914
1031
  }
915
1032
 
916
- if (!name && !description) return null;
917
- return { name, description };
1033
+ const options = isObjectForm ? firstArg : (args[2] ?? null);
1034
+ const toolErrorSchemaVersion = toolErrorSchemaVersionFromOptions(
1035
+ options,
1036
+ context,
1037
+ );
1038
+ const toolErrorSchemaVersionUnknown = toolErrorSchemaVersion === null;
1039
+
1040
+ if (
1041
+ !name &&
1042
+ !description &&
1043
+ toolErrorSchemaVersion === undefined &&
1044
+ !toolErrorSchemaVersionUnknown
1045
+ ) {
1046
+ return null;
1047
+ }
1048
+ return {
1049
+ name,
1050
+ description,
1051
+ toolErrorSchemaVersion: toolErrorSchemaVersion ?? null,
1052
+ toolErrorSchemaVersionUnknown,
1053
+ };
918
1054
  }
919
1055
 
920
1056
  function resolveExportExpression(
@@ -1397,14 +1533,18 @@ async function analyzeSourceGraph(
1397
1533
  .sort((left, right) => left.filePath.localeCompare(right.filePath)),
1398
1534
  }),
1399
1535
  );
1400
- const playName =
1401
- extractDefinedPlayNameForExport(sourceCode, exportName) ??
1402
- (exportName === 'default' ? extractDefinedPlayName(sourceCode) : null);
1403
- const playDescription =
1404
- extractDefinedPlayDescriptionForExport(sourceCode, exportName) ??
1536
+ const metadata =
1537
+ extractDefinedPlayMetadata(sourceCode, exportName) ??
1405
1538
  (exportName === 'default'
1406
- ? extractDefinedPlayDescription(sourceCode)
1539
+ ? extractDefinedPlayMetadata(sourceCode, null)
1407
1540
  : null);
1541
+ if (metadata?.toolErrorSchemaVersionUnknown) {
1542
+ throw new Error(
1543
+ 'definePlay compatibility.toolErrorSchemaVersion must be the static literal 0 or 1.',
1544
+ );
1545
+ }
1546
+ const playName = metadata?.name ?? null;
1547
+ const playDescription = metadata?.description ?? null;
1408
1548
 
1409
1549
  return {
1410
1550
  sourceCode,
@@ -1424,6 +1564,7 @@ async function analyzeSourceGraph(
1424
1564
  },
1425
1565
  playName,
1426
1566
  playDescription,
1567
+ toolErrorSchemaVersion: metadata?.toolErrorSchemaVersion ?? null,
1427
1568
  importedPlayDependencies: [...importedPlayDependencies.values()].sort(
1428
1569
  (left, right) => left.filePath.localeCompare(right.filePath),
1429
1570
  ),
@@ -1672,6 +1813,10 @@ export async function bundlePlayFile(
1672
1813
  entryFile: absolutePath,
1673
1814
  sourceHash: analysis.sourceHash,
1674
1815
  importPolicy: analysis.importPolicy,
1816
+ compatibility: buildPlayContractCompatibility({
1817
+ toolErrorSchemaVersion:
1818
+ analysis.toolErrorSchemaVersion ?? undefined,
1819
+ }),
1675
1820
  cacheHit: true,
1676
1821
  },
1677
1822
  sourceCode: analysis.sourceCode,
@@ -1736,7 +1881,9 @@ export async function bundlePlayFile(
1736
1881
  bundledCode: executableCode,
1737
1882
  sourceMap: normalizedSourceMap,
1738
1883
  importPolicy: analysis.importPolicy,
1739
- compatibility: buildPlayContractCompatibility(),
1884
+ compatibility: buildPlayContractCompatibility({
1885
+ toolErrorSchemaVersion: analysis.toolErrorSchemaVersion ?? undefined,
1886
+ }),
1740
1887
  generatedAt: Date.now(),
1741
1888
  cacheHit: false,
1742
1889
  };