deepline 0.1.21 → 0.1.22

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/index.mjs CHANGED
@@ -195,7 +195,7 @@ function resolveConfig(options) {
195
195
  }
196
196
 
197
197
  // src/version.ts
198
- var SDK_VERSION = "0.1.21";
198
+ var SDK_VERSION = "0.1.22";
199
199
  var SDK_API_CONTRACT = "2026-05-runs-v2";
200
200
 
201
201
  // ../shared_libs/play-runtime/coordinator-headers.ts
@@ -487,7 +487,7 @@ function isRecord(value) {
487
487
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
488
488
  }
489
489
  function normalizePlayStatus(raw) {
490
- const status = typeof raw.status === "string" ? raw.status : typeof raw.temporalStatus === "string" ? mapLegacyTemporalStatus(raw.temporalStatus) : "running";
490
+ const status = typeof raw.status === "string" ? raw.status : "running";
491
491
  const runId = typeof raw.runId === "string" ? raw.runId : typeof raw.workflowId === "string" ? raw.workflowId : "";
492
492
  return {
493
493
  ...raw,
@@ -495,23 +495,6 @@ function normalizePlayStatus(raw) {
495
495
  status
496
496
  };
497
497
  }
498
- function mapLegacyTemporalStatus(status) {
499
- switch (status.trim().toUpperCase()) {
500
- case "PENDING":
501
- return "queued";
502
- case "COMPLETED":
503
- return "completed";
504
- case "FAILED":
505
- return "failed";
506
- case "CANCELLED":
507
- case "TERMINATED":
508
- case "TIMED_OUT":
509
- return "cancelled";
510
- case "RUNNING":
511
- default:
512
- return "running";
513
- }
514
- }
515
498
  function decodeBase64Bytes(value) {
516
499
  const binary = atob(value);
517
500
  const bytes = new Uint8Array(binary.length);
@@ -520,6 +503,79 @@ function decodeBase64Bytes(value) {
520
503
  }
521
504
  return bytes;
522
505
  }
506
+ function readStringArray(value) {
507
+ return Array.isArray(value) ? value.filter((line) => typeof line === "string") : [];
508
+ }
509
+ function getPlayLiveEventPayload(event) {
510
+ return event.payload && typeof event.payload === "object" ? event.payload : {};
511
+ }
512
+ function normalizeLiveStatus(value) {
513
+ if (value === "queued" || value === "running" || value === "waiting" || value === "completed" || value === "failed" || value === "cancelled") {
514
+ return value;
515
+ }
516
+ return null;
517
+ }
518
+ function updatePlayLiveStatusState(state, event) {
519
+ const payload = getPlayLiveEventPayload(event);
520
+ if (event.type === "play.run.log") {
521
+ state.logs.push(...readStringArray(payload.lines));
522
+ return null;
523
+ }
524
+ if (event.type !== "play.run.snapshot" && event.type !== "play.run.status" && event.type !== "play.run.final_status") {
525
+ return null;
526
+ }
527
+ const runId = typeof payload.runId === "string" && payload.runId ? payload.runId : state.runId;
528
+ const status = normalizeLiveStatus(payload.status) ?? state.status;
529
+ const logs = readStringArray(payload.logs);
530
+ if (logs.length > 0 || event.type === "play.run.snapshot") {
531
+ state.logs = logs;
532
+ }
533
+ if ("result" in payload) {
534
+ state.result = payload.result;
535
+ }
536
+ if (typeof payload.error === "string" && payload.error.trim()) {
537
+ state.error = payload.error;
538
+ }
539
+ state.runId = runId;
540
+ state.status = status;
541
+ const progressRecord = payload.progress && typeof payload.progress === "object" && !Array.isArray(payload.progress) ? payload.progress : {};
542
+ const next = {
543
+ ...payload,
544
+ runId,
545
+ status,
546
+ progress: {
547
+ ...progressRecord,
548
+ status: typeof progressRecord.status === "string" ? progressRecord.status : status,
549
+ logs: state.logs,
550
+ ...state.error ? { error: state.error } : {}
551
+ },
552
+ ..."result" in state ? { result: state.result } : {}
553
+ };
554
+ state.latest = next;
555
+ return next;
556
+ }
557
+ function playRunResultFromStatus(status, startedAt, fallbackRunId) {
558
+ return {
559
+ success: status.status === "completed",
560
+ runId: status.runId || fallbackRunId,
561
+ result: status.result,
562
+ logs: status.progress?.logs ?? [],
563
+ durationMs: Date.now() - startedAt,
564
+ error: status.progress?.error ?? (status.status !== "completed" ? status.status : void 0)
565
+ };
566
+ }
567
+ function playRunStatusFromState(state) {
568
+ return {
569
+ runId: state.runId,
570
+ status: state.status,
571
+ progress: {
572
+ status: state.status,
573
+ logs: state.logs,
574
+ ...state.error ? { error: state.error } : {}
575
+ },
576
+ ..."result" in state ? { result: state.result } : {}
577
+ };
578
+ }
523
579
  var DeeplineClient = class {
524
580
  http;
525
581
  config;
@@ -629,7 +685,7 @@ var DeeplineClient = class {
629
685
  /**
630
686
  * Search available tools using Deepline's ranked backend search.
631
687
  *
632
- * This is the same discovery surface used by the legacy CLI: it ranks across
688
+ * This is the same discovery surface used by the CLI: it ranks across
633
689
  * tool metadata, categories, agent guidance, and input schema fields.
634
690
  */
635
691
  async searchTools(options = {}) {
@@ -722,7 +778,7 @@ var DeeplineClient = class {
722
778
  * `progress.logs`; they are not part of the user output object.
723
779
  *
724
780
  * @param request - Play run configuration (name, code, input, etc.)
725
- * @returns Workflow metadata including the `workflowId` for status polling
781
+ * @returns Run metadata including the public `workflowId`
726
782
  *
727
783
  * @example
728
784
  * ```typescript
@@ -1014,9 +1070,6 @@ var DeeplineClient = class {
1014
1070
  * Internal/advanced primitive. Public callers should usually prefer
1015
1071
  * {@link runPlay}, {@link PlayJob.get}, or `deepline play run --watch`.
1016
1072
  *
1017
- * Poll this method until `status` reaches a terminal state:
1018
- * `'completed'`, `'failed'`, or `'cancelled'`.
1019
- *
1020
1073
  * @param workflowId - Play-run id from {@link startPlayRun}
1021
1074
  * @returns Current status with progress logs and partial results
1022
1075
  *
@@ -1038,35 +1091,11 @@ var DeeplineClient = class {
1038
1091
  );
1039
1092
  return normalizePlayStatus(response);
1040
1093
  }
1041
- /**
1042
- * Get the lightweight tail-polling status for a play execution.
1043
- *
1044
- * This is intentionally smaller than {@link getPlayStatus}: it returns the
1045
- * fields needed for CLI log tailing while the run is in flight, without
1046
- * forcing the API to rebuild final result views on every poll. Call
1047
- * {@link getPlayStatus} once after a terminal state for the full result.
1048
- */
1049
- async getPlayTailStatus(workflowId, options) {
1050
- const params = new URLSearchParams({ mode: "tail" });
1051
- if (typeof options?.afterLogIndex === "number") {
1052
- params.set("afterLogIndex", String(options.afterLogIndex));
1053
- }
1054
- if (typeof options?.waitMs === "number") {
1055
- params.set("waitMs", String(options.waitMs));
1056
- }
1057
- if (options?.terminalOnly) {
1058
- params.set("terminalOnly", "true");
1059
- }
1060
- const response = await this.http.get(
1061
- `/api/v2/plays/run/${encodeURIComponent(workflowId)}?${params.toString()}`
1062
- );
1063
- return normalizePlayStatus(response);
1064
- }
1065
1094
  /**
1066
1095
  * Stream semantic play-run events using the same SSE feed as the dashboard.
1067
1096
  *
1068
- * Consumers should still keep a polling fallback: SSE is the fast live-update
1069
- * transport, while the status endpoints remain the authoritative recovery path.
1097
+ * The server emits a canonical `play.run.snapshot` event first for every
1098
+ * connection, then incremental live events until terminal state or reconnect.
1070
1099
  */
1071
1100
  async *streamPlayRunEvents(workflowId, options) {
1072
1101
  const headers = options?.lastEventId && options.lastEventId.trim() ? { "Last-Event-ID": options.lastEventId.trim() } : void 0;
@@ -1086,7 +1115,7 @@ var DeeplineClient = class {
1086
1115
  *
1087
1116
  * Sends a stop request for the run.
1088
1117
  *
1089
- * @param workflowId - Temporal workflow ID to cancel
1118
+ * @param workflowId - Public Deepline play-run id to cancel
1090
1119
  *
1091
1120
  * @example
1092
1121
  * ```typescript
@@ -1102,7 +1131,7 @@ var DeeplineClient = class {
1102
1131
  /**
1103
1132
  * Stop a running play execution, including open HITL waits.
1104
1133
  *
1105
- * @param workflowId - Temporal workflow ID to stop
1134
+ * @param workflowId - Public Deepline play-run id to stop
1106
1135
  * @param options.reason - Optional audit/debug reason
1107
1136
  */
1108
1137
  async stopPlay(workflowId, options) {
@@ -1174,32 +1203,42 @@ var DeeplineClient = class {
1174
1203
  );
1175
1204
  return response.runs ?? [];
1176
1205
  }
1177
- /**
1178
- * Fetch the lightweight tail status for a run using the public runs resource model.
1179
- *
1180
- * This is the SDK equivalent of:
1181
- *
1182
- * ```bash
1183
- * deepline runs tail <run-id> --json
1184
- * ```
1185
- */
1206
+ /** Read the canonical run stream and return the latest run snapshot. */
1186
1207
  async tailRun(runId, options) {
1187
- const afterLogIndex = typeof options?.afterLogIndex === "number" ? options.afterLogIndex : typeof options?.cursor === "number" ? options.cursor : typeof options?.cursor === "string" && options.cursor.trim() ? Number(options.cursor) : void 0;
1188
- const params = new URLSearchParams();
1189
- if (Number.isFinite(afterLogIndex)) {
1190
- params.set("afterLogIndex", String(Number(afterLogIndex)));
1208
+ const state = {
1209
+ runId,
1210
+ status: "running",
1211
+ logs: [],
1212
+ latest: null
1213
+ };
1214
+ let terminal = false;
1215
+ for await (const event of this.streamPlayRunEvents(runId, {
1216
+ mode: "cli",
1217
+ signal: options?.signal
1218
+ })) {
1219
+ const status = updatePlayLiveStatusState(state, event);
1220
+ if (!status) {
1221
+ continue;
1222
+ }
1223
+ terminal = TERMINAL_PLAY_STATUSES.has(status.status);
1224
+ if (terminal) {
1225
+ break;
1226
+ }
1191
1227
  }
1192
- if (typeof options?.waitMs === "number") {
1193
- params.set("waitMs", String(options.waitMs));
1228
+ if (terminal && state.latest) {
1229
+ return await this.getRunStatus(state.latest.runId || runId).catch(
1230
+ () => state.latest ?? playRunStatusFromState(state)
1231
+ );
1194
1232
  }
1195
- if (options?.terminalOnly) {
1196
- params.set("terminalOnly", "true");
1233
+ if (state.latest) {
1234
+ return state.latest;
1197
1235
  }
1198
- const suffix = params.toString() ? `?${params.toString()}` : "";
1199
- const response = await this.http.get(
1200
- `/api/v2/runs/${encodeURIComponent(runId)}/tail${suffix}`
1236
+ throw new DeeplineError(
1237
+ `Run stream for ${runId} ended before the initial snapshot.`,
1238
+ void 0,
1239
+ "PLAY_RUN_STREAM_EMPTY",
1240
+ { runId }
1201
1241
  );
1202
- return normalizePlayStatus(response);
1203
1242
  }
1204
1243
  /**
1205
1244
  * Fetch persisted logs for a run using the public runs resource model.
@@ -1356,11 +1395,11 @@ var DeeplineClient = class {
1356
1395
  // Plays — high-level orchestration
1357
1396
  // ——————————————————————————————————————————————————————————
1358
1397
  /**
1359
- * Run a play end-to-end: submit, poll until terminal, return result.
1398
+ * Run a play end-to-end: submit, stream until terminal, return result.
1360
1399
  *
1361
1400
  * This is the highest-level play execution method. It submits the play,
1362
- * polls for status updates, and returns a structured result with logs
1363
- * and timing. Supports cancellation via `AbortSignal`.
1401
+ * reads the canonical run stream for status updates, and returns a structured
1402
+ * result with logs and timing. Supports cancellation via `AbortSignal`.
1364
1403
  *
1365
1404
  * @param code - Source string fallback; pass the bundled artifact in `options.artifact`
1366
1405
  * @param csvPath - Input CSV path, or `null`
@@ -1376,7 +1415,6 @@ var DeeplineClient = class {
1376
1415
  * const logs = status.progress?.logs ?? [];
1377
1416
  * console.log(`[${status.status}] ${logs.length} log lines`);
1378
1417
  * },
1379
- * pollIntervalMs: 1000,
1380
1418
  * });
1381
1419
  *
1382
1420
  * if (result.success) {
@@ -1407,33 +1445,53 @@ var DeeplineClient = class {
1407
1445
  packagedFiles: options?.packagedFiles,
1408
1446
  force: options?.force
1409
1447
  });
1410
- const pollInterval = options?.pollIntervalMs ?? 500;
1411
1448
  const start = Date.now();
1412
- while (true) {
1449
+ const state = {
1450
+ runId: workflowId,
1451
+ status: "running",
1452
+ logs: [],
1453
+ latest: null
1454
+ };
1455
+ if (options?.signal?.aborted) {
1456
+ await this.cancelPlay(workflowId);
1457
+ return {
1458
+ success: false,
1459
+ runId: workflowId,
1460
+ logs: [],
1461
+ durationMs: Date.now() - start,
1462
+ error: "Cancelled by user"
1463
+ };
1464
+ }
1465
+ for await (const event of this.streamPlayRunEvents(workflowId, {
1466
+ mode: "cli",
1467
+ signal: options?.signal
1468
+ })) {
1413
1469
  if (options?.signal?.aborted) {
1414
1470
  await this.cancelPlay(workflowId);
1415
1471
  return {
1416
1472
  success: false,
1417
1473
  runId: workflowId,
1418
- logs: [],
1474
+ logs: state.logs,
1419
1475
  durationMs: Date.now() - start,
1420
1476
  error: "Cancelled by user"
1421
1477
  };
1422
1478
  }
1423
- const status = await this.getPlayStatus(workflowId);
1479
+ const status = updatePlayLiveStatusState(state, event);
1480
+ if (!status) {
1481
+ continue;
1482
+ }
1424
1483
  options?.onProgress?.(status);
1425
1484
  if (TERMINAL_PLAY_STATUSES.has(status.status)) {
1426
- return {
1427
- success: status.status === "completed",
1428
- runId: status.runId || workflowId,
1429
- result: status.result,
1430
- logs: status.progress?.logs ?? [],
1431
- durationMs: Date.now() - start,
1432
- error: status.progress?.error ?? (status.status !== "completed" ? status.status : void 0)
1433
- };
1485
+ const finalStatus = await this.getPlayStatus(status.runId || workflowId).catch(() => status);
1486
+ return playRunResultFromStatus(finalStatus, start, workflowId);
1434
1487
  }
1435
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
1436
1488
  }
1489
+ throw new DeeplineError(
1490
+ `Run stream for ${workflowId} ended before the run reached a terminal state.`,
1491
+ void 0,
1492
+ "PLAY_RUN_STREAM_ENDED",
1493
+ { runId: workflowId, workflowId }
1494
+ );
1437
1495
  }
1438
1496
  // ——————————————————————————————————————————————————————————
1439
1497
  // Health
@@ -524,6 +524,26 @@ type WorkflowRunOutput = {
524
524
  durationMs: number;
525
525
  };
526
526
 
527
+ type LiveNodeProgressSnapshot = {
528
+ completed?: number;
529
+ total?: number;
530
+ failed?: number;
531
+ message?: string;
532
+ updatedAt?: number;
533
+ startedAt?: number;
534
+ completedAt?: number;
535
+ artifactTableNamespace?: string | null;
536
+ };
537
+
538
+ type LiveNodeProgressMap = Record<string, LiveNodeProgressSnapshot>;
539
+
540
+ type WorkerCtxCallbacks = {
541
+ onNodeProgress?: (input: {
542
+ nodeId: string;
543
+ progress: LiveNodeProgressSnapshot;
544
+ }) => void;
545
+ };
546
+
527
547
  function nowMs(): number {
528
548
  return Date.now();
529
549
  }
@@ -1092,6 +1112,29 @@ async function callToolDirect(
1092
1112
  let lastError: Error | null = null;
1093
1113
 
1094
1114
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1115
+ if (toolId === 'test_transient_500' || toolId === 'test_transient_429') {
1116
+ const syntheticResult = executeSyntheticTransientRetry(
1117
+ toolId,
1118
+ input,
1119
+ attempt,
1120
+ );
1121
+ if (syntheticResult.ok) {
1122
+ return wrapWorkerToolResult(
1123
+ toolId,
1124
+ syntheticResult.result,
1125
+ syntheticToolMetadata(toolId),
1126
+ );
1127
+ }
1128
+ lastError = new Error(
1129
+ `tool ${toolId} ${syntheticResult.status} attempt ${attempt}/${maxAttempts}: ${syntheticResult.message}`,
1130
+ );
1131
+ if (attempt >= maxAttempts) {
1132
+ throw lastError;
1133
+ }
1134
+ await new Promise((resolve) => setTimeout(resolve, 1_000));
1135
+ continue;
1136
+ }
1137
+
1095
1138
  const res = await fetchRuntimeApi(req.baseUrl, path, {
1096
1139
  method: 'POST',
1097
1140
  headers: {
@@ -1277,6 +1320,41 @@ async function executeSyntheticTestRateLimitBatch(
1277
1320
  };
1278
1321
  }
1279
1322
 
1323
+ type SyntheticTransientRetryResult =
1324
+ | { ok: true; result: Record<string, unknown> }
1325
+ | { ok: false; status: number; message: string };
1326
+
1327
+ function executeSyntheticTransientRetry(
1328
+ toolId: string,
1329
+ input: Record<string, unknown>,
1330
+ attempt: number,
1331
+ ): SyntheticTransientRetryResult {
1332
+ const failuresBeforeSuccess =
1333
+ typeof input.failures_before_success === 'number' &&
1334
+ Number.isInteger(input.failures_before_success) &&
1335
+ input.failures_before_success >= 0
1336
+ ? input.failures_before_success
1337
+ : 1;
1338
+ if (attempt <= failuresBeforeSuccess) {
1339
+ const status = toolId === 'test_transient_429' ? 429 : 502;
1340
+ return {
1341
+ ok: false,
1342
+ status,
1343
+ message: `Synthetic transient ${status} for attempt ${attempt}`,
1344
+ };
1345
+ }
1346
+ return {
1347
+ ok: true,
1348
+ result: {
1349
+ status: 'completed',
1350
+ provider: 'test',
1351
+ key: String(input.key ?? 'transient'),
1352
+ attempts: attempt,
1353
+ recovered: attempt > 1,
1354
+ },
1355
+ };
1356
+ }
1357
+
1280
1358
  function executeSyntheticTestRateLimit(
1281
1359
  input: Record<string, unknown>,
1282
1360
  ): Record<string, unknown> {
@@ -2561,6 +2639,7 @@ function createMinimalWorkerCtx(
2561
2639
  env: WorkerEnv,
2562
2640
  workflowStep?: WorkflowStep,
2563
2641
  abortSignal?: AbortSignal,
2642
+ callbacks?: WorkerCtxCallbacks,
2564
2643
  ): unknown {
2565
2644
  let playCallCount = 0;
2566
2645
  const parentChildCalls: Record<string, number> = {};
@@ -2635,6 +2714,7 @@ function createMinimalWorkerCtx(
2635
2714
  opts?: WorkerMapOptions,
2636
2715
  ): Promise<unknown> => {
2637
2716
  const mapStartedAt = nowMs();
2717
+ const mapNodeId = `map:${name}`;
2638
2718
  const sliced = rows;
2639
2719
  const baseOffset = 0;
2640
2720
  const fieldEntries = Object.entries(fieldsDef);
@@ -2655,6 +2735,30 @@ function createMinimalWorkerCtx(
2655
2735
  softWorkflowStepBudget: plan?.chunkPlan.softWorkflowStepBudget,
2656
2736
  });
2657
2737
  const outputFields = fieldEntries.map(([field]) => field);
2738
+ const updateMapProgress = (progress: LiveNodeProgressSnapshot) => {
2739
+ callbacks?.onNodeProgress?.({
2740
+ nodeId: mapNodeId,
2741
+ progress: {
2742
+ artifactTableNamespace: name,
2743
+ failed: 0,
2744
+ ...progress,
2745
+ updatedAt: progress.updatedAt ?? nowMs(),
2746
+ },
2747
+ });
2748
+ };
2749
+ const formatMapProgressMessage = (completed: number, total?: number) =>
2750
+ typeof total === 'number' && Number.isFinite(total) && total > 0
2751
+ ? `${completed.toLocaleString()} / ${total.toLocaleString()} rows processed`
2752
+ : `${completed.toLocaleString()} rows processed`;
2753
+ updateMapProgress({
2754
+ completed: 0,
2755
+ total: streaming ? undefined : sliced.length,
2756
+ startedAt: mapStartedAt,
2757
+ message: formatMapProgressMessage(
2758
+ 0,
2759
+ streaming ? undefined : sliced.length,
2760
+ ),
2761
+ });
2658
2762
  const explicitRowKeysSeen =
2659
2763
  opts?.key === undefined ? null : new Map<string, number>();
2660
2764
  const resolveExplicitKeyValue = (
@@ -3156,6 +3260,14 @@ function createMinimalWorkerCtx(
3156
3260
  `Map completed: ${totalRowsWritten} results ` +
3157
3261
  `(${totalRowsExecuted} executed, ${totalRowsCached} already satisfied) ` +
3158
3262
  `inserted=${totalRowsInserted} skipped=${totalRowsSkipped}`;
3263
+ const completedAt = nowMs();
3264
+ updateMapProgress({
3265
+ completed: totalRowsWritten,
3266
+ total: totalRowsWritten,
3267
+ completedAt,
3268
+ updatedAt: completedAt,
3269
+ message: formatMapProgressMessage(totalRowsWritten, totalRowsWritten),
3270
+ });
3159
3271
  emitEvent({
3160
3272
  type: 'log',
3161
3273
  level: 'info',
@@ -3199,6 +3311,10 @@ function createMinimalWorkerCtx(
3199
3311
  totalRowsDuplicateReused += chunkResult.rowsDuplicateReused;
3200
3312
  totalRowsInserted += chunkResult.rowsInserted;
3201
3313
  totalRowsSkipped += chunkResult.rowsSkipped;
3314
+ updateMapProgress({
3315
+ completed: totalRowsWritten,
3316
+ message: formatMapProgressMessage(totalRowsWritten),
3317
+ });
3202
3318
  if (out.length < 10) {
3203
3319
  out.push(...chunkResult.preview.slice(0, 10 - out.length));
3204
3320
  }
@@ -3230,6 +3346,11 @@ function createMinimalWorkerCtx(
3230
3346
  totalRowsDuplicateReused += chunkResult.rowsDuplicateReused;
3231
3347
  totalRowsInserted += chunkResult.rowsInserted;
3232
3348
  totalRowsSkipped += chunkResult.rowsSkipped;
3349
+ updateMapProgress({
3350
+ completed: totalRowsWritten,
3351
+ total: sliced.length,
3352
+ message: formatMapProgressMessage(totalRowsWritten, sliced.length),
3353
+ });
3233
3354
  if (out.length < 10) {
3234
3355
  out.push(...chunkResult.preview.slice(0, 10 - out.length));
3235
3356
  }
@@ -3252,6 +3373,11 @@ function createMinimalWorkerCtx(
3252
3373
  totalRowsInserted = chunkResult.rowsInserted;
3253
3374
  totalRowsSkipped = chunkResult.rowsSkipped;
3254
3375
  out.push(...chunkResult.preview);
3376
+ updateMapProgress({
3377
+ completed: chunkResult.rowsWritten,
3378
+ total: sliced.length,
3379
+ message: formatMapProgressMessage(chunkResult.rowsWritten, sliced.length),
3380
+ });
3255
3381
  const dataset = finalize(chunkResult.rowsWritten);
3256
3382
  recordRunnerPerfTrace({
3257
3383
  req,
@@ -3919,6 +4045,7 @@ async function executeRunRequest(
3919
4045
  // sees the final terminal status with no intermediate logs/progress.
3920
4046
  let liveLogs: string[] = [];
3921
4047
  let liveLogsDirty = false;
4048
+ let liveNodeProgress: LiveNodeProgressMap = {};
3922
4049
  let lastLiveLogFlushAt =
3923
4050
  nowMs() - LIVE_LOG_FLUSH_INTERVAL_MS + LIVE_LOG_FIRST_FLUSH_DELAY_MS;
3924
4051
  let liveLogFlushInFlight: Promise<void> = Promise.resolve();
@@ -3928,6 +4055,21 @@ async function executeRunRequest(
3928
4055
  liveLogs = [...liveLogs, trimmed].slice(-LIVE_LOG_BUFFER_LIMIT);
3929
4056
  liveLogsDirty = true;
3930
4057
  };
4058
+ const updateLiveNodeProgress = (input: {
4059
+ nodeId: string;
4060
+ progress: LiveNodeProgressSnapshot;
4061
+ }) => {
4062
+ const nodeId = input.nodeId.trim();
4063
+ if (!nodeId) return;
4064
+ liveNodeProgress = {
4065
+ ...liveNodeProgress,
4066
+ [nodeId]: {
4067
+ ...(liveNodeProgress[nodeId] ?? {}),
4068
+ ...input.progress,
4069
+ },
4070
+ };
4071
+ };
4072
+ const liveNodeProgressSnapshot = () => ({ ...liveNodeProgress });
3931
4073
  const flushLiveLogs = (force: boolean): void => {
3932
4074
  if (!options?.persistResultDatasets) return;
3933
4075
  if (!liveLogsDirty && !force) return;
@@ -3946,6 +4088,7 @@ async function executeRunRequest(
3946
4088
  status: 'running',
3947
4089
  runtimeBackend: 'cf_workflows_dynamic_worker',
3948
4090
  liveLogs: snapshot,
4091
+ liveNodeProgress: liveNodeProgressSnapshot(),
3949
4092
  lastCheckpointAt: now,
3950
4093
  });
3951
4094
  } catch {
@@ -3975,6 +4118,7 @@ async function executeRunRequest(
3975
4118
  env,
3976
4119
  workflowStep,
3977
4120
  abortSignal,
4121
+ { onNodeProgress: updateLiveNodeProgress },
3978
4122
  );
3979
4123
  try {
3980
4124
  const playStartedAt = nowMs();
@@ -4017,12 +4161,14 @@ async function executeRunRequest(
4017
4161
  action: 'update_run_status',
4018
4162
  playId: req.runId,
4019
4163
  status: 'completed',
4164
+ error: null,
4020
4165
  result: terminalResult,
4021
4166
  runtimeBackend: 'cf_workflows_dynamic_worker',
4022
4167
  waitKind: null,
4023
4168
  waitUntil: null,
4024
4169
  activeBoundaryId: null,
4025
4170
  liveLogs,
4171
+ liveNodeProgress: liveNodeProgressSnapshot(),
4026
4172
  lastCheckpointAt: nowMs(),
4027
4173
  });
4028
4174
  recordRunnerPerfTrace({
@@ -4092,6 +4238,7 @@ async function executeRunRequest(
4092
4238
  waitUntil: null,
4093
4239
  activeBoundaryId: null,
4094
4240
  liveLogs,
4241
+ liveNodeProgress: liveNodeProgressSnapshot(),
4095
4242
  lastCheckpointAt: nowMs(),
4096
4243
  });
4097
4244
  await finalizeWorkerComputeBilling({