deepline 0.2.30 → 0.2.31

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.
@@ -2155,6 +2155,9 @@ export class DeeplineClient {
2155
2155
  // defaults to absurd; callers normally omit this field.
2156
2156
  ...(request.profile ? { profile: request.profile } : {}),
2157
2157
  ...(integrationMode ? { integrationMode } : {}),
2158
+ ...(request.fixtureBehavior
2159
+ ? { fixtureBehavior: request.fixtureBehavior }
2160
+ : {}),
2158
2161
  ...(runtime ? { runtime } : {}),
2159
2162
  ...(testPolicyOverrides ? { testPolicyOverrides } : {}),
2160
2163
  },
@@ -2225,6 +2228,9 @@ export class DeeplineClient {
2225
2228
  : {}),
2226
2229
  ...(request.profile ? { profile: request.profile } : {}),
2227
2230
  ...(integrationMode ? { integrationMode } : {}),
2231
+ ...(request.fixtureBehavior
2232
+ ? { fixtureBehavior: request.fixtureBehavior }
2233
+ : {}),
2228
2234
  ...(runtime ? { runtime } : {}),
2229
2235
  ...(testPolicyOverrides ? { testPolicyOverrides } : {}),
2230
2236
  };
@@ -160,7 +160,7 @@ export const SDK_RELEASE = {
160
160
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
161
161
  // exposed storage-dependent synchronous access. This deliberate minor
162
162
  // release keeps lazy paging semantics independent of row residency.
163
- version: '0.2.30',
163
+ version: '0.2.31',
164
164
  contracts: {
165
165
  api: {
166
166
  name: 'sdk-http-api',
@@ -1394,6 +1394,8 @@ export interface StartPlayRunRequest {
1394
1394
  profile?: string;
1395
1395
  /** Optional per-run provider execution mode for eval/smoke runs. */
1396
1396
  integrationMode?: 'live' | 'eval_stub' | 'fixture';
1397
+ /** Fixture-only provider response timing and outcome simulation. */
1398
+ fixtureBehavior?: import('../../shared_libs/play-runtime/fixture-behavior').FixtureBehavior;
1397
1399
  /** Internal runtime estate selection. The app host remains unchanged. */
1398
1400
  runtime?: PlayRuntimeSelection;
1399
1401
  /** Internal/dev-only runtime policy overrides for black-box durability tests. */
@@ -992,6 +992,9 @@ async function postAppRuntimeApi<TResponse>(
992
992
  : AbortSignal.timeout(requestTimeoutMs),
993
993
  });
994
994
  } catch (error) {
995
+ if (context.signal?.aborted) {
996
+ throw context.signal.reason ?? error;
997
+ }
995
998
  if (
996
999
  attempt < maxAttempts &&
997
1000
  isRetryableAppRuntimeFetchError({ action: body.action, error })
@@ -35,6 +35,12 @@ import {
35
35
  runtimeLeaseHeartbeatIntervalFromExpiry,
36
36
  } from './lease-policy';
37
37
  import { createRuntimeReceiptHeartbeatSupervisor } from './receipt-heartbeat-supervisor';
38
+ import {
39
+ shouldRouteFixtureProvider,
40
+ shouldRouteFixtureToolId,
41
+ validateFixtureBehavior,
42
+ waitForFixtureResponseDelay,
43
+ } from './fixture-behavior';
38
44
  import { dispatchBoundedSettled } from './bounded-dispatch';
39
45
  import type { PlayQueueHint } from './governor/rate-state-backend';
40
46
  import type { MapRowOutcome } from './durability-store';
@@ -1622,6 +1628,22 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
1622
1628
  };
1623
1629
 
1624
1630
  constructor(options: ContextOptions) {
1631
+ const fixtureBehavior = validateFixtureBehavior(options.fixtureBehavior);
1632
+ if (fixtureBehavior.ok === false) {
1633
+ throw new Error(fixtureBehavior.error);
1634
+ }
1635
+ if (
1636
+ fixtureBehavior.behavior !== null &&
1637
+ options.integrationMode !== 'fixture'
1638
+ ) {
1639
+ throw new Error(
1640
+ 'fixtureBehavior is only valid when integrationMode is fixture.',
1641
+ );
1642
+ }
1643
+ options = {
1644
+ ...options,
1645
+ fixtureBehavior: fixtureBehavior.behavior,
1646
+ };
1625
1647
  this.#options = options;
1626
1648
  this.checkpoint = options.checkpoint ?? emptyCheckpoint();
1627
1649
  this.durableMappedToolResultsBackedByReceipts = Boolean(
@@ -6720,6 +6742,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
6720
6742
  }
6721
6743
  }
6722
6744
 
6745
+ private fixtureProviderPacingDisabled(): boolean {
6746
+ return (
6747
+ this.#options.integrationMode === 'fixture' &&
6748
+ this.#options.enforceFixtureProviderPacing !== true
6749
+ );
6750
+ }
6751
+
6723
6752
  private toolDispatchLane(request: ToolCallRequest): {
6724
6753
  key: string;
6725
6754
  readyCount: number;
@@ -8983,15 +9012,16 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8983
9012
  getPayload: (request: ToolCallRequest) => request.input,
8984
9013
  });
8985
9014
 
9015
+ const batchParallelismCeiling =
9016
+ this.governor.policy.pacing.workerToolBatchDefaultParallelism;
8986
9017
  const batchSize =
8987
- compiledBatches.length > 0
9018
+ compiledBatches.length > 0 &&
9019
+ !this.fixtureProviderPacingDisabled()
8988
9020
  ? await this.resourceGovernor.suggestedToolParallelism(
8989
9021
  compiledBatches[0]!.batchOperation,
8990
- this.governor.policy.pacing
8991
- .workerToolBatchDefaultParallelism,
9022
+ batchParallelismCeiling,
8992
9023
  )
8993
- : this.governor.policy.pacing
8994
- .workerToolBatchDefaultParallelism;
9024
+ : batchParallelismCeiling;
8995
9025
  await executeChunkedRequests({
8996
9026
  requests: compiledBatches,
8997
9027
  batchSize,
@@ -9182,11 +9212,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9182
9212
  // each in-flight call, so this only trims the launch burst.
9183
9213
  const toolCallConcurrencyCeiling =
9184
9214
  this.governor.policy.concurrency.toolCalls;
9185
- const shapedToolParallelism =
9186
- await this.resourceGovernor.suggestedToolParallelism(
9187
- toolId,
9188
- toolCallConcurrencyCeiling,
9189
- );
9215
+ const shapedToolParallelism = this.fixtureProviderPacingDisabled()
9216
+ ? toolCallConcurrencyCeiling
9217
+ : await this.resourceGovernor.suggestedToolParallelism(
9218
+ toolId,
9219
+ toolCallConcurrencyCeiling,
9220
+ );
9190
9221
  const dispatchWidth = Math.min(
9191
9222
  toolCallConcurrencyCeiling,
9192
9223
  Math.max(1, shapedToolParallelism),
@@ -9563,6 +9594,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9563
9594
  });
9564
9595
  let transportAttempt = 0;
9565
9596
  let invocationAttempt = 0;
9597
+ let physicalAttempt = 0;
9566
9598
  const retryToolTransportFailure = async (input: {
9567
9599
  error: unknown;
9568
9600
  elapsedMs: number;
@@ -9626,11 +9658,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9626
9658
  };
9627
9659
 
9628
9660
  while (true) {
9661
+ physicalAttempt += 1;
9629
9662
  let response: Response | null = null;
9630
9663
  let responseData: Record<string, unknown> | null = null;
9631
9664
  let responseErrorText: string | null = null;
9632
9665
  let providerCallStartedAt: number | null = null;
9633
9666
  let providerCallElapsedMs: number | null = null;
9667
+ let integrationFetchStartedAt: number | null = null;
9634
9668
  let fetchDispatched = false;
9635
9669
  // Receipt ownership is a liveness contract, not a one-time check.
9636
9670
  // Keep it alive for the whole provider HTTP request. The cadence
@@ -9641,15 +9675,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9641
9675
  timeoutMs || hasReceiptHeartbeat ? new AbortController() : null;
9642
9676
  const receiptHeartbeat = options?.heartbeatReceipt;
9643
9677
  let heartbeatFailure: unknown = null;
9644
- const timeoutHandle = timeoutMs
9645
- ? setTimeout(() => {
9646
- abortController?.abort(
9647
- new Error(
9648
- `Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
9649
- ),
9650
- );
9651
- }, timeoutMs)
9652
- : null;
9678
+ let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
9653
9679
  try {
9654
9680
  const ownershipStartedAt = Date.now();
9655
9681
  await options?.beforeProviderCall?.();
@@ -9705,7 +9731,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9705
9731
  fixtureExecution &&
9706
9732
  this.#options.enforceFixtureProviderPacing === true;
9707
9733
  const fixtureOnlyExecution =
9708
- fixtureExecution && !enforceFixtureProviderPacing;
9734
+ this.fixtureProviderPacingDisabled();
9709
9735
  if (
9710
9736
  fixtureOnlyExecution &&
9711
9737
  !this.fixtureProviderPacingBypassLogged
@@ -9730,69 +9756,181 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9730
9756
  toolId,
9731
9757
  signal: abortController?.signal,
9732
9758
  });
9733
- const integrationFetchStartedAt = Date.now();
9734
- providerCallStartedAt = integrationFetchStartedAt;
9735
9759
  try {
9736
- fetchDispatched = true;
9737
- response = await fetch(url, {
9738
- method: 'POST',
9739
- signal: abortController?.signal,
9740
- headers: {
9741
- 'Content-Type': 'application/json',
9742
- Authorization: `Bearer ${this.#options.executorToken}`,
9743
- [EXECUTE_RESPONSE_CONTRACT_HEADER]:
9744
- V2_EXECUTE_RESPONSE_CONTRACT,
9745
- [EXECUTE_RESPONSE_INTENT_HEADER]: 'dataset',
9746
- [EXECUTE_TOOL_METADATA_HEADER]: 'true',
9747
- [TOOL_EXECUTION_ERROR_SCHEMA_HEADER]: String(
9748
- toolErrorSchemaVersion,
9749
- ),
9750
- 'x-deepline-request-id': deeplineRequestId,
9751
- ...(providerIdempotencyKey
9752
- ? {
9753
- 'x-deepline-idempotency-key':
9754
- providerIdempotencyKey,
9755
- }
9756
- : {}),
9757
- ...protectionHeaders,
9758
- ...(this.#options.runtimeTestFaultHeader
9759
- ? {
9760
- 'x-deepline-test-fault':
9761
- this.#options.runtimeTestFaultHeader,
9762
- }
9763
- : {}),
9764
- },
9765
- body: serializedRequestBody(invocationAttempt),
9766
- });
9767
- if (response.ok) {
9768
- try {
9769
- responseData = await readToolExecuteResponseBody({
9770
- toolId,
9771
- abortController,
9772
- read: () =>
9773
- response!.json() as Promise<Record<string, unknown>>,
9774
- });
9775
- } catch (error) {
9776
- if (error instanceof SyntaxError) {
9777
- throw new ToolExecuteInvalidJsonError(error);
9760
+ // Provider admission is our queue, not provider execution.
9761
+ // Start the tool deadline only after admission so a busy
9762
+ // runtime cannot consume the remote-call budget while this
9763
+ // attempt is still waiting for permission to leave.
9764
+ if (
9765
+ timeoutMs &&
9766
+ abortController &&
9767
+ !abortController.signal.aborted
9768
+ ) {
9769
+ timeoutHandle = setTimeout(() => {
9770
+ abortController.abort(
9771
+ new Error(
9772
+ `Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
9773
+ ),
9774
+ );
9775
+ }, timeoutMs);
9776
+ }
9777
+ const fixtureBehavior = this.#options.fixtureBehavior;
9778
+ if (
9779
+ fixtureExecution &&
9780
+ fixtureBehavior &&
9781
+ shouldRouteFixtureToolId(toolId)
9782
+ ) {
9783
+ const canonicalProvider =
9784
+ (await this.#options.getToolProvider?.(toolId))?.trim() ||
9785
+ provider;
9786
+ if (shouldRouteFixtureProvider(canonicalProvider)) {
9787
+ if (!durableCallReceiptKey) {
9788
+ throw new Error(
9789
+ 'Configured fixture response timing requires a durable call receipt key.',
9790
+ );
9791
+ }
9792
+ const canonicalOperation =
9793
+ (
9794
+ await this.#options.getToolOperation?.(toolId)
9795
+ )?.trim() || toolId;
9796
+ const stableRequestKey = `${canonicalProvider}:${canonicalOperation}:${durableCallReceiptKey}`;
9797
+ const behaviorDigest = stableDigest(
9798
+ JSON.stringify(fixtureBehavior),
9799
+ );
9800
+ const requestKeyDigest = stableDigest(stableRequestKey);
9801
+ let selected: {
9802
+ delayMs: number;
9803
+ sampleIndex: number;
9804
+ } | null = null;
9805
+ const delayStartedAt = Date.now();
9806
+ providerCallStartedAt = delayStartedAt;
9807
+ try {
9808
+ selected = await waitForFixtureResponseDelay({
9809
+ behavior: fixtureBehavior,
9810
+ stableRequestKey,
9811
+ signal: abortController?.signal,
9812
+ onSelected: (value) => {
9813
+ selected = value;
9814
+ this.log(
9815
+ `[fixture.response_delay] ${JSON.stringify({
9816
+ outcome: 'scheduled',
9817
+ provider: canonicalProvider,
9818
+ operation: canonicalOperation,
9819
+ behavior_digest: `sha256_${behaviorDigest}`,
9820
+ request_key_digest: `sha256_${requestKeyDigest}`,
9821
+ delay_ms: value.delayMs,
9822
+ sample_index: value.sampleIndex,
9823
+ physical_attempt: physicalAttempt,
9824
+ })}`,
9825
+ );
9826
+ },
9827
+ });
9828
+ } catch (error) {
9829
+ this.log(
9830
+ `[fixture.response_delay] ${JSON.stringify({
9831
+ outcome: 'aborted',
9832
+ provider: canonicalProvider,
9833
+ operation: canonicalOperation,
9834
+ behavior_digest: `sha256_${behaviorDigest}`,
9835
+ request_key_digest: `sha256_${requestKeyDigest}`,
9836
+ delay_ms: selected?.delayMs ?? null,
9837
+ sample_index: selected?.sampleIndex ?? null,
9838
+ physical_attempt: physicalAttempt,
9839
+ elapsed_ms: Date.now() - delayStartedAt,
9840
+ })}`,
9841
+ );
9842
+ throw error;
9778
9843
  }
9779
- throw new ToolExecuteResponseBodyTransportError(error);
9844
+ this.log(
9845
+ `[fixture.response_delay] ${JSON.stringify({
9846
+ outcome: 'completed',
9847
+ provider: canonicalProvider,
9848
+ operation: canonicalOperation,
9849
+ behavior_digest: `sha256_${behaviorDigest}`,
9850
+ request_key_digest: `sha256_${requestKeyDigest}`,
9851
+ delay_ms: selected.delayMs,
9852
+ sample_index: selected.sampleIndex,
9853
+ physical_attempt: physicalAttempt,
9854
+ elapsed_ms: Date.now() - delayStartedAt,
9855
+ })}`,
9856
+ );
9780
9857
  }
9781
- } else {
9782
- try {
9783
- responseErrorText = await readToolExecuteResponseBody({
9784
- toolId,
9785
- abortController,
9786
- read: () => response!.text(),
9787
- });
9788
- } catch (error) {
9789
- throw new ToolExecuteResponseBodyTransportError(error);
9858
+ }
9859
+ const integrationRequestLease =
9860
+ await this.governor.acquireIntegrationRequestSlot({
9861
+ signal: abortController?.signal,
9862
+ });
9863
+ try {
9864
+ integrationFetchStartedAt = Date.now();
9865
+ providerCallStartedAt ??= integrationFetchStartedAt;
9866
+ fetchDispatched = true;
9867
+ response = await fetch(url, {
9868
+ method: 'POST',
9869
+ signal: abortController?.signal,
9870
+ headers: {
9871
+ 'Content-Type': 'application/json',
9872
+ Authorization: `Bearer ${this.#options.executorToken}`,
9873
+ [EXECUTE_RESPONSE_CONTRACT_HEADER]:
9874
+ V2_EXECUTE_RESPONSE_CONTRACT,
9875
+ [EXECUTE_RESPONSE_INTENT_HEADER]: 'dataset',
9876
+ [EXECUTE_TOOL_METADATA_HEADER]: 'true',
9877
+ [TOOL_EXECUTION_ERROR_SCHEMA_HEADER]: String(
9878
+ toolErrorSchemaVersion,
9879
+ ),
9880
+ 'x-deepline-request-id': deeplineRequestId,
9881
+ ...(providerIdempotencyKey
9882
+ ? {
9883
+ 'x-deepline-idempotency-key':
9884
+ providerIdempotencyKey,
9885
+ }
9886
+ : {}),
9887
+ ...protectionHeaders,
9888
+ ...(this.#options.runtimeTestFaultHeader
9889
+ ? {
9890
+ 'x-deepline-test-fault':
9891
+ this.#options.runtimeTestFaultHeader,
9892
+ }
9893
+ : {}),
9894
+ },
9895
+ body: serializedRequestBody(invocationAttempt),
9896
+ });
9897
+ if (response.ok) {
9898
+ try {
9899
+ responseData = await readToolExecuteResponseBody({
9900
+ toolId,
9901
+ abortController,
9902
+ read: () =>
9903
+ response!.json() as Promise<
9904
+ Record<string, unknown>
9905
+ >,
9906
+ });
9907
+ } catch (error) {
9908
+ if (error instanceof SyntaxError) {
9909
+ throw new ToolExecuteInvalidJsonError(error);
9910
+ }
9911
+ throw new ToolExecuteResponseBodyTransportError(error);
9912
+ }
9913
+ } else {
9914
+ try {
9915
+ responseErrorText = await readToolExecuteResponseBody({
9916
+ toolId,
9917
+ abortController,
9918
+ read: () => response!.text(),
9919
+ });
9920
+ } catch (error) {
9921
+ throw new ToolExecuteResponseBodyTransportError(error);
9922
+ }
9790
9923
  }
9924
+ } finally {
9925
+ integrationRequestLease.release();
9791
9926
  }
9792
9927
  } finally {
9793
9928
  providerPermit.release();
9794
9929
  }
9795
- if (runtimeReceiptReadTraceEnabled) {
9930
+ if (
9931
+ runtimeReceiptReadTraceEnabled &&
9932
+ integrationFetchStartedAt !== null
9933
+ ) {
9796
9934
  this.log(
9797
9935
  `[perf] tool call id=${toolId} phase=integration_fetch_body elapsed_ms=${Date.now() - integrationFetchStartedAt} status=${response.status}`,
9798
9936
  );
@@ -9877,8 +10015,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9877
10015
  durableFenceVerificationSignal,
9878
10016
  )
9879
10017
  .catch(() => false)) === true);
10018
+ // Fixture execution has no provider-side effect. Replaying the
10019
+ // same stable request after our own transport fails is always
10020
+ // safe, even when this runner lacks a live-provider fence.
10021
+ const fixtureReplaySafe =
10022
+ this.#options.integrationMode === 'fixture';
9880
10023
  const transportReplaySafe =
9881
- !ambiguousDispatchedFailure || durableInvocationFenceVerified;
10024
+ !ambiguousDispatchedFailure ||
10025
+ durableInvocationFenceVerified ||
10026
+ fixtureReplaySafe;
9882
10027
  if (!transportReplaySafe) {
9883
10028
  const diagnostic = describeTransportError(transportError);
9884
10029
  this.log(
@@ -42,6 +42,7 @@ import type {
42
42
  ToolExecutionErrorSchemaVersion,
43
43
  ToolExecutionFailureV1,
44
44
  } from '../tool-execution-error';
45
+ import type { FixtureBehavior } from './fixture-behavior';
45
46
 
46
47
  export interface RowState {
47
48
  results: Map<string, unknown>;
@@ -512,6 +513,8 @@ export interface ContextOptions {
512
513
  vercelProtectionBypassToken?: string | null;
513
514
  /** Optional per-run integration execution mode for provider calls. */
514
515
  integrationMode?: 'live' | 'eval_stub' | 'fixture';
516
+ /** Internal fixture-only simulation of provider response residence. */
517
+ fixtureBehavior?: FixtureBehavior | null;
515
518
  /** Preview/dev test seam that applies provider pacing to fixture responses. */
516
519
  enforceFixtureProviderPacing?: boolean;
517
520
  orgId?: string;
@@ -641,6 +644,7 @@ export interface ContextOptions {
641
644
  resolvePlay?: (playRef: string) => Promise<ResolvedPlayExecution | null>;
642
645
  getToolQueueHints?: (toolId: string) => Promise<readonly PlayQueueHint[]>;
643
646
  getToolProvider?: (toolId: string) => Promise<string | null>;
647
+ getToolOperation?: (toolId: string) => Promise<string | null>;
644
648
  getToolRetryPolicy?: (
645
649
  toolId: string,
646
650
  input: Record<string, unknown>,
@@ -1,3 +1,8 @@
1
+ import {
2
+ validateFixtureBehavior,
3
+ type FixtureBehavior,
4
+ } from './fixture-behavior';
5
+
1
6
  export const RUNTIME_EXECUTION_CAPABILITIES = [
2
7
  'artifact.read',
3
8
  'file.read',
@@ -37,6 +42,7 @@ export type RuntimeAuthorityDescriptor = {
37
42
  allowedSecrets: string[];
38
43
  maxCreditsPerRun?: number | null;
39
44
  integrationMode?: 'live' | 'eval_stub' | 'fixture' | null;
45
+ fixtureBehavior?: FixtureBehavior | null;
40
46
  synthetic?: boolean | null;
41
47
  actorUserId?: string | null;
42
48
  actorEmail?: string | null;
@@ -91,11 +97,15 @@ export function requireRuntimeAuthorityDescriptor(
91
97
  const capabilities = normalizeRuntimeExecutionCapabilities(
92
98
  descriptor.capabilities,
93
99
  );
100
+ const fixtureBehavior = validateFixtureBehavior(descriptor.fixtureBehavior);
94
101
  if (
95
102
  requiredStrings.some(
96
103
  (entry) => typeof entry !== 'string' || entry.trim().length === 0,
97
104
  ) ||
98
- capabilities.length === 0
105
+ capabilities.length === 0 ||
106
+ !fixtureBehavior.ok ||
107
+ (fixtureBehavior.behavior !== null &&
108
+ descriptor.integrationMode !== 'fixture')
99
109
  ) {
100
110
  throw new Error(
101
111
  'Durable play launch contains an invalid runtimeAuthority descriptor.',
@@ -114,6 +124,7 @@ export function requireRuntimeAuthorityDescriptor(
114
124
  : [],
115
125
  maxCreditsPerRun: descriptor.maxCreditsPerRun ?? null,
116
126
  integrationMode: descriptor.integrationMode ?? null,
127
+ fixtureBehavior: fixtureBehavior.ok ? fixtureBehavior.behavior : null,
117
128
  synthetic: descriptor.synthetic === true,
118
129
  actorUserId: descriptor.actorUserId ?? null,
119
130
  actorEmail: descriptor.actorEmail ?? null,