deepline 0.2.0 → 0.2.2

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 (32) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +17 -3
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/shared_libs/observability/scheduled-job-errors.ts +95 -0
  4. package/dist/bundling-sources/shared_libs/play-runtime/backend.ts +19 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/modal-runtime-config.ts +104 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +4 -1
  7. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +177 -10
  8. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-modal-fallback.ts +218 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +26 -7
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +34 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +380 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/index.ts +28 -3
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +240 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +28 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +17 -2
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +188 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +77 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +7 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +10 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +39 -0
  21. package/dist/cli/index.js +50 -26
  22. package/dist/cli/index.mjs +50 -26
  23. package/dist/index.d.mts +5 -3
  24. package/dist/index.d.ts +5 -3
  25. package/dist/index.js +50 -5
  26. package/dist/index.mjs +50 -5
  27. package/dist/plays/bundle-play-file.d.mts +2 -2
  28. package/dist/plays/bundle-play-file.d.ts +2 -2
  29. package/dist/plays/bundle-play-file.mjs +7 -1
  30. package/dist/{tool-execution-error-YDz7UMl-.d.mts → tool-execution-error-4-rhemLQ.d.mts} +7 -1
  31. package/dist/{tool-execution-error-YDz7UMl-.d.ts → tool-execution-error-4-rhemLQ.d.ts} +7 -1
  32. package/package.json +1 -1
@@ -10,11 +10,15 @@ import type {
10
10
  } from '@shared_libs/play-runtime/protocol';
11
11
 
