@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.
- package/dist/evermind/index.d.ts +6 -0
- package/dist/evermind/index.d.ts.map +1 -1
- package/dist/evermind/index.js +5 -0
- package/dist/evermind/index.js.map +1 -1
- package/dist/evermind/runtime.d.ts +158 -0
- package/dist/evermind/runtime.d.ts.map +1 -0
- package/dist/evermind/runtime.js +236 -0
- package/dist/evermind/runtime.js.map +1 -0
- package/dist/evermind/textCoherence.d.ts +55 -0
- package/dist/evermind/textCoherence.d.ts.map +1 -0
- package/dist/evermind/textCoherence.js +218 -0
- package/dist/evermind/textCoherence.js.map +1 -0
- package/dist/evermind/toolCall.d.ts +175 -0
- package/dist/evermind/toolCall.d.ts.map +1 -0
- package/dist/evermind/toolCall.js +398 -0
- package/dist/evermind/toolCall.js.map +1 -0
- package/dist/evermind/wordLexicon.d.ts +103 -0
- package/dist/evermind/wordLexicon.d.ts.map +1 -0
- package/dist/evermind/wordLexicon.js +361 -0
- package/dist/evermind/wordLexicon.js.map +1 -0
- package/package.json +3 -3
- package/src/evermind/index.ts +48 -0
- package/src/evermind/runtime.ts +364 -0
- package/src/evermind/textCoherence.ts +248 -0
- package/src/evermind/toolCall.ts +523 -0
- package/src/evermind/wordLexicon.ts +395 -0
|
@@ -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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"}
|