deepline 0.1.211 → 0.1.213

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 (54) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +317 -340
  2. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/customer-console.ts +23 -0
  3. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +1 -1
  4. package/dist/bundling-sources/sdk/src/client.ts +118 -3
  5. package/dist/bundling-sources/sdk/src/index.ts +2 -0
  6. package/dist/bundling-sources/sdk/src/play.ts +8 -1
  7. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  8. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +11 -1
  9. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  10. package/dist/bundling-sources/sdk/src/types.ts +47 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +302 -30
  12. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-placement.ts +14 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +57 -23
  14. package/dist/bundling-sources/shared_libs/play-runtime/completed-receipt-cache.ts +166 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +164 -121
  16. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +42 -1
  18. package/dist/bundling-sources/shared_libs/play-runtime/dataset-id.ts +10 -4
  19. package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +9 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +163 -7
  21. package/dist/bundling-sources/shared_libs/play-runtime/gateway-auth-session.ts +93 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +14 -2
  23. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +4 -2
  24. package/dist/bundling-sources/shared_libs/play-runtime/play-call-execution.ts +1 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +16 -1
  26. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +21 -1
  27. package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +10 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +80 -3
  29. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +128 -6
  30. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +7 -0
  31. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +94 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +91 -6
  33. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +19 -10
  34. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +540 -92
  35. package/dist/bundling-sources/shared_libs/play-runtime/runtime-postgres-admission.ts +162 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +18 -4
  37. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +16 -17
  38. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +29 -9
  39. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +22 -1
  40. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +1 -0
  41. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +79 -1
  42. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -4
  43. package/dist/cli/index.js +637 -229
  44. package/dist/cli/index.mjs +637 -229
  45. package/dist/{compiler-manifest-j6z8qzpd.d.mts → compiler-manifest-BhgZ23A4.d.mts} +1 -0
  46. package/dist/{compiler-manifest-j6z8qzpd.d.ts → compiler-manifest-BhgZ23A4.d.ts} +1 -0
  47. package/dist/index.d.mts +84 -11
  48. package/dist/index.d.ts +84 -11
  49. package/dist/index.js +225 -43
  50. package/dist/index.mjs +225 -43
  51. package/dist/plays/bundle-play-file.d.mts +2 -2
  52. package/dist/plays/bundle-play-file.d.ts +2 -2
  53. package/dist/plays/bundle-play-file.mjs +65 -4
  54. package/package.json +1 -1
