deepline 0.1.12 → 0.1.19

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.
Files changed (80) hide show
  1. package/README.md +14 -6
  2. package/dist/cli/index.js +1298 -711
  3. package/dist/cli/index.mjs +1294 -707
  4. package/dist/index.d.mts +199 -23
  5. package/dist/index.d.ts +199 -23
  6. package/dist/index.js +219 -13
  7. package/dist/index.mjs +219 -13
  8. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +68 -12
  9. package/dist/repo/apps/play-runner-workers/src/entry.ts +241 -51
  10. package/dist/repo/sdk/src/client.ts +237 -0
  11. package/dist/repo/sdk/src/config.ts +125 -8
  12. package/dist/repo/sdk/src/http.ts +10 -2
  13. package/dist/repo/sdk/src/play.ts +19 -36
  14. package/dist/repo/sdk/src/plays/bundle-play-file.ts +22 -8
  15. package/dist/repo/sdk/src/plays/local-file-discovery.ts +207 -160
  16. package/dist/repo/sdk/src/types.ts +25 -0
  17. package/dist/repo/sdk/src/version.ts +2 -2
  18. package/dist/repo/shared_libs/play-runtime/tool-result.ts +237 -145
  19. package/dist/repo/shared_libs/plays/bundling/index.ts +206 -229
  20. package/dist/repo/shared_libs/plays/dataset.ts +28 -0
  21. package/package.json +5 -4
  22. package/dist/cli/index.js.map +0 -1
  23. package/dist/cli/index.mjs.map +0 -1
  24. package/dist/index.js.map +0 -1
  25. package/dist/index.mjs.map +0 -1
  26. package/dist/repo/apps/play-runner-workers/src/runtime/README.md +0 -21
  27. package/dist/repo/apps/play-runner-workers/src/runtime/batching.ts +0 -177
  28. package/dist/repo/apps/play-runner-workers/src/runtime/execution-plan.ts +0 -52
  29. package/dist/repo/apps/play-runner-workers/src/runtime/tool-batch.ts +0 -100
  30. package/dist/repo/sdk/src/cli/commands/auth.ts +0 -500
  31. package/dist/repo/sdk/src/cli/commands/billing.ts +0 -188
  32. package/dist/repo/sdk/src/cli/commands/csv.ts +0 -123
  33. package/dist/repo/sdk/src/cli/commands/db.ts +0 -119
  34. package/dist/repo/sdk/src/cli/commands/feedback.ts +0 -40
  35. package/dist/repo/sdk/src/cli/commands/org.ts +0 -117
  36. package/dist/repo/sdk/src/cli/commands/play.ts +0 -3441
  37. package/dist/repo/sdk/src/cli/commands/tools.ts +0 -687
  38. package/dist/repo/sdk/src/cli/dataset-stats.ts +0 -415
  39. package/dist/repo/sdk/src/cli/index.ts +0 -148
  40. package/dist/repo/sdk/src/cli/progress.ts +0 -149
  41. package/dist/repo/sdk/src/cli/skills-sync.ts +0 -141
  42. package/dist/repo/sdk/src/cli/trace.ts +0 -61
  43. package/dist/repo/sdk/src/cli/utils.ts +0 -145
  44. package/dist/repo/sdk/src/compat.ts +0 -77
  45. package/dist/repo/shared_libs/observability/node-tracing.ts +0 -129
  46. package/dist/repo/shared_libs/observability/tracing.ts +0 -98
  47. package/dist/repo/shared_libs/play-runtime/context.ts +0 -4242
  48. package/dist/repo/shared_libs/play-runtime/ctx-contract.ts +0 -250
  49. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +0 -725
  50. package/dist/repo/shared_libs/play-runtime/dataset-id.ts +0 -10
  51. package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +0 -304
  52. package/dist/repo/shared_libs/play-runtime/db-session.ts +0 -462
  53. package/dist/repo/shared_libs/play-runtime/live-events.ts +0 -214
  54. package/dist/repo/shared_libs/play-runtime/live-state-contract.ts +0 -50
  55. package/dist/repo/shared_libs/play-runtime/map-execution-frame.ts +0 -114
  56. package/dist/repo/shared_libs/play-runtime/map-row-identity.ts +0 -158
  57. package/dist/repo/shared_libs/play-runtime/progress-emitter.ts +0 -172
  58. package/dist/repo/shared_libs/play-runtime/protocol.ts +0 -121
  59. package/dist/repo/shared_libs/play-runtime/public-play-contract.ts +0 -42
  60. package/dist/repo/shared_libs/play-runtime/result-normalization.ts +0 -33
  61. package/dist/repo/shared_libs/play-runtime/runtime-api.ts +0 -1873
  62. package/dist/repo/shared_libs/play-runtime/runtime-constraints.ts +0 -2
  63. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-neon-serverless.ts +0 -201
  64. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver-pg.ts +0 -48
  65. package/dist/repo/shared_libs/play-runtime/runtime-pg-driver.ts +0 -84
  66. package/dist/repo/shared_libs/play-runtime/static-pipeline-types.ts +0 -147
  67. package/dist/repo/shared_libs/play-runtime/suspension.ts +0 -68
  68. package/dist/repo/shared_libs/play-runtime/tracing.ts +0 -31
  69. package/dist/repo/shared_libs/play-runtime/waterfall-replay.ts +0 -75
  70. package/dist/repo/shared_libs/play-runtime/worker-api-types.ts +0 -140
  71. package/dist/repo/shared_libs/plays/artifact-transport.ts +0 -14
  72. package/dist/repo/shared_libs/plays/artifact-types.ts +0 -49
  73. package/dist/repo/shared_libs/plays/compiler-manifest.ts +0 -186
  74. package/dist/repo/shared_libs/plays/definition.ts +0 -264
  75. package/dist/repo/shared_libs/plays/file-refs.ts +0 -11
  76. package/dist/repo/shared_libs/plays/rate-limit-scheduler.ts +0 -206
  77. package/dist/repo/shared_libs/plays/resolve-static-pipeline.ts +0 -164
  78. package/dist/repo/shared_libs/plays/runtime-validation.ts +0 -395
  79. package/dist/repo/shared_libs/temporal/constants.ts +0 -39
  80. package/dist/repo/shared_libs/temporal/preview-config.ts +0 -153
