@gaunt-sloth/core 2.0.0-beta.4 → 2.0.0-beta.6
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/config/schema.d.ts +4 -0
- package/dist/config/schema.js +64 -2
- package/dist/config/schema.js.map +1 -1
- package/dist/config/tokenBudget.d.ts +88 -0
- package/dist/config/tokenBudget.js +155 -0
- package/dist/config/tokenBudget.js.map +1 -0
- package/dist/config/types.d.ts +33 -4
- package/dist/config/types.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/core/GthAbstractAgent.d.ts +24 -0
- package/dist/core/GthAbstractAgent.js +37 -1
- package/dist/core/GthAbstractAgent.js.map +1 -1
- package/dist/core/GthAgentRunner.d.ts +216 -1
- package/dist/core/GthAgentRunner.js +424 -3
- package/dist/core/GthAgentRunner.js.map +1 -1
- package/dist/core/GthLangChainAgent.d.ts +196 -0
- package/dist/core/GthLangChainAgent.js +392 -2
- package/dist/core/GthLangChainAgent.js.map +1 -1
- package/dist/core/approvals/approvalRequest.d.ts +142 -0
- package/dist/core/approvals/approvalRequest.js +198 -3
- package/dist/core/approvals/approvalRequest.js.map +1 -1
- package/dist/core/approvals/conversationGrants.d.ts +60 -0
- package/dist/core/approvals/conversationGrants.js +77 -0
- package/dist/core/approvals/conversationGrants.js.map +1 -0
- package/dist/core/approvals/grants.d.ts +16 -0
- package/dist/core/approvals/grants.js +20 -5
- package/dist/core/approvals/grants.js.map +1 -1
- package/dist/core/compaction.d.ts +181 -0
- package/dist/core/compaction.js +293 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/compactionThreshold.d.ts +158 -0
- package/dist/core/compactionThreshold.js +183 -0
- package/dist/core/compactionThreshold.js.map +1 -0
- package/dist/core/contextWindow.d.ts +146 -0
- package/dist/core/contextWindow.js +256 -0
- package/dist/core/contextWindow.js.map +1 -0
- package/dist/core/exitOutputChannel.d.ts +51 -0
- package/dist/core/exitOutputChannel.js +65 -0
- package/dist/core/exitOutputChannel.js.map +1 -0
- package/dist/core/refusal.d.ts +17 -2
- package/dist/core/refusal.js +80 -14
- package/dist/core/refusal.js.map +1 -1
- package/dist/core/runStats.d.ts +1 -1
- package/dist/core/terminationNotice.d.ts +8 -0
- package/dist/core/terminationNotice.js +10 -4
- package/dist/core/terminationNotice.js.map +1 -1
- package/dist/core/terminationReason.d.ts +28 -0
- package/dist/core/terminationReason.js +27 -0
- package/dist/core/terminationReason.js.map +1 -1
- package/dist/core/types.d.ts +35 -1
- package/dist/core/types.js.map +1 -1
- package/dist/history/checkpointRetention.d.ts +279 -0
- package/dist/history/checkpointRetention.js +567 -0
- package/dist/history/checkpointRetention.js.map +1 -0
- package/dist/history/checkpointSaver.d.ts +93 -0
- package/dist/history/checkpointSaver.js +464 -0
- package/dist/history/checkpointSaver.js.map +1 -0
- package/dist/history/historyEnabled.d.ts +27 -0
- package/dist/history/historyEnabled.js +23 -0
- package/dist/history/historyEnabled.js.map +1 -0
- package/dist/history/historyFormat.d.ts +27 -0
- package/dist/history/historyFormat.js +125 -2
- package/dist/history/historyFormat.js.map +1 -1
- package/dist/history/historyStore.d.ts +61 -0
- package/dist/history/historyStore.js +180 -7
- package/dist/history/historyStore.js.map +1 -1
- package/dist/history/recordSession.d.ts +84 -22
- package/dist/history/recordSession.js +187 -12
- package/dist/history/recordSession.js.map +1 -1
- package/dist/history/sessionCheckpointer.d.ts +48 -0
- package/dist/history/sessionCheckpointer.js +200 -0
- package/dist/history/sessionCheckpointer.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/modelCatalog.d.ts +14 -0
- package/dist/providers/modelCatalog.js +4 -0
- package/dist/providers/modelCatalog.js.map +1 -1
- package/dist/providers/modelDiscovery.d.ts +3 -1
- package/dist/providers/modelDiscovery.js +22 -8
- package/dist/providers/modelDiscovery.js.map +1 -1
- package/dist/providers/ollama.js +3 -19
- package/dist/providers/ollama.js.map +1 -1
- package/dist/runtime/conversation.js +7 -1
- package/dist/runtime/conversation.js.map +1 -1
- package/dist/runtime/singleShot.js +6 -1
- package/dist/runtime/singleShot.js.map +1 -1
- package/dist/utils/consoleUtils.d.ts +77 -0
- package/dist/utils/consoleUtils.js +81 -0
- package/dist/utils/consoleUtils.js.map +1 -1
- package/package.json +2 -2
- package/schema/gsloth-config.schema.json +33 -14
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* EXT-161 — **the preventive compaction threshold: one number, one place it is decided.**
|
|
4
|
+
*
|
|
5
|
+
* `contextWindow.ts` answers "how big is the window". This module answers the question that
|
|
6
|
+
* actually gates a compaction: **at what prompt size do we fold the conversation, and who said
|
|
7
|
+
* so.** Three things can say so, in this order:
|
|
8
|
+
*
|
|
9
|
+
* 1. **the running session** — `/autocompact 300K`, which wins for the rest of the session;
|
|
10
|
+
* 2. **the user's config** — the `autocompact` key;
|
|
11
|
+
* 3. **the default** — derived from the resolved window by the guard, which is the only place that
|
|
12
|
+
* knows how many tokens are being held back for the answer.
|
|
13
|
+
*
|
|
14
|
+
* **Compaction is ON BY DEFAULT.** That is a ruling, and it is a deliberate exception to the rule
|
|
15
|
+
* that a default-on middleware must not touch `state.messages` — compaction changes what the model
|
|
16
|
+
* sees by construction, which is the whole feature. The exception is paid for the way the rule
|
|
17
|
+
* demands: the shape of the history a compaction leaves behind is pinned per provider by
|
|
18
|
+
* `compaction.ts`'s invariants (a)–(d), a compaction announces itself in the transcript, the
|
|
19
|
+
* resolved number and its provenance are readable with `/status`, and the off switch is one key —
|
|
20
|
+
* `autocompact: false`.
|
|
21
|
+
*
|
|
22
|
+
* **Why the provenance is carried rather than recomputed.** A threshold that is wrong is diagnosed
|
|
23
|
+
* by knowing where the number came from; a `/status` that reports a models.dev-derived number after
|
|
24
|
+
* a human typed `/autocompact 50000` would send the next diagnosis to the wrong place entirely. So
|
|
25
|
+
* a session override re-labels the provenance, and {@link AutocompactController} is the single
|
|
26
|
+
* object both the guard and `/status` read, over one memoised window resolution.
|
|
27
|
+
*/
|
|
28
|
+
import { type TokenBudget } from '#src/config/tokenBudget.js';
|
|
29
|
+
import type { ContextWindowOrigin, ResolvedContextWindow } from '#src/core/contextWindow.js';
|
|
30
|
+
/**
|
|
31
|
+
* The share of a model's context window that `gth init` seeds as an explicit threshold.
|
|
32
|
+
*
|
|
33
|
+
* Init writes an **absolute number** derived from this rather than the percentage itself, because
|
|
34
|
+
* the point of seeding is that the user opens their config and sees the number that will actually
|
|
35
|
+
* be enforced. A percentage would leave them one lookup away from it.
|
|
36
|
+
*
|
|
37
|
+
* 0.8 leaves a fifth of the window for the answer and the tool round that follows it — comfortably
|
|
38
|
+
* more than the flat answer reserve on any real cloud window, which is what makes the seeded number
|
|
39
|
+
* the binding one rather than a decoration.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DEFAULT_AUTOCOMPACT_SEED_FRACTION = 0.8;
|
|
42
|
+
/**
|
|
43
|
+
* The `autocompact` config value, in every form the key accepts.
|
|
44
|
+
*
|
|
45
|
+
* `false` disables it; `true` (and an absent key) is on with the derived default; a bare count or
|
|
46
|
+
* suffixed string is on with that threshold; the object form spells both out. The shorthand union
|
|
47
|
+
* mirrors `toolLoopGuard`, which is the shape a reader of this config already knows.
|
|
48
|
+
*/
|
|
49
|
+
export type AutocompactConfig = boolean | number | string | {
|
|
50
|
+
enabled?: boolean;
|
|
51
|
+
threshold?: number | string;
|
|
52
|
+
};
|
|
53
|
+
/** The `autocompact` key as the read site sees it, after defaulting and parsing. */
|
|
54
|
+
export interface ResolvedAutocompactConfig {
|
|
55
|
+
/** Whether preventive compaction may fire at all. On by default — ruled. */
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
/** The configured budget, or `null` when the user named none and the default applies. */
|
|
58
|
+
budget: TokenBudget | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the `autocompact` config key.
|
|
62
|
+
*
|
|
63
|
+
* Defaulting happens **here, at the read site**, not in `DEFAULT_CONFIG`, so an absent key stays
|
|
64
|
+
* absent in the effective-config snapshot and the snapshot does not churn — the same placement
|
|
65
|
+
* `injectModelContext` and `toolLoopGuard` use.
|
|
66
|
+
*
|
|
67
|
+
* A malformed threshold raises the `TokenBudgetError` from the shared parser, which config
|
|
68
|
+
* validation turns into a field-scoped issue naming the offending text. It must never resolve to a
|
|
69
|
+
* number: `NaN`, `0` and 4097 are all thresholds the user cannot see and did not choose.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveAutocompactConfig(raw: unknown): ResolvedAutocompactConfig;
|
|
72
|
+
/** Where the threshold number in force came from — what `/status` names. */
|
|
73
|
+
export type AutocompactThresholdOrigin =
|
|
74
|
+
/** A `/autocompact` typed in this session; outranks the config for the rest of it. */
|
|
75
|
+
'session'
|
|
76
|
+
/** The `autocompact` key in the user's config. */
|
|
77
|
+
| 'config'
|
|
78
|
+
/** Derived from the resolved window, holding back room for the answer. */
|
|
79
|
+
| 'default'
|
|
80
|
+
/** Nothing will fire: switched off, or no window and no absolute threshold to fall back on. */
|
|
81
|
+
| 'none';
|
|
82
|
+
/** The whole picture, as `/status` prints it and the guard enforces it. */
|
|
83
|
+
export interface AutocompactStatus {
|
|
84
|
+
/** Whether preventive compaction may fire at all (the config off switch). */
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The prompt size, in tokens, at which the conversation is folded — or `null` when nothing will
|
|
88
|
+
* fire preventively, which is what an unknown window with no absolute threshold must produce.
|
|
89
|
+
*/
|
|
90
|
+
thresholdTokens: number | null;
|
|
91
|
+
/** Where that number came from. */
|
|
92
|
+
thresholdOrigin: AutocompactThresholdOrigin;
|
|
93
|
+
/** The resolved context window, or `null` when no source knew it. */
|
|
94
|
+
window: number | null;
|
|
95
|
+
/** Which source the window came from. */
|
|
96
|
+
windowOrigin: ContextWindowOrigin;
|
|
97
|
+
/** The budget exactly as written, when one was written — so `/status` can echo `80%` as `80%`. */
|
|
98
|
+
budget: TokenBudget | null;
|
|
99
|
+
}
|
|
100
|
+
/** What {@link AutocompactController} needs to build a status. */
|
|
101
|
+
export interface AutocompactControllerOptions {
|
|
102
|
+
/** The `autocompact` key, already read through {@link resolveAutocompactConfig}. */
|
|
103
|
+
config: ResolvedAutocompactConfig;
|
|
104
|
+
/** The one memoised window resolution this session uses — shared with the guard. */
|
|
105
|
+
window: Pick<ResolvedContextWindow, 'read'>;
|
|
106
|
+
/**
|
|
107
|
+
* The threshold to use when the user named none, given a known window: the guard's
|
|
108
|
+
* `window − reserve`. Supplied as a callback because only the guard knows the reserve, and
|
|
109
|
+
* duplicating that arithmetic here is how the two would come to disagree about what "full" means.
|
|
110
|
+
*/
|
|
111
|
+
defaultThreshold: (_window: number) => number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* **The single object the guard and `/status` both read.**
|
|
115
|
+
*
|
|
116
|
+
* Holds the session override — the write half of `/autocompact` — and resolves it against the
|
|
117
|
+
* config and the window on demand. It is mutable by design and the guard reads it through a
|
|
118
|
+
* closure rather than capturing a value, because `createContextGuardMiddleware` runs its factory
|
|
119
|
+
* once per session and its hook holds no state: a captured threshold could never be changed by a
|
|
120
|
+
* command typed later, which is precisely what `/autocompact` has to do.
|
|
121
|
+
*/
|
|
122
|
+
export declare class AutocompactController {
|
|
123
|
+
private readonly options;
|
|
124
|
+
private sessionBudget;
|
|
125
|
+
constructor(options: AutocompactControllerOptions);
|
|
126
|
+
/**
|
|
127
|
+
* Set the threshold for the rest of this session, overriding the config.
|
|
128
|
+
*
|
|
129
|
+
* Deliberately takes an already-parsed {@link TokenBudget} rather than raw text: parsing is the
|
|
130
|
+
* shared parser's job, and a second entry point that took a string would be a second place the
|
|
131
|
+
* grammar could drift.
|
|
132
|
+
*
|
|
133
|
+
* **Refused while the config has compaction off.** `autocompact: false` means nothing fires,
|
|
134
|
+
* whatever number is named, so recording the budget would only make the next {@link status}
|
|
135
|
+
* describe a threshold that can never trigger. Nothing is recorded and the status is unchanged;
|
|
136
|
+
* the surface reads `enabled: false` off the status that comes back and says so. Turning it back
|
|
137
|
+
* on is a config edit — removing the key, or setting a threshold there — not a session command.
|
|
138
|
+
*/
|
|
139
|
+
setSessionBudget(budget: TokenBudget): void;
|
|
140
|
+
/** The session override in force, or `null`. Read by `/autocompact` with no argument. */
|
|
141
|
+
get sessionOverride(): TokenBudget | null;
|
|
142
|
+
/** Whether the config off switch leaves anything to do at all. */
|
|
143
|
+
get enabled(): boolean;
|
|
144
|
+
/** The full picture — the one call `/status` makes. */
|
|
145
|
+
status(): Promise<AutocompactStatus>;
|
|
146
|
+
/** The number the guard compares against, or `null` for "never fire". */
|
|
147
|
+
threshold(): Promise<number | null>;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The absolute threshold `gth init` writes for a model whose window it resolved — a plain number,
|
|
151
|
+
* because the point of seeding is that the user can read the enforced value straight out of their
|
|
152
|
+
* config.
|
|
153
|
+
*
|
|
154
|
+
* Returns `null` when the window is unknown, and the caller then writes **no key at all**: seeding
|
|
155
|
+
* a guess would put a number in the user's config that looks chosen and was not, which is worse
|
|
156
|
+
* than the absent key that leaves the runtime default in charge.
|
|
157
|
+
*/
|
|
158
|
+
export declare function seedAutocompactThreshold(window: number | null): number | null;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* EXT-161 — **the preventive compaction threshold: one number, one place it is decided.**
|
|
4
|
+
*
|
|
5
|
+
* `contextWindow.ts` answers "how big is the window". This module answers the question that
|
|
6
|
+
* actually gates a compaction: **at what prompt size do we fold the conversation, and who said
|
|
7
|
+
* so.** Three things can say so, in this order:
|
|
8
|
+
*
|
|
9
|
+
* 1. **the running session** — `/autocompact 300K`, which wins for the rest of the session;
|
|
10
|
+
* 2. **the user's config** — the `autocompact` key;
|
|
11
|
+
* 3. **the default** — derived from the resolved window by the guard, which is the only place that
|
|
12
|
+
* knows how many tokens are being held back for the answer.
|
|
13
|
+
*
|
|
14
|
+
* **Compaction is ON BY DEFAULT.** That is a ruling, and it is a deliberate exception to the rule
|
|
15
|
+
* that a default-on middleware must not touch `state.messages` — compaction changes what the model
|
|
16
|
+
* sees by construction, which is the whole feature. The exception is paid for the way the rule
|
|
17
|
+
* demands: the shape of the history a compaction leaves behind is pinned per provider by
|
|
18
|
+
* `compaction.ts`'s invariants (a)–(d), a compaction announces itself in the transcript, the
|
|
19
|
+
* resolved number and its provenance are readable with `/status`, and the off switch is one key —
|
|
20
|
+
* `autocompact: false`.
|
|
21
|
+
*
|
|
22
|
+
* **Why the provenance is carried rather than recomputed.** A threshold that is wrong is diagnosed
|
|
23
|
+
* by knowing where the number came from; a `/status` that reports a models.dev-derived number after
|
|
24
|
+
* a human typed `/autocompact 50000` would send the next diagnosis to the wrong place entirely. So
|
|
25
|
+
* a session override re-labels the provenance, and {@link AutocompactController} is the single
|
|
26
|
+
* object both the guard and `/status` read, over one memoised window resolution.
|
|
27
|
+
*/
|
|
28
|
+
import { parseTokenBudget, resolveTokenBudget } from '#src/config/tokenBudget.js';
|
|
29
|
+
/**
|
|
30
|
+
* The share of a model's context window that `gth init` seeds as an explicit threshold.
|
|
31
|
+
*
|
|
32
|
+
* Init writes an **absolute number** derived from this rather than the percentage itself, because
|
|
33
|
+
* the point of seeding is that the user opens their config and sees the number that will actually
|
|
34
|
+
* be enforced. A percentage would leave them one lookup away from it.
|
|
35
|
+
*
|
|
36
|
+
* 0.8 leaves a fifth of the window for the answer and the tool round that follows it — comfortably
|
|
37
|
+
* more than the flat answer reserve on any real cloud window, which is what makes the seeded number
|
|
38
|
+
* the binding one rather than a decoration.
|
|
39
|
+
*/
|
|
40
|
+
export const DEFAULT_AUTOCOMPACT_SEED_FRACTION = 0.8;
|
|
41
|
+
/**
|
|
42
|
+
* Read the `autocompact` config key.
|
|
43
|
+
*
|
|
44
|
+
* Defaulting happens **here, at the read site**, not in `DEFAULT_CONFIG`, so an absent key stays
|
|
45
|
+
* absent in the effective-config snapshot and the snapshot does not churn — the same placement
|
|
46
|
+
* `injectModelContext` and `toolLoopGuard` use.
|
|
47
|
+
*
|
|
48
|
+
* A malformed threshold raises the `TokenBudgetError` from the shared parser, which config
|
|
49
|
+
* validation turns into a field-scoped issue naming the offending text. It must never resolve to a
|
|
50
|
+
* number: `NaN`, `0` and 4097 are all thresholds the user cannot see and did not choose.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveAutocompactConfig(raw) {
|
|
53
|
+
if (raw === undefined || raw === null)
|
|
54
|
+
return { enabled: true, budget: null };
|
|
55
|
+
if (typeof raw === 'boolean')
|
|
56
|
+
return { enabled: raw, budget: null };
|
|
57
|
+
if (typeof raw === 'number' || typeof raw === 'string') {
|
|
58
|
+
return { enabled: true, budget: parseTokenBudget(raw) };
|
|
59
|
+
}
|
|
60
|
+
if (typeof raw === 'object') {
|
|
61
|
+
const value = raw;
|
|
62
|
+
const enabled = value.enabled !== false;
|
|
63
|
+
const budget = value.threshold === undefined || value.threshold === null
|
|
64
|
+
? null
|
|
65
|
+
: parseTokenBudget(value.threshold);
|
|
66
|
+
return { enabled, budget };
|
|
67
|
+
}
|
|
68
|
+
return { enabled: true, budget: null };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* **The single object the guard and `/status` both read.**
|
|
72
|
+
*
|
|
73
|
+
* Holds the session override — the write half of `/autocompact` — and resolves it against the
|
|
74
|
+
* config and the window on demand. It is mutable by design and the guard reads it through a
|
|
75
|
+
* closure rather than capturing a value, because `createContextGuardMiddleware` runs its factory
|
|
76
|
+
* once per session and its hook holds no state: a captured threshold could never be changed by a
|
|
77
|
+
* command typed later, which is precisely what `/autocompact` has to do.
|
|
78
|
+
*/
|
|
79
|
+
export class AutocompactController {
|
|
80
|
+
options;
|
|
81
|
+
sessionBudget = null;
|
|
82
|
+
constructor(options) {
|
|
83
|
+
this.options = options;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Set the threshold for the rest of this session, overriding the config.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately takes an already-parsed {@link TokenBudget} rather than raw text: parsing is the
|
|
89
|
+
* shared parser's job, and a second entry point that took a string would be a second place the
|
|
90
|
+
* grammar could drift.
|
|
91
|
+
*
|
|
92
|
+
* **Refused while the config has compaction off.** `autocompact: false` means nothing fires,
|
|
93
|
+
* whatever number is named, so recording the budget would only make the next {@link status}
|
|
94
|
+
* describe a threshold that can never trigger. Nothing is recorded and the status is unchanged;
|
|
95
|
+
* the surface reads `enabled: false` off the status that comes back and says so. Turning it back
|
|
96
|
+
* on is a config edit — removing the key, or setting a threshold there — not a session command.
|
|
97
|
+
*/
|
|
98
|
+
setSessionBudget(budget) {
|
|
99
|
+
if (!this.options.config.enabled)
|
|
100
|
+
return;
|
|
101
|
+
this.sessionBudget = budget;
|
|
102
|
+
}
|
|
103
|
+
/** The session override in force, or `null`. Read by `/autocompact` with no argument. */
|
|
104
|
+
get sessionOverride() {
|
|
105
|
+
return this.sessionBudget;
|
|
106
|
+
}
|
|
107
|
+
/** Whether the config off switch leaves anything to do at all. */
|
|
108
|
+
get enabled() {
|
|
109
|
+
return this.options.config.enabled;
|
|
110
|
+
}
|
|
111
|
+
/** The full picture — the one call `/status` makes. */
|
|
112
|
+
async status() {
|
|
113
|
+
const reading = await this.options.window.read();
|
|
114
|
+
const budget = this.sessionBudget ?? this.options.config.budget;
|
|
115
|
+
const budgetOrigin = this.sessionBudget
|
|
116
|
+
? 'session'
|
|
117
|
+
: this.options.config.budget
|
|
118
|
+
? 'config'
|
|
119
|
+
: 'default';
|
|
120
|
+
if (!this.options.config.enabled) {
|
|
121
|
+
return {
|
|
122
|
+
enabled: false,
|
|
123
|
+
thresholdTokens: null,
|
|
124
|
+
thresholdOrigin: 'none',
|
|
125
|
+
window: reading.tokens,
|
|
126
|
+
windowOrigin: reading.origin,
|
|
127
|
+
budget,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// A named budget resolves against the window — which an absolute count does not need, so an
|
|
131
|
+
// explicit `300K` still fires on a model nothing knows the window of. A PERCENTAGE without a
|
|
132
|
+
// window cannot resolve, and falls through to the same "nothing fires" answer as no threshold
|
|
133
|
+
// at all rather than to a guess.
|
|
134
|
+
const named = budget ? resolveTokenBudget(budget, reading.tokens) : null;
|
|
135
|
+
if (named !== null) {
|
|
136
|
+
return {
|
|
137
|
+
enabled: true,
|
|
138
|
+
thresholdTokens: named,
|
|
139
|
+
thresholdOrigin: budgetOrigin === 'default' ? 'config' : budgetOrigin,
|
|
140
|
+
window: reading.tokens,
|
|
141
|
+
windowOrigin: reading.origin,
|
|
142
|
+
budget,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (reading.tokens === null) {
|
|
146
|
+
return {
|
|
147
|
+
enabled: true,
|
|
148
|
+
thresholdTokens: null,
|
|
149
|
+
thresholdOrigin: 'none',
|
|
150
|
+
window: null,
|
|
151
|
+
windowOrigin: reading.origin,
|
|
152
|
+
budget,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
enabled: true,
|
|
157
|
+
thresholdTokens: this.options.defaultThreshold(reading.tokens),
|
|
158
|
+
thresholdOrigin: 'default',
|
|
159
|
+
window: reading.tokens,
|
|
160
|
+
windowOrigin: reading.origin,
|
|
161
|
+
budget,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** The number the guard compares against, or `null` for "never fire". */
|
|
165
|
+
async threshold() {
|
|
166
|
+
return (await this.status()).thresholdTokens;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The absolute threshold `gth init` writes for a model whose window it resolved — a plain number,
|
|
171
|
+
* because the point of seeding is that the user can read the enforced value straight out of their
|
|
172
|
+
* config.
|
|
173
|
+
*
|
|
174
|
+
* Returns `null` when the window is unknown, and the caller then writes **no key at all**: seeding
|
|
175
|
+
* a guess would put a number in the user's config that looks chosen and was not, which is worse
|
|
176
|
+
* than the absent key that leaves the runtime default in charge.
|
|
177
|
+
*/
|
|
178
|
+
export function seedAutocompactThreshold(window) {
|
|
179
|
+
if (window === null || !Number.isFinite(window) || window <= 0)
|
|
180
|
+
return null;
|
|
181
|
+
return Math.max(1, Math.floor(window * DEFAULT_AUTOCOMPACT_SEED_FRACTION));
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=compactionThreshold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compactionThreshold.js","sourceRoot":"","sources":["../../src/core/compactionThreshold.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type CatalogOptions, type ProviderCatalog } from '#src/providers/modelCatalog.js';
|
|
2
|
+
import type { ProviderId } from '#src/providers/modelDiscovery.js';
|
|
3
|
+
/**
|
|
4
|
+
* GS2-59 — default context window (`num_ctx`) for Ollama models. Ollama's OWN default is 4096, but
|
|
5
|
+
* gaunt-sloth's agentic prompt (system + full lean toolset + a tool result) already lands ~4000
|
|
6
|
+
* tokens; at 4096 a thinking model (e.g. gemma4:31b) spends its entire remaining budget on the
|
|
7
|
+
* reasoning field and emits EMPTY `content` on the turn after a tool executes — the GS2-59
|
|
8
|
+
* blank-answer regression. The OpenAI-compat `/v1` shim IGNORES `num_ctx`; the native `/api/chat`
|
|
9
|
+
* path honors it.
|
|
10
|
+
*
|
|
11
|
+
* 16384 is chosen as the largest window that is BOTH safely above the ~4000-token starvation point
|
|
12
|
+
* (4× headroom for reasoning + a few tool results) AND fits constrained consumer VRAM: Ollama
|
|
13
|
+
* preallocates the KV cache at `num_ctx`, so on a box where a large model already spills partly to
|
|
14
|
+
* CPU (e.g. a 19GB model on ~18GB of GPU), a 32768 cache tips the GPU allocation into an
|
|
15
|
+
* out-of-memory error. 16384 was verified live to run the agentic tool→synthesis turn on such a
|
|
16
|
+
* box; 32768 OOM'd it. Overridable per config via `llm.numCtx` — raise it if you have the VRAM and
|
|
17
|
+
* run long sessions, lower it on very tight hardware. NOTE: a per-request `num_ctx` overrides the
|
|
18
|
+
* daemon's `OLLAMA_CONTEXT_LENGTH`, so a user who tuned their server window higher should set
|
|
19
|
+
* `llm.numCtx` to match rather than rely on the server default.
|
|
20
|
+
*
|
|
21
|
+
* It lives here rather than in the provider module because two things now need it — the client the
|
|
22
|
+
* provider builds and the guard that has to know what that client will send — and a second copy is
|
|
23
|
+
* how the guard would come to reason about a window the request does not use.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEFAULT_OLLAMA_NUM_CTX = 16384;
|
|
26
|
+
/**
|
|
27
|
+
* The model's context window in tokens, or `null` when it is not known.
|
|
28
|
+
*
|
|
29
|
+
* Asynchronous because a source may have to ask the provider. Called on every model call, so an
|
|
30
|
+
* implementation that does I/O is expected to memoise; {@link resolveContextWindowSource} does.
|
|
31
|
+
*/
|
|
32
|
+
export type ContextWindowSource = () => Promise<number | null>;
|
|
33
|
+
/** A source that never knows — the honest answer for every provider no source is wired for. */
|
|
34
|
+
export declare const UNKNOWN_CONTEXT_WINDOW: ContextWindowSource;
|
|
35
|
+
/**
|
|
36
|
+
* The fields the window resolver reads off a chat model, without depending on its class.
|
|
37
|
+
*
|
|
38
|
+
* Structural rather than a `ChatOllama` import on purpose: the provider module loads
|
|
39
|
+
* `@langchain/ollama` dynamically so a session that never uses ollama never pays for it, and typing
|
|
40
|
+
* against the class here would undo that.
|
|
41
|
+
*/
|
|
42
|
+
export interface OllamaLikeModel {
|
|
43
|
+
_llmType?: () => string;
|
|
44
|
+
numCtx?: number;
|
|
45
|
+
model?: string;
|
|
46
|
+
baseUrl?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* **The ollama window: what we send, capped by what the model can hold.**
|
|
50
|
+
*
|
|
51
|
+
* The effective window is `numCtx` — the number gaunt-sloth puts on every native `/api/chat`
|
|
52
|
+
* request, which overrides the daemon's own `OLLAMA_CONTEXT_LENGTH` — and never the daemon default,
|
|
53
|
+
* because the request is what decides. It is then capped by the model's own `context_length` from
|
|
54
|
+
* `/api/show`: asking for more than the model has does not buy more room, so guarding against the
|
|
55
|
+
* larger number would let exactly the truncation this guard exists to prevent happen anyway.
|
|
56
|
+
*
|
|
57
|
+
* **Fail-soft in one direction only.** No daemon, a slow daemon, an unreadable answer — the
|
|
58
|
+
* configured number stands, because that is still the number the request will carry. Nothing here
|
|
59
|
+
* throws, and nothing here degrades a known window to unknown.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createOllamaContextWindowSource(llm: OllamaLikeModel): ContextWindowSource;
|
|
62
|
+
/** Where a resolved context window came from — carried so `/status` can say, and a wrong one is
|
|
63
|
+
* diagnosable instead of mysterious. */
|
|
64
|
+
export type ContextWindowOrigin = 'ollama' | 'models.dev' | 'profile' | 'unknown';
|
|
65
|
+
/** A resolved window and its provenance. `tokens: null` means "not known", and never "zero". */
|
|
66
|
+
export interface ContextWindowReading {
|
|
67
|
+
tokens: number | null;
|
|
68
|
+
origin: ContextWindowOrigin;
|
|
69
|
+
}
|
|
70
|
+
/** How each origin is described to a user, in a sentence that says where to go to change it. */
|
|
71
|
+
export declare const CONTEXT_WINDOW_ORIGIN_LABELS: Readonly<Record<ContextWindowOrigin, string>>;
|
|
72
|
+
/**
|
|
73
|
+
* The model's window as the LangChain provider package declares it: `llm.profile.maxInputTokens`.
|
|
74
|
+
*
|
|
75
|
+
* A getter on `BaseLanguageModel` that the base class answers with `{}` and each provider package
|
|
76
|
+
* overrides, so an id its table has never heard of yields `undefined` rather than an error —
|
|
77
|
+
* measured: `gpt-4o-mini` gives 128000 and `mistralai/mistral-7b-instruct` gives nothing. Wrapped
|
|
78
|
+
* in a `try` because it is a getter on someone else's object and a throw here would take down a
|
|
79
|
+
* resolution that has a perfectly good answer to fall back to.
|
|
80
|
+
*/
|
|
81
|
+
export declare function readProfileContextWindow(llm: unknown): number | null;
|
|
82
|
+
/** What {@link resolveContextWindow} needs beyond the model itself — all of it injectable. */
|
|
83
|
+
export interface ContextWindowResolutionOptions {
|
|
84
|
+
/**
|
|
85
|
+
* The gth provider namespace (`anthropic`, `google-genai`, …) — `config.modelProviderType`, NOT
|
|
86
|
+
* the model class's `_llmType()`. The two disagree exactly where it matters: `huggingface`
|
|
87
|
+
* reports `openai`, and both Gemini providers report `google`, so keying the catalog on the
|
|
88
|
+
* class's own label would read the wrong provider's slice or none at all.
|
|
89
|
+
*/
|
|
90
|
+
providerId?: string;
|
|
91
|
+
/** The model id as models.dev keys it — `llm.model`, e.g. `claude-sonnet-4-5`. */
|
|
92
|
+
modelId?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Options threaded to {@link getProviderCatalog} (cache dir, TTL, fetch impl) for hermetic tests.
|
|
95
|
+
*
|
|
96
|
+
* **`cacheOnly` defaults to `true` here and nowhere else.** This resolution sits in front of the
|
|
97
|
+
* first model call of a session, and a cold `api.json` fetch is a few MB behind a 10 s timeout —
|
|
98
|
+
* a delay the user would experience as the agent hanging before it said anything, to decide a
|
|
99
|
+
* threshold that already has a fallback. `gth init` passes `cacheOnly: false` because it is an
|
|
100
|
+
* explicit, interactive step that can afford to wait, and filling the cache there is what makes
|
|
101
|
+
* the runtime read a hit.
|
|
102
|
+
*/
|
|
103
|
+
catalogOptions?: CatalogOptions;
|
|
104
|
+
/** Catalog reader override; {@link getProviderCatalog} when omitted. Injected by the tests. */
|
|
105
|
+
catalogReader?: (_providerId: ProviderId, _options: CatalogOptions) => Promise<ProviderCatalog | null>;
|
|
106
|
+
/** Profile reader override; {@link readProfileContextWindow} when omitted. Injected by the tests. */
|
|
107
|
+
profileReader?: (_llm: unknown) => number | null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* One resolution, read two ways: {@link ResolvedContextWindow.source} for the guard, which wants
|
|
111
|
+
* only the number, and {@link ResolvedContextWindow.read} for `/status`, which also wants the
|
|
112
|
+
* provenance.
|
|
113
|
+
*
|
|
114
|
+
* They are handed out as a pair from a single factory call, over a single memoised promise, so the
|
|
115
|
+
* number `/status` prints is by construction the number the guard enforced. Two independent
|
|
116
|
+
* resolvers could disagree — a stale catalog on one and a fresh fetch on the other — and a
|
|
117
|
+
* `/status` that describes a threshold nobody is using is worse than no `/status` line at all.
|
|
118
|
+
*/
|
|
119
|
+
export interface ResolvedContextWindow {
|
|
120
|
+
/** The window in tokens, or `null` for unknown. Memoised; safe to call before every model call. */
|
|
121
|
+
source: ContextWindowSource;
|
|
122
|
+
/** The same resolution with its provenance attached. Shares the memoised promise. */
|
|
123
|
+
read: () => Promise<ContextWindowReading>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* **The one place a model is matched to a context window.** Tries ollama, then models.dev, then the
|
|
127
|
+
* LangChain profile, and answers `{ tokens: null, origin: 'unknown' }` when none of them knows.
|
|
128
|
+
*
|
|
129
|
+
* **There is deliberately no `?? DEFAULT` anywhere on this path.** One fallback turns every unknown
|
|
130
|
+
* window into a confident wrong number, which is the 4097 failure this file opens by naming.
|
|
131
|
+
*
|
|
132
|
+
* The whole resolution is memoised as a PROMISE, for the reason the ollama source already gives:
|
|
133
|
+
* this runs before every model call, the answer cannot change during a session, and holding the
|
|
134
|
+
* promise rather than the value means concurrent calls share one catalog read instead of racing to
|
|
135
|
+
* make several.
|
|
136
|
+
*/
|
|
137
|
+
export declare function resolveContextWindow(llm: unknown, options?: ContextWindowResolutionOptions): ResolvedContextWindow;
|
|
138
|
+
/**
|
|
139
|
+
* The window as a bare number, for a caller that does not need the provenance.
|
|
140
|
+
*
|
|
141
|
+
* A thin wrapper over {@link resolveContextWindow} rather than a second implementation, so the two
|
|
142
|
+
* cannot answer differently. With no options it consults only the sources that need no
|
|
143
|
+
* configuration — ollama and the profile — which is why a plain object with neither still answers
|
|
144
|
+
* `null`.
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveContextWindowSource(llm: unknown, options?: ContextWindowResolutionOptions): ContextWindowSource;
|