12
12
  export type PlayRunnerRuntimeResource = {
13
- kind: 'daytona_sandbox';
13
+ kind: 'daytona_sandbox' | 'modal_sandbox';
14
14
  sandboxId: string;
15
+ /** Runtime estate that owns provider credentials and durable cleanup. */
16
+ runtimeEnvironment?: 'preview' | 'production';
15
17
  daytonaEnvironment?: 'preview' | 'production';
16
18
  /** Stable, non-secret provider ownership domain returned by Daytona. */
17
19
  daytonaOrganizationId?: string;
20
+ /** Stable, non-secret Modal ownership domain returned at acquisition. */
21
+ modalAppId?: string;
18
22
  billingStartedAt: number;
19
23
  billingEndedAt?: number | null;
20
24
  /** Customer liability ceiling for this physical sandbox. */
@@ -36,6 +40,22 @@ export type RuntimeResourceTerminalReason =
36
40
  | 'cancelled'
37
41
  | 'lease_expired';
38
42
 
43
+ /**
44
+ * Safe, provider-edge lifecycle evidence. The scheduler records these before
45
+ * relying on a non-idempotent provider outcome; raw provider responses,
46
+ * commands, inputs, and credentials are intentionally not part of the type.
47
+ */
48
+ export type PlayRunnerRuntimeLifecycleEvent = {
49
+ type:
50
+ | 'daytona_create_call_started'
51
+ | 'daytona_create_call_succeeded'
52
+ | 'daytona_create_call_failed';
53
+ occurredAtMs: number;
54
+ providerAttempt: number;
55
+ sandboxId?: string;
56
+ errorClass?: 'timeout' | 'capacity' | 'rate_limit' | 'other';
57
+ };
58
+
39
59
  export class RuntimeResourceFenceLostError extends Error {
40
60
  constructor(message: string) {
41
61
  super(message);
@@ -51,6 +71,13 @@ export interface PlayRunnerCallbacks {
51
71
  onRuntimeResourceAcquired?: (
52
72
  resource: PlayRunnerRuntimeResource,
53
73
  ) => void | Promise<void>;
74
+ /**
75
+ * Durable scheduler-owned evidence for a provider create call. Backends
76
+ * await this callback at the create boundary; it is not best-effort logging.
77
+ */
78
+ onRuntimeLifecycleEvent?: (
79
+ event: PlayRunnerRuntimeLifecycleEvent,
80
+ ) => Promise<void>;
54
81
  /**
55
82
  * Scheduler-owned readiness read for a detached runner attempt. Daytona uses
56
83
  * this direct control-plane port before parking; it is never serialized into
@@ -1,3 +1,5 @@
1
+ import { PLAY_RUNTIME_BACKENDS, type PlayRuntimeBackendId } from './backend';
2
+
1
3
  export const PLAY_RUNTIME_ENVIRONMENTS = ['preview'] as const;
2
4
 
3
5
  export type PlayRuntimeEnvironment = (typeof PLAY_RUNTIME_ENVIRONMENTS)[number];
@@ -6,6 +8,8 @@ export type PlayRuntimeSelection = {
6
8
  environment: 'preview';
7
9
  /** Caller-named isolation scope inside a remote runtime environment. */
8
10
  namespace: string;
11
+ /** Explicit managed-sandbox executor. Omission preserves Daytona compatibility. */
12
+ backend?: Extract<PlayRuntimeBackendId, 'daytona' | 'modal'>;
9
13
  };
10
14
 
11
15
  const PLAY_RUNTIME_NAMESPACE_PATTERN = /^[a-z][a-z0-9-]{0,30}$/;
@@ -24,14 +28,25 @@ export function normalizePlayRuntimeSelection(
24
28
  const record = value as Record<string, unknown>;
25
29
  if (
26
30
  Object.keys(record).some(
27
- (key) => key !== 'environment' && key !== 'namespace',
31
+ (key) =>
32
+ key !== 'environment' && key !== 'namespace' && key !== 'backend',
28
33
  ) ||
29
34
  record.environment !== 'preview'
30
35
  ) {
31
36
  return null;
32
37
  }
33
38
  const namespace = normalizePlayRuntimeNamespace(record.namespace);
34
- return namespace ? { environment: 'preview', namespace } : null;
39
+ if (!namespace) return null;
40
+ if (record.backend === undefined) {
41
+ return { environment: 'preview', namespace };
42
+ }
43
+ if (
44
+ record.backend !== PLAY_RUNTIME_BACKENDS.daytona &&
45
+ record.backend !== PLAY_RUNTIME_BACKENDS.modal
46
+ ) {
47
+ return null;
48
+ }
49
+ return { environment: 'preview', namespace, backend: record.backend };
35
50
  }
36
51
 
37
52
  export function normalizePlayRuntimeEnvironment(
@@ -0,0 +1,188 @@
1
+ import { PLAY_RUNTIME_BACKENDS, type PlayRuntimeBackendId } from './backend';
2
+
3
+ export const RUNTIME_SANDBOX_PROVIDERS = {
4
+ daytona: 'daytona',
5
+ modal: 'modal',
6
+ } as const;
7
+
8
+ export type RuntimeSandboxProvider =
9
+ (typeof RUNTIME_SANDBOX_PROVIDERS)[keyof typeof RUNTIME_SANDBOX_PROVIDERS];
10
+
11
+ /**
12
+ * Stable durable ids. Changing provider order or transition rules creates a
13
+ * new version instead of changing the meaning of an already-queued launch.
14
+ */
15
+ export const RUNTIME_SANDBOX_PLACEMENT_POLICIES = {
16
+ daytonaOnlyV1: 'daytona_only@1',
17
+ daytonaThenModalV1: 'daytona_then_modal@1',
18
+ modalOnlyV1: 'modal_only@1',
19
+ } as const;
20
+
21
+ export type RuntimeSandboxPlacementPolicyId =
22
+ (typeof RUNTIME_SANDBOX_PLACEMENT_POLICIES)[keyof typeof RUNTIME_SANDBOX_PLACEMENT_POLICIES];
23
+
24
+ export type RuntimeSandboxPlacementFailureReason =
25
+ | 'provider_capacity_exhausted'
26
+ | 'provider_start_timeout_before_execution';
27
+
28
+ export type RuntimeSandboxPlacementTransition = Readonly<{
29
+ from: RuntimeSandboxProvider;
30
+ to: RuntimeSandboxProvider;
31
+ stage: 'acquisition';
32
+ reasons: readonly RuntimeSandboxPlacementFailureReason[];
33
+ }>;
34
+
35
+ export type RuntimeSandboxPlacementPolicy = Readonly<{
36
+ id: RuntimeSandboxPlacementPolicyId;
37
+ compatibilityRuntimeBackend:
38
+ | typeof PLAY_RUNTIME_BACKENDS.daytona
39
+ | typeof PLAY_RUNTIME_BACKENDS.modal;
40
+ providers: readonly [RuntimeSandboxProvider, ...RuntimeSandboxProvider[]];
41
+ transitions: readonly RuntimeSandboxPlacementTransition[];
42
+ requiredCredentialEnv: readonly string[];
43
+ cleanupProviders: readonly RuntimeSandboxProvider[];
44
+ }>;
45
+
46
+ const DAYTONA_THEN_MODAL_V1: RuntimeSandboxPlacementPolicy = {
47
+ id: RUNTIME_SANDBOX_PLACEMENT_POLICIES.daytonaThenModalV1,
48
+ compatibilityRuntimeBackend: PLAY_RUNTIME_BACKENDS.daytona,
49
+ providers: [
50
+ RUNTIME_SANDBOX_PROVIDERS.daytona,
51
+ RUNTIME_SANDBOX_PROVIDERS.modal,
52
+ ],
53
+ transitions: [
54
+ {
55
+ from: RUNTIME_SANDBOX_PROVIDERS.daytona,
56
+ to: RUNTIME_SANDBOX_PROVIDERS.modal,
57
+ stage: 'acquisition',
58
+ reasons: [
59
+ 'provider_capacity_exhausted',
60
+ 'provider_start_timeout_before_execution',
61
+ ],
62
+ },
63
+ ],
64
+ requiredCredentialEnv: [
65
+ 'DAYTONA_API_KEY',
66
+ 'MODAL_TOKEN_ID',
67
+ 'MODAL_TOKEN_SECRET',
68
+ ],
69
+ cleanupProviders: [
70
+ RUNTIME_SANDBOX_PROVIDERS.daytona,
71
+ RUNTIME_SANDBOX_PROVIDERS.modal,
72
+ ],
73
+ };
74
+
75
+ /**
76
+ * Frozen compatibility policy for launches written before placement policy ids
77
+ * existed. It is retained until every pre-policy release lane has drained.
78
+ */
79
+ const DAYTONA_ONLY_V1: RuntimeSandboxPlacementPolicy = {
80
+ id: RUNTIME_SANDBOX_PLACEMENT_POLICIES.daytonaOnlyV1,
81
+ compatibilityRuntimeBackend: PLAY_RUNTIME_BACKENDS.daytona,
82
+ providers: [RUNTIME_SANDBOX_PROVIDERS.daytona],
83
+ transitions: [],
84
+ requiredCredentialEnv: ['DAYTONA_API_KEY'],
85
+ cleanupProviders: [RUNTIME_SANDBOX_PROVIDERS.daytona],
86
+ };
87
+
88
+ const MODAL_ONLY_V1: RuntimeSandboxPlacementPolicy = {
89
+ id: RUNTIME_SANDBOX_PLACEMENT_POLICIES.modalOnlyV1,
90
+ compatibilityRuntimeBackend: PLAY_RUNTIME_BACKENDS.modal,
91
+ providers: [RUNTIME_SANDBOX_PROVIDERS.modal],
92
+ transitions: [],
93
+ requiredCredentialEnv: ['MODAL_TOKEN_ID', 'MODAL_TOKEN_SECRET'],
94
+ cleanupProviders: [RUNTIME_SANDBOX_PROVIDERS.modal],
95
+ };
96
+
97
+ export const RUNTIME_SANDBOX_PLACEMENT_POLICY_CATALOG: Readonly<
98
+ Record<RuntimeSandboxPlacementPolicyId, RuntimeSandboxPlacementPolicy>
99
+ > = {
100
+ [RUNTIME_SANDBOX_PLACEMENT_POLICIES.daytonaOnlyV1]: DAYTONA_ONLY_V1,
101
+ [RUNTIME_SANDBOX_PLACEMENT_POLICIES.daytonaThenModalV1]:
102
+ DAYTONA_THEN_MODAL_V1,
103
+ [RUNTIME_SANDBOX_PLACEMENT_POLICIES.modalOnlyV1]: MODAL_ONLY_V1,
104
+ };
105
+
106
+ export function resolveRuntimeSandboxPlacementPolicy(
107
+ id: RuntimeSandboxPlacementPolicyId | string,
108
+ ): RuntimeSandboxPlacementPolicy {
109
+ const normalized = id.trim();
110
+ const policy =
111
+ RUNTIME_SANDBOX_PLACEMENT_POLICY_CATALOG[
112
+ normalized as RuntimeSandboxPlacementPolicyId
113
+ ];
114
+ if (!policy) {
115
+ throw new Error(
116
+ `Unsupported runtime sandbox placement policy "${normalized}". Expected one of: ${Object.keys(
117
+ RUNTIME_SANDBOX_PLACEMENT_POLICY_CATALOG,
118
+ ).join(', ')}.`,
119
+ );
120
+ }
121
+ return policy;
122
+ }
123
+
124
+ /**
125
+ * Compatibility Adapter for the public runtimeBackend selector. The returned
126
+ * policy id, not this selector, is the durable execution contract.
127
+ */
128
+ export function runtimeSandboxPlacementPolicyForBackend(
129
+ backend: PlayRuntimeBackendId,
130
+ ): RuntimeSandboxPlacementPolicy | null {
131
+ if (backend === PLAY_RUNTIME_BACKENDS.daytona) {
132
+ return DAYTONA_THEN_MODAL_V1;
133
+ }
134
+ if (backend === PLAY_RUNTIME_BACKENDS.modal) {
135
+ return MODAL_ONLY_V1;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ export function runtimeSandboxPlacementPoliciesForBackend(
141
+ backend: PlayRuntimeBackendId,
142
+ ): readonly RuntimeSandboxPlacementPolicy[] {
143
+ if (backend === PLAY_RUNTIME_BACKENDS.localProcess) return [];
144
+ return Object.values(RUNTIME_SANDBOX_PLACEMENT_POLICY_CATALOG).filter(
145
+ (policy) => policy.compatibilityRuntimeBackend === backend,
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Old queued launches have no policy id. Derive their historical meaning from
151
+ * runtimeBackend only when the durable field is absent. Unknown stored ids fail
152
+ * loudly rather than silently changing execution semantics.
153
+ */
154
+ export function resolveLaunchRuntimeSandboxPlacementPolicy(input: {
155
+ runtimeSandboxPlacementPolicyId?: string | null;
156
+ runtimeBackend: PlayRuntimeBackendId;
157
+ }): RuntimeSandboxPlacementPolicy | null {
158
+ const storedId = input.runtimeSandboxPlacementPolicyId?.trim();
159
+ if (storedId) {
160
+ const policy = resolveRuntimeSandboxPlacementPolicy(storedId);
161
+ if (policy.compatibilityRuntimeBackend !== input.runtimeBackend) {
162
+ throw new Error(
163
+ `Runtime sandbox placement policy ${policy.id} is incompatible with runtime backend ${input.runtimeBackend}.`,
164
+ );
165
+ }
166
+ return policy;
167
+ }
168
+ if (input.runtimeBackend === PLAY_RUNTIME_BACKENDS.daytona) {
169
+ return DAYTONA_ONLY_V1;
170
+ }
171
+ return runtimeSandboxPlacementPolicyForBackend(input.runtimeBackend);
172
+ }
173
+
174
+ export function canTransitionRuntimeSandboxPlacement(input: {
175
+ policy: RuntimeSandboxPlacementPolicy;
176
+ from: RuntimeSandboxProvider;
177
+ to: RuntimeSandboxProvider;
178
+ stage: 'acquisition';
179
+ reason: RuntimeSandboxPlacementFailureReason;
180
+ }): boolean {
181
+ return input.policy.transitions.some(
182
+ (transition) =>
183
+ transition.from === input.from &&
184
+ transition.to === input.to &&
185
+ transition.stage === input.stage &&
186
+ transition.reasons.includes(input.reason),
187
+ );
188
+ }
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  resolveDaytonaSandboxComputeItem,
3
+ resolveModalSandboxComputeItem,
3
4
  type ComputeBillingItem,
4
5
  } from './worker-api-types';
5
6
 
@@ -9,6 +10,82 @@ function finiteNonNegative(value: unknown): number | null {
9
10
  : null;
10
11
  }
11
12
 
13
+ /** Project every provider-owned sandbox into a distinct internal compute item. */
14
+ export function resolveRuntimeSandboxComputeItems(input: {
15
+ resources: readonly unknown[];
16
+ endedAt: number;
17
+ }): ComputeBillingItem[] {
18
+ const daytonaResources: unknown[] = [];
19
+ const modalResources = new Map<
20
+ string,
21
+ {
22
+ startedAt: number;
23
+ endedAt: number | null;
24
+ physicalCpuCores: number | null;
25
+ memoryGiB: number | null;
26
+ maxBillingDurationSeconds: number | null;
27
+ }
28
+ >();
29
+ for (const resource of input.resources) {
30
+ if (!resource || typeof resource !== 'object' || Array.isArray(resource)) {
31
+ continue;
32
+ }
33
+ const value = resource as Record<string, unknown>;
34
+ if (value.kind === 'daytona_sandbox') {
35
+ daytonaResources.push(resource);
36
+ continue;
37
+ }
38
+ if (value.kind !== 'modal_sandbox') continue;
39
+ const sandboxId =
40
+ typeof value.sandboxId === 'string' ? value.sandboxId.trim() : '';
41
+ const startedAt = finiteNonNegative(value.billingStartedAt);
42
+ if (!sandboxId || startedAt === null) continue;
43
+ const endedAt = finiteNonNegative(value.billingEndedAt);
44
+ const existing = modalResources.get(sandboxId);
45
+ modalResources.set(sandboxId, {
46
+ startedAt: Math.min(existing?.startedAt ?? startedAt, startedAt),
47
+ endedAt:
48
+ endedAt === null
49
+ ? (existing?.endedAt ?? null)
50
+ : Math.max(existing?.endedAt ?? endedAt, endedAt),
51
+ physicalCpuCores:
52
+ finiteNonNegative(value.cpu) ?? existing?.physicalCpuCores ?? null,
53
+ memoryGiB:
54
+ finiteNonNegative(value.memoryGiB) ?? existing?.memoryGiB ?? null,
55
+ maxBillingDurationSeconds:
56
+ finiteNonNegative(value.maxBillingDurationSeconds) ??
57
+ existing?.maxBillingDurationSeconds ??
58
+ null,
59
+ });
60
+ }
61
+ const items = resolveDaytonaRuntimeComputeItems({
62
+ resources: daytonaResources,
63
+ endedAt: input.endedAt,
64
+ });
65
+ for (const [sandboxId, resource] of modalResources) {
66
+ const observedEndedAt = resource.endedAt ?? input.endedAt;
67
+ const endedAt =
68
+ resource.maxBillingDurationSeconds === null
69
+ ? observedEndedAt
70
+ : Math.min(
71
+ observedEndedAt,
72
+ resource.startedAt + resource.maxBillingDurationSeconds * 1_000,
73
+ );
74
+ items.push(
75
+ resolveModalSandboxComputeItem({
76
+ itemId: `modal:${sandboxId}`,
77
+ wallTimeSeconds: Math.max(0, endedAt - resource.startedAt) / 1_000,
78
+ physicalCpuCores: resource.physicalCpuCores ?? 0.5,
79
+ memoryGiB: resource.memoryGiB ?? 1,
80
+ sandboxId,
81
+ startedAt: resource.startedAt,
82
+ endedAt,
83
+ }),
84
+ );
85
+ }
86
+ return items;
87
+ }
88
+
12
89
  /** Project executor resource facts into one idempotent item per real sandbox. */
13
90
  export function resolveDaytonaRuntimeComputeItems(input: {
14
91
  resources: readonly unknown[];
@@ -21,6 +21,7 @@ import type { RuntimeAuthorityDescriptor } from './execution-capabilities';
21
21
  import type { PlayRunnerRuntimeTiming } from './protocol';
22
22
  import type { RuntimeTestPolicyOverrides } from './test-runtime-seams';
23
23
  import type { PlayRunInputPayload } from './play-input';
24
+ import type { RuntimeSandboxPlacementPolicyId } from './runtime-sandbox-placement-policy';
24
25
 
25
26
  export const PLAY_SCHEDULER_BACKENDS = {
26
27
  /**
@@ -114,6 +115,12 @@ export type PlaySchedulerSubmitInput = {
114
115
  executionProfile?: string | null;
115
116
  /** runner backend to use for executing attempts */
116
117
  runtimeBackend: string;
118
+ /**
119
+ * Versioned managed-sandbox placement contract. Optional only so launches
120
+ * queued before the policy field existed can drain using runtimeBackend's
121
+ * historical meaning.
122
+ */
123
+ runtimeSandboxPlacementPolicyId?: RuntimeSandboxPlacementPolicyId | null;
117
124
  /** dedup backend for cross-attempt cross-process idempotency */
118
125
  dedupBackend: string;
119
126
  /** If known at submit time, total input rows (for partition decisions). */
@@ -32,6 +32,16 @@ export type PlayExecutionSuspension =
32
32
  * the key under which its pushed terminal is recorded and its wake event
33
33
  * is named. */
34
34
  runnerAttempt: number;
35
+ /** Missing on historical records, which are Daytona. */
36
+ sandboxProvider?: 'daytona' | 'modal';
37
+ /** Versioned physical identity for provider-safe wake and cleanup.
38
+ * Legacy fields remain until every launch-pinned release has drained. */
39
+ runtimeSandboxRef?: {
40
+ schemaVersion: 1;
41
+ provider: 'daytona' | 'modal';
42
+ resourceId: string;
43
+ routingDomain: string | null;
44
+ };
35
45
  sandboxId: string;
36
46
  sessionId: string;
37
47
  cmdId: string;
@@ -42,6 +42,12 @@ export const DAYTONA_COMPUTE_PRICING_USD = {
42
42
  includedStorageGiB: 5,
43
43
  } as const;
44
44
 
45
+ /** Modal Sandbox on-demand rates, per second (modal.com/pricing, 2026-07-29). */
46
+ export const MODAL_SANDBOX_COMPUTE_PRICING_USD = {
47
+ physicalCoreSecond: 0.00003942,
48
+ memoryGiBSecond: 0.00000667,
49
+ } as const;
50
+
45
51
  const NEON_COMPUTE_PRICING_USD = {
46
52
  cuHour: 0.106,
47
53
  defaultCu: 1,
@@ -94,6 +100,39 @@ export function resolveDaytonaSandboxComputeItem(input: {
94
100
  };
95
101
  }
96
102
 
103
+ export function resolveModalSandboxComputeItem(input: {
104
+ itemId: string;
105
+ wallTimeSeconds: number;
106
+ physicalCpuCores: number;
107
+ memoryGiB: number;
108
+ sandboxId?: string | null;
109
+ startedAt?: number;
110
+ endedAt?: number;
111
+ }): ComputeBillingItem {
112
+ const billableSeconds = Math.max(1, Math.ceil(input.wallTimeSeconds));
113
+ const physicalCpuCores = Math.max(0, input.physicalCpuCores);
114
+ const memoryGiB = Math.max(0, input.memoryGiB);
115
+ const providerCostUsd = roundUsd(
116
+ billableSeconds *
117
+ (physicalCpuCores * MODAL_SANDBOX_COMPUTE_PRICING_USD.physicalCoreSecond +
118
+ memoryGiB * MODAL_SANDBOX_COMPUTE_PRICING_USD.memoryGiBSecond),
119
+ );
120
+ return {
121
+ itemId: input.itemId,
122
+ source: 'modal',
123
+ unit: 'sandbox_second',
124
+ units: billableSeconds,
125
+ providerCostUsd,
126
+ metadata: {
127
+ sandboxId: input.sandboxId ?? null,
128
+ physicalCpuCores,
129
+ memoryGiB,
130
+ startedAt: input.startedAt,
131
+ endedAt: input.endedAt,
132
+ },
133
+ };
134
+ }
135
+
97
136
  export function resolveNeonComputeItem(input: {
98
137
  itemId: string;
99
138
  activeSeconds: number;
package/dist/cli/index.js CHANGED
@@ -1040,7 +1040,7 @@ var SDK_RELEASE = {
1040
1040
  // 0.2.0 makes Dataset Handles uniformly async-only after 0.1.320 briefly
1041
1041
  // exposed storage-dependent synchronous access. This deliberate minor
1042
1042
  // release keeps lazy paging semantics independent of row residency.
1043
- version: "0.2.0",
1043
+ version: "0.2.2",
1044
1044
  contracts: {
1045
1045
  api: {
1046
1046
  name: "sdk-http-api",
@@ -3247,6 +3247,33 @@ function usesExtendedTheirstackJobSearchBudget(endpointId, payload) {
3247
3247
  return maxAgeDays !== null && maxAgeDays >= 365 ? true : usesLongExplicitDateWindow(payload);
3248
3248
  }
3249
3249
 
3250
+ // ../shared_libs/play-runtime/backend.ts
3251
+ var PLAY_RUNTIME_BACKENDS = {
3252
+ localProcess: "local_process",
3253
+ daytona: "daytona",
3254
+ modal: "modal"
3255
+ };
3256
+ var PLAY_ARTIFACT_KINDS = {
3257
+ cjsNode20: "cjs_node20"
3258
+ };
3259
+ var PLAY_BACKEND_DESCRIPTORS = {
3260
+ [PLAY_RUNTIME_BACKENDS.localProcess]: {
3261
+ id: PLAY_RUNTIME_BACKENDS.localProcess,
3262
+ artifactKind: PLAY_ARTIFACT_KINDS.cjsNode20,
3263
+ label: "Local node subprocess"
3264
+ },
3265
+ [PLAY_RUNTIME_BACKENDS.daytona]: {
3266
+ id: PLAY_RUNTIME_BACKENDS.daytona,
3267
+ artifactKind: PLAY_ARTIFACT_KINDS.cjsNode20,
3268
+ label: "Daytona sandbox"
3269
+ },
3270
+ [PLAY_RUNTIME_BACKENDS.modal]: {
3271
+ id: PLAY_RUNTIME_BACKENDS.modal,
3272
+ artifactKind: PLAY_ARTIFACT_KINDS.cjsNode20,
3273
+ label: "Modal sandbox"
3274
+ }
3275
+ };
3276
+
3250
3277
  // ../shared_libs/play-runtime/runtime-environment.ts
3251
3278
  var PLAY_RUNTIME_ENVIRONMENTS = ["preview"];
3252
3279
  var PLAY_RUNTIME_NAMESPACE_PATTERN = /^[a-z][a-z0-9-]{0,30}$/;
@@ -3259,12 +3286,19 @@ function normalizePlayRuntimeSelection(value) {
3259
3286
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
3260
3287
  const record = value;
3261
3288
  if (Object.keys(record).some(
3262
- (key) => key !== "environment" && key !== "namespace"
3289
+ (key) => key !== "environment" && key !== "namespace" && key !== "backend"
3263
3290
  ) || record.environment !== "preview") {
3264
3291
  return null;
3265
3292
  }
3266
3293
  const namespace = normalizePlayRuntimeNamespace(record.namespace);
3267
- return namespace ? { environment: "preview", namespace } : null;
3294
+ if (!namespace) return null;
3295
+ if (record.backend === void 0) {
3296
+ return { environment: "preview", namespace };
3297
+ }
3298
+ if (record.backend !== PLAY_RUNTIME_BACKENDS.daytona && record.backend !== PLAY_RUNTIME_BACKENDS.modal) {
3299
+ return null;
3300
+ }
3301
+ return { environment: "preview", namespace, backend: record.backend };
3268
3302
  }
3269
3303
  function normalizePlayRuntimeEnvironment(value) {
3270
3304
  return typeof value === "string" && PLAY_RUNTIME_ENVIRONMENTS.includes(value) ? value : null;
@@ -3297,7 +3331,7 @@ function resolvePlayRunRuntimeSelection(request) {
3297
3331
  const runtime = normalizePlayRuntimeSelection(request.runtime);
3298
3332
  if (!runtime) {
3299
3333
  throw new DeeplineError(
3300
- 'runtime must be exactly { environment: "preview", namespace } with namespace matching ^[a-z][a-z0-9-]{0,30}$.',
3334
+ 'runtime must be { environment: "preview", namespace, backend?: "daytona" | "modal" } with namespace matching ^[a-z][a-z0-9-]{0,30}$.',
3301
3335
  void 0,
3302
3336
  "INVALID_RUNTIME_SELECTION"
3303
3337
  );
@@ -3333,7 +3367,18 @@ function resolvePlayRunRuntimeSelection(request) {
3333
3367
  "INVALID_RUNTIME_NAMESPACE"
3334
3368
  );
3335
3369
  }
3336
- return { environment, namespace };
3370
+ const configuredBackend = process.env.DEEPLINE_PLAY_RUNNER_BACKEND?.trim();
3371
+ if (!configuredBackend) {
3372
+ return { environment, namespace };
3373
+ }
3374
+ if (configuredBackend !== "daytona" && configuredBackend !== "modal") {
3375
+ throw new DeeplineError(
3376
+ `DEEPLINE_PLAY_RUNNER_BACKEND must be daytona or modal for preview runtime selection. Received "${configuredBackend}".`,
3377
+ void 0,
3378
+ "INVALID_RUNTIME_BACKEND"
3379
+ );
3380
+ }
3381
+ return { environment, namespace, backend: configuredBackend };
3337
3382
  }
3338
3383
  function runtimeSelectionHeaders(runtime) {
3339
3384
  if (!runtime) return void 0;
@@ -12067,27 +12112,6 @@ ${hint}`;
12067
12112
  });
12068
12113
  }
12069
12114
 
12070
- // ../shared_libs/play-runtime/backend.ts
12071
- var PLAY_RUNTIME_BACKENDS = {
12072
- localProcess: "local_process",
12073
- daytona: "daytona"
12074
- };
12075
- var PLAY_ARTIFACT_KINDS = {
12076
- cjsNode20: "cjs_node20"
12077
- };
12078
- var PLAY_BACKEND_DESCRIPTORS = {
12079
- [PLAY_RUNTIME_BACKENDS.localProcess]: {
12080
- id: PLAY_RUNTIME_BACKENDS.localProcess,
12081
- artifactKind: PLAY_ARTIFACT_KINDS.cjsNode20,
12082
- label: "Local node subprocess"
12083
- },
12084
- [PLAY_RUNTIME_BACKENDS.daytona]: {
12085
- id: PLAY_RUNTIME_BACKENDS.daytona,
12086
- artifactKind: PLAY_ARTIFACT_KINDS.cjsNode20,
12087
- label: "Daytona sandbox"
12088
- }
12089
- };
12090
-
12091
12115
  // ../shared_libs/play-runtime/dedup-backend.ts
12092
12116
  var PLAY_DEDUP_BACKENDS = {
12093
12117
  inMemory: "in_memory",