@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.
- 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/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,146 @@
|
|
|
1
|
+
import { type CatalogOptions, type ProviderCatalog } from '#src/providers/modelCatalog.js';
|
|
2
|
+
import type { ProviderId } from '#src/providers/modelDiscovery.js';
|
|
3
|
+
/**
|
|
4
|
+
* GS2-59 — default context window (`num_ctx`) for Ollama models. Ollama's OWN default is 4096, but
|
|
5
|
+
* gaunt-sloth's agentic prompt (system + full lean toolset + a tool result) already lands ~4000
|
|
6
|
+
* tokens; at 4096 a thinking model (e.g. gemma4:31b) spends its entire remaining budget on the
|
|
7
|
+
* reasoning field and emits EMPTY `content` on the turn after a tool executes — the GS2-59
|
|
8
|
+
* blank-answer regression. The OpenAI-compat `/v1` shim IGNORES `num_ctx`; the native `/api/chat`
|
|
9
|
+
* path honors it.
|
|
10
|
+
*
|
|
11
|
+
* 16384 is chosen as the largest window that is BOTH safely above the ~4000-token starvation point
|
|
12
|
+
* (4× headroom for reasoning + a few tool results) AND fits constrained consumer VRAM: Ollama
|
|
13
|
+
* preallocates the KV cache at `num_ctx`, so on a box where a large model already spills partly to
|
|
14
|
+
* CPU (e.g. a 19GB model on ~18GB of GPU), a 32768 cache tips the GPU allocation into an
|
|
15
|
+
* out-of-memory error. 16384 was verified live to run the agentic tool→synthesis turn on such a
|
|
16
|
+
* box; 32768 OOM'd it. Overridable per config via `llm.numCtx` — raise it if you have the VRAM and
|
|
17
|
+
* run long sessions, lower it on very tight hardware. NOTE: a per-request `num_ctx` overrides the
|
|
18
|
+
* daemon's `OLLAMA_CONTEXT_LENGTH`, so a user who tuned their server window higher should set
|
|
19
|
+
* `llm.numCtx` to match rather than rely on the server default.
|
|
20
|
+
*
|
|
21
|
+
* It lives here rather than in the provider module because two things now need it — the client the
|
|
22
|
+
* provider builds and the guard that has to know what that client will send — and a second copy is
|
|
23
|
+
* how the guard would come to reason about a window the request does not use.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEFAULT_OLLAMA_NUM_CTX = 16384;
|
|
26
|
+
/**
|
|
27
|
+
* The model's context window in tokens, or `null` when it is not known.
|
|
28
|
+
*
|
|
29
|
+
* Asynchronous because a source may have to ask the provider. Called on every model call, so an
|
|
30
|
+
* implementation that does I/O is expected to memoise; {@link resolveContextWindowSource} does.
|
|
31
|
+
*/
|
|
32
|
+
export type ContextWindowSource = () => Promise<number | null>;
|
|
33
|
+
/** A source that never knows — the honest answer for every provider no source is wired for. */
|
|
34
|
+
export declare const UNKNOWN_CONTEXT_WINDOW: ContextWindowSource;
|
|
35
|
+
/**
|
|
36
|
+
* The fields the window resolver reads off a chat model, without depending on its class.
|
|
37
|
+
*
|
|
38
|
+
* Structural rather than a `ChatOllama` import on purpose: the provider module loads
|
|
39
|
+
* `@langchain/ollama` dynamically so a session that never uses ollama never pays for it, and typing
|
|
40
|
+
* against the class here would undo that.
|
|
41
|
+
*/
|
|
42
|
+
export interface OllamaLikeModel {
|
|
43
|
+
_llmType?: () => string;
|
|
44
|
+
numCtx?: number;
|
|
45
|
+
model?: string;
|
|
46
|
+
baseUrl?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* **The ollama window: what we send, capped by what the model can hold.**
|
|
50
|
+
*
|
|
51
|
+
* The effective window is `numCtx` — the number gaunt-sloth puts on every native `/api/chat`
|
|
52
|
+
* request, which overrides the daemon's own `OLLAMA_CONTEXT_LENGTH` — and never the daemon default,
|
|
53
|
+
* because the request is what decides. It is then capped by the model's own `context_length` from
|
|
54
|
+
* `/api/show`: asking for more than the model has does not buy more room, so guarding against the
|
|
55
|
+
* larger number would let exactly the truncation this guard exists to prevent happen anyway.
|
|
56
|
+
*
|
|
57
|
+
* **Fail-soft in one direction only.** No daemon, a slow daemon, an unreadable answer — the
|
|
58
|
+
* configured number stands, because that is still the number the request will carry. Nothing here
|
|
59
|
+
* throws, and nothing here degrades a known window to unknown.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createOllamaContextWindowSource(llm: OllamaLikeModel): ContextWindowSource;
|
|
62
|
+
/** Where a resolved context window came from — carried so `/status` can say, and a wrong one is
|
|
63
|
+
* diagnosable instead of mysterious. */
|
|
64
|
+
export type ContextWindowOrigin = 'ollama' | 'models.dev' | 'profile' | 'unknown';
|
|
65
|
+
/** A resolved window and its provenance. `tokens: null` means "not known", and never "zero". */
|
|
66
|
+
export interface ContextWindowReading {
|
|
67
|
+
tokens: number | null;
|
|
68
|
+
origin: ContextWindowOrigin;
|
|
69
|
+
}
|
|
70
|
+
/** How each origin is described to a user, in a sentence that says where to go to change it. */
|
|
71
|
+
export declare const CONTEXT_WINDOW_ORIGIN_LABELS: Readonly<Record<ContextWindowOrigin, string>>;
|
|
72
|
+
/**
|
|
73
|
+
* The model's window as the LangChain provider package declares it: `llm.profile.maxInputTokens`.
|
|
74
|
+
*
|
|
75
|
+
* A getter on `BaseLanguageModel` that the base class answers with `{}` and each provider package
|
|
76
|
+
* overrides, so an id its table has never heard of yields `undefined` rather than an error —
|
|
77
|
+
* measured: `gpt-4o-mini` gives 128000 and `mistralai/mistral-7b-instruct` gives nothing. Wrapped
|
|
78
|
+
* in a `try` because it is a getter on someone else's object and a throw here would take down a
|
|
79
|
+
* resolution that has a perfectly good answer to fall back to.
|
|
80
|
+
*/
|
|
81
|
+
export declare function readProfileContextWindow(llm: unknown): number | null;
|
|
82
|
+
/** What {@link resolveContextWindow} needs beyond the model itself — all of it injectable. */
|
|
83
|
+
export interface ContextWindowResolutionOptions {
|
|
84
|
+
/**
|
|
85
|
+
* The gth provider namespace (`anthropic`, `google-genai`, …) — `config.modelProviderType`, NOT
|
|
86
|
+
* the model class's `_llmType()`. The two disagree exactly where it matters: `huggingface`
|
|
87
|
+
* reports `openai`, and both Gemini providers report `google`, so keying the catalog on the
|
|
88
|
+
* class's own label would read the wrong provider's slice or none at all.
|
|
89
|
+
*/
|
|
90
|
+
providerId?: string;
|
|
91
|
+
/** The model id as models.dev keys it — `llm.model`, e.g. `claude-sonnet-4-5`. */
|
|
92
|
+
modelId?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Options threaded to {@link getProviderCatalog} (cache dir, TTL, fetch impl) for hermetic tests.
|
|
95
|
+
*
|
|
96
|
+
* **`cacheOnly` defaults to `true` here and nowhere else.** This resolution sits in front of the
|
|
97
|
+
* first model call of a session, and a cold `api.json` fetch is a few MB behind a 10 s timeout —
|
|
98
|
+
* a delay the user would experience as the agent hanging before it said anything, to decide a
|
|
99
|
+
* threshold that already has a fallback. `gth init` passes `cacheOnly: false` because it is an
|
|
100
|
+
* explicit, interactive step that can afford to wait, and filling the cache there is what makes
|
|
101
|
+
* the runtime read a hit.
|
|
102
|
+
*/
|
|
103
|
+
catalogOptions?: CatalogOptions;
|
|
104
|
+
/** Catalog reader override; {@link getProviderCatalog} when omitted. Injected by the tests. */
|
|
105
|
+
catalogReader?: (_providerId: ProviderId, _options: CatalogOptions) => Promise<ProviderCatalog | null>;
|
|
106
|
+
/** Profile reader override; {@link readProfileContextWindow} when omitted. Injected by the tests. */
|
|
107
|
+
profileReader?: (_llm: unknown) => number | null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* One resolution, read two ways: {@link ResolvedContextWindow.source} for the guard, which wants
|
|
111
|
+
* only the number, and {@link ResolvedContextWindow.read} for `/status`, which also wants the
|
|
112
|
+
* provenance.
|
|
113
|
+
*
|
|
114
|
+
* They are handed out as a pair from a single factory call, over a single memoised promise, so the
|
|
115
|
+
* number `/status` prints is by construction the number the guard enforced. Two independent
|
|
116
|
+
* resolvers could disagree — a stale catalog on one and a fresh fetch on the other — and a
|
|
117
|
+
* `/status` that describes a threshold nobody is using is worse than no `/status` line at all.
|
|
118
|
+
*/
|
|
119
|
+
export interface ResolvedContextWindow {
|
|
120
|
+
/** The window in tokens, or `null` for unknown. Memoised; safe to call before every model call. */
|
|
121
|
+
source: ContextWindowSource;
|
|
122
|
+
/** The same resolution with its provenance attached. Shares the memoised promise. */
|
|
123
|
+
read: () => Promise<ContextWindowReading>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* **The one place a model is matched to a context window.** Tries ollama, then models.dev, then the
|
|
127
|
+
* LangChain profile, and answers `{ tokens: null, origin: 'unknown' }` when none of them knows.
|
|
128
|
+
*
|
|
129
|
+
* **There is deliberately no `?? DEFAULT` anywhere on this path.** One fallback turns every unknown
|
|
130
|
+
* window into a confident wrong number, which is the 4097 failure this file opens by naming.
|
|
131
|
+
*
|
|
132
|
+
* The whole resolution is memoised as a PROMISE, for the reason the ollama source already gives:
|
|
133
|
+
* this runs before every model call, the answer cannot change during a session, and holding the
|
|
134
|
+
* promise rather than the value means concurrent calls share one catalog read instead of racing to
|
|
135
|
+
* make several.
|
|
136
|
+
*/
|
|
137
|
+
export declare function resolveContextWindow(llm: unknown, options?: ContextWindowResolutionOptions): ResolvedContextWindow;
|
|
138
|
+
/**
|
|
139
|
+
* The window as a bare number, for a caller that does not need the provenance.
|
|
140
|
+
*
|
|
141
|
+
* A thin wrapper over {@link resolveContextWindow} rather than a second implementation, so the two
|
|
142
|
+
* cannot answer differently. With no options it consults only the sources that need no
|
|
143
|
+
* configuration — ollama and the profile — which is why a plain object with neither still answers
|
|
144
|
+
* `null`.
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveContextWindowSource(llm: unknown, options?: ContextWindowResolutionOptions): ContextWindowSource;
|
|
@@ -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
|
/**
|