@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.
Files changed (94) 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/approvalRequest.d.ts +142 -0
  22. package/dist/core/approvals/approvalRequest.js +198 -3
  23. package/dist/core/approvals/approvalRequest.js.map +1 -1
  24. package/dist/core/approvals/conversationGrants.d.ts +60 -0
  25. package/dist/core/approvals/conversationGrants.js +77 -0
  26. package/dist/core/approvals/conversationGrants.js.map +1 -0
  27. package/dist/core/approvals/grants.d.ts +16 -0
  28. package/dist/core/approvals/grants.js +20 -5
  29. package/dist/core/approvals/grants.js.map +1 -1
  30. package/dist/core/compaction.d.ts +181 -0
  31. package/dist/core/compaction.js +293 -0
  32. package/dist/core/compaction.js.map +1 -0
  33. package/dist/core/compactionThreshold.d.ts +158 -0
  34. package/dist/core/compactionThreshold.js +183 -0
  35. package/dist/core/compactionThreshold.js.map +1 -0
  36. package/dist/core/contextWindow.d.ts +146 -0
  37. package/dist/core/contextWindow.js +256 -0
  38. package/dist/core/contextWindow.js.map +1 -0
  39. package/dist/core/exitOutputChannel.d.ts +51 -0
  40. package/dist/core/exitOutputChannel.js +65 -0
  41. package/dist/core/exitOutputChannel.js.map +1 -0
  42. package/dist/core/refusal.d.ts +17 -2
  43. package/dist/core/refusal.js +80 -14
  44. package/dist/core/refusal.js.map +1 -1
  45. package/dist/core/runStats.d.ts +1 -1
  46. package/dist/core/terminationNotice.d.ts +8 -0
  47. package/dist/core/terminationNotice.js +10 -4
  48. package/dist/core/terminationNotice.js.map +1 -1
  49. package/dist/core/terminationReason.d.ts +28 -0
  50. package/dist/core/terminationReason.js +27 -0
  51. package/dist/core/terminationReason.js.map +1 -1
  52. package/dist/core/types.d.ts +35 -1
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/history/checkpointRetention.d.ts +279 -0
  55. package/dist/history/checkpointRetention.js +567 -0
  56. package/dist/history/checkpointRetention.js.map +1 -0
  57. package/dist/history/checkpointSaver.d.ts +93 -0
  58. package/dist/history/checkpointSaver.js +464 -0
  59. package/dist/history/checkpointSaver.js.map +1 -0
  60. package/dist/history/historyEnabled.d.ts +27 -0
  61. package/dist/history/historyEnabled.js +23 -0
  62. package/dist/history/historyEnabled.js.map +1 -0
  63. package/dist/history/historyFormat.d.ts +27 -0
  64. package/dist/history/historyFormat.js +125 -2
  65. package/dist/history/historyFormat.js.map +1 -1
  66. package/dist/history/historyStore.d.ts +61 -0
  67. package/dist/history/historyStore.js +180 -7
  68. package/dist/history/historyStore.js.map +1 -1
  69. package/dist/history/recordSession.d.ts +84 -22
  70. package/dist/history/recordSession.js +187 -12
  71. package/dist/history/recordSession.js.map +1 -1
  72. package/dist/history/sessionCheckpointer.d.ts +48 -0
  73. package/dist/history/sessionCheckpointer.js +200 -0
  74. package/dist/history/sessionCheckpointer.js.map +1 -0
  75. package/dist/index.d.ts +6 -0
  76. package/dist/index.js +8 -0
  77. package/dist/index.js.map +1 -1
  78. package/dist/providers/modelCatalog.d.ts +14 -0
  79. package/dist/providers/modelCatalog.js +4 -0
  80. package/dist/providers/modelCatalog.js.map +1 -1
  81. package/dist/providers/modelDiscovery.d.ts +3 -1
  82. package/dist/providers/modelDiscovery.js +22 -8
  83. package/dist/providers/modelDiscovery.js.map +1 -1
  84. package/dist/providers/ollama.js +3 -19
  85. package/dist/providers/ollama.js.map +1 -1
  86. package/dist/runtime/conversation.js +7 -1
  87. package/dist/runtime/conversation.js.map +1 -1
  88. package/dist/runtime/singleShot.js +6 -1
  89. package/dist/runtime/singleShot.js.map +1 -1
  90. package/dist/utils/consoleUtils.d.ts +77 -0
  91. package/dist/utils/consoleUtils.js +81 -0
  92. package/dist/utils/consoleUtils.js.map +1 -1
  93. package/package.json +2 -2
  94. package/schema/gsloth-config.schema.json +33 -14
