deepline 0.3.19 → 0.3.20

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.
@@ -192,7 +192,7 @@ export const SDK_RELEASE = {
192
192
  // 0.3.0 introduces raw-v2: complete scrubbed provider responses are
193
193
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
194
194
  // getters keep their established compatibility behavior.
195
- version: '0.3.19',
195
+ version: '0.3.20',
196
196
  updateSummary:
197
197
  'New raw-v2 tool responses preserve complete scrubbed provider envelopes at toolResponse.rawV2, including JSON:API included resources, links, cursors, and totals. Existing toolResponse.raw and declared getters keep working unchanged.',
198
198
  contracts: {
@@ -376,8 +376,10 @@ type InlineCompositionStore = {
376
376
  toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion;
377
377
  /** Immutable authoring semantics pinned by the child play artifact. */
378
378
  authoringContractEdition: PlayAuthoringContractEdition;
379
- /** Immutable execute response contract pinned by the child artifact. */
379
+ /** Execute response contract pinned by the child artifact. */
380
380
  toolResponseContract: ToolResponseContract;
381
+ /** Explicit response-transform revision used for durable receipt identity. */
382
+ toolResponseReceiptRevision?: string;
381
383
  };
382
384
  const inlineCompositionContext =
383
385
  new AsyncLocalStorage<InlineCompositionStore>();
@@ -1340,6 +1342,11 @@ type ToolExecutionApiOptions = {
1340
1342
  timeoutMs?: number;
1341
1343
  durableCallReceiptKey?: string | null;
1342
1344
  executionAuthScopeDigest?: string | null;
1345
+ /**
1346
+ * Historical receipt identity that anchors external provider idempotency
1347
+ * while a newer response contract uses a distinct durable receipt.
1348
+ */
1349
+ providerIdempotencyReceiptKey?: string | null;
1343
1350
  providerIdempotencyKey?: string | null;
1344
1351
  receiptLeaseExpiresAt?: string | null;
1345
1352
  beforeProviderCall?: () => Promise<void> | void;
@@ -2648,6 +2655,18 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2648
2655
  );
2649
2656
  }
2650
2657
 
2658
+ /**
2659
+ * Only explicitly declared response transformations affect receipt reuse.
2660
+ * Missing preserves historical artifact/receipt identity, while the public
2661
+ * response header still normalizes it to the legacy V2 behavior above.
2662
+ */
2663
+ private get currentToolResponseReceiptRevision(): string | undefined {
2664
+ if (this.activeInlineComposition) {
2665
+ return this.activeInlineComposition.toolResponseReceiptRevision;
2666
+ }
2667
+ return this.#options.toolResponseReceiptRevision;
2668
+ }
2669
+
2651
2670
  private get currentAuthoringContractEdition(): PlayAuthoringContractEdition {
2652
2671
  return (
2653
2672
  this.activeInlineComposition?.authoringContractEdition ??
@@ -4026,13 +4045,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4026
4045
  );
4027
4046
  }
4028
4047
 
4029
- private async durableToolCallCacheKeyForScope(input: {
4030
- toolId: string;
4031
- requestInput: Record<string, unknown>;
4032
- executionAuthScopeDigest?: string | null;
4033
- staleAfterSeconds?: number | null;
4034
- playLocalScope?: string | null;
4035
- }): Promise<string> {
4048
+ private async durableToolCallCacheKeyForScope(
4049
+ input: {
4050
+ toolId: string;
4051
+ requestInput: Record<string, unknown>;
4052
+ executionAuthScopeDigest?: string | null;
4053
+ staleAfterSeconds?: number | null;
4054
+ playLocalScope?: string | null;
4055
+ },
4056
+ toolResponseReceiptRevision: string | null | undefined = this
4057
+ .currentToolResponseReceiptRevision,
4058
+ ): Promise<string> {
4036
4059
  const providerActionVersion =
4037
4060
  (await this.#options.getToolActionCacheVersion?.(input.toolId))?.trim() ??
4038
4061
  '';
@@ -4051,21 +4074,29 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4051
4074
  executionAuthScopeDigest,
4052
4075
  }),
4053
4076
  providerActionVersion,
4077
+ toolResponseReceiptRevision,
4054
4078
  staleAfterSeconds: input.staleAfterSeconds,
4055
4079
  cacheEpochMs: this.durableCallCacheEpochMs,
4056
4080
  });
