deepline 0.1.32 → 0.1.35

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,
@@ -118,6 +119,7 @@ import {
118
119
  type CsvRenameOptions,
119
120
  } from '../../../shared_libs/play-runtime/csv-rename';
120
121
  import { coordinatorRequestHeaders } from '../../../shared_libs/play-runtime/coordinator-headers';
122
+ import { normalizePlayRunFailure } from '../../../shared_libs/play-runtime/run-failure';
121
123
  import type {
122
124
  LiveNodeProgressMap,
123
125
  LiveNodeProgressSnapshot,
@@ -184,6 +186,100 @@ type WorkerFileRef = {
184
186
  };
185
187
 
186
188
  const EXECUTE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
189
+ const EXECUTE_RESPONSE_CONTRACT_HEADER = 'x-deepline-execute-response-contract';
190
+ const V2_EXECUTE_RESPONSE_CONTRACT = 'v2-tool-execution-result';
191
+
192
+ class ToolHttpError extends Error {
193
+ readonly billing: Record<string, unknown> | null;
194
+
195
+ constructor(message: string, billing: Record<string, unknown> | null) {
196
+ super(message);
197
+ this.name = 'ToolHttpError';
198
+ this.billing = billing;
199
+ }
200
+ }
201
+
202
+ function formatCreditAmount(value: unknown): string {
203
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
204
+ return String(value ?? '-');
205
+ }
206
+ return Number(value.toFixed(8)).toString();
207
+ }
208
+
209
+ function getStringField(value: unknown, key: string): string | null {
210
+ if (!isRecord(value)) return null;
211
+ const field = value[key];
212
+ return typeof field === 'string' && field.trim() ? field : null;
213
+ }
214
+
215
+ function getObjectField(value: unknown, key: string): Record<string, unknown> | null {
216
+ if (!isRecord(value)) return null;
217
+ const field = value[key];
218
+ return isRecord(field) ? field : null;
219
+ }
220
+
221
+ function isInsufficientCreditsBilling(
222
+ billing: Record<string, unknown> | null,
223
+ ): billing is Record<string, unknown> {
224
+ return billing?.kind === 'insufficient_credits';
225
+ }
226
+
227
+ function formatInsufficientCreditsMessage(input: {
228
+ billing: Record<string, unknown>;
229
+ toolId: string;
230
+ }): string {
231
+ const operation =
232
+ getStringField(input.billing, 'operation_id') ??
233
+ getStringField(input.billing, 'operation') ??
234
+ input.toolId;
235
+ const balance = formatCreditAmount(input.billing.balance_credits);
236
+ const required = formatCreditAmount(input.billing.required_credits);
237
+ const recommended = formatCreditAmount(
238
+ input.billing.recommended_add_credits ?? input.billing.needed_credits,
239
+ );
240
+ const billingUrl = getStringField(input.billing, 'billing_url');
241
+ const addSuffix =
242
+ billingUrl && recommended !== '-'
243
+ ? ` Add >=${recommended} at ${billingUrl}.`
244
+ : billingUrl
245
+ ? ` Add credits at ${billingUrl}.`
246
+ : '';
247
+ return `Workspace balance ${balance} < required ${required} for ${operation}.${addSuffix}`;
248
+ }
249
+
250
+ function normalizeToolHttpErrorMessage(input: {
251
+ toolId: string;
252
+ status: number;
253
+ attempt: number;
254
+ maxAttempts: number;
255
+ bodyText: string;
256
+ }): ToolHttpError {
257
+ let parsed: Record<string, unknown> | null = null;
258
+ try {
259
+ const candidate = JSON.parse(input.bodyText);
260
+ parsed = isRecord(candidate) ? candidate : null;
261
+ } catch {
262
+ parsed = null;
263
+ }
264
+ const billing = getObjectField(parsed, 'billing');
265
+ if (isInsufficientCreditsBilling(billing)) {
266
+ return new ToolHttpError(
267
+ `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${formatInsufficientCreditsMessage({
268
+ billing,
269
+ toolId: input.toolId,
270
+ })}`,
271
+ billing,
272
+ );
273
+ }
274
+ return new ToolHttpError(
275
+ `tool ${input.toolId} ${input.status} attempt ${input.attempt}/${input.maxAttempts}: ${input.bodyText.slice(0, 500)}`,
276
+ billing,
277
+ );
278
+ }
279
+
280
+ function extractErrorBilling(error: unknown): Record<string, unknown> | null {
281
+ return error instanceof ToolHttpError ? error.billing : null;
282
+ }
187
283
 
