deepline 0.3.149 → 0.3.150

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.
@@ -36,7 +36,7 @@ import type { ToolResultBilling } from '../../shared_libs/plays/tool-result-type
36
36
  * @module
37
37
  */
38
38
  import { resolveConfig } from './config.js';
39
- import { DeeplineError } from './errors.js';
39
+ import { DeeplineError, ToolExecutionError } from './errors.js';
40
40
  import { HttpClient } from './http.js';
41
41
  import { PRODUCT_NOTIFICATION_SLACK_OAUTH_SCOPES } from '../../shared_libs/product-notifications/contract.js';
42
42
  import {
@@ -222,6 +222,114 @@ const MONITOR_NON_RETRYABLE_MUTATION_OPTIONS = {
222
222
  exactUrlOnly: true,
223
223
  };
224
224
  const RAW_V2_EXECUTE_RESPONSE_CONTRACT = RAW_V2_TOOL_RESPONSE_CONTRACT;
225
+ const DEFAULT_EXECUTION_RECOVERY_TIMEOUT_MS = 15 * 60 * 1_000;
226
+
227
+ function validateExecutionIdempotencyKey(key: string): void {
228
+ if (
229
+ typeof key !== 'string' ||
230
+ key.length < 1 ||
231
+ key.length > 200 ||
232
+ !/^[A-Za-z0-9._:-]+$/.test(key)
233
+ ) {
234
+ throw new DeeplineError(
235
+ 'Execution idempotency keys must be 1–200 ASCII letters, digits, dots, underscores, colons, or hyphens.',
236
+ undefined,
237
+ 'IDEMPOTENCY_KEY_INVALID',
238
+ );
239
+ }
240
+ }
241
+
242
+ function isExecutionInProgressResponse(value: unknown): boolean {
243
+ return (
244
+ typeof value === 'object' &&
245
+ value !== null &&
246
+ typeof (value as { executionRecovery?: { state?: unknown } })
247
+ .executionRecovery === 'object' &&
248
+ (value as { executionRecovery: { state?: unknown } }).executionRecovery
249
+ .state === 'running'
250
+ );
251
+ }
252
+
253
+ function getExecutionRecoveryState(
254
+ value: unknown,
255
+ ): ExecutionRecovery['state'] | null {
256
+ if (
257
+ typeof value !== 'object' ||
258
+ value === null ||
259
+ typeof (value as { executionRecovery?: { state?: unknown } })
260
+ .executionRecovery !== 'object'
261
+ ) {
262
+ return null;
263
+ }
264
+ const state = (value as { executionRecovery: { state?: unknown } })
265
+ .executionRecovery.state;
266
+ return state === 'running' ||
267
+ state === 'completed' ||
268
+ state === 'outcome_unknown'
269
+ ? state
270
+ : null;
271
+ }
272
+
273
+ function isRecoverableExecutionAttemptError(error: unknown): boolean {
274
+ if (error instanceof ToolExecutionError) {
275
+ return (
276
+ error.code === 'EXECUTION_IN_PROGRESS' ||
277
+ (error.origin === 'deepline' &&
278
+ error.category === 'network' &&
279
+ error.networkScope === 'client_to_deepline')
280
+ );
281
+ }
282
+ return (
283
+ error instanceof DeeplineError &&
284
+ (error.code === 'EXECUTION_IN_PROGRESS' ||
285
+ (error.code?.startsWith('NETWORK_') === true &&
286
+ error.statusCode === undefined))
287
+ );
288
+ }
289
+
290
+ function isRecoverableExecutionLookupError(error: unknown): boolean {
291
+ if (isRecoverableExecutionAttemptError(error)) return true;
292
+ return (
293
+ error instanceof DeeplineError &&
294
+ (error.statusCode === 429 ||
295
+ (error.statusCode !== undefined && error.statusCode >= 500))
296
+ );
297
+ }
298
+
299
+ function timeoutWithinRecoveryBudget(
300
+ requestTimeoutMs: number | undefined,
301
+ defaultRequestTimeoutMs: number,
302
+ remainingMs: number,
303
+ ): number {
304
+ return Math.min(requestTimeoutMs ?? defaultRequestTimeoutMs, remainingMs);
305
+ }
306
+
307
+ function executionRecoveryError(input: {
308
+ toolId: string;
309
+ idempotencyKey: string;
310
+ code:
311
+ | 'EXECUTION_IN_PROGRESS'
312
+ | 'EXECUTION_OUTCOME_UNKNOWN'
313
+ | 'EXECUTION_RECOVERY_TIMEOUT';
314
+ message: string;
315
+ }): ToolExecutionError {
316
+ return new ToolExecutionError(input.message, {
317
+ toolId: input.toolId,
318
+ provider: null,
319
+ operation: input.toolId,
320
+ code: input.code,
321
+ origin: 'deepline',
322
+ category:
323
+ input.code === 'EXECUTION_OUTCOME_UNKNOWN' ? 'unknown' : 'conflict',
324
+ retryable: input.code !== 'EXECUTION_OUTCOME_UNKNOWN',
325
+ statusCode: null,
326
+ requestId: null,
327
+ retryAfterMs: null,
328
+ networkKind: null,
329
+ networkScope: null,
330
+ publicDetails: { idempotencyKey: input.idempotencyKey },
331
+ });
332
+ }
225
333
  const COMPILE_MANIFEST_RETRY_DELAYS_MS = [250, 1_000];
226
334
  const REGISTER_PLAY_ARTIFACTS_COMPILE_CONCURRENCY = 3;
227
335
  const REGISTER_PLAY_ARTIFACTS_MAX_BATCH_COUNT = 3;
@@ -547,6 +655,40 @@ type ExecuteToolRawOptions = {
547
655
  metadata?: Record<string, unknown>;
548
656
  timeout?: number;
549
657
  maxRetries?: number;
658
+ /** Reuse this stable key to recover or retry an ambiguous execution. */
659
+ idempotencyKey?: string;
660
+ /** Generate a key and enable recoverable execution before dispatch. */
661
+ recover?: boolean;
662
+ /** Called and awaited with the key before the first network request. */
663
+ onExecution?: (execution: { idempotencyKey: string }) => void | Promise<void>;
664
+ /** Maximum time to reconnect to an in-progress keyed execution. Defaults to 15 minutes. */
665
+ recoveryTimeoutMs?: number;
666
+ };
667
+
668
+ /** Durable state returned for a keyed tool execution. */
669
+ export type ExecutionRecovery = {
670
+ /** Stable caller key used to resume or replay this execution. */
671
+ idempotencyKey: string;
672
+ /** Whether the execution is still running, completed, or has an unknown provider outcome. */
673
+ state: 'running' | 'completed' | 'outcome_unknown';
674
+ /** Whether this response came from an existing durable execution. */
675
+ replayed: boolean;
676
+ /** ISO timestamp after which the completed execution can no longer be replayed. */
677
+ expiresAt?: string;
678
+ };
679
+
680
+ /** Durable lookup result for a keyed tool execution. */
681
+ export type ExecutionByKeyResult = {
682
+ /** Recovery state and the key that owns the execution. */
683
+ executionRecovery: ExecutionRecovery;
684
+ /** Provider tool associated with the execution. */
685
+ toolId: string;
686
+ /** Original server-owned billing request ID, allocated before provider dispatch. */
687
+ requestId?: string;
688
+ /** HTTP status saved with the original response, when it was terminal. */
689
+ responseStatus?: number;
690
+ /** Saved execution response, when one is available for replay. */
691
+ response?: unknown;
550
692
  };
551
693
 
552
694
  /**
@@ -3121,6 +3263,41 @@ export class DeeplineClient {
3121
3263
  input: Record<string, unknown>,
3122
3264
  options?: ExecuteToolRawOptions,
3123
3265
  ): Promise<ToolExecution<TData, TMeta>> {
3266
+ // Snapshot JSON input before the caller's awaited onExecution callback and
3267
+ // before any retry, so every POST is byte-equivalent in meaning.
3268
+ const inputSnapshot = JSON.parse(JSON.stringify(input)) as Record<
3269
+ string,
3270
+ unknown
3271
+ >;
3272
+ const metadataSnapshot = options?.metadata
3273
+ ? (JSON.parse(JSON.stringify(options.metadata)) as Record<
3274
+ string,
3275
+ unknown
3276
+ >)
3277
+ : undefined;
3278
+ const idempotencyKey =
3279
+ options?.idempotencyKey ??
3280
+ (options?.recover ? crypto.randomUUID() : undefined);
3281
+ if (idempotencyKey !== undefined) {
3282
+ validateExecutionIdempotencyKey(idempotencyKey);
3283
+ if (
3284
+ options?.recoveryTimeoutMs !== undefined &&
3285
+ (!Number.isFinite(options.recoveryTimeoutMs) ||
3286
+ options.recoveryTimeoutMs < 0)
3287
+ ) {
3288
+ throw new DeeplineError(
3289
+ 'recoveryTimeoutMs must be a finite, non-negative number.',
3290
+ undefined,
3291
+ 'IDEMPOTENCY_KEY_INVALID',
3292
+ );
3293
+ }
3294
+ await options?.onExecution?.({ idempotencyKey });
3295
+ const timeoutMs =
3296
+ options?.timeout ?? resolveToolExecuteTimeoutMs(toolId, inputSnapshot);
3297
+ await this.lookupExecutionByKey(idempotencyKey, timeoutMs);
3298
+ }
3299
+ const timeoutMs =
3300
+ options?.timeout ?? resolveToolExecuteTimeoutMs(toolId, inputSnapshot);
3124
3301
  const headers = {
3125
3302
  [EXECUTE_RESPONSE_CONTRACT_HEADER]: RAW_V2_EXECUTE_RESPONSE_CONTRACT,
3126
3303
  [TOOL_EXECUTION_ERROR_SCHEMA_HEADER]: String(
@@ -3130,22 +3307,180 @@ export class DeeplineClient {
3130
3307
  ? { [INCLUDE_TOOL_METADATA_HEADER]: 'true' }
3131
3308
  : {}),
3132
3309
  [EXECUTE_RESPONSE_INTENT_HEADER]: options?.responseIntent ?? 'raw',
3310
+ ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
3133
3311
  };
3134
- const response = await this.http.post<ToolExecution<TData, TMeta>>(
3135
- `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`,
3136
- {
3137
- payload: input,
3138
- ...(options?.metadata ? { metadata: options.metadata } : {}),
3139
- },
3140
- headers,
3312
+ const request = (requestTimeoutMs: number | undefined) =>
3313
+ this.http.post<ToolExecution<TData, TMeta>>(
3314
+ `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`,
3315
+ {
3316
+ payload: inputSnapshot,
3317
+ ...(metadataSnapshot ? { metadata: metadataSnapshot } : {}),
3318
+ },
3319
+ headers,
3320
+ {
3321
+ timeout: requestTimeoutMs,
3322
+ maxRetries: idempotencyKey ? 0 : (options?.maxRetries ?? 0),
3323
+ exactUrlOnly: true,
3324
+ toolId,
3325
+ idempotencyKey,
3326
+ },
3327
+ );
3328
+ let response: ToolExecution<TData, TMeta>;
3329
+ let recoveryDeadline: number | null = null;
3330
+ const recoveryTimeoutMs =
3331
+ options?.recoveryTimeoutMs ?? DEFAULT_EXECUTION_RECOVERY_TIMEOUT_MS;
3332
+ let retryDelayMs = 1_000;
3333
+ const recoveryTimeoutError = () =>
3334
+ executionRecoveryError({
3335
+ toolId,
3336
+ idempotencyKey: idempotencyKey!,
3337
+ code: 'EXECUTION_RECOVERY_TIMEOUT',
3338
+ message: `Execution recovery for ${idempotencyKey} did not finish before the recovery timeout. Inspect client.executions.getByKey() and resume with the same idempotency key.`,
3339
+ });
3340
+ const waitForRecoveryRetry = async (deadline: number) => {
3341
+ const remainingMs = deadline - Date.now();
3342
+ if (remainingMs <= 0) throw recoveryTimeoutError();
3343
+ await new Promise((resolve) =>
3344
+ setTimeout(resolve, Math.min(retryDelayMs, remainingMs)),
3345
+ );
3346
+ retryDelayMs = Math.min(retryDelayMs * 2, 5_000);
3347
+ };
3348
+ const lookupDuringRecovery = async () => {
3349
+ while (true) {
3350
+ const remainingMs = recoveryDeadline! - Date.now();
3351
+ if (remainingMs <= 0) throw recoveryTimeoutError();
3352
+ try {
3353
+ return await this.lookupExecutionByKey(
3354
+ idempotencyKey!,
3355
+ timeoutWithinRecoveryBudget(
3356
+ timeoutMs,
3357
+ this.config.timeout,
3358
+ remainingMs,
3359
+ ),
3360
+ );
3361
+ } catch (error) {
3362
+ if (!isRecoverableExecutionLookupError(error)) throw error;
3363
+ await waitForRecoveryRetry(recoveryDeadline!);
3364
+ }
3365
+ }
3366
+ };
3367
+ while (true) {
3368
+ const remainingMs =
3369
+ recoveryDeadline === null ? null : recoveryDeadline - Date.now();
3370
+ if (remainingMs !== null && remainingMs <= 0) {
3371
+ throw recoveryTimeoutError();
3372
+ }
3373
+ try {
3374
+ response = await request(
3375
+ remainingMs === null
3376
+ ? timeoutMs
3377
+ : timeoutWithinRecoveryBudget(
3378
+ timeoutMs,
3379
+ this.config.timeout,
3380
+ remainingMs,
3381
+ ),
3382
+ );
3383
+ if (getExecutionRecoveryState(response) === 'outcome_unknown') {
3384
+ if (!idempotencyKey) break;
3385
+ throw executionRecoveryError({
3386
+ toolId,
3387
+ idempotencyKey,
3388
+ code: 'EXECUTION_OUTCOME_UNKNOWN',
3389
+ message: `The outcome of execution ${idempotencyKey} is unknown. Do not retry with a new key; inspect client.executions.getByKey().`,
3390
+ });
3391
+ }
3392
+ if (!idempotencyKey || !isExecutionInProgressResponse(response)) {
3393
+ break;
3394
+ }
3395
+ if (recoveryDeadline === null) {
3396
+ recoveryDeadline = Date.now() + recoveryTimeoutMs;
3397
+ }
3398
+ } catch (error) {
3399
+ if (
3400
+ idempotencyKey &&
3401
+ error instanceof DeeplineError &&
3402
+ error.code === 'EXECUTION_OUTCOME_UNKNOWN'
3403
+ ) {
3404
+ throw executionRecoveryError({
3405
+ toolId,
3406
+ idempotencyKey,
3407
+ code: 'EXECUTION_OUTCOME_UNKNOWN',
3408
+ message: error.message,
3409
+ });
3410
+ }
3411
+ if (!idempotencyKey || !isRecoverableExecutionAttemptError(error)) {
3412
+ throw error;
3413
+ }
3414
+ if (recoveryDeadline === null) {
3415
+ recoveryDeadline = Date.now() + recoveryTimeoutMs;
3416
+ }
3417
+ }
3418
+ await waitForRecoveryRetry(recoveryDeadline!);
3419
+ const recovered = await lookupDuringRecovery();
3420
+ if (getExecutionRecoveryState(recovered) === 'outcome_unknown') {
3421
+ throw executionRecoveryError({
3422
+ toolId,
3423
+ idempotencyKey,
3424
+ code: 'EXECUTION_OUTCOME_UNKNOWN',
3425
+ message: `The outcome of execution ${idempotencyKey} is unknown. Do not retry with a new key; inspect client.executions.getByKey().`,
3426
+ });
3427
+ }
3428
+ }
3429
+ const materialized = materializeToolExecutionResponse(response);
3430
+ return idempotencyKey ? { ...materialized, idempotencyKey } : materialized;
3431
+ }
3432
+
3433
+ /** Read the durable state of a keyed tool execution. */
3434
+ async getExecutionByKey(
3435
+ idempotencyKey: string,
3436
+ ): Promise<ExecutionByKeyResult> {
3437
+ validateExecutionIdempotencyKey(idempotencyKey);
3438
+ const result = await this.lookupExecutionByKey(idempotencyKey);
3439
+ if (!result) {
3440
+ throw new DeeplineError(
3441
+ `No execution exists for idempotency key ${idempotencyKey}.`,
3442
+ 404,
3443
+ 'EXECUTION_NOT_FOUND',
3444
+ );
3445
+ }
3446
+ return result;
3447
+ }
3448
+
3449
+ private async lookupExecutionByKey(
3450
+ idempotencyKey: string,
3451
+ timeoutMs?: number,
3452
+ ): Promise<ExecutionByKeyResult | null> {
3453
+ validateExecutionIdempotencyKey(idempotencyKey);
3454
+ let supported = false;
3455
+ const result = await this.http.get<ExecutionByKeyResult | null>(
3456
+ `/api/v2/executions/by-key/${encodeURIComponent(idempotencyKey)}`,
3141
3457
  {
3142
- timeout: options?.timeout ?? resolveToolExecuteTimeoutMs(toolId, input),
3143
- maxRetries: options?.maxRetries ?? 0,
3144
3458
  exactUrlOnly: true,
3145
- toolId,
3459
+ maxRetries: 0,
3460
+ timeout: timeoutMs,
3461
+ idempotencyKey,
3462
+ allowNotFound: true,
3463
+ onResponse: (response) => {
3464
+ supported =
3465
+ response.headers.get('X-Deepline-Idempotency-Supported') === 'true';
3466
+ },
3146
3467
  },
3147
3468
  );
3148
- return materializeToolExecutionResponse(response);
3469
+ if (!supported) {
3470
+ throw new DeeplineError(
3471
+ 'This Deepline server does not support recoverable tool executions; no tool was dispatched.',
3472
+ 422,
3473
+ 'IDEMPOTENCY_NOT_SUPPORTED',
3474
+ );
3475
+ }
3476
+ return result;
3477
+ }
3478
+
3479
+ /** Public recovery namespace. */
3480
+ get executions(): {
3481
+ getByKey: (key: string) => Promise<ExecutionByKeyResult>;
3482
+ } {
3483
+ return { getByKey: (key) => this.getExecutionByKey(key) };
3149
3484
  }
3150
3485
 
3151
3486
  /**
@@ -76,6 +76,9 @@ interface RequestOptions {
76
76
  preserveRateLimitResponse?: boolean;
77
77
  /** Enables endpoint-specific structured tool failure mapping. */
78
78
  toolId?: string;
79
+ onResponse?: (response: Response) => void;
80
+ allowNotFound?: boolean;
81
+ idempotencyKey?: string;
79
82
  }
