@seanhogg/builderforce-memory 2026.9.31 → 2026.9.32

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.
@@ -0,0 +1,218 @@
1
+ /**
2
+ * textCoherence — pure, zero-dependency text-quality primitives shared by every
3
+ * surface that must decide whether text a project's OWN SSM (Evermind) produced is
4
+ * fit to show a user. Kept dependency-free (no db/env/engine imports) so it can be
5
+ * imported by the memory-first resolver, the model probe and the gateway vendor —
6
+ * and by a browser client — without an import cycle.
7
+ */
8
+ import { detectLatinLanguage, isCodeishToken, isFunctionWord, scoreEnglishWordiness, } from './wordLexicon.js';
9
+ /** A reply shorter than this isn't a real answer — fall through to the LLM. Mirrors
10
+ * the cloud/BrainService threshold so every surface adopts Evermind identically. */
11
+ export const EVERMIND_ANSWER_MIN_CHARS = 20;
12
+ /** Human-readable explanations, one per failure signal. */
13
+ const FAILURE_DETAIL = {
14
+ 'empty': 'the model returned nothing',
15
+ 'replacement-chars': 'the output contains Unicode replacement characters (broken token decoding)',
16
+ 'repetition': 'the decoder is stuck repeating the same word or phrase',
17
+ 'dominant-token': 'the output recycles a handful of words — the head has collapsed onto part of its vocabulary',
18
+ 'unbalanced-delimiters': 'the output closes brackets it never opened',
19
+ 'no-function-words': 'the output has no recognisable function words in any supported language',
20
+ 'non-words': 'most of the content words are not real words — the head is emitting invented tokens',
21
+ };
22
+ /**
23
+ * Cheap coherence gate for text a project's OWN SSM (Evermind) produced. An
24
+ * under-trained head emits fluent-LOOKING garbage — broken UTF-8 (Unicode
25
+ * replacement chars from byte-level BPE), degenerate word repetition ("commit …
26
+ * commit … commit", "in the in the"), and fluent-shaped prose made entirely of
27
+ * INVENTED words — that trivially clears a length check. Serving it makes the
28
+ * assistant reply in gibberish, so this gate rejects it and the caller falls through
29
+ * to a real LLM (a garbled reply IS a memory miss).
30
+ *
31
+ * Signals 1–4 are structural and language-agnostic. Signals 5–6 consult a lexicon and
32
+ * are therefore GUARDED: they run only on predominantly-Latin, non-code text, and the
33
+ * dictionary check only when the text is confidently English — so an es/fr/de/pt/it
34
+ * reply, a CJK reply, or a code block is never judged by an English word list.
35
+ */
36
+ export function assessTextCoherence(text, opts = {}) {
37
+ const t = (text ?? '').trim();
38
+ if (!t)
39
+ return fail('empty');
40
+ // 1) Broken token decoding. A byte-level BPE emitting a low-probability token
41
+ // sequence frequently yields invalid UTF-8 → the replacement char. Real
42
+ // answers effectively never contain it → near-certain garbage.
43
+ if (t.includes('�'))
44
+ return fail('replacement-chars');
45
+ const words = t.toLowerCase().match(/\p{L}+/gu) ?? [];
46
+ // Too few alphabetic tokens to score structurally (CJK collapses to one run); it
47
+ // already cleared the length + replacement-char gates, so accept (a handful of
48
+ // clean words is a real answer).
49
+ if (words.length < 6)
50
+ return pass();
51
+ // 2) Adjacent runaway repetition: a stuck decoder repeats a token ("commit
52
+ // commit") or a bigram ("in the in the"). A little is normal prose.
53
+ let rep = 0;
54
+ for (let i = 1; i < words.length; i++)
55
+ if (words[i] === words[i - 1])
56
+ rep++;
57
+ for (let i = 3; i < words.length; i++) {
58
+ if (words[i] === words[i - 2] && words[i - 1] === words[i - 3])
59
+ rep++;
60
+ }
61
+ if (rep >= 3 && rep / words.length > 0.06)
62
+ return fail('repetition');
63
+ // 3) Content-word collapse: an under-trained head that overfit its corpus stops
64
+ // reaching for new words and recycles a handful ("update update the file update
65
+ // the file update"). Judged on CONTENT words only — grammar is supposed to repeat,
66
+ // and the length proxy this replaces (any token of 3+ letters) counted `the`,
67
+ // `and`, `was` and `not` as content, which is why its floors had to be set so high
68
+ // that they stopped meaning anything on a short reply.
69
+ if (collapsedVocabulary(words))
70
+ return fail('dominant-token');
71
+ // 4) Orphaned closing delimiters. The checks above key on REPETITION, so they miss
72
+ // the other failure mode of an under-trained byte-BPE head: fluent-shaped text made
73
+ // of invented words, each different. What that text does emit is punctuation it
74
+ // never opened — it learned that `)` and `.` follow tokens, not that a bracket has
75
+ // to be opened first. Structural, language-agnostic, and cheap: real prose and real
76
+ // code balance their delimiters. The floor of TWO spares a legitimate stray paren.
77
+ if (unmatchedClosers(t) >= 2)
78
+ return fail('unbalanced-delimiters');
79
+ return assessVocabulary(t, words, opts);
80
+ }
81
+ /**
82
+ * Signals 5–6 — the LEXICAL half of the gate, split out so the guards that make it
83
+ * safe live in one readable place. This is what closes the gap the structural checks
84
+ * cannot see: invented words with balanced punctuation and no repetition.
85
+ *
86
+ * It is skipped entirely unless the text is predominantly Latin-script and mostly
87
+ * prose (not code), because outside that envelope a word list proves nothing.
88
+ */
89
+ function assessVocabulary(t, words, opts) {
90
+ // Raw whitespace tokens paired with their alphabetic core, so the scorer can tell an
91
+ // identifier (`projectEvermindRef`, `api/src/x.ts`, `v2.1`) from a prose word.
92
+ const rawTokens = [];
93
+ const coreWords = [];
94
+ for (const raw of t.split(/\s+/u)) {
95
+ const core = raw
96
+ .replace(/^[^\p{L}]+/u, '')
97
+ .replace(/[^\p{L}]+$/u, '')
98
+ .toLowerCase()
99
+ .replace(/[''’-]/gu, '');
100
+ if (!core || !/^\p{L}+$/u.test(core)) {
101
+ rawTokens.push(raw);
102
+ coreWords.push('');
103
+ continue;
104
+ }
105
+ rawTokens.push(raw);
106
+ coreWords.push(core);
107
+ }
108
+ // Code-dominant text (a diff, a config dump, a stack trace) is legitimately made of
109
+ // non-words — never judge it lexically.
110
+ const codeish = rawTokens.filter((r) => r && isCodeishToken(r)).length;
111
+ if (rawTokens.length > 0 && codeish / rawTokens.length > 0.3)
112
+ return pass();
113
+ const lang = detectLatinLanguage(words, t);
114
+ if (!lang.latin)
115
+ return pass(); // CJK / Cyrillic / Arabic — out of scope by design.
116
+ // 5) No function words at all. Prose in EVERY supported language is ≥ ~10% function
117
+ // words; a long Latin-script, non-code passage with essentially none is not a
118
+ // language. This is the check that catches gibberish too degenerate to even
119
+ // identify a language for — the case the dictionary check below can't reach.
120
+ if (words.length >= 25 && lang.anyShare < 0.04)
121
+ return fail('no-function-words');
122
+ // 6) Invented-word share. English-only: a reply that votes decisively for Spanish,
123
+ // French, German, Portuguese or Italian is never measured against an English
124
+ // lexicon. Everything else — English wins, or nothing wins and English still
125
+ // leads — IS measured, because "no language won" describes gibberish at least as
126
+ // often as it describes prose, and passing it unjudged was the hole that let
127
+ // invented-word output through. The scorer additionally forgives any token that
128
+ // echoes the prompt or recurs in the text, so jargon-dense real answers pass while
129
+ // each-word-different gibberish does not.
130
+ if (!lang.englishLeads)
131
+ return pass();
132
+ const score = scoreEnglishWordiness(coreWords.filter(Boolean), rawTokens.filter((_, i) => !!coreWords[i]), opts.context);
133
+ if (score.scored && score.unknown >= 5 && score.unknownShare >= 0.5) {
134
+ return {
135
+ coherent: false,
136
+ failure: 'non-words',
137
+ detail: `${FAILURE_DETAIL['non-words']} (${score.unknown}/${score.eligible} unrecognised)`,
138
+ };
139
+ }
140
+ return pass();
141
+ }
142
+ const pass = () => ({ coherent: true, failure: null, detail: '' });
143
+ const fail = (failure) => ({ coherent: false, failure, detail: FAILURE_DETAIL[failure] });
144
+ /**
145
+ * Boolean form of {@link assessTextCoherence} — the shape every existing caller uses.
146
+ * Prefer the assessment when you have somewhere to SHOW the reason.
147
+ */
148
+ export function looksLikeCoherentText(text, opts = {}) {
149
+ return assessTextCoherence(text, opts).coherent;
150
+ }
151
+ /** Is this text both substantive (long enough) AND coherent — the ONE bar every serve
152
+ * path applies before showing SSM output to a user. Shared so the length threshold and
153
+ * the gate can never drift apart between the memory resolver, the vendor and the probe. */
154
+ export function isServableText(text, opts = {}) {
155
+ const t = (text ?? '').trim();
156
+ if (t.length < EVERMIND_ANSWER_MIN_CHARS) {
157
+ return { coherent: false, failure: 'empty', detail: `the model returned less than ${EVERMIND_ANSWER_MIN_CHARS} characters` };
158
+ }
159
+ return assessTextCoherence(t, opts);
160
+ }
161
+ /**
162
+ * Has the head stopped reaching for new words? Two independent shapes of the same
163
+ * failure, measured over CONTENT words (see {@link isFunctionWord}) so that ordinary
164
+ * grammar cannot trip either one:
165
+ *
166
+ * - **Too few distinct words for the length.** A reply whose content vocabulary is at
167
+ * most half its content tokens is recycling, not composing. This is what works on a
168
+ * SHORT reply, where any absolute occurrence floor is out of reach: `update update
169
+ * the file update the file update` is 7 content tokens drawn from 2 words.
170
+ * - **One word taking most of the reply.** The long-form shape, where enough distinct
171
+ * words survive to clear the diversity bar but one of them still dominates — the
172
+ * observed `commit … commit … commit` sample sprayed it ~15 times.
173
+ *
174
+ * The dominance arm is measured against CONTENT tokens and needs a body of them, which
175
+ * is what spares a legitimately repetitive real answer: "every commit on that branch is
176
+ * a merge commit, so the commit history reads as one commit per pull request" says
177
+ * `commit` five times and is a perfectly good sentence — the previous rule, which
178
+ * measured against ALL tokens with a fixed floor of 5 occurrences, rejected it.
179
+ */
180
+ function collapsedVocabulary(words) {
181
+ const freq = new Map();
182
+ let tokens = 0;
183
+ for (const w of words) {
184
+ if (w.length < 3 || isFunctionWord(w))
185
+ continue;
186
+ tokens++;
187
+ freq.set(w, (freq.get(w) ?? 0) + 1);
188
+ }
189
+ // Below this there is no vocabulary to have collapsed — a handful of content words is
190
+ // a terse answer, and every ratio over it is noise.
191
+ if (tokens < 5)
192
+ return false;
193
+ if (freq.size / tokens <= 0.5)
194
+ return true;
195
+ let maxCount = 0;
196
+ for (const c of freq.values())
197
+ if (c > maxCount)
198
+ maxCount = c;
199
+ return tokens >= 12 && maxCount / tokens > 0.4;
200
+ }
201
+ /** How many closing brackets appear with no opener before them. Pure counting — a
202
+ * balanced or over-opened text scores 0. Shared by {@link assessTextCoherence}. */
203
+ function unmatchedClosers(text) {
204
+ let depth = 0;
205
+ let orphans = 0;
206
+ for (const ch of text) {
207
+ if (ch === '(' || ch === '[' || ch === '{')
208
+ depth++;
209
+ else if (ch === ')' || ch === ']' || ch === '}') {
210
+ if (depth > 0)
211
+ depth--;
212
+ else
213
+ orphans++;
214
+ }
215
+ }
216
+ return orphans;
217
+ }
218
+ //# sourceMappingURL=textCoherence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textCoherence.js","sourceRoot":"","sources":["../../src/evermind/textCoherence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * toolCall — function calling for Evermind, a project's OWN SSM.
3
+ *
4
+ * Evermind used to REFUSE every tool-bearing request, on the reasoning that a head
5
+ * which generates raw text cannot emit `tool_calls` and that narrating a call is
6
+ * worse than an error. The refusal was right about the failure mode and wrong about
7
+ * the remedy: "can the architecture emit a JSON tool call?" is not a property of the
8
+ * weights at all, it is a property of the DECODING STRATEGY. A tiny under-trained
9
+ * head asked to free-generate `{"name":…,"arguments":{…}}` will indeed produce
10
+ * malformed junk — so this module never asks it to.
11
+ *
12
+ * The approach is schema-driven CONSTRAINED DECODING (the jsonformer/outlines shape):
13
+ * the JSON is ASSEMBLED HERE as a real JS object from the tool's JSON Schema, and the
14
+ * model is only ever consulted for the leaves it is qualified to fill:
15
+ *
16
+ * • a finite choice (which tool, an `enum`, a boolean) → every candidate is SCORED
17
+ * by likelihood and the argmax wins, so the answer is always a legal value;
18
+ * • a free value (a string, a number) → generated, then parsed/escaped by us.
19
+ *
20
+ * Structural validity is therefore guaranteed BY CONSTRUCTION — the braces, quotes,
21
+ * commas and key names are never sampled, so there is no such thing as a malformed
22
+ * Evermind tool call. `JSON.stringify` of a real object cannot be invalid JSON.
23
+ *
24
+ * What that does NOT buy is a good CHOICE, and this is where the original file's
25
+ * instinct is kept rather than discarded. A head that cannot tell `read_file` from
26
+ * `open_pull_request` will still pick one — structurally perfect, semantically a coin
27
+ * flip — and an agent loop acting on coin flips writes files for no reason. So the
28
+ * planner reports the MARGIN between its best and runner-up candidate, and the vendor
29
+ * refuses (400 → cascade) when the head is measurably guessing. Fitness to tool-call
30
+ * is now measured, the same way {@link ./textCoherence} measures fitness to answer in
31
+ * prose, instead of being assumed absent.
32
+ *
33
+ * Pure + port-based (no engine, no R2, no db imports) so the whole planner is unit
34
+ * testable against a fake decoder; the model-backed adapter lives in
35
+ * {@link ./runtime}.
36
+ */
37
+ /** A tool the model may call, normalized out of the OpenAI wire shape. */
38
+ export interface NormalizedTool {
39
+ name: string;
40
+ description: string;
41
+ /** JSON Schema for the arguments object (`{ type:'object', properties, required }`). */
42
+ parameters: JsonSchema;
43
+ }
44
+ /** The slice of JSON Schema this decoder understands. Anything unrecognised is
45
+ * filled as a string, which is always representable. */
46
+ export interface JsonSchema {
47
+ type?: string | string[];
48
+ description?: string;
49
+ enum?: unknown[];
50
+ properties?: Record<string, JsonSchema>;
51
+ required?: string[];
52
+ items?: JsonSchema;
53
+ default?: unknown;
54
+ minimum?: number;
55
+ maximum?: number;
56
+ maxLength?: number;
57
+ minItems?: number;
58
+ maxItems?: number;
59
+ }
60
+ /**
61
+ * What the caller's `tool_choice` resolved to. Mirrors the OpenAI contract:
62
+ * `none` never calls, `auto` lets the model decide, `required` must call something,
63
+ * and a pinned function forces exactly one.
64
+ */
65
+ export type ToolChoicePlan = {
66
+ mode: 'none';
67
+ } | {
68
+ mode: 'auto';
69
+ } | {
70
+ mode: 'required';
71
+ } | {
72
+ mode: 'forced';
73
+ name: string;
74
+ };
75
+ /**
76
+ * The two capabilities the planner needs from a language model. Kept this narrow so
77
+ * the planner has no engine dependency — {@link ./runtime} supplies the
78
+ * EvermindLM-backed implementation and tests supply a deterministic fake.
79
+ */
80
+ export interface EvermindToolDecoder {
81
+ /**
82
+ * Length-normalized mean log-probability of `continuation` following `prompt`
83
+ * (higher = more likely). Length normalization is what makes candidates of
84
+ * different token counts comparable — without it the shortest tool name wins
85
+ * every vote purely for being short.
86
+ */
87
+ score(prompt: string, continuation: string): number;
88
+ /** Free-text continuation of `prompt`, at most `maxTokens` new tokens. */
89
+ generate(prompt: string, maxTokens: number): string;
90
+ }
91
+ /**
92
+ * Minimum separation (in mean log-prob) between the winning candidate and the
93
+ * runner-up for a choice to count as a DECISION rather than a coin flip.
94
+ *
95
+ * This is the tool-calling analogue of the coherence bar: it is what stops a
96
+ * structurally-perfect-but-meaningless call from driving an agent loop. Tuned low
97
+ * on purpose — the job is to catch a head with no preference at all (margins at
98
+ * ~1e-3), not to demand confidence a small model never shows.
99
+ */
100
+ export declare const TOOL_CHOICE_MIN_MARGIN = 0.02;
101
+ /**
102
+ * The margin bar in force for a raw override (the host reads it from its own config —
103
+ * the gateway's `EVERMIND_TOOL_CHOICE_MIN_MARGIN`). ONE resolver, shared by the
104
+ * serve-time gate and the bench, so the bench can never report a bar the gateway is
105
+ * not applying.
106
+ *
107
+ * The default is an analytic placeholder and documented as one; the override is what
108
+ * lets it be CALIBRATED from logged margins without a deploy per candidate value.
109
+ */
110
+ export declare function resolveToolChoiceMinMargin(override?: string | null): number;
111
+ /**
112
+ * Normalize the OpenAI `tools` array into {@link NormalizedTool}s, accepting both the
113
+ * nested chat-completions shape (`{ type:'function', function:{…} }`) and the already
114
+ * flattened Responses shape (`{ type:'function', name, … }`) — the gateway carries
115
+ * both, and a vendor that understood only one would silently see zero tools.
116
+ * Entries without a usable name are dropped rather than half-registered.
117
+ */
118
+ export declare function normalizeEvermindTools(tools: unknown[] | undefined): NormalizedTool[];
119
+ /**
120
+ * Resolve the caller's `tool_choice` against the tools actually on offer. A pinned
121
+ * function that isn't in the list degrades to `required` rather than forcing a name
122
+ * the caller never declared (which would produce a call no agent loop can dispatch).
123
+ */
124
+ export declare function resolveEvermindToolChoice(toolChoice: unknown, tools: NormalizedTool[]): ToolChoicePlan;
125
+ /**
126
+ * The tool catalogue, rendered into the prompt so the head's choice is INFORMED by
127
+ * the descriptions rather than made from the tool names alone. Shared by the decision
128
+ * vote and every argument fill so the model sees one consistent context.
129
+ */
130
+ export declare function renderToolsPreamble(tools: NormalizedTool[]): string;
131
+ /** One planned call. */
132
+ export interface EvermindPlannedCall {
133
+ name: string;
134
+ arguments: Record<string, unknown>;
135
+ }
136
+ /** A planned call plus the evidence for how confidently it was chosen. */
137
+ export interface EvermindToolPlan {
138
+ /** The FIRST chosen call, or null when the head elected to answer in prose
139
+ * (`auto` only). Kept as the single-call view every existing reader uses;
140
+ * {@link EvermindToolPlan.calls} is the full list. */
141
+ call: EvermindPlannedCall | null;
142
+ /**
143
+ * Every call this turn emits, in order.
144
+ *
145
+ * The OpenAI shape has always been an ARRAY and frontier models emit parallel
146
+ * calls; Evermind used to plan exactly one, so a caller that relies on parallel
147
+ * calls silently got serialized behaviour. After the first call the head is asked
148
+ * "another call?" — the same continue-vote the array filler already uses for list
149
+ * length — so a model with nothing more to do still emits exactly one.
150
+ * Empty when `call` is null.
151
+ */
152
+ calls: EvermindPlannedCall[];
153
+ /**
154
+ * Separation between the winning tool and the runner-up. The vendor gates on this:
155
+ * a head with no preference is guessing, and a guessed tool call is exactly the
156
+ * "silently doing the wrong thing" this backend refuses to do.
157
+ * `Infinity` when the choice was forced or unopposed (nothing was guessed).
158
+ */
159
+ margin: number;
160
+ }
161
+ /**
162
+ * Choose a tool and fill its arguments under the caller's `tool_choice` contract.
163
+ *
164
+ * The decision and every argument come from the model; the JSON structure comes from
165
+ * the schema. Returns `{ call: null }` only in `auto` mode, and only when answering
166
+ * directly out-scored every tool — `required`/`forced` always produce a call.
167
+ */
168
+ export declare function planEvermindToolCall(decoder: EvermindToolDecoder, basePrompt: string, tools: NormalizedTool[], choice: ToolChoicePlan): EvermindToolPlan;
169
+ /** Wrap a planned call in the OpenAI `tool_calls` wire shape. `id` is supplied by the
170
+ * caller so the completion builder controls id generation (and tests stay stable). */
171
+ export declare function toOpenAIToolCall(call: {
172
+ name: string;
173
+ arguments: Record<string, unknown>;
174
+ }, id: string): Record<string, unknown>;
175
+ //# sourceMappingURL=toolCall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toolCall.d.ts","sourceRoot":"","sources":["../../src/evermind/toolCall.ts"],"names":[],"mappings":"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"}