deepline 0.1.210 → 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.
@@ -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,
@@ -4598,6 +4599,11 @@ function createMinimalWorkerCtx(
4598
4599
  opts?: WorkerMapOptions,
4599
4600
  ): Promise<unknown> => {
4600
4601
  const mapStartedAt = nowMs();
4602
+ const mapLatencyProfile = new MapLatencyProfile(
4603
+ req.testPolicyOverrides?.mapLatencyProfile === true,
4604
+ name,
4605
+ mapStartedAt,
4606
+ );
4601
4607
  const mapNodeId = `map:${name}`;
4602
4608
  const inputRows = rows;
4603
4609
  const rowCountHint = datasetRowCountHint(inputRows);
@@ -4935,6 +4941,7 @@ function createMinimalWorkerCtx(
4935
4941
  blockedRows: prepared.blockedRows.length,
4936
4942
  },
4937
4943
  });
4944
+ mapLatencyProfile.recordPhase('prepare_rows', nowMs() - prepareStartedAt);
4938
4945
  const progressTotalRows = rowCountHint ?? chunkRows.length;
4939
4946
  const preparedCompletedRows = Math.min(
4940
4947
  progressTotalRows,
@@ -5215,7 +5222,10 @@ function createMinimalWorkerCtx(
5215
5222
  deserializeDurableValue: deserializeDurableStepValue,
5216
5223
  nowMs,
5217
5224
  batchGraceMs: req.testPolicyOverrides?.batchGraceMs,
5218
- recordPerfTrace: (trace) => recordRunnerPerfTrace({ req, ...trace }),
5225
+ recordPerfTrace: (trace) => {
5226
+ mapLatencyProfile.recordToolTrace(trace);
5227
+ recordRunnerPerfTrace({ req, ...trace });
5228
+ },
5219
5229
  abortSignal,
5220
5230
  onRequestsSettled: reportSettledToolRequests,
5221
5231
  callbacks,
@@ -5505,9 +5515,16 @@ function createMinimalWorkerCtx(
5505
5515
  if (circuitError) throw circuitError;
5506
5516
  const myIndex = idx++;
5507
5517
  if (myIndex >= rowsToExecute.length) return;
5518
+ const rowQueueStartedAt = mapLatencyProfile.enabled ? nowMs() : 0;
5508
5519
  const rowSlot = await governor.acquireRowSlot({
5509
5520
  signal: abortSignal,
5510
5521
  });
5522
+ if (mapLatencyProfile.enabled) {
5523
+ mapLatencyProfile.recordRowQueue(nowMs() - rowQueueStartedAt);
5524
+ }
5525
+ const rowExecutionStartedAt = mapLatencyProfile.enabled
5526
+ ? nowMs()
5527
+ : 0;
5511
5528
  let rowMarkedActive = false;
5512
5529
  try {
5513
5530
  startedExecutedRows += 1;
@@ -5744,6 +5761,11 @@ function createMinimalWorkerCtx(
5744
5761
  // single clean run failure.
5745
5762
  }
5746
5763
  } finally {
5764
+ if (mapLatencyProfile.enabled) {
5765
+ mapLatencyProfile.recordRowExecution(
5766
+ nowMs() - rowExecutionStartedAt,
5767
+ );
5768
+ }
5747
5769
  if (rowMarkedActive) {
5748
5770
  activeExecutedRows = Math.max(0, activeExecutedRows - 1);
5749
5771
  reportExecutionHeartbeat(false);
@@ -5796,6 +5818,10 @@ function createMinimalWorkerCtx(
5796
5818
  concurrency,
5797
5819
  },
5798
5820
  });
5821
+ mapLatencyProfile.recordPhase(
5822
+ 'worker_execution',
5823
+ nowMs() - workersStartedAt,
5824
+ );
5799
5825
  const buildRowFailureSamples = () =>
5800
5826
  failedRowEntries
5801
5827
  .map((failure, executedIndex) =>
@@ -5923,6 +5949,10 @@ function createMinimalWorkerCtx(
5923
5949
  rowsToExecute: rowsToExecute.length,
5924
5950
  },
5925
5951
  });
5952
+ mapLatencyProfile.recordPhase(
5953
+ 'persist_rows',
5954
+ nowMs() - persistRowsStartedAt,
5955
+ );
5926
5956
  } catch (error) {
5927
5957
  if (
5928
5958
  workflowStep &&
@@ -6028,6 +6058,7 @@ function createMinimalWorkerCtx(
6028
6058
  ms: nowMs() - hashStartedAt,
6029
6059
  extra: { mapName: name, chunkIndex, rows: out.length },
6030
6060
  });
6061
+ mapLatencyProfile.recordPhase('hash', nowMs() - hashStartedAt);
6031
6062
  recordRunnerPerfTrace({
6032
6063
  req,
6033
6064
  phase: 'runner.map_chunk.total',
@@ -6643,6 +6674,27 @@ function createMinimalWorkerCtx(
6643
6674
  chunks: dispatchResult.chunksExecuted,
6644
6675
  },
6645
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
+ }
6646
6698
  return dataset;
6647
6699
  };
6648
6700
 
@@ -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
+ }
@@ -2000,8 +2000,14 @@ export class WorkerToolBatchScheduler {
2000
2000
  toolId: string,
2001
2001
  requests: WorkerToolBatchRequest[],
2002
2002
  ): Promise<void> {
2003
+ const receiptPrepareStartedAt = this.options.nowMs();
2003
2004
  const { claimedRequests, deferredClaimedRequests } =
2004
2005
  await this.prepareDurableToolRequests(requests);
2006
+ this.options.recordPerfTrace?.({
2007
+ phase: 'runner.tool.receipt_prepare',
2008
+ ms: this.options.nowMs() - receiptPrepareStartedAt,
2009
+ extra: { toolId, requests: requests.length },
2010
+ });
2005
2011
  await this.executeClaimedToolRequests(
2006
2012
  toolId,
2007
2013
  requests.length,
@@ -2071,6 +2077,7 @@ export class WorkerToolBatchScheduler {
2071
2077
  // charges tool budget, holds a global tool-concurrency slot, and
2072
2078
  // applies per-(org,provider) pacing before the call runs.
2073
2079
  let slot: { release: () => void };
2080
+ const admissionStartedAt = this.options.nowMs();
2074
2081
  try {
2075
2082
  slot = await this.options.governor.acquireToolSlot(toolId, {
2076
2083
  signal: this.options.abortSignal,
@@ -2082,6 +2089,11 @@ export class WorkerToolBatchScheduler {
2082
2089
  );
2083
2090
  return;
2084
2091
  }
2092
+ this.options.recordPerfTrace?.({
2093
+ phase: 'runner.tool.admission',
2094
+ ms: this.options.nowMs() - admissionStartedAt,
2095
+ extra: { toolId, requests: 1 },
2096
+ });
2085
2097
  try {
2086
2098
  let result: unknown;
2087
2099
  try {
@@ -2100,59 +2112,68 @@ export class WorkerToolBatchScheduler {
2100
2112
  claimed,
2101
2113
  })
2102
2114
  : null;
2103
- result = await executeWithWorkerReceiptHeartbeats({
2104
- playName: this.options.req.playName,
2105
- runId: this.options.req.runId,
2106
- runAttempt: this.options.req.runAttempt ?? 0,
2107
- ...(this.options.receiptStore
2108
- ? { receiptStore: this.options.receiptStore }
2109
- : {}),
2110
- budgetMeter: this.options.budgetMeter,
2111
- heartbeatIntervalMs:
2112
- this.options.runtimeReceiptHeartbeatIntervalMs,
2113
- targets: claimed.receiptKey
2114
- ? [
2115
- {
2116
- key: claimed.receiptKey,
2117
- leaseId: claimed.receiptLeaseId,
2118
- },
2119
- ]
2120
- : [],
2121
- execute: () =>
2122
- this.options.executeTool({
2123
- id: request.id,
2124
- toolId,
2125
- input: request.input,
2126
- workflowStep: request.workflowStep,
2127
- callbacks: this.options.callbacks,
2128
- onProviderBackpressure: (retryAfterMs) =>
2129
- this.reportBackpressure(toolId, retryAfterMs),
2130
- onRetryAttempt: () =>
2131
- this.options.governor.chargeBudget('retry'),
2132
- transientHttpRetrySafe:
2133
- toolContract?.retrySafeTransientHttp === true,
2134
- ...(durableReceiptKey
2135
- ? {
2136
- durableCallReceiptKey: durableReceiptKey,
2137
- executionAuthScopeDigest:
2138
- request.executionAuthScopeDigest,
2139
- providerIdempotencyKey:
2140
- providerIdempotencyKeyForClaimed({
2141
- req: this.options.req,
2142
- claimed,
2143
- }),
2144
- }
2145
- : {}),
2146
- runtimeTimeoutMs: resolveWorkerToolRuntimeTimeoutMs(
2147
- [claimed],
2148
- {
2149
- resolveOwnerTimeoutMs: (request) =>
2150
- request.runtimeTimeoutMs,
2151
- },
2152
- ),
2153
- directOptions: request.directOptions,
2154
- }),
2155
- });
2115
+ const providerStartedAt = this.options.nowMs();
2116
+ try {
2117
+ result = await executeWithWorkerReceiptHeartbeats({
2118
+ playName: this.options.req.playName,
2119
+ runId: this.options.req.runId,
2120
+ runAttempt: this.options.req.runAttempt ?? 0,
2121
+ ...(this.options.receiptStore
2122
+ ? { receiptStore: this.options.receiptStore }
2123
+ : {}),
2124
+ budgetMeter: this.options.budgetMeter,
2125
+ heartbeatIntervalMs:
2126
+ this.options.runtimeReceiptHeartbeatIntervalMs,
2127
+ targets: claimed.receiptKey
2128
+ ? [
2129
+ {
2130
+ key: claimed.receiptKey,
2131
+ leaseId: claimed.receiptLeaseId,
2132
+ },
2133
+ ]
2134
+ : [],
2135
+ execute: () =>
2136
+ this.options.executeTool({
2137
+ id: request.id,
2138
+ toolId,
2139
+ input: request.input,
2140
+ workflowStep: request.workflowStep,
2141
+ callbacks: this.options.callbacks,
2142
+ onProviderBackpressure: (retryAfterMs) =>
2143
+ this.reportBackpressure(toolId, retryAfterMs),
2144
+ onRetryAttempt: () =>
2145
+ this.options.governor.chargeBudget('retry'),
2146
+ transientHttpRetrySafe:
2147
+ toolContract?.retrySafeTransientHttp === true,
2148
+ ...(durableReceiptKey
2149
+ ? {
2150
+ durableCallReceiptKey: durableReceiptKey,
2151
+ executionAuthScopeDigest:
2152
+ request.executionAuthScopeDigest,
2153
+ providerIdempotencyKey:
2154
+ providerIdempotencyKeyForClaimed({
2155
+ req: this.options.req,
2156
+ claimed,
2157
+ }),
2158
+ }
2159
+ : {}),
2160
+ runtimeTimeoutMs: resolveWorkerToolRuntimeTimeoutMs(
2161
+ [claimed],
2162
+ {
2163
+ resolveOwnerTimeoutMs: (request) =>
2164
+ request.runtimeTimeoutMs,
2165
+ },
2166
+ ),
2167
+ directOptions: request.directOptions,
2168
+ }),
2169
+ });
2170
+ } finally {
2171
+ this.options.recordPerfTrace?.({
2172
+ phase: 'runner.tool.provider',
2173
+ ms: this.options.nowMs() - providerStartedAt,
2174
+ extra: { toolId, requests: 1 },
2175
+ });
2176
+ }
2156
2177
  } catch (error) {
2157
2178
  if (error instanceof RuntimeLeaseLostError) {
2158
2179
  this.rejectRequests(claimed, error);
@@ -2173,10 +2194,16 @@ export class WorkerToolBatchScheduler {
2173
2194
  return;
2174
2195
  }
2175
2196
  try {
2197
+ const receiptCompleteStartedAt = this.options.nowMs();
2176
2198
  this.settleRequests(
2177
2199
  claimed,
2178
2200
  await this.completeDurableToolRequest(claimed, result),
2179
2201
  );
2202
+ this.options.recordPerfTrace?.({
2203
+ phase: 'runner.tool.receipt_complete',
2204
+ ms: this.options.nowMs() - receiptCompleteStartedAt,
2205
+ extra: { toolId, requests: 1 },
2206
+ });
2180
2207
  } catch (receiptError) {
2181
2208
  this.rejectRequests(claimed, receiptError);
2182
2209
  }
@@ -2261,12 +2288,22 @@ export class WorkerToolBatchScheduler {
2261
2288
  }
2262
2289
  // One provider call per batch -> one tool slot (budget + global
2263
2290
  // concurrency + per-(org,provider) pacing) around the whole batch.
2291
+ const admissionStartedAt = this.options.nowMs();
2264
2292
  const slot = await this.options.governor.acquireToolSlot(
2265
2293
  batch.batchOperation,
2266
2294
  {
2267
2295
  signal: this.options.abortSignal,
2268
2296
  },
2269
2297
  );
2298
+ this.options.recordPerfTrace?.({
2299
+ phase: 'runner.tool.admission',
2300
+ ms: this.options.nowMs() - admissionStartedAt,
2301
+ extra: {
2302
+ toolId: batch.batchOperation,
2303
+ requests: batch.memberRequests.length,
2304
+ batched: true,
2305
+ },
2306
+ });
2270
2307
  try {
2271
2308
  await this.markAdmittedToolRequestsRunning(batch.memberRequests);
2272
2309
  this.options.budgetMeter?.count('provider');
@@ -2281,54 +2318,69 @@ export class WorkerToolBatchScheduler {
2281
2318
  batchOperation: batch.batchOperation,
2282
2319
  receiptKeys,
2283
2320
  });
2284
- return await executeWithWorkerReceiptHeartbeats({
2285
- playName: this.options.req.playName,
2286
- runId: this.options.req.runId,
2287
- runAttempt: this.options.req.runAttempt ?? 0,
2288
- ...(this.options.receiptStore
2289
- ? { receiptStore: this.options.receiptStore }
2290
- : {}),
2291
- budgetMeter: this.options.budgetMeter,
2292
- heartbeatIntervalMs: this.options.runtimeReceiptHeartbeatIntervalMs,
2293
- targets: batch.memberRequests.map((member) => ({
2294
- key: member.receiptKey,
2295
- leaseId: member.receiptLeaseId,
2296
- })),
2297
- execute: () =>
2298
- this.options.executeTool({
2299
- id: `batch:${batch.memberRequests.map((request) => request.request.id).join('|')}`,
2300
- toolId: batch.batchOperation,
2301
- input: batch.batchPayload,
2302
- callbacks: this.options.callbacks,
2303
- onProviderBackpressure: (retryAfterMs) =>
2304
- this.reportBackpressure(input.sourceToolId, retryAfterMs),
2305
- onRetryAttempt: () =>
2306
- this.options.governor.chargeBudget('retry'),
2307
- transientHttpRetrySafe:
2308
- toolContract?.retrySafeTransientHttp === true,
2309
- durableCallReceiptKey: aggregateReceiptKey,
2310
- executionAuthScopeDigest:
2311
- batch.memberRequests[0]?.request.executionAuthScopeDigest ??
2312
- null,
2313
- providerIdempotencyKey: buildBatchProviderIdempotencyKey({
2314
- aggregateReceiptKey,
2315
- receiptKeys,
2316
- providerIdempotencyKeys: batch.memberRequests.map((member) =>
2317
- providerIdempotencyKeyForClaimed({
2318
- req: this.options.req,
2319
- claimed: member,
2320
- }),
2321
+ const providerStartedAt = this.options.nowMs();
2322
+ try {
2323
+ return await executeWithWorkerReceiptHeartbeats({
2324
+ playName: this.options.req.playName,
2325
+ runId: this.options.req.runId,
2326
+ runAttempt: this.options.req.runAttempt ?? 0,
2327
+ ...(this.options.receiptStore
2328
+ ? { receiptStore: this.options.receiptStore }
2329
+ : {}),
2330
+ budgetMeter: this.options.budgetMeter,
2331
+ heartbeatIntervalMs:
2332
+ this.options.runtimeReceiptHeartbeatIntervalMs,
2333
+ targets: batch.memberRequests.map((member) => ({
2334
+ key: member.receiptKey,
2335
+ leaseId: member.receiptLeaseId,
2336
+ })),
2337
+ execute: () =>
2338
+ this.options.executeTool({
2339
+ id: `batch:${batch.memberRequests.map((request) => request.request.id).join('|')}`,
2340
+ toolId: batch.batchOperation,
2341
+ input: batch.batchPayload,
2342
+ callbacks: this.options.callbacks,
2343
+ onProviderBackpressure: (retryAfterMs) =>
2344
+ this.reportBackpressure(input.sourceToolId, retryAfterMs),
2345
+ onRetryAttempt: () =>
2346
+ this.options.governor.chargeBudget('retry'),
2347
+ transientHttpRetrySafe:
2348
+ toolContract?.retrySafeTransientHttp === true,
2349
+ durableCallReceiptKey: aggregateReceiptKey,
2350
+ executionAuthScopeDigest:
2351
+ batch.memberRequests[0]?.request.executionAuthScopeDigest ??
2352
+ null,
2353
+ providerIdempotencyKey: buildBatchProviderIdempotencyKey({
2354
+ aggregateReceiptKey,
2355
+ receiptKeys,
2356
+ providerIdempotencyKeys: batch.memberRequests.map(
2357
+ (member) =>
2358
+ providerIdempotencyKeyForClaimed({
2359
+ req: this.options.req,
2360
+ claimed: member,
2361
+ }),
2362
+ ),
2363
+ }),
2364
+ runtimeTimeoutMs: resolveWorkerToolRuntimeTimeoutMs(
2365
+ batch.memberRequests,
2366
+ {
2367
+ resolveOwnerTimeoutMs: (request) =>
2368
+ request.runtimeTimeoutMs,
2369
+ },
2321
2370
  ),
2322
2371
  }),
2323
- runtimeTimeoutMs: resolveWorkerToolRuntimeTimeoutMs(
2324
- batch.memberRequests,
2325
- {
2326
- resolveOwnerTimeoutMs: (request) =>
2327
- request.runtimeTimeoutMs,
2328
- },
2329
- ),
2330
- }),
2331
- });
2372
+ });
2373
+ } finally {
2374
+ this.options.recordPerfTrace?.({
2375
+ phase: 'runner.tool.provider',
2376
+ ms: this.options.nowMs() - providerStartedAt,
2377
+ extra: {
2378
+ toolId: batch.batchOperation,
2379
+ requests: batch.memberRequests.length,
2380
+ batched: true,
2381
+ },
2382
+ });
2383
+ }
2332
2384
  } finally {
2333
2385
  slot.release();
2334
2386
  }
@@ -2389,6 +2441,7 @@ export class WorkerToolBatchScheduler {
2389
2441
  : entry.request.memberRequests.map(() => null);
2390
2442
  let completedResults: WorkerToolCompletionResult[];
2391
2443
  try {
2444
+ const receiptCompleteStartedAt = this.options.nowMs();
2392
2445
  completedResults = await this.completeDurableToolRequests(
2393
2446
  entry.request.memberRequests.map((claimed, index) => ({
2394
2447
  claimed,
@@ -2399,6 +2452,15 @@ export class WorkerToolBatchScheduler {
2399
2452
  ),
2400
2453
  })),
2401
2454
  );
2455
+ this.options.recordPerfTrace?.({
2456
+ phase: 'runner.tool.receipt_complete',
2457
+ ms: this.options.nowMs() - receiptCompleteStartedAt,
2458
+ extra: {
2459
+ toolId: entry.request.batchOperation,
2460
+ requests: entry.request.memberRequests.length,
2461
+ batched: true,
2462
+ },
2463
+ });
2402
2464
  } catch (receiptError) {
2403
2465
  for (const claimed of entry.request.memberRequests) {
2404
2466
  this.rejectRequests(claimed, receiptError);
@@ -173,6 +173,8 @@ export type PlayBindings = {
173
173
  * older clients can continue to register revisions during the migration.
174
174
  */
175
175
  description?: string;
176
+ /** Allow compilers to bundle this named handler directly without a child run. */
177
+ inline?: boolean;
176
178
  /** Optional per-run billing controls enforced by the runtime. */
177
179
  billing?: {
178
180
  /** Stop the run before a billed action would push total run credits above this cap. */
@@ -106,10 +106,10 @@ export const SDK_RELEASE = {
106
106
  // 0.1.154 removes the short-lived generated enrich StepOptions recompute
107
107
  // fields shipped in 0.1.153.
108
108
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
109
- version: '0.1.210',
109
+ version: '0.1.211',
110
110
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
111
111
  supportPolicy: {
112
- latest: '0.1.210',
112
+ latest: '0.1.211',
113
113
  minimumSupported: '0.1.53',
114
114
  deprecatedBelow: '0.1.53',
115
115
  commandMinimumSupported: [