@@ -66,6 +66,22 @@ export type RuntimeStatusUpdate = {
66
66
  * positional cursor stay correct once the buffer has rotated.
67
67
  */
68
68
  liveLogTotalCount?: number;
69
+ /**
70
+ * Monotonic scheduler attempt that owns the positional log channel. A new
71
+ * attempt starts at offset zero without colliding with earlier attempts.
72
+ */
73
+ liveLogProducerAttempt?: number;
74
+ datasetLifecycleEvents?: Array<{
75
+ datasetId: string;
76
+ path: string;
77
+ tableNamespace: string;
78
+ phase: 'registered' | 'available' | 'failed';
79
+ persistedRows?: number;
80
+ succeededRows?: number;
81
+ failedRows?: number;
82
+ complete?: boolean;
83
+ at: number;
84
+ }>;
69
85
  /**
70
86
  * Explicit terminal-output replay for final runner logs when the caller must
71
87
  * keep `status` nonterminal. Direct-worker/Postgres progress finalizers do this so
@@ -75,6 +91,8 @@ export type RuntimeStatusUpdate = {
75
91
  terminalLogReplay?: {
76
92
  lines: string[];
77
93
  totalCount?: number;
94
+ dedupeMode?: 'semantic' | 'exact';
95
+ producerAttempt?: number;
78
96
  } | null;
79
97
  liveTimeline?: PlayRunTimelineEntry[];
80
98
  liveNodeProgress?: PlayVisualNodeProgressMap;
@@ -189,11 +207,27 @@ type RuntimeApiRequest =
189
207
  runId: string;
190
208
  key: string;
191
209
  }
210
+ | {
211
+ action: 'acquire_runtime_receipt_execution_lock';
212
+ playName: string;
213
+ runId: string;
214
+ key: string;
215
+ ownerExecutionId: string;
216
+ ttlMs: number;
217
+ }
218
+ | {
219
+ action: 'release_runtime_receipt_execution_lock';
220
+ playName: string;
221
+ runId: string;
222
+ key: string;
223
+ ownerExecutionId: string;
224
+ }
192
225
  | {
193
226
  action: 'claim_runtime_step_receipt';
194
227
  playName: string;
195
228
  runId: string;
196
229
  key: string;
230
+ leaseId?: string;
197
231
  runAttempt?: number | null;
198
232
  leaseAware?: boolean;
199
233
  reclaimRunning?: boolean;
@@ -247,6 +281,7 @@ type RuntimeApiRequest =
247
281
  playName: string;
248
282
  runId: string;
249
283
  keys: string[];
284
+ leaseIds?: string[];
250
285
  runAttempt?: number | null;
251
286
  leaseAware?: boolean;
252
287
  reclaimRunning?: boolean;
@@ -347,12 +382,84 @@ function applyRetryJitter(delayMs: number): number {
347
382
  const APP_RUNTIME_API_DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
348
383
  const RUN_STATUS_LEDGER_SNAPSHOT_CACHE_LIMIT = 1_000;
349
384
  const runStatusLedgerSnapshots = new Map<string, PlayRunLedgerSnapshot>();
350
- // Positional log cursor per run: count of liveLogs lines already forwarded to
351
- // the ledger from this process. Process-local on purpose a fresh process
352
- // re-sends from offset 0 and Run Log Stream ingestion skips the overlap
353
- // positionally (exactly-once on the channel, repeated identical lines kept).
385
+ // Positional log cursor per run attempt: count of liveLogs lines already
386
+ // forwarded from this process. A fresh process re-sends from offset 0; durable
387
+ // ingestion skips overlap within that same attempt.
354
388
  const runLogChannelSentCounts = new Map<string, number>();
355
389
  const runStatusUpdateChains = new Map<string, Promise<void>>();
390
+ const APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES = 128 * 1024;
391
+ const APP_RUNTIME_EVENT_BATCH_MAX_JSON_BYTES = 512 * 1024;
392
+ const appRuntimeTextEncoder = new TextEncoder();
393
+
394
+ function splitRunEventForAppRuntime(
395
+ event: PlayRunLedgerEvent,
396
+ ): PlayRunLedgerEvent[] {
397
+ if (event.type !== 'log.appended' || event.lines.length === 0) return [event];
398
+ const split: PlayRunLedgerEvent[] = [];
399
+ let index = 0;
400
+ while (index < event.lines.length) {
401
+ const start = index;
402
+ let bytes = 2;
403
+ while (index < event.lines.length) {
404
+ const lineBytes = appRuntimeTextEncoder.encode(
405
+ JSON.stringify(event.lines[index]!),
406
+ ).length;
407
+ if (
408
+ index > start &&
409
+ bytes + lineBytes + 1 > APP_RUNTIME_LOG_EVENT_MAX_JSON_BYTES
410
+ ) {
411
+ break;
412
+ }
413
+ bytes += lineBytes + 1;
414
+ index += 1;
415
+ }
416
+ split.push({
417
+ ...event,
418
+ lines: event.lines.slice(start, index),
419
+ ...(typeof event.channelOffset === 'number'
420
+ ? { channelOffset: event.channelOffset + start }
421
+ : {}),
422
+ });
423
+ }
424
+ return split;
425
+ }
426
+
427
+ export function partitionRunEventsForAppRuntime(
428
+ events: readonly PlayRunLedgerEvent[],
429
+ ): PlayRunLedgerEvent[][] {
430
+ const batches: PlayRunLedgerEvent[][] = [];
431
+ let current: PlayRunLedgerEvent[] = [];
432
+ let currentBytes = 2;
433
+ for (const event of events.flatMap(splitRunEventForAppRuntime)) {
434
+ const eventBytes = appRuntimeTextEncoder.encode(
435
+ JSON.stringify(event),
436
+ ).length;
437
+ if (
438
+ current.length > 0 &&
439
+ currentBytes + eventBytes + 1 > APP_RUNTIME_EVENT_BATCH_MAX_JSON_BYTES
440
+ ) {
441
+ batches.push(current);
442
+ current = [];
443
+ currentBytes = 2;
444
+ }
445
+ current.push(event);
446
+ currentBytes += eventBytes + 1;
447
+ }
448
+ if (current.length > 0) batches.push(current);
449
+ return batches;
450
+ }
451
+
452
+ async function appendRunEventBatchesViaAppRuntime(
453
+ context: WorkerRuntimeApiContext,
454
+ input: { playId: string; events: readonly PlayRunLedgerEvent[] },
455
+ ): Promise<void> {
456
+ for (const events of partitionRunEventsForAppRuntime(input.events)) {
457
+ await appendRunEventsViaAppRuntime(context, {
458
+ playId: input.playId,
459
+ events,
460
+ });
461
+ }
462
+ }
356
463
 
357
464
  function isTerminalRuntimeStatus(status: string): boolean {
358
465
  return (
@@ -364,6 +471,10 @@ function isTerminalRuntimeStatus(status: string): boolean {
364
471
  );
365
472
  }
366
473
 
474
+ function runLogChannelKey(runId: string, producerAttempt?: number): string {
475
+ return `${runId}:${producerAttempt ?? 'legacy'}`;
476
+ }
477
+
367
478
  function rememberRunStatusLedgerSnapshot(
368
479
  runId: string,
369
480
  snapshot: PlayRunLedgerSnapshot,
@@ -376,7 +487,11 @@ function rememberRunStatusLedgerSnapshot(
376
487
  const oldestRunId = runStatusLedgerSnapshots.keys().next().value;
377
488
  if (!oldestRunId) break;
378
489
  runStatusLedgerSnapshots.delete(oldestRunId);
379
- runLogChannelSentCounts.delete(oldestRunId);
490
+ for (const key of runLogChannelSentCounts.keys()) {
491
+ if (key.startsWith(`${oldestRunId}:`)) {
492
+ runLogChannelSentCounts.delete(key);
493
+ }
494
+ }
380
495
  runStatusUpdateChains.delete(oldestRunId);
381
496
  }
382
497
  }
@@ -399,12 +514,16 @@ function isRetryableAppRuntimeResponse(input: {
399
514
  if (input.status === 500 && isTransientAppRuntimeFailureBody(input.body)) {
400
515
  return true;
401
516
  }
517
+ return isRetryableAppRuntimeResponseStatus(input.status);
518
+ }
519
+
520
+ function isRetryableAppRuntimeResponseStatus(status: number): boolean {
402
521
  return (
403
- input.status === 429 ||
404
- (input.status >= 520 && input.status <= 524) ||
405
- input.status === 502 ||
406
- input.status === 503 ||
407
- input.status === 504
522
+ status === 429 ||
523
+ (status >= 520 && status <= 524) ||
524
+ status === 502 ||
525
+ status === 503 ||
526
+ status === 504
408
527
  );
409
528
  }
410
529
 
@@ -601,6 +720,38 @@ function sleep(ms: number): Promise<void> {
601
720
  return new Promise((resolve) => setTimeout(resolve, ms));
602
721
  }
603
722
 
723
+ async function retryAppRuntimeBodyTimeoutOrThrow(input: {
724
+ action: RuntimeApiRequest['action'];
725
+ attempt: number;
726
+ maxAttempts: number;
727
+ error: unknown;
728
+ retryAfterMs?: number | null;
729
+ }): Promise<void> {
730
+ if (!isRequestTimeoutAbort(input.error)) {
731
+ throw input.error;
732
+ }
733
+ if (
734
+ input.attempt < input.maxAttempts &&
735
+ isRetryableAppRuntimeFetchError({
736
+ action: input.action,
737
+ error: input.error,
738
+ })
739
+ ) {
740
+ await sleep(
741
+ Math.max(
742
+ applyRetryJitter(appRuntimeRetryDelayMs(input.action, input.attempt)),
743
+ input.retryAfterMs ?? 0,
744
+ ),
745
+ );
746
+ return;
747
+ }
748
+ throw new AppRuntimeApiTransportError({
749
+ action: input.action,
750
+ attempts: input.attempt,
751
+ cause: input.error,
752
+ });
753
+ }
754
+
604
755
  export class AppRuntimeApiTransportError extends Error {
605
756
  readonly action: RuntimeApiRequest['action'];
606
757
  readonly attempts: number;
@@ -691,9 +842,40 @@ async function postAppRuntimeApi<TResponse>(
691
842
  });
692
843
  }
693
844
  if (response.ok) {
694
- return (await response.json()) as TResponse;
845
+ try {
846
+ return (await response.json()) as TResponse;
847
+ } catch (error) {
848
+ await retryAppRuntimeBodyTimeoutOrThrow({
849
+ action: body.action,
850
+ attempt,
851
+ maxAttempts,
852
+ error,
853
+ });
854
+ continue;
855
+ }
856
+ }
857
+ let responseText: string;
858
+ try {
859
+ responseText = await response.text();
860
+ } catch (error) {
861
+ if (
862
+ isRequestTimeoutAbort(error) &&
863
+ !isRetryableAppRuntimeResponseStatus(response.status)
864
+ ) {
865
+ throw new Error(
866
+ `App runtime API ${body.action} failed with status ${response.status}: response body timed out`,
867
+ { cause: error },
868
+ );
869
+ }
870
+ await retryAppRuntimeBodyTimeoutOrThrow({
871
+ action: body.action,
872
+ attempt,
873
+ maxAttempts,
874
+ error,
875
+ retryAfterMs: appRuntimeRetryAfterMs(response, ''),
876
+ });
877
+ continue;
695
878
  }
696
- const responseText = await response.text();
697
879
  if (
698
880
  attempt < maxAttempts &&
699
881
  isRetryableAppRuntimeResponse({
@@ -917,7 +1099,9 @@ async function updateRunStatusViaAppRuntimeUnlocked(
917
1099
  // durable stream can recover any progress flush that was lost in transit.
918
1100
  const sentCount = isTerminalRuntimeStatus(update.status)
919
1101
  ? 0
920
- : (runLogChannelSentCounts.get(update.playId) ?? 0);
1102
+ : (runLogChannelSentCounts.get(
1103
+ runLogChannelKey(update.playId, update.liveLogProducerAttempt),
1104
+ ) ?? 0);
921
1105
  const logSlice = Array.isArray(update.liveLogs)
922
1106
  ? slicePositionalLogLines({
923
1107
  bufferLines: update.liveLogs,
@@ -932,6 +1116,7 @@ async function updateRunStatusViaAppRuntimeUnlocked(
932
1116
  ? {
933
1117
  lines: update.liveLogs,
934
1118
  totalCount: update.liveLogTotalCount,
1119
+ producerAttempt: update.liveLogProducerAttempt,
935
1120
  }
936
1121
  : null;
937
1122
  const statusPatchLogSlice = terminalLogReplay ? null : logSlice;
@@ -944,6 +1129,7 @@ async function updateRunStatusViaAppRuntimeUnlocked(
944
1129
  lastCheckpointAt: update.lastCheckpointAt ?? null,
945
1130
  liveLogs: statusPatchLogSlice?.lines ?? null,
946
1131
  liveLogsChannelOffset: statusPatchLogSlice?.channelOffset ?? null,
1132
+ liveLogsProducerAttempt: update.liveLogProducerAttempt ?? null,
947
1133
  liveNodeProgress: update.liveNodeProgress ?? null,
948
1134
  result: update.result,
949
1135
  },
@@ -951,33 +1137,87 @@ async function updateRunStatusViaAppRuntimeUnlocked(
951
1137
  now: update.lastCheckpointAt ?? Date.now(),
952
1138
  source: 'worker',
953
1139
  });
954
- if (terminalLogReplay && terminalLogReplay.lines.length > 0) {
955
- events.push(
956
- ...buildTerminalLogReplayEvents({
957
- runId: update.playId,
958
- source: 'worker',
959
- occurredAt: update.lastCheckpointAt ?? Date.now(),
960
- lines: terminalLogReplay.lines,
961
- liveLogTotalCount: terminalLogReplay.totalCount,
962
- }),
963
- );
1140
+ for (const event of update.datasetLifecycleEvents ?? []) {
1141
+ const current = previousSnapshot.datasetsById[event.datasetId];
1142
+ const changesSnapshot =
1143
+ !current ||
1144
+ current.path !== event.path ||
1145
+ current.tableNamespace !== event.tableNamespace ||
1146
+ current.phase !== event.phase ||
1147
+ (event.persistedRows ?? 0) > current.persistedRows ||
1148
+ (event.succeededRows ?? 0) > current.succeededRows ||
1149
+ (event.failedRows ?? 0) > current.failedRows ||
1150
+ (event.complete === true && current.complete !== true);
1151
+ if (!changesSnapshot) continue;
1152
+ events.push({
1153
+ type: 'dataset.lifecycle',
1154
+ runId: update.playId,
1155
+ source: 'worker',
1156
+ occurredAt: event.at,
1157
+ datasetId: event.datasetId,
1158
+ path: event.path,
1159
+ tableNamespace: event.tableNamespace,
1160
+ phase: event.phase,
1161
+ ...(event.persistedRows === undefined
1162
+ ? {}
1163
+ : { persistedRows: event.persistedRows }),
1164
+ ...(event.succeededRows === undefined
1165
+ ? {}
1166
+ : { succeededRows: event.succeededRows }),
1167
+ ...(event.failedRows === undefined
1168
+ ? {}
1169
+ : { failedRows: event.failedRows }),
1170
+ ...(event.complete === undefined ? {} : { complete: event.complete }),
1171
+ });
964
1172
  }
965
- if (events.length === 0) {
1173
+ const terminalLogEvents =
1174
+ terminalLogReplay && terminalLogReplay.lines.length > 0
1175
+ ? buildTerminalLogReplayEvents({
1176
+ runId: update.playId,
1177
+ source: 'worker',
1178
+ occurredAt: update.lastCheckpointAt ?? Date.now(),
1179
+ lines: terminalLogReplay.lines,
1180
+ liveLogTotalCount: terminalLogReplay.totalCount,
1181
+ dedupeMode: terminalLogReplay.dedupeMode,
1182
+ producerAttempt: terminalLogReplay.producerAttempt,
1183
+ })
1184
+ : [];
1185
+ if (events.length === 0 && terminalLogEvents.length === 0) {
966
1186
  return;
967
1187
  }
968
- await appendRunEventsViaAppRuntime(context, {
969
- playId: update.playId,
970
- events,
971
- });
1188
+ const combinedEvents = [...events, ...terminalLogEvents];
1189
+ if (partitionRunEventsForAppRuntime(combinedEvents).length === 1) {
1190
+ // Preserve the single-mutation fast path for ordinary bounded updates.
1191
+ await appendRunEventsViaAppRuntime(context, {
1192
+ playId: update.playId,
1193
+ events: combinedEvents,
1194
+ });
1195
+ } else {
1196
+ // Persist terminal lifecycle first so an oversized or interrupted
1197
+ // best-effort log heal can never prevent settlement. Each bounded log
1198
+ // batch is a separate mutation; retries are safe because ingestion owns
1199
+ // positional reconciliation.
1200
+ await appendRunEventBatchesViaAppRuntime(context, {
1201
+ playId: update.playId,
1202
+ events,
1203
+ });
1204
+ await appendRunEventBatchesViaAppRuntime(context, {
1205
+ playId: update.playId,
1206
+ events: terminalLogEvents,
1207
+ });
1208
+ }
972
1209
  if (statusPatchLogSlice) {
973
1210
  runLogChannelSentCounts.set(
974
- update.playId,
1211
+ runLogChannelKey(update.playId, update.liveLogProducerAttempt),
975
1212
  statusPatchLogSlice.channelOffset + statusPatchLogSlice.lines.length,
976
1213
  );
977
1214
  }
978
1215
  rememberRunStatusLedgerSnapshot(
979
1216
  update.playId,
980
- reducePlayRunLedgerEvents(previousSnapshot, events),
1217
+ reducePlayRunLedgerEvents(previousSnapshot, [
1218
+ ...events,
1219
+ ...terminalLogEvents,
1220
+ ]),
981
1221
  );
982
1222
  }
983
1223
 
@@ -1109,6 +1349,38 @@ export async function getRuntimeStepReceiptViaAppRuntime(
1109
1349
  });
1110
1350
  }
1111
1351
 
1352
+ export async function acquireRuntimeReceiptExecutionLockViaAppRuntime(
1353
+ context: WorkerRuntimeApiContext,
1354
+ input: Omit<
1355
+ Extract<
1356
+ RuntimeApiRequest,
1357
+ { action: 'acquire_runtime_receipt_execution_lock' }
1358
+ >,
1359
+ 'action'
1360
+ >,
1361
+ ): Promise<{ ownerExecutionId: string; expiresAt: string } | null> {
1362
+ return await postAppRuntimeApi(context, {
1363
+ action: 'acquire_runtime_receipt_execution_lock',
1364
+ ...input,
1365
+ });
1366
+ }
1367
+
1368
+ export async function releaseRuntimeReceiptExecutionLockViaAppRuntime(
1369
+ context: WorkerRuntimeApiContext,
1370
+ input: Omit<
1371
+ Extract<
1372
+ RuntimeApiRequest,
1373
+ { action: 'release_runtime_receipt_execution_lock' }
1374
+ >,
1375
+ 'action'
1376
+ >,
1377
+ ): Promise<boolean> {
1378
+ return await postAppRuntimeApi(context, {
1379
+ action: 'release_runtime_receipt_execution_lock',
1380
+ ...input,
1381
+ });
1382
+ }
1383
+
1112
1384
  export async function getRuntimeStepReceiptsViaAppRuntime(
1113
1385
  context: WorkerRuntimeApiContext,
1114
1386
  input: Omit<
@@ -0,0 +1,14 @@
1
+ import type { PlayCallExecution } from './play-call-execution';
2
+
3
+ export function resolveChildExecutionPlacement(input: {
4
+ requiresWorkflow: boolean;
5
+ execution?: PlayCallExecution | null;
6
+ }): { strategy: 'inline' | 'scheduled' } {
7
+ return {
8
+ strategy:
9
+ input.execution === 'child-workflow' ||
10
+ (input.execution !== 'inline' && input.requiresWorkflow)
11
+ ? 'scheduled'
12
+ : 'inline',
13
+ };
14
+ }
@@ -2,6 +2,8 @@ import {
2
2
  flattenStaticPipeline,
3
3
  type PlayStaticPipeline,
4
4
  } from '../plays/static-pipeline';
5
+ import type { PlayCallExecution } from './play-call-execution';
6
+ import { resolveChildExecutionPlacement } from './child-execution-placement';
5
7
 
6
8
  export type ChildExecutionStrategy = 'inline' | 'scheduled';
7
9
 
@@ -12,7 +14,9 @@ export type ChildExecutionDecision = {
12
14
  | 'missing_static_contract'
13
15
  | 'dataset_child'
14
16
  | 'event_wait_child'
15
- | 'explicit_timeout';
17
+ | 'explicit_timeout'
18
+ | 'explicit_inline'
19
+ | 'explicit_child_workflow';
16
20
  };
17
21
 
18
22
  /**
@@ -23,31 +27,61 @@ export type ChildExecutionDecision = {
23
27
  export function resolveChildExecutionStrategy(input: {
24
28
  pipeline: PlayStaticPipeline | null | undefined;
25
29
  timeoutMs?: number | null;
30
+ execution?: PlayCallExecution | null;
31
+ childPlayName?: string | null;
26
32
  }): ChildExecutionDecision {
27
- if (input.timeoutMs != null) {
28
- return { strategy: 'scheduled', reason: 'explicit_timeout' };
33
+ if (input.execution === 'child-workflow') {
34
+ return { strategy: 'scheduled', reason: 'explicit_child_workflow' };
29
35
  }
30
- if (!input.pipeline) {
31
- return { strategy: 'scheduled', reason: 'missing_static_contract' };
36
+ const steps = input.pipeline ? flattenStaticPipeline(input.pipeline) : [];
37
+ const authoredDecision: ChildExecutionDecision =
38
+ input.timeoutMs != null
39
+ ? { strategy: 'scheduled', reason: 'explicit_timeout' }
40
+ : !input.pipeline
41
+ ? { strategy: 'scheduled', reason: 'missing_static_contract' }
42
+ : input.pipeline.tableNamespace ||
43
+ input.pipeline.sheetContract ||
44
+ steps.some((step) => step.type === 'dataset' || step.type === 'csv')
45
+ ? { strategy: 'scheduled', reason: 'dataset_child' }
46
+ : steps.some(
47
+ (step) =>
48
+ step.type === 'tool' &&
49
+ (step.isEventWait || step.toolId === 'test_wait_for_event'),
50
+ )
51
+ ? { strategy: 'scheduled', reason: 'event_wait_child' }
52
+ : { strategy: 'inline', reason: 'scalar_child' };
53
+ if (input.execution === 'inline') {
54
+ if (
55
+ authoredDecision.strategy === 'scheduled' &&
56
+ authoredDecision.reason !== 'missing_static_contract'
57
+ ) {
58
+ throw new Error(
59
+ inlineExecutionRequiresScalarChildMessage(
60
+ input.childPlayName?.trim() || 'child',
61
+ authoredDecision.reason,
62
+ ),
63
+ );
64
+ }
65
+ return { strategy: 'inline', reason: 'explicit_inline' };
32
66
  }
33
- const substeps = flattenStaticPipeline(input.pipeline);
34
- if (
35
- input.pipeline.tableNamespace ||
36
- input.pipeline.sheetContract ||
37
- substeps.some((step) => step.type === 'dataset' || step.type === 'csv')
38
- ) {
39
- return { strategy: 'scheduled', reason: 'dataset_child' };
67
+ const placement = resolveChildExecutionPlacement({
68
+ requiresWorkflow: authoredDecision.strategy === 'scheduled',
69
+ execution: input.execution,
70
+ });
71
+ if (placement.strategy === 'inline') {
72
+ return { strategy: 'inline', reason: 'scalar_child' };
40
73
  }
41
- if (
42
- substeps.some(
43
- (step) =>
44
- step.type === 'tool' &&
45
- (step.isEventWait === true || step.toolId === 'test_wait_for_event'),
46
- )
47
- ) {
48
- return { strategy: 'scheduled', reason: 'event_wait_child' };
49
- }
50
- return { strategy: 'inline', reason: 'scalar_child' };
74
+ return authoredDecision;
75
+ }
76
+
77
+ export function inlineExecutionRequiresScalarChildMessage(
78
+ childPlayName: string,
79
+ reason: ChildExecutionDecision['reason'],
80
+ ): string {
81
+ return (
82
+ `ctx.runPlay("${childPlayName}") requested execution: "inline", but the child requires a child workflow ` +
83
+ `(${reason}). Remove the explicit execution option or use execution: "child-workflow".`
84
+ );
51
85
  }
52
86
 
53
87
  /**
@@ -63,7 +97,7 @@ export function inlineChildScheduledInRowMessage(
63
97
  return (
64
98
  `ctx.runPlay("${childPlayName}") runs per row in the "${rowContextField}" batch, ` +
65
99
  'but it is a scheduling-required child (it uses ctx.dataset()/ctx.csv(), waits on an ' +
66
- 'event, or sets an explicit timeout). Row-scoped children must be scalar. ' +
100
+ 'event, sets an explicit timeout, or explicitly requests a child workflow). Row-scoped children must be scalar. ' +
67
101
  'Give the batch ONE dataset child at play level instead of fanning a scheduled run out per row.'
68
102
  );
69
103
  }