deepline 0.1.10 → 0.1.12

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 (34) hide show
  1. package/README.md +4 -4
  2. package/dist/cli/index.js +509 -353
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/cli/index.mjs +513 -358
  5. package/dist/cli/index.mjs.map +1 -1
  6. package/dist/index.d.mts +250 -305
  7. package/dist/index.d.ts +250 -305
  8. package/dist/index.js +174 -286
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +174 -285
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +23 -13
  13. package/dist/repo/apps/play-runner-workers/src/entry.ts +581 -1220
  14. package/dist/repo/sdk/src/cli/commands/play.ts +381 -247
  15. package/dist/repo/sdk/src/cli/commands/tools.ts +1 -1
  16. package/dist/repo/sdk/src/cli/dataset-stats.ts +86 -12
  17. package/dist/repo/sdk/src/client.ts +54 -51
  18. package/dist/repo/sdk/src/index.ts +7 -16
  19. package/dist/repo/sdk/src/play.ts +122 -135
  20. package/dist/repo/sdk/src/plays/bundle-play-file.ts +6 -3
  21. package/dist/repo/sdk/src/tool-output.ts +0 -111
  22. package/dist/repo/sdk/src/types.ts +2 -0
  23. package/dist/repo/sdk/src/version.ts +1 -1
  24. package/dist/repo/sdk/src/worker-play-entry.ts +3 -0
  25. package/dist/repo/shared_libs/play-runtime/context.ts +510 -267
  26. package/dist/repo/shared_libs/play-runtime/csv-rename.ts +180 -0
  27. package/dist/repo/shared_libs/play-runtime/ctx-types.ts +13 -1
  28. package/dist/repo/shared_libs/play-runtime/tool-result.ts +139 -114
  29. package/dist/repo/shared_libs/plays/bundling/index.ts +68 -5
  30. package/dist/repo/shared_libs/plays/compiler-manifest.ts +1 -1
  31. package/dist/repo/shared_libs/plays/dataset.ts +1 -1
  32. package/dist/repo/shared_libs/plays/runtime-validation.ts +8 -28
  33. package/package.json +1 -1
  34. package/dist/repo/apps/play-runner-workers/src/runtime/tool-result.ts +0 -184
@@ -2,9 +2,9 @@
2
2
  * PlayContextImpl — the cloud execution engine.
3
3
  *
4
4
  * Batching model:
