@juspay/neurolink 10.3.0 → 10.4.0
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/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +430 -417
- package/dist/cli/loop/optionsSchema.js +4 -0
- package/dist/constants/contextWindows.d.ts +24 -0
- package/dist/constants/contextWindows.js +50 -0
- package/dist/context/errorDetection.d.ts +6 -0
- package/dist/context/errorDetection.js +18 -0
- package/dist/context/stepBudgetGuard.d.ts +17 -3
- package/dist/context/stepBudgetGuard.js +53 -14
- package/dist/core/baseProvider.d.ts +12 -0
- package/dist/core/baseProvider.js +18 -0
- package/dist/core/modules/GenerationHandler.js +208 -42
- package/dist/core/modules/ToolsManager.d.ts +17 -0
- package/dist/core/modules/ToolsManager.js +99 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/knowledge/context.d.ts +18 -0
- package/dist/knowledge/context.js +91 -0
- package/dist/knowledge/defaults.d.ts +24 -0
- package/dist/knowledge/defaults.js +29 -0
- package/dist/knowledge/engine.d.ts +35 -0
- package/dist/knowledge/engine.js +179 -0
- package/dist/knowledge/index.d.ts +15 -0
- package/dist/knowledge/index.js +15 -0
- package/dist/knowledge/indexCache.d.ts +19 -0
- package/dist/knowledge/indexCache.js +109 -0
- package/dist/knowledge/knowledgeIndex.d.ts +41 -0
- package/dist/knowledge/knowledgeIndex.js +204 -0
- package/dist/knowledge/normalize.d.ts +32 -0
- package/dist/knowledge/normalize.js +74 -0
- package/dist/knowledge/resolve.d.ts +18 -0
- package/dist/knowledge/resolve.js +156 -0
- package/dist/knowledge/retrieval.d.ts +16 -0
- package/dist/knowledge/retrieval.js +221 -0
- package/dist/lib/constants/contextWindows.d.ts +24 -0
- package/dist/lib/constants/contextWindows.js +50 -0
- package/dist/lib/context/errorDetection.d.ts +6 -0
- package/dist/lib/context/errorDetection.js +18 -0
- package/dist/lib/context/stepBudgetGuard.d.ts +17 -3
- package/dist/lib/context/stepBudgetGuard.js +53 -14
- package/dist/lib/core/baseProvider.d.ts +12 -0
- package/dist/lib/core/baseProvider.js +18 -0
- package/dist/lib/core/modules/GenerationHandler.js +208 -42
- package/dist/lib/core/modules/ToolsManager.d.ts +17 -0
- package/dist/lib/core/modules/ToolsManager.js +99 -1
- package/dist/lib/files/fileTools.d.ts +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +3 -0
- package/dist/lib/knowledge/context.d.ts +18 -0
- package/dist/lib/knowledge/context.js +92 -0
- package/dist/lib/knowledge/defaults.d.ts +24 -0
- package/dist/lib/knowledge/defaults.js +30 -0
- package/dist/lib/knowledge/engine.d.ts +35 -0
- package/dist/lib/knowledge/engine.js +180 -0
- package/dist/lib/knowledge/index.d.ts +15 -0
- package/dist/lib/knowledge/index.js +16 -0
- package/dist/lib/knowledge/indexCache.d.ts +19 -0
- package/dist/lib/knowledge/indexCache.js +110 -0
- package/dist/lib/knowledge/knowledgeIndex.d.ts +41 -0
- package/dist/lib/knowledge/knowledgeIndex.js +205 -0
- package/dist/lib/knowledge/normalize.d.ts +32 -0
- package/dist/lib/knowledge/normalize.js +75 -0
- package/dist/lib/knowledge/resolve.d.ts +18 -0
- package/dist/lib/knowledge/resolve.js +157 -0
- package/dist/lib/knowledge/retrieval.d.ts +16 -0
- package/dist/lib/knowledge/retrieval.js +222 -0
- package/dist/lib/mcp/toolDiscoveryService.js +59 -20
- package/dist/lib/neurolink.d.ts +14 -1
- package/dist/lib/neurolink.js +142 -12
- package/dist/lib/providers/litellm.d.ts +27 -19
- package/dist/lib/providers/litellm.js +171 -92
- package/dist/lib/providers/openaiChatCompletionsBase.d.ts +37 -1
- package/dist/lib/providers/openaiChatCompletionsBase.js +201 -33
- package/dist/lib/providers/openaiChatCompletionsClient.d.ts +23 -5
- package/dist/lib/providers/openaiChatCompletionsClient.js +94 -14
- package/dist/lib/proxy/proxyFetch.d.ts +17 -0
- package/dist/lib/proxy/proxyFetch.js +42 -4
- package/dist/lib/types/config.d.ts +12 -0
- package/dist/lib/types/conversation.d.ts +1 -1
- package/dist/lib/types/dynamic.d.ts +12 -0
- package/dist/lib/types/generate.d.ts +32 -2
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/knowledge.d.ts +342 -0
- package/dist/lib/types/knowledge.js +30 -0
- package/dist/lib/types/openaiCompatible.d.ts +2 -0
- package/dist/lib/types/providers.d.ts +9 -0
- package/dist/lib/types/stream.d.ts +14 -0
- package/dist/lib/utils/errorHandling.js +8 -2
- package/dist/lib/utils/schemaConversion.d.ts +16 -0
- package/dist/lib/utils/schemaConversion.js +165 -8
- package/dist/lib/utils/timeout.d.ts +22 -0
- package/dist/lib/utils/timeout.js +72 -12
- package/dist/lib/utils/tokenLimits.js +22 -0
- package/dist/lib/utils/toolCallRepair.d.ts +8 -0
- package/dist/lib/utils/toolCallRepair.js +4 -1
- package/dist/mcp/toolDiscoveryService.js +59 -20
- package/dist/neurolink.d.ts +14 -1
- package/dist/neurolink.js +142 -12
- package/dist/providers/litellm.d.ts +27 -19
- package/dist/providers/litellm.js +171 -92
- package/dist/providers/openaiChatCompletionsBase.d.ts +37 -1
- package/dist/providers/openaiChatCompletionsBase.js +201 -33
- package/dist/providers/openaiChatCompletionsClient.d.ts +23 -5
- package/dist/providers/openaiChatCompletionsClient.js +94 -14
- package/dist/proxy/proxyFetch.d.ts +17 -0
- package/dist/proxy/proxyFetch.js +42 -4
- package/dist/types/config.d.ts +12 -0
- package/dist/types/conversation.d.ts +1 -1
- package/dist/types/dynamic.d.ts +12 -0
- package/dist/types/generate.d.ts +32 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/knowledge.d.ts +342 -0
- package/dist/types/knowledge.js +29 -0
- package/dist/types/openaiCompatible.d.ts +2 -0
- package/dist/types/providers.d.ts +9 -0
- package/dist/types/stream.d.ts +14 -0
- package/dist/utils/errorHandling.js +8 -2
- package/dist/utils/schemaConversion.d.ts +16 -0
- package/dist/utils/schemaConversion.js +165 -8
- package/dist/utils/timeout.d.ts +22 -0
- package/dist/utils/timeout.js +72 -12
- package/dist/utils/tokenLimits.js +22 -0
- package/dist/utils/toolCallRepair.d.ts +8 -0
- package/dist/utils/toolCallRepair.js +4 -1
- package/package.json +4 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic text normalization for knowledge grounding.
|
|
3
|
+
*
|
|
4
|
+
* The SAME normalizer runs at index-build time and query time so that a
|
|
5
|
+
* configuration key, a technical identifier, and a user's phrasing all
|
|
6
|
+
* collapse to the same comparable token stream:
|
|
7
|
+
*
|
|
8
|
+
* "Multi-Step Flow" -> "multi step flow"
|
|
9
|
+
* "enableMultiStepFlow" -> "enable multi step flow"
|
|
10
|
+
* "ACCOUNT_STATUS" -> "account status"
|
|
11
|
+
* " Access Policy " -> "access policy"
|
|
12
|
+
*
|
|
13
|
+
* The pipeline is intentionally conservative: it splits identifiers and
|
|
14
|
+
* separators and folds case/diacritics, but it NEVER invents synonyms.
|
|
15
|
+
* Reviewed acronym and spelling expansions belong in an entry's `aliases`,
|
|
16
|
+
* not in this function.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Normalize an arbitrary string into a single space-delimited lowercase
|
|
20
|
+
* phrase. Idempotent: `normalizeText(normalizeText(x)) === normalizeText(x)`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const normalizeText: (input: string) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Normalize then split into tokens. Empty input yields an empty array.
|
|
25
|
+
* Numbers and alphanumeric identifiers are preserved as their own tokens.
|
|
26
|
+
*/
|
|
27
|
+
export declare const tokenize: (input: string) => string[];
|
|
28
|
+
/**
|
|
29
|
+
* Normalize each candidate phrase and drop blanks/duplicates while preserving
|
|
30
|
+
* first-seen order. Used to build the exact-key and alias key sets.
|
|
31
|
+
*/
|
|
32
|
+
export declare const normalizePhrases: (phrases: string[]) => string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic text normalization for knowledge grounding.
|
|
3
|
+
*
|
|
4
|
+
* The SAME normalizer runs at index-build time and query time so that a
|
|
5
|
+
* configuration key, a technical identifier, and a user's phrasing all
|
|
6
|
+
* collapse to the same comparable token stream:
|
|
7
|
+
*
|
|
8
|
+
* "Multi-Step Flow" -> "multi step flow"
|
|
9
|
+
* "enableMultiStepFlow" -> "enable multi step flow"
|
|
10
|
+
* "ACCOUNT_STATUS" -> "account status"
|
|
11
|
+
* " Access Policy " -> "access policy"
|
|
12
|
+
*
|
|
13
|
+
* The pipeline is intentionally conservative: it splits identifiers and
|
|
14
|
+
* separators and folds case/diacritics, but it NEVER invents synonyms.
|
|
15
|
+
* Reviewed acronym and spelling expansions belong in an entry's `aliases`,
|
|
16
|
+
* not in this function.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Insert spaces at camelCase / PascalCase / acronym boundaries. Must run
|
|
20
|
+
* BEFORE lowercasing because it relies on case information.
|
|
21
|
+
*
|
|
22
|
+
* "enableURLReading" -> "enable URL Reading"
|
|
23
|
+
* "customAccessOptionsV2" -> "custom Access Options V2"
|
|
24
|
+
*/
|
|
25
|
+
const splitIdentifierBoundaries = (input) => input
|
|
26
|
+
// lower/number followed by an uppercase: "enableTwo" -> "enable Two"
|
|
27
|
+
.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, "$1 $2")
|
|
28
|
+
// acronym run followed by a Word: "URLReading" -> "URL Reading"
|
|
29
|
+
.replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, "$1 $2");
|
|
30
|
+
/**
|
|
31
|
+
* Normalize an arbitrary string into a single space-delimited lowercase
|
|
32
|
+
* phrase. Idempotent: `normalizeText(normalizeText(x)) === normalizeText(x)`.
|
|
33
|
+
*/
|
|
34
|
+
export const normalizeText = (input) => {
|
|
35
|
+
if (!input) {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
const boundaries = splitIdentifierBoundaries(
|
|
39
|
+
// NFKC folds compatibility forms; NFD + mark-strip removes diacritics.
|
|
40
|
+
input
|
|
41
|
+
.normalize("NFKC")
|
|
42
|
+
.normalize("NFD")
|
|
43
|
+
.replace(/\p{M}+/gu, ""));
|
|
44
|
+
return (boundaries
|
|
45
|
+
// any run of non-letter/non-number (snake_case, kebab-case, dots, slashes,
|
|
46
|
+
// punctuation, and existing whitespace) collapses to a single space.
|
|
47
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
48
|
+
.trim()
|
|
49
|
+
.toLowerCase());
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Normalize then split into tokens. Empty input yields an empty array.
|
|
53
|
+
* Numbers and alphanumeric identifiers are preserved as their own tokens.
|
|
54
|
+
*/
|
|
55
|
+
export const tokenize = (input) => {
|
|
56
|
+
const normalized = normalizeText(input);
|
|
57
|
+
return normalized ? normalized.split(" ") : [];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Normalize each candidate phrase and drop blanks/duplicates while preserving
|
|
61
|
+
* first-seen order. Used to build the exact-key and alias key sets.
|
|
62
|
+
*/
|
|
63
|
+
export const normalizePhrases = (phrases) => {
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const phrase of phrases) {
|
|
67
|
+
const normalized = normalizeText(phrase);
|
|
68
|
+
if (normalized && !seen.has(normalized)) {
|
|
69
|
+
seen.add(normalized);
|
|
70
|
+
out.push(normalized);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { KnowledgeEntryInput, KnowledgeManifest, KnowledgeNormalizeOptions, KnowledgeNormalizeResult, KnowledgeSource, NormalizedKnowledgeEntry } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve one authored entry into a complete `NormalizedKnowledgeEntry`,
|
|
4
|
+
* materializing every omitted optional (optional arrays to `[]`, `body` to "",
|
|
5
|
+
* `kind` to "text", `status` to "active") so downstream code never re-checks
|
|
6
|
+
* it. Authored arrays are cloned so the normalized snapshot is isolated from
|
|
7
|
+
* caller-owned references.
|
|
8
|
+
*/
|
|
9
|
+
export declare const resolveEntry: (input: KnowledgeEntryInput, version: string) => NormalizedKnowledgeEntry;
|
|
10
|
+
/** Expand a build manifest into sources — one per catalog. */
|
|
11
|
+
export declare const manifestToSources: (manifest: KnowledgeManifest) => KnowledgeSource[];
|
|
12
|
+
/**
|
|
13
|
+
* Resolve and validate every source into normalized entries. `validation.ok`
|
|
14
|
+
* is false when any error-level issue was found; the caller (index builder)
|
|
15
|
+
* must not swap in an index built from an invalid set. Async so the public
|
|
16
|
+
* contract is stable if a future source kind needs asynchronous loading.
|
|
17
|
+
*/
|
|
18
|
+
export declare const normalizeAndValidate: (sources: KnowledgeSource[], options: KnowledgeNormalizeOptions) => Promise<KnowledgeNormalizeResult>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source loading, default resolution, and generic validation.
|
|
3
|
+
*
|
|
4
|
+
* Required fields (id/title/summary/domain/integrations) come straight
|
|
5
|
+
* from the authored entry; omitted optionals fall back to SDK defaults. This
|
|
6
|
+
* covers only provider-neutral invariants (ids, required fields, enum values,
|
|
7
|
+
* relationship integrity). Host-specific rules — configuration-key coverage,
|
|
8
|
+
* taxonomy conflicts — are enforced by the host build BEFORE the manifest
|
|
9
|
+
* reaches NeuroLink. Errors block indexing; warnings are surfaced but do not.
|
|
10
|
+
*/
|
|
11
|
+
import { decodeArray, decodeString } from "type-decoder/dist/index.js";
|
|
12
|
+
import { normalizeText } from "./normalize.js";
|
|
13
|
+
const SDK_DEFAULT_KIND = "text";
|
|
14
|
+
const SDK_DEFAULT_STATUS = "active";
|
|
15
|
+
const STATUSES = new Set(["draft", "active", "deprecated"]);
|
|
16
|
+
const KINDS = new Set([
|
|
17
|
+
"concept",
|
|
18
|
+
"text",
|
|
19
|
+
"configuration",
|
|
20
|
+
"tool",
|
|
21
|
+
"procedure",
|
|
22
|
+
"policy",
|
|
23
|
+
"troubleshooting",
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Resolve one authored entry into a complete `NormalizedKnowledgeEntry`,
|
|
27
|
+
* materializing every omitted optional (optional arrays to `[]`, `body` to "",
|
|
28
|
+
* `kind` to "text", `status` to "active") so downstream code never re-checks
|
|
29
|
+
* it. Authored arrays are cloned so the normalized snapshot is isolated from
|
|
30
|
+
* caller-owned references.
|
|
31
|
+
*/
|
|
32
|
+
export const resolveEntry = (input, version) => ({
|
|
33
|
+
id: input.id,
|
|
34
|
+
title: input.title,
|
|
35
|
+
summary: input.summary,
|
|
36
|
+
domain: input.domain,
|
|
37
|
+
integrations: decodeArray(input.integrations, decodeString) ?? [],
|
|
38
|
+
kind: input.kind ?? SDK_DEFAULT_KIND,
|
|
39
|
+
status: input.status ?? SDK_DEFAULT_STATUS,
|
|
40
|
+
body: input.body ?? "",
|
|
41
|
+
aliases: [...(input.aliases ?? [])],
|
|
42
|
+
keywords: [...(input.keywords ?? [])],
|
|
43
|
+
relatedEntryIds: [...(input.relatedEntryIds ?? [])],
|
|
44
|
+
parentEntryId: input.parentEntryId,
|
|
45
|
+
version,
|
|
46
|
+
});
|
|
47
|
+
/** Expand a build manifest into sources — one per catalog. */
|
|
48
|
+
export const manifestToSources = (manifest) => manifest.catalogs.map((catalog) => ({
|
|
49
|
+
id: catalog.id,
|
|
50
|
+
version: manifest.contentVersion,
|
|
51
|
+
entries: catalog.entries,
|
|
52
|
+
}));
|
|
53
|
+
/** Resolve a source's entries and its effective version. */
|
|
54
|
+
const loadSourceEntries = (source, manifestVersion) => ({
|
|
55
|
+
id: source.id,
|
|
56
|
+
version: source.version ?? manifestVersion,
|
|
57
|
+
entries: source.entries,
|
|
58
|
+
});
|
|
59
|
+
const error = (code, message, extra) => ({ level: "error", code, message, ...extra });
|
|
60
|
+
const warn = (code, message, extra) => ({ level: "warning", code, message, ...extra });
|
|
61
|
+
/**
|
|
62
|
+
* Resolve and validate every source into normalized entries. `validation.ok`
|
|
63
|
+
* is false when any error-level issue was found; the caller (index builder)
|
|
64
|
+
* must not swap in an index built from an invalid set. Async so the public
|
|
65
|
+
* contract is stable if a future source kind needs asynchronous loading.
|
|
66
|
+
*/
|
|
67
|
+
export const normalizeAndValidate = async (sources, options) => {
|
|
68
|
+
const issues = [];
|
|
69
|
+
const entries = [];
|
|
70
|
+
const seenIds = new Set();
|
|
71
|
+
const aliasOwner = new Map();
|
|
72
|
+
for (const source of sources) {
|
|
73
|
+
const loaded = loadSourceEntries(source, options.manifestVersion);
|
|
74
|
+
if (!loaded.id) {
|
|
75
|
+
issues.push(error("missing-source-id", "Source has no id"));
|
|
76
|
+
}
|
|
77
|
+
for (const input of loaded.entries) {
|
|
78
|
+
if (!input.id) {
|
|
79
|
+
issues.push(error("missing-entry-id", "Entry has no id", { sourceId: loaded.id }));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (seenIds.has(input.id)) {
|
|
83
|
+
issues.push(error("duplicate-entry-id", `Duplicate entry id "${input.id}"`, {
|
|
84
|
+
entryId: input.id,
|
|
85
|
+
}));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
seenIds.add(input.id);
|
|
89
|
+
if (!input.title || !input.title.trim()) {
|
|
90
|
+
issues.push(error("missing-title", "Entry is missing a title", {
|
|
91
|
+
entryId: input.id,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
if (!input.summary || !input.summary.trim()) {
|
|
95
|
+
issues.push(error("missing-summary", "Entry is missing a summary", {
|
|
96
|
+
entryId: input.id,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
if (!input.domain || !input.domain.trim()) {
|
|
100
|
+
issues.push(error("missing-domain", "Entry is missing a domain", {
|
|
101
|
+
entryId: input.id,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
if (input.integrations === undefined) {
|
|
105
|
+
issues.push(error("missing-integrations", "Entry is missing integrations", {
|
|
106
|
+
entryId: input.id,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
else if (decodeArray(input.integrations, decodeString) === null) {
|
|
110
|
+
issues.push(error("bad-integrations", "Entry integrations must be an array of strings", { entryId: input.id }));
|
|
111
|
+
}
|
|
112
|
+
const normalized = resolveEntry(input, loaded.version);
|
|
113
|
+
if (!KINDS.has(normalized.kind)) {
|
|
114
|
+
issues.push(error("bad-kind", `Unsupported kind "${normalized.kind}"`, {
|
|
115
|
+
entryId: normalized.id,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
if (!STATUSES.has(normalized.status)) {
|
|
119
|
+
issues.push(error("bad-status", `Unsupported status "${normalized.status}"`, {
|
|
120
|
+
entryId: normalized.id,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
for (const alias of normalized.aliases) {
|
|
124
|
+
const key = normalizeText(alias);
|
|
125
|
+
if (!key) {
|
|
126
|
+
issues.push(warn("empty-alias", `Alias "${alias}" is empty after normalization`, { entryId: normalized.id }));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const owner = aliasOwner.get(key);
|
|
130
|
+
if (owner && owner !== normalized.id) {
|
|
131
|
+
issues.push(warn("duplicate-alias", `Alias "${alias}" also used by "${owner}"`, { entryId: normalized.id }));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
aliasOwner.set(key, normalized.id);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
entries.push(normalized);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Relationship integrity — resolved after all entries are collected.
|
|
141
|
+
const idSet = new Set(entries.map((entry) => entry.id));
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
for (const related of entry.relatedEntryIds) {
|
|
144
|
+
if (!idSet.has(related)) {
|
|
145
|
+
issues.push(warn("broken-relation", `relatedEntryId "${related}" not found`, {
|
|
146
|
+
entryId: entry.id,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (entry.parentEntryId && !idSet.has(entry.parentEntryId)) {
|
|
151
|
+
issues.push(warn("broken-parent", `parentEntryId "${entry.parentEntryId}" not found`, { entryId: entry.id }));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const ok = !issues.some((issue) => issue.level === "error");
|
|
155
|
+
return { entries, validation: { ok, issues } };
|
|
156
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexical-first retrieval pipeline.
|
|
3
|
+
*
|
|
4
|
+
* For one turn: authorize/filter BEFORE ranking, resolve exact identifiers and
|
|
5
|
+
* reviewed aliases via query n-gram lookup, add field-aware BM25 over a bounded
|
|
6
|
+
* query+history window, combine the deterministic signals, take the top
|
|
7
|
+
* candidates, expand a bounded set of directly-related entries, and classify
|
|
8
|
+
* confidence. No LLM, no embeddings, no vector store.
|
|
9
|
+
*/
|
|
10
|
+
import type { KnowledgeIndexSnapshot, KnowledgeResolvedRetrieval, KnowledgeRetrievalRequest, KnowledgeSelection } from "../types/index.js";
|
|
11
|
+
/**
|
|
12
|
+
* Run retrieval against a ready snapshot. Returns the primary selection, a
|
|
13
|
+
* bounded relationship expansion, the scored candidate list (diagnostics), and
|
|
14
|
+
* a confidence class. Context assembly is a separate, later step.
|
|
15
|
+
*/
|
|
16
|
+
export declare const retrieve: (snapshot: KnowledgeIndexSnapshot, request: KnowledgeRetrievalRequest, config: KnowledgeResolvedRetrieval, blockedDomains?: string[]) => KnowledgeSelection;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lexical-first retrieval pipeline.
|
|
3
|
+
*
|
|
4
|
+
* For one turn: authorize/filter BEFORE ranking, resolve exact identifiers and
|
|
5
|
+
* reviewed aliases via query n-gram lookup, add field-aware BM25 over a bounded
|
|
6
|
+
* query+history window, combine the deterministic signals, take the top
|
|
7
|
+
* candidates, expand a bounded set of directly-related entries, and classify
|
|
8
|
+
* confidence. No LLM, no embeddings, no vector store.
|
|
9
|
+
*/
|
|
10
|
+
import { tokenize } from "./normalize.js";
|
|
11
|
+
/** Per-turn history text cap so a long prior turn cannot dominate the query. */
|
|
12
|
+
const HISTORY_TURN_CHARS = 400;
|
|
13
|
+
/** All contiguous token n-grams (length 1..maxLen), de-duplicated. */
|
|
14
|
+
const generateNGrams = (tokens, maxLen) => {
|
|
15
|
+
const grams = [];
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
const limit = Math.min(maxLen, tokens.length);
|
|
18
|
+
for (let size = 1; size <= limit; size += 1) {
|
|
19
|
+
for (let start = 0; start + size <= tokens.length; start += 1) {
|
|
20
|
+
const gram = tokens.slice(start, start + size).join(" ");
|
|
21
|
+
if (!seen.has(gram)) {
|
|
22
|
+
seen.add(gram);
|
|
23
|
+
grams.push(gram);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return grams;
|
|
28
|
+
};
|
|
29
|
+
/** Longest indexed exact/alias phrase length, derived from the active snapshot. */
|
|
30
|
+
const getMaxIndexedPhraseLen = (...indexes) => {
|
|
31
|
+
let maxLen = 0;
|
|
32
|
+
for (const index of indexes) {
|
|
33
|
+
for (const phrase of index.keys()) {
|
|
34
|
+
const length = phrase === "" ? 0 : phrase.split(" ").length;
|
|
35
|
+
maxLen = Math.max(maxLen, length);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return maxLen;
|
|
39
|
+
};
|
|
40
|
+
/** Look each n-gram up in a phrase index; returns entry id -> matched phrases. */
|
|
41
|
+
const lookupPhrases = (grams, index) => {
|
|
42
|
+
const hits = new Map();
|
|
43
|
+
for (const gram of grams) {
|
|
44
|
+
const ids = index.get(gram);
|
|
45
|
+
if (!ids) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const id of ids) {
|
|
49
|
+
const matched = hits.get(id) ?? [];
|
|
50
|
+
matched.push(gram);
|
|
51
|
+
hits.set(id, matched);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return hits;
|
|
55
|
+
};
|
|
56
|
+
/** Tokens fed to BM25: the current query plus a bounded recent-turn window. */
|
|
57
|
+
const buildLexicalTokens = (request) => {
|
|
58
|
+
const parts = [request.query];
|
|
59
|
+
for (const turn of request.recentTurns) {
|
|
60
|
+
parts.push(turn.text.slice(0, HISTORY_TURN_CHARS));
|
|
61
|
+
}
|
|
62
|
+
return parts.flatMap((part) => tokenize(part));
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Authorization + metadata filter. Applied BEFORE ranking so restricted or
|
|
66
|
+
* disabled-integration content never enters candidates, traces, or model context.
|
|
67
|
+
*/
|
|
68
|
+
const isAuthorized = (entry, request, blockedDomains) => {
|
|
69
|
+
if (entry.status !== "active") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (blockedDomains && blockedDomains.includes(entry.domain)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (entry.integrations.length > 0) {
|
|
76
|
+
const active = new Set();
|
|
77
|
+
for (const integration of request.enabledIntegrations) {
|
|
78
|
+
active.add(integration.toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
if (!entry.integrations.some((integration) => active.has(integration.toLowerCase()))) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
};
|
|
86
|
+
const compareCandidates = (left, right) => {
|
|
87
|
+
if (right.score !== left.score) {
|
|
88
|
+
return right.score - left.score;
|
|
89
|
+
}
|
|
90
|
+
if (left.id < right.id) {
|
|
91
|
+
return -1;
|
|
92
|
+
}
|
|
93
|
+
if (left.id > right.id) {
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
return 0;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Confidence class. `high` requires an exact/alias hit at the top; otherwise a
|
|
100
|
+
* multi-field or dominant lexical top is `medium`, a single weak signal is
|
|
101
|
+
* `low`, and no candidates is `none`. Thresholds are pre-tuning heuristics.
|
|
102
|
+
*/
|
|
103
|
+
const classifyConfidence = (candidates) => {
|
|
104
|
+
if (candidates.length === 0) {
|
|
105
|
+
return "none";
|
|
106
|
+
}
|
|
107
|
+
const [top, second] = candidates;
|
|
108
|
+
if (top.exact || top.alias) {
|
|
109
|
+
return "high";
|
|
110
|
+
}
|
|
111
|
+
const fieldsMatched = Object.keys(top.fieldScores).length;
|
|
112
|
+
const dominant = second ? top.score >= 1.5 * second.score : false;
|
|
113
|
+
if (fieldsMatched >= 2 || dominant) {
|
|
114
|
+
return "medium";
|
|
115
|
+
}
|
|
116
|
+
return "low";
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Run retrieval against a ready snapshot. Returns the primary selection, a
|
|
120
|
+
* bounded relationship expansion, the scored candidate list (diagnostics), and
|
|
121
|
+
* a confidence class. Context assembly is a separate, later step.
|
|
122
|
+
*/
|
|
123
|
+
export const retrieve = (snapshot, request, config, blockedDomains) => {
|
|
124
|
+
const queryTokens = tokenize(request.query);
|
|
125
|
+
const maxPhraseLen = getMaxIndexedPhraseLen(snapshot.exactIndex, snapshot.aliasIndex);
|
|
126
|
+
const grams = generateNGrams(queryTokens, maxPhraseLen);
|
|
127
|
+
const exactHits = lookupPhrases(grams, snapshot.exactIndex);
|
|
128
|
+
const aliasHits = lookupPhrases(grams, snapshot.aliasIndex);
|
|
129
|
+
const eligibleEntryIds = new Set();
|
|
130
|
+
for (const [id, entry] of snapshot.entriesById) {
|
|
131
|
+
if (isAuthorized(entry, request, blockedDomains)) {
|
|
132
|
+
eligibleEntryIds.add(id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const lexicalTokens = buildLexicalTokens(request);
|
|
136
|
+
const lexicalMatches = snapshot.lexical.search(lexicalTokens, config.candidateLimit * 2, eligibleEntryIds);
|
|
137
|
+
const lexicalById = new Map(lexicalMatches.map((match) => [match.id, match]));
|
|
138
|
+
const candidateIds = new Set();
|
|
139
|
+
for (const id of exactHits.keys()) {
|
|
140
|
+
if (eligibleEntryIds.has(id)) {
|
|
141
|
+
candidateIds.add(id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const id of aliasHits.keys()) {
|
|
145
|
+
if (eligibleEntryIds.has(id)) {
|
|
146
|
+
candidateIds.add(id);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const match of lexicalMatches) {
|
|
150
|
+
candidateIds.add(match.id);
|
|
151
|
+
}
|
|
152
|
+
const candidates = [];
|
|
153
|
+
for (const id of candidateIds) {
|
|
154
|
+
const entry = snapshot.entriesById.get(id);
|
|
155
|
+
if (!entry) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const exact = exactHits.has(id);
|
|
159
|
+
const alias = aliasHits.has(id);
|
|
160
|
+
const lexicalMatch = lexicalById.get(id);
|
|
161
|
+
const lexical = lexicalMatch?.score ?? 0;
|
|
162
|
+
let score = lexical;
|
|
163
|
+
if (exact) {
|
|
164
|
+
score += config.exactBoost;
|
|
165
|
+
}
|
|
166
|
+
if (alias) {
|
|
167
|
+
score += config.aliasBoost;
|
|
168
|
+
}
|
|
169
|
+
candidates.push({
|
|
170
|
+
id,
|
|
171
|
+
score,
|
|
172
|
+
exact,
|
|
173
|
+
alias,
|
|
174
|
+
lexical,
|
|
175
|
+
fieldScores: lexicalMatch?.fieldScores ?? {},
|
|
176
|
+
matchedPhrases: [
|
|
177
|
+
...(exactHits.get(id) ?? []),
|
|
178
|
+
...(aliasHits.get(id) ?? []),
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
candidates.sort(compareCandidates);
|
|
183
|
+
const trimmed = candidates.slice(0, config.candidateLimit);
|
|
184
|
+
const primary = [];
|
|
185
|
+
for (const candidate of trimmed.slice(0, config.resultLimit)) {
|
|
186
|
+
const entry = snapshot.entriesById.get(candidate.id);
|
|
187
|
+
if (entry) {
|
|
188
|
+
primary.push(entry);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const selectedIds = new Set(primary.map((entry) => entry.id));
|
|
192
|
+
const expanded = [];
|
|
193
|
+
for (const entry of primary) {
|
|
194
|
+
if (expanded.length >= config.relationLimit) {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
const relations = snapshot.relationIndex.get(entry.id) ?? [];
|
|
198
|
+
for (const relatedId of relations) {
|
|
199
|
+
if (expanded.length >= config.relationLimit) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
if (selectedIds.has(relatedId)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const relatedEntry = snapshot.entriesById.get(relatedId);
|
|
206
|
+
if (!relatedEntry ||
|
|
207
|
+
!isAuthorized(relatedEntry, request, blockedDomains)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
selectedIds.add(relatedId);
|
|
211
|
+
expanded.push(relatedEntry);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
primary,
|
|
216
|
+
expanded,
|
|
217
|
+
candidates: trimmed,
|
|
218
|
+
confidence: classifyConfidence(trimmed),
|
|
219
|
+
candidateCount: candidates.length,
|
|
220
|
+
};
|
|
221
|
+
};
|
|
@@ -30,6 +30,30 @@ export declare const MODEL_CONTEXT_WINDOWS: Record<string, Record<string, number
|
|
|
30
30
|
export declare function registerRuntimeContextWindow(provider: string, model: string, contextWindow: number): void;
|
|
31
31
|
/** Test hook: clear runtime-discovered windows (state is module-global). */
|
|
32
32
|
export declare function clearRuntimeContextWindows(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Runtime-discovered window for an exact provider/model pair, or undefined.
|
|
35
|
+
*
|
|
36
|
+
* Unlike {@link getContextWindowSize} this NEVER falls back to static table
|
|
37
|
+
* values. Callers use it to distinguish "the serving infrastructure told us
|
|
38
|
+
* the real window" (safe to hard-enforce: clamp max_tokens, fail fast) from
|
|
39
|
+
* "static guess" (advisory only — hard-enforcing a guessed window would
|
|
40
|
+
* falsely reject requests that the real deployment accepts).
|
|
41
|
+
*/
|
|
42
|
+
export declare function getRuntimeContextWindow(provider: string, model?: string): number | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Register a runtime-discovered output-token ceiling for a provider/model
|
|
45
|
+
* pair. Later registrations overwrite earlier ones (rediscovery refreshes
|
|
46
|
+
* values). Non-positive/non-finite ceilings are ignored so a malformed
|
|
47
|
+
* discovery source can never shrink an output budget to zero.
|
|
48
|
+
*/
|
|
49
|
+
export declare function registerRuntimeOutputCeiling(provider: string, model: string, maxOutputTokens: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* Runtime-discovered output ceiling for an exact provider/model pair, or
|
|
52
|
+
* undefined when the serving infrastructure has not advertised one.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getRuntimeOutputCeiling(provider: string, model?: string): number | undefined;
|
|
55
|
+
/** Test hook: clear runtime-discovered output ceilings (state is module-global). */
|
|
56
|
+
export declare function clearRuntimeOutputCeilings(): void;
|
|
33
57
|
/**
|
|
34
58
|
* Resolve context window size for a provider/model combination.
|
|
35
59
|
*
|
|
@@ -420,6 +420,56 @@ export function registerRuntimeContextWindow(provider, model, contextWindow) {
|
|
|
420
420
|
export function clearRuntimeContextWindows() {
|
|
421
421
|
RUNTIME_CONTEXT_WINDOWS.clear();
|
|
422
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Runtime-discovered window for an exact provider/model pair, or undefined.
|
|
425
|
+
*
|
|
426
|
+
* Unlike {@link getContextWindowSize} this NEVER falls back to static table
|
|
427
|
+
* values. Callers use it to distinguish "the serving infrastructure told us
|
|
428
|
+
* the real window" (safe to hard-enforce: clamp max_tokens, fail fast) from
|
|
429
|
+
* "static guess" (advisory only — hard-enforcing a guessed window would
|
|
430
|
+
* falsely reject requests that the real deployment accepts).
|
|
431
|
+
*/
|
|
432
|
+
export function getRuntimeContextWindow(provider, model) {
|
|
433
|
+
if (!model) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
return RUNTIME_CONTEXT_WINDOWS.get(`${provider}:${model}`);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Runtime-discovered output-token ceilings, keyed `${provider}:${model}` —
|
|
440
|
+
* the `max_output_tokens` the serving infrastructure advertises for a model
|
|
441
|
+
* (e.g. LiteLLM `/model/info`). Same async-populate/sync-read contract as
|
|
442
|
+
* {@link registerRuntimeContextWindow}. Consumed by `getSafeMaxTokens` so
|
|
443
|
+
* requested maxTokens is clamped to the deployed model's real cap instead of
|
|
444
|
+
* a static per-provider table value.
|
|
445
|
+
*/
|
|
446
|
+
const RUNTIME_OUTPUT_CEILINGS = new Map();
|
|
447
|
+
/**
|
|
448
|
+
* Register a runtime-discovered output-token ceiling for a provider/model
|
|
449
|
+
* pair. Later registrations overwrite earlier ones (rediscovery refreshes
|
|
450
|
+
* values). Non-positive/non-finite ceilings are ignored so a malformed
|
|
451
|
+
* discovery source can never shrink an output budget to zero.
|
|
452
|
+
*/
|
|
453
|
+
export function registerRuntimeOutputCeiling(provider, model, maxOutputTokens) {
|
|
454
|
+
if (!Number.isFinite(maxOutputTokens) || maxOutputTokens <= 0) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
RUNTIME_OUTPUT_CEILINGS.set(`${provider}:${model}`, maxOutputTokens);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Runtime-discovered output ceiling for an exact provider/model pair, or
|
|
461
|
+
* undefined when the serving infrastructure has not advertised one.
|
|
462
|
+
*/
|
|
463
|
+
export function getRuntimeOutputCeiling(provider, model) {
|
|
464
|
+
if (!model) {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
return RUNTIME_OUTPUT_CEILINGS.get(`${provider}:${model}`);
|
|
468
|
+
}
|
|
469
|
+
/** Test hook: clear runtime-discovered output ceilings (state is module-global). */
|
|
470
|
+
export function clearRuntimeOutputCeilings() {
|
|
471
|
+
RUNTIME_OUTPUT_CEILINGS.clear();
|
|
472
|
+
}
|
|
423
473
|
/**
|
|
424
474
|
* Resolve context window size for a provider/model combination.
|
|
425
475
|
*
|
|
@@ -23,6 +23,12 @@ export declare function getContextOverflowProvider(error: unknown): string | nul
|
|
|
23
23
|
export declare function parseProviderOverflowDetails(error: unknown): {
|
|
24
24
|
actualTokens: number;
|
|
25
25
|
budgetTokens: number;
|
|
26
|
+
/**
|
|
27
|
+
* Output tokens the rejected request asked for, when the message states
|
|
28
|
+
* them separately (vllm/LiteLLM phrasing). Lets recovery re-fit
|
|
29
|
+
* max_tokens instead of shrinking the input.
|
|
30
|
+
*/
|
|
31
|
+
requestedOutputTokens?: number;
|
|
26
32
|
} | null;
|
|
27
33
|
/**
|
|
28
34
|
* Extract error message from various error formats.
|
|
@@ -113,6 +113,24 @@ export function parseProviderOverflowDetails(error) {
|
|
|
113
113
|
budgetTokens: parseInt(openaiMax[1].replace(/,/g, ""), 10),
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
+
// vllm / LiteLLM-proxied backends: "This model's maximum context length is
|
|
117
|
+
// N tokens. However, you requested X output tokens and your prompt contains
|
|
118
|
+
// at least Y input tokens..." — input and requested-output are stated
|
|
119
|
+
// separately (no "resulted in"), so recovery can re-fit max_tokens to
|
|
120
|
+
// N − Y instead of shrinking the input.
|
|
121
|
+
const vllmInput = message.match(/prompt\s+contains\s+at\s+least\s+(\d[\d,]{0,19})\s+input\s+tokens/i);
|
|
122
|
+
if (vllmInput && openaiMax) {
|
|
123
|
+
const requestedOutput = message.match(/requested\s+(\d[\d,]{0,19})\s+output\s+tokens/i);
|
|
124
|
+
return {
|
|
125
|
+
actualTokens: parseInt(vllmInput[1].replace(/,/g, ""), 10),
|
|
126
|
+
budgetTokens: parseInt(openaiMax[1].replace(/,/g, ""), 10),
|
|
127
|
+
...(requestedOutput
|
|
128
|
+
? {
|
|
129
|
+
requestedOutputTokens: parseInt(requestedOutput[1].replace(/,/g, ""), 10),
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
116
134
|
// Anthropic pattern: "X tokens > Y token limit" or "X tokens, limit Y"
|
|
117
135
|
// Use single character-class number groups to prevent ReDoS (CodeQL: js/polynomial-redos)
|
|
118
136
|
const anthropicMatch = message.match(/(\d[\d,]{0,19})\s*tokens?\s*[>:]\s*(\d[\d,]{0,19})/i);
|
|
@@ -41,7 +41,21 @@ export declare function estimateStepMessagesTokens(messages: readonly ModelMessa
|
|
|
41
41
|
export declare function estimateFixedOverheadTokens(system: unknown, tools: Record<string, unknown> | undefined, provider?: string): number;
|
|
42
42
|
/**
|
|
43
43
|
* Create a per-step budget guard. Returns a function that, given the step's
|
|
44
|
-
* messages
|
|
45
|
-
*
|
|
44
|
+
* messages (and optionally the REAL input-token count the provider reported
|
|
45
|
+
* for the previous step), returns a compacted replacement array when the
|
|
46
|
+
* projected request exceeds the threshold — or `undefined` when no change is
|
|
47
|
+
* needed.
|
|
48
|
+
*
|
|
49
|
+
* Two dynamic behaviours:
|
|
50
|
+
* - the available-input budget is re-resolved on EVERY invocation, so
|
|
51
|
+
* runtime window discovery (`/model/info`, overflow self-healing) that
|
|
52
|
+
* lands mid-loop takes effect immediately instead of the guard staying
|
|
53
|
+
* frozen on the value captured at loop start;
|
|
54
|
+
* - usage feedback calibrates the estimator: the ratio between the real
|
|
55
|
+
* prompt tokens of the previous step and this guard's own estimate for
|
|
56
|
+
* what that step sent scales later estimates (only UP — underestimates
|
|
57
|
+
* overflow, overestimates merely compact earlier), eliminating the
|
|
58
|
+
* char-based estimator's drift on dense code/diff content without
|
|
59
|
+
* shipping a tokenizer.
|
|
46
60
|
*/
|
|
47
|
-
export declare function createStepBudgetGuard(config: StepBudgetGuardConfig): (messages: readonly ModelMessage[]) => ModelMessage[] | undefined;
|
|
61
|
+
export declare function createStepBudgetGuard(config: StepBudgetGuardConfig): (messages: readonly ModelMessage[], observedInputTokensLastStep?: number) => ModelMessage[] | undefined;
|