deepline 0.3.58 → 0.3.60

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.
@@ -2057,6 +2057,9 @@ export class DeeplineClient {
2057
2057
  currentPublishedVersion:
2058
2058
  play.currentPublishedVersion ?? play.liveRevision?.version ?? null,
2059
2059
  latestRunId: play.latestRunId ?? detail.latestRuns[0]?.workflowId ?? null,
2060
+ ...(play.triggerMetadata
2061
+ ? { triggerMetadata: play.triggerMetadata }
2062
+ : {}),
2060
2063
  ...(play.runtimeLimit ? { runtimeLimit: play.runtimeLimit } : {}),
2061
2064
  ...(play.activeScheduledPlays
2062
2065
  ? { activeScheduledPlays: play.activeScheduledPlays }
@@ -94,6 +94,27 @@ export type ProjectPinTarget =
94
94
  candidates: string[];
95
95
  };
96
96
 
97
+ /**
98
+ * Where the CLI obtained the credentials for the current invocation.
99
+ *
100
+ * This is provenance only. SDK consumers may intentionally use host-level
101
+ * credentials, so CLI-only safety policy must be enforced by the CLI command
102
+ * dispatcher rather than by {@link resolveConfig}.
103
+ */
104
+ export type CliAuthProvenance = {
105
+ scope: 'env' | 'folder' | 'global';
106
+ project:
107
+ | {
108
+ state: 'project';
109
+ dir: string;
110
+ pinPath: string;
111
+ source: 'marker' | 'cowork' | 'folder';
112
+ }
113
+ | { state: 'not_project' }
114
+ | { state: 'ambiguous_cowork_project'; candidates: string[] };
115
+ folderAuthPath: string | null;
116
+ };
117
+
97
118
  /**
98
119
  * Convert a base URL to a filesystem-safe slug for per-host config storage.
99
120
  *
@@ -676,6 +697,75 @@ export function getActiveProjectAuthSource(
676
697
  return loadProjectEnvCandidates(startDir)[0] ?? null;
677
698
  }
678
699
 
700
+ function findNearestProjectMarkerDir(startDir: string): string | null {
701
+ let current = resolve(startDir);
702
+ while (true) {
703
+ if (
704
+ COWORK_PROJECT_MARKERS.some((marker) => existsSync(join(current, marker)))
705
+ ) {
706
+ return current;
707
+ }
708
+ const parent = dirname(current);
709
+ if (parent === current) return null;
710
+ current = parent;
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Resolve credential provenance and whether the command is being run from a
716
+ * project. A project is a marked checkout, a Cowork-mounted project, or a
717
+ * folder that already carries Deepline project auth.
718
+ *
719
+ * The result intentionally does not expose an API key. It lets the CLI reject
720
+ * cloud mutations which would otherwise inherit a different project's shared
721
+ * host-level organization selection.
722
+ */
723
+ export function resolveCliAuthProvenance(
724
+ config: Pick<ResolvedConfig, 'baseUrl' | 'apiKey'>,
725
+ startDir: string = process.cwd(),
726
+ ): CliAuthProvenance {
727
+ const envApiKey = process.env[API_KEY_ENV]?.trim();
728
+ const folderAuth = getResolvedProjectAuthSource(
729
+ config.baseUrl,
730
+ config.apiKey,
731
+ startDir,
732
+ );
733
+ const pinTarget = resolveProjectPinTarget(startDir);
734
+ const markerDir = findNearestProjectMarkerDir(startDir);
735
+ const project = !pinTarget.ok
736
+ ? {
737
+ state: 'ambiguous_cowork_project' as const,
738
+ candidates: pinTarget.candidates,
739
+ }
740
+ : folderAuth
741
+ ? {
742
+ state: 'project' as const,
743
+ dir: dirname(folderAuth.filePath),
744
+ pinPath: folderAuth.filePath,
745
+ source: 'folder' as const,
746
+ }
747
+ : pinTarget.source === 'cowork'
748
+ ? {
749
+ state: 'project' as const,
750
+ dir: pinTarget.dir,
751
+ pinPath: join(pinTarget.dir, PROJECT_DEEPLINE_ENV_FILE),
752
+ source: 'cowork' as const,
753
+ }
754
+ : markerDir
755
+ ? {
756
+ state: 'project' as const,
757
+ dir: markerDir,
758
+ pinPath: join(markerDir, PROJECT_DEEPLINE_ENV_FILE),
759
+ source: 'marker' as const,
760
+ }
761
+ : { state: 'not_project' as const };
762
+ return {
763
+ scope: envApiKey ? 'env' : folderAuth ? 'folder' : 'global',
764
+ project,
765
+ folderAuthPath: folderAuth?.filePath ?? null,
766
+ };
767
+ }
768
+
679
769
  export {
680
770
  baseUrlSlug,
681
771
  loadCliEnv,
@@ -241,7 +241,8 @@ export type PlayFetchResponse = PlayAuthoringFetchResponse;
241
241
  *
242
242
  * @sdkReference runtime 030
243
243
  */
244
- export type PlayBindings = PlayAuthoringBindings;
244
+ export type PlayBindings<TInput = Record<string, unknown>> =
245
+ PlayAuthoringBindings<TInput>;
245
246
  export type SqlListenerOperation = PlaySqlListenerOperation;
246
247
  export type SqlListenerFilterScalar = PlaySqlListenerFilterScalar;
247
248
  export type SqlListenerFilterOperator = PlaySqlListenerFilterOperator;
@@ -873,14 +874,28 @@ export type DefinedPlay<
873
874
  DeeplineNamedPlay<TInput, TOutput>
874
875
  >;
875
876
 
876
- type PlayMetadata = {
877
+ type PlayHandlerInput<THandler> = THandler extends (
878
+ context: DeeplinePlayRuntimeContext,
879
+ input: infer TInput,
880
+ ) => Promise<PlayReturnObject>
881
+ ? TInput
882
+ : never;
883
+
884
+ type PlayHandlerOutput<THandler> = THandler extends (
885
+ context: DeeplinePlayRuntimeContext,
886
+ input: unknown,
887
+ ) => Promise<infer TOutput extends PlayReturnObject>
888
+ ? TOutput
889
+ : never;
890
+
891
+ type PlayMetadata<TInput = Record<string, unknown>> = {
877
892
  name: string;
878
893
  description?: string;
879
- bindings?: PlayBindings;
894
+ bindings?: PlayBindings<TInput>;
880
895
  inputSchema?: Record<string, unknown>;
881
- billing?: PlayBindings['billing'];
882
- runtime?: PlayBindings['runtime'];
883
- compatibility?: PlayBindings['compatibility'];
896
+ billing?: PlayBindings<TInput>['billing'];
897
+ runtime?: PlayBindings<TInput>['runtime'];
898
+ compatibility?: PlayBindings<TInput>['compatibility'];
884
899
  };
885
900
 
886
901
  const PLAY_METADATA_SYMBOL = Symbol.for('deepline.play.metadata');
@@ -1608,6 +1623,18 @@ export function defineInput<TInput>(
1608
1623
  export function definePlay<TInput, TOutput extends PlayReturnObject>(
1609
1624
  config: DefinePlayConfig<TInput, TOutput>,
1610
1625
  ): DefinedPlay<TInput, TOutput>;
1626
+ /** @internal Contextually type unannotated monitor-event handlers as unknown. */
1627
+ export function definePlay<TOutput extends PlayReturnObject>(
1628
+ name: string,
1629
+ fn: (
1630
+ context: DeeplinePlayRuntimeContext,
1631
+ input: unknown,
1632
+ ) => Promise<TOutput>,
1633
+ bindings: PlayBindings<never> & {
1634
+ readonly sqlListeners: readonly SqlListenerDeclaration[];
1635
+ },
1636
+ ): DefinedPlay<unknown, TOutput>;
1637
+ /* eslint-disable @typescript-eslint/no-explicit-any -- This constraint must infer a concrete contravariant handler input. */
1611
1638
  /**
1612
1639
  * Define a play with a name and function.
1613
1640
  *
@@ -1616,11 +1643,17 @@ export function definePlay<TInput, TOutput extends PlayReturnObject>(
1616
1643
  * @param bindings - Play configuration, including runtime limits and triggers.
1617
1644
  * @returns Play handle.
1618
1645
  */
1619
- export function definePlay<TInput, TOutput extends PlayReturnObject>(
1646
+ export function definePlay<
1647
+ THandler extends (
1648
+ context: DeeplinePlayRuntimeContext,
1649
+ input: any,
1650
+ ) => Promise<PlayReturnObject>,
1651
+ >(
1620
1652
  name: string,
1621
- fn: (ctx: DeeplinePlayRuntimeContext, input: TInput) => Promise<TOutput>,
1622
- bindings?: PlayBindings,
1623
- ): DefinedPlay<TInput, TOutput>;
1653
+ fn: THandler,
1654
+ bindings?: PlayBindings<NoInfer<PlayHandlerInput<THandler>>>,
1655
+ ): DefinedPlay<PlayHandlerInput<THandler>, PlayHandlerOutput<THandler>>;
1656
+ /* eslint-enable @typescript-eslint/no-explicit-any */
1624
1657
  /**
1625
1658
  * @sdkReference runtime 010
1626
1659
  */
@@ -1630,7 +1663,7 @@ export function definePlay<TInput, TOutput extends PlayReturnObject>(
1630
1663
  ctx: DeeplinePlayRuntimeContext,
1631
1664
  input: TInput,
1632
1665
  ) => Promise<TOutput>,
1633
- maybeBindings?: PlayBindings,
1666
+ maybeBindings?: PlayBindings<TInput>,
1634
1667
  ): DefinedPlay<TInput, TOutput> {
1635
1668
  const config =
1636
1669
  typeof nameOrConfig === 'string'
@@ -1690,7 +1723,7 @@ export function definePlay<TInput, TOutput extends PlayReturnObject>(
1690
1723
  );
1691
1724
  }
1692
1725
 
1693
- const metadata: PlayMetadata = {
1726
+ const metadata: PlayMetadata<TInput> = {
1694
1727
  name,
1695
1728
  ...(description ? { description } : {}),
1696
1729
  ...(bindings ? { bindings } : {}),
@@ -199,7 +199,7 @@ export const SDK_RELEASE = {
199
199
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
- version: '0.3.58',
202
+ version: '0.3.60',
203
203
  updateSummary:
204
204
  'Automatic CLI updates are now enabled by default. To opt out, run `deepline settings autoupdate off`; use `deepline settings autoupdate on` to re-enable updates or `deepline settings autoupdate pin <version>` to hold an exact release. This release also adds raw-v2 tool responses at toolResponse.rawV2 while preserving existing toolResponse.raw and declared getters.',
205
205
  packageCapabilities: {
@@ -1089,6 +1089,20 @@ export interface PlayDefinitionDetail {
1089
1089
  liveRevision?: PlayRevisionSummary | null;
1090
1090
  /** `true` if the working revision differs from the live revision. */
1091
1091
  isDraftDirty?: boolean;
1092
+ /** Live trigger state. This is operational state, not a source declaration. */
1093
+ triggerStatus?: PlayTriggerStatus;
1094
+ /**
1095
+ * Additive scheduler metadata for human/UI rendering. Values are absent for
1096
+ * plays with no cron binding and must never be inferred from source alone.
1097
+ */
1098
+ triggerMetadata?: {
1099
+ cronSchedule?: string | null;
1100
+ cronTimezone?: string | null;
1101
+ nextScheduledAt?: number | null;
1102
+ lastScheduledAt?: number | null;
1103
+ blockedReason?: string | null;
1104
+ [key: string]: unknown;
1105
+ } | null;
1092
1106
  /** Effective sandbox limit from the live revision, or the working draft before first publish. */
1093
1107
  runtimeLimit?: PlayRuntimeLimit | null;
1094
1108
  /** Present for a Play with a cron trigger. Advisory only; publish remains authoritative. */
@@ -1125,11 +1139,14 @@ export interface PlayListItem {
1125
1139
  currentRevision?: PlayRevisionSummary | null;
1126
1140
  liveRevision?: PlayRevisionSummary | null;
1127
1141
  aliases?: string[];
1128
- triggerStatus?: {
1129
- cron: string | null;
1130
- webhook: string | null;
1131
- blockedReason: string | null;
1132
- };
1142
+ triggerStatus?: PlayTriggerStatus;
1143
+ }
1144
+
1145
+ /** Additive live trigger state shared by list, get, and describe. */
1146
+ export interface PlayTriggerStatus {
1147
+ cron: string | null;
1148
+ webhook: string | null;
1149
+ blockedReason: string | null;
1133
1150
  }
1134
1151
 
1135
1152
  export interface ProductNotificationEventDefinition {
@@ -1202,11 +1219,9 @@ export interface PlayDescription {
1202
1219
  */
1203
1220
  liveVersion?: number | null;
1204
1221
  /** Whether this play's cron and webhook triggers are armed. */
1205
- triggerStatus?: {
1206
- cron: string | null;
1207
- webhook: string | null;
1208
- blockedReason: string | null;
1209
- };
1222
+ triggerStatus?: PlayTriggerStatus;
1223
+ /** Additive cron timing metadata for an operational description. */
1224
+ triggerMetadata?: PlayDefinitionDetail['triggerMetadata'];
1210
1225
  isDraftDirty?: boolean;
1211
1226
  latestRunId?: string | null;
1212
1227
  /** Effective sandbox limit from the revision named runs use. */
@@ -1512,7 +1527,12 @@ export interface PlayCheckSqlListenerEventSummary {
1512
1527
  export interface PlayCheckTriggersSummary {
1513
1528
  sqlListeners?: PlayCheckSqlListenerTrigger[];
1514
1529
  sqlListenerEvent?: PlayCheckSqlListenerEventSummary;
1515
- cron?: { schedule: string; timezone?: string };
1530
+ cron?: {
1531
+ schedule: string;
1532
+ timezone?: string;
1533
+ /** Static input supplied to every run started by this cron binding. */
1534
+ input?: Record<string, unknown>;
1535
+ };
1516
1536
  webhook?: true;
1517
1537
  }
1518
1538
 
@@ -30,6 +30,7 @@ import {
30
30
  compileRequestsWithStrategy,
31
31
  executeChunkedRequests,
32
32
  } from './batch-runtime';
33
+ import type { CompiledRequestBatch } from './batch-runtime';
33
34
  import type { AnyBatchOperationStrategy } from './batching-types';
34
35
  import {
35
36
  PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
@@ -112,6 +113,7 @@ import {
112
113
  import {
113
114
  isProviderUnavailable,
114
115
  serializeToolExecutionFailure,
116
+ ToolExecutionError,
115
117
  TOOL_EXECUTION_ERROR_SCHEMA_VERSION,
116
118
  TOOL_EXECUTION_ERROR_SCHEMA_HEADER,
117
119
  type ToolExecutionErrorSchemaVersion,
@@ -169,10 +171,10 @@ import {
169
171
  import {
170
172
  buildDurableCtxCallCacheKey,
171
173
  buildDurableRunPlayInvocationScope,
172
- buildDurableToolAggregateProviderIdempotencyKey,
173
174
  buildDurableToolAggregateReceiptKey,
174
175
  buildDurableToolCallAuthScopeDigest,
175
176
  buildDurableToolCallCacheKey,
177
+ buildDurableToolPayloadProviderIdempotencyKey,
176
178
  buildDurableToolProviderIdempotencyKey,
177
179
  buildDurableToolReceiptPrefix,
178
180
  resolveDurableCallCachePolicy,
@@ -2035,6 +2037,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2035
2037
  private readonly toolDispatchQueuedAtByLane = new Map<string, number>();
2036
2038
  private readonly toolDispatcherWakeWaiters = new Set<() => void>();
2037
2039
  private toolDispatcherFailure: unknown | null = null;
2040
+ /**
2041
+ * Deepline's own zero-credit denial is run-fatal. Keep it independently of
2042
+ * customer control flow so a broad `try/catch` cannot turn it into a
2043
+ * successful Play result. Provider account capacity remains a normal typed
2044
+ * waterfall miss.
2045
+ */
2046
+ private deeplineInsufficientCreditsFailure: ToolExecutionError | null = null;
2038
2047
  private toolCallResolvers = new Map<
2039
2048
  string,
2040
2049
  { resolve: (value: unknown) => void; reject: (reason: unknown) => void }
@@ -2217,10 +2226,30 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2217
2226
  : {}),
2218
2227
  }
2219
2228
  : undefined,
2220
- ) as Promise<TOutput>;
2229
+ ).catch((error: unknown) => {
2230
+ this.recordDeeplineInsufficientCreditsFailure(error);
2231
+ throw error;
2232
+ }) as Promise<TOutput>;
2221
2233
  },
2222
2234
  };
2223
2235
 
2236
+ private recordDeeplineInsufficientCreditsFailure(error: unknown): void {
2237
+ if (
2238
+ error instanceof ToolExecutionError &&
2239
+ error.origin === 'deepline' &&
2240
+ error.code === 'INSUFFICIENT_CREDITS'
2241
+ ) {
2242
+ this.deeplineInsufficientCreditsFailure ??= error;
2243
+ }
2244
+ }
2245
+
2246
+ /** Called by the runner after customer code returns, before a success settles. */
2247
+ assertNoDeeplineInsufficientCreditsFailure(): void {
2248
+ if (this.deeplineInsufficientCreditsFailure) {
2249
+ throw this.deeplineInsufficientCreditsFailure;
2250
+ }
2251
+ }
2252
+
2224
2253
  async tool<TOutput = unknown>(
2225
2254
  key: string,
2226
2255
  toolId: string,
@@ -11273,6 +11302,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11273
11302
  strategy,
11274
11303
  getPayload: (request: ToolCallRequest) => request.input,
11275
11304
  });
11305
+ const compileOwnedPhysicalBatch = (
11306
+ owned: ToolCallRequest[],
11307
+ ): CompiledRequestBatch<ToolCallRequest> => {
11308
+ const compiled = compileRequestsWithStrategy({
11309
+ requests: owned,
11310
+ strategy,
11311
+ getPayload: (request: ToolCallRequest) => request.input,
11312
+ });
11313
+ if (compiled.length !== 1) {
11314
+ throw new Error(
11315
+ 'Native batch owned members must compile into exactly one provider request.',
11316
+ );
11317
+ }
11318
+ return compiled[0]!;
11319
+ };
11276
11320
  const batchParallelismCeiling =
11277
11321
  this.governor.policy.pacing.workerToolBatchDefaultParallelism;
11278
11322
  const batchSize =
@@ -11282,6 +11326,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11282
11326
  batchParallelismCeiling,
11283
11327
  )
11284
11328
  : batchParallelismCeiling;
11329
+ const batchProviderActionVersion =
11330
+ (await this.#options.getToolActionCacheVersion?.(toolId))?.trim() ??
11331
+ 'unversioned';
11285
11332
  const successful: ToolCallRequest[] = [];
11286
11333
 
11287
11334
  const deliver = (request: ToolCallRequest, result: ToolExecuteResult) => {
@@ -11332,6 +11379,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11332
11379
  string,
11333
11380
  {
11334
11381
  members: ToolCallRequest[];
11382
+ compiled: CompiledRequestBatch<ToolCallRequest>;
11335
11383
  providerOperationKey: string;
11336
11384
  receiptLeaseExpiresAt: string | null;
11337
11385
  timeoutMs: number | undefined;
@@ -11380,26 +11428,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11380
11428
  detail.members[0]?.executionAuthScopeDigest ?? null,
11381
11429
  providerIdempotencyReceiptKey:
11382
11430
  detail.providerOperationKey,
11383
- providerIdempotencyKey:
11384
- buildDurableToolAggregateProviderIdempotencyKey({
11385
- aggregateReceiptKey: detail.providerOperationKey,
11386
- receiptKeys: detail.members.map(
11387
- (member) =>
11388
- member.providerIdempotencyKeyBase ??
11389
- member.receiptKey!,
11390
- ),
11391
- providerIdempotencyKeys: detail.members.map(
11392
- (member) =>
11393
- this.providerIdempotencyKeyForToolCall({
11394
- cacheKey:
11395
- member.providerIdempotencyKeyBase ??
11396
- member.receiptKey!,
11397
- force: member.force === true,
11398
- leaseId: member.receiptLeaseId,
11399
- logicalCallId: member.logicalCallId,
11400
- }),
11401
- ),
11402
- }),
11431
+ providerIdempotencyKey: detail.providerOperationKey,
11403
11432
  receiptLeaseExpiresAt: detail.receiptLeaseExpiresAt,
11404
11433
  timeoutMs: detail.timeoutMs,
11405
11434
  },
@@ -11477,10 +11506,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11477
11506
  (member) => batchRequestByCallId.get(member.memberId)!,
11478
11507
  );
11479
11508
  const receiptKeys = owned.map((request) => request.receiptKey!);
11480
- const providerReceiptKeys = owned.map(
11481
- (request) =>
11482
- request.providerIdempotencyKeyBase ?? request.receiptKey!,
11483
- );
11484
11509
  const aggregateReceiptKey = buildDurableToolAggregateReceiptKey({
11485
11510
  receiptKeys,
11486
11511
  prefix: 'batch',
@@ -11489,16 +11514,36 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11489
11514
  toolId: batch.batchOperation,
11490
11515
  }),
11491
11516
  });
11492
- const providerOperationKey = buildDurableToolAggregateReceiptKey({
11493
- receiptKeys: providerReceiptKeys,
11494
- prefix: 'batch',
11495
- aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
11517
+ const compiled = compileOwnedPhysicalBatch(owned);
11518
+ if (compiled.batchOperation !== batch.batchOperation) {
11519
+ throw new Error(
11520
+ 'Native batch owned members changed provider operation during compilation.',
11521
+ );
11522
+ }
11523
+ const providerOperationKey =
11524
+ buildDurableToolPayloadProviderIdempotencyKey({
11496
11525
  orgId: this.#options.orgId,
11497
11526
  toolId: batch.batchOperation,
11498
- }),
11499
- });
11527
+ executionAuthScopeDigest:
11528
+ owned[0]?.executionAuthScopeDigest ?? null,
11529
+ providerActionVersion: batchProviderActionVersion,
11530
+ payload: compiled.batchPayload,
11531
+ forceRefreshIdentities: owned
11532
+ .filter((request) => request.force === true)
11533
+ .map((request) =>
11534
+ this.providerIdempotencyKeyForToolCall({
11535
+ cacheKey:
11536
+ request.providerIdempotencyKeyBase ??
11537
+ request.receiptKey!,
11538
+ force: true,
11539
+ leaseId: request.receiptLeaseId,
11540
+ logicalCallId: request.logicalCallId,
11541
+ }),
11542
+ ),
11543
+ });
11500
11544
  dispatchDetails.set(aggregateReceiptKey, {
11501
11545
  members: owned,
11546
+ compiled,
11502
11547
  providerOperationKey,
11503
11548
  receiptLeaseExpiresAt: owned.reduce<string | null>(
11504
11549
  (earliest, request) => {
@@ -11523,7 +11568,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11523
11568
  return {
11524
11569
  ...members[0]!.call,
11525
11570
  tool: { id: batch.batchOperation, providerActionVersion: '' },
11526
- payload: batch.batchPayload,
11571
+ payload: compiled.batchPayload,
11527
11572
  compatibility: {
11528
11573
  resultReceiptKey: aggregateReceiptKey,
11529
11574
  providerOperationKey,
@@ -11531,7 +11576,40 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11531
11576
  };
11532
11577
  },
11533
11578
  splitResult: async ({ result: aggregate, members }) => {
11534
- const split = batch.splitResults(
11579
+ const receiptKeys = members.map((member) => {
11580
+ const request = batchRequestByCallId.get(member.memberId);
11581
+ if (!request?.receiptKey) {
11582
+ throw new Error(
11583
+ `Native batch Tool Call lost receipt for ${member.memberId}.`,
11584
+ );
11585
+ }
11586
+ return request.receiptKey;
11587
+ });
11588
+ const aggregateReceiptKey = buildDurableToolAggregateReceiptKey({
11589
+ receiptKeys,
11590
+ prefix: 'batch',
11591
+ aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
11592
+ orgId: this.#options.orgId,
11593
+ toolId: batch.batchOperation,
11594
+ }),
11595
+ });
11596
+ const detail = dispatchDetails.get(aggregateReceiptKey);
11597
+ if (!detail) {
11598
+ throw new Error(
11599
+ 'Native batch Tool Call lost its split compilation details.',
11600
+ );
11601
+ }
11602
+ if (
11603
+ detail.members.length !== members.length ||
11604
+ !detail.members.every(
11605
+ (request, index) => request.callId === members[index]?.memberId,
11606
+ )
11607
+ ) {
11608
+ throw new Error(
11609
+ 'Native batch Tool Call split members changed in transit.',
11610
+ );
11611
+ }
11612
+ const split = detail.compiled.splitResults(
11535
11613
  legacyResultForBatchSplitter(aggregate),
11536
11614
  );
11537
11615
  return await Promise.all(
@@ -12573,11 +12651,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12573
12651
  const receiptKeys = batch.memberRequests.map(
12574
12652
  (request) => request.cacheKey,
12575
12653
  );
12576
- const providerIdempotencyReceiptKeys =
12577
- batch.memberRequests.map(
12578
- (request) =>
12579
- request.providerIdempotencyKeyBase ?? request.cacheKey,
12580
- );
12581
12654
  const aggregateReceiptKey =
12582
12655
  buildDurableToolAggregateReceiptKey({
12583
12656
  receiptKeys,
@@ -12588,13 +12661,31 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12588
12661
  }),
12589
12662
  });
12590
12663
  const aggregateProviderIdempotencyReceiptKey =
12591
- buildDurableToolAggregateReceiptKey({
12592
- receiptKeys: providerIdempotencyReceiptKeys,
12593
- prefix: 'batch',
12594
- aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
12595
- orgId: this.#options.orgId,
12596
- toolId: batch.batchOperation,
12597
- }),
12664
+ buildDurableToolPayloadProviderIdempotencyKey({
12665
+ orgId: this.#options.orgId,
12666
+ toolId: batch.batchOperation,
12667
+ executionAuthScopeDigest:
12668
+ batch.memberRequests[0]?.executionAuthScopeDigest ??
12669
+ null,
12670
+ providerActionVersion:
12671
+ (
12672
+ await this.#options.getToolActionCacheVersion?.(
12673
+ batch.batchOperation,
12674
+ )
12675
+ )?.trim() ?? 'unversioned',
12676
+ payload: batch.batchPayload,
12677
+ forceRefreshIdentities: batch.memberRequests
12678
+ .filter((request) => request.force === true)
12679
+ .map((request) =>
12680
+ this.providerIdempotencyKeyForToolCall({
12681
+ cacheKey:
12682
+ request.providerIdempotencyKeyBase ??
12683
+ request.cacheKey,
12684
+ force: true,
12685
+ leaseId: request.receiptLeaseId,
12686
+ logicalCallId: request.logicalCallId,
12687
+ }),
12688
+ ),
12598
12689
  });
12599
12690
  let releaseToolSlot: () => void = () => undefined;
12600
12691
  try {
@@ -12621,22 +12712,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12621
12712
  providerIdempotencyReceiptKey:
12622
12713
  aggregateProviderIdempotencyReceiptKey,
12623
12714
  providerIdempotencyKey:
12624
- buildDurableToolAggregateProviderIdempotencyKey({
12625
- aggregateReceiptKey:
12626
- aggregateProviderIdempotencyReceiptKey,
12627
- receiptKeys: providerIdempotencyReceiptKeys,
12628
- providerIdempotencyKeys: batch.memberRequests.map(
12629
- (request) =>
12630
- this.providerIdempotencyKeyForToolCall({
12631
- cacheKey:
12632
- request.providerIdempotencyKeyBase ??
12633
- request.cacheKey,
12634
- force: request.force === true,
12635
- leaseId: request.receiptLeaseId,
12636
- logicalCallId: request.logicalCallId,
12637
- }),
12638
- ),
12639
- }),
12715
+ aggregateProviderIdempotencyReceiptKey,
12640
12716
  receiptLeaseExpiresAt: batch.memberRequests.reduce<
12641
12717
  string | null
12642
12718
  >((earliest, request) => {
@@ -241,3 +241,42 @@ export function buildDurableToolAggregateProviderIdempotencyKey(input: {
241
241
  stableStringify({ receiptKeys, providerIdempotencyKeys }),
242
242
  )}`;
243
243
  }
244
+
245
+ /**
246
+ * Provider-native batch caching is keyed by the exact compiled provider
247
+ * payload. Receipt identities coordinate ownership and terminal results; they
248
+ * cannot stand in for request identity because cache hits and foreign leases
249
+ * can remove members after the original batch was planned.
250
+ *
251
+ * An explicit force refresh is deliberately a separate namespace: it asks for
252
+ * a fresh provider operation even when the payload itself is unchanged.
253
+ */
254
+ export function buildDurableToolPayloadProviderIdempotencyKey(input: {
255
+ orgId?: string | null;
256
+ toolId: string;
257
+ executionAuthScopeDigest?: string | null;
258
+ providerActionVersion?: string | null;
259
+ payload: Record<string, unknown>;
260
+ forceRefreshIdentities?: readonly string[];
261
+ }): string {
262
+ const toolId = input.toolId.trim();
263
+ if (!toolId) {
264
+ throw new Error(
265
+ 'Provider payload idempotency key needs a non-empty toolId.',
266
+ );
267
+ }
268
+ const forceRefreshIdentities = input.forceRefreshIdentities
269
+ ?.map((identity) => identity.trim())
270
+ .filter(Boolean);
271
+ return `${buildDurableToolReceiptPrefix({
272
+ orgId: input.orgId,
273
+ toolId,
274
+ })}payload-v1:${sha256Hex(
275
+ stableStringify({
276
+ executionAuthScopeDigest: input.executionAuthScopeDigest?.trim() || null,
277
+ providerActionVersion: input.providerActionVersion?.trim() || null,
278
+ payload: input.payload,
279
+ ...(forceRefreshIdentities?.length ? { forceRefreshIdentities } : {}),
280
+ }),
281
+ )}`;
282
+ }