deepline 0.1.174 → 0.1.176

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.
@@ -103,6 +103,7 @@ export type PlayWorkflowParams = {
103
103
  executionPlan?: ExecutionPlan | null;
104
104
  childPlayManifests?: PlayRuntimeManifestMap | null;
105
105
  playCallGovernance?: PlayCallGovernanceSnapshot | null;
106
+ forceToolRefresh?: boolean | null;
106
107
  preloadedDbSessions?: PreloadedRuntimeDbSession[] | null;
107
108
  preloadedDbSessionRef?: {
108
109
  runId: string;
@@ -2214,6 +2215,7 @@ function buildChildWorkflowParams(input: {
2214
2215
  executorToken: childToken,
2215
2216
  baseUrl,
2216
2217
  integrationMode: normalizeIntegrationMode(body.integrationMode),
2218
+ forceToolRefresh: body.forceToolRefresh === true,
2217
2219
  orgId,
2218
2220
  userEmail: typeof body.userEmail === 'string' ? body.userEmail : '',
2219
2221
  userId: typeof body.userId === 'string' ? body.userId : null,
@@ -2274,6 +2276,7 @@ function runRequestFromPlayWorkflowParams(
2274
2276
  executionPlan: params.executionPlan ?? null,
2275
2277
  childPlayManifests: params.childPlayManifests ?? null,
2276
2278
  playCallGovernance: params.playCallGovernance ?? null,
2279
+ force: params.forceToolRefresh === true,
2277
2280
  preloadedDbSessions: params.preloadedDbSessions ?? null,
2278
2281
  inlineChildRunRegistered:
2279
2282
  params.runtimeBackend === 'cf_workflows_dynamic_worker_inline_child',
@@ -270,6 +270,7 @@ type RunRequest = {
270
270
  playName: string;
271
271
  graphHash?: string | null;
272
272
  userEmail: string | null;
273
+ force?: boolean | null;
273
274
  runtimeInput: Record<string, unknown>;
274
275
  /** Optional inline CSV rows (for plays where ctx.csv was passed inline data). */
275
276
  inlineCsv?: { name: string; rows: Record<string, unknown>[] } | null;
@@ -1857,7 +1858,9 @@ class WorkerToolBatchScheduler {
1857
1858
  id,
1858
1859
  cacheKey: receiptKey,
1859
1860
  receiptKey,
1860
- force: options?.force === true,
1861
+ force:
1862
+ options?.force === true ||
1863
+ !!this.req.force,
1861
1864
  receiptWaitMaxAttempts: resolveRuntimeToolReceiptWaitMaxAttempts(
1862
1865
  typeof runtimeOptions?.receiptWaitMs === 'number'
1863
1866
  ? { max_wait_ms: runtimeOptions.receiptWaitMs }
@@ -4088,6 +4091,7 @@ async function prepareMapRows(input: {
4088
4091
  rows: Record<string, unknown>[];
4089
4092
  inputOffset: number;
4090
4093
  outputFields: string[];
4094
+ force?: boolean;
4091
4095
  }): Promise<{
4092
4096
  inserted: number;
4093
4097
  skipped: number;
@@ -4109,6 +4113,7 @@ async function prepareMapRows(input: {
4109
4113
  }),
4110
4114
  rows,
4111
4115
  inputOffset: input.inputOffset,
4116
+ ...(input.force ? { force: true } : {}),
4112
4117
  });
4113
4118
  for (const timing of result.timings ?? []) {
4114
4119
  const phase =
@@ -4682,9 +4687,7 @@ function createMinimalWorkerCtx(
4682
4687
  return formatMapProgressMessage(completed, input.total);
4683
4688
  };
4684
4689
  const formatMapProcessingMessage = (rowsToExecute: number) =>
4685
- rowsToExecute > 0
4686
- ? `Processing ${rowsToExecute.toLocaleString()} rows`
4687
- : null;
4690
+ rowsToExecute > 0 ? `${rowsToExecute.toLocaleString()} rows` : null;
4688
4691
  const formatMapExecutionHeartbeatMessage = (input: {
4689
4692
  rowsToExecute: number;
4690
4693
  startedRows: number;
@@ -4698,7 +4701,7 @@ function createMinimalWorkerCtx(
4698
4701
  const waitingRows = Math.max(0, rowsToExecute - startedRows);
4699
4702
  const parts = [
4700
4703
  activeRows > 0 ? `${activeRows.toLocaleString()} active` : null,
4701
- waitingRows > 0 ? `${waitingRows.toLocaleString()} waiting` : null,
4704
+ waitingRows > 0 ? `${waitingRows.toLocaleString()} queued` : null,
4702
4705
  completedRows > 0 ? `${completedRows.toLocaleString()} done` : null,
4703
4706
  ].filter((part): part is string => Boolean(part));
4704
4707
  const base =
@@ -4889,6 +4892,7 @@ function createMinimalWorkerCtx(
4889
4892
  ...mapRowOutcomeRuntimeFields({ key: rowKey }),
4890
4893
  })),
4891
4894
  inputOffset: baseOffset + chunkStart,
4895
+ force: !!req.force,
4892
4896
  });
4893
4897
  recordRunnerPerfTrace({
4894
4898
  req,
@@ -5366,7 +5370,9 @@ function createMinimalWorkerCtx(
5366
5370
  request.input,
5367
5371
  workflowStep,
5368
5372
  {
5369
- force: request.force === true,
5373
+ force:
5374
+ request.force === true ||
5375
+ !!req.force,
5370
5376
  staleAfterSeconds: request.staleAfterSeconds,
5371
5377
  },
5372
5378
  );
@@ -6354,7 +6360,9 @@ function createMinimalWorkerCtx(
6354
6360
  request.input,
6355
6361
  workflowStep,
6356
6362
  {
6357
- force: request.force === true,
6363
+ force:
6364
+ request.force === true ||
6365
+ !!req.force,
6358
6366
  staleAfterSeconds: request.staleAfterSeconds,
6359
6367
  },
6360
6368
  {
@@ -6500,6 +6508,7 @@ function createMinimalWorkerCtx(
6500
6508
  callbackBaseUrl: req.callbackUrl,
6501
6509
  baseUrl: req.baseUrl,
6502
6510
  integrationMode: req.integrationMode ?? null,
6511
+ forceToolRefresh: req.force === true,
6503
6512
  parentExecutorToken: req.executorToken,
6504
6513
  userEmail: req.userEmail ?? '',
6505
6514
  profile: 'workers_edge',
@@ -7956,6 +7965,7 @@ function runRequestFromWorkflowParams(
7956
7965
  runtimeInput: isRecord(params.input)
7957
7966
  ? (params.input as Record<string, unknown>)
7958
7967
  : {},
7968
+ force: params.forceToolRefresh === true,
7959
7969
  inlineCsv: isInlineCsv(params.inlineCsv) ? params.inlineCsv : null,
7960
7970
  inputFiles:
7961
7971
  inputFile && inputStorageKey
@@ -90,6 +90,7 @@ const COMPILE_MANIFEST_RETRY_DELAYS_MS = [250, 1_000];
90
90
  const REGISTER_PLAY_ARTIFACTS_COMPILE_CONCURRENCY = 3;
91
91
  const REGISTER_PLAY_ARTIFACTS_MAX_BATCH_COUNT = 3;
92
92
  const REGISTER_PLAY_ARTIFACTS_MAX_BATCH_BYTES = 2_500_000;
93
+ const DEEPLINEAGENT_EXECUTE_TIMEOUT_MS = 15 * 60 * 1000;
93
94
 
94
95
  function normalizePlayRunIntegrationMode(
95
96
  value: unknown,
@@ -202,8 +203,21 @@ function chunkRegisterPlayArtifacts<T>(artifacts: T[]): T[][] {
202
203
  type ExecuteToolRawOptions = {
203
204
  includeToolMetadata?: boolean;
204
205
  responseIntent?: 'raw' | 'row_artifact';
206
+ timeout?: number;
207
+ maxRetries?: number;
205
208
  };
206
209
 
210
+ function resolveToolExecuteTimeoutMs(toolId: string): number | undefined {
211
+ const normalized = toolId.trim().toLowerCase();
212
+ return normalized === 'deeplineagent' ||
213
+ normalized === 'deeplineagent_deeplineagent' ||
214
+ normalized === 'ai_inference' ||
215
+ normalized === 'deeplineagent_ai_inference' ||
216
+ normalized === 'aiinference'
217
+ ? DEEPLINEAGENT_EXECUTE_TIMEOUT_MS
218
+ : undefined;
219
+ }
220
+
207
221
  /**
208
222
  * Standard provider/tool execution envelope returned by low-level SDK calls.
209
223
  *
@@ -1212,7 +1226,12 @@ export class DeeplineClient {
1212
1226
  `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`,
1213
1227
  { payload: input },
1214
1228
  headers,
1215
- { forbiddenAsApiError: true },
1229
+ {
1230
+ forbiddenAsApiError: true,
1231
+ timeout: options?.timeout ?? resolveToolExecuteTimeoutMs(toolId),
1232
+ maxRetries: options?.maxRetries ?? 0,
1233
+ exactUrlOnly: true,
1234
+ },
1216
1235
  );
1217
1236
  }
1218
1237
 
@@ -1285,6 +1304,7 @@ export class DeeplineClient {
1285
1304
  */
1286
1305
  async startPlayRun(request: StartPlayRunRequest): Promise<PlayRunStart> {
1287
1306
  const integrationMode = resolvePlayRunIntegrationMode(request);
1307
+ const forceToolRefresh = request.forceToolRefresh === true;
1288
1308
  const response = await this.http.post<Record<string, unknown>>(
1289
1309
  '/api/v2/plays/run',
1290
1310
  {
@@ -1319,6 +1339,7 @@ export class DeeplineClient {
1319
1339
  ? { packagedFiles: request.packagedFiles }
1320
1340
  : {}),
1321
1341
  ...(request.force ? { force: true } : {}),
1342
+ ...(forceToolRefresh ? { forceToolRefresh: true } : {}),
1322
1343
  ...(typeof request.waitForCompletionMs === 'number'
1323
1344
  ? { waitForCompletionMs: request.waitForCompletionMs }
1324
1345
  : {}),
@@ -1348,6 +1369,7 @@ export class DeeplineClient {
1348
1369
  options?: { signal?: AbortSignal },
1349
1370
  ): AsyncGenerator<PlayLiveEvent> {
1350
1371
  const integrationMode = resolvePlayRunIntegrationMode(request);
1372
+ const forceToolRefresh = request.forceToolRefresh === true;
1351
1373
  const body = {
1352
1374
  ...(request.name ? { name: request.name } : {}),
1353
1375
  ...(request.revisionId ? { revisionId: request.revisionId } : {}),
@@ -1380,6 +1402,7 @@ export class DeeplineClient {
1380
1402
  ? { packagedFiles: request.packagedFiles }
1381
1403
  : {}),
1382
1404
  ...(request.force ? { force: true } : {}),
1405
+ ...(forceToolRefresh ? { forceToolRefresh: true } : {}),
1383
1406
  ...(typeof request.waitForCompletionMs === 'number'
1384
1407
  ? { waitForCompletionMs: request.waitForCompletionMs }
1385
1408
  : {}),
@@ -1630,6 +1653,7 @@ export class DeeplineClient {
1630
1653
  inputFile?: PlayStagedFileRef | null;
1631
1654
  packagedFiles?: PlayStagedFileRef[];
1632
1655
  force?: boolean;
1656
+ forceToolRefresh?: boolean;
1633
1657
  }): Promise<PlayRunStart> {
1634
1658
  const compilerManifest =
1635
1659
  input.compilerManifest ??
@@ -1665,6 +1689,7 @@ export class DeeplineClient {
1665
1689
  ? { packagedFiles: input.packagedFiles }
1666
1690
  : {}),
1667
1691
  ...(input.force ? { force: true } : {}),
1692
+ ...(input.forceToolRefresh ? { forceToolRefresh: true } : {}),
1668
1693
  });
1669
1694
  }
1670
1695
 
@@ -1706,6 +1731,7 @@ export class DeeplineClient {
1706
1731
  inputFile?: PlayStagedFileRef | null;
1707
1732
  packagedFiles?: PlayStagedFileRef[];
1708
1733
  force?: boolean;
1734
+ forceToolRefresh?: boolean;
1709
1735
  },
1710
1736
  ): Promise<PlayRunStart> {
1711
1737
  const runtimeInput = options?.input ? { ...options.input } : {};
@@ -1766,6 +1792,7 @@ export class DeeplineClient {
1766
1792
  ? { packagedFiles: options.packagedFiles }
1767
1793
  : {}),
1768
1794
  ...(options?.force ? { force: true } : {}),
1795
+ ...(options?.forceToolRefresh ? { forceToolRefresh: true } : {}),
1769
1796
  });
1770
1797
  }
1771
1798
 
@@ -2858,6 +2885,8 @@ export class DeeplineClient {
2858
2885
  packagedFiles?: PlayStagedFileRef[];
2859
2886
  /** Compatibility flag; active sibling runs are allowed. */
2860
2887
  force?: boolean;
2888
+ /** Explicit cache-bypass flag for durable dataset and tool-call reuse. */
2889
+ forceToolRefresh?: boolean;
2861
2890
  },
2862
2891
  ): Promise<PlayRunResult> {
2863
2892
  const { workflowId } = await this.submitPlay(code, csvPath, name, {
@@ -2868,6 +2897,7 @@ export class DeeplineClient {
2868
2897
  inputFile: options?.inputFile,
2869
2898
  packagedFiles: options?.packagedFiles,
2870
2899
  force: options?.force,
2900
+ forceToolRefresh: options?.forceToolRefresh,
2871
2901
  });
2872
2902
  const start = Date.now();
2873
2903
  const state: PlayLiveStatusState = {
@@ -59,6 +59,10 @@ interface RequestOptions {
59
59
  * most mutating API calls must fail loudly after the first server response.
60
60
  */
61
61
  retryApiErrors?: boolean;
62
+ /** Per-request retry override. Use 0 for possibly delivered mutating calls. */
63
+ maxRetries?: number;
64
+ /** Disable localhost/127.0.0.1 failover for non-idempotent requests. */
65
+ exactUrlOnly?: boolean;
62
66
  }
63
67
 
64
68
  interface StreamOptions {
@@ -222,10 +226,14 @@ export class HttpClient {
222
226
  }
223
227
 
224
228
  let lastError: Error | null = null;
225
- const candidateUrls = buildCandidateUrls(url);
229
+ const candidateUrls = options?.exactUrlOnly
230
+ ? [url]
231
+ : buildCandidateUrls(url);
226
232
  let retryAfterDelayMs: number | null = null;
227
233
 
228
- for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
234
+ const maxRetries = options?.maxRetries ?? this.config.maxRetries;
235
+
236
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
229
237
  if (attempt > 0) {
230
238
  const backoffMs = Math.min(1000 * Math.pow(2, attempt - 1), 30_000);
231
239
  const delayMs =
@@ -270,7 +278,7 @@ export class HttpClient {
270
278
  if (response.status === 429) {
271
279
  const retryAfter = parseRetryAfter(response);
272
280
  lastError = new RateLimitError(retryAfter);
273
- if (attempt < this.config.maxRetries) {
281
+ if (attempt < maxRetries) {
274
282
  retryAfterDelayMs = retryAfter;
275
283
  break;
276
284
  }
@@ -311,7 +319,7 @@ export class HttpClient {
311
319
  : {}),
312
320
  },
313
321
  );
314
- if (retryableApiError && attempt < this.config.maxRetries) {
322
+ if (retryableApiError && attempt < maxRetries) {
315
323
  retryAfterDelayMs = parseOptionalRetryAfter(response);
316
324
  break;
317
325
  }
@@ -350,7 +358,7 @@ export class HttpClient {
350
358
  lastError = new DeeplineError(msg, response.status, apiErrorCode, {
351
359
  response: parsed,
352
360
  });
353
- if (retryableApiError && attempt < this.config.maxRetries) {
361
+ if (retryableApiError && attempt < maxRetries) {
354
362
  retryAfterDelayMs = parseOptionalRetryAfter(response);
355
363
  break;
356
364
  }
@@ -371,7 +379,7 @@ export class HttpClient {
371
379
  }
372
380
  }
373
381
 
374
- if (attempt < this.config.maxRetries) continue;
382
+ if (attempt < maxRetries) continue;
375
383
  }
376
384
 
377
385
  if (lastError instanceof DeeplineError) {
@@ -487,7 +495,11 @@ export class HttpClient {
487
495
  headers?: Record<string, string>,
488
496
  options?: Pick<
489
497
  RequestOptions,
490
- 'forbiddenAsApiError' | 'retryApiErrors' | 'timeout'
498
+ | 'forbiddenAsApiError'
499
+ | 'retryApiErrors'
500
+ | 'timeout'
501
+ | 'maxRetries'
502
+ | 'exactUrlOnly'
491
503
  >,
492
504
  ): Promise<T> {
493
505
  return this.request<T>(path, {
@@ -225,6 +225,7 @@ export async function harnessStartSheetDataset(input: {
225
225
  rows: Array<Record<string, unknown>>;
226
226
  runId: string;
227
227
  inputOffset?: number;
228
+ force?: boolean;
228
229
  userEmail?: string | null;
229
230
  }): Promise<{
230
231
  inserted: number;
@@ -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.174',
107
+ version: '0.1.176',
108
108
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
109
109
  supportPolicy: {
110
- latest: '0.1.174',
110
+ latest: '0.1.176',
111
111
  minimumSupported: '0.1.53',
112
112
  deprecatedBelow: '0.1.53',
113
113
  commandMinimumSupported: [
@@ -998,6 +998,8 @@ export interface StartPlayRunRequest {
998
998
  packagedFiles?: unknown[];
999
999
  /** Compatibility flag; active sibling runs are allowed. */
1000
1000
  force?: boolean;
1001
+ /** Explicit cache-bypass flag for durable dataset and tool-call reuse. */
1002
+ forceToolRefresh?: boolean;
1001
1003
  /** Optionally let the start request wait briefly and return a terminal result. */
1002
1004
  waitForCompletionMs?: number;
1003
1005
  /**
@@ -190,11 +190,33 @@ const MAP_INCREMENTAL_PERSIST_INTERVAL_MS = 100;
190
190
  const MAP_FRAME_FLUSH_INTERVAL_MS = 250;
191
191
  const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
192
192
  const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
193
+ const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
193
194
  const FETCH_TRANSPORT_MAX_ATTEMPTS = 3;
194
195
  const FETCH_TRANSPORT_RETRY_DELAY_MS = 100;
195
196
  type SafeFetchModule = typeof import('@shared_libs/security/safe-fetch');
196
197
  let safeFetchModule: Promise<SafeFetchModule> | null = null;
197
198
 
199
+ export function resolveToolRuntimeTimeoutMs(
200
+ toolId: string,
201
+ requestedTimeoutMs?: number,
202
+ ): number | undefined {
203
+ if (
204
+ typeof requestedTimeoutMs === 'number' &&
205
+ Number.isFinite(requestedTimeoutMs) &&
206
+ requestedTimeoutMs > 0
207
+ ) {
208
+ return Math.max(1, Math.ceil(requestedTimeoutMs));
209
+ }
210
+ const normalized = toolId.trim().toLowerCase();
211
+ return normalized === 'deeplineagent' ||
212
+ normalized === 'deeplineagent_deeplineagent' ||
213
+ normalized === 'ai_inference' ||
214
+ normalized === 'deeplineagent_ai_inference' ||
215
+ normalized === 'aiinference'
216
+ ? DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS
217
+ : undefined;
218
+ }
219
+
198
220
  function loadSafeFetch(): Promise<SafeFetchModule> {
199
221
  safeFetchModule ??= import('@shared_libs/security/safe-fetch');
200
222
  return safeFetchModule;
@@ -1579,7 +1601,9 @@ export class PlayContextImpl {
1579
1601
  staleAfterSeconds?: number | null;
1580
1602
  } {
1581
1603
  return {
1582
- force: options?.force === true,
1604
+ force:
1605
+ options?.force === true ||
1606
+ this.#options.cachePolicy?.forceToolRefresh === true,
1583
1607
  staleAfterSeconds: options?.staleAfterSeconds ?? null,
1584
1608
  };
1585
1609
  }
@@ -2152,6 +2176,8 @@ export class PlayContextImpl {
2152
2176
  playId: this.#options.playId,
2153
2177
  runId: this.#options.runId,
2154
2178
  staticPipeline: this.#options.staticPipeline,
2179
+ forceRefresh:
2180
+ this.#options.cachePolicy?.forceToolRefresh === true,
2155
2181
  },
2156
2182
  );
2157
2183
  resolvedTableNamespace = normalizeTableNamespace(
@@ -3454,7 +3480,7 @@ export class PlayContextImpl {
3454
3480
  ? null
3455
3481
  : this.getCachedToolResult(toolId, directCacheKey);
3456
3482
  if (cached?.done) {
3457
- this.log(`Calling tool: ${toolId} recovered from checkpoint`);
3483
+ this.log(`Tool cache hit: ${toolId} exact payload`);
3458
3484
  return await this.wrapToolExecutionResult({
3459
3485
  toolId,
3460
3486
  status: cached.result == null ? 'no_result' : 'completed',
@@ -3465,9 +3491,13 @@ export class PlayContextImpl {
3465
3491
  }),
3466
3492
  });
3467
3493
  }
3468
- this.log(`Calling tool: ${toolId}`);
3494
+ this.log(
3495
+ toolCachePolicy.force
3496
+ ? `Calling tool: ${toolId} (force)`
3497
+ : `Calling tool: ${toolId}`,
3498
+ );
3469
3499
  const execution = await this.callToolExecutionAPI(toolId, input, {
3470
- timeoutMs: options?.timeoutMs,
3500
+ timeoutMs: resolveToolRuntimeTimeoutMs(toolId, options?.timeoutMs),
3471
3501
  });
3472
3502
  const wrapped = await this.wrapToolExecutionResult({
3473
3503
  toolId,
@@ -3513,7 +3543,7 @@ export class PlayContextImpl {
3513
3543
  ? null
3514
3544
  : this.getCachedToolResult(toolId, toolResultCacheKey);
3515
3545
  if (cached?.done) {
3516
- this.log(` Row ${rowId} ${toolId}: recovered from checkpoint`);
3546
+ this.log(` Row ${rowId} ${toolId}: cache hit exact payload`);
3517
3547
  return await this.wrapToolExecutionResult({
3518
3548
  toolId,
3519
3549
  status: cached.result == null ? 'no_result' : 'completed',
@@ -3545,6 +3575,10 @@ export class PlayContextImpl {
3545
3575
  },
3546
3576
  dataPatch: {},
3547
3577
  });
3578
+ const timeoutMs = resolveToolRuntimeTimeoutMs(
3579
+ toolId,
3580
+ options?.timeoutMs,
3581
+ );
3548
3582
  this.toolCallQueue.push({
3549
3583
  callId,
3550
3584
  cacheKey: toolResultCacheKey,
@@ -3554,9 +3588,7 @@ export class PlayContextImpl {
3554
3588
  fieldName,
3555
3589
  toolId,
3556
3590
  input,
3557
- ...(typeof options?.timeoutMs === 'number'
3558
- ? { timeoutMs: options.timeoutMs }
3559
- : {}),
3591
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
3560
3592
  ...(typeof options?.receiptWaitMs === 'number'
3561
3593
  ? { receiptWaitMs: options.receiptWaitMs }
3562
3594
  : {}),
@@ -3582,7 +3614,7 @@ export class PlayContextImpl {
3582
3614
  staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
3583
3615
  markSkipped: () => {
3584
3616
  this.log(
3585
- `ctx.tools.execute(${toolId}): no-op due completed receipt ${toolRequestIdentity} (label: ${normalizedKey})`,
3617
+ `Tool cache hit: ${toolId} exact payload (${normalizedKey}:${toolRequestIdentity})`,
3586
3618
  );
3587
3619
  },
3588
3620
  onClaimedResult: (output, receiptKey) =>
@@ -4303,7 +4335,9 @@ export class PlayContextImpl {
4303
4335
  const pendingRequests: ToolCallRequest[] = [];
4304
4336
  const recoveredRequests: ToolCallRequest[] = [];
4305
4337
  for (const req of requests) {
4306
- const cached = this.getCachedToolResult(toolId, req.cacheKey);
4338
+ const cached = req.force
4339
+ ? undefined
4340
+ : this.getCachedToolResult(toolId, req.cacheKey);
4307
4341
  if (cached?.done) {
4308
4342
  this.log(` Row ${req.rowId} ${toolId}: recovered from checkpoint`);
4309
4343
  const resolver = this.toolCallResolvers.get(req.callId);
@@ -4906,11 +4940,7 @@ export class PlayContextImpl {
4906
4940
  );
4907
4941
  }
4908
4942
  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;
4943
+ const timeoutMs = resolveToolRuntimeTimeoutMs(toolId, options?.timeoutMs);
4914
4944
 
4915
4945
  // The Governor's tool slot is the single seam for tool-call budget + the
4916
4946
  // global tool-concurrency backstop + per-(org, provider) pacing. It blocks
@@ -410,6 +410,10 @@ export interface ContextOptions {
410
410
  checkpoint?: PlayCheckpoint;
411
411
  /** Enables durable boundary replay such as ctx.sleep() resumptions. */
412
412
  durableBoundaries?: boolean;
413
+ /** Run-level cache policy. forceToolRefresh bypasses ctx.tools.execute receipts. */
414
+ cachePolicy?: {
415
+ forceToolRefresh?: boolean;
416
+ };
413
417
  /** Called after each durable batch completes — for Temporal heartbeating. */
414
418
  onBatchComplete?: (checkpoint: PlayCheckpoint) => void;
415
419
  /** Called when the runtime emits a new execution log line. */
@@ -435,6 +439,7 @@ export interface ContextOptions {
435
439
  playId?: string;
436
440
  runId?: string;
437
441
  staticPipeline?: PlayStaticPipeline | null;
442
+ forceRefresh?: boolean;
438
443
  },
439
444
  ) => Promise<MapStartResult>;
440
445
  /**
@@ -58,21 +58,15 @@ export function buildDurableToolCallCacheKey(input: {
58
58
  const orgId = input.orgId?.trim() || 'org';
59
59
  const playId = input.playId.trim();
60
60
  if (!playId) {
61
- throw new Error(
62
- 'Durable tool call cache key requires a non-empty play id.',
63
- );
61
+ throw new Error('Missing durable tool play id.');
64
62
  }
65
63
  const toolId = input.toolId.trim();
66
64
  if (!toolId) {
67
- throw new Error(
68
- 'Durable tool call cache key requires a non-empty tool id.',
69
- );
65
+ throw new Error('Missing durable tool id.');
70
66
  }
71
- const providerActionVersion = input.providerActionVersion?.trim();
72
- if (!providerActionVersion) {
73
- throw new Error(
74
- 'Durable tool call cache key requires a provider action version.',
75
- );
67
+ const p = input.providerActionVersion?.trim();
68
+ if (!p) {
69
+ throw new Error('Missing durable tool action version.');
76
70
  }
77
71
  const digest = sha256Hex(
78
72
  stableStringify({
@@ -83,7 +77,7 @@ export function buildDurableToolCallCacheKey(input: {
83
77
  normalizedToolId: normalizeTableNamespace(toolId),
84
78
  requestInput: input.requestInput,
85
79
  authScopeDigest: input.authScopeDigest ?? null,
86
- providerActionVersion,
80
+ providerActionVersion: p,
87
81
  cachePolicyVersion:
88
82
  input.cachePolicyVersion ?? DURABLE_CALL_CACHE_POLICY_VERSION,
89
83
  staleBucket: durableCacheStaleBucket({
@@ -23,6 +23,7 @@ export interface PlayRunnerContextConfig {
23
23
  userEmail?: string;
24
24
  convexUrl?: string;
25
25
  staticPipeline?: PlayStaticPipeline | null;
26
+ forceToolRefresh?: boolean | null;
26
27
  /**
27
28
  * Controls how scoped Postgres sessions enter the runner. `preloaded` means
28
29
  * the launch config carries short-lived encrypted sessions minted before