@@ -204,11 +204,18 @@ type WorkerEnv = {
204
204
  * loud error. Loud failures > silent fallbacks.
205
205
  */
206
206
  HARNESS?: import('../../play-harness-worker/src/rpc-types').PlayHarnessRpc;
207
+ VERCEL_PROTECTION_BYPASS_TOKEN?: string;
207
208
  };
208
209
 
209
210
  let cachedRuntimeApiBinding: WorkerEnv['RUNTIME_API'] | null = null;
211
+ let cachedRuntimeApiVercelBypassToken: string | null = null;
210
212
  function captureRuntimeApiBinding(env: WorkerEnv): void {
211
213
  cachedRuntimeApiBinding = env.RUNTIME_API ?? null;
214
+ cachedRuntimeApiVercelBypassToken =
215
+ typeof env.VERCEL_PROTECTION_BYPASS_TOKEN === 'string' &&
216
+ env.VERCEL_PROTECTION_BYPASS_TOKEN.trim()
217
+ ? env.VERCEL_PROTECTION_BYPASS_TOKEN.trim()
218
+ : null;
212
219
  }
213
220
 
214
221
  let cachedCoordinatorBinding: WorkerEnv['COORDINATOR'] | null = null;
@@ -288,22 +295,20 @@ async function probeHarnessOnce(
288
295
  }
289
296
  }
290
297
  /**
291
- * Routes runtime API requests through the in-process RUNTIME_API binding.
292
- * The coordinator MUST provide this binding there is no public-fetch fallback.
298
+ * Routes runtime API requests through the in-process RUNTIME_API binding when
299
+ * Cloudflare exposes the coordinator WorkerEntrypoint export. Some workflow
300
+ * execution paths do not expose those exports; there we keep the older public
301
+ * fetch transport so the play still reaches the same authenticated handler.
293
302
  */
294
303
  const RUNTIME_API_TIMEOUT_MS = 30_000;
295
304
  const RUNTIME_API_PLAY_RUN_TIMEOUT_MS = 75_000;
305
+ let loggedMissingRuntimeApiBinding = false;
296
306
 