188
284
  /** R2 binding injected by the Worker runtime (when present in deploy metadata). */
189
285
  type WorkerEnv = {
@@ -983,40 +1079,25 @@ function isToolExecuteRecord(value: unknown): value is Record<string, unknown> {
983
1079
  }
984
1080
 
985
1081
  function normalizeToolExecuteArgs(
986
- requestOrKey: unknown,
987
- toolId?: unknown,
988
- input?: unknown,
1082
+ request: unknown,
989
1083
  ): { 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 };
1084
+ if (!isToolExecuteRecord(request)) {
1085
+ throw new Error(
1086
+ 'ctx.tools.execute requires a request object: ctx.tools.execute({ id, tool, input, description }).',
1087
+ );
1006
1088
  }
1007
-
1008
1089
  if (
1009
- typeof requestOrKey !== 'string' ||
1010
- !requestOrKey.trim() ||
1011
- typeof toolId !== 'string' ||
1012
- !toolId ||
1013
- !isToolExecuteRecord(input)
1090
+ typeof request.id !== 'string' ||
1091
+ !request.id.trim() ||
1092
+ typeof request.tool !== 'string' ||
1093
+ !request.tool.trim() ||
1094
+ !isToolExecuteRecord(request.input)
1014
1095
  ) {
1015
1096
  throw new Error(
1016
- 'ctx.tools.execute(key, toolId, input) requires a tool ID and input object.',
1097
+ 'ctx.tools.execute({ id, tool, input }) requires a non-empty id, tool string, and input object.',
1017
1098
  );
1018
1099
  }
1019
- return { id: requestOrKey.trim(), toolId, input };
1100
+ return { id: request.id.trim(), toolId: request.tool, input: request.input };
1020
1101
  }
1021
1102
 
1022
1103
  function integrationEventType(eventKey: string): string {
@@ -1149,13 +1230,14 @@ async function callToolDirect(
1149
1230
  'content-type': 'application/json',
1150
1231
  authorization: `Bearer ${req.executorToken}`,
1151
1232
  'x-deepline-request-id': `${req.runId}:${toolId}:${id}:attempt:${attempt}`,
1233
+ [EXECUTE_RESPONSE_CONTRACT_HEADER]: V2_EXECUTE_RESPONSE_CONTRACT,
1152
1234
  [EXECUTE_TOOL_METADATA_HEADER]: 'true',
1153
1235
  },
1154
1236
  body: JSON.stringify({ payload: input }),
1155
1237
  });
