deepline 0.1.287 → 0.1.288
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/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +228 -94
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +13 -1
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -155,7 +155,7 @@ export const SDK_RELEASE = {
|
|
|
155
155
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
156
156
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
157
157
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
158
|
-
version: '0.1.
|
|
158
|
+
version: '0.1.288',
|
|
159
159
|
contracts: {
|
|
160
160
|
api: {
|
|
161
161
|
name: 'sdk-http-api',
|
|
@@ -309,6 +309,10 @@ const MAP_FRAME_FLUSH_INTERVAL_MS = 250;
|
|
|
309
309
|
const TOOL_BATCH_COALESCE_WINDOW_MS = 5;
|
|
310
310
|
const TOOL_SCALAR_COALESCE_WINDOW_MS = 5;
|
|
311
311
|
const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
|
|
312
|
+
// The receipt gateway fully buffers the integration response before sending
|
|
313
|
+
// headers to the runner. Once those headers arrive, a long body stall is a
|
|
314
|
+
// broken gateway-to-runner connection, not slow provider work.
|
|
315
|
+
export const TOOL_EXECUTE_RESPONSE_BODY_TIMEOUT_MS = 30_000;
|
|
312
316
|
const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
313
317
|
const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
|
|
314
318
|
// Every runtime-API tool fetch needs a client-side deadline. Without one, a
|
|
@@ -352,6 +356,35 @@ class ToolExecuteResponseBodyTransportError extends Error {
|
|
|
352
356
|
}
|
|
353
357
|
}
|
|
354
358
|
|
|
359
|
+
class ToolExecuteResponseBodyTimeoutError extends Error {
|
|
360
|
+
constructor(toolId: string) {
|
|
361
|
+
super(
|
|
362
|
+
`Tool ${toolId} response body was not delivered within ${TOOL_EXECUTE_RESPONSE_BODY_TIMEOUT_MS}ms after response headers.`,
|
|
363
|
+
);
|
|
364
|
+
this.name = 'ToolExecuteResponseBodyTimeoutError';
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function readToolExecuteResponseBody<T>(input: {
|
|
369
|
+
toolId: string;
|
|
370
|
+
abortController: AbortController | null;
|
|
371
|
+
read: () => Promise<T>;
|
|
372
|
+
}): Promise<T> {
|
|
373
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
374
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
375
|
+
timeoutHandle = setTimeout(() => {
|
|
376
|
+
const error = new ToolExecuteResponseBodyTimeoutError(input.toolId);
|
|
377
|
+
input.abortController?.abort(error);
|
|
378
|
+
reject(error);
|
|
379
|
+
}, TOOL_EXECUTE_RESPONSE_BODY_TIMEOUT_MS);
|
|
380
|
+
});
|
|
381
|
+
try {
|
|
382
|
+
return await Promise.race([input.read(), timeout]);
|
|
383
|
+
} finally {
|
|
384
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
355
388
|
class ToolExecuteInvalidJsonError extends Error {
|
|
356
389
|
readonly cause: unknown;
|
|
357
390
|
|
|
@@ -1289,6 +1322,7 @@ export class PlayContextImpl {
|
|
|
1289
1322
|
createSecretRedactionContext();
|
|
1290
1323
|
private mapInvocationIndex = 0;
|
|
1291
1324
|
private readonly stepCallIndexByKey = new Map<string, number>();
|
|
1325
|
+
private readonly toolCallIndexByKey = new Map<string, number>();
|
|
1292
1326
|
/**
|
|
1293
1327
|
* Parent-level inline-child aggregates. Maintained only by the single-writer
|
|
1294
1328
|
* progress path (never per child) so concurrent fan-out cannot contend. See
|
|
@@ -3281,7 +3315,9 @@ export class PlayContextImpl {
|
|
|
3281
3315
|
const executionNonce =
|
|
3282
3316
|
typeof wrapped.job_id === 'string' && wrapped.job_id.trim()
|
|
3283
3317
|
? wrapped.job_id.trim()
|
|
3284
|
-
:
|
|
3318
|
+
: stableDigest(
|
|
3319
|
+
`${this.currentRunId}:${toolId}:${stableStringify(originalRequestInput)}:${datasetLimit}`,
|
|
3320
|
+
);
|
|
3285
3321
|
const datasetId = `tool-list:${sha256Hex(
|
|
3286
3322
|
`${toolId}:${this.currentRunId}:${sql}:${datasetLimit}:${executionNonce}`,
|
|
3287
3323
|
)}`;
|
|
@@ -3294,6 +3330,10 @@ export class PlayContextImpl {
|
|
|
3294
3330
|
toolId,
|
|
3295
3331
|
originalRequestInput,
|
|
3296
3332
|
{
|
|
3333
|
+
durableCallReceiptKey: `${buildDurableToolReceiptPrefix({
|
|
3334
|
+
orgId: this.#options.orgId ?? 'unknown-org',
|
|
3335
|
+
toolId,
|
|
3336
|
+
})}${stableDigest(`${datasetId}:${offset}:${limit}`)}`,
|
|
3297
3337
|
timeoutMs: resolveToolRuntimeTimeoutMs(toolId),
|
|
3298
3338
|
customerDbDataset: {
|
|
3299
3339
|
limit: datasetLimit,
|
|
@@ -6461,6 +6501,14 @@ export class PlayContextImpl {
|
|
|
6461
6501
|
toolId,
|
|
6462
6502
|
requestInput: input,
|
|
6463
6503
|
});
|
|
6504
|
+
const store = rowContext.getStore();
|
|
6505
|
+
let logicalCallId: string | null = null;
|
|
6506
|
+
if (!store) {
|
|
6507
|
+
const callIndexKey = `${this.currentExecutionScope.receipt.namespace}:workflow:${normalizedKey}:${toolRequestIdentity}`;
|
|
6508
|
+
const callIndex = this.toolCallIndexByKey.get(callIndexKey) ?? 0;
|
|
6509
|
+
this.toolCallIndexByKey.set(callIndexKey, callIndex + 1);
|
|
6510
|
+
logicalCallId = stableDigest(`${callIndexKey}:${callIndex}`);
|
|
6511
|
+
}
|
|
6464
6512
|
let executionAuthScopeDigest =
|
|
6465
6513
|
(await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null;
|
|
6466
6514
|
const eventWaitHandler =
|
|
@@ -6476,7 +6524,6 @@ export class PlayContextImpl {
|
|
|
6476
6524
|
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
6477
6525
|
});
|
|
6478
6526
|
const checkpointCacheKeys = [durableCacheKey];
|
|
6479
|
-
const store = rowContext.getStore();
|
|
6480
6527
|
|
|
6481
6528
|
const executeTool = async (context?: {
|
|
6482
6529
|
leaseId?: string | null;
|
|
@@ -6531,18 +6578,28 @@ export class PlayContextImpl {
|
|
|
6531
6578
|
: `Calling tool: ${toolId}`,
|
|
6532
6579
|
);
|
|
6533
6580
|
const directReceiptLeaseId = context?.leaseId?.trim() || null;
|
|
6581
|
+
if (!logicalCallId) {
|
|
6582
|
+
throw new Error(
|
|
6583
|
+
'Direct tool execution requires a stable logical call identity.',
|
|
6584
|
+
);
|
|
6585
|
+
}
|
|
6586
|
+
const physicalDirectKey = cacheableToolResult
|
|
6587
|
+
? directCacheKey
|
|
6588
|
+
: `${buildDurableToolReceiptPrefix({
|
|
6589
|
+
orgId: this.#options.orgId ?? 'unknown-org',
|
|
6590
|
+
toolId,
|
|
6591
|
+
})}${stableDigest(`${this.currentRunId}:${logicalCallId}:always-fresh`)}`;
|
|
6534
6592
|
const execution = await this.callToolExecutionAPI(toolId, input, {
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
: {}),
|
|
6593
|
+
durableCallReceiptKey: physicalDirectKey,
|
|
6594
|
+
executionAuthScopeDigest,
|
|
6595
|
+
providerIdempotencyKey: cacheableToolResult
|
|
6596
|
+
? this.providerIdempotencyKeyForToolCall({
|
|
6597
|
+
cacheKey: directCacheKey,
|
|
6598
|
+
force: toolCachePolicy.force,
|
|
6599
|
+
leaseId: directReceiptLeaseId,
|
|
6600
|
+
logicalCallId,
|
|
6601
|
+
})
|
|
6602
|
+
: physicalDirectKey,
|
|
6546
6603
|
timeoutMs: resolveToolRuntimeTimeoutMs(toolId, options?.timeoutMs),
|
|
6547
6604
|
...(directReceiptLeaseId &&
|
|
6548
6605
|
(this.#options.markRuntimeStepReceiptRunning ||
|
|
@@ -6692,7 +6749,7 @@ export class PlayContextImpl {
|
|
|
6692
6749
|
) {
|
|
6693
6750
|
try {
|
|
6694
6751
|
const toolRetryPolicy = await this.#options
|
|
6695
|
-
.getToolRetryPolicy?.(toolId)
|
|
6752
|
+
.getToolRetryPolicy?.(toolId, input)
|
|
6696
6753
|
.catch(() => null);
|
|
6697
6754
|
return await this.executeWithRuntimeReceipt(
|
|
6698
6755
|
'tool',
|
|
@@ -8547,11 +8604,17 @@ export class PlayContextImpl {
|
|
|
8547
8604
|
cacheKey: string;
|
|
8548
8605
|
force?: boolean;
|
|
8549
8606
|
leaseId?: string | null;
|
|
8607
|
+
logicalCallId?: string;
|
|
8550
8608
|
}): string {
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8609
|
+
let fallbackAttemptId: string | null = null;
|
|
8610
|
+
if (input.force === true && !input.leaseId) {
|
|
8611
|
+
if (!input.logicalCallId) {
|
|
8612
|
+
throw new Error(
|
|
8613
|
+
'Forced tool execution without a receipt lease requires a stable logical call identity.',
|
|
8614
|
+
);
|
|
8615
|
+
}
|
|
8616
|
+
fallbackAttemptId = `${this.currentRunId}:${input.logicalCallId}`;
|
|
8617
|
+
}
|
|
8555
8618
|
return buildDurableToolProviderIdempotencyKey({
|
|
8556
8619
|
receiptKey: input.cacheKey,
|
|
8557
8620
|
force: input.force,
|
|
@@ -8570,7 +8633,13 @@ export class PlayContextImpl {
|
|
|
8570
8633
|
'executorToken and baseUrl are required for tool API calls (cloud execution only)',
|
|
8571
8634
|
);
|
|
8572
8635
|
}
|
|
8573
|
-
const
|
|
8636
|
+
const requestsDurableInvocationFence =
|
|
8637
|
+
this.#options.requestDurableInvocationFence === true ||
|
|
8638
|
+
this.#options.durableInvocationFence === true;
|
|
8639
|
+
const executeSuffix = requestsDurableInvocationFence
|
|
8640
|
+
? 'execute-fenced-v1'
|
|
8641
|
+
: 'execute';
|
|
8642
|
+
const url = `${this.#options.baseUrl}/api/v2/integrations/${encodeURIComponent(toolId)}/${executeSuffix}`;
|
|
8574
8643
|
const timeoutMs = resolveToolRuntimeTimeoutMs(toolId, options?.timeoutMs);
|
|
8575
8644
|
const provider = toolId.split(/[._]/)[0]?.trim() || 'provider';
|
|
8576
8645
|
const activityId = `provider:${toolId}`;
|
|
@@ -8607,12 +8676,89 @@ export class PlayContextImpl {
|
|
|
8607
8676
|
async (span) => {
|
|
8608
8677
|
const httpFailureAttempts =
|
|
8609
8678
|
createToolExecuteHttpFailureAttemptTracker();
|
|
8679
|
+
// Snapshot the caller-controlled payload exactly once. The replay
|
|
8680
|
+
// decision and every physical attempt must refer to the same bytes,
|
|
8681
|
+
// even if the original object has getters or is later mutated.
|
|
8682
|
+
const toolInputSnapshot = JSON.parse(JSON.stringify(input)) as Record<
|
|
8683
|
+
string,
|
|
8684
|
+
unknown
|
|
8685
|
+
>;
|
|
8610
8686
|
const retryPolicy = await this.#options
|
|
8611
|
-
.getToolRetryPolicy?.(toolId)
|
|
8687
|
+
.getToolRetryPolicy?.(toolId, toolInputSnapshot)
|
|
8612
8688
|
.catch(() => null);
|
|
8613
8689
|
const retrySafeTransientHttp =
|
|
8614
8690
|
retryPolicy?.retrySafeTransientHttp === true;
|
|
8691
|
+
const invocationOrgId = this.#options.orgId?.trim() ?? '';
|
|
8692
|
+
const durableCallReceiptKey =
|
|
8693
|
+
options?.durableCallReceiptKey?.trim() ||
|
|
8694
|
+
(requestsDurableInvocationFence && invocationOrgId
|
|
8695
|
+
? `${buildDurableToolReceiptPrefix({
|
|
8696
|
+
orgId: invocationOrgId,
|
|
8697
|
+
toolId,
|
|
8698
|
+
})}${stableDigest(
|
|
8699
|
+
`transport:${this.currentRunId}:${toolId}:${crypto.randomUUID()}`,
|
|
8700
|
+
)}`
|
|
8701
|
+
: null);
|
|
8702
|
+
const executionAuthScopeDigest = options?.executionAuthScopeDigest
|
|
8703
|
+
? options.executionAuthScopeDigest.trim() || null
|
|
8704
|
+
: ((await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null);
|
|
8705
|
+
const providerIdempotencyKey =
|
|
8706
|
+
options?.providerIdempotencyKey?.trim() || durableCallReceiptKey;
|
|
8707
|
+
// Correlation identity is stable across every transport retry,
|
|
8708
|
+
// including calls without a durable receipt.
|
|
8709
|
+
const deeplineRequestId = providerIdempotencyKey
|
|
8710
|
+
? `ctx-tool-${stableDigest(providerIdempotencyKey).slice(0, 32)}`
|
|
8711
|
+
: `ctx-tool-${crypto.randomUUID()}`;
|
|
8712
|
+
const serializedRequestBody = (invocationAttempt: number) =>
|
|
8713
|
+
JSON.stringify({
|
|
8714
|
+
payload: toolInputSnapshot,
|
|
8715
|
+
metadata: {
|
|
8716
|
+
parent_run_id: this.#options.runId,
|
|
8717
|
+
invocation_attempt: invocationAttempt,
|
|
8718
|
+
...(requestsDurableInvocationFence
|
|
8719
|
+
? { invocation_fence_version: 1 }
|
|
8720
|
+
: {}),
|
|
8721
|
+
...(durableCallReceiptKey
|
|
8722
|
+
? {
|
|
8723
|
+
durable_call_receipt_key: durableCallReceiptKey,
|
|
8724
|
+
...(executionAuthScopeDigest
|
|
8725
|
+
? {
|
|
8726
|
+
execution_auth_scope_digest:
|
|
8727
|
+
executionAuthScopeDigest,
|
|
8728
|
+
}
|
|
8729
|
+
: {}),
|
|
8730
|
+
}
|
|
8731
|
+
: {}),
|
|
8732
|
+
...(options?.customerDbDataset
|
|
8733
|
+
? {
|
|
8734
|
+
query_result_dataset: {
|
|
8735
|
+
limit: options.customerDbDataset.limit,
|
|
8736
|
+
offset: options.customerDbDataset.offset,
|
|
8737
|
+
page_size: options.customerDbDataset.pageSize,
|
|
8738
|
+
total_rows: options.customerDbDataset.totalRows,
|
|
8739
|
+
},
|
|
8740
|
+
...(isCustomerDbDatasetTool(toolId)
|
|
8741
|
+
? {
|
|
8742
|
+
customer_db_dataset: {
|
|
8743
|
+
limit: options.customerDbDataset.limit,
|
|
8744
|
+
offset: options.customerDbDataset.offset,
|
|
8745
|
+
page_size: options.customerDbDataset.pageSize,
|
|
8746
|
+
total_rows: options.customerDbDataset.totalRows,
|
|
8747
|
+
},
|
|
8748
|
+
}
|
|
8749
|
+
: {}),
|
|
8750
|
+
}
|
|
8751
|
+
: {}),
|
|
8752
|
+
...(providerIdempotencyKey
|
|
8753
|
+
? { provider_idempotency_key: providerIdempotencyKey }
|
|
8754
|
+
: {}),
|
|
8755
|
+
},
|
|
8756
|
+
...(this.#options.integrationMode
|
|
8757
|
+
? { integration_mode: this.#options.integrationMode }
|
|
8758
|
+
: {}),
|
|
8759
|
+
});
|
|
8615
8760
|
let transportAttempt = 0;
|
|
8761
|
+
let invocationAttempt = 0;
|
|
8616
8762
|
const retryToolTransportFailure = async (input: {
|
|
8617
8763
|
error: unknown;
|
|
8618
8764
|
elapsedMs: number;
|
|
@@ -8662,23 +8808,7 @@ export class PlayContextImpl {
|
|
|
8662
8808
|
let responseErrorText: string | null = null;
|
|
8663
8809
|
let providerCallStartedAt: number | null = null;
|
|
8664
8810
|
let providerCallElapsedMs: number | null = null;
|
|
8665
|
-
|
|
8666
|
-
options?.durableCallReceiptKey?.trim() || null;
|
|
8667
|
-
const executionAuthScopeDigest =
|
|
8668
|
-
durableCallReceiptKey && options?.executionAuthScopeDigest
|
|
8669
|
-
? options.executionAuthScopeDigest.trim() || null
|
|
8670
|
-
: durableCallReceiptKey
|
|
8671
|
-
? ((await this.resolveToolAuthScopeDigest(toolId))?.trim() ??
|
|
8672
|
-
null)
|
|
8673
|
-
: null;
|
|
8674
|
-
const providerIdempotencyKey =
|
|
8675
|
-
options?.providerIdempotencyKey?.trim() || durableCallReceiptKey;
|
|
8676
|
-
// Every gateway request needs a correlation ID. When the call has
|
|
8677
|
-
// an idempotency key, keep its stable ID across retries; otherwise
|
|
8678
|
-
// give this individual attempt a fresh ID for gateway pairing.
|
|
8679
|
-
const deeplineRequestId = providerIdempotencyKey
|
|
8680
|
-
? `ctx-tool-${stableDigest(providerIdempotencyKey).slice(0, 32)}`
|
|
8681
|
-
: `ctx-tool-${crypto.randomUUID()}`;
|
|
8811
|
+
let fetchDispatched = false;
|
|
8682
8812
|
// Receipt ownership is a liveness contract, not a one-time check.
|
|
8683
8813
|
// Keep it alive for the whole provider HTTP request. The cadence
|
|
8684
8814
|
// comes from the store-issued expiry because a remote runner may
|
|
@@ -8747,6 +8877,7 @@ export class PlayContextImpl {
|
|
|
8747
8877
|
const integrationFetchStartedAt = Date.now();
|
|
8748
8878
|
providerCallStartedAt = integrationFetchStartedAt;
|
|
8749
8879
|
try {
|
|
8880
|
+
fetchDispatched = true;
|
|
8750
8881
|
response = await fetch(url, {
|
|
8751
8882
|
method: 'POST',
|
|
8752
8883
|
signal: abortController?.signal,
|
|
@@ -8765,59 +8896,23 @@ export class PlayContextImpl {
|
|
|
8765
8896
|
}
|
|
8766
8897
|
: {}),
|
|
8767
8898
|
...protectionHeaders,
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
...(durableCallReceiptKey
|
|
8774
|
-
? {
|
|
8775
|
-
durable_call_receipt_key: durableCallReceiptKey,
|
|
8776
|
-
...(executionAuthScopeDigest
|
|
8777
|
-
? {
|
|
8778
|
-
execution_auth_scope_digest:
|
|
8779
|
-
executionAuthScopeDigest,
|
|
8780
|
-
}
|
|
8781
|
-
: {}),
|
|
8782
|
-
}
|
|
8783
|
-
: {}),
|
|
8784
|
-
...(options?.customerDbDataset
|
|
8785
|
-
? {
|
|
8786
|
-
query_result_dataset: {
|
|
8787
|
-
limit: options.customerDbDataset.limit,
|
|
8788
|
-
offset: options.customerDbDataset.offset,
|
|
8789
|
-
page_size: options.customerDbDataset.pageSize,
|
|
8790
|
-
total_rows: options.customerDbDataset.totalRows,
|
|
8791
|
-
},
|
|
8792
|
-
...(isCustomerDbDatasetTool(toolId)
|
|
8793
|
-
? {
|
|
8794
|
-
customer_db_dataset: {
|
|
8795
|
-
limit: options.customerDbDataset.limit,
|
|
8796
|
-
offset: options.customerDbDataset.offset,
|
|
8797
|
-
page_size:
|
|
8798
|
-
options.customerDbDataset.pageSize,
|
|
8799
|
-
total_rows:
|
|
8800
|
-
options.customerDbDataset.totalRows,
|
|
8801
|
-
},
|
|
8802
|
-
}
|
|
8803
|
-
: {}),
|
|
8804
|
-
}
|
|
8805
|
-
: {}),
|
|
8806
|
-
...(providerIdempotencyKey
|
|
8807
|
-
? { provider_idempotency_key: providerIdempotencyKey }
|
|
8808
|
-
: {}),
|
|
8809
|
-
},
|
|
8810
|
-
...(this.#options.integrationMode
|
|
8811
|
-
? { integration_mode: this.#options.integrationMode }
|
|
8899
|
+
...(this.#options.runtimeTestFaultHeader
|
|
8900
|
+
? {
|
|
8901
|
+
'x-deepline-test-fault':
|
|
8902
|
+
this.#options.runtimeTestFaultHeader,
|
|
8903
|
+
}
|
|
8812
8904
|
: {}),
|
|
8813
|
-
}
|
|
8905
|
+
},
|
|
8906
|
+
body: serializedRequestBody(invocationAttempt),
|
|
8814
8907
|
});
|
|
8815
8908
|
if (response.ok) {
|
|
8816
8909
|
try {
|
|
8817
|
-
responseData =
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8910
|
+
responseData = await readToolExecuteResponseBody({
|
|
8911
|
+
toolId,
|
|
8912
|
+
abortController,
|
|
8913
|
+
read: () =>
|
|
8914
|
+
response!.json() as Promise<Record<string, unknown>>,
|
|
8915
|
+
});
|
|
8821
8916
|
} catch (error) {
|
|
8822
8917
|
if (error instanceof SyntaxError) {
|
|
8823
8918
|
throw new ToolExecuteInvalidJsonError(error);
|
|
@@ -8826,7 +8921,11 @@ export class PlayContextImpl {
|
|
|
8826
8921
|
}
|
|
8827
8922
|
} else {
|
|
8828
8923
|
try {
|
|
8829
|
-
responseErrorText = await
|
|
8924
|
+
responseErrorText = await readToolExecuteResponseBody({
|
|
8925
|
+
toolId,
|
|
8926
|
+
abortController,
|
|
8927
|
+
read: () => response!.text(),
|
|
8928
|
+
});
|
|
8830
8929
|
} catch (error) {
|
|
8831
8930
|
throw new ToolExecuteResponseBodyTransportError(error);
|
|
8832
8931
|
}
|
|
@@ -8887,11 +8986,39 @@ export class PlayContextImpl {
|
|
|
8887
8986
|
402,
|
|
8888
8987
|
);
|
|
8889
8988
|
}
|
|
8890
|
-
const
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8989
|
+
const ambiguousDispatchedFailure =
|
|
8990
|
+
fetchDispatched &&
|
|
8991
|
+
(response === null ||
|
|
8992
|
+
error instanceof ToolExecuteResponseBodyTransportError);
|
|
8993
|
+
const hasDurableInvocationIdentity = Boolean(
|
|
8994
|
+
durableCallReceiptKey &&
|
|
8995
|
+
providerIdempotencyKey &&
|
|
8996
|
+
executionAuthScopeDigest,
|
|
8997
|
+
);
|
|
8998
|
+
// A post-header body deadline aborts the fetch controller solely
|
|
8999
|
+
// to stop consuming that response. It must not also cancel the
|
|
9000
|
+
// independent gateway health check that establishes whether the
|
|
9001
|
+
// committed response can be replayed safely. Preserve genuine
|
|
9002
|
+
// caller/runtime cancellation by dropping the signal only when
|
|
9003
|
+
// this exact body deadline caused the abort.
|
|
9004
|
+
const durableFenceVerificationSignal =
|
|
9005
|
+
abortController?.signal.aborted === true &&
|
|
9006
|
+
abortController.signal.reason instanceof
|
|
9007
|
+
ToolExecuteResponseBodyTimeoutError
|
|
9008
|
+
? undefined
|
|
9009
|
+
: abortController?.signal;
|
|
9010
|
+
const durableInvocationFenceVerified =
|
|
9011
|
+
hasDurableInvocationIdentity &&
|
|
9012
|
+
requestsDurableInvocationFence &&
|
|
9013
|
+
(this.#options.durableInvocationFence === true ||
|
|
9014
|
+
(await this.#options
|
|
9015
|
+
.verifyDurableInvocationFence?.(
|
|
9016
|
+
durableFenceVerificationSignal,
|
|
9017
|
+
)
|
|
9018
|
+
.catch(() => false)) === true);
|
|
9019
|
+
const transportReplaySafe =
|
|
9020
|
+
!ambiguousDispatchedFailure || durableInvocationFenceVerified;
|
|
9021
|
+
if (!transportReplaySafe) {
|
|
8895
9022
|
const diagnostic = describeTransportError(transportError);
|
|
8896
9023
|
this.log(
|
|
8897
9024
|
`[runtime.transport_failure] ${JSON.stringify({
|
|
@@ -8905,13 +9032,17 @@ export class PlayContextImpl {
|
|
|
8905
9032
|
: Date.now() - providerCallStartedAt,
|
|
8906
9033
|
request_id: deeplineRequestId,
|
|
8907
9034
|
aborted: abortController?.signal.aborted === true,
|
|
8908
|
-
response_headers_received:
|
|
9035
|
+
response_headers_received: response !== null,
|
|
8909
9036
|
retry_safe: false,
|
|
8910
9037
|
error: diagnostic,
|
|
8911
9038
|
})}`,
|
|
8912
9039
|
);
|
|
9040
|
+
const failureBoundary =
|
|
9041
|
+
error instanceof ToolExecuteResponseBodyTransportError
|
|
9042
|
+
? 'response body transport failed after response headers'
|
|
9043
|
+
: 'request transport failed after dispatch before response headers';
|
|
8913
9044
|
throw new ToolHttpError(
|
|
8914
|
-
`Tool ${toolId}
|
|
9045
|
+
`Tool ${toolId} ${failureBoundary}; the ambiguous call was not retried because this runtime has no durable invocation fence: ${diagnostic.message ?? 'unknown transport error'}`,
|
|
8915
9046
|
null,
|
|
8916
9047
|
0,
|
|
8917
9048
|
'repairable',
|
|
@@ -9004,6 +9135,9 @@ export class PlayContextImpl {
|
|
|
9004
9135
|
);
|
|
9005
9136
|
}
|
|
9006
9137
|
if (failure.shouldRetry) {
|
|
9138
|
+
if (failure.reason !== 'gateway_invocation_in_progress') {
|
|
9139
|
+
invocationAttempt += 1;
|
|
9140
|
+
}
|
|
9007
9141
|
if (failure.chargeRetryBudget) {
|
|
9008
9142
|
await this.governor.chargeBudget('retry');
|
|
9009
9143
|
}
|
|
@@ -512,6 +512,15 @@ export interface ContextOptions {
|
|
|
512
512
|
/** Short-lived HMAC-signed internal token for tool callbacks. Required for cloud execution. */
|
|
513
513
|
executorToken?: string;
|
|
514
514
|
baseUrl?: string;
|
|
515
|
+
/**
|
|
516
|
+
* The integration base URL persists each keyed execute response before
|
|
517
|
+
* exposing it. Only runtime adapters that route through that gateway may
|
|
518
|
+
* enable ambiguous transport replay.
|
|
519
|
+
*/
|
|
520
|
+
durableInvocationFence?: boolean;
|
|
521
|
+
requestDurableInvocationFence?: boolean;
|
|
522
|
+
verifyDurableInvocationFence?: (signal?: AbortSignal) => Promise<boolean>;
|
|
523
|
+
runtimeTestFaultHeader?: string | null;
|
|
515
524
|
/**
|
|
516
525
|
* Runtime-sheet transport selected by the runner. Daytona sandboxes use the
|
|
517
526
|
* execution gateway and must never fall back to minting direct DB sessions.
|
|
@@ -644,7 +653,10 @@ export interface ContextOptions {
|
|
|
644
653
|
resolvePlay?: (playRef: string) => Promise<ResolvedPlayExecution | null>;
|
|
645
654
|
getToolQueueHints?: (toolId: string) => Promise<readonly PlayQueueHint[]>;
|
|
646
655
|
getToolProvider?: (toolId: string) => Promise<string | null>;
|
|
647
|
-
getToolRetryPolicy?: (
|
|
656
|
+
getToolRetryPolicy?: (
|
|
657
|
+
toolId: string,
|
|
658
|
+
input: Record<string, unknown>,
|
|
659
|
+
) => Promise<{
|
|
648
660
|
retrySafeTransientHttp?: boolean;
|
|
649
661
|
requiresExecutionFence?: boolean;
|
|
650
662
|
} | null>;
|
|
@@ -6,6 +6,7 @@ export type RuntimeTestFaultName =
|
|
|
6
6
|
| 'receipt_complete_write_fail'
|
|
7
7
|
| 'receipt_fail_write_fail'
|
|
8
8
|
| 'worker_receipt_complete_write_fail'
|
|
9
|
+
| 'invocation_response_delivery_abort'
|
|
9
10
|
/**
|
|
10
11
|
* Holds an already checked-out receipt-gateway scheduler client for the
|
|
11
12
|
* supplied bounded millisecond value. Preview/CI only: this creates a
|
|
@@ -70,6 +71,7 @@ const SUPPORTED_RUNTIME_TEST_FAULTS = new Set<RuntimeTestFaultName>([
|
|
|
70
71
|
'receipt_complete_write_fail',
|
|
71
72
|
'receipt_fail_write_fail',
|
|
72
73
|
'worker_receipt_complete_write_fail',
|
|
74
|
+
'invocation_response_delivery_abort',
|
|
73
75
|
'receipt_gateway_hold_ms',
|
|
74
76
|
]);
|
|
75
77
|
|
|
@@ -21,6 +21,8 @@ export const TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS = 1_000;
|
|
|
21
21
|
export const TOOL_EXECUTE_RETRY_DELAY_MAX_MS = 5_000;
|
|
22
22
|
export const TOOL_EXECUTE_BARE_RATE_LIMIT_BACKPRESSURE_MS = 60_000;
|
|
23
23
|
export const TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE = 'AUTH_SCOPE_CHANGED';
|
|
24
|
+
export const TOOL_EXECUTE_CUSTOMER_DB_STORAGE_UNAVAILABLE_CODE =
|
|
25
|
+
'CUSTOMER_DB_STORAGE_UNAVAILABLE';
|
|
24
26
|
/**
|
|
25
27
|
* A provider/action-local outcome contract emits this only when a provider has
|
|
26
28
|
* explicitly said that the same idempotency key is still executing. It is not
|
|
@@ -28,6 +30,9 @@ export const TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE = 'AUTH_SCOPE_CHANGED';
|
|
|
28
30
|
*/
|
|
29
31
|
export const TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_CODE =
|
|
30
32
|
'UPSTREAM_IDEMPOTENCY_IN_PROGRESS';
|
|
33
|
+
export const TOOL_EXECUTE_GATEWAY_INVOCATION_IN_PROGRESS_CODE =
|
|
34
|
+
'GATEWAY_INVOCATION_IN_PROGRESS';
|
|
35
|
+
export const TOOL_EXECUTE_GATEWAY_INVOCATION_IN_PROGRESS_MAX_ATTEMPTS = 65;
|
|
31
36
|
|
|
32
37
|
export class ToolExecuteAuthScopeChangedError extends Error {
|
|
33
38
|
readonly code = TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE;
|
|
@@ -45,6 +50,8 @@ export type ToolExecuteHttpRetryDecision = {
|
|
|
45
50
|
reason:
|
|
46
51
|
| 'rate_limit'
|
|
47
52
|
| 'idempotency_in_progress'
|
|
53
|
+
| 'gateway_invocation_in_progress'
|
|
54
|
+
| 'customer_db_storage_unavailable'
|
|
48
55
|
| 'retry_safe_transient_5xx'
|
|
49
56
|
| 'unsafe_transient_5xx'
|
|
50
57
|
| 'hard_billing_error'
|
|
@@ -94,6 +101,35 @@ function isIdempotencyInProgressResponse(input: {
|
|
|
94
101
|
);
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
function isGatewayInvocationInProgressResponse(input: {
|
|
105
|
+
status: number;
|
|
106
|
+
bodyText: string;
|
|
107
|
+
}): boolean {
|
|
108
|
+
return (
|
|
109
|
+
input.status === 409 &&
|
|
110
|
+
parseJsonObject(input.bodyText)?.code ===
|
|
111
|
+
TOOL_EXECUTE_GATEWAY_INVOCATION_IN_PROGRESS_CODE
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isCustomerDbStorageUnavailableResponse(input: {
|
|
116
|
+
toolId: string;
|
|
117
|
+
status: number;
|
|
118
|
+
bodyText: string;
|
|
119
|
+
}): boolean {
|
|
120
|
+
if (
|
|
121
|
+
input.status !== 503 ||
|
|
122
|
+
(input.toolId !== 'query_customer_db' &&
|
|
123
|
+
input.toolId !== 'customer_db_query_customer_db')
|
|
124
|
+
) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return (
|
|
128
|
+
parseJsonObject(input.bodyText)?.code ===
|
|
129
|
+
TOOL_EXECUTE_CUSTOMER_DB_STORAGE_UNAVAILABLE_CODE
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
97
133
|
function idempotencyInProgressRetryDelayMs(attempt: number): number {
|
|
98
134
|
return (
|
|
99
135
|
TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_RETRY_DELAYS_MS[
|
|
@@ -125,6 +161,8 @@ export function parseToolExecuteAuthScopeChangedError(input: {
|
|
|
125
161
|
function decideToolExecuteHttpRetry(input: {
|
|
126
162
|
status: number;
|
|
127
163
|
idempotencyInProgress?: boolean;
|
|
164
|
+
gatewayInvocationInProgress?: boolean;
|
|
165
|
+
customerDbStorageUnavailable?: boolean;
|
|
128
166
|
hardBillingFailure?: boolean;
|
|
129
167
|
hasRetryAfterHeader?: boolean;
|
|
130
168
|
transientHttpRetrySafe?: boolean;
|
|
@@ -151,6 +189,13 @@ function decideToolExecuteHttpRetry(input: {
|
|
|
151
189
|
reason: 'rate_limit',
|
|
152
190
|
};
|
|
153
191
|
}
|
|
192
|
+
if (input.gatewayInvocationInProgress) {
|
|
193
|
+
return {
|
|
194
|
+
retryable: true,
|
|
195
|
+
attemptCap: TOOL_EXECUTE_GATEWAY_INVOCATION_IN_PROGRESS_MAX_ATTEMPTS,
|
|
196
|
+
reason: 'gateway_invocation_in_progress',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
154
199
|
if (input.idempotencyInProgress) {
|
|
155
200
|
return {
|
|
156
201
|
retryable: true,
|
|
@@ -158,6 +203,16 @@ function decideToolExecuteHttpRetry(input: {
|
|
|
158
203
|
reason: 'idempotency_in_progress',
|
|
159
204
|
};
|
|
160
205
|
}
|
|
206
|
+
// This typed response is emitted for a PostgreSQL permission denial. The
|
|
207
|
+
// failed statement cannot commit, so it is safe to retry even when the SQL
|
|
208
|
+
// itself is a mutation. Do not generalize this to arbitrary 503 responses.
|
|
209
|
+
if (input.customerDbStorageUnavailable) {
|
|
210
|
+
return {
|
|
211
|
+
retryable: true,
|
|
212
|
+
attemptCap: TOOL_EXECUTE_TRANSIENT_HTTP_MAX_ATTEMPTS,
|
|
213
|
+
reason: 'customer_db_storage_unavailable',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
161
216
|
if (input.status >= 500 && input.status < 600) {
|
|
162
217
|
if (!input.transientHttpRetrySafe) {
|
|
163
218
|
return {
|
|
@@ -186,6 +241,8 @@ export function createToolExecuteHttpFailureAttemptTracker(): ToolExecuteHttpFai
|
|
|
186
241
|
> = {
|
|
187
242
|
rate_limit: 0,
|
|
188
243
|
idempotency_in_progress: 0,
|
|
244
|
+
gateway_invocation_in_progress: 0,
|
|
245
|
+
customer_db_storage_unavailable: 0,
|
|
189
246
|
retry_safe_transient_5xx: 0,
|
|
190
247
|
unsafe_transient_5xx: 0,
|
|
191
248
|
hard_billing_error: 0,
|
|
@@ -200,6 +257,15 @@ export function createToolExecuteHttpFailureAttemptTracker(): ToolExecuteHttpFai
|
|
|
200
257
|
status: input.status,
|
|
201
258
|
bodyText: input.bodyText ?? '',
|
|
202
259
|
}),
|
|
260
|
+
gatewayInvocationInProgress: isGatewayInvocationInProgressResponse({
|
|
261
|
+
status: input.status,
|
|
262
|
+
bodyText: input.bodyText ?? '',
|
|
263
|
+
}),
|
|
264
|
+
customerDbStorageUnavailable: isCustomerDbStorageUnavailableResponse({
|
|
265
|
+
toolId: input.toolId,
|
|
266
|
+
status: input.status,
|
|
267
|
+
bodyText: input.bodyText ?? '',
|
|
268
|
+
}),
|
|
203
269
|
hasRetryAfterHeader: true,
|
|
204
270
|
transientHttpRetrySafe: input.transientHttpRetrySafe === true,
|
|
205
271
|
});
|
|
@@ -240,9 +306,15 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
240
306
|
typeof input.retryAfterHeader === 'string' &&
|
|
241
307
|
input.retryAfterHeader.trim().length > 0;
|
|
242
308
|
const idempotencyInProgress = isIdempotencyInProgressResponse(input);
|
|
309
|
+
const gatewayInvocationInProgress =
|
|
310
|
+
isGatewayInvocationInProgressResponse(input);
|
|
311
|
+
const customerDbStorageUnavailable =
|
|
312
|
+
isCustomerDbStorageUnavailableResponse(input);
|
|
243
313
|
const initialRetryDecision = decideToolExecuteHttpRetry({
|
|
244
314
|
status: input.status,
|
|
245
315
|
idempotencyInProgress,
|
|
316
|
+
gatewayInvocationInProgress,
|
|
317
|
+
customerDbStorageUnavailable,
|
|
246
318
|
hasRetryAfterHeader,
|
|
247
319
|
transientHttpRetrySafe,
|
|
248
320
|
});
|
|
@@ -264,6 +336,8 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
264
336
|
const retryDecision = decideToolExecuteHttpRetry({
|
|
265
337
|
status: input.status,
|
|
266
338
|
idempotencyInProgress,
|
|
339
|
+
gatewayInvocationInProgress,
|
|
340
|
+
customerDbStorageUnavailable,
|
|
267
341
|
hardBillingFailure,
|
|
268
342
|
hasRetryAfterHeader,
|
|
269
343
|
transientHttpRetrySafe,
|
|
@@ -295,6 +369,9 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
295
369
|
),
|
|
296
370
|
)
|
|
297
371
|
: retryDecision.reason === 'idempotency_in_progress' &&
|
|
372
|
+
!hasRetryAfterHeader
|
|
373
|
+
? idempotencyInProgressRetryDelayMs(input.attempt)
|
|
374
|
+
: retryDecision.reason === 'gateway_invocation_in_progress' &&
|
|
298
375
|
!hasRetryAfterHeader
|
|
299
376
|
? idempotencyInProgressRetryDelayMs(input.attempt)
|
|
300
377
|
: retryAfterMs > 0
|
|
@@ -317,6 +394,8 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
317
394
|
? retryAfterMs
|
|
318
395
|
: TOOL_EXECUTE_BARE_RATE_LIMIT_BACKPRESSURE_MS
|
|
319
396
|
: null,
|
|
320
|
-
chargeRetryBudget:
|
|
397
|
+
chargeRetryBudget:
|
|
398
|
+
shouldRetry &&
|
|
399
|
+
retryDecision.reason !== 'gateway_invocation_in_progress',
|
|
321
400
|
};
|
|
322
401
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -718,7 +718,7 @@ var SDK_RELEASE = {
|
|
|
718
718
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
719
719
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
720
720
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
721
|
-
version: "0.1.
|
|
721
|
+
version: "0.1.288",
|
|
722
722
|
contracts: {
|
|
723
723
|
api: {
|
|
724
724
|
name: "sdk-http-api",
|
package/dist/cli/index.mjs
CHANGED
|
@@ -703,7 +703,7 @@ var SDK_RELEASE = {
|
|
|
703
703
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
704
704
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
705
705
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
706
|
-
version: "0.1.
|
|
706
|
+
version: "0.1.288",
|
|
707
707
|
contracts: {
|
|
708
708
|
api: {
|
|
709
709
|
name: "sdk-http-api",
|
package/dist/index.js
CHANGED
|
@@ -438,7 +438,7 @@ var SDK_RELEASE = {
|
|
|
438
438
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
439
439
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
440
440
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
441
|
-
version: "0.1.
|
|
441
|
+
version: "0.1.288",
|
|
442
442
|
contracts: {
|
|
443
443
|
api: {
|
|
444
444
|
name: "sdk-http-api",
|
package/dist/index.mjs
CHANGED
|
@@ -367,7 +367,7 @@ var SDK_RELEASE = {
|
|
|
367
367
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
368
368
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
369
369
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
370
|
-
version: "0.1.
|
|
370
|
+
version: "0.1.288",
|
|
371
371
|
contracts: {
|
|
372
372
|
api: {
|
|
373
373
|
name: "sdk-http-api",
|