deepline 0.1.231 → 0.1.232
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 +31 -15
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +18 -12
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +18 -3
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +8 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +174 -55
- package/dist/bundling-sources/shared_libs/play-runtime/governor/coordinator-rate-state-backend.ts +13 -9
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +66 -27
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +12 -1
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +4 -3
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
|
@@ -1454,7 +1454,7 @@ async function executeTool(
|
|
|
1454
1454
|
providerIdempotencyKey?: string | null;
|
|
1455
1455
|
},
|
|
1456
1456
|
workflowStep?: WorkflowStep,
|
|
1457
|
-
onProviderBackpressure?: (retryAfterMs: number) => void
|
|
1457
|
+
onProviderBackpressure?: (retryAfterMs: number) => void | Promise<void>,
|
|
1458
1458
|
onRetryAttempt?: () => void | Promise<void>,
|
|
1459
1459
|
transientHttpRetrySafe = false,
|
|
1460
1460
|
abortSignal?: AbortSignal,
|
|
@@ -1500,7 +1500,7 @@ async function executeToolWithLifecycle(
|
|
|
1500
1500
|
},
|
|
1501
1501
|
workflowStep: WorkflowStep | undefined,
|
|
1502
1502
|
callbacks: WorkerCtxCallbacks | undefined,
|
|
1503
|
-
onProviderBackpressure?: (retryAfterMs: number) => void
|
|
1503
|
+
onProviderBackpressure?: (retryAfterMs: number) => void | Promise<void>,
|
|
1504
1504
|
onRetryAttempt?: () => void | Promise<void>,
|
|
1505
1505
|
transientHttpRetrySafe = false,
|
|
1506
1506
|
abortSignal?: AbortSignal,
|
|
@@ -1690,7 +1690,7 @@ async function callToolDirect(
|
|
|
1690
1690
|
executionAuthScopeDigest?: string | null;
|
|
1691
1691
|
providerIdempotencyKey?: string | null;
|
|
1692
1692
|
},
|
|
1693
|
-
onProviderBackpressure?: (retryAfterMs: number) => void
|
|
1693
|
+
onProviderBackpressure?: (retryAfterMs: number) => void | Promise<void>,
|
|
1694
1694
|
// Invoked once per in-process retry attempt (429 / retryable 5xx / synthetic
|
|
1695
1695
|
// transient) so the Governor charges chargeBudget('retry') per attempt — the
|
|
1696
1696
|
// same runaway guard the cjs runner applies (context.ts charges retry on each
|
|
@@ -1877,7 +1877,7 @@ async function callToolDirect(
|
|
|
1877
1877
|
if (failure.backpressureDelayMs !== null) {
|
|
1878
1878
|
// Feed the provider's backpressure into the shared pacer even on the
|
|
1879
1879
|
// final attempt so the (org, provider) bucket backs off across isolates.
|
|
1880
|
-
onProviderBackpressure?.(failure.backpressureDelayMs);
|
|
1880
|
+
await onProviderBackpressure?.(failure.backpressureDelayMs);
|
|
1881
1881
|
}
|
|
1882
1882
|
if (!failure.shouldRetry) {
|
|
1883
1883
|
throw lastError;
|
|
@@ -2616,7 +2616,7 @@ function isWorkerFetchResponse(value: unknown): value is WorkerFetchResponse {
|
|
|
2616
2616
|
async function postRuntimeEgressFetch(
|
|
2617
2617
|
req: RunRequest,
|
|
2618
2618
|
payload: RuntimeEgressFetchPayload,
|
|
2619
|
-
onProviderBackpressure?: (retryAfterMs: number) => void
|
|
2619
|
+
onProviderBackpressure?: (retryAfterMs: number) => void | Promise<void>,
|
|
2620
2620
|
onRetryAttempt?: () => void | Promise<void>,
|
|
2621
2621
|
): Promise<WorkerFetchResponse> {
|
|
2622
2622
|
let lastError: Error | null = null;
|
|
@@ -2667,7 +2667,7 @@ async function postRuntimeEgressFetch(
|
|
|
2667
2667
|
Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
|
|
2668
2668
|
? Math.ceil(retryAfterSeconds * 1000)
|
|
2669
2669
|
: 1_000;
|
|
2670
|
-
onProviderBackpressure?.(retryAfterMs);
|
|
2670
|
+
await onProviderBackpressure?.(retryAfterMs);
|
|
2671
2671
|
if (attempt >= TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS) {
|
|
2672
2672
|
throw lastError;
|
|
2673
2673
|
}
|
|
@@ -3776,7 +3776,7 @@ function createWorkerToolAuthScopeDigestResolver(req: RunRequest) {
|
|
|
3776
3776
|
// (RuntimeToolAuthScopeDigestResolver), whose `invalidate` member is
|
|
3777
3777
|
// REQUIRED — the AUTH_SCOPE_CHANGED re-claim wiring below calls it directly,
|
|
3778
3778
|
// and a resolver without eviction support must be a type error.
|
|
3779
|
-
|
|
3779
|
+
const digestResolver = createRuntimeToolAuthScopeDigestResolver({
|
|
3780
3780
|
missingToolIdMessage: 'Tool auth scope needs tool id.',
|
|
3781
3781
|
fetchDigest: async (normalized) => {
|
|
3782
3782
|
const runScopedPath =
|
|
@@ -3807,12 +3807,19 @@ function createWorkerToolAuthScopeDigestResolver(req: RunRequest) {
|
|
|
3807
3807
|
return digest;
|
|
3808
3808
|
},
|
|
3809
3809
|
});
|
|
3810
|
+
return digestResolver;
|
|
3810
3811
|
}
|
|
3811
3812
|
|
|
3812
3813
|
function createWorkerPacingResolver(req: RunRequest): WorkerPacingResolver {
|
|
3813
3814
|
const cache = new Map<
|
|
3814
3815
|
string,
|
|
3815
|
-
Promise<
|
|
3816
|
+
Promise<
|
|
3817
|
+
| (ResolvedPacingPolicy & {
|
|
3818
|
+
retrySafeTransientHttp: boolean;
|
|
3819
|
+
r: { bucketId: string; token: string } | null;
|
|
3820
|
+
})
|
|
3821
|
+
| null
|
|
3822
|
+
>
|
|
3816
3823
|
>();
|
|
3817
3824
|
return (toolId: string) => {
|
|
3818
3825
|
const normalized = String(toolId || '').trim();
|
|
@@ -3840,6 +3847,7 @@ function createWorkerPacingResolver(req: RunRequest): WorkerPacingResolver {
|
|
|
3840
3847
|
provider?: unknown;
|
|
3841
3848
|
queueHints?: unknown;
|
|
3842
3849
|
retry?: unknown;
|
|
3850
|
+
r?: { bucketId: string; token: string };
|
|
3843
3851
|
} | null;
|
|
3844
3852
|
if (!body) return null;
|
|
3845
3853
|
const pacing = pacingPolicyFromUnknownQueueHints(body.queueHints);
|
|
@@ -3857,6 +3865,7 @@ function createWorkerPacingResolver(req: RunRequest): WorkerPacingResolver {
|
|
|
3857
3865
|
: '',
|
|
3858
3866
|
rules: [],
|
|
3859
3867
|
}),
|
|
3868
|
+
r: body.r ?? null,
|
|
3860
3869
|
retrySafeTransientHttp: retry.retrySafeTransientHttp === true,
|
|
3861
3870
|
};
|
|
3862
3871
|
})();
|
|
@@ -3883,11 +3892,13 @@ function resumeGovernanceFromRequest(req: RunRequest): GovernanceSnapshot {
|
|
|
3883
3892
|
};
|
|
3884
3893
|
}
|
|
3885
3894
|
|
|
3886
|
-
function createGovernorForRun(
|
|
3895
|
+
function createGovernorForRun(
|
|
3896
|
+
req: RunRequest,
|
|
3897
|
+
resolvePacing: WorkerPacingResolver,
|
|
3898
|
+
): {
|
|
3887
3899
|
governor: PlayExecutionGovernor;
|
|
3888
3900
|
resolvePacing: WorkerPacingResolver;
|
|
3889
3901
|
} {
|
|
3890
|
-
const resolvePacing = createWorkerPacingResolver(req);
|
|
3891
3902
|
const governor = createPlayExecutionGovernor({
|
|
3892
3903
|
adapter: 'esm_workers',
|
|
3893
3904
|
scope: {
|
|
@@ -3906,6 +3917,8 @@ function createGovernorForRun(req: RunRequest): {
|
|
|
3906
3917
|
}),
|
|
3907
3918
|
),
|
|
3908
3919
|
resolvePacing,
|
|
3920
|
+
resolveRateScope: async (toolId) =>
|
|
3921
|
+
(await resolvePacing(toolId))?.r ?? null,
|
|
3909
3922
|
resume: resumeGovernanceFromRequest(req),
|
|
3910
3923
|
});
|
|
3911
3924
|
return { governor, resolvePacing };
|
|
@@ -3944,12 +3957,15 @@ function createMinimalWorkerCtx(
|
|
|
3944
3957
|
leasedSheetNamespaces?: Set<string>,
|
|
3945
3958
|
receiptSalvage?: ReceiptSalvageBuffer,
|
|
3946
3959
|
): unknown {
|
|
3947
|
-
const
|
|
3948
|
-
|
|
3949
|
-
const resolveToolActionCacheVersion =
|
|
3950
|
-
createWorkerToolActionCacheVersionResolver(req);
|
|
3960
|
+
const resolveToolPacing = createWorkerPacingResolver(req);
|
|
3961
|
+
const { governor } = createGovernorForRun(req, resolveToolPacing);
|
|
3951
3962
|
const resolveToolAuthScopeDigest =
|
|
3952
3963
|
createWorkerToolAuthScopeDigestResolver(req);
|
|
3964
|
+
const resolveToolActionCacheVersion =
|
|
3965
|
+
createWorkerToolActionCacheVersionResolver(req);
|
|
3966
|
+
// Play-call depth/count/per-parent budgets, child-play concurrency, and the
|
|
3967
|
+
// lineage snapshot are owned by the Governor (createGovernorForRun above).
|
|
3968
|
+
// The worker keeps only substrate mechanism here.
|
|
3953
3969
|
const stepCallCounts: Record<string, number> = {};
|
|
3954
3970
|
const secretRedactor = createSecretRedactionContext();
|
|
3955
3971
|
|
|
@@ -4004,7 +4020,7 @@ function createMinimalWorkerCtx(
|
|
|
4004
4020
|
input: Record<string, unknown>;
|
|
4005
4021
|
workflowStep?: unknown;
|
|
4006
4022
|
callbacks?: WorkerCtxCallbacks;
|
|
4007
|
-
onProviderBackpressure?: (retryAfterMs: number) => void
|
|
4023
|
+
onProviderBackpressure?: (retryAfterMs: number) => void | Promise<void>;
|
|
4008
4024
|
onRetryAttempt?: () => void | Promise<void>;
|
|
4009
4025
|
transientHttpRetrySafe?: boolean;
|
|
4010
4026
|
durableCallReceiptKey?: string | null;
|
|
@@ -73,7 +73,11 @@ import type { WorkerWorkBudgetMeter } from './work-budget';
|
|
|
73
73
|
export type WorkerPacingResolver = (
|
|
74
74
|
toolId: string,
|
|
75
75
|
) => Promise<
|
|
76
|
-
(ResolvedPacingPolicy & {
|
|
76
|
+
| (ResolvedPacingPolicy & {
|
|
77
|
+
retrySafeTransientHttp: boolean;
|
|
78
|
+
r: { bucketId: string; token: string } | null;
|
|
79
|
+
})
|
|
80
|
+
| null
|
|
77
81
|
>;
|
|
78
82
|
|
|
79
83
|
export type WorkerToolActionCacheVersionResolver = (
|
|
@@ -111,7 +115,7 @@ export type WorkerToolDispatchExecuteTool = (input: {
|
|
|
111
115
|
input: Record<string, unknown>;
|
|
112
116
|
workflowStep?: WorkflowStepLike;
|
|
113
117
|
callbacks?: WorkerToolDispatchCallbacks;
|
|
114
|
-
onProviderBackpressure?: (retryAfterMs: number) => void
|
|
118
|
+
onProviderBackpressure?: (retryAfterMs: number) => void | Promise<void>;
|
|
115
119
|
onRetryAttempt?: () => void | Promise<void>;
|
|
116
120
|
transientHttpRetrySafe?: boolean;
|
|
117
121
|
durableCallReceiptKey?: string | null;
|
|
@@ -561,17 +565,19 @@ export class WorkerToolBatchScheduler {
|
|
|
561
565
|
* isolates. Provider comes from the same pacing resolver the Governor uses
|
|
562
566
|
* (the worker has no local catalog), so callers pass only the toolId.
|
|
563
567
|
*/
|
|
564
|
-
private reportBackpressure(
|
|
568
|
+
private async reportBackpressure(
|
|
569
|
+
toolId: string,
|
|
570
|
+
retryAfterMs: number,
|
|
571
|
+
): Promise<void> {
|
|
565
572
|
if (retryAfterMs <= 0) return;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
})();
|
|
573
|
+
const pacing = await this.options.resolvePacing(toolId).catch(() => null);
|
|
574
|
+
if (pacing?.provider) {
|
|
575
|
+
await this.options.governor.reportProviderBackpressure({
|
|
576
|
+
provider: pacing.provider,
|
|
577
|
+
toolId,
|
|
578
|
+
retryAfterMs,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
575
581
|
}
|
|
576
582
|
|
|
577
583
|
async execute(
|
|
@@ -108,10 +108,10 @@ export const SDK_RELEASE = {
|
|
|
108
108
|
// 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
|
|
109
109
|
// 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
|
|
110
110
|
// automatically without blocking their current command.
|
|
111
|
-
version: '0.1.
|
|
111
|
+
version: '0.1.232',
|
|
112
112
|
apiContract: '2026-06-dataset-handle-results-hard-cutover',
|
|
113
113
|
supportPolicy: {
|
|
114
|
-
latest: '0.1.
|
|
114
|
+
latest: '0.1.232',
|
|
115
115
|
minimumSupported: '0.1.53',
|
|
116
116
|
deprecatedBelow: '0.1.219',
|
|
117
117
|
commandMinimumSupported: [
|
|
@@ -57,10 +57,12 @@ export {
|
|
|
57
57
|
import {
|
|
58
58
|
createDefaultGovernanceSnapshot,
|
|
59
59
|
createPlayExecutionGovernor,
|
|
60
|
+
defaultPacingForTool,
|
|
60
61
|
type GovernanceSnapshot,
|
|
61
62
|
type PacingResolver,
|
|
62
63
|
type PlayExecutionGovernor,
|
|
63
64
|
} from './governor/governor';
|
|
65
|
+
import { resolveExecutionPolicy } from './governor/policy';
|
|
64
66
|
import {
|
|
65
67
|
createRuntimeResourceGovernor,
|
|
66
68
|
type RuntimeResourceGovernor,
|
|
@@ -1142,12 +1144,20 @@ function cloneMapFrame(frame: MapExecutionFrame): MapExecutionFrame {
|
|
|
1142
1144
|
*/
|
|
1143
1145
|
function createPacingResolver(
|
|
1144
1146
|
getToolQueueHints: ContextOptions['getToolQueueHints'],
|
|
1147
|
+
getToolProvider: ContextOptions['getToolProvider'],
|
|
1145
1148
|
): PacingResolver {
|
|
1146
1149
|
return async (toolId: string) => {
|
|
1147
1150
|
const builtin = pacingPolicyForTool(toolId, []);
|
|
1148
1151
|
if (builtin) return builtin;
|
|
1149
1152
|
const hints = getToolQueueHints ? await getToolQueueHints(toolId) : [];
|
|
1150
|
-
|
|
1153
|
+
const declared = pacingPolicyForTool(toolId, hints);
|
|
1154
|
+
if (declared) return declared;
|
|
1155
|
+
const provider = (await getToolProvider?.(toolId))?.trim();
|
|
1156
|
+
if (!provider) return null;
|
|
1157
|
+
return {
|
|
1158
|
+
...defaultPacingForTool(toolId, resolveExecutionPolicy('cjs_node20')),
|
|
1159
|
+
provider,
|
|
1160
|
+
};
|
|
1151
1161
|
};
|
|
1152
1162
|
}
|
|
1153
1163
|
|
|
@@ -1375,7 +1385,11 @@ export class PlayContextImpl {
|
|
|
1375
1385
|
},
|
|
1376
1386
|
rateState: options.rateState ?? new InMemoryRateStateBackend(),
|
|
1377
1387
|
budgetState: options.budgetState,
|
|
1378
|
-
resolvePacing: createPacingResolver(
|
|
1388
|
+
resolvePacing: createPacingResolver(
|
|
1389
|
+
options.getToolQueueHints,
|
|
1390
|
+
options.getToolProvider,
|
|
1391
|
+
),
|
|
1392
|
+
resolveRateScope: options.getToolRateScope,
|
|
1379
1393
|
// A root run keeps depth 0 (its first child play is depth 1) but still
|
|
1380
1394
|
// seeds its own play id into the ancestry/currentPlayId so the cycle guard
|
|
1381
1395
|
// catches a child re-invoking the root. createDefaultGovernanceSnapshot
|
|
@@ -8533,8 +8547,9 @@ export class PlayContextImpl {
|
|
|
8533
8547
|
? await this.#options.getToolQueueHints(toolId)
|
|
8534
8548
|
: [];
|
|
8535
8549
|
const provider = hints[0]?.provider?.trim() || `tool:${toolId}`;
|
|
8536
|
-
this.resourceGovernor.reportProviderBackpressure({
|
|
8550
|
+
await this.resourceGovernor.reportProviderBackpressure({
|
|
8537
8551
|
provider,
|
|
8552
|
+
toolId,
|
|
8538
8553
|
retryAfterMs,
|
|
8539
8554
|
});
|
|
8540
8555
|
}
|
|
@@ -641,12 +641,20 @@ export interface ContextOptions {
|
|
|
641
641
|
runtimeSheetBackedMapDatasets?: boolean;
|
|
642
642
|
resolvePlay?: (playRef: string) => Promise<ResolvedPlayExecution | null>;
|
|
643
643
|
getToolQueueHints?: (toolId: string) => Promise<readonly PlayQueueHint[]>;
|
|
644
|
+
getToolProvider?: (toolId: string) => Promise<string | null>;
|
|
644
645
|
getToolRetryPolicy?: (toolId: string) => Promise<{
|
|
645
646
|
retrySafeTransientHttp?: boolean;
|
|
646
647
|
requiresExecutionFence?: boolean;
|
|
647
648
|
} | null>;
|
|
648
649
|
getToolActionCacheVersion?: (toolId: string) => Promise<string> | string;
|
|
649
650
|
getToolAuthScopeDigest?: (toolId: string) => Promise<string> | string;
|
|
651
|
+
getToolRateScope?: (
|
|
652
|
+
toolId: string,
|
|
653
|
+
provider: string,
|
|
654
|
+
) =>
|
|
655
|
+
| Promise<{ bucketId: string; token: string } | null>
|
|
656
|
+
| { bucketId: string; token: string }
|
|
657
|
+
| null;
|
|
650
658
|
invalidateToolAuthScopeDigest?: (toolId: string) => void;
|
|
651
659
|
getToolTargetGetters?: (
|
|
652
660
|
toolId: string,
|
package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts
CHANGED
|
@@ -89,20 +89,8 @@ export interface RateStateCooldown {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
interface LeasedBlock {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* `maxConcurrency` mint lease tokens; each id maps to a durable slot that must
|
|
95
|
-
* be released (or TTL-expired) so the shared concurrency count drains.
|
|
96
|
-
*/
|
|
97
|
-
leaseIds: string[];
|
|
98
|
-
/**
|
|
99
|
-
* Pure-rate (RPS-only) permits with no concurrency slot behind them. The
|
|
100
|
-
* server decremented the provider window by this count already, so drawing
|
|
101
|
-
* one locally is a no-op that needs no release token. Tracked separately from
|
|
102
|
-
* `leaseIds` so a pure-RPS block (the common `defaultPacingForTool` shape)
|
|
103
|
-
* actually amortizes its whole grant across acquires instead of burning it.
|
|
104
|
-
*/
|
|
105
|
-
windowPermits: number;
|
|
92
|
+
permits: Array<[scheduledAtMs: number, leaseId: string | null]>;
|
|
93
|
+
rateScopeToken: string | null;
|
|
106
94
|
expiresAt: number;
|
|
107
95
|
rulesKey: string;
|
|
108
96
|
rules: PacingRule[];
|
|
@@ -110,6 +98,7 @@ interface LeasedBlock {
|
|
|
110
98
|
|
|
111
99
|
interface PendingRelease {
|
|
112
100
|
bucketId: string;
|
|
101
|
+
rateScopeToken: string | null;
|
|
113
102
|
rules: PacingRule[];
|
|
114
103
|
leaseIds: string[];
|
|
115
104
|
flushing: boolean;
|
|
@@ -156,6 +145,8 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
156
145
|
private readonly waiters = new Map<string, number>();
|
|
157
146
|
/** Last-known server cooldown per bucket (surfaced for PROVIDER_EXHAUSTED). */
|
|
158
147
|
private readonly cooldowns = new Map<string, RateStateCooldown>();
|
|
148
|
+
private readonly effectiveRps = new Map<string, number>();
|
|
149
|
+
private readonly pendingSuccesses = new Map<string, number>();
|
|
159
150
|
private pendingReleaseFailure: Error | null = null;
|
|
160
151
|
|
|
161
152
|
constructor(context: WorkerRuntimeApiContext, options: Options = {}) {
|
|
@@ -220,6 +211,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
220
211
|
|
|
221
212
|
async acquire(input: {
|
|
222
213
|
bucketId: string;
|
|
214
|
+
rateScopeToken?: string | null;
|
|
223
215
|
rules: readonly PacingRule[];
|
|
224
216
|
signal?: AbortSignal;
|
|
225
217
|
}): Promise<PacingPermit> {
|
|
@@ -249,13 +241,29 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
249
241
|
: new Error('Rate-state acquire aborted.');
|
|
250
242
|
}
|
|
251
243
|
const localPermit = this.drawFromBlock(bucketId, rulesKey);
|
|
252
|
-
if (localPermit)
|
|
244
|
+
if (localPermit) {
|
|
245
|
+
const waitMs = localPermit.scheduledAtMs - this.now();
|
|
246
|
+
if (waitMs > 0) await this.sleep(waitMs);
|
|
247
|
+
if (signal?.aborted) {
|
|
248
|
+
localPermit.permit.release();
|
|
249
|
+
throw signal.reason instanceof Error
|
|
250
|
+
? signal.reason
|
|
251
|
+
: new Error('Rate-state acquire aborted.');
|
|
252
|
+
}
|
|
253
|
+
return localPermit.permit;
|
|
254
|
+
}
|
|
253
255
|
// Count this acquirer as waiting on the refill so the single-flight refill
|
|
254
256
|
// can size its block to the live wave. Decremented once a block lands (or
|
|
255
257
|
// the refill errors) — see the finally in `refillBlock`.
|
|
256
258
|
this.waiters.set(bucketId, (this.waiters.get(bucketId) ?? 0) + 1);
|
|
257
259
|
try {
|
|
258
|
-
await this.refillBlock(
|
|
260
|
+
await this.refillBlock(
|
|
261
|
+
bucketId,
|
|
262
|
+
input.rateScopeToken?.trim() || null,
|
|
263
|
+
ordered,
|
|
264
|
+
rulesKey,
|
|
265
|
+
signal,
|
|
266
|
+
);
|
|
259
267
|
} finally {
|
|
260
268
|
const remaining = (this.waiters.get(bucketId) ?? 1) - 1;
|
|
261
269
|
if (remaining > 0) this.waiters.set(bucketId, remaining);
|
|
@@ -279,7 +287,15 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
279
287
|
): number {
|
|
280
288
|
const floor = APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE;
|
|
281
289
|
if (rules.some((rule) => rule.maxConcurrency != null)) return floor;
|
|
290
|
+
const configuredRps = Math.min(
|
|
291
|
+
...rules.map((rule) => (rule.requestsPerWindow / rule.windowMs) * 1_000),
|
|
292
|
+
);
|
|
293
|
+
const horizonBudget = Math.max(
|
|
294
|
+
1,
|
|
295
|
+
Math.ceil(this.effectiveRps.get(bucketId) ?? configuredRps),
|
|
296
|
+
);
|
|
282
297
|
const windowBudget = Math.min(
|
|
298
|
+
horizonBudget,
|
|
283
299
|
...rules
|
|
284
300
|
.map((rule) => rule.requestsPerWindow)
|
|
285
301
|
.filter((value) => Number.isFinite(value) && value > 0),
|
|
@@ -353,6 +369,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
353
369
|
*/
|
|
354
370
|
private refillBlock(
|
|
355
371
|
bucketId: string,
|
|
372
|
+
rateScopeToken: string | null,
|
|
356
373
|
ordered: PacingRule[],
|
|
357
374
|
rulesKey: string,
|
|
358
375
|
signal?: AbortSignal,
|
|
@@ -361,6 +378,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
361
378
|
if (existing) return existing;
|
|
362
379
|
const promise = this.performRefill(
|
|
363
380
|
bucketId,
|
|
381
|
+
rateScopeToken,
|
|
364
382
|
ordered,
|
|
365
383
|
rulesKey,
|
|
366
384
|
signal,
|
|
@@ -375,6 +393,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
375
393
|
|
|
376
394
|
private async performRefill(
|
|
377
395
|
bucketId: string,
|
|
396
|
+
rateScopeToken: string | null,
|
|
378
397
|
ordered: PacingRule[],
|
|
379
398
|
rulesKey: string,
|
|
380
399
|
signal?: AbortSignal,
|
|
@@ -397,19 +416,45 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
397
416
|
if (signal?.aborted) return;
|
|
398
417
|
}
|
|
399
418
|
try {
|
|
419
|
+
const observedSuccesses = this.pendingSuccesses.get(bucketId) ?? 0;
|
|
400
420
|
const response = await this.port.acquire({
|
|
401
421
|
bucketId,
|
|
422
|
+
rateScopeToken,
|
|
402
423
|
// Stamp adaptiveMaxRps on default-pacing rules so the DB row's ceiling
|
|
403
424
|
// matches the old in-memory AIMD range.
|
|
404
425
|
rules: withAdaptiveMaxRps(ordered),
|
|
405
426
|
requested: this.requestedForBucket(bucketId, ordered),
|
|
427
|
+
observedSuccesses,
|
|
406
428
|
schedulerSchema: this.schedulerSchema,
|
|
407
429
|
});
|
|
430
|
+
if (observedSuccesses > 0) {
|
|
431
|
+
const remaining = Math.max(
|
|
432
|
+
0,
|
|
433
|
+
(this.pendingSuccesses.get(bucketId) ?? 0) - observedSuccesses,
|
|
434
|
+
);
|
|
435
|
+
if (remaining > 0) this.pendingSuccesses.set(bucketId, remaining);
|
|
436
|
+
else this.pendingSuccesses.delete(bucketId);
|
|
437
|
+
}
|
|
408
438
|
this.recordCooldown(bucketId, response);
|
|
439
|
+
if (
|
|
440
|
+
Number.isFinite(response.effectiveRequestsPerSecond) &&
|
|
441
|
+
Number(response.effectiveRequestsPerSecond) > 0
|
|
442
|
+
) {
|
|
443
|
+
this.effectiveRps.set(
|
|
444
|
+
bucketId,
|
|
445
|
+
Number(response.effectiveRequestsPerSecond),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
409
448
|
if (response.granted > 0) {
|
|
410
449
|
// Merge the whole grant into the block and let acquirers re-draw. No
|
|
411
450
|
// permit is handed out here, so nothing is double-drawn.
|
|
412
|
-
this.mergeGrantedBlock(
|
|
451
|
+
this.mergeGrantedBlock(
|
|
452
|
+
bucketId,
|
|
453
|
+
rateScopeToken,
|
|
454
|
+
ordered,
|
|
455
|
+
rulesKey,
|
|
456
|
+
response,
|
|
457
|
+
);
|
|
413
458
|
return;
|
|
414
459
|
}
|
|
415
460
|
// Live path: the server told us to wait past the threshold. Fail fast
|
|
@@ -456,9 +501,14 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
456
501
|
*/
|
|
457
502
|
private mergeGrantedBlock(
|
|
458
503
|
bucketId: string,
|
|
504
|
+
rateScopeToken: string | null,
|
|
459
505
|
ordered: PacingRule[],
|
|
460
506
|
rulesKey: string,
|
|
461
|
-
response: {
|
|
507
|
+
response: {
|
|
508
|
+
granted: number;
|
|
509
|
+
leaseIds?: unknown;
|
|
510
|
+
scheduledAtMs?: unknown;
|
|
511
|
+
},
|
|
462
512
|
): void {
|
|
463
513
|
const hasConcurrency = ordered.some((rule) => rule.maxConcurrency != null);
|
|
464
514
|
const leaseIds = Array.isArray(response.leaseIds)
|
|
@@ -477,38 +527,71 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
477
527
|
`Rate-state store returned ${leaseIds.length} lease tokens for pure-rate bucket ${bucketId}.`,
|
|
478
528
|
);
|
|
479
529
|
}
|
|
530
|
+
// Rolling deploy compatibility: an older gateway returns no schedule. It
|
|
531
|
+
// has already debited the block under the legacy token-bucket contract, so
|
|
532
|
+
// keep those permits immediately usable until every gateway serves the new
|
|
533
|
+
// globally reserved timestamps.
|
|
534
|
+
const scheduledAtMs = Array.isArray(response.scheduledAtMs)
|
|
535
|
+
? response.scheduledAtMs.map(Number)
|
|
536
|
+
: Array.from({ length: response.granted }, () => this.now());
|
|
537
|
+
if (
|
|
538
|
+
scheduledAtMs.length !== response.granted ||
|
|
539
|
+
scheduledAtMs.some((value) => !Number.isFinite(value) || value < 0)
|
|
540
|
+
) {
|
|
541
|
+
throw new Error(
|
|
542
|
+
`Rate-state store granted ${response.granted} permits for ${bucketId} with ${scheduledAtMs.length} dispatch timestamps.`,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
480
545
|
this.mergeBlock(
|
|
481
546
|
bucketId,
|
|
482
|
-
|
|
483
|
-
|
|
547
|
+
rateScopeToken,
|
|
548
|
+
scheduledAtMs.map(
|
|
549
|
+
(scheduledAtMs, index): [number, string | null] => [
|
|
550
|
+
scheduledAtMs,
|
|
551
|
+
hasConcurrency ? (leaseIds[index] ?? null) : null,
|
|
552
|
+
],
|
|
553
|
+
),
|
|
484
554
|
rulesKey,
|
|
485
555
|
ordered,
|
|
486
556
|
);
|
|
487
557
|
}
|
|
488
558
|
|
|
489
|
-
penalize(input: {
|
|
559
|
+
async penalize(input: {
|
|
560
|
+
bucketId: string;
|
|
561
|
+
rateScopeToken?: string | null;
|
|
562
|
+
cooldownMs: number;
|
|
563
|
+
}): Promise<void> {
|
|
490
564
|
if (input.cooldownMs <= 0) return;
|
|
491
565
|
this.drainBlock(input.bucketId);
|
|
492
|
-
|
|
493
|
-
.penalize({
|
|
566
|
+
try {
|
|
567
|
+
await this.port.penalize({
|
|
494
568
|
bucketId: input.bucketId,
|
|
569
|
+
rateScopeToken: input.rateScopeToken?.trim() || null,
|
|
495
570
|
cooldownMs: input.cooldownMs,
|
|
496
571
|
schedulerSchema: this.schedulerSchema,
|
|
497
|
-
})
|
|
498
|
-
.catch((error) => {
|
|
499
|
-
const normalized = normalizeError(error);
|
|
500
|
-
this.pendingReleaseFailure = new Error(
|
|
501
|
-
`Rate-state penalize failed for ${input.bucketId}: ${normalized.message}`,
|
|
502
|
-
);
|
|
503
|
-
this.onStoreFailure({
|
|
504
|
-
bucketId: input.bucketId,
|
|
505
|
-
error: normalized.message,
|
|
506
|
-
});
|
|
507
572
|
});
|
|
573
|
+
} catch (error) {
|
|
574
|
+
const normalized = normalizeError(error);
|
|
575
|
+
this.onStoreFailure({
|
|
576
|
+
bucketId: input.bucketId,
|
|
577
|
+
error: normalized.message,
|
|
578
|
+
});
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Rate-state penalize failed for ${input.bucketId}: ${normalized.message}`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
observeSuccess(input: { bucketId: string }): void {
|
|
586
|
+
this.pendingSuccesses.set(
|
|
587
|
+
input.bucketId,
|
|
588
|
+
Math.min(1_000_000, (this.pendingSuccesses.get(input.bucketId) ?? 0) + 1),
|
|
589
|
+
);
|
|
508
590
|
}
|
|
509
591
|
|
|
510
592
|
private permitFor(
|
|
511
593
|
bucketId: string,
|
|
594
|
+
rateScopeToken: string | null,
|
|
512
595
|
rules: PacingRule[],
|
|
513
596
|
leaseId: string | null,
|
|
514
597
|
): PacingPermit {
|
|
@@ -520,35 +603,49 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
520
603
|
release: () => {
|
|
521
604
|
if (released) return;
|
|
522
605
|
released = true;
|
|
523
|
-
this.releaseReserved(
|
|
606
|
+
this.releaseReserved(
|
|
607
|
+
bucketId,
|
|
608
|
+
rateScopeToken,
|
|
609
|
+
rules,
|
|
610
|
+
leaseId ? [leaseId] : [],
|
|
611
|
+
);
|
|
524
612
|
},
|
|
525
613
|
};
|
|
526
614
|
}
|
|
527
615
|
|
|
528
616
|
private mergeBlock(
|
|
529
617
|
bucketId: string,
|
|
530
|
-
|
|
531
|
-
|
|
618
|
+
rateScopeToken: string | null,
|
|
619
|
+
permits: LeasedBlock['permits'],
|
|
532
620
|
rulesKey: string,
|
|
533
621
|
rules: PacingRule[],
|
|
534
622
|
): void {
|
|
535
|
-
const
|
|
623
|
+
const lastScheduledAtMs = permits.at(-1)?.[0] ?? this.now();
|
|
624
|
+
const freshExpiresAt =
|
|
625
|
+
Math.max(this.now(), lastScheduledAtMs) + leasedBlockTtlMs(rules);
|
|
536
626
|
const existing = this.blocks.get(bucketId);
|
|
537
627
|
if (
|
|
538
628
|
existing &&
|
|
539
629
|
existing.rulesKey === rulesKey &&
|
|
540
630
|
existing.expiresAt > this.now()
|
|
541
631
|
) {
|
|
542
|
-
existing.
|
|
543
|
-
existing.
|
|
544
|
-
existing.expiresAt = Math.
|
|
632
|
+
existing.permits.push(...permits);
|
|
633
|
+
existing.permits.sort((a, b) => a[0] - b[0]);
|
|
634
|
+
existing.expiresAt = Math.max(existing.expiresAt, freshExpiresAt);
|
|
545
635
|
return;
|
|
546
636
|
}
|
|
547
637
|
if (existing)
|
|
548
|
-
this.releaseReserved(
|
|
638
|
+
this.releaseReserved(
|
|
639
|
+
bucketId,
|
|
640
|
+
existing.rateScopeToken,
|
|
641
|
+
existing.rules,
|
|
642
|
+
existing.permits.flatMap(([, leaseId]) =>
|
|
643
|
+
leaseId ? [leaseId] : [],
|
|
644
|
+
),
|
|
645
|
+
);
|
|
549
646
|
this.blocks.set(bucketId, {
|
|
550
|
-
|
|
551
|
-
|
|
647
|
+
permits,
|
|
648
|
+
rateScopeToken,
|
|
552
649
|
expiresAt: freshExpiresAt,
|
|
553
650
|
rulesKey,
|
|
554
651
|
rules,
|
|
@@ -558,7 +655,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
558
655
|
private drawFromBlock(
|
|
559
656
|
bucketId: string,
|
|
560
657
|
rulesKey: string,
|
|
561
|
-
): PacingPermit | null {
|
|
658
|
+
): { permit: PacingPermit; scheduledAtMs: number } | null {
|
|
562
659
|
const block = this.blocks.get(bucketId);
|
|
563
660
|
if (!block) return null;
|
|
564
661
|
if (block.rulesKey !== rulesKey || block.expiresAt <= this.now()) {
|
|
@@ -567,30 +664,50 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
567
664
|
// decremented the provider window for them, and the rule-scoped TTL is
|
|
568
665
|
// capped so at most one block's worth is stranded per bucket/window).
|
|
569
666
|
// Concurrency leases must be released so the shared slot count drains.
|
|
570
|
-
this.releaseReserved(
|
|
667
|
+
this.releaseReserved(
|
|
668
|
+
bucketId,
|
|
669
|
+
block.rateScopeToken,
|
|
670
|
+
block.rules,
|
|
671
|
+
block.permits.flatMap(([, leaseId]) =>
|
|
672
|
+
leaseId ? [leaseId] : [],
|
|
673
|
+
),
|
|
674
|
+
);
|
|
571
675
|
return null;
|
|
572
676
|
}
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
);
|
|
576
|
-
// Concurrency blocks draw a lease token (release semantics); pure-rate
|
|
577
|
-
// blocks draw an untracked window permit that the server already accounted.
|
|
578
|
-
const leaseId = hasConcurrency ? (block.leaseIds.pop() ?? null) : null;
|
|
579
|
-
if (!hasConcurrency && block.windowPermits > 0) block.windowPermits -= 1;
|
|
580
|
-
if (block.leaseIds.length === 0 && block.windowPermits === 0)
|
|
677
|
+
const scheduled = block.permits.shift();
|
|
678
|
+
if (!scheduled) {
|
|
581
679
|
this.blocks.delete(bucketId);
|
|
582
|
-
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
if (block.permits.length === 0) this.blocks.delete(bucketId);
|
|
683
|
+
return {
|
|
684
|
+
permit: this.permitFor(
|
|
685
|
+
bucketId,
|
|
686
|
+
block.rateScopeToken,
|
|
687
|
+
block.rules,
|
|
688
|
+
scheduled[1],
|
|
689
|
+
),
|
|
690
|
+
scheduledAtMs: scheduled[0],
|
|
691
|
+
};
|
|
583
692
|
}
|
|
584
693
|
|
|
585
694
|
private drainBlock(bucketId: string): void {
|
|
586
695
|
const block = this.blocks.get(bucketId);
|
|
587
696
|
if (!block) return;
|
|
588
697
|
this.blocks.delete(bucketId);
|
|
589
|
-
this.releaseReserved(
|
|
698
|
+
this.releaseReserved(
|
|
699
|
+
bucketId,
|
|
700
|
+
block.rateScopeToken,
|
|
701
|
+
block.rules,
|
|
702
|
+
block.permits.flatMap(([, leaseId]) =>
|
|
703
|
+
leaseId ? [leaseId] : [],
|
|
704
|
+
),
|
|
705
|
+
);
|
|
590
706
|
}
|
|
591
707
|
|
|
592
708
|
private releaseReserved(
|
|
593
709
|
bucketId: string,
|
|
710
|
+
rateScopeToken: string | null,
|
|
594
711
|
rules: readonly PacingRule[],
|
|
595
712
|
leaseIds: readonly string[],
|
|
596
713
|
): void {
|
|
@@ -603,6 +720,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
603
720
|
const key = `${bucketId}\0${rulesSignature(ordered)}`;
|
|
604
721
|
const pending = this.pendingReleases.get(key) ?? {
|
|
605
722
|
bucketId,
|
|
723
|
+
rateScopeToken,
|
|
606
724
|
rules: ordered,
|
|
607
725
|
leaseIds: [],
|
|
608
726
|
flushing: false,
|
|
@@ -623,6 +741,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
|
|
|
623
741
|
try {
|
|
624
742
|
await this.port.release({
|
|
625
743
|
bucketId: pending.bucketId,
|
|
744
|
+
rateScopeToken: pending.rateScopeToken,
|
|
626
745
|
rules: pending.rules,
|
|
627
746
|
leaseIds,
|
|
628
747
|
schedulerSchema: this.schedulerSchema,
|
package/dist/bundling-sources/shared_libs/play-runtime/governor/coordinator-rate-state-backend.ts
CHANGED
|
@@ -167,22 +167,26 @@ export class CoordinatorRateStateBackend implements RateStateBackend {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
penalize(input: {
|
|
170
|
+
async penalize(input: {
|
|
171
|
+
bucketId: string;
|
|
172
|
+
cooldownMs: number;
|
|
173
|
+
}): Promise<void> {
|
|
171
174
|
if (input.cooldownMs <= 0) return;
|
|
172
175
|
// Drop any cached block for this bucket so the cooldown takes effect on the
|
|
173
176
|
// very next acquire instead of being masked by already-leased permits.
|
|
174
177
|
this.blocks.delete(input.bucketId);
|
|
175
|
-
|
|
176
|
-
.ratePenalize({
|
|
178
|
+
try {
|
|
179
|
+
await this.port.ratePenalize({
|
|
177
180
|
bucketId: input.bucketId,
|
|
178
181
|
cooldownMs: input.cooldownMs,
|
|
179
|
-
})
|
|
180
|
-
.catch((error) => {
|
|
181
|
-
this.onDegraded({
|
|
182
|
-
bucketId: input.bucketId,
|
|
183
|
-
error: error instanceof Error ? error.message : String(error),
|
|
184
|
-
});
|
|
185
182
|
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.onDegraded({
|
|
185
|
+
bucketId: input.bucketId,
|
|
186
|
+
error: error instanceof Error ? error.message : String(error),
|
|
187
|
+
});
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
/**
|
|
@@ -70,6 +70,14 @@ export type PacingResolver = (
|
|
|
70
70
|
toolId: string,
|
|
71
71
|
) => Promise<{ provider: string; rules: PacingRule[] } | null>;
|
|
72
72
|
|
|
73
|
+
export type RateScopeResolver = (
|
|
74
|
+
toolId: string,
|
|
75
|
+
provider: string,
|
|
76
|
+
) =>
|
|
77
|
+
| Promise<{ bucketId: string; token: string } | null>
|
|
78
|
+
| { bucketId: string; token: string }
|
|
79
|
+
| null;
|
|
80
|
+
|
|
73
81
|
export function defaultPacingForTool(
|
|
74
82
|
toolId: string,
|
|
75
83
|
policy: ResolvedExecutionPolicy,
|
|
@@ -96,12 +104,9 @@ export interface PlayExecutionGovernor {
|
|
|
96
104
|
/** Block until a map-row slot is free. */
|
|
97
105
|
acquireRowSlot(opts?: { signal?: AbortSignal }): Promise<WorkLease>;
|
|
98
106
|
/**
|
|
99
|
-
* Block until
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* failed/aborted acquire never consumes budget). A run over tool budget still
|
|
103
|
-
* acquires and holds a slot + pacing permit before the breach is detected; the
|
|
104
|
-
* breach surfaces only once the call is otherwise cleared to run.
|
|
107
|
+
* Block until the per-provider pacer permit and then a global
|
|
108
|
+
* tool-concurrency slot are free, charge the tool-call budget, and return a
|
|
109
|
+
* lease. Parked future permits never consume scarce execution slots.
|
|
105
110
|
*/
|
|
106
111
|
acquireToolSlot(
|
|
107
112
|
toolId: string,
|
|
@@ -132,12 +137,19 @@ export interface PlayExecutionGovernor {
|
|
|
132
137
|
/** Feed a provider's Retry-After back into the shared pacer. */
|
|
133
138
|
reportProviderBackpressure(input: {
|
|
134
139
|
provider: string;
|
|
140
|
+
/**
|
|
141
|
+
* The tool whose provider call received the 429. Credential-scoped rate
|
|
142
|
+
* buckets are resolved per tool, so delayed feedback must retain this
|
|
143
|
+
* identity instead of selecting the latest scope for the provider.
|
|
144
|
+
*/
|
|
145
|
+
toolId?: string;
|
|
135
146
|
retryAfterMs: number;
|
|
136
|
-
}): void
|
|
147
|
+
}): Promise<void>;
|
|
137
148
|
|
|
138
149
|
/** Feed clean provider calls back into adaptive admission. */
|
|
139
150
|
observeProviderSuccess(input: {
|
|
140
151
|
provider: string;
|
|
152
|
+
toolId?: string;
|
|
141
153
|
latencyMs?: number | null;
|
|
142
154
|
}): void;
|
|
143
155
|
/** Feed success back using the same tool-to-provider resolution as acquire. */
|
|
@@ -155,6 +167,7 @@ interface GovernorInput {
|
|
|
155
167
|
rateState: RateStateBackend;
|
|
156
168
|
budgetState?: BudgetStateBackend;
|
|
157
169
|
resolvePacing: PacingResolver;
|
|
170
|
+
resolveRateScope?: RateScopeResolver;
|
|
158
171
|
adaptiveAdmission?: RuntimeAdaptiveAdmission;
|
|
159
172
|
resume?: GovernanceSnapshot;
|
|
160
173
|
}
|
|
@@ -246,6 +259,7 @@ export function createPlayExecutionGovernor(
|
|
|
246
259
|
});
|
|
247
260
|
const budgetState = input.budgetState ?? new InMemoryBudgetStateBackend();
|
|
248
261
|
const providerByTool = new Map<string, string>();
|
|
262
|
+
const scopeByTool = new Map<string, [string, string]>();
|
|
249
263
|
|
|
250
264
|
// When the rate-state backend owns pacing authoritatively (the Absurd/Node
|
|
251
265
|
// app-runtime Postgres pacer runs the whole token bucket + AIMD in the row),
|
|
@@ -256,7 +270,20 @@ export function createPlayExecutionGovernor(
|
|
|
256
270
|
// undefined and keep the in-memory adaptive admission (byte-identical).
|
|
257
271
|
const dbAuthoritativePacing = input.rateState.kind === 'app_runtime_postgres';
|
|
258
272
|
|
|
259
|
-
const
|
|
273
|
+
const orgBucket = (provider: string) => `${input.scope.orgId}:${provider}`;
|
|
274
|
+
|
|
275
|
+
async function resolveScope(toolId: string, provider: string) {
|
|
276
|
+
const resolved = await input.resolveRateScope?.(toolId, provider);
|
|
277
|
+
const scope = [
|
|
278
|
+
resolved?.bucketId ?? orgBucket(provider),
|
|
279
|
+
resolved?.token ?? '',
|
|
280
|
+
] as [string, string];
|
|
281
|
+
scopeByTool.set(toolId, scope);
|
|
282
|
+
return scope;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const scopeFor = (toolId: string | undefined, provider: string) =>
|
|
286
|
+
scopeByTool.get(toolId ?? '') ?? ([orgBucket(provider), ''] as [string, string]);
|
|
260
287
|
|
|
261
288
|
async function resolveAdaptivePacing(toolId: string): Promise<{
|
|
262
289
|
provider: string;
|
|
@@ -348,23 +375,26 @@ export function createPlayExecutionGovernor(
|
|
|
348
375
|
|
|
349
376
|
acquireRowSlot: (opts) => rowSlots.acquire(opts?.signal),
|
|
350
377
|
async acquireToolSlot(toolId, opts) {
|
|
351
|
-
// 1.
|
|
352
|
-
const slot = await toolSlots.acquire(opts?.signal);
|
|
353
|
-
// 2. per-(org,provider) pacing. The provider comes from the pacing
|
|
378
|
+
// 1. Per-provider pacing. The provider comes from the pacing
|
|
354
379
|
// resolver, so callers only need the toolId. No rules → no pacing.
|
|
355
|
-
|
|
380
|
+
const pacing = await resolveAdaptivePacing(toolId);
|
|
381
|
+
const rateScope = await resolveScope(toolId, pacing.provider);
|
|
382
|
+
const permit = await input.rateState.acquire({
|
|
383
|
+
bucketId: rateScope[0],
|
|
384
|
+
rateScopeToken: rateScope[1],
|
|
385
|
+
rules: pacing.rules,
|
|
386
|
+
signal: opts?.signal,
|
|
387
|
+
});
|
|
388
|
+
// 2. Acquire a scarce execution slot only after the provider permit is
|
|
389
|
+
// due. If the slot wait aborts, refund any concurrency-bearing permit.
|
|
390
|
+
let slot: WorkLease;
|
|
356
391
|
try {
|
|
357
|
-
|
|
358
|
-
permit = await input.rateState.acquire({
|
|
359
|
-
bucketId: bucketId(pacing.provider),
|
|
360
|
-
rules: pacing.rules,
|
|
361
|
-
signal: opts?.signal,
|
|
362
|
-
});
|
|
392
|
+
slot = await toolSlots.acquire(opts?.signal);
|
|
363
393
|
} catch (error) {
|
|
364
|
-
|
|
394
|
+
permit.release();
|
|
365
395
|
throw error;
|
|
366
396
|
}
|
|
367
|
-
// 3.
|
|
397
|
+
// 3. Charge the budget only once the call is actually cleared to run, so a
|
|
368
398
|
// failed/aborted acquisition never permanently consumes tool budget.
|
|
369
399
|
try {
|
|
370
400
|
await chargeBudget('toolCall');
|
|
@@ -409,7 +439,7 @@ export function createPlayExecutionGovernor(
|
|
|
409
439
|
resolveRowConcurrency: (requested) =>
|
|
410
440
|
resolveRowConcurrency(policy, requested),
|
|
411
441
|
|
|
412
|
-
reportProviderBackpressure(bp) {
|
|
442
|
+
async reportProviderBackpressure(bp) {
|
|
413
443
|
// DB-authoritative pacer: route backpressure ONLY to the row via
|
|
414
444
|
// penalize. The in-memory adaptive admission must not also halve, or the
|
|
415
445
|
// provider would be throttled twice (once in the row, once in memory).
|
|
@@ -420,13 +450,22 @@ export function createPlayExecutionGovernor(
|
|
|
420
450
|
retryAfterMs: bp.retryAfterMs,
|
|
421
451
|
});
|
|
422
452
|
}
|
|
423
|
-
|
|
424
|
-
|
|
453
|
+
const rateScope = scopeFor(bp.toolId, bp.provider);
|
|
454
|
+
await input.rateState.penalize({
|
|
455
|
+
bucketId: rateScope[0],
|
|
456
|
+
...(rateScope[1] ? { rateScopeToken: rateScope[1] } : {}),
|
|
425
457
|
cooldownMs: bp.retryAfterMs,
|
|
426
458
|
});
|
|
427
459
|
},
|
|
428
460
|
|
|
429
461
|
observeProviderSuccess(success) {
|
|
462
|
+
if (dbAuthoritativePacing) {
|
|
463
|
+
const rateScope = scopeFor(success.toolId, success.provider);
|
|
464
|
+
input.rateState.observeSuccess?.({
|
|
465
|
+
bucketId: rateScope[0],
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
430
469
|
adaptiveAdmission.observeProviderSuccess({
|
|
431
470
|
orgId: input.scope.orgId,
|
|
432
471
|
provider: success.provider,
|
|
@@ -438,9 +477,9 @@ export function createPlayExecutionGovernor(
|
|
|
438
477
|
const provider =
|
|
439
478
|
providerByTool.get(success.toolId) ??
|
|
440
479
|
defaultPacingForTool(success.toolId, policy).provider;
|
|
441
|
-
|
|
442
|
-
orgId: input.scope.orgId,
|
|
480
|
+
governor.observeProviderSuccess({
|
|
443
481
|
provider,
|
|
482
|
+
toolId: success.toolId,
|
|
444
483
|
latencyMs: success.latencyMs,
|
|
445
484
|
});
|
|
446
485
|
},
|
|
@@ -507,8 +546,8 @@ function createInlineChildGovernor(
|
|
|
507
546
|
root.suggestedParallelism(toolId, fallback),
|
|
508
547
|
chargeBudget: (kind, amount) => root.chargeBudget(kind, amount),
|
|
509
548
|
resolveRowConcurrency: (requested) => root.resolveRowConcurrency(requested),
|
|
510
|
-
reportProviderBackpressure: (input) =>
|
|
511
|
-
root.reportProviderBackpressure(input),
|
|
549
|
+
reportProviderBackpressure: async (input) =>
|
|
550
|
+
await root.reportProviderBackpressure(input),
|
|
512
551
|
observeProviderSuccess: (input) => root.observeProviderSuccess(input),
|
|
513
552
|
observeToolSuccess: (input) => root.observeToolSuccess(input),
|
|
514
553
|
async forkInlineChild(input) {
|
|
@@ -86,6 +86,7 @@ export interface RateStateBackend {
|
|
|
86
86
|
*/
|
|
87
87
|
acquire(input: {
|
|
88
88
|
bucketId: string;
|
|
89
|
+
rateScopeToken?: string | null;
|
|
89
90
|
rules: readonly PacingRule[];
|
|
90
91
|
signal?: AbortSignal;
|
|
91
92
|
}): Promise<PacingPermit>;
|
|
@@ -94,7 +95,17 @@ export interface RateStateBackend {
|
|
|
94
95
|
* Feed a server-observed Retry-After back so future acquires for this bucket
|
|
95
96
|
* back off. Advisory and idempotent; never un-charges an in-flight call.
|
|
96
97
|
*/
|
|
97
|
-
penalize(input: {
|
|
98
|
+
penalize(input: {
|
|
99
|
+
bucketId: string;
|
|
100
|
+
rateScopeToken?: string | null;
|
|
101
|
+
cooldownMs: number;
|
|
102
|
+
}): void | Promise<void>;
|
|
103
|
+
|
|
104
|
+
/** Record confirmed provider success for adaptive recovery. */
|
|
105
|
+
observeSuccess?(input: {
|
|
106
|
+
bucketId: string;
|
|
107
|
+
rateScopeToken?: string | null;
|
|
108
|
+
}): void;
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
const NOOP_PERMIT: PacingPermit = { release() {} };
|
|
@@ -25,14 +25,24 @@ export type PlayRunnerRateStateBackendConfig =
|
|
|
25
25
|
|
|
26
26
|
export type PlayRunnerRateStateAcquireInput = {
|
|
27
27
|
bucketId: string;
|
|
28
|
+
rateScopeToken?: string | null;
|
|
28
29
|
rules: PacingRule[];
|
|
29
30
|
requested: number;
|
|
31
|
+
/** Confirmed provider successes since this runner's previous reservation. */
|
|
32
|
+
observedSuccesses?: number;
|
|
30
33
|
schedulerSchema?: string | null;
|
|
31
34
|
};
|
|
32
35
|
|
|
33
36
|
export type PlayRunnerRateStateAcquireResult = {
|
|
34
37
|
granted: number;
|
|
35
38
|
waitMs: number;
|
|
39
|
+
/**
|
|
40
|
+
* One globally reserved dispatch timestamp per granted permit. The runtime
|
|
41
|
+
* may cache the block, but must not dispatch a permit before its timestamp.
|
|
42
|
+
*/
|
|
43
|
+
scheduledAtMs?: number[];
|
|
44
|
+
/** Live shared rate after adaptive backpressure/recovery. */
|
|
45
|
+
effectiveRequestsPerSecond?: number;
|
|
36
46
|
/** One independently-expiring concurrency reservation per granted permit. */
|
|
37
47
|
leaseIds?: string[];
|
|
38
48
|
/**
|
|
@@ -50,6 +60,7 @@ export type PlayRunnerRateStateAcquireResult = {
|
|
|
50
60
|
|
|
51
61
|
export type PlayRunnerRateStateReleaseInput = {
|
|
52
62
|
bucketId: string;
|
|
63
|
+
rateScopeToken?: string | null;
|
|
53
64
|
rules: PacingRule[];
|
|
54
65
|
leaseIds: string[];
|
|
55
66
|
schedulerSchema?: string | null;
|
|
@@ -57,6 +68,7 @@ export type PlayRunnerRateStateReleaseInput = {
|
|
|
57
68
|
|
|
58
69
|
export type PlayRunnerRateStatePenalizeInput = {
|
|
59
70
|
bucketId: string;
|
|
71
|
+
rateScopeToken?: string | null;
|
|
60
72
|
cooldownMs: number;
|
|
61
73
|
schedulerSchema?: string | null;
|
|
62
74
|
};
|
|
@@ -67,8 +67,9 @@ export interface RuntimeResourceGovernor {
|
|
|
67
67
|
resolveRowConcurrency(requested?: number): number;
|
|
68
68
|
reportProviderBackpressure(input: {
|
|
69
69
|
provider: string;
|
|
70
|
+
toolId?: string;
|
|
70
71
|
retryAfterMs: number;
|
|
71
|
-
}): void
|
|
72
|
+
}): Promise<void>;
|
|
72
73
|
observe(input: RuntimeResourceObservation): void;
|
|
73
74
|
snapshot(): RuntimeResourceSnapshot;
|
|
74
75
|
}
|
|
@@ -308,8 +309,8 @@ export function createRuntimeResourceGovernor(input: {
|
|
|
308
309
|
return input.executionGovernor.resolveRowConcurrency(requested);
|
|
309
310
|
},
|
|
310
311
|
|
|
311
|
-
reportProviderBackpressure(backpressure) {
|
|
312
|
-
input.executionGovernor.reportProviderBackpressure(backpressure);
|
|
312
|
+
async reportProviderBackpressure(backpressure) {
|
|
313
|
+
await input.executionGovernor.reportProviderBackpressure(backpressure);
|
|
313
314
|
observe({
|
|
314
315
|
providerResourceKey: backpressure.provider,
|
|
315
316
|
provider429: true,
|
package/dist/cli/index.js
CHANGED
|
@@ -625,10 +625,10 @@ var SDK_RELEASE = {
|
|
|
625
625
|
// 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
|
|
626
626
|
// 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
|
|
627
627
|
// automatically without blocking their current command.
|
|
628
|
-
version: "0.1.
|
|
628
|
+
version: "0.1.232",
|
|
629
629
|
apiContract: "2026-06-dataset-handle-results-hard-cutover",
|
|
630
630
|
supportPolicy: {
|
|
631
|
-
latest: "0.1.
|
|
631
|
+
latest: "0.1.232",
|
|
632
632
|
minimumSupported: "0.1.53",
|
|
633
633
|
deprecatedBelow: "0.1.219",
|
|
634
634
|
commandMinimumSupported: [
|
package/dist/cli/index.mjs
CHANGED
|
@@ -610,10 +610,10 @@ var SDK_RELEASE = {
|
|
|
610
610
|
// 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
|
|
611
611
|
// 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
|
|
612
612
|
// automatically without blocking their current command.
|
|
613
|
-
version: "0.1.
|
|
613
|
+
version: "0.1.232",
|
|
614
614
|
apiContract: "2026-06-dataset-handle-results-hard-cutover",
|
|
615
615
|
supportPolicy: {
|
|
616
|
-
latest: "0.1.
|
|
616
|
+
latest: "0.1.232",
|
|
617
617
|
minimumSupported: "0.1.53",
|
|
618
618
|
deprecatedBelow: "0.1.219",
|
|
619
619
|
commandMinimumSupported: [
|
package/dist/index.js
CHANGED
|
@@ -424,10 +424,10 @@ var SDK_RELEASE = {
|
|
|
424
424
|
// 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
|
|
425
425
|
// 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
|
|
426
426
|
// automatically without blocking their current command.
|
|
427
|
-
version: "0.1.
|
|
427
|
+
version: "0.1.232",
|
|
428
428
|
apiContract: "2026-06-dataset-handle-results-hard-cutover",
|
|
429
429
|
supportPolicy: {
|
|
430
|
-
latest: "0.1.
|
|
430
|
+
latest: "0.1.232",
|
|
431
431
|
minimumSupported: "0.1.53",
|
|
432
432
|
deprecatedBelow: "0.1.219",
|
|
433
433
|
commandMinimumSupported: [
|
package/dist/index.mjs
CHANGED
|
@@ -354,10 +354,10 @@ var SDK_RELEASE = {
|
|
|
354
354
|
// 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
|
|
355
355
|
// 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
|
|
356
356
|
// automatically without blocking their current command.
|
|
357
|
-
version: "0.1.
|
|
357
|
+
version: "0.1.232",
|
|
358
358
|
apiContract: "2026-06-dataset-handle-results-hard-cutover",
|
|
359
359
|
supportPolicy: {
|
|
360
|
-
latest: "0.1.
|
|
360
|
+
latest: "0.1.232",
|
|
361
361
|
minimumSupported: "0.1.53",
|
|
362
362
|
deprecatedBelow: "0.1.219",
|
|
363
363
|
commandMinimumSupported: [
|