deepline 0.3.289 → 0.3.291

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 (64) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +8 -6
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/sdk/src/runs/overview.ts +16 -6
  4. package/dist/bundling-sources/sdk/src/types.ts +21 -2
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +41 -11
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +317 -63
  7. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +10 -2
  8. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +3 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +1 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +36 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +19 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/worker-terminal-projection.ts +6 -5
  14. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +12 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/run-event-catalog.ts +2 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +74 -33
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +73 -3
  19. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +39 -2
  20. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +72 -9
  21. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +2 -3
  22. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +77 -10
  23. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +1 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +2 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-pool-options.ts +145 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +17 -9
  27. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -139
  28. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +19 -5
  29. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +176 -18
  30. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +7 -5
  31. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1404 -478
  32. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +55 -25
  33. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-dispatch-authority.ts +97 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +34 -15
  35. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +558 -57
  36. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +10 -1
  37. package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +6 -1
  38. package/dist/bundling-sources/shared_libs/play-runtime/test-admission-batching.ts +103 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +22 -5
  40. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +35 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +68 -8
  42. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +2 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/zerobounce-batching.ts +75 -28
  44. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +2 -0
  45. package/dist/cli/index.js +195 -582
  46. package/dist/cli/index.mjs +195 -582
  47. package/dist/{client-FR_u0dxT.d.ts → client-Ct4jmvAg.d.ts} +29 -8
  48. package/dist/{client-phK1Ixcl.d.mts → client-Ct9GKrwu.d.mts} +29 -8
  49. package/dist/{compiler-manifest-9SQtUuKb.d.mts → compiler-manifest-qtTNYvBl.d.mts} +2 -0
  50. package/dist/{compiler-manifest-9SQtUuKb.d.ts → compiler-manifest-qtTNYvBl.d.ts} +2 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/index.d.ts +4 -4
  53. package/dist/index.js +14 -5
  54. package/dist/index.mjs +14 -5
  55. package/dist/install-integrity.json +7 -4
  56. package/dist/plays/bundle-play-file.d.mts +2 -2
  57. package/dist/plays/bundle-play-file.d.ts +2 -2
  58. package/dist/plays/run-request.d.mts +2 -2
  59. package/dist/plays/run-request.d.ts +2 -2
  60. package/dist/release.d.mts +1 -1
  61. package/dist/release.d.ts +1 -1
  62. package/dist/release.js +1 -1
  63. package/dist/release.mjs +1 -1
  64. package/package.json +1 -1
@@ -216,7 +216,11 @@ import {
216
216
  ToolExecuteAuthScopeChangedError,
217
217
  type ToolExecuteHttpFailureOutcome,
218
218
  } from './tool-execute-retry-policy';