4057
4081
  }
4058
4082
 
4059
- private async durableToolCallCacheKey(input: {
4060
- toolId: string;
4061
- requestInput: Record<string, unknown>;
4062
- executionAuthScopeDigest?: string | null;
4063
- staleAfterSeconds?: number | null;
4064
- }): Promise<string> {
4065
- return await this.durableToolCallCacheKeyForScope({
4066
- ...input,
4067
- playLocalScope: this.currentGovernance.currentPlayId,
4068
- });
4083
+ private async durableToolCallCacheKey(
4084
+ input: {
4085
+ toolId: string;
4086
+ requestInput: Record<string, unknown>;
4087
+ executionAuthScopeDigest?: string | null;
4088
+ staleAfterSeconds?: number | null;
4089
+ },
4090
+ toolResponseReceiptRevision: string | null | undefined = this
4091
+ .currentToolResponseReceiptRevision,
4092
+ ): Promise<string> {
4093
+ return await this.durableToolCallCacheKeyForScope(
4094
+ {
4095
+ ...input,
4096
+ playLocalScope: this.currentGovernance.currentPlayId,
4097
+ },
4098
+ toolResponseReceiptRevision,
4099
+ );
4069
4100
  }
4070
4101
 
4071
4102
  private async resolveToolAuthScopeDigest(
@@ -8968,6 +8999,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8968
8999
  executionAuthScopeDigest,
8969
9000
  staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
8970
9001
  });
9002
+ let providerIdempotencyKeyBase = await this.durableToolCallCacheKey(
9003
+ {
9004
+ toolId,
9005
+ requestInput: input,
9006
+ executionAuthScopeDigest,
9007
+ staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
9008
+ },
9009
+ null,
9010
+ );
8971
9011
  const checkpointCacheKeys = [durableCacheKey];
8972
9012
 
8973
9013
  const executeTool = async (context?: {
@@ -9091,9 +9131,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9091
9131
  callKey: normalizedKey,
9092
9132
  }),
9093
9133
  executionAuthScopeDigest,
9134
+ providerIdempotencyReceiptKey: cacheableToolResult
9135
+ ? providerIdempotencyKeyBase
9136
+ : physicalDirectKey,
9094
9137
  providerIdempotencyKey: cacheableToolResult
9095
9138
  ? this.providerIdempotencyKeyForToolCall({
9096
- cacheKey: directCacheKey,
9139
+ cacheKey: providerIdempotencyKeyBase,
9097
9140
  force: toolCachePolicy.force,
9098
9141
  leaseId: directReceiptLeaseId,
9099
9142
  logicalCallId,
@@ -9241,6 +9284,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9241
9284
  this.enqueueToolCall({
9242
9285
  callId,
9243
9286
  cacheKey: toolResultCacheKey,
9287
+ providerIdempotencyKeyBase,
9244
9288
  cacheable: cacheableToolResult,
9245
9289
  receiptKey: cacheableToolResult ? durableCacheKey : null,
9246
9290
  executionAuthScopeDigest,
@@ -9361,6 +9405,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9361
9405
  executionAuthScopeDigest,
9362
9406
  staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
9363
9407
  });
9408
+ providerIdempotencyKeyBase = await this.durableToolCallCacheKey(
9409
+ {
9410
+ toolId,
9411
+ requestInput: input,
9412
+ executionAuthScopeDigest,
9413
+ staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
9414
+ },
9415
+ null,
9416
+ );
9364
9417
  checkpointCacheKeys[0] = durableCacheKey;
9365
9418
  }
9366
9419
  }
@@ -9523,14 +9576,18 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9523
9576
  `Unable to resolve play "${resolvedName}" for ctx.runPlay(...).`,
9524
9577
  );
9525
9578
  }
