@tangle-network/agent-knowledge 21.0.0 → 22.0.1
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 +27 -3
- package/README.md +5 -4
- package/dist/cli.js +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/{inspect-DXN6SE2o.js → inspect-DBxyQSdt.js} +159 -38
- package/dist/inspect-DBxyQSdt.js.map +1 -0
- package/dist/search/index.js +1 -1
- package/dist/{search-BBr5eUyT.js → search-aZdP2685.js} +81 -17
- package/dist/{search-BBr5eUyT.js.map → search-aZdP2685.js.map} +1 -1
- package/docs/compatibility.md +7 -6
- package/package.json +5 -5
- package/dist/inspect-DXN6SE2o.js.map +0 -1
package/dist/search/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as buildKnowledgeLexicalIndex, i as searchKnowledgePages, t as KNOWLEDGE_SEARCH_RETRIEVER_ID } from "../search-
|
|
1
|
+
import { a as buildKnowledgeLexicalIndex, i as searchKnowledgePages, t as KNOWLEDGE_SEARCH_RETRIEVER_ID } from "../search-aZdP2685.js";
|
|
2
2
|
export { KNOWLEDGE_SEARCH_RETRIEVER_ID, buildKnowledgeLexicalIndex, searchKnowledgePages };
|
|
@@ -23,29 +23,25 @@ function lexicalPageTerms(page, tokenize, fieldBoosts) {
|
|
|
23
23
|
length
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Inverted index from per-page terms already computed with `tokenize` and `fieldBoosts`.
|
|
28
|
+
*
|
|
29
|
+
* Posting lists are built when a term is first looked up, not up front. A query reads a handful of
|
|
30
|
+
* terms, while assembling every list walks every distinct term of every page: on a 910-page run
|
|
31
|
+
* store that was about 440 ms of each search that followed a write, because every write starts a
|
|
32
|
+
* new page view and so a new index (agent-knowledge#203). Enumerating the postings builds them all
|
|
33
|
+
* once, so the map behaves exactly as an eagerly built one.
|
|
34
|
+
*/
|
|
27
35
|
function assembleLexicalIndex(pages, terms, tokenize, fieldBoosts) {
|
|
28
|
-
const postings = /* @__PURE__ */ new Map();
|
|
29
36
|
const documentLengths = [];
|
|
30
37
|
let totalLength = 0;
|
|
31
|
-
|
|
38
|
+
for (const { length } of terms) {
|
|
32
39
|
documentLengths.push(length);
|
|
33
40
|
totalLength += length;
|
|
34
|
-
|
|
35
|
-
let list = postings.get(term);
|
|
36
|
-
if (!list) {
|
|
37
|
-
list = [];
|
|
38
|
-
postings.set(term, list);
|
|
39
|
-
}
|
|
40
|
-
list.push({
|
|
41
|
-
ordinal,
|
|
42
|
-
tf
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
});
|
|
41
|
+
}
|
|
46
42
|
return {
|
|
47
43
|
pages,
|
|
48
|
-
postings,
|
|
44
|
+
postings: new LazyPostings(terms),
|
|
49
45
|
documentLengths,
|
|
50
46
|
averageDocumentLength: pages.length > 0 ? totalLength / pages.length : 0,
|
|
51
47
|
documentCount: pages.length,
|
|
@@ -53,6 +49,74 @@ function assembleLexicalIndex(pages, terms, tokenize, fieldBoosts) {
|
|
|
53
49
|
fieldBoosts
|
|
54
50
|
};
|
|
55
51
|
}
|
|
52
|
+
const NO_POSTINGS = Object.freeze([]);
|
|
53
|
+
/** A read-only postings map that builds one term's list on first lookup, in ordinal order. */
|
|
54
|
+
var LazyPostings = class {
|
|
55
|
+
#terms;
|
|
56
|
+
#lists = /* @__PURE__ */ new Map();
|
|
57
|
+
#all;
|
|
58
|
+
constructor(terms) {
|
|
59
|
+
this.#terms = terms;
|
|
60
|
+
}
|
|
61
|
+
get(term) {
|
|
62
|
+
if (this.#all) return this.#all.get(term);
|
|
63
|
+
let list = this.#lists.get(term);
|
|
64
|
+
if (list === void 0) {
|
|
65
|
+
const found = [];
|
|
66
|
+
this.#terms.forEach(({ frequencies }, ordinal) => {
|
|
67
|
+
const tf = frequencies.get(term);
|
|
68
|
+
if (tf !== void 0) found.push({
|
|
69
|
+
ordinal,
|
|
70
|
+
tf
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
list = found.length === 0 ? NO_POSTINGS : found;
|
|
74
|
+
this.#lists.set(term, list);
|
|
75
|
+
}
|
|
76
|
+
return list.length === 0 ? void 0 : list;
|
|
77
|
+
}
|
|
78
|
+
has(term) {
|
|
79
|
+
return this.get(term) !== void 0;
|
|
80
|
+
}
|
|
81
|
+
get size() {
|
|
82
|
+
return this.#every().size;
|
|
83
|
+
}
|
|
84
|
+
forEach(callback, thisArg) {
|
|
85
|
+
for (const [term, list] of this.#every()) callback.call(thisArg, list, term, this);
|
|
86
|
+
}
|
|
87
|
+
entries() {
|
|
88
|
+
return this.#every().entries();
|
|
89
|
+
}
|
|
90
|
+
keys() {
|
|
91
|
+
return this.#every().keys();
|
|
92
|
+
}
|
|
93
|
+
values() {
|
|
94
|
+
return this.#every().values();
|
|
95
|
+
}
|
|
96
|
+
[Symbol.iterator]() {
|
|
97
|
+
return this.entries();
|
|
98
|
+
}
|
|
99
|
+
#every() {
|
|
100
|
+
if (this.#all) return this.#all;
|
|
101
|
+
const all = /* @__PURE__ */ new Map();
|
|
102
|
+
this.#terms.forEach(({ frequencies }, ordinal) => {
|
|
103
|
+
for (const [term, tf] of frequencies) {
|
|
104
|
+
let list = all.get(term);
|
|
105
|
+
if (!list) {
|
|
106
|
+
list = [];
|
|
107
|
+
all.set(term, list);
|
|
108
|
+
}
|
|
109
|
+
list.push({
|
|
110
|
+
ordinal,
|
|
111
|
+
tf
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
this.#lists.clear();
|
|
116
|
+
this.#all = all;
|
|
117
|
+
return all;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
56
120
|
//#endregion
|
|
57
121
|
//#region src/lexical-index.ts
|
|
58
122
|
const STOP_WORDS = /* @__PURE__ */ new Set([
|
|
@@ -272,4 +336,4 @@ function reasonsFor(page, query) {
|
|
|
272
336
|
//#endregion
|
|
273
337
|
export { buildKnowledgeLexicalIndex as a, tokenizeText as c, lexicalPageTerms as d, searchKnowledgePages as i, DEFAULT_FIELD_BOOSTS as l, reciprocalRankFusion as n, scoreBm25 as o, searchKnowledge as r, tokenizeQuery as s, KNOWLEDGE_SEARCH_RETRIEVER_ID as t, assembleLexicalIndex as u };
|
|
274
338
|
|
|
275
|
-
//# sourceMappingURL=search-
|
|
339
|
+
//# sourceMappingURL=search-aZdP2685.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-BBr5eUyT.js","names":[],"sources":["../src/lexical-postings.ts","../src/lexical-index.ts","../src/search.ts"],"sourcesContent":["import type {\n KnowledgeLexicalFieldBoosts,\n KnowledgeLexicalIndex,\n KnowledgeLexicalPosting,\n} from './lexical-index'\nimport type { KnowledgePage } from './types'\n\nexport const DEFAULT_FIELD_BOOSTS: Readonly<Required<KnowledgeLexicalFieldBoosts>> = Object.freeze({\n title: 3,\n path: 2,\n text: 1,\n})\n\n/** Field-boosted term frequencies and length of one page. */\nexport interface LexicalPageTerms {\n readonly frequencies: ReadonlyMap<string, number>\n readonly length: number\n}\n\nexport function lexicalPageTerms(\n page: KnowledgePage,\n tokenize: (text: string) => string[],\n fieldBoosts: Readonly<Required<KnowledgeLexicalFieldBoosts>>,\n): LexicalPageTerms {\n const frequencies = new Map<string, number>()\n let length = 0\n for (const [text, boost] of [\n [page.title, fieldBoosts.title],\n [page.path.replace(/\\.md$/, ''), fieldBoosts.path],\n [page.text, fieldBoosts.text],\n ] as const) {\n if (boost === 0) continue\n for (const token of tokenize(text)) {\n frequencies.set(token, (frequencies.get(token) ?? 0) + boost)\n length += boost\n }\n }\n return { frequencies, length }\n}\n\n/** Inverted index from per-page terms already computed with `tokenize` and `fieldBoosts`. */\nexport function assembleLexicalIndex(\n pages: readonly KnowledgePage[],\n terms: readonly LexicalPageTerms[],\n tokenize: (text: string) => string[],\n fieldBoosts: Readonly<Required<KnowledgeLexicalFieldBoosts>>,\n): KnowledgeLexicalIndex {\n const postings = new Map<string, KnowledgeLexicalPosting[]>()\n const documentLengths: number[] = []\n let totalLength = 0\n terms.forEach(({ frequencies, length }, ordinal) => {\n documentLengths.push(length)\n totalLength += length\n for (const [term, tf] of frequencies) {\n let list = postings.get(term)\n if (!list) {\n list = []\n postings.set(term, list)\n }\n list.push({ ordinal, tf })\n }\n })\n return {\n pages,\n postings,\n documentLengths,\n averageDocumentLength: pages.length > 0 ? totalLength / pages.length : 0,\n documentCount: pages.length,\n tokenize,\n fieldBoosts,\n }\n}\n","import { assembleLexicalIndex, DEFAULT_FIELD_BOOSTS, lexicalPageTerms } from './lexical-postings'\nimport type { KnowledgePage } from './types'\n\nconst STOP_WORDS = new Set([\n 'the',\n 'is',\n 'a',\n 'an',\n 'what',\n 'how',\n 'are',\n 'was',\n 'were',\n 'to',\n 'for',\n 'of',\n 'with',\n 'by',\n 'in',\n 'on',\n 'and',\n])\n\n/**\n * The token stream of one text: lower-cased, split on whitespace and\n * punctuation, single characters and stop words removed, and a CJK run\n * expanded into its bigrams and characters. Repeats are kept so a term\n * frequency can be counted. Indexing and querying share this function, so the\n * two vocabularies cannot drift.\n */\nexport function tokenizeText(text: string): string[] {\n const raw = text\n .toLowerCase()\n .split(/[\\s,,。!?、;:\"\"''()()\\-_/\\\\·~~…]+/)\n .filter((token) => token.length > 1 && !STOP_WORDS.has(token))\n const tokens: string[] = []\n for (const token of raw) {\n if (/[\\u4e00-\\u9fff\\u3400-\\u4dbf]/.test(token) && token.length > 2) {\n const chars = [...token]\n for (let i = 0; i < chars.length - 1; i++) tokens.push(chars[i]! + chars[i + 1]!)\n tokens.push(...chars)\n }\n tokens.push(token)\n }\n return tokens\n}\n\n/** The distinct query terms, in first-occurrence order. */\nexport function tokenizeQuery(query: string): string[] {\n return [...new Set(tokenizeText(query))]\n}\n\nexport interface KnowledgeLexicalFieldBoosts {\n /** Multiplier for a term occurrence in the page title. Defaults to 3. */\n title?: number\n /** Multiplier for a term occurrence in the page path without its extension. Defaults to 2. */\n path?: number\n /** Multiplier for a term occurrence in the page body. Defaults to 1. */\n text?: number\n}\n\nexport interface KnowledgeLexicalIndexOptions {\n /** Token stream of one text. Defaults to `tokenizeText`. */\n tokenize?: (text: string) => string[]\n fieldBoosts?: KnowledgeLexicalFieldBoosts\n}\n\nexport interface KnowledgeLexicalPosting {\n /** Position of the page in `KnowledgeLexicalIndex.pages`. */\n ordinal: number\n /** Field-boosted term frequency in that page. */\n tf: number\n}\n\n/**\n * Inverted index over a fixed page list.\n *\n * Ordinals are positions in `pages`. `documentLengths` are field-boosted token\n * counts, so length normalization and term frequency use one scale.\n */\nexport interface KnowledgeLexicalIndex {\n readonly pages: readonly KnowledgePage[]\n readonly postings: ReadonlyMap<string, readonly KnowledgeLexicalPosting[]>\n readonly documentLengths: readonly number[]\n readonly averageDocumentLength: number\n readonly documentCount: number\n readonly tokenize: (text: string) => string[]\n readonly fieldBoosts: Readonly<Required<KnowledgeLexicalFieldBoosts>>\n}\n\nexport interface Bm25Options {\n /** Term-frequency saturation. Defaults to 1.2. */\n k1?: number\n /** Length-normalization strength in [0, 1]. Defaults to 0.75. */\n b?: number\n}\n\nexport interface Bm25Hit {\n page: KnowledgePage\n score: number\n}\n\nexport function buildKnowledgeLexicalIndex(\n pages: readonly KnowledgePage[],\n options: KnowledgeLexicalIndexOptions = {},\n): KnowledgeLexicalIndex {\n const tokenize = options.tokenize ?? tokenizeText\n const fieldBoosts = resolveFieldBoosts(options.fieldBoosts)\n return assembleLexicalIndex(\n pages,\n pages.map((page) => lexicalPageTerms(page, tokenize, fieldBoosts)),\n tokenize,\n fieldBoosts,\n )\n}\n\n/**\n * Okapi BM25 over the distinct query terms, with the Lucene inverse document\n * frequency `ln(1 + (N - df + 0.5) / (df + 0.5))`, which is positive for every\n * indexed term. Pages with no matching term are absent. The result is ordered\n * by score, then by path, so it does not depend on page order.\n */\nexport function scoreBm25(\n index: KnowledgeLexicalIndex,\n tokens: readonly string[],\n options: Bm25Options = {},\n): Bm25Hit[] {\n const k1 = options.k1 ?? 1.2\n const b = options.b ?? 0.75\n if (!Number.isFinite(k1) || k1 < 0) throw new Error(`bm25 k1 must be >= 0, got ${String(k1)}`)\n if (!Number.isFinite(b) || b < 0 || b > 1) {\n throw new Error(`bm25 b must lie in [0, 1], got ${String(b)}`)\n }\n const scores = new Map<number, number>()\n const { documentCount, averageDocumentLength, documentLengths } = index\n for (const term of new Set(tokens)) {\n const list = index.postings.get(term)\n if (!list) continue\n const df = list.length\n const idf = Math.log(1 + (documentCount - df + 0.5) / (df + 0.5))\n for (const { ordinal, tf } of list) {\n const lengthRatio =\n averageDocumentLength > 0 ? documentLengths[ordinal]! / averageDocumentLength : 1\n const saturated = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * lengthRatio))\n scores.set(ordinal, (scores.get(ordinal) ?? 0) + idf * saturated)\n }\n }\n return [...scores.entries()]\n .map(([ordinal, score]) => ({ page: index.pages[ordinal]!, score }))\n .sort(\n (left, right) => right.score - left.score || left.page.path.localeCompare(right.page.path),\n )\n}\n\nfunction resolveFieldBoosts(\n boosts: KnowledgeLexicalFieldBoosts | undefined,\n): Readonly<Required<KnowledgeLexicalFieldBoosts>> {\n const resolved = { ...DEFAULT_FIELD_BOOSTS, ...boosts }\n for (const [field, boost] of Object.entries(resolved)) {\n if (!Number.isFinite(boost) || boost < 0) {\n throw new Error(`lexical field boost ${field} must be >= 0, got ${String(boost)}`)\n }\n }\n return Object.freeze(resolved)\n}\n","import { buildKnowledgeLexicalIndex, type KnowledgeLexicalIndex, scoreBm25 } from './lexical-index'\nimport type { KnowledgeId, KnowledgeIndex, KnowledgePage, KnowledgeSearchResult } from './types'\n\nconst RRF_K = 60\n\n/**\n * Identity of the ranking `searchKnowledge` performs: BM25 over title, path,\n * and body, exact-title and phrase matches ahead of bag-of-words matches, and\n * reciprocal rank fusion with link and shared-source structure. Declare it as\n * the retriever id of a retrieval receipt minted from these results.\n */\nexport const KNOWLEDGE_SEARCH_RETRIEVER_ID = 'bm25-rrf-v1'\n\nexport interface SearchKnowledgeOptions {\n /** Maximum results returned. Defaults to 10. */\n limit?: number\n /** Match pages whose stable id is one of these values. */\n pageIds?: readonly KnowledgeId[]\n /** Match pages carrying at least one of these tags. */\n tags?: readonly string[]\n /** Match the exact string stored in `frontmatter.kind`. */\n kinds?: readonly string[]\n /**\n * Drop pages whose own evidence refuted them. Defaults to false: a caller\n * that reads history needs them, and a silent change of what search returns\n * is worse than an explicit option.\n */\n excludeInvalidated?: boolean\n /** Additional caller-owned filter, applied before either ranking stage. */\n predicate?: (page: KnowledgePage) => boolean\n /**\n * A lexical index built from exactly the searched pages, supplied by a caller\n * that searches one page set repeatedly. When absent, one is built for this\n * call.\n */\n lexicalIndex?: KnowledgeLexicalIndex\n}\n\n/**\n * A retrieval result with an explicit citation handle.\n *\n * Unscoped search returns `page.id`; a run-scoped brief qualifies ambiguous ids\n * through the citation resolver. Later writes should persist the returned value. Keeping it at the result's top level prevents tool\n * renderers from accidentally hiding the only stable handle a model can copy.\n */\nexport interface KnowledgeSearchHit extends KnowledgeSearchResult {\n citationId: KnowledgeId\n}\n\nexport function searchKnowledge(\n index: KnowledgeIndex,\n query: string,\n limit?: number,\n): KnowledgeSearchHit[]\nexport function searchKnowledge(\n index: KnowledgeIndex,\n query: string,\n options?: SearchKnowledgeOptions,\n): KnowledgeSearchHit[]\nexport function searchKnowledge(\n index: KnowledgeIndex,\n query: string,\n limitOrOptions: number | SearchKnowledgeOptions = 10,\n): KnowledgeSearchHit[] {\n return searchKnowledgePages(index.pages, query, limitOrOptions)\n}\n\n/**\n * Rank a page set that is not a built index, such as the chain a run can see.\n *\n * `searchKnowledge` is this function over `index.pages`, so both entry points\n * rank identically and neither can drift from the other.\n */\nexport function searchKnowledgePages(\n pages: readonly KnowledgePage[],\n query: string,\n limitOrOptions: number | SearchKnowledgeOptions = 10,\n): KnowledgeSearchHit[] {\n const trimmed = query.trim()\n if (trimmed === '') return []\n const options =\n typeof limitOrOptions === 'number' ? { limit: limitOrOptions } : { ...limitOrOptions }\n const limit = options.limit ?? 10\n if (!Number.isInteger(limit) || limit < 0) {\n throw new Error(`search limit must be a non-negative integer, got ${String(limit)}`)\n }\n\n const matched = filterPages(pages, options)\n const lexicalIndex = options.lexicalIndex\n ? assertLexicalIndexMatches(options.lexicalIndex, pages)\n : buildKnowledgeLexicalIndex(pages)\n const lexicalRanked = rankLexical(matched, trimmed, lexicalIndex)\n const graphRanked = rankByGraph(matched, lexicalRanked, pages)\n // Stable ids are citation addresses, not unique document identities across stores.\n // Fuse the same page objects used by BM25, then return those exact pages.\n const scores = fuseRanks([lexicalRanked, graphRanked])\n\n const ranked = [...scores.entries()]\n .map(([page, score]) => ({ page, score }))\n .sort((a, b) => b.score - a.score || comparePages(a.page, b.page))\n .slice(0, limit)\n\n // Normalize against the top hit so callers can compare against natural\n // [0, 1] thresholds. Raw RRF values are typically ~0.016, which reads as\n // \"no relevance\" to humans even when the result is the best available.\n // The top hit becomes 1 by definition; lower-ranked hits scale linearly.\n const topScore = ranked[0]?.score ?? 0\n\n return ranked.map((item, i) => ({\n citationId: item.page.id,\n page: item.page,\n score: item.score,\n rrfScore: item.score,\n normalizedScore: topScore > 0 ? item.score / topScore : 0,\n rank: i + 1,\n snippet: buildSnippet(item.page.text, trimmed),\n reasons: reasonsFor(item.page, trimmed),\n }))\n}\n\nexport function reciprocalRankFusion(rankLists: string[][], k = RRF_K): Map<string, number> {\n return fuseRanks(rankLists, k)\n}\n\n// The public string-key helper and document retrieval share one ranking implementation.\nfunction fuseRanks<T>(rankLists: readonly (readonly T[])[], k = RRF_K): Map<T, number> {\n const scores = new Map<T, number>()\n for (const list of rankLists) {\n list.forEach((id, idx) => {\n scores.set(id, (scores.get(id) ?? 0) + 1 / (k + idx + 1))\n })\n }\n return scores\n}\n\nfunction filterPages(\n pages: readonly KnowledgePage[],\n options: SearchKnowledgeOptions,\n): KnowledgePage[] {\n const pageIds = options.pageIds ? new Set(options.pageIds) : null\n const tags = options.tags ? new Set(options.tags) : null\n const kinds = options.kinds ? new Set(options.kinds) : null\n\n return pages.filter((page) => {\n if (options.excludeInvalidated && page.invalidation !== undefined) return false\n if (pageIds && !pageIds.has(page.id)) return false\n if (tags && !page.tags.some((tag) => tags.has(tag))) return false\n if (kinds) {\n const kind = page.frontmatter.kind\n if (typeof kind !== 'string' || !kinds.has(kind)) return false\n }\n return options.predicate?.(page) ?? true\n })\n}\n\nfunction assertLexicalIndexMatches(\n lexicalIndex: KnowledgeLexicalIndex,\n pages: readonly KnowledgePage[],\n): KnowledgeLexicalIndex {\n if (\n lexicalIndex.pages.length !== pages.length ||\n lexicalIndex.pages.some((page, ordinal) => page !== pages[ordinal])\n ) {\n throw new Error('lexical index was not built from the searched pages')\n }\n return lexicalIndex\n}\n\n/**\n * The lexical rank list. An exact title or path match outranks a title that\n * contains the query, which outranks a body that contains the query, which\n * outranks a bag-of-words match; BM25 orders pages inside each of those tiers.\n * The tiers are an ordering, not a score, so a strong bag-of-words page cannot\n * overtake an exact match by term repetition alone.\n */\nfunction rankLexical(\n pages: KnowledgePage[],\n query: string,\n lexicalIndex: KnowledgeLexicalIndex,\n): KnowledgePage[] {\n const tokens = [...new Set(lexicalIndex.tokenize(query))]\n const bm25 = new Map(scoreBm25(lexicalIndex, tokens).map((hit) => [hit.page, hit.score]))\n const phrase = query.toLowerCase()\n return pages\n .flatMap((page) => {\n const score = bm25.get(page) ?? 0\n // A query that tokenizes to nothing (stop words, single characters) can\n // still match as a phrase; otherwise only pages with a scored term are\n // candidates, and every phrase match is one of them.\n if (score === 0 && tokens.length > 0) return []\n const tier = phraseTier(page, phrase)\n if (score === 0 && tier === 0) return []\n return [{ page, tier, score }]\n })\n .sort((a, b) => b.tier - a.tier || b.score - a.score || comparePages(a.page, b.page))\n .map((item) => item.page)\n}\n\nfunction phraseTier(page: KnowledgePage, phrase: string): number {\n const title = page.title.toLowerCase()\n if (title === phrase || page.path.toLowerCase().endsWith(`${phrase}.md`)) return 3\n if (title.includes(phrase)) return 2\n if (page.text.toLowerCase().includes(phrase)) return 1\n return 0\n}\n\nfunction rankByGraph(\n pages: KnowledgePage[],\n lexicalRanked: KnowledgePage[],\n visiblePages: readonly KnowledgePage[],\n): KnowledgePage[] {\n if (lexicalRanked.length === 0) return []\n // A bare link to a duplicate id has no unambiguous target. Scoped callers\n // qualify links before ranking; unscoped callers must not invent that edge.\n const counts = new Map<string, number>()\n for (const page of visiblePages) counts.set(page.id, (counts.get(page.id) ?? 0) + 1)\n const seeds = new Set(\n lexicalRanked\n .slice(0, 5)\n .filter((page) => counts.get(page.id) === 1)\n .map((page) => page.id),\n )\n return pages\n .map((page) => ({\n page,\n score:\n page.outLinks.filter((link) => seeds.has(link)).length +\n page.sourceIds.filter((source) =>\n lexicalRanked.some((seed) => seed.sourceIds.includes(source)),\n ).length,\n }))\n .filter((item) => item.score > 0)\n .sort((a, b) => b.score - a.score || comparePages(a.page, b.page))\n .map((item) => item.page)\n}\n\nfunction comparePages(a: KnowledgePage, b: KnowledgePage): number {\n return a.path.localeCompare(b.path) || a.id.localeCompare(b.id)\n}\n\nfunction buildSnippet(text: string, query: string): string {\n const compact = text.replace(/\\s+/g, ' ').trim()\n const idx = compact.toLowerCase().indexOf(query.toLowerCase())\n if (idx < 0) return compact.slice(0, 180)\n return compact.slice(Math.max(0, idx - 80), Math.min(compact.length, idx + query.length + 100))\n}\n\nfunction reasonsFor(page: KnowledgePage, query: string): string[] {\n const lower = `${page.title}\\n${page.text}`.toLowerCase()\n const reasons: string[] = []\n if (lower.includes(query.toLowerCase())) reasons.push('phrase')\n if (page.sourceIds.length > 0) reasons.push('sourced')\n if (page.outLinks.length > 0) reasons.push('linked')\n return reasons\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"search-aZdP2685.js","names":[],"sources":["../src/lexical-postings.ts","../src/lexical-index.ts","../src/search.ts"],"sourcesContent":["import type {\n KnowledgeLexicalFieldBoosts,\n KnowledgeLexicalIndex,\n KnowledgeLexicalPosting,\n} from './lexical-index'\nimport type { KnowledgePage } from './types'\n\nexport const DEFAULT_FIELD_BOOSTS: Readonly<Required<KnowledgeLexicalFieldBoosts>> = Object.freeze({\n title: 3,\n path: 2,\n text: 1,\n})\n\n/** Field-boosted term frequencies and length of one page. */\nexport interface LexicalPageTerms {\n readonly frequencies: ReadonlyMap<string, number>\n readonly length: number\n}\n\nexport function lexicalPageTerms(\n page: KnowledgePage,\n tokenize: (text: string) => string[],\n fieldBoosts: Readonly<Required<KnowledgeLexicalFieldBoosts>>,\n): LexicalPageTerms {\n const frequencies = new Map<string, number>()\n let length = 0\n for (const [text, boost] of [\n [page.title, fieldBoosts.title],\n [page.path.replace(/\\.md$/, ''), fieldBoosts.path],\n [page.text, fieldBoosts.text],\n ] as const) {\n if (boost === 0) continue\n for (const token of tokenize(text)) {\n frequencies.set(token, (frequencies.get(token) ?? 0) + boost)\n length += boost\n }\n }\n return { frequencies, length }\n}\n\n/**\n * Inverted index from per-page terms already computed with `tokenize` and `fieldBoosts`.\n *\n * Posting lists are built when a term is first looked up, not up front. A query reads a handful of\n * terms, while assembling every list walks every distinct term of every page: on a 910-page run\n * store that was about 440 ms of each search that followed a write, because every write starts a\n * new page view and so a new index (agent-knowledge#203). Enumerating the postings builds them all\n * once, so the map behaves exactly as an eagerly built one.\n */\nexport function assembleLexicalIndex(\n pages: readonly KnowledgePage[],\n terms: readonly LexicalPageTerms[],\n tokenize: (text: string) => string[],\n fieldBoosts: Readonly<Required<KnowledgeLexicalFieldBoosts>>,\n): KnowledgeLexicalIndex {\n const documentLengths: number[] = []\n let totalLength = 0\n for (const { length } of terms) {\n documentLengths.push(length)\n totalLength += length\n }\n return {\n pages,\n postings: new LazyPostings(terms),\n documentLengths,\n averageDocumentLength: pages.length > 0 ? totalLength / pages.length : 0,\n documentCount: pages.length,\n tokenize,\n fieldBoosts,\n }\n}\n\nconst NO_POSTINGS: readonly KnowledgeLexicalPosting[] = Object.freeze([])\n\n/** A read-only postings map that builds one term's list on first lookup, in ordinal order. */\nclass LazyPostings implements ReadonlyMap<string, readonly KnowledgeLexicalPosting[]> {\n readonly #terms: readonly LexicalPageTerms[]\n readonly #lists = new Map<string, readonly KnowledgeLexicalPosting[]>()\n #all: Map<string, readonly KnowledgeLexicalPosting[]> | undefined\n\n constructor(terms: readonly LexicalPageTerms[]) {\n this.#terms = terms\n }\n\n get(term: string): readonly KnowledgeLexicalPosting[] | undefined {\n if (this.#all) return this.#all.get(term)\n let list = this.#lists.get(term)\n if (list === undefined) {\n const found: KnowledgeLexicalPosting[] = []\n this.#terms.forEach(({ frequencies }, ordinal) => {\n const tf = frequencies.get(term)\n if (tf !== undefined) found.push({ ordinal, tf })\n })\n list = found.length === 0 ? NO_POSTINGS : found\n this.#lists.set(term, list)\n }\n return list.length === 0 ? undefined : list\n }\n\n has(term: string): boolean {\n return this.get(term) !== undefined\n }\n\n get size(): number {\n return this.#every().size\n }\n\n forEach(\n callback: (\n value: readonly KnowledgeLexicalPosting[],\n key: string,\n map: ReadonlyMap<string, readonly KnowledgeLexicalPosting[]>,\n ) => void,\n thisArg?: unknown,\n ): void {\n for (const [term, list] of this.#every()) callback.call(thisArg, list, term, this)\n }\n\n entries(): MapIterator<[string, readonly KnowledgeLexicalPosting[]]> {\n return this.#every().entries()\n }\n\n keys(): MapIterator<string> {\n return this.#every().keys()\n }\n\n values(): MapIterator<readonly KnowledgeLexicalPosting[]> {\n return this.#every().values()\n }\n\n [Symbol.iterator](): MapIterator<[string, readonly KnowledgeLexicalPosting[]]> {\n return this.entries()\n }\n\n #every(): Map<string, readonly KnowledgeLexicalPosting[]> {\n if (this.#all) return this.#all\n const all = new Map<string, KnowledgeLexicalPosting[]>()\n this.#terms.forEach(({ frequencies }, ordinal) => {\n for (const [term, tf] of frequencies) {\n let list = all.get(term)\n if (!list) {\n list = []\n all.set(term, list)\n }\n list.push({ ordinal, tf })\n }\n })\n this.#lists.clear()\n this.#all = all\n return all\n }\n}\n","import { assembleLexicalIndex, DEFAULT_FIELD_BOOSTS, lexicalPageTerms } from './lexical-postings'\nimport type { KnowledgePage } from './types'\n\nconst STOP_WORDS = new Set([\n 'the',\n 'is',\n 'a',\n 'an',\n 'what',\n 'how',\n 'are',\n 'was',\n 'were',\n 'to',\n 'for',\n 'of',\n 'with',\n 'by',\n 'in',\n 'on',\n 'and',\n])\n\n/**\n * The token stream of one text: lower-cased, split on whitespace and\n * punctuation, single characters and stop words removed, and a CJK run\n * expanded into its bigrams and characters. Repeats are kept so a term\n * frequency can be counted. Indexing and querying share this function, so the\n * two vocabularies cannot drift.\n */\nexport function tokenizeText(text: string): string[] {\n const raw = text\n .toLowerCase()\n .split(/[\\s,,。!?、;:\"\"''()()\\-_/\\\\·~~…]+/)\n .filter((token) => token.length > 1 && !STOP_WORDS.has(token))\n const tokens: string[] = []\n for (const token of raw) {\n if (/[\\u4e00-\\u9fff\\u3400-\\u4dbf]/.test(token) && token.length > 2) {\n const chars = [...token]\n for (let i = 0; i < chars.length - 1; i++) tokens.push(chars[i]! + chars[i + 1]!)\n tokens.push(...chars)\n }\n tokens.push(token)\n }\n return tokens\n}\n\n/** The distinct query terms, in first-occurrence order. */\nexport function tokenizeQuery(query: string): string[] {\n return [...new Set(tokenizeText(query))]\n}\n\nexport interface KnowledgeLexicalFieldBoosts {\n /** Multiplier for a term occurrence in the page title. Defaults to 3. */\n title?: number\n /** Multiplier for a term occurrence in the page path without its extension. Defaults to 2. */\n path?: number\n /** Multiplier for a term occurrence in the page body. Defaults to 1. */\n text?: number\n}\n\nexport interface KnowledgeLexicalIndexOptions {\n /** Token stream of one text. Defaults to `tokenizeText`. */\n tokenize?: (text: string) => string[]\n fieldBoosts?: KnowledgeLexicalFieldBoosts\n}\n\nexport interface KnowledgeLexicalPosting {\n /** Position of the page in `KnowledgeLexicalIndex.pages`. */\n ordinal: number\n /** Field-boosted term frequency in that page. */\n tf: number\n}\n\n/**\n * Inverted index over a fixed page list.\n *\n * Ordinals are positions in `pages`. `documentLengths` are field-boosted token\n * counts, so length normalization and term frequency use one scale.\n */\nexport interface KnowledgeLexicalIndex {\n readonly pages: readonly KnowledgePage[]\n readonly postings: ReadonlyMap<string, readonly KnowledgeLexicalPosting[]>\n readonly documentLengths: readonly number[]\n readonly averageDocumentLength: number\n readonly documentCount: number\n readonly tokenize: (text: string) => string[]\n readonly fieldBoosts: Readonly<Required<KnowledgeLexicalFieldBoosts>>\n}\n\nexport interface Bm25Options {\n /** Term-frequency saturation. Defaults to 1.2. */\n k1?: number\n /** Length-normalization strength in [0, 1]. Defaults to 0.75. */\n b?: number\n}\n\nexport interface Bm25Hit {\n page: KnowledgePage\n score: number\n}\n\nexport function buildKnowledgeLexicalIndex(\n pages: readonly KnowledgePage[],\n options: KnowledgeLexicalIndexOptions = {},\n): KnowledgeLexicalIndex {\n const tokenize = options.tokenize ?? tokenizeText\n const fieldBoosts = resolveFieldBoosts(options.fieldBoosts)\n return assembleLexicalIndex(\n pages,\n pages.map((page) => lexicalPageTerms(page, tokenize, fieldBoosts)),\n tokenize,\n fieldBoosts,\n )\n}\n\n/**\n * Okapi BM25 over the distinct query terms, with the Lucene inverse document\n * frequency `ln(1 + (N - df + 0.5) / (df + 0.5))`, which is positive for every\n * indexed term. Pages with no matching term are absent. The result is ordered\n * by score, then by path, so it does not depend on page order.\n */\nexport function scoreBm25(\n index: KnowledgeLexicalIndex,\n tokens: readonly string[],\n options: Bm25Options = {},\n): Bm25Hit[] {\n const k1 = options.k1 ?? 1.2\n const b = options.b ?? 0.75\n if (!Number.isFinite(k1) || k1 < 0) throw new Error(`bm25 k1 must be >= 0, got ${String(k1)}`)\n if (!Number.isFinite(b) || b < 0 || b > 1) {\n throw new Error(`bm25 b must lie in [0, 1], got ${String(b)}`)\n }\n const scores = new Map<number, number>()\n const { documentCount, averageDocumentLength, documentLengths } = index\n for (const term of new Set(tokens)) {\n const list = index.postings.get(term)\n if (!list) continue\n const df = list.length\n const idf = Math.log(1 + (documentCount - df + 0.5) / (df + 0.5))\n for (const { ordinal, tf } of list) {\n const lengthRatio =\n averageDocumentLength > 0 ? documentLengths[ordinal]! / averageDocumentLength : 1\n const saturated = (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * lengthRatio))\n scores.set(ordinal, (scores.get(ordinal) ?? 0) + idf * saturated)\n }\n }\n return [...scores.entries()]\n .map(([ordinal, score]) => ({ page: index.pages[ordinal]!, score }))\n .sort(\n (left, right) => right.score - left.score || left.page.path.localeCompare(right.page.path),\n )\n}\n\nfunction resolveFieldBoosts(\n boosts: KnowledgeLexicalFieldBoosts | undefined,\n): Readonly<Required<KnowledgeLexicalFieldBoosts>> {\n const resolved = { ...DEFAULT_FIELD_BOOSTS, ...boosts }\n for (const [field, boost] of Object.entries(resolved)) {\n if (!Number.isFinite(boost) || boost < 0) {\n throw new Error(`lexical field boost ${field} must be >= 0, got ${String(boost)}`)\n }\n }\n return Object.freeze(resolved)\n}\n","import { buildKnowledgeLexicalIndex, type KnowledgeLexicalIndex, scoreBm25 } from './lexical-index'\nimport type { KnowledgeId, KnowledgeIndex, KnowledgePage, KnowledgeSearchResult } from './types'\n\nconst RRF_K = 60\n\n/**\n * Identity of the ranking `searchKnowledge` performs: BM25 over title, path,\n * and body, exact-title and phrase matches ahead of bag-of-words matches, and\n * reciprocal rank fusion with link and shared-source structure. Declare it as\n * the retriever id of a retrieval receipt minted from these results.\n */\nexport const KNOWLEDGE_SEARCH_RETRIEVER_ID = 'bm25-rrf-v1'\n\nexport interface SearchKnowledgeOptions {\n /** Maximum results returned. Defaults to 10. */\n limit?: number\n /** Match pages whose stable id is one of these values. */\n pageIds?: readonly KnowledgeId[]\n /** Match pages carrying at least one of these tags. */\n tags?: readonly string[]\n /** Match the exact string stored in `frontmatter.kind`. */\n kinds?: readonly string[]\n /**\n * Drop pages whose own evidence refuted them. Defaults to false: a caller\n * that reads history needs them, and a silent change of what search returns\n * is worse than an explicit option.\n */\n excludeInvalidated?: boolean\n /** Additional caller-owned filter, applied before either ranking stage. */\n predicate?: (page: KnowledgePage) => boolean\n /**\n * A lexical index built from exactly the searched pages, supplied by a caller\n * that searches one page set repeatedly. When absent, one is built for this\n * call.\n */\n lexicalIndex?: KnowledgeLexicalIndex\n}\n\n/**\n * A retrieval result with an explicit citation handle.\n *\n * Unscoped search returns `page.id`; a run-scoped brief qualifies ambiguous ids\n * through the citation resolver. Later writes should persist the returned value. Keeping it at the result's top level prevents tool\n * renderers from accidentally hiding the only stable handle a model can copy.\n */\nexport interface KnowledgeSearchHit extends KnowledgeSearchResult {\n citationId: KnowledgeId\n}\n\nexport function searchKnowledge(\n index: KnowledgeIndex,\n query: string,\n limit?: number,\n): KnowledgeSearchHit[]\nexport function searchKnowledge(\n index: KnowledgeIndex,\n query: string,\n options?: SearchKnowledgeOptions,\n): KnowledgeSearchHit[]\nexport function searchKnowledge(\n index: KnowledgeIndex,\n query: string,\n limitOrOptions: number | SearchKnowledgeOptions = 10,\n): KnowledgeSearchHit[] {\n return searchKnowledgePages(index.pages, query, limitOrOptions)\n}\n\n/**\n * Rank a page set that is not a built index, such as the chain a run can see.\n *\n * `searchKnowledge` is this function over `index.pages`, so both entry points\n * rank identically and neither can drift from the other.\n */\nexport function searchKnowledgePages(\n pages: readonly KnowledgePage[],\n query: string,\n limitOrOptions: number | SearchKnowledgeOptions = 10,\n): KnowledgeSearchHit[] {\n const trimmed = query.trim()\n if (trimmed === '') return []\n const options =\n typeof limitOrOptions === 'number' ? { limit: limitOrOptions } : { ...limitOrOptions }\n const limit = options.limit ?? 10\n if (!Number.isInteger(limit) || limit < 0) {\n throw new Error(`search limit must be a non-negative integer, got ${String(limit)}`)\n }\n\n const matched = filterPages(pages, options)\n const lexicalIndex = options.lexicalIndex\n ? assertLexicalIndexMatches(options.lexicalIndex, pages)\n : buildKnowledgeLexicalIndex(pages)\n const lexicalRanked = rankLexical(matched, trimmed, lexicalIndex)\n const graphRanked = rankByGraph(matched, lexicalRanked, pages)\n // Stable ids are citation addresses, not unique document identities across stores.\n // Fuse the same page objects used by BM25, then return those exact pages.\n const scores = fuseRanks([lexicalRanked, graphRanked])\n\n const ranked = [...scores.entries()]\n .map(([page, score]) => ({ page, score }))\n .sort((a, b) => b.score - a.score || comparePages(a.page, b.page))\n .slice(0, limit)\n\n // Normalize against the top hit so callers can compare against natural\n // [0, 1] thresholds. Raw RRF values are typically ~0.016, which reads as\n // \"no relevance\" to humans even when the result is the best available.\n // The top hit becomes 1 by definition; lower-ranked hits scale linearly.\n const topScore = ranked[0]?.score ?? 0\n\n return ranked.map((item, i) => ({\n citationId: item.page.id,\n page: item.page,\n score: item.score,\n rrfScore: item.score,\n normalizedScore: topScore > 0 ? item.score / topScore : 0,\n rank: i + 1,\n snippet: buildSnippet(item.page.text, trimmed),\n reasons: reasonsFor(item.page, trimmed),\n }))\n}\n\nexport function reciprocalRankFusion(rankLists: string[][], k = RRF_K): Map<string, number> {\n return fuseRanks(rankLists, k)\n}\n\n// The public string-key helper and document retrieval share one ranking implementation.\nfunction fuseRanks<T>(rankLists: readonly (readonly T[])[], k = RRF_K): Map<T, number> {\n const scores = new Map<T, number>()\n for (const list of rankLists) {\n list.forEach((id, idx) => {\n scores.set(id, (scores.get(id) ?? 0) + 1 / (k + idx + 1))\n })\n }\n return scores\n}\n\nfunction filterPages(\n pages: readonly KnowledgePage[],\n options: SearchKnowledgeOptions,\n): KnowledgePage[] {\n const pageIds = options.pageIds ? new Set(options.pageIds) : null\n const tags = options.tags ? new Set(options.tags) : null\n const kinds = options.kinds ? new Set(options.kinds) : null\n\n return pages.filter((page) => {\n if (options.excludeInvalidated && page.invalidation !== undefined) return false\n if (pageIds && !pageIds.has(page.id)) return false\n if (tags && !page.tags.some((tag) => tags.has(tag))) return false\n if (kinds) {\n const kind = page.frontmatter.kind\n if (typeof kind !== 'string' || !kinds.has(kind)) return false\n }\n return options.predicate?.(page) ?? true\n })\n}\n\nfunction assertLexicalIndexMatches(\n lexicalIndex: KnowledgeLexicalIndex,\n pages: readonly KnowledgePage[],\n): KnowledgeLexicalIndex {\n if (\n lexicalIndex.pages.length !== pages.length ||\n lexicalIndex.pages.some((page, ordinal) => page !== pages[ordinal])\n ) {\n throw new Error('lexical index was not built from the searched pages')\n }\n return lexicalIndex\n}\n\n/**\n * The lexical rank list. An exact title or path match outranks a title that\n * contains the query, which outranks a body that contains the query, which\n * outranks a bag-of-words match; BM25 orders pages inside each of those tiers.\n * The tiers are an ordering, not a score, so a strong bag-of-words page cannot\n * overtake an exact match by term repetition alone.\n */\nfunction rankLexical(\n pages: KnowledgePage[],\n query: string,\n lexicalIndex: KnowledgeLexicalIndex,\n): KnowledgePage[] {\n const tokens = [...new Set(lexicalIndex.tokenize(query))]\n const bm25 = new Map(scoreBm25(lexicalIndex, tokens).map((hit) => [hit.page, hit.score]))\n const phrase = query.toLowerCase()\n return pages\n .flatMap((page) => {\n const score = bm25.get(page) ?? 0\n // A query that tokenizes to nothing (stop words, single characters) can\n // still match as a phrase; otherwise only pages with a scored term are\n // candidates, and every phrase match is one of them.\n if (score === 0 && tokens.length > 0) return []\n const tier = phraseTier(page, phrase)\n if (score === 0 && tier === 0) return []\n return [{ page, tier, score }]\n })\n .sort((a, b) => b.tier - a.tier || b.score - a.score || comparePages(a.page, b.page))\n .map((item) => item.page)\n}\n\nfunction phraseTier(page: KnowledgePage, phrase: string): number {\n const title = page.title.toLowerCase()\n if (title === phrase || page.path.toLowerCase().endsWith(`${phrase}.md`)) return 3\n if (title.includes(phrase)) return 2\n if (page.text.toLowerCase().includes(phrase)) return 1\n return 0\n}\n\nfunction rankByGraph(\n pages: KnowledgePage[],\n lexicalRanked: KnowledgePage[],\n visiblePages: readonly KnowledgePage[],\n): KnowledgePage[] {\n if (lexicalRanked.length === 0) return []\n // A bare link to a duplicate id has no unambiguous target. Scoped callers\n // qualify links before ranking; unscoped callers must not invent that edge.\n const counts = new Map<string, number>()\n for (const page of visiblePages) counts.set(page.id, (counts.get(page.id) ?? 0) + 1)\n const seeds = new Set(\n lexicalRanked\n .slice(0, 5)\n .filter((page) => counts.get(page.id) === 1)\n .map((page) => page.id),\n )\n return pages\n .map((page) => ({\n page,\n score:\n page.outLinks.filter((link) => seeds.has(link)).length +\n page.sourceIds.filter((source) =>\n lexicalRanked.some((seed) => seed.sourceIds.includes(source)),\n ).length,\n }))\n .filter((item) => item.score > 0)\n .sort((a, b) => b.score - a.score || comparePages(a.page, b.page))\n .map((item) => item.page)\n}\n\nfunction comparePages(a: KnowledgePage, b: KnowledgePage): number {\n return a.path.localeCompare(b.path) || a.id.localeCompare(b.id)\n}\n\nfunction buildSnippet(text: string, query: string): string {\n const compact = text.replace(/\\s+/g, ' ').trim()\n const idx = compact.toLowerCase().indexOf(query.toLowerCase())\n if (idx < 0) return compact.slice(0, 180)\n return compact.slice(Math.max(0, idx - 80), Math.min(compact.length, idx + query.length + 100))\n}\n\nfunction reasonsFor(page: KnowledgePage, query: string): string[] {\n const lower = `${page.title}\\n${page.text}`.toLowerCase()\n const reasons: string[] = []\n if (lower.includes(query.toLowerCase())) reasons.push('phrase')\n if (page.sourceIds.length > 0) reasons.push('sourced')\n if (page.outLinks.length > 0) reasons.push('linked')\n return reasons\n}\n"],"mappings":"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"}
|
package/docs/compatibility.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Compatibility and peer versions
|
|
2
2
|
|
|
3
|
-
Knowledge
|
|
3
|
+
Knowledge 22 requires Node.js 22.12 or later and Interface 5.
|
|
4
4
|
|
|
5
|
-
Eval 0.
|
|
6
|
-
0.
|
|
7
|
-
Official optimizer checks require the matching `agent-eval-rpc==0.
|
|
5
|
+
Eval 0.213 is the supported minor. Development and packed-package checks use its exact
|
|
6
|
+
0.213.0 floor, with one physical copy each of Eval, Core 0.12, Interface 5 and Zod 4.6.5.
|
|
7
|
+
Official optimizer checks require the matching `agent-eval-rpc==0.213.0` Python bridge.
|
|
8
8
|
The new cohort retains Trace Contract 2.
|
|
9
9
|
|
|
10
|
-
Use Knowledge
|
|
11
|
-
|
|
10
|
+
Use Knowledge 21 with Interface 4 and Eval 0.212. Use Knowledge 20 with Interface 3
|
|
11
|
+
and Eval 0.209 through 0.211. Use Knowledge 19 with Interface 2 and Eval 0.201
|
|
12
|
+
through 0.208, and Knowledge 17 with older Eval releases.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-knowledge",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "22.0.1",
|
|
4
4
|
"description": "Build, search, evaluate, and improve source-backed knowledge bases.",
|
|
5
5
|
"homepage": "https://github.com/tangle-network/agent-knowledge#readme",
|
|
6
6
|
"repository": {
|
|
@@ -85,15 +85,15 @@
|
|
|
85
85
|
"zod": "4.6.5"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
|
-
"@tangle-network/agent-eval": ">=0.
|
|
89
|
-
"@tangle-network/agent-interface": "^
|
|
88
|
+
"@tangle-network/agent-eval": ">=0.213.0 <0.214.0",
|
|
89
|
+
"@tangle-network/agent-interface": "^5.0.0"
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
92
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
93
93
|
"@biomejs/biome": "^2.5.14",
|
|
94
94
|
"@neo4j-labs/agent-memory": "0.5.0",
|
|
95
|
-
"@tangle-network/agent-eval": "0.
|
|
96
|
-
"@tangle-network/agent-interface": "
|
|
95
|
+
"@tangle-network/agent-eval": "0.213.0",
|
|
96
|
+
"@tangle-network/agent-interface": "5.0.0",
|
|
97
97
|
"@types/node": "^26.6.2",
|
|
98
98
|
"mem0ai": "3.3.0",
|
|
99
99
|
"oxc-parser": "0.151.0",
|