deepline 0.1.209 → 0.1.211

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 (24) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +0 -2
  2. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +155 -139
  3. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +15 -0
  4. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-latency-profile.ts +90 -0
  5. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +173 -103
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +25 -92
  7. package/dist/bundling-sources/sdk/src/play.ts +4 -2
  8. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  9. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +4 -5
  10. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +348 -109
  11. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +1 -2
  12. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +2 -4
  13. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +42 -3
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +5 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/single-flight.ts +31 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +12 -1
  17. package/dist/bundling-sources/shared_libs/plays/dataset.ts +41 -5
  18. package/dist/cli/index.js +94 -16
  19. package/dist/cli/index.mjs +94 -16
  20. package/dist/index.d.mts +18 -3
  21. package/dist/index.d.ts +18 -3
  22. package/dist/index.js +22 -5
  23. package/dist/index.mjs +22 -5
  24. package/package.json +1 -1
@@ -2445,8 +2445,6 @@ function runRequestFromPlayWorkflowParams(
2445
2445
  preloadedDbSessions: params.preloadedDbSessions ?? null,
2446
2446
  runtimeTestFaultHeader: params.runtimeTestFaultHeader ?? null,
2447
2447
  testPolicyOverrides: params.testPolicyOverrides ?? null,
2448
- inlineChildRunRegistered:
2449
- params.runtimeBackend === 'cf_workflows_dynamic_worker_inline_child',
2450
2448
  coordinatorUrl: params.coordinatorUrl ?? null,
2451
2449
  coordinatorInternalToken: params.coordinatorInternalToken ?? null,
2452
2450
  totalRows: params.totalRows,
@@ -32,6 +32,7 @@ import {
32
32
  type WorkflowEvent,
33
33
  type WorkflowStep,
34
34
  } from 'cloudflare:workers';
35
+ import { AsyncLocalStorage } from 'node:async_hooks';
35
36
  import {
36
37
  deterministicMapChunkStepName,
37
38
  type ExecutionPlan,
@@ -40,7 +41,6 @@ import {
40
41
  STANDARD_PLAY_RUNTIME_LIMIT_LABEL,
41
42
  STANDARD_PLAY_RUNTIME_LIMIT_SECONDS,
42
43
  } from '../../../shared_libs/play-runtime/runtime-constants';
43
- import { childSubmitIdempotencyKey } from '../../../shared_libs/play-runtime/child-run-id';
44
44
  import {
45
45
  createPlayExecutionGovernor,
46
46
  type GovernanceSnapshot,
@@ -104,7 +104,7 @@ import {
104
104
  import { createDeferredPlayDataset } from '../../../shared_libs/plays/dataset';
105
105
  import {
106
106
  buildDurableCtxCallCacheKey,
107
- buildDurableRunPlaySemanticKey,
107
+ buildDurableRunPlayInvocationScope,
108
108
  } from '../../../shared_libs/play-runtime/durable-call-cache';
109
109
  import {
110
110
  resolveRuntimeToolReceiptWaitMaxAttempts,
@@ -166,6 +166,7 @@ import {
166
166
  createWorkerWorkBudgetMeter,
167
167
  type WorkerWorkBudgetMeter,
168
168
  } from './runtime/work-budget';
169
+ import { MapLatencyProfile } from './runtime/map-latency-profile';
169
170
  import {
170
171
  WORKER_RUN_DISPATCHER_DEFAULT_MAX_RESIDENT_ROW_BYTES,
171
172
  WorkerRunWorkDispatcher,
@@ -368,8 +369,6 @@ type RunRequest = {
368
369
  /** Internal ctx.runPlay lineage. Public SDK/users never see this. */
369
370
  playCallGovernance?: PlayCallGovernanceSnapshot | null;
370
371
  preloadedDbSessions?: PreloadedRuntimeDbSession[] | null;
371
- /** Coordinator already created the child run row before invoking /run-inline. */
372
- inlineChildRunRegistered?: boolean | null;
373
372
  /** Cloudflare coordinator URL for direct Workflow control-plane signals. */
374
373
  coordinatorUrl?: string | null;
375
374
  /** Request-scoped coordinator auth token for preview/dev direct control calls. */
@@ -1148,11 +1147,29 @@ async function drainRunnerPerfTraces(req: RunRequest): Promise<void> {
1148
1147
 
1149
1148
  let workerRequestSequence = 0;
1150
1149
 
1150
+ const workerRunPlayRowContext = new AsyncLocalStorage<{
1151
+ tableNamespace: string;
1152
+ rowKey: string;
1153
+ fieldName: string;
1154
+ }>();
1155
+
1151
1156
  function makeRequestId(): string {
1152
1157
  workerRequestSequence = (workerRequestSequence + 1) % Number.MAX_SAFE_INTEGER;
1153
1158
  return `worker-request-${workerRequestSequence}`;
1154
1159
  }
1155
1160
 
1161
+ async function executeFreshChildInvocation<T>(
1162
+ input: {
1163
+ parentRunId: string;
1164
+ key: string;
1165
+ invocationScope: string;
1166
+ },
1167
+ execute: (invocationId: string) => Promise<T>,
1168
+ ): Promise<T> {
1169
+ const invocationId = `${input.parentRunId}:runPlay:${input.key}:${input.invocationScope}`;
1170
+ return await execute(invocationId);
1171
+ }
1172
+
1156
1173
  function makeRuntimeSheetAttempt(input: {
1157
1174
  runId: string;
1158
1175
  runAttempt?: number | null;
@@ -1665,27 +1682,6 @@ async function hashChildPlayEventKey(input: unknown): Promise<string> {
1665
1682
  return (await hashJson(input)).slice(0, 32);
1666
1683
  }
1667
1684
 
1668
- function workerChildRevisionFingerprint(input: {
1669
- playName: string;
1670
- manifest: PlayRuntimeManifest | null | undefined;
1671
- }): string | null {
1672
- const manifest = input.manifest;
1673
- if (!manifest) return null;
1674
- return sha256Hex(
1675
- stableStringify({
1676
- playId: manifest.playName?.trim() || input.playName,
1677
- codeFormat: 'cjs_module',
1678
- artifactHash: manifest.artifactHash ?? null,
1679
- graphHash: manifest.graphHash ?? null,
1680
- sourceHash: null,
1681
- sourceCodeHash:
1682
- typeof manifest.sourceCode === 'string'
1683
- ? sha256Hex(manifest.sourceCode)
1684
- : null,
1685
- }),
1686
- );
1687
- }
1688
-
1689
1685
  type RuntimeResolvedPlayResponse = {
1690
1686
  manifest?: PlayRuntimeManifest | null;
1691
1687
  };
@@ -4603,6 +4599,11 @@ function createMinimalWorkerCtx(
4603
4599
  opts?: WorkerMapOptions,
4604
4600
  ): Promise<unknown> => {
4605
4601
  const mapStartedAt = nowMs();
4602
+ const mapLatencyProfile = new MapLatencyProfile(
4603
+ req.testPolicyOverrides?.mapLatencyProfile === true,
4604
+ name,
4605
+ mapStartedAt,
4606
+ );
4606
4607
  const mapNodeId = `map:${name}`;
4607
4608
  const inputRows = rows;
4608
4609
  const rowCountHint = datasetRowCountHint(inputRows);
@@ -4940,6 +4941,7 @@ function createMinimalWorkerCtx(
4940
4941
  blockedRows: prepared.blockedRows.length,
4941
4942
  },
4942
4943
  });
4944
+ mapLatencyProfile.recordPhase('prepare_rows', nowMs() - prepareStartedAt);
4943
4945
  const progressTotalRows = rowCountHint ?? chunkRows.length;
4944
4946
  const preparedCompletedRows = Math.min(
4945
4947
  progressTotalRows,
@@ -5220,7 +5222,10 @@ function createMinimalWorkerCtx(
5220
5222
  deserializeDurableValue: deserializeDurableStepValue,
5221
5223
  nowMs,
5222
5224
  batchGraceMs: req.testPolicyOverrides?.batchGraceMs,
5223
- recordPerfTrace: (trace) => recordRunnerPerfTrace({ req, ...trace }),
5225
+ recordPerfTrace: (trace) => {
5226
+ mapLatencyProfile.recordToolTrace(trace);
5227
+ recordRunnerPerfTrace({ req, ...trace });
5228
+ },
5224
5229
  abortSignal,
5225
5230
  onRequestsSettled: reportSettledToolRequests,
5226
5231
  callbacks,
@@ -5510,9 +5515,16 @@ function createMinimalWorkerCtx(
5510
5515
  if (circuitError) throw circuitError;
5511
5516
  const myIndex = idx++;
5512
5517
  if (myIndex >= rowsToExecute.length) return;
5518
+ const rowQueueStartedAt = mapLatencyProfile.enabled ? nowMs() : 0;
5513
5519
  const rowSlot = await governor.acquireRowSlot({
5514
5520
  signal: abortSignal,
5515
5521
  });
5522
+ if (mapLatencyProfile.enabled) {
5523
+ mapLatencyProfile.recordRowQueue(nowMs() - rowQueueStartedAt);
5524
+ }
5525
+ const rowExecutionStartedAt = mapLatencyProfile.enabled
5526
+ ? nowMs()
5527
+ : 0;
5516
5528
  let rowMarkedActive = false;
5517
5529
  try {
5518
5530
  startedExecutedRows += 1;
@@ -5580,37 +5592,49 @@ function createMinimalWorkerCtx(
5580
5592
  })
5581
5593
  : null,
5582
5594
  });
5583
- const resolved = await executeWorkerStepResolver(
5584
- value,
5585
- enriched,
5586
- rowCtx,
5587
- absoluteIndex,
5588
- previousCell,
5589
- isWorkerStepProgram(value)
5590
- ? {
5591
- parentField: key,
5592
- path: [],
5593
- outputs: stepProgramOutputs,
5594
- onOutput: async (stepOutput) => {
5595
- generatedOutputFields.add(stepOutput.columnName);
5596
- const status = stepOutput.status ?? 'completed';
5597
- if (status === 'skipped') stepCellsSkipped += 1;
5598
- else stepCellsCompleted += 1;
5599
- await enqueueLiveRowUpdate({
5600
- key: entry.rowKey,
5601
- dataPatch: {
5602
- [stepOutput.columnName]: stepOutput.value,
5603
- },
5604
- cellMetaPatch: {
5605
- [stepOutput.columnName]: {
5606
- status,
5607
- stage: stepOutput.stepId,
5608
- },
5595
+ const resolved = await workerRunPlayRowContext.run(
5596
+ {
5597
+ tableNamespace: name,
5598
+ rowKey: entry.rowKey,
5599
+ fieldName: key,
5600
+ },
5601
+ () =>
5602
+ executeWorkerStepResolver(
5603
+ value,
5604
+ enriched,
5605
+ rowCtx,
5606
+ absoluteIndex,
5607
+ previousCell,
5608
+ isWorkerStepProgram(value)
5609
+ ? {
5610
+ parentField: key,
5611
+ path: [],
5612
+ outputs: stepProgramOutputs,
5613
+ onOutput: async (stepOutput) => {
5614
+ generatedOutputFields.add(
5615
+ stepOutput.columnName,
5616
+ );
5617
+ const status =
5618
+ stepOutput.status ?? 'completed';
5619
+ if (status === 'skipped')
5620
+ stepCellsSkipped += 1;
5621
+ else stepCellsCompleted += 1;
5622
+ await enqueueLiveRowUpdate({
5623
+ key: entry.rowKey,
5624
+ dataPatch: {
5625
+ [stepOutput.columnName]: stepOutput.value,
5626
+ },
5627
+ cellMetaPatch: {
5628
+ [stepOutput.columnName]: {
5629
+ status,
5630
+ stage: stepOutput.stepId,
5631
+ },
5632
+ },
5633
+ });
5609
5634
  },
5610
- });
5611
- },
5612
- }
5613
- : undefined,
5635
+ }
5636
+ : undefined,
5637
+ ),
5614
5638
  );
5615
5639
  enriched[key] = resolved.value;
5616
5640
  fieldOutputs[key] = resolved.value;
@@ -5737,6 +5761,11 @@ function createMinimalWorkerCtx(
5737
5761
  // single clean run failure.
5738
5762
  }
5739
5763
  } finally {
5764
+ if (mapLatencyProfile.enabled) {
5765
+ mapLatencyProfile.recordRowExecution(
5766
+ nowMs() - rowExecutionStartedAt,
5767
+ );
5768
+ }
5740
5769
  if (rowMarkedActive) {
5741
5770
  activeExecutedRows = Math.max(0, activeExecutedRows - 1);
5742
5771
  reportExecutionHeartbeat(false);
@@ -5789,6 +5818,10 @@ function createMinimalWorkerCtx(
5789
5818
  concurrency,
5790
5819
  },
5791
5820
  });
5821
+ mapLatencyProfile.recordPhase(
5822
+ 'worker_execution',
5823
+ nowMs() - workersStartedAt,
5824
+ );
5792
5825
  const buildRowFailureSamples = () =>
5793
5826
  failedRowEntries
5794
5827
  .map((failure, executedIndex) =>
@@ -5916,6 +5949,10 @@ function createMinimalWorkerCtx(
5916
5949
  rowsToExecute: rowsToExecute.length,
5917
5950
  },
5918
5951
  });
5952
+ mapLatencyProfile.recordPhase(
5953
+ 'persist_rows',
5954
+ nowMs() - persistRowsStartedAt,
5955
+ );
5919
5956
  } catch (error) {
5920
5957
  if (
5921
5958
  workflowStep &&
@@ -6021,6 +6058,7 @@ function createMinimalWorkerCtx(
6021
6058
  ms: nowMs() - hashStartedAt,
6022
6059
  extra: { mapName: name, chunkIndex, rows: out.length },
6023
6060
  });
6061
+ mapLatencyProfile.recordPhase('hash', nowMs() - hashStartedAt);
6024
6062
  recordRunnerPerfTrace({
6025
6063
  req,
6026
6064
  phase: 'runner.map_chunk.total',
@@ -6636,6 +6674,27 @@ function createMinimalWorkerCtx(
6636
6674
  chunks: dispatchResult.chunksExecuted,
6637
6675
  },
6638
6676
  });
6677
+ if (mapLatencyProfile.enabled) {
6678
+ const endedAt = nowMs();
6679
+ const summary = mapLatencyProfile.summary(endedAt, {
6680
+ chunks: dispatchResult.chunksExecuted,
6681
+ rows: totalRowsWritten,
6682
+ });
6683
+ recordRunnerPerfTrace({
6684
+ req,
6685
+ phase: 'runner.map.profile',
6686
+ ms: endedAt - mapStartedAt,
6687
+ extra: summary,
6688
+ });
6689
+ // Local Absurd does not always forward dynamic-worker perf traces. Keep
6690
+ // one bounded copy in the durable run log for the diagnostic command.
6691
+ emitEvent({
6692
+ type: 'log',
6693
+ level: 'info',
6694
+ message: `[perf] map_latency_profile ${JSON.stringify(summary)}`,
6695
+ ts: endedAt,
6696
+ });
6697
+ }
6639
6698
  return dataset;
6640
6699
  };
6641
6700
 
@@ -6933,7 +6992,6 @@ function createMinimalWorkerCtx(
6933
6992
  options?: {
6934
6993
  description?: string;
6935
6994
  timeoutMs?: number;
6936
- staleAfterSeconds?: number;
6937
6995
  },
6938
6996
  ): Promise<unknown> {
6939
6997
  const normalizedKey = normalizeContextKey(key, 'runPlay');
@@ -6947,34 +7005,49 @@ function createMinimalWorkerCtx(
6947
7005
  playRef: resolvedName,
6948
7006
  cache: childManifestCache,
6949
7007
  });
6950
- const receiptKey = buildDurableCtxCallCacheKey({
6951
- orgId: req.orgId,
6952
- playId: req.playName,
6953
- kind: 'runPlay',
6954
- id: normalizedKey,
6955
- semanticKey: buildDurableRunPlaySemanticKey({
6956
- childPlayName: resolvedName,
6957
- childRevisionFingerprint: workerChildRevisionFingerprint({
6958
- playName: resolvedName,
6959
- manifest: childManifest,
6960
- }),
6961
- input,
6962
- }),
6963
- staleAfterSeconds: options?.staleAfterSeconds,
7008
+ const rowScope = workerRunPlayRowContext.getStore();
7009
+ const invocationScope = buildDurableRunPlayInvocationScope({
7010
+ childPlayName: resolvedName,
7011
+ input,
7012
+ rowScope: rowScope
7013
+ ? {
7014
+ fieldName: rowScope.fieldName,
7015
+ rowKey: rowScope.rowKey,
7016
+ tableNamespace: rowScope.tableNamespace,
7017
+ }
7018
+ : null,
6964
7019
  });
6965
- return await executeWithRuntimeReceipt(
6966
- receiptKey,
6967
- async (receiptContext, wasFailed) => {
7020
+ return await executeFreshChildInvocation(
7021
+ {
7022
+ parentRunId: req.runId,
7023
+ key: normalizedKey,
7024
+ invocationScope,
7025
+ },
7026
+ async (childInvocationId) => {
6968
7027
  // The Governor owns the play-call lineage: forkChild does the cycle
6969
7028
  // guard, depth/per-parent/playCall/descendant budget charges, and
6970
7029
  // returns the snapshot to thread into the child so budgets accumulate
6971
- // across isolates. Charged inside the receipt boundary so a replay
6972
- // (cache hit) never double-charges.
6973
- const childRunId = `${req.runId}:child:${normalizedKey}`;
6974
- const childGovernance = await governor.forkChild({
6975
- childPlayName: resolvedName,
6976
- childRunId,
6977
- });
7030
+ // across isolates. Every ctx.runPlay call is a fresh child invocation.
7031
+ const childRunId = `${req.runId}:child:${childInvocationId}`;
7032
+ // Checkpoint governance admission separately from the child result.
7033
+ // Workflow replay reuses this admission and the coordinator's stable
7034
+ // child launch identity, but ctx.runPlay never caches child output.
7035
+ const forkChild = () =>
7036
+ governor.forkChild({
7037
+ childPlayName: resolvedName,
7038
+ childRunId,
7039
+ });
7040
+ const childGovernance = workflowStep
7041
+ ? await (
7042
+ workflowStep.do as unknown as <T>(
7043
+ name: string,
7044
+ callback: () => Promise<T>,
7045
+ ) => Promise<T>
7046
+ )(
7047
+ `runPlay-governance:${sha256Hex(childInvocationId).slice(0, 24)}`,
7048
+ forkChild,
7049
+ )
7050
+ : await forkChild();
6978
7051
  const nextDepth = childGovernance.callDepth;
6979
7052
  const nextParentCalls =
6980
7053
  governor.snapshot().parentChildCalls[req.playName] ?? 0;
@@ -7047,11 +7120,7 @@ function createMinimalWorkerCtx(
7047
7120
  options?.timeoutMs == null && !childNeedsWorkflowScheduler,
7048
7121
  body: {
7049
7122
  name: resolvedName,
7050
- childIdempotencyKey: childSubmitIdempotencyKey({
7051
- receiptKey,
7052
- wasFailed,
7053
- leaseId: receiptContext.leaseId,
7054
- }),
7123
+ childIdempotencyKey: childInvocationId,
7055
7124
  input: isRecord(input) ? input : {},
7056
7125
  orgId: req.orgId,
7057
7126
  callbackUrl: req.callbackUrl,
@@ -7329,12 +7398,6 @@ function createMinimalWorkerCtx(
7329
7398
  childPlaySlot?.release();
7330
7399
  }
7331
7400
  },
7332
- {
7333
- forceFailedRefresh: true,
7334
- repairRunningReceiptForSameRunAfterWaitTimeout: true,
7335
- runningReceiptWaitMaxAttempts:
7336
- resolveRuntimeToolReceiptWaitMaxAttempts(input),
7337
- },
7338
7401
  );
7339
7402
  },
7340
7403
  async fetch(
@@ -7615,13 +7678,6 @@ async function handleRunInline(
7615
7678
  const probeStartedAt = nowMs();
7616
7679
  await probeHarnessOnce(env, runPrefix);
7617
7680
  traceInline('inline.probe_harness', probeStartedAt);
7618
- if (!req.inlineChildRunRegistered) {
7619
- const registerStartedAt = nowMs();
7620
- await registerInlineChildRun(req);
7621
- traceInline('inline.register_child_run', registerStartedAt);
7622
- } else {
7623
- traceInline('inline.register_child_run', nowMs(), { skipped: true });
7624
- }
7625
7681
  const executeStartedAt = nowMs();
7626
7682
  const output = await executeRunRequest(
7627
7683
  req,
@@ -7667,46 +7723,6 @@ async function handleRunInline(
7667
7723
  }
7668
7724
  }
7669
7725
 
7670
- async function registerInlineChildRun(req: RunRequest): Promise<void> {
7671
- const snapshot = isRecord(req.contractSnapshot) ? req.contractSnapshot : {};
7672
- const artifactMetadata = isRecord(snapshot.artifactMetadata)
7673
- ? snapshot.artifactMetadata
7674
- : {};
7675
- const governance = req.playCallGovernance;
7676
- await postRuntimeApi(req.baseUrl, req.executorToken, {
7677
- action: 'start_inline_child_run',
7678
- playName: req.playName,
7679
- runId: req.runId,
7680
- parentRunId: governance?.parentRunId,
7681
- rootRunId: governance?.rootRunId,
7682
- workflowFamilyKey:
7683
- governance?.rootRunId ?? governance?.parentRunId ?? req.runId,
7684
- artifactStorageKey:
7685
- typeof artifactMetadata.storageKey === 'string'
7686
- ? artifactMetadata.storageKey
7687
- : undefined,
7688
- artifactHash:
7689
- typeof artifactMetadata.artifactHash === 'string'
7690
- ? artifactMetadata.artifactHash
7691
- : undefined,
7692
- graphHash:
7693
- typeof artifactMetadata.graphHash === 'string'
7694
- ? artifactMetadata.graphHash
7695
- : undefined,
7696
- runtimeBackend: 'workers_edge',
7697
- schedulerBackend: 'inline_child',
7698
- executionProfile: 'workers_edge',
7699
- maxCreditsPerRun: extractMaxCreditsPerRun(req.contractSnapshot),
7700
- staticPipeline: snapshot.staticPipeline ?? null,
7701
- source:
7702
- snapshot.source === 'published' ||
7703
- snapshot.source === 'ad_hoc' ||
7704
- snapshot.source === 'draft'
7705
- ? snapshot.source
7706
- : 'published',
7707
- });
7708
- }
7709
-
7710
7726
  /** Cap on run log lines retained in the terminal output compatibility shape. */
7711
7727
  const RUN_LOG_BUFFER_LIMIT = 500;
7712
7728
  /** Min wall-clock interval between live run-ledger flushes during a run. */
@@ -337,6 +337,21 @@ function countToolSubstep(substep: PlayStaticSubstep): MapToolStats {
337
337
  minProviderBranchCoalescedCount(branchStats),
338
338
  ];
339
339
  }
340
+ if (substep.kind === 'loop') {
341
+ // The static extractor records every switch arm in `steps` when a loop
342
+ // walks a fixed provider list. Each arm can execute on a different
343
+ // iteration, so using the conditional's max-one-branch estimate would
344
+ // budget a seven-provider waterfall as one provider call per row.
345
+ return substep.steps.reduce<MapToolStats>((stats, step) => {
346
+ const stepStats =
347
+ step.type === 'control_flow' &&
348
+ step.kind === 'conditional' &&
349
+ step.steps.length > 0
350
+ ? countToolSubsteps(step.steps)
351
+ : countToolSubstep(step);
352
+ return addStats(stats, stepStats);
353
+ }, [0, 0, 0, 0, 0]);
354
+ }
340
355
  return countToolSubsteps(substep.steps);
341
356
  }
342
357
 
@@ -0,0 +1,90 @@
1
+ type ToolTrace = {
2
+ phase: string;
3
+ ms?: number;
4
+ extra?: Record<string, unknown>;
5
+ };
6
+
7
+ type Totals = Record<string, number>;
8
+
9
+ function add(totals: Totals, key: string, ms: number | undefined): void {
10
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return;
11
+ totals[key] = (totals[key] ?? 0) + ms;
12
+ }
13
+
14
+ /**
15
+ * Opt-in map profiler. It aggregates in memory and produces one terminal
16
+ * trace, so normal runs do not pay per-row/per-tool telemetry cost.
17
+ */
18
+ export class MapLatencyProfile {
19
+ private readonly phaseWorkMs: Totals = {};
20
+ private readonly toolWorkMs: Totals = {};
21
+ private readonly counts: Totals = {};
22
+
23
+ constructor(
24
+ readonly enabled: boolean,
25
+ private readonly mapName: string,
26
+ private readonly startedAtMs: number,
27
+ ) {}
28
+
29
+ recordPhase(phase: string, ms: number, count = 1): void {
30
+ if (!this.enabled) return;
31
+ add(this.phaseWorkMs, phase, ms);
32
+ this.counts[`${phase}_count`] =
33
+ (this.counts[`${phase}_count`] ?? 0) + count;
34
+ }
35
+
36
+ recordRowQueue(ms: number): void {
37
+ this.recordPhase('row_slot_queue', ms);
38
+ }
39
+
40
+ recordRowExecution(ms: number): void {
41
+ this.recordPhase('row_execution', ms);
42
+ }
43
+
44
+ recordToolTrace(trace: ToolTrace): void {
45
+ if (!this.enabled) return;
46
+ switch (trace.phase) {
47
+ case 'runner.tool.request':
48
+ add(this.toolWorkMs, 'request_end_to_end', trace.ms);
49
+ this.counts.tool_requests = (this.counts.tool_requests ?? 0) + 1;
50
+ break;
51
+ case 'runner.tool.drain':
52
+ add(this.toolWorkMs, 'drain', trace.ms);
53
+ this.counts.tool_drains = (this.counts.tool_drains ?? 0) + 1;
54
+ break;
55
+ case 'runner.tool.group':
56
+ add(this.toolWorkMs, 'group', trace.ms);
57
+ this.counts.tool_groups = (this.counts.tool_groups ?? 0) + 1;
58
+ break;
59
+ case 'runner.tool.receipt_prepare':
60
+ add(this.toolWorkMs, 'receipt_prepare', trace.ms);
61
+ break;
62
+ case 'runner.tool.admission':
63
+ add(this.toolWorkMs, 'admission_wait', trace.ms);
64
+ break;
65
+ case 'runner.tool.provider':
66
+ add(this.toolWorkMs, 'provider_execute', trace.ms);
67
+ break;
68
+ case 'runner.tool.receipt_complete':
69
+ add(this.toolWorkMs, 'receipt_complete', trace.ms);
70
+ break;
71
+ default:
72
+ return;
73
+ }
74
+ }
75
+
76
+ summary(endedAtMs: number, input: { chunks: number; rows: number }) {
77
+ return {
78
+ mapName: this.mapName,
79
+ mapWallMs: Math.max(0, endedAtMs - this.startedAtMs),
80
+ chunks: input.chunks,
81
+ rows: input.rows,
82
+ // These are summed work durations. They can exceed mapWallMs when rows
83
+ // or provider calls overlap; the terminal trace must not mislabel them
84
+ // as a wall-clock decomposition.
85
+ phaseWorkMs: this.phaseWorkMs,
86
+ toolWorkMs: this.toolWorkMs,
87
+ counts: this.counts,
88
+ };
89
+ }
90
+ }