deepline 0.2.30 → 0.2.32
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.
- package/dist/bundling-sources/sdk/src/client.ts +6 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +252 -80
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +4 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +12 -1
- package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +421 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +18 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +12 -2
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +12 -6
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-modal-fallback.ts +4 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +100 -3
- package/dist/cli/index.js +253 -7
- package/dist/cli/index.mjs +253 -7
- package/dist/index.d.mts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +3 -1
- package/dist/index.mjs +3 -1
- package/dist/install-integrity.json +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
export const FIXTURE_BEHAVIOR_VERSION = 1 as const;
|
|
2
|
+
export const FIXTURE_BEHAVIOR_RESPONSE_VERSION = 2 as const;
|
|
3
|
+
export const MAX_FIXTURE_RESPONSE_DELAY_SAMPLES = 256;
|
|
4
|
+
export const MAX_FIXTURE_RESPONSE_DELAY_MS = 8 * 60_000;
|
|
5
|
+
export const MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH = 500;
|
|
6
|
+
|
|
7
|
+
export type FixtureBehaviorV1 = {
|
|
8
|
+
version: typeof FIXTURE_BEHAVIOR_VERSION;
|
|
9
|
+
responseDelaySamplesMs: number[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type FixtureResponseSample = {
|
|
13
|
+
delayMs: number;
|
|
14
|
+
when?: {
|
|
15
|
+
provider?: string;
|
|
16
|
+
operation?: string;
|
|
17
|
+
};
|
|
18
|
+
httpError?: {
|
|
19
|
+
status: number;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type FixtureBehaviorV2 = {
|
|
25
|
+
version: typeof FIXTURE_BEHAVIOR_RESPONSE_VERSION;
|
|
26
|
+
responseSamples: FixtureResponseSample[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type FixtureBehavior = FixtureBehaviorV1 | FixtureBehaviorV2;
|
|
30
|
+
|
|
31
|
+
/** Providers whose fixture-mode path intentionally remains a real local/test adapter. */
|
|
32
|
+
const FIXTURE_MODE_BYPASS_PROVIDERS = new Set([
|
|
33
|
+
'neon',
|
|
34
|
+
'local',
|
|
35
|
+
'test',
|
|
36
|
+
'test_high',
|
|
37
|
+
'test_low',
|
|
38
|
+
'test_unhinted',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
type FixtureBehaviorValidationResult =
|
|
42
|
+
| { ok: true; behavior: FixtureBehavior | null }
|
|
43
|
+
| { ok: false; error: string };
|
|
44
|
+
|
|
45
|
+
export function validateFixtureBehavior(
|
|
46
|
+
value: unknown,
|
|
47
|
+
): FixtureBehaviorValidationResult {
|
|
48
|
+
if (value === undefined || value === null) {
|
|
49
|
+
return { ok: true, behavior: null };
|
|
50
|
+
}
|
|
51
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
52
|
+
return { ok: false, error: 'fixtureBehavior must be a JSON object.' };
|
|
53
|
+
}
|
|
54
|
+
const record = value as Record<string, unknown>;
|
|
55
|
+
const supportedKeys =
|
|
56
|
+
record.version === FIXTURE_BEHAVIOR_RESPONSE_VERSION
|
|
57
|
+
? new Set(['version', 'responseSamples'])
|
|
58
|
+
: new Set(['version', 'responseDelaySamplesMs']);
|
|
59
|
+
const unknownKeys = Object.keys(record).filter(
|
|
60
|
+
(key) => !supportedKeys.has(key),
|
|
61
|
+
);
|
|
62
|
+
if (unknownKeys.length > 0) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
error: `Unsupported fixtureBehavior field "${unknownKeys[0]}".`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
record.version !== FIXTURE_BEHAVIOR_VERSION &&
|
|
70
|
+
record.version !== FIXTURE_BEHAVIOR_RESPONSE_VERSION
|
|
71
|
+
) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error:
|
|
75
|
+
`fixtureBehavior.version must be ${FIXTURE_BEHAVIOR_VERSION} or ` +
|
|
76
|
+
`${FIXTURE_BEHAVIOR_RESPONSE_VERSION}.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (record.version === FIXTURE_BEHAVIOR_RESPONSE_VERSION) {
|
|
80
|
+
if (
|
|
81
|
+
!Array.isArray(record.responseSamples) ||
|
|
82
|
+
record.responseSamples.length === 0 ||
|
|
83
|
+
record.responseSamples.length > MAX_FIXTURE_RESPONSE_DELAY_SAMPLES
|
|
84
|
+
) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
error:
|
|
88
|
+
'fixtureBehavior.responseSamples must contain between 1 and ' +
|
|
89
|
+
`${MAX_FIXTURE_RESPONSE_DELAY_SAMPLES} values.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const samples: FixtureResponseSample[] = [];
|
|
93
|
+
for (let index = 0; index < record.responseSamples.length; index += 1) {
|
|
94
|
+
const value = record.responseSamples[index];
|
|
95
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: `fixtureBehavior.responseSamples[${index}] must be an object.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const sample = value as Record<string, unknown>;
|
|
102
|
+
const sampleUnknownKeys = Object.keys(sample).filter(
|
|
103
|
+
(key) => key !== 'delayMs' && key !== 'when' && key !== 'httpError',
|
|
104
|
+
);
|
|
105
|
+
if (sampleUnknownKeys.length > 0) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error:
|
|
109
|
+
`Unsupported fixtureBehavior.responseSamples[${index}] field ` +
|
|
110
|
+
`"${sampleUnknownKeys[0]}".`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
typeof sample.delayMs !== 'number' ||
|
|
115
|
+
!Number.isSafeInteger(sample.delayMs) ||
|
|
116
|
+
sample.delayMs < 0 ||
|
|
117
|
+
sample.delayMs > MAX_FIXTURE_RESPONSE_DELAY_MS
|
|
118
|
+
) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error:
|
|
122
|
+
`fixtureBehavior.responseSamples[${index}].delayMs must be an ` +
|
|
123
|
+
`integer between 0 and ${MAX_FIXTURE_RESPONSE_DELAY_MS}.`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
let when: FixtureResponseSample['when'];
|
|
127
|
+
if (sample.when !== undefined) {
|
|
128
|
+
if (
|
|
129
|
+
!sample.when ||
|
|
130
|
+
typeof sample.when !== 'object' ||
|
|
131
|
+
Array.isArray(sample.when)
|
|
132
|
+
) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
error:
|
|
136
|
+
`fixtureBehavior.responseSamples[${index}].when must be an ` +
|
|
137
|
+
'object.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const condition = sample.when as Record<string, unknown>;
|
|
141
|
+
const conditionUnknownKeys = Object.keys(condition).filter(
|
|
142
|
+
(key) => key !== 'provider' && key !== 'operation',
|
|
143
|
+
);
|
|
144
|
+
if (conditionUnknownKeys.length > 0) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
error:
|
|
148
|
+
`Unsupported fixtureBehavior.responseSamples[${index}].when ` +
|
|
149
|
+
`field "${conditionUnknownKeys[0]}".`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const provider = normalizeFixtureScopeValue(condition.provider, 100);
|
|
153
|
+
const operation = normalizeFixtureScopeValue(condition.operation, 200);
|
|
154
|
+
if (provider.error || operation.error) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
error:
|
|
158
|
+
`fixtureBehavior.responseSamples[${index}].when ` +
|
|
159
|
+
(provider.error ?? operation.error),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!provider.value && !operation.value) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
error:
|
|
166
|
+
`fixtureBehavior.responseSamples[${index}].when must select a ` +
|
|
167
|
+
'provider or operation.',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
when = {
|
|
171
|
+
...(provider.value ? { provider: provider.value } : {}),
|
|
172
|
+
...(operation.value ? { operation: operation.value } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
let httpError: FixtureResponseSample['httpError'];
|
|
176
|
+
if (sample.httpError !== undefined) {
|
|
177
|
+
if (
|
|
178
|
+
!sample.httpError ||
|
|
179
|
+
typeof sample.httpError !== 'object' ||
|
|
180
|
+
Array.isArray(sample.httpError)
|
|
181
|
+
) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
error:
|
|
185
|
+
`fixtureBehavior.responseSamples[${index}].httpError must be ` +
|
|
186
|
+
'an object.',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const error = sample.httpError as Record<string, unknown>;
|
|
190
|
+
const errorUnknownKeys = Object.keys(error).filter(
|
|
191
|
+
(key) => key !== 'status' && key !== 'message',
|
|
192
|
+
);
|
|
193
|
+
if (errorUnknownKeys.length > 0) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error:
|
|
197
|
+
`Unsupported fixtureBehavior.responseSamples[${index}]` +
|
|
198
|
+
`.httpError field "${errorUnknownKeys[0]}".`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (
|
|
202
|
+
typeof error.status !== 'number' ||
|
|
203
|
+
!Number.isSafeInteger(error.status) ||
|
|
204
|
+
error.status < 400 ||
|
|
205
|
+
error.status > 599
|
|
206
|
+
) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
error:
|
|
210
|
+
`fixtureBehavior.responseSamples[${index}].httpError.status ` +
|
|
211
|
+
'must be an integer between 400 and 599.',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (
|
|
215
|
+
typeof error.message !== 'string' ||
|
|
216
|
+
!error.message.trim() ||
|
|
217
|
+
error.message.length > MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH
|
|
218
|
+
) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
error:
|
|
222
|
+
`fixtureBehavior.responseSamples[${index}].httpError.message ` +
|
|
223
|
+
`must contain 1-${MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH} characters.`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
httpError = { status: error.status, message: error.message.trim() };
|
|
227
|
+
}
|
|
228
|
+
samples.push({
|
|
229
|
+
delayMs: sample.delayMs,
|
|
230
|
+
...(when ? { when } : {}),
|
|
231
|
+
...(httpError ? { httpError } : {}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
behavior: {
|
|
237
|
+
version: FIXTURE_BEHAVIOR_RESPONSE_VERSION,
|
|
238
|
+
responseSamples: samples,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
!Array.isArray(record.responseDelaySamplesMs) ||
|
|
244
|
+
record.responseDelaySamplesMs.length === 0 ||
|
|
245
|
+
record.responseDelaySamplesMs.length > MAX_FIXTURE_RESPONSE_DELAY_SAMPLES
|
|
246
|
+
) {
|
|
247
|
+
return {
|
|
248
|
+
ok: false,
|
|
249
|
+
error:
|
|
250
|
+
'fixtureBehavior.responseDelaySamplesMs must contain between 1 and ' +
|
|
251
|
+
`${MAX_FIXTURE_RESPONSE_DELAY_SAMPLES} values.`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const samples: number[] = [];
|
|
255
|
+
for (
|
|
256
|
+
let index = 0;
|
|
257
|
+
index < record.responseDelaySamplesMs.length;
|
|
258
|
+
index += 1
|
|
259
|
+
) {
|
|
260
|
+
const value = record.responseDelaySamplesMs[index];
|
|
261
|
+
if (
|
|
262
|
+
typeof value !== 'number' ||
|
|
263
|
+
!Number.isSafeInteger(value) ||
|
|
264
|
+
value < 0 ||
|
|
265
|
+
value > MAX_FIXTURE_RESPONSE_DELAY_MS
|
|
266
|
+
) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
error:
|
|
270
|
+
`fixtureBehavior.responseDelaySamplesMs[${index}] must be an integer ` +
|
|
271
|
+
`between 0 and ${MAX_FIXTURE_RESPONSE_DELAY_MS}.`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
samples.push(value);
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
ok: true,
|
|
278
|
+
behavior: {
|
|
279
|
+
version: FIXTURE_BEHAVIOR_VERSION,
|
|
280
|
+
responseDelaySamplesMs: samples,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function normalizeFixtureScopeValue(
|
|
286
|
+
value: unknown,
|
|
287
|
+
maxLength: number,
|
|
288
|
+
): { value?: string; error?: string } {
|
|
289
|
+
if (value === undefined) return {};
|
|
290
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
291
|
+
return { error: 'values must be non-empty strings.' };
|
|
292
|
+
}
|
|
293
|
+
const normalized = value.trim().toLowerCase();
|
|
294
|
+
if (normalized.length > maxLength) {
|
|
295
|
+
return { error: `values must be at most ${maxLength} characters.` };
|
|
296
|
+
}
|
|
297
|
+
return { value: normalized };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function requireFixtureBehavior(value: unknown): FixtureBehavior {
|
|
301
|
+
const result = validateFixtureBehavior(value);
|
|
302
|
+
if (result.ok === false) throw new Error(result.error);
|
|
303
|
+
if (!result.behavior) {
|
|
304
|
+
throw new Error('fixtureBehavior is required.');
|
|
305
|
+
}
|
|
306
|
+
return result.behavior;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function shouldRouteFixtureProvider(provider: string): boolean {
|
|
310
|
+
const normalized = provider.trim().toLowerCase();
|
|
311
|
+
return Boolean(normalized) && !FIXTURE_MODE_BYPASS_PROVIDERS.has(normalized);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function shouldRouteFixtureToolId(toolId: string): boolean {
|
|
315
|
+
const normalized = toolId.trim().toLowerCase();
|
|
316
|
+
if (!normalized) return false;
|
|
317
|
+
return ![...FIXTURE_MODE_BYPASS_PROVIDERS].some(
|
|
318
|
+
(provider) =>
|
|
319
|
+
normalized === provider || normalized.startsWith(`${provider}_`),
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function stableFixtureRequestHash(value: string): number {
|
|
324
|
+
let hash = 0x811c9dc5;
|
|
325
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
326
|
+
hash ^= value.charCodeAt(index);
|
|
327
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
328
|
+
}
|
|
329
|
+
return hash >>> 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function selectFixtureResponseDelay(input: {
|
|
333
|
+
behavior: FixtureBehavior;
|
|
334
|
+
stableRequestKey: string;
|
|
335
|
+
}): { delayMs: number; sampleIndex: number } {
|
|
336
|
+
const selected = selectFixtureResponseSample(input);
|
|
337
|
+
return { delayMs: selected.delayMs, sampleIndex: selected.sampleIndex };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function selectFixtureResponseSample(input: {
|
|
341
|
+
behavior: FixtureBehavior;
|
|
342
|
+
stableRequestKey: string;
|
|
343
|
+
}): FixtureResponseSample & { sampleIndex: number } {
|
|
344
|
+
const stableRequestKey = input.stableRequestKey.trim();
|
|
345
|
+
if (!stableRequestKey) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
'Configured fixture response timing requires a stable request key.',
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const samples =
|
|
351
|
+
input.behavior.version === FIXTURE_BEHAVIOR_VERSION
|
|
352
|
+
? input.behavior.responseDelaySamplesMs.map((delayMs, sampleIndex) => ({
|
|
353
|
+
delayMs,
|
|
354
|
+
sampleIndex,
|
|
355
|
+
}))
|
|
356
|
+
: selectEligibleResponseSamples(
|
|
357
|
+
input.behavior.responseSamples,
|
|
358
|
+
stableRequestKey,
|
|
359
|
+
);
|
|
360
|
+
const eligibleIndex =
|
|
361
|
+
stableFixtureRequestHash(
|
|
362
|
+
`fixture-behavior-v${input.behavior.version}:${stableRequestKey}`,
|
|
363
|
+
) % samples.length;
|
|
364
|
+
return { ...samples[eligibleIndex]! };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function selectEligibleResponseSamples(
|
|
368
|
+
samples: FixtureResponseSample[],
|
|
369
|
+
stableRequestKey: string,
|
|
370
|
+
): Array<FixtureResponseSample & { sampleIndex: number }> {
|
|
371
|
+
const [provider = '', operation = ''] = stableRequestKey
|
|
372
|
+
.toLowerCase()
|
|
373
|
+
.split(':', 3);
|
|
374
|
+
const indexed = samples.map((sample, sampleIndex) => ({
|
|
375
|
+
...sample,
|
|
376
|
+
sampleIndex,
|
|
377
|
+
}));
|
|
378
|
+
const scoped = indexed.filter(
|
|
379
|
+
(sample) =>
|
|
380
|
+
sample.when &&
|
|
381
|
+
(!sample.when.provider || sample.when.provider === provider) &&
|
|
382
|
+
(!sample.when.operation || sample.when.operation === operation),
|
|
383
|
+
);
|
|
384
|
+
if (scoped.length > 0) return scoped;
|
|
385
|
+
const fallback = indexed.filter((sample) => !sample.when);
|
|
386
|
+
if (fallback.length > 0) return fallback;
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Configured fixture behavior has no response samples for ${provider}:${operation}.`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export async function waitForFixtureResponseDelay(input: {
|
|
393
|
+
behavior: FixtureBehavior;
|
|
394
|
+
stableRequestKey: string;
|
|
395
|
+
signal?: AbortSignal | null;
|
|
396
|
+
onSelected?: (selected: { delayMs: number; sampleIndex: number }) => void;
|
|
397
|
+
}): Promise<{ delayMs: number; sampleIndex: number }> {
|
|
398
|
+
const selected = selectFixtureResponseDelay(input);
|
|
399
|
+
input.onSelected?.(selected);
|
|
400
|
+
if (selected.delayMs === 0) return selected;
|
|
401
|
+
await new Promise<void>((resolve, reject) => {
|
|
402
|
+
if (input.signal?.aborted) {
|
|
403
|
+
reject(input.signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const cleanup = () => {
|
|
407
|
+
input.signal?.removeEventListener('abort', onAbort);
|
|
408
|
+
};
|
|
409
|
+
const timer = setTimeout(() => {
|
|
410
|
+
cleanup();
|
|
411
|
+
resolve();
|
|
412
|
+
}, selected.delayMs);
|
|
413
|
+
const onAbort = () => {
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
cleanup();
|
|
416
|
+
reject(input.signal?.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
417
|
+
};
|
|
418
|
+
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
419
|
+
});
|
|
420
|
+
return selected;
|
|
421
|
+
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* at once" policy. See ADR 0007 + CONTEXT.md.
|
|
7
7
|
*
|
|
8
8
|
* Surface (small, by design):
|
|
9
|
-
* - acquireRowSlot / acquireToolSlot
|
|
9
|
+
* - acquireRowSlot / acquireToolSlot / acquireIntegrationRequestSlot
|
|
10
|
+
* → local execution leases
|
|
10
11
|
* - acquireProviderPermit → per-attempt provider admission
|
|
11
12
|
* - chargeBudget → throws on breach
|
|
12
13
|
* - forkInlineChild → inline recursion scope
|
|
@@ -111,6 +112,14 @@ export interface PlayExecutionGovernor {
|
|
|
111
112
|
opts?: { signal?: AbortSignal },
|
|
112
113
|
): Promise<WorkLease>;
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Admit one brief runtime-to-app HTTP fetch/body exchange. Long provider or
|
|
117
|
+
* fixture residence happens before this edge and must not occupy this slot.
|
|
118
|
+
*/
|
|
119
|
+
acquireIntegrationRequestSlot(opts?: {
|
|
120
|
+
signal?: AbortSignal;
|
|
121
|
+
}): Promise<WorkLease>;
|
|
122
|
+
|
|
114
123
|
/**
|
|
115
124
|
* Block until one physical provider HTTP attempt is admitted under the
|
|
116
125
|
* provider's shared rate/concurrency rules. Call this immediately before the
|
|
@@ -259,6 +268,9 @@ export function createPlayExecutionGovernor(
|
|
|
259
268
|
|
|
260
269
|
const rowSlots = new Semaphore(policy.concurrency.rowMax);
|
|
261
270
|
const toolSlots = new Semaphore(policy.concurrency.toolCalls);
|
|
271
|
+
const integrationRequestSlots = new Semaphore(
|
|
272
|
+
policy.concurrency.integrationRequests,
|
|
273
|
+
);
|
|
262
274
|
const adaptiveAdmission =
|
|
263
275
|
input.adaptiveAdmission ??
|
|
264
276
|
createInMemoryAdaptiveAdmission({
|
|
@@ -402,6 +414,9 @@ export function createPlayExecutionGovernor(
|
|
|
402
414
|
return slot;
|
|
403
415
|
},
|
|
404
416
|
|
|
417
|
+
acquireIntegrationRequestSlot: (opts) =>
|
|
418
|
+
integrationRequestSlots.acquire(opts?.signal),
|
|
419
|
+
|
|
405
420
|
async acquireProviderPermit(toolId, opts) {
|
|
406
421
|
// The rate ticket is an admission for a physical outbound attempt, not a
|
|
407
422
|
// reservation made while receipt ownership, headers, or local tool slots
|
|
@@ -575,6 +590,8 @@ function createInlineChildGovernor(
|
|
|
575
590
|
policy: root.policy,
|
|
576
591
|
acquireRowSlot: (opts) => root.acquireRowSlot(opts),
|
|
577
592
|
acquireToolSlot: (toolId, opts) => root.acquireToolSlot(toolId, opts),
|
|
593
|
+
acquireIntegrationRequestSlot: (opts) =>
|
|
594
|
+
root.acquireIntegrationRequestSlot(opts),
|
|
578
595
|
acquireProviderPermit: (toolId, opts) =>
|
|
579
596
|
root.acquireProviderPermit(toolId, opts),
|
|
580
597
|
suggestedParallelism: (toolId, fallback) =>
|
|
@@ -28,6 +28,11 @@ export interface ExecutionConcurrencyPolicy {
|
|
|
28
28
|
readonly rowMax: number;
|
|
29
29
|
/** Global backstop on concurrently in-flight tool calls across all providers. */
|
|
30
30
|
readonly toolCalls: number;
|
|
31
|
+
/**
|
|
32
|
+
* Brief runtime-to-app HTTP requests concurrently using sockets. Logical
|
|
33
|
+
* provider residence does not consume this admission.
|
|
34
|
+
*/
|
|
35
|
+
readonly integrationRequests: number;
|
|
31
36
|
/** Runner-local cap on concurrently active tool scheduling groups. */
|
|
32
37
|
readonly toolDispatchGroups: number;
|
|
33
38
|
/** Runner-local cap on active scheduling groups for one scalar/batch lane. */
|
|
@@ -89,9 +94,14 @@ export const SHARED_EXECUTION_POLICY: ResolvedExecutionPolicy = {
|
|
|
89
94
|
// matches the default: throughput grows through provider pacing/batching,
|
|
90
95
|
// while row payload size can only reduce admission through the byte budget.
|
|
91
96
|
rowMax: 1_000,
|
|
92
|
-
// Global
|
|
93
|
-
//
|
|
97
|
+
// Global logical-call backstop. A logical call can spend most of its life
|
|
98
|
+
// waiting on a provider/fixture response without holding a network socket.
|
|
94
99
|
toolCalls: 256,
|
|
100
|
+
// Physical runtime-to-app fetch/body admission. Keep short socket bursts
|
|
101
|
+
// below the sandbox/Vercel transport cliff while allowing many more logical
|
|
102
|
+
// calls to remain resident. This is deliberately independent of provider
|
|
103
|
+
// rate limits: it protects Deepline's own transport edge.
|
|
104
|
+
integrationRequests: 64,
|
|
95
105
|
// Scheduling groups contain bounded-dispatch workers and receipt buffers.
|
|
96
106
|
// Keep their promise topology finite even when pipelined replacement rows
|
|
97
107
|
// become ready one at a time behind a slower provider pacer.
|
|
@@ -15,6 +15,7 @@ import type { PlaySandboxRuntimeLimits } from './sandbox-runtime-limits';
|
|
|
15
15
|
import type { PlayRunInputPayload } from './play-input';
|
|
16
16
|
import type { PlayRunFailureDetails } from './run-failure';
|
|
17
17
|
import type { ToolExecutionErrorSchemaVersion } from '../tool-execution-error';
|
|
18
|
+
import type { FixtureBehavior } from './fixture-behavior';
|
|
18
19
|
|
|
19
20
|
export type PlayRunnerRateStateBackendConfig =
|
|
20
21
|
| {
|
|
@@ -122,6 +123,8 @@ export interface PlayRunnerContextConfig {
|
|
|
122
123
|
runtimeTestFaultHeader?: string | null;
|
|
123
124
|
vercelProtectionBypassToken?: string | null;
|
|
124
125
|
integrationMode?: 'live' | 'eval_stub' | 'fixture';
|
|
126
|
+
/** Validated internal fixture-only provider response simulation. */
|
|
127
|
+
fixtureBehavior?: FixtureBehavior | null;
|
|
125
128
|
/** Preview/dev test seam that applies provider pacing to fixture responses. */
|
|
126
129
|
enforceFixtureProviderPacing?: boolean;
|
|
127
130
|
/** Immutable tool-error payload schema copied from the run contract. */
|
package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts
CHANGED
|
@@ -64,6 +64,7 @@ export type OneShotDaytonaSandboxLifecycle = {
|
|
|
64
64
|
|
|
65
65
|
export type DaytonaSandboxAcquisitionUnavailableReason =
|
|
66
66
|
| 'daytona_total_cpu_limit_exceeded'
|
|
67
|
+
| 'daytona_acquisition_rate_limited'
|
|
67
68
|
| 'daytona_sandbox_start_timeout';
|
|
68
69
|
|
|
69
70
|
/**
|
|
@@ -86,14 +87,19 @@ export class DaytonaSandboxAcquisitionUnavailableError extends Error {
|
|
|
86
87
|
export function resolveDaytonaSandboxAcquisitionUnavailableReason(
|
|
87
88
|
errors: readonly string[],
|
|
88
89
|
): DaytonaSandboxAcquisitionUnavailableReason | null {
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
|
|
90
|
+
if (errors.length === 0) return null;
|
|
91
|
+
const isCpuLimit = (error: string) =>
|
|
92
|
+
/Total CPU limit exceeded\.\s*Maximum allowed:\s*\d+/i.test(error);
|
|
93
|
+
const isRateLimit = (error: string) =>
|
|
94
|
+
/ThrottlerException|Too Many Requests|(?:status code|HTTP)\s*429/i.test(
|
|
95
|
+
error,
|
|
96
|
+
);
|
|
97
|
+
if (errors.every(isCpuLimit)) {
|
|
95
98
|
return 'daytona_total_cpu_limit_exceeded';
|
|
96
99
|
}
|
|
100
|
+
if (errors.every((error) => isCpuLimit(error) || isRateLimit(error))) {
|
|
101
|
+
return 'daytona_acquisition_rate_limited';
|
|
102
|
+
}
|
|
97
103
|
return null;
|
|
98
104
|
}
|
|
99
105
|
|
|
@@ -47,7 +47,10 @@ function classifyDaytonaAcquisitionFailure(
|
|
|
47
47
|
if (!(error instanceof DaytonaSandboxAcquisitionUnavailableError)) {
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
|
-
if (
|
|
50
|
+
if (
|
|
51
|
+
error.reason === 'daytona_total_cpu_limit_exceeded' ||
|
|
52
|
+
error.reason === 'daytona_acquisition_rate_limited'
|
|
53
|
+
) {
|
|
51
54
|
return 'provider_capacity_exhausted';
|
|
52
55
|
}
|
|
53
56
|
if (error.reason === 'daytona_sandbox_start_timeout') {
|