5
- * 1. ctx.map("table_key", rows, { key: "row_id" }).step("field", resolver) starts all row field resolvers concurrently
5
+ * 1. ctx.map("table_key", rows, { field: resolver }) starts all row field resolvers concurrently
6
6
  * 2. ctx.waterfall() calls inside field resolvers QUEUE requests (don't execute)
7
- * 3. ctx.tools.execute({ id, tool, input }) calls inside field resolvers also QUEUE
7
+ * 3. ctx.tools.execute() calls inside field resolvers also QUEUE
8
8
  * 4. After all rows have queued, executeBatchedWaterfalls() runs provider-by-provider
9
9
  * 5. Each provider batch = real HTTP call to /api/v2/integrations/{toolId}/execute
10
10
  * 6. Results resolve suspended row promises, rows complete
@@ -26,7 +26,6 @@ import {
26
26
  resolveWaterfallToolId,
27
27
  } from './batch-runtime';
28
28
  import { PlayRateLimitScheduler } from '@shared_libs/plays/rate-limit-scheduler';
29
- import { normalizePlayToolResult } from './result-normalization';
30
29
  import {
31
30
  cloneToolExecuteResultWithExecution,
32
31
  createToolExecuteResult,
@@ -38,19 +37,12 @@ import { sqlSafePlayColumnName } from '@shared_libs/plays/static-pipeline';
38
37
  import { createRuntimeDatasetId } from './dataset-id';
39
38
  import {
40
39
  derivePlayRowIdentity,
40
+ derivePlayRowIdentityFromKey,
41
41
  MAP_KEY_NAMESPACE_MAX_LENGTH,
42
42
  normalizeTableNamespace,
43
43
  resolveStaleMapTableNamespace,
44
44
  } from '@shared_libs/plays/row-identity';
45
- import {
46
- assertUniqueExplicitMapKeys,
47
- createExplicitMapKeyResolver,
48
- deriveMapRowIdentity,
49
- MapRowIdentity,
50
- } from './map-row-identity';
51
- import { MapExecutionFrameStore } from './map-execution-frame';
52
- import { PlayProgressEmitter } from './progress-emitter';
53
- import { WaterfallReplayStore } from './waterfall-replay';
45
+ import { cloneCsvAliasedRow, stripCsvProjectedFields } from './csv-rename';
54
46
  import { setSpanAttributes, withActiveSpan } from './tracing';
55
47
  import { DISALLOWED_RUN_JAVASCRIPT_TOOL_MESSAGE } from './runtime-constraints';
56
48
  import {
@@ -64,12 +56,9 @@ import type {
64
56
  WaterfallRequest,
65
57
  WaterfallOptions,
66
58
  InlineWaterfallSpec,
67
- MapDefinitionOptions,
68
59
  MapOptions,
69
- MapRunOptions,
70
60
  ToolCallRequest,
71
61
  ToolBatchResult,
72
- ToolExecutionRequest,
73
62
  ContextOptions,
74
63
  PlayCallOptions,
75
64
  PlayCheckpoint,
@@ -78,6 +67,7 @@ import type {
78
67
  PlayRowUpdate,
79
68
  MapFieldDefinition,
80
69
  MapFieldResolver,
70
+ ToolCallOptions,
81
71
  PlayExecutionGovernanceLimits,
82
72
  PlayExecutionGovernanceState,
83
73
  ResolvedPlayExecution,
@@ -111,10 +101,6 @@ const PURE_JS_HEARTBEAT_ROW_INTERVAL = 250;
111
101
  const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
112
102
  const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
113
103
  const TOOL_TRANSIENT_HTTP_MAX_ATTEMPTS = 3;
114
- const TRANSIENT_HTTP_RETRY_SAFE_TOOL_IDS = new Set([
115
- 'test_company_search',
116
- 'test_transient_500',
117
- ]);
118
104
  const EXECUTE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
119
105
  const IN_MEMORY_STEP_RESULT_PREVIEW_LIMIT = 25;
120
106
  const WATERFALL_ROW_MATCH_LOG_SAMPLE_LIMIT = 10;
@@ -169,16 +155,6 @@ function isRuntimeConditionalStepResolver(
169
155
  );
170
156
  }
171
157
 
172
- function isMapDefinitionOptions(
173
- value: unknown,
174
- ): value is Omit<MapOptions, 'description'> {
175
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
176
- return false;
177
- }
178
- const keys = Object.keys(value);
179
- return keys.every((key) => key === 'key' || key === 'staleAfterSeconds');
180
- }
181
-
182
158
  class RuntimeMapBuilder<T extends Record<string, unknown>> {
183
159
  private readonly program: RuntimeStepProgram = {
184
160
  kind: 'steps',
@@ -189,7 +165,6 @@ class RuntimeMapBuilder<T extends Record<string, unknown>> {
189
165
  private readonly ctx: PlayContextImpl,
190
166
  private readonly key: string,
191
167
  private readonly items: PlayDatasetInput<T>,
192
- private readonly mapOptions?: MapDefinitionOptions<T>,
193
168
  ) {}
194
169
 
195
170
  step(name: string, resolver: RuntimeStepProgramStep['resolver']): this {
@@ -202,17 +177,13 @@ class RuntimeMapBuilder<T extends Record<string, unknown>> {
202
177
  return this;
203
178
  }
204
179
 
205
- run(options?: MapRunOptions): Promise<PlayDataset<Record<string, unknown>>> {
206
- if (
207
- options &&
208
- Object.keys(options).some((optionKey) => optionKey !== 'description')
209
- ) {
210
- throw new Error('ctx.map(...).run() only accepts description.');
211
- }
212
- return this.ctx.runStepProgramMap(this.key, this.items, this.program, {
213
- ...this.mapOptions,
214
- ...options,
215
- } as MapOptions<T>);
180
+ run(options?: MapOptions<T>): Promise<PlayDataset<Record<string, unknown>>> {
181
+ return this.ctx.runStepProgramMap(
182
+ this.key,
183
+ this.items,
184
+ this.program,
185
+ options,
186
+ );
216
187
  }
217
188
  }
218
189
 
@@ -258,17 +229,6 @@ function parseRetryAfterMs(header: string | null): number {
258
229
  return TOOL_RETRY_AFTER_FALLBACK_MS;
259
230
  }
260
231
 
261
- function isRetryableTransientToolHttpStatus(
262
- toolId: string,
263
- status: number,
264
- ): boolean {
265
- return (
266
- status >= 500 &&
267
- status < 600 &&
268
- TRANSIENT_HTTP_RETRY_SAFE_TOOL_IDS.has(toolId)
269
- );
270
- }
271
-
272
232
  function parseJsonOrNull(bodyText: string): unknown | null {
273
233
  if (!bodyText.trim()) return null;
274
234
  try {
@@ -302,22 +262,18 @@ function parseExecuteToolMetadata(
302
262
  const readGetters = (value: unknown): Record<string, readonly string[]> => {
303
263
  if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
304
264
  return Object.fromEntries(
305
- Object.entries(value as Record<string, unknown>).flatMap(
306
- ([key, paths]) => {
307
- if (!Array.isArray(paths)) return [];
308
- const normalized = paths.filter(
309
- (path): path is string =>
310
- typeof path === 'string' && path.trim().length > 0,
311
- );
312
- return normalized.length > 0 ? [[key, normalized]] : [];
313
- },
314
- ),
265
+ Object.entries(value as Record<string, unknown>).flatMap(([key, paths]) => {
266
+ if (!Array.isArray(paths)) return [];
267
+ const normalized = paths.filter(
268
+ (path): path is string => typeof path === 'string' && path.trim().length > 0,
269
+ );
270
+ return normalized.length > 0 ? [[key, normalized]] : [];
271
+ }),
315
272
  );
316
273
  };
317
274
  const listExtractorPaths = Array.isArray(record.listExtractorPaths)
318
275
  ? record.listExtractorPaths.filter(
319
- (path): path is string =>
320
- typeof path === 'string' && path.trim().length > 0,
276
+ (path): path is string => typeof path === 'string' && path.trim().length > 0,
321
277
  )
322
278
  : [];
323
279
  return {
@@ -359,6 +315,14 @@ function emptyCheckpoint(): PlayCheckpoint {
359
315
  };
360
316
  }
361
317
 
318
+ function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
319
+ return {
320
+ ...frame,
321
+ completedRowKeys: [...frame.completedRowKeys],
322
+ pendingRowKeys: [...frame.pendingRowKeys],
323
+ };
324
+ }
325
+
362
326
  function createDefaultGovernanceState(
363
327
  options: ContextOptions,
364
328
  ): PlayExecutionGovernanceState {
@@ -482,7 +446,7 @@ export class PlayContextImpl {
482
446
  private resolvers = new Map<string, (value: unknown) => void>();
483
447
  private waterfallQueue = new Map<string, WaterfallRequest[]>();
484
448
  private toolCallQueue: ToolCallRequest[] = [];
485
- private toolCallResolvers = new Map<string, Array<(value: unknown) => void>>();
449
+ private toolCallResolvers = new Map<string, (value: unknown) => void>();
486
450
  private options: ContextOptions;
487
451
  private logBuffer: string[] = [];
488
452
  private checkpoint: PlayCheckpoint;
@@ -503,22 +467,56 @@ export class PlayContextImpl {
503
467
  private directToolCallIndex = 0;
504
468
  private sleepBoundaryIndex = 0;
505
469
  private fetchCallIndex = 0;
470
+ private mapInvocationIndex = 0;
506
471
  private readonly stepCallIndexByKey = new Map<string, number>();
507
472
  private readonly waterfallMatchLogCounts = new Map<string, number>();
508
473
  private readonly rateLimitScheduler: PlayRateLimitScheduler;
509
474
  private readonly governance: PlayExecutionGovernanceState;
510
- private readonly mapRowIdentity = new MapRowIdentity();
511
- private readonly mapFrames: MapExecutionFrameStore;
512
- private readonly progress: PlayProgressEmitter;
513
- private readonly waterfallReplay: WaterfallReplayStore;
514
475
  readonly tools = {
515
476
  execute: <TOutput = unknown>(
516
- request: ToolExecutionRequest,
517
- ): Promise<TOutput> => this.executeTool(request) as Promise<TOutput>,
477
+ requestOrKey:
478
+ | {
479
+ id: string;
480
+ tool: string;
481
+ input: Record<string, unknown>;
482
+ description?: string;
483
+ }
484
+ | string,
485
+ toolId?: string,
486
+ input?: Record<string, unknown>,
487
+ options?: ToolCallOptions,
488
+ ): Promise<TOutput> => {
489
+ if (typeof requestOrKey === 'object') {
490
+ return this.executeTool(
491
+ requestOrKey.id,
492
+ requestOrKey.tool,
493
+ requestOrKey.input,
494
+ requestOrKey.description
495
+ ? { description: requestOrKey.description }
496
+ : undefined,
497
+ ) as Promise<TOutput>;
498
+ }
499
+ if (!toolId || !input) {
500
+ throw new Error(
501
+ 'ctx.tools.execute(key, toolId, input) requires a tool ID and input.',
502
+ );
503
+ }
504
+ return this.executeTool(
505
+ requestOrKey,
506
+ toolId,
507
+ input,
508
+ options,
509
+ ) as Promise<TOutput>;
510
+ },
518
511
  };
519
512
 
520
- tool<TOutput = unknown>(request: ToolExecutionRequest): Promise<TOutput> {
521
- return this.tools.execute<TOutput>(request);
513
+ tool<TOutput = unknown>(
514
+ key: string,
515
+ toolId: string,
516
+ input: Record<string, unknown>,
517
+ options?: ToolCallOptions,
518
+ ): Promise<TOutput> {
519
+ return this.tools.execute<TOutput>(key, toolId, input, options);
522
520
  }
523
521
 
524
522
  constructor(options: ContextOptions) {
@@ -527,16 +525,6 @@ export class PlayContextImpl {
527
525
  getQueueHints: options.getToolQueueHints,
528
526
  });
529
527
  this.checkpoint = options.checkpoint ?? emptyCheckpoint();
530
- this.mapFrames = new MapExecutionFrameStore(this.checkpoint, (event) =>
531
- this.emitExecutionEvent(event),
532
- );
533
- this.progress = new PlayProgressEmitter(
534
- options.onRowUpdate,
535
- options.onExecutionEvent,
536
- () => rowContext.getStore() ?? null,
537
- isInlineWaterfallToolStep,
538
- );
539
- this.waterfallReplay = new WaterfallReplayStore(this.checkpoint);
540
528
  this.governance = {
541
529
  ...(options.governance ?? createDefaultGovernanceState(options)),
542
530
  inFlightPlayCallsByPlayId:
@@ -557,14 +545,73 @@ export class PlayContextImpl {
557
545
  tableNamespace: string | null,
558
546
  update: Omit<PlayRowUpdate, 'key'>,
559
547
  ): void {
560
- this.progress.rowUpdate(key, tableNamespace, update);
548
+ const rowScope = rowContext.getStore()?.mapScope;
549
+ if (rowScope && key) {
550
+ this.emitExecutionEvent({
551
+ type: 'map.row.updated',
552
+ mapInvocationId: rowScope.mapInvocationId,
553
+ mapNodeId: rowScope.mapNodeId ?? null,
554
+ logicalNamespace: rowScope.logicalNamespace,
555
+ artifactTableNamespace: rowScope.artifactTableNamespace,
556
+ rowKey: key,
557
+ rowStatus: update.status,
558
+ fieldName: rowContext.getStore()?.fieldName ?? null,
559
+ stage: update.stage ?? null,
560
+ provider: update.provider ?? null,
561
+ at: Date.now(),
562
+ });
563
+ }
564
+ if (!key || !this.options.onRowUpdate) {
565
+ return;
566
+ }
567
+ void this.options.onRowUpdate({
568
+ ...update,
569
+ key,
570
+ tableNamespace,
571
+ });
561
572
  }
562
573
 
563
574
  private emitExecutionEvent(event: PlayExecutionEvent): void {
564
575
  if (!this.options.onExecutionEvent) {
565
576
  return;
566
577
  }
567
- this.progress.executionEvent(event);
578
+ void this.options.onExecutionEvent(event);
579
+ }
580
+
581
+ private setMapFrame(frame: MapExecutionFrame): void {
582
+ this.checkpoint.mapFrames = {
583
+ ...(this.checkpoint.mapFrames ?? {}),
584
+ [frame.mapInvocationId]: cloneMapFrame(frame),
585
+ };
586
+ }
587
+
588
+ private createMapExecutionScope(input: {
589
+ logicalNamespace: string;
590
+ artifactTableNamespace: string;
591
+ mapNodeId?: string | null;
592
+ explicitKey?:
593
+ | ((row: Record<string, unknown>, index: number) => string)
594
+ | null;
595
+ }): MapExecutionScope {
596
+ const mapInvocationId = `${input.logicalNamespace}:${this.mapInvocationIndex}`;
597
+ this.mapInvocationIndex += 1;
598
+ const explicitKey = input.explicitKey ?? null;
599
+ return {
600
+ mapInvocationId,
601
+ mapNodeId: input.mapNodeId ?? null,
602
+ logicalNamespace: input.logicalNamespace,
603
+ artifactTableNamespace: input.artifactTableNamespace,
604
+ rowIdentity: (row, index) =>
605
+ explicitKey
606
+ ? derivePlayRowIdentityFromKey(
607
+ explicitKey(row, index ?? 0),
608
+ input.artifactTableNamespace,
609
+ )
610
+ : derivePlayRowIdentity(
611
+ stripCsvProjectedFields(row),
612
+ input.artifactTableNamespace,
613
+ ),
614
+ };
568
615
  }
569
616
 
570
617
  private normalizeContextKey(key: string, operation: string): string {
@@ -743,7 +790,35 @@ export class PlayContextImpl {
743
790
  reused?: boolean;
744
791
  dataPatch?: Record<string, unknown>;
745
792
  }): void {
746
- this.progress.fieldMetaUpdate(input);
793
+ if (!input.fieldName) {
794
+ this.emitScopedRowUpdate(input.key, input.tableNamespace, {
795
+ rowId: input.rowId,
796
+ status: input.rowStatus,
797
+ stage: input.stage ?? null,
798
+ provider: input.provider ?? null,
799
+ error: input.error ?? null,
800
+ dataPatch: input.dataPatch ?? {},
801
+ });
802
+ return;
803
+ }
804
+
805
+ this.emitScopedRowUpdate(input.key, input.tableNamespace, {
806
+ rowId: input.rowId,
807
+ status: input.rowStatus,
808
+ stage: input.stage ?? null,
809
+ provider: input.provider ?? null,
810
+ error: input.error ?? null,
811
+ dataPatch: input.dataPatch ?? {},
812
+ cellMetaPatch: {
813
+ [input.fieldName]: {
814
+ status: input.status,
815
+ stage: input.stage ?? null,
816
+ provider: input.provider ?? null,
817
+ error: input.error ?? null,
818
+ ...(input.reused !== undefined ? { reused: input.reused } : {}),
819
+ },
820
+ },
821
+ });
747
822
  }
748
823
 
749
824
  private emitCellUpdate(input: {
@@ -765,7 +840,23 @@ export class PlayContextImpl {
765
840
  error?: string | null;
766
841
  value?: unknown;
767
842
  }): void {
768
- this.progress.cellUpdate(input);
843
+ this.emitScopedRowUpdate(input.key, input.tableNamespace, {
844
+ rowId: input.rowId,
845
+ status: input.rowStatus,
846
+ stage: input.stage ?? null,
847
+ provider: input.provider ?? null,
848
+ error: input.error ?? null,
849
+ dataPatch:
850
+ input.value === undefined ? {} : { [input.columnName]: input.value },
851
+ cellMetaPatch: {
852
+ [input.columnName]: {
853
+ status: input.status,
854
+ stage: input.stage ?? null,
855
+ provider: input.provider ?? null,
856
+ error: input.error ?? null,
857
+ },
858
+ },
859
+ });
769
860
  }
770
861
 
771
862
  private emitQueuedInlineWaterfallSteps(
@@ -774,12 +865,18 @@ export class PlayContextImpl {
774
865
  tableNamespace: string | null,
775
866
  spec: InlineWaterfallSpec,
776
867
  ): void {
777
- this.progress.queuedInlineWaterfallSteps({
778
- rowId,
779
- key,
780
- tableNamespace,
781
- spec,
782
- });
868
+ for (const step of spec.steps) {
869
+ this.emitCellUpdate({
870
+ rowId,
871
+ key,
872
+ tableNamespace,
873
+ columnName: sqlSafePlayColumnName(`${spec.id}.${step.id}`),
874
+ status: 'queued',
875
+ stage: step.id,
876
+ provider: isInlineWaterfallToolStep(step) ? step.toolId : 'code',
877
+ value: null,
878
+ });
879
+ }
783
880
  }
784
881
 
785
882
  private isCompletedFieldValue(value: unknown): boolean {
@@ -1038,8 +1135,7 @@ export class PlayContextImpl {
1038
1135
  return createToolExecuteResult({
1039
1136
  status: input.status,
1040
1137
  result: input.result,
1041
- metadata:
1042
- input.metadata ?? (await this.resolveToolResultMetadata(input.toolId)),
1138
+ metadata: input.metadata ?? (await this.resolveToolResultMetadata(input.toolId)),
1043
1139
  execution: {
1044
1140
  idempotent: true,
1045
1141
  cached: input.cached,
@@ -1072,11 +1168,9 @@ export class PlayContextImpl {
1072
1168
  });
1073
1169
  this.cacheToolResult(toolId, cacheKey, wrapped);
1074
1170
 
1075
- const resolvers = this.toolCallResolvers.get(request.callId);
1076
- if (resolvers) {
1077
- for (const resolver of resolvers) {
1078
- resolver(wrapped);
1079
- }
1171
+ const resolver = this.toolCallResolvers.get(request.callId);
1172
+ if (resolver) {
1173
+ resolver(wrapped);
1080
1174
  this.toolCallResolvers.delete(request.callId);
1081
1175
  }
1082
1176
 
@@ -1124,11 +1218,6 @@ export class PlayContextImpl {
1124
1218
  );
1125
1219
  }
1126
1220
 
1127
- map<T extends Record<string, unknown>>(
1128
- key: string,
1129
- items: PlayDatasetInput<T>,
1130
- options: MapDefinitionOptions<T>,
1131
- ): RuntimeMapBuilder<T>;
1132
1221
  map<T extends Record<string, unknown>>(
1133
1222
  key: string,
1134
1223
  items: PlayDatasetInput<T>,
@@ -1145,10 +1234,7 @@ export class PlayContextImpl {
1145
1234
  >(
1146
1235
  key: string,
1147
1236
  items: PlayDatasetInput<T>,
1148
- input?:
1149
- | MapFieldDefinition<T, TColumns>
1150
- | RuntimeStepProgram
1151
- | MapDefinitionOptions<T>,
1237
+ input?: MapFieldDefinition<T, TColumns> | RuntimeStepProgram,
1152
1238
  options?: MapOptions<T>,
1153
1239
  ): RuntimeMapBuilder<T> | Promise<PlayDataset<Record<string, unknown>>> {
1154
1240
  if (rowContext.getStore()) {
@@ -1158,20 +1244,17 @@ export class PlayContextImpl {
1158
1244
  }
1159
1245
  this.assertNoDuplicateConcurrentMapBackedPlay();
1160
1246
 
1161
- if (input === undefined || isMapDefinitionOptions(input)) {
1162
- return new RuntimeMapBuilder(
1163
- this,
1164
- key,
1165
- items,
1166
- input as MapDefinitionOptions<T> | undefined,
1167
- );
1247
+ if (input === undefined) {
1248
+ return new RuntimeMapBuilder(this, key, items);
1168
1249
  }
1169
1250
 
1170
1251
  if (isRuntimeStepProgram(input)) {
1171
1252
  return this.runStepProgramMap(key, items, input, options);
1172
1253
  }
1173
1254
 
1174
- throw new Error('ctx.map() accepts key, rows, and map options only.');
1255
+ throw new Error(
1256
+ 'ctx.map(key, rows, fields, options) was removed. Use ctx.map(key, rows).step(...).run(options).',
1257
+ );
1175
1258
  }
1176
1259
 
1177
1260
  async runStepProgramMap<T extends Record<string, unknown>>(
@@ -1213,7 +1296,7 @@ export class PlayContextImpl {
1213
1296
  } catch (error) {
1214
1297
  throw new Error(
1215
1298
  error instanceof Error
1216
- ? `${error.message} Example: ctx.map(\"${normalizedMapNamespace}\", rows, { key: \"stable_id\", staleAfterSeconds: 86400 }).step("company", row => row.domain).run({ description: "..." }).`
1299
+ ? `${error.message} Example: ctx.map(\"${normalizedMapNamespace}\", rows, { company: ... }, { staleAfterSeconds: 86400 }).`
1217
1300
  : `ctx.map() key must normalize to <= ${MAP_KEY_NAMESPACE_MAX_LENGTH} characters.`,
1218
1301
  );
1219
1302
  }
@@ -1234,23 +1317,66 @@ export class PlayContextImpl {
1234
1317
  let itemsToProcess: Array<Record<string, unknown>> = [];
1235
1318
  let completedItemsByKey: Map<string, Record<string, unknown>> | null = null;
1236
1319
  const mapFieldNames = Object.keys(input);
1237
- const userKeyFn = options?.key;
1238
- const explicitKeyResolver = createExplicitMapKeyResolver({
1239
- mapNamespace: normalizedMapNamespace,
1240
- fieldNames: mapFieldNames,
1241
- key: userKeyFn as Parameters<
1242
- typeof createExplicitMapKeyResolver
1243
- >[0]['key'],
1244
- });
1320
+ const stripFieldOutputs = (row: Record<string, unknown>) =>
1321
+ Object.fromEntries(
1322
+ Object.entries(row).filter(
1323
+ ([fieldName]) => !mapFieldNames.includes(fieldName),
1324
+ ),
1325
+ );
1326
+
1327
+ const userKeyOption = options?.key;
1328
+ let explicitKeyResolver:
1329
+ | ((row: Record<string, unknown>, index: number) => string)
1330
+ | null = null;
1331
+ if (userKeyOption !== undefined) {
1332
+ explicitKeyResolver = (row, index) => {
1333
+ const inputRow = stripFieldOutputs(row);
1334
+ const raw =
1335
+ typeof userKeyOption === 'function'
1336
+ ? (
1337
+ userKeyOption as (
1338
+ row: Record<string, unknown>,
1339
+ index: number,
1340
+ ) => string | number | readonly unknown[]
1341
+ )(inputRow, index)
1342
+ : Array.isArray(userKeyOption)
1343
+ ? userKeyOption.map((fieldName) => inputRow[fieldName])
1344
+ : inputRow[String(userKeyOption)];
1345
+ const parts = Array.isArray(raw) ? raw : [raw];
1346
+ if (parts.some((part) => part === null || part === undefined)) {
1347
+ throw new Error(
1348
+ `ctx.map("${normalizedMapNamespace}") key returned null or undefined for row ${index}. ` +
1349
+ 'Return a non-empty string or number derived from a stable input column (e.g. row.email ?? row.domain).',
1350
+ );
1351
+ }
1352
+ const normalizedParts = parts.map((part) => {
1353
+ if (typeof part === 'number') {
1354
+ return Number.isFinite(part) ? String(part) : '';
1355
+ }
1356
+ return String(part).trim();
1357
+ });
1358
+ if (normalizedParts.some((part) => !part)) {
1359
+ throw new Error(
1360
+ `ctx.map("${normalizedMapNamespace}") key returned an empty value for row ${index}. ` +
1361
+ 'Return a non-empty string or finite number derived from a stable input column.',
1362
+ );
1363
+ }
1364
+ return normalizedParts.length === 1
1365
+ ? normalizedParts[0]!
1366
+ : JSON.stringify(normalizedParts);
1367
+ };
1368
+ }
1245
1369
 
1246
1370
  const rowIdentity = (row: Record<string, unknown>, index = 0) =>
1247
- deriveMapRowIdentity({
1248
- row,
1249
- index,
1250
- artifactTableNamespace: resolvedTableNamespace,
1251
- fieldNames: mapFieldNames,
1252
- explicitKey: explicitKeyResolver,
1253
- });
1371
+ explicitKeyResolver
1372
+ ? derivePlayRowIdentityFromKey(
1373
+ explicitKeyResolver(row, index),
1374
+ resolvedTableNamespace,
1375
+ )
1376
+ : derivePlayRowIdentity(
1377
+ stripCsvProjectedFields(stripFieldOutputs(row)),
1378
+ resolvedTableNamespace,
1379
+ );
1254
1380
 
1255
1381
  const materializedItems = await materializePlayDatasetInput(items);
1256
1382
  totalInputCount = materializedItems.length;
@@ -1259,14 +1385,29 @@ export class PlayContextImpl {
1259
1385
  this.toOutputRow(item as Record<string, unknown>),
1260
1386
  );
1261
1387
 
1262
- assertUniqueExplicitMapKeys({
1263
- mapNamespace: normalizedMapNamespace,
1264
- rows: rawItems,
1265
- resolver: explicitKeyResolver,
1266
- });
1388
+ if (explicitKeyResolver) {
1389
+ const seenKeys = new Map<string, number>();
1390
+ for (let index = 0; index < rawItems.length; index += 1) {
1391
+ const keyValue = explicitKeyResolver(rawItems[index]!, index);
1392
+ const previousIndex = seenKeys.get(keyValue);
1393
+ if (previousIndex !== undefined) {
1394
+ throw new Error(
1395
+ `ctx.map("${normalizedMapNamespace}") key function produced duplicate value "${keyValue}" for rows ${previousIndex} and ${index}. ` +
1396
+ 'Each row must produce a unique key. Combine columns (e.g. `${row.email}|${row.company}`) or pick a column that is unique per row.',
1397
+ );
1398
+ }
1399
+ seenKeys.set(keyValue, index);
1400
+ }
1401
+ }
1267
1402
  if (this.options.onMapStart) {
1403
+ const mapStartRows = explicitKeyResolver
1404
+ ? rawItems.map((row, index) => ({
1405
+ ...row,
1406
+ __deeplineRowKey: rowIdentity(row, index),
1407
+ }))
1408
+ : rawItems;
1268
1409
  const mapStartResult = await this.options.onMapStart(
1269
- rawItems,
1410
+ mapStartRows,
1270
1411
  resolvedTableNamespace,
1271
1412
  {
1272
1413
  playName: this.options.playName,
@@ -1278,16 +1419,30 @@ export class PlayContextImpl {
1278
1419
  resolvedTableNamespace = normalizeTableNamespace(
1279
1420
  mapStartResult.tableNamespace,
1280
1421
  );
1422
+ const persistedRowIdentity = (
1423
+ row: Record<string, unknown>,
1424
+ index = 0,
1425
+ ) =>
1426
+ typeof row.__deeplineRowKey === 'string'
1427
+ ? row.__deeplineRowKey
1428
+ : rowIdentity(row, index);
1281
1429
  const pendingKeys = new Set(
1282
- mapStartResult.pendingRows.map((row) => rowIdentity(row)),
1430
+ mapStartResult.pendingRows.map((row, index) =>
1431
+ persistedRowIdentity(row, index),
1432
+ ),
1283
1433
  );
1284
1434
  itemsToProcess = materializedItems
1285
- .map((item) => this.toOutputRow(item as Record<string, unknown>))
1286
- .filter((row) => pendingKeys.has(rowIdentity(row)));
1435
+ .map((item, index) => ({
1436
+ row: this.toOutputRow(item as Record<string, unknown>),
1437
+ index,
1438
+ }))
1439
+ .filter(({ row, index }) => pendingKeys.has(rowIdentity(row, index)))
1440
+ .map(({ row }) => row);
1287
1441
  if (mapStartResult.completedRows.length > 0) {
1288
1442
  completedItemsByKey = new Map();
1289
- for (const row of mapStartResult.completedRows) {
1290
- const rowKey = rowIdentity(row);
1443
+ for (let index = 0; index < mapStartResult.completedRows.length; index += 1) {
1444
+ const row = mapStartResult.completedRows[index]!;
1445
+ const rowKey = persistedRowIdentity(row, index);
1291
1446
  if (rowKey) completedItemsByKey.set(rowKey, row);
1292
1447
  }
1293
1448
  this.log(
@@ -1296,22 +1451,38 @@ export class PlayContextImpl {
1296
1451
  }
1297
1452
  }
1298
1453
 
1299
- const mapScope = this.mapRowIdentity.createScope({
1454
+ const mapScope = this.createMapExecutionScope({
1300
1455
  logicalNamespace: normalizedMapNamespace,
1301
1456
  artifactTableNamespace: resolvedTableNamespace,
1302
1457
  explicitKey: explicitKeyResolver,
1303
- fieldNames: mapFieldNames,
1304
1458
  });
1305
1459
  const completedRowKeys =
1306
1460
  completedItemsByKey != null ? [...completedItemsByKey.keys()] : [];
1307
- const pendingRowKeys = itemsToProcess.map((item) =>
1308
- mapScope.rowIdentity(this.toOutputRow(item)),
1461
+ const pendingRowKeys = itemsToProcess.map((item, index) =>
1462
+ mapScope.rowIdentity(this.toOutputRow(item), index),
1309
1463
  );
1310
- this.mapFrames.start({
1311
- scope: mapScope,
1464
+ this.setMapFrame({
1465
+ mapInvocationId: mapScope.mapInvocationId,
1466
+ mapNodeId: mapScope.mapNodeId ?? null,
1467
+ logicalNamespace: mapScope.logicalNamespace,
1468
+ artifactTableNamespace: mapScope.artifactTableNamespace,
1469
+ status: 'running',
1312
1470
  totalRows: totalInputCount,
1313
1471
  completedRowKeys,
1314
1472
  pendingRowKeys,
1473
+ startedAt: Date.now(),
1474
+ updatedAt: Date.now(),
1475
+ });
1476
+ this.emitExecutionEvent({
1477
+ type: 'map.started',
1478
+ mapInvocationId: mapScope.mapInvocationId,
1479
+ mapNodeId: mapScope.mapNodeId ?? null,
1480
+ logicalNamespace: mapScope.logicalNamespace,
1481
+ artifactTableNamespace: mapScope.artifactTableNamespace,
1482
+ totalRows: totalInputCount,
1483
+ completedRows: completedRowKeys.length,
1484
+ pendingRows: pendingRowKeys.length,
1485
+ at: Date.now(),
1315
1486
  });
1316
1487
 
1317
1488
  const mapResults = await this.runFieldMap(
@@ -1334,19 +1505,19 @@ export class PlayContextImpl {
1334
1505
  const rowIdentityRow = this.toOutputRow(
1335
1506
  itemsToProcess[index] as Record<string, unknown>,
1336
1507
  );
1337
- const rowKey = rowIdentity(rowIdentityRow);
1508
+ const rowKey = rowIdentity(rowIdentityRow, index);
1338
1509
  if (rowKey) resultsByKey.set(rowKey, row);
1339
1510
  }
1340
- return rawItems.map((rawItem) => {
1341
- const rowKey = rowIdentity(rawItem);
1342
- return (
1511
+ return rawItems.map((rawItem, index) => {
1512
+ const rowKey = rowIdentity(rawItem, index);
1513
+ return this.toPublicOutputRow(
1343
1514
  resultsByKey.get(rowKey) ??
1344
- completedItemsByKey!.get(rowKey) ??
1345
- rawItem
1515
+ completedItemsByKey!.get(rowKey) ??
1516
+ rawItem,
1346
1517
  );
1347
1518
  });
1348
1519
  })()
1349
- : mapResults;
1520
+ : mapResults.map((row) => this.toPublicOutputRow(row));
1350
1521
 
1351
1522
  return createDeferredPlayDataset({
1352
1523
  datasetKind: 'map',
@@ -1355,7 +1526,7 @@ export class PlayContextImpl {
1355
1526
  resolvedTableNamespace,
1356
1527
  ),
1357
1528
  count: results.length,
1358
- previewRows: results.slice(0, 10),
1529
+ previewRows: results.slice(0, 5),
1359
1530
  tableNamespace: resolvedTableNamespace,
1360
1531
  resolvers: {
1361
1532
  count: async () => results.length,
@@ -1397,9 +1568,11 @@ export class PlayContextImpl {
1397
1568
  const normalizedTableNamespace = mapScope.artifactTableNamespace;
1398
1569
  const rowIdentity = (row: Record<string, unknown>, index = 0) =>
1399
1570
  mapScope.rowIdentity(
1400
- Object.fromEntries(
1401
- Object.entries(row).filter(
1402
- ([fieldName]) => !mapFieldNames.includes(fieldName),
1571
+ stripCsvProjectedFields(
1572
+ Object.fromEntries(
1573
+ Object.entries(row).filter(
1574
+ ([fieldName]) => !mapFieldNames.includes(fieldName),
1575
+ ),
1403
1576
  ),
1404
1577
  ),
1405
1578
  index,
@@ -1441,13 +1614,52 @@ export class PlayContextImpl {
1441
1614
  completedRowKey?: string | null;
1442
1615
  pendingRowKey?: string | null;
1443
1616
  activeBoundaryId?: string | null;
1617
+ failedDelta?: number;
1444
1618
  emitEventType?: PlayExecutionEvent['type'];
1445
1619
  }) => {
1446
- this.mapFrames.updateProgress({
1447
- ...input,
1448
- scope: mapScope,
1449
- totalRows,
1450
- });
1620
+ const existing =
1621
+ this.checkpoint.mapFrames?.[mapScope.mapInvocationId] ?? null;
1622
+ if (!existing) {
1623
+ return;
1624
+ }
1625
+ const completedRowKeys = new Set(existing.completedRowKeys);
1626
+ const pendingRowKeys = new Set(existing.pendingRowKeys);
1627
+ if (input.completedRowKey?.trim()) {
1628
+ completedRowKeys.add(input.completedRowKey.trim());
1629
+ pendingRowKeys.delete(input.completedRowKey.trim());
1630
+ }
1631
+ if (input.pendingRowKey?.trim()) {
1632
+ pendingRowKeys.add(input.pendingRowKey.trim());
1633
+ }
1634
+ const nextFrame: MapExecutionFrame = {
1635
+ ...existing,
1636
+ status: input.status ?? existing.status,
1637
+ completedRowKeys: [...completedRowKeys],
1638
+ pendingRowKeys: [...pendingRowKeys],
1639
+ ...(input.activeBoundaryId !== undefined
1640
+ ? { activeBoundaryId: input.activeBoundaryId }
1641
+ : {}),
1642
+ updatedAt: Date.now(),
1643
+ };
1644
+ this.setMapFrame(nextFrame);
1645
+ if (input.emitEventType) {
1646
+ this.emitExecutionEvent({
1647
+ type: input.emitEventType,
1648
+ mapInvocationId: mapScope.mapInvocationId,
1649
+ mapNodeId: mapScope.mapNodeId ?? null,
1650
+ logicalNamespace: mapScope.logicalNamespace,
1651
+ artifactTableNamespace: mapScope.artifactTableNamespace,
1652
+ completedRows: nextFrame.completedRowKeys.length,
1653
+ failedRows: Math.max(
1654
+ 0,
1655
+ totalRows -
1656
+ nextFrame.completedRowKeys.length -
1657
+ nextFrame.pendingRowKeys.length,
1658
+ ),
1659
+ totalRows,
1660
+ at: Date.now(),
1661
+ } as PlayExecutionEvent);
1662
+ }
1451
1663
  };
1452
1664
 
1453
1665
  if (this.canUsePureJsMapFastPath(definition)) {
@@ -1456,10 +1668,12 @@ export class PlayContextImpl {
1456
1668
  fieldEntries,
1457
1669
  visibleFields,
1458
1670
  normalizedTableNamespace,
1671
+ (row, index) => mapScope.rowIdentity(row, index),
1459
1672
  );
1460
- for (const row of results) {
1673
+ for (let index = 0; index < results.length; index += 1) {
1674
+ const row = results[index]!;
1461
1675
  updateMapFrameProgress({
1462
- completedRowKey: rowIdentity(row),
1676
+ completedRowKey: rowIdentity(row, index),
1463
1677
  });
1464
1678
  }
1465
1679
  if (emitTerminalEvent) {
@@ -1527,7 +1741,7 @@ export class PlayContextImpl {
1527
1741
  await this.resolveMapFieldValue(
1528
1742
  resolver,
1529
1743
  item,
1530
- { ...baseRow, ...computedFields },
1744
+ cloneCsvAliasedRow(baseRow, computedFields),
1531
1745
  idx,
1532
1746
  ),
1533
1747
  );
@@ -1544,7 +1758,7 @@ export class PlayContextImpl {
1544
1758
  });
1545
1759
  }
1546
1760
 
1547
- const merged = { ...baseRow, ...computedFields };
1761
+ const merged = cloneCsvAliasedRow(baseRow, computedFields);
1548
1762
  activeFieldName = null;
1549
1763
  updateMapFrameProgress({
1550
1764
  completedRowKey: rowKey,
@@ -1557,11 +1771,7 @@ export class PlayContextImpl {
1557
1771
  error: null,
1558
1772
  dataPatch: {},
1559
1773
  });
1560
- return Object.fromEntries(
1561
- Object.entries(merged).filter(
1562
- ([fieldName]) => !fieldName.startsWith('_'),
1563
- ),
1564
- );
1774
+ return this.toPublicOutputRow(merged);
1565
1775
  } catch (error) {
1566
1776
  if (isPlayRowExecutionSuspendedError(error)) {
1567
1777
  this.pendingRowEventBoundaries.push(error.boundary);
@@ -1620,17 +1830,17 @@ export class PlayContextImpl {
1620
1830
  items[index] as Record<string, unknown>,
1621
1831
  );
1622
1832
  if (result === WAITING_ROW) {
1623
- const key = rowIdentity(rawItem);
1833
+ const key = rowIdentity(rawItem, index);
1624
1834
  if (key) {
1625
1835
  completedRowKeys.add(key);
1626
1836
  }
1627
1837
  continue;
1628
1838
  }
1629
1839
  const row = result as Record<string, unknown>;
1630
- const key = rowIdentity(row);
1840
+ const key = rowIdentity(row, index);
1631
1841
  if (key) completedRowKeys.add(key);
1632
1842
  }
1633
- this.mapFrames.set({
1843
+ this.setMapFrame({
1634
1844
  ...(this.checkpoint.mapFrames?.[mapScope.mapInvocationId] ?? {
1635
1845
  mapInvocationId: mapScope.mapInvocationId,
1636
1846
  logicalNamespace: mapScope.logicalNamespace,
@@ -1638,8 +1848,8 @@ export class PlayContextImpl {
1638
1848
  status: 'suspended' as const,
1639
1849
  totalRows,
1640
1850
  completedRowKeys: [...completedRowKeys],
1641
- pendingRowKeys: items.map((item) =>
1642
- rowIdentity(this.toOutputRow(item as Record<string, unknown>)),
1851
+ pendingRowKeys: items.map((item, index) =>
1852
+ rowIdentity(this.toOutputRow(item as Record<string, unknown>), index),
1643
1853
  ),
1644
1854
  startedAt: Date.now(),
1645
1855
  updatedAt: Date.now(),
@@ -1666,9 +1876,10 @@ export class PlayContextImpl {
1666
1876
  const results = settledResults.filter(
1667
1877
  (result): result is Record<string, unknown> => result !== WAITING_ROW,
1668
1878
  );
1669
- for (const row of results) {
1879
+ for (let index = 0; index < results.length; index += 1) {
1880
+ const row = results[index]!;
1670
1881
  updateMapFrameProgress({
1671
- completedRowKey: rowIdentity(row),
1882
+ completedRowKey: rowIdentity(row, index),
1672
1883
  });
1673
1884
  }
1674
1885
  if (emitTerminalEvent) {
@@ -1714,7 +1925,7 @@ export class PlayContextImpl {
1714
1925
  index: number,
1715
1926
  path: string[],
1716
1927
  ): Promise<unknown> {
1717
- let currentRow = { ...row };
1928
+ let currentRow = cloneCsvAliasedRow(row);
1718
1929
  const produced: Record<string, unknown> = {};
1719
1930
  for (const step of program.steps) {
1720
1931
  const value = await this.executeStepProgramStep(step, currentRow, index, [
@@ -1722,7 +1933,7 @@ export class PlayContextImpl {
1722
1933
  step.name,
1723
1934
  ]);
1724
1935
  produced[step.name] = value;
1725
- currentRow = { ...currentRow, [step.name]: value };
1936
+ currentRow = cloneCsvAliasedRow(currentRow, { [step.name]: value });
1726
1937
  }
1727
1938
  if (typeof program.returnResolver === 'function') {
1728
1939
  return await program.returnResolver(currentRow, this, index);
@@ -1804,7 +2015,6 @@ export class PlayContextImpl {
1804
2015
  ) {
1805
2016
  return false;
1806
2017
  }
1807
- const toolExecuteMarker = ['.tools', 'execute('].join('.');
1808
2018
  return Object.values(definition).every((resolver) => {
1809
2019
  if (typeof resolver !== 'function') {
1810
2020
  return true;
@@ -1812,7 +2022,7 @@ export class PlayContextImpl {
1812
2022
 
1813
2023
  const source = Function.prototype.toString.call(resolver);
1814
2024
  return (
1815
- !source.includes(toolExecuteMarker) && !source.includes('.waterfall(')
2025
+ !source.includes('.tools.execute(') && !source.includes('.waterfall(')
1816
2026
  );
1817
2027
  });
1818
2028
  }
@@ -1822,13 +2032,11 @@ export class PlayContextImpl {
1822
2032
  fieldEntries: [string, MapFieldDefinition<T>[string]][],
1823
2033
  visibleFields: string[],
1824
2034
  tableNamespace: string,
2035
+ rowIdentity: (row: Record<string, unknown>, index: number) => string,
1825
2036
  ): Promise<Array<Record<string, unknown>>> {
1826
2037
  const results: Array<Record<string, unknown>> = [];
1827
2038
  this.pureMapExecutionActive = true;
1828
2039
 
1829
- const rowIdentity = (row: Record<string, unknown>) =>
1830
- derivePlayRowIdentity(row, tableNamespace);
1831
-
1832
2040
  try {
1833
2041
  for (let index = 0; index < items.length; index += 1) {
1834
2042
  const item = items[index]!;
@@ -1841,7 +2049,7 @@ export class PlayContextImpl {
1841
2049
  activeFieldName = fieldName;
1842
2050
  if (this.shouldReuseExistingFieldValue(baseRow, fieldName)) {
1843
2051
  computedFields[fieldName] = baseRow[fieldName];
1844
- this.emitScopedRowUpdate(rowIdentity(baseRow), tableNamespace, {
2052
+ this.emitScopedRowUpdate(rowIdentity(baseRow, index), tableNamespace, {
1845
2053
  rowId: index,
1846
2054
  status: undefined,
1847
2055
  stage: null,
@@ -1858,7 +2066,7 @@ export class PlayContextImpl {
1858
2066
  continue;
1859
2067
  }
1860
2068
 
1861
- this.emitScopedRowUpdate(rowIdentity(baseRow), tableNamespace, {
2069
+ this.emitScopedRowUpdate(rowIdentity(baseRow, index), tableNamespace, {
1862
2070
  rowId: index,
1863
2071
  status: 'running',
1864
2072
  stage: fieldName,
@@ -1876,10 +2084,10 @@ export class PlayContextImpl {
1876
2084
  computedFields[fieldName] = await this.resolveMapFieldValue(
1877
2085
  resolver,
1878
2086
  item,
1879
- { ...baseRow, ...computedFields },
2087
+ cloneCsvAliasedRow(baseRow, computedFields),
1880
2088
  index,
1881
2089
  );
1882
- this.emitScopedRowUpdate(rowIdentity(baseRow), tableNamespace, {
2090
+ this.emitScopedRowUpdate(rowIdentity(baseRow, index), tableNamespace, {
1883
2091
  rowId: index,
1884
2092
  status: undefined,
1885
2093
  stage: 'completed',
@@ -1897,16 +2105,13 @@ export class PlayContextImpl {
1897
2105
  });
1898
2106
  }
1899
2107
 
1900
- const merged = { ...baseRow, ...computedFields };
1901
2108
  results.push(
1902
- Object.fromEntries(
1903
- Object.entries(merged).filter(
1904
- ([fieldName]) => !fieldName.startsWith('_'),
1905
- ),
2109
+ this.toPublicOutputRow(
2110
+ cloneCsvAliasedRow(baseRow, computedFields),
1906
2111
  ),
1907
2112
  );
1908
2113
 
1909
- const rowKey = rowIdentity(baseRow);
2114
+ const rowKey = rowIdentity(baseRow, index);
1910
2115
  activeFieldName = null;
1911
2116
  this.emitScopedRowUpdate(rowKey, tableNamespace, {
1912
2117
  rowId: index,
@@ -1917,7 +2122,7 @@ export class PlayContextImpl {
1917
2122
  dataPatch: {},
1918
2123
  });
1919
2124
  } catch (error) {
1920
- const rowKey = rowIdentity(baseRow);
2125
+ const rowKey = rowIdentity(baseRow, index);
1921
2126
  this.emitScopedRowUpdate(rowKey, tableNamespace, {
1922
2127
  rowId: index,
1923
2128
  status: 'failed',
@@ -1964,7 +2169,7 @@ export class PlayContextImpl {
1964
2169
  private async drainQueuedWork<T>(promises: Promise<T>[]): Promise<void> {
1965
2170
  // Drain loop: each pass resolves queued waterfalls/tool calls.
1966
2171
  // When a batch resolves, rows resume and may queue MORE calls
1967
- // (e.g. row executes one tool request, then queues another sequentially).
2172
+ // (e.g. row does tools.execute('a') then tools.execute('b') sequentially).
1968
2173
  // We keep looping until nothing new is queued and all rows finish.
1969
2174
  //
1970
2175
  // Important: an unresolved Promise by itself does not keep Node alive. The
@@ -2041,6 +2246,13 @@ export class PlayContextImpl {
2041
2246
  return { value: item };
2042
2247
  }
2043
2248
 
2249
+ private toPublicOutputRow(row: Record<string, unknown>): Record<string, unknown> {
2250
+ const stripped = stripCsvProjectedFields(row);
2251
+ return Object.fromEntries(
2252
+ Object.entries(stripped).filter(([fieldName]) => !fieldName.startsWith('_')),
2253
+ );
2254
+ }
2255
+
2044
2256
  async waterfall(
2045
2257
  toolNameOrSpec: string | InlineWaterfallSpec,
2046
2258
  input: Record<string, unknown>,
@@ -2081,13 +2293,10 @@ export class PlayContextImpl {
2081
2293
  }
2082
2294
 
2083
2295
  // Check if this was already resolved in a previous attempt (checkpoint)
2084
- const resolved = this.waterfallReplay.getResolved(queueKey, {
2085
- rowId,
2086
- rowKey: store.rowKey ?? null,
2087
- });
2088
- if (resolved.found) {
2296
+ const resolved = this.checkpoint.resolvedWaterfalls[queueKey];
2297
+ if (resolved && rowId in resolved) {
2089
2298
  this.log(` Row ${rowId} ${toolName}: recovered from checkpoint`);
2090
- return resolved.value;
2299
+ return resolved[rowId];
2091
2300
  }
2092
2301
 
2093
2302
  return new Promise((resolve) => {
@@ -2137,9 +2346,13 @@ export class PlayContextImpl {
2137
2346
  });
2138
2347
  }
2139
2348
 
2140
- private async executeTool(request: ToolExecutionRequest): Promise<unknown> {
2141
- const { tool: toolId, input } = request;
2142
- const normalizedKey = this.normalizeContextKey(request.id, 'tool');
2349
+ private async executeTool(
2350
+ key: string,
2351
+ toolId: string,
2352
+ input: Record<string, unknown>,
2353
+ options?: ToolCallOptions,
2354
+ ): Promise<unknown> {
2355
+ const normalizedKey = this.normalizeContextKey(key, 'tool');
2143
2356
 
2144
2357
  const eventWaitHandler =
2145
2358
  (await this.options.getIntegrationEventWaitHandler?.(toolId)) ?? null;
@@ -2160,16 +2373,14 @@ export class PlayContextImpl {
2160
2373
 
2161
2374
  if (this.pureMapExecutionActive && !store) {
2162
2375
  throw new Error(
2163
- 'Tool execution cannot run inside the pure-JS fast path. Call ctx.tools.execute({ id, tool, input }) directly in the map definition so the batching runtime can stay enabled.',
2376
+ 'ctx.tools.execute() cannot run inside the pure-JS fast path. Call it directly in the map definition so the batching runtime can stay enabled.',
2164
2377
  );
2165
2378
  }
2166
2379
 
2167
2380
  if (!store) {
2168
2381
  const directRowId = -(this.directToolCallIndex + 1);
2169
2382
  this.directToolCallIndex += 1;
2170
- const directCacheKey = this.buildToolResultCacheKey({
2171
- rowId: directRowId,
2172
- });
2383
+ const directCacheKey = this.buildToolResultCacheKey({ rowId: directRowId });
2173
2384
  const cached = this.getCachedToolResult(toolId, directCacheKey);
2174
2385
  if (cached?.done) {
2175
2386
  this.log(`Calling tool: ${toolId} recovered from checkpoint`);
@@ -2209,9 +2420,15 @@ export class PlayContextImpl {
2209
2420
  const callId = [
2210
2421
  store.tableNamespace?.trim() || 'map',
2211
2422
  store.rowKey?.trim() || String(rowId),
2212
- fieldName?.trim() || 'field',
2213
2423
  normalizedKey,
2424
+ toolId,
2214
2425
  ].join(':');
2426
+ if (this.toolCallResolvers.has(callId)) {
2427
+ throw new Error(
2428
+ `ctx.tools.execute("${normalizedKey}") was called more than once concurrently for the same row. ` +
2429
+ 'Use a unique id for each row-scoped tool call.',
2430
+ );
2431
+ }
2215
2432
 
2216
2433
  const cached = this.getCachedToolResult(
2217
2434
  toolId,
@@ -2240,12 +2457,7 @@ export class PlayContextImpl {
2240
2457
  }
2241
2458
 
2242
2459
  return new Promise((resolve) => {
2243
- const existingResolvers = this.toolCallResolvers.get(callId);
2244
- if (existingResolvers) {
2245
- existingResolvers.push(resolve);
2246
- return;
2247
- }
2248
- this.toolCallResolvers.set(callId, [resolve]);
2460
+ this.toolCallResolvers.set(callId, resolve);
2249
2461
  this.emitScopedFieldMetaUpdate({
2250
2462
  rowId,
2251
2463
  key: store.rowKey ?? null,
@@ -2266,7 +2478,7 @@ export class PlayContextImpl {
2266
2478
  input,
2267
2479
  tableNamespace: store.tableNamespace,
2268
2480
  rowKey: store.rowKey ?? null,
2269
- description: normalizeStepDescription(request.description),
2481
+ description: normalizeStepDescription(options?.description),
2270
2482
  });
2271
2483
  });
2272
2484
  };
@@ -2282,7 +2494,7 @@ export class PlayContextImpl {
2282
2494
  {
2283
2495
  markSkipped: () => {
2284
2496
  this.log(
2285
- `ctx.tools.execute({ id: ${normalizedKey}, tool: ${toolId} }): no-op due completed receipt`,
2497
+ `ctx.tools.execute(${toolId}): no-op due completed receipt ${normalizedKey}`,
2286
2498
  );
2287
2499
  },
2288
2500
  execute: executeTool,
@@ -2602,7 +2814,7 @@ export class PlayContextImpl {
2602
2814
  const normalizedKey = this.normalizeContextKey(key, 'fetch');
2603
2815
  if (rowContext.getStore()) {
2604
2816
  throw new Error(
2605
- 'ctx.fetch() must run outside ctx.map(); use ctx.tools.execute({ id, tool, input }) for row-level external requests so Deepline can batch and checkpoint them.',
2817
+ 'ctx.fetch() must run outside ctx.map(); use ctx.tools.execute(...) for row-level external requests so Deepline can batch and checkpoint them.',
2606
2818
  );
2607
2819
  }
2608
2820
 
@@ -2685,9 +2897,7 @@ export class PlayContextImpl {
2685
2897
  }
2686
2898
 
2687
2899
  const rowStore = rowContext.getStore();
2688
- const scope = rowStore
2689
- ? `row-${rowStore.tableNamespace?.trim() || 'map'}:${rowStore.rowKey?.trim() || String(rowStore.rowId)}`
2690
- : 'workflow';
2900
+ const scope = rowStore ? `row-${rowStore.rowId}` : 'workflow';
2691
2901
  const callIndexKey = `${scope}:${normalizedKey}`;
2692
2902
  const callIndex = this.stepCallIndexByKey.get(callIndexKey) ?? 0;
2693
2903
  this.stepCallIndexByKey.set(callIndexKey, callIndex + 1);
@@ -2829,18 +3039,20 @@ export class PlayContextImpl {
2829
3039
  }),
2830
3040
  getCachedResults: (provider) => {
2831
3041
  const batchKey = `${queueKey}:${provider}`;
2832
- const recovered = this.waterfallReplay.readProviderBatch({
2833
- batchKey,
2834
- requests,
2835
- });
2836
- if (recovered) {
2837
- this.log(` ${provider}: skipping (recovered from checkpoint)`);
2838
- }
2839
- return recovered;
3042
+ const cached = this.checkpoint.completedBatches[batchKey];
3043
+ if (!cached) return null;
3044
+ this.log(` ${provider}: skipping (recovered from checkpoint)`);
3045
+ return cached.map((entry) => ({
3046
+ request: requests.find((request) => request.rowId === entry.rowId)!,
3047
+ result: entry.result,
3048
+ }));
2840
3049
  },
2841
3050
  storeCachedResults: (provider, results) => {
2842
3051
  const batchKey = `${queueKey}:${provider}`;
2843
- this.waterfallReplay.writeProviderBatch(batchKey, results);
3052
+ this.checkpoint.completedBatches[batchKey] = results.map((entry) => ({
3053
+ rowId: entry.request.rowId,
3054
+ result: entry.result,
3055
+ }));
2844
3056
  },
2845
3057
  executeProviderRequests: async (provider, pending) => {
2846
3058
  const providerToolId = resolveWaterfallToolId(provider, toolName);
@@ -2971,7 +3183,7 @@ export class PlayContextImpl {
2971
3183
  if (wState?.status === 'pending') {
2972
3184
  wState.status = 'failed';
2973
3185
  wState.error = 'All providers exhausted';
2974
- this.waterfallReplay.setResolved(queueKey, req, null);
3186
+ this.checkpoint.resolvedWaterfalls[queueKey]![req.rowId] = null;
2975
3187
 
2976
3188
  const resolver = this.resolvers.get(`${req.rowId}-${queueKey}`);
2977
3189
  if (resolver) {
@@ -3080,8 +3292,26 @@ export class PlayContextImpl {
3080
3292
  execute: async (request) => {
3081
3293
  const codeStepCtx = {
3082
3294
  tools: {
3083
- execute: async (request: ToolExecutionRequest) =>
3084
- await this.callToolAPI(request.tool, request.input),
3295
+ execute: async (
3296
+ requestOrKey:
3297
+ | { tool: string; input: Record<string, unknown> }
3298
+ | string,
3299
+ toolId?: string,
3300
+ payload?: Record<string, unknown>,
3301
+ ) => {
3302
+ if (typeof requestOrKey === 'object') {
3303
+ return await this.callToolAPI(
3304
+ requestOrKey.tool,
3305
+ requestOrKey.input,
3306
+ );
3307
+ }
3308
+ if (!toolId || !payload) {
3309
+ throw new Error(
3310
+ 'inline waterfall ctx.tools.execute requires a tool and input.',
3311
+ );
3312
+ }
3313
+ return await this.callToolAPI(toolId, payload);
3314
+ },
3085
3315
  },
3086
3316
  };
3087
3317
  return await step.run(request.input, codeStepCtx);
@@ -3447,7 +3677,7 @@ export class PlayContextImpl {
3447
3677
  if (wState?.status === 'pending') {
3448
3678
  wState.status = 'failed';
3449
3679
  wState.error = 'All waterfall steps exhausted';
3450
- this.waterfallReplay.setResolved(queueKey, req, null);
3680
+ this.checkpoint.resolvedWaterfalls[queueKey]![req.rowId] = null;
3451
3681
  const resolver = this.resolvers.get(`${req.rowId}-${queueKey}`);
3452
3682
  if (resolver) {
3453
3683
  resolver(null);
@@ -3499,14 +3729,7 @@ export class PlayContextImpl {
3499
3729
 
3500
3730
  wState.status = 'complete';
3501
3731
  wState.result = result;
3502
- this.waterfallReplay.setResolved(
3503
- queueKey,
3504
- {
3505
- rowId,
3506
- rowKey,
3507
- },
3508
- result,
3509
- );
3732
+ this.checkpoint.resolvedWaterfalls[queueKey]![rowId] = result;
3510
3733
 
3511
3734
  const resolver = this.resolvers.get(`${rowId}-${queueKey}`);
3512
3735
  if (resolver) {
@@ -3558,11 +3781,9 @@ export class PlayContextImpl {
3558
3781
  );
3559
3782
  if (cached?.done) {
3560
3783
  this.log(` Row ${req.rowId} ${toolId}: recovered from checkpoint`);
3561
- const resolvers = this.toolCallResolvers.get(req.callId);
3562
- if (resolvers) {
3563
- for (const resolver of resolvers) {
3564
- resolver(cached.result);
3565
- }
3784
+ const resolver = this.toolCallResolvers.get(req.callId);
3785
+ if (resolver) {
3786
+ resolver(cached.result);
3566
3787
  this.toolCallResolvers.delete(req.callId);
3567
3788
  }
3568
3789
  } else {
@@ -3620,10 +3841,7 @@ export class PlayContextImpl {
3620
3841
  },
3621
3842
  });
3622
3843
  } else {
3623
- await executeChunkedRequests<
3624
- ToolCallRequest,
3625
- ToolExecutionResponse
3626
- >({
3844
+ await executeChunkedRequests<ToolCallRequest, ToolExecutionResponse>({
3627
3845
  requests: pendingRequests,
3628
3846
  batchSize: await this.rateLimitScheduler.getSuggestedParallelism(
3629
3847
  toolId,
@@ -3772,8 +3990,26 @@ export class PlayContextImpl {
3772
3990
  toolNameOrSpec.output,
3773
3991
  await step.run(input, {
3774
3992
  tools: {
3775
- execute: async (request: ToolExecutionRequest) =>
3776
- await this.callToolAPI(request.tool, request.input),
3993
+ execute: async (
3994
+ requestOrKey:
3995
+ | { tool: string; input: Record<string, unknown> }
3996
+ | string,
3997
+ toolId?: string,
3998
+ payload?: Record<string, unknown>,
3999
+ ) => {
4000
+ if (typeof requestOrKey === 'object') {
4001
+ return await this.callToolAPI(
4002
+ requestOrKey.tool,
4003
+ requestOrKey.input,
4004
+ );
4005
+ }
4006
+ if (!toolId || !payload) {
4007
+ throw new Error(
4008
+ 'inline waterfall ctx.tools.execute requires a tool and input.',
4009
+ );
4010
+ }
4011
+ return await this.callToolAPI(toolId, payload);
4012
+ },
3777
4013
  },
3778
4014
  }),
3779
4015
  )
@@ -3830,7 +4066,13 @@ export class PlayContextImpl {
3830
4066
  toolId: string,
3831
4067
  input: Record<string, unknown>,
3832
4068
  ): Promise<unknown> {
3833
- return (await this.callToolExecutionAPI(toolId, input)).result;
4069
+ const execution = await this.callToolExecutionAPI(toolId, input);
4070
+ return execution.result != null
4071
+ && typeof execution.result === 'object'
4072
+ && !Array.isArray(execution.result)
4073
+ && 'data' in execution.result
4074
+ ? execution.result.data
4075
+ : execution.result;
3834
4076
  }
3835
4077
 
3836
4078
  private async callToolExecutionAPI(
@@ -3895,7 +4137,8 @@ export class PlayContextImpl {
3895
4137
  if (!response.ok) {
3896
4138
  const text = await response.text();
3897
4139
  if (
3898
- isRetryableTransientToolHttpStatus(toolId, response.status) &&
4140
+ response.status >= 500 &&
4141
+ response.status < 600 &&
3899
4142
  rateLimitAttempt + 1 < TOOL_TRANSIENT_HTTP_MAX_ATTEMPTS
3900
4143
  ) {
3901
4144
  rateLimitAttempt += 1;
@@ -3947,24 +4190,24 @@ export class PlayContextImpl {
3947
4190
  }
3948
4191
 
3949
4192
  const data = (await response.json()) as Record<string, unknown>;
3950
- const normalized = normalizePlayToolResult(data.result ?? data);
4193
+ const result = data.result ?? data;
3951
4194
  const status =
3952
4195
  typeof data.status === 'string'
3953
4196
  ? data.status
3954
- : normalized == null
4197
+ : result == null
3955
4198
  ? 'no_result'
3956
4199
  : 'completed';
3957
4200
  setSpanAttributes(span, {
3958
4201
  'plays.tool_result_kind':
3959
- normalized == null
4202
+ result == null
3960
4203
  ? 'null'
3961
- : Array.isArray(normalized)
4204
+ : Array.isArray(result)
3962
4205
  ? 'array'
3963
- : typeof normalized,
4206
+ : typeof result,
3964
4207
  });
3965
4208
  return {
3966
4209
  status,
3967
- result: normalized,
4210
+ result,
3968
4211
  metadata: parseExecuteToolMetadata(toolId, data),
3969
4212
  };
3970
4213
  }