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.
@@ -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
- requestOrKey: unknown,
987
- toolId?: unknown,
988
- input?: unknown,
1084
+ request: unknown,
989
1085
  ): { id: string; toolId: string; input: Record<string, unknown> } {
990
- if (isToolExecuteRecord(requestOrKey)) {
991
- const id = requestOrKey.id;
992
- const tool = requestOrKey.tool;
993
- const requestInput = requestOrKey.input;
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 requestOrKey !== 'string' ||
1010
- !requestOrKey.trim() ||
1011
- typeof toolId !== 'string' ||
1012
- !toolId ||
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(key, toolId, input) requires a tool ID and input object.',
1099
+ 'ctx.tools.execute({ id, tool, input }) requires a non-empty id, tool string, and input object.',
1017
1100
  );
1018
1101
  }
1019
- return { id: requestOrKey.trim(), toolId, input };
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.result ?? body) as unknown;
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 = new Error(
1175
- `tool ${toolId} ${res.status} attempt ${attempt}/${maxAttempts}: ${text.slice(0, 500)}`,
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
- ? isRecordLike(entry.result.result)
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(requestOrKey, toolId, toolInput),
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
- if (!name.trim()) {
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 executeToolWithLifecycle(
3843
- req,
3844
- { id: key, toolId, input },
3845
- workflowStep,
3846
- callbacks,
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
- return executeToolWithLifecycle(
3859
- req,
3860
- normalizeToolExecuteArgs(requestOrKey, toolId, input),
3861
- workflowStep,
3862
- callbacks,
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 message = error instanceof Error ? error.message : String(error);
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
+ }