deepline 0.1.182 → 0.1.184
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/apps/play-runner-workers/src/child-play-await.ts +203 -6
- package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +8 -1
- package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +639 -109
- package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +3 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +1752 -1899
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +116 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +542 -78
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +292 -11
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/run-work-dispatcher.ts +519 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +2381 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +18 -5
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/work-budget.ts +130 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/worker-platform-budget.ts +39 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +2 -0
- package/dist/bundling-sources/sdk/src/client.ts +41 -0
- package/dist/bundling-sources/sdk/src/http.ts +23 -8
- package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +56 -3
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/sdk/src/types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +133 -10
- package/dist/bundling-sources/shared_libs/play-runtime/auth-scope-resolver.ts +92 -0
- package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -4
- package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +101 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1612 -287
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +105 -6
- package/dist/bundling-sources/shared_libs/play-runtime/dedup-backend.ts +0 -0
- package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durability-store.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +116 -16
- package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +186 -20
- package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +133 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +245 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +5 -5
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +14 -0
- package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +52 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +41 -0
- package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +3 -2
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +124 -0
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +6 -2
- package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +48 -2
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +158 -15
- package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +45 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-paths.ts +21 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2038 -262
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +42 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +183 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +88 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +8 -1
- package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +19 -1
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +343 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +437 -0
- package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +83 -6
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +28 -11
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +38 -6
- package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +24 -17
- package/dist/cli/index.js +76 -17
- package/dist/cli/index.mjs +76 -17
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -9
- package/dist/index.mjs +63 -9
- package/dist/plays/bundle-play-file.mjs +20 -6
- package/package.json +1 -1
|
@@ -23,12 +23,28 @@ import {
|
|
|
23
23
|
} from './batch-runtime';
|
|
24
24
|
import type { PlayQueueHint } from './governor/rate-state-backend';
|
|
25
25
|
import type { MapRowOutcome } from './durability-store';
|
|
26
|
+
import type { WorkReceiptFailureKind } from './work-receipts';
|
|
26
27
|
import {
|
|
27
28
|
completedMapRowOutcome,
|
|
28
29
|
failedMapRowOutcome,
|
|
29
30
|
mapRowOutcomeRuntimeFields,
|
|
30
31
|
resolveMapRowOutcomeKey,
|
|
31
32
|
} from './map-row-outcome';
|
|
33
|
+
import { RuntimeSheetRowsBlockedError } from './runtime-sheet-errors';
|
|
34
|
+
import { buildChildRunId } from './child-run-id';
|
|
35
|
+
import type { PlayCallGovernanceSnapshot } from './scheduler-backend';
|
|
36
|
+
import {
|
|
37
|
+
RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER,
|
|
38
|
+
SYNTHETIC_RUN_HEADER,
|
|
39
|
+
} from './coordinator-headers';
|
|
40
|
+
import {
|
|
41
|
+
PLAY_RUNTIME_API_COMPAT_PATH,
|
|
42
|
+
PLAY_RUNTIME_CHILD_EXECUTOR_TOKEN_COMPAT_PATH,
|
|
43
|
+
} from './runtime-api-paths';
|
|
44
|
+
export {
|
|
45
|
+
RuntimeSheetRowsBlockedError,
|
|
46
|
+
type RuntimeSheetBlockedRowDetail,
|
|
47
|
+
} from './runtime-sheet-errors';
|
|
32
48
|
import {
|
|
33
49
|
createDefaultGovernanceSnapshot,
|
|
34
50
|
createPlayExecutionGovernor,
|
|
@@ -64,30 +80,38 @@ import {
|
|
|
64
80
|
TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS,
|
|
65
81
|
classifyToolExecuteHttpFailure,
|
|
66
82
|
createToolExecuteHttpFailureAttemptTracker,
|
|
83
|
+
parseToolExecuteAuthScopeChangedError,
|
|
84
|
+
ToolExecuteAuthScopeChangedError,
|
|
67
85
|
} from './tool-execute-retry-policy';
|
|
68
86
|
import {
|
|
69
87
|
buildDurableCtxCallCacheKey,
|
|
88
|
+
buildDurableRunPlaySemanticKey,
|
|
89
|
+
buildDurableToolAggregateProviderIdempotencyKey,
|
|
90
|
+
buildDurableToolAggregateReceiptKey,
|
|
70
91
|
buildDurableToolCallAuthScopeDigest,
|
|
71
92
|
buildDurableToolCallCacheKey,
|
|
93
|
+
buildDurableToolProviderIdempotencyKey,
|
|
94
|
+
buildDurableToolReceiptPrefix,
|
|
72
95
|
} from './durable-call-cache';
|
|
73
96
|
import {
|
|
74
|
-
|
|
75
|
-
isCustomerDbDatasetTool,
|
|
76
|
-
isQueryResultDatasetReadRequest,
|
|
77
|
-
isQueryResultDatasetTool,
|
|
78
|
-
} from './query-result-dataset';
|
|
79
|
-
import {
|
|
97
|
+
RuntimeReceiptLeaseLostError,
|
|
80
98
|
RuntimeReceiptWaitTimeoutError,
|
|
81
99
|
executeWithDurableRuntimeReceipt,
|
|
82
100
|
resolveRuntimeToolReceiptWaitMaxAttempts,
|
|
101
|
+
runtimeReceiptFailureKindForError,
|
|
83
102
|
runtimeReceiptOutput as durableRuntimeReceiptOutput,
|
|
84
103
|
waitForCompletedRuntimeReceipt,
|
|
85
104
|
type DurableReceiptExecutionStore,
|
|
86
105
|
} from './durable-receipt-execution';
|
|
106
|
+
import {
|
|
107
|
+
QUERY_RESULT_DATASET_PAGE_SIZE,
|
|
108
|
+
isCustomerDbDatasetTool,
|
|
109
|
+
isQueryResultDatasetReadRequest,
|
|
110
|
+
isQueryResultDatasetTool,
|
|
111
|
+
} from './query-result-dataset';
|
|
87
112
|
import { isRowIsolationExemptError } from './row-isolation';
|
|
88
113
|
import {
|
|
89
114
|
createRuntimePersistenceLatch,
|
|
90
|
-
PROVIDER_DISPATCH_WAVE_SIZE,
|
|
91
115
|
RuntimePersistenceCircuitOpenError,
|
|
92
116
|
tripRuntimePersistenceLatch,
|
|
93
117
|
type RuntimePersistenceLatch,
|
|
@@ -119,7 +143,9 @@ import { DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE } from './runtime-constraints';
|
|
|
119
143
|
import {
|
|
120
144
|
PlayExecutionSuspendedError,
|
|
121
145
|
PlayRowExecutionSuspendedError,
|
|
146
|
+
isPlayExecutionSuspendedError,
|
|
122
147
|
isPlayRowExecutionSuspendedError,
|
|
148
|
+
type PlayExecutionSuspension,
|
|
123
149
|
} from './suspension';
|
|
124
150
|
import {
|
|
125
151
|
createSecretRedactionContext,
|
|
@@ -171,6 +197,7 @@ import {
|
|
|
171
197
|
type StepProgramDatasetColumnInput,
|
|
172
198
|
type StepProgramDatasetOptions,
|
|
173
199
|
} from './step-program-dataset-builder';
|
|
200
|
+
import { readRuntimeSheetDatasetRows } from './runtime-api';
|
|
174
201
|
|
|
175
202
|
/**
|
|
176
203
|
* SECURITY: AsyncLocalStorage is per async execution, not a cross-run cache.
|
|
@@ -207,9 +234,118 @@ const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
|
207
234
|
const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
|
|
208
235
|
const FETCH_TRANSPORT_MAX_ATTEMPTS = 3;
|
|
209
236
|
const FETCH_TRANSPORT_RETRY_DELAY_MS = 100;
|
|
237
|
+
const NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS = 100;
|
|
238
|
+
const NODE_RUNTIME_MAP_VISIBILITY_RETRY_MS = 25;
|
|
210
239
|
type SafeFetchModule = typeof import('@shared_libs/security/safe-fetch');
|
|
211
240
|
let safeFetchModule: Promise<SafeFetchModule> | null = null;
|
|
212
241
|
|
|
242
|
+
export async function waitForNodeRuntimeMapRowsVisible(input: {
|
|
243
|
+
mapName: string;
|
|
244
|
+
tableNamespace: string;
|
|
245
|
+
runId?: string | null;
|
|
246
|
+
expectedRows: number;
|
|
247
|
+
updatedRows: number;
|
|
248
|
+
readVisibleRowCount: () => Promise<number>;
|
|
249
|
+
sleep?: (ms: number) => Promise<void>;
|
|
250
|
+
log?: (line: string) => void;
|
|
251
|
+
}): Promise<number> {
|
|
252
|
+
if (input.expectedRows <= 0) return 0;
|
|
253
|
+
|
|
254
|
+
const sleep =
|
|
255
|
+
input.sleep ??
|
|
256
|
+
((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
257
|
+
let lastVisibleRows = -1;
|
|
258
|
+
for (
|
|
259
|
+
let attempt = 1;
|
|
260
|
+
attempt <= NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS;
|
|
261
|
+
attempt += 1
|
|
262
|
+
) {
|
|
263
|
+
lastVisibleRows = await input.readVisibleRowCount();
|
|
264
|
+
if (lastVisibleRows >= input.expectedRows) {
|
|
265
|
+
if (attempt > 1) {
|
|
266
|
+
input.log?.(
|
|
267
|
+
`Runtime sheet visibility barrier satisfied ctx.dataset("${input.mapName}") ` +
|
|
268
|
+
`after ${attempt} read(s): visible ${lastVisibleRows}/${input.expectedRows}; ` +
|
|
269
|
+
`wrote ${input.updatedRows}/${input.expectedRows}; run ${input.runId ?? 'unknown'}.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return lastVisibleRows;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (attempt === 1) {
|
|
276
|
+
input.log?.(
|
|
277
|
+
`Runtime sheet visibility barrier waiting ctx.dataset("${input.mapName}"): ` +
|
|
278
|
+
`visible ${lastVisibleRows}/${input.expectedRows}; ` +
|
|
279
|
+
`wrote ${input.updatedRows}/${input.expectedRows}; run ${input.runId ?? 'unknown'}.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (attempt < NODE_RUNTIME_MAP_VISIBILITY_MAX_ATTEMPTS) {
|
|
283
|
+
await sleep(NODE_RUNTIME_MAP_VISIBILITY_RETRY_MS);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Runtime sheet visibility mismatch for ctx.dataset("${input.mapName}"): ` +
|
|
289
|
+
`expected ${input.expectedRows} terminal visible row(s), saw ${lastVisibleRows}; ` +
|
|
290
|
+
`write reported ${input.updatedRows}; run ${input.runId ?? 'unknown'}.`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function reconcileNodeRuntimeMapResultsWithPersistedSheet(input: {
|
|
295
|
+
mapName: string;
|
|
296
|
+
tableNamespace: string;
|
|
297
|
+
runId?: string | null;
|
|
298
|
+
expectedRows: number;
|
|
299
|
+
currentRows: Record<string, unknown>[];
|
|
300
|
+
failedRowCount: number;
|
|
301
|
+
readPersistedRows: (input: {
|
|
302
|
+
limit: number;
|
|
303
|
+
offset: number;
|
|
304
|
+
rowMode: 'all';
|
|
305
|
+
}) => Promise<Record<string, unknown>[]>;
|
|
306
|
+
log?: (line: string) => void;
|
|
307
|
+
}): Promise<Record<string, unknown>[]> {
|
|
308
|
+
if (
|
|
309
|
+
input.failedRowCount > 0 ||
|
|
310
|
+
input.expectedRows <= 0 ||
|
|
311
|
+
input.currentRows.length >= input.expectedRows
|
|
312
|
+
) {
|
|
313
|
+
return input.currentRows;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const persistedRows: Record<string, unknown>[] = [];
|
|
317
|
+
let offset = 0;
|
|
318
|
+
while (persistedRows.length < input.expectedRows) {
|
|
319
|
+
const page = await input.readPersistedRows({
|
|
320
|
+
limit: Math.min(10_000, input.expectedRows - persistedRows.length),
|
|
321
|
+
offset,
|
|
322
|
+
rowMode: 'all',
|
|
323
|
+
});
|
|
324
|
+
if (page.length === 0) break;
|
|
325
|
+
persistedRows.push(...page);
|
|
326
|
+
offset += page.length;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (persistedRows.length < input.expectedRows) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Runtime sheet finalization mismatch for ctx.dataset("${input.mapName}"): ` +
|
|
332
|
+
`expected ${input.expectedRows} terminal persisted row(s), saw ${persistedRows.length}; ` +
|
|
333
|
+
`in-memory Node map results reported ${input.currentRows.length}; run ${input.runId ?? 'unknown'}.`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (persistedRows.length > input.currentRows.length) {
|
|
338
|
+
input.log?.(
|
|
339
|
+
`Runtime sheet finalization reconciled ctx.dataset("${input.mapName}") ` +
|
|
340
|
+
`from ${input.currentRows.length}/${input.expectedRows} in-memory Node row(s) ` +
|
|
341
|
+
`to ${persistedRows.length} terminal persisted row(s).`,
|
|
342
|
+
);
|
|
343
|
+
return persistedRows;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return input.currentRows;
|
|
347
|
+
}
|
|
348
|
+
|
|
213
349
|
export function resolveToolRuntimeTimeoutMs(
|
|
214
350
|
toolId: string,
|
|
215
351
|
requestedTimeoutMs?: number,
|
|
@@ -304,6 +440,9 @@ function finitePositiveInteger(value: unknown): number | null {
|
|
|
304
440
|
|
|
305
441
|
type ToolExecutionApiOptions = {
|
|
306
442
|
timeoutMs?: number;
|
|
443
|
+
durableCallReceiptKey?: string | null;
|
|
444
|
+
executionAuthScopeDigest?: string | null;
|
|
445
|
+
providerIdempotencyKey?: string | null;
|
|
307
446
|
customerDbDataset?: {
|
|
308
447
|
limit: number;
|
|
309
448
|
offset: number;
|
|
@@ -347,6 +486,16 @@ function persistableMapRowBytes(row: PersistableMapRow): number {
|
|
|
347
486
|
return JSON.stringify(row).length;
|
|
348
487
|
}
|
|
349
488
|
|
|
489
|
+
function failedMapRowLogLabel(row: PersistableMapRow): string {
|
|
490
|
+
const rowId = row.data?.row_id;
|
|
491
|
+
if (typeof rowId === 'string' && rowId.trim()) return rowId.trim();
|
|
492
|
+
if (row.key?.trim()) return row.key.trim();
|
|
493
|
+
if (typeof row.inputIndex === 'number' && Number.isFinite(row.inputIndex)) {
|
|
494
|
+
return `input:${Math.floor(row.inputIndex)}`;
|
|
495
|
+
}
|
|
496
|
+
return 'unknown';
|
|
497
|
+
}
|
|
498
|
+
|
|
350
499
|
type FieldMapRunResult = {
|
|
351
500
|
completedRows: PersistableMapRow[];
|
|
352
501
|
failedRows: PersistableMapRow[];
|
|
@@ -508,9 +657,7 @@ function durableCtxKey(input: {
|
|
|
508
657
|
staleAfterSeconds?: number | null;
|
|
509
658
|
}): string {
|
|
510
659
|
if (input.operation === 'tool') {
|
|
511
|
-
throw new Error(
|
|
512
|
-
'Durable ctx key is only for non-tool call boundaries; tools use buildDurableToolCallCacheKey.',
|
|
513
|
-
);
|
|
660
|
+
throw new Error('Tool calls use tool receipt keys.');
|
|
514
661
|
}
|
|
515
662
|
return buildDurableCtxCallCacheKey({
|
|
516
663
|
orgId: input.orgId,
|
|
@@ -756,6 +903,13 @@ export class PlayContextImpl {
|
|
|
756
903
|
eventKey: string;
|
|
757
904
|
timeoutMs: number;
|
|
758
905
|
}> = [];
|
|
906
|
+
private pendingChildPlayBoundaries: Array<{
|
|
907
|
+
boundaryId: string;
|
|
908
|
+
childRunId: string;
|
|
909
|
+
childPlayName: string;
|
|
910
|
+
}> = [];
|
|
911
|
+
private activeScheduledChildPlayCalls = 0;
|
|
912
|
+
private scheduledChildPlayQuiescenceWaiters: Array<() => void> = [];
|
|
759
913
|
private processedRowCount = 0;
|
|
760
914
|
private sleepBoundaryIndex = 0;
|
|
761
915
|
private fetchCallIndex = 0;
|
|
@@ -891,13 +1045,18 @@ export class PlayContextImpl {
|
|
|
891
1045
|
options.workflowId ||
|
|
892
1046
|
'anonymous-play';
|
|
893
1047
|
const rootRunId = options.runId ?? options.workflowId ?? 'anonymous-run';
|
|
1048
|
+
if (options.requireSharedRateState && !options.rateState) {
|
|
1049
|
+
throw new Error(
|
|
1050
|
+
'Shared rate-state backend is required for this Node runtime substrate.',
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
894
1053
|
this.governor = createPlayExecutionGovernor({
|
|
895
1054
|
adapter: 'cjs_node20',
|
|
896
1055
|
scope: {
|
|
897
1056
|
orgId: options.orgId ?? 'org',
|
|
898
1057
|
rootRunId: options.runId ?? options.workflowId ?? 'run',
|
|
899
1058
|
},
|
|
900
|
-
rateState: new InMemoryRateStateBackend(),
|
|
1059
|
+
rateState: options.rateState ?? new InMemoryRateStateBackend(),
|
|
901
1060
|
resolvePacing: createPacingResolver(options.getToolQueueHints),
|
|
902
1061
|
// A root run keeps depth 0 (its first child play is depth 1) but still
|
|
903
1062
|
// seeds its own play id into the ancestry/currentPlayId so the cycle guard
|
|
@@ -923,6 +1082,263 @@ export class PlayContextImpl {
|
|
|
923
1082
|
return `${this.governance.currentRunId}:${localId}`;
|
|
924
1083
|
}
|
|
925
1084
|
|
|
1085
|
+
private shouldLaunchChildPlaysThroughScheduler(): boolean {
|
|
1086
|
+
return (
|
|
1087
|
+
this.#options.requireSharedRateState === true &&
|
|
1088
|
+
this.#options.durableBoundaries === true
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private childPlayBoundaryId(childRunId: string): string {
|
|
1093
|
+
return this.durableBoundaryId(`runPlay:${childRunId}`);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
private recordPendingChildPlayBoundary(input: {
|
|
1097
|
+
boundaryId: string;
|
|
1098
|
+
childRunId: string;
|
|
1099
|
+
childPlayName: string;
|
|
1100
|
+
}): void {
|
|
1101
|
+
this.pendingChildPlayBoundaries.push(input);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
private beginScheduledChildPlayCall(): { release: () => void } {
|
|
1105
|
+
this.activeScheduledChildPlayCalls += 1;
|
|
1106
|
+
let released = false;
|
|
1107
|
+
return {
|
|
1108
|
+
release: () => {
|
|
1109
|
+
if (released) return;
|
|
1110
|
+
released = true;
|
|
1111
|
+
this.activeScheduledChildPlayCalls = Math.max(
|
|
1112
|
+
0,
|
|
1113
|
+
this.activeScheduledChildPlayCalls - 1,
|
|
1114
|
+
);
|
|
1115
|
+
if (this.activeScheduledChildPlayCalls !== 0) return;
|
|
1116
|
+
for (const resolve of this.scheduledChildPlayQuiescenceWaiters.splice(
|
|
1117
|
+
0,
|
|
1118
|
+
)) {
|
|
1119
|
+
resolve();
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
private async waitForScheduledChildPlayQuiescence(): Promise<void> {
|
|
1126
|
+
if (this.activeScheduledChildPlayCalls === 0) return;
|
|
1127
|
+
await new Promise<void>((resolve) => {
|
|
1128
|
+
this.scheduledChildPlayQuiescenceWaiters.push(resolve);
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
private consumePendingChildPlaySuspension(): PlayExecutionSuspension | null {
|
|
1133
|
+
if (this.pendingChildPlayBoundaries.length === 0) return null;
|
|
1134
|
+
const boundaries = [
|
|
1135
|
+
...new Map(
|
|
1136
|
+
this.pendingChildPlayBoundaries.map((boundary) => [
|
|
1137
|
+
boundary.boundaryId,
|
|
1138
|
+
boundary,
|
|
1139
|
+
]),
|
|
1140
|
+
).values(),
|
|
1141
|
+
];
|
|
1142
|
+
this.pendingChildPlayBoundaries = [];
|
|
1143
|
+
if (boundaries.length === 1) {
|
|
1144
|
+
const boundary = boundaries[0]!;
|
|
1145
|
+
return {
|
|
1146
|
+
kind: 'child_play',
|
|
1147
|
+
boundaryId: boundary.boundaryId,
|
|
1148
|
+
childRunId: boundary.childRunId,
|
|
1149
|
+
childPlayName: boundary.childPlayName,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
kind: 'child_play_batch',
|
|
1154
|
+
boundaries,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
private childPlayBoundaryOutput<TOutput>(input: {
|
|
1159
|
+
boundaryId: string;
|
|
1160
|
+
childRunId: string;
|
|
1161
|
+
childPlayName: string;
|
|
1162
|
+
}): { found: true; output: TOutput } | { found: false } {
|
|
1163
|
+
const existing = this.checkpoint.resolvedBoundaries?.[input.boundaryId];
|
|
1164
|
+
if (!existing || existing.kind !== 'child_play') return { found: false };
|
|
1165
|
+
if (existing.childRunId !== input.childRunId) return { found: false };
|
|
1166
|
+
if (existing.status === 'completed') {
|
|
1167
|
+
return { found: true, output: existing.output as TOutput };
|
|
1168
|
+
}
|
|
1169
|
+
const message =
|
|
1170
|
+
typeof existing.error === 'string' && existing.error.trim()
|
|
1171
|
+
? existing.error.trim()
|
|
1172
|
+
: `Child play ${input.childPlayName} (${input.childRunId}) ${existing.status}.`;
|
|
1173
|
+
throw new Error(message);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
private recordPlayCallStep(input: {
|
|
1177
|
+
playId: string;
|
|
1178
|
+
description?: string | null;
|
|
1179
|
+
nestedSteps?: PlayStep[];
|
|
1180
|
+
}): void {
|
|
1181
|
+
const step = {
|
|
1182
|
+
type: 'play_call' as const,
|
|
1183
|
+
playId: input.playId,
|
|
1184
|
+
nestedSteps: input.nestedSteps ?? [],
|
|
1185
|
+
description: normalizeStepDescription(input.description ?? undefined),
|
|
1186
|
+
};
|
|
1187
|
+
if (this.activeDatasetStep) {
|
|
1188
|
+
this.activeDatasetStep.substeps.push(step);
|
|
1189
|
+
} else {
|
|
1190
|
+
this.steps.push(step);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
private runtimeApiHeaders(): Record<string, string> {
|
|
1195
|
+
if (!this.#options.executorToken?.trim()) {
|
|
1196
|
+
throw new Error(
|
|
1197
|
+
'ctx.runPlay scheduled child launch requires an executor token.',
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
Authorization: `Bearer ${this.#options.executorToken.trim()}`,
|
|
1202
|
+
'Content-Type': 'application/json',
|
|
1203
|
+
...(this.#options.runtimeSchedulerSchema?.trim()
|
|
1204
|
+
? {
|
|
1205
|
+
[RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER]:
|
|
1206
|
+
this.#options.runtimeSchedulerSchema.trim(),
|
|
1207
|
+
[SYNTHETIC_RUN_HEADER]: '1',
|
|
1208
|
+
}
|
|
1209
|
+
: {}),
|
|
1210
|
+
...(this.#options.vercelProtectionBypassToken?.trim()
|
|
1211
|
+
? {
|
|
1212
|
+
'x-vercel-protection-bypass':
|
|
1213
|
+
this.#options.vercelProtectionBypassToken.trim(),
|
|
1214
|
+
}
|
|
1215
|
+
: {}),
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private runtimeApiBaseUrl(): string {
|
|
1220
|
+
const baseUrl = this.#options.baseUrl?.trim();
|
|
1221
|
+
if (!baseUrl) {
|
|
1222
|
+
throw new Error('ctx.runPlay scheduled child launch requires baseUrl.');
|
|
1223
|
+
}
|
|
1224
|
+
return baseUrl.replace(/\/$/, '');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private async launchScheduledChildPlay(input: {
|
|
1228
|
+
key: string;
|
|
1229
|
+
childRunId: string;
|
|
1230
|
+
childPlayName: string;
|
|
1231
|
+
childGovernance: GovernanceSnapshot;
|
|
1232
|
+
childInput: Record<string, unknown>;
|
|
1233
|
+
description?: string | null;
|
|
1234
|
+
}): Promise<void> {
|
|
1235
|
+
const parentPlayName =
|
|
1236
|
+
this.governance.currentPlayId ||
|
|
1237
|
+
this.#options.playName ||
|
|
1238
|
+
this.#options.playId ||
|
|
1239
|
+
'anonymous-play';
|
|
1240
|
+
const governance: PlayCallGovernanceSnapshot = {
|
|
1241
|
+
rootRunId: this.governance.rootRunId,
|
|
1242
|
+
parentRunId: this.governance.currentRunId,
|
|
1243
|
+
parentPlayName,
|
|
1244
|
+
key: input.key,
|
|
1245
|
+
ancestryPlayIds: [...this.governance.ancestryPlayIds],
|
|
1246
|
+
callDepth: input.childGovernance.callDepth,
|
|
1247
|
+
playCallCount: input.childGovernance.playCallCount,
|
|
1248
|
+
toolCallCount: input.childGovernance.toolCallCount,
|
|
1249
|
+
retryCount: input.childGovernance.retryCount,
|
|
1250
|
+
descendantCount: input.childGovernance.descendantCount,
|
|
1251
|
+
waterfallStepExecutions: input.childGovernance.waterfallStepExecutions,
|
|
1252
|
+
};
|
|
1253
|
+
const response = await fetch(
|
|
1254
|
+
`${this.runtimeApiBaseUrl()}/api/v2/plays/run`,
|
|
1255
|
+
{
|
|
1256
|
+
method: 'POST',
|
|
1257
|
+
headers: this.runtimeApiHeaders(),
|
|
1258
|
+
body: JSON.stringify({
|
|
1259
|
+
name: input.childPlayName,
|
|
1260
|
+
workflowId: input.childRunId,
|
|
1261
|
+
input: input.childInput,
|
|
1262
|
+
profile: 'hatchet',
|
|
1263
|
+
waitForCompletionMs: 0,
|
|
1264
|
+
internalRunPlay: {
|
|
1265
|
+
...governance,
|
|
1266
|
+
description: input.description ?? null,
|
|
1267
|
+
},
|
|
1268
|
+
}),
|
|
1269
|
+
},
|
|
1270
|
+
);
|
|
1271
|
+
if (!response.ok) {
|
|
1272
|
+
const body = (await response.json().catch(() => null)) as {
|
|
1273
|
+
error?: unknown;
|
|
1274
|
+
} | null;
|
|
1275
|
+
const message =
|
|
1276
|
+
typeof body?.error === 'string' && body.error.trim()
|
|
1277
|
+
? body.error.trim()
|
|
1278
|
+
: response.statusText;
|
|
1279
|
+
throw new Error(
|
|
1280
|
+
`ctx.runPlay(${input.key}) failed to launch child play "${input.childPlayName}": ${message} (status ${response.status}).`,
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
private async readScheduledChildTerminal<TOutput>(input: {
|
|
1286
|
+
childRunId: string;
|
|
1287
|
+
childPlayName: string;
|
|
1288
|
+
}): Promise<
|
|
1289
|
+
| { status: 'completed'; output: TOutput }
|
|
1290
|
+
| { status: 'failed' | 'cancelled'; error: string }
|
|
1291
|
+
| null
|
|
1292
|
+
> {
|
|
1293
|
+
const response = await fetch(
|
|
1294
|
+
`${this.runtimeApiBaseUrl()}${PLAY_RUNTIME_API_COMPAT_PATH}`,
|
|
1295
|
+
{
|
|
1296
|
+
method: 'POST',
|
|
1297
|
+
headers: this.runtimeApiHeaders(),
|
|
1298
|
+
body: JSON.stringify({
|
|
1299
|
+
action: 'read_child_run_terminal_snapshot',
|
|
1300
|
+
parentRunId: this.governance.currentRunId,
|
|
1301
|
+
childRunId: input.childRunId,
|
|
1302
|
+
childPlayName: input.childPlayName,
|
|
1303
|
+
}),
|
|
1304
|
+
},
|
|
1305
|
+
);
|
|
1306
|
+
const body = (await response.json().catch(() => null)) as {
|
|
1307
|
+
state?: {
|
|
1308
|
+
data?: {
|
|
1309
|
+
status?: unknown;
|
|
1310
|
+
result?: unknown;
|
|
1311
|
+
error?: unknown;
|
|
1312
|
+
};
|
|
1313
|
+
};
|
|
1314
|
+
error?: unknown;
|
|
1315
|
+
} | null;
|
|
1316
|
+
if (!response.ok) {
|
|
1317
|
+
const message =
|
|
1318
|
+
typeof body?.error === 'string' && body.error.trim()
|
|
1319
|
+
? body.error.trim()
|
|
1320
|
+
: response.statusText;
|
|
1321
|
+
throw new Error(
|
|
1322
|
+
`ctx.runPlay failed to read child terminal snapshot for "${input.childPlayName}": ${message} (status ${response.status}).`,
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
const data = body?.state?.data;
|
|
1326
|
+
const status = String(data?.status ?? '').toLowerCase();
|
|
1327
|
+
if (status === 'completed') {
|
|
1328
|
+
return { status, output: data?.result as TOutput };
|
|
1329
|
+
}
|
|
1330
|
+
if (status === 'failed' || status === 'cancelled') {
|
|
1331
|
+
return {
|
|
1332
|
+
status,
|
|
1333
|
+
error:
|
|
1334
|
+
typeof data?.error === 'string' && data.error.trim()
|
|
1335
|
+
? data.error.trim()
|
|
1336
|
+
: `Child play ${input.childPlayName} (${input.childRunId}) ${status}.`,
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
return null;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
926
1342
|
private emitScopedRowUpdate(
|
|
927
1343
|
key: string | null,
|
|
928
1344
|
tableNamespace: string | null,
|
|
@@ -990,7 +1406,7 @@ export class PlayContextImpl {
|
|
|
990
1406
|
this.#options.runId
|
|
991
1407
|
) {
|
|
992
1408
|
const response = await fetch(
|
|
993
|
-
`${this.#options.baseUrl.replace(/\/$/, '')}
|
|
1409
|
+
`${this.#options.baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`,
|
|
994
1410
|
{
|
|
995
1411
|
method: 'POST',
|
|
996
1412
|
headers: {
|
|
@@ -1043,8 +1459,7 @@ export class PlayContextImpl {
|
|
|
1043
1459
|
artifactTableNamespace: string;
|
|
1044
1460
|
mapNodeId?: string | null;
|
|
1045
1461
|
explicitKey?:
|
|
1046
|
-
|
|
1047
|
-
| null;
|
|
1462
|
+
((row: Record<string, unknown>, index: number) => string) | null;
|
|
1048
1463
|
}): MapExecutionScope {
|
|
1049
1464
|
const mapInvocationId = `${input.logicalNamespace}:${this.mapInvocationIndex}`;
|
|
1050
1465
|
this.mapInvocationIndex += 1;
|
|
@@ -1102,6 +1517,7 @@ export class PlayContextImpl {
|
|
|
1102
1517
|
runId: string,
|
|
1103
1518
|
reclaimRunning = false,
|
|
1104
1519
|
forceRefresh = false,
|
|
1520
|
+
forceFailedRefresh = false,
|
|
1105
1521
|
): Promise<RuntimeStepReceipt | null> {
|
|
1106
1522
|
if (!this.#options.claimRuntimeStepReceipt) {
|
|
1107
1523
|
return null;
|
|
@@ -1109,8 +1525,11 @@ export class PlayContextImpl {
|
|
|
1109
1525
|
const claimed = await this.#options.claimRuntimeStepReceipt({
|
|
1110
1526
|
key,
|
|
1111
1527
|
runId,
|
|
1528
|
+
runAttempt: this.currentRunAttempt,
|
|
1529
|
+
leaseAware: true,
|
|
1112
1530
|
...(reclaimRunning ? { reclaimRunning: true } : {}),
|
|
1113
1531
|
...(forceRefresh ? { forceRefresh: true } : {}),
|
|
1532
|
+
...(forceFailedRefresh ? { forceFailedRefresh: true } : {}),
|
|
1114
1533
|
});
|
|
1115
1534
|
if (!claimed || typeof claimed.key !== 'string' || !claimed.key.trim()) {
|
|
1116
1535
|
return null;
|
|
@@ -1126,6 +1545,7 @@ export class PlayContextImpl {
|
|
|
1126
1545
|
key: string,
|
|
1127
1546
|
runId: string,
|
|
1128
1547
|
output: unknown | null,
|
|
1548
|
+
leaseId?: string | null,
|
|
1129
1549
|
): Promise<RuntimeStepReceipt | null> {
|
|
1130
1550
|
assertNoSecretTaint(output, 'ctx.step receipt output');
|
|
1131
1551
|
if (!this.#options.completeRuntimeStepReceipt) {
|
|
@@ -1134,6 +1554,8 @@ export class PlayContextImpl {
|
|
|
1134
1554
|
const completed = await this.#options.completeRuntimeStepReceipt({
|
|
1135
1555
|
key,
|
|
1136
1556
|
runId,
|
|
1557
|
+
runAttempt: this.currentRunAttempt,
|
|
1558
|
+
...(leaseId ? { leaseId } : {}),
|
|
1137
1559
|
output,
|
|
1138
1560
|
});
|
|
1139
1561
|
if (
|
|
@@ -1150,10 +1572,167 @@ export class PlayContextImpl {
|
|
|
1150
1572
|
};
|
|
1151
1573
|
}
|
|
1152
1574
|
|
|
1575
|
+
private async releaseRuntimeStepReceipt(
|
|
1576
|
+
key: string,
|
|
1577
|
+
runId: string,
|
|
1578
|
+
leaseId?: string | null,
|
|
1579
|
+
): Promise<RuntimeStepReceipt | null> {
|
|
1580
|
+
if (!this.#options.releaseRuntimeStepReceipt) {
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
const released = await this.#options.releaseRuntimeStepReceipt({
|
|
1584
|
+
key,
|
|
1585
|
+
runId,
|
|
1586
|
+
runAttempt: this.currentRunAttempt,
|
|
1587
|
+
...(leaseId ? { leaseId } : {}),
|
|
1588
|
+
});
|
|
1589
|
+
if (!released || typeof released.key !== 'string' || !released.key.trim()) {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
return {
|
|
1593
|
+
...released,
|
|
1594
|
+
key: released.key.trim(),
|
|
1595
|
+
runId: released.runId ?? null,
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
private async heartbeatRuntimeStepReceipt(
|
|
1600
|
+
key: string,
|
|
1601
|
+
runId: string,
|
|
1602
|
+
leaseId: string,
|
|
1603
|
+
): Promise<RuntimeStepReceipt | null> {
|
|
1604
|
+
if (!this.#options.heartbeatRuntimeStepReceipts) {
|
|
1605
|
+
return null;
|
|
1606
|
+
}
|
|
1607
|
+
const [receipt] = await this.#options.heartbeatRuntimeStepReceipts({
|
|
1608
|
+
runId,
|
|
1609
|
+
runAttempt: this.currentRunAttempt,
|
|
1610
|
+
leaseId,
|
|
1611
|
+
keys: [key],
|
|
1612
|
+
});
|
|
1613
|
+
if (!receipt || typeof receipt.key !== 'string' || !receipt.key.trim()) {
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
return {
|
|
1617
|
+
...receipt,
|
|
1618
|
+
key: receipt.key.trim(),
|
|
1619
|
+
runId: receipt.runId ?? null,
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
private async assertRuntimeToolReceiptOwnership(
|
|
1624
|
+
requests: ToolCallRequest[],
|
|
1625
|
+
): Promise<void> {
|
|
1626
|
+
const targets = requests.flatMap((request) => {
|
|
1627
|
+
const receiptKey = request.receiptKey?.trim() || null;
|
|
1628
|
+
if (!receiptKey) return [];
|
|
1629
|
+
const leaseId = request.receiptLeaseId?.trim() || null;
|
|
1630
|
+
if (!leaseId) return [];
|
|
1631
|
+
return [{ receiptKey, leaseId }];
|
|
1632
|
+
});
|
|
1633
|
+
if (targets.length === 0) return;
|
|
1634
|
+
|
|
1635
|
+
const byLeaseId = new Map<string, string[]>();
|
|
1636
|
+
for (const target of targets) {
|
|
1637
|
+
const keys = byLeaseId.get(target.leaseId) ?? [];
|
|
1638
|
+
keys.push(target.receiptKey);
|
|
1639
|
+
byLeaseId.set(target.leaseId, keys);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (this.#options.heartbeatRuntimeStepReceipts) {
|
|
1643
|
+
for (const [leaseId, keys] of byLeaseId) {
|
|
1644
|
+
const receipts = await this.#options.heartbeatRuntimeStepReceipts({
|
|
1645
|
+
runId: this.currentRunId,
|
|
1646
|
+
runAttempt: this.currentRunAttempt,
|
|
1647
|
+
leaseId,
|
|
1648
|
+
keys,
|
|
1649
|
+
});
|
|
1650
|
+
this.assertRuntimeToolReceiptHeartbeatResult(keys, leaseId, receipts);
|
|
1651
|
+
}
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (
|
|
1656
|
+
!this.#options.getRuntimeStepReceipt &&
|
|
1657
|
+
!this.#options.getRuntimeStepReceipts
|
|
1658
|
+
) {
|
|
1659
|
+
throw new RuntimeReceiptLeaseLostError({
|
|
1660
|
+
receiptKey: targets[0]?.receiptKey ?? 'unknown',
|
|
1661
|
+
runId: this.currentRunId,
|
|
1662
|
+
leaseId: targets[0]?.leaseId ?? 'unknown',
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const latest = await this.getRuntimeStepReceipts(
|
|
1667
|
+
targets.map((target) => target.receiptKey),
|
|
1668
|
+
);
|
|
1669
|
+
for (const target of targets) {
|
|
1670
|
+
const receipt = latest.get(target.receiptKey);
|
|
1671
|
+
if (!this.runtimeToolReceiptStillOwned(receipt, target.leaseId)) {
|
|
1672
|
+
throw new RuntimeReceiptLeaseLostError({
|
|
1673
|
+
receiptKey: target.receiptKey,
|
|
1674
|
+
runId: this.currentRunId,
|
|
1675
|
+
leaseId: target.leaseId,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
private assertRuntimeToolReceiptHeartbeatResult(
|
|
1682
|
+
keys: string[],
|
|
1683
|
+
leaseId: string,
|
|
1684
|
+
receipts: Array<RuntimeStepReceipt | null>,
|
|
1685
|
+
): void {
|
|
1686
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
1687
|
+
const receipt = this.normalizeRuntimeStepReceipt(
|
|
1688
|
+
keys[index] ?? '',
|
|
1689
|
+
receipts[index],
|
|
1690
|
+
);
|
|
1691
|
+
if (!this.runtimeToolReceiptStillOwned(receipt, leaseId)) {
|
|
1692
|
+
throw new RuntimeReceiptLeaseLostError({
|
|
1693
|
+
receiptKey: keys[index] ?? 'unknown',
|
|
1694
|
+
runId: this.currentRunId,
|
|
1695
|
+
leaseId,
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
private runtimeToolReceiptStillOwned(
|
|
1702
|
+
receipt: RuntimeStepReceipt | null | undefined,
|
|
1703
|
+
leaseId: string,
|
|
1704
|
+
): boolean {
|
|
1705
|
+
if (!receipt || receipt.status !== 'running') return false;
|
|
1706
|
+
if (receipt.leaseId !== leaseId) return false;
|
|
1707
|
+
const ownerRunId = receipt.leaseOwnerRunId ?? receipt.runId ?? null;
|
|
1708
|
+
return !ownerRunId || ownerRunId === this.currentRunId;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
private runtimeToolReceiptStillOwnedByCurrentAttempt(
|
|
1712
|
+
receipt: RuntimeStepReceipt | null | undefined,
|
|
1713
|
+
leaseId: string | null | undefined,
|
|
1714
|
+
): boolean {
|
|
1715
|
+
if (!receipt || receipt.status !== 'running') return false;
|
|
1716
|
+
if (leaseId?.trim()) {
|
|
1717
|
+
if (receipt.leaseId !== leaseId.trim()) return false;
|
|
1718
|
+
} else if (receipt.leaseId != null) {
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
const ownerRunId = receipt.leaseOwnerRunId ?? receipt.runId ?? null;
|
|
1722
|
+
if (ownerRunId !== this.currentRunId) return false;
|
|
1723
|
+
const ownerAttempt =
|
|
1724
|
+
typeof receipt.leaseOwnerAttempt === 'number'
|
|
1725
|
+
? receipt.leaseOwnerAttempt
|
|
1726
|
+
: 0;
|
|
1727
|
+
return ownerAttempt === this.currentRunAttempt;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1153
1730
|
private async failRuntimeStepReceipt(
|
|
1154
1731
|
key: string,
|
|
1155
1732
|
runId: string,
|
|
1156
1733
|
error: string,
|
|
1734
|
+
leaseId?: string | null,
|
|
1735
|
+
failureKind?: WorkReceiptFailureKind | null,
|
|
1157
1736
|
): Promise<RuntimeStepReceipt | null> {
|
|
1158
1737
|
if (!this.#options.failRuntimeStepReceipt) {
|
|
1159
1738
|
return null;
|
|
@@ -1161,6 +1740,9 @@ export class PlayContextImpl {
|
|
|
1161
1740
|
const failed = await this.#options.failRuntimeStepReceipt({
|
|
1162
1741
|
key,
|
|
1163
1742
|
runId,
|
|
1743
|
+
runAttempt: this.currentRunAttempt,
|
|
1744
|
+
...(leaseId ? { leaseId } : {}),
|
|
1745
|
+
...(failureKind ? { failureKind } : {}),
|
|
1164
1746
|
error,
|
|
1165
1747
|
});
|
|
1166
1748
|
if (!failed || typeof failed.key !== 'string' || !failed.key.trim()) {
|
|
@@ -1220,6 +1802,7 @@ export class PlayContextImpl {
|
|
|
1220
1802
|
runId: string,
|
|
1221
1803
|
reclaimRunning = false,
|
|
1222
1804
|
forceRefresh = false,
|
|
1805
|
+
forceFailedRefresh = false,
|
|
1223
1806
|
): Promise<Map<string, RuntimeStepReceipt>> {
|
|
1224
1807
|
const uniqueKeys = [
|
|
1225
1808
|
...new Set(keys.map((key) => key.trim()).filter(Boolean)),
|
|
@@ -1229,8 +1812,11 @@ export class PlayContextImpl {
|
|
|
1229
1812
|
? await this.#options.claimRuntimeStepReceipts({
|
|
1230
1813
|
keys: uniqueKeys,
|
|
1231
1814
|
runId,
|
|
1815
|
+
runAttempt: this.currentRunAttempt,
|
|
1816
|
+
leaseAware: true,
|
|
1232
1817
|
...(reclaimRunning ? { reclaimRunning: true } : {}),
|
|
1233
1818
|
...(forceRefresh ? { forceRefresh: true } : {}),
|
|
1819
|
+
...(forceFailedRefresh ? { forceFailedRefresh: true } : {}),
|
|
1234
1820
|
})
|
|
1235
1821
|
: await Promise.all(
|
|
1236
1822
|
uniqueKeys.map((key) =>
|
|
@@ -1239,6 +1825,7 @@ export class PlayContextImpl {
|
|
|
1239
1825
|
runId,
|
|
1240
1826
|
reclaimRunning,
|
|
1241
1827
|
forceRefresh,
|
|
1828
|
+
forceFailedRefresh,
|
|
1242
1829
|
),
|
|
1243
1830
|
),
|
|
1244
1831
|
);
|
|
@@ -1254,26 +1841,44 @@ export class PlayContextImpl {
|
|
|
1254
1841
|
}
|
|
1255
1842
|
|
|
1256
1843
|
private async completeRuntimeStepReceipts(
|
|
1257
|
-
receipts: Array<{
|
|
1844
|
+
receipts: Array<{
|
|
1845
|
+
key: string;
|
|
1846
|
+
runId: string;
|
|
1847
|
+
runAttempt?: number | null;
|
|
1848
|
+
output: unknown | null;
|
|
1849
|
+
leaseId?: string | null;
|
|
1850
|
+
}>,
|
|
1258
1851
|
): Promise<Map<string, RuntimeStepReceipt>> {
|
|
1259
1852
|
const normalizedInputs = receipts.filter((receipt) => receipt.key.trim());
|
|
1260
1853
|
if (normalizedInputs.length === 0) return new Map();
|
|
1261
1854
|
for (const receipt of normalizedInputs) {
|
|
1262
1855
|
assertNoSecretTaint(receipt.output, 'ctx.tool receipt output');
|
|
1263
1856
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1857
|
+
let completed: Array<RuntimeStepReceipt | null>;
|
|
1858
|
+
try {
|
|
1859
|
+
completed = this.#options.completeRuntimeStepReceipts
|
|
1860
|
+
? await this.#options.completeRuntimeStepReceipts({
|
|
1861
|
+
receipts: normalizedInputs.map((receipt) => ({
|
|
1862
|
+
...receipt,
|
|
1863
|
+
runAttempt: receipt.runAttempt ?? this.currentRunAttempt,
|
|
1864
|
+
})),
|
|
1865
|
+
})
|
|
1866
|
+
: await Promise.all(
|
|
1867
|
+
normalizedInputs.map((receipt) =>
|
|
1868
|
+
this.completeRuntimeStepReceipt(
|
|
1869
|
+
receipt.key,
|
|
1870
|
+
receipt.runId,
|
|
1871
|
+
receipt.output,
|
|
1872
|
+
receipt.leaseId,
|
|
1873
|
+
),
|
|
1274
1874
|
),
|
|
1275
|
-
)
|
|
1276
|
-
|
|
1875
|
+
);
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
await this.recoverRuntimeStepReceiptCompletionsAfterBulkError(
|
|
1878
|
+
normalizedInputs,
|
|
1879
|
+
);
|
|
1880
|
+
throw error;
|
|
1881
|
+
}
|
|
1277
1882
|
const byKey = new Map<string, RuntimeStepReceipt>();
|
|
1278
1883
|
for (let index = 0; index < completed.length; index += 1) {
|
|
1279
1884
|
const normalized = this.normalizeRuntimeStepReceipt(
|
|
@@ -1282,17 +1887,123 @@ export class PlayContextImpl {
|
|
|
1282
1887
|
);
|
|
1283
1888
|
if (normalized) byKey.set(normalized.key, normalized);
|
|
1284
1889
|
}
|
|
1890
|
+
await this.reconcileMissingRuntimeStepReceiptCompletions(
|
|
1891
|
+
normalizedInputs,
|
|
1892
|
+
byKey,
|
|
1893
|
+
);
|
|
1285
1894
|
return byKey;
|
|
1286
1895
|
}
|
|
1287
1896
|
|
|
1897
|
+
private async reconcileMissingRuntimeStepReceiptCompletions(
|
|
1898
|
+
receipts: Array<{
|
|
1899
|
+
key: string;
|
|
1900
|
+
runId: string;
|
|
1901
|
+
runAttempt?: number | null;
|
|
1902
|
+
output: unknown | null;
|
|
1903
|
+
leaseId?: string | null;
|
|
1904
|
+
}>,
|
|
1905
|
+
completedByKey: Map<string, RuntimeStepReceipt>,
|
|
1906
|
+
): Promise<void> {
|
|
1907
|
+
if (
|
|
1908
|
+
receipts.length === completedByKey.size ||
|
|
1909
|
+
(!this.#options.getRuntimeStepReceipt &&
|
|
1910
|
+
!this.#options.getRuntimeStepReceipts)
|
|
1911
|
+
) {
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
const missingKeys = receipts
|
|
1915
|
+
.map((receipt) => receipt.key.trim())
|
|
1916
|
+
.filter((key) => key && !completedByKey.has(key));
|
|
1917
|
+
if (missingKeys.length === 0) return;
|
|
1918
|
+
const latest = await this.getRuntimeStepReceipts(missingKeys);
|
|
1919
|
+
for (const key of missingKeys) {
|
|
1920
|
+
const recovered = latest.get(key);
|
|
1921
|
+
if (
|
|
1922
|
+
recovered?.status === 'completed' ||
|
|
1923
|
+
recovered?.status === 'skipped'
|
|
1924
|
+
) {
|
|
1925
|
+
completedByKey.set(key, recovered);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const recoveredCount = missingKeys.filter((key) =>
|
|
1929
|
+
completedByKey.has(key),
|
|
1930
|
+
).length;
|
|
1931
|
+
if (recoveredCount > 0) {
|
|
1932
|
+
this.log(
|
|
1933
|
+
`Runtime tool receipt completion response reconciled ${recoveredCount}/${missingKeys.length} missing receipt(s) from durable store read-back.`,
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
private async recoverRuntimeStepReceiptCompletionsAfterBulkError(
|
|
1939
|
+
receipts: Array<{
|
|
1940
|
+
key: string;
|
|
1941
|
+
runId: string;
|
|
1942
|
+
runAttempt?: number | null;
|
|
1943
|
+
output: unknown | null;
|
|
1944
|
+
leaseId?: string | null;
|
|
1945
|
+
}>,
|
|
1946
|
+
): Promise<void> {
|
|
1947
|
+
if (
|
|
1948
|
+
receipts.length === 0 ||
|
|
1949
|
+
(!this.#options.getRuntimeStepReceipt &&
|
|
1950
|
+
!this.#options.getRuntimeStepReceipts)
|
|
1951
|
+
) {
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
let latest: Map<string, RuntimeStepReceipt>;
|
|
1955
|
+
try {
|
|
1956
|
+
latest = await this.getRuntimeStepReceipts(
|
|
1957
|
+
receipts.map((receipt) => receipt.key),
|
|
1958
|
+
);
|
|
1959
|
+
} catch {
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
await Promise.allSettled(
|
|
1963
|
+
receipts.map(async (receipt) => {
|
|
1964
|
+
const recovered = latest.get(receipt.key);
|
|
1965
|
+
if (
|
|
1966
|
+
recovered?.status === 'completed' ||
|
|
1967
|
+
recovered?.status === 'skipped'
|
|
1968
|
+
) {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
if (
|
|
1972
|
+
!this.runtimeToolReceiptStillOwnedByCurrentAttempt(
|
|
1973
|
+
recovered,
|
|
1974
|
+
receipt.leaseId,
|
|
1975
|
+
)
|
|
1976
|
+
) {
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
await this.completeRuntimeStepReceipt(
|
|
1980
|
+
receipt.key,
|
|
1981
|
+
receipt.runId,
|
|
1982
|
+
receipt.output,
|
|
1983
|
+
receipt.leaseId,
|
|
1984
|
+
);
|
|
1985
|
+
}),
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1288
1989
|
private async failRuntimeStepReceipts(
|
|
1289
|
-
receipts: Array<{
|
|
1990
|
+
receipts: Array<{
|
|
1991
|
+
key: string;
|
|
1992
|
+
runId: string;
|
|
1993
|
+
runAttempt?: number | null;
|
|
1994
|
+
error: string;
|
|
1995
|
+
leaseId?: string | null;
|
|
1996
|
+
failureKind?: WorkReceiptFailureKind | null;
|
|
1997
|
+
}>,
|
|
1290
1998
|
): Promise<Map<string, RuntimeStepReceipt>> {
|
|
1291
1999
|
const normalizedInputs = receipts.filter((receipt) => receipt.key.trim());
|
|
1292
2000
|
if (normalizedInputs.length === 0) return new Map();
|
|
1293
2001
|
const failed = this.#options.failRuntimeStepReceipts
|
|
1294
2002
|
? await this.#options.failRuntimeStepReceipts({
|
|
1295
|
-
receipts: normalizedInputs
|
|
2003
|
+
receipts: normalizedInputs.map((receipt) => ({
|
|
2004
|
+
...receipt,
|
|
2005
|
+
runAttempt: receipt.runAttempt ?? this.currentRunAttempt,
|
|
2006
|
+
})),
|
|
1296
2007
|
})
|
|
1297
2008
|
: await Promise.all(
|
|
1298
2009
|
normalizedInputs.map((receipt) =>
|
|
@@ -1300,6 +2011,8 @@ export class PlayContextImpl {
|
|
|
1300
2011
|
receipt.key,
|
|
1301
2012
|
receipt.runId,
|
|
1302
2013
|
receipt.error,
|
|
2014
|
+
receipt.leaseId,
|
|
2015
|
+
receipt.failureKind,
|
|
1303
2016
|
),
|
|
1304
2017
|
),
|
|
1305
2018
|
);
|
|
@@ -1327,45 +2040,98 @@ export class PlayContextImpl {
|
|
|
1327
2040
|
);
|
|
1328
2041
|
}
|
|
1329
2042
|
|
|
1330
|
-
private async
|
|
2043
|
+
private async durableToolCallCacheKeyForScope(input: {
|
|
1331
2044
|
toolId: string;
|
|
1332
2045
|
requestInput: Record<string, unknown>;
|
|
2046
|
+
executionAuthScopeDigest?: string | null;
|
|
1333
2047
|
staleAfterSeconds?: number | null;
|
|
2048
|
+
playLocalScope?: string | null;
|
|
1334
2049
|
}): Promise<string> {
|
|
1335
2050
|
const providerActionVersion =
|
|
1336
2051
|
(await this.#options.getToolActionCacheVersion?.(input.toolId))?.trim() ??
|
|
1337
2052
|
'';
|
|
2053
|
+
const executionAuthScopeDigest =
|
|
2054
|
+
input.executionAuthScopeDigest?.trim() ||
|
|
2055
|
+
(await this.resolveToolAuthScopeDigest(input.toolId))?.trim() ||
|
|
2056
|
+
null;
|
|
1338
2057
|
return buildDurableToolCallCacheKey({
|
|
1339
2058
|
orgId: this.#options.orgId,
|
|
1340
|
-
|
|
2059
|
+
playLocalScope: input.playLocalScope,
|
|
1341
2060
|
toolId: input.toolId,
|
|
1342
2061
|
requestInput: input.requestInput,
|
|
1343
2062
|
authScopeDigest: buildDurableToolCallAuthScopeDigest({
|
|
1344
2063
|
orgId: this.#options.orgId,
|
|
1345
|
-
userEmail: this.#options.userEmail,
|
|
1346
2064
|
toolId: input.toolId,
|
|
2065
|
+
executionAuthScopeDigest,
|
|
1347
2066
|
}),
|
|
1348
2067
|
providerActionVersion,
|
|
1349
2068
|
staleAfterSeconds: input.staleAfterSeconds,
|
|
1350
2069
|
});
|
|
1351
2070
|
}
|
|
1352
2071
|
|
|
2072
|
+
private async durableToolCallCacheKey(input: {
|
|
2073
|
+
toolId: string;
|
|
2074
|
+
requestInput: Record<string, unknown>;
|
|
2075
|
+
executionAuthScopeDigest?: string | null;
|
|
2076
|
+
staleAfterSeconds?: number | null;
|
|
2077
|
+
}): Promise<string> {
|
|
2078
|
+
return await this.durableToolCallCacheKeyForScope({
|
|
2079
|
+
...input,
|
|
2080
|
+
playLocalScope: this.governance.currentPlayId,
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
private async resolveToolAuthScopeDigest(
|
|
2085
|
+
toolId: string,
|
|
2086
|
+
): Promise<string | null> {
|
|
2087
|
+
const configured = await this.#options.getToolAuthScopeDigest?.(toolId);
|
|
2088
|
+
if (typeof configured === 'string' && configured.trim()) {
|
|
2089
|
+
return configured.trim();
|
|
2090
|
+
}
|
|
2091
|
+
return null;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
private invalidateToolAuthScopeDigest(toolId: string): void {
|
|
2095
|
+
this.#options.invalidateToolAuthScopeDigest?.(toolId);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
1353
2098
|
private durableReceiptExecutionStore(): DurableReceiptExecutionStore {
|
|
1354
2099
|
return {
|
|
1355
2100
|
enabled: Boolean(this.#options.claimRuntimeStepReceipt),
|
|
1356
2101
|
get: (receiptKey) => this.getRuntimeStepReceipt(receiptKey),
|
|
1357
2102
|
getMany: (receiptKeys) => this.getRuntimeStepReceipts(receiptKeys),
|
|
1358
|
-
claim: (
|
|
2103
|
+
claim: (
|
|
2104
|
+
receiptKey,
|
|
2105
|
+
runId,
|
|
2106
|
+
reclaimRunning,
|
|
2107
|
+
forceRefresh,
|
|
2108
|
+
forceFailedRefresh,
|
|
2109
|
+
) =>
|
|
1359
2110
|
this.claimRuntimeStepReceipt(
|
|
1360
2111
|
receiptKey,
|
|
1361
2112
|
runId,
|
|
1362
2113
|
reclaimRunning,
|
|
1363
2114
|
forceRefresh,
|
|
2115
|
+
forceFailedRefresh,
|
|
2116
|
+
),
|
|
2117
|
+
complete: (receiptKey, runId, output, leaseId) =>
|
|
2118
|
+
this.completeRuntimeStepReceipt(receiptKey, runId, output, leaseId),
|
|
2119
|
+
release: (receiptKey, runId, leaseId) =>
|
|
2120
|
+
this.releaseRuntimeStepReceipt(receiptKey, runId, leaseId),
|
|
2121
|
+
...(this.#options.heartbeatRuntimeStepReceipts
|
|
2122
|
+
? {
|
|
2123
|
+
heartbeat: (receiptKey, runId, leaseId) =>
|
|
2124
|
+
this.heartbeatRuntimeStepReceipt(receiptKey, runId, leaseId),
|
|
2125
|
+
}
|
|
2126
|
+
: {}),
|
|
2127
|
+
fail: (receiptKey, runId, error, leaseId, failureKind) =>
|
|
2128
|
+
this.failRuntimeStepReceipt(
|
|
2129
|
+
receiptKey,
|
|
2130
|
+
runId,
|
|
2131
|
+
error,
|
|
2132
|
+
leaseId,
|
|
2133
|
+
failureKind,
|
|
1364
2134
|
),
|
|
1365
|
-
complete: (receiptKey, runId, output) =>
|
|
1366
|
-
this.completeRuntimeStepReceipt(receiptKey, runId, output),
|
|
1367
|
-
fail: (receiptKey, runId, error) =>
|
|
1368
|
-
this.failRuntimeStepReceipt(receiptKey, runId, error),
|
|
1369
2135
|
canPersistFailure: Boolean(this.#options.failRuntimeStepReceipt),
|
|
1370
2136
|
canPersistCompletion: Boolean(this.#options.completeRuntimeStepReceipt),
|
|
1371
2137
|
};
|
|
@@ -1407,7 +2173,8 @@ export class PlayContextImpl {
|
|
|
1407
2173
|
source: DurableReceiptRecoverySource,
|
|
1408
2174
|
) => T;
|
|
1409
2175
|
onClaimedResult?: (output: T, receiptKey: string) => T;
|
|
1410
|
-
|
|
2176
|
+
shouldPersistFailure?: (error: unknown) => boolean;
|
|
2177
|
+
execute: (context: { leaseId: string | null }) => Promise<T>;
|
|
1411
2178
|
},
|
|
1412
2179
|
): Promise<T> {
|
|
1413
2180
|
const receiptKey =
|
|
@@ -1436,6 +2203,7 @@ export class PlayContextImpl {
|
|
|
1436
2203
|
markSkipped: opts.markSkipped,
|
|
1437
2204
|
onRecovered: opts.onRecovered,
|
|
1438
2205
|
onClaimedResult: opts.onClaimedResult,
|
|
2206
|
+
shouldPersistFailure: opts.shouldPersistFailure,
|
|
1439
2207
|
formatError: (error) => this.formatRuntimeError(error),
|
|
1440
2208
|
log: (message) => this.log(message),
|
|
1441
2209
|
execute: opts.execute,
|
|
@@ -1446,6 +2214,13 @@ export class PlayContextImpl {
|
|
|
1446
2214
|
return this.#options.runId ?? this.governance.currentRunId ?? 'unknown';
|
|
1447
2215
|
}
|
|
1448
2216
|
|
|
2217
|
+
private get currentRunAttempt(): number {
|
|
2218
|
+
const attempt = this.#options.runAttempt;
|
|
2219
|
+
return typeof attempt === 'number' && Number.isFinite(attempt)
|
|
2220
|
+
? Math.max(0, Math.floor(attempt))
|
|
2221
|
+
: 0;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
1449
2224
|
private emitScopedFieldMetaUpdate(input: {
|
|
1450
2225
|
rowId: number;
|
|
1451
2226
|
key: string | null;
|
|
@@ -1659,12 +2434,17 @@ export class PlayContextImpl {
|
|
|
1659
2434
|
|
|
1660
2435
|
private effectiveToolCallCachePolicy(options?: ToolCallOptions): {
|
|
1661
2436
|
force: boolean;
|
|
2437
|
+
forceFailedRefresh: boolean;
|
|
1662
2438
|
staleAfterSeconds?: number | null;
|
|
1663
2439
|
} {
|
|
1664
2440
|
return {
|
|
1665
2441
|
force:
|
|
1666
2442
|
options?.force === true ||
|
|
1667
2443
|
this.#options.cachePolicy?.forceToolRefresh === true,
|
|
2444
|
+
forceFailedRefresh:
|
|
2445
|
+
options?.force === true ||
|
|
2446
|
+
this.#options.cachePolicy?.forceToolRefresh === true ||
|
|
2447
|
+
this.#options.cachePolicy?.forceFailedToolRefresh === true,
|
|
1668
2448
|
staleAfterSeconds: options?.staleAfterSeconds ?? null,
|
|
1669
2449
|
};
|
|
1670
2450
|
}
|
|
@@ -1685,6 +2465,19 @@ export class PlayContextImpl {
|
|
|
1685
2465
|
return this.checkpoint.completedToolBatches[toolId]?.[rowCacheKey];
|
|
1686
2466
|
}
|
|
1687
2467
|
|
|
2468
|
+
private getCachedToolResultCandidate(
|
|
2469
|
+
toolId: string,
|
|
2470
|
+
rowCacheKeys: readonly string[],
|
|
2471
|
+
): { cacheKey: string; result: ToolBatchResult } | null {
|
|
2472
|
+
for (const rowCacheKey of rowCacheKeys) {
|
|
2473
|
+
const cached = this.getCachedToolResult(toolId, rowCacheKey);
|
|
2474
|
+
if (cached?.done) {
|
|
2475
|
+
return { cacheKey: rowCacheKey, result: cached };
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
return null;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
1688
2481
|
private cacheToolResult(
|
|
1689
2482
|
toolId: string,
|
|
1690
2483
|
rowCacheKey: string,
|
|
@@ -1914,6 +2707,7 @@ export class PlayContextImpl {
|
|
|
1914
2707
|
{
|
|
1915
2708
|
key: receiptKey,
|
|
1916
2709
|
runId: this.currentRunId,
|
|
2710
|
+
leaseId: request.receiptLeaseId ?? null,
|
|
1917
2711
|
output: serializeToolExecuteResult(wrapped),
|
|
1918
2712
|
},
|
|
1919
2713
|
])
|
|
@@ -1969,12 +2763,13 @@ export class PlayContextImpl {
|
|
|
1969
2763
|
{
|
|
1970
2764
|
key: receiptKey,
|
|
1971
2765
|
runId: this.currentRunId,
|
|
2766
|
+
leaseId: entry.request.receiptLeaseId ?? null,
|
|
1972
2767
|
output: serializeToolExecuteResult(entry.wrapped),
|
|
1973
2768
|
},
|
|
1974
2769
|
]
|
|
1975
2770
|
: [];
|
|
1976
2771
|
});
|
|
1977
|
-
let completedByKey
|
|
2772
|
+
let completedByKey = new Map<string, RuntimeStepReceipt>();
|
|
1978
2773
|
if (receiptInputs.length > 0) {
|
|
1979
2774
|
try {
|
|
1980
2775
|
completedByKey = await this.completeRuntimeStepReceipts(receiptInputs);
|
|
@@ -1982,10 +2777,15 @@ export class PlayContextImpl {
|
|
|
1982
2777
|
// Completion failed AFTER successful (billed) tool calls: trip the
|
|
1983
2778
|
// breaker so remaining rows stop dispatching new provider calls.
|
|
1984
2779
|
tripRuntimePersistenceLatch(this.persistenceLatch, receiptError);
|
|
2780
|
+
await Promise.all(
|
|
2781
|
+
wrappedEntries.map((entry) =>
|
|
2782
|
+
this.rejectToolCall(toolId, entry.request, receiptError, {
|
|
2783
|
+
persistReceiptFailure: false,
|
|
2784
|
+
}),
|
|
2785
|
+
),
|
|
2786
|
+
);
|
|
1985
2787
|
throw receiptError;
|
|
1986
2788
|
}
|
|
1987
|
-
} else {
|
|
1988
|
-
completedByKey = new Map<string, RuntimeStepReceipt>();
|
|
1989
2789
|
}
|
|
1990
2790
|
const results: unknown[] = [];
|
|
1991
2791
|
for (const entry of wrappedEntries) {
|
|
@@ -2009,9 +2809,23 @@ export class PlayContextImpl {
|
|
|
2009
2809
|
completed: RuntimeStepReceipt | null | undefined,
|
|
2010
2810
|
): Promise<unknown> {
|
|
2011
2811
|
const cacheKey = request.cacheKey;
|
|
2812
|
+
const receiptKey = request.receiptKey?.trim() || null;
|
|
2813
|
+
const canPersistReceiptCompletion = Boolean(
|
|
2814
|
+
this.#options.completeRuntimeStepReceipt ||
|
|
2815
|
+
this.#options.completeRuntimeStepReceipts,
|
|
2816
|
+
);
|
|
2817
|
+
if (
|
|
2818
|
+
receiptKey &&
|
|
2819
|
+
canPersistReceiptCompletion &&
|
|
2820
|
+
completed?.status !== 'completed' &&
|
|
2821
|
+
completed?.status !== 'skipped'
|
|
2822
|
+
) {
|
|
2823
|
+
throw new Error(
|
|
2824
|
+
`Durable tool call ${receiptKey} lost receipt ownership before completion.`,
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2012
2827
|
const finalWrapped =
|
|
2013
|
-
|
|
2014
|
-
completed.runId !== this.currentRunId
|
|
2828
|
+
completed?.status === 'completed' || completed?.status === 'skipped'
|
|
2015
2829
|
? await this.wrapToolExecutionResult({
|
|
2016
2830
|
toolId,
|
|
2017
2831
|
status:
|
|
@@ -2030,8 +2844,8 @@ export class PlayContextImpl {
|
|
|
2030
2844
|
: {
|
|
2031
2845
|
kind: 'cache',
|
|
2032
2846
|
cacheKey,
|
|
2033
|
-
receiptKey:
|
|
2034
|
-
attachedToReceiptKey:
|
|
2847
|
+
receiptKey: receiptKey ?? '',
|
|
2848
|
+
attachedToReceiptKey: receiptKey,
|
|
2035
2849
|
},
|
|
2036
2850
|
),
|
|
2037
2851
|
})
|
|
@@ -2076,7 +2890,9 @@ export class PlayContextImpl {
|
|
|
2076
2890
|
{
|
|
2077
2891
|
key: receiptKey,
|
|
2078
2892
|
runId: this.currentRunId,
|
|
2893
|
+
leaseId: request.receiptLeaseId ?? null,
|
|
2079
2894
|
error: message,
|
|
2895
|
+
failureKind: runtimeReceiptFailureKindForError(error),
|
|
2080
2896
|
},
|
|
2081
2897
|
]);
|
|
2082
2898
|
} catch (receiptError) {
|
|
@@ -2190,8 +3006,7 @@ export class PlayContextImpl {
|
|
|
2190
3006
|
_key: string,
|
|
2191
3007
|
_items: PlayDatasetInput<T>,
|
|
2192
3008
|
_input?:
|
|
2193
|
-
|
|
2194
|
-
| RuntimeStepProgram,
|
|
3009
|
+
MapFieldDefinition<T, Record<string, unknown>> | RuntimeStepProgram,
|
|
2195
3010
|
_options?: DatasetOptions<T>,
|
|
2196
3011
|
): never {
|
|
2197
3012
|
void _key;
|
|
@@ -2277,8 +3092,7 @@ export class PlayContextImpl {
|
|
|
2277
3092
|
|
|
2278
3093
|
const userKeyOption = options?.key;
|
|
2279
3094
|
let explicitKeyResolver:
|
|
2280
|
-
|
|
2281
|
-
| null = null;
|
|
3095
|
+
((row: Record<string, unknown>, index: number) => string) | null = null;
|
|
2282
3096
|
if (userKeyOption !== undefined) {
|
|
2283
3097
|
explicitKeyResolver = (row, index) => {
|
|
2284
3098
|
const inputRow = stripFieldOutputs(row);
|
|
@@ -2360,6 +3174,7 @@ export class PlayContextImpl {
|
|
|
2360
3174
|
this.toOutputRow(item as Record<string, unknown>),
|
|
2361
3175
|
);
|
|
2362
3176
|
itemOriginalIndexes = materializedItems.map((_item, index) => index);
|
|
3177
|
+
const mapStartSeedRowsByKey = new Map<string, Record<string, unknown>>();
|
|
2363
3178
|
if (this.#options.onMapStart) {
|
|
2364
3179
|
const shouldPassRowKey = explicitKeyResolver != null;
|
|
2365
3180
|
// toSerializableCsvAliasedRow (not a plain spread): projected CSV
|
|
@@ -2381,6 +3196,7 @@ export class PlayContextImpl {
|
|
|
2381
3196
|
playName: this.#options.playName,
|
|
2382
3197
|
playId: this.#options.playId,
|
|
2383
3198
|
runId: this.#options.runId,
|
|
3199
|
+
executorToken: this.#options.executorToken,
|
|
2384
3200
|
staticPipeline: this.#options.staticPipeline,
|
|
2385
3201
|
forceRefresh: this.#options.cachePolicy?.forceToolRefresh === true,
|
|
2386
3202
|
},
|
|
@@ -2388,13 +3204,14 @@ export class PlayContextImpl {
|
|
|
2388
3204
|
resolvedTableNamespace = normalizeTableNamespace(
|
|
2389
3205
|
mapStartResult.tableNamespace,
|
|
2390
3206
|
);
|
|
3207
|
+
if ((mapStartResult.blockedRows?.length ?? 0) > 0) {
|
|
3208
|
+
throw new RuntimeSheetRowsBlockedError({
|
|
3209
|
+
tableNamespace: resolvedTableNamespace,
|
|
3210
|
+
blockedRows: mapStartResult.blockedRows ?? [],
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
2391
3213
|
const persistedRowIdentity = (row: Record<string, unknown>, index = 0) =>
|
|
2392
3214
|
resolveMapRowOutcomeKey(row) ?? rowIdentity(row, index);
|
|
2393
|
-
const pendingKeys = new Set(
|
|
2394
|
-
mapStartResult.pendingRows.map((row, index) =>
|
|
2395
|
-
persistedRowIdentity(row, index),
|
|
2396
|
-
),
|
|
2397
|
-
);
|
|
2398
3215
|
const pendingRowsByKey = new Map<string, Record<string, unknown>>();
|
|
2399
3216
|
for (
|
|
2400
3217
|
let index = 0;
|
|
@@ -2405,15 +3222,20 @@ export class PlayContextImpl {
|
|
|
2405
3222
|
const rowKey = persistedRowIdentity(row, index);
|
|
2406
3223
|
if (rowKey) pendingRowsByKey.set(rowKey, row);
|
|
2407
3224
|
}
|
|
2408
|
-
const
|
|
3225
|
+
for (const row of mapStartResult.completedRows ?? []) {
|
|
3226
|
+
const rowKey = persistedRowIdentity(row);
|
|
3227
|
+
if (!rowKey) continue;
|
|
3228
|
+
mapStartSeedRowsByKey.set(rowKey, row);
|
|
3229
|
+
}
|
|
3230
|
+
const seededItems = materializedItems
|
|
2409
3231
|
.map((item, index) => ({
|
|
2410
3232
|
row: this.toOutputRow(item as Record<string, unknown>),
|
|
2411
3233
|
index,
|
|
2412
3234
|
}))
|
|
2413
|
-
.filter(({ row, index }) => pendingKeys.has(rowIdentity(row, index)))
|
|
2414
3235
|
.map(({ row, index }) => {
|
|
2415
3236
|
const rowKey = rowIdentity(row, index);
|
|
2416
|
-
const persisted =
|
|
3237
|
+
const persisted =
|
|
3238
|
+
pendingRowsByKey.get(rowKey) ?? mapStartSeedRowsByKey.get(rowKey);
|
|
2417
3239
|
// Persisted rows lose the non-enumerable CSV alias projections on
|
|
2418
3240
|
// the sheet round-trip. Merge the persisted data/meta over the
|
|
2419
3241
|
// in-memory row so key functions and column resolvers keep seeing
|
|
@@ -2423,8 +3245,8 @@ export class PlayContextImpl {
|
|
|
2423
3245
|
index,
|
|
2424
3246
|
};
|
|
2425
3247
|
});
|
|
2426
|
-
itemsToProcess =
|
|
2427
|
-
itemOriginalIndexes =
|
|
3248
|
+
itemsToProcess = seededItems.map((item) => item.row);
|
|
3249
|
+
itemOriginalIndexes = seededItems.map((item) => item.index);
|
|
2428
3250
|
}
|
|
2429
3251
|
|
|
2430
3252
|
const mapScope = this.createMapExecutionScope({
|
|
@@ -2495,6 +3317,7 @@ export class PlayContextImpl {
|
|
|
2495
3317
|
playName: this.#options.playName,
|
|
2496
3318
|
playId: this.#options.playId,
|
|
2497
3319
|
runId: this.#options.runId,
|
|
3320
|
+
executorToken: this.#options.executorToken,
|
|
2498
3321
|
tableNamespace: resolvedTableNamespace,
|
|
2499
3322
|
rows: chunk,
|
|
2500
3323
|
outputFields: datasetColumnNames.filter((field) =>
|
|
@@ -2580,7 +3403,7 @@ export class PlayContextImpl {
|
|
|
2580
3403
|
const directCompletedResults = mapResult.completedRows.map((row) =>
|
|
2581
3404
|
this.toPublicOutputRow(row.data),
|
|
2582
3405
|
);
|
|
2583
|
-
|
|
3406
|
+
let results =
|
|
2584
3407
|
mapResult.failedRows.length === 0 &&
|
|
2585
3408
|
directCompletedResults.length === rawItems.length
|
|
2586
3409
|
? directCompletedResults
|
|
@@ -2633,6 +3456,49 @@ export class PlayContextImpl {
|
|
|
2633
3456
|
}
|
|
2634
3457
|
this.activeMapCellMeta = null;
|
|
2635
3458
|
|
|
3459
|
+
if (this.#options.onMapRowsCompleted) {
|
|
3460
|
+
results = await reconcileNodeRuntimeMapResultsWithPersistedSheet({
|
|
3461
|
+
mapName: normalizedMapNamespace,
|
|
3462
|
+
tableNamespace: resolvedTableNamespace,
|
|
3463
|
+
runId: this.#options.runId,
|
|
3464
|
+
expectedRows: totalInputCount,
|
|
3465
|
+
currentRows: results,
|
|
3466
|
+
failedRowCount: mapResult.failedRows.length,
|
|
3467
|
+
log: (line) => this.log(line),
|
|
3468
|
+
readPersistedRows: async (readInput) => {
|
|
3469
|
+
if (
|
|
3470
|
+
!this.#options.baseUrl ||
|
|
3471
|
+
!this.#options.executorToken ||
|
|
3472
|
+
!this.#options.playName ||
|
|
3473
|
+
!this.#options.runId
|
|
3474
|
+
) {
|
|
3475
|
+
throw new Error(
|
|
3476
|
+
`Runtime sheet finalization mismatch for ctx.dataset("${normalizedMapNamespace}"): ` +
|
|
3477
|
+
'cannot verify terminal persisted rows because the Node runtime context is missing ' +
|
|
3478
|
+
'baseUrl, executorToken, playName, or runId.',
|
|
3479
|
+
);
|
|
3480
|
+
}
|
|
3481
|
+
const result = await readRuntimeSheetDatasetRows(
|
|
3482
|
+
{
|
|
3483
|
+
baseUrl: this.#options.baseUrl,
|
|
3484
|
+
executorToken: this.#options.executorToken,
|
|
3485
|
+
playName: this.#options.playName,
|
|
3486
|
+
userEmail: this.#options.userEmail,
|
|
3487
|
+
runId: this.#options.runId,
|
|
3488
|
+
},
|
|
3489
|
+
{
|
|
3490
|
+
tableNamespace: resolvedTableNamespace,
|
|
3491
|
+
runId: this.#options.runId,
|
|
3492
|
+
rowMode: readInput.rowMode,
|
|
3493
|
+
limit: readInput.limit,
|
|
3494
|
+
offset: readInput.offset,
|
|
3495
|
+
},
|
|
3496
|
+
);
|
|
3497
|
+
return result.rows;
|
|
3498
|
+
},
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
|
|
2636
3502
|
return createDeferredPlayDataset({
|
|
2637
3503
|
datasetKind: 'map',
|
|
2638
3504
|
datasetId: createRuntimeDatasetId(
|
|
@@ -2909,7 +3775,7 @@ export class PlayContextImpl {
|
|
|
2909
3775
|
this.lastDatasetStep = datasetStep;
|
|
2910
3776
|
this.activeDatasetStep = null;
|
|
2911
3777
|
this.log(
|
|
2912
|
-
`Map completed: ${results.length + completedRows} results (${results.length}
|
|
3778
|
+
`Map completed: ${results.length + completedRows} results (${results.length} succeeded, 0 failed, ${completedRows} duplicate keys skipped)`,
|
|
2913
3779
|
);
|
|
2914
3780
|
return {
|
|
2915
3781
|
completedRows: results.map((row, index) =>
|
|
@@ -3183,6 +4049,17 @@ export class PlayContextImpl {
|
|
|
3183
4049
|
}
|
|
3184
4050
|
return result.value;
|
|
3185
4051
|
});
|
|
4052
|
+
const accountedRowIndexes = new Set<number>();
|
|
4053
|
+
for (const row of completedRowsToPersist) {
|
|
4054
|
+
if (typeof row.inputIndex === 'number') {
|
|
4055
|
+
accountedRowIndexes.add(row.inputIndex);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
for (const row of failedRowsToPersist) {
|
|
4059
|
+
if (typeof row.inputIndex === 'number') {
|
|
4060
|
+
accountedRowIndexes.add(row.inputIndex);
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
3186
4063
|
if (this.pendingRowEventBoundaries.length > 0) {
|
|
3187
4064
|
const completedRowKeys = new Set<string>();
|
|
3188
4065
|
for (let index = 0; index < settledResults.length; index += 1) {
|
|
@@ -3195,6 +4072,7 @@ export class PlayContextImpl {
|
|
|
3195
4072
|
if (key) {
|
|
3196
4073
|
completedRowKeys.add(key);
|
|
3197
4074
|
}
|
|
4075
|
+
accountedRowIndexes.add(executionRowIndex(index));
|
|
3198
4076
|
continue;
|
|
3199
4077
|
}
|
|
3200
4078
|
const row = result as Record<string, unknown>;
|
|
@@ -3238,6 +4116,19 @@ export class PlayContextImpl {
|
|
|
3238
4116
|
boundaries: uniqueBoundaries,
|
|
3239
4117
|
});
|
|
3240
4118
|
}
|
|
4119
|
+
if (accountedRowIndexes.size !== items.length) {
|
|
4120
|
+
const expectedRowIndexes = items.map((_item, index) =>
|
|
4121
|
+
executionRowIndex(index),
|
|
4122
|
+
);
|
|
4123
|
+
const missingRowIndexes = expectedRowIndexes.filter(
|
|
4124
|
+
(index) => !accountedRowIndexes.has(index),
|
|
4125
|
+
);
|
|
4126
|
+
throw new Error(
|
|
4127
|
+
`Runtime map execution accounting mismatch for ctx.dataset("${normalizedTableNamespace}"): ` +
|
|
4128
|
+
`expected ${items.length} row(s), accounted for ${accountedRowIndexes.size}; ` +
|
|
4129
|
+
`missing input index(es): ${missingRowIndexes.slice(0, 10).join(', ') || 'unknown'}.`,
|
|
4130
|
+
);
|
|
4131
|
+
}
|
|
3241
4132
|
const results = settledResults.filter(
|
|
3242
4133
|
(result): result is Record<string, unknown> =>
|
|
3243
4134
|
result !== WAITING_ROW && result !== FAILED_ROW,
|
|
@@ -3249,8 +4140,18 @@ export class PlayContextImpl {
|
|
|
3249
4140
|
emitEventType: 'map.completed',
|
|
3250
4141
|
});
|
|
3251
4142
|
}
|
|
4143
|
+
for (const failedRow of failedRowsToPersist.slice(0, 3)) {
|
|
4144
|
+
this.log(
|
|
4145
|
+
`row ${failedMapRowLogLabel(failedRow)} failed: ${failedRow.error ?? 'unknown error'}`,
|
|
4146
|
+
);
|
|
4147
|
+
}
|
|
4148
|
+
if (failedRowsToPersist.length > 3) {
|
|
4149
|
+
this.log(
|
|
4150
|
+
`${failedRowsToPersist.length - 3} additional row failure(s) omitted from map log`,
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
3252
4153
|
this.log(
|
|
3253
|
-
`Map completed: ${results.length + completedRows} results (${results.length}
|
|
4154
|
+
`Map completed: ${results.length + completedRows} results (${results.length} succeeded, ${failedRowsToPersist.length} failed, ${completedRows} duplicate keys skipped)`,
|
|
3254
4155
|
);
|
|
3255
4156
|
return {
|
|
3256
4157
|
completedRows: [...completedRowsToPersist].sort(
|
|
@@ -3560,6 +4461,13 @@ export class PlayContextImpl {
|
|
|
3560
4461
|
if (!hasPendingWork) {
|
|
3561
4462
|
const status = await raceSettled();
|
|
3562
4463
|
if (status === 'done' && this.toolCallQueue.length === 0) {
|
|
4464
|
+
if (!this.activeDatasetStep) {
|
|
4465
|
+
const childPlaySuspension =
|
|
4466
|
+
this.consumePendingChildPlaySuspension();
|
|
4467
|
+
if (childPlaySuspension) {
|
|
4468
|
+
throw new PlayExecutionSuspendedError(childPlaySuspension);
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
3563
4471
|
break;
|
|
3564
4472
|
}
|
|
3565
4473
|
|
|
@@ -3656,18 +4564,23 @@ export class PlayContextImpl {
|
|
|
3656
4564
|
toolId,
|
|
3657
4565
|
requestInput: input,
|
|
3658
4566
|
});
|
|
4567
|
+
let executionAuthScopeDigest =
|
|
4568
|
+
(await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null;
|
|
3659
4569
|
const cacheableToolResult = !isQueryResultDatasetReadRequest(toolId, input);
|
|
3660
|
-
|
|
4570
|
+
let durableCacheKey = await this.durableToolCallCacheKey({
|
|
3661
4571
|
toolId,
|
|
3662
4572
|
requestInput: input,
|
|
4573
|
+
executionAuthScopeDigest,
|
|
3663
4574
|
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
3664
4575
|
});
|
|
3665
|
-
const
|
|
4576
|
+
const checkpointCacheKeys = [durableCacheKey];
|
|
3666
4577
|
const eventWaitHandler =
|
|
3667
4578
|
(await this.#options.getIntegrationEventWaitHandler?.(toolId)) ?? null;
|
|
3668
4579
|
const store = rowContext.getStore();
|
|
3669
4580
|
|
|
3670
|
-
const executeTool = async (
|
|
4581
|
+
const executeTool = async (context?: {
|
|
4582
|
+
leaseId?: string | null;
|
|
4583
|
+
}): Promise<unknown> => {
|
|
3671
4584
|
if (eventWaitHandler) {
|
|
3672
4585
|
return this.executeIntegrationEventWaitTool(
|
|
3673
4586
|
toolId,
|
|
@@ -3694,17 +4607,17 @@ export class PlayContextImpl {
|
|
|
3694
4607
|
? null
|
|
3695
4608
|
: toolCachePolicy.force
|
|
3696
4609
|
? null
|
|
3697
|
-
: this.
|
|
3698
|
-
if (cached
|
|
3699
|
-
this.log(`
|
|
4610
|
+
: this.getCachedToolResultCandidate(toolId, checkpointCacheKeys);
|
|
4611
|
+
if (cached) {
|
|
4612
|
+
this.log(`Calling tool: ${toolId} recovered from checkpoint`);
|
|
3700
4613
|
return await this.wrapToolExecutionResult({
|
|
3701
4614
|
toolId,
|
|
3702
|
-
status: cached.result == null ? 'no_result' : 'completed',
|
|
3703
|
-
result: cached.result,
|
|
4615
|
+
status: cached.result.result == null ? 'no_result' : 'completed',
|
|
4616
|
+
result: cached.result.result,
|
|
3704
4617
|
requestInput: input,
|
|
3705
4618
|
execution: toolExecutionMetadataForOutcome({
|
|
3706
4619
|
kind: 'checkpoint',
|
|
3707
|
-
cacheKey:
|
|
4620
|
+
cacheKey: cached.cacheKey,
|
|
3708
4621
|
}),
|
|
3709
4622
|
});
|
|
3710
4623
|
}
|
|
@@ -3714,6 +4627,17 @@ export class PlayContextImpl {
|
|
|
3714
4627
|
: `Calling tool: ${toolId}`,
|
|
3715
4628
|
);
|
|
3716
4629
|
const execution = await this.callToolExecutionAPI(toolId, input, {
|
|
4630
|
+
...(cacheableToolResult
|
|
4631
|
+
? {
|
|
4632
|
+
durableCallReceiptKey: directCacheKey,
|
|
4633
|
+
executionAuthScopeDigest,
|
|
4634
|
+
providerIdempotencyKey: this.providerIdempotencyKeyForToolCall({
|
|
4635
|
+
cacheKey: directCacheKey,
|
|
4636
|
+
force: toolCachePolicy.force,
|
|
4637
|
+
leaseId: context?.leaseId ?? null,
|
|
4638
|
+
}),
|
|
4639
|
+
}
|
|
4640
|
+
: {}),
|
|
3717
4641
|
timeoutMs: resolveToolRuntimeTimeoutMs(toolId, options?.timeoutMs),
|
|
3718
4642
|
});
|
|
3719
4643
|
const wrapped = await this.wrapToolExecutionResult({
|
|
@@ -3763,17 +4687,17 @@ export class PlayContextImpl {
|
|
|
3763
4687
|
? null
|
|
3764
4688
|
: toolCachePolicy.force
|
|
3765
4689
|
? null
|
|
3766
|
-
: this.
|
|
3767
|
-
if (cached
|
|
3768
|
-
this.log(` Row ${rowId} ${toolId}:
|
|
4690
|
+
: this.getCachedToolResultCandidate(toolId, checkpointCacheKeys);
|
|
4691
|
+
if (cached) {
|
|
4692
|
+
this.log(` Row ${rowId} ${toolId}: recovered from checkpoint`);
|
|
3769
4693
|
return await this.wrapToolExecutionResult({
|
|
3770
4694
|
toolId,
|
|
3771
|
-
status: cached.result == null ? 'no_result' : 'completed',
|
|
3772
|
-
result: cached.result,
|
|
4695
|
+
status: cached.result.result == null ? 'no_result' : 'completed',
|
|
4696
|
+
result: cached.result.result,
|
|
3773
4697
|
requestInput: input,
|
|
3774
4698
|
execution: toolExecutionMetadataForOutcome({
|
|
3775
4699
|
kind: 'checkpoint',
|
|
3776
|
-
cacheKey:
|
|
4700
|
+
cacheKey: cached.cacheKey,
|
|
3777
4701
|
}),
|
|
3778
4702
|
});
|
|
3779
4703
|
}
|
|
@@ -3806,8 +4730,10 @@ export class PlayContextImpl {
|
|
|
3806
4730
|
callId,
|
|
3807
4731
|
cacheKey: toolResultCacheKey,
|
|
3808
4732
|
cacheable: cacheableToolResult,
|
|
3809
|
-
receiptKey:
|
|
4733
|
+
receiptKey: cacheableToolResult ? durableCacheKey : null,
|
|
4734
|
+
executionAuthScopeDigest,
|
|
3810
4735
|
force: toolCachePolicy.force,
|
|
4736
|
+
forceFailedRefresh: toolCachePolicy.forceFailedRefresh,
|
|
3811
4737
|
rowId,
|
|
3812
4738
|
fieldName,
|
|
3813
4739
|
toolId,
|
|
@@ -3827,41 +4753,81 @@ export class PlayContextImpl {
|
|
|
3827
4753
|
return await executeTool();
|
|
3828
4754
|
}
|
|
3829
4755
|
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
4756
|
+
for (
|
|
4757
|
+
let authScopeAttempt = 0;
|
|
4758
|
+
authScopeAttempt < 2;
|
|
4759
|
+
authScopeAttempt += 1
|
|
4760
|
+
) {
|
|
4761
|
+
try {
|
|
4762
|
+
return await this.executeWithRuntimeReceipt(
|
|
4763
|
+
'tool',
|
|
4764
|
+
normalizedKey,
|
|
4765
|
+
this.currentRunId,
|
|
4766
|
+
{
|
|
4767
|
+
receiptKey: durableCacheKey,
|
|
4768
|
+
semanticKey: toolRequestIdentity,
|
|
4769
|
+
force: toolCachePolicy.force,
|
|
4770
|
+
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
4771
|
+
markSkipped: () => {
|
|
4772
|
+
this.log(
|
|
4773
|
+
`ctx.tools.execute(${toolId}): no-op due completed receipt ${toolRequestIdentity} (label: ${normalizedKey})`,
|
|
4774
|
+
);
|
|
4775
|
+
},
|
|
4776
|
+
onClaimedResult: (output, receiptKey) =>
|
|
4777
|
+
markToolExecuteResultExecutionOutcome(output, {
|
|
4778
|
+
kind: 'live',
|
|
4779
|
+
receiptKey,
|
|
4780
|
+
}),
|
|
4781
|
+
onRecovered: (output, _receipt, source) =>
|
|
4782
|
+
markToolExecuteResultExecutionOutcome(
|
|
4783
|
+
output,
|
|
4784
|
+
toolExecutionOutcomeForDurableReceipt({
|
|
4785
|
+
source,
|
|
4786
|
+
receiptKey: durableCacheKey,
|
|
4787
|
+
}),
|
|
4788
|
+
),
|
|
4789
|
+
shouldPersistFailure: (error) =>
|
|
4790
|
+
!(error instanceof ToolExecuteAuthScopeChangedError),
|
|
4791
|
+
execute: ({ leaseId }) => executeTool({ leaseId }),
|
|
4792
|
+
runningReceiptWaitMaxAttempts:
|
|
4793
|
+
resolveRuntimeToolReceiptWaitMaxAttempts(
|
|
4794
|
+
typeof options?.receiptWaitMs === 'number'
|
|
4795
|
+
? { max_wait_ms: options.receiptWaitMs }
|
|
4796
|
+
: input,
|
|
4797
|
+
),
|
|
4798
|
+
},
|
|
4799
|
+
);
|
|
4800
|
+
} catch (error) {
|
|
4801
|
+
if (
|
|
4802
|
+
!(error instanceof ToolExecuteAuthScopeChangedError) ||
|
|
4803
|
+
authScopeAttempt > 0
|
|
4804
|
+
) {
|
|
4805
|
+
throw error;
|
|
4806
|
+
}
|
|
4807
|
+
// Make the bounded auth-scope re-claim observable in run logs instead
|
|
4808
|
+
// of a silent continuation: the credential identity changed after the
|
|
4809
|
+
// receipt key was prepared, so we evict the cached digest, re-resolve,
|
|
4810
|
+
// and re-claim a fresh receipt under the new scope before retrying once.
|
|
4811
|
+
this.log(
|
|
4812
|
+
`ctx.tools.execute(${toolId}): auth_scope_changed_reclaim ` +
|
|
4813
|
+
`(label: ${normalizedKey}); credential identity changed mid-run, ` +
|
|
4814
|
+
`re-resolving auth scope and re-claiming a fresh receipt under the ` +
|
|
4815
|
+
`new scope (attempt ${authScopeAttempt + 1}/2).`,
|
|
4816
|
+
);
|
|
4817
|
+
this.invalidateToolAuthScopeDigest(toolId);
|
|
4818
|
+
executionAuthScopeDigest =
|
|
4819
|
+
(await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null;
|
|
4820
|
+
durableCacheKey = await this.durableToolCallCacheKey({
|
|
4821
|
+
toolId,
|
|
4822
|
+
requestInput: input,
|
|
4823
|
+
executionAuthScopeDigest,
|
|
4824
|
+
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
4825
|
+
});
|
|
4826
|
+
checkpointCacheKeys[0] = durableCacheKey;
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
throw new ToolExecuteAuthScopeChangedError();
|
|
3865
4831
|
}
|
|
3866
4832
|
|
|
3867
4833
|
private async executeIntegrationEventWaitTool(
|
|
@@ -3987,163 +4953,349 @@ export class PlayContextImpl {
|
|
|
3987
4953
|
if (!resolvedName.trim()) {
|
|
3988
4954
|
throw new Error('ctx.runPlay(...) requires a resolvable play name.');
|
|
3989
4955
|
}
|
|
4956
|
+
const scheduledChildPlayCall = this.shouldLaunchChildPlaysThroughScheduler()
|
|
4957
|
+
? this.beginScheduledChildPlayCall()
|
|
4958
|
+
: null;
|
|
3990
4959
|
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
// Reserve depth + per-parent + descendant/play-call budget on the parent
|
|
4009
|
-
// and fork the lineage-global snapshot for the child. forkChild owns the
|
|
4010
|
-
// cycle guard, depth cap, per-parent cap, and play/descendant charges, so
|
|
4011
|
-
// a recovered receipt (which skips this body) never consumes budget. The
|
|
4012
|
-
// child run id stays deterministic — it matches the prior post-increment
|
|
4013
|
-
// playCallCount — so receipt keys and replay are stable.
|
|
4014
|
-
const childRunId = `${this.governance.rootRunId}:${resolvedName}:${this.governor.snapshot().playCallCount + 1}`;
|
|
4015
|
-
const childGovernance = this.governor.forkChild({
|
|
4960
|
+
try {
|
|
4961
|
+
if (!this.#options.resolvePlay) {
|
|
4962
|
+
throw new Error(
|
|
4963
|
+
'ctx.runPlay(...) is unavailable because no play resolver was configured.',
|
|
4964
|
+
);
|
|
4965
|
+
}
|
|
4966
|
+
const resolvePlay = this.#options.resolvePlay;
|
|
4967
|
+
const resolvedPlay = await resolvePlay(resolvedName);
|
|
4968
|
+
if (!resolvedPlay) {
|
|
4969
|
+
throw new Error(
|
|
4970
|
+
`Unable to resolve play "${resolvedName}" for ctx.runPlay(...).`,
|
|
4971
|
+
);
|
|
4972
|
+
}
|
|
4973
|
+
const childRevisionFingerprint =
|
|
4974
|
+
resolvedPlayRevisionFingerprint(resolvedPlay);
|
|
4975
|
+
const runPlayRowScope = rowContext.getStore();
|
|
4976
|
+
const runPlaySemanticKey = buildDurableRunPlaySemanticKey({
|
|
4016
4977
|
childPlayName: resolvedName,
|
|
4017
|
-
|
|
4978
|
+
childRevisionFingerprint,
|
|
4979
|
+
input,
|
|
4980
|
+
rowScope: runPlayRowScope
|
|
4981
|
+
? {
|
|
4982
|
+
fieldName: runPlayRowScope.fieldName,
|
|
4983
|
+
rowId: runPlayRowScope.rowId,
|
|
4984
|
+
rowKey: runPlayRowScope.rowKey ?? null,
|
|
4985
|
+
tableNamespace: runPlayRowScope.tableNamespace ?? null,
|
|
4986
|
+
}
|
|
4987
|
+
: null,
|
|
4018
4988
|
});
|
|
4019
|
-
const childPlaySlot = await this.governor.acquireChildPlaySlot();
|
|
4020
|
-
const rowStore = rowContext.getStore();
|
|
4021
|
-
const producer = {
|
|
4022
|
-
kind: 'play' as const,
|
|
4023
|
-
id: normalizedKey,
|
|
4024
|
-
playId: resolvedName,
|
|
4025
|
-
displayName: displayNameFromProducerId(resolvedName),
|
|
4026
|
-
runId: childRunId,
|
|
4027
|
-
};
|
|
4028
4989
|
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
runId: childGovernance.currentRunId,
|
|
4050
|
-
staticPipeline: resolvedPlay.staticPipeline ?? null,
|
|
4051
|
-
checkpoint: this.checkpoint,
|
|
4052
|
-
governance: childGovernance,
|
|
4053
|
-
getRuntimeStepReceipt: undefined,
|
|
4054
|
-
getRuntimeStepReceipts: undefined,
|
|
4055
|
-
claimRuntimeStepReceipt: undefined,
|
|
4056
|
-
claimRuntimeStepReceipts: undefined,
|
|
4057
|
-
completeRuntimeStepReceipt: undefined,
|
|
4058
|
-
completeRuntimeStepReceipts: undefined,
|
|
4059
|
-
failRuntimeStepReceipt: undefined,
|
|
4060
|
-
failRuntimeStepReceipts: undefined,
|
|
4061
|
-
skipRuntimeStepReceipt: undefined,
|
|
4062
|
-
});
|
|
4063
|
-
const childExecution = this.executeResolvedPlay(
|
|
4064
|
-
resolvedPlay,
|
|
4065
|
-
childContext,
|
|
4990
|
+
const executePlayCall = async (): Promise<TOutput> => {
|
|
4991
|
+
// Reserve depth + per-parent + descendant/play-call budget on the parent
|
|
4992
|
+
// and fork the lineage-global snapshot for the child. forkChild owns the
|
|
4993
|
+
// cycle guard, depth cap, per-parent cap, and play/descendant charges, so
|
|
4994
|
+
// a recovered receipt (which skips this body) never consumes budget.
|
|
4995
|
+
//
|
|
4996
|
+
// The child run id is derived through the shared canonical builder so the
|
|
4997
|
+
// in-process runtime and the workers_edge coordinator produce the same
|
|
4998
|
+
// FORMAT (`play/<slug>/run/child-<digest>`) via the same code. The digest
|
|
4999
|
+
// is deterministic across replay because every input (parent lineage,
|
|
5000
|
+
// call key, input) is stable. Per-substrate difference: in-process has no
|
|
5001
|
+
// coordinator idempotency key and no compiled child graphHash, so it uses
|
|
5002
|
+
// the semantic fallback with graphHash omitted; workers_edge feeds the
|
|
5003
|
+
// durable child idempotency key. See child-run-id.ts.
|
|
5004
|
+
const childRunId = buildChildRunId({
|
|
5005
|
+
childPlayName: resolvedName,
|
|
5006
|
+
parentRunId: this.currentRunId,
|
|
5007
|
+
parentPlayName: this.#options.playName,
|
|
5008
|
+
key: normalizedKey,
|
|
5009
|
+
runPlaySemanticKey,
|
|
4066
5010
|
input,
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
const
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
stage: resolvedName,
|
|
4079
|
-
provider: 'deepline_native',
|
|
4080
|
-
error: null,
|
|
4081
|
-
producer,
|
|
4082
|
-
dataPatch: {},
|
|
4083
|
-
});
|
|
4084
|
-
}
|
|
4085
|
-
const step = {
|
|
4086
|
-
type: 'play_call' as const,
|
|
5011
|
+
graphHash: null,
|
|
5012
|
+
});
|
|
5013
|
+
const childGovernance = this.governor.forkChild({
|
|
5014
|
+
childPlayName: resolvedName,
|
|
5015
|
+
childRunId,
|
|
5016
|
+
});
|
|
5017
|
+
const childPlaySlot = await this.governor.acquireChildSubmitSlot();
|
|
5018
|
+
const rowStore = rowContext.getStore();
|
|
5019
|
+
const producer = {
|
|
5020
|
+
kind: 'play' as const,
|
|
5021
|
+
id: normalizedKey,
|
|
4087
5022
|
playId: resolvedName,
|
|
4088
|
-
|
|
4089
|
-
|
|
5023
|
+
displayName: displayNameFromProducerId(resolvedName),
|
|
5024
|
+
runId: childRunId,
|
|
4090
5025
|
};
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
5026
|
+
|
|
5027
|
+
try {
|
|
5028
|
+
if (rowStore) {
|
|
5029
|
+
this.emitScopedFieldMetaUpdate({
|
|
5030
|
+
rowId: rowStore.rowId,
|
|
5031
|
+
key: rowStore.rowKey ?? null,
|
|
5032
|
+
tableNamespace: rowStore.tableNamespace ?? null,
|
|
5033
|
+
fieldName: rowStore.fieldName,
|
|
5034
|
+
status: 'running',
|
|
5035
|
+
rowStatus: 'running',
|
|
5036
|
+
stage: resolvedName,
|
|
5037
|
+
provider: 'deepline_native',
|
|
5038
|
+
error: null,
|
|
5039
|
+
producer,
|
|
5040
|
+
dataPatch: {},
|
|
5041
|
+
});
|
|
5042
|
+
}
|
|
5043
|
+
if (this.shouldLaunchChildPlaysThroughScheduler()) {
|
|
5044
|
+
const boundaryId = this.childPlayBoundaryId(
|
|
5045
|
+
childGovernance.currentRunId,
|
|
5046
|
+
);
|
|
5047
|
+
const checkpointResult = this.childPlayBoundaryOutput<TOutput>({
|
|
5048
|
+
boundaryId,
|
|
5049
|
+
childRunId: childGovernance.currentRunId,
|
|
5050
|
+
childPlayName: resolvedName,
|
|
5051
|
+
});
|
|
5052
|
+
if (checkpointResult.found) {
|
|
5053
|
+
if (rowStore) {
|
|
5054
|
+
this.emitScopedFieldMetaUpdate({
|
|
5055
|
+
rowId: rowStore.rowId,
|
|
5056
|
+
key: rowStore.rowKey ?? null,
|
|
5057
|
+
tableNamespace: rowStore.tableNamespace ?? null,
|
|
5058
|
+
fieldName: rowStore.fieldName,
|
|
5059
|
+
status: 'completed',
|
|
5060
|
+
rowStatus: 'running',
|
|
5061
|
+
stage: resolvedName,
|
|
5062
|
+
provider: 'deepline_native',
|
|
5063
|
+
error: null,
|
|
5064
|
+
producer,
|
|
5065
|
+
dataPatch: {},
|
|
5066
|
+
});
|
|
5067
|
+
}
|
|
5068
|
+
this.recordPlayCallStep({
|
|
5069
|
+
playId: resolvedName,
|
|
5070
|
+
description: options?.description,
|
|
5071
|
+
});
|
|
5072
|
+
return checkpointResult.output;
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
await this.launchScheduledChildPlay({
|
|
5076
|
+
key: normalizedKey,
|
|
5077
|
+
childRunId: childGovernance.currentRunId,
|
|
5078
|
+
childPlayName: resolvedName,
|
|
5079
|
+
childGovernance,
|
|
5080
|
+
childInput: input,
|
|
5081
|
+
description: options?.description ?? null,
|
|
5082
|
+
});
|
|
5083
|
+
childPlaySlot.release();
|
|
5084
|
+
const terminal = await this.readScheduledChildTerminal<TOutput>({
|
|
5085
|
+
childRunId: childGovernance.currentRunId,
|
|
5086
|
+
childPlayName: resolvedName,
|
|
5087
|
+
});
|
|
5088
|
+
if (terminal?.status === 'completed') {
|
|
5089
|
+
this.checkpoint.resolvedBoundaries = {
|
|
5090
|
+
...(this.checkpoint.resolvedBoundaries ?? {}),
|
|
5091
|
+
[boundaryId]: {
|
|
5092
|
+
kind: 'child_play',
|
|
5093
|
+
childRunId: childGovernance.currentRunId,
|
|
5094
|
+
childPlayName: resolvedName,
|
|
5095
|
+
status: 'completed',
|
|
5096
|
+
output: terminal.output,
|
|
5097
|
+
completedAt: Date.now(),
|
|
5098
|
+
},
|
|
5099
|
+
};
|
|
5100
|
+
if (rowStore) {
|
|
5101
|
+
this.emitScopedFieldMetaUpdate({
|
|
5102
|
+
rowId: rowStore.rowId,
|
|
5103
|
+
key: rowStore.rowKey ?? null,
|
|
5104
|
+
tableNamespace: rowStore.tableNamespace ?? null,
|
|
5105
|
+
fieldName: rowStore.fieldName,
|
|
5106
|
+
status: 'completed',
|
|
5107
|
+
rowStatus: 'running',
|
|
5108
|
+
stage: resolvedName,
|
|
5109
|
+
provider: 'deepline_native',
|
|
5110
|
+
error: null,
|
|
5111
|
+
producer,
|
|
5112
|
+
dataPatch: {},
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
5115
|
+
this.recordPlayCallStep({
|
|
5116
|
+
playId: resolvedName,
|
|
5117
|
+
description: options?.description,
|
|
5118
|
+
});
|
|
5119
|
+
return terminal.output;
|
|
5120
|
+
}
|
|
5121
|
+
if (terminal) {
|
|
5122
|
+
throw new Error(terminal.error);
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5125
|
+
const suspension = {
|
|
5126
|
+
kind: 'child_play',
|
|
5127
|
+
boundaryId,
|
|
5128
|
+
childRunId: childGovernance.currentRunId,
|
|
5129
|
+
childPlayName: resolvedName,
|
|
5130
|
+
} as const;
|
|
5131
|
+
this.recordPendingChildPlayBoundary(suspension);
|
|
5132
|
+
scheduledChildPlayCall?.release();
|
|
5133
|
+
await this.waitForScheduledChildPlayQuiescence();
|
|
5134
|
+
throw new PlayExecutionSuspendedError(
|
|
5135
|
+
this.consumePendingChildPlaySuspension() ?? suspension,
|
|
5136
|
+
);
|
|
5137
|
+
}
|
|
5138
|
+
const childContext = createPlayContext({
|
|
5139
|
+
...this.#options,
|
|
5140
|
+
playId: resolvedName,
|
|
5141
|
+
playName: resolvedName,
|
|
5142
|
+
runId: childGovernance.currentRunId,
|
|
5143
|
+
executorToken: await this.mintChildExecutorToken({
|
|
5144
|
+
parentRunId: this.currentRunId,
|
|
5145
|
+
parentPlayName: this.#options.playName,
|
|
5146
|
+
childRunId: childGovernance.currentRunId,
|
|
5147
|
+
childPlayName: resolvedName,
|
|
5148
|
+
}),
|
|
5149
|
+
staticPipeline: resolvedPlay.staticPipeline ?? null,
|
|
5150
|
+
checkpoint: this.checkpoint,
|
|
5151
|
+
governance: childGovernance,
|
|
5152
|
+
getRuntimeStepReceipt: undefined,
|
|
5153
|
+
getRuntimeStepReceipts: undefined,
|
|
5154
|
+
claimRuntimeStepReceipt: undefined,
|
|
5155
|
+
claimRuntimeStepReceipts: undefined,
|
|
5156
|
+
completeRuntimeStepReceipt: undefined,
|
|
5157
|
+
completeRuntimeStepReceipts: undefined,
|
|
5158
|
+
failRuntimeStepReceipt: undefined,
|
|
5159
|
+
failRuntimeStepReceipts: undefined,
|
|
5160
|
+
heartbeatRuntimeStepReceipts: undefined,
|
|
5161
|
+
skipRuntimeStepReceipt: undefined,
|
|
4111
5162
|
});
|
|
5163
|
+
const childExecution = this.executeResolvedPlay(
|
|
5164
|
+
resolvedPlay,
|
|
5165
|
+
childContext,
|
|
5166
|
+
input,
|
|
5167
|
+
);
|
|
5168
|
+
await childContext.drainQueuedWork([childExecution]);
|
|
5169
|
+
const result = await childExecution;
|
|
5170
|
+
if (rowStore) {
|
|
5171
|
+
this.emitScopedFieldMetaUpdate({
|
|
5172
|
+
rowId: rowStore.rowId,
|
|
5173
|
+
key: rowStore.rowKey ?? null,
|
|
5174
|
+
tableNamespace: rowStore.tableNamespace ?? null,
|
|
5175
|
+
fieldName: rowStore.fieldName,
|
|
5176
|
+
status: 'completed',
|
|
5177
|
+
rowStatus: 'running',
|
|
5178
|
+
stage: resolvedName,
|
|
5179
|
+
provider: 'deepline_native',
|
|
5180
|
+
error: null,
|
|
5181
|
+
producer,
|
|
5182
|
+
dataPatch: {},
|
|
5183
|
+
});
|
|
5184
|
+
}
|
|
5185
|
+
const step = {
|
|
5186
|
+
type: 'play_call' as const,
|
|
5187
|
+
playId: resolvedName,
|
|
5188
|
+
nestedSteps: childContext.getSteps(),
|
|
5189
|
+
description: normalizeStepDescription(options?.description),
|
|
5190
|
+
};
|
|
5191
|
+
if (this.activeDatasetStep) {
|
|
5192
|
+
this.activeDatasetStep.substeps.push(step);
|
|
5193
|
+
} else {
|
|
5194
|
+
this.steps.push(step);
|
|
5195
|
+
}
|
|
5196
|
+
return result as TOutput;
|
|
5197
|
+
} catch (error) {
|
|
5198
|
+
if (isPlayExecutionSuspendedError(error)) {
|
|
5199
|
+
throw error;
|
|
5200
|
+
}
|
|
5201
|
+
if (rowStore) {
|
|
5202
|
+
this.emitScopedFieldMetaUpdate({
|
|
5203
|
+
rowId: rowStore.rowId,
|
|
5204
|
+
key: rowStore.rowKey ?? null,
|
|
5205
|
+
tableNamespace: rowStore.tableNamespace ?? null,
|
|
5206
|
+
fieldName: rowStore.fieldName,
|
|
5207
|
+
status: 'failed',
|
|
5208
|
+
rowStatus: 'running',
|
|
5209
|
+
stage: resolvedName,
|
|
5210
|
+
provider: 'deepline_native',
|
|
5211
|
+
error: this.formatRuntimeError(error),
|
|
5212
|
+
producer,
|
|
5213
|
+
dataPatch: {},
|
|
5214
|
+
});
|
|
5215
|
+
}
|
|
5216
|
+
throw error;
|
|
5217
|
+
} finally {
|
|
5218
|
+
childPlaySlot.release();
|
|
4112
5219
|
}
|
|
4113
|
-
|
|
4114
|
-
} finally {
|
|
4115
|
-
childPlaySlot.release();
|
|
4116
|
-
}
|
|
4117
|
-
};
|
|
5220
|
+
};
|
|
4118
5221
|
|
|
4119
|
-
|
|
4120
|
-
|
|
5222
|
+
return await this.executeWithRuntimeReceipt<TOutput>(
|
|
5223
|
+
'runPlay',
|
|
5224
|
+
normalizedKey,
|
|
5225
|
+
this.currentRunId,
|
|
5226
|
+
{
|
|
5227
|
+
semanticKey: runPlaySemanticKey,
|
|
5228
|
+
staleAfterSeconds: options?.staleAfterSeconds,
|
|
5229
|
+
markSkipped: () => {
|
|
5230
|
+
this.log(
|
|
5231
|
+
`ctx.runPlay(${normalizedKey}): no-op due completed receipt`,
|
|
5232
|
+
);
|
|
5233
|
+
},
|
|
5234
|
+
repairRunningReceiptForSameRunAfterWaitTimeout: true,
|
|
5235
|
+
runningReceiptWaitMaxAttempts:
|
|
5236
|
+
resolveRuntimeToolReceiptWaitMaxAttempts(input),
|
|
5237
|
+
execute: executePlayCall,
|
|
5238
|
+
},
|
|
5239
|
+
);
|
|
5240
|
+
} finally {
|
|
5241
|
+
scheduledChildPlayCall?.release();
|
|
4121
5242
|
}
|
|
5243
|
+
}
|
|
4122
5244
|
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
5245
|
+
private async mintChildExecutorToken(input: {
|
|
5246
|
+
parentRunId: string;
|
|
5247
|
+
parentPlayName?: string | null;
|
|
5248
|
+
childRunId: string;
|
|
5249
|
+
childPlayName: string;
|
|
5250
|
+
}): Promise<string | undefined> {
|
|
5251
|
+
if (!this.#options.executorToken || !this.#options.baseUrl) {
|
|
5252
|
+
return this.#options.executorToken;
|
|
5253
|
+
}
|
|
5254
|
+
const parentPlayName = input.parentPlayName?.trim();
|
|
5255
|
+
if (!parentPlayName) {
|
|
5256
|
+
return this.#options.executorToken;
|
|
5257
|
+
}
|
|
5258
|
+
const response = await fetch(
|
|
5259
|
+
`${this.#options.baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_CHILD_EXECUTOR_TOKEN_COMPAT_PATH}`,
|
|
4127
5260
|
{
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
`ctx.runPlay(${normalizedKey}): no-op due completed receipt`,
|
|
4139
|
-
);
|
|
5261
|
+
method: 'POST',
|
|
5262
|
+
headers: {
|
|
5263
|
+
Authorization: `Bearer ${this.#options.executorToken}`,
|
|
5264
|
+
'Content-Type': 'application/json',
|
|
5265
|
+
...(this.#options.vercelProtectionBypassToken?.trim()
|
|
5266
|
+
? {
|
|
5267
|
+
'x-vercel-protection-bypass':
|
|
5268
|
+
this.#options.vercelProtectionBypassToken.trim(),
|
|
5269
|
+
}
|
|
5270
|
+
: {}),
|
|
4140
5271
|
},
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
5272
|
+
body: JSON.stringify({
|
|
5273
|
+
parentRunId: input.parentRunId,
|
|
5274
|
+
parentPlayName,
|
|
5275
|
+
childRunId: input.childRunId,
|
|
5276
|
+
childPlayName: input.childPlayName,
|
|
5277
|
+
}),
|
|
4145
5278
|
},
|
|
4146
5279
|
);
|
|
5280
|
+
const body = (await response.json().catch(() => null)) as {
|
|
5281
|
+
executorToken?: unknown;
|
|
5282
|
+
error?: unknown;
|
|
5283
|
+
} | null;
|
|
5284
|
+
if (!response.ok) {
|
|
5285
|
+
const message =
|
|
5286
|
+
typeof body?.error === 'string' && body.error.trim()
|
|
5287
|
+
? body.error.trim()
|
|
5288
|
+
: response.statusText;
|
|
5289
|
+
throw new Error(
|
|
5290
|
+
`ctx.runPlay failed to mint child executor token for "${input.childPlayName}": ${message} (status ${response.status}).`,
|
|
5291
|
+
);
|
|
5292
|
+
}
|
|
5293
|
+
if (typeof body?.executorToken !== 'string' || !body.executorToken.trim()) {
|
|
5294
|
+
throw new Error(
|
|
5295
|
+
`ctx.runPlay failed to mint child executor token for "${input.childPlayName}": response did not include executorToken.`,
|
|
5296
|
+
);
|
|
5297
|
+
}
|
|
5298
|
+
return body.executorToken.trim();
|
|
4147
5299
|
}
|
|
4148
5300
|
|
|
4149
5301
|
/**
|
|
@@ -4630,7 +5782,11 @@ export class PlayContextImpl {
|
|
|
4630
5782
|
const followers = liveFollowersByOwnerCallId.get(owner.callId) ?? [];
|
|
4631
5783
|
liveFollowersByOwnerCallId.delete(owner.callId);
|
|
4632
5784
|
for (const request of [owner, ...followers]) {
|
|
4633
|
-
await this.rejectToolCall(toolId, request, error
|
|
5785
|
+
await this.rejectToolCall(toolId, request, error, {
|
|
5786
|
+
persistReceiptFailure: !(
|
|
5787
|
+
error instanceof ToolExecuteAuthScopeChangedError
|
|
5788
|
+
),
|
|
5789
|
+
});
|
|
4634
5790
|
}
|
|
4635
5791
|
};
|
|
4636
5792
|
const requestsWithLiveFollowers = (
|
|
@@ -4685,7 +5841,11 @@ export class PlayContextImpl {
|
|
|
4685
5841
|
) {
|
|
4686
5842
|
const requestsByReceiptKey = new Map<
|
|
4687
5843
|
string,
|
|
4688
|
-
{
|
|
5844
|
+
{
|
|
5845
|
+
requests: ToolCallRequest[];
|
|
5846
|
+
forceRefresh: boolean;
|
|
5847
|
+
forceFailedRefresh: boolean;
|
|
5848
|
+
}
|
|
4689
5849
|
>();
|
|
4690
5850
|
const localOnlyRequests: ToolCallRequest[] = [];
|
|
4691
5851
|
for (const request of pendingRequests) {
|
|
@@ -4697,17 +5857,37 @@ export class PlayContextImpl {
|
|
|
4697
5857
|
const group = requestsByReceiptKey.get(receiptKey) ?? {
|
|
4698
5858
|
requests: [],
|
|
4699
5859
|
forceRefresh: false,
|
|
5860
|
+
forceFailedRefresh: false,
|
|
4700
5861
|
};
|
|
4701
5862
|
group.requests.push(request);
|
|
4702
5863
|
group.forceRefresh ||= request.force === true;
|
|
5864
|
+
group.forceFailedRefresh ||= request.forceFailedRefresh === true;
|
|
4703
5865
|
requestsByReceiptKey.set(receiptKey, group);
|
|
4704
5866
|
}
|
|
4705
5867
|
const normalReceiptKeys = [...requestsByReceiptKey]
|
|
4706
|
-
.filter(
|
|
5868
|
+
.filter(
|
|
5869
|
+
([, group]) => !group.forceRefresh && !group.forceFailedRefresh,
|
|
5870
|
+
)
|
|
4707
5871
|
.map(([receiptKey]) => receiptKey);
|
|
4708
5872
|
const forcedReceiptKeys = [...requestsByReceiptKey]
|
|
4709
5873
|
.filter(([, group]) => group.forceRefresh)
|
|
4710
5874
|
.map(([receiptKey]) => receiptKey);
|
|
5875
|
+
const failedRefreshReceiptKeys = [...requestsByReceiptKey]
|
|
5876
|
+
.filter(
|
|
5877
|
+
([, group]) => !group.forceRefresh && group.forceFailedRefresh,
|
|
5878
|
+
)
|
|
5879
|
+
.map(([receiptKey]) => receiptKey);
|
|
5880
|
+
const forcedExisting = forcedReceiptKeys.length
|
|
5881
|
+
? await this.getRuntimeStepReceipts(forcedReceiptKeys)
|
|
5882
|
+
: new Map<string, RuntimeStepReceipt>();
|
|
5883
|
+
const forcedRunningReceiptKeys = new Set(
|
|
5884
|
+
[...forcedExisting]
|
|
5885
|
+
.filter(([, receipt]) => receipt.status === 'running')
|
|
5886
|
+
.map(([receiptKey]) => receiptKey),
|
|
5887
|
+
);
|
|
5888
|
+
const claimableForcedReceiptKeys = forcedReceiptKeys.filter(
|
|
5889
|
+
(receiptKey) => !forcedRunningReceiptKeys.has(receiptKey),
|
|
5890
|
+
);
|
|
4711
5891
|
const claims =
|
|
4712
5892
|
normalReceiptKeys.length > 0
|
|
4713
5893
|
? await this.claimRuntimeStepReceipts(
|
|
@@ -4716,17 +5896,30 @@ export class PlayContextImpl {
|
|
|
4716
5896
|
)
|
|
4717
5897
|
: new Map<string, RuntimeStepReceipt>();
|
|
4718
5898
|
const forcedClaims =
|
|
4719
|
-
|
|
5899
|
+
claimableForcedReceiptKeys.length > 0
|
|
4720
5900
|
? await this.claimRuntimeStepReceipts(
|
|
4721
|
-
|
|
5901
|
+
claimableForcedReceiptKeys,
|
|
4722
5902
|
this.currentRunId,
|
|
5903
|
+
false,
|
|
4723
5904
|
true,
|
|
5905
|
+
)
|
|
5906
|
+
: new Map<string, RuntimeStepReceipt>();
|
|
5907
|
+
const failedRefreshClaims =
|
|
5908
|
+
failedRefreshReceiptKeys.length > 0
|
|
5909
|
+
? await this.claimRuntimeStepReceipts(
|
|
5910
|
+
failedRefreshReceiptKeys,
|
|
5911
|
+
this.currentRunId,
|
|
5912
|
+
false,
|
|
5913
|
+
false,
|
|
4724
5914
|
true,
|
|
4725
5915
|
)
|
|
4726
5916
|
: new Map<string, RuntimeStepReceipt>();
|
|
4727
5917
|
for (const [receiptKey, claim] of forcedClaims) {
|
|
4728
5918
|
claims.set(receiptKey, claim);
|
|
4729
5919
|
}
|
|
5920
|
+
for (const [receiptKey, claim] of failedRefreshClaims) {
|
|
5921
|
+
claims.set(receiptKey, claim);
|
|
5922
|
+
}
|
|
4730
5923
|
const claimedRequests: ToolCallRequest[] = [...localOnlyRequests];
|
|
4731
5924
|
const durableRecoveredRequests: ToolCallRequest[] = [];
|
|
4732
5925
|
const resolveRequestsFromReceipt = async (
|
|
@@ -4738,9 +5931,7 @@ export class PlayContextImpl {
|
|
|
4738
5931
|
if (!request) return;
|
|
4739
5932
|
const receiptKey = request.receiptKey?.trim() || null;
|
|
4740
5933
|
if (!receiptKey) {
|
|
4741
|
-
throw new Error(
|
|
4742
|
-
`Durable tool receipt ${source} recovery requires a receipt key.`,
|
|
4743
|
-
);
|
|
5934
|
+
throw new Error(`${source} tool recovery needs receipt key.`);
|
|
4744
5935
|
}
|
|
4745
5936
|
const wrapped = await this.wrapToolExecutionResult({
|
|
4746
5937
|
toolId,
|
|
@@ -4794,6 +5985,7 @@ export class PlayContextImpl {
|
|
|
4794
5985
|
const reclaimExistingRunningReceiptAfterWait = async (
|
|
4795
5986
|
receiptKey: string,
|
|
4796
5987
|
requestsForKey: ToolCallRequest[],
|
|
5988
|
+
forceAfterWait = false,
|
|
4797
5989
|
): Promise<void> => {
|
|
4798
5990
|
const waitMaxAttempts =
|
|
4799
5991
|
requestsForKey.length > 0
|
|
@@ -4808,15 +6000,18 @@ export class PlayContextImpl {
|
|
|
4808
6000
|
)
|
|
4809
6001
|
: undefined;
|
|
4810
6002
|
try {
|
|
4811
|
-
await
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
receiptKey,
|
|
4815
|
-
waitMaxAttempts,
|
|
4816
|
-
),
|
|
4817
|
-
'in_flight',
|
|
6003
|
+
const completed = await this.waitForCompletedRuntimeToolReceipt(
|
|
6004
|
+
receiptKey,
|
|
6005
|
+
waitMaxAttempts,
|
|
4818
6006
|
);
|
|
4819
|
-
|
|
6007
|
+
if (!forceAfterWait) {
|
|
6008
|
+
await resolveRequestsFromReceipt(
|
|
6009
|
+
requestsForKey,
|
|
6010
|
+
completed,
|
|
6011
|
+
'in_flight',
|
|
6012
|
+
);
|
|
6013
|
+
return;
|
|
6014
|
+
}
|
|
4820
6015
|
} catch (error) {
|
|
4821
6016
|
if (!(error instanceof RuntimeReceiptWaitTimeoutError)) {
|
|
4822
6017
|
for (const request of requestsForKey) {
|
|
@@ -4832,6 +6027,7 @@ export class PlayContextImpl {
|
|
|
4832
6027
|
[receiptKey],
|
|
4833
6028
|
this.currentRunId,
|
|
4834
6029
|
true,
|
|
6030
|
+
forceAfterWait,
|
|
4835
6031
|
)
|
|
4836
6032
|
).get(receiptKey);
|
|
4837
6033
|
if (
|
|
@@ -4862,14 +6058,31 @@ export class PlayContextImpl {
|
|
|
4862
6058
|
) {
|
|
4863
6059
|
const [owner, ...waiters] = requestsForKey;
|
|
4864
6060
|
if (!owner) return;
|
|
6061
|
+
if (!reclaimed.leaseId) {
|
|
6062
|
+
throw new RuntimeReceiptLeaseLostError({
|
|
6063
|
+
receiptKey,
|
|
6064
|
+
runId: this.currentRunId,
|
|
6065
|
+
leaseId: 'missing',
|
|
6066
|
+
});
|
|
6067
|
+
}
|
|
6068
|
+
owner.receiptLeaseId = reclaimed.leaseId ?? null;
|
|
4865
6069
|
if (waiters.length > 0) {
|
|
4866
6070
|
liveFollowersByOwnerCallId.set(owner.callId, waiters);
|
|
4867
6071
|
}
|
|
4868
6072
|
try {
|
|
6073
|
+
await this.assertRuntimeToolReceiptOwnership([owner]);
|
|
4869
6074
|
const execution = await this.callToolExecutionAPI(
|
|
4870
6075
|
toolId,
|
|
4871
6076
|
owner.input,
|
|
4872
6077
|
{
|
|
6078
|
+
durableCallReceiptKey: receiptKey,
|
|
6079
|
+
executionAuthScopeDigest: owner.executionAuthScopeDigest,
|
|
6080
|
+
providerIdempotencyKey:
|
|
6081
|
+
this.providerIdempotencyKeyForToolCall({
|
|
6082
|
+
cacheKey: owner.cacheKey,
|
|
6083
|
+
force: owner.force === true,
|
|
6084
|
+
leaseId: owner.receiptLeaseId,
|
|
6085
|
+
}),
|
|
4873
6086
|
timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([owner]),
|
|
4874
6087
|
},
|
|
4875
6088
|
);
|
|
@@ -4904,8 +6117,13 @@ export class PlayContextImpl {
|
|
|
4904
6117
|
const requestsForKey = group.requests;
|
|
4905
6118
|
const claim = claims.get(receiptKey);
|
|
4906
6119
|
if (!claim) {
|
|
4907
|
-
|
|
4908
|
-
|
|
6120
|
+
durableExistingRunningHandlers.push(
|
|
6121
|
+
reclaimExistingRunningReceiptAfterWait(
|
|
6122
|
+
receiptKey,
|
|
6123
|
+
requestsForKey,
|
|
6124
|
+
group.forceRefresh,
|
|
6125
|
+
),
|
|
6126
|
+
);
|
|
4909
6127
|
continue;
|
|
4910
6128
|
}
|
|
4911
6129
|
if (claim?.status === 'completed' || claim?.status === 'skipped') {
|
|
@@ -4928,6 +6146,16 @@ export class PlayContextImpl {
|
|
|
4928
6146
|
claim.claimState !== 'existing'
|
|
4929
6147
|
) {
|
|
4930
6148
|
const [owner, ...waiters] = requestsForKey;
|
|
6149
|
+
if (!claim.leaseId) {
|
|
6150
|
+
throw new RuntimeReceiptLeaseLostError({
|
|
6151
|
+
receiptKey,
|
|
6152
|
+
runId: this.currentRunId,
|
|
6153
|
+
leaseId: 'missing',
|
|
6154
|
+
});
|
|
6155
|
+
}
|
|
6156
|
+
if (owner) {
|
|
6157
|
+
owner.receiptLeaseId = claim.leaseId ?? null;
|
|
6158
|
+
}
|
|
4931
6159
|
claimOwnerWithLiveFollowers(owner, waiters);
|
|
4932
6160
|
continue;
|
|
4933
6161
|
}
|
|
@@ -4939,6 +6167,7 @@ export class PlayContextImpl {
|
|
|
4939
6167
|
reclaimExistingRunningReceiptAfterWait(
|
|
4940
6168
|
receiptKey,
|
|
4941
6169
|
requestsForKey,
|
|
6170
|
+
group.forceRefresh,
|
|
4942
6171
|
),
|
|
4943
6172
|
);
|
|
4944
6173
|
continue;
|
|
@@ -4947,6 +6176,7 @@ export class PlayContextImpl {
|
|
|
4947
6176
|
reclaimExistingRunningReceiptAfterWait(
|
|
4948
6177
|
receiptKey,
|
|
4949
6178
|
requestsForKey,
|
|
6179
|
+
group.forceRefresh,
|
|
4950
6180
|
),
|
|
4951
6181
|
);
|
|
4952
6182
|
}
|
|
@@ -4971,18 +6201,13 @@ export class PlayContextImpl {
|
|
|
4971
6201
|
compiledBatches.length > 0
|
|
4972
6202
|
? await this.governor.suggestedParallelism(
|
|
4973
6203
|
compiledBatches[0]!.batchOperation,
|
|
4974
|
-
|
|
6204
|
+
this.governor.policy.pacing
|
|
6205
|
+
.workerToolBatchDefaultParallelism,
|
|
4975
6206
|
)
|
|
4976
|
-
:
|
|
6207
|
+
: this.governor.policy.pacing.workerToolBatchDefaultParallelism;
|
|
4977
6208
|
await executeChunkedRequests({
|
|
4978
6209
|
requests: compiledBatches,
|
|
4979
6210
|
batchSize,
|
|
4980
|
-
// Bounded dispatch wave (postgres_fast parity with workers_edge):
|
|
4981
|
-
// cap the provider calls (counted by batch member size) a single
|
|
4982
|
-
// chunk dispatches before the latch is read again, so a
|
|
4983
|
-
// persistence failure prevents the rest of a large batch group.
|
|
4984
|
-
maxChunkWeight: PROVIDER_DISPATCH_WAVE_SIZE,
|
|
4985
|
-
weightOf: (batch) => batch.memberRequests.length,
|
|
4986
6211
|
execute: async (batch) => {
|
|
4987
6212
|
// Circuit breaker: skip dispatching this batch's provider call
|
|
4988
6213
|
// once a persistence failure has occurred in this run.
|
|
@@ -4993,10 +6218,42 @@ export class PlayContextImpl {
|
|
|
4993
6218
|
this.persistenceLatch,
|
|
4994
6219
|
);
|
|
4995
6220
|
}
|
|
6221
|
+
await this.assertRuntimeToolReceiptOwnership(
|
|
6222
|
+
batch.memberRequests,
|
|
6223
|
+
);
|
|
6224
|
+
const receiptKeys = batch.memberRequests.map(
|
|
6225
|
+
(request) => request.cacheKey,
|
|
6226
|
+
);
|
|
6227
|
+
const aggregateReceiptKey = buildDurableToolAggregateReceiptKey(
|
|
6228
|
+
{
|
|
6229
|
+
receiptKeys,
|
|
6230
|
+
prefix: 'batch',
|
|
6231
|
+
aggregateReceiptPrefix: buildDurableToolReceiptPrefix({
|
|
6232
|
+
orgId: this.#options.orgId,
|
|
6233
|
+
toolId: batch.batchOperation,
|
|
6234
|
+
}),
|
|
6235
|
+
},
|
|
6236
|
+
);
|
|
4996
6237
|
return await this.callToolAPI(
|
|
4997
6238
|
batch.batchOperation,
|
|
4998
6239
|
batch.batchPayload,
|
|
4999
6240
|
{
|
|
6241
|
+
durableCallReceiptKey: aggregateReceiptKey,
|
|
6242
|
+
executionAuthScopeDigest:
|
|
6243
|
+
batch.memberRequests[0]?.executionAuthScopeDigest ?? null,
|
|
6244
|
+
providerIdempotencyKey:
|
|
6245
|
+
buildDurableToolAggregateProviderIdempotencyKey({
|
|
6246
|
+
aggregateReceiptKey,
|
|
6247
|
+
receiptKeys,
|
|
6248
|
+
providerIdempotencyKeys: batch.memberRequests.map(
|
|
6249
|
+
(request) =>
|
|
6250
|
+
this.providerIdempotencyKeyForToolCall({
|
|
6251
|
+
cacheKey: request.cacheKey,
|
|
6252
|
+
force: request.force === true,
|
|
6253
|
+
leaseId: request.receiptLeaseId,
|
|
6254
|
+
}),
|
|
6255
|
+
),
|
|
6256
|
+
}),
|
|
5000
6257
|
timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners(
|
|
5001
6258
|
batch.memberRequests,
|
|
5002
6259
|
),
|
|
@@ -5042,13 +6299,9 @@ export class PlayContextImpl {
|
|
|
5042
6299
|
},
|
|
5043
6300
|
});
|
|
5044
6301
|
} else {
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
// wave size. Respect a lower provider-pacing suggestion when present.
|
|
5049
|
-
const batchSize = Math.min(
|
|
5050
|
-
await this.governor.suggestedParallelism(toolId, 50),
|
|
5051
|
-
PROVIDER_DISPATCH_WAVE_SIZE,
|
|
6302
|
+
const batchSize = await this.governor.suggestedParallelism(
|
|
6303
|
+
toolId,
|
|
6304
|
+
this.governor.policy.pacing.suggestedMaxParallelism,
|
|
5052
6305
|
);
|
|
5053
6306
|
for (
|
|
5054
6307
|
let start = 0;
|
|
@@ -5071,10 +6324,24 @@ export class PlayContextImpl {
|
|
|
5071
6324
|
return;
|
|
5072
6325
|
}
|
|
5073
6326
|
try {
|
|
6327
|
+
await this.assertRuntimeToolReceiptOwnership([request]);
|
|
5074
6328
|
const execution = await this.callToolExecutionAPI(
|
|
5075
6329
|
toolId,
|
|
5076
6330
|
request.input,
|
|
5077
6331
|
{
|
|
6332
|
+
...(request.receiptKey
|
|
6333
|
+
? {
|
|
6334
|
+
durableCallReceiptKey: request.receiptKey,
|
|
6335
|
+
executionAuthScopeDigest:
|
|
6336
|
+
request.executionAuthScopeDigest,
|
|
6337
|
+
providerIdempotencyKey:
|
|
6338
|
+
this.providerIdempotencyKeyForToolCall({
|
|
6339
|
+
cacheKey: request.receiptKey,
|
|
6340
|
+
force: request.force === true,
|
|
6341
|
+
leaseId: request.receiptLeaseId,
|
|
6342
|
+
}),
|
|
6343
|
+
}
|
|
6344
|
+
: {}),
|
|
5078
6345
|
timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
|
|
5079
6346
|
request,
|
|
5080
6347
|
]),
|
|
@@ -5198,6 +6465,23 @@ export class PlayContextImpl {
|
|
|
5198
6465
|
: execution.result;
|
|
5199
6466
|
}
|
|
5200
6467
|
|
|
6468
|
+
private providerIdempotencyKeyForToolCall(input: {
|
|
6469
|
+
cacheKey: string;
|
|
6470
|
+
force?: boolean;
|
|
6471
|
+
leaseId?: string | null;
|
|
6472
|
+
}): string {
|
|
6473
|
+
const fallbackAttemptId =
|
|
6474
|
+
input.force === true && !input.leaseId
|
|
6475
|
+
? `${this.currentRunId}:${crypto.randomUUID()}`
|
|
6476
|
+
: null;
|
|
6477
|
+
return buildDurableToolProviderIdempotencyKey({
|
|
6478
|
+
receiptKey: input.cacheKey,
|
|
6479
|
+
force: input.force,
|
|
6480
|
+
receiptLeaseId: input.leaseId,
|
|
6481
|
+
fallbackAttemptId,
|
|
6482
|
+
});
|
|
6483
|
+
}
|
|
6484
|
+
|
|
5201
6485
|
private async callToolExecutionAPI(
|
|
5202
6486
|
toolId: string,
|
|
5203
6487
|
input: Record<string, unknown>,
|
|
@@ -5240,6 +6524,20 @@ export class PlayContextImpl {
|
|
|
5240
6524
|
|
|
5241
6525
|
while (true) {
|
|
5242
6526
|
let response: Response;
|
|
6527
|
+
const durableCallReceiptKey =
|
|
6528
|
+
options?.durableCallReceiptKey?.trim() || null;
|
|
6529
|
+
const executionAuthScopeDigest =
|
|
6530
|
+
durableCallReceiptKey && options?.executionAuthScopeDigest
|
|
6531
|
+
? options.executionAuthScopeDigest.trim() || null
|
|
6532
|
+
: durableCallReceiptKey
|
|
6533
|
+
? ((await this.resolveToolAuthScopeDigest(toolId))?.trim() ??
|
|
6534
|
+
null)
|
|
6535
|
+
: null;
|
|
6536
|
+
const providerIdempotencyKey =
|
|
6537
|
+
options?.providerIdempotencyKey?.trim() || durableCallReceiptKey;
|
|
6538
|
+
const deeplineRequestId = providerIdempotencyKey
|
|
6539
|
+
? `ctx-tool-${stableDigest(providerIdempotencyKey).slice(0, 32)}`
|
|
6540
|
+
: null;
|
|
5243
6541
|
const abortController = timeoutMs ? new AbortController() : null;
|
|
5244
6542
|
const timeoutHandle = timeoutMs
|
|
5245
6543
|
? setTimeout(() => {
|
|
@@ -5261,6 +6559,12 @@ export class PlayContextImpl {
|
|
|
5261
6559
|
V2_EXECUTE_RESPONSE_CONTRACT,
|
|
5262
6560
|
[EXECUTE_RESPONSE_INTENT_HEADER]: 'dataset',
|
|
5263
6561
|
[EXECUTE_TOOL_METADATA_HEADER]: 'true',
|
|
6562
|
+
...(deeplineRequestId
|
|
6563
|
+
? {
|
|
6564
|
+
'x-deepline-request-id': deeplineRequestId,
|
|
6565
|
+
'x-deepline-idempotency-key': providerIdempotencyKey!,
|
|
6566
|
+
}
|
|
6567
|
+
: {}),
|
|
5264
6568
|
...(this.#options.vercelProtectionBypassToken?.trim()
|
|
5265
6569
|
? {
|
|
5266
6570
|
'x-vercel-protection-bypass':
|
|
@@ -5272,6 +6576,17 @@ export class PlayContextImpl {
|
|
|
5272
6576
|
payload: input,
|
|
5273
6577
|
metadata: {
|
|
5274
6578
|
parent_run_id: this.#options.runId,
|
|
6579
|
+
...(durableCallReceiptKey
|
|
6580
|
+
? {
|
|
6581
|
+
durable_call_receipt_key: durableCallReceiptKey,
|
|
6582
|
+
...(executionAuthScopeDigest
|
|
6583
|
+
? {
|
|
6584
|
+
execution_auth_scope_digest:
|
|
6585
|
+
executionAuthScopeDigest,
|
|
6586
|
+
}
|
|
6587
|
+
: {}),
|
|
6588
|
+
}
|
|
6589
|
+
: {}),
|
|
5275
6590
|
...(options?.customerDbDataset
|
|
5276
6591
|
? {
|
|
5277
6592
|
query_result_dataset: {
|
|
@@ -5293,6 +6608,9 @@ export class PlayContextImpl {
|
|
|
5293
6608
|
: {}),
|
|
5294
6609
|
}
|
|
5295
6610
|
: {}),
|
|
6611
|
+
...(providerIdempotencyKey
|
|
6612
|
+
? { provider_idempotency_key: providerIdempotencyKey }
|
|
6613
|
+
: {}),
|
|
5296
6614
|
},
|
|
5297
6615
|
...(this.#options.integrationMode
|
|
5298
6616
|
? { integration_mode: this.#options.integrationMode }
|
|
@@ -5337,6 +6655,13 @@ export class PlayContextImpl {
|
|
|
5337
6655
|
|
|
5338
6656
|
if (!response.ok) {
|
|
5339
6657
|
const text = await response.text();
|
|
6658
|
+
const authScopeChanged = parseToolExecuteAuthScopeChangedError({
|
|
6659
|
+
status: response.status,
|
|
6660
|
+
bodyText: text,
|
|
6661
|
+
});
|
|
6662
|
+
if (authScopeChanged) {
|
|
6663
|
+
throw authScopeChanged;
|
|
6664
|
+
}
|
|
5340
6665
|
const httpFailureAttempt = httpFailureAttempts.next({
|
|
5341
6666
|
toolId,
|
|
5342
6667
|
status: response.status,
|