9526
- const childCompatibility = normalizePlayContractCompatibility(
9579
+ const childCompatibilitySnapshot =
9527
9580
  resolvedPlay.contractSnapshot?.compatibility ??
9528
- resolvedPlay.artifact?.compatibility ??
9529
- buildPlayContractCompatibility(),
9581
+ resolvedPlay.artifact?.compatibility ??
9582
+ buildPlayContractCompatibility();
9583
+ const childCompatibility = normalizePlayContractCompatibility(
9584
+ childCompatibilitySnapshot,
9530
9585
  );
9531
9586
  const childToolErrorSchemaVersion =
9532
9587
  childCompatibility.toolErrorSchemaVersion;
9533
9588
  const childToolResponseContract = childCompatibility.toolResponseContract;
9589
+ const childToolResponseReceiptRevision =
9590
+ childCompatibility.toolResponseReceiptRevision;
9534
9591
  const childExecutionDecision = resolveChildExecutionStrategy({
9535
9592
  pipeline: resolvedPlay.staticPipeline,
9536
9593
  timeoutMs: options?.timeoutMs,
@@ -9604,6 +9661,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9604
9661
  staticPipeline: resolvedPlay.staticPipeline ?? null,
9605
9662
  toolErrorSchemaVersion: childToolErrorSchemaVersion,
9606
9663
  toolResponseContract: childToolResponseContract,
9664
+ toolResponseReceiptRevision: childToolResponseReceiptRevision,
9607
9665
  authoringContractEdition:
9608
9666
  childCompatibility.authoringContractEdition,
9609
9667
  },
@@ -10882,12 +10940,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10882
10940
  durableCallReceiptKey: receiptKey,
10883
10941
  playNodeScope: playNodeScopeForToolCallRequest(owner),
10884
10942
  executionAuthScopeDigest: owner.executionAuthScopeDigest,
10943
+ providerIdempotencyReceiptKey:
10944
+ owner.providerIdempotencyKeyBase ?? owner.cacheKey,
10885
10945
  receiptLeaseExpiresAt: owner.receiptLeaseExpiresAt,
10886
10946
  heartbeatReceipt: () =>
10887
10947
  this.renewRuntimeToolReceiptOwnership([owner]),
10888
10948
  providerIdempotencyKey:
10889
10949
  this.providerIdempotencyKeyForToolCall({
10890
- cacheKey: owner.cacheKey,
10950
+ cacheKey:
10951
+ owner.providerIdempotencyKeyBase ?? owner.cacheKey,
10891
10952
  force: owner.force === true,
10892
10953
  leaseId: owner.receiptLeaseId,
10893
10954
  }),
@@ -11219,6 +11280,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11219
11280
  const receiptKeys = batch.memberRequests.map(
11220
11281
  (request) => request.cacheKey,
11221
11282
  );
11283
+ const providerIdempotencyReceiptKeys =
11284
+ batch.memberRequests.map(
11285
+ (request) =>
11286
+ request.providerIdempotencyKeyBase ?? request.cacheKey,
11287
+ );
11222
11288
  const aggregateReceiptKey =
11223
11289
  buildDurableToolAggregateReceiptKey({
11224
11290
  receiptKeys,
@@ -11228,6 +11294,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11228
11294
  toolId: batch.batchOperation,
11229
11295
  }),
11230
11296
  });
11297
+ const aggregateProviderIdempotencyReceiptKey =
11298
+ buildDurableToolAggregateReceiptKey({
11299
+ receiptKeys: providerIdempotencyReceiptKeys,
11300
+ prefix: 'batch',
11301
+ aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
11302
+ orgId: this.#options.orgId,
11303
+ toolId: batch.batchOperation,
11304
+ }),
11305
+ });
11231
11306
  let releaseToolSlot: () => void = () => undefined;
