@myagentroam/agent 0.11.62 → 0.11.63
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/cli/catalog-file.d.ts +2 -0
- package/dist/cli/main.js +1 -0
- package/dist/model/configuration.d.ts +1 -0
- package/dist/model/configuration.js +1 -0
- package/dist/runtime/context/gc-admission.d.ts +5 -0
- package/dist/runtime/context/gc-admission.js +23 -0
- package/dist/runtime/context/gc-candidates-code.js +5 -6
- package/dist/runtime/context/gc-candidates-exec.js +3 -4
- package/dist/runtime/context/gc-candidates-read-patch.js +3 -4
- package/dist/runtime/context/gc-candidates-web.d.ts +1 -1
- package/dist/runtime/context/gc-candidates-web.js +6 -7
- package/dist/runtime/context/gc-runtime.js +3 -57
- package/dist/runtime/context/gc-tokens.d.ts +0 -4
- package/dist/runtime/context/gc-tokens.js +0 -18
- package/dist/runtime/context/gc.d.ts +1 -1
- package/dist/runtime/context/gc.js +24 -139
- package/dist/sdk/execution-context-coordinator.d.ts +1 -0
- package/dist/sdk/execution-context-coordinator.js +3 -1
- package/dist/sdk/session-execution-factory.js +4 -1
- package/dist/session/context-gc-checkpoint.d.ts +1 -4
- package/dist/session/context-gc-checkpoint.js +10 -22
- package/dist/session/context-gc-contract.d.ts +8 -8
- package/dist/session/store/jsonl/jsonl-store.js +1 -4
- package/package.json +1 -1
- package/dist/runtime/context/gc-economics.d.ts +0 -35
- package/dist/runtime/context/gc-economics.js +0 -93
|
@@ -30,6 +30,7 @@ export declare function serializeModel(model: CliModelConfiguration): {
|
|
|
30
30
|
defaultReasoningEffort?: "low" | "medium" | "high" | "xhigh" | "max" | "ultra" | undefined;
|
|
31
31
|
titleGeneration: boolean;
|
|
32
32
|
codeMode: boolean;
|
|
33
|
+
gcEnabled?: boolean | undefined;
|
|
33
34
|
fastMode?: boolean | undefined;
|
|
34
35
|
serviceTier?: "default" | "auto" | "flex" | "priority" | "fast" | "ultrafast" | undefined;
|
|
35
36
|
hostedWebSearch: boolean;
|
|
@@ -65,6 +66,7 @@ export declare function serializeModel(model: CliModelConfiguration): {
|
|
|
65
66
|
defaultReasoningEffort?: "low" | "medium" | "high" | "xhigh" | "max" | "ultra" | undefined;
|
|
66
67
|
titleGeneration: boolean;
|
|
67
68
|
codeMode: boolean;
|
|
69
|
+
gcEnabled?: boolean | undefined;
|
|
68
70
|
fastMode?: boolean | undefined;
|
|
69
71
|
serviceTier?: "default" | "auto" | "flex" | "priority" | "fast" | "ultrafast" | undefined;
|
|
70
72
|
hostedWebSearch: boolean;
|
package/dist/cli/main.js
CHANGED
|
@@ -147,6 +147,7 @@ async function modelsCommand(home, arguments_, codexAuthFile) {
|
|
|
147
147
|
const defaultReasoningEffortOption = option('--default-reasoning-effort');
|
|
148
148
|
const model = {
|
|
149
149
|
useResourceUrl: existing?.useResourceUrl ?? false,
|
|
150
|
+
gcEnabled: existing?.gcEnabled ?? true,
|
|
150
151
|
id,
|
|
151
152
|
name: required(option('--name', existing?.name), '--name'),
|
|
152
153
|
protocol,
|
|
@@ -56,6 +56,7 @@ declare const rawModelSchema: z.ZodObject<{
|
|
|
56
56
|
subagentModel: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
57
57
|
titleGeneration: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
58
58
|
codeMode: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
59
|
+
gcEnabled: z.ZodOptional<z.ZodBoolean>;
|
|
59
60
|
useResourceUrl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
60
61
|
fastMode: z.ZodOptional<z.ZodBoolean>;
|
|
61
62
|
serviceTier: z.ZodOptional<z.ZodEnum<{
|
|
@@ -38,6 +38,7 @@ const modelFieldsSchema = z
|
|
|
38
38
|
subagentModel: z.boolean().optional().default(false),
|
|
39
39
|
titleGeneration: z.boolean().optional().default(false),
|
|
40
40
|
codeMode: z.boolean().optional().default(false),
|
|
41
|
+
gcEnabled: z.boolean().optional(),
|
|
41
42
|
useResourceUrl: z.boolean().optional().default(false),
|
|
42
43
|
fastMode: z.boolean().optional(),
|
|
43
44
|
serviceTier: z.enum(['auto', 'default', 'flex', 'priority', 'fast', 'ultrafast']).optional(),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ModelUsage } from '../../model/contracts.js';
|
|
2
|
+
export declare const COMPACT_THRESHOLD_RATIO: number;
|
|
3
|
+
export declare function contextGcMinimumReclaimedTokens(contextWindowTokens: number): number;
|
|
4
|
+
export declare function contextGcEligibleUsage(usage: ModelUsage, contextWindowTokens: number): number | undefined;
|
|
5
|
+
export declare function modelContextUsage(value: ModelUsage | Record<string, unknown>): number | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AGENT_EXECUTION_POLICY } from '../execution-policy.js';
|
|
2
|
+
const CONTEXT_GC_START_RATIO = 0.5;
|
|
3
|
+
export const COMPACT_THRESHOLD_RATIO = AGENT_EXECUTION_POLICY.backgroundCompactionPercent / 100;
|
|
4
|
+
export function contextGcMinimumReclaimedTokens(contextWindowTokens) {
|
|
5
|
+
return Math.floor(contextWindowTokens / 10) + 1;
|
|
6
|
+
}
|
|
7
|
+
export function contextGcEligibleUsage(usage, contextWindowTokens) {
|
|
8
|
+
const contextUsageTokens = modelContextUsage(usage);
|
|
9
|
+
if (contextUsageTokens === undefined ||
|
|
10
|
+
!Number.isSafeInteger(contextWindowTokens) ||
|
|
11
|
+
contextWindowTokens <= 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
const usageRatio = contextUsageTokens / contextWindowTokens;
|
|
14
|
+
if (usageRatio < CONTEXT_GC_START_RATIO || usageRatio >= COMPACT_THRESHOLD_RATIO)
|
|
15
|
+
return undefined;
|
|
16
|
+
return contextUsageTokens;
|
|
17
|
+
}
|
|
18
|
+
export function modelContextUsage(value) {
|
|
19
|
+
const context = value.contextInputTokens ?? value.inputTokens;
|
|
20
|
+
return typeof context === 'number' && Number.isSafeInteger(context) && context >= 0
|
|
21
|
+
? context
|
|
22
|
+
: undefined;
|
|
23
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { retainedCodeInputs } from '../../session/context-gc-observations.js';
|
|
2
2
|
import { contextGcTargetKey } from '../../session/context-gc-checkpoint.js';
|
|
3
3
|
import { codeCellCompletionSequence, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, hasGenericContextGcPreview, normalizeContextGcCodeModeInput } from '../../session/context-gc-replacements.js';
|
|
4
|
-
import {
|
|
5
|
-
import { contextGcProtectionMature } from '../../session/context-gc-protection.js';
|
|
4
|
+
import { CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS, contextGcProtectionMature } from '../../session/context-gc-protection.js';
|
|
6
5
|
import { targetFor, isRecord } from './gc-candidates.js';
|
|
7
6
|
export function collectCodeModeCandidates(input, groups, existingTargets, candidateSavings) {
|
|
8
7
|
const eligible = [];
|
|
@@ -44,9 +43,9 @@ export function collectCodeModeCandidates(input, groups, existingTargets, candid
|
|
|
44
43
|
continue;
|
|
45
44
|
if (contextGcProtectionMature(protection))
|
|
46
45
|
eligible.push(candidate);
|
|
47
|
-
else if (candidate.observedRollouts <
|
|
46
|
+
else if (candidate.observedRollouts < CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS) {
|
|
48
47
|
protectedTokens += reclaimedTokens;
|
|
49
|
-
retryAfterRollouts = Math.min(retryAfterRollouts,
|
|
48
|
+
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS - candidate.observedRollouts);
|
|
50
49
|
}
|
|
51
50
|
}
|
|
52
51
|
if (group.failed || !group.completed)
|
|
@@ -107,9 +106,9 @@ export function collectCodeModeCandidates(input, groups, existingTargets, candid
|
|
|
107
106
|
continue;
|
|
108
107
|
if (contextGcProtectionMature(protection))
|
|
109
108
|
eligible.push(candidate);
|
|
110
|
-
else if (observedRollouts <
|
|
109
|
+
else if (observedRollouts < CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS) {
|
|
111
110
|
protectedTokens += reclaimedTokens;
|
|
112
|
-
retryAfterRollouts = Math.min(retryAfterRollouts,
|
|
111
|
+
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS - observedRollouts);
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
return { eligible, protectedTokens, retryAfterRollouts };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { contextGcTargetKey } from '../../session/context-gc-checkpoint.js';
|
|
2
2
|
import { contextGcExecInputReplacement, contextGcExecOutputReplacement, normalizeContextGcExecResult } from '../../session/context-gc-replacements.js';
|
|
3
|
-
import {
|
|
4
|
-
import { contextGcProtectionMature } from '../../session/context-gc-protection.js';
|
|
3
|
+
import { CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS, contextGcProtectionMature } from '../../session/context-gc-protection.js';
|
|
5
4
|
import { targetFor, isRecord } from './gc-candidates.js';
|
|
6
5
|
import { terminalExecLifecycles } from '../../session/context-gc-exec-lifecycle.js';
|
|
7
6
|
export function collectExecCandidates(input, groups, existingTargets, candidateSavings) {
|
|
@@ -85,9 +84,9 @@ export function collectExecCandidates(input, groups, existingTargets, candidateS
|
|
|
85
84
|
mature.push(replacement);
|
|
86
85
|
observedRollouts = Math.min(observedRollouts, protection.rollouts);
|
|
87
86
|
}
|
|
88
|
-
else if (protection.rollouts <
|
|
87
|
+
else if (protection.rollouts < CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS) {
|
|
89
88
|
awaitingRollouts.push(replacement);
|
|
90
|
-
retryAfterRollouts = Math.min(retryAfterRollouts,
|
|
89
|
+
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS - protection.rollouts);
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
if (awaitingRollouts.length)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { contextGcLogicalPathKey, contextGcTargetKey } from '../../session/context-gc-checkpoint.js';
|
|
2
2
|
import { contextGcApplyPatchInputReplacement, contextGcPatchResultFromCompletedPayload, contextGcPatchResultFromFailedPayload, contextGcStaleReadReplacement, contextGcReadRangeCovers, contextGcCoveredReadReplacement } from '../../session/context-gc-replacements.js';
|
|
3
3
|
import { parseReadContextOutput } from '../../prompts/read-context.js';
|
|
4
|
-
import {
|
|
5
|
-
import { contextGcProtectionMature } from '../../session/context-gc-protection.js';
|
|
4
|
+
import { CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS, contextGcProtectionMature } from '../../session/context-gc-protection.js';
|
|
6
5
|
import { targetFor } from './gc-candidates.js';
|
|
7
6
|
export function collectDeterministicCandidates(input, groups, existingReplacements, existingTargets, candidateSavings) {
|
|
8
7
|
const eligible = [];
|
|
@@ -108,9 +107,9 @@ export function collectDeterministicCandidates(input, groups, existingReplacemen
|
|
|
108
107
|
mature.push(replacement);
|
|
109
108
|
observedRollouts = Math.min(observedRollouts, protection.rollouts);
|
|
110
109
|
}
|
|
111
|
-
else if (protection.rollouts <
|
|
110
|
+
else if (protection.rollouts < CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS) {
|
|
112
111
|
awaitingRollouts.push(replacement);
|
|
113
|
-
retryAfterRollouts = Math.min(retryAfterRollouts,
|
|
112
|
+
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS - protection.rollouts);
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
if (awaitingRollouts.length) {
|
|
@@ -4,7 +4,7 @@ export declare function collectWebCandidates(input: ContextGcPlannerInput, group
|
|
|
4
4
|
protectedTokens: number;
|
|
5
5
|
retryAfterRollouts: number;
|
|
6
6
|
};
|
|
7
|
-
/** Generic successful tool results share output
|
|
7
|
+
/** Generic successful tool results share output retention rules, without inferring resource state. */
|
|
8
8
|
export declare function collectToolOutputCandidates(input: ContextGcPlannerInput, groups: ReadonlyMap<string, ToolGroup>, existingTargets: ReadonlySet<string>, candidateSavings: CandidateSavings): {
|
|
9
9
|
eligible: ContextCandidate[];
|
|
10
10
|
protectedTokens: number;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { contextGcTargetKey } from '../../session/context-gc-checkpoint.js';
|
|
2
2
|
import { completedTodoWrite, todoInputReplacement, todoSnapshotReplacement, visibleTodoSnapshot } from '../../session/context-gc-todo.js';
|
|
3
3
|
import { contextGcWebOutputReplacement, contextGcToolOutputReplacement, contextGcSkillOutputReplacement, hasGenericContextGcPreview, isGenericContextGcTool } from '../../session/context-gc-replacements.js';
|
|
4
|
-
import {
|
|
5
|
-
import { contextGcProtectionMature } from '../../session/context-gc-protection.js';
|
|
4
|
+
import { CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS, contextGcProtectionMature } from '../../session/context-gc-protection.js';
|
|
6
5
|
import { targetFor, isRecord } from './gc-candidates.js';
|
|
7
6
|
export function collectWebCandidates(input, groups, existingTargets, candidateSavings) {
|
|
8
7
|
const eligible = [];
|
|
@@ -49,14 +48,14 @@ export function collectWebCandidates(input, groups, existingTargets, candidateSa
|
|
|
49
48
|
continue;
|
|
50
49
|
if (contextGcProtectionMature(protection))
|
|
51
50
|
eligible.push(candidate);
|
|
52
|
-
else if (observedRollouts <
|
|
51
|
+
else if (observedRollouts < CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS) {
|
|
53
52
|
protectedTokens += reclaimedTokens;
|
|
54
|
-
retryAfterRollouts = Math.min(retryAfterRollouts,
|
|
53
|
+
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS - observedRollouts);
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
return { eligible, protectedTokens, retryAfterRollouts };
|
|
58
57
|
}
|
|
59
|
-
/** Generic successful tool results share output
|
|
58
|
+
/** Generic successful tool results share output retention rules, without inferring resource state. */
|
|
60
59
|
export function collectToolOutputCandidates(input, groups, existingTargets, candidateSavings) {
|
|
61
60
|
const eligible = [];
|
|
62
61
|
let protectedTokens = 0;
|
|
@@ -260,9 +259,9 @@ export function collectToolOutputCandidates(input, groups, existingTargets, cand
|
|
|
260
259
|
continue;
|
|
261
260
|
if (contextGcProtectionMature(protection))
|
|
262
261
|
eligible.push(candidate);
|
|
263
|
-
else if (observedRollouts <
|
|
262
|
+
else if (observedRollouts < CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS) {
|
|
264
263
|
protectedTokens += reclaimedTokens;
|
|
265
|
-
retryAfterRollouts = Math.min(retryAfterRollouts,
|
|
264
|
+
retryAfterRollouts = Math.min(retryAfterRollouts, CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS - observedRollouts);
|
|
266
265
|
}
|
|
267
266
|
}
|
|
268
267
|
return { eligible, protectedTokens, retryAfterRollouts };
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
-
import { ContextGcCheckpointCache,
|
|
3
|
-
import {
|
|
2
|
+
import { ContextGcCheckpointCache, contextGcReplacementKey } from '../../session/context-gc-checkpoint.js';
|
|
3
|
+
import { contextGcEligibleUsage, inspectContextGc } from './gc.js';
|
|
4
4
|
import { buildContextProjection } from './projection.js';
|
|
5
5
|
/** Only scheduling metadata survives a call; records and validation caches do not. */
|
|
6
6
|
export class ContextGcRuntime {
|
|
7
7
|
#scope = '';
|
|
8
8
|
#lastUsage;
|
|
9
|
-
#checkpoint;
|
|
10
|
-
#rolloutsSinceCheckpoint = 0;
|
|
11
9
|
#failures = 0;
|
|
12
10
|
#remaining = 0;
|
|
13
11
|
reset() {
|
|
14
12
|
this.#scope = '';
|
|
15
13
|
this.#lastUsage = undefined;
|
|
16
|
-
this.#checkpoint = undefined;
|
|
17
|
-
this.#rolloutsSinceCheckpoint = 0;
|
|
18
14
|
this.#failures = 0;
|
|
19
15
|
this.#remaining = 0;
|
|
20
16
|
}
|
|
@@ -44,40 +40,18 @@ export class ContextGcRuntime {
|
|
|
44
40
|
this.#remaining = 0;
|
|
45
41
|
}
|
|
46
42
|
this.#lastUsage = usage;
|
|
47
|
-
if (this.#checkpoint)
|
|
48
|
-
this.#rolloutsSinceCheckpoint++;
|
|
49
43
|
const maximumInterval = usage / input.contextWindowTokens >= 0.8 ? 2 : 8;
|
|
50
44
|
this.#remaining = Math.min(this.#remaining, maximumInterval - 1);
|
|
51
45
|
if (this.#remaining > 0 && !mediaRequired) {
|
|
52
46
|
this.#remaining--;
|
|
53
47
|
return false;
|
|
54
48
|
}
|
|
55
|
-
if (!mediaRequired &&
|
|
56
|
-
this.#checkpoint?.trigger.modelId === input.modelId &&
|
|
57
|
-
this.#checkpoint.trigger.contextWindowTokens === input.contextWindowTokens &&
|
|
58
|
-
!this.#checkpoint.protectedCandidatesPending &&
|
|
59
|
-
this.#rolloutsSinceCheckpoint < CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
|
|
60
|
-
usage - this.#checkpoint.trigger.contextInputTokens <
|
|
61
|
-
contextGcMinimumUsageGrowth(input.contextWindowTokens))
|
|
62
|
-
return false;
|
|
63
49
|
const attempt = await attemptContextGc(input);
|
|
64
50
|
if (attempt.result) {
|
|
65
|
-
this.#checkpoint = {
|
|
66
|
-
trigger: { ...attempt.result.projection.checkpoint.trigger },
|
|
67
|
-
protectedCandidatesPending: attempt.result.projection.checkpoint.metrics.protectedCandidateTokens > 0
|
|
68
|
-
};
|
|
69
|
-
this.#rolloutsSinceCheckpoint = 0;
|
|
70
51
|
this.#failures = 0;
|
|
71
|
-
this.#remaining =
|
|
52
|
+
this.#remaining = 0;
|
|
72
53
|
}
|
|
73
54
|
else {
|
|
74
|
-
if (attempt.checkpointTrigger)
|
|
75
|
-
this.#checkpoint = {
|
|
76
|
-
trigger: { ...attempt.checkpointTrigger },
|
|
77
|
-
protectedCandidatesPending: attempt.protectedCandidatesPending === true
|
|
78
|
-
};
|
|
79
|
-
if (attempt.checkpointTrigger)
|
|
80
|
-
this.#rolloutsSinceCheckpoint = 0;
|
|
81
55
|
this.#failures = Math.min(3, this.#failures + 1);
|
|
82
56
|
this.#remaining =
|
|
83
57
|
Math.min(2 ** this.#failures, maximumInterval, attempt.retryAfterRollouts ?? Infinity) - 1;
|
|
@@ -107,30 +81,6 @@ async function attemptContextGc(input) {
|
|
|
107
81
|
records =
|
|
108
82
|
input.records ?? (await input.store.readExecutionState(input.sessionId)).contextRecords;
|
|
109
83
|
const checkpointCache = new ContextGcCheckpointCache();
|
|
110
|
-
const previous = checkpointCache.read(records);
|
|
111
|
-
if (previous) {
|
|
112
|
-
deferred = {
|
|
113
|
-
...deferred,
|
|
114
|
-
checkpointTrigger: previous.payload.trigger,
|
|
115
|
-
protectedCandidatesPending: previous.payload.metrics.protectedCandidateTokens > 0
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
if (!mediaRequired &&
|
|
119
|
-
previous?.payload.trigger.modelId === input.modelId &&
|
|
120
|
-
previous.payload.trigger.contextWindowTokens === input.contextWindowTokens) {
|
|
121
|
-
const rolloutGrowth = records.filter((record) => record.sequence > previous.record.sequence &&
|
|
122
|
-
record.type === 'model.attempt' &&
|
|
123
|
-
isRecord(record.payload) &&
|
|
124
|
-
record.payload.purpose === 'EXECUTION' &&
|
|
125
|
-
record.payload.state === 'SUCCEEDED').length;
|
|
126
|
-
const minimumRollouts = previous.payload.metrics.protectedCandidateTokens > 0
|
|
127
|
-
? 2
|
|
128
|
-
: CONTEXT_GC_POLICY.largeExecProtectionRollouts;
|
|
129
|
-
if (rolloutGrowth < minimumRollouts &&
|
|
130
|
-
contextUsage - previous.payload.trigger.contextInputTokens <
|
|
131
|
-
contextGcMinimumUsageGrowth(input.contextWindowTokens))
|
|
132
|
-
return deferred;
|
|
133
|
-
}
|
|
134
84
|
const projection = buildContextProjection(records, checkpointCache);
|
|
135
85
|
const messages = projection.entries.map((entry) => entry.message);
|
|
136
86
|
const inspection = inspectContextGc({
|
|
@@ -155,11 +105,8 @@ async function attemptContextGc(input) {
|
|
|
155
105
|
if (inspection.retryAfterRollouts !== undefined)
|
|
156
106
|
deferred = {
|
|
157
107
|
...deferred,
|
|
158
|
-
protectedCandidatesPending: true,
|
|
159
108
|
retryAfterRollouts: inspection.retryAfterRollouts
|
|
160
109
|
};
|
|
161
|
-
else if (!inspection.plan)
|
|
162
|
-
deferred = { ...deferred, protectedCandidatesPending: false };
|
|
163
110
|
const plan = inspection.plan;
|
|
164
111
|
if (!plan)
|
|
165
112
|
return deferred;
|
|
@@ -206,7 +153,6 @@ async function matchingCheckpointWasPersisted(store, sessionId, draft, records,
|
|
|
206
153
|
if (latest?.payload.boundarySequence !== draft.boundarySequence ||
|
|
207
154
|
!isDeepStrictEqual(latest.payload.base, draft.base) ||
|
|
208
155
|
!isDeepStrictEqual(latest.payload.trigger, draft.trigger) ||
|
|
209
|
-
!isDeepStrictEqual(latest.payload.economics, draft.economics) ||
|
|
210
156
|
!isDeepStrictEqual(latest.payload.inlineMedia, draft.inlineMedia) ||
|
|
211
157
|
!isDeepStrictEqual(latest.payload.metrics, draft.metrics))
|
|
212
158
|
return false;
|
|
@@ -20,7 +20,3 @@ export declare function contextGcTokenEntries(input: TokenContext, messages: rea
|
|
|
20
20
|
export declare function contextGcReclaimedTokens(input: TokenContext, before: readonly ModelMessage[], after: readonly ModelMessage[]): number;
|
|
21
21
|
export declare function contextGcPrefixTokens(input: TokenContext): number;
|
|
22
22
|
export declare function contextGcMessageTokens(input: TokenContext, messages: readonly ModelMessage[]): number;
|
|
23
|
-
export declare function contextGcCacheImpact(input: TokenContext, before: readonly ModelMessage[], after: readonly ModelMessage[], cacheRead: number, reclaimedTokens: number): {
|
|
24
|
-
preservedCachedPrefixTokens: number;
|
|
25
|
-
reclaimedCachedTokens: number;
|
|
26
|
-
};
|
|
@@ -83,24 +83,6 @@ export function contextGcPrefixTokens(input) {
|
|
|
83
83
|
export function contextGcMessageTokens(input, messages) {
|
|
84
84
|
return contextGcTokenEntries(input, messages).reduce((sum, entry) => sum + entry.tokens, 0);
|
|
85
85
|
}
|
|
86
|
-
export function contextGcCacheImpact(input, before, after, cacheRead, reclaimedTokens) {
|
|
87
|
-
const afterByKey = new Map(contextGcTokenEntries(input, after).map((entry) => [entry.key, entry]));
|
|
88
|
-
let offset = contextGcPrefixTokens(input);
|
|
89
|
-
let earliest = cacheRead;
|
|
90
|
-
let reclaimedCachedTokens = 0;
|
|
91
|
-
for (const entry of contextGcTokenEntries(input, before)) {
|
|
92
|
-
const replacement = afterByKey.get(entry.key);
|
|
93
|
-
if (entry.text !== replacement?.text) {
|
|
94
|
-
earliest = Math.min(earliest, offset);
|
|
95
|
-
reclaimedCachedTokens += Math.min(Math.max(0, entry.tokens - (replacement?.tokens ?? 0)), Math.max(0, cacheRead - offset));
|
|
96
|
-
}
|
|
97
|
-
offset += entry.tokens;
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
preservedCachedPrefixTokens: Math.max(0, Math.min(cacheRead, earliest)),
|
|
101
|
-
reclaimedCachedTokens: Math.min(reclaimedTokens, reclaimedCachedTokens)
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
86
|
function tokensFor(input, text) {
|
|
105
87
|
const cached = input.tokenCache?.get(text);
|
|
106
88
|
if (cached !== undefined)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ContextGcPlan, type ContextGcInspection, type ContextGcPlannerInput } from './gc-contracts.js';
|
|
2
|
-
export {
|
|
2
|
+
export { contextGcMinimumReclaimedTokens, contextGcEligibleUsage } from './gc-admission.js';
|
|
3
3
|
export { type ContextGcPlan, type ContextGcInspection, type ContextGcPlannerInput } from './gc-contracts.js';
|
|
4
4
|
export declare function planContextGc(input: ContextGcPlannerInput): ContextGcPlan | undefined;
|
|
5
5
|
export declare function inspectContextGc(input: ContextGcPlannerInput): ContextGcInspection;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { ContextGcCheckpointCache, contextGcBaseFromRecords,
|
|
1
|
+
import { ContextGcCheckpointCache, contextGcBaseFromRecords, contextGcTargetKey, normalizeContextGcReplacements } from '../../session/context-gc-checkpoint.js';
|
|
2
2
|
import { buildContextGcProtection } from '../../session/context-gc-protection.js';
|
|
3
|
-
import { CONTEXT_GC_TOKEN_ESTIMATOR,
|
|
3
|
+
import { CONTEXT_GC_TOKEN_ESTIMATOR, contextGcMessageTokens, contextGcPrefixTokens, contextGcReclaimedTokens } from './gc-tokens.js';
|
|
4
4
|
import { applyContextGcReplacementsToMessages, providerToolCallId } from './projection.js';
|
|
5
|
-
import {
|
|
5
|
+
import { contextGcMinimumReclaimedTokens, contextGcEligibleUsage, modelContextUsage } from './gc-admission.js';
|
|
6
6
|
import { buildToolGroups, countSuccessfulNormalRollouts, isRecord } from './gc-candidates.js';
|
|
7
7
|
import { collectCodeModeCandidates } from './gc-candidates-code.js';
|
|
8
8
|
import { collectDeterministicCandidates } from './gc-candidates-read-patch.js';
|
|
9
9
|
import { collectExecCandidates } from './gc-candidates-exec.js';
|
|
10
10
|
import { collectToolOutputCandidates, collectWebCandidates } from './gc-candidates-web.js';
|
|
11
|
-
export {
|
|
11
|
+
export { contextGcMinimumReclaimedTokens, contextGcEligibleUsage } from './gc-admission.js';
|
|
12
12
|
export function planContextGc(input) {
|
|
13
13
|
return inspectContextGc(input).plan;
|
|
14
14
|
}
|
|
@@ -36,20 +36,8 @@ export function inspectContextGc(input) {
|
|
|
36
36
|
protectedCandidateTokens: text.plan?.metrics.protectedCandidateTokens ?? 0
|
|
37
37
|
};
|
|
38
38
|
if (text.plan) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
replacements: text.plan.draft.replacements,
|
|
42
|
-
observedRollouts: text.plan.metrics.minimumObservedRollouts,
|
|
43
|
-
evidenceSequence: text.plan.draft.boundarySequence
|
|
44
|
-
}
|
|
45
|
-
], state, media.project);
|
|
46
|
-
if (combined?.economics.passes) {
|
|
47
|
-
const plan = finalizePlan(input, combined, true);
|
|
48
|
-
if (plan)
|
|
49
|
-
return { plan };
|
|
50
|
-
}
|
|
51
|
-
if (media.reason === 'TEXT_GC')
|
|
52
|
-
return text;
|
|
39
|
+
const { project: _project, ...inlineMedia } = media;
|
|
40
|
+
return { ...text, plan: { ...text.plan, draft: { ...text.plan.draft, inlineMedia } } };
|
|
53
41
|
}
|
|
54
42
|
// Byte pressure has its own admission signal; it never relaxes text GC eligibility.
|
|
55
43
|
const onlyMedia = evaluateCandidateSet(input, input.checkpointCache.read(input.records)?.payload.replacements ?? [], [
|
|
@@ -60,7 +48,7 @@ export function inspectContextGc(input) {
|
|
|
60
48
|
}
|
|
61
49
|
], { ...state, stage: 'INLINE_MEDIA', mediaOnly: true }, media.project);
|
|
62
50
|
const plan = onlyMedia && finalizePlan(input, onlyMedia, true);
|
|
63
|
-
return plan ? { plan } : text;
|
|
51
|
+
return plan ? { ...text, plan } : text;
|
|
64
52
|
}
|
|
65
53
|
function inspectTextGc(input) {
|
|
66
54
|
const contextUsageTokens = contextGcEligibleUsage(input.usage, input.contextWindowTokens);
|
|
@@ -77,26 +65,6 @@ function inspectTextGc(input) {
|
|
|
77
65
|
return {};
|
|
78
66
|
const normalRolloutOrdinal = countSuccessfulNormalRollouts(input.records, input.modelId, input.contextWindowTokens);
|
|
79
67
|
const previous = input.checkpointCache.read(input.records);
|
|
80
|
-
const checkpointEligible = contextGcCheckpointIntervalEligible({
|
|
81
|
-
usageSequence: usageRecord.sequence,
|
|
82
|
-
contextInputTokens: contextUsageTokens,
|
|
83
|
-
contextWindowTokens: input.contextWindowTokens,
|
|
84
|
-
normalRolloutOrdinal,
|
|
85
|
-
modelId: input.modelId,
|
|
86
|
-
platform: input.platform
|
|
87
|
-
}, previous?.payload.trigger);
|
|
88
|
-
// Discovery and economic evaluation have their own low-growth opportunities;
|
|
89
|
-
// neither a protected candidate nor periodic discovery makes a draft eligible.
|
|
90
|
-
if (previous?.payload.trigger.modelId === input.modelId &&
|
|
91
|
-
previous.payload.trigger.contextWindowTokens === input.contextWindowTokens) {
|
|
92
|
-
const rolloutGrowth = normalRolloutOrdinal - previous.payload.trigger.normalRolloutOrdinal;
|
|
93
|
-
if (rolloutGrowth < 2 ||
|
|
94
|
-
(previous.payload.metrics.protectedCandidateTokens === 0 &&
|
|
95
|
-
rolloutGrowth < CONTEXT_GC_POLICY.largeExecProtectionRollouts &&
|
|
96
|
-
contextUsageTokens - previous.payload.trigger.contextInputTokens <
|
|
97
|
-
contextGcMinimumUsageGrowth(input.contextWindowTokens)))
|
|
98
|
-
return {};
|
|
99
|
-
}
|
|
100
68
|
if (input.messages.length !== input.messageSources.length)
|
|
101
69
|
return {};
|
|
102
70
|
const groups = buildToolGroups(input.records, input.messages, input.messageSources);
|
|
@@ -118,13 +86,14 @@ function inspectTextGc(input) {
|
|
|
118
86
|
codeMode.protectedTokens +
|
|
119
87
|
web.protectedTokens +
|
|
120
88
|
tools.protectedTokens;
|
|
121
|
-
const
|
|
89
|
+
const eligible = [
|
|
122
90
|
...deterministic.eligible,
|
|
123
91
|
...exec.eligible,
|
|
124
92
|
...codeMode.eligible,
|
|
125
93
|
...web.eligible,
|
|
126
94
|
...tools.eligible
|
|
127
|
-
]
|
|
95
|
+
];
|
|
96
|
+
const combined = evaluateCandidateSet(input, existing, eligible, {
|
|
128
97
|
stage: 'UNIFIED',
|
|
129
98
|
contextUsageTokens,
|
|
130
99
|
usageRecord,
|
|
@@ -132,7 +101,7 @@ function inspectTextGc(input) {
|
|
|
132
101
|
protectedCandidateTokens
|
|
133
102
|
});
|
|
134
103
|
const retryAfterRollouts = Math.min(deterministic.retryAfterRollouts, exec.retryAfterRollouts, codeMode.retryAfterRollouts, web.retryAfterRollouts, tools.retryAfterRollouts);
|
|
135
|
-
if (combined
|
|
104
|
+
if (combined) {
|
|
136
105
|
const plan = finalizePlan(input, combined);
|
|
137
106
|
return plan
|
|
138
107
|
? {
|
|
@@ -143,10 +112,11 @@ function inspectTextGc(input) {
|
|
|
143
112
|
}
|
|
144
113
|
: {};
|
|
145
114
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
115
|
+
// An already mature but sub-threshold batch can combine with newly mature
|
|
116
|
+
// candidates on the next rollout; no-candidate backoff must not delay it.
|
|
117
|
+
if (eligible.length > 0)
|
|
118
|
+
return { retryAfterRollouts: 1 };
|
|
119
|
+
return protectedCandidateTokens > 0 && Number.isFinite(retryAfterRollouts)
|
|
150
120
|
? { retryAfterRollouts }
|
|
151
121
|
: {};
|
|
152
122
|
}
|
|
@@ -170,23 +140,15 @@ function finalizePlan(input, evaluated, includeMedia = false) {
|
|
|
170
140
|
beforeTokens: evaluated.beforeTokens,
|
|
171
141
|
afterTokens: evaluated.afterTokens,
|
|
172
142
|
reclaimedTokens: evaluated.reclaimedTokens,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
requiredPositiveNetRollouts: evaluated.economics.requiredPositiveNetRollouts,
|
|
177
|
-
projectedRolloutsUntilCompactAfterGc: evaluated.projectedRolloutsUntilCompactAfterGc,
|
|
178
|
-
projectedTokensBeforeGc: evaluated.projectedTokensBeforeGc,
|
|
179
|
-
projectedTokensAfterGc: evaluated.projectedTokensAfterGc,
|
|
180
|
-
contextGrowthSampleCount: evaluated.contextGrowthSamples.length,
|
|
181
|
-
contextGrowthP75Tokens: evaluated.contextGrowthP75Tokens,
|
|
182
|
-
expectedNetSavedCostUnits: evaluated.economics.expectedNetSavedCostUnits,
|
|
143
|
+
...(evaluated.mediaOnly
|
|
144
|
+
? {}
|
|
145
|
+
: { minimumRequiredReclaimedTokens: evaluated.minimumReclaimedTokens }),
|
|
183
146
|
minimumObservedRollouts: evaluated.minimumObservedRollouts,
|
|
184
147
|
protectedCandidateTokens: evaluated.protectedCandidateTokens,
|
|
185
148
|
eligibleCandidateCount: evaluated.eligibleCandidateCount,
|
|
186
149
|
eligibleTargetCount: replacements.reduce((count, replacement) => count +
|
|
187
150
|
1 +
|
|
188
|
-
(replacement.kind === 'TOOL_CALL_INPUT' ? replacement.providerAliasTargets.length : 0), 0)
|
|
189
|
-
cachePenaltyKnown: input.usage.cacheReadTokens !== undefined
|
|
151
|
+
(replacement.kind === 'TOOL_CALL_INPUT' ? replacement.providerAliasTargets.length : 0), 0)
|
|
190
152
|
};
|
|
191
153
|
const draft = {
|
|
192
154
|
...(inlineMedia ? { inlineMedia } : {}),
|
|
@@ -206,7 +168,6 @@ function finalizePlan(input, evaluated, includeMedia = false) {
|
|
|
206
168
|
modelId: input.modelId,
|
|
207
169
|
platform: input.platform
|
|
208
170
|
},
|
|
209
|
-
economics: CONTEXT_GC_POLICY,
|
|
210
171
|
metrics
|
|
211
172
|
};
|
|
212
173
|
return { stage: evaluated.stage, draft, metrics };
|
|
@@ -222,48 +183,23 @@ function evaluateCandidateSet(input, existing, candidates, state, mediaProjectio
|
|
|
222
183
|
return undefined;
|
|
223
184
|
const projected = applyContextGcReplacementsToMessages(input.messages, input.messageSources, replacements);
|
|
224
185
|
const projectedMessages = mediaProjection?.(projected.messages) ?? projected.messages;
|
|
225
|
-
const
|
|
186
|
+
const baseline = applyContextGcReplacementsToMessages(input.messages, input.messageSources, existing);
|
|
187
|
+
const baselineMessages = mediaProjection?.(baseline.messages) ?? baseline.messages;
|
|
188
|
+
const reclaimedTokens = contextGcReclaimedTokens(input, baselineMessages, projectedMessages);
|
|
226
189
|
const minimumReclaimedTokens = contextGcMinimumReclaimedTokens(input.contextWindowTokens);
|
|
227
190
|
if (!state.mediaOnly && reclaimedTokens < minimumReclaimedTokens)
|
|
228
191
|
return undefined;
|
|
229
|
-
const beforeTokens = contextGcPrefixTokens(input) + contextGcMessageTokens(input,
|
|
192
|
+
const beforeTokens = contextGcPrefixTokens(input) + contextGcMessageTokens(input, baselineMessages);
|
|
230
193
|
const afterTokens = contextGcPrefixTokens(input) + contextGcMessageTokens(input, projectedMessages);
|
|
231
194
|
if (!state.mediaOnly && reclaimedTokens === 0)
|
|
232
195
|
return undefined;
|
|
233
|
-
const cache = contextGcCacheImpact(input, input.messages, projectedMessages, input.usage.cacheReadTokens ?? 0, reclaimedTokens);
|
|
234
|
-
const growthSamples = collectContextGrowthSamples(input.records, input.modelId, input.contextWindowTokens, input.checkpointCache);
|
|
235
|
-
const contextGrowthP75Tokens = percentile75(growthSamples);
|
|
236
|
-
const compactThresholdTokens = Math.floor(input.contextWindowTokens * COMPACT_THRESHOLD_RATIO);
|
|
237
|
-
// Usage describes the request before its generated output and tool results.
|
|
238
|
-
const pendingMessages = input.messages.filter((_message, index) => input.messageSources[index].sequence > state.usageRecord.sequence);
|
|
239
|
-
const pendingTokens = Math.max(0, input.usage.outputTokens ?? 0) + contextGcMessageTokens(input, pendingMessages);
|
|
240
|
-
const projectedTokensBeforeGc = state.contextUsageTokens + pendingTokens;
|
|
241
|
-
const projectedTokensAfterGc = Math.max(0, projectedTokensBeforeGc - reclaimedTokens);
|
|
242
|
-
const projectedRolloutsUntilCompactAfterGc = contextGrowthP75Tokens <= 0
|
|
243
|
-
? 0
|
|
244
|
-
: Math.max(0, Math.floor((compactThresholdTokens - projectedTokensAfterGc) / Math.max(1, contextGrowthP75Tokens)));
|
|
245
|
-
const economics = evaluateContextGcEconomics({
|
|
246
|
-
contextUsageTokens: state.contextUsageTokens,
|
|
247
|
-
contextWindowTokens: input.contextWindowTokens,
|
|
248
|
-
reclaimedTokens,
|
|
249
|
-
reclaimedCachedTokens: cache.reclaimedCachedTokens,
|
|
250
|
-
cacheReadTokens: input.usage.cacheReadTokens ?? 0,
|
|
251
|
-
preservedCachedPrefixTokens: cache.preservedCachedPrefixTokens
|
|
252
|
-
});
|
|
253
196
|
return {
|
|
254
197
|
...state,
|
|
255
|
-
economics,
|
|
256
|
-
reclaimedCachedTokens: cache.reclaimedCachedTokens,
|
|
257
198
|
replacements,
|
|
258
199
|
beforeTokens,
|
|
259
200
|
afterTokens,
|
|
260
201
|
reclaimedTokens,
|
|
261
|
-
contextGrowthSamples: growthSamples,
|
|
262
|
-
contextGrowthP75Tokens,
|
|
263
202
|
minimumReclaimedTokens,
|
|
264
|
-
projectedRolloutsUntilCompactAfterGc,
|
|
265
|
-
projectedTokensBeforeGc,
|
|
266
|
-
projectedTokensAfterGc,
|
|
267
203
|
boundarySequence: input.records.at(-1)?.sequence ?? state.usageRecord.sequence,
|
|
268
204
|
minimumObservedRollouts: Math.min(...candidates.map((candidate) => candidate.observedRollouts)),
|
|
269
205
|
eligibleCandidateCount: candidates.length
|
|
@@ -286,51 +222,6 @@ function latestUsageRecord(records, selectedModelId) {
|
|
|
286
222
|
}
|
|
287
223
|
return latest;
|
|
288
224
|
}
|
|
289
|
-
function collectContextGrowthSamples(records, modelId, contextWindowTokens, checkpointCache) {
|
|
290
|
-
const previous = checkpointCache.read(records);
|
|
291
|
-
const base = contextGcBaseFromRecords(records);
|
|
292
|
-
const startSequence = previous?.record.sequence ?? (base?.type === 'COMPACT' ? base.sequence : base?.firstSequence);
|
|
293
|
-
let activeModelId = modelId;
|
|
294
|
-
let activeWindow = contextWindowTokens;
|
|
295
|
-
let previousUsage;
|
|
296
|
-
const samples = [];
|
|
297
|
-
for (const record of records) {
|
|
298
|
-
if (startSequence !== undefined && record.sequence <= startSequence)
|
|
299
|
-
continue;
|
|
300
|
-
if (record.type === 'execution.header' && isRecord(record.payload)) {
|
|
301
|
-
const nextModel = modelIdFromRecord(record);
|
|
302
|
-
const nextWindow = record.payload.contextWindowTokens;
|
|
303
|
-
if (nextModel !== modelId ||
|
|
304
|
-
(typeof nextWindow === 'number' && nextWindow !== contextWindowTokens)) {
|
|
305
|
-
activeModelId = nextModel ?? activeModelId;
|
|
306
|
-
activeWindow =
|
|
307
|
-
typeof nextWindow === 'number' && Number.isSafeInteger(nextWindow)
|
|
308
|
-
? nextWindow
|
|
309
|
-
: activeWindow;
|
|
310
|
-
previousUsage = undefined;
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
activeModelId = modelId;
|
|
314
|
-
activeWindow = contextWindowTokens;
|
|
315
|
-
}
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (activeModelId !== modelId ||
|
|
319
|
-
activeWindow !== contextWindowTokens ||
|
|
320
|
-
!isRecord(record.payload) ||
|
|
321
|
-
record.payload.type !== 'usage.updated')
|
|
322
|
-
continue;
|
|
323
|
-
const usage = modelContextUsage(record.payload);
|
|
324
|
-
if (usage === undefined) {
|
|
325
|
-
previousUsage = undefined;
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
if (previousUsage !== undefined && usage > previousUsage)
|
|
329
|
-
samples.push(usage - previousUsage);
|
|
330
|
-
previousUsage = usage;
|
|
331
|
-
}
|
|
332
|
-
return samples.slice(-CONTEXT_GC_POLICY.contextGrowthSampleWindow);
|
|
333
|
-
}
|
|
334
225
|
/** Measure only affected call families, retaining native aliases for provider lowering. */
|
|
335
226
|
function createCandidateSavingsEstimator(input, existing) {
|
|
336
227
|
const byCall = new Map();
|
|
@@ -377,9 +268,3 @@ function modelIdFromRecord(record) {
|
|
|
377
268
|
return undefined;
|
|
378
269
|
return typeof record.payload.modelId === 'string' ? record.payload.modelId : undefined;
|
|
379
270
|
}
|
|
380
|
-
function percentile75(values) {
|
|
381
|
-
if (values.length === 0)
|
|
382
|
-
return 0;
|
|
383
|
-
const sorted = [...values].sort((left, right) => left - right);
|
|
384
|
-
return sorted[Math.max(0, Math.ceil(sorted.length * 0.75) - 1)];
|
|
385
|
-
}
|
|
@@ -23,6 +23,7 @@ export declare class ExecutionContextCoordinator {
|
|
|
23
23
|
readonly requests: Pick<CompactRequests, 'drain'>;
|
|
24
24
|
readonly signal: AbortSignal;
|
|
25
25
|
readonly window: number;
|
|
26
|
+
readonly gcEnabled?: boolean;
|
|
26
27
|
readonly gc: Pick<ContextGcRuntime, 'run'>;
|
|
27
28
|
readonly gcBase: Omit<ContextGcRuntimeInput, 'system' | 'tools' | 'usage' | 'inlineMedia' | 'mediaView' | 'getActiveProcessIds'>;
|
|
28
29
|
start(input?: {
|
|
@@ -80,10 +80,12 @@ export class ExecutionContextCoordinator {
|
|
|
80
80
|
this.ports.signal.throwIfAborted();
|
|
81
81
|
this.ports.execution.assertContextValid();
|
|
82
82
|
});
|
|
83
|
-
if (!this.ports.compact.pending)
|
|
83
|
+
if (this.ports.gcEnabled !== false && !this.ports.compact.pending)
|
|
84
84
|
await this.gc(round, usage, await this.ports.resources.gcCandidate(), true);
|
|
85
85
|
}
|
|
86
86
|
async gc(round, usage, media, includeProcesses = false) {
|
|
87
|
+
if (this.ports.gcEnabled === false || this.ports.compact.pending)
|
|
88
|
+
return false;
|
|
87
89
|
const appendBoundary = this.ports.execution.appendBoundary;
|
|
88
90
|
const committed = await this.ports.gc.run({
|
|
89
91
|
...this.ports.gcBase,
|
|
@@ -571,6 +571,7 @@ export function createSessionExecutionFactory(ports) {
|
|
|
571
571
|
requests: compactRequests,
|
|
572
572
|
signal: controller.signal,
|
|
573
573
|
window: selectedModel.contextWindowTokens,
|
|
574
|
+
gcEnabled: selectedModel.gcEnabled ?? true,
|
|
574
575
|
gc: contextGc,
|
|
575
576
|
gcBase: {
|
|
576
577
|
store,
|
|
@@ -724,7 +725,9 @@ export function createSessionExecutionFactory(ports) {
|
|
|
724
725
|
},
|
|
725
726
|
prepareResources: async ({ candidateRequest, request, toolScope }) => {
|
|
726
727
|
resourceInput.observe(candidateRequest.messages);
|
|
727
|
-
const media =
|
|
728
|
+
const media = selectedModel.gcEnabled === false
|
|
729
|
+
? undefined
|
|
730
|
+
: await resourceInput.gcCandidate(request, candidateRequest.images);
|
|
728
731
|
if (media?.reason === 'HARD_LIMIT') {
|
|
729
732
|
const committed = await contextCoordinator.gc({ request, toolScope }, modelLifecycle.usage, media);
|
|
730
733
|
if (committed)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionRecord } from './record-contract.js';
|
|
2
|
-
import type { ContextGcPlatform, ContextGcBase, ContextGcTarget, ContextGcReplacement,
|
|
2
|
+
import type { ContextGcPlatform, ContextGcBase, ContextGcTarget, ContextGcReplacement, ContextGcCheckpointPayload } from './context-gc-contract.js';
|
|
3
3
|
export type { ContextGcPlatform, ContextGcEconomicsPolicy, ContextGcBase, ContextGcTarget, ContextGcReadItemReplacement, ContextGcReplacement, ContextGcMetrics, ContextGcCheckpointDraft, ContextGcCheckpointPayload } from './context-gc-contract.js';
|
|
4
4
|
export declare const CONTEXT_GC_CHECKPOINT_VERSION: 1;
|
|
5
5
|
export declare const CONTEXT_GC_MAX_TARGETS = 4096;
|
|
@@ -8,9 +8,6 @@ export declare class ContextGcCheckpointCache {
|
|
|
8
8
|
#private;
|
|
9
9
|
read(records: readonly SessionRecord[], boundarySequence?: number): ReturnType<typeof latestValidContextGcCheckpoint>;
|
|
10
10
|
}
|
|
11
|
-
export declare function contextGcMinimumUsageGrowth(contextWindowTokens: number): number;
|
|
12
|
-
/** Compare adjacent checkpoint triggers only within the same model/window scope. */
|
|
13
|
-
export declare function contextGcCheckpointIntervalEligible(trigger: ContextGcCheckpointDraft['trigger'], previous?: ContextGcCheckpointDraft['trigger']): boolean;
|
|
14
11
|
export declare function contextGcLogicalPathKey(path: string, workspace: string, platform?: ContextGcPlatform): string | undefined;
|
|
15
12
|
export declare function contextGcTargetKey(target: ContextGcTarget): string;
|
|
16
13
|
export declare function contextGcReplacementKey(replacement: ContextGcReplacement): string;
|
|
@@ -22,19 +22,6 @@ export class ContextGcCheckpointCache {
|
|
|
22
22
|
return value;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
export function contextGcMinimumUsageGrowth(contextWindowTokens) {
|
|
26
|
-
return Math.max(2_000, Math.floor(contextWindowTokens * 0.05));
|
|
27
|
-
}
|
|
28
|
-
/** Compare adjacent checkpoint triggers only within the same model/window scope. */
|
|
29
|
-
export function contextGcCheckpointIntervalEligible(trigger, previous) {
|
|
30
|
-
if (!previous ||
|
|
31
|
-
trigger.modelId !== previous.modelId ||
|
|
32
|
-
trigger.contextWindowTokens !== previous.contextWindowTokens)
|
|
33
|
-
return true;
|
|
34
|
-
return (trigger.normalRolloutOrdinal - previous.normalRolloutOrdinal >= 2 &&
|
|
35
|
-
trigger.contextInputTokens - previous.contextInputTokens >=
|
|
36
|
-
contextGcMinimumUsageGrowth(trigger.contextWindowTokens));
|
|
37
|
-
}
|
|
38
25
|
export function contextGcLogicalPathKey(path, workspace, platform) {
|
|
39
26
|
if (!path || !workspace || path.includes('\0') || workspace.includes('\0'))
|
|
40
27
|
return undefined;
|
|
@@ -126,7 +113,7 @@ export function parseContextGcCheckpoint(record) {
|
|
|
126
113
|
!Array.isArray(payload.replacements) ||
|
|
127
114
|
(payload.replacements.length === 0 && payload.inlineMedia === undefined) ||
|
|
128
115
|
!isContextGcTrigger(payload.trigger) ||
|
|
129
|
-
!isContextGcEconomicsPolicy(payload.economics) ||
|
|
116
|
+
(payload.economics !== undefined && !isContextGcEconomicsPolicy(payload.economics)) ||
|
|
130
117
|
!isContextGcMetrics(payload.metrics) ||
|
|
131
118
|
(payload.inlineMedia !== undefined && !parseInlineMediaGc(payload.inlineMedia)))
|
|
132
119
|
return undefined;
|
|
@@ -141,7 +128,7 @@ export function parseContextGcCheckpoint(record) {
|
|
|
141
128
|
boundarySequence: payload.boundarySequence,
|
|
142
129
|
replacements,
|
|
143
130
|
trigger: structuredClone(payload.trigger),
|
|
144
|
-
economics: structuredClone(payload.economics),
|
|
131
|
+
...(payload.economics === undefined ? {} : { economics: structuredClone(payload.economics) }),
|
|
145
132
|
metrics: structuredClone(payload.metrics),
|
|
146
133
|
...(payload.inlineMedia === undefined
|
|
147
134
|
? {}
|
|
@@ -1129,17 +1116,17 @@ function isContextGcMetrics(value) {
|
|
|
1129
1116
|
'beforeTokens',
|
|
1130
1117
|
'afterTokens',
|
|
1131
1118
|
'reclaimedTokens',
|
|
1132
|
-
'grossReductionRate',
|
|
1133
|
-
'cacheRebuildPenaltyCostUnits',
|
|
1134
|
-
'projectedRolloutsUntilCompactAfterGc',
|
|
1135
|
-
'contextGrowthSampleCount',
|
|
1136
|
-
'contextGrowthP75Tokens',
|
|
1137
1119
|
'minimumObservedRollouts',
|
|
1138
1120
|
'protectedCandidateTokens',
|
|
1139
1121
|
'eligibleCandidateCount',
|
|
1140
1122
|
'eligibleTargetCount'
|
|
1141
1123
|
];
|
|
1142
1124
|
const optionalNonnegative = [
|
|
1125
|
+
'grossReductionRate',
|
|
1126
|
+
'cacheRebuildPenaltyCostUnits',
|
|
1127
|
+
'projectedRolloutsUntilCompactAfterGc',
|
|
1128
|
+
'contextGrowthSampleCount',
|
|
1129
|
+
'contextGrowthP75Tokens',
|
|
1143
1130
|
'effectiveMinimumGrossReductionRate',
|
|
1144
1131
|
'compactDeferralCreditCostUnits',
|
|
1145
1132
|
'minimumRequiredReclaimedTokens',
|
|
@@ -1150,11 +1137,12 @@ function isContextGcMetrics(value) {
|
|
|
1150
1137
|
];
|
|
1151
1138
|
return (nonnegative.every((key) => isNonnegativeFinite(value[key])) &&
|
|
1152
1139
|
[
|
|
1153
|
-
'requiredPositiveNetRollouts',
|
|
1140
|
+
...(value.requiredPositiveNetRollouts === undefined ? [] : ['requiredPositiveNetRollouts']),
|
|
1154
1141
|
...(value.requiredTargetRoiRollouts === undefined ? [] : ['requiredTargetRoiRollouts'])
|
|
1155
1142
|
].every((key) => isNonnegativeFinite(value[key]) || (value.stage === 'INLINE_MEDIA' && value[key] === null)) &&
|
|
1156
1143
|
optionalNonnegative.every((key) => value[key] === undefined || isNonnegativeFinite(value[key])) &&
|
|
1157
|
-
|
|
1144
|
+
(value.expectedNetSavedCostUnits === undefined ||
|
|
1145
|
+
Number.isFinite(value.expectedNetSavedCostUnits)) &&
|
|
1158
1146
|
(value.cacheInvestmentRoi === undefined || Number.isFinite(value.cacheInvestmentRoi)) &&
|
|
1159
1147
|
(value.tokenEstimator === undefined || value.tokenEstimator === 'UTF8_BYTES_V1') &&
|
|
1160
1148
|
(value.cachePenaltyKnown === undefined || typeof value.cachePenaltyKnown === 'boolean'));
|
|
@@ -105,21 +105,21 @@ export interface ContextGcMetrics {
|
|
|
105
105
|
readonly beforeTokens: number;
|
|
106
106
|
readonly afterTokens: number;
|
|
107
107
|
readonly reclaimedTokens: number;
|
|
108
|
-
readonly grossReductionRate
|
|
108
|
+
readonly grossReductionRate?: number;
|
|
109
109
|
readonly effectiveMinimumGrossReductionRate?: number;
|
|
110
110
|
readonly minimumRequiredReclaimedTokens?: number;
|
|
111
|
-
readonly cacheRebuildPenaltyCostUnits
|
|
111
|
+
readonly cacheRebuildPenaltyCostUnits?: number;
|
|
112
112
|
readonly compactDeferralCreditCostUnits?: number;
|
|
113
113
|
readonly imminentCompactAvoidanceCreditCostUnits?: number;
|
|
114
114
|
readonly appliedCompactCreditCostUnits?: number;
|
|
115
|
-
readonly requiredPositiveNetRollouts
|
|
115
|
+
readonly requiredPositiveNetRollouts?: number | null;
|
|
116
116
|
readonly requiredTargetRoiRollouts?: number | null;
|
|
117
|
-
readonly projectedRolloutsUntilCompactAfterGc
|
|
117
|
+
readonly projectedRolloutsUntilCompactAfterGc?: number;
|
|
118
118
|
readonly projectedTokensBeforeGc?: number;
|
|
119
119
|
readonly projectedTokensAfterGc?: number;
|
|
120
|
-
readonly contextGrowthSampleCount
|
|
121
|
-
readonly contextGrowthP75Tokens
|
|
122
|
-
readonly expectedNetSavedCostUnits
|
|
120
|
+
readonly contextGrowthSampleCount?: number;
|
|
121
|
+
readonly contextGrowthP75Tokens?: number;
|
|
122
|
+
readonly expectedNetSavedCostUnits?: number;
|
|
123
123
|
readonly cacheInvestmentRoi?: number;
|
|
124
124
|
readonly minimumObservedRollouts: number;
|
|
125
125
|
readonly protectedCandidateTokens: number;
|
|
@@ -141,7 +141,7 @@ export interface ContextGcCheckpointDraft {
|
|
|
141
141
|
readonly modelId: string;
|
|
142
142
|
readonly platform: ContextGcPlatform;
|
|
143
143
|
};
|
|
144
|
-
readonly economics
|
|
144
|
+
readonly economics?: ContextGcEconomicsPolicy;
|
|
145
145
|
readonly metrics: ContextGcMetrics;
|
|
146
146
|
}
|
|
147
147
|
export interface ContextGcCheckpointPayload extends ContextGcCheckpointDraft {
|
|
@@ -9,7 +9,7 @@ import { cloneCoreJson } from '../../extension-checkpoint.js';
|
|
|
9
9
|
import { acquireFileLock } from '../../../storage/file-lock.js';
|
|
10
10
|
import { todoItemsFromSnapshot } from '../../todo.js';
|
|
11
11
|
import { applyCodeModeStateMutations, parseCodeModeStateMutations, restoreCodeModeState } from '../../code-state.js';
|
|
12
|
-
import { contextGcBaseFromRecords,
|
|
12
|
+
import { contextGcBaseFromRecords, latestValidContextGcCheckpoint, normalizeContextGcReplacements, sameContextGcBase, validateContextGcCheckpoint } from '../../context-gc-checkpoint.js';
|
|
13
13
|
import { recordToolResources, readToolResultResource, writeToolResultResource } from '../../tool-result-resources.js';
|
|
14
14
|
import { beginTreeDeleteTransaction, finishTreeDelete, finishTreeDeletePreparation, readTreeDeleteTransaction, recoverSessionStorageTransaction, stageTreeDelete } from './transaction-recovery.js';
|
|
15
15
|
import { extensionCheckpointFromPayload, indexExtensionCheckpoint, restoreExtensionCheckpoint, updateSessionCurrentState } from '../current-state.js';
|
|
@@ -1293,9 +1293,6 @@ export class JsonlSessionStore {
|
|
|
1293
1293
|
!sameContextGcBase(contextGcBaseFromRecords(records, boundarySequence), draft.base))
|
|
1294
1294
|
throw new AgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC planning boundary is invalid.');
|
|
1295
1295
|
const previous = latestValidContextGcCheckpoint(records, boundarySequence);
|
|
1296
|
-
if ((!draft.inlineMedia || draft.inlineMedia.reason === 'TEXT_GC') &&
|
|
1297
|
-
!contextGcCheckpointIntervalEligible(draft.trigger, previous?.payload.trigger))
|
|
1298
|
-
throw new AgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint interval validation failed.');
|
|
1299
1296
|
const plannedReplacements = normalizeContextGcReplacements(draft.replacements);
|
|
1300
1297
|
if (!plannedReplacements || (plannedReplacements.length === 0 && !draft.inlineMedia))
|
|
1301
1298
|
throw new AgentError('MAR_AGENT_SESSION_CORRUPT', 'Context GC checkpoint must contain a replacement.');
|
package/package.json
CHANGED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { ModelUsage } from '../../model/contracts.js';
|
|
2
|
-
export declare const CONTEXT_GC_POLICY: {
|
|
3
|
-
version: 1;
|
|
4
|
-
minimumPaybackRollouts: number;
|
|
5
|
-
maximumPaybackRollouts: number;
|
|
6
|
-
contextGrowthSampleWindow: number;
|
|
7
|
-
minimumContextGrowthSamples: number;
|
|
8
|
-
minimumObservationRollouts: number;
|
|
9
|
-
stageAProtectionRollouts: number;
|
|
10
|
-
largeExecProtectionRollouts: number;
|
|
11
|
-
normalInputCostRate: number;
|
|
12
|
-
cacheReadCostRate: number;
|
|
13
|
-
};
|
|
14
|
-
export declare const COMPACT_THRESHOLD_RATIO: number;
|
|
15
|
-
interface ContextGcEconomicsInput {
|
|
16
|
-
readonly contextUsageTokens: number;
|
|
17
|
-
readonly contextWindowTokens: number;
|
|
18
|
-
readonly reclaimedTokens: number;
|
|
19
|
-
readonly reclaimedCachedTokens: number;
|
|
20
|
-
readonly cacheReadTokens: number;
|
|
21
|
-
readonly preservedCachedPrefixTokens: number;
|
|
22
|
-
}
|
|
23
|
-
export interface ContextGcEconomicsResult {
|
|
24
|
-
readonly cacheRebuildPenaltyCostUnits: number;
|
|
25
|
-
readonly requiredPositiveNetRollouts: number | null;
|
|
26
|
-
readonly expectedNetSavedCostUnits: number;
|
|
27
|
-
readonly grossReductionRate: number;
|
|
28
|
-
readonly passes: boolean;
|
|
29
|
-
}
|
|
30
|
-
export declare function evaluateContextGcEconomics(input: ContextGcEconomicsInput): ContextGcEconomicsResult;
|
|
31
|
-
export declare function contextGcMinimumReclaimedTokens(contextWindowTokens: number): number;
|
|
32
|
-
export declare function contextGcEligibleUsage(usage: ModelUsage, contextWindowTokens: number): number | undefined;
|
|
33
|
-
export declare function modelContextUsage(value: ModelUsage | Record<string, unknown>): number | undefined;
|
|
34
|
-
export declare function clamp(value: number, minimum: number, maximum: number): number;
|
|
35
|
-
export {};
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { AGENT_EXECUTION_POLICY } from '../execution-policy.js';
|
|
2
|
-
import { CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS } from '../../session/context-gc-protection.js';
|
|
3
|
-
export const CONTEXT_GC_POLICY = {
|
|
4
|
-
version: 1,
|
|
5
|
-
minimumPaybackRollouts: 30,
|
|
6
|
-
maximumPaybackRollouts: 60,
|
|
7
|
-
contextGrowthSampleWindow: 16,
|
|
8
|
-
minimumContextGrowthSamples: 6,
|
|
9
|
-
minimumObservationRollouts: 1,
|
|
10
|
-
stageAProtectionRollouts: CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS,
|
|
11
|
-
largeExecProtectionRollouts: CONTEXT_GC_TEXT_PROTECTION_ROLLOUTS,
|
|
12
|
-
normalInputCostRate: 1,
|
|
13
|
-
cacheReadCostRate: 0.05
|
|
14
|
-
};
|
|
15
|
-
const CONTEXT_GC_START_RATIO = 0.5;
|
|
16
|
-
export const COMPACT_THRESHOLD_RATIO = AGENT_EXECUTION_POLICY.backgroundCompactionPercent / 100;
|
|
17
|
-
export function evaluateContextGcEconomics(input) {
|
|
18
|
-
const reclaimedTokens = Math.max(0, input.reclaimedTokens);
|
|
19
|
-
const cacheReadTokens = Math.max(0, input.cacheReadTokens);
|
|
20
|
-
const preservedCachedPrefixTokens = clamp(input.preservedCachedPrefixTokens, 0, cacheReadTokens);
|
|
21
|
-
const invalidatedCachedSuffixTokens = cacheReadTokens - preservedCachedPrefixTokens;
|
|
22
|
-
const reclaimedCachedTokens = clamp(input.reclaimedCachedTokens, 0, invalidatedCachedSuffixTokens);
|
|
23
|
-
const invalidatedRetainedCachedTokens = Math.max(0, invalidatedCachedSuffixTokens - reclaimedCachedTokens);
|
|
24
|
-
// Calculate token-derived costs in cache-read units to keep exact zero exact.
|
|
25
|
-
const unitsPerNormalInput = CONTEXT_GC_POLICY.normalInputCostRate / CONTEXT_GC_POLICY.cacheReadCostRate;
|
|
26
|
-
const cacheRebuildPenalty = invalidatedRetainedCachedTokens * (unitsPerNormalInput - 1);
|
|
27
|
-
const cacheRebuildPenaltyCostUnits = cacheRebuildPenalty * CONTEXT_GC_POLICY.cacheReadCostRate;
|
|
28
|
-
const reclaimedUncachedTokens = Math.max(0, reclaimedTokens - reclaimedCachedTokens);
|
|
29
|
-
const firstRolloutSavings = reclaimedCachedTokens + reclaimedUncachedTokens * unitsPerNormalInput;
|
|
30
|
-
const steadyRolloutSavings = reclaimedTokens;
|
|
31
|
-
const netAt = (rollouts) => rollouts <= 0
|
|
32
|
-
? -cacheRebuildPenalty
|
|
33
|
-
: firstRolloutSavings +
|
|
34
|
-
Math.max(0, rollouts - 1) * steadyRolloutSavings -
|
|
35
|
-
cacheRebuildPenalty;
|
|
36
|
-
let requiredPositiveNetRollouts;
|
|
37
|
-
if (netAt(1) > 0)
|
|
38
|
-
requiredPositiveNetRollouts = 1;
|
|
39
|
-
else if (steadyRolloutSavings <= 0)
|
|
40
|
-
requiredPositiveNetRollouts = null;
|
|
41
|
-
else {
|
|
42
|
-
requiredPositiveNetRollouts = Math.max(2, Math.floor(-netAt(1) / steadyRolloutSavings) + 2);
|
|
43
|
-
if (netAt(requiredPositiveNetRollouts - 1) > 0)
|
|
44
|
-
requiredPositiveNetRollouts--;
|
|
45
|
-
else if (netAt(requiredPositiveNetRollouts) <= 0)
|
|
46
|
-
requiredPositiveNetRollouts++;
|
|
47
|
-
}
|
|
48
|
-
const expectedNetSavedCostUnits = netAt(requiredPositiveNetRollouts ?? 0) * CONTEXT_GC_POLICY.cacheReadCostRate;
|
|
49
|
-
const grossReductionRate = reclaimedTokens / Math.max(1, Math.max(0, input.contextUsageTokens));
|
|
50
|
-
const minimumReclaimedTokens = contextGcMinimumReclaimedTokens(input.contextWindowTokens);
|
|
51
|
-
const usageRatio = input.contextUsageTokens / input.contextWindowTokens;
|
|
52
|
-
const progress = clamp((usageRatio - CONTEXT_GC_START_RATIO) / (COMPACT_THRESHOLD_RATIO - CONTEXT_GC_START_RATIO), 0, 1);
|
|
53
|
-
const paybackWindow = CONTEXT_GC_POLICY.minimumPaybackRollouts +
|
|
54
|
-
progress *
|
|
55
|
-
(CONTEXT_GC_POLICY.maximumPaybackRollouts - CONTEXT_GC_POLICY.minimumPaybackRollouts);
|
|
56
|
-
const withinWindow = requiredPositiveNetRollouts !== null && requiredPositiveNetRollouts <= paybackWindow;
|
|
57
|
-
const passes = reclaimedTokens > 0 &&
|
|
58
|
-
reclaimedTokens >= minimumReclaimedTokens &&
|
|
59
|
-
Number.isFinite(usageRatio) &&
|
|
60
|
-
usageRatio >= CONTEXT_GC_START_RATIO &&
|
|
61
|
-
usageRatio < COMPACT_THRESHOLD_RATIO &&
|
|
62
|
-
withinWindow;
|
|
63
|
-
return {
|
|
64
|
-
cacheRebuildPenaltyCostUnits,
|
|
65
|
-
requiredPositiveNetRollouts,
|
|
66
|
-
expectedNetSavedCostUnits,
|
|
67
|
-
grossReductionRate,
|
|
68
|
-
passes
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
export function contextGcMinimumReclaimedTokens(contextWindowTokens) {
|
|
72
|
-
return Math.ceil(contextWindowTokens / 20);
|
|
73
|
-
}
|
|
74
|
-
export function contextGcEligibleUsage(usage, contextWindowTokens) {
|
|
75
|
-
const contextUsageTokens = modelContextUsage(usage);
|
|
76
|
-
if (contextUsageTokens === undefined ||
|
|
77
|
-
!Number.isSafeInteger(contextWindowTokens) ||
|
|
78
|
-
contextWindowTokens <= 0)
|
|
79
|
-
return undefined;
|
|
80
|
-
const usageRatio = contextUsageTokens / contextWindowTokens;
|
|
81
|
-
if (usageRatio < CONTEXT_GC_START_RATIO || usageRatio >= COMPACT_THRESHOLD_RATIO)
|
|
82
|
-
return undefined;
|
|
83
|
-
return contextUsageTokens;
|
|
84
|
-
}
|
|
85
|
-
export function modelContextUsage(value) {
|
|
86
|
-
const context = value.contextInputTokens ?? value.inputTokens;
|
|
87
|
-
return typeof context === 'number' && Number.isSafeInteger(context) && context >= 0
|
|
88
|
-
? context
|
|
89
|
-
: undefined;
|
|
90
|
-
}
|
|
91
|
-
export function clamp(value, minimum, maximum) {
|
|
92
|
-
return Math.min(maximum, Math.max(minimum, value));
|
|
93
|
-
}
|