deepline 0.3.148 → 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.
- package/dist/bundling-sources/sdk/src/client.ts +347 -12
- package/dist/bundling-sources/sdk/src/http.ts +28 -2
- package/dist/bundling-sources/sdk/src/index.ts +4 -0
- package/dist/bundling-sources/sdk/src/play.ts +55 -5
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/observability/dlq.ts +1 -0
- package/dist/bundling-sources/shared_libs/observability/queue-health.ts +31 -0
- package/dist/bundling-sources/shared_libs/observability/queue-item-connectors.ts +13 -1
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
- package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +33 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +94 -23
- package/dist/cli/index.js +393 -17
- package/dist/cli/index.mjs +399 -20
- package/dist/index.d.mts +61 -2
- package/dist/index.d.ts +61 -2
- package/dist/index.js +236 -13
- package/dist/index.mjs +236 -13
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
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
|
-
|
|
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.
|
|
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: {
|
|
@@ -367,8 +367,39 @@ export type QueueRetirementCandidate = {
|
|
|
367
367
|
readonly itemId: string;
|
|
368
368
|
readonly bucket: 'blocked' | 'deadLettered';
|
|
369
369
|
readonly ageMs: number;
|
|
370
|
+
/** Native generation. An absent value must never suppress unresolved work. */
|
|
371
|
+
readonly sourceRevision?: string;
|
|
370
372
|
};
|
|
371
373
|
|
|
374
|
+
/** The runtime item reader and bounded health observer must derive the same
|
|
375
|
+
* opaque revision from the same native row, or an old operator disposition
|
|
376
|
+
* could hide a later failure of the same item identity. */
|
|
377
|
+
export function runtimeQueueItemRevision(fields: {
|
|
378
|
+
updatedAt?: unknown;
|
|
379
|
+
createdAt?: unknown;
|
|
380
|
+
state?: unknown;
|
|
381
|
+
status?: unknown;
|
|
382
|
+
leaseExpiresAt?: unknown;
|
|
383
|
+
failedAt?: unknown;
|
|
384
|
+
}): string {
|
|
385
|
+
const timestamp = (value: unknown): string | undefined => {
|
|
386
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) {
|
|
387
|
+
return value.toISOString();
|
|
388
|
+
}
|
|
389
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
390
|
+
const date = new Date(value);
|
|
391
|
+
if (Number.isFinite(date.getTime())) return date.toISOString();
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
394
|
+
};
|
|
395
|
+
return [
|
|
396
|
+
timestamp(fields.updatedAt) ?? timestamp(fields.createdAt) ?? '',
|
|
397
|
+
String(fields.state ?? fields.status ?? ''),
|
|
398
|
+
timestamp(fields.leaseExpiresAt) ?? '',
|
|
399
|
+
timestamp(fields.failedAt) ?? '',
|
|
400
|
+
].join(':');
|
|
401
|
+
}
|
|
402
|
+
|
|
372
403
|
/**
|
|
373
404
|
* Remove operator-retired identities from a bounded source sample. The source
|
|
374
405
|
* still owns every row; the disposition overlay only changes what operators
|
|
@@ -35,6 +35,12 @@ export type QueueItemConnectorRegisterOptions = {
|
|
|
35
35
|
readonly replaceUnavailable?: boolean;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
function isUnavailableRegistration(
|
|
39
|
+
registration: QueueItemConnectorRegistration,
|
|
40
|
+
): boolean {
|
|
41
|
+
return registration.connector.id.endsWith('-unavailable');
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
/**
|
|
39
45
|
* The registry is the routing boundary: the control service looks up a queue
|
|
40
46
|
* by its stable public id and never switches on a native table or provider.
|
|
@@ -72,7 +78,7 @@ export class QueueItemConnectorRegistry {
|
|
|
72
78
|
if (
|
|
73
79
|
existing &&
|
|
74
80
|
options.replaceUnavailable &&
|
|
75
|
-
!existing
|
|
81
|
+
!isUnavailableRegistration(existing)
|
|
76
82
|
) {
|
|
77
83
|
throw new Error(
|
|
78
84
|
`Queue connector ${connector.queueId} is already owned by ${existing.connector.id}; replacement is not allowed.`,
|
|
@@ -90,6 +96,12 @@ export class QueueItemConnectorRegistry {
|
|
|
90
96
|
return this.registrations.has(queueId);
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
/** True only after the owner has replaced the safe unavailable placeholder. */
|
|
100
|
+
hasOwnerConnector(queueId: string): boolean {
|
|
101
|
+
const registration = this.registrations.get(queueId);
|
|
102
|
+
return registration !== undefined && !isUnavailableRegistration(registration);
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
list(): readonly QueueItemConnectorRegistration[] {
|
|
94
106
|
return [...this.registrations.values()];
|
|
95
107
|
}
|