deepline 0.3.201 → 0.3.203
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 +62 -1
- package/dist/bundling-sources/sdk/src/http.ts +55 -6
- package/dist/bundling-sources/sdk/src/index.ts +1 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
- package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
- package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +238 -204
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +136 -25
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
- package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +1 -1
- package/dist/cli/index.js +362 -55
- package/dist/cli/index.mjs +365 -57
- package/dist/index.d.mts +129 -1
- package/dist/index.d.ts +129 -1
- package/dist/index.js +64 -7
- package/dist/index.mjs +64 -7
- package/dist/install-integrity.json +3 -0
- 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
|
@@ -125,6 +125,11 @@ import {
|
|
|
125
125
|
ProviderExhaustedError,
|
|
126
126
|
} from './run-failure';
|
|
127
127
|
import { PROVIDER_EXHAUSTED_MAX_WAIT_MS } from './governor/app-runtime-rate-state-backend';
|
|
128
|
+
import {
|
|
129
|
+
observeProviderNoProgress,
|
|
130
|
+
providerNoProgressExpired,
|
|
131
|
+
PROVIDER_NO_PROGRESS_MAX_MS,
|
|
132
|
+
} from './provider-no-progress';
|
|
128
133
|
import {
|
|
129
134
|
buildPlayContractCompatibility,
|
|
130
135
|
normalizePlayContractCompatibility,
|
|
@@ -174,6 +179,7 @@ import {
|
|
|
174
179
|
createToolCallReceiptCohortJob,
|
|
175
180
|
createRuntimeStepToolResultReceipts,
|
|
176
181
|
createToolCallJob,
|
|
182
|
+
resolveToolCallResultReceiptPolicy,
|
|
177
183
|
ToolCallReceiptLeaseLostError,
|
|
178
184
|
type ToolCallReceiptCohortStore,
|
|
179
185
|
type ToolCallReceiptCohortTimings,
|
|
@@ -197,6 +203,7 @@ import {
|
|
|
197
203
|
classifyToolExecuteHttpFailure,
|
|
198
204
|
createToolExecuteHttpFailureAttemptTracker,
|
|
199
205
|
parseToolExecuteRetryAfterMs,
|
|
206
|
+
parseToolExecuteRetryAtMs,
|
|
200
207
|
parseToolExecuteAuthScopeChangedError,
|
|
201
208
|
ToolExecuteAuthScopeChangedError,
|
|
202
209
|
type ToolExecuteHttpFailureOutcome,
|
|
@@ -494,6 +501,8 @@ const rowContext = new AsyncLocalStorage<{
|
|
|
494
501
|
* null between segments. Measurement only; see `RowResolverGapTracker`.
|
|
495
502
|
*/
|
|
496
503
|
resolverGap?: RowResolverGapTracker;
|
|
504
|
+
/** Shared through nested fields, reset for each row execution attempt. */
|
|
505
|
+
providerBackpressure?: { pending: PlayRowProviderBackpressureError | null };
|
|
497
506
|
}>();
|
|
498
507
|
|
|
499
508
|
/**
|
|
@@ -562,6 +571,7 @@ const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
|
|
|
562
571
|
// broken gateway-to-runner connection, not slow provider work.
|
|
563
572
|
export const TOOL_EXECUTE_RESPONSE_BODY_TIMEOUT_MS = 30_000;
|
|
564
573
|
const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
574
|
+
const TOOL_PROVIDER_RESIDENT_WAIT_BUDGET_MS = 5_000;
|
|
565
575
|
const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
|
|
566
576
|
// Every runtime-API tool fetch needs a client-side deadline. Without one, a
|
|
567
577
|
// stalled execute request (e.g. a credential-less call whose gateway connection
|
|
@@ -3104,15 +3114,23 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
3104
3114
|
/**
|
|
3105
3115
|
* Called at ctx effect boundaries and by the runner before it can settle a
|
|
3106
3116
|
* normal result. This makes suspension sticky even when authored code uses a
|
|
3107
|
-
* broad catch around
|
|
3117
|
+
* broad catch around a sleep or provider hold. A row hold stays row-local
|
|
3118
|
+
* until its siblings drain and the map commits the scheduler handoff.
|
|
3108
3119
|
*/
|
|
3109
3120
|
assertNoPendingDurableSuspension(): void {
|
|
3121
|
+
this.#options.executionSignal?.throwIfAborted();
|
|
3110
3122
|
this.assertNoSlackHitlTimeoutFailure();
|
|
3123
|
+
this.assertNoRowProviderBackpressure();
|
|
3111
3124
|
if (this.pendingDurableSuspension) {
|
|
3112
3125
|
throw new PlayExecutionSuspendedError(this.pendingDurableSuspension);
|
|
3113
3126
|
}
|
|
3114
3127
|
}
|
|
3115
3128
|
|
|
3129
|
+
private assertNoRowProviderBackpressure(): void {
|
|
3130
|
+
const pending = rowContext.getStore()?.providerBackpressure?.pending;
|
|
3131
|
+
if (pending) throw pending;
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3116
3134
|
/**
|
|
3117
3135
|
* The single seam that hands this invocation back to the scheduler. The
|
|
3118
3136
|
* first suspension wins: a later boundary reached by code that caught the
|
|
@@ -6795,7 +6813,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6795
6813
|
execution: {
|
|
6796
6814
|
mode: 'map',
|
|
6797
6815
|
ownerRunId: this.currentReceiptOwnerRunId,
|
|
6798
|
-
force: input.
|
|
6816
|
+
force: input.resultReceipts
|
|
6817
|
+
? this.mappedToolResultReceiptPolicy(input.request).force
|
|
6818
|
+
: input.request.force === true,
|
|
6799
6819
|
},
|
|
6800
6820
|
};
|
|
6801
6821
|
if (alwaysFresh) {
|
|
@@ -6829,8 +6849,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6829
6849
|
}
|
|
6830
6850
|
|
|
6831
6851
|
/**
|
|
6832
|
-
* A forced call bypasses
|
|
6833
|
-
* gateway invocation must bypass that
|
|
6852
|
+
* A forced call bypasses an earlier run's shared result, so its physical
|
|
6853
|
+
* gateway invocation must bypass that result's replay identity too. Keep
|
|
6834
6854
|
* the transport key deterministic for one logical call: a retry of that
|
|
6835
6855
|
* call still converges, while a later explicit force gets a fresh identity.
|
|
6836
6856
|
*/
|
|
@@ -6847,6 +6867,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
6847
6867
|
})}${stableDigest(`forced-provider-invocation:${input.providerOperationKey}`)}`;
|
|
6848
6868
|
}
|
|
6849
6869
|
|
|
6870
|
+
private mappedToolResultReceiptPolicy(request: ToolCallRequest) {
|
|
6871
|
+
if (!request.receiptKey || !request.logicalCallId) {
|
|
6872
|
+
throw new Error(
|
|
6873
|
+
'Mapped Tool Call needs stable result and call identities.',
|
|
6874
|
+
);
|
|
6875
|
+
}
|
|
6876
|
+
return resolveToolCallResultReceiptPolicy({
|
|
6877
|
+
orgId: this.#options.orgId,
|
|
6878
|
+
toolId: request.toolId,
|
|
6879
|
+
resultReceiptKey: request.receiptKey,
|
|
6880
|
+
runId: this.currentRunId,
|
|
6881
|
+
logicalCallId: request.logicalCallId,
|
|
6882
|
+
force: request.force === true,
|
|
6883
|
+
});
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6850
6886
|
/**
|
|
6851
6887
|
* Ordinary operations do not carry provider-idempotency metadata, but the
|
|
6852
6888
|
* execution relay still needs a semantic signal that an explicit force is
|
|
@@ -10733,6 +10769,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10733
10769
|
const rowKey = executionRowKey(baseRow, idx);
|
|
10734
10770
|
const rowIndex = executionRowIndex(idx);
|
|
10735
10771
|
const computedFields: Record<string, unknown> = {};
|
|
10772
|
+
// A provider hold is scheduler control, not a catchable row outcome.
|
|
10773
|
+
// Keep its intent outside authored code so catch/fallback logic cannot
|
|
10774
|
+
// publish a successful row that silently omitted an unfinished call.
|
|
10775
|
+
const providerBackpressure = {
|
|
10776
|
+
pending: null as PlayRowProviderBackpressureError | null,
|
|
10777
|
+
};
|
|
10736
10778
|
let activeFieldName: string | null = null;
|
|
10737
10779
|
// ADR 0019: the row's read order, computed once. Every cell reads a
|
|
10738
10780
|
// prefix of it — the inputs, then each column this run wrote — so this
|
|
@@ -10793,6 +10835,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
10793
10835
|
mapStallObserver: mapStallObserver ?? undefined,
|
|
10794
10836
|
mapStallResolverToken: resolverObservation?.token,
|
|
10795
10837
|
resolverGap,
|
|
10838
|
+
providerBackpressure,
|
|
10796
10839
|
},
|
|
10797
10840
|
async () => {
|
|
10798
10841
|
try {
|
|
@@ -12675,7 +12718,18 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
12675
12718
|
return resolver;
|
|
12676
12719
|
}
|
|
12677
12720
|
|
|
12678
|
-
|
|
12721
|
+
let value: unknown;
|
|
12722
|
+
try {
|
|
12723
|
+
value = await resolver(row, this, fields, index, previousCell);
|
|
12724
|
+
} catch (error) {
|
|
12725
|
+
// Cancellation and persistence/ownership failures stay run-fatal. A
|
|
12726
|
+
// provider wait must never convert them into another scheduled retry.
|
|
12727
|
+
if (isRowIsolationExemptError(error)) throw error;
|
|
12728
|
+
// Also retain the wait when authored fallback code throws a different
|
|
12729
|
+
// error after catching it. This check is local to the held row.
|
|
12730
|
+
this.assertNoRowProviderBackpressure();
|
|
12731
|
+
throw error;
|
|
12732
|
+
}
|
|
12679
12733
|
// This is the common user-code return seam for every mapped field path
|
|
12680
12734
|
// (incremental, pure, and step-program adapters). Check before callers
|
|
12681
12735
|
// serialize a cell, emit a completion event, or advance a map frame.
|
|
@@ -12772,6 +12826,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
12772
12826
|
options?: ToolCallOptions,
|
|
12773
12827
|
): Promise<unknown> {
|
|
12774
12828
|
if (!rowContext.getStore()) this.assertRowAdmissionOpen();
|
|
12829
|
+
this.assertNoPendingDurableSuspension();
|
|
12775
12830
|
if (options?.description !== undefined) {
|
|
12776
12831
|
validatePlayAuthoringField(
|
|
12777
12832
|
'ctx.tools.execute.description',
|
|
@@ -13184,6 +13239,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13184
13239
|
toolId,
|
|
13185
13240
|
})}${stableDigest(`${this.currentRunId}:${logicalCallId}:always-fresh`)}`;
|
|
13186
13241
|
const resultReceiptKey = directResultReceiptKey;
|
|
13242
|
+
const resultReceiptPolicy = resolveToolCallResultReceiptPolicy({
|
|
13243
|
+
orgId: this.#options.orgId,
|
|
13244
|
+
toolId,
|
|
13245
|
+
resultReceiptKey,
|
|
13246
|
+
runId: this.currentRunId,
|
|
13247
|
+
logicalCallId,
|
|
13248
|
+
force: toolCachePolicy.force,
|
|
13249
|
+
canStoreResult: Boolean(
|
|
13250
|
+
this.#options.claimRuntimeStepReceipt &&
|
|
13251
|
+
(this.#options.getRuntimeStepReceipt ||
|
|
13252
|
+
this.#options.getRuntimeStepReceipts) &&
|
|
13253
|
+
(this.#options.completeRuntimeStepReceipt ||
|
|
13254
|
+
this.#options.completeRuntimeStepReceipts),
|
|
13255
|
+
),
|
|
13256
|
+
});
|
|
13187
13257
|
const toolRetryPolicy = await this.#options
|
|
13188
13258
|
.getToolRetryPolicy?.(toolId, input)
|
|
13189
13259
|
.catch(() => null);
|
|
@@ -13337,7 +13407,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13337
13407
|
},
|
|
13338
13408
|
}
|
|
13339
13409
|
: {}),
|
|
13340
|
-
recover: ({ result,
|
|
13410
|
+
recover: ({ result, source }) =>
|
|
13341
13411
|
cloneToolExecuteResultWithExecution(
|
|
13342
13412
|
result,
|
|
13343
13413
|
toolExecutionMetadataForOutcome(
|
|
@@ -13372,14 +13442,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13372
13442
|
execution: {
|
|
13373
13443
|
mode: 'direct',
|
|
13374
13444
|
ownerRunId: this.currentReceiptOwnerRunId,
|
|
13375
|
-
force:
|
|
13445
|
+
force: resultReceiptPolicy.force,
|
|
13376
13446
|
},
|
|
13377
13447
|
};
|
|
13378
13448
|
const wrapped = toolCall
|
|
13379
13449
|
? await toolCall.call({
|
|
13380
13450
|
...call,
|
|
13381
13451
|
compatibility: {
|
|
13382
|
-
resultReceiptKey,
|
|
13452
|
+
resultReceiptKey: resultReceiptPolicy.resultReceiptKey,
|
|
13383
13453
|
providerOperationKey,
|
|
13384
13454
|
},
|
|
13385
13455
|
})
|
|
@@ -13593,7 +13663,20 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13593
13663
|
};
|
|
13594
13664
|
|
|
13595
13665
|
if (store || !cacheableToolResult) {
|
|
13596
|
-
|
|
13666
|
+
try {
|
|
13667
|
+
return await executeTool();
|
|
13668
|
+
} catch (error) {
|
|
13669
|
+
if (
|
|
13670
|
+
error instanceof PlayRowProviderBackpressureError &&
|
|
13671
|
+
store?.providerBackpressure
|
|
13672
|
+
) {
|
|
13673
|
+
// The batch dispatcher has no reliable single-row async scope.
|
|
13674
|
+
// Capture the signal at delivery to its actual requesting row,
|
|
13675
|
+
// before authored code can catch the rejected tool promise.
|
|
13676
|
+
store.providerBackpressure.pending ??= error;
|
|
13677
|
+
}
|
|
13678
|
+
throw error;
|
|
13679
|
+
}
|
|
13597
13680
|
}
|
|
13598
13681
|
|
|
13599
13682
|
for (
|
|
@@ -14554,6 +14637,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
14554
14637
|
|
|
14555
14638
|
async sleep(ms: number): Promise<void> {
|
|
14556
14639
|
if (!rowContext.getStore()) this.assertRowAdmissionOpen();
|
|
14640
|
+
this.assertNoPendingDurableSuspension();
|
|
14557
14641
|
this.assertInlineChildContract('suspending_child');
|
|
14558
14642
|
const delayMs =
|
|
14559
14643
|
this.currentRuntimeSemantics.validation === 'strict'
|
|
@@ -15555,7 +15639,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
15555
15639
|
const request = input.requestForReceipt.get(receiptKey);
|
|
15556
15640
|
const candidates = [
|
|
15557
15641
|
receiptKey,
|
|
15558
|
-
...(request?.
|
|
15642
|
+
...(request?.force === true
|
|
15643
|
+
? []
|
|
15644
|
+
: (request?.legacyReceiptKeys ?? [])),
|
|
15559
15645
|
];
|
|
15560
15646
|
for (const candidate of candidates) {
|
|
15561
15647
|
const record = await asRecord(
|
|
@@ -15577,8 +15663,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
15577
15663
|
...new Set(
|
|
15578
15664
|
resultReceiptKeys.flatMap((receiptKey) => [
|
|
15579
15665
|
receiptKey,
|
|
15580
|
-
...(input.requestForReceipt.get(receiptKey)
|
|
15581
|
-
|
|
15666
|
+
...(input.requestForReceipt.get(receiptKey)?.force === true
|
|
15667
|
+
? []
|
|
15668
|
+
: (input.requestForReceipt.get(receiptKey)
|
|
15669
|
+
?.legacyReceiptKeys ?? [])),
|
|
15582
15670
|
]),
|
|
15583
15671
|
),
|
|
15584
15672
|
];
|
|
@@ -15777,12 +15865,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
15777
15865
|
const requestsForReceipt = new Map<string, ToolCallRequest[]>();
|
|
15778
15866
|
const requestByCallId = new Map<string, ToolCallRequest>();
|
|
15779
15867
|
for (const request of requests) {
|
|
15780
|
-
const receiptKey =
|
|
15781
|
-
|
|
15782
|
-
throw new Error(
|
|
15783
|
-
'Cacheable non-native map Tool Call cohort requires receipt keys.',
|
|
15784
|
-
);
|
|
15785
|
-
}
|
|
15868
|
+
const receiptKey =
|
|
15869
|
+
this.mappedToolResultReceiptPolicy(request).resultReceiptKey;
|
|
15786
15870
|
requestForReceipt.set(receiptKey, request);
|
|
15787
15871
|
const shared = requestsForReceipt.get(receiptKey);
|
|
15788
15872
|
if (shared) shared.push(request);
|
|
@@ -15806,8 +15890,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
15806
15890
|
},
|
|
15807
15891
|
members: requests.map((request) => ({
|
|
15808
15892
|
memberId: request.callId,
|
|
15809
|
-
|
|
15810
|
-
force: request.force === true,
|
|
15893
|
+
...this.mappedToolResultReceiptPolicy(request),
|
|
15811
15894
|
forceFailedRefresh: request.forceFailedRefresh === true,
|
|
15812
15895
|
waitMaxAttempts: resolveRuntimeToolReceiptWaitMaxAttempts(
|
|
15813
15896
|
typeof request.receiptWaitMs === 'number'
|
|
@@ -15990,7 +16073,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
15990
16073
|
request,
|
|
15991
16074
|
dispatchToolId: toolId,
|
|
15992
16075
|
dispatchPayload: request.input,
|
|
15993
|
-
resultReceiptKey:
|
|
16076
|
+
resultReceiptKey:
|
|
16077
|
+
this.mappedToolResultReceiptPolicy(request).resultReceiptKey,
|
|
15994
16078
|
providerOperationKey,
|
|
15995
16079
|
resultReceipts: receipts,
|
|
15996
16080
|
apiOptions: {
|
|
@@ -16052,12 +16136,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
16052
16136
|
const requestsForReceipt = new Map<string, ToolCallRequest[]>();
|
|
16053
16137
|
const requestByCallId = new Map<string, ToolCallRequest>();
|
|
16054
16138
|
for (const request of requests) {
|
|
16055
|
-
const receiptKey =
|
|
16056
|
-
|
|
16057
|
-
throw new Error(
|
|
16058
|
-
'Cacheable native map Tool Call cohort requires receipt keys.',
|
|
16059
|
-
);
|
|
16060
|
-
}
|
|
16139
|
+
const receiptKey =
|
|
16140
|
+
this.mappedToolResultReceiptPolicy(request).resultReceiptKey;
|
|
16061
16141
|
requestForReceipt.set(receiptKey, request);
|
|
16062
16142
|
const shared = requestsForReceipt.get(receiptKey);
|
|
16063
16143
|
if (shared) shared.push(request);
|
|
@@ -16101,12 +16181,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
16101
16181
|
const successful: ToolCallRequest[] = [];
|
|
16102
16182
|
|
|
16103
16183
|
const deliver = (request: ToolCallRequest, result: ToolExecuteResult) => {
|
|
16184
|
+
const presented =
|
|
16185
|
+
request.force === true
|
|
16186
|
+
? cloneToolExecuteResultWithExecution(result, {
|
|
16187
|
+
...result._metadata.execution,
|
|
16188
|
+
receiptKey: request.receiptKey!,
|
|
16189
|
+
...(result._metadata.execution.attachedToReceiptKey
|
|
16190
|
+
? { attachedToReceiptKey: request.receiptKey! }
|
|
16191
|
+
: {}),
|
|
16192
|
+
})
|
|
16193
|
+
: result;
|
|
16104
16194
|
if (request.cacheable !== false) {
|
|
16105
|
-
this.cacheToolResult(toolId, request.cacheKey,
|
|
16195
|
+
this.cacheToolResult(toolId, request.cacheKey, presented);
|
|
16106
16196
|
}
|
|
16107
16197
|
const resolver = this.toolCallResolvers.get(request.callId);
|
|
16108
16198
|
if (resolver) {
|
|
16109
|
-
resolver.resolve(
|
|
16199
|
+
resolver.resolve(presented);
|
|
16110
16200
|
this.toolCallResolvers.delete(request.callId);
|
|
16111
16201
|
}
|
|
16112
16202
|
this.emitScopedFieldMetaUpdate({
|
|
@@ -16115,12 +16205,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
16115
16205
|
tableNamespace: request.tableNamespace ?? null,
|
|
16116
16206
|
fieldName: request.fieldName,
|
|
16117
16207
|
status:
|
|
16118
|
-
|
|
16208
|
+
presented._metadata.execution.cached === true ? 'cached' : 'running',
|
|
16119
16209
|
rowStatus: 'running',
|
|
16120
16210
|
stage: toolId,
|
|
16121
16211
|
provider: null,
|
|
16122
16212
|
error: null,
|
|
16123
|
-
...(
|
|
16213
|
+
...(presented._metadata.execution.cached === true
|
|
16214
|
+
? { reused: true }
|
|
16215
|
+
: {}),
|
|
16124
16216
|
dataPatch: {},
|
|
16125
16217
|
});
|
|
16126
16218
|
successful.push(request);
|
|
@@ -16309,10 +16401,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
16309
16401
|
execution: {
|
|
16310
16402
|
mode: 'map',
|
|
16311
16403
|
ownerRunId: this.currentReceiptOwnerRunId,
|
|
16312
|
-
force: request.force
|
|
16404
|
+
force: this.mappedToolResultReceiptPolicy(request).force,
|
|
16313
16405
|
},
|
|
16314
16406
|
compatibility: {
|
|
16315
|
-
resultReceiptKey:
|
|
16407
|
+
resultReceiptKey:
|
|
16408
|
+
this.mappedToolResultReceiptPolicy(request)
|
|
16409
|
+
.resultReceiptKey,
|
|
16316
16410
|
providerOperationKey: request.receiptKey!,
|
|
16317
16411
|
},
|
|
16318
16412
|
},
|
|
@@ -16328,7 +16422,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
16328
16422
|
compiled.batchOperation,
|
|
16329
16423
|
)
|
|
16330
16424
|
)?.trim() ?? '';
|
|
16331
|
-
const receiptKeys = owned.map(
|
|
16425
|
+
const receiptKeys = owned.map(
|
|
16426
|
+
(request) =>
|
|
16427
|
+
this.mappedToolResultReceiptPolicy(request).resultReceiptKey,
|
|
16428
|
+
);
|
|
16332
16429
|
const aggregateReceiptKey = buildDurableToolAggregateReceiptKey({
|
|
16333
16430
|
receiptKeys,
|
|
16334
16431
|
prefix: 'batch',
|
|
@@ -16501,14 +16598,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
16501
16598
|
// before they are delivered to rows.
|
|
16502
16599
|
const durable = await store.read({
|
|
16503
16600
|
resultReceiptKeys: entry.request.memberRequests.map(
|
|
16504
|
-
(request) =>
|
|
16601
|
+
(request) =>
|
|
16602
|
+
this.mappedToolResultReceiptPolicy(request).resultReceiptKey,
|
|
16505
16603
|
),
|
|
16506
16604
|
});
|
|
16507
16605
|
const byReceipt = new Map(
|
|
16508
16606
|
durable.map((record) => [record.resultReceiptKey, record]),
|
|
16509
16607
|
);
|
|
16510
16608
|
for (const request of entry.request.memberRequests) {
|
|
16511
|
-
const record = byReceipt.get(
|
|
16609
|
+
const record = byReceipt.get(
|
|
16610
|
+
this.mappedToolResultReceiptPolicy(request).resultReceiptKey,
|
|
16611
|
+
);
|
|
16512
16612
|
if (
|
|
16513
16613
|
record &&
|
|
16514
16614
|
(record.status === 'completed' ||
|
|
@@ -18557,6 +18657,52 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18557
18657
|
const executionBaseUrl =
|
|
18558
18658
|
this.#options.executionGatewayBaseUrl?.trim() || this.#options.baseUrl;
|
|
18559
18659
|
const toolErrorSchemaVersion = this.currentToolErrorSchemaVersion;
|
|
18660
|
+
const providerNoProgressScope = stableDigest(
|
|
18661
|
+
`${provider}\u0000${executionAuthScopeDigest ?? ''}`,
|
|
18662
|
+
);
|
|
18663
|
+
const providerNoProgressError = (retryAtMs: number) =>
|
|
18664
|
+
createToolHttpError(
|
|
18665
|
+
toolErrorSchemaVersion,
|
|
18666
|
+
`Provider ${provider} made no progress within ${PROVIDER_NO_PROGRESS_MAX_MS / 60_000} minutes. Completed work was saved; retry the failed work after the provider recovers.`,
|
|
18667
|
+
null,
|
|
18668
|
+
429,
|
|
18669
|
+
'repairable',
|
|
18670
|
+
{
|
|
18671
|
+
toolId,
|
|
18672
|
+
provider,
|
|
18673
|
+
operation: toolId,
|
|
18674
|
+
code: 'PROVIDER_EXHAUSTED',
|
|
18675
|
+
origin: 'provider',
|
|
18676
|
+
category: 'rate_limit',
|
|
18677
|
+
retryable: true,
|
|
18678
|
+
statusCode: 429,
|
|
18679
|
+
requestId: null,
|
|
18680
|
+
retryAfterMs: Math.max(0, retryAtMs - Date.now()),
|
|
18681
|
+
networkKind: null,
|
|
18682
|
+
networkScope: null,
|
|
18683
|
+
},
|
|
18684
|
+
);
|
|
18685
|
+
const observeProviderWait = (retryAtMs: number) => {
|
|
18686
|
+
const startedAtMsByScope =
|
|
18687
|
+
(this.checkpoint.providerNoProgressStartedAtMsByScope ??= {});
|
|
18688
|
+
const outcome = observeProviderNoProgress({
|
|
18689
|
+
startedAtMsByScope,
|
|
18690
|
+
scope: providerNoProgressScope,
|
|
18691
|
+
retryAtMs,
|
|
18692
|
+
nowMs: Date.now(),
|
|
18693
|
+
});
|
|
18694
|
+
if (!outcome.exhausted) return;
|
|
18695
|
+
this.runtimeLog(
|
|
18696
|
+
`[runtime.provider.no_progress] ${JSON.stringify({
|
|
18697
|
+
provider,
|
|
18698
|
+
started_at: new Date(outcome.startedAtMs).toISOString(),
|
|
18699
|
+
deadline_at: new Date(outcome.deadlineMs).toISOString(),
|
|
18700
|
+
retry_at: new Date(retryAtMs).toISOString(),
|
|
18701
|
+
})}`,
|
|
18702
|
+
{ level: 'warn' },
|
|
18703
|
+
);
|
|
18704
|
+
throw providerNoProgressError(retryAtMs);
|
|
18705
|
+
};
|
|
18560
18706
|
const timeoutMs = resolveToolRuntimeTimeoutMs(
|
|
18561
18707
|
toolId,
|
|
18562
18708
|
options?.timeoutMs,
|
|
@@ -18594,6 +18740,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18594
18740
|
const toolInputSnapshotMemory: {
|
|
18595
18741
|
current: RuntimeMemoryLedgerHandle | null;
|
|
18596
18742
|
} = { current: null };
|
|
18743
|
+
let preserveProviderRetryCursorForSuspension = false;
|
|
18744
|
+
let clearProviderRetryCursor = () => {};
|
|
18597
18745
|
try {
|
|
18598
18746
|
return await withActiveSpan(
|
|
18599
18747
|
'plays.tool.execute',
|
|
@@ -18703,11 +18851,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18703
18851
|
`transport:${this.currentRunId}:${toolId}:${crypto.randomUUID()}`,
|
|
18704
18852
|
)}`
|
|
18705
18853
|
: null);
|
|
18854
|
+
// A durable row replay reconstructs this function from scratch. Keep
|
|
18855
|
+
// the gateway's invocation sequence in the checkpoint, keyed by the
|
|
18856
|
+
// stable result receipt identity, until this logical call completes.
|
|
18857
|
+
const providerRetryCursorKey =
|
|
18858
|
+
this.#options.durableBoundaries && durableCallReceiptKey
|
|
18859
|
+
? stableDigest(`provider-retry:${durableCallReceiptKey}`)
|
|
18860
|
+
: null;
|
|
18861
|
+
const savedProviderRetryCursor = providerRetryCursorKey
|
|
18862
|
+
? this.checkpoint.providerRetryCursors?.[providerRetryCursorKey]
|
|
18863
|
+
: undefined;
|
|
18706
18864
|
const executionRequestNonce = crypto.randomUUID();
|
|
18707
|
-
//
|
|
18708
|
-
//
|
|
18709
|
-
//
|
|
18710
|
-
|
|
18865
|
+
// Transport ambiguity must retain the billing identity. An ordinary
|
|
18866
|
+
// (unfenced) call advances it only after a definite provider-owned
|
|
18867
|
+
// 429 released that attempt's reservation. Fenced calls retain it:
|
|
18868
|
+
// the gateway-qualified retry owns their distinct charge lifecycle.
|
|
18869
|
+
let deeplineRequestId = `ctx-tool-${stableDigest(executionRequestNonce).slice(0, 32)}`;
|
|
18711
18870
|
// The provider operation has a semantic identity independent of
|
|
18712
18871
|
// the response receipt. Its key is supplied by the caller that owns
|
|
18713
18872
|
// that compatibility contract; never substitute request/billing
|
|
@@ -18758,7 +18917,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18758
18917
|
// *durable* provider attempt. The execution gateway validates and
|
|
18759
18918
|
// attests it before a retried call can use its fresh charge
|
|
18760
18919
|
// lifecycle key; it is not a general retry counter.
|
|
18761
|
-
let providerRateLimitRetryFromInvocationAttempt: number | null =
|
|
18920
|
+
let providerRateLimitRetryFromInvocationAttempt: number | null =
|
|
18921
|
+
savedProviderRetryCursor?.retryFromInvocationAttempt ?? null;
|
|
18762
18922
|
const serializedRequestBody = (invocationAttempt: number) =>
|
|
18763
18923
|
JSON.stringify({
|
|
18764
18924
|
payload: shouldResolveAsyncOperationInRuntime
|
|
@@ -18859,8 +19019,89 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18859
19019
|
: {}),
|
|
18860
19020
|
});
|
|
18861
19021
|
let transportAttempt = 0;
|
|
18862
|
-
let invocationAttempt =
|
|
19022
|
+
let invocationAttempt =
|
|
19023
|
+
savedProviderRetryCursor?.invocationAttempt ?? 0;
|
|
19024
|
+
clearProviderRetryCursor = () => {
|
|
19025
|
+
if (
|
|
19026
|
+
!providerRetryCursorKey ||
|
|
19027
|
+
!this.checkpoint.providerRetryCursors
|
|
19028
|
+
) {
|
|
19029
|
+
return;
|
|
19030
|
+
}
|
|
19031
|
+
delete this.checkpoint.providerRetryCursors[providerRetryCursorKey];
|
|
19032
|
+
if (
|
|
19033
|
+
Object.keys(this.checkpoint.providerRetryCursors).length === 0
|
|
19034
|
+
) {
|
|
19035
|
+
delete this.checkpoint.providerRetryCursors;
|
|
19036
|
+
}
|
|
19037
|
+
};
|
|
19038
|
+
const persistProviderRetryCursor = (retryFrom: number) => {
|
|
19039
|
+
if (!providerRetryCursorKey) return;
|
|
19040
|
+
const cursor = {
|
|
19041
|
+
invocationAttempt: retryFrom + 1,
|
|
19042
|
+
retryFromInvocationAttempt: retryFrom,
|
|
19043
|
+
};
|
|
19044
|
+
this.checkpoint.providerRetryCursors ??= {};
|
|
19045
|
+
this.checkpoint.providerRetryCursors[providerRetryCursorKey] =
|
|
19046
|
+
cursor;
|
|
19047
|
+
};
|
|
18863
19048
|
let physicalAttempt = 0;
|
|
19049
|
+
// Once this logical call receives an HTTP 429, keep a small
|
|
19050
|
+
// phase trace for its retry so a stalled retry can be located without
|
|
19051
|
+
// logging request, receipt, organization, or provider identifiers.
|
|
19052
|
+
let provider429RetryDiagnosticNextInvocationAttempt: number | null =
|
|
19053
|
+
null;
|
|
19054
|
+
let provider429RetryDiagnosticStartedAt: number | null = null;
|
|
19055
|
+
const logProvider429RetryPhase = (phase: string) => {
|
|
19056
|
+
if (
|
|
19057
|
+
provider429RetryDiagnosticNextInvocationAttempt === null ||
|
|
19058
|
+
provider429RetryDiagnosticStartedAt === null
|
|
19059
|
+
)
|
|
19060
|
+
return;
|
|
19061
|
+
this.runtimeLog(
|
|
19062
|
+
`[runtime.provider_429_retry_phase] ${JSON.stringify({
|
|
19063
|
+
next_invocation_attempt:
|
|
19064
|
+
provider429RetryDiagnosticNextInvocationAttempt,
|
|
19065
|
+
phase,
|
|
19066
|
+
elapsed_ms: Math.round(
|
|
19067
|
+
performance.now() - provider429RetryDiagnosticStartedAt,
|
|
19068
|
+
),
|
|
19069
|
+
})}`,
|
|
19070
|
+
);
|
|
19071
|
+
};
|
|
19072
|
+
const executionSignal = this.#options.executionSignal;
|
|
19073
|
+
const toolAbortSignal =
|
|
19074
|
+
executionSignal && options?.abortSignal
|
|
19075
|
+
? AbortSignal.any([executionSignal, options.abortSignal])
|
|
19076
|
+
: (executionSignal ?? options?.abortSignal);
|
|
19077
|
+
// A short provider hold costs less than checkpointing, parking, and
|
|
19078
|
+
// starting another sandbox. Once we wait locally, bound all elapsed
|
|
19079
|
+
// residency for this logical call, including subsequent HTTP/pacer
|
|
19080
|
+
// work. Repeated zero or expired holds must also yield durably.
|
|
19081
|
+
let residentProviderWaitStartedAt: number | null = null;
|
|
19082
|
+
const canWaitForProviderInSandbox = (waitMs: number) => {
|
|
19083
|
+
const elapsedMs =
|
|
19084
|
+
residentProviderWaitStartedAt === null
|
|
19085
|
+
? 0
|
|
19086
|
+
: performance.now() - residentProviderWaitStartedAt;
|
|
19087
|
+
return (
|
|
19088
|
+
Math.max(1, waitMs) <=
|
|
19089
|
+
Math.max(0, TOOL_PROVIDER_RESIDENT_WAIT_BUDGET_MS - elapsedMs)
|
|
19090
|
+
);
|
|
19091
|
+
};
|
|
19092
|
+
const waitForToolRetry = async (
|
|
19093
|
+
waitMs: number,
|
|
19094
|
+
providerHold: boolean,
|
|
19095
|
+
) => {
|
|
19096
|
+
await options?.parkProviderCall?.();
|
|
19097
|
+
if (providerHold && residentProviderWaitStartedAt === null) {
|
|
19098
|
+
residentProviderWaitStartedAt = performance.now();
|
|
19099
|
+
}
|
|
19100
|
+
await this.sleepWithCheckpointHeartbeat(
|
|
19101
|
+
waitMs,
|
|
19102
|
+
options?.abortSignal,
|
|
19103
|
+
);
|
|
19104
|
+
};
|
|
18864
19105
|
const retryToolTransportFailure = async (input: {
|
|
18865
19106
|
error: unknown;
|
|
18866
19107
|
elapsedMs: number;
|
|
@@ -18946,6 +19187,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18946
19187
|
let providerTelemetryKey: string | null = null;
|
|
18947
19188
|
let providerRunInFlightAtDispatch: number | null = null;
|
|
18948
19189
|
const acquireProviderPermit = async (signal?: AbortSignal) => {
|
|
19190
|
+
logProvider429RetryPhase('provider_permit_wait_started');
|
|
18949
19191
|
const permit = await this.resourceGovernor.acquireProviderPermit({
|
|
18950
19192
|
toolId,
|
|
18951
19193
|
signal,
|
|
@@ -18955,6 +19197,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18955
19197
|
providerPacingGrant = permit.pacingGrant ?? null;
|
|
18956
19198
|
providerGatewayDispatchProof = permit.gatewayDispatchProof ?? null;
|
|
18957
19199
|
providerTelemetryKey = permit.provider;
|
|
19200
|
+
logProvider429RetryPhase('provider_permit_acquired');
|
|
18958
19201
|
return permit;
|
|
18959
19202
|
};
|
|
18960
19203
|
|
|
@@ -18967,6 +19210,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
18967
19210
|
}
|
|
18968
19211
|
|
|
18969
19212
|
while (true) {
|
|
19213
|
+
toolAbortSignal?.throwIfAborted();
|
|
19214
|
+
if (
|
|
19215
|
+
providerNoProgressExpired({
|
|
19216
|
+
startedAtMsByScope:
|
|
19217
|
+
this.checkpoint.providerNoProgressStartedAtMsByScope,
|
|
19218
|
+
scope: providerNoProgressScope,
|
|
19219
|
+
nowMs: Date.now(),
|
|
19220
|
+
})
|
|
19221
|
+
) {
|
|
19222
|
+
throw providerNoProgressError(Date.now());
|
|
19223
|
+
}
|
|
18970
19224
|
physicalAttempt += 1;
|
|
18971
19225
|
providerConcurrencyGrant = null;
|
|
18972
19226
|
providerPacingGrant = null;
|
|
@@ -19012,28 +19266,28 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19012
19266
|
// not share the coordinator's TTL environment.
|
|
19013
19267
|
const hasReceiptHeartbeat = Boolean(options?.heartbeatReceipt);
|
|
19014
19268
|
const abortController =
|
|
19015
|
-
timeoutMs || hasReceiptHeartbeat ||
|
|
19269
|
+
timeoutMs || hasReceiptHeartbeat || toolAbortSignal
|
|
19016
19270
|
? new AbortController()
|
|
19017
19271
|
: null;
|
|
19018
19272
|
const forwardAbortSignal = () => {
|
|
19019
|
-
if (!
|
|
19020
|
-
abortController?.abort(
|
|
19273
|
+
if (!toolAbortSignal?.aborted) return;
|
|
19274
|
+
abortController?.abort(toolAbortSignal.reason);
|
|
19021
19275
|
};
|
|
19022
19276
|
const receiptHeartbeat = options?.heartbeatReceipt;
|
|
19023
19277
|
let heartbeatFailure: unknown = null;
|
|
19024
19278
|
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
19025
19279
|
try {
|
|
19026
|
-
if (
|
|
19280
|
+
if (toolAbortSignal?.aborted) {
|
|
19027
19281
|
forwardAbortSignal();
|
|
19028
19282
|
} else {
|
|
19029
|
-
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
{ once: true },
|
|
19033
|
-
);
|
|
19283
|
+
toolAbortSignal?.addEventListener('abort', forwardAbortSignal, {
|
|
19284
|
+
once: true,
|
|
19285
|
+
});
|
|
19034
19286
|
}
|
|
19035
19287
|
const ownershipStartedAt = Date.now();
|
|
19288
|
+
logProvider429RetryPhase('receipt_ownership_check_started');
|
|
19036
19289
|
await options?.beforeProviderCall?.();
|
|
19290
|
+
logProvider429RetryPhase('receipt_ownership_check_completed');
|
|
19037
19291
|
if (runtimeReceiptReadTraceEnabled) {
|
|
19038
19292
|
this.runtimeLog(
|
|
19039
19293
|
`[perf] tool call id=${toolId} phase=before_provider_ownership elapsed_ms=${Date.now() - ownershipStartedAt}`,
|
|
@@ -19083,6 +19337,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19083
19337
|
acquireProviderPermit: async ({ signal }) =>
|
|
19084
19338
|
await acquireProviderPermit(signal),
|
|
19085
19339
|
tryAcquireProviderPermit: () => {
|
|
19340
|
+
logProvider429RetryPhase(
|
|
19341
|
+
'provider_permit_acquire_attempted',
|
|
19342
|
+
);
|
|
19086
19343
|
const permit =
|
|
19087
19344
|
this.resourceGovernor.tryAcquireProviderPermit({
|
|
19088
19345
|
toolId,
|
|
@@ -19093,6 +19350,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19093
19350
|
providerGatewayDispatchProof =
|
|
19094
19351
|
permit.gatewayDispatchProof ?? null;
|
|
19095
19352
|
providerTelemetryKey = permit.provider;
|
|
19353
|
+
logProvider429RetryPhase('provider_permit_acquired');
|
|
19096
19354
|
return permit;
|
|
19097
19355
|
},
|
|
19098
19356
|
tryAcquireExecutionSlot: ({ providerResourceKey }) =>
|
|
@@ -19299,6 +19557,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19299
19557
|
providerCallStartedAt =
|
|
19300
19558
|
dispatchedProviderCallStartedAt;
|
|
19301
19559
|
fetchDispatched = true;
|
|
19560
|
+
logProvider429RetryPhase('http_dispatch_started');
|
|
19302
19561
|
activeProviderRequest =
|
|
19303
19562
|
this.beginProviderRequest(provider);
|
|
19304
19563
|
const dispatchedResponse = await fetch(url, {
|
|
@@ -19356,6 +19615,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19356
19615
|
// it has been read and verified below.
|
|
19357
19616
|
response = dispatchedResponse;
|
|
19358
19617
|
if (dispatchedResponse.status === 429) {
|
|
19618
|
+
provider429RetryDiagnosticStartedAt =
|
|
19619
|
+
performance.now();
|
|
19620
|
+
provider429RetryDiagnosticNextInvocationAttempt =
|
|
19621
|
+
invocationAttempt + 1;
|
|
19622
|
+
logProvider429RetryPhase(
|
|
19623
|
+
'http_429_response_received',
|
|
19624
|
+
);
|
|
19359
19625
|
providerRequestsInFlightAt429 =
|
|
19360
19626
|
activeProviderRequest?.inFlightCount() ?? null;
|
|
19361
19627
|
const observedAt = Date.now();
|
|
@@ -19365,8 +19631,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19365
19631
|
// the conservative bare-429 cooldown. Do not replace
|
|
19366
19632
|
// it with the generic one-second HTTP retry fallback.
|
|
19367
19633
|
providerRetryAtMs = retryAfter?.trim()
|
|
19368
|
-
?
|
|
19369
|
-
parseToolExecuteRetryAfterMs(
|
|
19634
|
+
? parseToolExecuteRetryAtMs(
|
|
19370
19635
|
retryAfter,
|
|
19371
19636
|
observedAt,
|
|
19372
19637
|
)
|
|
@@ -19495,16 +19760,34 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19495
19760
|
}
|
|
19496
19761
|
if (dispatchedResponse.status === 429) {
|
|
19497
19762
|
const bodyText = dispatchedResponseErrorText ?? '';
|
|
19498
|
-
|
|
19763
|
+
let attempt = httpFailureAttempts.next({
|
|
19499
19764
|
toolId,
|
|
19500
19765
|
status: dispatchedResponse.status,
|
|
19501
19766
|
bodyText,
|
|
19502
19767
|
transientHttpRetrySafe:
|
|
19503
19768
|
retrySafeTransientHttp || fixtureReplaySafe,
|
|
19504
19769
|
});
|
|
19505
|
-
|
|
19770
|
+
let failure = classifyToolExecuteHttpFailure({
|
|
19771
|
+
toolId,
|
|
19772
|
+
status: dispatchedResponse.status,
|
|
19506
19773
|
attempt,
|
|
19507
|
-
|
|
19774
|
+
bodyText,
|
|
19775
|
+
schemaVersion: toolErrorSchemaVersion,
|
|
19776
|
+
retryAfterHeader:
|
|
19777
|
+
dispatchedResponse.headers.get('retry-after'),
|
|
19778
|
+
transientHttpRetrySafe:
|
|
19779
|
+
retrySafeTransientHttp || fixtureReplaySafe,
|
|
19780
|
+
providerLatencyMs:
|
|
19781
|
+
Date.now() - dispatchedProviderCallStartedAt,
|
|
19782
|
+
});
|
|
19783
|
+
if (failure.backpressureDelayMs !== null) {
|
|
19784
|
+
// The durable invocation attempt survives a map
|
|
19785
|
+
// suspension; use its absolute 1-based count for
|
|
19786
|
+
// the provider retry budget instead of the
|
|
19787
|
+
// process-local HTTP tracker, which resets when
|
|
19788
|
+
// a new runner resumes this row.
|
|
19789
|
+
attempt = invocationAttempt + 1;
|
|
19790
|
+
failure = classifyToolExecuteHttpFailure({
|
|
19508
19791
|
toolId,
|
|
19509
19792
|
status: dispatchedResponse.status,
|
|
19510
19793
|
attempt,
|
|
@@ -19516,7 +19799,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19516
19799
|
retrySafeTransientHttp || fixtureReplaySafe,
|
|
19517
19800
|
providerLatencyMs:
|
|
19518
19801
|
Date.now() - dispatchedProviderCallStartedAt,
|
|
19519
|
-
})
|
|
19802
|
+
});
|
|
19803
|
+
}
|
|
19804
|
+
preclassifiedHttpFailure.current = {
|
|
19805
|
+
attempt,
|
|
19806
|
+
failure,
|
|
19520
19807
|
};
|
|
19521
19808
|
}
|
|
19522
19809
|
activeProviderRequest?.release();
|
|
@@ -19649,14 +19936,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19649
19936
|
}
|
|
19650
19937
|
if (error instanceof ProviderExhaustedError) {
|
|
19651
19938
|
const retryAtMs = Date.parse(error.retryAt);
|
|
19652
|
-
|
|
19653
|
-
|
|
19654
|
-
|
|
19655
|
-
|
|
19656
|
-
|
|
19657
|
-
|
|
19658
|
-
|
|
19659
|
-
|
|
19939
|
+
observeProviderWait(
|
|
19940
|
+
Number.isFinite(retryAtMs) ? retryAtMs : Date.now() + 1_000,
|
|
19941
|
+
);
|
|
19942
|
+
const waitMs = Number.isFinite(retryAtMs)
|
|
19943
|
+
? Math.max(0, Math.ceil(retryAtMs - Date.now()))
|
|
19944
|
+
: 1_000;
|
|
19945
|
+
if (
|
|
19946
|
+
!this.#options.durableBoundaries ||
|
|
19947
|
+
canWaitForProviderInSandbox(waitMs)
|
|
19948
|
+
) {
|
|
19949
|
+
await waitForToolRetry(
|
|
19950
|
+
this.#options.durableBoundaries
|
|
19951
|
+
? waitMs
|
|
19952
|
+
: Math.max(1_000, waitMs),
|
|
19953
|
+
true,
|
|
19660
19954
|
);
|
|
19661
19955
|
continue;
|
|
19662
19956
|
}
|
|
@@ -19812,10 +20106,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19812
20106
|
if (timeoutHandle) {
|
|
19813
20107
|
clearTimeout(timeoutHandle);
|
|
19814
20108
|
}
|
|
19815
|
-
|
|
19816
|
-
'abort',
|
|
19817
|
-
forwardAbortSignal,
|
|
19818
|
-
);
|
|
20109
|
+
toolAbortSignal?.removeEventListener('abort', forwardAbortSignal);
|
|
19819
20110
|
}
|
|
19820
20111
|
|
|
19821
20112
|
if (!response) {
|
|
@@ -19950,26 +20241,47 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
19950
20241
|
);
|
|
19951
20242
|
providerBackpressureReported = true;
|
|
19952
20243
|
}
|
|
19953
|
-
//
|
|
19954
|
-
//
|
|
19955
|
-
//
|
|
19956
|
-
// unrelated rows from making progress.
|
|
20244
|
+
// Provider permits are already released. Brief holds can use
|
|
20245
|
+
// the existing retry loop; longer or accumulated holds yield
|
|
20246
|
+
// the row slot and resume from durable completed receipts.
|
|
19957
20247
|
const durableProviderWaitMs =
|
|
19958
20248
|
failure.providerPacingWaitMs ?? failure.backpressureDelayMs;
|
|
20249
|
+
const providerHoldRetryAtMs =
|
|
20250
|
+
durableProviderWaitMs === null
|
|
20251
|
+
? null
|
|
20252
|
+
: ((failure.providerPacingWaitMs !== null
|
|
20253
|
+
? Date.now() + failure.providerPacingWaitMs
|
|
20254
|
+
: providerRetryAtMs) ??
|
|
20255
|
+
Date.now() + durableProviderWaitMs);
|
|
20256
|
+
const remainingProviderWaitMs =
|
|
20257
|
+
providerHoldRetryAtMs === null
|
|
20258
|
+
? null
|
|
20259
|
+
: Math.max(0, Math.ceil(providerHoldRetryAtMs - Date.now()));
|
|
19959
20260
|
if (
|
|
20261
|
+
failure.backpressureDelayMs !== null ||
|
|
20262
|
+
failure.providerPacingWaitMs !== null
|
|
20263
|
+
) {
|
|
20264
|
+
observeProviderWait(
|
|
20265
|
+
providerHoldRetryAtMs ?? Date.now() + 1_000,
|
|
20266
|
+
);
|
|
20267
|
+
}
|
|
20268
|
+
if (
|
|
20269
|
+
failure.shouldRetry &&
|
|
19960
20270
|
this.#options.durableBoundaries &&
|
|
19961
|
-
|
|
19962
|
-
|
|
20271
|
+
remainingProviderWaitMs !== null &&
|
|
20272
|
+
!canWaitForProviderInSandbox(remainingProviderWaitMs)
|
|
19963
20273
|
) {
|
|
19964
|
-
|
|
19965
|
-
(
|
|
19966
|
-
|
|
19967
|
-
|
|
20274
|
+
if (failure.backpressureDelayMs !== null) {
|
|
20275
|
+
persistProviderRetryCursor(invocationAttempt);
|
|
20276
|
+
}
|
|
20277
|
+
const retryAtMs = providerHoldRetryAtMs!;
|
|
19968
20278
|
const rowScope = rowContext.getStore();
|
|
19969
20279
|
if (
|
|
19970
20280
|
this.#options.durableBoundaries &&
|
|
19971
20281
|
(options?.providerBackpressureMapRows || rowScope)
|
|
19972
20282
|
) {
|
|
20283
|
+
preserveProviderRetryCursorForSuspension =
|
|
20284
|
+
failure.backpressureDelayMs !== null;
|
|
19973
20285
|
throw new PlayRowProviderBackpressureError(
|
|
19974
20286
|
provider,
|
|
19975
20287
|
retryAtMs,
|
|
@@ -20018,6 +20330,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
20018
20330
|
!shedKnownNotDispatched
|
|
20019
20331
|
) {
|
|
20020
20332
|
const previousInvocationAttempt = invocationAttempt;
|
|
20333
|
+
if (failure.backpressureDelayMs !== null) {
|
|
20334
|
+
persistProviderRetryCursor(previousInvocationAttempt);
|
|
20335
|
+
}
|
|
20021
20336
|
invocationAttempt += 1;
|
|
20022
20337
|
providerRateLimitRetryFromInvocationAttempt =
|
|
20023
20338
|
failure.backpressureDelayMs !== null
|
|
@@ -20027,6 +20342,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
20027
20342
|
if (failure.chargeRetryBudget) {
|
|
20028
20343
|
await this.governor.chargeBudget('retry');
|
|
20029
20344
|
}
|
|
20345
|
+
const retryDelayMs =
|
|
20346
|
+
remainingProviderWaitMs ?? failure.retryDelayMs;
|
|
20030
20347
|
const retryAttributePrefix =
|
|
20031
20348
|
failure.reason === 'provider_pacing_wait'
|
|
20032
20349
|
? 'provider_pacing_wait'
|
|
@@ -20035,14 +20352,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
20035
20352
|
: 'transient_http';
|
|
20036
20353
|
span.setAttribute(
|
|
20037
20354
|
`plays.${retryAttributePrefix}_retry_after_ms`,
|
|
20038
|
-
|
|
20355
|
+
retryDelayMs,
|
|
20039
20356
|
);
|
|
20040
20357
|
span.setAttribute(
|
|
20041
20358
|
`plays.${retryAttributePrefix}_attempt`,
|
|
20042
20359
|
httpFailureAttempt,
|
|
20043
20360
|
);
|
|
20044
20361
|
this.runtimeLog(
|
|
20045
|
-
`Tool ${toolId} returned ${response.status} (${failure.reason}); retrying after ${
|
|
20362
|
+
`Tool ${toolId} returned ${response.status} (${failure.reason}); retrying after ${retryDelayMs}ms ` +
|
|
20046
20363
|
`(request_id=${deeplineRequestId ?? 'none'} gateway=${transportGatewayOriginForDiagnostic(url)})`,
|
|
20047
20364
|
{ level: 'warn' },
|
|
20048
20365
|
);
|
|
@@ -20064,16 +20381,26 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
20064
20381
|
? 'rate_limit'
|
|
20065
20382
|
: 'provider_error',
|
|
20066
20383
|
retryAt:
|
|
20067
|
-
|
|
20068
|
-
? Date.now() + failure.providerPacingWaitMs
|
|
20069
|
-
: Date.now() + failure.retryDelayMs,
|
|
20384
|
+
providerHoldRetryAtMs ?? Date.now() + retryDelayMs,
|
|
20070
20385
|
attempt: httpFailureAttempt,
|
|
20071
20386
|
},
|
|
20072
20387
|
observedAt: Date.now(),
|
|
20073
20388
|
},
|
|
20074
20389
|
});
|
|
20075
|
-
await
|
|
20076
|
-
|
|
20390
|
+
await waitForToolRetry(
|
|
20391
|
+
retryDelayMs,
|
|
20392
|
+
remainingProviderWaitMs !== null,
|
|
20393
|
+
);
|
|
20394
|
+
logProvider429RetryPhase('retry_sleep_completed');
|
|
20395
|
+
if (
|
|
20396
|
+
!usesDurableInvocationFence &&
|
|
20397
|
+
failure.backpressureDelayMs !== null
|
|
20398
|
+
) {
|
|
20399
|
+
// A classified provider rejection has no completed value to
|
|
20400
|
+
// charge. The next physical attempt needs a new reservation;
|
|
20401
|
+
// durable result and provider-operation keys stay unchanged.
|
|
20402
|
+
deeplineRequestId = `ctx-tool-${stableDigest(`${executionRequestNonce}:provider-rate-limit-retry:${invocationAttempt}`).slice(0, 32)}`;
|
|
20403
|
+
}
|
|
20077
20404
|
this.emitExecutionEvent({
|
|
20078
20405
|
type: 'activity.observed',
|
|
20079
20406
|
observation: {
|
|
@@ -20141,13 +20468,30 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
20141
20468
|
: typeof resolved.result,
|
|
20142
20469
|
});
|
|
20143
20470
|
toolCallSucceeded = true;
|
|
20471
|
+
if (this.checkpoint.providerNoProgressStartedAtMsByScope) {
|
|
20472
|
+
delete this.checkpoint.providerNoProgressStartedAtMsByScope[
|
|
20473
|
+
providerNoProgressScope
|
|
20474
|
+
];
|
|
20475
|
+
}
|
|
20144
20476
|
return receiptOutputRef
|
|
20145
20477
|
? { ...resolved, receiptOutputRef }
|
|
20146
20478
|
: resolved;
|
|
20147
20479
|
}
|
|
20148
20480
|
},
|
|
20149
20481
|
);
|
|
20482
|
+
} catch (error) {
|
|
20483
|
+
// The checkpoint becomes durable with the scheduler's suspension handoff.
|
|
20484
|
+
// Keep cursors only on that path; terminal failures and cancellation must
|
|
20485
|
+
// not leave retry state behind for an unrelated future execution.
|
|
20486
|
+
preserveProviderRetryCursorForSuspension =
|
|
20487
|
+
preserveProviderRetryCursorForSuspension ||
|
|
20488
|
+
error instanceof PlayRowProviderBackpressureError ||
|
|
20489
|
+
isPlayExecutionSuspendedError(error);
|
|
20490
|
+
throw error;
|
|
20150
20491
|
} finally {
|
|
20492
|
+
if (!preserveProviderRetryCursorForSuspension) {
|
|
20493
|
+
clearProviderRetryCursor();
|
|
20494
|
+
}
|
|
20151
20495
|
toolInputSnapshotMemory.current?.release();
|
|
20152
20496
|
if (retryActivityEmitted) {
|
|
20153
20497
|
this.emitExecutionEvent({
|
|
@@ -20207,21 +20551,41 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
20207
20551
|
);
|
|
20208
20552
|
}
|
|
20209
20553
|
|
|
20210
|
-
private async sleepWithCheckpointHeartbeat(
|
|
20554
|
+
private async sleepWithCheckpointHeartbeat(
|
|
20555
|
+
ms: number,
|
|
20556
|
+
callerSignal?: AbortSignal,
|
|
20557
|
+
): Promise<void> {
|
|
20211
20558
|
const waitMs = Math.max(1, Math.ceil(ms));
|
|
20212
|
-
const startedAt =
|
|
20213
|
-
|
|
20214
|
-
|
|
20559
|
+
const startedAt = performance.now();
|
|
20560
|
+
const executionSignal = this.#options.executionSignal;
|
|
20561
|
+
const signal =
|
|
20562
|
+
executionSignal && callerSignal
|
|
20563
|
+
? AbortSignal.any([executionSignal, callerSignal])
|
|
20564
|
+
: (executionSignal ?? callerSignal);
|
|
20565
|
+
|
|
20566
|
+
signal?.throwIfAborted();
|
|
20567
|
+
while (performance.now() - startedAt < waitMs) {
|
|
20215
20568
|
this.#options.onBatchComplete?.(this.checkpoint);
|
|
20216
|
-
|
|
20217
|
-
|
|
20218
|
-
|
|
20219
|
-
|
|
20569
|
+
signal?.throwIfAborted();
|
|
20570
|
+
const remainingMs = waitMs - (performance.now() - startedAt);
|
|
20571
|
+
await new Promise<void>((resolve, reject) => {
|
|
20572
|
+
const onAbort = () => {
|
|
20573
|
+
clearTimeout(timer);
|
|
20574
|
+
signal?.removeEventListener('abort', onAbort);
|
|
20575
|
+
reject(signal?.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
20576
|
+
};
|
|
20577
|
+
const timer = setTimeout(
|
|
20578
|
+
() => {
|
|
20579
|
+
signal?.removeEventListener('abort', onAbort);
|
|
20580
|
+
resolve();
|
|
20581
|
+
},
|
|
20220
20582
|
Math.min(TOOL_RETRY_HEARTBEAT_INTERVAL_MS, Math.max(1, remainingMs)),
|
|
20221
|
-
)
|
|
20222
|
-
|
|
20583
|
+
);
|
|
20584
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
20585
|
+
});
|
|
20223
20586
|
}
|
|
20224
20587
|
|
|
20588
|
+
signal?.throwIfAborted();
|
|
20225
20589
|
this.#options.onBatchComplete?.(this.checkpoint);
|
|
20226
20590
|
}
|
|
20227
20591
|
}
|