deepline 0.1.182 → 0.1.183

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 (66) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-await.ts +203 -6
  2. package/dist/bundling-sources/apps/play-runner-workers/src/child-play-submit.ts +8 -1
  3. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +633 -107
  4. package/dist/bundling-sources/apps/play-runner-workers/src/dedup-do.ts +3 -2
  5. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +1747 -1890
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +116 -0
  7. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +542 -78
  8. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +292 -11
  9. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/run-work-dispatcher.ts +519 -0
  10. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +2381 -0
  11. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +18 -5
  12. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/work-budget.ts +130 -0
  13. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/worker-platform-budget.ts +39 -0
  14. package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry-state.ts +2 -0
  15. package/dist/bundling-sources/sdk/src/client.ts +41 -0
  16. package/dist/bundling-sources/sdk/src/http.ts +23 -8
  17. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +56 -3
  18. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  19. package/dist/bundling-sources/sdk/src/types.ts +2 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +132 -10
  21. package/dist/bundling-sources/shared_libs/play-runtime/auth-scope-resolver.ts +92 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -4
  23. package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +8 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +101 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +1605 -281
  26. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +105 -6
  27. package/dist/bundling-sources/shared_libs/play-runtime/dedup-backend.ts +0 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +1 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/durability-store.ts +4 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +116 -16
  31. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +186 -20
  32. package/dist/bundling-sources/shared_libs/play-runtime/dynamic-worker-version.ts +2 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/execution-ledger-store.ts +133 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +245 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +5 -5
  36. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +14 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/lease-policy.ts +52 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +41 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/providers.ts +3 -2
  40. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +124 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/receipt-status.ts +6 -2
  42. package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +48 -2
  43. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +158 -15
  44. package/dist/bundling-sources/shared_libs/play-runtime/run-terminal-source.ts +45 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +9 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +2037 -262
  47. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +42 -0
  48. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-attempt-state-machine.ts +183 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-errors.ts +88 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +8 -1
  51. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +103 -0
  52. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +19 -1
  53. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +343 -0
  54. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +43 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +437 -0
  57. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +83 -6
  58. package/dist/bundling-sources/shared_libs/security/safe-outbound-fetch.ts +24 -17
  59. package/dist/cli/index.js +76 -17
  60. package/dist/cli/index.mjs +76 -17
  61. package/dist/index.d.mts +2 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.js +63 -9
  64. package/dist/index.mjs +63 -9
  65. package/dist/plays/bundle-play-file.mjs +2 -2
  66. package/package.json +1 -1
@@ -1,9 +1,14 @@
1
1
  import {
2
2
  chooseMapChunkSize,
3
+ EXECUTION_PLAN_DEFAULTS,
3
4
  type ExecutionPlan,
4
5
  } from '../../../../shared_libs/play-runtime/execution-plan';
5
- import { getPlayRuntimeBatchStrategy } from '../../../../shared_libs/play-runtime/play-runtime-batching-registry';
6
6
  import { TOOL_CALLING_MAP_CHUNK_SIZE } from '../../../../shared_libs/play-runtime/map-chunk-limits';
