deepline 0.3.145 → 0.3.146

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.
@@ -3080,7 +3080,7 @@ export class DeeplineClient {
3080
3080
  * guaranteed support for every model. Runtime AI SDK/Gateway errors remain
3081
3081
  * authoritative for model-gated values.
3082
3082
  *
3083
- * @param model - Gateway model id such as `"openai/gpt-5.5"`
3083
+ * @param model - Exact-case Gateway model id such as `"openai/gpt-5.6-luna"`
3084
3084
  * @returns Model metadata, provider option shapes, and runnable examples
3085
3085
  */
3086
3086
  async describeModel(model: string): Promise<DeeplineAgentModelDescription> {
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.145',
203
+ version: '0.3.146',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -79,6 +79,12 @@ export type AsyncOperationResultMaterialization = {
79
79
 
80
80
  export type AsyncOperationContractWire = {
81
81
  version: 1;
82
+ /**
83
+ * Keep a default synchronous Play call in the runner process: launch the
84
+ * provider job, expose its handle in run progress, then poll companion tools
85
+ * until terminal. Direct tools.execute callers keep their own legacy policy.
86
+ */
87
+ runtimeManagedWait?: true;
82
88
  /** Preserve the provider's historical public async metadata exactly. */
83
89
  compatibility?: {
84
90
  /** null intentionally suppresses the legacy asyncGetAction projection. */
@@ -285,10 +291,7 @@ export function defineAsyncOperation(
285
291
  positiveInteger(contract.polling.intervalMs, 'polling.intervalMs');
286
292
  positiveInteger(contract.polling.timeoutMs, 'polling.timeoutMs');
287
293
  if (contract.polling.checkTimeoutMs !== undefined) {
288
- positiveInteger(
289
- contract.polling.checkTimeoutMs,
290
- 'polling.checkTimeoutMs',
291
- );
294
+ positiveInteger(contract.polling.checkTimeoutMs, 'polling.checkTimeoutMs');
292
295
  }
293
296
  if (contract.polling.intervalInputField !== undefined) {
294
297
  required(contract.polling.intervalInputField, 'polling.intervalInputField');
@@ -906,6 +909,24 @@ export async function executeAsyncOperationLifecycle<Response>(input: {
906
909
  responseValue: (response: Response) => unknown;
907
910
  sleep?: (ms: number) => Promise<void>;
908
911
  now?: () => number;
912
+ /** Called after the provider handle is known and before the first poll. */
913
+ onJobStarted?: (input: { jobId: string }) => void;
914
+ /** Called after each successful status read, including running outcomes. */
915
+ onPoll?: (input: {
916
+ jobId: string;
917
+ pollAttempt: number;
918
+ elapsedMs: number;
919
+ result: unknown;
920
+ outcome: AsyncOperationTerminalOutcome;
921
+ }) => void;
922
+ /** Called when an upstream 429 delays a status check. */
923
+ onRateLimit?: (input: {
924
+ jobId: string;
925
+ pollAttempt: number;
926
+ rateLimitAttempt: number;
927
+ retryAfterMs: number | null;
928
+ delayMs: number;
929
+ }) => void;
909
930
  /** Legacy direct callers historically poll once immediately after launch. */
910
931
  pollImmediately?: boolean;
911
932
  }): Promise<
@@ -939,6 +960,7 @@ export async function executeAsyncOperationLifecycle<Response>(input: {
939
960
  `Async operation ${input.contract.lifecycle.startAction} returned running without a provider job id.`,
940
961
  );
941
962
  }
963
+ input.onJobStarted?.({ jobId });
942
964
  const sleep =
943
965
  input.sleep ??
944
966
  ((ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
@@ -1072,6 +1094,13 @@ export async function executeAsyncOperationLifecycle<Response>(input: {
1072
1094
  rateLimitAttempt: consecutiveRateLimitAttempts,
1073
1095
  retryAfterMs: rateLimit.retryAfterMs,
1074
1096
  });
1097
+ input.onRateLimit?.({
1098
+ jobId,
1099
+ pollAttempt,
1100
+ rateLimitAttempt: consecutiveRateLimitAttempts,
1101
+ retryAfterMs: rateLimit.retryAfterMs,
1102
+ delayMs: nextPollDelayMs,
1103
+ });
1075
1104
  continue;
1076
1105
  }
1077
1106
  if (!pollResponse) {
@@ -1083,6 +1112,13 @@ export async function executeAsyncOperationLifecycle<Response>(input: {
1083
1112
  input.contract,
1084
1113
  previousPollResult,
1085
1114
  );
1115
+ input.onPoll?.({
1116
+ jobId,
1117
+ pollAttempt,
1118
+ elapsedMs: Math.max(0, now() - startedAt),
1119
+ result: previousPollResult,
1120
+ outcome: classification.outcome,
1121
+ });
1086
1122
  if (classification.outcome === 'running') {
1087
1123
  pollAttempt += 1;
1088
1124
  continue;
@@ -267,7 +267,14 @@ import {
267
267
  type DurableReceiptExecutionStore,
268
268
  } from './durable-receipt-execution';
269
269
  import {
270
+ asyncOperationLaunchInput,
271
+ buildAsyncOperationActionInput,
272
+ classifyAsyncOperationResult,
273
+ executeAsyncOperationLifecycle,
274
+ extractAsyncOperationJobId,
275
+ readAsyncOperationPath,
270
276
  asyncOperationShouldWait,
277
+ hasExplicitAsyncOperationTerminalMatch,
271
278
  type AsyncOperationContractWire,
272
279
  } from './async-operation';
273
280
  import {
@@ -1510,6 +1517,13 @@ function cancelRuntimeResponseBody(response: Response): void {
1510
1517
  }
1511
1518
  }
1512
1519
 
1520
+ function shellQuoteProviderJobCommandValue(value: string): string {
1521
+ // Close the single-quoted token, emit a literal apostrophe, then reopen it.
1522
+ // This keeps copied status commands safe even when provider job IDs contain
1523
+ // shell metacharacters.
1524
+ return `'${value.replaceAll("'", "'\"'\"'")}'`;
1525
+ }
1526
+
1513
1527
  const NO_SECRET_RESOLUTION_RETRY: SecretResolutionRetryDecision = {
1514
1528
  retry: false,
1515
1529
  retryDelayMs: 0,
@@ -1859,6 +1873,7 @@ type NativeBatchBodyAttribution = {
1859
1873
 
1860
1874
  type ToolExecutionApiOptions = {
1861
1875
  timeoutMs?: number;
1876
+ abortSignal?: AbortSignal;
1862
1877
  durableCallReceiptKey?: string | null;
1863
1878
  /**
1864
1879
  * Complete-result receipt the execution relay may publish before returning
@@ -8886,6 +8901,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8886
8901
  ),
8887
8902
  pendingRowsCount: Math.max(0, totalInputCount - processedCount),
8888
8903
  failedRowsCount: Math.max(0, failedCount - staleFailedKeys.size),
8904
+ supersededRows: staleCompletedKeys.size + staleFailedKeys.size,
8889
8905
  startedAt:
8890
8906
  this.checkpoint.mapFrames?.[mapScope.mapInvocationId]?.startedAt ??
8891
8907
  Date.now(),
@@ -8915,6 +8931,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8915
8931
  ),
8916
8932
  progressFailedOffset: () =>
8917
8933
  Math.max(0, failedCount - staleFailedKeys.size),
8934
+ progressSupersededOffset: () =>
8935
+ staleCompletedKeys.size + staleFailedKeys.size,
8918
8936
  progressTotalRows: () => totalInputCount,
8919
8937
  rowFeed: execution.rowFeed,
8920
8938
  retainedRowsMemoryBudgetBytes:
@@ -9140,6 +9158,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9140
9158
  completedRowsCount: completedRows,
9141
9159
  pendingRowsCount: 0,
9142
9160
  failedRowsCount: failedCount,
9161
+ supersededRows: staleCompletedKeys.size + staleFailedKeys.size,
9143
9162
  startedAt:
9144
9163
  this.checkpoint.mapFrames?.[mapScope.mapInvocationId]?.startedAt ??
9145
9164
  Date.now(),
@@ -9154,6 +9173,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9154
9173
  completedRows,
9155
9174
  failedRows: failedCount,
9156
9175
  totalRows: totalInputCount,
9176
+ supersededRows: staleCompletedKeys.size + staleFailedKeys.size,
9157
9177
  at: Date.now(),
9158
9178
  });
9159
9179
  throw new AllAdmittedRowsFailedError({
@@ -9182,6 +9202,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9182
9202
  ),
9183
9203
  pendingRowsCount: 0,
9184
9204
  failedRowsCount: failedCount,
9205
+ supersededRows: staleCompletedKeys.size + staleFailedKeys.size,
9185
9206
  startedAt:
9186
9207
  this.checkpoint.mapFrames?.[mapScope.mapInvocationId]?.startedAt ??
9187
9208
  Date.now(),
@@ -9202,6 +9223,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9202
9223
  ),
9203
9224
  failedRows: failedCount,
9204
9225
  totalRows: totalInputCount,
9226
+ supersededRows: staleCompletedKeys.size + staleFailedKeys.size,
9205
9227
  at: Date.now(),
9206
9228
  });
9207
9229
 
@@ -9986,6 +10008,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9986
10008
  retainedRowsMemoryBudgetBytes?: number;
9987
10009
  progressCompletedOffset?: number | (() => number);
9988
10010
  progressFailedOffset?: number | (() => number);
10011
+ progressSupersededOffset?: number | (() => number);
9989
10012
  progressTotalRows?: number | (() => number);
9990
10013
  /**
9991
10014
  * When set, `items`, `executionRowKeys` and `executionRowIndexes` grow
@@ -10329,6 +10352,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10329
10352
  liveNumber(runtimeOptions?.progressFailedOffset, 0) +
10330
10353
  liveFrameSets.failedRowsCount,
10331
10354
  }),
10355
+ supersededRows: liveNumber(
10356
+ runtimeOptions?.progressSupersededOffset,
10357
+ existing.supersededRows ?? 0,
10358
+ ),
10332
10359
  ...(input.activeBoundaryId !== undefined
10333
10360
  ? { activeBoundaryId: input.activeBoundaryId }
10334
10361
  : {}),
@@ -10358,6 +10385,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10358
10385
  completedRowKeys.size,
10359
10386
  failedRows,
10360
10387
  totalRows: currentTotalRows(),
10388
+ supersededRows: liveNumber(
10389
+ runtimeOptions?.progressSupersededOffset,
10390
+ existing.supersededRows ?? 0,
10391
+ ),
10361
10392
  // Inline child aggregates ride the single-writer progress event so
10362
10393
  // fan-out never contends on a per-child mutation. See ADR 0013.
10363
10394
  ...this.inlineChildAggregateEventFields(),
@@ -17781,6 +17812,189 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17781
17812
  this.providerBodyHandlesByRowColumn.delete(rowKey);
17782
17813
  }
17783
17814
 
17815
+ private async resolveAsyncOperationInRuntime(input: {
17816
+ toolId: string;
17817
+ contract: AsyncOperationContractWire;
17818
+ startInput: Record<string, unknown>;
17819
+ startResponse: ParsedToolExecuteResponse;
17820
+ }): Promise<ParsedToolExecuteResponse> {
17821
+ const startResult = input.startResponse.toolResponse?.raw;
17822
+ const jobId = extractAsyncOperationJobId(input.contract, startResult);
17823
+ const hasTerminalMatch = hasExplicitAsyncOperationTerminalMatch(
17824
+ input.contract,
17825
+ startResult,
17826
+ );
17827
+ const startClassification = classifyAsyncOperationResult(
17828
+ input.contract,
17829
+ startResult,
17830
+ );
17831
+ const launchIsRunning =
17832
+ input.contract.launch?.unclassifiedJobOutcome === 'running' &&
17833
+ !hasTerminalMatch;
17834
+
17835
+ if (
17836
+ hasTerminalMatch &&
17837
+ (startClassification.outcome === 'failed' ||
17838
+ startClassification.outcome === 'cancelled')
17839
+ ) {
17840
+ const statusAction = jobId
17841
+ ? input.contract.lifecycle.pollActions[0]
17842
+ : undefined;
17843
+ const statusCommand =
17844
+ jobId && statusAction
17845
+ ? ` Check its status with: deepline tools execute ${shellQuoteProviderJobCommandValue(statusAction.action)}` +
17846
+ ` --input ${shellQuoteProviderJobCommandValue(JSON.stringify(buildAsyncOperationActionInput({ mapping: statusAction.input, jobId, startInput: input.startInput, startResult })))} --json`
17847
+ : '';
17848
+ throw new Error(
17849
+ `${startClassification.message ?? `Provider task ${jobId ?? input.toolId} ended ${startClassification.outcome}.`}${statusCommand}`,
17850
+ );
17851
+ }
17852
+
17853
+ if (!jobId) {
17854
+ if (launchIsRunning) {
17855
+ throw new Error(
17856
+ `Async operation ${input.toolId} returned no provider job id; it could not be polled safely.`,
17857
+ );
17858
+ }
17859
+ return input.startResponse;
17860
+ }
17861
+ if (hasTerminalMatch && startClassification.outcome !== 'running') {
17862
+ return input.startResponse;
17863
+ }
17864
+ if (!launchIsRunning && startClassification.outcome !== 'running') {
17865
+ return input.startResponse;
17866
+ }
17867
+
17868
+ const statusAction = input.contract.lifecycle.pollActions[0];
17869
+ if (!statusAction) {
17870
+ throw new Error(
17871
+ `Async operation ${input.toolId} has no status action for provider job ${jobId}.`,
17872
+ );
17873
+ }
17874
+ const statusInput = buildAsyncOperationActionInput({
17875
+ mapping: statusAction.input,
17876
+ jobId,
17877
+ startInput: input.startInput,
17878
+ startResult,
17879
+ });
17880
+ const statusCommand =
17881
+ `deepline tools execute ${shellQuoteProviderJobCommandValue(statusAction.action)}` +
17882
+ ` --input ${shellQuoteProviderJobCommandValue(JSON.stringify(statusInput))} --json`;
17883
+ let lastProgressLogAtMs = 0;
17884
+ const lifecycle = await executeAsyncOperationLifecycle({
17885
+ contract: input.contract,
17886
+ startInput: input.startInput,
17887
+ startResult,
17888
+ jobId,
17889
+ executeAction: ({ action, input: actionInput, timeoutMs, abortSignal }) =>
17890
+ this.callToolExecutionAPI(action, actionInput, {
17891
+ timeoutMs,
17892
+ abortSignal,
17893
+ skipAsyncOperationResolution: true,
17894
+ uncachedAsyncLifecycleCall: true,
17895
+ }),
17896
+ responseValue: (response) => response.toolResponse?.raw,
17897
+ onJobStarted: () => {
17898
+ this.runtimeLog(
17899
+ `Provider task ${jobId} is running. This Play remains synchronous while it checks for completion. ` +
17900
+ `Canceling the Play stops this wait, not the accepted provider job; it may still incur Deepline credits. ` +
17901
+ `Check it separately with: ${statusCommand}`,
17902
+ );
17903
+ },
17904
+ onPoll: ({ pollAttempt, elapsedMs, outcome, result }) => {
17905
+ if (
17906
+ outcome !== 'running' ||
17907
+ elapsedMs - lastProgressLogAtMs >= 15_000
17908
+ ) {
17909
+ const statusPaths = input.contract.terminal.rules.flatMap((rule) =>
17910
+ rule.conditions.map((condition) => condition.path),
17911
+ );
17912
+ const providerStatus = statusPaths
17913
+ .map((path) => readAsyncOperationPath(result, path))
17914
+ .find((value): value is string => typeof value === 'string');
17915
+ this.runtimeLog(
17916
+ `Provider task ${jobId}: status check ${pollAttempt + 1} returned ${outcome}` +
17917
+ `${providerStatus ? ` (provider status: ${providerStatus})` : ''} after ${Math.round(elapsedMs / 1_000)}s.`,
17918
+ );
17919
+ lastProgressLogAtMs = elapsedMs;
17920
+ }
17921
+ },
17922
+ onRateLimit: ({
17923
+ pollAttempt,
17924
+ rateLimitAttempt,
17925
+ retryAfterMs,
17926
+ delayMs,
17927
+ }) => {
17928
+ this.runtimeLog(
17929
+ `Provider task ${jobId}: status check received HTTP 429 on retry ${rateLimitAttempt} (after ${pollAttempt} successful checks). ` +
17930
+ `${retryAfterMs === null ? 'No Retry-After was supplied' : `Retry-After was ${retryAfterMs}ms`}; retrying after ${delayMs}ms.`,
17931
+ );
17932
+ },
17933
+ }).catch((error: unknown) => {
17934
+ const surfacedError =
17935
+ error instanceof Error ? error : new Error(String(error));
17936
+ surfacedError.message =
17937
+ `${surfacedError.message}\nProvider task ${jobId} may still be running. ` +
17938
+ `Check its status with: ${statusCommand}`;
17939
+ throw surfacedError;
17940
+ });
17941
+
17942
+ if (lifecycle.kind === 'timed_out') {
17943
+ const timeoutError = new Error(
17944
+ `Provider task ${jobId} did not finish before the wait limit. ` +
17945
+ `Check its status with: ${statusCommand}`,
17946
+ );
17947
+ this.runtimeLog(timeoutError.message, { level: 'error' });
17948
+ throw timeoutError;
17949
+ }
17950
+ if (
17951
+ lifecycle.classification.outcome === 'failed' ||
17952
+ lifecycle.classification.outcome === 'cancelled'
17953
+ ) {
17954
+ const terminalError = new Error(
17955
+ `${lifecycle.classification.message ?? `Provider task ${jobId} ended ${lifecycle.classification.outcome}.`} ` +
17956
+ `Check its status with: ${statusCommand}`,
17957
+ );
17958
+ this.runtimeLog(terminalError.message, { level: 'error' });
17959
+ throw terminalError;
17960
+ }
17961
+ if (!lifecycle.materialized) {
17962
+ const error = new Error(
17963
+ `Provider task ${jobId} completed without a materialized result. ` +
17964
+ `Check its status with: ${statusCommand}`,
17965
+ );
17966
+ this.runtimeLog(error.message, { level: 'error' });
17967
+ throw error;
17968
+ }
17969
+
17970
+ const materialized = lifecycle.materialized;
17971
+ const originalRawV2 = input.startResponse.toolResponse?.rawV2;
17972
+ const rawV2 =
17973
+ originalRawV2 &&
17974
+ typeof originalRawV2 === 'object' &&
17975
+ !Array.isArray(originalRawV2)
17976
+ ? {
17977
+ ...(originalRawV2 as Record<string, unknown>),
17978
+ data: materialized.result,
17979
+ }
17980
+ : { data: materialized.result };
17981
+ const responseMeta = input.startResponse.toolResponse?.meta;
17982
+ return {
17983
+ ...input.startResponse,
17984
+ status: materialized.outcome === 'no_result' ? 'no_result' : 'completed',
17985
+ toolResponse: {
17986
+ ...input.startResponse.toolResponse,
17987
+ raw: materialized.result,
17988
+ rawV2,
17989
+ view: 'data',
17990
+ },
17991
+ result: {
17992
+ data: materialized.result,
17993
+ ...(responseMeta ? { meta: responseMeta } : {}),
17994
+ },
17995
+ };
17996
+ }
17997
+
17784
17998
  private async callToolExecutionAPI(
17785
17999
  toolId: string,
17786
18000
  input: Record<string, unknown>,
@@ -17803,6 +18017,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
17803
18017
  const shouldWaitForAsyncOperation = Boolean(
17804
18018
  asyncContract && asyncOperationShouldWait(asyncContract, input),
17805
18019
  );
18020
+ const shouldResolveAsyncOperationInRuntime = Boolean(
18021
+ asyncContract?.runtimeManagedWait === true &&
18022
+ asyncContract.lifecycle.startAction === toolId &&
18023
+ shouldWaitForAsyncOperation,
18024
+ );
18025
+ const shouldWaitAtExecutionBoundary =
18026
+ shouldWaitForAsyncOperation && !shouldResolveAsyncOperationInRuntime;
17806
18027
  // An explicitly opted-in caller-managed launch gives the Play author the
17807
18028
  // provider handle and leaves polling to the authored graph. Do not let a
17808
18029
  // provider's legacy synchronous helper turn a wide start wave into
@@ -18035,7 +18256,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18035
18256
  let providerRateLimitRetryFromInvocationAttempt: number | null = null;
18036
18257
  const serializedRequestBody = (invocationAttempt: number) =>
18037
18258
  JSON.stringify({
18038
- payload: toolInputSnapshot,
18259
+ payload: shouldResolveAsyncOperationInRuntime
18260
+ ? asyncOperationLaunchInput(asyncContract!, toolInputSnapshot)
18261
+ : toolInputSnapshot,
18039
18262
  metadata: {
18040
18263
  parent_run_id: this.#options.runId,
18041
18264
  invocation_attempt: invocationAttempt,
@@ -18050,7 +18273,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18050
18273
  // admission proof distinct from the optional provider fence:
18051
18274
  // ordinary calls are governor-managed too.
18052
18275
  runtime_governor_managed: true,
18053
- ...(shouldWaitForAsyncOperation
18276
+ ...(shouldWaitAtExecutionBoundary
18054
18277
  ? { async_operation_wait: true }
18055
18278
  : {}),
18056
18279
  ...(shouldPreferAsyncProviderExecution
@@ -18278,11 +18501,26 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18278
18501
  // not share the coordinator's TTL environment.
18279
18502
  const hasReceiptHeartbeat = Boolean(options?.heartbeatReceipt);
18280
18503
  const abortController =
18281
- timeoutMs || hasReceiptHeartbeat ? new AbortController() : null;
18504
+ timeoutMs || hasReceiptHeartbeat || options?.abortSignal
18505
+ ? new AbortController()
18506
+ : null;
18507
+ const forwardAbortSignal = () => {
18508
+ if (!options?.abortSignal?.aborted) return;
18509
+ abortController?.abort(options.abortSignal.reason);
18510
+ };
18282
18511
  const receiptHeartbeat = options?.heartbeatReceipt;
18283
18512
  let heartbeatFailure: unknown = null;
18284
18513
  let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
18285
18514
  try {
18515
+ if (options?.abortSignal?.aborted) {
18516
+ forwardAbortSignal();
18517
+ } else {
18518
+ options?.abortSignal?.addEventListener(
18519
+ 'abort',
18520
+ forwardAbortSignal,
18521
+ { once: true },
18522
+ );
18523
+ }
18286
18524
  const ownershipStartedAt = Date.now();
18287
18525
  await options?.beforeProviderCall?.();
18288
18526
  if (runtimeReceiptReadTraceEnabled) {
@@ -18971,6 +19209,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18971
19209
  if (timeoutHandle) {
18972
19210
  clearTimeout(timeoutHandle);
18973
19211
  }
19212
+ options?.abortSignal?.removeEventListener(
19213
+ 'abort',
19214
+ forwardAbortSignal,
19215
+ );
18974
19216
  }
18975
19217
 
18976
19218
  if (!response) {
@@ -19235,6 +19477,14 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19235
19477
  );
19236
19478
  }
19237
19479
  const parsed = parseToolExecuteResponse(toolId, responseData);
19480
+ const resolved = shouldResolveAsyncOperationInRuntime
19481
+ ? await this.resolveAsyncOperationInRuntime({
19482
+ toolId,
19483
+ contract: asyncContract!,
19484
+ startInput: toolInputSnapshot,
19485
+ startResponse: parsed,
19486
+ })
19487
+ : parsed;
19238
19488
  // Only the configured execution relay may mint this header. The
19239
19489
  // ordinary app route (and every direct provider path) keeps the
19240
19490
  // established full-payload completion behavior even if an
@@ -19249,14 +19499,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19249
19499
  : null;
19250
19500
  setSpanAttributes(span, {
19251
19501
  'plays.tool_result_kind':
19252
- parsed.result == null
19502
+ resolved.result == null
19253
19503
  ? 'null'
19254
- : Array.isArray(parsed.result)
19504
+ : Array.isArray(resolved.result)
19255
19505
  ? 'array'
19256
- : typeof parsed.result,
19506
+ : typeof resolved.result,
19257
19507
  });
19258
19508
  toolCallSucceeded = true;
19259
- return receiptOutputRef ? { ...parsed, receiptOutputRef } : parsed;
19509
+ return receiptOutputRef
19510
+ ? { ...resolved, receiptOutputRef }
19511
+ : resolved;
19260
19512
  }
19261
19513
  },
19262
19514
  );
@@ -489,6 +489,8 @@ export interface MapExecutionFrame {
489
489
  completedRowsCount?: number;
490
490
  pendingRowsCount?: number;
491
491
  failedRowsCount?: number;
492
+ /** Terminal rows omitted from this run's latest view because a newer write won. */
493
+ supersededRows?: number;
492
494
  activeBoundaryId?: string | null;
493
495
  startedAt: number;
494
496
  updatedAt: number;
@@ -590,6 +592,8 @@ export type PlayExecutionEvent =
590
592
  totalRows: number;
591
593
  completedRows: number;
592
594
  pendingRows: number;
595
+ /** Rows omitted because a newer Runtime Sheet write already won. */
596
+ supersededRows?: number;
593
597
  at: number;
594
598
  }
595
599
  | {
@@ -601,6 +605,8 @@ export type PlayExecutionEvent =
601
605
  totalRows: number;
602
606
  completedRows: number;
603
607
  pendingRows: number;
608
+ /** Rows omitted because a newer Runtime Sheet write already won. */
609
+ supersededRows?: number;
604
610
  at: number;
605
611
  }
606
612
  | {
@@ -612,6 +618,8 @@ export type PlayExecutionEvent =
612
618
  completedRows: number;
613
619
  failedRows: number;
614
620
  totalRows?: number;
621
+ /** Rows whose Runtime Sheet write lost to a newer write; not failures. */
622
+ supersededRows?: number;
615
623
  /**
616
624
  * Inline child-play composition aggregates for this run. Maintained by the
617
625
  * single-writer progress path (never per child), so fan-out cannot contend.
@@ -644,6 +652,8 @@ export type PlayExecutionEvent =
644
652
  completedRows: number;
645
653
  failedRows: number;
646
654
  totalRows?: number;
655
+ /** Rows whose Runtime Sheet write lost to a newer write; not failures. */
656
+ supersededRows?: number;
647
657
  /**
648
658
  * Inline child-play composition aggregates for this run. Maintained by the
649
659
  * single-writer progress path (never per child). See ADR 0013.
@@ -371,6 +371,7 @@ export type PlayRunnerResult =
371
371
  /** Customer-safe runtime observations that must survive bounded log tails. */
372
372
  runtimeDiagnostics?: CtxFetchHttpFailureDiagnostic[];
373
373
  outputRowCount?: number;
374
+ rowOutcomes?: PlayRunnerRowOutcomeSummary;
374
375
  logs: string[];
375
376
  stats: Record<string, unknown>;
376
377
  steps: PlayStep[];
@@ -406,5 +407,14 @@ export type PlayRunnerResult =
406
407
  totalRows?: number;
407
408
  inserted?: number;
408
409
  skipped?: number;
410
+ rowOutcomes?: PlayRunnerRowOutcomeSummary;
409
411
  runtimeTiming?: PlayRunnerRuntimeTiming;
410
412
  };
413
+
414
+ export type PlayRunnerRowOutcomeSummary = {
415
+ completedRows: number;
416
+ failedRows: number;
417
+ totalRows: number;
418
+ /** Rows omitted because a newer Runtime Sheet write took precedence. */
419
+ supersededRows?: number;
420
+ };