deepline 0.1.283 → 0.1.285
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/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +269 -59
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +9 -4
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +33 -6
- package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/row-isolation.ts +25 -10
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +4 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -155,7 +155,7 @@ export const SDK_RELEASE = {
|
|
|
155
155
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
156
156
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
157
157
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
158
|
-
version: '0.1.
|
|
158
|
+
version: '0.1.285',
|
|
159
159
|
contracts: {
|
|
160
160
|
api: {
|
|
161
161
|
name: 'sdk-http-api',
|
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS,
|
|
108
108
|
classifyToolExecuteHttpFailure,
|
|
109
109
|
createToolExecuteHttpFailureAttemptTracker,
|
|
110
|
+
parseToolExecuteRetryAfterMs,
|
|
110
111
|
parseToolExecuteAuthScopeChangedError,
|
|
111
112
|
ToolExecuteAuthScopeChangedError,
|
|
112
113
|
} from './tool-execute-retry-policy';
|
|
@@ -303,9 +304,10 @@ const MAP_INCREMENTAL_PERSIST_CHUNK_ROWS = 100;
|
|
|
303
304
|
const MAP_INCREMENTAL_PERSIST_CHUNK_BYTES = 1 * 1024 * 1024;
|
|
304
305
|
const MAP_INCREMENTAL_PERSIST_INTERVAL_MS = 100;
|
|
305
306
|
const MAP_FRAME_FLUSH_INTERVAL_MS = 250;
|
|
306
|
-
//
|
|
307
|
-
// window is fixed from the first item and never resets on later arrivals.
|
|
307
|
+
// Tool scheduling lanes wait at most this long for same-lane row continuations.
|
|
308
|
+
// The window is fixed from the first item and never resets on later arrivals.
|
|
308
309
|
const TOOL_BATCH_COALESCE_WINDOW_MS = 5;
|
|
310
|
+
const TOOL_SCALAR_COALESCE_WINDOW_MS = 5;
|
|
309
311
|
const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
|
|
310
312
|
const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
311
313
|
const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
|
|
@@ -335,6 +337,31 @@ const MAX_INLINE_CHILD_INVOCATIONS_PER_ROW = 512;
|
|
|
335
337
|
// Bound the retained failure detail carried in parent aggregates so a fully
|
|
336
338
|
// failing large map cannot grow the progress event without limit.
|
|
337
339
|
const MAX_INLINE_CHILD_FAILURE_DETAIL = 25;
|
|
340
|
+
|
|
341
|
+
class ToolExecuteResponseBodyTransportError extends Error {
|
|
342
|
+
readonly cause: unknown;
|
|
343
|
+
|
|
344
|
+
constructor(cause: unknown) {
|
|
345
|
+
super(
|
|
346
|
+
cause instanceof Error
|
|
347
|
+
? cause.message
|
|
348
|
+
: 'Tool execute response body transport failed.',
|
|
349
|
+
);
|
|
350
|
+
this.name = 'ToolExecuteResponseBodyTransportError';
|
|
351
|
+
this.cause = cause;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
class ToolExecuteInvalidJsonError extends Error {
|
|
356
|
+
readonly cause: unknown;
|
|
357
|
+
|
|
358
|
+
constructor(cause: unknown) {
|
|
359
|
+
super('Tool execute response body was not valid JSON.');
|
|
360
|
+
this.name = 'ToolExecuteInvalidJsonError';
|
|
361
|
+
this.cause = cause;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
338
365
|
// A single batched runtime-step-receipt request (get/claim/complete/fail/seed)
|
|
339
366
|
// carries at most this many keys. At map scale a run can hold 5k-10k receipt
|
|
340
367
|
// keys; sending them all in one request made the server claim/complete them
|
|
@@ -1211,11 +1238,11 @@ export class PlayContextImpl {
|
|
|
1211
1238
|
private rowStates = new Map<number, RowState>();
|
|
1212
1239
|
private toolCallQueue: ToolCallRequest[] = [];
|
|
1213
1240
|
/**
|
|
1214
|
-
* Fixed, non-resetting coalescing deadlines for
|
|
1241
|
+
* Fixed, non-resetting coalescing deadlines for ready scheduling lanes. A
|
|
1215
1242
|
* deadline starts when the first request enters an empty batch bucket. New
|
|
1216
1243
|
* arrivals never push it back, so sustained traffic cannot starve dispatch.
|
|
1217
1244
|
*/
|
|
1218
|
-
private readonly
|
|
1245
|
+
private readonly toolDispatchQueuedAtByLane = new Map<string, number>();
|
|
1219
1246
|
private readonly toolDispatcherWakeWaiters = new Set<() => void>();
|
|
1220
1247
|
private toolDispatcherFailure: unknown | null = null;
|
|
1221
1248
|
private toolCallResolvers = new Map<
|
|
@@ -1396,14 +1423,14 @@ export class PlayContextImpl {
|
|
|
1396
1423
|
this.checkpoint = options.checkpoint ?? emptyCheckpoint();
|
|
1397
1424
|
this.durableMappedToolResultsBackedByReceipts = Boolean(
|
|
1398
1425
|
(options.claimRuntimeStepReceipt || options.claimRuntimeStepReceipts) &&
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1426
|
+
(options.getRuntimeStepReceipt || options.getRuntimeStepReceipts) &&
|
|
1427
|
+
(options.completeRuntimeStepReceipt ||
|
|
1428
|
+
options.completeRuntimeStepReceipts),
|
|
1402
1429
|
);
|
|
1403
1430
|
this.durableDirectToolResultsBackedByReceipts = Boolean(
|
|
1404
1431
|
options.claimRuntimeStepReceipt &&
|
|
1405
|
-
|
|
1406
|
-
|
|
1432
|
+
options.getRuntimeStepReceipt &&
|
|
1433
|
+
options.completeRuntimeStepReceipt,
|
|
1407
1434
|
);
|
|
1408
1435
|
if (this.durableDirectToolResultsBackedByReceipts) {
|
|
1409
1436
|
// A resumed durable runner may receive a legacy checkpoint containing
|
|
@@ -6078,15 +6105,41 @@ export class PlayContextImpl {
|
|
|
6078
6105
|
}
|
|
6079
6106
|
}
|
|
6080
6107
|
|
|
6081
|
-
private
|
|
6108
|
+
private toolDispatchLane(request: ToolCallRequest): {
|
|
6109
|
+
key: string;
|
|
6110
|
+
readyCount: number;
|
|
6111
|
+
maxGroupSize: number;
|
|
6112
|
+
coalesceWindowMs: number;
|
|
6113
|
+
} {
|
|
6082
6114
|
const strategy =
|
|
6083
6115
|
this.#options.getBatchOperationStrategy?.(request.toolId) ?? null;
|
|
6084
|
-
if (
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6116
|
+
if (strategy) {
|
|
6117
|
+
return {
|
|
6118
|
+
key: [
|
|
6119
|
+
'batch',
|
|
6120
|
+
request.toolId,
|
|
6121
|
+
request.executionAuthScopeDigest ?? '',
|
|
6122
|
+
String(strategy.toBucketKey(request.input)),
|
|
6123
|
+
].join('\u0000'),
|
|
6124
|
+
readyCount: strategy.maxBatchSize,
|
|
6125
|
+
maxGroupSize: this.governor.policy.concurrency.toolCalls,
|
|
6126
|
+
coalesceWindowMs:
|
|
6127
|
+
this.#options.toolBatchCoalesceWindowMs ??
|
|
6128
|
+
TOOL_BATCH_COALESCE_WINDOW_MS,
|
|
6129
|
+
};
|
|
6130
|
+
}
|
|
6131
|
+
return {
|
|
6132
|
+
key: [
|
|
6133
|
+
'scalar',
|
|
6134
|
+
request.toolId,
|
|
6135
|
+
request.executionAuthScopeDigest ?? '',
|
|
6136
|
+
].join('\u0000'),
|
|
6137
|
+
readyCount: this.governor.policy.pacing.workerToolBatchDefaultParallelism,
|
|
6138
|
+
maxGroupSize: this.governor.policy.concurrency.toolCalls,
|
|
6139
|
+
coalesceWindowMs:
|
|
6140
|
+
this.#options.toolScalarCoalesceWindowMs ??
|
|
6141
|
+
TOOL_SCALAR_COALESCE_WINDOW_MS,
|
|
6142
|
+
};
|
|
6090
6143
|
}
|
|
6091
6144
|
|
|
6092
6145
|
private wakeToolDispatcher(): void {
|
|
@@ -6109,10 +6162,10 @@ export class PlayContextImpl {
|
|
|
6109
6162
|
return;
|
|
6110
6163
|
}
|
|
6111
6164
|
this.toolCallQueue.push(request);
|
|
6112
|
-
const
|
|
6113
|
-
if (
|
|
6114
|
-
this.
|
|
6115
|
-
|
|
6165
|
+
const lane = this.toolDispatchLane(request);
|
|
6166
|
+
if (!this.toolDispatchQueuedAtByLane.has(lane.key)) {
|
|
6167
|
+
this.toolDispatchQueuedAtByLane.set(
|
|
6168
|
+
lane.key,
|
|
6116
6169
|
this.toolBatchDispatcherNowMs(),
|
|
6117
6170
|
);
|
|
6118
6171
|
}
|
|
@@ -6122,7 +6175,7 @@ export class PlayContextImpl {
|
|
|
6122
6175
|
private rejectQueuedToolCalls(error: unknown): void {
|
|
6123
6176
|
const queued = this.toolCallQueue;
|
|
6124
6177
|
this.toolCallQueue = [];
|
|
6125
|
-
this.
|
|
6178
|
+
this.toolDispatchQueuedAtByLane.clear();
|
|
6126
6179
|
for (const request of queued) {
|
|
6127
6180
|
const resolver = this.toolCallResolvers.get(request.callId);
|
|
6128
6181
|
if (!resolver) continue;
|
|
@@ -6131,39 +6184,45 @@ export class PlayContextImpl {
|
|
|
6131
6184
|
}
|
|
6132
6185
|
}
|
|
6133
6186
|
|
|
6134
|
-
private takeDispatchableToolCalls(
|
|
6187
|
+
private takeDispatchableToolCalls(
|
|
6188
|
+
nowMs: number,
|
|
6189
|
+
blockedLaneKeys: ReadonlySet<string>,
|
|
6190
|
+
): {
|
|
6135
6191
|
requests: ToolCallRequest[];
|
|
6192
|
+
laneKeys: Set<string>;
|
|
6136
6193
|
nextDeadlineMs: number | null;
|
|
6137
6194
|
} {
|
|
6138
6195
|
if (this.toolCallQueue.length === 0) {
|
|
6139
|
-
return { requests: [], nextDeadlineMs: null };
|
|
6196
|
+
return { requests: [], laneKeys: new Set(), nextDeadlineMs: null };
|
|
6140
6197
|
}
|
|
6141
6198
|
|
|
6142
|
-
const
|
|
6143
|
-
const
|
|
6199
|
+
const laneCounts = new Map<string, number>();
|
|
6200
|
+
const lanes = new Map<
|
|
6201
|
+
string,
|
|
6202
|
+
{
|
|
6203
|
+
readyCount: number;
|
|
6204
|
+
maxGroupSize: number;
|
|
6205
|
+
coalesceWindowMs: number;
|
|
6206
|
+
}
|
|
6207
|
+
>();
|
|
6144
6208
|
for (const request of this.toolCallQueue) {
|
|
6145
|
-
const
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
const strategy =
|
|
6149
|
-
this.#options.getBatchOperationStrategy?.(request.toolId) ?? null;
|
|
6150
|
-
if (strategy) batchMaxSizes.set(batchKey, strategy.maxBatchSize);
|
|
6209
|
+
const lane = this.toolDispatchLane(request);
|
|
6210
|
+
laneCounts.set(lane.key, (laneCounts.get(lane.key) ?? 0) + 1);
|
|
6211
|
+
lanes.set(lane.key, lane);
|
|
6151
6212
|
}
|
|
6152
6213
|
|
|
6153
|
-
const
|
|
6214
|
+
const readyLaneKeys = new Set<string>();
|
|
6154
6215
|
let nextDeadlineMs: number | null = null;
|
|
6155
|
-
for (const [
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6216
|
+
for (const [laneKey, count] of laneCounts) {
|
|
6217
|
+
if (blockedLaneKeys.has(laneKey)) continue;
|
|
6218
|
+
const queuedAt = this.toolDispatchQueuedAtByLane.get(laneKey) ?? nowMs;
|
|
6219
|
+
if (!this.toolDispatchQueuedAtByLane.has(laneKey)) {
|
|
6220
|
+
this.toolDispatchQueuedAtByLane.set(laneKey, queuedAt);
|
|
6159
6221
|
}
|
|
6160
|
-
const
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
const maxBatchSize = batchMaxSizes.get(batchKey) ?? 1;
|
|
6165
|
-
if (count >= maxBatchSize || deadline <= nowMs) {
|
|
6166
|
-
readyBatchKeys.add(batchKey);
|
|
6222
|
+
const lane = lanes.get(laneKey)!;
|
|
6223
|
+
const deadline = queuedAt + lane.coalesceWindowMs;
|
|
6224
|
+
if (count >= lane.readyCount || deadline <= nowMs) {
|
|
6225
|
+
readyLaneKeys.add(laneKey);
|
|
6167
6226
|
} else {
|
|
6168
6227
|
nextDeadlineMs =
|
|
6169
6228
|
nextDeadlineMs == null
|
|
@@ -6174,19 +6233,27 @@ export class PlayContextImpl {
|
|
|
6174
6233
|
|
|
6175
6234
|
const requests: ToolCallRequest[] = [];
|
|
6176
6235
|
const remaining: ToolCallRequest[] = [];
|
|
6236
|
+
const selectedByLane = new Map<string, number>();
|
|
6177
6237
|
for (const request of this.toolCallQueue) {
|
|
6178
|
-
const
|
|
6179
|
-
|
|
6238
|
+
const lane = this.toolDispatchLane(request);
|
|
6239
|
+
const selectedCount = selectedByLane.get(lane.key) ?? 0;
|
|
6240
|
+
if (
|
|
6241
|
+
readyLaneKeys.has(lane.key) &&
|
|
6242
|
+
requests.length < this.governor.policy.concurrency.toolCalls &&
|
|
6243
|
+
selectedCount < lane.maxGroupSize
|
|
6244
|
+
) {
|
|
6180
6245
|
requests.push(request);
|
|
6246
|
+
selectedByLane.set(lane.key, selectedCount + 1);
|
|
6181
6247
|
} else {
|
|
6182
6248
|
remaining.push(request);
|
|
6183
6249
|
}
|
|
6184
6250
|
}
|
|
6185
6251
|
this.toolCallQueue = remaining;
|
|
6186
|
-
|
|
6187
|
-
|
|
6252
|
+
const selectedLaneKeys = new Set(selectedByLane.keys());
|
|
6253
|
+
for (const laneKey of selectedLaneKeys) {
|
|
6254
|
+
this.toolDispatchQueuedAtByLane.delete(laneKey);
|
|
6188
6255
|
}
|
|
6189
|
-
return { requests, nextDeadlineMs };
|
|
6256
|
+
return { requests, laneKeys: selectedLaneKeys, nextDeadlineMs };
|
|
6190
6257
|
}
|
|
6191
6258
|
|
|
6192
6259
|
private waitForToolDispatcherWake(deadlineMs: number | null): Promise<void> {
|
|
@@ -6216,10 +6283,25 @@ export class PlayContextImpl {
|
|
|
6216
6283
|
}
|
|
6217
6284
|
|
|
6218
6285
|
private async drainQueuedWork<T>(promises: Promise<T>[]): Promise<void> {
|
|
6219
|
-
// One dispatcher owns ready work, fixed
|
|
6220
|
-
//
|
|
6221
|
-
// never waits for unrelated sibling calls from the previous column.
|
|
6286
|
+
// One dispatcher owns ready work, fixed lane coalescing deadlines, and the
|
|
6287
|
+
// bounded in-flight registry. A row resumes after its own receipt persists;
|
|
6288
|
+
// it never waits for unrelated sibling calls from the previous column.
|
|
6222
6289
|
const inFlightToolExecutions = new Set<Promise<void>>();
|
|
6290
|
+
const activeGroupsByLane = new Map<string, number>();
|
|
6291
|
+
const maxInFlightGroups = Math.max(
|
|
6292
|
+
1,
|
|
6293
|
+
Math.floor(
|
|
6294
|
+
this.#options.toolDispatcherMaxInFlightGroups ??
|
|
6295
|
+
this.governor.policy.concurrency.toolDispatchGroups,
|
|
6296
|
+
),
|
|
6297
|
+
);
|
|
6298
|
+
const maxInFlightGroupsPerLane = Math.max(
|
|
6299
|
+
1,
|
|
6300
|
+
Math.floor(
|
|
6301
|
+
this.#options.toolDispatcherMaxInFlightGroupsPerLane ??
|
|
6302
|
+
this.governor.policy.concurrency.toolDispatchGroupsPerLane,
|
|
6303
|
+
),
|
|
6304
|
+
);
|
|
6223
6305
|
let rowsSettled = false;
|
|
6224
6306
|
void Promise.allSettled(promises).then(() => {
|
|
6225
6307
|
rowsSettled = true;
|
|
@@ -6238,9 +6320,22 @@ export class PlayContextImpl {
|
|
|
6238
6320
|
throw this.toolDispatcherFailure;
|
|
6239
6321
|
}
|
|
6240
6322
|
|
|
6241
|
-
const
|
|
6242
|
-
|
|
6323
|
+
const blockedLaneKeys = new Set(
|
|
6324
|
+
[...activeGroupsByLane]
|
|
6325
|
+
.filter(([, count]) => count >= maxInFlightGroupsPerLane)
|
|
6326
|
+
.map(([laneKey]) => laneKey),
|
|
6243
6327
|
);
|
|
6328
|
+
const dispatchable =
|
|
6329
|
+
inFlightToolExecutions.size >= maxInFlightGroups
|
|
6330
|
+
? {
|
|
6331
|
+
requests: [] as ToolCallRequest[],
|
|
6332
|
+
laneKeys: new Set<string>(),
|
|
6333
|
+
nextDeadlineMs: null,
|
|
6334
|
+
}
|
|
6335
|
+
: this.takeDispatchableToolCalls(
|
|
6336
|
+
this.toolBatchDispatcherNowMs(),
|
|
6337
|
+
blockedLaneKeys,
|
|
6338
|
+
);
|
|
6244
6339
|
if (dispatchable.requests.length > 0) {
|
|
6245
6340
|
pass += 1;
|
|
6246
6341
|
this.log(` Batch pass ${pass}`);
|
|
@@ -6249,12 +6344,27 @@ export class PlayContextImpl {
|
|
|
6249
6344
|
`queued=${this.toolCallQueue.length} ` +
|
|
6250
6345
|
`in_flight_groups=${inFlightToolExecutions.size}`,
|
|
6251
6346
|
);
|
|
6347
|
+
for (const laneKey of dispatchable.laneKeys) {
|
|
6348
|
+
activeGroupsByLane.set(
|
|
6349
|
+
laneKey,
|
|
6350
|
+
(activeGroupsByLane.get(laneKey) ?? 0) + 1,
|
|
6351
|
+
);
|
|
6352
|
+
}
|
|
6252
6353
|
const tracked = this.executeBatchedToolCalls(dispatchable.requests)
|
|
6253
6354
|
.catch((error) => {
|
|
6254
6355
|
this.toolDispatcherFailure ??= error;
|
|
6255
6356
|
this.rejectQueuedToolCalls(error);
|
|
6256
6357
|
})
|
|
6257
6358
|
.finally(() => {
|
|
6359
|
+
for (const laneKey of dispatchable.laneKeys) {
|
|
6360
|
+
const remainingGroups =
|
|
6361
|
+
(activeGroupsByLane.get(laneKey) ?? 1) - 1;
|
|
6362
|
+
if (remainingGroups > 0) {
|
|
6363
|
+
activeGroupsByLane.set(laneKey, remainingGroups);
|
|
6364
|
+
} else {
|
|
6365
|
+
activeGroupsByLane.delete(laneKey);
|
|
6366
|
+
}
|
|
6367
|
+
}
|
|
6258
6368
|
inFlightToolExecutions.delete(tracked);
|
|
6259
6369
|
this.wakeToolDispatcher();
|
|
6260
6370
|
});
|
|
@@ -8547,7 +8657,9 @@ export class PlayContextImpl {
|
|
|
8547
8657
|
};
|
|
8548
8658
|
|
|
8549
8659
|
while (true) {
|
|
8550
|
-
let response: Response;
|
|
8660
|
+
let response: Response | null = null;
|
|
8661
|
+
let responseData: Record<string, unknown> | null = null;
|
|
8662
|
+
let responseErrorText: string | null = null;
|
|
8551
8663
|
let providerCallStartedAt: number | null = null;
|
|
8552
8664
|
let providerCallElapsedMs: number | null = null;
|
|
8553
8665
|
const durableCallReceiptKey =
|
|
@@ -8700,12 +8812,31 @@ export class PlayContextImpl {
|
|
|
8700
8812
|
: {}),
|
|
8701
8813
|
}),
|
|
8702
8814
|
});
|
|
8815
|
+
if (response.ok) {
|
|
8816
|
+
try {
|
|
8817
|
+
responseData = (await response.json()) as Record<
|
|
8818
|
+
string,
|
|
8819
|
+
unknown
|
|
8820
|
+
>;
|
|
8821
|
+
} catch (error) {
|
|
8822
|
+
if (error instanceof SyntaxError) {
|
|
8823
|
+
throw new ToolExecuteInvalidJsonError(error);
|
|
8824
|
+
}
|
|
8825
|
+
throw new ToolExecuteResponseBodyTransportError(error);
|
|
8826
|
+
}
|
|
8827
|
+
} else {
|
|
8828
|
+
try {
|
|
8829
|
+
responseErrorText = await response.text();
|
|
8830
|
+
} catch (error) {
|
|
8831
|
+
throw new ToolExecuteResponseBodyTransportError(error);
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8703
8834
|
} finally {
|
|
8704
8835
|
providerPermit.release();
|
|
8705
8836
|
}
|
|
8706
8837
|
if (runtimeReceiptReadTraceEnabled) {
|
|
8707
8838
|
this.log(
|
|
8708
|
-
`[perf] tool call id=${toolId} phase=
|
|
8839
|
+
`[perf] tool call id=${toolId} phase=integration_fetch_body elapsed_ms=${Date.now() - integrationFetchStartedAt} status=${response.status}`,
|
|
8709
8840
|
);
|
|
8710
8841
|
}
|
|
8711
8842
|
providerCallElapsedMs = Date.now() - providerCallStartedAt;
|
|
@@ -8723,13 +8854,80 @@ export class PlayContextImpl {
|
|
|
8723
8854
|
if (heartbeatFailure) {
|
|
8724
8855
|
throw heartbeatFailure;
|
|
8725
8856
|
}
|
|
8857
|
+
if (error instanceof ToolExecuteInvalidJsonError) {
|
|
8858
|
+
throw new ToolHttpError(
|
|
8859
|
+
`Tool ${toolId} returned an invalid JSON response body.`,
|
|
8860
|
+
null,
|
|
8861
|
+
response?.status ?? 0,
|
|
8862
|
+
'repairable',
|
|
8863
|
+
);
|
|
8864
|
+
}
|
|
8726
8865
|
const transportError = abortController?.signal.aborted
|
|
8727
8866
|
? abortController.signal.reason instanceof Error
|
|
8728
8867
|
? abortController.signal.reason
|
|
8729
8868
|
: new Error(
|
|
8730
8869
|
`Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
|
|
8731
8870
|
)
|
|
8732
|
-
: error
|
|
8871
|
+
: error instanceof ToolExecuteResponseBodyTransportError
|
|
8872
|
+
? error.cause
|
|
8873
|
+
: error;
|
|
8874
|
+
if (
|
|
8875
|
+
error instanceof ToolExecuteResponseBodyTransportError &&
|
|
8876
|
+
response?.status === 402
|
|
8877
|
+
) {
|
|
8878
|
+
const diagnostic = describeTransportError(transportError);
|
|
8879
|
+
throw new ToolHttpError(
|
|
8880
|
+
`Tool ${toolId} returned HTTP 402 but its response body could not be read; the run was halted because the payment or capacity denial could not be classified safely: ${diagnostic.message ?? 'unknown transport error'}`,
|
|
8881
|
+
{
|
|
8882
|
+
kind: 'billing_cap_exceeded',
|
|
8883
|
+
code: 'HTTP_402_BODY_UNREADABLE',
|
|
8884
|
+
error_category: 'billing',
|
|
8885
|
+
failure_origin: 'unknown',
|
|
8886
|
+
},
|
|
8887
|
+
402,
|
|
8888
|
+
);
|
|
8889
|
+
}
|
|
8890
|
+
const responseBodyReplaySafe =
|
|
8891
|
+
!(error instanceof ToolExecuteResponseBodyTransportError) ||
|
|
8892
|
+
retrySafeTransientHttp ||
|
|
8893
|
+
response?.status === 429;
|
|
8894
|
+
if (!responseBodyReplaySafe) {
|
|
8895
|
+
const diagnostic = describeTransportError(transportError);
|
|
8896
|
+
this.log(
|
|
8897
|
+
`[runtime.transport_failure] ${JSON.stringify({
|
|
8898
|
+
tool_id: toolId,
|
|
8899
|
+
gateway_origin: transportGatewayOriginForDiagnostic(url),
|
|
8900
|
+
attempt: 1,
|
|
8901
|
+
max_attempts: 1,
|
|
8902
|
+
elapsed_ms:
|
|
8903
|
+
providerCallStartedAt === null
|
|
8904
|
+
? 0
|
|
8905
|
+
: Date.now() - providerCallStartedAt,
|
|
8906
|
+
request_id: deeplineRequestId,
|
|
8907
|
+
aborted: abortController?.signal.aborted === true,
|
|
8908
|
+
response_headers_received: true,
|
|
8909
|
+
retry_safe: false,
|
|
8910
|
+
error: diagnostic,
|
|
8911
|
+
})}`,
|
|
8912
|
+
);
|
|
8913
|
+
throw new ToolHttpError(
|
|
8914
|
+
`Tool ${toolId} response body transport failed after response headers; the ambiguous call was not retried because this operation is not declared retry-safe: ${diagnostic.message ?? 'unknown transport error'}`,
|
|
8915
|
+
null,
|
|
8916
|
+
0,
|
|
8917
|
+
'repairable',
|
|
8918
|
+
);
|
|
8919
|
+
}
|
|
8920
|
+
if (
|
|
8921
|
+
error instanceof ToolExecuteResponseBodyTransportError &&
|
|
8922
|
+
response?.status === 429
|
|
8923
|
+
) {
|
|
8924
|
+
await this.reportToolBackpressure(
|
|
8925
|
+
toolId,
|
|
8926
|
+
parseToolExecuteRetryAfterMs(
|
|
8927
|
+
response.headers.get('retry-after'),
|
|
8928
|
+
),
|
|
8929
|
+
);
|
|
8930
|
+
}
|
|
8733
8931
|
await retryToolTransportFailure({
|
|
8734
8932
|
error: transportError,
|
|
8735
8933
|
elapsedMs:
|
|
@@ -8746,10 +8944,15 @@ export class PlayContextImpl {
|
|
|
8746
8944
|
}
|
|
8747
8945
|
}
|
|
8748
8946
|
|
|
8947
|
+
if (!response) {
|
|
8948
|
+
throw new Error(
|
|
8949
|
+
`Tool ${toolId} transport completed without an HTTP response.`,
|
|
8950
|
+
);
|
|
8951
|
+
}
|
|
8749
8952
|
span.setAttribute('plays.http_status_code', response.status);
|
|
8750
8953
|
|
|
8751
8954
|
if (!response.ok) {
|
|
8752
|
-
const text =
|
|
8955
|
+
const text = responseErrorText ?? '';
|
|
8753
8956
|
if (
|
|
8754
8957
|
isDeeplineDeveloperTunnelOrigin502({
|
|
8755
8958
|
url,
|
|
@@ -8864,8 +9067,15 @@ export class PlayContextImpl {
|
|
|
8864
9067
|
throw failure.error;
|
|
8865
9068
|
}
|
|
8866
9069
|
|
|
8867
|
-
|
|
8868
|
-
|
|
9070
|
+
if (!responseData) {
|
|
9071
|
+
throw new ToolHttpError(
|
|
9072
|
+
`Tool ${toolId} returned an empty successful response body.`,
|
|
9073
|
+
null,
|
|
9074
|
+
response.status,
|
|
9075
|
+
'repairable',
|
|
9076
|
+
);
|
|
9077
|
+
}
|
|
9078
|
+
const parsed = parseToolExecuteResponse(toolId, responseData);
|
|
8869
9079
|
setSpanAttributes(span, {
|
|
8870
9080
|
'plays.tool_result_kind':
|
|
8871
9081
|
parsed.result == null
|
|
@@ -690,11 +690,16 @@ export interface ContextOptions {
|
|
|
690
690
|
nowMs: () => number;
|
|
691
691
|
schedule: (delayMs: number, wake: () => void) => () => void;
|
|
692
692
|
};
|
|
693
|
-
// How long
|
|
694
|
-
// dispatching, measured from the first queued item.
|
|
695
|
-
// production; tests override it to a large value to make coalescing
|
|
696
|
-
// deterministic instead of racing wall-clock milliseconds on slow CI.
|
|
693
|
+
// How long provider-native batch calls wait for same-key row continuations
|
|
694
|
+
// before dispatching, measured from the first queued item.
|
|
697
695
|
toolBatchCoalesceWindowMs?: number;
|
|
696
|
+
// Scalar calls remain separate physical requests, but wait in one scheduling
|
|
697
|
+
// lane for this fixed window so pipelined replacement rows launch together.
|
|
698
|
+
toolScalarCoalesceWindowMs?: number;
|
|
699
|
+
/** Deterministic test override for the runner-local scheduling-group cap. */
|
|
700
|
+
toolDispatcherMaxInFlightGroups?: number;
|
|
701
|
+
/** Deterministic test override for the active-group cap per scheduling lane. */
|
|
702
|
+
toolDispatcherMaxInFlightGroupsPerLane?: number;
|
|
698
703
|
queryCustomerDb?: CustomerDbQueryHandler;
|
|
699
704
|
executeStructuredPlayDefinition?: (input: {
|
|
700
705
|
definition: PlayStructuredDefinition;
|
|
@@ -268,6 +268,7 @@ export function createPlayExecutionGovernor(
|
|
|
268
268
|
const budgetState = input.budgetState ?? new InMemoryBudgetStateBackend();
|
|
269
269
|
const providerByTool = new Map<string, string>();
|
|
270
270
|
const scopeByTool = new Map<string, [string, string]>();
|
|
271
|
+
const providerConcurrencySlotsByTool = new Map<string, Semaphore>();
|
|
271
272
|
|
|
272
273
|
// When the rate-state backend owns pacing authoritatively (the Absurd/Node
|
|
273
274
|
// app-runtime Postgres pacer runs the whole token bucket + AIMD in the row),
|
|
@@ -405,12 +406,38 @@ export function createPlayExecutionGovernor(
|
|
|
405
406
|
// the declared rolling window under concurrent runners.
|
|
406
407
|
const pacing = await resolveAdaptivePacing(toolId);
|
|
407
408
|
const rateScope = await resolveScope(toolId, pacing.provider);
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
409
|
+
const declaredMaxConcurrency = Math.min(
|
|
410
|
+
...pacing.rules.flatMap((rule) =>
|
|
411
|
+
rule.maxConcurrency != null ? [rule.maxConcurrency] : [],
|
|
412
|
+
),
|
|
413
|
+
);
|
|
414
|
+
const localConcurrencySlot = Number.isFinite(declaredMaxConcurrency)
|
|
415
|
+
? await (() => {
|
|
416
|
+
let slots = providerConcurrencySlotsByTool.get(toolId);
|
|
417
|
+
if (!slots) {
|
|
418
|
+
slots = new Semaphore(Math.max(1, declaredMaxConcurrency));
|
|
419
|
+
providerConcurrencySlotsByTool.set(toolId, slots);
|
|
420
|
+
}
|
|
421
|
+
return slots.acquire(opts?.signal);
|
|
422
|
+
})()
|
|
423
|
+
: null;
|
|
424
|
+
try {
|
|
425
|
+
const rateLease = await input.rateState.acquire({
|
|
426
|
+
bucketId: rateScope[0],
|
|
427
|
+
rateScopeToken: rateScope[1],
|
|
428
|
+
rules: pacing.rules,
|
|
429
|
+
signal: opts?.signal,
|
|
430
|
+
});
|
|
431
|
+
return {
|
|
432
|
+
release() {
|
|
433
|
+
rateLease.release();
|
|
434
|
+
localConcurrencySlot?.release();
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
} catch (error) {
|
|
438
|
+
localConcurrencySlot?.release();
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
414
441
|
},
|
|
415
442
|
|
|
416
443
|
async suggestedParallelism(toolId, fallback) {
|
|
@@ -28,6 +28,10 @@ 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
|
+
/** Runner-local cap on concurrently active tool scheduling groups. */
|
|
32
|
+
readonly toolDispatchGroups: number;
|
|
33
|
+
/** Runner-local cap on active scheduling groups for one scalar/batch lane. */
|
|
34
|
+
readonly toolDispatchGroupsPerLane: number;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/**
|
|
@@ -88,6 +92,14 @@ export const SHARED_EXECUTION_POLICY: ResolvedExecutionPolicy = {
|
|
|
88
92
|
// Global all-provider backstop. Per-provider pacing is the real limit; this
|
|
89
93
|
// just stops a single run from opening an absurd number of sockets at once.
|
|
90
94
|
toolCalls: 256,
|
|
95
|
+
// Scheduling groups contain bounded-dispatch workers and receipt buffers.
|
|
96
|
+
// Keep their promise topology finite even when pipelined replacement rows
|
|
97
|
+
// become ready one at a time behind a slower provider pacer.
|
|
98
|
+
toolDispatchGroups: 32,
|
|
99
|
+
// Preserve limited overlap so a fast row can enter its next column while a
|
|
100
|
+
// prior group's receipt persistence is still settling. Provider permits
|
|
101
|
+
// enforce declared maxConcurrency across these overlapping groups.
|
|
102
|
+
toolDispatchGroupsPerLane: 4,
|
|
91
103
|
},
|
|
92
104
|
budgets: {
|
|
93
105
|
// Runaway guards, not workload limits. A 5,000-row map calling several tools
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isHardBillingToolHttpError } from './tool-http-errors';
|
|
1
|
+
import { isHardBillingToolHttpError, ToolHttpError } from './tool-http-errors';
|
|
2
2
|
import { isRuntimePersistenceCircuitOpenError } from './persistence-latch';
|
|
3
3
|
import { isWorkspaceStorageNotReadyFailure } from './run-failure';
|
|
4
4
|
|
|
@@ -17,10 +17,7 @@ export function isAbortLikeError(error: unknown): boolean {
|
|
|
17
17
|
if (!error) return false;
|
|
18
18
|
if (error instanceof WorkflowAbortError) return true;
|
|
19
19
|
if (error instanceof Error) {
|
|
20
|
-
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
return /\b(cancell?ed|aborted|terminate[d]?)\b/i.test(error.message);
|
|
20
|
+
return error.name === 'WorkflowAbort' || error.name === 'AbortError';
|
|
24
21
|
}
|
|
25
22
|
return false;
|
|
26
23
|
}
|
|
@@ -36,6 +33,23 @@ export function isAbortLikeError(error: unknown): boolean {
|
|
|
36
33
|
*/
|
|
37
34
|
export function isRowIsolationExemptError(error: unknown): boolean {
|
|
38
35
|
if (isAbortLikeError(error)) return true;
|
|
36
|
+
// A remote tool can return arbitrary provider/database wording. Its message
|
|
37
|
+
// must never be mistaken for a failure of the runner's own receipt or sheet
|
|
38
|
+
// persistence plane. Hard Deepline billing denials are the sole typed tool
|
|
39
|
+
// failure that intentionally remains run-fatal.
|
|
40
|
+
if (error instanceof ToolHttpError) {
|
|
41
|
+
return isHardBillingToolHttpError(error);
|
|
42
|
+
}
|
|
43
|
+
const nestedErrors =
|
|
44
|
+
error && typeof error === 'object'
|
|
45
|
+
? (error as { errors?: unknown }).errors
|
|
46
|
+
: undefined;
|
|
47
|
+
if (
|
|
48
|
+
Array.isArray(nestedErrors) &&
|
|
49
|
+
nestedErrors.some(isRowIsolationExemptError)
|
|
50
|
+
) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
39
53
|
if (error instanceof Error && error.name === 'GovernorBudgetError') {
|
|
40
54
|
return true;
|
|
41
55
|
}
|
|
@@ -63,12 +77,13 @@ function isRuntimeReceiptPersistenceError(error: unknown): boolean {
|
|
|
63
77
|
|
|
64
78
|
function isRuntimeStoragePersistenceError(error: unknown): boolean {
|
|
65
79
|
if (!error || typeof error !== 'object') return false;
|
|
80
|
+
if (error instanceof ToolHttpError) return false;
|
|
66
81
|
const nestedErrors = (error as { errors?: unknown }).errors;
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return
|
|
82
|
+
if (Array.isArray(nestedErrors) && nestedErrors.length > 0) {
|
|
83
|
+
// AggregateError.message is caller-controlled summary text. Once typed
|
|
84
|
+
// children exist, classify those children and never fall through to a
|
|
85
|
+
// misleading container message.
|
|
86
|
+
return nestedErrors.some(isRuntimeStoragePersistenceError);
|
|
72
87
|
}
|
|
73
88
|
const message =
|
|
74
89
|
error instanceof Error ? error.message : String((error as object) ?? '');
|
|
@@ -107,6 +107,10 @@ export type PlaySchedulerSubmitInput = {
|
|
|
107
107
|
userEmail: string;
|
|
108
108
|
userId?: string | null;
|
|
109
109
|
source?: 'published' | 'ad_hoc' | 'draft';
|
|
110
|
+
/** Invocation origin, distinct from the artifact source above. */
|
|
111
|
+
triggerSource?: 'api' | 'webhook' | 'cron' | 'sql_listener';
|
|
112
|
+
/** Null lets a durable trigger wait for capacity; a number bounds queue delay. */
|
|
113
|
+
queueMaxDelaySeconds?: number | null;
|
|
110
114
|
executionProfile?: string | null;
|
|
111
115
|
/** runner backend to use for executing attempts */
|
|
112
116
|
runtimeBackend: string;
|
package/dist/cli/index.js
CHANGED
|
@@ -718,7 +718,7 @@ var SDK_RELEASE = {
|
|
|
718
718
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
719
719
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
720
720
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
721
|
-
version: "0.1.
|
|
721
|
+
version: "0.1.285",
|
|
722
722
|
contracts: {
|
|
723
723
|
api: {
|
|
724
724
|
name: "sdk-http-api",
|
package/dist/cli/index.mjs
CHANGED
|
@@ -703,7 +703,7 @@ var SDK_RELEASE = {
|
|
|
703
703
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
704
704
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
705
705
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
706
|
-
version: "0.1.
|
|
706
|
+
version: "0.1.285",
|
|
707
707
|
contracts: {
|
|
708
708
|
api: {
|
|
709
709
|
name: "sdk-http-api",
|
package/dist/index.js
CHANGED
|
@@ -438,7 +438,7 @@ var SDK_RELEASE = {
|
|
|
438
438
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
439
439
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
440
440
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
441
|
-
version: "0.1.
|
|
441
|
+
version: "0.1.285",
|
|
442
442
|
contracts: {
|
|
443
443
|
api: {
|
|
444
444
|
name: "sdk-http-api",
|
package/dist/index.mjs
CHANGED
|
@@ -367,7 +367,7 @@ var SDK_RELEASE = {
|
|
|
367
367
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
368
368
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
369
369
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
370
|
-
version: "0.1.
|
|
370
|
+
version: "0.1.285",
|
|
371
371
|
contracts: {
|
|
372
372
|
api: {
|
|
373
373
|
name: "sdk-http-api",
|