11232
11307
  try {
11233
11308
  const execution = await this.callToolExecutionAPI(
@@ -11247,14 +11322,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11247
11322
  executionAuthScopeDigest:
11248
11323
  batch.memberRequests[0]?.executionAuthScopeDigest ??
11249
11324
  null,
11325
+ providerIdempotencyReceiptKey:
11326
+ aggregateProviderIdempotencyReceiptKey,
11250
11327
  providerIdempotencyKey:
11251
11328
  buildDurableToolAggregateProviderIdempotencyKey({
11252
- aggregateReceiptKey,
11253
- receiptKeys,
11329
+ aggregateReceiptKey:
11330
+ aggregateProviderIdempotencyReceiptKey,
11331
+ receiptKeys: providerIdempotencyReceiptKeys,
11254
11332
  providerIdempotencyKeys: batch.memberRequests.map(
11255
11333
  (request) =>
11256
11334
  this.providerIdempotencyKeyForToolCall({
11257
- cacheKey: request.cacheKey,
11335
+ cacheKey:
11336
+ request.providerIdempotencyKeyBase ??
11337
+ request.cacheKey,
11258
11338
  force: request.force === true,
11259
11339
  leaseId: request.receiptLeaseId,
11260
11340
  }),
@@ -11481,9 +11561,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11481
11561
  durableCallReceiptKey: request.receiptKey,
11482
11562
  executionAuthScopeDigest:
11483
11563
  request.executionAuthScopeDigest,
11564
+ providerIdempotencyReceiptKey:
11565
+ request.providerIdempotencyKeyBase ??
11566
+ request.receiptKey,
11484
11567
  providerIdempotencyKey:
11485
11568
  this.providerIdempotencyKeyForToolCall({
11486
- cacheKey: request.receiptKey,
11569
+ cacheKey:
11570
+ request.providerIdempotencyKeyBase ??
11571
+ request.receiptKey,
11487
11572
  force: request.force === true,
11488
11573
  leaseId: request.receiptLeaseId,
11489
11574
  }),
@@ -11783,6 +11868,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11783
11868
  : ((await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null);
11784
11869
  const providerIdempotencyKey =
11785
11870
  options?.providerIdempotencyKey?.trim() || durableCallReceiptKey;
11871
+ const providerIdempotencyReceiptKey =
11872
+ options?.providerIdempotencyReceiptKey?.trim() ||
11873
+ durableCallReceiptKey;
11786
11874
  // Correlation identity is stable across every transport retry,
11787
11875
  // including calls without a durable receipt.
11788
11876
  const deeplineRequestId = providerIdempotencyKey
@@ -11836,6 +11924,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11836
11924
  ...(providerIdempotencyKey
11837
11925
  ? { provider_idempotency_key: providerIdempotencyKey }
11838
11926
  : {}),
11927
+ ...(providerIdempotencyReceiptKey
11928
+ ? {
11929
+ provider_idempotency_receipt_key:
11930
+ providerIdempotencyReceiptKey,
11931
+ }
11932
+ : {}),
11839
11933
  ...(playNodeScope
11840
11934
  ? { play_node_scope: playNodeScopeToWire(playNodeScope) }
11841
11935
  : {}),
@@ -60,6 +60,8 @@ export interface RowState {
60
60
  export interface ToolCallRequest {
61
61
  callId: string;
62
62
  cacheKey: string;
63
+ /** Stable external-operation identity, independent of receipt serialization. */
64
+ providerIdempotencyKeyBase?: string | null;
63
65
  cacheable?: boolean;
64
66
  receiptKey?: string | null;
65
67
  executionAuthScopeDigest?: string | null;
@@ -575,6 +577,8 @@ export interface ContextOptions {
575
577
  toolErrorSchemaVersion?: ToolExecutionErrorSchemaVersion;
576
578
  /** Execute-result contract pinned by the immutable Play artifact. */
577
579
  toolResponseContract?: ToolResponseContract;
580
+ /** Explicit response-transform revision used for durable receipt identity. */
581
+ toolResponseReceiptRevision?: string;
578
582
  /** Short-lived HMAC-signed internal token for tool callbacks. Required for cloud execution. */
579
583
  executorToken?: string;
580
584
  baseUrl?: string;
@@ -7,6 +7,7 @@ import {
7
7
  // resolve.
8
8
  } from '../plays/row-identity';
9
9
  import { resolveDurableCallCachePolicy } from './durable-call-policy';
10
+ import { normalizeToolResponseReceiptRevision } from './tool-response-contract';
10
11
 
11
12
  export {
12
13
  DURABLE_CALL_STALE_AFTER_SECONDS_ERROR,
@@ -45,6 +46,11 @@ export function buildDurableToolCallCacheKey(input: {
45
46
  /** Run-stable clock used to choose the stale bucket. */
46
47
  cacheEpochMs?: number;
47
48
  playLocalScope?: string | null;
49
+ /**
50
+ * Explicit revision for a response transformation. Bump it only when the
51
+ * serialized tool output changes; omit it for historical artifacts.
52
+ */
53
+ toolResponseReceiptRevision?: string | null;
48
54
  }): string {
49
55
  const orgId = input.orgId?.trim() || 'org';
50
56
  const toolId = input.toolId.trim();
@@ -58,6 +64,11 @@ export function buildDurableToolCallCacheKey(input: {
58
64
  );
59
65
  }
60
66
  const playLocalScope = input.playLocalScope?.trim() || 'play';
67
+ // Keep historical digests byte-for-byte stable when no explicit revision
68
+ // exists on the stored artifact.
69
+ const toolResponseReceiptRevision = normalizeToolResponseReceiptRevision(
70
+ input.toolResponseReceiptRevision,
71
+ );
61
72
  const digest = sha256Hex(
62
73
  stableStringify({
63
74
  kind: 'tool' satisfies DurableCallKind,
@@ -70,6 +81,7 @@ export function buildDurableToolCallCacheKey(input: {
70
81
  providerActionVersion,
71
82
  cachePolicyVersion:
72
83
  input.cachePolicyVersion ?? DURABLE_CALL_CACHE_POLICY_VERSION,
84
+ ...(toolResponseReceiptRevision ? { toolResponseReceiptRevision } : {}),
73
85
  staleBucket: durableCacheStaleBucket({
74
86
  staleAfterSeconds: input.staleAfterSeconds,
75
87
  nowMs: input.cacheEpochMs,
@@ -141,6 +141,8 @@ export interface PlayRunnerContextConfig {
141
141
  toolErrorSchemaVersion?: ToolExecutionErrorSchemaVersion;
142
142
  /** Immutable execute response contract copied from the Play artifact. */
143
143
  toolResponseContract?: ToolResponseContract;
144
+ /** Explicit response-transform revision participating in receipt identity. */
145
+ toolResponseReceiptRevision?: string;
144
146
  orgId?: string;
145
147
  workflowId?: string;
146
148
  playId?: string;
@@ -1,10 +1,23 @@
1
1
  /**
2
2
  * Public tool-response contracts shared by the API, SDK, Play bundler, and
3
- * runtime. A response contract is transport behavior, never Play authoring.
3
+ * runtime. This is a versioned response transformation contract: a change to
4
+ * the persisted public tool-result shape must introduce a new value here and
5
+ * have newly built Play artifacts select it. Receipt-cache revisions are
6
+ * intentionally separate: ordinary response-contract changes do not refresh
7
+ * durable receipts unless the response transformation changed as well.
4
8
  */
5
9
  export const V2_TOOL_RESPONSE_CONTRACT = 'v2-tool-response' as const;
6
10
  export const RAW_V2_TOOL_RESPONSE_CONTRACT = 'raw-v2' as const;
7
11
 
12
+ /**
13
+ * Deliberate durable-receipt boundary for the raw-v2 response transformation.
14
+ * Bump only when that transformation changes serialized tool output. Do not
15
+ * bump for ordinary protocol, authoring, or transport contract changes.
16
+ */
17
+ export const RAW_V2_TOOL_RESPONSE_RECEIPT_REVISION = 'raw-v2-receipt-v1';
18
+ const TOOL_RESPONSE_RECEIPT_REVISION_PATTERN =
19
+ /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,63})$/;
20
+
8
21
  export type ToolResponseContract =
9
22
  | typeof V2_TOOL_RESPONSE_CONTRACT
10
23
  | typeof RAW_V2_TOOL_RESPONSE_CONTRACT;
@@ -20,6 +33,19 @@ export function isToolResponseContract(
20
33
  );
21
34
  }
22
35
 
36
+ /**
37
+ * Preserves the distinction between an old artifact with no declared response
38
+ * contract and an artifact that explicitly selected the legacy V2 contract.
39
+ * Receipt reuse is controlled separately by `toolResponseReceiptRevision`.
40
+ */
41
+ export function declaredToolResponseContract(
42
+ value: unknown,
43
+ ): ToolResponseContract | undefined {
44
+ if (value == null) return undefined;
45
+ if (isToolResponseContract(value)) return value;
46
+ throw new UnsupportedToolResponseContractError(value);
47
+ }
48
+
23
49
  export class UnsupportedToolResponseContractError extends Error {
24
50
  constructor(value: unknown) {
25
51
  super(
@@ -29,13 +55,34 @@ export class UnsupportedToolResponseContractError extends Error {
29
55
  }
30
56
  }
31
57
 
58
+ export class InvalidToolResponseReceiptRevisionError extends Error {
59
+ constructor(value: unknown) {
60
+ super(
61
+ `Tool response receipt revision must be a non-empty static identifier (letters, numbers, '.', '_', or '-'); received ${String(value)}.`,
62
+ );
63
+ this.name = 'InvalidToolResponseReceiptRevisionError';
64
+ }
65
+ }
66
+
67
+ /** Missing means a historical artifact and preserves its original receipt key. */
68
+ export function normalizeToolResponseReceiptRevision(
69
+ value: unknown,
70
+ ): string | undefined {
71
+ if (value == null) return undefined;
72
+ if (
73
+ typeof value === 'string' &&
74
+ TOOL_RESPONSE_RECEIPT_REVISION_PATTERN.test(value)
75
+ ) {
76
+ return value;
77
+ }
78
+ throw new InvalidToolResponseReceiptRevisionError(value);
79
+ }
80
+
32
81
  /** Missing artifact compatibility predates canonical bodies and stays V2. */
33
82
  export function normalizeToolResponseContract(
34
83
  value: unknown,
35
84
  ): ToolResponseContract {
36
- if (value == null) return V2_TOOL_RESPONSE_CONTRACT;
37
- if (isToolResponseContract(value)) return value;
38
- throw new UnsupportedToolResponseContractError(value);
85
+ return declaredToolResponseContract(value) ?? V2_TOOL_RESPONSE_CONTRACT;
39
86
  }
40
87
 
41
88
  export function legacyRawFromToolResponseRawV2(
@@ -31,6 +31,8 @@ export type PlayArtifactCompatibility = {
31
31
  authoringContractEdition?: PlayAuthoringContractEdition;
32
32
  /** Missing preserves the raw-only V2 execute response contract. */
33
33
  toolResponseContract?: ToolResponseContract;
34
+ /** Missing preserves the receipt namespace of artifacts created before this revision existed. */
35
+ toolResponseReceiptRevision?: string;
34
36
  };
35
37
 
36
38
  /** The only executable Play artifact contract. */
@@ -344,7 +344,12 @@ export type PlayAuthoringAstBindings = {
344
344
  export type PlayAuthoringBindings = {
345
345
  description?: string;
346
346
  compatibility?: {
347
- toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion;
347
+ toolErrorSchemaVersion?: ToolExecutionErrorSchemaVersion;
348
+ /**
349
+ * Bump only when a Play's response transformation changes serialized tool
350
+ * output and requires fresh durable tool receipts.
351
+ */
352
+ toolResponseReceiptRevision?: string;
348
353
  };
349
354
  inline?: boolean;
350
355
  billing?: {
@@ -1103,6 +1108,28 @@ export const PLAY_AUTHORING_FIELD_REGISTRY = {
1103
1108
  errorMessage:
1104
1109
  'compatibility.toolErrorSchemaVersion must be the static literal 0 or 1.',
1105
1110
  },
1111
+ 'compatibility.toolResponseReceiptRevision': {
1112
+ schema: Type.String({
1113
+ minLength: 1,
1114
+ maxLength: 64,
1115
+ pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$',
1116
+ }),
1117
+ fixtures: {
1118
+ valid: 'raw-v2-receipt-v1',
1119
+ invalid: 'has spaces',
1120
+ absent: undefined,
1121
+ unresolved: { expression: 'revision' },
1122
+ edition1: undefined,
1123
+ },
1124
+ referenceType: 'string',
1125
+ required: false,
1126
+ resolution: 'static-required',
1127
+ issueCode: 'play_authoring_binding_invalid',
1128
+ description:
1129
+ 'Explicit durable-receipt revision for a response transformation; bump only when serialized tool output changes.',
1130
+ errorMessage:
1131
+ 'compatibility.toolResponseReceiptRevision must be a non-empty static identifier using letters, numbers, dots, underscores, or hyphens.',
1132
+ },
1106
1133
  inline: {
1107
1134
  schema: Type.Boolean(),
1108
1135
  fixtures: {
@@ -2488,7 +2515,7 @@ export const PLAY_AUTHORING_CLOUD_TYPE_DECLARATIONS = [
2488
2515
  '};',
2489
2516
  'export type PlayBindings = {',
2490
2517
  ` description?: ${cloudReferenceType('description')};`,
2491
- ` compatibility?: { toolErrorSchemaVersion: ${cloudReferenceType('compatibility.toolErrorSchemaVersion')} };`,
2518
+ ` compatibility?: { toolErrorSchemaVersion?: ${cloudReferenceType('compatibility.toolErrorSchemaVersion')}; toolResponseReceiptRevision?: ${cloudReferenceType('compatibility.toolResponseReceiptRevision')} };`,
2492
2519
  ` inline?: ${cloudReferenceType('inline')};`,
2493
2520
  ` billing?: { maxCreditsPerRun?: ${cloudReferenceType('billing.maxCreditsPerRun')} };`,
2494
2521
  ` runtime?: { timeout?: ${cloudReferenceType('runtime.timeout')}; size?: ${cloudReferenceType('runtime.size')} };`,
@@ -66,7 +66,7 @@ import { PLAY_AUTHORING_CONTRACT_EDITION } from '../authoring-contract';
66
66
  // of the artifact bytes. Do not reuse a local bundle cached before either
67
67
  // compatibility selection or docflow instrumentation entered graph analysis.
68
68
  // Keep this aligned with the app and SDK adapters' cache namespace.
69
- const PLAY_BUNDLE_CACHE_VERSION = 35;
69
+ const PLAY_BUNDLE_CACHE_VERSION = 36;
70
70
  const PLAY_ARTIFACT_CACHE_DIR = join(
71
71
  tmpdir(),
72
72
  `deepline-play-artifacts-v${PLAY_BUNDLE_CACHE_VERSION}`,
@@ -205,6 +205,7 @@ type SourceGraphAnalysis = {
205
205
  playDescription: string | null;
206
206
  sandboxRuntimeDeclaration: PlaySandboxRuntimeDeclaration | null;
207
207
  toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion | null;
208
+ toolResponseReceiptRevision: string | null;
208
209
  importedPlayDependencies: ImportedPlayDependency[];
209
210
  };
210
211
 
@@ -583,6 +584,8 @@ type ExtractedPlayMetadata = {
583
584
  sandboxRuntimeDeclaration: PlaySandboxRuntimeDeclaration | null;
584
585
  toolErrorSchemaVersion: ToolExecutionErrorSchemaVersion | null;
585
586
  toolErrorSchemaVersionUnknown: boolean;
587
+ toolResponseReceiptRevision: string | null;
588
+ toolResponseReceiptRevisionUnknown: boolean;
586
589
  };
587
590
 
588
591
  function parsePlaySourceAst(sourceCode: string): AstNode | null {
@@ -1020,12 +1023,47 @@ function toolErrorSchemaVersionFromOptions(
1020
1023
  'toolErrorSchemaVersion',
1021
1024
  context,
1022
1025
  );
1026
+ if (schema.kind === 'absent') return undefined;
1023
1027
  if (schema.kind !== 'found') return null;
1024
1028
  const schemaVersion = staticNumberFromExpression(schema.value, context);
1025
1029
  if (schemaVersion === 0 || schemaVersion === 1) return schemaVersion;
1026
1030
  return null;
1027
1031
  }
1028
1032
 
1033
+ function toolResponseReceiptRevisionFromOptions(
1034
+ node: AstNode | null | undefined,
1035
+ context: PlayMetadataExtractionContext,
1036
+ ): string | null | undefined {
1037
+ const directCompatibility = resolveStaticProperty(
1038
+ node,
1039
+ 'compatibility',
1040
+ context,
1041
+ );
1042
+ if (directCompatibility.kind === 'unknown') return null;
1043
+
1044
+ const bindings = resolveStaticProperty(node, 'bindings', context);
1045
+ if (bindings.kind === 'unknown') return null;
1046
+ const bindingCompatibility =
1047
+ bindings.kind === 'found'
1048
+ ? resolveStaticProperty(bindings.value, 'compatibility', context)
1049
+ : ({ kind: 'absent' } satisfies StaticPropertyResolution);
1050
+ if (bindingCompatibility.kind === 'unknown') return null;
1051
+
1052
+ const compatibility =
1053
+ directCompatibility.kind === 'found'
1054
+ ? directCompatibility
1055
+ : bindingCompatibility;
1056
+ if (compatibility.kind === 'absent') return undefined;
1057
+ const revision = resolveStaticProperty(
1058
+ compatibility.value,
1059
+ 'toolResponseReceiptRevision',
1060
+ context,
1061
+ );
1062
+ if (revision.kind === 'absent') return undefined;
1063
+ if (revision.kind !== 'found') return null;
1064
+ return staticStringFromExpression(revision.value, context);
1065
+ }
1066
+
1029
1067
  function sandboxRuntimeDeclarationFromOptions(
1030
1068
  node: AstNode | null | undefined,
1031
1069
  context: PlayMetadataExtractionContext,
@@ -1169,12 +1207,20 @@ function playMetadataFromDefinePlayCall(
1169
1207
  context,
1170
1208
  );
1171
1209
  const toolErrorSchemaVersionUnknown = toolErrorSchemaVersion === null;
1210
+ const toolResponseReceiptRevision = toolResponseReceiptRevisionFromOptions(
1211
+ options,
1212
+ context,
1213
+ );
1214
+ const toolResponseReceiptRevisionUnknown =
1215
+ toolResponseReceiptRevision === null;
1172
1216
 
1173
1217
  if (
1174
1218
  !name &&
1175
1219
  !description &&
1176
1220
  toolErrorSchemaVersion === undefined &&
1177
- !toolErrorSchemaVersionUnknown
1221
+ !toolErrorSchemaVersionUnknown &&
1222
+ toolResponseReceiptRevision === undefined &&
1223
+ !toolResponseReceiptRevisionUnknown
1178
1224
  ) {
1179
1225
  return null;
1180
1226
  }
@@ -1184,6 +1230,8 @@ function playMetadataFromDefinePlayCall(
1184
1230
  sandboxRuntimeDeclaration,
1185
1231
  toolErrorSchemaVersion: toolErrorSchemaVersion ?? null,
1186
1232
  toolErrorSchemaVersionUnknown,
1233
+ toolResponseReceiptRevision: toolResponseReceiptRevision ?? null,
1234
+ toolResponseReceiptRevisionUnknown,
1187
1235
  };
1188
1236
  }
1189
1237
 
@@ -2039,6 +2087,11 @@ async function analyzeSourceGraph(
2039
2087
  'definePlay compatibility.toolErrorSchemaVersion must be the static literal 0 or 1.',
2040
2088
  );
2041
2089
  }
2090
+ if (metadata?.toolResponseReceiptRevisionUnknown) {
2091
+ throw new Error(
2092
+ 'definePlay compatibility.toolResponseReceiptRevision must be a static non-empty string.',
2093
+ );
2094
+ }
2042
2095
  const playName = metadata?.name ?? null;
2043
2096
  const playDescription = metadata?.description ?? null;
2044
2097
  const sandboxRuntimeDeclaration = metadata?.sandboxRuntimeDeclaration ?? null;
@@ -2063,6 +2116,7 @@ async function analyzeSourceGraph(
2063
2116
  playDescription,
2064
2117
  sandboxRuntimeDeclaration,
2065
2118
  toolErrorSchemaVersion: metadata?.toolErrorSchemaVersion ?? null,
2119
+ toolResponseReceiptRevision: metadata?.toolResponseReceiptRevision ?? null,
2066
2120
  importedPlayDependencies: [...importedPlayDependencies.values()].sort(
2067
2121
  (left, right) => left.filePath.localeCompare(right.filePath),
2068
2122
  ),
@@ -2340,6 +2394,8 @@ export async function bundlePlayFile(
2340
2394
  compatibility: buildPlayContractCompatibility({
2341
2395
  toolErrorSchemaVersion:
2342
2396
  analysis.toolErrorSchemaVersion ?? undefined,
2397
+ toolResponseReceiptRevision:
2398
+ analysis.toolResponseReceiptRevision ?? undefined,
2343
2399
  }),
2344
2400
  cacheHit: true,
2345
2401
  },
@@ -2408,6 +2464,8 @@ export async function bundlePlayFile(
2408
2464
  importPolicy,
2409
2465
  compatibility: buildPlayContractCompatibility({
2410
2466
  toolErrorSchemaVersion: analysis.toolErrorSchemaVersion ?? undefined,
2467
+ toolResponseReceiptRevision:
2468
+ analysis.toolResponseReceiptRevision ?? undefined,
2411
2469
  }),
2412
2470
  generatedAt: Date.now(),
2413
2471
  cacheHit: false,