@gaunt-sloth/core 2.0.0-beta.5 → 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.
Files changed (91) hide show
  1. package/dist/config/schema.d.ts +4 -0
  2. package/dist/config/schema.js +64 -2
  3. package/dist/config/schema.js.map +1 -1
  4. package/dist/config/tokenBudget.d.ts +88 -0
  5. package/dist/config/tokenBudget.js +155 -0
  6. package/dist/config/tokenBudget.js.map +1 -0
  7. package/dist/config/types.d.ts +33 -4
  8. package/dist/config/types.js.map +1 -1
  9. package/dist/config.d.ts +1 -0
  10. package/dist/config.js +5 -0
  11. package/dist/config.js.map +1 -1
  12. package/dist/core/GthAbstractAgent.d.ts +24 -0
  13. package/dist/core/GthAbstractAgent.js +37 -1
  14. package/dist/core/GthAbstractAgent.js.map +1 -1
  15. package/dist/core/GthAgentRunner.d.ts +216 -1
  16. package/dist/core/GthAgentRunner.js +424 -3
  17. package/dist/core/GthAgentRunner.js.map +1 -1
  18. package/dist/core/GthLangChainAgent.d.ts +196 -0
  19. package/dist/core/GthLangChainAgent.js +392 -2
  20. package/dist/core/GthLangChainAgent.js.map +1 -1
  21. package/dist/core/approvals/conversationGrants.d.ts +60 -0
  22. package/dist/core/approvals/conversationGrants.js +77 -0
  23. package/dist/core/approvals/conversationGrants.js.map +1 -0
  24. package/dist/core/approvals/grants.d.ts +16 -0
  25. package/dist/core/approvals/grants.js +20 -5
  26. package/dist/core/approvals/grants.js.map +1 -1
  27. package/dist/core/compaction.d.ts +181 -0
  28. package/dist/core/compaction.js +293 -0
  29. package/dist/core/compaction.js.map +1 -0
  30. package/dist/core/compactionThreshold.d.ts +158 -0
  31. package/dist/core/compactionThreshold.js +183 -0
  32. package/dist/core/compactionThreshold.js.map +1 -0
  33. package/dist/core/contextWindow.d.ts +146 -0
  34. package/dist/core/contextWindow.js +256 -0
  35. package/dist/core/contextWindow.js.map +1 -0
  36. package/dist/core/exitOutputChannel.d.ts +51 -0
  37. package/dist/core/exitOutputChannel.js +65 -0
  38. package/dist/core/exitOutputChannel.js.map +1 -0
  39. package/dist/core/refusal.d.ts +17 -2
  40. package/dist/core/refusal.js +80 -14
  41. package/dist/core/refusal.js.map +1 -1
  42. package/dist/core/runStats.d.ts +1 -1
  43. package/dist/core/terminationNotice.d.ts +8 -0
  44. package/dist/core/terminationNotice.js +10 -4
  45. package/dist/core/terminationNotice.js.map +1 -1
  46. package/dist/core/terminationReason.d.ts +28 -0
  47. package/dist/core/terminationReason.js +27 -0
  48. package/dist/core/terminationReason.js.map +1 -1
  49. package/dist/core/types.d.ts +35 -1
  50. package/dist/core/types.js.map +1 -1
  51. package/dist/history/checkpointRetention.d.ts +279 -0
  52. package/dist/history/checkpointRetention.js +567 -0
  53. package/dist/history/checkpointRetention.js.map +1 -0
  54. package/dist/history/checkpointSaver.d.ts +93 -0
  55. package/dist/history/checkpointSaver.js +464 -0
  56. package/dist/history/checkpointSaver.js.map +1 -0
  57. package/dist/history/historyEnabled.d.ts +27 -0
  58. package/dist/history/historyEnabled.js +23 -0
  59. package/dist/history/historyEnabled.js.map +1 -0
  60. package/dist/history/historyFormat.d.ts +27 -0
  61. package/dist/history/historyFormat.js +125 -2
  62. package/dist/history/historyFormat.js.map +1 -1
  63. package/dist/history/historyStore.d.ts +61 -0
  64. package/dist/history/historyStore.js +180 -7
  65. package/dist/history/historyStore.js.map +1 -1
  66. package/dist/history/recordSession.d.ts +84 -22
  67. package/dist/history/recordSession.js +187 -12
  68. package/dist/history/recordSession.js.map +1 -1
  69. package/dist/history/sessionCheckpointer.d.ts +48 -0
  70. package/dist/history/sessionCheckpointer.js +200 -0
  71. package/dist/history/sessionCheckpointer.js.map +1 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.js +8 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/providers/modelCatalog.d.ts +14 -0
  76. package/dist/providers/modelCatalog.js +4 -0
  77. package/dist/providers/modelCatalog.js.map +1 -1
  78. package/dist/providers/modelDiscovery.d.ts +3 -1
  79. package/dist/providers/modelDiscovery.js +22 -8
  80. package/dist/providers/modelDiscovery.js.map +1 -1
  81. package/dist/providers/ollama.js +3 -19
  82. package/dist/providers/ollama.js.map +1 -1
  83. package/dist/runtime/conversation.js +7 -1
  84. package/dist/runtime/conversation.js.map +1 -1
  85. package/dist/runtime/singleShot.js +6 -1
  86. package/dist/runtime/singleShot.js.map +1 -1
  87. package/dist/utils/consoleUtils.d.ts +77 -0
  88. package/dist/utils/consoleUtils.js +81 -0
  89. package/dist/utils/consoleUtils.js.map +1 -1
  90. package/package.json +2 -2
  91. package/schema/gsloth-config.schema.json +33 -14
