deepline 0.3.72 → 0.3.73
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 +551 -571
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +40 -10
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +2 -14
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/native-batch.ts +9 -92
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +159 -621
- package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +19 -13
- package/dist/bundling-sources/shared_libs/plays/contracts.ts +1 -4
- package/dist/bundling-sources/shared_libs/plays/tool-response-contract.ts +2 -4
- 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
|
@@ -175,6 +175,7 @@ import {
|
|
|
175
175
|
buildDurableToolAggregateReceiptKey,
|
|
176
176
|
buildDurableToolCallAuthScopeDigest,
|
|
177
177
|
buildDurableToolCallCacheKey,
|
|
178
|
+
buildLegacyRevisionedDurableToolCallCacheKey,
|
|
178
179
|
buildDurableToolPayloadProviderIdempotencyKey,
|
|
179
180
|
buildDurableToolProviderIdempotencyKey,
|
|
180
181
|
buildDurableToolReceiptPrefix,
|
|
@@ -1395,6 +1396,8 @@ function playNodeScopeForToolCallRequest(
|
|
|
1395
1396
|
type ToolExecutionApiOptions = {
|
|
1396
1397
|
timeoutMs?: number;
|
|
1397
1398
|
durableCallReceiptKey?: string | null;
|
|
1399
|
+
/** The receipt key is a completed-result cache for this read-only call. */
|
|
1400
|
+
completedResultCache?: boolean;
|
|
1398
1401
|
executionAuthScopeDigest?: string | null;
|
|
1399
1402
|
/**
|
|
1400
1403
|
* Historical receipt identity used for the API's provider/billing
|
|
@@ -2763,9 +2766,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
2763
2766
|
}
|
|
2764
2767
|
|
|
2765
2768
|
/**
|
|
2766
|
-
*
|
|
2767
|
-
*
|
|
2768
|
-
*
|
|
2769
|
+
* A real published artifact pins its response presentation in its launch
|
|
2770
|
+
* contract. In-process callers that omit the option are runtime consumers,
|
|
2771
|
+
* not historical Play artifacts, and retain the canonical result they pass
|
|
2772
|
+
* in. This keeps the context API backward-compatible while letting a pinned
|
|
2773
|
+
* legacy artifact request its old projection after a cache read.
|
|
2774
|
+
*/
|
|
2775
|
+
private get hasPinnedToolResponseContract(): boolean {
|
|
2776
|
+
return (
|
|
2777
|
+
this.activeInlineComposition !== null ||
|
|
2778
|
+
this.#options.toolResponseContract !== undefined
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
/**
|
|
2783
|
+
* Compatibility metadata retained for already-published artifacts. It never
|
|
2784
|
+
* changes the completed-result cache identity.
|
|
2769
2785
|
*/
|
|
2770
2786
|
private get currentToolResponseReceiptRevision(): string | undefined {
|
|
2771
2787
|
if (this.activeInlineComposition) {
|
|
@@ -4020,14 +4036,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4020
4036
|
);
|
|
4021
4037
|
if (normalized) byKey.set(normalized.key, normalized);
|
|
4022
4038
|
}
|
|
4023
|
-
await this.
|
|
4039
|
+
await this.reconcileIncompleteRuntimeStepReceiptCompletions(
|
|
4024
4040
|
normalizedInputs,
|
|
4025
4041
|
byKey,
|
|
4026
4042
|
);
|
|
4027
4043
|
return byKey;
|
|
4028
4044
|
}
|
|
4029
4045
|
|
|
4030
|
-
private async
|
|
4046
|
+
private async reconcileIncompleteRuntimeStepReceiptCompletions(
|
|
4031
4047
|
receipts: Array<{
|
|
4032
4048
|
key: string;
|
|
4033
4049
|
runId: string;
|
|
@@ -4038,32 +4054,45 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4038
4054
|
completedByKey: Map<string, RuntimeStepReceipt>,
|
|
4039
4055
|
): Promise<void> {
|
|
4040
4056
|
if (
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
!this.#options.getRuntimeStepReceipts)
|
|
4057
|
+
!this.#options.getRuntimeStepReceipt &&
|
|
4058
|
+
!this.#options.getRuntimeStepReceipts
|
|
4044
4059
|
) {
|
|
4045
4060
|
return;
|
|
4046
4061
|
}
|
|
4047
|
-
|
|
4062
|
+
// The receipt gateway is allowed to return a compact terminal
|
|
4063
|
+
// acknowledgement without `output`. That is sufficient for old callers
|
|
4064
|
+
// which keep the live result, but a completed-result cache must hydrate
|
|
4065
|
+
// the stored output before handing it to a cache cohort.
|
|
4066
|
+
const incompleteKeys = receipts
|
|
4048
4067
|
.map((receipt) => receipt.key.trim())
|
|
4049
|
-
.filter((key) =>
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4068
|
+
.filter((key) => {
|
|
4069
|
+
if (!key) return false;
|
|
4070
|
+
const completed = completedByKey.get(key);
|
|
4071
|
+
return (
|
|
4072
|
+
!completed ||
|
|
4073
|
+
((completed.status === 'completed' ||
|
|
4074
|
+
completed.status === 'skipped') &&
|
|
4075
|
+
completed.output === undefined)
|
|
4076
|
+
);
|
|
4077
|
+
});
|
|
4078
|
+
if (incompleteKeys.length === 0) return;
|
|
4079
|
+
const latest = await this.getRuntimeStepReceipts(incompleteKeys);
|
|
4080
|
+
for (const key of incompleteKeys) {
|
|
4053
4081
|
const recovered = latest.get(key);
|
|
4054
4082
|
if (
|
|
4055
|
-
recovered?.status === 'completed' ||
|
|
4056
|
-
|
|
4083
|
+
(recovered?.status === 'completed' ||
|
|
4084
|
+
recovered?.status === 'skipped') &&
|
|
4085
|
+
recovered.output !== undefined
|
|
4057
4086
|
) {
|
|
4058
4087
|
completedByKey.set(key, recovered);
|
|
4059
4088
|
}
|
|
4060
4089
|
}
|
|
4061
|
-
const recoveredCount =
|
|
4062
|
-
completedByKey.
|
|
4090
|
+
const recoveredCount = incompleteKeys.filter(
|
|
4091
|
+
(key) => completedByKey.get(key)?.output !== undefined,
|
|
4063
4092
|
).length;
|
|
4064
4093
|
if (recoveredCount > 0) {
|
|
4065
4094
|
this.runtimeLog(
|
|
4066
|
-
`Runtime tool receipt completion response
|
|
4095
|
+
`Runtime tool receipt completion response hydrated ${recoveredCount}/${incompleteKeys.length} incomplete receipt(s) from durable store read-back.`,
|
|
4067
4096
|
);
|
|
4068
4097
|
}
|
|
4069
4098
|
}
|
|
@@ -4178,17 +4207,39 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4178
4207
|
);
|
|
4179
4208
|
}
|
|
4180
4209
|
|
|
4181
|
-
private async durableToolCallCacheKeyForScope(
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4210
|
+
private async durableToolCallCacheKeyForScope(input: {
|
|
4211
|
+
toolId: string;
|
|
4212
|
+
requestInput: Record<string, unknown>;
|
|
4213
|
+
executionAuthScopeDigest?: string | null;
|
|
4214
|
+
staleAfterSeconds?: number | null;
|
|
4215
|
+
playLocalScope?: string | null;
|
|
4216
|
+
}): Promise<string> {
|
|
4217
|
+
const identity = await this.durableToolCallCacheIdentity(input);
|
|
4218
|
+
return buildDurableToolCallCacheKey(identity);
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
private async legacyDurableToolCallCacheKeyForScope(input: {
|
|
4222
|
+
toolId: string;
|
|
4223
|
+
requestInput: Record<string, unknown>;
|
|
4224
|
+
executionAuthScopeDigest?: string | null;
|
|
4225
|
+
staleAfterSeconds?: number | null;
|
|
4226
|
+
playLocalScope?: string | null;
|
|
4227
|
+
}): Promise<string | null> {
|
|
4228
|
+
const revision = this.currentToolResponseReceiptRevision;
|
|
4229
|
+
if (!revision) return null;
|
|
4230
|
+
return buildLegacyRevisionedDurableToolCallCacheKey({
|
|
4231
|
+
...(await this.durableToolCallCacheIdentity(input)),
|
|
4232
|
+
toolResponseReceiptRevision: revision,
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
private async durableToolCallCacheIdentity(input: {
|
|
4237
|
+
toolId: string;
|
|
4238
|
+
requestInput: Record<string, unknown>;
|
|
4239
|
+
executionAuthScopeDigest?: string | null;
|
|
4240
|
+
staleAfterSeconds?: number | null;
|
|
4241
|
+
playLocalScope?: string | null;
|
|
4242
|
+
}): Promise<Parameters<typeof buildDurableToolCallCacheKey>[0]> {
|
|
4192
4243
|
const providerActionVersion =
|
|
4193
4244
|
(await this.#options.getToolActionCacheVersion?.(input.toolId))?.trim() ??
|
|
4194
4245
|
'';
|
|
@@ -4196,7 +4247,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4196
4247
|
input.executionAuthScopeDigest?.trim() ||
|
|
4197
4248
|
(await this.resolveToolAuthScopeDigest(input.toolId))?.trim() ||
|
|
4198
4249
|
null;
|
|
4199
|
-
return
|
|
4250
|
+
return {
|
|
4200
4251
|
orgId: this.#options.orgId,
|
|
4201
4252
|
playLocalScope: input.playLocalScope,
|
|
4202
4253
|
toolId: input.toolId,
|
|
@@ -4207,29 +4258,33 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
4207
4258
|
executionAuthScopeDigest,
|
|
4208
4259
|
}),
|
|
4209
4260
|
providerActionVersion,
|
|
4210
|
-
toolResponseReceiptRevision,
|
|
4211
4261
|
staleAfterSeconds: input.staleAfterSeconds,
|
|
4212
4262
|
cacheEpochMs: this.durableCallCacheEpochMs,
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
|
|
4266
|
+
private async durableToolCallCacheKey(input: {
|
|
4267
|
+
toolId: string;
|
|
4268
|
+
requestInput: Record<string, unknown>;
|
|
4269
|
+
executionAuthScopeDigest?: string | null;
|
|
4270
|
+
staleAfterSeconds?: number | null;
|
|
4271
|
+
}): Promise<string> {
|
|
4272
|
+
return await this.durableToolCallCacheKeyForScope({
|
|
4273
|
+
...input,
|
|
4274
|
+
playLocalScope: this.currentGovernance.currentPlayId,
|
|
4213
4275
|
});
|
|
4214
4276
|
}
|
|
4215
4277
|
|
|
4216
|
-
private async
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
.
|
|
4225
|
-
|
|
4226
|
-
return await this.durableToolCallCacheKeyForScope(
|
|
4227
|
-
{
|
|
4228
|
-
...input,
|
|
4229
|
-
playLocalScope: this.currentGovernance.currentPlayId,
|
|
4230
|
-
},
|
|
4231
|
-
toolResponseReceiptRevision,
|
|
4232
|
-
);
|
|
4278
|
+
private async legacyDurableToolCallCacheKey(input: {
|
|
4279
|
+
toolId: string;
|
|
4280
|
+
requestInput: Record<string, unknown>;
|
|
4281
|
+
executionAuthScopeDigest?: string | null;
|
|
4282
|
+
staleAfterSeconds?: number | null;
|
|
4283
|
+
}): Promise<string | null> {
|
|
4284
|
+
return await this.legacyDurableToolCallCacheKeyForScope({
|
|
4285
|
+
...input,
|
|
4286
|
+
playLocalScope: this.currentGovernance.currentPlayId,
|
|
4287
|
+
});
|
|
4233
4288
|
}
|
|
4234
4289
|
|
|
4235
4290
|
private async resolveToolAuthScopeDigest(
|
|
@@ -5379,6 +5434,23 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
5379
5434
|
return null;
|
|
5380
5435
|
}
|
|
5381
5436
|
|
|
5437
|
+
private canReuseToolResultForCurrentResponseContract(
|
|
5438
|
+
result: unknown,
|
|
5439
|
+
): boolean {
|
|
5440
|
+
if (this.currentToolResponseContract !== RAW_V2_TOOL_RESPONSE_CONTRACT) {
|
|
5441
|
+
return true;
|
|
5442
|
+
}
|
|
5443
|
+
const toolResult = isToolExecuteResult(result)
|
|
5444
|
+
? result
|
|
5445
|
+
: isSerializedToolExecuteResult(result)
|
|
5446
|
+
? deserializeToolExecuteResult(result)
|
|
5447
|
+
: null;
|
|
5448
|
+
return (
|
|
5449
|
+
toolResult !== null &&
|
|
5450
|
+
Object.prototype.hasOwnProperty.call(toolResult.toolResponse, 'rawV2')
|
|
5451
|
+
);
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5382
5454
|
private cacheToolResult(
|
|
5383
5455
|
toolId: string,
|
|
5384
5456
|
rowCacheKey: string,
|
|
@@ -5491,6 +5563,38 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
5491
5563
|
);
|
|
5492
5564
|
}
|
|
5493
5565
|
|
|
5566
|
+
/**
|
|
5567
|
+
* Receipts retain the canonical raw-v2 provider outcome. Older artifacts
|
|
5568
|
+
* receive their legacy projection only at the Play boundary, after cache
|
|
5569
|
+
* lookup and persistence have finished.
|
|
5570
|
+
*/
|
|
5571
|
+
private materializeToolExecutionResult(
|
|
5572
|
+
result: ToolExecuteResult,
|
|
5573
|
+
): ToolExecuteResult {
|
|
5574
|
+
if (
|
|
5575
|
+
this.currentToolResponseContract === RAW_V2_TOOL_RESPONSE_CONTRACT ||
|
|
5576
|
+
!this.hasPinnedToolResponseContract ||
|
|
5577
|
+
!Object.prototype.hasOwnProperty.call(result.toolResponse, 'rawV2')
|
|
5578
|
+
) {
|
|
5579
|
+
return result;
|
|
5580
|
+
}
|
|
5581
|
+
const serialized = serializeToolExecuteResult(result);
|
|
5582
|
+
if (serialized.__kind !== 'deepline.tool_execute_result.v2') {
|
|
5583
|
+
return result;
|
|
5584
|
+
}
|
|
5585
|
+
return deserializeToolExecuteResult({
|
|
5586
|
+
...serialized,
|
|
5587
|
+
__kind: 'deepline.tool_execute_result.v1',
|
|
5588
|
+
toolResponse: {
|
|
5589
|
+
raw: legacyRawFromToolResponseRawV2(
|
|
5590
|
+
serialized.toolResponse.rawV2,
|
|
5591
|
+
serialized.toolResponse.view,
|
|
5592
|
+
serialized.toolResponse.responseMeta,
|
|
5593
|
+
),
|
|
5594
|
+
},
|
|
5595
|
+
});
|
|
5596
|
+
}
|
|
5597
|
+
|
|
5494
5598
|
private attachCustomerDbDatasetResult(
|
|
5495
5599
|
toolId: string,
|
|
5496
5600
|
requestInput: Record<string, unknown> | undefined,
|
|
@@ -9272,16 +9376,26 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9272
9376
|
executionAuthScopeDigest,
|
|
9273
9377
|
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
9274
9378
|
});
|
|
9275
|
-
let
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9379
|
+
let legacyDurableCacheKey = await this.legacyDurableToolCallCacheKey({
|
|
9380
|
+
toolId,
|
|
9381
|
+
requestInput: input,
|
|
9382
|
+
executionAuthScopeDigest,
|
|
9383
|
+
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
9384
|
+
});
|
|
9385
|
+
// Only explicitly fenced operations use this key as a provider identity.
|
|
9386
|
+
// Ordinary calls use it solely as their completed-result cache key.
|
|
9387
|
+
let providerIdempotencyKeyBase = durableCacheKey;
|
|
9388
|
+
let directResultReceiptKey = durableCacheKey;
|
|
9389
|
+
// A raw-v2 artifact must never read an older receipt that cannot render
|
|
9390
|
+
// raw-v2. If both the primary and its revisioned recovery receipt are
|
|
9391
|
+
// unrenderable, make a live call rather than returning either cache entry.
|
|
9392
|
+
// We cannot safely replace an immutable completed receipt, so that rare
|
|
9393
|
+
// incompatible state deliberately forgoes caching until it is repaired.
|
|
9394
|
+
let bypassDirectResultCache = false;
|
|
9395
|
+
const checkpointCacheKeys = [
|
|
9396
|
+
durableCacheKey,
|
|
9397
|
+
...(legacyDurableCacheKey ? [legacyDurableCacheKey] : []),
|
|
9398
|
+
];
|
|
9285
9399
|
|
|
9286
9400
|
const executeTool = async (context?: {
|
|
9287
9401
|
leaseId?: string | null;
|
|
@@ -9310,7 +9424,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9310
9424
|
if (!store) {
|
|
9311
9425
|
const directCacheKey = durableCacheKey;
|
|
9312
9426
|
let initialDirectReceipt: RuntimeStepReceipt | null | undefined;
|
|
9313
|
-
const
|
|
9427
|
+
const cachedCandidate = !cacheableToolResult
|
|
9314
9428
|
? null
|
|
9315
9429
|
: toolCachePolicy.force
|
|
9316
9430
|
? null
|
|
@@ -9319,6 +9433,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9319
9433
|
checkpointCacheKeys,
|
|
9320
9434
|
'direct',
|
|
9321
9435
|
);
|
|
9436
|
+
const cached =
|
|
9437
|
+
cachedCandidate &&
|
|
9438
|
+
this.canReuseToolResultForCurrentResponseContract(
|
|
9439
|
+
cachedCandidate.result.result,
|
|
9440
|
+
)
|
|
9441
|
+
? cachedCandidate
|
|
9442
|
+
: null;
|
|
9322
9443
|
if (cached) {
|
|
9323
9444
|
this.runtimeLog(`Calling tool: ${toolId} recovered from checkpoint`);
|
|
9324
9445
|
// Started and settled are paired in a finally: an uncovered throw
|
|
@@ -9342,7 +9463,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9342
9463
|
outcome: 'cached',
|
|
9343
9464
|
durationMs: 0,
|
|
9344
9465
|
});
|
|
9345
|
-
return recovered;
|
|
9466
|
+
return this.materializeToolExecutionResult(recovered);
|
|
9346
9467
|
} finally {
|
|
9347
9468
|
if (!settled) {
|
|
9348
9469
|
this.emitToolCallLifecycle('settled', toolId, normalizedKey, {
|
|
@@ -9361,17 +9482,36 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9361
9482
|
const historicalReceipt = await this.withRuntimeReceiptHydrationTurn(
|
|
9362
9483
|
async () => await this.getRuntimeStepReceipt(directCacheKey),
|
|
9363
9484
|
);
|
|
9364
|
-
|
|
9485
|
+
const historicalOutput = historicalReceipt
|
|
9486
|
+
? this.runtimeReceiptOutput<unknown>(historicalReceipt)
|
|
9487
|
+
: undefined;
|
|
9488
|
+
const historicalIsTerminal =
|
|
9489
|
+
historicalReceipt?.status === 'completed' ||
|
|
9490
|
+
historicalReceipt?.status === 'skipped';
|
|
9491
|
+
const historicalCanServeCurrentContract =
|
|
9492
|
+
!historicalIsTerminal ||
|
|
9493
|
+
this.canReuseToolResultForCurrentResponseContract(historicalOutput);
|
|
9494
|
+
initialDirectReceipt = historicalCanServeCurrentContract
|
|
9495
|
+
? historicalReceipt
|
|
9496
|
+
: null;
|
|
9497
|
+
if (!historicalCanServeCurrentContract) {
|
|
9498
|
+
if (!legacyDurableCacheKey) {
|
|
9499
|
+
throw new Error(
|
|
9500
|
+
`Tool ${toolId} has an unrenderable legacy result but no response-contract recovery cache key.`,
|
|
9501
|
+
);
|
|
9502
|
+
}
|
|
9503
|
+
directResultReceiptKey = legacyDurableCacheKey;
|
|
9504
|
+
}
|
|
9365
9505
|
if (
|
|
9366
9506
|
historicalReceipt &&
|
|
9367
|
-
|
|
9368
|
-
|
|
9507
|
+
historicalIsTerminal &&
|
|
9508
|
+
historicalCanServeCurrentContract &&
|
|
9369
9509
|
!isSerializedToolExecuteResult(historicalReceipt.output) &&
|
|
9370
9510
|
!isToolExecuteResult(historicalReceipt.output)
|
|
9371
9511
|
) {
|
|
9372
9512
|
const historicalOutput =
|
|
9373
9513
|
this.runtimeReceiptOutput<unknown>(historicalReceipt);
|
|
9374
|
-
|
|
9514
|
+
const recoveredHistorical = markToolExecuteResultExecutionOutcome(
|
|
9375
9515
|
historicalOutput,
|
|
9376
9516
|
toolExecutionOutcomeForDurableReceipt({
|
|
9377
9517
|
source: 'cache',
|
|
@@ -9379,6 +9519,43 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9379
9519
|
cacheKey: directCacheKey,
|
|
9380
9520
|
}),
|
|
9381
9521
|
);
|
|
9522
|
+
return isToolExecuteResult(recoveredHistorical)
|
|
9523
|
+
? this.materializeToolExecutionResult(recoveredHistorical)
|
|
9524
|
+
: recoveredHistorical;
|
|
9525
|
+
}
|
|
9526
|
+
if (
|
|
9527
|
+
(!historicalReceipt || !historicalCanServeCurrentContract) &&
|
|
9528
|
+
legacyDurableCacheKey
|
|
9529
|
+
) {
|
|
9530
|
+
const legacyReceipt = await this.withRuntimeReceiptHydrationTurn(
|
|
9531
|
+
async () =>
|
|
9532
|
+
await this.getRuntimeStepReceipt(legacyDurableCacheKey!),
|
|
9533
|
+
);
|
|
9534
|
+
if (
|
|
9535
|
+
legacyReceipt &&
|
|
9536
|
+
(legacyReceipt.status === 'completed' ||
|
|
9537
|
+
legacyReceipt.status === 'skipped') &&
|
|
9538
|
+
legacyReceipt.output !== undefined
|
|
9539
|
+
) {
|
|
9540
|
+
const recoveredLegacy = markToolExecuteResultExecutionOutcome(
|
|
9541
|
+
this.runtimeReceiptOutput(legacyReceipt),
|
|
9542
|
+
toolExecutionOutcomeForDurableReceipt({
|
|
9543
|
+
source: 'cache',
|
|
9544
|
+
receiptKey: legacyDurableCacheKey,
|
|
9545
|
+
cacheKey: directCacheKey,
|
|
9546
|
+
}),
|
|
9547
|
+
);
|
|
9548
|
+
if (
|
|
9549
|
+
this.canReuseToolResultForCurrentResponseContract(
|
|
9550
|
+
recoveredLegacy,
|
|
9551
|
+
)
|
|
9552
|
+
) {
|
|
9553
|
+
return isToolExecuteResult(recoveredLegacy)
|
|
9554
|
+
? this.materializeToolExecutionResult(recoveredLegacy)
|
|
9555
|
+
: recoveredLegacy;
|
|
9556
|
+
}
|
|
9557
|
+
bypassDirectResultCache = true;
|
|
9558
|
+
}
|
|
9382
9559
|
}
|
|
9383
9560
|
}
|
|
9384
9561
|
this.runtimeLog(
|
|
@@ -9416,23 +9593,27 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9416
9593
|
'Direct tool execution requires a stable logical call identity.',
|
|
9417
9594
|
);
|
|
9418
9595
|
}
|
|
9419
|
-
const
|
|
9596
|
+
const useDirectResultReceipts =
|
|
9597
|
+
cacheableToolResult && !bypassDirectResultCache;
|
|
9598
|
+
const physicalDirectKey = useDirectResultReceipts
|
|
9420
9599
|
? directCacheKey
|
|
9421
9600
|
: `${buildDurableToolReceiptPrefix({
|
|
9422
9601
|
orgId: this.#options.orgId ?? 'unknown-org',
|
|
9423
9602
|
toolId,
|
|
9424
9603
|
})}${stableDigest(`${this.currentRunId}:${logicalCallId}:always-fresh`)}`;
|
|
9604
|
+
const resultReceiptKey = directResultReceiptKey;
|
|
9425
9605
|
const providerOperationKey = providerIdempotencyKeyBase;
|
|
9426
|
-
const resultReceiptKey = directCacheKey;
|
|
9427
9606
|
const toolRetryPolicy = await this.#options
|
|
9428
9607
|
.getToolRetryPolicy?.(toolId, input)
|
|
9429
9608
|
.catch(() => null);
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9609
|
+
if (
|
|
9610
|
+
bypassDirectResultCache &&
|
|
9611
|
+
toolRetryPolicy?.requiresExecutionFence === true
|
|
9612
|
+
) {
|
|
9613
|
+
throw new Error(
|
|
9614
|
+
`Tool ${toolId} has an unrenderable completed result under an execution fence and cannot safely refresh it.`,
|
|
9615
|
+
);
|
|
9616
|
+
}
|
|
9436
9617
|
const dispatcher = {
|
|
9437
9618
|
dispatch: async ({
|
|
9438
9619
|
providerOperationKey: operationKey,
|
|
@@ -9440,7 +9621,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9440
9621
|
providerOperationKey: string | null;
|
|
9441
9622
|
}) => {
|
|
9442
9623
|
if (
|
|
9443
|
-
|
|
9624
|
+
useDirectResultReceipts &&
|
|
9444
9625
|
operationKey !== providerOperationKey
|
|
9445
9626
|
) {
|
|
9446
9627
|
throw new Error(
|
|
@@ -9453,18 +9634,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9453
9634
|
startedAt: directCallStartedAt,
|
|
9454
9635
|
input,
|
|
9455
9636
|
options: {
|
|
9456
|
-
...(
|
|
9637
|
+
...(useDirectResultReceipts
|
|
9457
9638
|
? {
|
|
9458
9639
|
durableCallReceiptKey: physicalDirectKey,
|
|
9640
|
+
completedResultCache: true,
|
|
9459
9641
|
executionAuthScopeDigest,
|
|
9460
|
-
providerIdempotencyReceiptKey: operationKey,
|
|
9461
|
-
providerIdempotencyKey:
|
|
9462
|
-
this.providerIdempotencyKeyForToolCall({
|
|
9463
|
-
cacheKey: providerIdempotencyKeyBase,
|
|
9464
|
-
force: toolCachePolicy.force,
|
|
9465
|
-
leaseId: directReceiptLeaseId,
|
|
9466
|
-
logicalCallId,
|
|
9467
|
-
}),
|
|
9468
9642
|
...(directReceiptLeaseId &&
|
|
9469
9643
|
(this.#options.heartbeatRuntimeStepReceipts ||
|
|
9470
9644
|
this.#options.getRuntimeStepReceipt ||
|
|
@@ -9487,6 +9661,24 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9487
9661
|
: {}),
|
|
9488
9662
|
}
|
|
9489
9663
|
: {}),
|
|
9664
|
+
...(toolRetryPolicy?.requiresExecutionFence === true
|
|
9665
|
+
? {
|
|
9666
|
+
providerIdempotencyReceiptKey:
|
|
9667
|
+
cacheableToolResult && operationKey
|
|
9668
|
+
? operationKey
|
|
9669
|
+
: physicalDirectKey,
|
|
9670
|
+
providerIdempotencyKey:
|
|
9671
|
+
this.providerIdempotencyKeyForToolCall({
|
|
9672
|
+
cacheKey:
|
|
9673
|
+
useDirectResultReceipts && operationKey
|
|
9674
|
+
? providerIdempotencyKeyBase
|
|
9675
|
+
: physicalDirectKey,
|
|
9676
|
+
force: cacheableToolResult && toolCachePolicy.force,
|
|
9677
|
+
leaseId: directReceiptLeaseId,
|
|
9678
|
+
logicalCallId,
|
|
9679
|
+
}),
|
|
9680
|
+
}
|
|
9681
|
+
: {}),
|
|
9490
9682
|
playNodeScope: buildPlayNodeScope({
|
|
9491
9683
|
toolId,
|
|
9492
9684
|
callKey: normalizedKey,
|
|
@@ -9511,14 +9703,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9511
9703
|
execution: toolExecutionMetadataForOutcome({
|
|
9512
9704
|
kind: 'live',
|
|
9513
9705
|
cacheKey: directCacheKey,
|
|
9514
|
-
...(
|
|
9706
|
+
...(useDirectResultReceipts
|
|
9515
9707
|
? { receiptKey: resultReceiptKey }
|
|
9516
9708
|
: {}),
|
|
9517
9709
|
}),
|
|
9518
9710
|
});
|
|
9519
9711
|
},
|
|
9520
9712
|
};
|
|
9521
|
-
const toolCall =
|
|
9713
|
+
const toolCall = useDirectResultReceipts
|
|
9522
9714
|
? createToolCallJob({
|
|
9523
9715
|
resultReceipts: createRuntimeStepToolResultReceipts({
|
|
9524
9716
|
store: this.durableReceiptExecutionStore(),
|
|
@@ -9526,7 +9718,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9526
9718
|
...(initialDirectReceipt !== undefined
|
|
9527
9719
|
? {
|
|
9528
9720
|
initialReceipt: {
|
|
9529
|
-
key:
|
|
9721
|
+
key: directResultReceiptKey,
|
|
9530
9722
|
receipt: initialDirectReceipt,
|
|
9531
9723
|
},
|
|
9532
9724
|
}
|
|
@@ -9537,6 +9729,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9537
9729
|
? {
|
|
9538
9730
|
executionLock: {
|
|
9539
9731
|
required: true,
|
|
9732
|
+
key: providerOperationKey,
|
|
9540
9733
|
ttlMs: Math.min(
|
|
9541
9734
|
600_000,
|
|
9542
9735
|
resolveToolRuntimeTimeoutMs(
|
|
@@ -9599,11 +9792,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9599
9792
|
})
|
|
9600
9793
|
: await createAlwaysFreshToolCallAdapter({ dispatcher }).call(call);
|
|
9601
9794
|
settleDirectCall(toolAttemptOutcomeForResult(wrapped));
|
|
9602
|
-
if (
|
|
9603
|
-
this.cacheToolResult(
|
|
9795
|
+
if (useDirectResultReceipts) {
|
|
9796
|
+
this.cacheToolResult(
|
|
9797
|
+
toolId,
|
|
9798
|
+
directResultReceiptKey,
|
|
9799
|
+
wrapped,
|
|
9800
|
+
'direct',
|
|
9801
|
+
);
|
|
9604
9802
|
this.#options.onBatchComplete?.(this.checkpoint);
|
|
9605
9803
|
}
|
|
9606
|
-
return wrapped;
|
|
9804
|
+
return this.materializeToolExecutionResult(wrapped);
|
|
9607
9805
|
} catch (error) {
|
|
9608
9806
|
settleDirectCall('failed', error);
|
|
9609
9807
|
throw error;
|
|
@@ -9637,17 +9835,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9637
9835
|
: this.getCachedToolResultCandidate(toolId, checkpointCacheKeys);
|
|
9638
9836
|
if (cached) {
|
|
9639
9837
|
this.runtimeLog(` Row ${rowId} ${toolId}: recovered from checkpoint`);
|
|
9640
|
-
return
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9838
|
+
return this.materializeToolExecutionResult(
|
|
9839
|
+
await this.wrapToolExecutionResult({
|
|
9840
|
+
toolId,
|
|
9841
|
+
status: cached.result.result == null ? 'no_result' : 'completed',
|
|
9842
|
+
result: cached.result.result,
|
|
9843
|
+
requestInput: input,
|
|
9844
|
+
execution: toolExecutionMetadataForOutcome({
|
|
9845
|
+
kind: 'checkpoint',
|
|
9846
|
+
cacheKey: cached.cacheKey,
|
|
9847
|
+
}),
|
|
9648
9848
|
}),
|
|
9649
|
-
|
|
9849
|
+
);
|
|
9650
9850
|
}
|
|
9851
|
+
const rowToolRetryPolicy = await this.#options
|
|
9852
|
+
.getToolRetryPolicy?.(toolId, input)
|
|
9853
|
+
.catch(() => null);
|
|
9651
9854
|
|
|
9652
9855
|
// ADR 0018: record this leg before it runs. `producer` still names the
|
|
9653
9856
|
// last leg (the usual cascade winner); `producers` keeps every leg, which
|
|
@@ -9702,7 +9905,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9702
9905
|
providerIdempotencyKeyBase,
|
|
9703
9906
|
cacheable: cacheableToolResult,
|
|
9704
9907
|
receiptKey: cacheableToolResult ? durableCacheKey : null,
|
|
9908
|
+
legacyReceiptKeys: legacyDurableCacheKey
|
|
9909
|
+
? [legacyDurableCacheKey]
|
|
9910
|
+
: [],
|
|
9705
9911
|
executionAuthScopeDigest,
|
|
9912
|
+
requiresExecutionFence:
|
|
9913
|
+
rowToolRetryPolicy?.requiresExecutionFence === true,
|
|
9706
9914
|
force: toolCachePolicy.force,
|
|
9707
9915
|
forceFailedRefresh: toolCachePolicy.forceFailedRefresh,
|
|
9708
9916
|
rowId,
|
|
@@ -9722,7 +9930,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9722
9930
|
return await pendingToolCall.then(
|
|
9723
9931
|
(value) => {
|
|
9724
9932
|
settleProducerAttempt(toolAttemptOutcomeForResult(value));
|
|
9725
|
-
return
|
|
9933
|
+
return this.materializeToolExecutionResult(
|
|
9934
|
+
value as ToolExecuteResult,
|
|
9935
|
+
);
|
|
9726
9936
|
},
|
|
9727
9937
|
(error: unknown) => {
|
|
9728
9938
|
settleProducerAttempt('failed');
|
|
@@ -9771,16 +9981,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
9771
9981
|
executionAuthScopeDigest,
|
|
9772
9982
|
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
9773
9983
|
});
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9984
|
+
legacyDurableCacheKey = await this.legacyDurableToolCallCacheKey({
|
|
9985
|
+
toolId,
|
|
9986
|
+
requestInput: input,
|
|
9987
|
+
executionAuthScopeDigest,
|
|
9988
|
+
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
9989
|
+
});
|
|
9990
|
+
providerIdempotencyKeyBase = durableCacheKey;
|
|
9991
|
+
directResultReceiptKey = durableCacheKey;
|
|
9992
|
+
bypassDirectResultCache = false;
|
|
9993
|
+
checkpointCacheKeys.splice(
|
|
9994
|
+
0,
|
|
9995
|
+
checkpointCacheKeys.length,
|
|
9996
|
+
durableCacheKey,
|
|
9997
|
+
...(legacyDurableCacheKey ? [legacyDurableCacheKey] : []),
|
|
9782
9998
|
);
|
|
9783
|
-
checkpointCacheKeys[0] = durableCacheKey;
|
|
9784
9999
|
}
|
|
9785
10000
|
}
|
|
9786
10001
|
|
|
@@ -11009,75 +11224,59 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11009
11224
|
/**
|
|
11010
11225
|
* Concrete RuntimeStepReceipt adapter for the map receipt-cohort Module.
|
|
11011
11226
|
*
|
|
11012
|
-
*
|
|
11013
|
-
*
|
|
11014
|
-
* protocol: both need an atomic claim/reclaim, a single cohort heartbeat,
|
|
11015
|
-
* and lease-fenced terminal fanout. Keeping that at one seam prevents a
|
|
11016
|
-
* native batch from becoming a second, subtly different receipt runtime.
|
|
11227
|
+
* Scalar and provider-native map calls share completed-result cache reads
|
|
11228
|
+
* and atomic successful inserts. A miss has no durable execution owner.
|
|
11017
11229
|
*/
|
|
11018
11230
|
private createRuntimeStepToolCallReceiptCohortStore(input: {
|
|
11019
11231
|
toolId: string;
|
|
11020
11232
|
requestForReceipt: ReadonlyMap<string, ToolCallRequest>;
|
|
11021
11233
|
}): ToolCallReceiptCohortStore {
|
|
11022
11234
|
const asRecord = async (
|
|
11023
|
-
|
|
11235
|
+
resultReceiptKey: string,
|
|
11236
|
+
storedReceiptKey: string,
|
|
11024
11237
|
receipt: RuntimeStepReceipt | null | undefined,
|
|
11025
11238
|
): Promise<ToolCallReceiptRecord | null> => {
|
|
11026
11239
|
if (!receipt) return null;
|
|
11027
|
-
const normalized = this.normalizeRuntimeStepReceipt(
|
|
11240
|
+
const normalized = this.normalizeRuntimeStepReceipt(
|
|
11241
|
+
storedReceiptKey,
|
|
11242
|
+
receipt,
|
|
11243
|
+
);
|
|
11028
11244
|
if (!normalized) return null;
|
|
11029
|
-
|
|
11245
|
+
if (
|
|
11246
|
+
normalized.status !== 'completed' &&
|
|
11247
|
+
normalized.status !== 'skipped'
|
|
11248
|
+
) {
|
|
11249
|
+
return null;
|
|
11250
|
+
}
|
|
11251
|
+
// A terminal acknowledgement without output is not a cache hit. The
|
|
11252
|
+
// completion path hydrates it first; if that read cannot provide an
|
|
11253
|
+
// output, retain the live result rather than resolving `undefined`.
|
|
11254
|
+
if (normalized.output === undefined) return null;
|
|
11255
|
+
const request = input.requestForReceipt.get(resultReceiptKey);
|
|
11030
11256
|
if (!request) {
|
|
11031
11257
|
throw new Error(
|
|
11032
|
-
`Unknown Tool Call Receipt Cohort receipt ${
|
|
11258
|
+
`Unknown Tool Call Receipt Cohort receipt ${resultReceiptKey}.`,
|
|
11033
11259
|
);
|
|
11034
11260
|
}
|
|
11035
|
-
const
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
attachedToReceiptKey: receiptKey,
|
|
11051
|
-
}),
|
|
11052
|
-
})
|
|
11053
|
-
: undefined;
|
|
11261
|
+
const result = await this.wrapToolExecutionResult({
|
|
11262
|
+
toolId: input.toolId,
|
|
11263
|
+
status:
|
|
11264
|
+
normalized.output === null || normalized.output === undefined
|
|
11265
|
+
? 'no_result'
|
|
11266
|
+
: 'completed',
|
|
11267
|
+
result: this.runtimeReceiptOutput(normalized),
|
|
11268
|
+
requestInput: request.input,
|
|
11269
|
+
execution: toolExecutionMetadataForOutcome({
|
|
11270
|
+
kind: 'cache',
|
|
11271
|
+
cacheKey: request.cacheKey,
|
|
11272
|
+
receiptKey: resultReceiptKey,
|
|
11273
|
+
attachedToReceiptKey: storedReceiptKey,
|
|
11274
|
+
}),
|
|
11275
|
+
});
|
|
11054
11276
|
return {
|
|
11055
|
-
resultReceiptKey
|
|
11277
|
+
resultReceiptKey,
|
|
11056
11278
|
status: normalized.status,
|
|
11057
|
-
|
|
11058
|
-
...(normalized.status === 'failed'
|
|
11059
|
-
? {
|
|
11060
|
-
error: runtimeReceiptFailureError(
|
|
11061
|
-
normalized,
|
|
11062
|
-
'Durable tool call failed',
|
|
11063
|
-
this.currentToolErrorSchemaVersion,
|
|
11064
|
-
),
|
|
11065
|
-
}
|
|
11066
|
-
: {}),
|
|
11067
|
-
...(normalized.leaseId &&
|
|
11068
|
-
(normalized.leaseOwnerRunId ?? normalized.runId)?.trim()
|
|
11069
|
-
? {
|
|
11070
|
-
lease: {
|
|
11071
|
-
leaseId: normalized.leaseId,
|
|
11072
|
-
ownerRunId: (normalized.leaseOwnerRunId ??
|
|
11073
|
-
normalized.runId)!.trim(),
|
|
11074
|
-
},
|
|
11075
|
-
}
|
|
11076
|
-
: {}),
|
|
11077
|
-
...(normalized.leaseExpiresAt
|
|
11078
|
-
? { leaseExpiresAt: normalized.leaseExpiresAt }
|
|
11079
|
-
: {}),
|
|
11080
|
-
...(normalized.claimState ? { claimState: normalized.claimState } : {}),
|
|
11279
|
+
result,
|
|
11081
11280
|
};
|
|
11082
11281
|
};
|
|
11083
11282
|
const recordsFor = async (
|
|
@@ -11086,10 +11285,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11086
11285
|
): Promise<ToolCallReceiptRecord[]> =>
|
|
11087
11286
|
(
|
|
11088
11287
|
await Promise.all(
|
|
11089
|
-
receiptKeys.map(
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11288
|
+
receiptKeys.map(async (receiptKey) => {
|
|
11289
|
+
const request = input.requestForReceipt.get(receiptKey);
|
|
11290
|
+
const candidates = [
|
|
11291
|
+
receiptKey,
|
|
11292
|
+
...(request?.legacyReceiptKeys ?? []),
|
|
11293
|
+
];
|
|
11294
|
+
for (const candidate of candidates) {
|
|
11295
|
+
const record = await asRecord(
|
|
11296
|
+
receiptKey,
|
|
11297
|
+
candidate,
|
|
11298
|
+
receipts.get(candidate),
|
|
11299
|
+
);
|
|
11300
|
+
if (record) return record;
|
|
11301
|
+
}
|
|
11302
|
+
return null;
|
|
11303
|
+
}),
|
|
11093
11304
|
)
|
|
11094
11305
|
).filter((record): record is ToolCallReceiptRecord => record !== null);
|
|
11095
11306
|
|
|
@@ -11099,83 +11310,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11099
11310
|
async () =>
|
|
11100
11311
|
await recordsFor(
|
|
11101
11312
|
resultReceiptKeys,
|
|
11102
|
-
await this.getRuntimeStepReceipts(
|
|
11313
|
+
await this.getRuntimeStepReceipts(
|
|
11314
|
+
[...new Set(
|
|
11315
|
+
resultReceiptKeys.flatMap((receiptKey) => [
|
|
11316
|
+
receiptKey,
|
|
11317
|
+
...(input.requestForReceipt.get(receiptKey)
|
|
11318
|
+
?.legacyReceiptKeys ?? []),
|
|
11319
|
+
]),
|
|
11320
|
+
)],
|
|
11321
|
+
),
|
|
11103
11322
|
),
|
|
11104
11323
|
),
|
|
11105
|
-
claim: async ({ receipts }) => {
|
|
11106
|
-
const claims = new Map<string, RuntimeStepReceipt>();
|
|
11107
|
-
const groups = new Map<string, Array<(typeof receipts)[number]>>();
|
|
11108
|
-
for (const receipt of receipts) {
|
|
11109
|
-
const groupKey = [
|
|
11110
|
-
receipt.reclaimRunning ? 'reclaim' : 'ordinary',
|
|
11111
|
-
receipt.force ? 'force' : 'cached',
|
|
11112
|
-
receipt.forceFailedRefresh ? 'failed-force' : 'failed-cached',
|
|
11113
|
-
].join(':');
|
|
11114
|
-
const group = groups.get(groupKey) ?? [];
|
|
11115
|
-
group.push(receipt);
|
|
11116
|
-
groups.set(groupKey, group);
|
|
11117
|
-
}
|
|
11118
|
-
for (const group of groups.values()) {
|
|
11119
|
-
const first = group[0]!;
|
|
11120
|
-
const claimed = await this.claimRuntimeStepReceipts(
|
|
11121
|
-
group.map((receipt) => receipt.resultReceiptKey),
|
|
11122
|
-
this.currentReceiptOwnerRunId,
|
|
11123
|
-
first.reclaimRunning,
|
|
11124
|
-
first.force,
|
|
11125
|
-
first.forceFailedRefresh,
|
|
11126
|
-
);
|
|
11127
|
-
for (const [key, receipt] of claimed) claims.set(key, receipt);
|
|
11128
|
-
}
|
|
11129
|
-
const safeFailedKeys = receipts.flatMap((receipt) => {
|
|
11130
|
-
const claimed = claims.get(receipt.resultReceiptKey);
|
|
11131
|
-
return claimed && this.canRetryWithoutProviderDispatch(claimed)
|
|
11132
|
-
? [receipt.resultReceiptKey]
|
|
11133
|
-
: [];
|
|
11134
|
-
});
|
|
11135
|
-
if (safeFailedKeys.length > 0) {
|
|
11136
|
-
const refreshed = await this.claimRuntimeStepReceipts(
|
|
11137
|
-
safeFailedKeys,
|
|
11138
|
-
this.currentReceiptOwnerRunId,
|
|
11139
|
-
false,
|
|
11140
|
-
false,
|
|
11141
|
-
true,
|
|
11142
|
-
);
|
|
11143
|
-
for (const [key, receipt] of refreshed) claims.set(key, receipt);
|
|
11144
|
-
}
|
|
11145
|
-
return await recordsFor(
|
|
11146
|
-
receipts.map((receipt) => receipt.resultReceiptKey),
|
|
11147
|
-
claims,
|
|
11148
|
-
);
|
|
11149
|
-
},
|
|
11150
|
-
heartbeat: async ({ receipts }) => {
|
|
11151
|
-
if (!this.#options.heartbeatRuntimeStepReceipts) {
|
|
11152
|
-
return await recordsFor(
|
|
11153
|
-
receipts.map((receipt) => receipt.resultReceiptKey),
|
|
11154
|
-
await this.getRuntimeStepReceipts(
|
|
11155
|
-
receipts.map((receipt) => receipt.resultReceiptKey),
|
|
11156
|
-
),
|
|
11157
|
-
);
|
|
11158
|
-
}
|
|
11159
|
-
const response = await this.#options.heartbeatRuntimeStepReceipts({
|
|
11160
|
-
runId: this.currentReceiptOwnerRunId,
|
|
11161
|
-
runAttempt: this.currentRunAttempt,
|
|
11162
|
-
keys: receipts.map((receipt) => receipt.resultReceiptKey),
|
|
11163
|
-
leaseIds: receipts.map((receipt) => receipt.lease.leaseId),
|
|
11164
|
-
});
|
|
11165
|
-
const byKey = new Map<string, RuntimeStepReceipt>();
|
|
11166
|
-
for (let index = 0; index < receipts.length; index += 1) {
|
|
11167
|
-
const request = receipts[index]!;
|
|
11168
|
-
const normalized = this.normalizeRuntimeStepReceipt(
|
|
11169
|
-
request.resultReceiptKey,
|
|
11170
|
-
response[index],
|
|
11171
|
-
);
|
|
11172
|
-
if (normalized) byKey.set(request.resultReceiptKey, normalized);
|
|
11173
|
-
}
|
|
11174
|
-
return await recordsFor(
|
|
11175
|
-
receipts.map((receipt) => receipt.resultReceiptKey),
|
|
11176
|
-
byKey,
|
|
11177
|
-
);
|
|
11178
|
-
},
|
|
11179
11324
|
complete: async ({ receipts }) => {
|
|
11180
11325
|
const completed = await this.completeRuntimeStepReceipts(
|
|
11181
11326
|
receipts.map((receipt) => ({
|
|
@@ -11190,33 +11335,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11190
11335
|
completed,
|
|
11191
11336
|
);
|
|
11192
11337
|
},
|
|
11193
|
-
fail: async ({ receipts }) => {
|
|
11194
|
-
const failed = await this.failRuntimeStepReceipts(
|
|
11195
|
-
receipts.map((receipt) => ({
|
|
11196
|
-
key: receipt.resultReceiptKey,
|
|
11197
|
-
runId: this.currentReceiptOwnerRunId,
|
|
11198
|
-
leaseId: receipt.lease.leaseId,
|
|
11199
|
-
error: this.formatRuntimeError(receipt.error),
|
|
11200
|
-
errorPayload: serializeToolExecutionFailure(receipt.error),
|
|
11201
|
-
failureKind: runtimeReceiptFailureKindForError(receipt.error),
|
|
11202
|
-
})),
|
|
11203
|
-
);
|
|
11204
|
-
return await recordsFor(
|
|
11205
|
-
receipts.map((receipt) => receipt.resultReceiptKey),
|
|
11206
|
-
failed,
|
|
11207
|
-
);
|
|
11208
|
-
},
|
|
11209
11338
|
};
|
|
11210
11339
|
}
|
|
11211
11340
|
|
|
11212
11341
|
/**
|
|
11213
|
-
*
|
|
11214
|
-
*
|
|
11215
|
-
*
|
|
11216
|
-
* Tool Call boundary to be only cosmetic. This is the narrow replacement
|
|
11217
|
-
* slice: cacheable members of a *non-native* map hand their whole receipt
|
|
11218
|
-
* lifecycle to the cohort Module, while this Context remains only the
|
|
11219
|
-
* transport/presentation Adapter.
|
|
11342
|
+
* Cacheable scalar map members share completed-result reads and successful
|
|
11343
|
+
* writes through the cohort Module. This Context only transports and
|
|
11344
|
+
* presents results.
|
|
11220
11345
|
*
|
|
11221
11346
|
* Do not widen this to physical native batches or always-fresh calls. A
|
|
11222
11347
|
* native batch owns a different aggregate operation receipt, and
|
|
@@ -11318,7 +11443,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11318
11443
|
const resolve = async (
|
|
11319
11444
|
request: ToolCallRequest,
|
|
11320
11445
|
result: ToolExecuteResult,
|
|
11321
|
-
source: 'cache' | '
|
|
11446
|
+
source: 'cache' | 'owner',
|
|
11322
11447
|
): Promise<void> => {
|
|
11323
11448
|
const receiptKey = request.receiptKey!;
|
|
11324
11449
|
const finalResult = cloneToolExecuteResultWithExecution(
|
|
@@ -11353,180 +11478,111 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11353
11478
|
};
|
|
11354
11479
|
const reject = async (request: ToolCallRequest, error: unknown) => {
|
|
11355
11480
|
await this.rejectToolCall(toolId, request, error, {
|
|
11356
|
-
//
|
|
11357
|
-
// legacy writer here would race its member terminal transition.
|
|
11481
|
+
// Failures are never cache entries, including failed local misses.
|
|
11358
11482
|
persistReceiptFailure: false,
|
|
11359
11483
|
});
|
|
11360
11484
|
};
|
|
11361
11485
|
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11486
|
+
const handled = new Set<string>();
|
|
11487
|
+
const followerResults: Promise<void>[] = [];
|
|
11488
|
+
const collectReadyMembers = (): Array<{
|
|
11489
|
+
request: ToolCallRequest;
|
|
11490
|
+
receipts: ToolResultReceipts;
|
|
11491
|
+
}> => {
|
|
11492
|
+
const owned: Array<{
|
|
11366
11493
|
request: ToolCallRequest;
|
|
11367
11494
|
receipts: ToolResultReceipts;
|
|
11368
|
-
}>
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
} else if (member.kind === 'following') {
|
|
11385
|
-
followOrRecover.push(
|
|
11386
|
-
member.result.then(
|
|
11387
|
-
async (result) => await resolve(request, result, 'in_flight'),
|
|
11388
|
-
async (error: unknown) => await reject(request, error),
|
|
11389
|
-
),
|
|
11390
|
-
);
|
|
11391
|
-
} else {
|
|
11392
|
-
request.receiptLeaseId = member.lease.leaseId;
|
|
11393
|
-
request.receiptLeaseExpiresAt = member.leaseExpiresAt ?? null;
|
|
11394
|
-
owned.push({ request, receipts: member.receipts });
|
|
11395
|
-
}
|
|
11495
|
+
}> = [];
|
|
11496
|
+
for (const request of requests) {
|
|
11497
|
+
if (handled.has(request.callId)) continue;
|
|
11498
|
+
const member = cohort.member(request.callId);
|
|
11499
|
+
handled.add(request.callId);
|
|
11500
|
+
if (member.kind === 'completed') {
|
|
11501
|
+
followerResults.push(resolve(request, member.result, member.source));
|
|
11502
|
+
} else if (member.kind === 'following') {
|
|
11503
|
+
followerResults.push(
|
|
11504
|
+
member.result.then(
|
|
11505
|
+
async (result) => await resolve(request, result, 'owner'),
|
|
11506
|
+
async (error: unknown) => await reject(request, error),
|
|
11507
|
+
),
|
|
11508
|
+
);
|
|
11509
|
+
} else {
|
|
11510
|
+
owned.push({ request, receipts: member.receipts });
|
|
11396
11511
|
}
|
|
11397
|
-
|
|
11398
|
-
|
|
11512
|
+
}
|
|
11513
|
+
return owned;
|
|
11514
|
+
};
|
|
11399
11515
|
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
),
|
|
11516
|
+
const width = Math.min(
|
|
11517
|
+
this.governor.policy.concurrency.toolCalls,
|
|
11518
|
+
Math.max(
|
|
11519
|
+
1,
|
|
11520
|
+
await this.resourceGovernor.suggestedToolParallelism(
|
|
11521
|
+
toolId,
|
|
11522
|
+
this.governor.policy.concurrency.toolCalls,
|
|
11408
11523
|
),
|
|
11409
|
-
)
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
|
|
11438
|
-
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
);
|
|
11449
|
-
}
|
|
11450
|
-
} catch (error) {
|
|
11451
|
-
if (error instanceof ToolCallReceiptLeaseLostError) {
|
|
11452
|
-
throw new RuntimeReceiptLeaseLostError({
|
|
11453
|
-
receiptKey: request.receiptKey!,
|
|
11454
|
-
runId: this.currentReceiptOwnerRunId,
|
|
11455
|
-
leaseId:
|
|
11456
|
-
request.receiptLeaseId ?? 'missing-cohort-lease',
|
|
11457
|
-
});
|
|
11458
|
-
}
|
|
11459
|
-
throw error;
|
|
11460
|
-
}
|
|
11461
|
-
},
|
|
11462
|
-
playNodeScope: playNodeScopeForToolCallRequest(request),
|
|
11463
|
-
durableCallReceiptKey: request.receiptKey,
|
|
11464
|
-
executionAuthScopeDigest: request.executionAuthScopeDigest,
|
|
11465
|
-
providerIdempotencyReceiptKey:
|
|
11466
|
-
request.providerIdempotencyKeyBase ?? request.receiptKey,
|
|
11467
|
-
providerIdempotencyKey:
|
|
11468
|
-
this.providerIdempotencyKeyForToolCall({
|
|
11469
|
-
cacheKey:
|
|
11470
|
-
request.providerIdempotencyKeyBase ??
|
|
11471
|
-
request.receiptKey!,
|
|
11472
|
-
force: request.force === true,
|
|
11473
|
-
leaseId: request.receiptLeaseId,
|
|
11474
|
-
logicalCallId: request.logicalCallId,
|
|
11475
|
-
}),
|
|
11476
|
-
receiptLeaseExpiresAt: request.receiptLeaseExpiresAt,
|
|
11477
|
-
timeoutMs:
|
|
11478
|
-
typeof request.timeoutMs === 'number' &&
|
|
11479
|
-
request.timeoutMs > 0
|
|
11480
|
-
? request.timeoutMs
|
|
11481
|
-
: undefined,
|
|
11482
|
-
retainToolSlot: (release) => {
|
|
11483
|
-
releaseToolSlot = release;
|
|
11484
|
-
},
|
|
11524
|
+
),
|
|
11525
|
+
);
|
|
11526
|
+
const dispatchOwned = async (
|
|
11527
|
+
owned: Array<{
|
|
11528
|
+
request: ToolCallRequest;
|
|
11529
|
+
receipts: ToolResultReceipts;
|
|
11530
|
+
}>,
|
|
11531
|
+
) =>
|
|
11532
|
+
await dispatchBoundedSettled(
|
|
11533
|
+
owned,
|
|
11534
|
+
width,
|
|
11535
|
+
async ({ request, receipts }) => {
|
|
11536
|
+
if (this.persistenceLatch.tripped) {
|
|
11537
|
+
const error = new RuntimePersistenceCircuitOpenError(
|
|
11538
|
+
this.persistenceLatch,
|
|
11539
|
+
);
|
|
11540
|
+
throw error;
|
|
11541
|
+
}
|
|
11542
|
+
let releaseToolSlot: () => void = () => undefined;
|
|
11543
|
+
try {
|
|
11544
|
+
const result = await this.executeMappedToolCallThroughModule({
|
|
11545
|
+
request,
|
|
11546
|
+
dispatchToolId: toolId,
|
|
11547
|
+
dispatchPayload: request.input,
|
|
11548
|
+
resultReceiptKey: request.receiptKey!,
|
|
11549
|
+
providerOperationKey:
|
|
11550
|
+
request.providerIdempotencyKeyBase ?? request.receiptKey!,
|
|
11551
|
+
resultReceipts: receipts,
|
|
11552
|
+
apiOptions: {
|
|
11553
|
+
playNodeScope: playNodeScopeForToolCallRequest(request),
|
|
11554
|
+
durableCallReceiptKey: request.receiptKey,
|
|
11555
|
+
completedResultCache: true,
|
|
11556
|
+
executionAuthScopeDigest: request.executionAuthScopeDigest,
|
|
11557
|
+
timeoutMs:
|
|
11558
|
+
typeof request.timeoutMs === 'number' && request.timeoutMs > 0
|
|
11559
|
+
? request.timeoutMs
|
|
11560
|
+
: undefined,
|
|
11561
|
+
retainToolSlot: (release) => {
|
|
11562
|
+
releaseToolSlot = release;
|
|
11485
11563
|
},
|
|
11486
|
-
}
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11493
|
-
}
|
|
11494
|
-
|
|
11564
|
+
},
|
|
11565
|
+
});
|
|
11566
|
+
await resolve(request, result, 'owner');
|
|
11567
|
+
} catch (error) {
|
|
11568
|
+
await reject(request, error);
|
|
11569
|
+
} finally {
|
|
11570
|
+
releaseToolSlot();
|
|
11571
|
+
}
|
|
11572
|
+
},
|
|
11573
|
+
);
|
|
11495
11574
|
|
|
11496
|
-
|
|
11497
|
-
|
|
11498
|
-
// overlap: a broken foreign read must not strand an already-owned
|
|
11499
|
-
// provider operation behind it.
|
|
11500
|
-
const initialDispatch = dispatchOwned(collectReadyMembers());
|
|
11501
|
-
const settlement = cohort.settle();
|
|
11502
|
-
const [initialResult, settlementResult] = await Promise.allSettled([
|
|
11503
|
-
initialDispatch,
|
|
11504
|
-
settlement,
|
|
11505
|
-
]);
|
|
11506
|
-
// A recovery can reclaim an expired receipt and turn it into another
|
|
11507
|
-
// owned member. Collect its final state after the initial work settles.
|
|
11508
|
-
const recoveredDispatch = await dispatchOwned(collectReadyMembers());
|
|
11509
|
-
await Promise.allSettled(followOrRecover);
|
|
11510
|
-
// dispatchBoundedSettled intentionally captures provider failures; all
|
|
11511
|
-
// affected row resolvers have already been rejected above.
|
|
11512
|
-
void initialResult;
|
|
11513
|
-
void recoveredDispatch;
|
|
11514
|
-
if (settlementResult.status === 'rejected') {
|
|
11515
|
-
throw settlementResult.reason;
|
|
11516
|
-
}
|
|
11517
|
-
} finally {
|
|
11518
|
-
heartbeat.stop();
|
|
11519
|
-
}
|
|
11575
|
+
await dispatchOwned(collectReadyMembers());
|
|
11576
|
+
await Promise.allSettled(followerResults);
|
|
11520
11577
|
return successful;
|
|
11521
11578
|
}
|
|
11522
11579
|
|
|
11523
11580
|
/**
|
|
11524
11581
|
* Executes provider-native batches through the Native Batch Tool Call
|
|
11525
|
-
* Module.
|
|
11526
|
-
*
|
|
11527
|
-
*
|
|
11528
|
-
*
|
|
11529
|
-
* to map rows.
|
|
11582
|
+
* Module. The Module reads exact completed-result cache entries, batches
|
|
11583
|
+
* misses, then publishes each successful split result. This Context only
|
|
11584
|
+
* compiles provider payloads, invokes transport, and presents terminal
|
|
11585
|
+
* member values to map rows.
|
|
11530
11586
|
*/
|
|
11531
11587
|
private async executeCacheableNativeMapToolCallCohort(input: {
|
|
11532
11588
|
toolId: string;
|
|
@@ -11581,9 +11637,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11581
11637
|
batchParallelismCeiling,
|
|
11582
11638
|
)
|
|
11583
11639
|
: batchParallelismCeiling;
|
|
11584
|
-
const batchProviderActionVersion =
|
|
11585
|
-
(await this.#options.getToolActionCacheVersion?.(toolId))?.trim() ??
|
|
11586
|
-
'unversioned';
|
|
11587
11640
|
const successful: ToolCallRequest[] = [];
|
|
11588
11641
|
|
|
11589
11642
|
const deliver = (request: ToolCallRequest, result: ToolExecuteResult) => {
|
|
@@ -11613,7 +11666,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11613
11666
|
};
|
|
11614
11667
|
const reject = async (request: ToolCallRequest, error: unknown) => {
|
|
11615
11668
|
await this.rejectToolCall(toolId, request, error, {
|
|
11616
|
-
//
|
|
11669
|
+
// Failed provider calls are never cache entries.
|
|
11617
11670
|
persistReceiptFailure: false,
|
|
11618
11671
|
});
|
|
11619
11672
|
};
|
|
@@ -11675,15 +11728,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11675
11728
|
call.payload,
|
|
11676
11729
|
{
|
|
11677
11730
|
durableCallReceiptKey: compatibility.resultReceiptKey,
|
|
11731
|
+
completedResultCache: true,
|
|
11678
11732
|
playNodeScope: playNodeScopeForToolCallRequest(
|
|
11679
11733
|
detail.members[0]!,
|
|
11680
11734
|
call.tool.id,
|
|
11681
11735
|
),
|
|
11682
11736
|
executionAuthScopeDigest:
|
|
11683
11737
|
detail.members[0]?.executionAuthScopeDigest ?? null,
|
|
11684
|
-
providerIdempotencyReceiptKey:
|
|
11685
|
-
detail.providerOperationKey,
|
|
11686
|
-
providerIdempotencyKey: detail.providerOperationKey,
|
|
11687
11738
|
receiptLeaseExpiresAt: detail.receiptLeaseExpiresAt,
|
|
11688
11739
|
timeoutMs: detail.timeoutMs,
|
|
11689
11740
|
},
|
|
@@ -11751,8 +11802,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11751
11802
|
},
|
|
11752
11803
|
compatibility: {
|
|
11753
11804
|
resultReceiptKey: request.receiptKey!,
|
|
11754
|
-
providerOperationKey:
|
|
11755
|
-
request.providerIdempotencyKeyBase ?? request.receiptKey!,
|
|
11805
|
+
providerOperationKey: request.receiptKey!,
|
|
11756
11806
|
},
|
|
11757
11807
|
},
|
|
11758
11808
|
})),
|
|
@@ -11775,27 +11825,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11775
11825
|
'Native batch owned members changed provider operation during compilation.',
|
|
11776
11826
|
);
|
|
11777
11827
|
}
|
|
11778
|
-
const providerOperationKey =
|
|
11779
|
-
buildDurableToolPayloadProviderIdempotencyKey({
|
|
11780
|
-
orgId: this.#options.orgId,
|
|
11781
|
-
toolId: batch.batchOperation,
|
|
11782
|
-
executionAuthScopeDigest:
|
|
11783
|
-
owned[0]?.executionAuthScopeDigest ?? null,
|
|
11784
|
-
providerActionVersion: batchProviderActionVersion,
|
|
11785
|
-
payload: compiled.batchPayload,
|
|
11786
|
-
forceRefreshIdentities: owned
|
|
11787
|
-
.filter((request) => request.force === true)
|
|
11788
|
-
.map((request) =>
|
|
11789
|
-
this.providerIdempotencyKeyForToolCall({
|
|
11790
|
-
cacheKey:
|
|
11791
|
-
request.providerIdempotencyKeyBase ??
|
|
11792
|
-
request.receiptKey!,
|
|
11793
|
-
force: true,
|
|
11794
|
-
leaseId: request.receiptLeaseId,
|
|
11795
|
-
logicalCallId: request.logicalCallId,
|
|
11796
|
-
}),
|
|
11797
|
-
),
|
|
11798
|
-
});
|
|
11828
|
+
const providerOperationKey = aggregateReceiptKey;
|
|
11799
11829
|
dispatchDetails.set(aggregateReceiptKey, {
|
|
11800
11830
|
members: owned,
|
|
11801
11831
|
compiled,
|
|
@@ -11885,8 +11915,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11885
11915
|
toolResponse: publicToolResponseForBatchedItem(
|
|
11886
11916
|
aggregate,
|
|
11887
11917
|
item,
|
|
11888
|
-
|
|
11889
|
-
RAW_V2_TOOL_RESPONSE_CONTRACT,
|
|
11918
|
+
true,
|
|
11890
11919
|
),
|
|
11891
11920
|
requestInput: request.input,
|
|
11892
11921
|
execution: toolExecutionMetadataForOutcome({
|
|
@@ -11899,60 +11928,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11899
11928
|
}),
|
|
11900
11929
|
);
|
|
11901
11930
|
},
|
|
11902
|
-
heartbeat: {
|
|
11903
|
-
// Claim replies carry the real expiry. A fixed fallback interval
|
|
11904
|
-
// can be too slow for a short-lived gateway lease, so derive the
|
|
11905
|
-
// one cohort supervisor from the earliest owned member instead.
|
|
11906
|
-
intervalMs: ({ members }) =>
|
|
11907
|
-
Math.max(
|
|
11908
|
-
1,
|
|
11909
|
-
Math.floor(
|
|
11910
|
-
runtimeLeaseHeartbeatIntervalFromExpiry({
|
|
11911
|
-
leaseExpiresAt: members.reduce<string | null>(
|
|
11912
|
-
(earliest, member) => {
|
|
11913
|
-
const expiresAt = member.leaseExpiresAt ?? null;
|
|
11914
|
-
if (!expiresAt) return earliest;
|
|
11915
|
-
if (!earliest) return expiresAt;
|
|
11916
|
-
return Date.parse(expiresAt) < Date.parse(earliest)
|
|
11917
|
-
? expiresAt
|
|
11918
|
-
: earliest;
|
|
11919
|
-
},
|
|
11920
|
-
null,
|
|
11921
|
-
),
|
|
11922
|
-
fallbackTtlMs: PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
|
|
11923
|
-
}) / 2,
|
|
11924
|
-
),
|
|
11925
|
-
),
|
|
11926
|
-
onTransientFailure: (error) => {
|
|
11927
|
-
this.runtimeLog(
|
|
11928
|
-
`Native Tool Call Receipt Cohort heartbeat transport failed; retrying: ${
|
|
11929
|
-
error instanceof Error ? error.message : String(error)
|
|
11930
|
-
}`,
|
|
11931
|
-
);
|
|
11932
|
-
},
|
|
11933
|
-
},
|
|
11934
11931
|
});
|
|
11935
11932
|
return { batch, result };
|
|
11936
11933
|
},
|
|
11937
11934
|
onChunkComplete: async (entries) => {
|
|
11938
11935
|
for (const entry of entries) {
|
|
11939
11936
|
if (entry.error !== undefined) {
|
|
11940
|
-
//
|
|
11941
|
-
//
|
|
11942
|
-
// duplicate provider work. It must not convert a failed bulk
|
|
11943
|
-
// persistence boundary into a successful map result: callers need
|
|
11944
|
-
// the loud failure so this run can be retried from the durable
|
|
11945
|
-
// receipts instead of continuing after an ambiguous bulk write.
|
|
11937
|
+
// A failed batch is not a cache entry. Surface it to every row so
|
|
11938
|
+
// a later run can make a fresh provider attempt.
|
|
11946
11939
|
for (const request of entry.request.memberRequests) {
|
|
11947
11940
|
await reject(request, entry.error);
|
|
11948
11941
|
}
|
|
11949
11942
|
throw entry.error;
|
|
11950
11943
|
}
|
|
11951
11944
|
if (!entry.result) {
|
|
11952
|
-
//
|
|
11953
|
-
//
|
|
11954
|
-
// the same cohort still reach their rows and a stale failure never
|
|
11955
|
-
// causes this Context to write a second terminal transition.
|
|
11945
|
+
// Successful split results must be present in the completed cache
|
|
11946
|
+
// before they are delivered to rows.
|
|
11956
11947
|
const durable = await store.read({
|
|
11957
11948
|
resultReceiptKeys: entry.request.memberRequests.map(
|
|
11958
11949
|
(request) => request.receiptKey!,
|
|
@@ -11973,8 +11964,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
11973
11964
|
} else {
|
|
11974
11965
|
await reject(
|
|
11975
11966
|
request,
|
|
11976
|
-
|
|
11977
|
-
entry.error ??
|
|
11967
|
+
entry.error ??
|
|
11978
11968
|
new Error(
|
|
11979
11969
|
'Native Tool Call did not reach a terminal receipt.',
|
|
11980
11970
|
),
|
|
@@ -12198,6 +12188,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
12198
12188
|
const cohortRequests = pendingRequests.filter(
|
|
12199
12189
|
(request) =>
|
|
12200
12190
|
request.cacheable !== false &&
|
|
12191
|
+
request.requiresExecutionFence !== true &&
|
|
12201
12192
|
Boolean(request.receiptKey?.trim()),
|
|
12202
12193
|
);
|
|
12203
12194
|
if (cohortRequests.length > 0) {
|
|
@@ -12230,6 +12221,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
12230
12221
|
const cohortRequests = pendingRequests.filter(
|
|
12231
12222
|
(request) =>
|
|
12232
12223
|
request.cacheable !== false &&
|
|
12224
|
+
request.requiresExecutionFence !== true &&
|
|
12233
12225
|
Boolean(request.receiptKey?.trim()),
|
|
12234
12226
|
);
|
|
12235
12227
|
if (cohortRequests.length > 0) {
|
|
@@ -13056,8 +13048,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13056
13048
|
: publicToolResponseForBatchedItem(
|
|
13057
13049
|
batchExecution.execution,
|
|
13058
13050
|
splitResults[index] ?? null,
|
|
13059
|
-
|
|
13060
|
-
RAW_V2_TOOL_RESPONSE_CONTRACT,
|
|
13051
|
+
true,
|
|
13061
13052
|
),
|
|
13062
13053
|
}),
|
|
13063
13054
|
),
|
|
@@ -13213,6 +13204,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13213
13204
|
...(request.receiptKey
|
|
13214
13205
|
? {
|
|
13215
13206
|
durableCallReceiptKey: request.receiptKey,
|
|
13207
|
+
completedResultCache: true,
|
|
13216
13208
|
executionAuthScopeDigest:
|
|
13217
13209
|
request.executionAuthScopeDigest,
|
|
13218
13210
|
providerIdempotencyReceiptKey:
|
|
@@ -13675,6 +13667,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13675
13667
|
.catch(() => null);
|
|
13676
13668
|
const retrySafeTransientHttp =
|
|
13677
13669
|
retryPolicy?.retrySafeTransientHttp === true;
|
|
13670
|
+
const requiresExecutionFence =
|
|
13671
|
+
retryPolicy?.requiresExecutionFence === true;
|
|
13672
|
+
const fixtureReplaySafe = this.#options.integrationMode === 'fixture';
|
|
13673
|
+
const readOnlyQuery = isQueryResultDatasetReadRequest(
|
|
13674
|
+
toolId,
|
|
13675
|
+
toolInputSnapshot,
|
|
13676
|
+
);
|
|
13677
|
+
// A completed result cache eliminates repeated successful work; it
|
|
13678
|
+
// does not prove an ambiguous provider write can be replayed. Only
|
|
13679
|
+
// a read-only query or an action which explicitly declares transient
|
|
13680
|
+
// retries safe may issue another physical request after dispatch.
|
|
13681
|
+
const ambiguousTransportReplayAllowed =
|
|
13682
|
+
readOnlyQuery || retrySafeTransientHttp;
|
|
13678
13683
|
const invocationOrgId = this.#options.orgId?.trim() ?? '';
|
|
13679
13684
|
const durableCallReceiptKey =
|
|
13680
13685
|
options?.durableCallReceiptKey?.trim() ||
|
|
@@ -13689,26 +13694,25 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13689
13694
|
const executionAuthScopeDigest = options?.executionAuthScopeDigest
|
|
13690
13695
|
? options.executionAuthScopeDigest.trim() || null
|
|
13691
13696
|
: ((await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null);
|
|
13692
|
-
const
|
|
13693
|
-
|
|
13694
|
-
|
|
13695
|
-
|
|
13696
|
-
|
|
13697
|
-
|
|
13698
|
-
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
};
|
|
13702
|
-
const providerIdempotencyKey =
|
|
13703
|
-
options?.providerIdempotencyKey?.trim() || durableCallReceiptKey;
|
|
13697
|
+
const executionRequestNonce = crypto.randomUUID();
|
|
13698
|
+
// This identifies one logical tool call to billing. Physical retry
|
|
13699
|
+
// count remains in request metadata, so a transport retry cannot
|
|
13700
|
+
// reserve or charge the same call twice.
|
|
13701
|
+
const deeplineRequestId = `ctx-tool-${stableDigest(executionRequestNonce).slice(0, 32)}`;
|
|
13702
|
+
// The provider operation has a semantic identity independent of
|
|
13703
|
+
// the response receipt. Its key is supplied by the caller that owns
|
|
13704
|
+
// that compatibility contract; never substitute request/billing
|
|
13705
|
+
// attribution for it.
|
|
13704
13706
|
const providerIdempotencyReceiptKey =
|
|
13705
|
-
options?.providerIdempotencyReceiptKey?.trim() ||
|
|
13706
|
-
|
|
13707
|
-
|
|
13708
|
-
|
|
13709
|
-
|
|
13710
|
-
|
|
13711
|
-
|
|
13707
|
+
options?.providerIdempotencyReceiptKey?.trim() || null;
|
|
13708
|
+
const providerExecutionFenceKey = requiresExecutionFence
|
|
13709
|
+
? options?.providerIdempotencyKey?.trim() || null
|
|
13710
|
+
: null;
|
|
13711
|
+
if (requiresExecutionFence && !providerExecutionFenceKey) {
|
|
13712
|
+
throw new Error(
|
|
13713
|
+
`Tool ${toolId} requires a provider execution fence but no provider operation identity was supplied.`,
|
|
13714
|
+
);
|
|
13715
|
+
}
|
|
13712
13716
|
// Every runtime provider call carries at least a tool-scoped
|
|
13713
13717
|
// attribution. Callers that know the execution location (row-scoped
|
|
13714
13718
|
// map cells, batched owners) supply the fuller scope.
|
|
@@ -13723,16 +13727,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13723
13727
|
...(requestsDurableInvocationFence
|
|
13724
13728
|
? { invocation_fence_version: 1 }
|
|
13725
13729
|
: {}),
|
|
13726
|
-
// Canonical provider-operation receipts validate this semantic
|
|
13727
|
-
// plan before replay. It is intentionally removed from the
|
|
13728
|
-
// legacy byte-receipt fingerprint so mixed runner artifacts
|
|
13729
|
-
// retain their existing provider-once behavior.
|
|
13730
|
-
...(requestsDurableInvocationFence
|
|
13731
|
-
? { canonical_operation: canonicalOperation }
|
|
13732
|
-
: {}),
|
|
13733
|
-
// The worker-owned receipt coordinates this physical durable
|
|
13734
|
-
// call. Provider idempotency is carried separately below so
|
|
13735
|
-
// exact-payload reuse never replaces batch receipt ownership.
|
|
13736
13730
|
...(durableCallReceiptKey
|
|
13737
13731
|
? {
|
|
13738
13732
|
durable_call_receipt_key: durableCallReceiptKey,
|
|
@@ -13744,10 +13738,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13744
13738
|
: {}),
|
|
13745
13739
|
}
|
|
13746
13740
|
: {}),
|
|
13747
|
-
...(
|
|
13741
|
+
...(providerExecutionFenceKey
|
|
13748
13742
|
? {
|
|
13749
|
-
|
|
13750
|
-
|
|
13743
|
+
provider_idempotency_key: providerExecutionFenceKey,
|
|
13744
|
+
...(providerIdempotencyReceiptKey
|
|
13745
|
+
? {
|
|
13746
|
+
provider_idempotency_receipt_key:
|
|
13747
|
+
providerIdempotencyReceiptKey,
|
|
13748
|
+
}
|
|
13749
|
+
: {}),
|
|
13751
13750
|
}
|
|
13752
13751
|
: {}),
|
|
13753
13752
|
...(options?.customerDbDataset
|
|
@@ -13770,9 +13769,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13770
13769
|
: {}),
|
|
13771
13770
|
}
|
|
13772
13771
|
: {}),
|
|
13773
|
-
...(providerIdempotencyKey
|
|
13774
|
-
? { provider_idempotency_key: providerIdempotencyKey }
|
|
13775
|
-
: {}),
|
|
13776
13772
|
...(playNodeScope
|
|
13777
13773
|
? { play_node_scope: playNodeScopeToWire(playNodeScope) }
|
|
13778
13774
|
: {}),
|
|
@@ -13789,6 +13785,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13789
13785
|
elapsedMs: number;
|
|
13790
13786
|
requestId: string | null;
|
|
13791
13787
|
aborted: boolean;
|
|
13788
|
+
/** The ingress rejected the request before any provider dispatch. */
|
|
13789
|
+
knownNotDispatched?: boolean;
|
|
13792
13790
|
}): Promise<void> => {
|
|
13793
13791
|
transportAttempt += 1;
|
|
13794
13792
|
const diagnostic = describeTransportError(input.error);
|
|
@@ -13804,7 +13802,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
13804
13802
|
error: diagnostic,
|
|
13805
13803
|
})}`,
|
|
13806
13804
|
);
|
|
13807
|
-
if (
|
|
13805
|
+
if (
|
|
13806
|
+
transportAttempt < TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS &&
|
|
13807
|
+
(input.knownNotDispatched === true ||
|
|
13808
|
+
ambiguousTransportReplayAllowed ||
|
|
13809
|
+
fixtureReplaySafe)
|
|
13810
|
+
) {
|
|
13811
|
+
invocationAttempt += 1;
|
|
13808
13812
|
await this.governor.chargeBudget('retry');
|
|
13809
13813
|
const retryAfterMs =
|
|
13810
13814
|
TOOL_RETRY_AFTER_FALLBACK_MS * transportAttempt;
|
|
@@ -14033,17 +14037,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
14033
14037
|
'Content-Type': 'application/json',
|
|
14034
14038
|
Authorization: `Bearer ${this.#options.executorToken}`,
|
|
14035
14039
|
[EXECUTE_RESPONSE_CONTRACT_HEADER]:
|
|
14036
|
-
|
|
14040
|
+
RAW_V2_TOOL_RESPONSE_CONTRACT,
|
|
14037
14041
|
[EXECUTE_RESPONSE_INTENT_HEADER]: 'dataset',
|
|
14038
14042
|
[EXECUTE_TOOL_METADATA_HEADER]: 'true',
|
|
14039
14043
|
[TOOL_EXECUTION_ERROR_SCHEMA_HEADER]: String(
|
|
14040
14044
|
toolErrorSchemaVersion,
|
|
14041
14045
|
),
|
|
14042
14046
|
'x-deepline-request-id': deeplineRequestId,
|
|
14043
|
-
...(
|
|
14047
|
+
...(providerExecutionFenceKey
|
|
14044
14048
|
? {
|
|
14045
14049
|
'x-deepline-idempotency-key':
|
|
14046
|
-
|
|
14050
|
+
providerExecutionFenceKey,
|
|
14047
14051
|
}
|
|
14048
14052
|
: {}),
|
|
14049
14053
|
...protectionHeaders,
|
|
@@ -14145,40 +14149,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
14145
14149
|
(response === null ||
|
|
14146
14150
|
error instanceof ToolExecuteResponseBodyTransportError ||
|
|
14147
14151
|
error instanceof ToolExecuteInvalidJsonError);
|
|
14148
|
-
const hasDurableInvocationIdentity = Boolean(
|
|
14149
|
-
durableCallReceiptKey &&
|
|
14150
|
-
providerIdempotencyKey &&
|
|
14151
|
-
executionAuthScopeDigest,
|
|
14152
|
-
);
|
|
14153
14152
|
// A post-header body deadline aborts the fetch controller solely
|
|
14154
14153
|
// to stop consuming that response. It must not also cancel the
|
|
14155
14154
|
// independent gateway health check that establishes whether the
|
|
14156
14155
|
// committed response can be replayed safely. Preserve genuine
|
|
14157
14156
|
// caller/runtime cancellation by dropping the signal only when
|
|
14158
14157
|
// this exact body deadline caused the abort.
|
|
14159
|
-
const durableFenceVerificationSignal =
|
|
14160
|
-
abortController?.signal.aborted === true &&
|
|
14161
|
-
abortController.signal.reason instanceof
|
|
14162
|
-
ToolExecuteResponseBodyTimeoutError
|
|
14163
|
-
? undefined
|
|
14164
|
-
: abortController?.signal;
|
|
14165
|
-
const durableInvocationFenceVerified =
|
|
14166
|
-
hasDurableInvocationIdentity &&
|
|
14167
|
-
requestsDurableInvocationFence &&
|
|
14168
|
-
(this.#options.durableInvocationFence === true ||
|
|
14169
|
-
(await this.#options
|
|
14170
|
-
.verifyDurableInvocationFence?.(
|
|
14171
|
-
durableFenceVerificationSignal,
|
|
14172
|
-
)
|
|
14173
|
-
.catch(() => false)) === true);
|
|
14174
|
-
// Fixture execution has no provider-side effect. Replaying the
|
|
14175
|
-
// same stable request after our own transport fails is always
|
|
14176
|
-
// safe, even when this runner lacks a live-provider fence.
|
|
14177
|
-
const fixtureReplaySafe =
|
|
14178
|
-
this.#options.integrationMode === 'fixture';
|
|
14179
14158
|
const transportReplaySafe =
|
|
14180
14159
|
!ambiguousDispatchedFailure ||
|
|
14181
|
-
|
|
14160
|
+
ambiguousTransportReplayAllowed ||
|
|
14182
14161
|
fixtureReplaySafe;
|
|
14183
14162
|
if (!transportReplaySafe) {
|
|
14184
14163
|
const diagnostic = describeTransportError(transportError);
|
|
@@ -14271,6 +14250,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
14271
14250
|
elapsedMs: providerCallElapsedMs ?? 0,
|
|
14272
14251
|
requestId: deeplineRequestId,
|
|
14273
14252
|
aborted: false,
|
|
14253
|
+
knownNotDispatched: true,
|
|
14274
14254
|
});
|
|
14275
14255
|
continue;
|
|
14276
14256
|
}
|
|
@@ -14308,13 +14288,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
|
|
|
14308
14288
|
failure.backpressureDelayMs,
|
|
14309
14289
|
);
|
|
14310
14290
|
}
|
|
14311
|
-
if (
|
|
14312
|
-
|
|
14313
|
-
|
|
14314
|
-
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14291
|
+
if (
|
|
14292
|
+
failure.shouldRetry &&
|
|
14293
|
+
(ambiguousTransportReplayAllowed ||
|
|
14294
|
+
retrySafeTransientHttp ||
|
|
14295
|
+
fixtureReplaySafe)
|
|
14296
|
+
) {
|
|
14297
|
+
invocationAttempt += 1;
|
|
14318
14298
|
if (failure.chargeRetryBudget) {
|
|
14319
14299
|
await this.governor.chargeBudget('retry');
|
|
14320
14300
|
}
|