deepline 0.1.169 → 0.1.171

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.
@@ -1153,7 +1153,6 @@ export class PlayDedup implements DurableObject {
1153
1153
  if (!state && timeoutMs > 0) {
1154
1154
  await new Promise<void>((resolve) => {
1155
1155
  let settled = false;
1156
- let timeout: ReturnType<typeof setTimeout>;
1157
1156
  const finish = () => {
1158
1157
  if (settled) return;
1159
1158
  settled = true;
@@ -1164,7 +1163,7 @@ export class PlayDedup implements DurableObject {
1164
1163
  clearTimeout(timeout);
1165
1164
  resolve();
1166
1165
  };
1167
- timeout = setTimeout(finish, timeoutMs);
1166
+ const timeout = setTimeout(finish, timeoutMs);
1168
1167
  if (!this.childTerminalWaiters.has(eventKey)) {
1169
1168
  this.childTerminalWaiters.set(eventKey, new Set());
1170
1169
  }
@@ -159,6 +159,8 @@ import {
159
159
  markWorkerToolReceiptResultCached,
160
160
  markWorkerToolReceiptResultExecution,
161
161
  planWorkerToolReceiptGroups,
162
+ resolveWorkerToolReceiptGroupWaitMaxAttempts,
163
+ resolveWorkerToolRuntimeTimeoutMs,
162
164
  } from './runtime/tool-receipts';
163
165
  // The harness stub forwards leaf calls (validation, runtime-api HTTP) into
164
166
  // the long-lived Play Harness Worker via env.HARNESS. We import the
@@ -563,11 +565,13 @@ function resolveRuntimeDeadlineRemainingMs(runtimeDeadlineMs?: number): number {
563
565
 
564
566
  function resolveToolRuntimeApiTimeout(input: {
565
567
  requestInput: Record<string, unknown>;
568
+ timeoutMs?: number;
566
569
  runtimeDeadlineMs?: number;
567
570
  }): { timeoutMs: number; timeoutErrorMessage?: string } {
568
- const toolTimeoutMs = resolveRuntimeToolReceiptWaitTimeoutMs(
569
- input.requestInput,
570
- );
571
+ const toolTimeoutMs =
572
+ typeof input.timeoutMs === 'number' && Number.isFinite(input.timeoutMs)
573
+ ? Math.max(1, Math.ceil(input.timeoutMs))
574
+ : resolveRuntimeToolReceiptWaitTimeoutMs(input.requestInput);
571
575
  const remainingMs = resolveRuntimeDeadlineRemainingMs(
572
576
  input.runtimeDeadlineMs,
573
577
  );
@@ -1265,6 +1269,7 @@ async function executeTool(
1265
1269
  transientHttpRetrySafe = false,
1266
1270
  abortSignal?: AbortSignal,
1267
1271
  runtimeDeadlineMs?: number,
1272
+ runtimeTimeoutMs?: number,
1268
1273
  ): Promise<ToolExecuteResult> {
1269
1274
  if (args.toolId === 'test_wait_for_event' && workflowStep) {
1270
1275
  const result = await waitForSyntheticIntegrationEvent(
@@ -1287,6 +1292,7 @@ async function executeTool(
1287
1292
  transientHttpRetrySafe,
1288
1293
  abortSignal,
1289
1294
  runtimeDeadlineMs,
1295
+ runtimeTimeoutMs,
1290
1296
  );
1291
1297
  }
1292
1298
 
@@ -1300,6 +1306,7 @@ async function executeToolWithLifecycle(
1300
1306
  transientHttpRetrySafe = false,
1301
1307
  abortSignal?: AbortSignal,
1302
1308
  runtimeDeadlineMs?: number,
1309
+ runtimeTimeoutMs?: number,
1303
1310
  ): Promise<ToolExecuteResult> {
1304
1311
  callbacks?.onToolCalled?.(args.toolId, nowMs());
1305
1312
  try {
@@ -1312,6 +1319,7 @@ async function executeToolWithLifecycle(
1312
1319
  transientHttpRetrySafe,
1313
1320
  abortSignal,
1314
1321
  runtimeDeadlineMs,
1322
+ runtimeTimeoutMs,
1315
1323
  );
1316
1324
  } catch (error) {
1317
1325
  callbacks?.onToolFailed?.(args.toolId, nowMs());
@@ -1329,6 +1337,8 @@ function normalizeToolExecuteArgs(request: unknown): {
1329
1337
  input: Record<string, unknown>;
1330
1338
  force?: boolean;
1331
1339
  staleAfterSeconds?: number;
1340
+ timeoutMs?: number;
1341
+ receiptWaitMs?: number;
1332
1342
  } {
1333
1343
  if (!isToolExecuteRecord(request)) {
1334
1344
  throw new Error(
@@ -1354,6 +1364,14 @@ function normalizeToolExecuteArgs(request: unknown): {
1354
1364
  ...(typeof request.staleAfterSeconds === 'number'
1355
1365
  ? { staleAfterSeconds: request.staleAfterSeconds }
1356
1366
  : {}),
1367
+ ...(typeof request.timeoutMs === 'number' &&
1368
+ Number.isFinite(request.timeoutMs)
1369
+ ? { timeoutMs: request.timeoutMs }
1370
+ : {}),
1371
+ ...(typeof request.receiptWaitMs === 'number' &&
1372
+ Number.isFinite(request.receiptWaitMs)
1373
+ ? { receiptWaitMs: request.receiptWaitMs }
1374
+ : {}),
1357
1375
  };
1358
1376
  }
1359
1377
 
@@ -1450,6 +1468,7 @@ async function callToolDirect(
1450
1468
  transientHttpRetrySafe = false,
1451
1469
  abortSignal?: AbortSignal,
1452
1470
  runtimeDeadlineMs?: number,
1471
+ runtimeTimeoutMs?: number,
1453
1472
  ): Promise<ToolExecuteResult> {
1454
1473
  const { id, toolId, input } = args;
1455
1474
  const path = `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`;
@@ -1464,6 +1483,7 @@ async function callToolDirect(
1464
1483
  try {
1465
1484
  const runtimeApiTimeout = resolveToolRuntimeApiTimeout({
1466
1485
  requestInput: input,
1486
+ timeoutMs: runtimeTimeoutMs,
1467
1487
  runtimeDeadlineMs,
1468
1488
  });
1469
1489
  res = await fetchRuntimeApi(
@@ -1640,6 +1660,7 @@ type WorkerToolBatchRequest = {
1640
1660
  receiptKey: string | null;
1641
1661
  force: boolean;
1642
1662
  receiptWaitMaxAttempts: number;
1663
+ runtimeTimeoutMs?: number;
1643
1664
  toolId: string;
1644
1665
  input: Record<string, unknown>;
1645
1666
  workflowStep?: WorkflowStep;
@@ -1658,6 +1679,20 @@ type PreparedWorkerToolBatchRequests = {
1658
1679
  deferredClaimedRequests: Promise<ClaimedWorkerToolBatchRequest[]>[];
1659
1680
  };
1660
1681
 
1682
+ function resolveClaimedWorkerToolRuntimeTimeoutMs(
1683
+ claimedRequests: ClaimedWorkerToolBatchRequest[],
1684
+ input: { runtimeDeadlineMs?: number },
1685
+ ): number | undefined {
1686
+ return resolveWorkerToolRuntimeTimeoutMs(claimedRequests, {
1687
+ resolveOwnerTimeoutMs: (request) =>
1688
+ resolveToolRuntimeApiTimeout({
1689
+ requestInput: request.input,
1690
+ timeoutMs: request.runtimeTimeoutMs,
1691
+ runtimeDeadlineMs: input.runtimeDeadlineMs,
1692
+ }).timeoutMs,
1693
+ });
1694
+ }
1695
+
1661
1696
  const WORKER_TOOL_BATCH_GRACE_MS = 250;
1662
1697
  const MAP_EXECUTION_HEARTBEAT_INTERVAL_MS = 5_000;
1663
1698
  const MAP_INCREMENTAL_PERSIST_CHUNK_ROWS = 100;
@@ -1802,6 +1837,7 @@ class WorkerToolBatchScheduler {
1802
1837
  input: Record<string, unknown>,
1803
1838
  workflowStep?: WorkflowStep,
1804
1839
  options?: { force?: boolean; staleAfterSeconds?: number | null },
1840
+ runtimeOptions?: { timeoutMs?: number; receiptWaitMs?: number },
1805
1841
  ): Promise<unknown> {
1806
1842
  const providerActionVersion =
1807
1843
  await this.resolveToolActionCacheVersion(toolId);
@@ -1819,7 +1855,14 @@ class WorkerToolBatchScheduler {
1819
1855
  cacheKey: receiptKey,
1820
1856
  receiptKey,
1821
1857
  force: options?.force === true,
1822
- receiptWaitMaxAttempts: resolveRuntimeToolReceiptWaitMaxAttempts(input),
1858
+ receiptWaitMaxAttempts: resolveRuntimeToolReceiptWaitMaxAttempts(
1859
+ typeof runtimeOptions?.receiptWaitMs === 'number'
1860
+ ? { max_wait_ms: runtimeOptions.receiptWaitMs }
1861
+ : input,
1862
+ ),
1863
+ ...(typeof runtimeOptions?.timeoutMs === 'number'
1864
+ ? { runtimeTimeoutMs: runtimeOptions.timeoutMs }
1865
+ : {}),
1823
1866
  toolId,
1824
1867
  input,
1825
1868
  workflowStep,
@@ -2005,7 +2048,10 @@ class WorkerToolBatchScheduler {
2005
2048
  try {
2006
2049
  const receipt = await this.waitForDurableToolReceipt({
2007
2050
  receiptKey: input.receiptKey,
2008
- maxAttempts: request.receiptWaitMaxAttempts,
2051
+ maxAttempts: resolveWorkerToolReceiptGroupWaitMaxAttempts(
2052
+ input.group,
2053
+ (groupRequest) => groupRequest.receiptWaitMaxAttempts,
2054
+ ),
2009
2055
  });
2010
2056
  await this.resolveCompletedDurableToolReceiptGroup({
2011
2057
  group: input.group,
@@ -2114,7 +2160,10 @@ class WorkerToolBatchScheduler {
2114
2160
  try {
2115
2161
  const receipt = await this.waitForDurableToolReceipt({
2116
2162
  receiptKey: input.receiptKey,
2117
- maxAttempts: request.receiptWaitMaxAttempts,
2163
+ maxAttempts: resolveWorkerToolReceiptGroupWaitMaxAttempts(
2164
+ input.group,
2165
+ (groupRequest) => groupRequest.receiptWaitMaxAttempts,
2166
+ ),
2118
2167
  });
2119
2168
  await this.resolveCompletedDurableToolReceiptGroup({
2120
2169
  group: input.group,
@@ -2516,6 +2565,9 @@ class WorkerToolBatchScheduler {
2516
2565
  toolContract?.retrySafeTransientHttp === true,
2517
2566
  this.abortSignal,
2518
2567
  this.runtimeDeadlineMs,
2568
+ resolveClaimedWorkerToolRuntimeTimeoutMs([claimed], {
2569
+ runtimeDeadlineMs: this.runtimeDeadlineMs,
2570
+ }),
2519
2571
  );
2520
2572
  } catch (error) {
2521
2573
  this.rejectRequests(
@@ -2686,6 +2738,9 @@ async function executeBatchedWorkerToolGroup(input: {
2686
2738
  toolContract?.retrySafeTransientHttp === true,
2687
2739
  input.abortSignal,
2688
2740
  input.runtimeDeadlineMs,
2741
+ resolveClaimedWorkerToolRuntimeTimeoutMs(batch.memberRequests, {
2742
+ runtimeDeadlineMs: input.runtimeDeadlineMs,
2743
+ }),
2689
2744
  );
2690
2745
  } catch (error) {
2691
2746
  input.callbacks?.onToolFailed?.(batch.batchOperation, nowMs());
@@ -4474,9 +4529,12 @@ function createMinimalWorkerCtx(
4474
4529
  const executeWithRuntimeReceipt = async <T>(
4475
4530
  key: string,
4476
4531
  execute: () => Promise<T> | T,
4477
- repairRunningReceiptForSameRun = false,
4478
- reclaimRunning = false,
4479
- repairRunningReceiptForSameRunAfterWaitTimeout = false,
4532
+ options: {
4533
+ repairRunningReceiptForSameRun?: boolean;
4534
+ reclaimRunning?: boolean;
4535
+ repairRunningReceiptForSameRunAfterWaitTimeout?: boolean;
4536
+ runningReceiptWaitMaxAttempts?: number;
4537
+ } = {},
4480
4538
  ): Promise<T> => {
4481
4539
  const serialized = await runWorkerRuntimeReceiptBoundary<unknown>({
4482
4540
  orgId: req.orgId,
@@ -4485,9 +4543,12 @@ function createMinimalWorkerCtx(
4485
4543
  key,
4486
4544
  receiptStore,
4487
4545
  execute: async () => serializeDurableStepValue(await execute()),
4488
- repairRunningReceiptForSameRun,
4489
- reclaimRunning,
4490
- repairRunningReceiptForSameRunAfterWaitTimeout,
4546
+ repairRunningReceiptForSameRun:
4547
+ options.repairRunningReceiptForSameRun ?? false,
4548
+ reclaimRunning: options.reclaimRunning ?? false,
4549
+ repairRunningReceiptForSameRunAfterWaitTimeout:
4550
+ options.repairRunningReceiptForSameRunAfterWaitTimeout ?? false,
4551
+ runningReceiptWaitMaxAttempts: options.runningReceiptWaitMaxAttempts,
4491
4552
  });
4492
4553
  return deserializeDurableStepValue(serialized) as T;
4493
4554
  };
@@ -4509,7 +4570,6 @@ function createMinimalWorkerCtx(
4509
4570
  )(name, async () => serializeDurableStepValue(await execute()));
4510
4571
  return deserializeDurableStepValue(serialized) as T;
4511
4572
  },
4512
- false,
4513
4573
  );
4514
4574
  };
4515
4575
  const nextCtxStepReceiptKey = (
@@ -6280,6 +6340,10 @@ function createMinimalWorkerCtx(
6280
6340
  force: request.force === true,
6281
6341
  staleAfterSeconds: request.staleAfterSeconds,
6282
6342
  },
6343
+ {
6344
+ timeoutMs: request.timeoutMs,
6345
+ receiptWaitMs: request.receiptWaitMs,
6346
+ },
6283
6347
  );
6284
6348
  },
6285
6349
  },
@@ -6674,9 +6738,11 @@ function createMinimalWorkerCtx(
6674
6738
  childPlaySlot?.release();
6675
6739
  }
6676
6740
  },
6677
- false,
6678
- false,
6679
- true,
6741
+ {
6742
+ repairRunningReceiptForSameRunAfterWaitTimeout: true,
6743
+ runningReceiptWaitMaxAttempts:
6744
+ resolveRuntimeToolReceiptWaitMaxAttempts(input),
6745
+ },
6680
6746
  );
6681
6747
  },
6682
6748
  async fetch(
@@ -17,7 +17,7 @@ export { TOOL_CALLING_MAP_CHUNK_SIZE };
17
17
  // Paid Cloudflare Workers support a much higher configured subrequest limit.
18
18
  // Keep a large buffer for coordinator/storage calls around row-level unbatched
19
19
  // tool RPCs, but avoid pathological one-row chunks for provider waterfalls.
20
- export const UNBATCHED_TOOL_SUBREQUESTS_PER_CHUNK_BUDGET = 500;
20
+ export const UNBATCHED_TOOL_SUBREQUESTS_PER_CHUNK_BUDGET = 200;
21
21
  // Fresh unbatched tool calls use one RUNTIME_API integration execute RPC and
22
22
  // one HARNESS durable-receipt completion RPC. Batch-cap rows by both.
23
23
  export const SUBREQUESTS_PER_UNBATCHED_TOOL_CALL = 2;
@@ -27,8 +27,11 @@ type RuntimeReceiptContext = {
27
27
  receiptStore: WorkerRuntimeReceiptStore;
28
28
  };
29
29
 
30
- const WORKER_RECEIPT_WAIT_MAX_ATTEMPTS = 240;
30
+ const WORKER_RECEIPT_DEFAULT_WAIT_MS = 300_000;
31
31
  const WORKER_RECEIPT_WAIT_DELAY_MS = 250;
32
+ const WORKER_RECEIPT_WAIT_MAX_ATTEMPTS = Math.ceil(
33
+ WORKER_RECEIPT_DEFAULT_WAIT_MS / WORKER_RECEIPT_WAIT_DELAY_MS,
34
+ );
32
35
 
33
36
  class RuntimeReceiptWaitTimeoutError extends Error {
34
37
  constructor(key: string) {
@@ -95,6 +95,60 @@ export function planWorkerToolReceiptGroups<TRequest>(
95
95
  };
96
96
  }
97
97
 
98
+ export function resolveWorkerToolReceiptGroupWaitMaxAttempts<TRequest>(
99
+ requests: TRequest[],
100
+ getWaitMaxAttempts: (request: TRequest) => number,
101
+ ): number {
102
+ return requests.reduce((maxAttempts, request) => {
103
+ const attempts = getWaitMaxAttempts(request);
104
+ return Number.isFinite(attempts) && attempts > maxAttempts
105
+ ? attempts
106
+ : maxAttempts;
107
+ }, 1);
108
+ }
109
+
110
+ export type WorkerToolRuntimeTimeoutRequest = {
111
+ input: Record<string, unknown>;
112
+ runtimeTimeoutMs?: number;
113
+ };
114
+
115
+ export type WorkerToolRuntimeTimeoutClaim = {
116
+ request: WorkerToolRuntimeTimeoutRequest;
117
+ followers: WorkerToolRuntimeTimeoutRequest[];
118
+ };
119
+
120
+ export function resolveWorkerToolRuntimeTimeoutMs(
121
+ claimedRequests: WorkerToolRuntimeTimeoutClaim[],
122
+ input: {
123
+ resolveOwnerTimeoutMs: (
124
+ request: WorkerToolRuntimeTimeoutRequest,
125
+ ) => number | undefined;
126
+ },
127
+ ): number | undefined {
128
+ let timeoutMs = 0;
129
+ for (const claimed of claimedRequests) {
130
+ const ownerTimeoutMs = input.resolveOwnerTimeoutMs(claimed.request);
131
+ if (
132
+ typeof ownerTimeoutMs === 'number' &&
133
+ Number.isFinite(ownerTimeoutMs) &&
134
+ ownerTimeoutMs > timeoutMs
135
+ ) {
136
+ timeoutMs = ownerTimeoutMs;
137
+ }
138
+ for (const follower of claimed.followers) {
139
+ const followerTimeoutMs = follower.runtimeTimeoutMs;
140
+ if (
141
+ typeof followerTimeoutMs === 'number' &&
142
+ Number.isFinite(followerTimeoutMs) &&
143
+ followerTimeoutMs > timeoutMs
144
+ ) {
145
+ timeoutMs = followerTimeoutMs;
146
+ }
147
+ }
148
+ }
149
+ return timeoutMs > 0 ? timeoutMs : undefined;
150
+ }
151
+
98
152
  export function markWorkerToolReceiptResultCached(
99
153
  value: unknown,
100
154
  cacheKey: string,
@@ -300,6 +300,10 @@ export type ToolExecutionRequest = {
300
300
  force?: boolean;
301
301
  /** Numeric TTL in seconds for this tool checkpoint. */
302
302
  staleAfterSeconds?: number;
303
+ /** Runtime transport timeout in milliseconds. This is not sent to the provider. */
304
+ timeoutMs?: number;
305
+ /** Follower wait budget in milliseconds before a running receipt is reclaimable. */
306
+ receiptWaitMs?: number;
303
307
  };
304
308
 
305
309
  export type StepResolver<Row, Value> = (
@@ -104,10 +104,10 @@ export const SDK_RELEASE = {
104
104
  // 0.1.111 ships dataset-native tool list getters and result row datasets.
105
105
  // 0.1.154 removes the short-lived generated enrich StepOptions recompute
106
106
  // fields shipped in 0.1.153.
107
- version: '0.1.169',
107
+ version: '0.1.171',
108
108
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
109
109
  supportPolicy: {
110
- latest: '0.1.169',
110
+ latest: '0.1.171',
111
111
  minimumSupported: '0.1.53',
112
112
  deprecatedBelow: '0.1.53',
113
113
  commandMinimumSupported: [
@@ -72,6 +72,7 @@ import {
72
72
  import {
73
73
  RuntimeReceiptWaitTimeoutError,
74
74
  executeWithDurableRuntimeReceipt,
75
+ resolveRuntimeToolReceiptWaitMaxAttempts,
75
76
  runtimeReceiptOutput as durableRuntimeReceiptOutput,
76
77
  waitForCompletedRuntimeReceipt,
77
78
  type DurableReceiptExecutionStore,
@@ -709,6 +710,8 @@ export class PlayContextImpl {
709
710
  description?: string;
710
711
  force?: boolean;
711
712
  staleAfterSeconds?: number;
713
+ timeoutMs?: number;
714
+ receiptWaitMs?: number;
712
715
  }): Promise<TOutput> => {
713
716
  if (!request || typeof request !== 'object' || Array.isArray(request)) {
714
717
  throw new Error(
@@ -735,7 +738,9 @@ export class PlayContextImpl {
735
738
  request.input,
736
739
  request.description ||
737
740
  request.force === true ||
738
- request.staleAfterSeconds !== undefined
741
+ request.staleAfterSeconds !== undefined ||
742
+ request.timeoutMs !== undefined ||
743
+ request.receiptWaitMs !== undefined
739
744
  ? {
740
745
  ...(request.description
741
746
  ? { description: request.description }
@@ -744,6 +749,12 @@ export class PlayContextImpl {
744
749
  ...(request.staleAfterSeconds !== undefined
745
750
  ? { staleAfterSeconds: request.staleAfterSeconds }
746
751
  : {}),
752
+ ...(request.timeoutMs !== undefined
753
+ ? { timeoutMs: request.timeoutMs }
754
+ : {}),
755
+ ...(request.receiptWaitMs !== undefined
756
+ ? { receiptWaitMs: request.receiptWaitMs }
757
+ : {}),
747
758
  }
748
759
  : undefined,
749
760
  ) as Promise<TOutput>;
@@ -1283,10 +1294,12 @@ export class PlayContextImpl {
1283
1294
 
1284
1295
  private async waitForCompletedRuntimeToolReceipt(
1285
1296
  key: string,
1297
+ maxAttempts?: number,
1286
1298
  ): Promise<RuntimeStepReceipt> {
1287
1299
  return await waitForCompletedRuntimeReceipt({
1288
1300
  receiptKey: key,
1289
1301
  store: this.durableReceiptExecutionStore(),
1302
+ maxAttempts,
1290
1303
  });
1291
1304
  }
1292
1305
 
@@ -1301,6 +1314,8 @@ export class PlayContextImpl {
1301
1314
  staleAfterSeconds?: number | null;
1302
1315
  repairRunningReceiptForSameRun?: boolean;
1303
1316
  repairRunningReceiptForSameRunAfterWaitTimeout?: boolean;
1317
+ runningReceiptWaitMaxAttempts?: number;
1318
+ runningReceiptWaitDelayMs?: number;
1304
1319
  reclaimRunning?: boolean;
1305
1320
  markSkipped?: (output: T) => Promise<void> | void;
1306
1321
  onRecovered?: (
@@ -1332,6 +1347,8 @@ export class PlayContextImpl {
1332
1347
  repairRunningReceiptForSameRun: opts.repairRunningReceiptForSameRun,
1333
1348
  repairRunningReceiptForSameRunAfterWaitTimeout:
1334
1349
  opts.repairRunningReceiptForSameRunAfterWaitTimeout,
1350
+ runningReceiptWaitMaxAttempts: opts.runningReceiptWaitMaxAttempts,
1351
+ runningReceiptWaitDelayMs: opts.runningReceiptWaitDelayMs,
1335
1352
  reclaimRunning: opts.reclaimRunning,
1336
1353
  markSkipped: opts.markSkipped,
1337
1354
  onRecovered: opts.onRecovered,
@@ -3449,7 +3466,9 @@ export class PlayContextImpl {
3449
3466
  });
3450
3467
  }
3451
3468
  this.log(`Calling tool: ${toolId}`);
3452
- const execution = await this.callToolExecutionAPI(toolId, input);
3469
+ const execution = await this.callToolExecutionAPI(toolId, input, {
3470
+ timeoutMs: options?.timeoutMs,
3471
+ });
3453
3472
  const wrapped = await this.wrapToolExecutionResult({
3454
3473
  toolId,
3455
3474
  status: execution.status,
@@ -3535,6 +3554,12 @@ export class PlayContextImpl {
3535
3554
  fieldName,
3536
3555
  toolId,
3537
3556
  input,
3557
+ ...(typeof options?.timeoutMs === 'number'
3558
+ ? { timeoutMs: options.timeoutMs }
3559
+ : {}),
3560
+ ...(typeof options?.receiptWaitMs === 'number'
3561
+ ? { receiptWaitMs: options.receiptWaitMs }
3562
+ : {}),
3538
3563
  tableNamespace: store.tableNamespace,
3539
3564
  rowKey: store.rowKey ?? null,
3540
3565
  description: normalizeStepDescription(options?.description),
@@ -3573,6 +3598,11 @@ export class PlayContextImpl {
3573
3598
  receiptKey: durableCacheKey,
3574
3599
  }),
3575
3600
  ),
3601
+ runningReceiptWaitMaxAttempts: resolveRuntimeToolReceiptWaitMaxAttempts(
3602
+ typeof options?.receiptWaitMs === 'number'
3603
+ ? { max_wait_ms: options.receiptWaitMs }
3604
+ : input,
3605
+ ),
3576
3606
  execute: executeTool,
3577
3607
  },
3578
3608
  );
@@ -3853,6 +3883,8 @@ export class PlayContextImpl {
3853
3883
  );
3854
3884
  },
3855
3885
  repairRunningReceiptForSameRunAfterWaitTimeout: true,
3886
+ runningReceiptWaitMaxAttempts:
3887
+ resolveRuntimeToolReceiptWaitMaxAttempts(input),
3856
3888
  execute: executePlayCall,
3857
3889
  },
3858
3890
  );
@@ -4334,6 +4366,49 @@ export class PlayContextImpl {
4334
4366
  await this.rejectToolCall(toolId, request, error);
4335
4367
  }
4336
4368
  };
4369
+ const requestsWithLiveFollowers = (
4370
+ owner: ToolCallRequest,
4371
+ ): ToolCallRequest[] => [
4372
+ owner,
4373
+ ...(liveFollowersByOwnerCallId.get(owner.callId) ?? []),
4374
+ ];
4375
+ const resolveRuntimeTimeoutMsForRequests = (
4376
+ requests: ToolCallRequest[],
4377
+ ): number | undefined => {
4378
+ const timeoutMs = Math.max(
4379
+ ...requests
4380
+ .map((request) => request.timeoutMs)
4381
+ .filter(
4382
+ (candidate): candidate is number =>
4383
+ typeof candidate === 'number' &&
4384
+ Number.isFinite(candidate) &&
4385
+ candidate > 0,
4386
+ ),
4387
+ 0,
4388
+ );
4389
+ return timeoutMs > 0 ? timeoutMs : undefined;
4390
+ };
4391
+ const resolveRuntimeTimeoutMsForClaimedOwners = (
4392
+ owners: ToolCallRequest[],
4393
+ ): number | undefined => {
4394
+ let timeoutMs = 0;
4395
+ for (const owner of owners) {
4396
+ if (
4397
+ typeof owner.timeoutMs !== 'number' ||
4398
+ !Number.isFinite(owner.timeoutMs) ||
4399
+ owner.timeoutMs <= 0
4400
+ ) {
4401
+ return undefined;
4402
+ }
4403
+ timeoutMs = Math.max(
4404
+ timeoutMs,
4405
+ resolveRuntimeTimeoutMsForRequests(
4406
+ requestsWithLiveFollowers(owner),
4407
+ ) ?? 0,
4408
+ );
4409
+ }
4410
+ return timeoutMs > 0 ? timeoutMs : undefined;
4411
+ };
4337
4412
 
4338
4413
  const durableExistingRunningHandlers: Promise<void>[] = [];
4339
4414
  if (
@@ -4452,10 +4527,25 @@ export class PlayContextImpl {
4452
4527
  receiptKey: string,
4453
4528
  requestsForKey: ToolCallRequest[],
4454
4529
  ): Promise<void> => {
4530
+ const waitMaxAttempts =
4531
+ requestsForKey.length > 0
4532
+ ? Math.max(
4533
+ ...requestsForKey.map((request) =>
4534
+ resolveRuntimeToolReceiptWaitMaxAttempts(
4535
+ typeof request.receiptWaitMs === 'number'
4536
+ ? { max_wait_ms: request.receiptWaitMs }
4537
+ : request.input,
4538
+ ),
4539
+ ),
4540
+ )
4541
+ : undefined;
4455
4542
  try {
4456
4543
  await resolveRequestsFromReceipt(
4457
4544
  requestsForKey,
4458
- await this.waitForCompletedRuntimeToolReceipt(receiptKey),
4545
+ await this.waitForCompletedRuntimeToolReceipt(
4546
+ receiptKey,
4547
+ waitMaxAttempts,
4548
+ ),
4459
4549
  'in_flight',
4460
4550
  );
4461
4551
  return;
@@ -4511,6 +4601,9 @@ export class PlayContextImpl {
4511
4601
  const execution = await this.callToolExecutionAPI(
4512
4602
  toolId,
4513
4603
  owner.input,
4604
+ {
4605
+ timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([owner]),
4606
+ },
4514
4607
  );
4515
4608
  const result = await this.resolveToolCall(
4516
4609
  toolId,
@@ -4619,6 +4712,11 @@ export class PlayContextImpl {
4619
4712
  await this.callToolAPI(
4620
4713
  batch.batchOperation,
4621
4714
  batch.batchPayload,
4715
+ {
4716
+ timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners(
4717
+ batch.memberRequests,
4718
+ ),
4719
+ },
4622
4720
  ),
4623
4721
  onChunkComplete: async (chunkResults) => {
4624
4722
  for (const entry of chunkResults) {
@@ -4674,6 +4772,11 @@ export class PlayContextImpl {
4674
4772
  const execution = await this.callToolExecutionAPI(
4675
4773
  toolId,
4676
4774
  request.input,
4775
+ {
4776
+ timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
4777
+ request,
4778
+ ]),
4779
+ },
4677
4780
  );
4678
4781
  const result = await this.resolveToolCall(
4679
4782
  toolId,
@@ -4778,8 +4881,9 @@ export class PlayContextImpl {
4778
4881
  private async callToolAPI(
4779
4882
  toolId: string,
4780
4883
  input: Record<string, unknown>,
4884
+ options?: { timeoutMs?: number },
4781
4885
  ): Promise<unknown> {
4782
- const execution = await this.callToolExecutionAPI(toolId, input);
4886
+ const execution = await this.callToolExecutionAPI(toolId, input, options);
4783
4887
  if (execution.toolResponse && 'raw' in execution.toolResponse) {
4784
4888
  return execution.toolResponse.raw;
4785
4889
  }
@@ -4794,6 +4898,7 @@ export class PlayContextImpl {
4794
4898
  private async callToolExecutionAPI(
4795
4899
  toolId: string,
4796
4900
  input: Record<string, unknown>,
4901
+ options?: { timeoutMs?: number },
4797
4902
  ): Promise<ParsedToolExecuteResponse> {
4798
4903
  if (!this.#options.executorToken || !this.#options.baseUrl) {
4799
4904
  throw new Error(
@@ -4801,6 +4906,11 @@ export class PlayContextImpl {
4801
4906
  );
4802
4907
  }
4803
4908
  const url = `${this.#options.baseUrl}/api/v2/integrations/${encodeURIComponent(toolId)}/execute`;
4909
+ const timeoutMs =
4910
+ typeof options?.timeoutMs === 'number' &&
4911
+ Number.isFinite(options.timeoutMs)
4912
+ ? Math.max(1, Math.ceil(options.timeoutMs))
4913
+ : undefined;
4804
4914
 
4805
4915
  // The Governor's tool slot is the single seam for tool-call budget + the
4806
4916
  // global tool-concurrency backstop + per-(org, provider) pacing. It blocks
@@ -4831,9 +4941,20 @@ export class PlayContextImpl {
4831
4941
 
4832
4942
  while (true) {
4833
4943
  let response: Response;
4944
+ const abortController = timeoutMs ? new AbortController() : null;
4945
+ const timeoutHandle = timeoutMs
4946
+ ? setTimeout(() => {
4947
+ abortController?.abort(
4948
+ new Error(
4949
+ `Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
4950
+ ),
4951
+ );
4952
+ }, timeoutMs)
4953
+ : null;
4834
4954
  try {
4835
4955
  response = await fetch(url, {
4836
4956
  method: 'POST',
4957
+ signal: abortController?.signal,
4837
4958
  headers: {
4838
4959
  'Content-Type': 'application/json',
4839
4960
  Authorization: `Bearer ${this.#options.executorToken}`,
@@ -4856,6 +4977,13 @@ export class PlayContextImpl {
4856
4977
  }),
4857
4978
  });
4858
4979
  } catch (error) {
4980
+ if (abortController?.signal.aborted) {
4981
+ throw abortController.signal.reason instanceof Error
4982
+ ? abortController.signal.reason
4983
+ : new Error(
4984
+ `Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
4985
+ );
4986
+ }
4859
4987
  transportAttempt += 1;
4860
4988
  const message =
4861
4989
  error instanceof Error ? error.message : String(error);
@@ -4876,6 +5004,10 @@ export class PlayContextImpl {
4876
5004
  throw new Error(
4877
5005
  `Tool ${toolId} transport failed calling ${url} after ${transportAttempt}/${TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS} attempts: ${message}`,
4878
5006
  );
5007
+ } finally {
5008
+ if (timeoutHandle) {
5009
+ clearTimeout(timeoutHandle);
5010
+ }
4879
5011
  }
4880
5012
 
4881
5013
  span.setAttribute('plays.http_status_code', response.status);