@@ -0,0 +1,293 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * GS2-23 — **the one conversation-compaction mechanism.** Folds the older part of a conversation
4
+ * into a single summary message and keeps a recent tail verbatim, so the model has room to go on.
5
+ *
6
+ * Every path that shortens a conversation goes through {@link compactMessages}: the manual
7
+ * `/compact` (this node), the reactive compact-and-retry on a context overflow (EXT-160) and the
8
+ * preventive threshold (EXT-161). Built separately they would disagree about what a context window
9
+ * is, so the invariants live here once, as tests rather than as comments:
10
+ *
11
+ * - **(a)** a `tool_call` is never separated from its `tool_result`: the cut moves so an `AIMessage`
12
+ * with `tool_calls` and every `ToolMessage` answering it fall on the same side, by widening the
13
+ * kept tail back to the pair's start;
14
+ * - **(b)** a system prompt survives untouched, and the summary is carried in a `HumanMessage` —
15
+ * never a mid-list `SystemMessage`, which `ChatAnthropic` rejects outright;
16
+ * - **(c)** the tail is never truncated at its end, so the last message of the input is the last
17
+ * message of the output: a history that ended on the pending user turn still does, and no
18
+ * compaction turns a completed exchange into a trailing assistant turn. The guarantee is
19
+ * relative to the input — the mechanism never CREATES a trailing assistant turn; a history a
20
+ * thrown turn left ending on its unanswered human turn still ends on it, and having the next
21
+ * human turn present before the model is invoked again is the caller's job;
22
+ * - **(d)** the operation converges: a previous summary is recognised by its marker and folded into
23
+ * the new one, so a compacted history compacted again holds exactly one summary message.
24
+ *
25
+ * The function is pure over its inputs and does no I/O of its own: the model call is injected as
26
+ * `summarize`, which is what lets the involuntary path bind a different model, a test bind a stub,
27
+ * and the manual command bind the session's own. Applying the result to the live graph is the
28
+ * runner's job (`GthAgentRunner.compactConversation`), through {@link replaceGraphMessages}.
29
+ */
30
+ import { AIMessage, HumanMessage, RemoveMessage, SystemMessage, ToolMessage, getBufferString, } from '@langchain/core/messages';
31
+ import { REMOVE_ALL_MESSAGES } from '@langchain/langgraph';
32
+ /**
33
+ * How many trailing messages a compaction keeps verbatim when the caller names no number.
34
+ *
35
+ * Small on purpose: a compaction is asked for when the conversation has grown too long, and the
36
+ * summary is what carries the rest. Six is roughly the last exchange with a tool call in it, or the
37
+ * last three plain exchanges — enough that the model still sees, word for word, what it was just
38
+ * doing. The kept tail can be wider than this when the cut would have split a tool pair (see
39
+ * {@link compactionCutIndex}).
40
+ */
41
+ export const DEFAULT_KEEP_RECENT = 6;
42
+ /**
43
+ * The marker a summary message carries in `additional_kwargs.lc_source`, so a later compaction can
44
+ * recognise it and fold it in rather than nest a summary inside a summary.
45
+ *
46
+ * It rides in the slot LangChain's own `summarizationMiddleware` uses (`lc_source: 'summarization'`),
47
+ * which is why {@link isCompactionSummary} accepts both values: a history that the opt-in
48
+ * middleware already summarised converges through here the same way one of ours does.
49
+ */
50
+ export const COMPACTION_SUMMARY_SOURCE = 'gth-compaction';
51
+ /** The marker LangChain's `summarizationMiddleware` stamps on the summary messages IT produces. */
52
+ const LANGCHAIN_SUMMARY_SOURCE = 'summarization';
53
+ /** The line the summary message opens with, so the model reads what follows as a summary. */
54
+ export const COMPACTION_SUMMARY_PREFIX = 'Here is a summary of the conversation to date:';
55
+ /** Whether a message is a summary produced by a previous compaction (ours or LangChain's). */
56
+ export function isCompactionSummary(message) {
57
+ if (!HumanMessage.isInstance(message))
58
+ return false;
59
+ const source = message.additional_kwargs?.lc_source;
60
+ return source === COMPACTION_SUMMARY_SOURCE || source === LANGCHAIN_SUMMARY_SOURCE;
61
+ }
62
+ /**
63
+ * The summary as the graph will hold it: a `HumanMessage`, marked so {@link isCompactionSummary}
64
+ * finds it again. A `HumanMessage` and not a `SystemMessage` because a system message anywhere but
65
+ * first is rejected by the Anthropic converter, and because the summary is conversation content —
66
+ * what the two parties said — rather than an instruction about how to behave.
67
+ */
68
+ export function createCompactionSummaryMessage(summaryText) {
69
+ return new HumanMessage({
70
+ content: `${COMPACTION_SUMMARY_PREFIX}\n\n${summaryText.trim()}`,
71
+ additional_kwargs: { lc_source: COMPACTION_SUMMARY_SOURCE },
72
+ });
73
+ }
74
+ /** The ids of the tool calls an AI message issued, or an empty set for any other message. */
75
+ function toolCallIdsOf(message) {
76
+ const ids = new Set();
77
+ if (!AIMessage.isInstance(message))
78
+ return ids;
79
+ for (const call of message.tool_calls ?? []) {
80
+ if (typeof call.id === 'string' && call.id.length > 0)
81
+ ids.add(call.id);
82
+ }
83
+ return ids;
84
+ }
85
+ /** How many leading `SystemMessage`s the list opens with — the system prompt, when it is in-band. */
86
+ function leadingSystemCount(messages) {
87
+ let count = 0;
88
+ while (count < messages.length && SystemMessage.isInstance(messages[count]))
89
+ count++;
90
+ return count;
91
+ }
92
+ /**
93
+ * The index of the `AIMessage` a tool result at `resultIndex` answers: the NEAREST earlier message
94
+ * that issued its `tool_call_id`, or `-1` for an orphan.
95
+ *
96
+ * Nearest, not earliest, because ids are not unique in every history: a replayed fixture and more
97
+ * than one provider reuse short ids (`call_0`) turn after turn. A result answers the most recent
98
+ * call with its id; pairing it with the first one ever issued would widen the tail back to the
99
+ * head of the conversation and fold nothing.
100
+ */
101
+ function issuerOf(conversation, resultIndex, id) {
102
+ for (let j = resultIndex - 1; j >= 0; j--) {
103
+ if (toolCallIdsOf(conversation[j]).has(id))
104
+ return j;
105
+ }
106
+ return -1;
107
+ }
108
+ /**
109
+ * Widen a proposed tail start backwards until no tool pair straddles it: for every `ToolMessage`
110
+ * at or after `start`, the `AIMessage` that issued its `tool_call_id` ({@link issuerOf}) must be at
111
+ * or after `start` too. Repeats until stable, because moving the start back can bring another
112
+ * answered call into the tail.
113
+ */
114
+ function widenTailToPairStart(conversation, start) {
115
+ let tailStart = start;
116
+ for (;;) {
117
+ let widenedTo = tailStart;
118
+ for (let i = tailStart; i < conversation.length; i++) {
119
+ const message = conversation[i];
120
+ if (!ToolMessage.isInstance(message) || !message.tool_call_id)
121
+ continue;
122
+ const issuer = issuerOf(conversation, i, message.tool_call_id);
123
+ if (issuer !== -1 && issuer < widenedTo)
124
+ widenedTo = issuer;
125
+ }
126
+ if (widenedTo === tailStart)
127
+ return tailStart;
128
+ tailStart = widenedTo;
129
+ }
130
+ }
131
+ /**
132
+ * **The cut rule.** Returns the index in `messages` where the kept tail begins, or `null` when
133
+ * there is nothing to fold.
134
+ *
135
+ * The tail is the last `keepRecent` messages, widened backwards so no `tool_call` is separated from
136
+ * its `tool_result` (`widenTailToPairStart`). A system prompt at the head is never part of
137
+ * either side. A previous summary sits at the head of the conversation and is always in the folded
138
+ * span, never in the tail — and when it would be the ONLY thing folded, nothing is folded: rewriting
139
+ * a summary with no new material behind it is the thrash invariant (d) forbids.
140
+ */
141
+ export function compactionCutIndex(messages, keepRecent = DEFAULT_KEEP_RECENT) {
142
+ const keep = Math.max(1, Math.floor(keepRecent));
143
+ const systemEnd = leadingSystemCount(messages);
144
+ const conversation = messages.slice(systemEnd);
145
+ if (conversation.length <= keep)
146
+ return null;
147
+ const tailStart = widenTailToPairStart(conversation, conversation.length - keep);
148
+ if (tailStart <= 0)
149
+ return null;
150
+ if (tailStart === 1 && isCompactionSummary(conversation[0]))
151
+ return null;
152
+ return systemEnd + tailStart;
153
+ }
154
+ /**
155
+ * Fold the older part of `messages` into one summary message. See the module doc for the four
156
+ * invariants this holds and the tests that pin them.
157
+ *
158
+ * `summarize` is called exactly once, over the folded span (a previous summary included, so its
159
+ * content is carried forward rather than lost), and its failure propagates: a compaction that
160
+ * cannot get a summary leaves the conversation alone and says so, rather than replacing history
161
+ * with an error string.
162
+ */
163
+ export async function compactMessages(input) {
164
+ const messages = [...input.messages];
165
+ const unchanged = {
166
+ messages,
167
+ summaryText: '',
168
+ removedCount: 0,
169
+ keptCount: messages.length,
170
+ changed: false,
171
+ };
172
+ const cut = compactionCutIndex(messages, input.keepRecent);
173
+ if (cut === null)
174
+ return unchanged;
175
+ const systemEnd = leadingSystemCount(messages);
176
+ const folded = messages.slice(systemEnd, cut);
177
+ const kept = messages.slice(cut);
178
+ const summaryText = (await input.summarize(folded, input.focus)).trim();
179
+ if (summaryText.length === 0) {
180
+ throw new Error('The model returned an empty summary, so the conversation was left unchanged.');
181
+ }
182
+ return {
183
+ messages: [
184
+ ...messages.slice(0, systemEnd),
185
+ createCompactionSummaryMessage(summaryText),
186
+ ...kept,
187
+ ],
188
+ summaryText,
189
+ removedCount: folded.length,
190
+ keptCount: kept.length,
191
+ changed: true,
192
+ };
193
+ }
194
+ /**
195
+ * The prompt the summariser sends, in the shape of LangChain's `summarizationMiddleware` prompt
196
+ * (role, objective, instructions, then the rendered messages) so the two mechanisms ask the model
197
+ * for the same kind of artifact. The `focus` block is the one addition: the user's own steer on
198
+ * what the summary should keep, appended when `/compact` was given free text.
199
+ */
200
+ export function buildCompactionPrompt(span, focus) {
201
+ const focusBlock = focus && focus.trim().length > 0
202
+ ? `\n<focus>\nThe user asked that the summary pay particular attention to: ${focus.trim()}\n</focus>\n`
203
+ : '';
204
+ return `<role>
205
+ Context Extraction Assistant
206
+ </role>
207
+
208
+ <primary_objective>
209
+ Your sole objective in this task is to extract the highest quality, most relevant context from the conversation history below.
210
+ </primary_objective>
211
+
212
+ <objective_information>
213
+ The conversation history below is about to be replaced by the context you extract, so that the conversation can continue with room to spare. Because of this, ensure the context you extract is the most important information to the overall goal: what was asked, what was decided, what was done, what was found, and what remains open.
214
+ </objective_information>
215
+
216
+ <instructions>
217
+ Extract and record all of the most important context from the conversation history. Do not repeat actions that were already completed; record their outcomes instead. Keep file paths, identifiers, commands, numbers and error messages exact. If the history opens with an earlier summary, fold it into yours rather than repeating it separately.
218
+ Respond ONLY with the extracted context. Do not include any additional information, or text before or after the extracted context.
219
+ </instructions>
220
+ ${focusBlock}
221
+ <messages>
222
+ Messages to summarize:
223
+ ${getBufferString(span)}
224
+ </messages>`;
225
+ }
226
+ /** The text of a model reply, whether it came back as a string or as content blocks. */
227
+ function contentText(content) {
228
+ if (typeof content === 'string')
229
+ return content;
230
+ if (!Array.isArray(content))
231
+ return '';
232
+ return content
233
+ .map((block) => {
234
+ if (typeof block === 'string')
235
+ return block;
236
+ if (block &&
237
+ typeof block === 'object' &&
238
+ typeof block.text === 'string') {
239
+ return block.text;
240
+ }
241
+ return '';
242
+ })
243
+ .join('');
244
+ }
245
+ /**
246
+ * A {@link ConversationSummarizer} bound to a chat model: renders the span with
247
+ * {@link buildCompactionPrompt} and returns the reply's text. The session model is what the runner
248
+ * binds; a caller with a cheaper model for summaries binds that instead.
249
+ */
250
+ export function createModelSummarizer(model) {
251
+ return async (span, focus) => contentText((await model.invoke(buildCompactionPrompt(span, focus))).content);
252
+ }
253
+ /** The characters of one message as a model would read them: its text, its tool calls, its result. */
254
+ function messageCharacters(message) {
255
+ let total = contentText(message.content).length;
256
+ if (AIMessage.isInstance(message)) {
257
+ for (const call of message.tool_calls ?? []) {
258
+ total += call.name.length;
259
+ try {
260
+ total += JSON.stringify(call.args ?? {}).length;
261
+ }
262
+ catch {
263
+ /* an argument that cannot be serialised contributes only its name */
264
+ }
265
+ }
266
+ }
267
+ return total;
268
+ }
269
+ /**
270
+ * The size of a conversation as a message count and a character estimate. Characters, not tokens,
271
+ * on purpose: precise token accounting belongs with the threshold that needs it (EXT-161), and a
272
+ * before/after comparison only has to agree with itself.
273
+ */
274
+ export function conversationSize(messages) {
275
+ let characters = 0;
276
+ for (const message of messages)
277
+ characters += messageCharacters(message);
278
+ return { messages: messages.length, characters };
279
+ }
280
+ /**
281
+ * Replace a thread's whole message list with `messages`, through the graph's own `updateState`.
282
+ *
283
+ * The write is `RemoveMessage(REMOVE_ALL_MESSAGES)` followed by the replacement, which the
284
+ * `messagesStateReducer` behind `state.messages` turns into "discard everything, keep what
285
+ * follows". Going through `updateState` is what makes a compaction durable for free: the graph
286
+ * writes a new checkpoint, so the next turn and a later resume both load the compacted history.
287
+ */
288
+ export async function replaceGraphMessages(graph, runConfig, messages) {
289
+ await graph.updateState(runConfig, {
290
+ messages: [new RemoveMessage({ id: REMOVE_ALL_MESSAGES }), ...messages],
291
+ });
292
+ }
293
+ //# sourceMappingURL=compaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"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"}
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAoB,MAAM,4BAA4B,CAAC;AAOpG;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,GAAG,CAAC;AAoBrD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAY;IACnD,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC9E,IAAI,OAAO,GAAG,KAAK,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IACpE,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACvD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1D,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,GAAiD,CAAC;QAChE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC;QACxC,MAAM,MAAM,GACV,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI;YACvD,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,gBAAgB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACxC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAC7B,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACzC,CAAC;AA8CD;;;;;;;;GAQG;AACH,MAAM,OAAO,qBAAqB;IACf,OAAO,CAA+B;IAC/C,aAAa,GAAuB,IAAI,CAAC;IAEjD,YAAY,OAAqC;QAC/C,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,MAAmB;QAClC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO;QACzC,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC;IAC9B,CAAC;IAED,yFAAyF;IACzF,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IAED,kEAAkE;IAClE,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;IACrC,CAAC;IAED,uDAAuD;IACvD,KAAK,CAAC,MAAM;QACV,MAAM,OAAO,GAAyB,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACvE,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;QAChE,MAAM,YAAY,GAA+B,IAAI,CAAC,aAAa;YACjE,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM;gBAC1B,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,SAAS,CAAC;QAEhB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACjC,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,eAAe,EAAE,IAAI;gBACrB,eAAe,EAAE,MAAM;gBACvB,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,YAAY,EAAE,OAAO,CAAC,MAAM;gBAC5B,MAAM;aACP,CAAC;QACJ,CAAC;QAED,4FAA4F;QAC5F,6FAA6F;QAC7F,8FAA8F;QAC9F,iCAAiC;QACjC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACzE,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,eAAe,EAAE,KAAK;gBACtB,eAAe,EAAE,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY;gBACrE,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,YAAY,EAAE,OAAO,CAAC,MAAM;gBAC5B,MAAM;aACP,CAAC;QACJ,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YAC5B,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,eAAe,EAAE,IAAI;gBACrB,eAAe,EAAE,MAAM;gBACvB,MAAM,EAAE,IAAI;gBACZ,YAAY,EAAE,OAAO,CAAC,MAAM;gBAC5B,MAAM;aACP,CAAC;QACJ,CAAC;QAED,OAAO;YACL,OAAO,EAAE,IAAI;YACb,eAAe,EAAE,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC;YAC9D,eAAe,EAAE,SAAS;YAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,YAAY,EAAE,OAAO,CAAC,MAAM;YAC5B,MAAM;SACP,CAAC;IACJ,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,SAAS;QACb,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,eAAe,CAAC;IAC/C,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAqB;IAC5D,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5E,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,iCAAiC,CAAC,CAAC,CAAC;AAC7E,CAAC"}