deepline 0.1.201 → 0.1.203
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/apps/play-runner-workers/src/entry.ts +30 -9
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +25 -0
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +139 -124
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +17 -2
- package/dist/bundling-sources/shared_libs/play-runtime/bounded-dispatch.ts +52 -0
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +154 -23
- package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +10 -0
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +121 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/adaptive-admission.ts +6 -1
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-budget-state-backend.ts +22 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +86 -19
- package/dist/bundling-sources/shared_libs/play-runtime/governor/block-reserving-budget-state-backend.ts +141 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/budget-state-backend.ts +43 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +129 -41
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +40 -2
- package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +92 -33
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +13 -5
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +122 -120
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +10 -8
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +140 -32
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-paths.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +225 -29
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-scheduler-topology.ts +26 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +21 -0
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +32 -10
- package/dist/cli/index.js +335 -77
- package/dist/cli/index.mjs +341 -83
- package/dist/index.js +3 -2
- package/dist/index.mjs +3 -2
- package/package.json +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/adaptive-tool-dispatcher.ts +0 -355
package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts
CHANGED
|
@@ -23,7 +23,20 @@ const MAX_ACQUIRE_SLEEP_MS = 5_000;
|
|
|
23
23
|
const STORE_FAILURE_RETRY_DELAYS_MS = [100, 250] as const;
|
|
24
24
|
|
|
25
25
|
interface LeasedBlock {
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Concurrency reservations, one per locally-cached permit. Only rules with a
|
|
28
|
+
* `maxConcurrency` mint lease tokens; each id maps to a durable slot that must
|
|
29
|
+
* be released (or TTL-expired) so the shared concurrency count drains.
|
|
30
|
+
*/
|
|
31
|
+
leaseIds: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Pure-rate (RPS-only) permits with no concurrency slot behind them. The
|
|
34
|
+
* server decremented the provider window by this count already, so drawing
|
|
35
|
+
* one locally is a no-op that needs no release token. Tracked separately from
|
|
36
|
+
* `leaseIds` so a pure-RPS block (the common `defaultPacingForTool` shape)
|
|
37
|
+
* actually amortizes its whole grant across acquires instead of burning it.
|
|
38
|
+
*/
|
|
39
|
+
windowPermits: number;
|
|
27
40
|
expiresAt: number;
|
|
28
41
|
rulesKey: string;
|
|
29
42
|
rules: PacingRule[];
|
|
@@ -92,10 +105,43 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
92
105
|
});
|
|
93
106
|
consecutiveFailures = 0;
|
|
94
107
|
if (response.granted > 0) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
const hasConcurrency = ordered.some(
|
|
109
|
+
(rule) => rule.maxConcurrency != null,
|
|
110
|
+
);
|
|
111
|
+
const leaseIds = Array.isArray(response.leaseIds)
|
|
112
|
+
? response.leaseIds.filter(
|
|
113
|
+
(leaseId): leaseId is string =>
|
|
114
|
+
typeof leaseId === 'string' && leaseId.trim().length > 0,
|
|
115
|
+
)
|
|
116
|
+
: [];
|
|
117
|
+
if (hasConcurrency && leaseIds.length !== response.granted) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Rate-state store granted ${response.granted} concurrency permits for ${bucketId} with ${leaseIds.length} lease tokens.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (!hasConcurrency && leaseIds.length > 0) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Rate-state store returned ${leaseIds.length} lease tokens for pure-rate bucket ${bucketId}.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
// Serve this acquire from the grant, then cache the rest so the whole
|
|
128
|
+
// block amortizes into a single round trip. Concurrency rules draw a
|
|
129
|
+
// lease token (needs release); pure-rate rules draw a window permit
|
|
130
|
+
// (already decremented server-side, no release).
|
|
131
|
+
const currentLeaseId = hasConcurrency ? leaseIds[0]! : null;
|
|
132
|
+
const remainingLeaseIds = hasConcurrency ? leaseIds.slice(1) : [];
|
|
133
|
+
const remainingWindowPermits = hasConcurrency
|
|
134
|
+
? 0
|
|
135
|
+
: response.granted - 1;
|
|
136
|
+
if (remainingLeaseIds.length > 0 || remainingWindowPermits > 0)
|
|
137
|
+
this.mergeBlock(
|
|
138
|
+
bucketId,
|
|
139
|
+
remainingLeaseIds,
|
|
140
|
+
remainingWindowPermits,
|
|
141
|
+
rulesKey,
|
|
142
|
+
ordered,
|
|
143
|
+
);
|
|
144
|
+
return this.permitFor(bucketId, ordered, currentLeaseId);
|
|
99
145
|
}
|
|
100
146
|
await this.sleep(
|
|
101
147
|
Math.max(1, Math.min(response.waitMs, MAX_ACQUIRE_SLEEP_MS)),
|
|
@@ -137,7 +183,11 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
137
183
|
});
|
|
138
184
|
}
|
|
139
185
|
|
|
140
|
-
private permitFor(
|
|
186
|
+
private permitFor(
|
|
187
|
+
bucketId: string,
|
|
188
|
+
rules: PacingRule[],
|
|
189
|
+
leaseId: string | null,
|
|
190
|
+
): PacingPermit {
|
|
141
191
|
if (!rules.some((rule) => rule.maxConcurrency != null)) {
|
|
142
192
|
return noopPacingPermit();
|
|
143
193
|
}
|
|
@@ -146,14 +196,15 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
146
196
|
release: () => {
|
|
147
197
|
if (released) return;
|
|
148
198
|
released = true;
|
|
149
|
-
this.releaseReserved(bucketId, rules,
|
|
199
|
+
this.releaseReserved(bucketId, rules, leaseId ? [leaseId] : []);
|
|
150
200
|
},
|
|
151
201
|
};
|
|
152
202
|
}
|
|
153
203
|
|
|
154
204
|
private mergeBlock(
|
|
155
205
|
bucketId: string,
|
|
156
|
-
|
|
206
|
+
leaseIds: string[],
|
|
207
|
+
windowPermits: number,
|
|
157
208
|
rulesKey: string,
|
|
158
209
|
rules: PacingRule[],
|
|
159
210
|
): void {
|
|
@@ -164,14 +215,16 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
164
215
|
existing.rulesKey === rulesKey &&
|
|
165
216
|
existing.expiresAt > this.now()
|
|
166
217
|
) {
|
|
167
|
-
existing.
|
|
218
|
+
existing.leaseIds.push(...leaseIds);
|
|
219
|
+
existing.windowPermits += windowPermits;
|
|
168
220
|
existing.expiresAt = Math.min(existing.expiresAt, freshExpiresAt);
|
|
169
221
|
return;
|
|
170
222
|
}
|
|
171
223
|
if (existing)
|
|
172
|
-
this.releaseReserved(bucketId, existing.rules, existing.
|
|
224
|
+
this.releaseReserved(bucketId, existing.rules, existing.leaseIds);
|
|
173
225
|
this.blocks.set(bucketId, {
|
|
174
|
-
|
|
226
|
+
leaseIds,
|
|
227
|
+
windowPermits,
|
|
175
228
|
expiresAt: freshExpiresAt,
|
|
176
229
|
rulesKey,
|
|
177
230
|
rules,
|
|
@@ -186,32 +239,46 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
186
239
|
if (!block) return null;
|
|
187
240
|
if (block.rulesKey !== rulesKey || block.expiresAt <= this.now()) {
|
|
188
241
|
this.blocks.delete(bucketId);
|
|
189
|
-
|
|
242
|
+
// Unused window permits are forfeited on TTL expiry (the server already
|
|
243
|
+
// decremented the provider window for them, and the LEASE_BLOCK_TTL_MS
|
|
244
|
+
// window is short so at most one block's worth is stranded per bucket).
|
|
245
|
+
// Concurrency leases must be released so the shared slot count drains.
|
|
246
|
+
this.releaseReserved(bucketId, block.rules, block.leaseIds);
|
|
190
247
|
return null;
|
|
191
248
|
}
|
|
192
|
-
block.
|
|
193
|
-
|
|
194
|
-
|
|
249
|
+
const hasConcurrency = block.rules.some(
|
|
250
|
+
(rule) => rule.maxConcurrency != null,
|
|
251
|
+
);
|
|
252
|
+
// Concurrency blocks draw a lease token (release semantics); pure-rate
|
|
253
|
+
// blocks draw an untracked window permit that the server already accounted.
|
|
254
|
+
const leaseId = hasConcurrency ? (block.leaseIds.pop() ?? null) : null;
|
|
255
|
+
if (!hasConcurrency && block.windowPermits > 0) block.windowPermits -= 1;
|
|
256
|
+
if (block.leaseIds.length === 0 && block.windowPermits === 0)
|
|
257
|
+
this.blocks.delete(bucketId);
|
|
258
|
+
return this.permitFor(bucketId, block.rules, leaseId);
|
|
195
259
|
}
|
|
196
260
|
|
|
197
261
|
private drainBlock(bucketId: string): void {
|
|
198
262
|
const block = this.blocks.get(bucketId);
|
|
199
263
|
if (!block) return;
|
|
200
264
|
this.blocks.delete(bucketId);
|
|
201
|
-
this.releaseReserved(bucketId, block.rules, block.
|
|
265
|
+
this.releaseReserved(bucketId, block.rules, block.leaseIds);
|
|
202
266
|
}
|
|
203
267
|
|
|
204
268
|
private releaseReserved(
|
|
205
269
|
bucketId: string,
|
|
206
270
|
rules: readonly PacingRule[],
|
|
207
|
-
|
|
271
|
+
leaseIds: readonly string[],
|
|
208
272
|
): void {
|
|
209
|
-
if (
|
|
273
|
+
if (
|
|
274
|
+
leaseIds.length === 0 ||
|
|
275
|
+
!rules.some((rule) => rule.maxConcurrency != null)
|
|
276
|
+
)
|
|
210
277
|
return;
|
|
211
278
|
void releaseRateStateViaAppRuntime(this.context, {
|
|
212
279
|
bucketId,
|
|
213
280
|
rules: [...rules],
|
|
214
|
-
|
|
281
|
+
leaseIds: [...leaseIds],
|
|
215
282
|
schedulerSchema: this.schedulerSchema,
|
|
216
283
|
}).catch((error) => {
|
|
217
284
|
const normalized = normalizeError(error);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PlayRunnerBudgetCharge,
|
|
3
|
+
PlayRunnerBudgetChargeResult,
|
|
4
|
+
} from '../protocol';
|
|
5
|
+
import type { BudgetStateBackend } from './budget-state-backend';
|
|
6
|
+
import { BudgetStateLimitError } from './budget-state-backend';
|
|
7
|
+
|
|
8
|
+
const RESERVATION_BLOCKS: Record<string, number> = {
|
|
9
|
+
toolCall: 256,
|
|
10
|
+
retry: 16,
|
|
11
|
+
waterfallStep: 256,
|
|
12
|
+
playCall: 16,
|
|
13
|
+
descendant: 16,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type BudgetStateChargePort = (input: {
|
|
17
|
+
rootRunId: string;
|
|
18
|
+
reservationId: string;
|
|
19
|
+
charges: readonly PlayRunnerBudgetCharge[];
|
|
20
|
+
}) => Promise<PlayRunnerBudgetChargeResult>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reserves durable budget in coarse blocks while charging each local operation
|
|
24
|
+
* exactly once. The durable port remains the authority across sandboxes and
|
|
25
|
+
* isolates; this cache only removes one RPC from the hot path per operation.
|
|
26
|
+
*/
|
|
27
|
+
export class BlockReservingBudgetStateBackend implements BudgetStateBackend {
|
|
28
|
+
private readonly remaining = new Map<string, number>();
|
|
29
|
+
private chargeTail: Promise<void> = Promise.resolve();
|
|
30
|
+
|
|
31
|
+
constructor(private readonly chargePort: BudgetStateChargePort) {}
|
|
32
|
+
|
|
33
|
+
charge(input: {
|
|
34
|
+
rootRunId: string;
|
|
35
|
+
charges: readonly PlayRunnerBudgetCharge[];
|
|
36
|
+
}): Promise<PlayRunnerBudgetChargeResult> {
|
|
37
|
+
const result = this.chargeTail.then(() => this.chargeSerial(input));
|
|
38
|
+
this.chargeTail = result.then(
|
|
39
|
+
() => undefined,
|
|
40
|
+
() => undefined,
|
|
41
|
+
);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async chargeSerial(input: {
|
|
46
|
+
rootRunId: string;
|
|
47
|
+
charges: readonly PlayRunnerBudgetCharge[];
|
|
48
|
+
}): Promise<PlayRunnerBudgetChargeResult> {
|
|
49
|
+
const reservations: PlayRunnerBudgetCharge[] = [];
|
|
50
|
+
const exactReservations: PlayRunnerBudgetCharge[] = [];
|
|
51
|
+
const previousRemaining = new Map<string, number | undefined>();
|
|
52
|
+
for (const charge of input.charges) {
|
|
53
|
+
const remaining = this.remaining.get(charge.key) ?? 0;
|
|
54
|
+
previousRemaining.set(charge.key, this.remaining.get(charge.key));
|
|
55
|
+
if (remaining >= charge.amount) {
|
|
56
|
+
this.remaining.set(charge.key, remaining - charge.amount);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const shortfall = charge.amount - remaining;
|
|
60
|
+
const block = Math.min(
|
|
61
|
+
charge.limit,
|
|
62
|
+
Math.max(shortfall, RESERVATION_BLOCKS[charge.key] ?? 1),
|
|
63
|
+
);
|
|
64
|
+
reservations.push({ ...charge, amount: block });
|
|
65
|
+
exactReservations.push({ ...charge, amount: shortfall });
|
|
66
|
+
this.remaining.set(charge.key, block - shortfall);
|
|
67
|
+
}
|
|
68
|
+
if (reservations.length === 0) return { counters: {} };
|
|
69
|
+
|
|
70
|
+
const restorePreviousRemaining = () => {
|
|
71
|
+
for (const [key, remaining] of previousRemaining) {
|
|
72
|
+
if (remaining === undefined) this.remaining.delete(key);
|
|
73
|
+
else this.remaining.set(key, remaining);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const request = async (
|
|
77
|
+
charges: readonly PlayRunnerBudgetCharge[],
|
|
78
|
+
reservationId: string,
|
|
79
|
+
) =>
|
|
80
|
+
await this.chargePort({
|
|
81
|
+
rootRunId: input.rootRunId,
|
|
82
|
+
reservationId,
|
|
83
|
+
charges,
|
|
84
|
+
});
|
|
85
|
+
const requestWithIdempotentRetry = async (
|
|
86
|
+
charges: readonly PlayRunnerBudgetCharge[],
|
|
87
|
+
reservationId: string,
|
|
88
|
+
) => {
|
|
89
|
+
try {
|
|
90
|
+
return await request(charges, reservationId);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const normalized = normalizeBudgetError(error);
|
|
93
|
+
if (normalized instanceof BudgetStateLimitError) throw normalized;
|
|
94
|
+
return await request(charges, reservationId).catch((retryError) => {
|
|
95
|
+
throw normalizeBudgetError(retryError);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
return await requestWithIdempotentRetry(
|
|
101
|
+
reservations,
|
|
102
|
+
crypto.randomUUID(),
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const normalized = normalizeBudgetError(error);
|
|
106
|
+
if (
|
|
107
|
+
normalized instanceof BudgetStateLimitError &&
|
|
108
|
+
reservations.some(
|
|
109
|
+
(reserved, index) =>
|
|
110
|
+
reserved.amount >
|
|
111
|
+
(exactReservations[index]?.amount ?? reserved.amount),
|
|
112
|
+
)
|
|
113
|
+
) {
|
|
114
|
+
for (const reservation of exactReservations) {
|
|
115
|
+
this.remaining.set(reservation.key, 0);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return await requestWithIdempotentRetry(
|
|
119
|
+
exactReservations,
|
|
120
|
+
crypto.randomUUID(),
|
|
121
|
+
);
|
|
122
|
+
} catch (exactError) {
|
|
123
|
+
restorePreviousRemaining();
|
|
124
|
+
throw normalizeBudgetError(exactError);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
restorePreviousRemaining();
|
|
128
|
+
throw normalized;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeBudgetError(error: unknown): Error {
|
|
134
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
135
|
+
const match = /Play execution (.+) budget exceeded \((\d+)\/(\d+)\)/i.exec(
|
|
136
|
+
normalized.message,
|
|
137
|
+
);
|
|
138
|
+
return match
|
|
139
|
+
? new BudgetStateLimitError(match[1]!, Number(match[2]), Number(match[3]))
|
|
140
|
+
: normalized;
|
|
141
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PlayRunnerBudgetCharge,
|
|
3
|
+
PlayRunnerBudgetChargeResult,
|
|
4
|
+
} from '../protocol';
|
|
5
|
+
|
|
6
|
+
export interface BudgetStateBackend {
|
|
7
|
+
charge(input: {
|
|
8
|
+
rootRunId: string;
|
|
9
|
+
charges: readonly PlayRunnerBudgetCharge[];
|
|
10
|
+
}): Promise<PlayRunnerBudgetChargeResult>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class BudgetStateLimitError extends Error {
|
|
14
|
+
constructor(
|
|
15
|
+
readonly key: string,
|
|
16
|
+
readonly observed: number,
|
|
17
|
+
readonly limit: number,
|
|
18
|
+
) {
|
|
19
|
+
super(`Play execution ${key} budget exceeded (${observed}/${limit}).`);
|
|
20
|
+
this.name = 'BudgetStateLimitError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class InMemoryBudgetStateBackend implements BudgetStateBackend {
|
|
25
|
+
private readonly counters = new Map<string, Record<string, number>>();
|
|
26
|
+
|
|
27
|
+
async charge(input: {
|
|
28
|
+
rootRunId: string;
|
|
29
|
+
charges: readonly PlayRunnerBudgetCharge[];
|
|
30
|
+
}): Promise<PlayRunnerBudgetChargeResult> {
|
|
31
|
+
const current = this.counters.get(input.rootRunId) ?? {};
|
|
32
|
+
const next = { ...current };
|
|
33
|
+
for (const charge of input.charges) {
|
|
34
|
+
const observed = (next[charge.key] ?? 0) + charge.amount;
|
|
35
|
+
if (observed > charge.limit) {
|
|
36
|
+
throw new BudgetStateLimitError(charge.key, observed, charge.limit);
|
|
37
|
+
}
|
|
38
|
+
next[charge.key] = observed;
|
|
39
|
+
}
|
|
40
|
+
this.counters.set(input.rootRunId, next);
|
|
41
|
+
return { counters: { ...next } };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -24,6 +24,11 @@ import {
|
|
|
24
24
|
createInMemoryAdaptiveAdmission,
|
|
25
25
|
type RuntimeAdaptiveAdmission,
|
|
26
26
|
} from './adaptive-admission';
|
|
27
|
+
import {
|
|
28
|
+
BudgetStateLimitError,
|
|
29
|
+
InMemoryBudgetStateBackend,
|
|
30
|
+
type BudgetStateBackend,
|
|
31
|
+
} from './budget-state-backend';
|
|
27
32
|
|
|
28
33
|
export interface WorkLease {
|
|
29
34
|
/** Free the slot / pacing permit. Idempotent. MUST be called in a finally. */
|
|
@@ -121,7 +126,7 @@ export interface PlayExecutionGovernor {
|
|
|
121
126
|
suggestedParallelism(toolId: string, fallback: number): Promise<number>;
|
|
122
127
|
|
|
123
128
|
/** Increment a monotonic budget counter; throws GovernorBudgetError on breach. */
|
|
124
|
-
chargeBudget(kind: BudgetKind, amount?: number): void
|
|
129
|
+
chargeBudget(kind: BudgetKind, amount?: number): Promise<void>;
|
|
125
130
|
|
|
126
131
|
/**
|
|
127
132
|
* Reserve depth + per-parent + descendant budget for a child play and return
|
|
@@ -140,7 +145,7 @@ export interface PlayExecutionGovernor {
|
|
|
140
145
|
forkChild(input: {
|
|
141
146
|
childPlayName: string;
|
|
142
147
|
childRunId: string;
|
|
143
|
-
}): GovernanceSnapshot
|
|
148
|
+
}): Promise<GovernanceSnapshot>;
|
|
144
149
|
|
|
145
150
|
/** Effective row concurrency: explicit request clamped to [1, rowMax], else default. */
|
|
146
151
|
resolveRowConcurrency(requested?: number): number;
|
|
@@ -156,6 +161,11 @@ export interface PlayExecutionGovernor {
|
|
|
156
161
|
provider: string;
|
|
157
162
|
latencyMs?: number | null;
|
|
158
163
|
}): void;
|
|
164
|
+
/** Feed success back using the same tool-to-provider resolution as acquire. */
|
|
165
|
+
observeToolSuccess(input: {
|
|
166
|
+
toolId: string;
|
|
167
|
+
latencyMs?: number | null;
|
|
168
|
+
}): void;
|
|
159
169
|
|
|
160
170
|
snapshot(): GovernanceSnapshot;
|
|
161
171
|
}
|
|
@@ -164,6 +174,7 @@ interface GovernorInput {
|
|
|
164
174
|
adapter: AdapterId;
|
|
165
175
|
scope: { orgId: string; rootRunId: string };
|
|
166
176
|
rateState: RateStateBackend;
|
|
177
|
+
budgetState?: BudgetStateBackend;
|
|
167
178
|
resolvePacing: PacingResolver;
|
|
168
179
|
adaptiveAdmission?: RuntimeAdaptiveAdmission;
|
|
169
180
|
resume?: GovernanceSnapshot;
|
|
@@ -258,6 +269,8 @@ export function createPlayExecutionGovernor(
|
|
|
258
269
|
initialRequestsPerSecond: policy.pacing.defaultProviderRequestsPerSecond,
|
|
259
270
|
maxRequestsPerSecond: policy.pacing.suggestedMaxParallelism,
|
|
260
271
|
});
|
|
272
|
+
const budgetState = input.budgetState ?? new InMemoryBudgetStateBackend();
|
|
273
|
+
const providerByTool = new Map<string, string>();
|
|
261
274
|
|
|
262
275
|
const bucketId = (provider: string) => `${input.scope.orgId}:${provider}`;
|
|
263
276
|
|
|
@@ -268,63 +281,85 @@ export function createPlayExecutionGovernor(
|
|
|
268
281
|
const resolvedPacing = await input.resolvePacing(toolId);
|
|
269
282
|
const declaredPacing =
|
|
270
283
|
resolvedPacing && resolvedPacing.rules.length > 0 ? resolvedPacing : null;
|
|
271
|
-
|
|
284
|
+
const pacing = adaptiveAdmission.resolvePacing({
|
|
272
285
|
orgId: input.scope.orgId,
|
|
273
286
|
toolId,
|
|
274
287
|
declaredPacing,
|
|
275
288
|
fallbackPacing: defaultPacingForTool(toolId, policy),
|
|
276
289
|
});
|
|
290
|
+
providerByTool.set(toolId, pacing.provider);
|
|
291
|
+
return pacing;
|
|
277
292
|
}
|
|
278
293
|
|
|
279
|
-
|
|
294
|
+
const budgetLimit = (kind: BudgetKind): number => {
|
|
295
|
+
switch (kind) {
|
|
296
|
+
case 'playCall':
|
|
297
|
+
return policy.budgets.maxPlayCallCount;
|
|
298
|
+
case 'toolCall':
|
|
299
|
+
return policy.budgets.maxToolCallCount;
|
|
300
|
+
case 'retry':
|
|
301
|
+
return policy.budgets.maxRetryCount;
|
|
302
|
+
case 'descendant':
|
|
303
|
+
return policy.budgets.maxDescendants;
|
|
304
|
+
case 'waterfallStep':
|
|
305
|
+
return policy.budgets.maxWaterfallStepExecutions;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
async function reserveBudgets(
|
|
310
|
+
charges: Array<{ kind: BudgetKind; amount: number }>,
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
try {
|
|
313
|
+
await budgetState.charge({
|
|
314
|
+
rootRunId: state.rootRunId,
|
|
315
|
+
charges: charges.map((charge) => ({
|
|
316
|
+
key: charge.kind,
|
|
317
|
+
amount: charge.amount,
|
|
318
|
+
limit: budgetLimit(charge.kind),
|
|
319
|
+
})),
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error instanceof BudgetStateLimitError) {
|
|
323
|
+
throw new GovernorBudgetError(
|
|
324
|
+
error.key as BudgetKind,
|
|
325
|
+
error.observed,
|
|
326
|
+
error.limit,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function chargeBudget(kind: BudgetKind, amount = 1): Promise<void> {
|
|
334
|
+
const current =
|
|
335
|
+
kind === 'playCall'
|
|
336
|
+
? state.playCallCount
|
|
337
|
+
: kind === 'toolCall'
|
|
338
|
+
? state.toolCallCount
|
|
339
|
+
: kind === 'retry'
|
|
340
|
+
? state.retryCount
|
|
341
|
+
: kind === 'descendant'
|
|
342
|
+
? state.descendantCount
|
|
343
|
+
: state.waterfallStepExecutions;
|
|
344
|
+
if (current + amount > budgetLimit(kind)) {
|
|
345
|
+
throw new GovernorBudgetError(kind, current + amount, budgetLimit(kind));
|
|
346
|
+
}
|
|
347
|
+
await reserveBudgets([{ kind, amount }]);
|
|
280
348
|
switch (kind) {
|
|
281
349
|
case 'playCall':
|
|
282
350
|
state.playCallCount += amount;
|
|
283
|
-
if (state.playCallCount > policy.budgets.maxPlayCallCount)
|
|
284
|
-
throw new GovernorBudgetError(
|
|
285
|
-
'playCall',
|
|
286
|
-
state.playCallCount,
|
|
287
|
-
policy.budgets.maxPlayCallCount,
|
|
288
|
-
);
|
|
289
351
|
return;
|
|
290
352
|
case 'toolCall':
|
|
291
353
|
state.toolCallCount += amount;
|
|
292
|
-
if (state.toolCallCount > policy.budgets.maxToolCallCount)
|
|
293
|
-
throw new GovernorBudgetError(
|
|
294
|
-
'toolCall',
|
|
295
|
-
state.toolCallCount,
|
|
296
|
-
policy.budgets.maxToolCallCount,
|
|
297
|
-
);
|
|
298
354
|
return;
|
|
299
355
|
case 'retry':
|
|
300
356
|
state.retryCount += amount;
|
|
301
|
-
if (state.retryCount > policy.budgets.maxRetryCount)
|
|
302
|
-
throw new GovernorBudgetError(
|
|
303
|
-
'retry',
|
|
304
|
-
state.retryCount,
|
|
305
|
-
policy.budgets.maxRetryCount,
|
|
306
|
-
);
|
|
307
357
|
return;
|
|
308
358
|
case 'descendant':
|
|
309
359
|
state.descendantCount += amount;
|
|
310
|
-
if (state.descendantCount > policy.budgets.maxDescendants)
|
|
311
|
-
throw new GovernorBudgetError(
|
|
312
|
-
'descendant',
|
|
313
|
-
state.descendantCount,
|
|
314
|
-
policy.budgets.maxDescendants,
|
|
315
|
-
);
|
|
316
360
|
return;
|
|
317
361
|
case 'waterfallStep':
|
|
318
362
|
state.waterfallStepExecutions += amount;
|
|
319
|
-
if (
|
|
320
|
-
state.waterfallStepExecutions >
|
|
321
|
-
policy.budgets.maxWaterfallStepExecutions
|
|
322
|
-
)
|
|
323
|
-
throw new GovernorBudgetError(
|
|
324
|
-
'waterfallStep',
|
|
325
|
-
state.waterfallStepExecutions,
|
|
326
|
-
policy.budgets.maxWaterfallStepExecutions,
|
|
327
|
-
);
|
|
328
363
|
return;
|
|
329
364
|
}
|
|
330
365
|
}
|
|
@@ -356,7 +391,7 @@ export function createPlayExecutionGovernor(
|
|
|
356
391
|
// 3. charge the budget only once the call is actually cleared to run, so a
|
|
357
392
|
// failed/aborted acquisition never permanently consumes tool budget.
|
|
358
393
|
try {
|
|
359
|
-
chargeBudget('toolCall');
|
|
394
|
+
await chargeBudget('toolCall');
|
|
360
395
|
} catch (error) {
|
|
361
396
|
permit.release();
|
|
362
397
|
slot.release();
|
|
@@ -388,7 +423,7 @@ export function createPlayExecutionGovernor(
|
|
|
388
423
|
|
|
389
424
|
chargeBudget,
|
|
390
425
|
|
|
391
|
-
forkChild(childInput) {
|
|
426
|
+
async forkChild(childInput) {
|
|
392
427
|
if (state.ancestryPlayIds.includes(childInput.childPlayName)) {
|
|
393
428
|
const chain = [...state.ancestryPlayIds, childInput.childPlayName].join(
|
|
394
429
|
' -> ',
|
|
@@ -410,11 +445,53 @@ export function createPlayExecutionGovernor(
|
|
|
410
445
|
nextParent,
|
|
411
446
|
policy.budgets.maxChildPlayCallsPerParent,
|
|
412
447
|
);
|
|
448
|
+
if (state.playCallCount + 1 > policy.budgets.maxPlayCallCount)
|
|
449
|
+
throw new GovernorBudgetError(
|
|
450
|
+
'playCall',
|
|
451
|
+
state.playCallCount + 1,
|
|
452
|
+
policy.budgets.maxPlayCallCount,
|
|
453
|
+
);
|
|
454
|
+
if (state.descendantCount + 1 > policy.budgets.maxDescendants)
|
|
455
|
+
throw new GovernorBudgetError(
|
|
456
|
+
'descendant',
|
|
457
|
+
state.descendantCount + 1,
|
|
458
|
+
policy.budgets.maxDescendants,
|
|
459
|
+
);
|
|
413
460
|
// Charge the run-wide play/descendant budgets on the parent. Charged at
|
|
414
461
|
// fork time (not after the caller's child-play slot acquire) and never
|
|
415
462
|
// refunded if that acquire fails — see the forkChild interface doc.
|
|
416
|
-
|
|
417
|
-
|
|
463
|
+
try {
|
|
464
|
+
await budgetState.charge({
|
|
465
|
+
rootRunId: state.rootRunId,
|
|
466
|
+
charges: [
|
|
467
|
+
{
|
|
468
|
+
key: 'playCall',
|
|
469
|
+
amount: 1,
|
|
470
|
+
limit: policy.budgets.maxPlayCallCount,
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
key: 'descendant',
|
|
474
|
+
amount: 1,
|
|
475
|
+
limit: policy.budgets.maxDescendants,
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
key: `childPerParent:${state.currentRunId}`,
|
|
479
|
+
amount: 1,
|
|
480
|
+
limit: policy.budgets.maxChildPlayCallsPerParent,
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
});
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (error instanceof BudgetStateLimitError) {
|
|
486
|
+
const budget = error.key.startsWith('childPerParent:')
|
|
487
|
+
? 'childPerParent'
|
|
488
|
+
: (error.key as BudgetKind);
|
|
489
|
+
throw new GovernorBudgetError(budget, error.observed, error.limit);
|
|
490
|
+
}
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
state.playCallCount += 1;
|
|
494
|
+
state.descendantCount += 1;
|
|
418
495
|
state.parentChildCalls[parentKey] = nextParent;
|
|
419
496
|
// Child seeds from the parent's accumulated counters → lineage-global budget.
|
|
420
497
|
return {
|
|
@@ -456,6 +533,17 @@ export function createPlayExecutionGovernor(
|
|
|
456
533
|
});
|
|
457
534
|
},
|
|
458
535
|
|
|
536
|
+
observeToolSuccess(success) {
|
|
537
|
+
const provider =
|
|
538
|
+
providerByTool.get(success.toolId) ??
|
|
539
|
+
defaultPacingForTool(success.toolId, policy).provider;
|
|
540
|
+
adaptiveAdmission.observeProviderSuccess({
|
|
541
|
+
orgId: input.scope.orgId,
|
|
542
|
+
provider,
|
|
543
|
+
latencyMs: success.latencyMs,
|
|
544
|
+
});
|
|
545
|
+
},
|
|
546
|
+
|
|
459
547
|
snapshot: () => ({
|
|
460
548
|
...state,
|
|
461
549
|
ancestryPlayIds: [...state.ancestryPlayIds],
|