219
- import { createToolHttpError } from './tool-http-errors';
219
+ import {
220
+ createToolHttpError,
221
+ attachToolCallFailureEvidenceFromResponse,
222
+ readToolCallFailureEvidence,
223
+ } from './tool-http-errors';
220
224
  import {
221
225
  describeTransportError,
222
226
  transportGatewayOriginForDiagnostic,
@@ -255,6 +259,7 @@ import {
255
259
  } from './play-artifact-runtime-contract';
256
260
  import {
257
261
  LOG_LEVELS,
262
+ classifyLogLine,
258
263
  formatCtxFetchHttpFailureDiagnostic,
259
264
  tagLogEntry,
260
265
  tagLogLevel,
@@ -446,7 +451,11 @@ import {
446
451
  RunLogBuffer,
447
452
  RUNNER_LOG_CONTEXT_MAX_BYTES,
448
453
  } from './run-log-buffer';
449
- import { normalizePlayLogContext, type PlayLogContext } from './log-context';
454
+ import {
455
+ normalizePlayLogContext,
456
+ type PlayLogContext,
457
+ type PlayLogMetadata,
458
+ } from './log-context';
450
459
  import type { AsyncPlayRunRef } from '../plays/authoring-contract';
451
460
  import { admitAsyncPlayLineage } from './async-play-invocation';
452
461
  import {
@@ -536,6 +545,11 @@ type InlineCompositionStore = {
536
545
  };
537
546
  const inlineCompositionContext =
538
547
  new AsyncLocalStorage<InlineCompositionStore>();
548
+ // Authored scope is async-local: concurrent steps and rows cannot steal attribution.
549
+ const authoredStepContext = new AsyncLocalStorage<{
550
+ context: PlayContextImpl;
551
+ stepId: string;
552
+ }>();
539
553
  const toolExecutionOverrides = new AsyncLocalStorage<{ force: boolean }>();
540
554
  const PROGRESS_HEARTBEAT_INTERVAL_MS = 1_000;
541
555
  const PURE_JS_HEARTBEAT_ROW_INTERVAL = 250;
@@ -1806,7 +1820,27 @@ type NativeBatchBodyAttribution = {
1806
1820
  release(): void;
1807
1821
  };
1808
1822
 
1823
+ type ToolBatchAdmissionObserver = {
1824
+ pending(requests: readonly ToolCallRequest[]): void;
1825
+ dispatched(requests: readonly ToolCallRequest[]): void;
1826
+ };
1827
+
1828
+ function settleToolBatchGroups(
1829
+ groups: ToolCallRequest[][],
1830
+ execute: (requests: ToolCallRequest[]) => Promise<void>,
1831
+ observer?: ToolBatchAdmissionObserver,
1832
+ ): Promise<PromiseSettledResult<void>[]> {
1833
+ return Promise.allSettled(
1834
+ groups.map((requests) =>
1835
+ execute(requests).finally(() => observer?.dispatched(requests)),
1836
+ ),
1837
+ );
1838
+ }
1839
+
1809
1840
  type ToolExecutionApiOptions = {
1841
+ /** Runner-local scheduling signals; never part of a provider/wire payload. */
1842
+ onProviderAdmissionPending?: () => void;
1843
+ onProviderDispatched?: () => void;
1810
1844
  timeoutMs?: number;
1811
1845
  abortSignal?: AbortSignal;
1812
1846
  durableCallReceiptKey?: string | null;
@@ -6367,11 +6401,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6367
6401
  ? {
6368
6402
  relayReceiptOutput: {
6369
6403
  receiptKey: input.resultReceiptKey,
6370
- execution: toolExecutionMetadataForOutcome({
6371
- kind: 'live',
6372
- cacheKey: input.request.cacheKey,
6373
- receiptKey: input.request.receiptKey,
6374
- }),
6404
+ execution: {
6405
+ ...toolExecutionMetadataForOutcome({
6406
+ kind: 'live',
6407
+ cacheKey: input.request.cacheKey,
6408
+ receiptKey: input.request.receiptKey,
6409
+ }),
6410
+ resultReceiptKey: input.resultReceiptKey,
6411
+ },
6375
6412
  metadata: await this.resolveToolResultMetadata(
6376
6413
  input.dispatchToolId,
6377
6414
  ),
@@ -6411,11 +6448,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6411
6448
  meta: execution.meta,
6412
6449
  billing: execution.billing,
6413
6450
  requestInput: input.dispatchPayload,
6414
- execution: toolExecutionMetadataForOutcome({
6415
- kind: 'live',
6416
- cacheKey: input.request.cacheKey,
6417
- receiptKey: input.request.receiptKey,
6418
- }),
6451
+ execution: {
6452
+ ...toolExecutionMetadataForOutcome({
6453
+ kind: 'live',
6454
+ cacheKey: input.request.cacheKey,
6455
+ receiptKey: input.request.receiptKey,
6456
+ }),
6457
+ ...(!alwaysFresh && input.resultReceiptKey
6458
+ ? { resultReceiptKey: input.resultReceiptKey }
6459
+ : {}),
6460
+ },
6419
6461
  });
6420
6462
  // First sub-phase of `resultDelivery`: the body was parsed above, and
6421
6463
  // this is the instant the row's result actually exists. Everything
@@ -7032,7 +7074,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
7032
7074
 
7033
7075
  private async serializeCellValue(value: unknown): Promise<unknown> {
7034
7076
  const serialized = isToolExecuteResult(value)
7035
- ? serializeToolExecuteResult(value)
7077
+ ? serializeToolExecuteResult(value, { includeCallEvidence: false })
7036
7078
  : await this.serializeDatasetCells(value);
7037
7079
  try {
7038
7080
  stringifyPostgresJson(serialized);
@@ -7053,7 +7095,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
7053
7095
  const visiting = new WeakSet<object>();
7054
7096
  const visit = async (current: unknown): Promise<unknown> => {
7055
7097
  if (isToolExecuteResult(current)) {
7056
- return serializeToolExecuteResult(current);
7098
+ return serializeToolExecuteResult(current, {
7099
+ includeCallEvidence: false,
7100
+ });
7057
7101
  }
7058
7102
  if (isPlayDataset(current)) {
7059
7103
  return await serializePlayDatasetCell(current, visit);
@@ -7359,7 +7403,20 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
7359
7403
  input.metadata ??
7360
7404
  (await this.resolveToolResultMetadata(input.toolId)),
7361
7405
  execution: input.execution,
7362
- meta: input.meta,
7406
+ meta: {
7407
+ ...input.meta,
7408
+ ...(input.execution.source === 'live' &&
7409
+ input.requestInput &&
7410
+ !input.meta?.callEvidence
7411
+ ? {
7412
+ callEvidence: {
7413
+ input: this.secretRedactor.redact(input.requestInput),
7414
+ providerRequestsUnavailable:
7415
+ 'This response did not retain provider HTTP evidence.',
7416
+ },
7417
+ }
7418
+ : {}),
7419
+ },
7363
7420
  billing: input.billing,
7364
7421
  }),
7365
7422
  );
@@ -11829,8 +11886,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
11829
11886
  request.executionAuthScopeDigest ?? '',
11830
11887
  String(strategy.toBucketKey(request.input)),
11831
11888
  ].join('\u0000'),
11832
- readyCount: strategy.maxBatchSize,
11833
- maxGroupSize: this.governor.policy.concurrency.toolCalls,
11889
+ readyCount: Math.min(
11890
+ strategy.maxBatchSize,
11891
+ this.governor.policy.concurrency.toolCalls,
11892
+ ),
11893
+ maxGroupSize: Math.min(
11894
+ strategy.maxBatchSize,
11895
+ this.governor.policy.concurrency.toolCalls,
11896
+ ),
11834
11897
  coalesceWindowMs: this.toolCoalesceWindowMs(
11835
11898
  this.#options.toolBatchCoalesceWindowMs,
11836
11899
  TOOL_BATCH_COALESCE_WINDOW_MS,
@@ -12030,6 +12093,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12030
12093
 
12031
12094
  const requests: ToolCallRequest[] = [];
12032
12095
  const remaining: ToolCallRequest[] = [];
12096
+ const remainingLaneKeys = new Set<string>();
12033
12097
  const selectedByLane = new Map<string, number>();
12034
12098
  for (const request of this.toolCallQueue) {
12035
12099
  const lane = this.toolDispatchLane(request);
@@ -12043,12 +12107,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12043
12107
  selectedByLane.set(lane.key, selectedCount + 1);
12044
12108
  } else {
12045
12109
  remaining.push(request);
12110
+ remainingLaneKeys.add(lane.key);
12046
12111
  }
12047
12112
  }
12048
12113
  this.toolCallQueue = remaining;
12049
12114
  const selectedLaneKeys = new Set(selectedByLane.keys());
12050
12115
  for (const laneKey of selectedLaneKeys) {
12051
- this.toolDispatchQueuedAtByLane.delete(laneKey);
12116
+ if (!remainingLaneKeys.has(laneKey)) {
12117
+ this.toolDispatchQueuedAtByLane.delete(laneKey);
12118
+ }
12052
12119
  }
12053
12120
  return { requests, laneKeys: selectedLaneKeys, nextDeadlineMs };
12054
12121
  }
@@ -12258,6 +12325,33 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12258
12325
  // it never waits for unrelated sibling calls from the previous column.
12259
12326
  const inFlightToolExecutions = new Map<Promise<void>, ToolCallRequest[]>();
12260
12327
  const activeGroupsByLane = new Map<string, number>();
12328
+ // Keep a native batch lane together while its physical request is
12329
+ // preparing or waiting for provider admission. Release it at dispatch, so
12330
+ // provider residence does not serialize the next batch.
12331
+ const pendingAdmissionByLane = new Map<string, Set<string>>();
12332
+ const admissionObserver: ToolBatchAdmissionObserver = {
12333
+ pending: (requests) => {
12334
+ for (const request of requests) {
12335
+ if (!this.#options.getBatchOperationStrategy?.(request.toolId))
12336
+ continue;
12337
+ const key = this.toolDispatchLane(request).key;
12338
+ const pending = pendingAdmissionByLane.get(key) ?? new Set<string>();
12339
+ pending.add(request.callId);
12340
+ pendingAdmissionByLane.set(key, pending);
12341
+ }
12342
+ },
12343
+ dispatched: (requests) => {
12344
+ let changed = false;
12345
+ for (const request of requests) {
12346
+ const key = this.toolDispatchLane(request).key;
12347
+ const pending = pendingAdmissionByLane.get(key);
12348
+ if (!pending?.delete(request.callId)) continue;
12349
+ changed = true;
12350
+ if (pending.size === 0) pendingAdmissionByLane.delete(key);
12351
+ }
12352
+ if (changed) this.wakeToolDispatcher();
12353
+ },
12354
+ };
12261
12355
  const maxInFlightGroups = Math.max(
12262
12356
  1,
12263
12357
  Math.floor(
@@ -12319,6 +12413,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12319
12413
  .filter(([, count]) => count >= maxInFlightGroupsPerLane)
12320
12414
  .map(([laneKey]) => laneKey),
12321
12415
  );
12416
+ for (const key of pendingAdmissionByLane.keys()) {
12417
+ blockedLaneKeys.add(key);
12418
+ }
12322
12419
  const dispatchable =
12323
12420
  inFlightToolExecutions.size >= maxInFlightGroups
12324
12421
  ? {
@@ -12361,7 +12458,11 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12361
12458
  (activeGroupsByLane.get(laneKey) ?? 0) + 1,
12362
12459
  );
12363
12460
  }
12364
- const tracked = this.executeBatchedToolCalls(dispatchable.requests)
12461
+ admissionObserver.pending(dispatchable.requests);
12462
+ const tracked = this.executeBatchedToolCalls(
12463
+ dispatchable.requests,
12464
+ admissionObserver,
12465
+ )
12365
12466
  .catch(async (error) => {
12366
12467
  this.toolDispatcherFailure ??= error;
12367
12468
  this.rejectQueuedToolCalls(error);
@@ -12371,6 +12472,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12371
12472
  );
12372
12473
  })
12373
12474
  .finally(() => {
12475
+ admissionObserver.dispatched(dispatchable.requests);
12374
12476
  this.observeToolDispatchGroupRoundTrip(
12375
12477
  this.toolBatchDispatcherNowMs() - launchedAtMs,
12376
12478
  );
@@ -12517,6 +12619,54 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12517
12619
  };
12518
12620
  }
12519
12621
 
12622
+ private async saveToolFailureEvidence(input: {
12623
+ toolId: string;
12624
+ callKey: string;
12625
+ input: Record<string, unknown>;
12626
+ error: unknown;
12627
+ context: PlayLogContext;
12628
+ }): Promise<string | undefined> {
12629
+ if (!this.#options.completeRuntimeStepReceipt) return;
12630
+ try {
12631
+ // An immutable diagnostic record in the SAME receipt store, discovered
12632
+ // through runtime_observability.events. It is never an executable result
12633
+ // cache and cannot change provider admission or replay semantics.
12634
+ const receipt = await this.completeRuntimeStepReceipt(
12635
+ `${buildDurableToolReceiptPrefix({
12636
+ orgId: this.#options.orgId ?? 'unknown-org',
12637
+ toolId: 'call-evidence',
12638
+ })}${stableDigest(`${this.currentRunId}:${crypto.randomUUID()}`)}`,
12639
+ this.currentRunId,
12640
+ this.secretRedactor.redact({
12641
+ kind: 'tool_call_evidence',
12642
+ toolId: input.toolId,
12643
+ callKey: input.callKey,
12644
+ status: 'failed',
12645
+ context: input.context,
12646
+ callEvidence: readToolCallFailureEvidence(input.error) ?? {
12647
+ input: input.input,
12648
+ providerRequestsUnavailable:
12649
+ 'This failure did not retain provider HTTP evidence.',
12650
+ },
12651
+ error:
12652
+ serializeToolExecutionFailure(input.error) ??
12653
+ this.formatRuntimeError(input.error),
12654
+ }),
12655
+ COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
12656
+ );
12657
+ if (!receipt)
12658
+ throw new Error('Saved failure evidence was not acknowledged.');
12659
+ return receipt.key;
12660
+ } catch (error) {
12661
+ // Keep the original failure and never repeat a paid operation just to
12662
+ // repair observability. A missing record is explicit in retained logs.
12663
+ this.runtimeLog(
12664
+ `Saved call details unavailable: ${this.formatRuntimeError(error)}`,
12665
+ { level: 'warn' },
12666
+ );
12667
+ }
12668
+ }
12669
+
12520
12670
  private async executeTool(
12521
12671
  key: string,
12522
12672
  toolId: string,
@@ -12545,13 +12695,31 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12545
12695
  !isPlayRowExecutionSuspendedError(error) &&
12546
12696
  !(error instanceof PlayRowProviderBackpressureError)
12547
12697
  ) {
12698
+ const scope = authoredStepContext.getStore();
12699
+ const savedKey = await this.saveToolFailureEvidence({
12700
+ toolId,
12701
+ callKey: context.call_key,
12702
+ context: {
12703
+ callKey: context.call_key,
12704
+ toolId,
12705
+ callStatus: 'failed',
12706
+ ...(scope?.context === this ? { stepId: scope.stepId } : {}),
12707
+ ...(context.dataset ? { dataset: context.dataset } : {}),
12708
+ ...(context.column ? { column: context.column } : {}),
12709
+ ...(context.row_key ? { rowKey: context.row_key } : {}),
12710
+ },
12711
+ input,
12712
+ error,
12713
+ });
12548
12714
  this.runtimeLog(
12549
12715
  JSON.stringify({
12550
12716
  event: 'tool_call_failed',
12551
12717
  ...context,
12718
+ ...(savedKey ? { receipt_key: savedKey } : {}),
12552
12719
  status: 'failed',
12553
12720
  duration_ms: Math.max(0, Date.now() - startedAt),
12554
12721
  }),
12722
+ { level: 'error' },
12555
12723
  );
12556
12724
  }
12557
12725
  throw error;
@@ -12562,7 +12730,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12562
12730
  JSON.stringify({
12563
12731
  event: 'tool_call_completed',
12564
12732
  ...context,
12565
- ...(execution?.receiptKey ? { receipt_key: execution.receiptKey } : {}),
12733
+ ...(execution?.resultReceiptKey || execution?.receiptKey
12734
+ ? { receipt_key: execution.resultReceiptKey ?? execution.receiptKey }
12735
+ : {}),
12566
12736
  ...(execution?.source ? { execution_source: execution.source } : {}),
12567
12737
  status: toolResult?.status ?? 'completed',
12568
12738
  // Caller wall time includes queueing/reuse and result delivery. It is
@@ -14222,7 +14392,21 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14222
14392
  ): void {
14223
14393
  const boundedLine = boundRunLogLine(line);
14224
14394
  const omittedBefore = this.logBuffer.snapshot().omittedLines;
14225
- for (const retained of this.logBuffer.append(line, terminal, context)) {
14395
+ const scope = authoredStepContext.getStore();
14396
+ const classified = classifyLogLine(line);
14397
+ const timestamp = classified.line.match(/^\[([^\]]+)\]/)?.[1];
14398
+ const parsedAt = timestamp ? Date.parse(timestamp) : NaN;
14399
+ const metadata: PlayLogMetadata = {
14400
+ level: classified.level,
14401
+ occurredAt: Number.isFinite(parsedAt) ? parsedAt : Date.now(),
14402
+ ...(scope?.context === this ? { stepId: scope.stepId } : {}),
14403
+ };
14404
+ for (const retained of this.logBuffer.append(
14405
+ line,
14406
+ terminal,
14407
+ context,
14408
+ metadata,
14409
+ )) {
14226
14410
  const originalLineWasRetained =
14227
14411
  retained === boundedLine &&
14228
14412
  this.logBuffer.snapshot().omittedLines === omittedBefore;
@@ -14231,6 +14415,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14231
14415
  originalLineWasRetained
14232
14416
  ? (this.logBuffer.contexts.at(-1) ?? undefined)
14233
14417
  : undefined,
14418
+ this.logBuffer.ids.at(-1),
14419
+ originalLineWasRetained
14420
+ ? (this.logBuffer.metadata.at(-1) ?? undefined)
14421
+ : undefined,
14234
14422
  );
14235
14423
  if (this.#options.verbose) console.log(retained);
14236
14424
  }
@@ -14894,42 +15082,80 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14894
15082
  return output;
14895
15083
  };
14896
15084
 
14897
- return this.executeWithRuntimeReceipt<T>(
14898
- 'step',
14899
- normalizedKey,
14900
- this.currentRunId,
14901
- {
14902
- semanticKey: rowStore
14903
- ? stableDigest(
14904
- stableStringify({
14905
- scope: 'row',
14906
- tableNamespace: rowStore.tableNamespace ?? null,
14907
- rowKey: rowStore.rowKey ?? null,
14908
- rowId: rowStore.rowKey ? null : rowStore.rowId,
14909
- fieldName: rowStore.fieldName ?? null,
14910
- callIndex,
14911
- // Row scope prevents unrelated map cells from sharing a
14912
- // checkpoint. A supplied semantic key identifies the actual
14913
- // work (for example, ctx.runSteps hashes its current input),
14914
- // so it must refine row scope rather than being discarded.
14915
- semanticKey: options?.semanticKey ?? null,
14916
- }),
14917
- )
14918
- : options?.semanticKey,
14919
- staleAfterSeconds: options?.staleAfterSeconds,
14920
- force: this.#options.cachePolicy?.forceStepRefresh === true,
14921
- markSkipped: (output) => {
14922
- assertJsonSerializableStepOutput(normalizedKey, output);
14923
- },
14924
- execute: executeStep,
14925
- },
14926
- );
15085
+ const observe = !rowStore;
15086
+ const startedAt = Date.now();
15087
+ if (observe)
15088
+ this.emitExecutionEvent({
15089
+ type: 'step.started',
15090
+ stepId: normalizedKey,
15091
+ at: startedAt,
15092
+ });
15093
+ try {
15094
+ const output = await authoredStepContext.run(
15095
+ { context: this, stepId: normalizedKey },
15096
+ () =>
15097
+ this.executeWithRuntimeReceipt<T>(
15098
+ 'step',
15099
+ normalizedKey,
15100
+ this.currentRunId,
15101
+ {
15102
+ semanticKey: rowStore
15103
+ ? stableDigest(
15104
+ stableStringify({
15105
+ scope: 'row',
15106
+ tableNamespace: rowStore.tableNamespace ?? null,
15107
+ rowKey: rowStore.rowKey ?? null,
15108
+ rowId: rowStore.rowKey ? null : rowStore.rowId,
15109
+ fieldName: rowStore.fieldName ?? null,
15110
+ callIndex,
15111
+ // Row scope prevents unrelated map cells from sharing a
15112
+ // checkpoint. A supplied semantic key identifies the actual
15113
+ // work (for example, ctx.runSteps hashes its current input),
15114
+ // so it must refine row scope rather than being discarded.
15115
+ semanticKey: options?.semanticKey ?? null,
15116
+ }),
15117
+ )
15118
+ : options?.semanticKey,
15119
+ staleAfterSeconds: options?.staleAfterSeconds,
15120
+ force: this.#options.cachePolicy?.forceStepRefresh === true,
15121
+ markSkipped: (output) => {
15122
+ assertJsonSerializableStepOutput(normalizedKey, output);
15123
+ },
15124
+ execute: executeStep,
15125
+ },
15126
+ ),
15127
+ );
15128
+ if (observe)
15129
+ this.emitExecutionEvent({
15130
+ type: 'step.completed',
15131
+ stepId: normalizedKey,
15132
+ at: Date.now(),
15133
+ });
15134
+ return output;
15135
+ } catch (error) {
15136
+ if (observe && !isControlledPlaySuspension(error))
15137
+ this.emitExecutionEvent({
15138
+ type: 'step.failed',
15139
+ stepId: normalizedKey,
15140
+ at: Date.now(),
15141
+ error: buildPlayDocflowNodeErrorPreview(error, this.secretRedactor),
15142
+ });
15143
+ throw error;
15144
+ }
14927
15145
  }
14928
15146
 
14929
15147
  getLogs(): string[] {
14930
15148
  return this.logBuffer.lines;
14931
15149
  }
14932
15150
 
15151
+ getLogMetadata(): Array<PlayLogMetadata | null> {
15152
+ return this.logBuffer.metadata.slice();
15153
+ }
15154
+
15155
+ getLogIds(): string[] {
15156
+ return this.logBuffer.ids.slice();
15157
+ }
15158
+
14933
15159
  getLogContexts(): Array<PlayLogContext | null> {
14934
15160
  return this.logBuffer.contexts.slice();
14935
15161
  }
@@ -15512,15 +15738,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15512
15738
  source: 'cache' | 'owner',
15513
15739
  ): Promise<void> => {
15514
15740
  const receiptKey = request.receiptKey!;
15515
- const finalResult = cloneToolExecuteResultWithExecution(
15516
- result,
15517
- toolExecutionMetadataForOutcome({
15741
+ const finalResult = cloneToolExecuteResultWithExecution(result, {
15742
+ ...toolExecutionMetadataForOutcome({
15518
15743
  kind: source === 'owner' ? 'live' : source,
15519
15744
  cacheKey: request.cacheKey,
15520
15745
  receiptKey,
15521
15746
  ...(source === 'owner' ? {} : { attachedToReceiptKey: receiptKey }),
15522
15747
  }),
15523
- );
15748
+ resultReceiptKey:
15749
+ this.mappedToolResultReceiptPolicy(request).resultReceiptKey,
15750
+ });
15524
15751
  this.cacheToolResult(toolId, request.cacheKey, finalResult);
15525
15752
  const resolver = this.toolCallResolvers.get(request.callId);
15526
15753
  if (resolver) {
@@ -15676,6 +15903,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15676
15903
  toolId: string;
15677
15904
  requests: ToolCallRequest[];
15678
15905
  strategy: AnyBatchOperationStrategy;
15906
+ admissionObserver?: ToolBatchAdmissionObserver;
15679
15907
  }): Promise<ToolCallRequest[]> {
15680
15908
  const { toolId, requests, strategy } = input;
15681
15909
  if (requests.length === 0) return [];
@@ -15952,6 +16180,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15952
16180
  ),
15953
16181
  executionAuthScopeDigest:
15954
16182
  detail.members[0]?.executionAuthScopeDigest ?? null,
16183
+ onProviderAdmissionPending: () =>
16184
+ input.admissionObserver?.pending(batch.memberRequests),
16185
+ onProviderDispatched: () =>
16186
+ input.admissionObserver?.dispatched(
16187
+ batch.memberRequests,
16188
+ ),
15955
16189
  providerIdempotencyKey: detail.providerOperationKey,
15956
16190
  providerIdempotencyReceiptKey:
15957
16191
  compatibility.resultReceiptKey,
@@ -16199,6 +16433,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16199
16433
  });
16200
16434
  return { batch, result };
16201
16435
  } finally {
16436
+ input.admissionObserver?.dispatched(batch.memberRequests);
16202
16437
  // A dispatch that never reached its split — a provider failure, a
16203
16438
  // cohort throw — must not leave the body registered against
16204
16439
  // nobody for the rest of the run.
@@ -16290,16 +16525,23 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16290
16525
 
16291
16526
  private async executeBatchedToolCalls(
16292
16527
  queuedToolCalls: ToolCallRequest[],
16528
+ admissionObserver?: ToolBatchAdmissionObserver,
16293
16529
  ): Promise<void> {
16294
- // Group by toolId
16530
+ // Preserve authentication partitions through physical batch compilation.
16295
16531
  const byTool = new Map<string, ToolCallRequest[]>();
16296
16532
  for (const req of queuedToolCalls) {
16297
- if (!byTool.has(req.toolId)) byTool.set(req.toolId, []);
16298
- byTool.get(req.toolId)!.push(req);
16299
- }
16300
-
16301
- const toolSettlements = await Promise.allSettled(
16302
- [...byTool.entries()].map(async ([toolId, requests]) => {
16533
+ const key = JSON.stringify([
16534
+ req.toolId,
16535
+ req.executionAuthScopeDigest ?? '',
16536
+ ]);
16537
+ if (!byTool.has(key)) byTool.set(key, []);
16538
+ byTool.get(key)!.push(req);
16539
+ }
16540
+ const toolGroups = [...byTool.values()];
16541
+ const toolSettlements = await settleToolBatchGroups(
16542
+ toolGroups,
16543
+ async (requests) => {
16544
+ const toolId = requests[0]!.toolId;
16303
16545
  this.runtimeLog(
16304
16546
  `Executing tool batch ${toolId}: ${requests.length} calls`,
16305
16547
  );
@@ -16430,6 +16672,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16430
16672
  toolId,
16431
16673
  requests: cohortRequests,
16432
16674
  strategy: operationStrategy,
16675
+ admissionObserver,
16433
16676
  })
16434
16677
  : await this.executeCacheableNonNativeMapToolCallCohort({
16435
16678
  toolId,
@@ -16524,6 +16767,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16524
16767
  providerOperationKey: aggregateProviderOperationKey,
16525
16768
  apiOptions: {
16526
16769
  durableCallReceiptKey: aggregateReceiptKey,
16770
+ onProviderAdmissionPending: () =>
16771
+ admissionObserver?.pending(batch.memberRequests),
16772
+ onProviderDispatched: () =>
16773
+ admissionObserver?.dispatched(batch.memberRequests),
16527
16774
  legacyProviderRetryIdentities: [
16528
16775
  {
16529
16776
  transportReceiptKey:
@@ -16785,7 +17032,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16785
17032
  this.#options.onBatchComplete?.(this.checkpoint);
16786
17033
  }
16787
17034
  }
16788
- }),
17035
+ },
17036
+ admissionObserver,
16789
17037
  );
16790
17038
  const rejected = toolSettlements.find(
16791
17039
  (settlement): settlement is PromiseRejectedResult =>
@@ -17944,6 +18192,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17944
18192
  let providerTelemetryKey: string | null = null;
17945
18193
  let providerRunInFlightAtDispatch: number | null = null;
17946
18194
  const acquireProviderPermit = async (signal?: AbortSignal) => {
18195
+ options?.onProviderAdmissionPending?.();
17947
18196
  logProvider429RetryPhase('provider_permit_wait_started');
17948
18197
  const permit = await this.resourceGovernor.acquireProviderPermit({
17949
18198
  toolId,
@@ -18094,6 +18343,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18094
18343
  acquireProviderPermit: async ({ signal }) =>
18095
18344
  await acquireProviderPermit(signal),
18096
18345
  tryAcquireProviderPermit: () => {
18346
+ options?.onProviderAdmissionPending?.();
18097
18347
  logProvider429RetryPhase(
18098
18348
  'provider_permit_acquire_attempted',
18099
18349
  );
@@ -18146,6 +18396,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18146
18396
  > | null;
18147
18397
  } = { current: null };
18148
18398
  const beginProviderExecution = () => {
18399
+ if (providerExecution.current === null) {
18400
+ options?.onProviderDispatched?.();
18401
+ }
18149
18402
  const execution =
18150
18403
  providerExecution.current ??
18151
18404
  this.resourceGovernor.beginProviderExecution({
@@ -19209,6 +19462,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19209
19462
  `gateway=${transportGatewayOriginForDiagnostic(url)} attempt=${httpFailureAttempt})`,
19210
19463
  { level: 'error' },
19211
19464
  );
19465
+ attachToolCallFailureEvidenceFromResponse(surfacedError, text);
19212
19466
  throw surfacedError;
19213
19467
  }
19214
19468
 
@@ -11,7 +11,7 @@ import type { PlayRunContractSnapshot } from '../plays/contracts';
11
11
  import type { PlayDatasetColumnTypes } from '../play-data-plane/contracts';
12
12
  import type { PlayStructuredDefinition } from '../plays/definition';
13
13
  import type { PlayStaticPipeline } from '../plays/static-pipeline';
14
- import type { PlayLogContext } from './log-context';
14
+ import type { PlayLogContext, PlayLogMetadata } from './log-context';
15
15
  import type {
16
16
  AsyncPlayRunRef,
17
17
  DurableCallStaleAfterSeconds,
@@ -514,6 +514,9 @@ export interface MapExecutionScope {
514
514
  }
515
515
 
516
516
  export type PlayExecutionEvent =
517
+ | { type: 'step.started'; stepId: string; at: number }
518
+ | { type: 'step.completed'; stepId: string; at: number }
519
+ | { type: 'step.failed'; stepId: string; error: string; at: number }
517
520
  | PlayActivityEvent
518
521
  | {
519
522
  type: 'docflow.node.hit';
@@ -851,7 +854,12 @@ export interface ContextOptions {
851
854
  /** Called after each durable batch completes for scheduler checkpointing. */
852
855
  onBatchComplete?: (checkpoint: PlayCheckpoint) => void;
853
856
  /** Called when the runtime emits a new execution log line. */
854
- onLog?: (line: string, context?: PlayLogContext) => void;
857
+ onLog?: (
858
+ line: string,
859
+ context?: PlayLogContext,
860
+ entryId?: string,
861
+ metadata?: PlayLogMetadata,
862
+ ) => void;
855
863
  /**
856
864
  * Runtime-provided HTTP transport for ctx.fetch. Node runtimes should provide
857
865
  * a transport that validates resolved IP addresses at connect time.
@@ -84,6 +84,7 @@ const FIXTURE_MODE_BYPASS_PROVIDERS = new Set([
84
84
  'test',
85
85
  'test_high',
86
86
  'test_low',
87
+ 'test_batching',
87
88
  // Dedicated stateful failure fixtures must reach their local custom handlers
88
89
  // even when the surrounding Play runs in fixture mode.
89
90
  'test_receipt',