@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,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"}
|
package/dist/core/refusal.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
/**
|
package/dist/core/refusal.js
CHANGED
|
@@ -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
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 (
|
|
119
|
-
|
|
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
|
|
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
|
|
145
|
-
* `additional_kwargs` and in both snake and camel case
|
|
146
|
-
*
|
|
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');
|
package/dist/core/refusal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refusal.js","sourceRoot":"","sources":["../../src/core/refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"refusal.js","sourceRoot":"","sources":["../../src/core/refusal.ts"],"names":[],"mappings":"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"}
|
package/dist/core/runStats.d.ts
CHANGED
|
@@ -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
|
|
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;
|