deepline 0.1.33 → 0.1.36
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/cli/index.js +991 -219
- package/dist/cli/index.mjs +924 -152
- package/dist/index.d.mts +124 -62
- package/dist/index.d.ts +124 -62
- package/dist/index.js +522 -45
- package/dist/index.mjs +522 -45
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +212 -1
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +129 -2
- package/dist/repo/apps/play-runner-workers/src/entry.ts +186 -75
- package/dist/repo/apps/play-runner-workers/src/runtime/receipts.ts +138 -0
- package/dist/repo/apps/play-runner-workers/src/workflow-retry.ts +50 -0
- package/dist/repo/sdk/src/client.ts +43 -32
- package/dist/repo/sdk/src/http.ts +44 -4
- package/dist/repo/sdk/src/index.ts +8 -5
- package/dist/repo/sdk/src/play.ts +126 -45
- package/dist/repo/sdk/src/plays/harness-stub.ts +2 -2
- package/dist/repo/sdk/src/tool-output.ts +22 -7
- package/dist/repo/sdk/src/types.ts +45 -11
- package/dist/repo/sdk/src/version.ts +2 -2
- package/dist/repo/shared_libs/play-runtime/run-failure.ts +49 -0
- package/dist/repo/shared_libs/play-runtime/tool-result.ts +154 -35
- package/package.json +1 -1
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
type ToolBatchRequest,
|
|
51
51
|
} from '../../../shared_libs/play-runtime/tool-batch-executor';
|
|
52
52
|
import {
|
|
53
|
+
adaptV2ExecuteResponseToToolResult,
|
|
53
54
|
createToolExecuteResult,
|
|
54
55
|
isToolExecuteResult,
|
|
55
56
|
type ToolExecuteResult,
|
|
@@ -61,6 +62,7 @@ import type { PlayRuntimeManifestMap } from '../../../shared_libs/plays/compiler
|
|
|
61
62
|
import {
|
|
62
63
|
derivePlayRowIdentity,
|
|
63
64
|
derivePlayRowIdentityFromKey,
|
|
65
|
+
deriveToolRequestIdentity,
|
|
64
66
|
} from '../../../shared_libs/plays/row-identity';
|
|
65
67
|
import {
|
|
66
68
|
getTopLevelPipelineSubsteps,
|
|
@@ -94,6 +96,7 @@ import {
|
|
|
94
96
|
type WorkerDatasetHandle,
|
|
95
97
|
type WorkerDatasetInput,
|
|
96
98
|
} from './runtime/dataset-handles';
|
|
99
|
+
import { runWorkerRuntimeReceiptBoundary } from './runtime/receipts';
|
|
97
100
|
// The harness stub forwards leaf calls (validation, runtime-api HTTP) into
|
|
98
101
|
// the long-lived Play Harness Worker via env.HARNESS. We import the
|
|
99
102
|
// `setHarnessBinding` setter eagerly so it's available the moment
|
|
@@ -118,6 +121,7 @@ import {
|
|
|
118
121
|
type CsvRenameOptions,
|
|
119
122
|
} from '../../../shared_libs/play-runtime/csv-rename';
|
|
120
123
|
import { coordinatorRequestHeaders } from '../../../shared_libs/play-runtime/coordinator-headers';
|
|
124
|
+
import { normalizePlayRunFailure } from '../../../shared_libs/play-runtime/run-failure';
|
|
121
125
|
import type {
|
|
122
126
|
LiveNodeProgressMap,
|
|
123
127
|
LiveNodeProgressSnapshot,
|
|
@@ -184,6 +188,100 @@ type WorkerFileRef = {
|
|
|
184
188
|
};
|
|
185
189
|
|
|
186
190
|
const EXECUTE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
|
|
191
|
+
const EXECUTE_RESPONSE_CONTRACT_HEADER = 'x-deepline-execute-response-contract';
|
|
192
|
+
const V2_EXECUTE_RESPONSE_CONTRACT = 'v2-tool-execution-result';
|
|
193
|
+
|
|
194
|
+
class ToolHttpError extends Error {
|
|
195
|
+
readonly billing: Record<string, unknown> | null;
|
|
196
|
+
|
|
197
|
+
constructor(message: string, billing: Record<string, unknown> | null) {
|
|
198
|
+
super(message);
|
|
199
|
+
this.name = 'ToolHttpError';
|
|
200
|
+
this.billing = billing;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatCreditAmount(value: unknown): string {
|
|
205
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
206
|
+
return String(value ?? '-');
|
|
207
|
+
}
|
|
208
|
+
return Number(value.toFixed(8)).toString();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getStringField(value: unknown, key: string): string | null {
|
|
212
|
+
if (!isRecord(value)) return null;
|
|
213
|
+
const field = value[key];
|
|
214
|
+
return typeof field === 'string' && field.trim() ? field : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getObjectField(value: unknown, key: string): Record<string, unknown> | null {
|
|
218
|
+
if (!isRecord(value)) return null;
|
|
219
|
+
const field = value[key];
|
|
220
|
+
return isRecord(field) ? field : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isInsufficientCreditsBilling(
|
|
224
|
+
billing: Record<string, unknown> | null,
|
|
225
|
+
): billing is Record<string, unknown> {
|
|
226
|
+
return billing?.kind === 'insufficient_credits';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function formatInsufficientCreditsMessage(input: {
|
|
230
|
+
billing: Record<string, unknown>;
|
|
231
|
+
toolId: string;
|
|
232
|
+
}): string {
|
|
233
|
+
const operation =
|
|
234
|
+
getStringField(input.billing, 'operation_id') ??
|
|
235
|
+
getStringField(input.billing, 'operation') ??
|
|
236
|
+
input.toolId;
|
|
237
|
+
const balance = formatCreditAmount(input.billing.balance_credits);
|
|
238
|
+
const required = formatCreditAmount(input.billing.required_credits);
|
|
239
|
+
const recommended = formatCreditAmount(
|
|
240
|
+
input.billing.recommended_add_credits ?? input.billing.needed_credits,
|
|
241
|
+
);
|
|
242
|
+
const billingUrl = getStringField(input.billing, 'billing_url');
|
|
243
|
+
const addSuffix =
|
|
244
|
+
billingUrl && recommended !== '-'
|
|
245
|
+
? ` Add >=${recommended} at ${billingUrl}.`
|
|
246
|
+
: billingUrl
|
|
247
|
+
? ` Add credits at ${billingUrl}.`
|
|
248
|
+
: '';
|
|
249
|
+
return `Workspace balance ${balance} < required ${required} for ${operation}.${addSuffix}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function normalizeToolHttpErrorMessage(input: {
|
|
253
|
+
toolId: string;
|
|
254
|
+
status: number;
|
|
255
|
+
attempt: number;
|
|
256
|
+
maxAttempts: number;
|
|
257
|
+
bodyText: string;
|
|
258
|
+
}): ToolHttpError {
|
|
259
|
+
let parsed: Record<string, unknown> | null = null;
|
|
260
|
+
try {
|
|
261
|
+
const candidate = JSON.parse(input.bodyText);
|
|
262
|
+
parsed = isRecord(candidate) ? candidate : null;
|
|
263
|
+
} catch {
|
|
264
|
+
parsed = null;
|
|
265
|
+
}
|
|
266
|
+
const billing = getObjectField(parsed, 'billing');
|
|
267
|
+
if (isInsufficientCreditsBilling(billing)) {
|
|
268
|
+
return new ToolHttpError(
|
|
269
|
+
`tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatInsufficientCreditsMessage({
|
|
270
|
+
billing,
|
|
271
|
+
toolId: input.toolId,
|
|
272
|
+
})}`,
|
|
273
|
+
billing,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return new ToolHttpError(
|
|
277
|
+
`tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${input.bodyText.slice(0, 500)}`,
|
|
278
|
+
billing,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function extractErrorBilling(error: unknown): Record<string, unknown> | null {
|
|
283
|
+
return error instanceof ToolHttpError ? error.billing : null;
|
|
284
|
+
}
|
|
187
285
|
|
|
188
286
|
/** R2 binding injected by the Worker runtime (when present in deploy metadata). */
|
|
189
287
|
type WorkerEnv = {
|
|
@@ -983,40 +1081,25 @@ function isToolExecuteRecord(value: unknown): value is Record<string, unknown> {
|
|
|
983
1081
|
}
|
|
984
1082
|
|
|
985
1083
|
function normalizeToolExecuteArgs(
|
|
986
|
-
|
|
987
|
-
toolId?: unknown,
|
|
988
|
-
input?: unknown,
|
|
1084
|
+
request: unknown,
|
|
989
1085
|
): { id: string; toolId: string; input: Record<string, unknown> } {
|
|
990
|
-
if (isToolExecuteRecord(
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
if (
|
|
995
|
-
typeof id !== 'string' ||
|
|
996
|
-
!id.trim() ||
|
|
997
|
-
typeof tool !== 'string' ||
|
|
998
|
-
!tool ||
|
|
999
|
-
!isToolExecuteRecord(requestInput)
|
|
1000
|
-
) {
|
|
1001
|
-
throw new Error(
|
|
1002
|
-
'ctx.tools.execute({ id, tool, input }) requires a non-empty id, tool string, and input object.',
|
|
1003
|
-
);
|
|
1004
|
-
}
|
|
1005
|
-
return { id: id.trim(), toolId: tool, input: requestInput };
|
|
1086
|
+
if (!isToolExecuteRecord(request)) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
'ctx.tools.execute requires a request object: ctx.tools.execute({ id, tool, input, description }).',
|
|
1089
|
+
);
|
|
1006
1090
|
}
|
|
1007
|
-
|
|
1008
1091
|
if (
|
|
1009
|
-
typeof
|
|
1010
|
-
!
|
|
1011
|
-
typeof
|
|
1012
|
-
!
|
|
1013
|
-
!isToolExecuteRecord(input)
|
|
1092
|
+
typeof request.id !== 'string' ||
|
|
1093
|
+
!request.id.trim() ||
|
|
1094
|
+
typeof request.tool !== 'string' ||
|
|
1095
|
+
!request.tool.trim() ||
|
|
1096
|
+
!isToolExecuteRecord(request.input)
|
|
1014
1097
|
) {
|
|
1015
1098
|
throw new Error(
|
|
1016
|
-
'ctx.tools.execute(
|
|
1099
|
+
'ctx.tools.execute({ id, tool, input }) requires a non-empty id, tool string, and input object.',
|
|
1017
1100
|
);
|
|
1018
1101
|
}
|
|
1019
|
-
return { id:
|
|
1102
|
+
return { id: request.id.trim(), toolId: request.tool, input: request.input };
|
|
1020
1103
|
}
|
|
1021
1104
|
|
|
1022
1105
|
function integrationEventType(eventKey: string): string {
|
|
@@ -1149,13 +1232,14 @@ async function callToolDirect(
|
|
|
1149
1232
|
'content-type': 'application/json',
|
|
1150
1233
|
authorization: `Bearer ${req.executorToken}`,
|
|
1151
1234
|
'x-deepline-request-id': `${req.runId}:${toolId}:${id}:attempt:${attempt}`,
|
|
1235
|
+
[EXECUTE_RESPONSE_CONTRACT_HEADER]: V2_EXECUTE_RESPONSE_CONTRACT,
|
|
1152
1236
|
[EXECUTE_TOOL_METADATA_HEADER]: 'true',
|
|
1153
1237
|
},
|
|
1154
1238
|
body: JSON.stringify({ payload: input }),
|
|
1155
1239
|
});
|
|
1156
1240
|
if (res.ok) {
|
|
1157
1241
|
const body = (await res.json()) as Record<string, unknown>;
|
|
1158
|
-
const result = (body
|
|
1242
|
+
const { result } = adaptV2ExecuteResponseToToolResult(body);
|
|
1159
1243
|
const status =
|
|
1160
1244
|
typeof body.status === 'string'
|
|
1161
1245
|
? body.status
|
|
@@ -1171,9 +1255,13 @@ async function callToolDirect(
|
|
|
1171
1255
|
}
|
|
1172
1256
|
|
|
1173
1257
|
const text = await res.text().catch(() => '');
|
|
1174
|
-
lastError =
|
|
1175
|
-
|
|
1176
|
-
|
|
1258
|
+
lastError = normalizeToolHttpErrorMessage({
|
|
1259
|
+
toolId,
|
|
1260
|
+
status: res.status,
|
|
1261
|
+
attempt,
|
|
1262
|
+
maxAttempts,
|
|
1263
|
+
bodyText: text,
|
|
1264
|
+
});
|
|
1177
1265
|
const retryable =
|
|
1178
1266
|
res.status === 429 ||
|
|
1179
1267
|
(res.status >= 500 && WORKER_RETRY_SAFE_5XX_TOOLS.has(toolId));
|
|
@@ -1692,9 +1780,7 @@ async function executeBatchedWorkerToolGroup(input: {
|
|
|
1692
1780
|
) => {
|
|
1693
1781
|
for (const entry of chunkResults) {
|
|
1694
1782
|
const batchResult = isToolExecuteResult(entry.result)
|
|
1695
|
-
?
|
|
1696
|
-
? entry.result.result.data
|
|
1697
|
-
: undefined
|
|
1783
|
+
? entry.result.toolOutput.raw
|
|
1698
1784
|
: entry.result;
|
|
1699
1785
|
const splitResults =
|
|
1700
1786
|
batchResult != null
|
|
@@ -1956,14 +2042,10 @@ async function executeWorkerWaterfall(
|
|
|
1956
2042
|
if (isWorkerInlineCodeStep(step)) {
|
|
1957
2043
|
result = await step.run(input, {
|
|
1958
2044
|
tools: {
|
|
1959
|
-
execute: async (
|
|
1960
|
-
requestOrKey: unknown,
|
|
1961
|
-
toolId?: unknown,
|
|
1962
|
-
toolInput?: unknown,
|
|
1963
|
-
) =>
|
|
2045
|
+
execute: async (request: unknown) =>
|
|
1964
2046
|
await executeToolWithLifecycle(
|
|
1965
2047
|
req,
|
|
1966
|
-
normalizeToolExecuteArgs(
|
|
2048
|
+
normalizeToolExecuteArgs(request),
|
|
1967
2049
|
workflowStep,
|
|
1968
2050
|
callbacks,
|
|
1969
2051
|
),
|
|
@@ -2731,6 +2813,7 @@ async function persistCompletedMapRows(input: {
|
|
|
2731
2813
|
await harnessPersistCompletedSheetRows({
|
|
2732
2814
|
baseUrl: input.req.baseUrl,
|
|
2733
2815
|
executorToken: input.req.executorToken,
|
|
2816
|
+
preloadedDbSessions: input.req.preloadedDbSessions ?? null,
|
|
2734
2817
|
playName: input.req.playName,
|
|
2735
2818
|
tableNamespace: input.tableNamespace,
|
|
2736
2819
|
sheetContract: augmentSheetContractWithDatasetFields({
|
|
@@ -2742,7 +2825,6 @@ async function persistCompletedMapRows(input: {
|
|
|
2742
2825
|
outputFields,
|
|
2743
2826
|
runId: input.req.runId,
|
|
2744
2827
|
userEmail: input.req.userEmail,
|
|
2745
|
-
preloadedDbSessions: input.req.preloadedDbSessions ?? null,
|
|
2746
2828
|
});
|
|
2747
2829
|
}
|
|
2748
2830
|
|
|
@@ -2762,6 +2844,7 @@ async function prepareMapRows(input: {
|
|
|
2762
2844
|
const result = await harnessStartSheetDataset({
|
|
2763
2845
|
baseUrl: input.req.baseUrl,
|
|
2764
2846
|
executorToken: input.req.executorToken,
|
|
2847
|
+
preloadedDbSessions: input.req.preloadedDbSessions ?? null,
|
|
2765
2848
|
playName: input.req.playName,
|
|
2766
2849
|
tableNamespace: input.tableNamespace,
|
|
2767
2850
|
sheetContract: augmentSheetContractWithDatasetFields({
|
|
@@ -2771,7 +2854,6 @@ async function prepareMapRows(input: {
|
|
|
2771
2854
|
rows: input.rows.map((row) => ({ ...row })),
|
|
2772
2855
|
runId: input.req.runId,
|
|
2773
2856
|
userEmail: input.req.userEmail,
|
|
2774
|
-
preloadedDbSessions: input.req.preloadedDbSessions ?? null,
|
|
2775
2857
|
});
|
|
2776
2858
|
for (const timing of result.timings ?? []) {
|
|
2777
2859
|
const phase =
|
|
@@ -2933,6 +3015,19 @@ function createMinimalWorkerCtx(
|
|
|
2933
3015
|
};
|
|
2934
3016
|
const rootGovernance = req.playCallGovernance;
|
|
2935
3017
|
const rootRunId = rootGovernance?.rootRunId ?? req.runId;
|
|
3018
|
+
const executeWithRuntimeReceipt = async <T>(
|
|
3019
|
+
key: string,
|
|
3020
|
+
execute: () => Promise<T> | T,
|
|
3021
|
+
): Promise<T> =>
|
|
3022
|
+
runWorkerRuntimeReceiptBoundary({
|
|
3023
|
+
baseUrl: req.baseUrl,
|
|
3024
|
+
executorToken: req.executorToken,
|
|
3025
|
+
playName: req.playName,
|
|
3026
|
+
runId: req.runId,
|
|
3027
|
+
key,
|
|
3028
|
+
postRuntimeApi,
|
|
3029
|
+
execute,
|
|
3030
|
+
});
|
|
2936
3031
|
// Local ancestry chain that always ENDS with the currently-executing play
|
|
2937
3032
|
// (req.playName). The /api/v2/plays/run lineage validator requires the
|
|
2938
3033
|
// submitted ancestry's tail to equal the executor token's play name (i.e.
|
|
@@ -3224,19 +3319,9 @@ function createMinimalWorkerCtx(
|
|
|
3224
3319
|
},
|
|
3225
3320
|
tools: {
|
|
3226
3321
|
...((ctx as { tools?: Record<string, unknown> }).tools ?? {}),
|
|
3227
|
-
execute: async (
|
|
3228
|
-
requestOrKey: unknown,
|
|
3229
|
-
toolId?: unknown,
|
|
3230
|
-
input?: unknown,
|
|
3231
|
-
_opts?: { description?: string },
|
|
3232
|
-
): Promise<unknown> => {
|
|
3233
|
-
void _opts;
|
|
3322
|
+
execute: async (requestArg: unknown): Promise<unknown> => {
|
|
3234
3323
|
assertNotAborted(abortSignal);
|
|
3235
|
-
const request = normalizeToolExecuteArgs(
|
|
3236
|
-
requestOrKey,
|
|
3237
|
-
toolId,
|
|
3238
|
-
input,
|
|
3239
|
-
);
|
|
3324
|
+
const request = normalizeToolExecuteArgs(requestArg);
|
|
3240
3325
|
return await toolBatchScheduler.execute(
|
|
3241
3326
|
request.id,
|
|
3242
3327
|
request.toolId,
|
|
@@ -3673,13 +3758,14 @@ function createMinimalWorkerCtx(
|
|
|
3673
3758
|
},
|
|
3674
3759
|
async step<T>(name: string, callback: () => Promise<T> | T): Promise<T> {
|
|
3675
3760
|
assertNotAborted(abortSignal);
|
|
3676
|
-
|
|
3761
|
+
const normalizedName = name.trim();
|
|
3762
|
+
if (!normalizedName) {
|
|
3677
3763
|
throw new Error('ctx.step(name, callback) requires a name.');
|
|
3678
3764
|
}
|
|
3679
3765
|
// Static pipeline JS blocks are already Workflow steps in the Workers
|
|
3680
3766
|
// backend. Nesting another `step.do` here can leave preview runs parked
|
|
3681
3767
|
// inside the JS stage before they reach subsequent event waits.
|
|
3682
|
-
return await callback
|
|
3768
|
+
return await executeWithRuntimeReceipt(`step:${normalizedName}`, callback);
|
|
3683
3769
|
},
|
|
3684
3770
|
async runSteps<T>(
|
|
3685
3771
|
program: WorkerStepProgram,
|
|
@@ -3839,27 +3925,33 @@ function createMinimalWorkerCtx(
|
|
|
3839
3925
|
input: Record<string, unknown>,
|
|
3840
3926
|
): Promise<unknown> => {
|
|
3841
3927
|
assertNotAborted(abortSignal);
|
|
3842
|
-
return
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3928
|
+
return await executeWithRuntimeReceipt(
|
|
3929
|
+
deriveToolRequestIdentity({ toolId, requestInput: input }),
|
|
3930
|
+
() =>
|
|
3931
|
+
executeToolWithLifecycle(
|
|
3932
|
+
req,
|
|
3933
|
+
{ id: key, toolId, input },
|
|
3934
|
+
workflowStep,
|
|
3935
|
+
callbacks,
|
|
3936
|
+
),
|
|
3847
3937
|
);
|
|
3848
3938
|
},
|
|
3849
3939
|
tools: {
|
|
3850
|
-
async execute(
|
|
3851
|
-
requestOrKey: unknown,
|
|
3852
|
-
toolId?: unknown,
|
|
3853
|
-
input?: unknown,
|
|
3854
|
-
_opts?: { description?: string },
|
|
3855
|
-
): Promise<unknown> {
|
|
3856
|
-
void _opts;
|
|
3940
|
+
async execute(requestArg: unknown): Promise<unknown> {
|
|
3857
3941
|
assertNotAborted(abortSignal);
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3942
|
+
const request = normalizeToolExecuteArgs(requestArg);
|
|
3943
|
+
return await executeWithRuntimeReceipt(
|
|
3944
|
+
deriveToolRequestIdentity({
|
|
3945
|
+
toolId: request.toolId,
|
|
3946
|
+
requestInput: request.input,
|
|
3947
|
+
}),
|
|
3948
|
+
() =>
|
|
3949
|
+
executeToolWithLifecycle(
|
|
3950
|
+
req,
|
|
3951
|
+
request,
|
|
3952
|
+
workflowStep,
|
|
3953
|
+
callbacks,
|
|
3954
|
+
),
|
|
3863
3955
|
);
|
|
3864
3956
|
},
|
|
3865
3957
|
},
|
|
@@ -4646,7 +4738,9 @@ async function executeRunRequest(
|
|
|
4646
4738
|
error instanceof Error ? error.message : 'Play run cancelled.',
|
|
4647
4739
|
);
|
|
4648
4740
|
}
|
|
4649
|
-
const
|
|
4741
|
+
const failure = normalizePlayRunFailure(error);
|
|
4742
|
+
const message = failure.message;
|
|
4743
|
+
const errorBilling = extractErrorBilling(error);
|
|
4650
4744
|
if (options?.persistResultDatasets) {
|
|
4651
4745
|
appendRunLogLine(
|
|
4652
4746
|
`${aborted ? '[cancelled]' : '[error]'} ${redactSecretsFromLogString(message)}`,
|
|
@@ -4657,6 +4751,23 @@ async function executeRunRequest(
|
|
|
4657
4751
|
source: 'worker',
|
|
4658
4752
|
occurredAt: nowMs(),
|
|
4659
4753
|
error: message,
|
|
4754
|
+
result: aborted
|
|
4755
|
+
? undefined
|
|
4756
|
+
: {
|
|
4757
|
+
success: false,
|
|
4758
|
+
status: 'failed',
|
|
4759
|
+
error: message,
|
|
4760
|
+
errors: [
|
|
4761
|
+
{
|
|
4762
|
+
code: failure.code,
|
|
4763
|
+
phase: failure.phase,
|
|
4764
|
+
message,
|
|
4765
|
+
retryable: failure.retryable,
|
|
4766
|
+
...(errorBilling ? { billing: errorBilling } : {}),
|
|
4767
|
+
...(failure.cause ? { cause: failure.cause } : {}),
|
|
4768
|
+
},
|
|
4769
|
+
],
|
|
4770
|
+
},
|
|
4660
4771
|
});
|
|
4661
4772
|
await finalizeWorkerComputeBilling({
|
|
4662
4773
|
req,
|
|
@@ -4890,6 +5001,7 @@ async function persistResultDatasets(
|
|
|
4890
5001
|
await harnessStartSheetDataset({
|
|
4891
5002
|
baseUrl: req.baseUrl,
|
|
4892
5003
|
executorToken: req.executorToken,
|
|
5004
|
+
preloadedDbSessions: req.preloadedDbSessions ?? null,
|
|
4893
5005
|
playName: req.playName,
|
|
4894
5006
|
tableNamespace: dataset.tableNamespace,
|
|
4895
5007
|
sheetContract: requireSheetContract(req, dataset.tableNamespace),
|
|
@@ -4897,7 +5009,6 @@ async function persistResultDatasets(
|
|
|
4897
5009
|
runId: req.runId,
|
|
4898
5010
|
inputOffset: 0,
|
|
4899
5011
|
userEmail: req.userEmail,
|
|
4900
|
-
preloadedDbSessions: req.preloadedDbSessions ?? null,
|
|
4901
5012
|
});
|
|
4902
5013
|
}
|
|
4903
5014
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export type RuntimeReceiptStatus =
|
|
2
|
+
| 'pending'
|
|
3
|
+
| 'running'
|
|
4
|
+
| 'completed'
|
|
5
|
+
| 'failed'
|
|
6
|
+
| 'skipped';
|
|
7
|
+
|
|
8
|
+
export type WorkerRuntimeReceipt = {
|
|
9
|
+
key: string;
|
|
10
|
+
status: RuntimeReceiptStatus;
|
|
11
|
+
output?: unknown;
|
|
12
|
+
error?: string;
|
|
13
|
+
runId?: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type WorkerRuntimeReceiptResponse = {
|
|
17
|
+
receipt?: WorkerRuntimeReceipt | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type WorkerRuntimeReceiptAction =
|
|
21
|
+
| {
|
|
22
|
+
action: 'get_runtime_step_receipt';
|
|
23
|
+
playName: string;
|
|
24
|
+
runId: string;
|
|
25
|
+
key: string;
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
action: 'claim_runtime_step_receipt';
|
|
29
|
+
playName: string;
|
|
30
|
+
runId: string;
|
|
31
|
+
key: string;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
action: 'complete_runtime_step_receipt';
|
|
35
|
+
playName: string;
|
|
36
|
+
runId: string;
|
|
37
|
+
key: string;
|
|
38
|
+
output: unknown;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
action: 'fail_runtime_step_receipt';
|
|
42
|
+
playName: string;
|
|
43
|
+
runId: string;
|
|
44
|
+
key: string;
|
|
45
|
+
error: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type RuntimeReceiptContext = {
|
|
49
|
+
baseUrl: string;
|
|
50
|
+
executorToken: string;
|
|
51
|
+
playName: string;
|
|
52
|
+
runId: string;
|
|
53
|
+
key: string;
|
|
54
|
+
postRuntimeApi: (
|
|
55
|
+
baseUrl: string,
|
|
56
|
+
executorToken: string,
|
|
57
|
+
body: WorkerRuntimeReceiptAction,
|
|
58
|
+
) => Promise<WorkerRuntimeReceiptResponse>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function scopedReceiptKey(input: {
|
|
62
|
+
playName: string;
|
|
63
|
+
runId: string;
|
|
64
|
+
key: string;
|
|
65
|
+
}): string {
|
|
66
|
+
return `run:${input.runId}:play:${input.playName}:${input.key}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function receiptOutput<T>(receipt: WorkerRuntimeReceipt): T {
|
|
70
|
+
return receipt.output as T;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function errorMessage(error: unknown): string {
|
|
74
|
+
return error instanceof Error ? error.message : String(error);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function runWorkerRuntimeReceiptBoundary<T>(
|
|
78
|
+
input: RuntimeReceiptContext & {
|
|
79
|
+
execute: () => Promise<T> | T;
|
|
80
|
+
},
|
|
81
|
+
): Promise<T> {
|
|
82
|
+
const key = scopedReceiptKey(input);
|
|
83
|
+
const existing = await input.postRuntimeApi(input.baseUrl, input.executorToken, {
|
|
84
|
+
action: 'get_runtime_step_receipt',
|
|
85
|
+
playName: input.playName,
|
|
86
|
+
runId: input.runId,
|
|
87
|
+
key,
|
|
88
|
+
});
|
|
89
|
+
if (
|
|
90
|
+
existing.receipt?.status === 'completed' ||
|
|
91
|
+
existing.receipt?.status === 'skipped'
|
|
92
|
+
) {
|
|
93
|
+
return receiptOutput<T>(existing.receipt);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const claimed = await input.postRuntimeApi(input.baseUrl, input.executorToken, {
|
|
97
|
+
action: 'claim_runtime_step_receipt',
|
|
98
|
+
playName: input.playName,
|
|
99
|
+
runId: input.runId,
|
|
100
|
+
key,
|
|
101
|
+
});
|
|
102
|
+
if (!claimed.receipt) {
|
|
103
|
+
if (
|
|
104
|
+
existing.receipt?.status === 'running' &&
|
|
105
|
+
existing.receipt.runId !== input.runId
|
|
106
|
+
) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Runtime receipt ${key} is already running for run ${existing.receipt.runId ?? 'unknown'}.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (existing.receipt?.status === 'failed') {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Runtime receipt ${key} is failed and could not be claimed: ${existing.receipt.error ?? 'unknown error'}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const output = await input.execute();
|
|
120
|
+
await input.postRuntimeApi(input.baseUrl, input.executorToken, {
|
|
121
|
+
action: 'complete_runtime_step_receipt',
|
|
122
|
+
playName: input.playName,
|
|
123
|
+
runId: input.runId,
|
|
124
|
+
key,
|
|
125
|
+
output,
|
|
126
|
+
});
|
|
127
|
+
return output;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
await input.postRuntimeApi(input.baseUrl, input.executorToken, {
|
|
130
|
+
action: 'fail_runtime_step_receipt',
|
|
131
|
+
playName: input.playName,
|
|
132
|
+
runId: input.runId,
|
|
133
|
+
key,
|
|
134
|
+
error: errorMessage(error),
|
|
135
|
+
});
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isCloudflareDurableObjectCodeUpdatedError,
|
|
3
|
+
normalizePlayRunFailure,
|
|
4
|
+
} from '../../../shared_libs/play-runtime/run-failure';
|
|
5
|
+
|
|
6
|
+
export const PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT = 1;
|
|
7
|
+
|
|
8
|
+
export type WorkflowRetryDecision =
|
|
9
|
+
| {
|
|
10
|
+
action: 'retry';
|
|
11
|
+
reason: string;
|
|
12
|
+
nextAttempt: number;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
action: 'fail';
|
|
17
|
+
reason: string | null;
|
|
18
|
+
message: string | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function decideWorkflowPlatformRetry(input: {
|
|
22
|
+
workflowStatus: string;
|
|
23
|
+
error: string | null;
|
|
24
|
+
retryAttempts: number;
|
|
25
|
+
}): WorkflowRetryDecision {
|
|
26
|
+
if (!input.error) {
|
|
27
|
+
return { action: 'fail', reason: null, message: null };
|
|
28
|
+
}
|
|
29
|
+
const retryablePlatformReset = isCloudflareDurableObjectCodeUpdatedError(
|
|
30
|
+
input.error,
|
|
31
|
+
);
|
|
32
|
+
if (
|
|
33
|
+
retryablePlatformReset &&
|
|
34
|
+
input.retryAttempts < PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT
|
|
35
|
+
) {
|
|
36
|
+
return {
|
|
37
|
+
action: 'retry',
|
|
38
|
+
reason: 'cloudflare_durable_object_code_updated',
|
|
39
|
+
nextAttempt: input.retryAttempts + 1,
|
|
40
|
+
message: normalizePlayRunFailure(input.error).message,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
action: 'fail',
|
|
45
|
+
reason: retryablePlatformReset
|
|
46
|
+
? 'cloudflare_durable_object_code_updated_retry_exhausted'
|
|
47
|
+
: null,
|
|
48
|
+
message: normalizePlayRunFailure(input.error).message,
|
|
49
|
+
};
|
|
50
|
+
}
|