7
+ import { getPlayRuntimeBatchStrategy } from '../../../../shared_libs/play-runtime/play-runtime-batching-registry';
8
+ import {
9
+ WORKER_PLATFORM_SUBREQUESTS_PER_UNBATCHED_TOOL_CALL,
10
+ WORKER_PLATFORM_SUBREQUEST_SOFT_LIMIT,
11
+ } from './worker-platform-budget';
7
12
  import {
8
13
  flattenStaticSubsteps,
9
14
  getTopLevelPipelineSubsteps,
@@ -14,15 +19,61 @@ import {
14
19
 
15
20
  export const CACHE_ENABLED_SIMPLE_MAP_CHUNK_SIZE = 10_000;
16
21
  export { TOOL_CALLING_MAP_CHUNK_SIZE };
17
- // Paid Cloudflare Workers support a much higher configured subrequest limit.
18
- // Keep a large buffer for coordinator/storage calls around row-level unbatched
19
- // tool RPCs. Very dense waterfalls may still need one-row chunks to stay under
20
- // the per-invocation subrequest budget.
21
- export const UNBATCHED_TOOL_SUBREQUESTS_PER_CHUNK_BUDGET = 200;
22
- // Fresh unbatched tool calls use runtime API, receipt, and coordinator/rate
23
- // callbacks on the Worker no-batch path. Batch-cap rows by the shared
24
- // per-call budget rather than a synthetic-tool special case.
25
- export const SUBREQUESTS_PER_UNBATCHED_TOOL_CALL = 5;
22
+ export const WORKER_SUBREQUEST_SAFE_TOOL_CALLS_PER_CHUNK =
23
+ TOOL_CALLING_MAP_CHUNK_SIZE;
24
+ export const UNBATCHED_TOOL_SUBREQUESTS_PER_CHUNK_BUDGET =
25
+ WORKER_PLATFORM_SUBREQUEST_SOFT_LIMIT;
26
+ export const SUBREQUESTS_PER_UNBATCHED_TOOL_CALL =
27
+ WORKER_PLATFORM_SUBREQUESTS_PER_UNBATCHED_TOOL_CALL;
28
+
29
+ // A batched tool over a chunk of N rows does not spend the unbatched provider
30
+ // budget for every row. Durable receipt claim/complete/fail are bulk RPCs for
31
+ // the whole drained tool group, but provider execution still happens once per
32
+ // compiled batch. Static planning cannot know input-dependent bucket keys, so
33
+ // it must assume worst-case provider fanout (one compiled batch per row) unless
34
+ // the batching registry grows a proof that all rows share a bucket.
35
+ export const BATCHED_TOOL_SUBREQUESTS_PER_BATCH = 2;
36
+ export const BULK_RECEIPT_SUBREQUESTS_PER_CHUNK = 3;
37
+ // Conservative batch size assumed only when a batchable tool's registry
38
+ // maxBatchSize is unknown. Smaller = more batches = more subrequests = safer.
39
+ export const ASSUMED_BATCH_SIZE_FLOOR = 100;
40
+ const MAX_BATCHABLE_SUBREQUEST_SAFE_ROWS = 1_000_000;
41
+
42
+ function assumedBatchSizeFor(minBatchableBatchSize: number): number {
43
+ return minBatchableBatchSize > 0
44
+ ? minBatchableBatchSize
45
+ : ASSUMED_BATCH_SIZE_FLOOR;
46
+ }
47
+
48
+ // Per-chunk subrequest cost of the batchable tool work in a map chunk. Shared
49
+ // by the chunk sizer (inverts it to find safe rows/chunk) and the live budget
50
+ // projection in entry.ts so the two never drift.
51
+ export function estimateBatchedToolChunkSubrequests(input: {
52
+ batchableToolCallsPerRow: number;
53
+ coalescedBatchableToolCallsPerRow?: number;
54
+ rows: number;
55
+ assumedBatchSize: number;
56
+ }): number {
57
+ if (input.batchableToolCallsPerRow <= 0 || input.rows <= 0) return 0;
58
+ const toolCallsPerRow = Math.max(1, input.batchableToolCallsPerRow);
59
+ const coalescedToolCallsPerRow = Math.min(
60
+ toolCallsPerRow,
61
+ Math.max(0, input.coalescedBatchableToolCallsPerRow ?? 0),
62
+ );
63
+ const worstCaseToolCallsPerRow = Math.max(
64
+ 0,
65
+ toolCallsPerRow - coalescedToolCallsPerRow,
66
+ );
67
+ const coalescedCompiledBatches =
68
+ Math.ceil(input.rows / Math.max(1, input.assumedBatchSize)) *
69
+ coalescedToolCallsPerRow;
70
+ const worstCaseCompiledBatches = input.rows * worstCaseToolCallsPerRow;
71
+ return (
72
+ BATCHED_TOOL_SUBREQUESTS_PER_BATCH *
73
+ (coalescedCompiledBatches + worstCaseCompiledBatches) +
74
+ BULK_RECEIPT_SUBREQUESTS_PER_CHUNK * toolCallsPerRow
75
+ );
76
+ }
26
77
  export type WorkerMapChunkPlanInput = {
27
78
  mapName: string;
28
79
  rowCountHint: number | null;
@@ -39,14 +90,61 @@ type ChunkSizingMap = Pick<
39
90
  | 'defaultChunkSize'
40
91
  | 'stepsPerChunk'
41
92
  > &
42
- Partial<Pick<PlanMap, 'externalStepFields'>>;
93
+ Partial<
94
+ Pick<PlanMap, 'externalStepFields' | 'stepFields' | 'waterfallStages'>
95
+ >;
43
96
  type ChunkSizingPlan = {
44
97
  maps: ChunkSizingMap[];
45
98
  toolDeclarations?: ExecutionPlan['toolDeclarations'];
46
99
  softWorkflowStepBudget?: number | null;
47
100
  };
48
101
 
49
- type MapToolStats = [totalToolCount: number, unbatchedSubrequestCount: number];
102
+ type MapToolStats = [
103
+ totalToolCount: number,
104
+ unbatchedToolCount: number,
105
+ unbatchedSubrequestCount: number,
106
+ // Smallest registry maxBatchSize across the batchable tools counted so far
107
+ // (0 when no batchable tool has been seen). Smallest = most batches = most
108
+ // subrequests, so it is the conservative choice for chunk sizing.
109
+ minBatchableBatchSize: number,
110
+ coalescedBatchableToolCount: number,
111
+ ];
112
+ type MapControlWorkStats = {
113
+ childPlaySubmits: number;
114
+ eventWaits: number;
115
+ };
116
+
117
+ const EMPTY_CONTROL_WORK_STATS: MapControlWorkStats = {
118
+ childPlaySubmits: 0,
119
+ eventWaits: 0,
120
+ };
121
+
122
+ export type WorkerMapDispatchPlan = {
123
+ rowsPerChunk: number;
124
+ estimatedExternalStepsPerRow: number;
125
+ workEstimate: WorkerMapWorkEstimate;
126
+ reason:
127
+ | 'external-pressure'
128
+ | 'tool-free-large-cache'
129
+ | 'tool-free-plan'
130
+ | 'default-plan';
131
+ };
132
+
133
+ export type WorkerMapWorkEstimate = {
134
+ cpuRowsPerRow: number;
135
+ runtimeSheetReadsPerChunk: number;
136
+ runtimeSheetWritesPerChunk: number;
137
+ durableReceiptOpsPerRow: number;
138
+ providerToolCallsPerRow: number;
139
+ unbatchedProviderToolCallsPerRow: number;
140
+ // Registry batch size assumed for the batchable tools in this map (smallest
141
+ // maxBatchSize across them, or ASSUMED_BATCH_SIZE_FLOOR when unknown). Drives
142
+ // the batched-tool subrequest projection.
143
+ assumedBatchSize: number;
144
+ coalescedBatchableProviderToolCallsPerRow: number;
145
+ childPlaySubmitsPerRow: number;
146
+ eventWaitsPerRow: number;
147
+ };
50
148
 
51
149
  function fieldRoot(field: string | null | undefined): string {
52
150
  return String(field ?? '').split('.', 1)[0] ?? '';
@@ -71,22 +169,82 @@ function declarationBelongsToMapOutput(
71
169
  );
72
170
  }
73
171
 
74
- function countTool(toolId: string): MapToolStats {
75
- if (getPlayRuntimeBatchStrategy(toolId) !== null) return [1, 0];
76
- return [1, SUBREQUESTS_PER_UNBATCHED_TOOL_CALL];
172
+ function countTool(
173
+ toolId: string,
174
+ options?: { staticSingleBucket?: boolean },
175
+ ): MapToolStats {
176
+ const batchStrategy = getPlayRuntimeBatchStrategy(toolId);
177
+ if (batchStrategy !== null) {
178
+ return [
179
+ 1,
180
+ 0,
181
+ 0,
182
+ Math.max(1, Math.floor(batchStrategy.maxBatchSize)),
183
+ options?.staticSingleBucket === true ? 1 : 0,
184
+ ];
185
+ }
186
+ return [1, 1, SUBREQUESTS_PER_UNBATCHED_TOOL_CALL, 0, 0];
187
+ }
188
+
189
+ function minPositiveBatchSize(a: number, b: number): number {
190
+ if (a <= 0) return Math.max(0, b);
191
+ if (b <= 0) return Math.max(0, a);
192
+ return Math.min(a, b);
193
+ }
194
+
195
+ function minProviderBranchCoalescedCount(
196
+ branchStats: readonly MapToolStats[],
197
+ ): number {
198
+ let minCoalesced: number | null = null;
199
+ for (const [total, , , , coalesced] of branchStats) {
200
+ if (total <= 0) continue;
201
+ minCoalesced =
202
+ minCoalesced === null ? coalesced : Math.min(minCoalesced, coalesced);
203
+ }
204
+ return minCoalesced ?? 0;
205
+ }
206
+
207
+ function escapeRegExp(value: string): string {
208
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
209
+ }
210
+
211
+ function hasStaticSingleBatchBucket(input: {
212
+ toolId: string;
213
+ paramsSource?: string | null;
214
+ sourceText?: string | null;
215
+ }): boolean {
216
+ const strategy = getPlayRuntimeBatchStrategy(input.toolId);
217
+ const bucketFields = strategy?.bucketKeyPayloadFields;
218
+ if (!bucketFields || bucketFields.length === 0) return false;
219
+ const source = [input.paramsSource, input.sourceText]
220
+ .filter((value): value is string => typeof value === 'string')
221
+ .join('\n');
222
+ if (!source) return false;
223
+ return bucketFields.every((field) => {
224
+ const pattern = new RegExp(
225
+ `(^|[^A-Za-z0-9_$])${escapeRegExp(field)}([^A-Za-z0-9_$]|$)`,
226
+ );
227
+ return !pattern.test(source);
228
+ });
77
229
  }
78
230
 
79
231
  function addStats(
80
- [total, subrequests]: MapToolStats,
232
+ [total, unbatched, subrequests, batchSize, coalesced]: MapToolStats,
81
233
  stats: MapToolStats,
82
234
  ): MapToolStats {
83
- return [total + stats[0], subrequests + stats[1]];
235
+ return [
236
+ total + stats[0],
237
+ unbatched + stats[1],
238
+ subrequests + stats[2],
239
+ minPositiveBatchSize(batchSize, stats[3]),
240
+ coalesced + stats[4],
241
+ ];
84
242
  }
85
243
 
86
244
  function countToolSubsteps(
87
245
  substeps: readonly PlayStaticSubstep[],
88
246
  ): MapToolStats {
89
- let stats: MapToolStats = [0, 0];
247
+ let stats: MapToolStats = [0, 0, 0, 0, 0];
90
248
 
91
249
  for (const substep of substeps) {
92
250
  stats = addStats(stats, countToolSubstep(substep));
@@ -97,14 +255,29 @@ function countToolSubsteps(
97
255
 
98
256
  function countToolSubstep(substep: PlayStaticSubstep): MapToolStats {
99
257
  if (substep.type === 'tool') {
100
- return countTool(substep.toolId);
258
+ return countTool(substep.toolId, {
259
+ staticSingleBucket: hasStaticSingleBatchBucket({
260
+ toolId: substep.toolId,
261
+ paramsSource: substep.paramsSource,
262
+ sourceText: substep.sourceText,
263
+ }),
264
+ });
101
265
  }
102
266
 
103
267
  if (substep.type === 'waterfall') {
104
- let stats: MapToolStats = [0, 0];
268
+ let stats: MapToolStats = [0, 0, 0, 0, 0];
105
269
  for (const step of substep.steps ?? []) {
106
270
  if (!step.toolId) continue;
107
- stats = addStats(stats, countTool(step.toolId));
271
+ stats = addStats(
272
+ stats,
273
+ countTool(step.toolId, {
274
+ staticSingleBucket: hasStaticSingleBatchBucket({
275
+ toolId: step.toolId,
276
+ paramsSource: step.paramsSource,
277
+ sourceText: substep.sourceText,
278
+ }),
279
+ }),
280
+ );
108
281
  }
109
282
  return stats;
110
283
  }
@@ -118,13 +291,26 @@ function countToolSubstep(substep: PlayStaticSubstep): MapToolStats {
118
291
  const branchStats = substep.branches.map((branch) =>
119
292
  countToolSubsteps(branch.steps),
120
293
  );
121
- return branchStats.reduce<MapToolStats>(
122
- ([totalMax, subrequestsMax], [total, subrequests]) => [
294
+ const maxBranchStats = branchStats.reduce<MapToolStats>(
295
+ (
296
+ [totalMax, unbatchedMax, subrequestsMax, batchSizeMin, coalescedMax],
297
+ [total, unbatched, subrequests, batchSize, coalesced],
298
+ ) => [
123
299
  Math.max(totalMax, total),
300
+ Math.max(unbatchedMax, unbatched),
124
301
  Math.max(subrequestsMax, subrequests),
302
+ minPositiveBatchSize(batchSizeMin, batchSize),
303
+ Math.max(coalescedMax, coalesced),
125
304
  ],
126
- [0, 0],
305
+ [0, 0, 0, 0, 0],
127
306
  );
307
+ return [
308
+ maxBranchStats[0],
309
+ maxBranchStats[1],
310
+ maxBranchStats[2],
311
+ maxBranchStats[3],
312
+ minProviderBranchCoalescedCount(branchStats),
313
+ ];
128
314
  }
129
315
  return countToolSubsteps(substep.steps);
130
316
  }
@@ -133,7 +319,77 @@ function countToolSubstep(substep: PlayStaticSubstep): MapToolStats {
133
319
  return countToolSubsteps(substep.steps ?? []);
134
320
  }
135
321
 
136
- return [0, 0];
322
+ return [0, 0, 0, 0, 0];
323
+ }
324
+
325
+ function addControlWorkStats(
326
+ left: MapControlWorkStats,
327
+ right: MapControlWorkStats,
328
+ ): MapControlWorkStats {
329
+ return {
330
+ childPlaySubmits: left.childPlaySubmits + right.childPlaySubmits,
331
+ eventWaits: left.eventWaits + right.eventWaits,
332
+ };
333
+ }
334
+
335
+ function maxControlWorkStats(
336
+ left: MapControlWorkStats,
337
+ right: MapControlWorkStats,
338
+ ): MapControlWorkStats {
339
+ return {
340
+ childPlaySubmits: Math.max(left.childPlaySubmits, right.childPlaySubmits),
341
+ eventWaits: Math.max(left.eventWaits, right.eventWaits),
342
+ };
343
+ }
344
+
345
+ function countControlWorkSubsteps(
346
+ substeps: readonly PlayStaticSubstep[],
347
+ ): MapControlWorkStats {
348
+ return substeps.reduce<MapControlWorkStats>(
349
+ (acc, substep) =>
350
+ addControlWorkStats(acc, countControlWorkSubstep(substep)),
351
+ EMPTY_CONTROL_WORK_STATS,
352
+ );
353
+ }
354
+
355
+ function countControlWorkSubstep(
356
+ substep: PlayStaticSubstep,
357
+ ): MapControlWorkStats {
358
+ if (substep.type === 'play_call') {
359
+ return { childPlaySubmits: 1, eventWaits: 0 };
360
+ }
361
+
362
+ if (substep.type === 'tool') {
363
+ return {
364
+ childPlaySubmits: 0,
365
+ eventWaits:
366
+ substep.isEventWait === true || substep.toolId === 'test_wait_for_event'
367
+ ? 1
368
+ : 0,
369
+ };
370
+ }
371
+
372
+ if (substep.type === 'step_suite') {
373
+ return countControlWorkSubsteps(substep.steps);
374
+ }
375
+
376
+ if (substep.type === 'control_flow') {
377
+ if (substep.kind === 'conditional' && substep.branches?.length) {
378
+ return substep.branches
379
+ .map((branch) => countControlWorkSubsteps(branch.steps))
380
+ .reduce<MapControlWorkStats>(
381
+ (acc, stats) => maxControlWorkStats(acc, stats),
382
+ EMPTY_CONTROL_WORK_STATS,
383
+ );
384
+ }
385
+ return countControlWorkSubsteps(substep.steps);
386
+ }
387
+
388
+ if (substep.type === 'dataset') {
389
+ return countControlWorkSubsteps(substep.steps ?? []);
390
+ }
391
+
392
+ return EMPTY_CONTROL_WORK_STATS;
137
393
  }
138
394
 
139
395
  function countStepSuiteTools(
@@ -153,11 +409,8 @@ function countStepSuiteTools(
153
409
  conditionalChildren.length < childStats.length / 2
154
410
  ) {
155
411
  return childStats.reduce<MapToolStats>(
156
- ([totalSum, subrequestsSum], { stats: [total, subrequests] }) => [
157
- totalSum + total,
158
- subrequestsSum + subrequests,
159
- ],
160
- [0, 0],
412
+ (acc, { stats }) => addStats(acc, stats),
413
+ [0, 0, 0, 0, 0],
161
414
  );
162
415
  }
163
416
 
@@ -168,11 +421,8 @@ function countStepSuiteTools(
168
421
  });
169
422
  if (!sameRoot) {
170
423
  return childStats.reduce<MapToolStats>(
171
- ([totalSum, subrequestsSum], { stats: [total, subrequests] }) => [
172
- totalSum + total,
173
- subrequestsSum + subrequests,
174
- ],
175
- [0, 0],
424
+ (acc, { stats }) => addStats(acc, stats),
425
+ [0, 0, 0, 0, 0],
176
426
  );
177
427
  }
178
428
 
@@ -183,18 +433,12 @@ function countStepSuiteTools(
183
433
  !(step.type === 'control_flow' && step.kind === 'conditional'),
184
434
  )
185
435
  .reduce<MapToolStats>(
186
- ([totalSum, subrequestsSum], { stats: [total, subrequests] }) => [
187
- totalSum + total,
188
- subrequestsSum + subrequests,
189
- ],
190
- [0, 0],
436
+ (acc, { stats }) => addStats(acc, stats),
437
+ [0, 0, 0, 0, 0],
191
438
  );
192
439
  const conditionalStats = conditionalChildren.reduce<MapToolStats>(
193
- ([totalSum, subrequestsSum], { stats: [total, subrequests] }) => [
194
- totalSum + total,
195
- subrequestsSum + subrequests,
196
- ],
197
- [0, 0],
440
+ (acc, { stats }) => addStats(acc, stats),
441
+ [0, 0, 0, 0, 0],
198
442
  );
199
443
 
200
444
  return addStats(unconditionalStats, conditionalStats);
@@ -203,11 +447,26 @@ function countStepSuiteTools(
203
447
  function countProducerTools(
204
448
  producers: readonly PlayStaticColumnProducer[],
205
449
  ): MapToolStats {
206
- let stats: MapToolStats = [0, 0];
450
+ let stats: MapToolStats = [0, 0, 0, 0, 0];
207
451
 
208
452
  for (const producer of producers) {
209
453
  if (producer.kind === 'tool' && producer.toolId) {
210
- stats = addStats(stats, countTool(producer.toolId));
454
+ stats = addStats(
455
+ stats,
456
+ countTool(producer.toolId, {
457
+ staticSingleBucket: hasStaticSingleBatchBucket({
458
+ toolId: producer.toolId,
459
+ paramsSource:
460
+ producer.substep.type === 'tool'
461
+ ? producer.substep.paramsSource
462
+ : undefined,
463
+ sourceText:
464
+ producer.substep.type === 'tool'
465
+ ? producer.substep.sourceText
466
+ : undefined,
467
+ }),
468
+ }),
469
+ );
211
470
  }
212
471
  if (producer.substep.type === 'waterfall') {
213
472
  stats = addStats(stats, countToolSubstep(producer.substep));
@@ -251,17 +510,42 @@ function countMapLocalTools(input: {
251
510
  );
252
511
  if (!dataset) return null;
253
512
 
254
- if (dataset.steps?.length) {
255
- return countToolSubsteps(dataset.steps);
256
- }
257
-
513
+ if (dataset.steps?.length) return countToolSubsteps(dataset.steps);
258
514
  if (dataset.columns?.length) {
259
515
  return countProducerTools(
260
516
  dataset.columns.flatMap((column) => column.producers),
261
517
  );
262
518
  }
519
+ return [0, 0, 0, 0, 0];
520
+ }
521
+
522
+ function countMapLocalControlWork(input: {
523
+ staticPipeline: PlayStaticPipeline | null | undefined;
524
+ planMap: ChunkSizingMap | null | undefined;
525
+ mapName: string;
526
+ }): MapControlWorkStats | null {
527
+ if (!input.staticPipeline || !input.planMap) return null;
528
+ const planMap = input.planMap;
529
+ const dataset = flattenStaticSubsteps(
530
+ getTopLevelPipelineSubsteps(input.staticPipeline),
531
+ ).find(
532
+ (substep): substep is Extract<PlayStaticSubstep, { type: 'dataset' }> =>
533
+ substep.type === 'dataset' &&
534
+ datasetBelongsToPlanMap(substep, planMap, input.mapName),
535
+ );
536
+ if (!dataset) return null;
263
537
 
264
- return [0, 0];
538
+ if (dataset.steps?.length) return countControlWorkSubsteps(dataset.steps);
539
+ if (dataset.columns?.length) {
540
+ return dataset.columns
541
+ .flatMap((column) => column.producers)
542
+ .reduce<MapControlWorkStats>(
543
+ (acc, producer) =>
544
+ addControlWorkStats(acc, countControlWorkSubstep(producer.substep)),
545
+ EMPTY_CONTROL_WORK_STATS,
546
+ );
547
+ }
548
+ return EMPTY_CONTROL_WORK_STATS;
265
549
  }
266
550
 
267
551
  function countFallbackDeclarationTools(input: {
@@ -269,7 +553,7 @@ function countFallbackDeclarationTools(input: {
269
553
  outputFields: readonly string[];
270
554
  externalStepFields?: readonly string[];
271
555
  }): MapToolStats {
272
- let stats: MapToolStats = [0, 0];
556
+ let stats: MapToolStats = [0, 0, 0, 0, 0];
273
557
 
274
558
  for (const declaration of input.declarations) {
275
559
  if (
@@ -292,7 +576,7 @@ function countDeclarationTools(
292
576
  ): MapToolStats {
293
577
  return declarations.reduce<MapToolStats>(
294
578
  (stats, declaration) => addStats(stats, countTool(declaration.toolId)),
295
- [0, 0],
579
+ [0, 0, 0, 0, 0],
296
580
  );
297
581
  }
298
582
 
@@ -347,6 +631,12 @@ function buildStaticChunkSizingPlan(
347
631
  export function chooseWorkerMapRowsPerChunk(
348
632
  input: WorkerMapChunkPlanInput,
349
633
  ): number {
634
+ return chooseWorkerMapDispatchPlan(input).rowsPerChunk;
635
+ }
636
+
637
+ export function chooseWorkerMapDispatchPlan(
638
+ input: WorkerMapChunkPlanInput,
639
+ ): WorkerMapDispatchPlan {
350
640
  const plan: ChunkSizingPlan | null = input.executionPlan
351
641
  ? {
352
642
  maps: input.executionPlan.maps,
@@ -365,6 +655,11 @@ export function chooseWorkerMapRowsPerChunk(
365
655
  planMap,
366
656
  mapName: input.mapName,
367
657
  });
658
+ const staticMapControlWorkStats = countMapLocalControlWork({
659
+ staticPipeline: input.staticPipeline,
660
+ planMap,
661
+ mapName: input.mapName,
662
+ });
368
663
  const mapToolStats =
369
664
  staticMapToolStats ??
370
665
  (plan && planMap
@@ -375,42 +670,211 @@ export function chooseWorkerMapRowsPerChunk(
375
670
  })
376
671
  : plan
377
672
  ? countDeclarationTools(plan.toolDeclarations ?? [])
378
- : [0, 0]);
673
+ : [0, 0, 0, 0, 0]);
674
+ const estimatedExternalStepsPerRow = estimateExternalStepsForMap({
675
+ planMap,
676
+ mapToolStats,
677
+ });
678
+ const workEstimate = estimateMapWork({
679
+ estimatedExternalStepsPerRow,
680
+ mapToolStats,
681
+ controlWorkStats: staticMapControlWorkStats ?? EMPTY_CONTROL_WORK_STATS,
682
+ });
683
+ const controlSubrequestsPerRow =
684
+ workEstimate.childPlaySubmitsPerRow + workEstimate.eventWaitsPerRow;
685
+ const allProviderCallsAreCoalescedBatchable =
686
+ workEstimate.providerToolCallsPerRow > 0 &&
687
+ workEstimate.unbatchedProviderToolCallsPerRow === 0 &&
688
+ workEstimate.coalescedBatchableProviderToolCallsPerRow >=
689
+ workEstimate.providerToolCallsPerRow &&
690
+ controlSubrequestsPerRow === 0;
691
+ const preferredChunkSize = allProviderCallsAreCoalescedBatchable
692
+ ? Math.max(
693
+ planMap?.defaultChunkSize ?? 0,
694
+ EXECUTION_PLAN_DEFAULTS.largeMapChunkSize,
695
+ )
696
+ : planMap?.defaultChunkSize;
379
697
  const rowsPerChunk = chooseMapChunkSize({
380
698
  totalRows: input.rowCountHint,
381
699
  mapCount: Math.max(1, plan?.maps.length ?? 1),
382
- stepsPerChunk: planMap?.stepsPerChunk ?? 1,
383
- preferredChunkSize: planMap?.defaultChunkSize,
700
+ stepsPerChunk:
701
+ estimatedExternalStepsPerRow > 0
702
+ ? Math.max(estimatedExternalStepsPerRow, planMap?.stepsPerChunk ?? 1)
703
+ : 1,
704
+ preferredChunkSize,
384
705
  softWorkflowStepBudget: plan?.softWorkflowStepBudget,
385
706
  });
707
+ if (estimatedExternalStepsPerRow > 0 || controlSubrequestsPerRow > 0) {
708
+ const subrequestSafeRows = subrequestSafeRowsForMap({
709
+ planMap,
710
+ mapToolStats,
711
+ staticMapToolStats,
712
+ estimatedExternalStepsPerRow,
713
+ workEstimate,
714
+ });
715
+ if (subrequestSafeRows < rowsPerChunk) {
716
+ const externalPressurePlan: WorkerMapDispatchPlan = {
717
+ rowsPerChunk: subrequestSafeRows,
718
+ estimatedExternalStepsPerRow,
719
+ workEstimate,
720
+ reason: 'external-pressure',
721
+ };
722
+ return externalPressurePlan;
723
+ }
724
+ return {
725
+ rowsPerChunk,
726
+ estimatedExternalStepsPerRow,
727
+ workEstimate,
728
+ reason: 'external-pressure',
729
+ };
730
+ }
386
731
 
387
- const toolFreeSimpleMap =
388
- !!planMap && planMap.stepsPerChunk === 1 && mapToolStats[0] === 0;
732
+ const toolFreeMap = !!planMap && mapToolStats[0] === 0;
733
+ const toolFreeSimpleMap = toolFreeMap && planMap.stepsPerChunk === 1;
389
734
  if (
390
735
  toolFreeSimpleMap &&
391
736
  (input.rowCountHint === null ||
392
737
  input.rowCountHint >= CACHE_ENABLED_SIMPLE_MAP_CHUNK_SIZE)
393
738
  ) {
394
- return Math.max(rowsPerChunk, CACHE_ENABLED_SIMPLE_MAP_CHUNK_SIZE);
395
- }
396
-
397
- if (mapToolStats[1] > 0) {
398
- const unbatchedSubrequestsPerRow =
399
- staticMapToolStats !== null
400
- ? mapToolStats[1]
401
- : Math.max(
402
- (planMap?.stepsPerChunk ?? 1) * SUBREQUESTS_PER_UNBATCHED_TOOL_CALL,
403
- mapToolStats[1],
404
- );
405
- const unbatchedToolRowsPerChunk = Math.max(
406
- 1,
407
- Math.floor(
408
- UNBATCHED_TOOL_SUBREQUESTS_PER_CHUNK_BUDGET /
409
- unbatchedSubrequestsPerRow,
410
- ),
739
+ const largeRowsPerChunk = Math.max(
740
+ rowsPerChunk,
741
+ CACHE_ENABLED_SIMPLE_MAP_CHUNK_SIZE,
411
742
  );
412
- return Math.min(rowsPerChunk, unbatchedToolRowsPerChunk);
743
+ return {
744
+ rowsPerChunk: largeRowsPerChunk,
745
+ estimatedExternalStepsPerRow: 0,
746
+ workEstimate,
747
+ reason: 'tool-free-large-cache',
748
+ };
413
749
  }
414
750
 
415
- return rowsPerChunk;
751
+ return {
752
+ rowsPerChunk,
753
+ estimatedExternalStepsPerRow: 0,
754
+ workEstimate,
755
+ reason: toolFreeMap ? 'tool-free-plan' : 'default-plan',
756
+ };
757
+ }
758
+
759
+ function estimateMapWork(input: {
760
+ estimatedExternalStepsPerRow: number;
761
+ mapToolStats: MapToolStats;
762
+ controlWorkStats: MapControlWorkStats;
763
+ }): WorkerMapWorkEstimate {
764
+ return {
765
+ cpuRowsPerRow: 1,
766
+ runtimeSheetReadsPerChunk: 1,
767
+ runtimeSheetWritesPerChunk: 1,
768
+ durableReceiptOpsPerRow: input.estimatedExternalStepsPerRow,
769
+ providerToolCallsPerRow: input.mapToolStats[0],
770
+ unbatchedProviderToolCallsPerRow: input.mapToolStats[1],
771
+ assumedBatchSize: assumedBatchSizeFor(input.mapToolStats[3]),
772
+ coalescedBatchableProviderToolCallsPerRow: input.mapToolStats[4],
773
+ childPlaySubmitsPerRow: input.controlWorkStats.childPlaySubmits,
774
+ eventWaitsPerRow: input.controlWorkStats.eventWaits,
775
+ };
776
+ }
777
+
778
+ function estimateExternalStepsForMap(input: {
779
+ planMap: ChunkSizingMap | undefined;
780
+ mapToolStats: MapToolStats;
781
+ }): number {
782
+ if (!input.planMap) return input.mapToolStats[0];
783
+ const explicitExternalSteps = Array.isArray(input.planMap.externalStepFields)
784
+ ? input.planMap.externalStepFields.length
785
+ : 0;
786
+ return Math.max(explicitExternalSteps, input.mapToolStats[0]);
787
+ }
788
+
789
+ function subrequestSafeRowsForMap(input: {
790
+ planMap: ChunkSizingMap | undefined;
791
+ mapToolStats: MapToolStats;
792
+ staticMapToolStats: MapToolStats | null;
793
+ estimatedExternalStepsPerRow: number;
794
+ workEstimate: WorkerMapWorkEstimate;
795
+ }): number {
796
+ const hasToolWork = input.mapToolStats[0] > 0;
797
+ const unbatchedToolCount = input.mapToolStats[1];
798
+ const unbatchedSubrequestCount = input.mapToolStats[2];
799
+ const controlSubrequestsPerRow =
800
+ input.workEstimate.childPlaySubmitsPerRow +
801
+ input.workEstimate.eventWaitsPerRow;
802
+ const controlRows =
803
+ controlSubrequestsPerRow > 0
804
+ ? Math.max(
805
+ 1,
806
+ Math.floor(
807
+ (UNBATCHED_TOOL_SUBREQUESTS_PER_CHUNK_BUDGET -
808
+ input.workEstimate.runtimeSheetReadsPerChunk -
809
+ input.workEstimate.runtimeSheetWritesPerChunk) /
810
+ controlSubrequestsPerRow,
811
+ ),
812
+ )
813
+ : Number.POSITIVE_INFINITY;
814
+ // Largest chunk whose batched tool work stays under the subrequest ceiling.
815
+ // Invert estimateBatchedToolChunkSubrequests. Static planning does not know
816
+ // bucket cardinality, so it assumes worst-case provider fanout (one compiled
817
+ // batch per row per batchable tool call) while keeping durable receipt RPCs
818
+ // fixed per drained tool group.
819
+ const batchableToolRows =
820
+ hasToolWork && unbatchedToolCount <= 0
821
+ ? (() => {
822
+ const batchableToolCallsPerRow = input.mapToolStats[0];
823
+ const costForRows = (rows: number) =>
824
+ estimateBatchedToolChunkSubrequests({
825
+ batchableToolCallsPerRow,
826
+ coalescedBatchableToolCallsPerRow:
827
+ input.workEstimate.coalescedBatchableProviderToolCallsPerRow,
828
+ rows,
829
+ assumedBatchSize: input.workEstimate.assumedBatchSize,
830
+ }) +
831
+ input.workEstimate.runtimeSheetReadsPerChunk +
832
+ input.workEstimate.runtimeSheetWritesPerChunk;
833
+ if (costForRows(1) > UNBATCHED_TOOL_SUBREQUESTS_PER_CHUNK_BUDGET) {
834
+ return 1;
835
+ }
836
+ let low = 1;
837
+ let high = MAX_BATCHABLE_SUBREQUEST_SAFE_ROWS;
838
+ while (low < high) {
839
+ const mid = Math.ceil((low + high) / 2);
840
+ if (
841
+ costForRows(mid) <= UNBATCHED_TOOL_SUBREQUESTS_PER_CHUNK_BUDGET
842
+ ) {
843
+ low = mid;
844
+ } else {
845
+ high = mid - 1;
846
+ }
847
+ }
848
+ return low;
849
+ })()
850
+ : Number.POSITIVE_INFINITY;
851
+ if (hasToolWork && unbatchedToolCount <= 0) {
852
+ return Math.min(batchableToolRows, controlRows);
853
+ }
854
+ const genericRows =
855
+ input.estimatedExternalStepsPerRow > 0
856
+ ? Math.max(
857
+ 1,
858
+ Math.floor(
859
+ WORKER_SUBREQUEST_SAFE_TOOL_CALLS_PER_CHUNK /
860
+ input.estimatedExternalStepsPerRow,
861
+ ),
862
+ )
863
+ : Number.POSITIVE_INFINITY;
864
+ if (unbatchedToolCount <= 0) return Math.min(genericRows, controlRows);
865
+ const unbatchedSubrequestsPerRow =
866
+ input.staticMapToolStats !== null
867
+ ? unbatchedSubrequestCount
868
+ : Math.max(
869
+ (input.planMap?.stepsPerChunk ?? 1) *
870
+ SUBREQUESTS_PER_UNBATCHED_TOOL_CALL,
871
+ unbatchedSubrequestCount,
872
+ );
873
+ const unbatchedRows = Math.max(
874
+ 1,
875
+ Math.floor(
876
+ UNBATCHED_TOOL_SUBREQUESTS_PER_CHUNK_BUDGET / unbatchedSubrequestsPerRow,
877
+ ),
878
+ );
879
+ return Math.min(unbatchedRows, controlRows);
416
880
  }