1156
1238
  if (res.ok) {
1157
1239
  const body = (await res.json()) as Record<string, unknown>;
1158
- const result = (body.result ?? body) as unknown;
1240
+ const { result } = adaptV2ExecuteResponseToToolResult(body);
1159
1241
  const status =
1160
1242
  typeof body.status === 'string'
1161
1243
  ? body.status
@@ -1171,9 +1253,13 @@ async function callToolDirect(
1171
1253
  }
1172
1254
 
1173
1255
  const text = await res.text().catch(() => '');
1174
- lastError = new Error(
1175
- `tool ${toolId} ${res.status} attempt ${attempt}/${maxAttempts}: ${text.slice(0, 500)}`,
1176
- );
1256
+ lastError = normalizeToolHttpErrorMessage({
1257
+ toolId,
1258
+ status: res.status,
1259
+ attempt,
1260
+ maxAttempts,
1261
+ bodyText: text,
1262
+ });
1177
1263
  const retryable =
1178
1264
  res.status === 429 ||
1179
1265
  (res.status >= 500 && WORKER_RETRY_SAFE_5XX_TOOLS.has(toolId));
@@ -1692,9 +1778,7 @@ async function executeBatchedWorkerToolGroup(input: {
1692
1778
  ) => {
1693
1779
  for (const entry of chunkResults) {
1694
1780
  const batchResult = isToolExecuteResult(entry.result)
1695
- ? isRecordLike(entry.result.result)
1696
- ? entry.result.result.data
1697
- : undefined
1781
+ ? entry.result.toolOutput.raw
1698
1782
  : entry.result;
1699
1783
  const splitResults =
1700
1784
  batchResult != null
@@ -1956,14 +2040,10 @@ async function executeWorkerWaterfall(
1956
2040
  if (isWorkerInlineCodeStep(step)) {
1957
2041
  result = await step.run(input, {
1958
2042
  tools: {
1959
- execute: async (
1960
- requestOrKey: unknown,
1961
- toolId?: unknown,
1962
- toolInput?: unknown,
1963
- ) =>
2043
+ execute: async (request: unknown) =>
1964
2044
  await executeToolWithLifecycle(
1965
2045
  req,
1966
- normalizeToolExecuteArgs(requestOrKey, toolId, toolInput),
2046
+ normalizeToolExecuteArgs(request),
1967
2047
  workflowStep,
1968
2048
  callbacks,
1969
2049
  ),
@@ -2731,6 +2811,7 @@ async function persistCompletedMapRows(input: {
2731
2811
  await harnessPersistCompletedSheetRows({
2732
2812
  baseUrl: input.req.baseUrl,
2733
2813
  executorToken: input.req.executorToken,
2814
+ preloadedDbSessions: input.req.preloadedDbSessions ?? null,
2734
2815
  playName: input.req.playName,
2735
2816
  tableNamespace: input.tableNamespace,
2736
2817
  sheetContract: augmentSheetContractWithDatasetFields({
@@ -2742,7 +2823,6 @@ async function persistCompletedMapRows(input: {
2742
2823
  outputFields,
2743
2824
  runId: input.req.runId,
2744
2825
  userEmail: input.req.userEmail,
2745
- preloadedDbSessions: input.req.preloadedDbSessions ?? null,
2746
2826
  });
2747
2827
  }
2748
2828
 
@@ -2762,6 +2842,7 @@ async function prepareMapRows(input: {
2762
2842
  const result = await harnessStartSheetDataset({
2763
2843
  baseUrl: input.req.baseUrl,
2764
2844
  executorToken: input.req.executorToken,
2845
+ preloadedDbSessions: input.req.preloadedDbSessions ?? null,
2765
2846
  playName: input.req.playName,
2766
2847
  tableNamespace: input.tableNamespace,
2767
2848
  sheetContract: augmentSheetContractWithDatasetFields({
@@ -2771,7 +2852,6 @@ async function prepareMapRows(input: {
2771
2852
  rows: input.rows.map((row) => ({ ...row })),
2772
2853
  runId: input.req.runId,
2773
2854
  userEmail: input.req.userEmail,
2774
- preloadedDbSessions: input.req.preloadedDbSessions ?? null,
2775
2855
  });
2776
2856
  for (const timing of result.timings ?? []) {
2777
2857
  const phase =
@@ -3224,19 +3304,9 @@ function createMinimalWorkerCtx(
3224
3304
  },
3225
3305
  tools: {
3226
3306
  ...((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;
3307
+ execute: async (requestArg: unknown): Promise<unknown> => {
3234
3308
  assertNotAborted(abortSignal);
3235
- const request = normalizeToolExecuteArgs(
3236
- requestOrKey,
3237
- toolId,
3238
- input,
3239
- );
3309
+ const request = normalizeToolExecuteArgs(requestArg);
3240
3310
  return await toolBatchScheduler.execute(
3241
3311
  request.id,
3242
3312
  request.toolId,
@@ -3847,17 +3917,11 @@ function createMinimalWorkerCtx(
3847
3917
  );
3848
3918
  },
3849
3919
  tools: {
3850
- async execute(
3851
- requestOrKey: unknown,
3852
- toolId?: unknown,
3853
- input?: unknown,
3854
- _opts?: { description?: string },
3855
- ): Promise<unknown> {
3856
- void _opts;
3920
+ async execute(requestArg: unknown): Promise<unknown> {
3857
3921
  assertNotAborted(abortSignal);
3858
3922
  return executeToolWithLifecycle(
3859
3923
  req,
3860
- normalizeToolExecuteArgs(requestOrKey, toolId, input),
3924
+ normalizeToolExecuteArgs(requestArg),
3861
3925
  workflowStep,
3862
3926
  callbacks,
3863
3927
  );
@@ -4646,7 +4710,9 @@ async function executeRunRequest(
4646
4710
  error instanceof Error ? error.message : 'Play run cancelled.',
4647
4711
  );
4648
4712
  }
4649
- const message = error instanceof Error ? error.message : String(error);
4713
+ const failure = normalizePlayRunFailure(error);
4714
+ const message = failure.message;
4715
+ const errorBilling = extractErrorBilling(error);
4650
4716
  if (options?.persistResultDatasets) {
4651
4717
  appendRunLogLine(
4652
4718
  `${aborted ? '[cancelled]' : '[error]'} ${redactSecretsFromLogString(message)}`,
@@ -4657,6 +4723,23 @@ async function executeRunRequest(
4657
4723
  source: 'worker',
4658
4724
  occurredAt: nowMs(),
4659
4725
  error: message,
4726
+ result: aborted
4727
+ ? undefined
4728
+ : {
4729
+ success: false,
4730
+ status: 'failed',
4731
+ error: message,
4732
+ errors: [
4733
+ {
4734
+ code: failure.code,
4735
+ phase: failure.phase,
4736
+ message,
4737
+ retryable: failure.retryable,
4738
+ ...(errorBilling ? { billing: errorBilling } : {}),
4739
+ ...(failure.cause ? { cause: failure.cause } : {}),
4740
+ },
4741
+ ],
4742
+ },
4660
4743
  });
4661
4744
  await finalizeWorkerComputeBilling({
4662
4745
  req,
@@ -4890,6 +4973,7 @@ async function persistResultDatasets(
4890
4973
  await harnessStartSheetDataset({
4891
4974
  baseUrl: req.baseUrl,
4892
4975
  executorToken: req.executorToken,
4976
+ preloadedDbSessions: req.preloadedDbSessions ?? null,
4893
4977
  playName: req.playName,
4894
4978
  tableNamespace: dataset.tableNamespace,
4895
4979
  sheetContract: requireSheetContract(req, dataset.tableNamespace),
@@ -4897,7 +4981,6 @@ async function persistResultDatasets(
4897
4981
  runId: req.runId,
4898
4982
  inputOffset: 0,
4899
4983
  userEmail: req.userEmail,
4900
- preloadedDbSessions: req.preloadedDbSessions ?? null,
4901
4984
  });
4902
4985
  }
4903
4986
  }
@@ -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
+ }
@@ -67,6 +67,8 @@ import type { PlayCompilerManifest } from '../../shared_libs/plays/compiler-mani
67
67
 
68
68
  const TERMINAL_PLAY_STATUSES = new Set(['completed', 'failed', 'cancelled']);
69
69
  const INCLUDE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
70
+ const EXECUTE_RESPONSE_CONTRACT_HEADER = 'x-deepline-execute-response-contract';
71
+ const V2_EXECUTE_RESPONSE_CONTRACT = 'v2-tool-execution-result';
70
72
  const COMPILE_MANIFEST_RETRY_DELAYS_MS = [250, 1_000];
71
73
 
72
74
  function sleep(ms: number): Promise<void> {
@@ -95,9 +97,14 @@ type ExecuteToolRawOptions = {
95
97
  export type ToolExecution<TData = unknown, TMeta = Record<string, unknown>> = {
96
98
  status: string;
97
99
  job_id?: string;
98
- result: {
99
- data: TData;
100
- meta?: TMeta;
100
+ toolExecutionResult: {
101
+ toolOutput: {
102
+ raw: TData;
103
+ meta?: TMeta;
104
+ };
105
+ extractedLists?: Record<string, unknown>;
106
+ extractedValues?: Record<string, unknown>;
107
+ meta?: Record<string, unknown>;
101
108
  };
102
109
  billing?: Record<string, unknown>;
103
110
  [key: string]: unknown;
@@ -552,19 +559,22 @@ export class DeeplineClient {
552
559
  /**
553
560
  * Execute a tool and return the standard execution envelope.
554
561
  *
555
- * The `result.data` field contains the provider payload. `result.meta`
556
- * contains provider/upstream metadata such as HTTP status or paging details.
562
+ * The `toolExecutionResult.toolOutput.raw` field contains the raw tool output.
563
+ * `toolExecutionResult.toolOutput.meta` contains tool/provider metadata.
557
564
  * Top-level fields such as `status`, `job_id`, and `billing` describe the
558
- * Deepline execution.
565
+ * Deepline execution envelope.
559
566
  */
560
567
  async executeTool<TData = unknown, TMeta = Record<string, unknown>>(
561
568
  toolId: string,
562
569
  input: Record<string, unknown>,
563
570
  options?: ExecuteToolRawOptions,
564
571
  ): Promise<ToolExecution<TData, TMeta>> {
565
- const headers = options?.includeToolMetadata
566
- ? { [INCLUDE_TOOL_METADATA_HEADER]: 'true' }
567
- : undefined;
572
+ const headers = {
573
+ [EXECUTE_RESPONSE_CONTRACT_HEADER]: V2_EXECUTE_RESPONSE_CONTRACT,
574
+ ...(options?.includeToolMetadata
575
+ ? { [INCLUDE_TOOL_METADATA_HEADER]: 'true' }
576
+ : {}),
577
+ };
568
578
  return this.http.post<ToolExecution<TData, TMeta>>(
569
579
  `/api/v2/integrations/${encodeURIComponent(toolId)}/execute`,
570
580
  { payload: input },
@@ -1029,34 +1039,37 @@ export class DeeplineClient {
1029
1039
  bytes: number;
1030
1040
  }>,
1031
1041
  ): Promise<PlayStagedFileRef[]> {
1032
- const formData = new FormData();
1033
- formData.set(
1034
- 'metadata',
1035
- JSON.stringify({
1036
- files: files.map((file, index) => ({
1037
- index,
1038
- logicalPath: file.logicalPath,
1039
- contentHash: file.contentHash,
1040
- contentType: file.contentType,
1041
- bytes: file.bytes,
1042
- })),
1043
- }),
1044
- );
1045
- for (const [index, file] of files.entries()) {
1046
- const bytes = decodeBase64Bytes(file.contentBase64);
1047
- const body = bytes.buffer.slice(
1048
- bytes.byteOffset,
1049
- bytes.byteOffset + bytes.byteLength,
1050
- ) as ArrayBuffer;
1042
+ const buildFormData = () => {
1043
+ const formData = new FormData();
1051
1044
  formData.set(
1052
- `file:${index}`,
1053
- new Blob([body], { type: file.contentType }),
1054
- file.logicalPath,
1045
+ 'metadata',
1046
+ JSON.stringify({
1047
+ files: files.map((file, index) => ({
1048
+ index,
1049
+ logicalPath: file.logicalPath,
1050
+ contentHash: file.contentHash,
1051
+ contentType: file.contentType,
1052
+ bytes: file.bytes,
1053
+ })),
1054
+ }),
1055
1055
  );
1056
- }
1056
+ for (const [index, file] of files.entries()) {
1057
+ const bytes = decodeBase64Bytes(file.contentBase64);
1058
+ const body = bytes.buffer.slice(
1059
+ bytes.byteOffset,
1060
+ bytes.byteOffset + bytes.byteLength,
1061
+ ) as ArrayBuffer;
1062
+ formData.set(
1063
+ `file:${index}`,
1064
+ new Blob([body], { type: file.contentType }),
1065
+ file.logicalPath,
1066
+ );
1067
+ }
1068
+ return formData;
1069
+ };
1057
1070
  const response = await this.http.postFormData<{ files: PlayStagedFileRef[] }>(
1058
1071
  '/api/v2/plays/files/stage',
1059
- formData,
1072
+ buildFormData,
1060
1073
  );
1061
1074
  return response.files;
1062
1075
  }