80
83
 
81
84
  interface StreamOptions {
@@ -423,6 +426,12 @@ export class HttpClient {
423
426
  signal: controller.signal,
424
427
  });
425
428
 
429
+ options?.onResponse?.(response);
430
+ if (response.status === 404 && options?.allowNotFound) {
431
+ clearTimeout(timeoutId);
432
+ return null as T;
433
+ }
434
+
426
435
  clearTimeout(timeoutId);
427
436
 
428
437
  const body = await response.text();
@@ -626,6 +635,9 @@ export class HttpClient {
626
635
  : 'unavailable',
627
636
  networkScope: 'client_to_deepline',
628
637
  details: mappedNetworkError.details,
638
+ publicDetails: options.idempotencyKey
639
+ ? { idempotencyKey: options.idempotencyKey }
640
+ : null,
629
641
  },
630
642
  ),
631
643
  lastError,
@@ -652,7 +664,14 @@ export class HttpClient {
652
664
  path: string,
653
665
  options?: Pick<
654
666
  RequestOptions,
655
- 'retryApiErrors' | 'timeout' | 'maxRetries' | 'exactUrlOnly' | 'toolId'
667
+ | 'retryApiErrors'
668
+ | 'timeout'
669
+ | 'maxRetries'
670
+ | 'exactUrlOnly'
671
+ | 'toolId'
672
+ | 'onResponse'
673
+ | 'allowNotFound'
674
+ | 'idempotencyKey'
656
675
  >,
657
676
  ): Promise<T> {
658
677
  return this.request<T>(path, {
@@ -777,7 +796,14 @@ export class HttpClient {
777
796
  headers?: Record<string, string>,
778
797
  options?: Pick<
779
798
  RequestOptions,
780
- 'retryApiErrors' | 'timeout' | 'maxRetries' | 'exactUrlOnly' | 'toolId'
799
+ | 'retryApiErrors'
800
+ | 'timeout'
801
+ | 'maxRetries'
802
+ | 'exactUrlOnly'
803
+ | 'toolId'
804
+ | 'onResponse'
805
+ | 'allowNotFound'
806
+ | 'idempotencyKey'
781
807
  >,
782
808
  ): Promise<T> {
783
809
  return this.request<T>(path, {
@@ -128,6 +128,8 @@ export type {
128
128
  RunsTailOptions,
129
129
  StopAllRunsOptions,
130
130
  ToolExecution,
131
+ ExecutionRecovery,
132
+ ExecutionByKeyResult,
131
133
  } from './client.js';
132
134
 
133
135
  // ——— Monitors framework ———
@@ -288,6 +290,8 @@ export type {
288
290
  DeeplinePlaysNamespace,
289
291
  DeeplinePlayRuntimeContext,
290
292
  DeeplineToolsNamespace,
293
+ DeeplineToolExecuteResult,
294
+ DeeplineExecutionsNamespace,
291
295
  DefinedPlay,
292
296
  ColumnMap,
293
297
  ColumnResolver,
@@ -109,7 +109,11 @@ import type {
109
109
  ToolDefinition,
110
110
  ToolMetadata,
111
111
  } from './types.js';
112
- import type { ToolExecution } from './client.js';
112
+ import type {
113
+ ExecutionByKeyResult,
114
+ ExecutionRecovery,
115
+ ToolExecution,
116
+ } from './client.js';
113
117
  import type {
114
118
  DurableCallStaleAfterSeconds,
115
119
  PlayAuthoringBindings,
@@ -660,7 +664,26 @@ export type DeeplineToolsNamespace = {
660
664
  execute(
661
665
  toolId: string,
662
666
  input: Record<string, unknown>,
663
- ): Promise<ToolExecuteResult>;
667
+ options?: {
668
+ idempotencyKey?: string;
669
+ recover?: boolean;
670
+ onExecution?: (execution: {
671
+ idempotencyKey: string;
672
+ }) => void | Promise<void>;
673
+ recoveryTimeoutMs?: number;
674
+ },
675
+ ): Promise<DeeplineToolExecuteResult>;
676
+ };
677
+
678
+ export type DeeplineToolExecuteResult = ToolExecuteResult & {
679
+ /** Stable key used to recover this direct tool execution, when enabled. */
680
+ idempotencyKey?: string;
681
+ executionRecovery?: ExecutionRecovery;
682
+ };
683
+
684
+ export type DeeplineExecutionsNamespace = {
685
+ /** Look up a recoverable tool execution by its stable idempotency key. */
686
+ getByKey(idempotencyKey: string): Promise<ExecutionByKeyResult>;
664
687
  };
665
688
 
666
689
  /**
@@ -1084,10 +1107,19 @@ export class DeeplineContext {
1084
1107
  execute: async (
1085
1108
  toolId: string,
1086
1109
  input: Record<string, unknown>,
1087
- ): Promise<ToolExecuteResult> => {
1110
+ options?: {
1111
+ idempotencyKey?: string;
1112
+ recover?: boolean;
1113
+ onExecution?: (execution: {
1114
+ idempotencyKey: string;
1115
+ }) => void | Promise<void>;
1116
+ recoveryTimeoutMs?: number;
1117
+ },
1118
+ ): Promise<DeeplineToolExecuteResult> => {
1088
1119
  const response = await this.client.executeTool(toolId, input, {
1089
1120
  includeToolMetadata: true,
1090
1121
  responseIntent: 'dataset',
1122
+ ...options,
1091
1123
  });
1092
1124
  return toolExecutionEnvelopeToResult(toolId, response, {
1093
1125
  client: this.client,
@@ -1097,6 +1129,11 @@ export class DeeplineContext {
1097
1129
  };
1098
1130
  }
1099
1131
 
1132
+ /** Durable state for recoverable direct tool executions. */
1133
+ get executions(): DeeplineExecutionsNamespace {
1134
+ return { getByKey: (key) => this.client.getExecutionByKey(key) };
1135
+ }
1136
+
1100
1137
  /**
1101
1138
  * Play discovery and named-play handles.
1102
1139
  *
@@ -1506,7 +1543,7 @@ function toolExecutionEnvelopeToResult(
1506
1543
  client: DeeplineClient;
1507
1544
  input: Record<string, unknown>;
1508
1545
  },
1509
- ): ToolExecuteResult {
1546
+ ): DeeplineToolExecuteResult {
1510
1547
  const raw = response.toolResponse?.raw ?? null;
1511
1548
  const rawV2 = response.toolResponse?.rawV2;
1512
1549
  const view = response.toolResponse?.view;
@@ -1516,7 +1553,7 @@ function toolExecutionEnvelopeToResult(
1516
1553
  : null;
1517
1554
  const toolMetadata = isRecord(metadata) ? metadata : {};
1518
1555
 
1519
- return attachSdkQueryResultDatasetResult(
1556
+ const result: DeeplineToolExecuteResult = attachSdkQueryResultDatasetResult(
1520
1557
  fallbackToolId,
1521
1558
  createToolExecuteResult({
1522
1559
  status:
@@ -1553,6 +1590,19 @@ function toolExecutionEnvelopeToResult(
1553
1590
  }),
1554
1591
  options,
1555
1592
  );
1593
+ const executionRecovery = isRecord(response.executionRecovery)
1594
+ ? (response.executionRecovery as unknown as ExecutionRecovery)
1595
+ : undefined;
1596
+ // `createToolExecuteResult` installs `toolOutput` and `_metadata` as
1597
+ // non-enumerable compatibility properties. Mutating the wrapper preserves
1598
+ // those aliases while adding recovery fields to the same public result.
1599
+ if (typeof response.idempotencyKey === 'string') {
1600
+ result.idempotencyKey = response.idempotencyKey;
1601
+ }
1602
+ if (executionRecovery) {
1603
+ result.executionRecovery = executionRecovery;
1604
+ }
1605
+ return result;
1556
1606
  }
1557
1607
 
1558
1608
  export function defineInput<TInput>(
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.149',
203
+ version: '0.3.150',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -561,6 +561,24 @@
561
561
  "failureAlert": "vercel_route_wrapper",
562
562
  "domainReportRequired": true
563
563
  },
564
+ {
565
+ "id": "vercel.public-tool-execution-blob-cleanup",
566
+ "name": "Public tool execution blob cleanup",
567
+ "scheduler": "vercel_cron",
568
+ "cadence": "*/15 * * * *",
569
+ "timezone": "UTC",
570
+ "source": "apps/deepline-api/vercel.json",
571
+ "locator": "/api/v2/cron/public-tool-execution-blob-cleanup",
572
+ "owner": "integrations",
573
+ "criticality": "maintenance",
574
+ "purpose": "Delete expired public execution response and launch blobs while retaining idempotency key and fingerprint tombstones.",
575
+ "successDefinition": "Every claimed expired receipt's recorded blobs are confirmed absent before their references are cleared; no eligible receipts is a valid no-op.",
576
+ "failureDefinition": "An R2 deletion/absence check fails or the fenced tombstone cleanup acknowledgement cannot complete.",
577
+ "startGraceMs": 300000,
578
+ "runTimeoutMs": 1800000,
579
+ "failureAlert": "vercel_route_wrapper",
580
+ "domainReportRequired": true
581
+ },
564
582
  {
565
583
  "id": "vercel.scheduled-work-health-snapshot",
566
584
  "name": "Scheduled-work health snapshot",
@@ -144,6 +144,39 @@ export async function r2ObjectExists(
144
144
  }
145
145
  }
146
146
 
147
+ /**
148
+ * Confirm an R2 object is absent without confusing a HEAD/auth/network failure
149
+ * with a successful deletion.
150
+ */
151
+ export async function confirmR2ObjectAbsent(
152
+ key: string,
153
+ kind: R2BucketKind = 'default',
154
+ ): Promise<boolean> {
155
+ const config = getR2Config(kind);
156
+ const client = getR2Client(config);
157
+ try {
158
+ await client.send(
159
+ new HeadObjectCommand({ Bucket: config.bucket, Key: key }),
160
+ );
161
+ return false;
162
+ } catch (error) {
163
+ const details = error as {
164
+ name?: string;
165
+ Code?: string;
166
+ $metadata?: { httpStatusCode?: number };
167
+ };
168
+ if (
169
+ details.$metadata?.httpStatusCode === 404 ||
170
+ details.name === 'NotFound' ||
171
+ details.name === 'NoSuchKey' ||
172
+ details.Code === 'NotFound' ||
173
+ details.Code === 'NoSuchKey'
174
+ )
175
+ return true;
176
+ throw error;
177
+ }
178
+ }
179
+
147
180
  export async function uploadBufferToR2(input: {
148
181
  key: string;
149
182
  body: Buffer | Uint8Array;