297
307
  async function fetchRuntimeApi(
298
308
  baseUrl: string,
299
309
  path: string,
300
310
  init: RequestInit,
301
311
  ): Promise<Response> {
302
- if (!cachedRuntimeApiBinding) {
303
- throw new Error(
304
- `[play-harness] RUNTIME_API binding missing — coordinator did not wire it before invoking the play. path=${path}`,
305
- );
306
- }
307
312
  const timeoutMs =
308
313
  path === '/api/v2/plays/run'
309
314
  ? RUNTIME_API_PLAY_RUN_TIMEOUT_MS
@@ -313,16 +318,25 @@ async function fetchRuntimeApi(
313
318
  try {
314
319
  const mergedInit: RequestInit = {
315
320
  ...init,
321
+ headers: runtimeApiHeaders(init.headers, cachedRuntimeApiBinding == null),
316
322
  signal: controller.signal,
317
323
  };
318
- const res = await cachedRuntimeApiBinding.fetch(
319
- new Request(`${baseUrl}${path}`, mergedInit),
324
+ if (!cachedRuntimeApiBinding) {
325
+ if (!loggedMissingRuntimeApiBinding) {
326
+ loggedMissingRuntimeApiBinding = true;
327
+ console.warn(
328
+ `[play-harness] RUNTIME_API binding missing; using public runtime API transport. path=${path}`,
329
+ );
330
+ }
331
+ return await fetch(`${baseUrl.replace(/\/$/, '')}${path}`, mergedInit);
332
+ }
333
+ return await cachedRuntimeApiBinding.fetch(
334
+ new Request(`${baseUrl.replace(/\/$/, '')}${path}`, mergedInit),
320
335
  );
321
- return res;
322
336
  } catch (err) {
323
337
  if (err instanceof Error && err.name === 'AbortError') {
324
338
  throw new Error(
325
- `[play-harness] RUNTIME_API call timed out after ${timeoutMs}ms. path=${path} baseUrl=${baseUrl}`,
339
+ `[play-harness] runtime API call timed out after ${timeoutMs}ms. path=${path} baseUrl=${baseUrl}`,
326
340
  );
327
341
  }
328
342
  throw err;
@@ -331,6 +345,24 @@ async function fetchRuntimeApi(
331
345
  }
332
346
  }
333
347
 
348
+ function runtimeApiHeaders(
349
+ headers: HeadersInit | undefined,
350
+ includeVercelBypass: boolean,
351
+ ): Headers {
352
+ const next = new Headers(headers);
353
+ if (includeVercelBypass) {
354
+ const bypassToken = cachedVercelProtectionBypassToken();
355
+ if (bypassToken) {
356
+ next.set('x-vercel-protection-bypass', bypassToken);
357
+ }
358
+ }
359
+ return next;
360
+ }
361
+
362
+ function cachedVercelProtectionBypassToken(): string | null {
363
+ return cachedRuntimeApiVercelBypassToken;
364
+ }
365
+
334
366
  const WORKER_PLAY_CALL_LIMITS = {
335
367
  maxPlayCallDepth: 6,
336
368
  maxPlayCallCount: 32,
@@ -350,6 +382,20 @@ function makeWorkerDataset<T extends Record<string, unknown>>(
350
382
  count?: number;
351
383
  datasetKind?: 'csv' | 'map';
352
384
  cacheSummary?: string | null;
385
+ workProgress?: {
386
+ total: number;
387
+ executed: number;
388
+ reused: number;
389
+ skipped: number;
390
+ pending: number;
391
+ failed: number;
392
+ degraded?: boolean;
393
+ duplicates?: {
394
+ exact?: number;
395
+ semantic?: number;
396
+ rejected?: number;
397
+ };
398
+ };
353
399
  },
354
400
  ): T[] & {
355
401
  count(): Promise<number>;
@@ -363,6 +409,7 @@ function makeWorkerDataset<T extends Record<string, unknown>>(
363
409
  const count = Math.max(0, Math.floor(options?.count ?? rows.length));
364
410
  const datasetKind = options?.datasetKind ?? 'map';
365
411
  const cacheSummary = options?.cacheSummary ?? null;
412
+ const workProgress = options?.workProgress;
366
413
  // Build the array result. JSON.stringify on arrays calls toJSON only if
367
414
  // present on the array itself — we attach below. The dataset metadata is
368
415
  // also exposed via own properties so plays can `enriched.count()` etc.
@@ -415,6 +462,10 @@ function makeWorkerDataset<T extends Record<string, unknown>>(
415
462
  value: cacheSummary,
416
463
  enumerable: false,
417
464
  });
465
+ Object.defineProperty(arr, '__deeplineWorkProgress', {
466
+ value: workProgress,
467
+ enumerable: false,
468
+ });
418
469
  // Plays often `return { rows: dataset, count: N }`. JSON.stringify on the
419
470
  // array would normally produce `[row, row, ...]` — we want the dataset
420
471
  // envelope shape instead so assertions seeing `result.rows.columns` pass.
@@ -435,6 +486,9 @@ function makeWorkerDataset<T extends Record<string, unknown>>(
435
486
  preview: plainRows,
436
487
  tableNamespace: name,
437
488
  ...(cacheSummary ? { cacheSummary } : {}),
489
+ ...(workProgress
490
+ ? { _metadata: { workProgress } }
491
+ : {}),
438
492
  };
439
493
  },
440
494
  enumerable: false,
@@ -454,6 +508,7 @@ type RunnerEvent =
454
508
  | { type: 'error'; message: string; stack?: string; ts: number };
455
509
 
456
510
  type WorkflowRunOutput = {
511
+ playName: string;
457
512
  result: unknown;
458
513
  outputRows: number;
459
514
  durationMs: number;
@@ -469,7 +524,12 @@ function makeRequestId(): string {
469
524
  }
470
525
 
471
526
  function publicCsvInputRow<T extends Record<string, unknown>>(row: T): T {
472
- return stripCsvProjectedFields(row) as T;
527
+ const stripped = stripCsvProjectedFields(row) as Record<string, unknown>;
528
+ return Object.fromEntries(
529
+ Object.entries(stripped).filter(
530
+ ([fieldName]) => !fieldName.startsWith('__deepline'),
531
+ ),
532
+ ) as T;
473
533
  }
474
534
 
475
535
  /**
@@ -714,6 +774,11 @@ function childPlayEventKey(input: { key: string; workflowId: string }): string {
714
774
  return `child_play_${hashChildPlayEventKey(`${input.key}:${input.workflowId}`)}_${readableKey}`;
715
775
  }
716
776
 
777
+ function workflowTimeoutFromMs(timeoutMs: number): string {
778
+ const seconds = Math.max(1, Math.ceil(timeoutMs / 1000));
779
+ return `${seconds} second${seconds === 1 ? '' : 's'}`;
780
+ }
781
+
717
782
  async function waitForChildPlayTerminalEvent(input: {
718
783
  req: RunRequest;
719
784
  workflowStep?: WorkflowStep;
@@ -734,11 +799,11 @@ async function waitForChildPlayTerminalEvent(input: {
734
799
  const event = (await (
735
800
  input.workflowStep.waitForEvent as unknown as (
736
801
  name: string,
737
- options: { type: string; timeout: number },
802
+ options: { type: string; timeout: string },
738
803
  ) => Promise<{ payload: unknown }>
739
804
  )(`child_play_terminal:${eventKey}`, {
740
805
  type: integrationEventType(eventKey),
741
- timeout: input.timeoutMs,
806
+ timeout: workflowTimeoutFromMs(input.timeoutMs),
742
807
  })) as { payload: unknown };
743
808
  const rawPayload = isRecord(event.payload) ? event.payload : {};
744
809
  const payload = isRecord(rawPayload.data) ? rawPayload.data : rawPayload;
@@ -909,15 +974,25 @@ async function waitForSyntheticIntegrationEvent(
909
974
  typeof input.timeout_ms === 'number' && Number.isFinite(input.timeout_ms)
910
975
  ? Math.max(1, Math.round(input.timeout_ms))
911
976
  : 30_000;
977
+ await postRuntimeApiBestEffort(req.baseUrl, req.executorToken, {
978
+ action: 'update_run_status',
979
+ playId: req.runId,
980
+ status: 'running',
981
+ runtimeBackend: 'cf_workflows_dynamic_worker',
982
+ waitKind: 'integration_event_batch',
983
+ waitUntil: nowMs() + timeoutMs,
984
+ activeBoundaryId: `integration_event:${eventKey}`,
985
+ lastCheckpointAt: nowMs(),
986
+ });
912
987
  try {
913
988
  const event = (await (
914
989
  workflowStep.waitForEvent as unknown as (
915
990
  name: string,
916
- options: { type: string; timeout: number },
991
+ options: { type: string; timeout: string },
917
992
  ) => Promise<{ payload: unknown }>
918
993
  )(`integration_event:${eventKey}`, {
919
994
  type: integrationEventType(eventKey),
920
- timeout: timeoutMs,
995
+ timeout: workflowTimeoutFromMs(timeoutMs),
921
996
  })) as { payload: unknown };
922
997
  const payload =
923
998
  event.payload &&
@@ -983,7 +1058,7 @@ async function callToolDirect(
983
1058
  headers: {
984
1059
  'content-type': 'application/json',
985
1060
  authorization: `Bearer ${req.executorToken}`,
986
- 'x-deepline-request-id': `${req.runId}:${toolId}:${id}`,
1061
+ 'x-deepline-request-id': `${req.runId}:${toolId}:${id}:attempt:${attempt}`,
987
1062
  [EXECUTE_TOOL_METADATA_HEADER]: 'true',
988
1063
  },
989
1064
  body: JSON.stringify({ payload: input }),
@@ -1455,7 +1530,9 @@ async function executeBatchedWorkerToolGroup(input: {
1455
1530
  ) => {
1456
1531
  for (const entry of chunkResults) {
1457
1532
  const batchResult = isToolExecuteResult(entry.result)
1458
- ? entry.result.result.data
1533
+ ? isRecordLike(entry.result.result)
1534
+ ? entry.result.result.data
1535
+ : undefined
1459
1536
  : entry.result;
1460
1537
  const splitResults =
1461
1538
  batchResult != null
@@ -1516,6 +1593,7 @@ type WorkerMapChunkSummary<T extends Record<string, unknown>> = {
1516
1593
  rowsWritten: number;
1517
1594
  rowsExecuted: number;
1518
1595
  rowsCached: number;
1596
+ rowsDuplicateReused: number;
1519
1597
  rowsInserted: number;
1520
1598
  rowsSkipped: number;
1521
1599
  outputDatasetId: string;
@@ -1641,22 +1719,32 @@ async function executeWorkerStepProgram(
1641
1719
  path: string[];
1642
1720
  outputs: RecordedStepProgramOutput[];
1643
1721
  },
1722
+ workflowStep?: WorkflowStep,
1644
1723
  ): Promise<unknown> {
1645
1724
  let currentRow: Record<string, unknown> = cloneCsvAliasedRow(inputRow);
1646
1725
  for (const step of program.steps) {
1647
1726
  const stepPath = [...(recorder?.path ?? []), step.name];
1648
- const resolution = await executeWorkerStepResolver(
1649
- step.resolver,
1650
- currentRow,
1651
- ctx,
1652
- index,
1653
- recorder
1654
- ? {
1655
- ...recorder,
1656
- path: stepPath,
1657
- }
1658
- : undefined,
1659
- );
1727
+ const runStep = async () =>
1728
+ await executeWorkerStepResolver(
1729
+ step.resolver,
1730
+ currentRow,
1731
+ ctx,
1732
+ index,
1733
+ recorder
1734
+ ? {
1735
+ ...recorder,
1736
+ path: stepPath,
1737
+ }
1738
+ : undefined,
1739
+ );
1740
+ const resolution = workflowStep
1741
+ ? await (
1742
+ workflowStep.do as unknown as (
1743
+ name: string,
1744
+ callback: () => Promise<WorkerStepResolution>,
1745
+ ) => Promise<WorkerStepResolution>
1746
+ )(stepPath.join('.'), runStep)
1747
+ : await runStep();
1660
1748
  const value = resolution.value;
1661
1749
  currentRow = cloneCsvAliasedRow(currentRow, { [step.name]: value });
1662
1750
  if (recorder) {
@@ -2569,7 +2657,17 @@ function createMinimalWorkerCtx(
2569
2657
  const rowsToExecuteEntries = chunkEntries.filter(
2570
2658
  ({ rowKey }) => pendingKeys.has(rowKey) || !completedKeys.has(rowKey),
2571
2659
  );
2572
- const rowsToExecute = rowsToExecuteEntries.map(({ row }) => row);
2660
+ const uniqueRowsToExecuteEntries = [
2661
+ ...new Map(
2662
+ rowsToExecuteEntries.map((entry) => [entry.rowKey, entry]),
2663
+ ).values(),
2664
+ ];
2665
+ const duplicateInputReuseCount = Math.max(
2666
+ 0,
2667
+ chunkEntries.length -
2668
+ new Set(chunkEntries.map((entry) => entry.rowKey)).size,
2669
+ );
2670
+ const rowsToExecute = uniqueRowsToExecuteEntries.map(({ row }) => row);
2573
2671
  const rowsInserted = prepared.inserted + missingPreparedRows.length;
2574
2672
  const rowsSkipped = Math.max(
2575
2673
  0,
@@ -2593,7 +2691,7 @@ function createMinimalWorkerCtx(
2593
2691
  if (abortSignal?.aborted) return;
2594
2692
  const myIndex = idx++;
2595
2693
  if (myIndex >= rowsToExecute.length) return;
2596
- const entry = rowsToExecuteEntries[myIndex]!;
2694
+ const entry = uniqueRowsToExecuteEntries[myIndex]!;
2597
2695
  const row = entry.row;
2598
2696
  const absoluteIndex = entry.absoluteIndex;
2599
2697
  const enriched: Record<string, unknown> = cloneCsvAliasedRow(row);
@@ -2701,14 +2799,33 @@ function createMinimalWorkerCtx(
2701
2799
  })(),
2702
2800
  );
2703
2801
  }
2704
- await Promise.all(workers);
2705
- if (executedRows.length > 0) {
2802
+ const persistExecutedRows = async () => {
2803
+ const rowsToPersist = executedRows
2804
+ .map((row, executedIndex) =>
2805
+ row
2806
+ ? {
2807
+ row,
2808
+ executedIndex,
2809
+ }
2810
+ : null,
2811
+ )
2812
+ .filter(
2813
+ (
2814
+ entry,
2815
+ ): entry is {
2816
+ row: T & Record<string, unknown>;
2817
+ executedIndex: number;
2818
+ } => entry !== null,
2819
+ );
2820
+ if (rowsToPersist.length === 0) {
2821
+ return;
2822
+ }
2706
2823
  await persistCompletedMapRows({
2707
2824
  req,
2708
2825
  tableNamespace: name,
2709
2826
  outputFields,
2710
2827
  extraOutputFields: Array.from(generatedOutputFields),
2711
- rows: executedRows.map((row, executedIndex) => ({
2828
+ rows: rowsToPersist.map(({ row, executedIndex }) => ({
2712
2829
  ...row,
2713
2830
  ...(executedCellMetaPatches[executedIndex]
2714
2831
  ? {
@@ -2716,9 +2833,19 @@ function createMinimalWorkerCtx(
2716
2833
  executedCellMetaPatches[executedIndex],
2717
2834
  }
2718
2835
  : {}),
2719
- __deeplineRowKey: rowsToExecuteEntries[executedIndex]!.rowKey,
2836
+ __deeplineRowKey:
2837
+ uniqueRowsToExecuteEntries[executedIndex]!.rowKey,
2720
2838
  })),
2721
2839
  });
2840
+ };
2841
+ const workerResults = await Promise.allSettled(workers);
2842
+ await persistExecutedRows();
2843
+ const rejectedWorker = workerResults.find(
2844
+ (result): result is PromiseRejectedResult =>
2845
+ result.status === 'rejected',
2846
+ );
2847
+ if (rejectedWorker) {
2848
+ throw rejectedWorker.reason;
2722
2849
  }
2723
2850
  const resultByKey = new Map<string, T & Record<string, unknown>>();
2724
2851
  for (const completedRow of prepared.completedRows) {
@@ -2739,7 +2866,7 @@ function createMinimalWorkerCtx(
2739
2866
  executedIndex += 1
2740
2867
  ) {
2741
2868
  const executedRow = executedRows[executedIndex]!;
2742
- const key = rowsToExecuteEntries[executedIndex]!.rowKey;
2869
+ const key = uniqueRowsToExecuteEntries[executedIndex]!.rowKey;
2743
2870
  if (key) resultByKey.set(key, executedRow);
2744
2871
  }
2745
2872
  const out = chunkRows
@@ -2755,7 +2882,8 @@ function createMinimalWorkerCtx(
2755
2882
  rowsRead: chunkRows.length,
2756
2883
  rowsWritten: out.length,
2757
2884
  rowsExecuted: executedRows.length,
2758
- rowsCached: prepared.completedRows.length,
2885
+ rowsCached: Math.max(0, out.length - executedRows.length),
2886
+ rowsDuplicateReused: duplicateInputReuseCount,
2759
2887
  rowsInserted,
2760
2888
  rowsSkipped,
2761
2889
  outputDatasetId: `map:${name}`,
@@ -2767,6 +2895,7 @@ function createMinimalWorkerCtx(
2767
2895
  const out: Array<T & Record<string, unknown>> = [];
2768
2896
  let totalRowsExecuted = 0;
2769
2897
  let totalRowsCached = 0;
2898
+ let totalRowsDuplicateReused = 0;
2770
2899
  let totalRowsInserted = 0;
2771
2900
  let totalRowsSkipped = 0;
2772
2901
 
@@ -2809,6 +2938,17 @@ function createMinimalWorkerCtx(
2809
2938
  return makeWorkerDataset(name, out, {
2810
2939
  count: totalRowsWritten,
2811
2940
  cacheSummary,
2941
+ workProgress: {
2942
+ total: totalRowsWritten,
2943
+ executed: totalRowsExecuted,
2944
+ reused: totalRowsCached,
2945
+ skipped: totalRowsCached,
2946
+ pending: 0,
2947
+ failed: 0,
2948
+ ...(totalRowsDuplicateReused > 0
2949
+ ? { duplicates: { exact: totalRowsDuplicateReused } }
2950
+ : {}),
2951
+ },
2812
2952
  });
2813
2953
  };
2814
2954
 
@@ -2829,6 +2969,7 @@ function createMinimalWorkerCtx(
2829
2969
  totalRowsWritten += chunkResult.rowsWritten;
2830
2970
  totalRowsExecuted += chunkResult.rowsExecuted;
2831
2971
  totalRowsCached += chunkResult.rowsCached;
2972
+ totalRowsDuplicateReused += chunkResult.rowsDuplicateReused;
2832
2973
  totalRowsInserted += chunkResult.rowsInserted;
2833
2974
  totalRowsSkipped += chunkResult.rowsSkipped;
2834
2975
  if (out.length < 10) {
@@ -2852,6 +2993,7 @@ function createMinimalWorkerCtx(
2852
2993
  totalRowsWritten += chunkResult.rowsWritten;
2853
2994
  totalRowsExecuted += chunkResult.rowsExecuted;
2854
2995
  totalRowsCached += chunkResult.rowsCached;
2996
+ totalRowsDuplicateReused += chunkResult.rowsDuplicateReused;
2855
2997
  totalRowsInserted += chunkResult.rowsInserted;
2856
2998
  totalRowsSkipped += chunkResult.rowsSkipped;
2857
2999
  if (out.length < 10) {
@@ -2865,6 +3007,7 @@ function createMinimalWorkerCtx(
2865
3007
  const chunkResult = await runChunkStep(sliced, 0, 0);
2866
3008
  totalRowsExecuted = chunkResult.rowsExecuted;
2867
3009
  totalRowsCached = chunkResult.rowsCached;
3010
+ totalRowsDuplicateReused = chunkResult.rowsDuplicateReused;
2868
3011
  totalRowsInserted = chunkResult.rowsInserted;
2869
3012
  totalRowsSkipped = chunkResult.rowsSkipped;
2870
3013
  out.push(...chunkResult.preview);
@@ -2917,18 +3060,39 @@ function createMinimalWorkerCtx(
2917
3060
  },
2918
3061
  async step<T>(name: string, callback: () => Promise<T> | T): Promise<T> {
2919
3062
  assertNotAborted(abortSignal);
2920
- if (!workflowStep) {
2921
- return await callback();
3063
+ if (!name.trim()) {
3064
+ throw new Error('ctx.step(name, callback) requires a name.');
2922
3065
  }
2923
- return await (
2924
- workflowStep.do as unknown as (
2925
- name: string,
2926
- callback: () => Promise<T>,
2927
- ) => Promise<T>
2928
- )(name, async () => {
2929
- assertNotAborted(abortSignal);
2930
- return await callback();
2931
- });
3066
+ // Static pipeline JS blocks are already Workflow steps in the Workers
3067
+ // backend. Nesting another `step.do` here can leave preview runs parked
3068
+ // inside the JS stage before they reach subsequent event waits.
3069
+ return await callback();
3070
+ },
3071
+ async runSteps<T>(
3072
+ program: WorkerStepProgram,
3073
+ input: Record<string, unknown>,
3074
+ opts?: { description?: string },
3075
+ ): Promise<T> {
3076
+ assertNotAborted(abortSignal);
3077
+ if (!isWorkerStepProgram(program)) {
3078
+ throw new Error('ctx.runSteps(program, input) requires steps().');
3079
+ }
3080
+ if (opts?.description) {
3081
+ emitEvent({
3082
+ type: 'log',
3083
+ level: 'info',
3084
+ message: String(opts.description),
3085
+ ts: nowMs(),
3086
+ });
3087
+ }
3088
+ return (await executeWorkerStepProgram(
3089
+ program,
3090
+ input,
3091
+ ctx,
3092
+ 0,
3093
+ undefined,
3094
+ workflowStep,
3095
+ )) as T;
2932
3096
  },
2933
3097
  async csv<T extends Record<string, unknown> = Record<string, unknown>>(
2934
3098
  arg: unknown,
@@ -3263,6 +3427,14 @@ function createMinimalWorkerCtx(
3263
3427
  return makeWorkerDataset(name, out, {
3264
3428
  count: totalRowsWritten,
3265
3429
  cacheSummary,
3430
+ workProgress: {
3431
+ total: totalRowsWritten,
3432
+ executed: totalRowsExecuted,
3433
+ reused: totalRowsCached,
3434
+ skipped: totalRowsCached,
3435
+ pending: 0,
3436
+ failed: 0,
3437
+ },
3266
3438
  });
3267
3439
  };
3268
3440
 
@@ -3619,11 +3791,11 @@ function createMinimalWorkerCtx(
3619
3791
  const event = (await (
3620
3792
  workflowStep.waitForEvent as unknown as (
3621
3793
  name: string,
3622
- options: { type: string; timeout: number },
3794
+ options: { type: string; timeout: string },
3623
3795
  ) => Promise<{ payload: unknown }>
3624
3796
  )(`wait_for_event:${workflowEventType(eventType)}`, {
3625
3797
  type: workflowEventType(eventType),
3626
- timeout: timeoutMs,
3798
+ timeout: workflowTimeoutFromMs(timeoutMs),
3627
3799
  })) as { payload: unknown };
3628
3800
  return event.payload ?? null;
3629
3801
  },
@@ -3804,6 +3976,9 @@ async function executeRunRequest(
3804
3976
  status: 'completed',
3805
3977
  result: terminalResult,
3806
3978
  runtimeBackend: 'cf_workflows_dynamic_worker',
3979
+ waitKind: null,
3980
+ waitUntil: null,
3981
+ activeBoundaryId: null,
3807
3982
  liveLogs,
3808
3983
  lastCheckpointAt: nowMs(),
3809
3984
  });
@@ -3825,6 +4000,7 @@ async function executeRunRequest(
3825
4000
  );
3826
4001
  });
3827
4002
  return {
4003
+ playName: req.playName,
3828
4004
  result: serializedResult,
3829
4005
  outputRows: inferOutputRows(serializedResult),
3830
4006
  durationMs: nowMs() - startedAt,
@@ -3847,6 +4023,9 @@ async function executeRunRequest(
3847
4023
  status: aborted ? 'cancelled' : 'failed',
3848
4024
  error: message,
3849
4025
  runtimeBackend: 'cf_workflows_dynamic_worker',
4026
+ waitKind: null,
4027
+ waitUntil: null,
4028
+ activeBoundaryId: null,
3850
4029
  liveLogs,
3851
4030
  lastCheckpointAt: nowMs(),
3852
4031
  });
@@ -4121,6 +4300,14 @@ function serializeValue(value: unknown, depth: number): unknown {
4121
4300
  ? (value as unknown as { __deeplineCacheSummary: string })
4122
4301
  .__deeplineCacheSummary
4123
4302
  : null;
4303
+ const workProgress =
4304
+ isRecord(
4305
+ (value as unknown as { __deeplineWorkProgress?: unknown })
4306
+ .__deeplineWorkProgress,
4307
+ )
4308
+ ? (value as unknown as { __deeplineWorkProgress: Record<string, unknown> })
4309
+ .__deeplineWorkProgress
4310
+ : null;
4124
4311
  const previewRows = value
4125
4312
  .slice(0, 5)
4126
4313
  .map((row) => serializeValue(row, depth + 1))
@@ -4138,6 +4325,9 @@ function serializeValue(value: unknown, depth: number): unknown {
4138
4325
  preview: previewRows,
4139
4326
  tableNamespace,
4140
4327
  ...(cacheSummary ? { cacheSummary } : {}),
4328
+ ...(workProgress
4329
+ ? { _metadata: { workProgress } }
4330
+ : {}),
4141
4331
  };
4142
4332
  }
4143
4333
  return value.map((entry) => serializeValue(entry, depth + 1));