@@ -0,0 +1,256 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * EXT-160 — **how many tokens the model in front of us will actually accept.**
4
+ *
5
+ * The pre-call context guard needs one number: the window, in tokens. This module resolves it, and
6
+ * its contract is the part that matters — **`null` means "unknown", and an unknown window never
7
+ * triggers anything.** A guard that guesses a window is worse than no guard: LangChain's own
8
+ * overflow fallback guesses 4097 for anything it does not recognise, which on a 262144-token local
9
+ * model would compact a conversation that had all the room in the world. So every source here
10
+ * either knows or says it does not.
11
+ *
12
+ * The source is **injectable and per-provider**, and there are three of them, tried in a fixed
13
+ * order by {@link resolveContextWindow}:
14
+ *
15
+ * 1. **ollama** — the number this session will actually put on the request (`num_ctx`), capped by
16
+ * the model's own `context_length`. Ollama is the only provider that cannot report an overflow
17
+ * at all: it silently drops the oldest tokens to fit and answers from the remainder, so knowing
18
+ * the number beforehand is the only defence.
19
+ * 2. **models.dev** ([[EXT-161]]) — the cloud catalog's `limit.context`, read through the
20
+ * cache-first slice `providers/modelCatalog.ts` already maintains.
21
+ * 3. **the LangChain profile** ([[EXT-161]]) — `llm.profile.maxInputTokens`, a backstop.
22
+ *
23
+ * **models.dev outranks the profile, and the order is a ruling rather than a preference.** The
24
+ * profile is a table compiled into a provider package, so it moves only when that package is
25
+ * republished and we bump it, while the catalog refreshes on a 24h TTL. That makes a profile
26
+ * **wrong as well as absent**: measured on this repo's pinned packages, `deepseek-chat` reports a
27
+ * 1,000,000-token window from `@langchain/deepseek`'s own table. Wrong-and-confident is the failure
28
+ * this order avoids — an overstated window means no preventive compaction at all, which is exactly
29
+ * the case the reactive seam then has to catch.
30
+ *
31
+ * **A model none of the three knows resolves to `null`, and `null` triggers nothing.** There is
32
+ * deliberately no default anywhere in this chain: LangChain's own overflow fallback guesses 4097
33
+ * for an unrecognised model, which on a 262144-token local model would compact a conversation that
34
+ * had all the room in the world, and on an unknown cloud model would compact at roughly 3.3k tokens
35
+ * with nothing on screen to say why.
36
+ */
37
+ import { debugLog } from '#src/utils/debugUtils.js';
38
+ import { getProviderCatalog, } from '#src/providers/modelCatalog.js';
39
+ /**
40
+ * GS2-59 — default context window (`num_ctx`) for Ollama models. Ollama's OWN default is 4096, but
41
+ * gaunt-sloth's agentic prompt (system + full lean toolset + a tool result) already lands ~4000
42
+ * tokens; at 4096 a thinking model (e.g. gemma4:31b) spends its entire remaining budget on the
43
+ * reasoning field and emits EMPTY `content` on the turn after a tool executes — the GS2-59
44
+ * blank-answer regression. The OpenAI-compat `/v1` shim IGNORES `num_ctx`; the native `/api/chat`
45
+ * path honors it.
46
+ *
47
+ * 16384 is chosen as the largest window that is BOTH safely above the ~4000-token starvation point
48
+ * (4× headroom for reasoning + a few tool results) AND fits constrained consumer VRAM: Ollama
49
+ * preallocates the KV cache at `num_ctx`, so on a box where a large model already spills partly to
50
+ * CPU (e.g. a 19GB model on ~18GB of GPU), a 32768 cache tips the GPU allocation into an
51
+ * out-of-memory error. 16384 was verified live to run the agentic tool→synthesis turn on such a
52
+ * box; 32768 OOM'd it. Overridable per config via `llm.numCtx` — raise it if you have the VRAM and
53
+ * run long sessions, lower it on very tight hardware. NOTE: a per-request `num_ctx` overrides the
54
+ * daemon's `OLLAMA_CONTEXT_LENGTH`, so a user who tuned their server window higher should set
55
+ * `llm.numCtx` to match rather than rely on the server default.
56
+ *
57
+ * It lives here rather than in the provider module because two things now need it — the client the
58
+ * provider builds and the guard that has to know what that client will send — and a second copy is
59
+ * how the guard would come to reason about a window the request does not use.
60
+ */
61
+ export const DEFAULT_OLLAMA_NUM_CTX = 16384;
62
+ /** How long the daemon gets to answer `/api/show` before the cap is treated as unknown. */
63
+ const SHOW_TIMEOUT_MS = 3000;
64
+ /** A source that never knows — the honest answer for every provider no source is wired for. */
65
+ export const UNKNOWN_CONTEXT_WINDOW = async () => null;
66
+ /** Whether a resolved chat model is the native `ChatOllama` client (`_llmType()` is `'ollama'`). */
67
+ function isOllamaModel(llm) {
68
+ try {
69
+ const type = llm?._llmType?.();
70
+ return type === 'ollama';
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ /**
77
+ * The model's own maximum from the daemon: `/api/show` reports `model_info` keyed by architecture,
78
+ * e.g. `gemma4.context_length`. Returns `null` on any failure — no daemon, a model the daemon does
79
+ * not have, a timeout, a response shaped differently by a future ollama — because a cap we could
80
+ * not read is not a cap of zero.
81
+ */
82
+ async function readOllamaModelCap(baseUrl, model) {
83
+ try {
84
+ const response = await fetch(`${baseUrl.replace(/\/+$/, '')}/api/show`, {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ model }),
88
+ signal: AbortSignal.timeout(SHOW_TIMEOUT_MS),
89
+ });
90
+ if (!response.ok)
91
+ return null;
92
+ const body = (await response.json());
93
+ const info = body?.model_info;
94
+ if (!info || typeof info !== 'object')
95
+ return null;
96
+ // Prefer the architecture the daemon itself names; fall back to any `*.context_length` key so a
97
+ // model whose `general.architecture` is missing still reports its window.
98
+ const architecture = info['general.architecture'];
99
+ const keys = typeof architecture === 'string' && architecture.length > 0
100
+ ? [`${architecture}.context_length`]
101
+ : Object.keys(info).filter((key) => key.endsWith('.context_length'));
102
+ for (const key of keys) {
103
+ const value = info[key];
104
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0)
105
+ return value;
106
+ }
107
+ return null;
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ /**
114
+ * **The ollama window: what we send, capped by what the model can hold.**
115
+ *
116
+ * The effective window is `numCtx` — the number gaunt-sloth puts on every native `/api/chat`
117
+ * request, which overrides the daemon's own `OLLAMA_CONTEXT_LENGTH` — and never the daemon default,
118
+ * because the request is what decides. It is then capped by the model's own `context_length` from
119
+ * `/api/show`: asking for more than the model has does not buy more room, so guarding against the
120
+ * larger number would let exactly the truncation this guard exists to prevent happen anyway.
121
+ *
122
+ * **Fail-soft in one direction only.** No daemon, a slow daemon, an unreadable answer — the
123
+ * configured number stands, because that is still the number the request will carry. Nothing here
124
+ * throws, and nothing here degrades a known window to unknown.
125
+ */
126
+ export function createOllamaContextWindowSource(llm) {
127
+ const configured = typeof llm.numCtx === 'number' && llm.numCtx > 0 ? llm.numCtx : undefined;
128
+ // `providers/ollama.ts` always sets `numCtx`, so the default below is reached only by a chat model
129
+ // a JS config built by hand.
130
+ const requested = configured ?? DEFAULT_OLLAMA_NUM_CTX;
131
+ const baseUrl = typeof llm.baseUrl === 'string' ? llm.baseUrl : undefined;
132
+ const model = typeof llm.model === 'string' ? llm.model : undefined;
133
+ // Memoised: the window cannot change during a session, and the guard runs before EVERY model
134
+ // call. A per-call `/api/show` would put a network round trip in front of each one. Held as the
135
+ // PROMISE so concurrent calls share the one request rather than racing to make several.
136
+ let pending;
137
+ return async () => {
138
+ if (!baseUrl || !model)
139
+ return requested;
140
+ pending ??= readOllamaModelCap(baseUrl, model);
141
+ const cap = await pending;
142
+ if (cap === null)
143
+ return requested;
144
+ const effective = Math.min(requested, cap);
145
+ if (effective !== requested) {
146
+ debugLog(`Ollama context window: requested num_ctx ${requested} exceeds ${model}'s own ` +
147
+ `${cap}; guarding against ${effective}.`);
148
+ }
149
+ return effective;
150
+ };
151
+ }
152
+ /** How each origin is described to a user, in a sentence that says where to go to change it. */
153
+ export const CONTEXT_WINDOW_ORIGIN_LABELS = {
154
+ ollama: "this session's num_ctx, capped by the model's own context length",
155
+ 'models.dev': 'the models.dev catalog',
156
+ profile: "the provider package's built-in model profile",
157
+ unknown: 'nowhere — no source knows this model, so nothing is compacted preventively',
158
+ };
159
+ /**
160
+ * The model's window as the LangChain provider package declares it: `llm.profile.maxInputTokens`.
161
+ *
162
+ * A getter on `BaseLanguageModel` that the base class answers with `{}` and each provider package
163
+ * overrides, so an id its table has never heard of yields `undefined` rather than an error —
164
+ * measured: `gpt-4o-mini` gives 128000 and `mistralai/mistral-7b-instruct` gives nothing. Wrapped
165
+ * in a `try` because it is a getter on someone else's object and a throw here would take down a
166
+ * resolution that has a perfectly good answer to fall back to.
167
+ */
168
+ export function readProfileContextWindow(llm) {
169
+ try {
170
+ const profile = llm?.profile;
171
+ const max = profile?.maxInputTokens;
172
+ return typeof max === 'number' && Number.isFinite(max) && max > 0 ? max : null;
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ }
178
+ /**
179
+ * **The one place a model is matched to a context window.** Tries ollama, then models.dev, then the
180
+ * LangChain profile, and answers `{ tokens: null, origin: 'unknown' }` when none of them knows.
181
+ *
182
+ * **There is deliberately no `?? DEFAULT` anywhere on this path.** One fallback turns every unknown
183
+ * window into a confident wrong number, which is the 4097 failure this file opens by naming.
184
+ *
185
+ * The whole resolution is memoised as a PROMISE, for the reason the ollama source already gives:
186
+ * this runs before every model call, the answer cannot change during a session, and holding the
187
+ * promise rather than the value means concurrent calls share one catalog read instead of racing to
188
+ * make several.
189
+ */
190
+ export function resolveContextWindow(llm, options = {}) {
191
+ const ollamaSource = isOllamaModel(llm) ? createOllamaContextWindowSource(llm) : null;
192
+ const catalogReader = options.catalogReader ?? getProviderCatalog;
193
+ const profileReader = options.profileReader ?? readProfileContextWindow;
194
+ const providerId = options.providerId?.trim();
195
+ const modelId = options.modelId?.trim();
196
+ let pending;
197
+ const resolve = async () => {
198
+ // 1. Ollama — the number the request will actually carry. It is asked first and not merely
199
+ // preferred: models.dev deliberately has no ollama entry (local models have no catalog
200
+ // row), so for ollama there is nothing below this to fall through to.
201
+ if (ollamaSource) {
202
+ try {
203
+ const tokens = await ollamaSource();
204
+ if (tokens !== null && Number.isFinite(tokens) && tokens > 0) {
205
+ return { tokens, origin: 'ollama' };
206
+ }
207
+ }
208
+ catch {
209
+ /* the ollama source is documented never to throw; a stub still might */
210
+ }
211
+ }
212
+ // 2. models.dev — RULED to outrank the profile. See the module docblock for why.
213
+ if (providerId && modelId) {
214
+ try {
215
+ const catalog = await catalogReader(providerId, {
216
+ cacheOnly: true,
217
+ ...options.catalogOptions,
218
+ });
219
+ const context = catalog?.models?.[modelId]?.limit?.context;
220
+ if (typeof context === 'number' && Number.isFinite(context) && context > 0) {
221
+ return { tokens: context, origin: 'models.dev' };
222
+ }
223
+ }
224
+ catch {
225
+ // `getProviderCatalog` never throws by contract (catalog availability must never block a
226
+ // model), so this catches an injected stub only — but a resolution that fell over here
227
+ // would take the profile backstop down with it, which is the opposite of degrading well.
228
+ }
229
+ }
230
+ // 3. The profile — a backstop, recorded as such so a wrong threshold is diagnosable.
231
+ try {
232
+ const profile = profileReader(llm);
233
+ if (profile !== null && Number.isFinite(profile) && profile > 0) {
234
+ return { tokens: profile, origin: 'profile' };
235
+ }
236
+ }
237
+ catch {
238
+ /* see readProfileContextWindow: someone else's getter */
239
+ }
240
+ return { tokens: null, origin: 'unknown' };
241
+ };
242
+ const read = () => (pending ??= resolve());
243
+ return { read, source: async () => (await read()).tokens };
244
+ }
245
+ /**
246
+ * The window as a bare number, for a caller that does not need the provenance.
247
+ *
248
+ * A thin wrapper over {@link resolveContextWindow} rather than a second implementation, so the two
249
+ * cannot answer differently. With no options it consults only the sources that need no
250
+ * configuration — ollama and the profile — which is why a plain object with neither still answers
251
+ * `null`.
252
+ */
253
+ export function resolveContextWindowSource(llm, options = {}) {
254
+ return resolveContextWindow(llm, options).source;
255
+ }
256
+ //# sourceMappingURL=contextWindow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contextWindow.js","sourceRoot":"","sources":["../../src/core/contextWindow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @module exitOutputChannel
3
+ * TUI-C56 — the queue for output that must OUTLIVE the session's own screen.
4
+ *
5
+ * TUI-C48 put the whole interactive session in the terminal's alternate screen. Ink treats
6
+ * alternate-screen teardown output as disposable by design: on unmount it restores the primary
7
+ * buffer without replaying prior frames, so everything the session painted — every notice, every
8
+ * answer — goes with the alternate buffer when the session ends. That is exactly what the
9
+ * full-screen surface is for, and it is the right default for a conversation.
10
+ *
11
+ * It is the wrong default for the handful of things a user must still have AFTER the session:
12
+ * a path they now have to go and open. The first of those is the `/debug-dump` archive, whose
13
+ * in-frame notice is genuinely useful mid-session and completely gone the moment the user exits —
14
+ * leaving them holding a diagnostic bundle they cannot find, with nothing having errored.
15
+ *
16
+ * So this is a channel, not a special case: a producer anywhere in the codebase can DEFER one
17
+ * block of text, and the surface that owns the terminal drains it once the terminal is its
18
+ * owner's again. Two halves, deliberately split:
19
+ *
20
+ * - **This module is the queue and nothing else.** No stream, no formatting, no policy about
21
+ * when a line is worth printing. That keeps it surface-agnostic and directly testable.
22
+ * - **The surface owns the write.** Only the surface knows when its terminal is back (for the
23
+ * Ink TUI: after `waitUntilExit()` has resolved, which is after Ink has left the alternate
24
+ * screen) and whether writing is appropriate at all (a piped run must not get an extra line
25
+ * its caller did not ask for). See `writeDeferredExitOutput` in the TUI session module.
26
+ *
27
+ * A surface whose output already survives its own exit — the readline (`--no-tui`) session, which
28
+ * never owned an alternate screen — simply never drains. Deferring is then a no-op that costs one
29
+ * array entry and prints nothing, which is what keeps the producers surface-agnostic: `/debug-dump`
30
+ * is one command with one behaviour, and the two surfaces differ only in whether the re-print is
31
+ * needed at all.
32
+ */
33
+ /**
34
+ * Defer one block of text until the surface hands the terminal back.
35
+ *
36
+ * The text should stand on its own: it lands on a restored screen with none of the session's
37
+ * framing around it and possibly a long scrollback above it, so a bare value with no label is not
38
+ * enough for a reader to know what they are looking at.
39
+ */
40
+ export declare function deferExitOutput(text: string): void;
41
+ /**
42
+ * Take everything deferred so far, emptying the queue. Draining is destructive so a second drain
43
+ * — a surface with more than one exit path reaching the drain twice — cannot double-print.
44
+ */
45
+ export declare function drainExitOutput(): string[];
46
+ /**
47
+ * Discard anything deferred without emitting it. A surface calls this as it starts, so a block
48
+ * deferred by something earlier in the same process can never surface at the end of a session
49
+ * that had nothing to do with it.
50
+ */
51
+ export declare function clearExitOutput(): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @module exitOutputChannel
3
+ * TUI-C56 — the queue for output that must OUTLIVE the session's own screen.
4
+ *
5
+ * TUI-C48 put the whole interactive session in the terminal's alternate screen. Ink treats
6
+ * alternate-screen teardown output as disposable by design: on unmount it restores the primary
7
+ * buffer without replaying prior frames, so everything the session painted — every notice, every
8
+ * answer — goes with the alternate buffer when the session ends. That is exactly what the
9
+ * full-screen surface is for, and it is the right default for a conversation.
10
+ *
11
+ * It is the wrong default for the handful of things a user must still have AFTER the session:
12
+ * a path they now have to go and open. The first of those is the `/debug-dump` archive, whose
13
+ * in-frame notice is genuinely useful mid-session and completely gone the moment the user exits —
14
+ * leaving them holding a diagnostic bundle they cannot find, with nothing having errored.
15
+ *
16
+ * So this is a channel, not a special case: a producer anywhere in the codebase can DEFER one
17
+ * block of text, and the surface that owns the terminal drains it once the terminal is its
18
+ * owner's again. Two halves, deliberately split:
19
+ *
20
+ * - **This module is the queue and nothing else.** No stream, no formatting, no policy about
21
+ * when a line is worth printing. That keeps it surface-agnostic and directly testable.
22
+ * - **The surface owns the write.** Only the surface knows when its terminal is back (for the
23
+ * Ink TUI: after `waitUntilExit()` has resolved, which is after Ink has left the alternate
24
+ * screen) and whether writing is appropriate at all (a piped run must not get an extra line
25
+ * its caller did not ask for). See `writeDeferredExitOutput` in the TUI session module.
26
+ *
27
+ * A surface whose output already survives its own exit — the readline (`--no-tui`) session, which
28
+ * never owned an alternate screen — simply never drains. Deferring is then a no-op that costs one
29
+ * array entry and prints nothing, which is what keeps the producers surface-agnostic: `/debug-dump`
30
+ * is one command with one behaviour, and the two surfaces differ only in whether the re-print is
31
+ * needed at all.
32
+ */
33
+ /**
34
+ * Blocks of deferred text, in the order they were deferred. Module-level, like the single
35
+ * subscriber in `toolOutputChannel`: one process hosts at most one interactive session, and no
36
+ * server surface reaches a producer (the slash-command registry is built only by the two
37
+ * interactive sessions).
38
+ */
39
+ const deferred = [];
40
+ /**
41
+ * Defer one block of text until the surface hands the terminal back.
42
+ *
43
+ * The text should stand on its own: it lands on a restored screen with none of the session's
44
+ * framing around it and possibly a long scrollback above it, so a bare value with no label is not
45
+ * enough for a reader to know what they are looking at.
46
+ */
47
+ export function deferExitOutput(text) {
48
+ deferred.push(text);
49
+ }
50
+ /**
51
+ * Take everything deferred so far, emptying the queue. Draining is destructive so a second drain
52
+ * — a surface with more than one exit path reaching the drain twice — cannot double-print.
53
+ */
54
+ export function drainExitOutput() {
55
+ return deferred.splice(0, deferred.length);
56
+ }
57
+ /**
58
+ * Discard anything deferred without emitting it. A surface calls this as it starts, so a block
59
+ * deferred by something earlier in the same process can never surface at the end of a session
60
+ * that had nothing to do with it.
61
+ */
62
+ export function clearExitOutput() {
63
+ deferred.length = 0;
64
+ }
65
+ //# sourceMappingURL=exitOutputChannel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exitOutputChannel.js","sourceRoot":"","sources":["../../src/core/exitOutputChannel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH;;;;;GAKG;AACH,MAAM,QAAQ,GAAa,EAAE,CAAC;AAE9B;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;AACtB,CAAC"}
@@ -30,8 +30,15 @@ import type { GthTerminationClassification } from '#src/core/terminationReason.j
30
30
  /** One detected refusal, normalized across providers. */
31
31
  export interface RefusalInfo {
32
32
  /** Best-effort provider family the signal came from (for logging / the surfaced message). */
33
- provider: 'openai' | 'anthropic' | 'bedrock' | 'unknown';
34
- /** The raw stop/finish reason token that flagged the refusal (e.g. `content_filter`). */
33
+ provider: 'openai' | 'anthropic' | 'bedrock' | 'google' | 'unknown';
34
+ /**
35
+ * The stop/finish reason token that flagged the refusal, lower-cased (e.g. `content_filter`).
36
+ *
37
+ * Lower-cased rather than raw because the token's case is a per-provider spelling, not a fact:
38
+ * Gemini shouts `SAFETY` where OpenAI writes `content_filter`, and this value is matched, logged
39
+ * and carried into the taxonomy's `detail` — the same normalization
40
+ * {@link detectOutputTruncation} already applies to its own token.
41
+ */
35
42
  reason: string;
36
43
  /** Any model-provided explanation text (empty string when the refusal carried none). */
37
44
  explanation: string;
@@ -42,6 +49,9 @@ export interface RefusalInfo {
42
49
  * reason indicates a content-policy refusal, else `null`. Defensive: any non-message / unexpected
43
50
  * shape yields `null`, so a normal turn is never mistaken for a refusal.
44
51
  *
52
+ * Reasons are compared lower-cased, because the case is a provider's spelling and not a fact —
53
+ * Gemini shouts `SAFETY` where OpenAI writes `content_filter`.
54
+ *
45
55
  * Covered shapes:
46
56
  * - OpenAI-family `finish_reason: 'content_filter'` (also under `additional_kwargs`).
47
57
  * - Anthropic `stop_reason: 'refusal'`.
@@ -51,6 +61,11 @@ export interface RefusalInfo {
51
61
  * - Bedrock Converse content filter: `stopReason`/`stop_reason`/`finish_reason` ===
52
62
  * `'content_filtered'` (EXT-41 — a distinct `StopReason` enum value from `guardrail_intervened`
53
63
  * that was previously mapped to `null`, i.e. a silent empty turn / false negative).
64
+ * - Gemini's safety reasons ({@link GEMINI_REFUSAL_REASONS}) under **camelCase `finishReason`**
65
+ * (CFG-41). That key is the half that has to come first: `@langchain/google`'s streamed chunk
66
+ * puts `model_provider` and nothing else in `response_metadata` and the reason only in
67
+ * `additional_kwargs.finishReason`, so a token added without the key would sit on a branch that
68
+ * never ran for Gemini.
54
69
  */
55
70
  export declare function detectRefusal(message: unknown): RefusalInfo | null;
56
71
  /**
@@ -66,12 +66,57 @@ function extractRefusalText(message) {
66
66
  return refusalField.trim();
67
67
  return '';
68
68
  }
69
+ /**
70
+ * The finish reasons Gemini uses when its safety system declined, lower-cased.
71
+ *
72
+ * Taken from `@langchain/google`'s own `mapGeminiFinishReason` — every reason that package
73
+ * classifies as a `content_filter`, rather than the two a bug report happened to name. The set is
74
+ * Gemini's, so nothing here can widen another provider's detection.
75
+ *
76
+ * Both spellings of the multi-word members are listed because the provider lists both.
77
+ */
78
+ const GEMINI_REFUSAL_REASONS = new Set([
79
+ 'safety',
80
+ 'recitation',
81
+ 'language',
82
+ 'blocklist',
83
+ 'prohibited_content',
84
+ 'prohibited-content',
85
+ 'spii',
86
+ 'image_safety',
87
+ 'image-safety',
88
+ 'image_prohibited_content',
89
+ 'image-prohibited-content',
90
+ 'image_recitation',
91
+ 'image-recitation',
92
+ ]);
93
+ /**
94
+ * The first string value among `keys` × `sources`, lower-cased — key-major, so an earlier key wins
95
+ * over an earlier source, else `undefined`.
96
+ *
97
+ * Skipping non-strings rather than stopping at the first *present* value is deliberate: a provider
98
+ * that writes a structured value under one spelling must not hide the plain token another spelling
99
+ * carries. Lower-casing here is what lets every comparison below be written once, in one case.
100
+ */
101
+ function firstReasonToken(sources, keys) {
102
+ for (const key of keys) {
103
+ for (const source of sources) {
104
+ const value = readField(source, key);
105
+ if (typeof value === 'string' && value.length > 0)
106
+ return value.toLowerCase();
107
+ }
108
+ }
109
+ return undefined;
110
+ }
69
111
  /**
70
112
  * Inspect a finished model message (an `AIMessage` / `AIMessageChunk`, or any object exposing
71
113
  * `response_metadata` / `additional_kwargs`) and return a {@link RefusalInfo} when its stop/finish
72
114
  * reason indicates a content-policy refusal, else `null`. Defensive: any non-message / unexpected
73
115
  * shape yields `null`, so a normal turn is never mistaken for a refusal.
74
116
  *
117
+ * Reasons are compared lower-cased, because the case is a provider's spelling and not a fact —
118
+ * Gemini shouts `SAFETY` where OpenAI writes `content_filter`.
119
+ *
75
120
  * Covered shapes:
76
121
  * - OpenAI-family `finish_reason: 'content_filter'` (also under `additional_kwargs`).
77
122
  * - Anthropic `stop_reason: 'refusal'`.
@@ -81,20 +126,28 @@ function extractRefusalText(message) {
81
126
  * - Bedrock Converse content filter: `stopReason`/`stop_reason`/`finish_reason` ===
82
127
  * `'content_filtered'` (EXT-41 — a distinct `StopReason` enum value from `guardrail_intervened`
83
128
  * that was previously mapped to `null`, i.e. a silent empty turn / false negative).
129
+ * - Gemini's safety reasons ({@link GEMINI_REFUSAL_REASONS}) under **camelCase `finishReason`**
130
+ * (CFG-41). That key is the half that has to come first: `@langchain/google`'s streamed chunk
131
+ * puts `model_provider` and nothing else in `response_metadata` and the reason only in
132
+ * `additional_kwargs.finishReason`, so a token added without the key would sit on a branch that
133
+ * never ran for Gemini.
84
134
  */
85
135
  export function detectRefusal(message) {
86
136
  if (!message || typeof message !== 'object')
87
137
  return null;
88
138
  const meta = readField(message, 'response_metadata');
89
139
  const kwargs = readField(message, 'additional_kwargs');
90
- // Gather the stop/finish reason from every place providers surface it.
91
- const finishReason = readField(meta, 'finish_reason') ?? readField(kwargs, 'finish_reason') ?? undefined;
92
- const stopReasonSnake = readField(meta, 'stop_reason') ?? readField(kwargs, 'stop_reason') ?? undefined;
93
- const stopReasonCamel = readField(meta, 'stopReason') ?? readField(kwargs, 'stopReason') ?? undefined;
94
- const asString = (v) => (typeof v === 'string' ? v : undefined);
95
- const finish = asString(finishReason);
96
- const stopSnake = asString(stopReasonSnake);
97
- const stopCamel = asString(stopReasonCamel);
140
+ const sources = [meta, kwargs];
141
+ // Gather the stop/finish reason from every place providers surface it, in both spellings.
142
+ const finish = firstReasonToken(sources, ['finish_reason', 'finishReason']);
143
+ // The two stop spellings are gathered as SEPARATE values and every branch below tests both, so
144
+ // each spelling is independently sufficient which is what the covered-shapes list above claims.
145
+ // Collapsing them into one first-match value looks equivalent and is not: a benign `stop_reason`
146
+ // in the same bag would then hide a refusing `stopReason`, turning a detected refusal into a
147
+ // silent `null`. A false negative is the one direction this detector must never acquire, so the
148
+ // three lines a collapse saves are not for sale.
149
+ const stopSnake = firstReasonToken(sources, ['stop_reason']);
150
+ const stopCamel = firstReasonToken(sources, ['stopReason']);
98
151
  const explanation = extractRefusalText(message);
99
152
  // OpenAI-family content filter.
100
153
  if (finish === 'content_filter') {
@@ -115,17 +168,22 @@ export function detectRefusal(message) {
115
168
  // EXT-41 — Bedrock Converse content filter. A distinct `StopReason` enum value from
116
169
  // `guardrail_intervened` (both live in the same AWS Converse `StopReason` enum); previously
117
170
  // unmapped, so a content-filtered turn returned `null` → the silent empty-turn false negative.
118
- if (stopCamel === 'content_filtered' ||
119
- stopSnake === 'content_filtered' ||
171
+ if (stopSnake === 'content_filtered' ||
172
+ stopCamel === 'content_filtered' ||
120
173
  finish === 'content_filtered') {
121
174
  return { provider: 'bedrock', reason: 'content_filtered', explanation };
122
175
  }
176
+ // CFG-41 — Gemini's safety system. Keyed on the finish reason only: these tokens are Gemini's
177
+ // own, and Gemini never spells the reason as a stop reason.
178
+ if (finish !== undefined && GEMINI_REFUSAL_REASONS.has(finish)) {
179
+ return { provider: 'google', reason: finish, explanation };
180
+ }
123
181
  return null;
124
182
  }
125
183
  /**
126
184
  * The provider spellings of "the answer hit the output cap", lower-cased.
127
185
  *
128
- * The same four places {@link detectRefusal} reads carry this one too, spelled per family: OpenAI's
186
+ * The places {@link detectRefusal} reads carry this one too, spelled per family: OpenAI's
129
187
  * `finish_reason: 'length'`, Anthropic's and Bedrock's `stop_reason: 'max_tokens'`, Gemini's
130
188
  * `finishReason: 'MAX_TOKENS'`, Ollama's `done_reason: 'length'`. LangChain normalizes none of it.
131
189
  *
@@ -141,9 +199,17 @@ const TRUNCATION_REASONS = new Set([
141
199
  'model_length',
142
200
  ]);
143
201
  /**
144
- * Every stop/finish reason token a message carries, lower-cased, from both `response_metadata` and
145
- * `additional_kwargs` and in both snake and camel case — the same four places {@link detectRefusal}
146
- * looks, so the two detections can never come to disagree about where the metadata lives.
202
+ * Every stop/finish reason token a message carries, lower-cased, from `response_metadata`,
203
+ * `additional_kwargs` and the message itself, in both snake and camel case.
204
+ *
205
+ * This reader is deliberately **wider** than {@link detectRefusal}'s, and the two differences are
206
+ * the point rather than drift. It also takes `done_reason` (Ollama's spelling, which carries a
207
+ * truncation and never a refusal) and the message's own top level, because a truncation is
208
+ * classified from whatever token is present; a refusal is claimed from a token whose *meaning* is
209
+ * known, so widening its reader would start reporting refusals a provider never declared. What the
210
+ * two must never disagree on is the **spelling** of a key, which is what CFG-41 cost: camelCase
211
+ * `finishReason` was read here and not there, so a Gemini refusal was truncation-classifiable and
212
+ * refusal-invisible at the same time.
147
213
  */
148
214
  function stopReasonTokens(message) {
149
215
  const meta = readField(message, 'response_metadata');
@@ -1 +1 @@
1
- {"version":3,"file":"refusal.js","sourceRoot":"","sources":["../../src/core/refusal.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"refusal.js","sourceRoot":"","sources":["../../src/core/refusal.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @packageDocumentation
3
3
  * GS2-16 — pure, fail-soft helpers that harvest per-run analytics (token usage + invoked tool
4
- * names) from LangChain messages, so the opt-in history recorder can populate `gth insights`
4
+ * names) from LangChain messages, so the local history recorder can populate `gth insights`
5
5
  * with real numbers instead of zeros.
6
6
  *
7
7
  * The extraction is deliberately structural and defensive (duck-typed reads guarded by a
@@ -107,5 +107,13 @@ export declare function terminationLogLine(reason: GthTerminationReason | null):
107
107
  *
108
108
  * Fail-soft in the strongest sense: explaining a failure must never become a second failure, so
109
109
  * every path here is wrapped and a throw is swallowed.
110
+ *
111
+ * **The whole notice, on one stream, at any console level.** Written through
112
+ * {@link displayNotice}, so the title and the body cannot land on different streams and cannot be
113
+ * filtered apart. `gate: 'always'` because {@link terminationCode} is the token a user quotes in a
114
+ * bug report and there is nowhere else they will find it: the session log is off by default and the
115
+ * debug log is not what anyone reads. A notice they cannot recover is not a notice worth quieting —
116
+ * and only a run that ended abnormally reaches here at all, since
117
+ * {@link shouldAnnounceTermination} declines the ordinary ones.
110
118
  */
111
119
  export declare function displayTermination(reason: GthTerminationReason | null): boolean;