@avantf/dsh-mem 0.1.1 → 0.3.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/LICENSE +1 -1
- package/README.md +97 -87
- package/lib/client.js +2359 -1174
- package/lib/client.js.map +1 -1
- package/lib/dsh-build.json +5 -5
- package/lib/index.js +1081 -668
- package/lib/interface-version.json +4 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +21 -1
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/data_home.d.ts +3 -0
- package/lib/types/data_home.d.ts.map +1 -0
- package/lib/types/data_home.js +26 -0
- package/lib/types/data_home.js.map +1 -0
- package/lib/types/envinit-bootstrap.d.ts +13 -3
- package/lib/types/envinit-bootstrap.js +14 -4
- package/lib/types/envinit.d.ts +11 -6
- package/lib/types/envinit.d.ts.map +1 -1
- package/lib/types/envinit.js +62 -155
- package/lib/types/envinit.js.map +1 -1
- package/lib/types/hints.d.ts +15 -16
- package/lib/types/hints.d.ts.map +1 -1
- package/lib/types/hints.js +16 -17
- package/lib/types/hints.js.map +1 -1
- package/lib/types/index.d.ts +33 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +110 -63
- package/lib/types/index.js.map +1 -1
- package/lib/types/interface_gate.d.ts +51 -0
- package/lib/types/interface_gate.d.ts.map +1 -0
- package/lib/types/interface_gate.js +84 -0
- package/lib/types/interface_gate.js.map +1 -0
- package/lib/types/prompt.d.ts +74 -4
- package/lib/types/prompt.d.ts.map +1 -1
- package/lib/types/prompt.js +85 -2
- package/lib/types/prompt.js.map +1 -1
- package/lib/types/provision.d.ts +29 -24
- package/lib/types/provision.d.ts.map +1 -1
- package/lib/types/provision.js +3 -59
- package/lib/types/provision.js.map +1 -1
- package/lib/types/remote.d.ts +7 -6
- package/lib/types/remote.d.ts.map +1 -1
- package/lib/types/remote.js +24 -10
- package/lib/types/remote.js.map +1 -1
- package/package.json +20 -22
package/lib/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { accessSync, chmodSync, constants, copyFileSync, createWriteStream, existsSync, mkdirSync, promises, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import z from "@deepseek-ai/schemastery";
|
|
4
|
-
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
5
|
-
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
6
3
|
import { basename, delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
7
|
-
import { z
|
|
4
|
+
import { z } from "zod";
|
|
8
5
|
import { homedir } from "node:os";
|
|
6
|
+
import z$1 from "@deepseek-ai/schemastery";
|
|
7
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
9
9
|
import { isSeq, parse, parseDocument } from "yaml";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { Readable } from "node:stream";
|
|
@@ -13,121 +13,16 @@ import { pipeline } from "node:stream/promises";
|
|
|
13
13
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import { createInflateRaw } from "node:zlib";
|
|
15
15
|
import Database from "better-sqlite3";
|
|
16
|
-
import { readFile } from "node:fs/promises";
|
|
17
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
|
-
//#region lib/types/prompt.js
|
|
19
|
-
/**
|
|
20
|
-
* The two system-prompt sections this plugin contributes (DESIGN §10).
|
|
21
|
-
*
|
|
22
|
-
* They exist for something a tool description cannot carry: a description says what a call DOES,
|
|
23
|
-
* never when to reach for it unprompted, and the default behaviour is to wait until asked. So each
|
|
24
|
-
* says only WHEN its store is worth writing or reading. Neither says anything about how memory is
|
|
25
|
-
* retained — that stays out of every model-facing string, held to the contract's
|
|
26
|
-
* `RETENTION_VOCABULARY`, the same list the tool descriptions are held to.
|
|
27
|
-
*
|
|
28
|
-
* The knowledge one is the section whose ABSENCE was measured: a user had just ingested
|
|
29
|
-
* 《Cgroup v2 技术综述》(which answers "cgroup v2 的内存保护" at score 1.0) and asked about it in a
|
|
30
|
-
* session whose prompt named only the memory tools, so the model never looked. A tool description
|
|
31
|
-
* cannot fix that by itself — `kb_query`'s says what the call does and even leads with 记忆 facts,
|
|
32
|
-
* which reads as a memory tool — and the model cannot know the library is relevant to a question
|
|
33
|
-
* without being told to check it. Hence the "先检索一次再作答" nudge, plus the explicit
|
|
34
|
-
* "你看不到库里有什么" clause: the thing that made the miss invisible is that nothing in the
|
|
35
|
-
* prompt acknowledged the store existed at all.
|
|
36
|
-
*
|
|
37
|
-
* `order` 3000/3010 are third-party slots: after the harness's per-tool guidance block (1000–2900)
|
|
38
|
-
* and before its toolset-level guidance (5000). They are not repository-owned placements, so they
|
|
39
|
-
* do not resolve through `getSectionOrder`.
|
|
40
|
-
*
|
|
41
|
-
* The sections say WHAT TO DO and never WHY. The reasoning behind each rule is measured and
|
|
42
|
-
* lives here, in DESIGN §10 and in the CHANGELOG — not in the prompt, where it costs tokens on
|
|
43
|
-
* every step and does not change the action.
|
|
44
|
-
*
|
|
45
|
-
* Kept in its own module — not in `index.ts` — so the text can be read and checked without the
|
|
46
|
-
* plugin entry's `@deepseek-ai/*` runtime imports, which resolve only in a harness workspace.
|
|
47
|
-
*/
|
|
48
|
-
const MEMORY_PROMPT_SECTION = {
|
|
49
|
-
name: "avantf:memory-usage",
|
|
50
|
-
order: 3e3,
|
|
51
|
-
text: "记忆(`mem_remember`):把**重要且跨会话仍成立**的事实主动记下来——用户偏好、项目决策、环境约束、接口约定、踩过的坑。同一事实的修正用 `update`,一句自包含的陈述。不要记录临时聊天内容、一次性任务的中间状态、能直接从代码或文档读到的内容。"
|
|
52
|
-
};
|
|
53
|
-
/**
|
|
54
|
-
* WHEN to consult the document knowledge base. See the module comment for why this is a separate
|
|
55
|
-
* section rather than a clause inside the memory one: the two stores answer different questions,
|
|
56
|
-
* and a session that needs the knowledge base may have no memory question at all.
|
|
57
|
-
*/
|
|
58
|
-
const KNOWLEDGE_PROMPT_SECTION = {
|
|
59
|
-
name: "avantf:knowledge-usage",
|
|
60
|
-
order: 3010,
|
|
61
|
-
text: "知识库(`kb_query` / `kb_add`)存放**用户提供的成篇资料**:文档、长说明、规范、综述等要按原文查阅的内容。回答涉及已入库资料的问题之前,先用 `kb_query` 检索;命中就按 `source_ref` 引用原文。用户给了值得长期留存的成篇内容就主动 `kb_add` 入库;一句话的事实属于记忆(`mem_remember`),不要入知识库。"
|
|
62
|
-
};
|
|
63
|
-
/**
|
|
64
|
-
* HOW to change a document that is already in the library.
|
|
65
|
-
*
|
|
66
|
-
* A separate section because it is a different job from the one above, and because the first
|
|
67
|
-
* section is already at 382 of its 400-character budget. It exists to prevent a measured failure:
|
|
68
|
-
* asked to "update the knowledge base", a model called the old all-in-one tool with a NEW title and
|
|
69
|
-
* silently created a sibling document (《… · 补遗》) while the original stayed behind — two
|
|
70
|
-
* overlapping documents, and the reader left to reconcile them. Changing a document is a FILE EDIT,
|
|
71
|
-
* so the section has to name the file tools and warn about the one that destroys the frontmatter.
|
|
72
|
-
*/
|
|
73
|
-
const KB_EDIT_PROMPT_SECTION = {
|
|
74
|
-
name: "avantf:kb-edit",
|
|
75
|
-
order: 3020,
|
|
76
|
-
text: "更新知识库时,要在原文上修改或追加,不要新建一个补充文档。用 `kb_list` 拿到受管 `.md` 的绝对路径,用文件工具修改。在用户明确要求时使用 `kb_remove` 删除。"
|
|
77
|
-
};
|
|
78
|
-
//#endregion
|
|
79
|
-
//#region lib/types/hints.js
|
|
80
|
-
/**
|
|
81
|
-
* The plugin's CONDITIONAL hints: one line per store, contributed only when that store already
|
|
82
|
-
* holds something relevant to what the user just sent (DESIGN §12).
|
|
83
|
-
*
|
|
84
|
-
* These are deliberately NOT part of the always-on usage sections. A usage section says WHEN a
|
|
85
|
-
* store is worth consulting and therefore has to be paid for on every step of every session; this
|
|
86
|
-
* says "there is something here, right now", which is only true sometimes. The two layers compose:
|
|
87
|
-
* the section is the fallback for the paraphrase the lexical probe misses, and the hint is the
|
|
88
|
-
* concrete nudge for the case it catches.
|
|
89
|
-
*
|
|
90
|
-
* ONE LINE PER STORE, never a merged sentence. The two stores answer different questions and
|
|
91
|
-
* either can be the relevant one, so `RelevanceHit` is a closed four-valued answer and each store
|
|
92
|
-
* gets its own contribution — a caller can then see, and later tune or drop, one without the other.
|
|
93
|
-
*
|
|
94
|
-
* WHY EACH LINE NAMES THE PLUGIN. This text is injected as runtime context, and the harness
|
|
95
|
-
* materialises that as a USER-role snapshot in the conversation (`dsh-agent-loop`'s
|
|
96
|
-
* `runtimeContext.project`). Without an author, a line like "记忆里有相关内容" reads as something
|
|
97
|
-
* the user said. The `[avantf-mem 插件]` prefix is what keeps the provenance honest.
|
|
98
|
-
*/
|
|
99
|
-
const MEMORY_HINT = "[avantf-mem 插件] 记忆里有与本次提问相关的事实;需要时用 `mem_recall` 检索。";
|
|
100
|
-
const KNOWLEDGE_HINT = "[avantf-mem 插件] 知识库里有与本次提问相关的内容;需要时用 `kb_query` 检索。";
|
|
101
|
-
/**
|
|
102
|
-
* Map one answer to the two contributions, as text.
|
|
103
|
-
*
|
|
104
|
-
* An empty string is the "contribute nothing" value: `renderPrompt`/`renderContextSections` drop
|
|
105
|
-
* empty text, so `none` (and the missing half of `memory`/`knowledge`) costs no tokens at all.
|
|
106
|
-
*/
|
|
107
|
-
function hintLines(hit) {
|
|
108
|
-
return {
|
|
109
|
-
memory: hit === "memory" || hit === "both" ? MEMORY_HINT : "",
|
|
110
|
-
knowledge: hit === "knowledge" || hit === "both" ? KNOWLEDGE_HINT : ""
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* The plain text of an inbox message, or `''` when it carries none.
|
|
115
|
-
*
|
|
116
|
-
* `dsh-agent-loop`'s own extractor returns `undefined` unless the message is exactly ONE text
|
|
117
|
-
* block; a steered or multi-part message would then silently produce no hint. Concatenating the
|
|
118
|
-
* text blocks keeps the probe working for those, and non-text blocks (images) are simply skipped —
|
|
119
|
-
* the probe is lexical, so it has nothing to say about them anyway.
|
|
120
|
-
*/
|
|
121
|
-
function messageText(message) {
|
|
122
|
-
const content = message?.content;
|
|
123
|
-
if (!Array.isArray(content)) return "";
|
|
124
|
-
return content.map((block) => {
|
|
125
|
-
const b = block;
|
|
126
|
-
return b?.type === "text" && typeof b.text === "string" ? b.text : "";
|
|
127
|
-
}).filter(Boolean).join("\n");
|
|
128
|
-
}
|
|
129
|
-
//#endregion
|
|
130
17
|
//#region ../contract/lib/types.js
|
|
18
|
+
/** Shared domain types for avantf-mem. Single source of truth in @avantf/mem-contract. */
|
|
19
|
+
const CATEGORY_VALUES = [
|
|
20
|
+
"user_pref",
|
|
21
|
+
"user_env",
|
|
22
|
+
"project",
|
|
23
|
+
"tool",
|
|
24
|
+
"general"
|
|
25
|
+
];
|
|
131
26
|
/**
|
|
132
27
|
* The fact statuses a caller may filter by, as the ONE list every surface reads: the tool schema's
|
|
133
28
|
* `z.enum`, the derived MCP inputSchema and DSH parameter spec, and the CLI's `--status` flag (which
|
|
@@ -187,26 +82,26 @@ const DEGRADED_WEIGHTS = {
|
|
|
187
82
|
* window); a positive value is clamped to it. Past the window transformers.js truncates
|
|
188
83
|
* with no error, so the adapter bounds the text first and warns once (DESIGN §20).
|
|
189
84
|
*/
|
|
190
|
-
const maxInputTokens = z
|
|
191
|
-
const semanticSchema = z
|
|
192
|
-
backend: z
|
|
193
|
-
local_model: z
|
|
194
|
-
dim: z
|
|
85
|
+
const maxInputTokens = z.number().int().nonnegative().default(0);
|
|
86
|
+
const semanticSchema = z.object({
|
|
87
|
+
backend: z.string().default("local_bge"),
|
|
88
|
+
local_model: z.string().default("Xenova/bge-small-zh-v1.5"),
|
|
89
|
+
dim: z.number().int().positive().default(512),
|
|
195
90
|
max_input_tokens: maxInputTokens,
|
|
196
|
-
mirror: z
|
|
197
|
-
cache_dir: z
|
|
198
|
-
auto_download: z
|
|
91
|
+
mirror: z.string().default("https://hf-mirror.com"),
|
|
92
|
+
cache_dir: z.string().default(""),
|
|
93
|
+
auto_download: z.boolean().default(true)
|
|
199
94
|
});
|
|
200
|
-
const rerankSchema = z
|
|
201
|
-
backend: z
|
|
202
|
-
local_model: z
|
|
95
|
+
const rerankSchema = z.object({
|
|
96
|
+
backend: z.string().default("none"),
|
|
97
|
+
local_model: z.string().default("Xenova/bge-reranker-base"),
|
|
203
98
|
max_input_tokens: maxInputTokens,
|
|
204
|
-
mirror: z
|
|
205
|
-
cache_dir: z
|
|
206
|
-
auto_download: z
|
|
99
|
+
mirror: z.string().default("https://hf-mirror.com"),
|
|
100
|
+
cache_dir: z.string().default(""),
|
|
101
|
+
auto_download: z.boolean().default(true)
|
|
207
102
|
});
|
|
208
|
-
const vectorStoreSchema = z
|
|
209
|
-
backend: z
|
|
103
|
+
const vectorStoreSchema = z.object({
|
|
104
|
+
backend: z.string().default("auto"),
|
|
210
105
|
/**
|
|
211
106
|
* Vector counts at which `backend: auto` migrates to an ANN index.
|
|
212
107
|
*
|
|
@@ -220,9 +115,9 @@ const vectorStoreSchema = z$1.object({
|
|
|
220
115
|
* sub-millisecond. At 8000 recall falls to 0.89, so for a larger corpus raise
|
|
221
116
|
* `hnswlib_ef_search` (or accept the loss) instead of moving this threshold.
|
|
222
117
|
*/
|
|
223
|
-
auto_thresholds: z
|
|
224
|
-
hnswlib: z
|
|
225
|
-
faiss: z
|
|
118
|
+
auto_thresholds: z.object({
|
|
119
|
+
hnswlib: z.number().int().positive().default(2e3),
|
|
120
|
+
faiss: z.number().int().positive().default(1e5)
|
|
226
121
|
}).default({
|
|
227
122
|
hnswlib: 2e3,
|
|
228
123
|
faiss: 1e5
|
|
@@ -234,20 +129,20 @@ const vectorStoreSchema = z$1.object({
|
|
|
234
129
|
* ~3 ms at n=2000). Recall falls as the corpus grows, so raise this before raising
|
|
235
130
|
* `auto_thresholds.hnswlib`. The store uses `max(ef_search, 8 × k)`.
|
|
236
131
|
*/
|
|
237
|
-
hnswlib_ef_search: z
|
|
132
|
+
hnswlib_ef_search: z.number().int().min(16).max(2048).default(256)
|
|
238
133
|
});
|
|
239
|
-
const retrieverSchema = z
|
|
240
|
-
weight_semantic: z
|
|
241
|
-
weight_fts: z
|
|
242
|
-
weight_jaccard: z
|
|
134
|
+
const retrieverSchema = z.object({
|
|
135
|
+
weight_semantic: z.number().min(0).max(1).default(.55),
|
|
136
|
+
weight_fts: z.number().min(0).max(1).default(.3),
|
|
137
|
+
weight_jaccard: z.number().min(0).max(1).default(.15),
|
|
243
138
|
/**
|
|
244
139
|
* Token budget for a retrieval RESULT (`0` = unlimited). `limit` bounds how many hits, never
|
|
245
140
|
* how much text, so a broad query could otherwise pour tens of thousands of characters into a
|
|
246
141
|
* model's context while a narrow one poured in a paragraph (DESIGN §20). Hits shortened by the
|
|
247
142
|
* budget are marked `truncated`.
|
|
248
143
|
*/
|
|
249
|
-
max_output_tokens: z
|
|
250
|
-
over_fetch_factor: z
|
|
144
|
+
max_output_tokens: z.number().int().nonnegative().default(8e3),
|
|
145
|
+
over_fetch_factor: z.number().int().positive().default(5),
|
|
251
146
|
/**
|
|
252
147
|
* Rows one non-semantic retrieval leg may hand to fusion (`0` = derived: `max(200, 4×overFetch)`).
|
|
253
148
|
*
|
|
@@ -261,59 +156,59 @@ const retrieverSchema = z$1.object({
|
|
|
261
156
|
* spend less memory than lose the tail: it is `retriever.leg_cap` that makes the cap a
|
|
262
157
|
* measurable variable instead of a constant baked into the store.
|
|
263
158
|
*/
|
|
264
|
-
leg_cap: z
|
|
159
|
+
leg_cap: z.number().int().nonnegative().default(0)
|
|
265
160
|
});
|
|
266
161
|
/**
|
|
267
162
|
* Trust & forgetting (TRUST_MODEL.md). The aging clock is the ACTIVE-DAY counter
|
|
268
163
|
* (`avantf_stats.trust_clock`), not wall time: downtime counts as at most
|
|
269
164
|
* `presence.gap_cap_days` so a shut-down system never "starves" its memories.
|
|
270
165
|
*/
|
|
271
|
-
const presenceSchema = z
|
|
272
|
-
mode: z
|
|
166
|
+
const presenceSchema = z.object({
|
|
167
|
+
mode: z.enum(["process"]).default("process"),
|
|
273
168
|
/** One presence advances the clock by at most this many days (downtime ⇒ 1 day). */
|
|
274
|
-
gap_cap_days: z
|
|
169
|
+
gap_cap_days: z.number().min(0).default(1),
|
|
275
170
|
/** Long-lived surfaces (plugin / MCP) tick this often; 0 = startup pass only. */
|
|
276
|
-
heartbeat_minutes: z
|
|
171
|
+
heartbeat_minutes: z.number().int().nonnegative().default(60)
|
|
277
172
|
});
|
|
278
|
-
const trustSchema = z
|
|
173
|
+
const trustSchema = z.object({
|
|
279
174
|
/** false = no presence, no decay, no reinforcement, no pinning (TTL/idle/purge keep running). */
|
|
280
|
-
enabled: z
|
|
175
|
+
enabled: z.boolean().default(true),
|
|
281
176
|
/** Initial trust of a new fact. */
|
|
282
|
-
start: z
|
|
177
|
+
start: z.number().min(0).max(1).default(.5),
|
|
283
178
|
/** Linear decay per active day: `0.5 / 90` ⇒ 90 active days from start to 0. */
|
|
284
|
-
decay_per_day: z
|
|
179
|
+
decay_per_day: z.number().min(0).max(1).default(.5 / 90),
|
|
285
180
|
/** Trust ≤ this ⇒ forgotten (archived 'forgot'). 0 = "reaching zero is forgetting". */
|
|
286
|
-
forget_threshold: z
|
|
181
|
+
forget_threshold: z.number().min(0).max(1).default(0),
|
|
287
182
|
/** Reaching this via explicit feedback promotes to permanent (snapped to 1.0). */
|
|
288
|
-
permanent_threshold: z
|
|
183
|
+
permanent_threshold: z.number().min(0).max(1).default(.9),
|
|
289
184
|
/** A recall of something at/below this raises it straight back to this value. */
|
|
290
|
-
recall_floor: z
|
|
185
|
+
recall_floor: z.number().min(0).max(1).default(.5),
|
|
291
186
|
/** Per-recall gain (before marginal decay). */
|
|
292
|
-
recall_delta: z
|
|
187
|
+
recall_delta: z.number().min(0).max(1).default(.03),
|
|
293
188
|
/** Effective reinforcement events per fact per 24h (R10: zero-gain ones don't count). */
|
|
294
|
-
recall_daily_cap: z
|
|
189
|
+
recall_daily_cap: z.number().int().nonnegative().default(3),
|
|
295
190
|
/** 1 = every in-window gain is equal; 0.5 = 0.03 / 0.015 / 0.0075 … */
|
|
296
|
-
recall_marginal_decay: z
|
|
191
|
+
recall_marginal_decay: z.number().min(0).max(1).default(1),
|
|
297
192
|
/** Recall alone can never exceed this (and never pins — see D11). */
|
|
298
|
-
recall_ceiling: z
|
|
193
|
+
recall_ceiling: z.number().min(0).max(1).default(.85),
|
|
299
194
|
/** Explicit `helpful` / `unhelpful` step. */
|
|
300
|
-
feedback_delta: z
|
|
195
|
+
feedback_delta: z.number().min(0).max(1).default(.05),
|
|
301
196
|
/** 0 = unlimited; >0 also caps feedback reinforcement per fact per 24h. */
|
|
302
|
-
feedback_daily_cap: z
|
|
197
|
+
feedback_daily_cap: z.number().int().nonnegative().default(0),
|
|
303
198
|
/** A revised fact inherits the settled trust (and pin) of the row it replaces. */
|
|
304
|
-
inherit_trust_on_update: z
|
|
199
|
+
inherit_trust_on_update: z.boolean().default(true),
|
|
305
200
|
/** Pinned (permanent) facts are never physically purged. */
|
|
306
|
-
purge_skips_pinned: z
|
|
201
|
+
purge_skips_pinned: z.boolean().default(true),
|
|
307
202
|
/** Calendar fallback: unused for this many days ⇒ archived 'idle'. */
|
|
308
|
-
idle_calendar_days: z
|
|
203
|
+
idle_calendar_days: z.number().int().positive().default(365),
|
|
309
204
|
/** Per-tick budget for the settle sweep (the other sweeps only touch critical rows). */
|
|
310
|
-
tick_max_facts: z
|
|
205
|
+
tick_max_facts: z.number().int().positive().default(5e3),
|
|
311
206
|
presence: presenceSchema.prefault({})
|
|
312
207
|
});
|
|
313
|
-
const lifecycleSchema = z
|
|
208
|
+
const lifecycleSchema = z.object({
|
|
314
209
|
/** Purge window, measured in ACTIVE days since `archived_clock`. */
|
|
315
|
-
purge_after_archived_days: z
|
|
316
|
-
contradiction_threshold: z
|
|
210
|
+
purge_after_archived_days: z.number().int().nonnegative().default(365),
|
|
211
|
+
contradiction_threshold: z.number().min(0).max(1).default(.6)
|
|
317
212
|
});
|
|
318
213
|
/**
|
|
319
214
|
* External tools and derived runtime state the plugin must PROVIDE, not merely use (DESIGN §13).
|
|
@@ -327,21 +222,21 @@ const lifecycleSchema = z$1.object({
|
|
|
327
222
|
* The mirror list defaults to DOMESTIC proxies, with the official source always tried last — a
|
|
328
223
|
* 35 MB download from GitHub is the difference between a usable first run and an abandoned one.
|
|
329
224
|
*/
|
|
330
|
-
const toolsSchema = z
|
|
225
|
+
const toolsSchema = z.object({
|
|
331
226
|
/** Managed install root; `<dir>/<tool>/<version>/bin/<binary>` per artifact. */
|
|
332
|
-
dir: z
|
|
227
|
+
dir: z.string().default(""),
|
|
333
228
|
/** URL templates tried before the official source; `{url}` is the original URL, `{file}` its name. */
|
|
334
|
-
mirror: z
|
|
229
|
+
mirror: z.array(z.string()).default([
|
|
335
230
|
"https://ghfast.top/{url}",
|
|
336
231
|
"https://ghproxy.net/{url}",
|
|
337
232
|
"https://gh-proxy.com/{url}"
|
|
338
233
|
]),
|
|
339
234
|
/** Download a missing artifact at startup. `false` = use only what is already on this machine. */
|
|
340
|
-
auto_install: z
|
|
235
|
+
auto_install: z.boolean().default(true)
|
|
341
236
|
});
|
|
342
237
|
/** The merged runtime config shared by memory and knowledge stores. */
|
|
343
|
-
const ConfigSchema = z
|
|
344
|
-
dataHome: z
|
|
238
|
+
const ConfigSchema = z.object({
|
|
239
|
+
dataHome: z.string().default("~/.avantf"),
|
|
345
240
|
semantic: semanticSchema.prefault({}),
|
|
346
241
|
rerank: rerankSchema.prefault({}),
|
|
347
242
|
vectorStore: vectorStoreSchema.prefault({}),
|
|
@@ -356,16 +251,7 @@ const defaultConfig = ConfigSchema.parse({});
|
|
|
356
251
|
* Memory store-specific config. Overrides on top of the common Config.
|
|
357
252
|
* Note: `db.path` is the single authoritative path for the memory DB.
|
|
358
253
|
*/
|
|
359
|
-
const MemoryConfigSchema = z
|
|
360
|
-
db: z$1.object({ path: z$1.string().default("") }).prefault({}),
|
|
361
|
-
category_values: z$1.array(z$1.string()).default([
|
|
362
|
-
"user_pref",
|
|
363
|
-
"user_env",
|
|
364
|
-
"project",
|
|
365
|
-
"tool",
|
|
366
|
-
"general"
|
|
367
|
-
])
|
|
368
|
-
});
|
|
254
|
+
const MemoryConfigSchema = z.object({ db: z.object({ path: z.string().default("") }).prefault({}) });
|
|
369
255
|
/**
|
|
370
256
|
* Ingestion boundary (DESIGN §8 / D5). `kb_ingest`'s `source_uri` is RESOLVED —
|
|
371
257
|
* a local path is read, an http(s) URL is fetched — and everything it reads lands
|
|
@@ -373,24 +259,24 @@ const MemoryConfigSchema = z$1.object({
|
|
|
373
259
|
* read-any-file / probe-the-intranet primitive for a steered agent. Safe by
|
|
374
260
|
* default, with one explicit opt-in per resource kind.
|
|
375
261
|
*/
|
|
376
|
-
const ingestSchema = z
|
|
262
|
+
const ingestSchema = z.object({
|
|
377
263
|
/** Roots a local `source_uri`/`kb_import` path may be read from; empty ⇒ the process workspace (`cwd`). */
|
|
378
|
-
local_roots: z
|
|
264
|
+
local_roots: z.array(z.string()).default([]),
|
|
379
265
|
/** Lift the roots check entirely — read any local path. */
|
|
380
|
-
allow_outside_workspace: z
|
|
266
|
+
allow_outside_workspace: z.boolean().default(false),
|
|
381
267
|
/** Allow `http(s)` fetches to loopback/private/link-local addresses. */
|
|
382
|
-
allow_private_network: z
|
|
268
|
+
allow_private_network: z.boolean().default(false)
|
|
383
269
|
});
|
|
384
270
|
/** Knowledge store-specific config. */
|
|
385
|
-
const KnowledgeConfigSchema = z
|
|
386
|
-
db: z
|
|
271
|
+
const KnowledgeConfigSchema = z.object({
|
|
272
|
+
db: z.object({ path: z.string().default("") }).prefault({}),
|
|
387
273
|
/**
|
|
388
274
|
* The managed document directory: one editable `.md` per document, holding the body the KB
|
|
389
275
|
* indexed. Empty ⇒ `<dataHome>/knowledge/docs`. It holds a COPY, never the origin: a
|
|
390
276
|
* `source_uri` is read once at ingest, so editing or deleting these files cannot touch
|
|
391
277
|
* whatever the document came from.
|
|
392
278
|
*/
|
|
393
|
-
docs: z
|
|
279
|
+
docs: z.object({ dir: z.string().default("") }).prefault({}),
|
|
394
280
|
/**
|
|
395
281
|
* Version control for the store's on-disk text, via the reusable `GitRepo`.
|
|
396
282
|
*
|
|
@@ -399,16 +285,16 @@ const KnowledgeConfigSchema = z$1.object({
|
|
|
399
285
|
* must not enter the history. Point `root` higher (e.g. at the whole store directory) to share
|
|
400
286
|
* more, and `ignore` is what keeps the databases out; the defaults already cover them.
|
|
401
287
|
*/
|
|
402
|
-
git: z
|
|
403
|
-
mode: z
|
|
404
|
-
root: z
|
|
405
|
-
ignore: z
|
|
288
|
+
git: z.object({
|
|
289
|
+
mode: z.enum(["auto", "off"]).default("auto"),
|
|
290
|
+
root: z.string().default(""),
|
|
291
|
+
ignore: z.array(z.string()).default([
|
|
406
292
|
"*.db",
|
|
407
293
|
"*.db-wal",
|
|
408
294
|
"*.db-shm"
|
|
409
295
|
])
|
|
410
296
|
}).prefault({}),
|
|
411
|
-
open: z
|
|
297
|
+
open: z.object({ editor: z.string().default("") }).prefault({}),
|
|
412
298
|
/**
|
|
413
299
|
* The knowledge-domain allowlist: the `domain` names a write may use. The default is a small
|
|
414
300
|
* general set; a deployment narrows or widens it here. An EXPLICIT EMPTY array means no
|
|
@@ -416,7 +302,7 @@ const KnowledgeConfigSchema = z$1.object({
|
|
|
416
302
|
* allowlist is non-empty additionally accepts every domain already present in its `documents`
|
|
417
303
|
* table, so renaming the taxonomy never strands existing documents.
|
|
418
304
|
*/
|
|
419
|
-
domains: z
|
|
305
|
+
domains: z.array(z.string()).default([
|
|
420
306
|
"design",
|
|
421
307
|
"api",
|
|
422
308
|
"ops",
|
|
@@ -431,13 +317,48 @@ const KnowledgeConfigSchema = z$1.object({
|
|
|
431
317
|
* rejected — the embedder bounds them and warns once — but their tail would only reach the
|
|
432
318
|
* FTS leg, so raise this only together with `semantic.max_input_tokens`.
|
|
433
319
|
*/
|
|
434
|
-
chunk_size: z
|
|
320
|
+
chunk_size: z.number().int().positive().default(500),
|
|
435
321
|
/** 10% of `chunk_size`: consecutive chunks share this much context (unchanged ratio). */
|
|
436
|
-
chunk_overlap: z
|
|
437
|
-
source_priority: z
|
|
322
|
+
chunk_overlap: z.number().int().nonnegative().default(50),
|
|
323
|
+
source_priority: z.enum(["workspace", "none"]).default("workspace"),
|
|
438
324
|
ingest: ingestSchema.prefault({})
|
|
439
325
|
});
|
|
440
326
|
//#endregion
|
|
327
|
+
//#region ../contract/lib/family.js
|
|
328
|
+
/**
|
|
329
|
+
* The family root and the two managed directories under it — ONE definition of a convention that
|
|
330
|
+
* four packages would otherwise each spell out.
|
|
331
|
+
*
|
|
332
|
+
* `@avantf/dsh-plugin-base` (the family's environment framework) installs everything it manages under a
|
|
333
|
+
* single root: `$AVANTF_HOME` when set, else `~/.avantf/env`. `tools` holds external binaries
|
|
334
|
+
* (pandoc), `models` the downloaded embedding/rerank weights in the flat layout the transformers.js
|
|
335
|
+
* cache reads. The engine's built-in defaults point HERE, not at the pre-framework
|
|
336
|
+
* `~/.avantf/{tools,models}`: those legacy directories are no longer a fallback anywhere, so a
|
|
337
|
+
* machine that never creates them loses nothing, and no process resolves a path that only works
|
|
338
|
+
* because somebody left a compatibility symlink behind.
|
|
339
|
+
*
|
|
340
|
+
* `provision` mirrors {@link familyHome} (it is deliberately dependency-free, so it cannot import
|
|
341
|
+
* this module); `packages/core/test/family_paths.spec.ts` pins the two copies together so the
|
|
342
|
+
* convention cannot drift.
|
|
343
|
+
*/
|
|
344
|
+
function familyHome$1(env = process.env) {
|
|
345
|
+
const configured = env["AVANTF_HOME"]?.trim() ?? "";
|
|
346
|
+
return configured !== "" ? expandTilde(configured) : join(homedir(), ".avantf", "env");
|
|
347
|
+
}
|
|
348
|
+
/** The managed external-binary root (`<family root>/tools`). */
|
|
349
|
+
function familyToolsDir$1(env = process.env) {
|
|
350
|
+
return join(familyHome$1(env), "tools");
|
|
351
|
+
}
|
|
352
|
+
/** The managed model-cache root (`<family root>/models`). */
|
|
353
|
+
function familyModelsDir(env = process.env) {
|
|
354
|
+
return join(familyHome$1(env), "models");
|
|
355
|
+
}
|
|
356
|
+
/** `~/x` → `<home>/x`, `~` → `<home>`; anything else is used as given. */
|
|
357
|
+
function expandTilde(path) {
|
|
358
|
+
if (path === "~") return homedir();
|
|
359
|
+
return path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
441
362
|
//#region ../contract/lib/tools.js
|
|
442
363
|
/**
|
|
443
364
|
* The closed value sets these tools accept, exported so no other surface has to retype them.
|
|
@@ -447,6 +368,15 @@ const KnowledgeConfigSchema = z$1.object({
|
|
|
447
368
|
* refusing the new value — with no test failing anywhere. One array, every consumer reading it.
|
|
448
369
|
* (`FACT_STATUSES` lives in `types.ts`, beside the `FactStatus` type it is the single source of.)
|
|
449
370
|
*/
|
|
371
|
+
/**
|
|
372
|
+
* The built-in category vocabulary, spelled for the fields that DESCRIBE it.
|
|
373
|
+
*
|
|
374
|
+
* `category` is free-form (`FactCategory` is `… | string`) and stays that way — this is a suggestion
|
|
375
|
+
* a caller can see, not a closed set the engine enforces. It is written once, in `types.ts`, and read
|
|
376
|
+
* here so the constant has a real consumer: before this, nothing at runtime ever read it, which meant
|
|
377
|
+
* editing it changed nothing anywhere.
|
|
378
|
+
*/
|
|
379
|
+
const CATEGORY_HINT = `内置分类:${CATEGORY_VALUES.join(" / ")};也可自定义。`;
|
|
450
380
|
const CONTRADICTION_RESOLUTIONS = ["true_positive", "false_positive"];
|
|
451
381
|
const QUERY_KINDS = [
|
|
452
382
|
"all",
|
|
@@ -454,6 +384,37 @@ const QUERY_KINDS = [
|
|
|
454
384
|
"doc_chunk"
|
|
455
385
|
];
|
|
456
386
|
/**
|
|
387
|
+
* The longest retrieval query the contract accepts, in characters.
|
|
388
|
+
*
|
|
389
|
+
* There is no useful query beyond this, and there IS a cost: a CJK query is expanded into one trigram
|
|
390
|
+
* OR-phrase per character and evaluated by SQLite **synchronously on the host's event loop** (200 000
|
|
391
|
+
* characters measured at 119 seconds, of which the expansion was 27–52 ms — the rest was `MATCH`). The
|
|
392
|
+
* cap belongs HERE because every entry point derives from this union: the plugin's tools, the MCP
|
|
393
|
+
* inputSchema, the CLI's argv validation and the browser payload are all this same object.
|
|
394
|
+
*/
|
|
395
|
+
const MAX_QUERY_CHARS = 2e3;
|
|
396
|
+
/** Why the cap exists, in the words the caller sees. */
|
|
397
|
+
const QUERY_TOO_LONG = `检索文本最长 ${String(MAX_QUERY_CHARS)} 字符:更长的输入会被展开成成百上千个 OR 短语并在宿主事件循环上同步执行。把范围收窄,或先用 kb_ingest 把整篇文档入库再检索。`;
|
|
398
|
+
/**
|
|
399
|
+
* Vocabulary that describes HOW memory is retained, banned from every model-facing string.
|
|
400
|
+
*
|
|
401
|
+
* The model cannot observe those clocks and has no action to take on them; the one thing such
|
|
402
|
+
* text enables is rewriting a fact to "refresh" it, which defeats the policy it just described.
|
|
403
|
+
* It binds every tool description and parameter description AND the DSH plugin's system-prompt
|
|
404
|
+
* section, because the prompt section is prompt text too and a second copy of the list is a copy
|
|
405
|
+
* that drifts.
|
|
406
|
+
*/
|
|
407
|
+
const RETENTION_VOCABULARY = [
|
|
408
|
+
"衰减",
|
|
409
|
+
"消退",
|
|
410
|
+
"遗忘",
|
|
411
|
+
"强化",
|
|
412
|
+
"活跃使用日",
|
|
413
|
+
"信任度",
|
|
414
|
+
"永久记忆",
|
|
415
|
+
"长期有效的信息"
|
|
416
|
+
];
|
|
417
|
+
/**
|
|
457
418
|
* Admin actions that return a FACT VIEW. Their payload goes through
|
|
458
419
|
* {@link modelFacingToolResult}, which drops the retention diagnostics; the other admin
|
|
459
420
|
* actions (`trust_diagnose`, `vectors_diagnose`, …) are diagnostics the caller asked for and
|
|
@@ -470,179 +431,179 @@ function modelFacingToolResult(toolKey, action, value) {
|
|
|
470
431
|
if (toolKey !== "admin") return value;
|
|
471
432
|
return FACT_VIEW_ADMIN_ACTIONS.includes(action) ? withoutRetentionDiagnostics(value) : value;
|
|
472
433
|
}
|
|
473
|
-
const RememberUnion = z
|
|
474
|
-
z
|
|
475
|
-
action: z
|
|
476
|
-
content: z
|
|
477
|
-
category: z
|
|
478
|
-
ttl_days: z
|
|
434
|
+
const RememberUnion = z.discriminatedUnion("action", [
|
|
435
|
+
z.object({
|
|
436
|
+
action: z.literal("add"),
|
|
437
|
+
content: z.string().min(1).describe("要写入或替换的事实内容:一句自包含的陈述。action=add 时必填。"),
|
|
438
|
+
category: z.string().optional().describe(`分类标签;可选,默认 general。${CATEGORY_HINT}`),
|
|
439
|
+
ttl_days: z.number().int().nonnegative().optional().describe("有效期天数(0 或省略 = 不设有效期;正整数 = 自写入起该天数后自动归档)。update 未给则继承被改写事实的 TTL。")
|
|
479
440
|
}),
|
|
480
|
-
z
|
|
481
|
-
action: z
|
|
482
|
-
fact_id: z
|
|
483
|
-
content: z
|
|
484
|
-
category: z
|
|
485
|
-
ttl_days: z
|
|
441
|
+
z.object({
|
|
442
|
+
action: z.literal("update"),
|
|
443
|
+
fact_id: z.number().int().positive().describe("要更新的事实 ID。action=update 时必填。"),
|
|
444
|
+
content: z.string().min(1).describe("替换后的新事实内容。action=update 时必填。"),
|
|
445
|
+
category: z.string().optional().describe(`新的分类标签;可选,未给则继承被改写事实的分类。${CATEGORY_HINT}`),
|
|
446
|
+
ttl_days: z.number().int().nonnegative().optional().describe("新的有效期天数(0 = 取消有效期);可选,未给则继承被改写事实的 TTL。")
|
|
486
447
|
}),
|
|
487
|
-
z
|
|
488
|
-
action: z
|
|
489
|
-
fact_id: z
|
|
490
|
-
reason: z
|
|
448
|
+
z.object({
|
|
449
|
+
action: z.literal("remove"),
|
|
450
|
+
fact_id: z.number().int().positive().describe("要删除的事实 ID。action=remove 时必填。"),
|
|
451
|
+
reason: z.string().optional().describe("删除原因;可选。")
|
|
491
452
|
}),
|
|
492
|
-
z
|
|
493
|
-
action: z
|
|
494
|
-
fact_id: z
|
|
453
|
+
z.object({
|
|
454
|
+
action: z.literal("helpful"),
|
|
455
|
+
fact_id: z.number().int().positive().describe("对回答有帮助的事实 ID。action=helpful 时必填。")
|
|
495
456
|
}),
|
|
496
|
-
z
|
|
497
|
-
action: z
|
|
498
|
-
fact_id: z
|
|
457
|
+
z.object({
|
|
458
|
+
action: z.literal("unhelpful"),
|
|
459
|
+
fact_id: z.number().int().positive().describe("对回答无帮助的事实 ID。action=unhelpful 时必填。")
|
|
499
460
|
})
|
|
500
461
|
]);
|
|
501
|
-
const RecallUnion = z
|
|
502
|
-
z
|
|
503
|
-
action: z
|
|
504
|
-
query: z
|
|
505
|
-
category: z
|
|
506
|
-
limit: z
|
|
507
|
-
max_tokens: z
|
|
462
|
+
const RecallUnion = z.discriminatedUnion("action", [
|
|
463
|
+
z.object({
|
|
464
|
+
action: z.literal("search"),
|
|
465
|
+
query: z.string().min(1).max(MAX_QUERY_CHARS, QUERY_TOO_LONG).describe("检索文本。action=search 时必填。"),
|
|
466
|
+
category: z.string().optional().describe("限定分类;可选。"),
|
|
467
|
+
limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选。"),
|
|
468
|
+
max_tokens: z.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0=不限制。缺省用配置 retrieval.max_output_tokens。")
|
|
508
469
|
}),
|
|
509
|
-
z
|
|
510
|
-
action: z
|
|
511
|
-
subj: z
|
|
512
|
-
pred: z
|
|
513
|
-
obj: z
|
|
514
|
-
query: z
|
|
515
|
-
limit: z
|
|
470
|
+
z.object({
|
|
471
|
+
action: z.literal("ask"),
|
|
472
|
+
subj: z.string().optional().describe("三元组主语;action=ask 时与 pred/obj/query 至少给一个。"),
|
|
473
|
+
pred: z.string().optional().describe("三元组谓语。"),
|
|
474
|
+
obj: z.string().optional().describe("三元组宾语。"),
|
|
475
|
+
query: z.string().optional().describe("自然语言问题;给了 query 就按整句检索。"),
|
|
476
|
+
limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
|
|
516
477
|
}),
|
|
517
|
-
z
|
|
518
|
-
action: z
|
|
519
|
-
subj: z
|
|
520
|
-
pred: z
|
|
521
|
-
second_pred: z
|
|
522
|
-
limit: z
|
|
478
|
+
z.object({
|
|
479
|
+
action: z.literal("chain"),
|
|
480
|
+
subj: z.string().min(1).describe("链式推理的起点实体。action=chain 时必填。"),
|
|
481
|
+
pred: z.string().optional().describe("第一跳关系;可选。"),
|
|
482
|
+
second_pred: z.string().optional().describe("第二跳关系;可选。"),
|
|
483
|
+
limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
|
|
523
484
|
}),
|
|
524
|
-
z
|
|
525
|
-
action: z
|
|
526
|
-
entity: z
|
|
527
|
-
category: z
|
|
528
|
-
limit: z
|
|
485
|
+
z.object({
|
|
486
|
+
action: z.literal("probe"),
|
|
487
|
+
entity: z.string().min(1).describe("要探查的实体名。action=probe 时必填。"),
|
|
488
|
+
category: z.string().optional().describe("限定分类;可选。"),
|
|
489
|
+
limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选。")
|
|
529
490
|
}),
|
|
530
|
-
z
|
|
531
|
-
action: z
|
|
532
|
-
entities: z
|
|
533
|
-
category: z
|
|
534
|
-
limit: z
|
|
491
|
+
z.object({
|
|
492
|
+
action: z.literal("reason"),
|
|
493
|
+
entities: z.array(z.string().min(1)).min(1).describe("参与推理的实体名数组(≥1)。action=reason 时必填。"),
|
|
494
|
+
category: z.string().optional().describe("限定分类;可选。"),
|
|
495
|
+
limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
|
|
535
496
|
}),
|
|
536
|
-
z
|
|
537
|
-
action: z
|
|
538
|
-
entity: z
|
|
539
|
-
category: z
|
|
540
|
-
limit: z
|
|
497
|
+
z.object({
|
|
498
|
+
action: z.literal("related"),
|
|
499
|
+
entity: z.string().min(1).describe("要查询相关事实的实体名。action=related 时必填。"),
|
|
500
|
+
category: z.string().optional().describe("限定分类;可选。"),
|
|
501
|
+
limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选,默认 10。")
|
|
541
502
|
}),
|
|
542
|
-
z
|
|
543
|
-
action: z
|
|
544
|
-
category: z
|
|
545
|
-
limit: z
|
|
503
|
+
z.object({
|
|
504
|
+
action: z.literal("contradict"),
|
|
505
|
+
category: z.string().optional().describe("限定分类;可选。"),
|
|
506
|
+
limit: z.number().int().positive().max(50).optional().describe("返回条数上限(1-50);可选。")
|
|
546
507
|
})
|
|
547
508
|
]);
|
|
548
|
-
const AdminUnion = z
|
|
549
|
-
z
|
|
550
|
-
z
|
|
551
|
-
action: z
|
|
552
|
-
category: z
|
|
553
|
-
status: z
|
|
554
|
-
limit: z
|
|
555
|
-
offset: z
|
|
509
|
+
const AdminUnion = z.discriminatedUnion("action", [
|
|
510
|
+
z.object({ action: z.literal("stats") }),
|
|
511
|
+
z.object({
|
|
512
|
+
action: z.literal("list"),
|
|
513
|
+
category: z.string().optional().describe("限定分类;可选。"),
|
|
514
|
+
status: z.enum(FACT_STATUSES).optional().describe("按状态过滤;可选,默认 active。"),
|
|
515
|
+
limit: z.number().int().positive().max(500).optional().describe("返回条数上限(1-500);可选,默认 50。"),
|
|
516
|
+
offset: z.number().int().nonnegative().optional().describe("分页偏移(从 0 开始);配合 limit 翻页;可选。")
|
|
556
517
|
}),
|
|
557
|
-
z
|
|
558
|
-
action: z
|
|
559
|
-
fact_id: z
|
|
518
|
+
z.object({
|
|
519
|
+
action: z.literal("detail"),
|
|
520
|
+
fact_id: z.number().int().positive().describe("要查看的事实 ID。action=detail 时必填。")
|
|
560
521
|
}),
|
|
561
|
-
z
|
|
562
|
-
action: z
|
|
563
|
-
fact_id: z
|
|
564
|
-
reason: z
|
|
522
|
+
z.object({
|
|
523
|
+
action: z.literal("archive"),
|
|
524
|
+
fact_id: z.number().int().positive().describe("要归档的事实 ID。action=archive 时必填。"),
|
|
525
|
+
reason: z.string().optional().describe("归档原因;可选。")
|
|
565
526
|
}),
|
|
566
|
-
z
|
|
567
|
-
action: z
|
|
568
|
-
fact_id: z
|
|
527
|
+
z.object({
|
|
528
|
+
action: z.literal("restore"),
|
|
529
|
+
fact_id: z.number().int().positive().describe("要恢复的事实 ID。action=restore 时必填。")
|
|
569
530
|
}),
|
|
570
|
-
z
|
|
571
|
-
action: z
|
|
572
|
-
fact_id: z
|
|
531
|
+
z.object({
|
|
532
|
+
action: z.literal("pin"),
|
|
533
|
+
fact_id: z.number().int().positive().describe("要标记为长期保留的事实 ID。action=pin 时必填。")
|
|
573
534
|
}),
|
|
574
|
-
z
|
|
575
|
-
action: z
|
|
576
|
-
fact_id: z
|
|
535
|
+
z.object({
|
|
536
|
+
action: z.literal("unpin"),
|
|
537
|
+
fact_id: z.number().int().positive().describe("要取消长期保留标记的事实 ID。action=unpin 时必填。")
|
|
577
538
|
}),
|
|
578
|
-
z
|
|
579
|
-
z
|
|
580
|
-
z
|
|
581
|
-
action: z
|
|
582
|
-
dry_run: z
|
|
539
|
+
z.object({ action: z.literal("trust_diagnose") }),
|
|
540
|
+
z.object({ action: z.literal("vectors_diagnose") }),
|
|
541
|
+
z.object({
|
|
542
|
+
action: z.literal("vectors_fix"),
|
|
543
|
+
dry_run: z.boolean().optional().describe("true=只诊断不写入;可选,默认 false。")
|
|
583
544
|
}),
|
|
584
|
-
z
|
|
585
|
-
z
|
|
586
|
-
action: z
|
|
587
|
-
contradiction_id: z
|
|
588
|
-
resolution: z
|
|
589
|
-
loser_fact_id: z
|
|
545
|
+
z.object({ action: z.literal("contradict_check") }),
|
|
546
|
+
z.object({
|
|
547
|
+
action: z.literal("contradict_resolve"),
|
|
548
|
+
contradiction_id: z.number().int().positive().describe("要裁决的矛盾 ID(来自 `mem_remember` 的 contradictions 或 `mem_recall` 的 contradict)。"),
|
|
549
|
+
resolution: z.enum(CONTRADICTION_RESOLUTIONS).describe("true_positive=确实是矛盾(配 loser_fact_id 归档错的一方);false_positive=两条并不冲突,关闭该配对且两条都保留。"),
|
|
550
|
+
loser_fact_id: z.number().int().positive().optional().describe("判错的一方(必须是该配对里的两条之一);给定时会被归档。仅 true_positive 可带。")
|
|
590
551
|
}),
|
|
591
|
-
z
|
|
552
|
+
z.object({ action: z.literal("maintenance") })
|
|
592
553
|
]);
|
|
593
554
|
/** The `source` a write gets when it omits one; a document's identity is `(domain, source, title)`. */
|
|
594
555
|
const DEFAULT_KB_SOURCE = "default";
|
|
595
|
-
const KbUnion = z
|
|
596
|
-
z
|
|
597
|
-
action: z
|
|
598
|
-
text: z
|
|
599
|
-
source_uri: z
|
|
600
|
-
domain: z
|
|
601
|
-
source: z
|
|
602
|
-
title: z
|
|
603
|
-
overwrite: z
|
|
556
|
+
const KbUnion = z.discriminatedUnion("action", [
|
|
557
|
+
z.object({
|
|
558
|
+
action: z.literal("ingest"),
|
|
559
|
+
text: z.string().min(1).optional().describe("要入库的文本;与 source_uri 至少给一个。"),
|
|
560
|
+
source_uri: z.string().optional().describe("来源:本地文件路径或 http(s) URL,内容会被读取后入库;与 text 至少给一个。"),
|
|
561
|
+
domain: z.string().min(1).describe("知识域;取值来自配置 knowledge.domains。action=ingest 时必填。"),
|
|
562
|
+
source: z.string().optional().default(DEFAULT_KB_SOURCE).describe("来源名;可选,缺省 default。"),
|
|
563
|
+
title: z.string().optional().describe("文档标题;可选。"),
|
|
564
|
+
overwrite: z.boolean().optional().describe("已确认覆盖同名文档;缺省拒绝。")
|
|
604
565
|
}),
|
|
605
|
-
z
|
|
606
|
-
action: z
|
|
607
|
-
paths: z
|
|
608
|
-
domain: z
|
|
609
|
-
source: z
|
|
610
|
-
overwrite: z
|
|
566
|
+
z.object({
|
|
567
|
+
action: z.literal("import"),
|
|
568
|
+
paths: z.array(z.string().min(1)).min(1).describe("要导入的文件路径数组(≥1)。action=import 时必填。"),
|
|
569
|
+
domain: z.string().min(1).describe("知识域;取值来自配置 knowledge.domains。action=import 时必填。"),
|
|
570
|
+
source: z.string().optional().default(DEFAULT_KB_SOURCE).describe("来源名;可选,缺省 default。"),
|
|
571
|
+
overwrite: z.boolean().optional().describe("已确认覆盖同名文档;缺省拒绝。")
|
|
611
572
|
}),
|
|
612
|
-
z
|
|
613
|
-
action: z
|
|
614
|
-
domain: z
|
|
615
|
-
source: z
|
|
616
|
-
limit: z
|
|
617
|
-
offset: z
|
|
573
|
+
z.object({
|
|
574
|
+
action: z.literal("list"),
|
|
575
|
+
domain: z.string().optional().describe("限定知识域;可选。"),
|
|
576
|
+
source: z.string().optional().describe("限定来源;可选。"),
|
|
577
|
+
limit: z.number().int().positive().optional().describe("本页最多返回多少篇文档(按 updated_at 倒序);可选,缺省不限。"),
|
|
578
|
+
offset: z.number().int().nonnegative().optional().describe("跳过多少篇(配合 limit 分页);可选,默认 0。")
|
|
618
579
|
}),
|
|
619
|
-
z
|
|
620
|
-
action: z
|
|
621
|
-
doc_id: z
|
|
580
|
+
z.object({
|
|
581
|
+
action: z.literal("detail"),
|
|
582
|
+
doc_id: z.number().int().positive().describe("文档 ID。action=detail 时必填。")
|
|
622
583
|
}),
|
|
623
|
-
z
|
|
624
|
-
action: z
|
|
625
|
-
doc_id: z
|
|
584
|
+
z.object({
|
|
585
|
+
action: z.literal("remove"),
|
|
586
|
+
doc_id: z.number().int().positive().describe("要删除的文档 ID。action=remove 时必填。")
|
|
626
587
|
}),
|
|
627
|
-
z
|
|
628
|
-
action: z
|
|
629
|
-
domain: z
|
|
630
|
-
dry_run: z
|
|
588
|
+
z.object({
|
|
589
|
+
action: z.literal("reindex"),
|
|
590
|
+
domain: z.string().optional().describe("只重建该知识域的索引;可选,缺省全部。"),
|
|
591
|
+
dry_run: z.boolean().optional().describe("true=只报告将重建多少(chunks/实体/向量),不写入;可选,默认 false。")
|
|
631
592
|
}),
|
|
632
|
-
z
|
|
633
|
-
action: z
|
|
634
|
-
doc_id: z
|
|
635
|
-
adopt: z
|
|
636
|
-
dry_run: z
|
|
593
|
+
z.object({
|
|
594
|
+
action: z.literal("sync"),
|
|
595
|
+
doc_id: z.number().int().positive().optional().describe("只同步该文档;可选,缺省检查全部。"),
|
|
596
|
+
adopt: z.boolean().optional().describe("true=认领:把该文档路径上**没有 frontmatter** 的文件重新摄入并补写 frontmatter(action=sync 且给了 doc_id 时才有意义)。整篇覆写会毁掉 frontmatter,此时 `sync` 会**自动**认领(守卫:文件完全没有 frontmatter、正文非空、指纹稳定、路径归属唯一);不满足守卫的会留在 `unclaimed` 里,用这个参数立即认领。"),
|
|
597
|
+
dry_run: z.boolean().optional().describe("true=只报告哪些文档的受管文件被改过(stale/missing/orphans),不重新摄入;可选,默认 false。")
|
|
637
598
|
})
|
|
638
599
|
]);
|
|
639
|
-
const QueryUnion = z
|
|
640
|
-
query: z
|
|
641
|
-
kind: z
|
|
642
|
-
max_tokens: z
|
|
643
|
-
domain: z
|
|
644
|
-
source: z
|
|
645
|
-
limit: z
|
|
600
|
+
const QueryUnion = z.object({
|
|
601
|
+
query: z.string().min(1).max(MAX_QUERY_CHARS, QUERY_TOO_LONG).describe("检索文本,必填。"),
|
|
602
|
+
kind: z.enum(QUERY_KINDS).optional().describe("结果类型:all/fact/doc_chunk;可选,默认 all。"),
|
|
603
|
+
max_tokens: z.number().int().nonnegative().optional().describe("本次结果的总 token 上限;0=不限制。缺省用配置 retrieval.max_output_tokens。"),
|
|
604
|
+
domain: z.string().optional().describe("限定知识域;可选。"),
|
|
605
|
+
source: z.string().optional().describe("限定来源;可选。"),
|
|
606
|
+
limit: z.number().int().positive().max(50).default(10).describe("返回条数上限(1-50);可选,默认 10。")
|
|
646
607
|
});
|
|
647
608
|
const REMEMBER_TOOL = {
|
|
648
609
|
key: "remember",
|
|
@@ -674,9 +635,9 @@ const QUERY_TOOL = {
|
|
|
674
635
|
description: "在**用户已入库的文档切片**与记忆事实里做统一检索(跨库)。只检索、不生成答案。",
|
|
675
636
|
input: QueryUnion
|
|
676
637
|
};
|
|
677
|
-
const KbDomainField = z
|
|
678
|
-
const KbSourceField = z
|
|
679
|
-
const KbTitleField = z
|
|
638
|
+
const KbDomainField = z.string().min(1).describe("知识域;取值来自配置 knowledge.domains。");
|
|
639
|
+
const KbSourceField = z.string().optional().default(DEFAULT_KB_SOURCE).describe("来源名;可选,缺省 default。");
|
|
640
|
+
const KbTitleField = z.string().optional().describe("文档标题;可选,缺省取正文首个标题或首行。");
|
|
680
641
|
const TOOL_SPECS = [
|
|
681
642
|
REMEMBER_TOOL,
|
|
682
643
|
RECALL_TOOL,
|
|
@@ -685,40 +646,40 @@ const TOOL_SPECS = [
|
|
|
685
646
|
key: "kb_add",
|
|
686
647
|
name: "kb_add",
|
|
687
648
|
description: "新增一篇文档入库。**只新增、不修改**:同一 `(domain, source, title)` 已存在时会被**拒绝**。给 text 或 source_uri 是单篇(text 是粘贴文本,source_uri 是本地文件路径或 http(s) URL,可摄入文本、PDF、pandoc 能读的文档格式与 .xlsx);给 paths 是批量导入文件或目录(目录只收 .md/.markdown/.txt/.json/.jsonl/.yaml/.yml/.pdf/.docx/.docm/.odt/.epub/.html/.htm/.xhtml/.tex/.rst/.ipynb/.csv/.tsv/.org/.rtf/.xlsx,不跟随符号链接)。",
|
|
688
|
-
input: z
|
|
649
|
+
input: z.object({
|
|
689
650
|
domain: KbDomainField,
|
|
690
651
|
source: KbSourceField,
|
|
691
652
|
title: KbTitleField,
|
|
692
|
-
text: z
|
|
693
|
-
source_uri: z
|
|
694
|
-
paths: z
|
|
653
|
+
text: z.string().min(1).optional().describe("要入库的文本;与 source_uri、paths 三选一。"),
|
|
654
|
+
source_uri: z.string().optional().describe("来源:本地文件路径或 http(s) URL,内容会被读取后入库。"),
|
|
655
|
+
paths: z.array(z.string().min(1)).min(1).optional().describe("批量导入的文件/目录路径(≥1);给了它就是批量导入。")
|
|
695
656
|
})
|
|
696
657
|
},
|
|
697
658
|
{
|
|
698
659
|
key: "kb_list",
|
|
699
660
|
name: "kb_list",
|
|
700
661
|
description: "列出知识库里的文档:doc_id、domain、source、title、**受管 .md 文件的绝对路径**与更新时间;给了 doc_id 时只取该篇。",
|
|
701
|
-
input: z
|
|
702
|
-
doc_id: z
|
|
703
|
-
domain: z
|
|
704
|
-
source: z
|
|
705
|
-
limit: z
|
|
706
|
-
offset: z
|
|
662
|
+
input: z.object({
|
|
663
|
+
doc_id: z.number().int().positive().optional().describe("给了 `doc_id` 时只取该篇;可选。"),
|
|
664
|
+
domain: z.string().optional().describe("限定知识域;可选,仅在未给 doc_id 时有意义。"),
|
|
665
|
+
source: z.string().optional().describe("限定来源;可选,仅在未给 doc_id 时有意义。"),
|
|
666
|
+
limit: z.number().int().positive().optional().describe("本页最多返回多少篇(按 updated_at 倒序);可选,缺省 50。"),
|
|
667
|
+
offset: z.number().int().nonnegative().optional().describe("跳过多少篇(配合 limit 分页);可选,默认 0。")
|
|
707
668
|
})
|
|
708
669
|
},
|
|
709
670
|
{
|
|
710
671
|
key: "kb_remove",
|
|
711
672
|
name: "kb_remove",
|
|
712
673
|
description: "删除一篇文档,连同它在 `knowledge.docs.dir` 下的受管 .md 副本。",
|
|
713
|
-
input: z
|
|
674
|
+
input: z.object({ doc_id: z.number().int().positive().describe("要删除的文档 ID。") })
|
|
714
675
|
},
|
|
715
676
|
{
|
|
716
677
|
key: "kb_reindex",
|
|
717
678
|
name: "kb_reindex",
|
|
718
679
|
description: "重建知识库索引(分块/实体/向量)。可选限定 domain。",
|
|
719
|
-
input: z
|
|
720
|
-
domain: z
|
|
721
|
-
dry_run: z
|
|
680
|
+
input: z.object({
|
|
681
|
+
domain: z.string().optional().describe("只重建该知识域的索引;可选,缺省全部。"),
|
|
682
|
+
dry_run: z.boolean().optional().describe("true=只报告将重建多少,不写入;可选,默认 false。")
|
|
722
683
|
})
|
|
723
684
|
},
|
|
724
685
|
QUERY_TOOL
|
|
@@ -899,8 +860,14 @@ function errorCode(error) {
|
|
|
899
860
|
* `~/` expansion and boolean env parsing (previously duplicated in core paths,
|
|
900
861
|
* both model adapters, and the config loader).
|
|
901
862
|
*/
|
|
902
|
-
/** Expand a
|
|
863
|
+
/** Expand `~/…` (and a BARE `~`) against the USER home directory.
|
|
864
|
+
*
|
|
865
|
+
* The bare form matters: `AVANTF_HOME=~` used to come back verbatim, i.e. as the RELATIVE path `~`,
|
|
866
|
+
* and everything built on it was written into the current working directory. The base kit's own
|
|
867
|
+
* `expandHome` always expanded it, so the two copies disagreed on exactly the input a shell makes
|
|
868
|
+
* easy to type. */
|
|
903
869
|
function expandHome$1(p) {
|
|
870
|
+
if (p === "~") return homedir();
|
|
904
871
|
return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
|
|
905
872
|
}
|
|
906
873
|
/** Parse a boolean env var: `0`/`false` → false, unset/empty → undefined, else true. */
|
|
@@ -937,21 +904,231 @@ function envAutoDownload() {
|
|
|
937
904
|
return project;
|
|
938
905
|
}
|
|
939
906
|
//#endregion
|
|
907
|
+
//#region lib/types/prompt.js
|
|
908
|
+
/**
|
|
909
|
+
* The three system-prompt sections this plugin contributes (DESIGN §10).
|
|
910
|
+
*
|
|
911
|
+
* The TEXTS are user-editable: each section's body lives in its own `.md` under the shared family
|
|
912
|
+
* prompt directory `<data_home>/prompts` (`PROMPT_FILES` maps file → section), and the constants
|
|
913
|
+
* below are both the built-in default and what a missing/blank file is filled with. The identity —
|
|
914
|
+
* section name and order — stays in code, so editing a file cannot silently move a section in the
|
|
915
|
+
* prompt. The loader itself is generic and lives in the family base (`PromptFiles`, exported by
|
|
916
|
+
* `@avantf/dsh-plugin-base`), because "ensure the file exists, otherwise use the default" is the same
|
|
917
|
+
* flow for every section — and taking it from the base at RUNTIME means fixing it takes one base
|
|
918
|
+
* release, not a plugin rebuild. When the base is unavailable the built-in defaults below are used
|
|
919
|
+
* and nothing is written to disk.
|
|
920
|
+
*
|
|
921
|
+
* They exist for something a tool description cannot carry: a description says what a call DOES,
|
|
922
|
+
* never when to reach for it unprompted, and the default behaviour is to wait until asked. So each
|
|
923
|
+
* says only WHEN its store is worth writing or reading. Neither says anything about how memory is
|
|
924
|
+
* retained — that stays out of every model-facing string, held to the contract's
|
|
925
|
+
* `RETENTION_VOCABULARY`, the same list the tool descriptions are held to.
|
|
926
|
+
*
|
|
927
|
+
* The knowledge one is the section whose ABSENCE was measured: a user had just ingested
|
|
928
|
+
* 《Cgroup v2 技术综述》(which answers "cgroup v2 的内存保护" at score 1.0) and asked about it in a
|
|
929
|
+
* session whose prompt named only the memory tools, so the model never looked. A tool description
|
|
930
|
+
* cannot fix that by itself — `kb_query`'s says what the call does and even leads with 记忆 facts,
|
|
931
|
+
* which reads as a memory tool — and the model cannot know the library is relevant to a question
|
|
932
|
+
* without being told to check it. Hence the "先检索一次再作答" nudge, plus the explicit
|
|
933
|
+
* "你看不到库里有什么" clause: the thing that made the miss invisible is that nothing in the
|
|
934
|
+
* prompt acknowledged the store existed at all.
|
|
935
|
+
*
|
|
936
|
+
* `order` 3000/3010 are third-party slots: after the harness's per-tool guidance block (1000–2900)
|
|
937
|
+
* and before its toolset-level guidance (5000). They are not repository-owned placements, so they
|
|
938
|
+
* do not resolve through `getSectionOrder`.
|
|
939
|
+
*
|
|
940
|
+
* The sections say WHAT TO DO and never WHY. The reasoning behind each rule is measured and
|
|
941
|
+
* lives here, in DESIGN §10 and in the CHANGELOG — not in the prompt, where it costs tokens on
|
|
942
|
+
* every step and does not change the action.
|
|
943
|
+
*
|
|
944
|
+
* Kept in its own module — not in `index.ts` — so the text can be read and checked without the
|
|
945
|
+
* plugin entry's `@deepseek-ai/*` runtime imports, which resolve only in a harness workspace.
|
|
946
|
+
*/
|
|
947
|
+
const MEMORY_PROMPT_SECTION = {
|
|
948
|
+
name: "avantf:memory-usage",
|
|
949
|
+
order: 3e3,
|
|
950
|
+
text: "记忆(`mem_remember`):把**重要且跨会话仍成立**的事实主动记下来——用户偏好、项目决策、环境约束、接口约定、踩过的坑。同一事实的修正用 `update`,一句自包含的陈述。不要记录临时聊天内容、一次性任务的中间状态、能直接从代码或文档读到的内容。"
|
|
951
|
+
};
|
|
952
|
+
/**
|
|
953
|
+
* WHEN to consult the document knowledge base. See the module comment for why this is a separate
|
|
954
|
+
* section rather than a clause inside the memory one: the two stores answer different questions,
|
|
955
|
+
* and a session that needs the knowledge base may have no memory question at all.
|
|
956
|
+
*/
|
|
957
|
+
const KNOWLEDGE_PROMPT_SECTION = {
|
|
958
|
+
name: "avantf:knowledge-usage",
|
|
959
|
+
order: 3010,
|
|
960
|
+
text: "知识库(`kb_query` / `kb_add`)存放**用户提供的成篇资料**:文档、长说明、规范、综述等要按原文查阅的内容。回答涉及已入库资料的问题之前,先用 `kb_query` 检索;命中就按 `source_ref` 引用原文。用户给了值得长期留存的成篇内容就主动 `kb_add` 入库;一句话的事实属于记忆(`mem_remember`),不要入知识库。"
|
|
961
|
+
};
|
|
962
|
+
/**
|
|
963
|
+
* Where each section's text comes from: one `.md` per section, inside the SHARED family prompt
|
|
964
|
+
* directory (`<data_home>/prompts`, see {@link PromptFiles}).
|
|
965
|
+
*
|
|
966
|
+
* Every avantf plugin writes its prompts into that one directory and owns a prefix — `mem-*` here,
|
|
967
|
+
* `work-*` in the work engine — so a deployment can find and diff all of its model-facing text in
|
|
968
|
+
* one place without any plugin having to guess which files belong to it. This manifest is that
|
|
969
|
+
* ownership: a `.md` it does not list is ignored (never read, never written, never deleted).
|
|
970
|
+
*
|
|
971
|
+
* The FILE owns the text; the CODE keeps the identity. Mapping a file name to a section HERE is what
|
|
972
|
+
* makes that true: no edit to the directory can move a section to a different point in the prompt
|
|
973
|
+
* (order) or change the name the harness dedupes on — and no file another plugin owns can be
|
|
974
|
+
* mistaken for one of these.
|
|
975
|
+
*
|
|
976
|
+
* `fallback` is taken from the constant above rather than retyped, so "the text that used to be
|
|
977
|
+
* hardcoded" and "the text a missing file is filled with" cannot drift apart.
|
|
978
|
+
*/
|
|
979
|
+
const PROMPT_FILES = [
|
|
980
|
+
{
|
|
981
|
+
file: "mem-memory-usage.md",
|
|
982
|
+
section: MEMORY_PROMPT_SECTION
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
file: "mem-knowledge-usage.md",
|
|
986
|
+
section: KNOWLEDGE_PROMPT_SECTION
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
file: "mem-kb-edit.md",
|
|
990
|
+
section: {
|
|
991
|
+
name: "avantf:kb-edit",
|
|
992
|
+
order: 3020,
|
|
993
|
+
text: "更新知识库时,要在原文上修改或追加,不要新建一个补充文档。用 `kb_list` 拿到受管 `.md` 的绝对路径,用文件工具修改。在用户明确要求时使用 `kb_remove` 删除。"
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
];
|
|
997
|
+
/** The generic loader's input: which file, and the body to write/use when it is missing or blank. */
|
|
998
|
+
function promptFileSpecs() {
|
|
999
|
+
return PROMPT_FILES.map(({ file, section }) => ({
|
|
1000
|
+
file,
|
|
1001
|
+
fallback: section.text
|
|
1002
|
+
}));
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Zip the loaded texts back onto their sections, in manifest order.
|
|
1006
|
+
*
|
|
1007
|
+
* A section whose text is missing from `loaded` (the caller loaded a partial manifest) keeps its
|
|
1008
|
+
* built-in default rather than losing its prompt entirely.
|
|
1009
|
+
*/
|
|
1010
|
+
function buildPromptSections(loaded) {
|
|
1011
|
+
const textByFile = new Map(loaded.map((entry) => [entry.file, entry.text]));
|
|
1012
|
+
return PROMPT_FILES.map(({ file, section }) => ({
|
|
1013
|
+
name: section.name,
|
|
1014
|
+
order: section.order,
|
|
1015
|
+
text: textByFile.get(file) ?? section.text
|
|
1016
|
+
}));
|
|
1017
|
+
}
|
|
1018
|
+
/** Explanatory phrasings the defaults are forbidden to contain; the spec guards the same shapes. */
|
|
1019
|
+
const WHY_WORDS = [
|
|
1020
|
+
"因为",
|
|
1021
|
+
"否则",
|
|
1022
|
+
"原因是",
|
|
1023
|
+
"之所以"
|
|
1024
|
+
];
|
|
1025
|
+
/**
|
|
1026
|
+
* What is worth saying about text the USER wrote — as warnings, never as edits.
|
|
1027
|
+
*
|
|
1028
|
+
* The defaults are held to hard rules by `prompt_section.spec.ts`: under the budget (they are
|
|
1029
|
+
* rendered into every model step), no retention vocabulary, no explanatory "why". An edited file
|
|
1030
|
+
* cannot be held to them — it is the user's prompt — but all three are MEASURED regressions rather
|
|
1031
|
+
* than style, so a line in the log is cheap and a silently worse prompt is not. Nothing is
|
|
1032
|
+
* truncated and nothing is refused: the file is injected exactly as written.
|
|
1033
|
+
*/
|
|
1034
|
+
function promptTextWarnings(sections) {
|
|
1035
|
+
const out = [];
|
|
1036
|
+
for (const section of sections) {
|
|
1037
|
+
const text = typeof section.text === "string" ? section.text : "";
|
|
1038
|
+
if (text.length >= 400) out.push(`${section.name} is ${String(text.length)} chars (every model step carries it; the built-in default is under ${String(400)})`);
|
|
1039
|
+
for (const word of RETENTION_VOCABULARY) if (text.includes(word)) out.push(`${section.name} describes retention ("${word}"), which invites rewriting a fact to refresh it`);
|
|
1040
|
+
for (const word of WHY_WORDS) if (text.includes(word)) out.push(`${section.name} explains why ("${word}"); the defaults state actions only`);
|
|
1041
|
+
}
|
|
1042
|
+
return out;
|
|
1043
|
+
}
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region lib/types/hints.js
|
|
1046
|
+
/**
|
|
1047
|
+
* The plugin's CONDITIONAL hint: ONE line, contributed only when either store already holds
|
|
1048
|
+
* something relevant to the message the user just sent (DESIGN §12).
|
|
1049
|
+
*
|
|
1050
|
+
* These are deliberately NOT part of the always-on usage sections. A usage section says WHEN a
|
|
1051
|
+
* store is worth consulting and therefore has to be paid for on every step of every session; this
|
|
1052
|
+
* says "there is something here, right now", which is only true sometimes. The two layers compose:
|
|
1053
|
+
* the section is the fallback for the paraphrase the lexical probe misses, and the hint is the
|
|
1054
|
+
* concrete nudge for the case it catches.
|
|
1055
|
+
*
|
|
1056
|
+
* ONE LINE FOR BOTH STORES, and it names ONE tool. `kb_query` is a cross-store retrieval (document
|
|
1057
|
+
* chunks AND memory facts), so a single line covers whichever store matched. Per-store lines used
|
|
1058
|
+
* to name two tools, which invited two overlapping calls — the memory facts came back twice, once
|
|
1059
|
+
* from `mem_recall` and again inside `kb_query`'s fused result — and made every memory↔knowledge
|
|
1060
|
+
* transition a fresh snapshot append. `mem_recall` remains the tool for the memory-only actions
|
|
1061
|
+
* (chain / probe / reason / contradict); the always-on memory usage section is where that is said.
|
|
1062
|
+
*
|
|
1063
|
+
* WHY THE LINE NAMES THE PLUGIN. This text is injected as runtime context, and the harness
|
|
1064
|
+
* materialises that as a USER-role snapshot in the conversation (`dsh-agent-loop`'s
|
|
1065
|
+
* `runtimeContext.project`). Without an author, a line like "记忆里有相关内容" reads as something
|
|
1066
|
+
* the user said. The `[avantf-mem]` prefix is what keeps the provenance honest.
|
|
1067
|
+
*/
|
|
1068
|
+
const RELEVANCE_HINT = "[avantf-mem] 记忆或知识库里有与上条用户消息相关的内容;需要时用 `kb_query` 检索。";
|
|
1069
|
+
/**
|
|
1070
|
+
* Map one answer to the contribution, as text.
|
|
1071
|
+
*
|
|
1072
|
+
* An empty string is the "contribute nothing" value: `renderContextSections` drops empty text, so
|
|
1073
|
+
* "nothing found" costs no tokens at all.
|
|
1074
|
+
*/
|
|
1075
|
+
function hintText(hit) {
|
|
1076
|
+
return hit ? RELEVANCE_HINT : "";
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* The plain text of an inbox message, or `''` when it carries none.
|
|
1080
|
+
*
|
|
1081
|
+
* `dsh-agent-loop`'s own extractor returns `undefined` unless the message is exactly ONE text
|
|
1082
|
+
* block; a steered or multi-part message would then silently produce no hint. Concatenating the
|
|
1083
|
+
* text blocks keeps the probe working for those, and non-text blocks (images) are simply skipped —
|
|
1084
|
+
* the probe is lexical, so it has nothing to say about them anyway.
|
|
1085
|
+
*/
|
|
1086
|
+
function messageText(message) {
|
|
1087
|
+
const content = message?.content;
|
|
1088
|
+
if (!Array.isArray(content)) return "";
|
|
1089
|
+
return content.map((block) => {
|
|
1090
|
+
const b = block;
|
|
1091
|
+
return b?.type === "text" && typeof b.text === "string" ? b.text : "";
|
|
1092
|
+
}).filter(Boolean).join("\n");
|
|
1093
|
+
}
|
|
1094
|
+
//#endregion
|
|
940
1095
|
//#region ../core/lib/config/paths.js
|
|
941
1096
|
/**
|
|
942
1097
|
* Resolve the data home (default `~/.avantf`).
|
|
943
1098
|
*
|
|
944
|
-
* Precedence, per DESIGN §3
|
|
945
|
-
*
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
1099
|
+
* Precedence, per DESIGN §3 and the family layering (root `AGENTS.md` 「边界与路径」: built-in →
|
|
1100
|
+
* profile → store → ENV → explicit): an EXPLICIT caller value (⑤) → `AVANTF_HOME` (④) → the
|
|
1101
|
+
* configured `common.dataHome` (②) → `~/.avantf`. The environment is a deployment override and
|
|
1102
|
+
* outranks the config file.
|
|
1103
|
+
*
|
|
1104
|
+
* `configured` cannot stand in for layer ⑤: the schema gives it a default, so it always holds a
|
|
1105
|
+
* value and would make an explicit argument indistinguishable from "nothing was configured" — which
|
|
1106
|
+
* is exactly how the env var used to win over a caller's own `dataHome`.
|
|
1107
|
+
*
|
|
1108
|
+
* The slots are NAMED for the same reason the base kit's copy is: the two implementations used to
|
|
1109
|
+
* disagree about their POSITIONAL order (`(explicit, env, common)` here, `(common, explicit)`…
|
|
1110
|
+
* whichever way a reader guessed), so "which slot does the profile's `dataHome` belong in?" was
|
|
1111
|
+
* answerable only by reading the call site. The base kit implements the same rule with the same named
|
|
1112
|
+
* shape, and `packages/plugin/test/family_pin.spec.ts` pins the two implementations together.
|
|
949
1113
|
*/
|
|
950
|
-
function resolveDataHome(
|
|
951
|
-
|
|
1114
|
+
function resolveDataHome(input) {
|
|
1115
|
+
const layer5 = input.explicit?.trim() ?? "";
|
|
1116
|
+
const fromEnv = process.env["AVANTF_HOME"]?.trim() ?? "";
|
|
1117
|
+
const layer2 = input.common.dataHome?.trim() ?? "";
|
|
1118
|
+
return expandHome$1(layer5 !== "" ? layer5 : fromEnv !== "" ? fromEnv : layer2 !== "" ? layer2 : "~/.avantf");
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Every config file lives in ONE directory, beside the prompt files.
|
|
1122
|
+
*
|
|
1123
|
+
* The layering is unchanged (① built-in → ② common → ③ store → ④ env → ⑤ explicit); only the
|
|
1124
|
+
* locations moved, so all of a deployment's editable text — `configs/*.yaml` and `prompts/*.md` —
|
|
1125
|
+
* is in one place instead of scattered through `memory/` and `knowledge/` next to the databases.
|
|
1126
|
+
*/
|
|
1127
|
+
function configsDir(home) {
|
|
1128
|
+
return join(home, "configs");
|
|
952
1129
|
}
|
|
953
1130
|
function commonConfigPath(home) {
|
|
954
|
-
return join(home, "
|
|
1131
|
+
return join(configsDir(home), "common.yaml");
|
|
955
1132
|
}
|
|
956
1133
|
function memoryDir(home) {
|
|
957
1134
|
return join(home, "memory");
|
|
@@ -960,10 +1137,10 @@ function knowledgeDir(home) {
|
|
|
960
1137
|
return join(home, "knowledge");
|
|
961
1138
|
}
|
|
962
1139
|
function memoryConfigPath(home) {
|
|
963
|
-
return join(
|
|
1140
|
+
return join(configsDir(home), "memory.yaml");
|
|
964
1141
|
}
|
|
965
1142
|
function knowledgeConfigPath(home) {
|
|
966
|
-
return join(
|
|
1143
|
+
return join(configsDir(home), "knowledge.yaml");
|
|
967
1144
|
}
|
|
968
1145
|
function memoryDbPath(home, override) {
|
|
969
1146
|
const fromEnv = process.env["AVANTF_MEM_DB"];
|
|
@@ -988,6 +1165,97 @@ function knowledgeDocsDir(home, override) {
|
|
|
988
1165
|
function ensureDataLayout(home) {
|
|
989
1166
|
mkdirSync(memoryDir(home), { recursive: true });
|
|
990
1167
|
mkdirSync(knowledgeDir(home), { recursive: true });
|
|
1168
|
+
mkdirSync(configsDir(home), { recursive: true });
|
|
1169
|
+
}
|
|
1170
|
+
//#endregion
|
|
1171
|
+
//#region ../core/lib/config/config_files.js
|
|
1172
|
+
/**
|
|
1173
|
+
* The default `configs/common.yaml`, written when the file is missing or blank.
|
|
1174
|
+
*
|
|
1175
|
+
* It is **comments only**, on purpose: the built-in defaults are already in force, so an all-comment
|
|
1176
|
+
* file changes NOTHING about how the engine behaves — it only puts the knobs, their defaults and the
|
|
1177
|
+
* two managed keys' escape hatches where a reader will find them. Uncommenting a line is the edit;
|
|
1178
|
+
* deleting the file brings the defaults straight back.
|
|
1179
|
+
*
|
|
1180
|
+
* The rule mirrors the prompt files (the plugins' `src/prompt_files.ts`): missing or blank is an
|
|
1181
|
+
* unfinished edit, not an empty configuration, so it is filled in. A file with ANY content is the
|
|
1182
|
+
* user's and is never rewritten.
|
|
1183
|
+
*
|
|
1184
|
+
* Keys must match the schema in `@avantf/mem-contract` (`ConfigSchema`): an unknown key in an
|
|
1185
|
+
* UNCOMMENTED line is warned about by the loader, so a wrong name here would be a trap rather than a
|
|
1186
|
+
* hint. `config_files.spec.ts` pins the ones this template names.
|
|
1187
|
+
*/
|
|
1188
|
+
const DEFAULT_COMMON_CONFIG = `# avantf-mem 公共配置(分层第②层)。这里全是注释:删掉/留空 = 用内建默认,取消注释即生效。
|
|
1189
|
+
# 分层(低→高):① 内建默认 → ② 本文件 → ③ 同目录的 memory.yaml / knowledge.yaml(缺失=不覆盖)
|
|
1190
|
+
# → ④ 环境变量 → ⑤ 插件/CLI 显式传参
|
|
1191
|
+
# 所有可编辑文本都在 ~/.avantf 下:configs/*.yaml(本目录)与 prompts/*.md(模型提示词)。
|
|
1192
|
+
|
|
1193
|
+
# 数据根不在这里配:它由 ④ AVANTF_HOME(或 CLI --data-home、插件 profile 的 dataHome)决定 ——
|
|
1194
|
+
# 本文件就在数据根里面,解析根的时候还没读到它,所以写在这里的 dataHome 对数据根无效(DESIGN §3)。
|
|
1195
|
+
|
|
1196
|
+
# semantic: # 嵌入后端(记忆与知识共用,跨库分数才可比)
|
|
1197
|
+
# backend: local_bge
|
|
1198
|
+
# local_model: Xenova/bge-small-zh-v1.5 # 必须是 ONNX 仓库
|
|
1199
|
+
# dim: 512
|
|
1200
|
+
# auto_download: true
|
|
1201
|
+
# ⚠️ cache_dir / mirror 由家族底座 @avantf/dsh-plugin-base 管理:写在这里会被忽略并告警。
|
|
1202
|
+
# 缓存目录用环境变量 AVANTF_MEM_MODEL_CACHE,镜像用 AVANTF_MEM_MODEL_MIRROR(或 HF_ENDPOINT)。
|
|
1203
|
+
|
|
1204
|
+
# rerank: # 重排的唯一开关;none = 不加载任何重排模型
|
|
1205
|
+
# backend: none
|
|
1206
|
+
# local_model: Xenova/bge-reranker-base
|
|
1207
|
+
|
|
1208
|
+
# vectorStore:
|
|
1209
|
+
# backend: auto # auto = 越过阈值自动从 local_numpy 迁到 hnswlib
|
|
1210
|
+
# auto_thresholds:
|
|
1211
|
+
# hnswlib: 2000
|
|
1212
|
+
# hnswlib_ef_search: 256 # ANN 的召回/速度旋钮,语料变大先调它
|
|
1213
|
+
|
|
1214
|
+
# retriever: # 三条检索腿在融合里的权重
|
|
1215
|
+
# weight_semantic: 0.55
|
|
1216
|
+
# weight_fts: 0.30
|
|
1217
|
+
# weight_jaccard: 0.15
|
|
1218
|
+
|
|
1219
|
+
# lifecycle:
|
|
1220
|
+
# purge_after_archived_days: 365 # 归档事实的物理清理窗口(活跃日)
|
|
1221
|
+
# contradiction_threshold: 0.6
|
|
1222
|
+
|
|
1223
|
+
# trust:
|
|
1224
|
+
# decay_per_day: 0.0055…
|
|
1225
|
+
# presence:
|
|
1226
|
+
# heartbeat_minutes: 60 # 常驻进程的心跳(活跃日时钟与生命周期扫描)
|
|
1227
|
+
|
|
1228
|
+
# tools: # 外部二进制(pandoc)的受管目录
|
|
1229
|
+
# dir: '' # 留空 = 家族根 tools(~/.avantf/env/tools);AVANTF_TOOLS_DIR 可覆盖
|
|
1230
|
+
# auto_install: true
|
|
1231
|
+
`;
|
|
1232
|
+
/**
|
|
1233
|
+
* Ensure the common config exists with content, and hand back the path either way.
|
|
1234
|
+
*
|
|
1235
|
+
* Never throws: an unwritable data home must not keep the engine (or the plugin mount) from working
|
|
1236
|
+
* — the built-in defaults are always available, and a missing file is the same thing to the loader
|
|
1237
|
+
* as an empty one.
|
|
1238
|
+
*/
|
|
1239
|
+
function ensureCommonConfigFile(path, logger) {
|
|
1240
|
+
try {
|
|
1241
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1242
|
+
if (existsSync(path)) {
|
|
1243
|
+
if (readFileSync(path, "utf8").replace(/^\uFEFF/u, "").trim() !== "") return;
|
|
1244
|
+
writeAtomic(path, DEFAULT_COMMON_CONFIG);
|
|
1245
|
+
logger?.info(`config file was blank and has been filled with the default: ${path}`);
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
writeAtomic(path, DEFAULT_COMMON_CONFIG);
|
|
1249
|
+
logger?.info(`config file created with the default (all comments): ${path}`);
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
logger?.warn(`config file ${path} is unusable (${error instanceof Error ? error.message : String(error)}); using the built-in defaults`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/** Temp file + rename, the same shape the managed document copy and the store config use. */
|
|
1255
|
+
function writeAtomic(path, text) {
|
|
1256
|
+
const tmp = `${path}.tmp-${String(process.pid)}`;
|
|
1257
|
+
writeFileSync(tmp, text, "utf8");
|
|
1258
|
+
renameSync(tmp, path);
|
|
991
1259
|
}
|
|
992
1260
|
//#endregion
|
|
993
1261
|
//#region ../core/lib/config/loader.js
|
|
@@ -1163,13 +1431,27 @@ function applyEnvLayer(common) {
|
|
|
1163
1431
|
return next;
|
|
1164
1432
|
}
|
|
1165
1433
|
/**
|
|
1166
|
-
* Load config with layering: ① built-in defaults → ② `~/.avantf/
|
|
1434
|
+
* Load config with layering: ① built-in defaults → ② `~/.avantf/configs/common.yaml`
|
|
1167
1435
|
* (common) → ③ `~/.avantf/{memory,knowledge}/config.yaml` (store) → ④ env →
|
|
1168
1436
|
* ⑤ explicit. Returns the resolved per-store config + data home.
|
|
1169
1437
|
*/
|
|
1170
1438
|
function loadConfig(opts) {
|
|
1171
1439
|
const logger = opts?.logger ?? defaultLogger;
|
|
1172
|
-
let common = {
|
|
1440
|
+
let common = {
|
|
1441
|
+
...defaultConfig,
|
|
1442
|
+
tools: {
|
|
1443
|
+
...defaultConfig.tools,
|
|
1444
|
+
dir: defaultConfig.tools.dir === "" ? familyToolsDir$1() : defaultConfig.tools.dir
|
|
1445
|
+
},
|
|
1446
|
+
semantic: {
|
|
1447
|
+
...defaultConfig.semantic,
|
|
1448
|
+
cache_dir: defaultConfig.semantic.cache_dir === "" ? familyModelsDir() : defaultConfig.semantic.cache_dir
|
|
1449
|
+
},
|
|
1450
|
+
rerank: {
|
|
1451
|
+
...defaultConfig.rerank,
|
|
1452
|
+
cache_dir: defaultConfig.rerank.cache_dir === "" ? familyModelsDir() : defaultConfig.rerank.cache_dir
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1173
1455
|
const managed = opts?.managedRoots;
|
|
1174
1456
|
if (managed !== void 0) common = {
|
|
1175
1457
|
...common,
|
|
@@ -1190,8 +1472,12 @@ function loadConfig(opts) {
|
|
|
1190
1472
|
...common,
|
|
1191
1473
|
dataHome: opts.dataHome
|
|
1192
1474
|
});
|
|
1193
|
-
const home = resolveDataHome(
|
|
1475
|
+
const home = resolveDataHome({
|
|
1476
|
+
common,
|
|
1477
|
+
explicit: opts?.dataHome
|
|
1478
|
+
});
|
|
1194
1479
|
ensureDataLayout(home);
|
|
1480
|
+
ensureCommonConfigFile(commonConfigPath(home), logger);
|
|
1195
1481
|
const commonYaml = readYamlOrNull(commonConfigPath(home));
|
|
1196
1482
|
if (commonYaml) {
|
|
1197
1483
|
stripManagedModelKeys(commonYaml, commonConfigPath(home), logger);
|
|
@@ -1236,7 +1522,7 @@ function loadConfig(opts) {
|
|
|
1236
1522
|
}
|
|
1237
1523
|
/** The built-in `tools` section (layer ①); the config schema defaults to exactly this shape. */
|
|
1238
1524
|
const DEFAULT_TOOLS_CONFIG = {
|
|
1239
|
-
dir: "
|
|
1525
|
+
dir: "",
|
|
1240
1526
|
mirror: [
|
|
1241
1527
|
"https://ghfast.top/{url}",
|
|
1242
1528
|
"https://ghproxy.net/{url}",
|
|
@@ -1251,8 +1537,24 @@ const DEFAULT_TOOLS_CONFIG = {
|
|
|
1251
1537
|
* tool already here?" before any runtime has installed its settings — which is what keeps a startup
|
|
1252
1538
|
* sweep from re-downloading a tool that is already on disk.
|
|
1253
1539
|
*/
|
|
1254
|
-
function defaultToolsDir() {
|
|
1255
|
-
return
|
|
1540
|
+
function defaultToolsDir(env = process.env) {
|
|
1541
|
+
return familyToolsDir(env);
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* The family root and its managed directories — `$AVANTF_HOME` when set, else `~/.avantf/env`.
|
|
1545
|
+
*
|
|
1546
|
+
* Mirrored from `@avantf/mem-contract`'s `familyHome()` / `familyToolsDir()`: this package is
|
|
1547
|
+
* deliberately dependency-free (node builtins only), so it cannot import them, and
|
|
1548
|
+
* `packages/core/test/family_paths.spec.ts` pins the two copies together so the convention cannot
|
|
1549
|
+
* drift. `@avantf/dsh-plugin-base` installs everything it manages under this one root.
|
|
1550
|
+
*/
|
|
1551
|
+
function familyHome(env = process.env) {
|
|
1552
|
+
const configured = env["AVANTF_HOME"];
|
|
1553
|
+
return configured !== void 0 && configured.trim() !== "" ? expandHome(configured.trim()) : join(homedir(), ".avantf", "env");
|
|
1554
|
+
}
|
|
1555
|
+
/** The managed external-binary root (`<family root>/tools`). */
|
|
1556
|
+
function familyToolsDir(env = process.env) {
|
|
1557
|
+
return join(familyHome(env), "tools");
|
|
1256
1558
|
}
|
|
1257
1559
|
/**
|
|
1258
1560
|
* The project-wide download kill switch (`AVANTF_MEM_AUTO_DOWNLOAD=0`), the same variable the model
|
|
@@ -1294,7 +1596,7 @@ function resolveToolsDir(config, env = process.env) {
|
|
|
1294
1596
|
const fromEnv = env["AVANTF_TOOLS_DIR"];
|
|
1295
1597
|
if (fromEnv !== void 0 && fromEnv !== "") return expandHome(fromEnv);
|
|
1296
1598
|
if (config.dir !== "") return expandHome(config.dir);
|
|
1297
|
-
return
|
|
1599
|
+
return defaultToolsDir(env);
|
|
1298
1600
|
}
|
|
1299
1601
|
/**
|
|
1300
1602
|
* Read the `tools` section out of an UNVALIDATED config object (the value comes from YAML, so every
|
|
@@ -1624,21 +1926,43 @@ async function extractArchive(pack, archive, destination, artifact) {
|
|
|
1624
1926
|
* Atomically publish a fully-built directory as the artifact's version directory.
|
|
1625
1927
|
*
|
|
1626
1928
|
* `renameSync` inside one filesystem is atomic, so a reader (another process, or the next `ensure`)
|
|
1627
|
-
* sees either the old
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1929
|
+
* sees either the old tree or the complete new one — never a half-written tool. A pre-existing target
|
|
1930
|
+
* is REPLACED, and it is moved ASIDE rather than deleted first: `rm -rf` on a real tool tree runs for
|
|
1931
|
+
* hundreds of milliseconds, and for that whole window the version directory did not exist, so a
|
|
1932
|
+
* concurrently started pandoc got ENOENT and retried the install. Two renames shorten that window to
|
|
1933
|
+
* microseconds, and if the second one fails the old tree is moved back — a failed publish leaves the
|
|
1934
|
+
* previous install in place instead of nothing.
|
|
1935
|
+
*
|
|
1936
|
+
* What this does NOT claim: there is no cross-process LOCK. Two processes can install the same
|
|
1937
|
+
* version at once and both pay for the download and the extract; the second publish wins (each
|
|
1938
|
+
* staging tree is complete, so the result is a complete `bin/<binary>` either way). A lock would have
|
|
1939
|
+
* to detect its own stale holders and would have to tell the waiter when the winner is done, which is
|
|
1940
|
+
* a coordination protocol rather than a rename — out of scope here, and recorded instead of implied.
|
|
1941
|
+
*
|
|
1942
|
+
* A crash between the two renames leaves the old tree under `<target>.old-<pid>-<rand>` beside the
|
|
1943
|
+
* version directory. Nothing enumerates that parent, so the residue is inert; the next install of the
|
|
1944
|
+
* same version replaces the target normally.
|
|
1630
1945
|
*/
|
|
1631
1946
|
function publishAtomically(staging, target, artifact) {
|
|
1632
1947
|
mkdirSync(dirname(target), { recursive: true });
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
})
|
|
1948
|
+
const moved = existsSync(target) ? `${target}.old-${String(process.pid)}-${Math.random().toString(36).slice(2, 8)}` : null;
|
|
1949
|
+
if (moved !== null) try {
|
|
1950
|
+
renameSync(target, moved);
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
throw new ProvisionError("extract", artifact, `替换旧版本失败(${target} → ${moved}):${describeError$1(error)}`);
|
|
1953
|
+
}
|
|
1637
1954
|
try {
|
|
1638
1955
|
renameSync(staging, target);
|
|
1639
1956
|
} catch (error) {
|
|
1957
|
+
if (moved !== null) try {
|
|
1958
|
+
renameSync(moved, target);
|
|
1959
|
+
} catch {}
|
|
1640
1960
|
throw new ProvisionError("extract", artifact, `原子落盘失败(${staging} → ${target}):${describeError$1(error)}`);
|
|
1641
1961
|
}
|
|
1962
|
+
if (moved !== null) rmSync(moved, {
|
|
1963
|
+
recursive: true,
|
|
1964
|
+
force: true
|
|
1965
|
+
});
|
|
1642
1966
|
}
|
|
1643
1967
|
function makeScratch(toolsDir, id) {
|
|
1644
1968
|
const base = `${toolsDir}/.tmp`;
|
|
@@ -2763,16 +3087,18 @@ async function parseQueryPattern(query) {
|
|
|
2763
3087
|
//#endregion
|
|
2764
3088
|
//#region ../core/lib/modelBootstrap.js
|
|
2765
3089
|
/**
|
|
2766
|
-
* This module does NOT import the family base `@avantf/dsh-
|
|
3090
|
+
* This module does NOT import the family base `@avantf/dsh-plugin-base` — and nothing else in the
|
|
3091
|
+
* engine does either.
|
|
2767
3092
|
*
|
|
2768
|
-
* `@avantf/mem` is inlined into the plugin bundle, and the base is a
|
|
2769
|
-
*
|
|
2770
|
-
*
|
|
2771
|
-
*
|
|
2772
|
-
*
|
|
2773
|
-
*
|
|
2774
|
-
*
|
|
2775
|
-
*
|
|
3093
|
+
* `@avantf/mem` is inlined into the plugin bundle, and the base is a **peer of the plugin** (plus a
|
|
3094
|
+
* `devDependency` that is only the source of the declared range): the plugin's inlined zero-dependency
|
|
3095
|
+
* bootstrap resolves the installed base at startup and loads it by file URL (`plugin/src/envinit.ts`).
|
|
3096
|
+
* A static import here would therefore put a top-level `import "@avantf/dsh-plugin-base"` in the
|
|
3097
|
+
* artifact that may resolve nowhere — the artifact would throw `ERR_MODULE_NOT_FOUND` during module
|
|
3098
|
+
* evaluation, before any of our code ran, which is exactly the failure the family forbids ("the base
|
|
3099
|
+
* missing must degrade the plugin, never prevent it from loading"). The plugin computes the verdict
|
|
3100
|
+
* itself (off the runtime-loaded base) and hands it over as PLAIN DATA; this module only reads it
|
|
3101
|
+
* (see AGENTS.md, «The three family hard constraints»).
|
|
2776
3102
|
*/
|
|
2777
3103
|
const COMPAT_PREFIX = "compat:";
|
|
2778
3104
|
/** Hard cap on the host-readiness wait (see {@link WarmOptions.waitForCapMs}). */
|
|
@@ -2945,7 +3271,7 @@ function runtimeOf(artifactEnv) {
|
|
|
2945
3271
|
* It used to be a parallel `warmModelsAsync(...)` call beside the tools sweep, which meant two
|
|
2946
3272
|
* startup mechanisms with two logging vocabularies and no single place that answered "is this host
|
|
2947
3273
|
* fully provisioned?". The model has no pinned version and no managed directory of its own ON THIS
|
|
2948
|
-
*
|
|
3274
|
+
* (the flat `<repo>/<file>` layout the transformers.js cache reads), so it
|
|
2949
3275
|
* declares neither; its `isPresent` is `false` because "is the model warm?" is not a question that
|
|
2950
3276
|
* can be answered without attempting the warm-up, and the warm-up is cheap when the cache is
|
|
2951
3277
|
* already there (measured ~1.2 s, in the background). The DSH plugin uses this sweep only when the
|
|
@@ -3521,7 +3847,7 @@ var LocalBgeBackend = class {
|
|
|
3521
3847
|
this.pipeFactory = pipeFactory;
|
|
3522
3848
|
this.env = {
|
|
3523
3849
|
mirror: process.env["AVANTF_MEM_MODEL_MIRROR"] ?? process.env["HF_ENDPOINT"] ?? env?.mirror ?? "https://hf-mirror.com",
|
|
3524
|
-
cacheDir: process.env["AVANTF_MEM_MODEL_CACHE"] ?? env?.cacheDir
|
|
3850
|
+
cacheDir: process.env["AVANTF_MEM_MODEL_CACHE"] ?? blankToFamilyModels$1(env?.cacheDir),
|
|
3525
3851
|
autoDownload: envAutoDownload() ?? env?.autoDownload ?? true
|
|
3526
3852
|
};
|
|
3527
3853
|
this.gate = new WarmGate(() => this.env.autoDownload);
|
|
@@ -3696,6 +4022,10 @@ function applyModelEnv(mod, env) {
|
|
|
3696
4022
|
mod.env.allowRemoteModels = env.autoDownload;
|
|
3697
4023
|
mod.env.allowLocalModels = true;
|
|
3698
4024
|
}
|
|
4025
|
+
/** A blank `cache_dir` means "not configured": the family root's `models`, never the pre-framework path. */
|
|
4026
|
+
function blankToFamilyModels$1(configured) {
|
|
4027
|
+
return configured !== void 0 && configured.trim() !== "" ? configured : familyModelsDir();
|
|
4028
|
+
}
|
|
3699
4029
|
//#endregion
|
|
3700
4030
|
//#region ../retrieval-core/lib/adapters/local_reranker.js
|
|
3701
4031
|
/** Rerank pairs are classified in batches to bound peak memory. */
|
|
@@ -3735,7 +4065,7 @@ var LocalReranker = class {
|
|
|
3735
4065
|
this.pipeFactory = pipeFactory;
|
|
3736
4066
|
this.env = {
|
|
3737
4067
|
mirror: process.env["AVANTF_MEM_MODEL_MIRROR"] ?? process.env["HF_ENDPOINT"] ?? env?.mirror ?? "https://hf-mirror.com",
|
|
3738
|
-
cacheDir: process.env["AVANTF_MEM_MODEL_CACHE"] ?? env?.cacheDir
|
|
4068
|
+
cacheDir: process.env["AVANTF_MEM_MODEL_CACHE"] ?? blankToFamilyModels(env?.cacheDir),
|
|
3739
4069
|
autoDownload: envAutoDownload() ?? env?.autoDownload ?? true
|
|
3740
4070
|
};
|
|
3741
4071
|
this.gate = new WarmGate(() => this.env.autoDownload);
|
|
@@ -3831,6 +4161,10 @@ async function createClassifier(model, env) {
|
|
|
3831
4161
|
applyModelEnv(mod, env);
|
|
3832
4162
|
return await mod.pipeline("text-classification", model);
|
|
3833
4163
|
}
|
|
4164
|
+
/** A blank `cache_dir` means "not configured": the family root's `models`, never the pre-framework path. */
|
|
4165
|
+
function blankToFamilyModels(configured) {
|
|
4166
|
+
return configured !== void 0 && configured.trim() !== "" ? configured : familyModelsDir();
|
|
4167
|
+
}
|
|
3834
4168
|
//#endregion
|
|
3835
4169
|
//#region ../retrieval-core/lib/adapters/local_numpy.js
|
|
3836
4170
|
/**
|
|
@@ -4764,7 +5098,7 @@ const PLAIN_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
4764
5098
|
* Resolving the pandoc binary for the converter.
|
|
4765
5099
|
*
|
|
4766
5100
|
* The binary is NOT this package's business to install — `@avantf/mem-provision` owns that, the
|
|
4767
|
-
* pinned version, the mirror list, and the
|
|
5101
|
+
* pinned version, the mirror list, and the managed-tools layout. What this module adds is the
|
|
4768
5102
|
* converter's view of it: one place that knows how to ask for the executable, where an explicit
|
|
4769
5103
|
* override goes, and why a missing pandoc is a hard failure rather than a fallback.
|
|
4770
5104
|
*
|
|
@@ -4796,7 +5130,7 @@ function setPandocProvisioning(settings) {
|
|
|
4796
5130
|
/** The current settings, defaulting to the managed directory + the package's mirror defaults. */
|
|
4797
5131
|
function pandocProvisioning() {
|
|
4798
5132
|
return provisioning ?? {
|
|
4799
|
-
toolsDir:
|
|
5133
|
+
toolsDir: defaultToolsDir(),
|
|
4800
5134
|
mirror: [],
|
|
4801
5135
|
autoInstall: true
|
|
4802
5136
|
};
|
|
@@ -5712,7 +6046,7 @@ function buildFtsQuery(query, tokenizer = resolveFtsTokenizer()) {
|
|
|
5712
6046
|
} else if (clean.length >= 2) parts.push(`"${clean}"`);
|
|
5713
6047
|
}
|
|
5714
6048
|
if (!parts.length) return null;
|
|
5715
|
-
return parts.join(" OR ");
|
|
6049
|
+
return [...new Set(parts)].slice(0, 64).join(" OR ");
|
|
5716
6050
|
}
|
|
5717
6051
|
//#endregion
|
|
5718
6052
|
//#region ../core/lib/db/conn.js
|
|
@@ -6793,12 +7127,20 @@ var FactsDao = class FactsDao {
|
|
|
6793
7127
|
linkSupersede(factId, supersedesId) {
|
|
6794
7128
|
this.db.prepare("UPDATE facts SET supersedes_id = ? WHERE fact_id = ? AND supersedes_id IS NULL").run(supersedesId, factId);
|
|
6795
7129
|
}
|
|
6796
|
-
/**
|
|
7130
|
+
/**
|
|
7131
|
+
* Restore an archived fact, settling its trust and refreshing the idle clock (spec §2.6).
|
|
7132
|
+
*
|
|
7133
|
+
* `AND status = 'archived'` is load-bearing, like the guard on every sibling above: this UPDATE
|
|
7134
|
+
* clears the archive bookkeeping and refreshes `last_retrieved_at`, so ungated it would take an
|
|
7135
|
+
* ACTIVE row and (a) push its idle clock forward — silently extending the life of a fact the next
|
|
7136
|
+
* tick was about to re-archive — and (b) reset `trust_score` to the settled value it computed. It
|
|
7137
|
+
* returned `1` either way, so the caller could not tell that a row it never asked about had moved.
|
|
7138
|
+
*/
|
|
6797
7139
|
restore(factId, trust, clock) {
|
|
6798
7140
|
return this.db.prepare(`UPDATE facts
|
|
6799
7141
|
SET status = 'active', archived_at = NULL, archive_reason = NULL, archived_clock = NULL,
|
|
6800
7142
|
trust_score = ?, settle_clock = ?, last_retrieved_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
6801
|
-
WHERE fact_id =
|
|
7143
|
+
WHERE fact_id = ? AND status = 'archived'`).run(trust, clock, factId).changes;
|
|
6802
7144
|
}
|
|
6803
7145
|
bumpHelpful(factId, delta) {
|
|
6804
7146
|
this.db.prepare("UPDATE facts SET helpful_count = MAX(0, helpful_count + ?) WHERE fact_id = ?").run(delta, factId);
|
|
@@ -8416,7 +8758,7 @@ var MemoryStore = class {
|
|
|
8416
8758
|
if (rows.length === 0) return {
|
|
8417
8759
|
logged: [],
|
|
8418
8760
|
checked: 0,
|
|
8419
|
-
pending:
|
|
8761
|
+
pending: this.facts.countPendingConflicts()
|
|
8420
8762
|
};
|
|
8421
8763
|
this.conflictDrainCursor = rows[rows.length - 1].fact_id;
|
|
8422
8764
|
const checked = this.contradictions.checkMany(rows.map((r) => r.fact_id));
|
|
@@ -9397,6 +9739,20 @@ var ChunksDao = class {
|
|
|
9397
9739
|
this.db.prepare("UPDATE doc_chunks SET semantic_vector = ?, embedding_model = ? WHERE chunk_id = ?").run(bytes, embeddingModel, chunkId);
|
|
9398
9740
|
}
|
|
9399
9741
|
/**
|
|
9742
|
+
* The doc's chunks WITH their stored vectors — the reuse index for a re-ingest.
|
|
9743
|
+
*
|
|
9744
|
+
* A re-ingest deletes and re-inserts every chunk (new ids), so without this the only way to keep a
|
|
9745
|
+
* vector is to re-encode the same text; `vectorReusable` already knows how to tell "unchanged", but
|
|
9746
|
+
* only `reindex` could use it. Read BEFORE the replace, inside its transaction.
|
|
9747
|
+
*/
|
|
9748
|
+
vectorStateForDoc(docId) {
|
|
9749
|
+
return this.db.prepare(`SELECT dc.chunk_id AS chunk_id, dc.text AS text, dc.content_hash AS content_hash,
|
|
9750
|
+
dc.embedding_model AS embedding_model, dc.entities_version AS entities_version,
|
|
9751
|
+
(dc.semantic_vector IS NOT NULL) AS has_vector, dc.semantic_vector AS vec
|
|
9752
|
+
FROM doc_chunks dc
|
|
9753
|
+
WHERE dc.doc_id = ?`).all(docId);
|
|
9754
|
+
}
|
|
9755
|
+
/**
|
|
9400
9756
|
* Write a batch of vectors in ONE transaction.
|
|
9401
9757
|
*
|
|
9402
9758
|
* `setVector` is autocommit: each chunk costs a commit (measured 14.2 µs/row against 2.2 µs in a
|
|
@@ -9507,8 +9863,11 @@ var ChunksDao = class {
|
|
|
9507
9863
|
* Entity-overlap candidates, most-shared first and CAPPED (see the memory store's sibling).
|
|
9508
9864
|
*
|
|
9509
9865
|
* `limit` is applied per name-batch and the batches are unioned, so the result is bounded by
|
|
9510
|
-
* `limit x ceil(names/batch)` —
|
|
9511
|
-
*
|
|
9866
|
+
* `limit x ceil(names/batch)` — corpus-independent, which is what this query can promise. The
|
|
9867
|
+
* caller is expected to enforce the real `cap` on the union AFTER scoring it (it re-computes the
|
|
9868
|
+
* true Jaccard in JS): a union of per-batch tops is not a top-N, and a leg that returns more than
|
|
9869
|
+
* the cap makes `scores.size === legCap` — the "was this leg trimmed" signal — report the opposite
|
|
9870
|
+
* of the truth. (The memory store's sibling does not batch, so its single `LIMIT` is already exact.)
|
|
9512
9871
|
*/
|
|
9513
9872
|
candidatesByEntityNames(names, domain, source, limit) {
|
|
9514
9873
|
if (limit <= 0) return [];
|
|
@@ -9983,10 +10342,10 @@ async function documentText(bytes, label) {
|
|
|
9983
10342
|
/**
|
|
9984
10343
|
* Where these knobs live. The boundary belongs to the knowledge store (only it
|
|
9985
10344
|
* ingests), so it sits in the per-store config — naming it in the errors matters,
|
|
9986
|
-
* because the obvious place to look is the common `~/.avantf/
|
|
10345
|
+
* because the obvious place to look is the common `~/.avantf/configs/common.yaml`, where
|
|
9987
10346
|
* the `knowledge.` prefix is a key the loader will reject as unknown.
|
|
9988
10347
|
*/
|
|
9989
|
-
const KB_CONFIG_FILE = "~/.avantf/knowledge
|
|
10348
|
+
const KB_CONFIG_FILE = "~/.avantf/configs/knowledge.yaml";
|
|
9990
10349
|
/** Strip IPv6 brackets and the FQDN root dot — `localhost.` and `[::1]` are `localhost` and `::1`. */
|
|
9991
10350
|
function normalizeHost(hostname) {
|
|
9992
10351
|
return (hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname).toLowerCase().replace(/\.$/, "");
|
|
@@ -10230,6 +10589,11 @@ function describePaths(paths) {
|
|
|
10230
10589
|
files: paths.length
|
|
10231
10590
|
};
|
|
10232
10591
|
}
|
|
10592
|
+
/**
|
|
10593
|
+
* The listing shape is the contract's {@link BrowseListing} (re-exported above for this package's
|
|
10594
|
+
* callers). The engine fills it in and the host Remote method declares that same interface as its
|
|
10595
|
+
* payload, so the shape has exactly one definition and one check.
|
|
10596
|
+
*/
|
|
10233
10597
|
function insideRoots(path, roots, allowAnywhere) {
|
|
10234
10598
|
if (allowAnywhere) return true;
|
|
10235
10599
|
return roots.some((root) => path === root || path.startsWith(root.endsWith(sep) ? root : root + sep));
|
|
@@ -10685,7 +11049,7 @@ var GitRepo = class {
|
|
|
10685
11049
|
//#region ../core/lib/config/domains.js
|
|
10686
11050
|
/**
|
|
10687
11051
|
* Append one 知识域 to the STORE config's top-level `domains` allowlist
|
|
10688
|
-
* (`~/.avantf/knowledge
|
|
11052
|
+
* (`~/.avantf/configs/knowledge.yaml`, layer ③ of `loader.ts`) and return the new list.
|
|
10689
11053
|
*
|
|
10690
11054
|
* This is the write half of the UI-only `kbAddDomain` path, so the user adds a domain once in the
|
|
10691
11055
|
* 知识 tab instead of hand-editing YAML. It writes to the STORE config rather than the common one:
|
|
@@ -11150,6 +11514,7 @@ var KnowledgeStore = class {
|
|
|
11150
11514
|
const result = this.db.transaction(() => {
|
|
11151
11515
|
const docId = this.docs.upsert(domain, source, docTitle, sourceUri);
|
|
11152
11516
|
const oldChunkIds = this.chunks.idsForDoc(docId);
|
|
11517
|
+
const reusable = this.reusableByHash(docId, this.vectorSpace());
|
|
11153
11518
|
this.chunks.deleteForDoc(docId);
|
|
11154
11519
|
this.docs.touch(docId, sourceUri);
|
|
11155
11520
|
return {
|
|
@@ -11162,11 +11527,12 @@ var KnowledgeStore = class {
|
|
|
11162
11527
|
sourceRef: `${domain}:${source}:${docId}:${idx}`,
|
|
11163
11528
|
charStart: chunk.start,
|
|
11164
11529
|
charEnd: chunk.end
|
|
11165
|
-
})))
|
|
11530
|
+
}))),
|
|
11531
|
+
reusable
|
|
11166
11532
|
};
|
|
11167
11533
|
})();
|
|
11168
11534
|
this.evictVectors(result.oldChunkIds);
|
|
11169
|
-
const indexed = await this.indexChunks(result.newChunkIds);
|
|
11535
|
+
const indexed = await this.indexChunks(result.newChunkIds, result.reusable);
|
|
11170
11536
|
return {
|
|
11171
11537
|
doc_id: result.doc_id,
|
|
11172
11538
|
chunks: chunks.length,
|
|
@@ -11246,22 +11612,88 @@ var KnowledgeStore = class {
|
|
|
11246
11612
|
* Extract entities per chunk (persisted) and encode/persist chunk vectors.
|
|
11247
11613
|
*
|
|
11248
11614
|
* Returns how many chunks did NOT get a vector, so the caller can put it in the tool result. A
|
|
11249
|
-
* missing model counts every chunk: the document is still searchable by
|
|
11250
|
-
* semantically, and "入库成功 N 段" must not imply otherwise.
|
|
11615
|
+
* missing model counts every chunk that had no reusable vector: the document is still searchable by
|
|
11616
|
+
* text and entity, but not semantically, and "入库成功 N 段" must not imply otherwise.
|
|
11251
11617
|
*/
|
|
11252
|
-
async indexChunks(chunkIds) {
|
|
11618
|
+
async indexChunks(chunkIds, reusable) {
|
|
11253
11619
|
if (!chunkIds.length) return { vectors_failed: 0 };
|
|
11254
11620
|
const rows = this.chunks.texts(chunkIds);
|
|
11255
11621
|
await this.replaceChunkEntities(rows);
|
|
11256
11622
|
this.chunks.setEntitiesVersion(rows.map((r) => r.chunk_id), 1);
|
|
11623
|
+
const space = this.vectorSpace();
|
|
11624
|
+
const { remaining } = this.writeReusedVectors(rows, space, reusable);
|
|
11257
11625
|
if (!this.semantic.isAvailable()) {
|
|
11258
11626
|
this.semantic.ensureWarm?.();
|
|
11259
|
-
return { vectors_failed:
|
|
11627
|
+
return { vectors_failed: remaining.length };
|
|
11260
11628
|
}
|
|
11261
|
-
const { failed } = await this.encodeAndStore(
|
|
11629
|
+
const { failed } = await this.encodeAndStore(remaining, space);
|
|
11262
11630
|
return { vectors_failed: failed };
|
|
11263
11631
|
}
|
|
11264
11632
|
/**
|
|
11633
|
+
* The doc's stored vectors that still APPLY, keyed by content hash (see {@link vectorReusable}).
|
|
11634
|
+
*
|
|
11635
|
+
* Called inside the ingest transaction, before the replace: two chunks with the same text are one
|
|
11636
|
+
* hash, and the first row wins — the reuse is only a cache lookup, so a duplicate entry cannot be
|
|
11637
|
+
* distinguished from a hit and need not be.
|
|
11638
|
+
*/
|
|
11639
|
+
reusableByHash(docId, space) {
|
|
11640
|
+
const out = /* @__PURE__ */ new Map();
|
|
11641
|
+
for (const row of this.chunks.vectorStateForDoc(docId)) {
|
|
11642
|
+
if (row.vec === null || row.content_hash === null) continue;
|
|
11643
|
+
if (!this.vectorReusable(row, space)) continue;
|
|
11644
|
+
if (!out.has(row.content_hash)) out.set(row.content_hash, row.vec);
|
|
11645
|
+
}
|
|
11646
|
+
return out;
|
|
11647
|
+
}
|
|
11648
|
+
/**
|
|
11649
|
+
* Write back the vectors a re-ingest can reuse, and report which rows still need the model.
|
|
11650
|
+
*
|
|
11651
|
+
* A row is only reused when the lookup misses nothing: any row whose write fails, or whose text has
|
|
11652
|
+
* no reusable vector, comes back in `remaining` and is encoded as before — so the reuse path cannot
|
|
11653
|
+
* leave a chunk unindexed that the plain path would have indexed.
|
|
11654
|
+
*/
|
|
11655
|
+
writeReusedVectors(rows, space, reusable) {
|
|
11656
|
+
if (reusable === void 0 || reusable.size === 0) return {
|
|
11657
|
+
reused: 0,
|
|
11658
|
+
remaining: [...rows]
|
|
11659
|
+
};
|
|
11660
|
+
const remaining = [];
|
|
11661
|
+
const writes = [];
|
|
11662
|
+
for (const r of rows) {
|
|
11663
|
+
const bytes = reusable.get(contentHash(r.text));
|
|
11664
|
+
if (bytes === void 0) remaining.push(r);
|
|
11665
|
+
else writes.push({
|
|
11666
|
+
chunk_id: r.chunk_id,
|
|
11667
|
+
bytes,
|
|
11668
|
+
embeddingModel: space,
|
|
11669
|
+
text: r.text
|
|
11670
|
+
});
|
|
11671
|
+
}
|
|
11672
|
+
if (writes.length === 0) return {
|
|
11673
|
+
reused: 0,
|
|
11674
|
+
remaining
|
|
11675
|
+
};
|
|
11676
|
+
try {
|
|
11677
|
+
this.chunks.setVectors(writes);
|
|
11678
|
+
} catch {
|
|
11679
|
+
return {
|
|
11680
|
+
reused: 0,
|
|
11681
|
+
remaining: [...remaining, ...writes.map((w) => ({
|
|
11682
|
+
chunk_id: w.chunk_id,
|
|
11683
|
+
text: w.text
|
|
11684
|
+
}))]
|
|
11685
|
+
};
|
|
11686
|
+
}
|
|
11687
|
+
for (const w of writes) {
|
|
11688
|
+
const vec = bytesToFloat32(w.bytes);
|
|
11689
|
+
if (vec !== null && vec.length === this.vstore.dim) this.vstore.add(w.chunk_id, vec);
|
|
11690
|
+
}
|
|
11691
|
+
return {
|
|
11692
|
+
reused: writes.length,
|
|
11693
|
+
remaining
|
|
11694
|
+
};
|
|
11695
|
+
}
|
|
11696
|
+
/**
|
|
11265
11697
|
* Encode texts ONE AT A TIME, writing every `WRITE_BATCH` vectors in one transaction.
|
|
11266
11698
|
*
|
|
11267
11699
|
* The model call is deliberately NOT batched, and that is a MEASURED decision, not an omission:
|
|
@@ -11298,6 +11730,7 @@ var KnowledgeStore = class {
|
|
|
11298
11730
|
encoded += writes.length;
|
|
11299
11731
|
} catch {
|
|
11300
11732
|
failed += writes.length;
|
|
11733
|
+
this.evictVectors(writes.map((w) => w.chunk_id));
|
|
11301
11734
|
}
|
|
11302
11735
|
}
|
|
11303
11736
|
if (failed > 0) retrievalLogger().warn(`kb index: ${String(failed)} of ${String(rows.length)} chunk vector(s) were not written — run kb_reindex to retry (the text/entity/FTS indexes are complete; only the semantic vectors are affected)`);
|
|
@@ -11771,7 +12204,8 @@ var KnowledgeStore = class {
|
|
|
11771
12204
|
const jaccard = [...qEntities].filter((e) => factSet.has(e)).length / union.size;
|
|
11772
12205
|
if (jaccard > 0) out.set(id, jaccard);
|
|
11773
12206
|
}
|
|
11774
|
-
return out;
|
|
12207
|
+
if (out.size <= cap) return out;
|
|
12208
|
+
return new Map([...out].sort((a, b) => b[1] - a[1] || a[0] - b[0]).slice(0, cap));
|
|
11775
12209
|
}
|
|
11776
12210
|
};
|
|
11777
12211
|
const HEADING_RE = /^\s{0,3}(#{1,6})\s+(.+?)\s*$/;
|
|
@@ -11991,9 +12425,7 @@ function buildRuntime(opts) {
|
|
|
11991
12425
|
knowledge,
|
|
11992
12426
|
logger,
|
|
11993
12427
|
relevance(text) {
|
|
11994
|
-
|
|
11995
|
-
const inKnowledge = looksRelevant(knowledge.lexicalProbe(text, 2));
|
|
11996
|
-
return inMemory && inKnowledge ? "both" : inMemory ? "memory" : inKnowledge ? "knowledge" : "none";
|
|
12428
|
+
return looksRelevant(memory.lexicalProbe(text, 2)) || looksRelevant(knowledge.lexicalProbe(text, 2));
|
|
11997
12429
|
},
|
|
11998
12430
|
async remember(req) {
|
|
11999
12431
|
switch (req.action) {
|
|
@@ -12187,7 +12619,13 @@ const resultSymbol = (method) => fieldSymbol(method, "result");
|
|
|
12187
12619
|
*
|
|
12188
12620
|
* Carries BOTH members on purpose: the host's validator moved from `codec.schema.parse` (0.1.5) to
|
|
12189
12621
|
* `codec.create()` (0.1.6), and each generation checks only its own. One shape therefore serves
|
|
12190
|
-
* either host
|
|
12622
|
+
* either host.
|
|
12623
|
+
*
|
|
12624
|
+
* Kept LOCAL — a two-line mirror of the base kit's `strictCodec`, not a runtime call — on purpose:
|
|
12625
|
+
* the Remote face is built at module load and must exist even when the base cannot be loaded at all,
|
|
12626
|
+
* so its construction may not depend on a runtime `import()` of the base. This is the "mirrors a
|
|
12627
|
+
* convention, so changing it takes a plugin release" side of the rule in the root AGENTS.md; the
|
|
12628
|
+
* shared, fixable-by-a-base-release code lives in `@avantf/dsh-plugin-base`'s kit.
|
|
12191
12629
|
*/
|
|
12192
12630
|
function strict(schema, typeSymbol) {
|
|
12193
12631
|
return {
|
|
@@ -12207,11 +12645,11 @@ function parameter(method, name, schema, acceptsUndefined) {
|
|
|
12207
12645
|
...acceptsUndefined === void 0 ? {} : { acceptsUndefined }
|
|
12208
12646
|
};
|
|
12209
12647
|
}
|
|
12210
|
-
const text = z
|
|
12211
|
-
const count = z
|
|
12212
|
-
const optionalText = z
|
|
12213
|
-
const bool = z
|
|
12214
|
-
const textArray = z
|
|
12648
|
+
const text = z.string();
|
|
12649
|
+
const count = z.number().int().optional();
|
|
12650
|
+
const optionalText = z.string().optional();
|
|
12651
|
+
const bool = z.boolean().optional();
|
|
12652
|
+
const textArray = z.array(z.string()).optional();
|
|
12215
12653
|
/** One direct Remote method carrying a single `args` object parameter. */
|
|
12216
12654
|
function direct(method, args) {
|
|
12217
12655
|
return {
|
|
@@ -12221,11 +12659,11 @@ function direct(method, args) {
|
|
|
12221
12659
|
method,
|
|
12222
12660
|
invocation: { kind: "direct" },
|
|
12223
12661
|
parameters: [parameter(method, "args", args)],
|
|
12224
|
-
result: strict(z
|
|
12662
|
+
result: strict(z.any(), resultSymbol(method))
|
|
12225
12663
|
};
|
|
12226
12664
|
}
|
|
12227
12665
|
const descriptors = [
|
|
12228
|
-
direct("remember", z
|
|
12666
|
+
direct("remember", z.object({
|
|
12229
12667
|
action: optionalText.optional(),
|
|
12230
12668
|
content: optionalText.optional(),
|
|
12231
12669
|
fact_id: count.optional(),
|
|
@@ -12233,7 +12671,7 @@ const descriptors = [
|
|
|
12233
12671
|
ttl_days: count.optional(),
|
|
12234
12672
|
reason: optionalText.optional()
|
|
12235
12673
|
})),
|
|
12236
|
-
direct("recall", z
|
|
12674
|
+
direct("recall", z.object({
|
|
12237
12675
|
action: optionalText.optional(),
|
|
12238
12676
|
query: optionalText.optional(),
|
|
12239
12677
|
entity: optionalText.optional(),
|
|
@@ -12246,7 +12684,7 @@ const descriptors = [
|
|
|
12246
12684
|
limit: count.optional(),
|
|
12247
12685
|
max_tokens: count.optional()
|
|
12248
12686
|
})),
|
|
12249
|
-
direct("admin", z
|
|
12687
|
+
direct("admin", z.object({
|
|
12250
12688
|
action: optionalText.optional(),
|
|
12251
12689
|
fact_id: count.optional(),
|
|
12252
12690
|
category: optionalText.optional(),
|
|
@@ -12259,7 +12697,7 @@ const descriptors = [
|
|
|
12259
12697
|
resolution: optionalText.optional(),
|
|
12260
12698
|
loser_fact_id: count.optional()
|
|
12261
12699
|
})),
|
|
12262
|
-
direct("kb", z
|
|
12700
|
+
direct("kb", z.object({
|
|
12263
12701
|
action: optionalText.optional(),
|
|
12264
12702
|
text: optionalText.optional(),
|
|
12265
12703
|
source_uri: optionalText.optional(),
|
|
@@ -12274,7 +12712,7 @@ const descriptors = [
|
|
|
12274
12712
|
overwrite: bool.optional(),
|
|
12275
12713
|
adopt: bool.optional()
|
|
12276
12714
|
})),
|
|
12277
|
-
direct("query", z
|
|
12715
|
+
direct("query", z.object({
|
|
12278
12716
|
query: text,
|
|
12279
12717
|
kind: optionalText.optional(),
|
|
12280
12718
|
domain: optionalText.optional(),
|
|
@@ -12282,16 +12720,20 @@ const descriptors = [
|
|
|
12282
12720
|
limit: count.optional(),
|
|
12283
12721
|
max_tokens: count.optional()
|
|
12284
12722
|
})),
|
|
12285
|
-
direct("classifySource", z
|
|
12286
|
-
direct("browseDir", z
|
|
12287
|
-
direct("kbDomains", z
|
|
12288
|
-
direct("kbAddDomain", z
|
|
12289
|
-
direct("openDoc", z
|
|
12723
|
+
direct("classifySource", z.object({ text: optionalText.optional() })),
|
|
12724
|
+
direct("browseDir", z.object({ path: optionalText.optional() })),
|
|
12725
|
+
direct("kbDomains", z.object({})),
|
|
12726
|
+
direct("kbAddDomain", z.object({ domain: text })),
|
|
12727
|
+
direct("openDoc", z.object({
|
|
12290
12728
|
doc_id: count,
|
|
12291
12729
|
target: optionalText.optional()
|
|
12292
12730
|
}))
|
|
12293
12731
|
];
|
|
12294
|
-
/**
|
|
12732
|
+
/** The zod schema a strict codec carries, whichever generation's codec type is in play. */
|
|
12733
|
+
function codecSchema(codec) {
|
|
12734
|
+
return codec.schema;
|
|
12735
|
+
}
|
|
12736
|
+
/** Declare one registered wire schema: the factory 0.1.6+ calls, plus the schema 0.1.5 reads. */
|
|
12295
12737
|
function declared(name, schema) {
|
|
12296
12738
|
return {
|
|
12297
12739
|
name,
|
|
@@ -12302,17 +12744,18 @@ function declared(name, schema) {
|
|
|
12302
12744
|
/**
|
|
12303
12745
|
* Host face: registered by the host half through `ctx.typert.register()`.
|
|
12304
12746
|
*
|
|
12305
|
-
* The schema
|
|
12306
|
-
*
|
|
12307
|
-
*
|
|
12308
|
-
*
|
|
12309
|
-
* `
|
|
12310
|
-
*
|
|
12747
|
+
* The schema values are named `z.ZodType` — this package's own zod — not the registry's field type,
|
|
12748
|
+
* for two reasons. There are two PHYSICALLY different zod copies in play (the workspace catalog's and
|
|
12749
|
+
* the one inside the installed harness build), so no imported field type can be both correct and
|
|
12750
|
+
* stable; and the registry's field type is generation-specific (`TypertSchema['schema']` in 0.1.5,
|
|
12751
|
+
* `TypertSchemaFactory['create']`'s return in 0.1.6+, the name gone in 0.1.7), so naming it is what
|
|
12752
|
+
* broke this file's compilation on 0.1.7. The two copies are structurally identical, which is why the
|
|
12753
|
+
* declared entries stay assignable to whichever entry type the installed host declares.
|
|
12311
12754
|
*/
|
|
12312
12755
|
const hostContribution = {
|
|
12313
12756
|
package: PACKAGE$1,
|
|
12314
12757
|
face: "host",
|
|
12315
|
-
schemas: descriptors.flatMap((descriptor) => [...descriptor.parameters.map((parameter) => declared(`${descriptor.method}${parameter.name}`, parameter.codec
|
|
12758
|
+
schemas: descriptors.flatMap((descriptor) => [...descriptor.parameters.map((parameter) => declared(`${descriptor.method}${parameter.name}`, codecSchema(parameter.codec))), declared(`${descriptor.method}Result`, codecSchema(descriptor.result))]),
|
|
12316
12759
|
model: {
|
|
12317
12760
|
services: [{
|
|
12318
12761
|
key: "avantfMemory",
|
|
@@ -12341,40 +12784,35 @@ const hostContribution = {
|
|
|
12341
12784
|
invocations: descriptors
|
|
12342
12785
|
};
|
|
12343
12786
|
//#endregion
|
|
12344
|
-
//#region lib/types/
|
|
12787
|
+
//#region lib/types/data_home.js
|
|
12345
12788
|
/**
|
|
12346
|
-
*
|
|
12789
|
+
* The plugin's data root, resolved through the FAMILY's layers.
|
|
12347
12790
|
*
|
|
12348
|
-
* The
|
|
12349
|
-
*
|
|
12350
|
-
*
|
|
12351
|
-
*
|
|
12352
|
-
*
|
|
12791
|
+
* The profile's `config.dataHome` is CONFIGURATION, so it takes the CONFIGURED layer: ⑤ an explicit
|
|
12792
|
+
* caller value → ④ `$AVANTF_HOME` → ② this configured value → `~/.avantf`. Handing it straight to the
|
|
12793
|
+
* engine's explicit slot instead — which is what this plugin used to do — promotes ② above ④, and the
|
|
12794
|
+
* same profile then resolves to two different directories depending on which half of the family reads
|
|
12795
|
+
* it: the work plugin resolves the same knob through its configured layer, so with `$AVANTF_HOME` set
|
|
12796
|
+
* work read `<env>/prompts` while this plugin read `<config>/prompts`. That directory is SHARED (both
|
|
12797
|
+
* plugins' editable prompt files live in it), so "the file you edited is read by one plugin and not
|
|
12798
|
+
* the other" is a real failure, not a nuance. `AGENTS.md` 「边界与路径」 states the rule this
|
|
12799
|
+
* implements: the configured value travels in the config layer, never in the explicit one.
|
|
12353
12800
|
*
|
|
12354
|
-
*
|
|
12355
|
-
*
|
|
12356
|
-
*
|
|
12357
|
-
*
|
|
12358
|
-
* atomic publish) is one shared mechanism, and the plugin only declares what it needs.
|
|
12801
|
+
* The engine's own copy of the rule does the work (it is the one the CLI and MCP resolve with, and
|
|
12802
|
+
* `test/family_pin.spec.ts` pins it against the base kit's). This module exists so the plugin's USE of
|
|
12803
|
+
* it is testable without the DSH harness: the entry module imports `@deepseek-ai/*` at load, which
|
|
12804
|
+
* resolves only inside a harness workspace.
|
|
12359
12805
|
*
|
|
12360
|
-
*
|
|
12361
|
-
* would throw `ERR_MODULE_NOT_FOUND` while the artifact was being evaluated (the base is not a
|
|
12362
|
-
* runtime dependency of the published plugin any more), and a bare dynamic `import()` would bypass
|
|
12363
|
-
* the framework's managed copy. The plugin loads the module the framework resolved, by file URL.
|
|
12364
|
-
*
|
|
12365
|
-
* The plugin computes the gate's verdict here and hands it to the engine as plain data
|
|
12366
|
-
* (`{ load, status, reason }`), so the inlined engine bundle never imports the base either.
|
|
12367
|
-
*
|
|
12368
|
-
* @module @avantf/dsh-mem/provision
|
|
12806
|
+
* @module data_home
|
|
12369
12807
|
*/
|
|
12370
|
-
/**
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
*/
|
|
12377
|
-
const
|
|
12808
|
+
/** Layer ② for the configured value; `$AVANTF_HOME` (④) and any explicit value (⑤) still outrank it. */
|
|
12809
|
+
function configDataHome(configured) {
|
|
12810
|
+
return resolveDataHome({ common: { dataHome: configured ?? "" } });
|
|
12811
|
+
}
|
|
12812
|
+
//#endregion
|
|
12813
|
+
//#region lib/types/provision.js
|
|
12814
|
+
/** The base package id, named here so the gate's messages need no import of it. */
|
|
12815
|
+
const COMPAT_PACKAGE = "@avantf/dsh-plugin-base";
|
|
12378
12816
|
/**
|
|
12379
12817
|
* The services this plugin calls, with the methods it calls on them.
|
|
12380
12818
|
*
|
|
@@ -12438,27 +12876,6 @@ function warn(log, message) {
|
|
|
12438
12876
|
} catch {}
|
|
12439
12877
|
}
|
|
12440
12878
|
/**
|
|
12441
|
-
* The range of `@avantf/dsh-compat` this build is written against.
|
|
12442
|
-
*
|
|
12443
|
-
* Read from THIS plugin's manifest, which is the single source of truth — the framework does not
|
|
12444
|
-
* pick it, and a hardcoded constant would silently keep provisioning an old range after the family
|
|
12445
|
-
* publishes a new one. The constant is only a fallback for a manifest that lost the entry.
|
|
12446
|
-
*
|
|
12447
|
-
* It lives in `devDependencies`, deliberately. Nothing imports the base: the framework fetches it
|
|
12448
|
-
* into its own managed home and this plugin loads it from there, so a RUNTIME dependency would
|
|
12449
|
-
* install a second copy on every consumer that no code path ever loads. `devDependencies` still
|
|
12450
|
-
* ships in the packed manifest (pnpm keeps it, rewriting `catalog:` like any other field), so the
|
|
12451
|
-
* range travels with the artifact; it just is not installed.
|
|
12452
|
-
*/
|
|
12453
|
-
async function compatRange(manifestUrl = new URL("../package.json", import.meta.url)) {
|
|
12454
|
-
try {
|
|
12455
|
-
const raw = JSON.parse(await readFile(manifestUrl, "utf8"));
|
|
12456
|
-
const declared = raw.devDependencies?.["@avantf/dsh-compat"] ?? raw.dependencies?.["@avantf/dsh-compat"];
|
|
12457
|
-
if (typeof declared === "string" && declared.trim() !== "") return declared;
|
|
12458
|
-
} catch {}
|
|
12459
|
-
return COMPAT_RANGE;
|
|
12460
|
-
}
|
|
12461
|
-
/**
|
|
12462
12879
|
* Build the plugin-specific half of the gate from a loaded base.
|
|
12463
12880
|
*
|
|
12464
12881
|
* Called ONCE per process (the loaded runtime is cached by `src/envinit.ts`), which is what makes the
|
|
@@ -12556,7 +12973,8 @@ function registerCompatMegaphone(ctx, verdict, log, compat) {
|
|
|
12556
12973
|
heading: "记忆插件未加载:与当前 dsh 的兼容性检查未通过,插件已拒绝加载(不注册服务、工具、prompt 段与真实 typert face)。",
|
|
12557
12974
|
warningsLabel: "风险提示:",
|
|
12558
12975
|
warnings: verdict.warnings,
|
|
12559
|
-
fix: "修复:在本仓库重建(`pnpm build:dsh`),或把 `@deepseek-ai/dsh` 换回本构建声明的版本。"
|
|
12976
|
+
fix: "修复:在本仓库重建(`pnpm build:dsh`),或把 `@deepseek-ai/dsh` 换回本构建声明的版本。",
|
|
12977
|
+
logPointer: (prefix) => `完整诊断见宿主日志里 ${prefix} 开头的行。`
|
|
12560
12978
|
})
|
|
12561
12979
|
});
|
|
12562
12980
|
}
|
|
@@ -12577,7 +12995,7 @@ function verifyRegisteredFaces(input) {
|
|
|
12577
12995
|
toolNames: input.toolNames
|
|
12578
12996
|
});
|
|
12579
12997
|
}
|
|
12580
|
-
const PACKAGE = "@avantf/dsh-
|
|
12998
|
+
const PACKAGE = "@avantf/dsh-plugin-base";
|
|
12581
12999
|
function parseVersion(input) {
|
|
12582
13000
|
const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(input.trim());
|
|
12583
13001
|
if (match === null) return void 0;
|
|
@@ -12803,7 +13221,7 @@ async function ensureFramework(options) {
|
|
|
12803
13221
|
warn(`supportedRange must be a string; got ${typeof options.supportedRange}`);
|
|
12804
13222
|
return;
|
|
12805
13223
|
}
|
|
12806
|
-
const supported = options.supportedRange ?? "
|
|
13224
|
+
const supported = options.supportedRange ?? ">=0.3.0 <1.0.0";
|
|
12807
13225
|
let dir;
|
|
12808
13226
|
try {
|
|
12809
13227
|
dir = dirname(createRequire(import.meta.url).resolve(`${PACKAGE}/package.json`));
|
|
@@ -12889,32 +13307,112 @@ function rangeOf(value) {
|
|
|
12889
13307
|
return typeof range === "string" && range.trim() !== "" ? range : void 0;
|
|
12890
13308
|
}
|
|
12891
13309
|
//#endregion
|
|
13310
|
+
//#region lib/types/interface_gate.js
|
|
13311
|
+
/** Read one member without letting a hostile module's getter escape. */
|
|
13312
|
+
function member(module, name) {
|
|
13313
|
+
try {
|
|
13314
|
+
return module?.[name];
|
|
13315
|
+
} catch {
|
|
13316
|
+
return;
|
|
13317
|
+
}
|
|
13318
|
+
}
|
|
13319
|
+
function cannotTell(reason) {
|
|
13320
|
+
return {
|
|
13321
|
+
status: "cannot-tell",
|
|
13322
|
+
reason
|
|
13323
|
+
};
|
|
13324
|
+
}
|
|
13325
|
+
/**
|
|
13326
|
+
* Whether the base's shared capabilities may be used under `verdict`.
|
|
13327
|
+
*
|
|
13328
|
+
* The ONE mapping from the gate's verdict to the loader's decision, exported so it is asserted
|
|
13329
|
+
* directly and the loader and its test cannot drift: only `incompatible` withholds the base;
|
|
13330
|
+
* `cannot-tell` does not, because it is not a negative answer. A `false` here is still a DEGRADATION
|
|
13331
|
+
* (own prompt defaults, gate skipped, legacy provisioning), never a refused mount.
|
|
13332
|
+
*/
|
|
13333
|
+
function baseIsUsable(verdict) {
|
|
13334
|
+
return verdict.status !== "incompatible";
|
|
13335
|
+
}
|
|
13336
|
+
/**
|
|
13337
|
+
* Where the build's bake record sits when no explicit URL is given: beside the built ENTRY
|
|
13338
|
+
* (`lib/index.js`), which is where `scripts/link-envinit.mjs` writes it. A per-file `tsc` emit of
|
|
13339
|
+
* this module lives one directory below that entry and looks one level up; the first readable record
|
|
13340
|
+
* wins. Under `vitest` (running from `src/`) neither exists, which is the correct "not baked".
|
|
13341
|
+
*/
|
|
13342
|
+
const BAKED_CANDIDATES = ["./interface-version.json", "../interface-version.json"];
|
|
13343
|
+
/**
|
|
13344
|
+
* Read this artifact's baked requirement through the loaded base's own reader.
|
|
13345
|
+
* @param read - the base's `readInterfaceRequirement`, taken off the loaded module.
|
|
13346
|
+
* @param bakedUrl - test seam: read EXACTLY this record instead of walking {@link BAKED_CANDIDATES}.
|
|
13347
|
+
*/
|
|
13348
|
+
function requirementOf(read, bakedUrl) {
|
|
13349
|
+
const candidates = bakedUrl === void 0 ? BAKED_CANDIDATES.map((relative) => new URL(relative, import.meta.url)) : [bakedUrl];
|
|
13350
|
+
for (const url of candidates) {
|
|
13351
|
+
let requirement;
|
|
13352
|
+
try {
|
|
13353
|
+
requirement = read(url);
|
|
13354
|
+
} catch {
|
|
13355
|
+
continue;
|
|
13356
|
+
}
|
|
13357
|
+
const version = requirement?.interfaceVersion;
|
|
13358
|
+
if (typeof version === "number" && Number.isInteger(version) && version > 0) return { interfaceVersion: version };
|
|
13359
|
+
}
|
|
13360
|
+
}
|
|
13361
|
+
/**
|
|
13362
|
+
* Ask the loaded base whether it implements the interface generation this artifact was built for.
|
|
13363
|
+
*
|
|
13364
|
+
* @param module - the loaded base module (only `checkInterface` / `readInterfaceRequirement` are
|
|
13365
|
+
* touched, each through a guarded read).
|
|
13366
|
+
* @param bakedUrl - test seam: the bake record to read; production leaves it undefined.
|
|
13367
|
+
* @returns a verdict that is never `incompatible` unless BOTH sides were readable and differ, and
|
|
13368
|
+
* never a throw.
|
|
13369
|
+
*/
|
|
13370
|
+
function interfaceVerdict(module, bakedUrl) {
|
|
13371
|
+
const check = member(module, "checkInterface");
|
|
13372
|
+
const read = member(module, "readInterfaceRequirement");
|
|
13373
|
+
if (typeof check !== "function" || typeof read !== "function") return cannotTell("the loaded base has no interface gate (checkInterface / readInterfaceRequirement missing) — it predates the runtime interface contract, so which generation it implements cannot be told");
|
|
13374
|
+
const requirement = requirementOf(read, bakedUrl);
|
|
13375
|
+
if (requirement === void 0) return cannotTell("this build has no baked interface requirement (lib/interface-version.json is missing or malformed), so the runtime interface gate cannot run");
|
|
13376
|
+
let verdict;
|
|
13377
|
+
try {
|
|
13378
|
+
verdict = check(requirement.interfaceVersion, module);
|
|
13379
|
+
} catch {
|
|
13380
|
+
return cannotTell("the loaded base threw while judging the interface generation");
|
|
13381
|
+
}
|
|
13382
|
+
const status = verdict?.status;
|
|
13383
|
+
if (status !== "ok" && status !== "incompatible" && status !== "cannot-tell") return cannotTell("the loaded base returned no usable interface verdict");
|
|
13384
|
+
return verdict;
|
|
13385
|
+
}
|
|
13386
|
+
//#endregion
|
|
12892
13387
|
//#region lib/types/envinit.js
|
|
12893
13388
|
/**
|
|
12894
|
-
* Environment initialisation for the memory plugin, through the family
|
|
12895
|
-
* `@avantf/dsh-
|
|
13389
|
+
* Environment initialisation for the memory plugin, through the family base
|
|
13390
|
+
* `@avantf/dsh-plugin-base`.
|
|
12896
13391
|
*
|
|
12897
|
-
* The shape is fixed by the
|
|
13392
|
+
* The shape is fixed by the base's DESIGN §7.2 and is the same for every plugin:
|
|
12898
13393
|
*
|
|
12899
|
-
* bootstrap (inlined) → load the
|
|
12900
|
-
*
|
|
13394
|
+
* bootstrap (inlined) → load the base → register + declare the remaining resources → dispatch the
|
|
13395
|
+
* `background` set → the plugin's own pre-mount check.
|
|
12901
13396
|
*
|
|
12902
|
-
* What this plugin needs, in
|
|
13397
|
+
* What this plugin needs, in two phases:
|
|
12903
13398
|
*
|
|
12904
|
-
* 1.
|
|
12905
|
-
* `@avantf/dsh-
|
|
12906
|
-
*
|
|
12907
|
-
*
|
|
12908
|
-
* `
|
|
12909
|
-
*
|
|
13399
|
+
* 1. **The gate, straight from the base** — the family compatibility gate is part of
|
|
13400
|
+
* `@avantf/dsh-plugin-base` and arrived WITH the module the bootstrap loaded. There is no
|
|
13401
|
+
* `mem:compat` item, no npm download and no managed `<home>/compat/**` copy any more; the base's
|
|
13402
|
+
* `compat` module provides the rules, probes, report and post-registration check, and
|
|
13403
|
+
* `runtimeFromCompat(framework)` derives this plugin's spec from it. The gate has to be runnable
|
|
13404
|
+
* BEFORE anything is registered, because a proven incompatible host must refuse the mount with
|
|
13405
|
+
* nothing half-registered. This replaces the bespoke self-heal the plugin used to run itself
|
|
13406
|
+
* (`@avantf/mem-provision`'s `ensureCompatBase`), whose private root (`<dataHome>/dsh-compat/**`)
|
|
13407
|
+
* the base deliberately does not read.
|
|
12910
13408
|
* 2. **`mem:pandoc`** (`binary-archive`, `startup: 'background'`) — the documented "expensive but
|
|
12911
13409
|
* not startup-blocking" resource. It is declared AFTER the runtime exists, because its spec
|
|
12912
13410
|
* comes from the resolved config (`tools.auto_install`, `tools.mirror`), and it is dispatched
|
|
12913
13411
|
* without waiting: the caller continues its own initialisation and hooks the outcome through
|
|
12914
13412
|
* `onSettled`.
|
|
12915
13413
|
* 3. **`mem:model`** (`model-cache`, `startup: 'background'`) — the embedding model. It uses the
|
|
12916
|
-
*
|
|
12917
|
-
* `@huggingface/transformers` reads under its `cacheDir` — so the runtime loads the
|
|
13414
|
+
* base's `flat` layout, whose files land at `<root>/<repo>/<file>` — exactly the shape
|
|
13415
|
+
* `@huggingface/transformers` reads under its `cacheDir` — so the runtime loads the base's
|
|
12918
13416
|
* copy instead of fetching a second one. For the default repo the file list is the narrow set
|
|
12919
13417
|
* the runtime actually opens (see {@link MODEL_FILES}); any other repo omits the list, because
|
|
12920
13418
|
* a fixed six-file list made the item fail for a repo whose file set differs. The endpoint is
|
|
@@ -12925,24 +13423,23 @@ function rangeOf(value) {
|
|
|
12925
13423
|
*
|
|
12926
13424
|
* Two hard rules:
|
|
12927
13425
|
*
|
|
12928
|
-
* - **The
|
|
13426
|
+
* - **The base is never statically imported.** Only `import type` and the inlined bootstrap
|
|
12929
13427
|
* are static; the value comes from `loadFramework()`. A top-level value import would throw before
|
|
12930
13428
|
* the bootstrap ran — exactly the broken-tree case the bootstrap exists for.
|
|
12931
|
-
* - **Nothing here refuses the mount.** "Cannot tell" is not "incompatible": a missing
|
|
13429
|
+
* - **Nothing here refuses the mount.** "Cannot tell" is not "incompatible": a missing base,
|
|
12932
13430
|
* an unavailable gate or a failed resource is a WARNING, and the plugin mounts with its tools,
|
|
12933
13431
|
* service, prompt and Remote face intact.
|
|
12934
13432
|
*
|
|
12935
13433
|
* @module @avantf/dsh-mem/envinit
|
|
12936
13434
|
*/
|
|
12937
|
-
/** The
|
|
12938
|
-
const FRAMEWORK_PACKAGE = "@avantf/dsh-
|
|
13435
|
+
/** The family base package, as declared in this plugin's `peerDependencies`. */
|
|
13436
|
+
const FRAMEWORK_PACKAGE = "@avantf/dsh-plugin-base";
|
|
12939
13437
|
/** The plugin namespace every item is declared under (`<plugin>:<name>`, framework DESIGN §13.4). */
|
|
12940
13438
|
const PLUGIN = "mem";
|
|
12941
13439
|
/**
|
|
12942
13440
|
* The item ids. `resolve()` / `onSettled` are keyed by them, so the DSH plugin compares against these
|
|
12943
13441
|
* names rather than re-spelling literals.
|
|
12944
13442
|
*/
|
|
12945
|
-
const COMPAT_ITEM = "mem:compat";
|
|
12946
13443
|
const PANDOC_ITEM = "mem:pandoc";
|
|
12947
13444
|
const MODEL_ITEM = "mem:model";
|
|
12948
13445
|
/**
|
|
@@ -13040,70 +13537,6 @@ function provisionLogger(log) {
|
|
|
13040
13537
|
};
|
|
13041
13538
|
}
|
|
13042
13539
|
/**
|
|
13043
|
-
* The file URL to `import()` for a managed npm copy.
|
|
13044
|
-
*
|
|
13045
|
-
* The framework hands back the package DIRECTORY (`<version dir>/node_modules/<name>`), and a
|
|
13046
|
-
* directory is not importable, so the entry is read from the copy's own manifest the way Node would:
|
|
13047
|
-
* `exports["."]` first, then `main`.
|
|
13048
|
-
*/
|
|
13049
|
-
async function entryUrlOf(packageDir) {
|
|
13050
|
-
let manifest;
|
|
13051
|
-
try {
|
|
13052
|
-
manifest = JSON.parse(await readFile(join(packageDir, "package.json"), "utf8"));
|
|
13053
|
-
} catch {
|
|
13054
|
-
return;
|
|
13055
|
-
}
|
|
13056
|
-
const dot = manifest.exports?.["."];
|
|
13057
|
-
const fromExports = typeof dot === "string" ? dot : dot?.default;
|
|
13058
|
-
return pathToFileURL(join(packageDir, typeof fromExports === "string" ? fromExports : typeof manifest.main === "string" ? manifest.main : "./index.js")).href;
|
|
13059
|
-
}
|
|
13060
|
-
/**
|
|
13061
|
-
* The resolved directory of a peer in THIS plugin's tree.
|
|
13062
|
-
*
|
|
13063
|
-
* The framework must reuse the host's own copy for identity-sensitive peers, and it cannot find them
|
|
13064
|
-
* by itself: its resolver looks at `process.cwd()` and at its own module URL — neither is the
|
|
13065
|
-
* plugin's tree. So the caller hands the directory over explicitly (`NpmPackageSpec.peers[].dir`);
|
|
13066
|
-
* without it the item fails with `peer/unsatisfied`.
|
|
13067
|
-
*/
|
|
13068
|
-
function peerDirOf(name) {
|
|
13069
|
-
const require = createRequire(import.meta.url);
|
|
13070
|
-
try {
|
|
13071
|
-
return dirname(require.resolve(`${name}/package.json`));
|
|
13072
|
-
} catch {
|
|
13073
|
-
try {
|
|
13074
|
-
let dir = dirname(require.resolve(name));
|
|
13075
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
13076
|
-
if (dir.endsWith(`/${name}`)) return dir;
|
|
13077
|
-
const parent = dirname(dir);
|
|
13078
|
-
if (parent === dir) break;
|
|
13079
|
-
dir = parent;
|
|
13080
|
-
}
|
|
13081
|
-
} catch {}
|
|
13082
|
-
}
|
|
13083
|
-
}
|
|
13084
|
-
/** The `mem:compat` item: the family compatibility gate, waited for before anything registers. */
|
|
13085
|
-
function compatItem(framework, range, zodDir) {
|
|
13086
|
-
return {
|
|
13087
|
-
id: COMPAT_ITEM,
|
|
13088
|
-
kind: framework.NPM_PACKAGE_KIND,
|
|
13089
|
-
spec: {
|
|
13090
|
-
name: COMPAT_PACKAGE,
|
|
13091
|
-
range,
|
|
13092
|
-
peers: [{
|
|
13093
|
-
name: "zod",
|
|
13094
|
-
...zodDir === void 0 ? {} : { dir: zodDir }
|
|
13095
|
-
}]
|
|
13096
|
-
},
|
|
13097
|
-
target: { root: "runtime" },
|
|
13098
|
-
onMissing: {
|
|
13099
|
-
atStartup: "degrade",
|
|
13100
|
-
atUse: "degrade"
|
|
13101
|
-
},
|
|
13102
|
-
startup: "blocking",
|
|
13103
|
-
schemaVersion: framework.ITEM_SCHEMA_VERSION
|
|
13104
|
-
};
|
|
13105
|
-
}
|
|
13106
|
-
/**
|
|
13107
13540
|
* The `mem:pandoc` item: the pinned pandoc release as a `binary-archive`.
|
|
13108
13541
|
*
|
|
13109
13542
|
* The packs are `@avantf/mem-provision`'s (`PANDOC_PACKS`), the single source of the URLs and the
|
|
@@ -13229,41 +13662,25 @@ async function loadOnce(options) {
|
|
|
13229
13662
|
warn(options.log, `${OWN_PREFIX} WARNING — ${FRAMEWORK_PACKAGE} could not be made available; the plugin falls back to its own provisioning and mounts anyway`);
|
|
13230
13663
|
return;
|
|
13231
13664
|
}
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
|
|
13235
|
-
|
|
13236
|
-
|
|
13237
|
-
autoDownload,
|
|
13238
|
-
deadlineMs
|
|
13239
|
-
}
|
|
13240
|
-
});
|
|
13241
|
-
provisioner.register(framework.npmPackageProvider());
|
|
13242
|
-
provisioner.declare({
|
|
13243
|
-
plugin: PLUGIN,
|
|
13244
|
-
items: [compatItem(framework, await compatRange(), peerDirOf("zod"))]
|
|
13245
|
-
});
|
|
13246
|
-
await provisioner.ensure({
|
|
13247
|
-
only: [COMPAT_ITEM],
|
|
13248
|
-
deadlineMs,
|
|
13249
|
-
onSettled: (entry) => {
|
|
13250
|
-
if (entry.action === "failed") warn(options.log, `${OWN_PREFIX} WARNING — ${COMPAT_ITEM} failed (${String(entry.code)}): ${entry.reason ?? ""}`);
|
|
13665
|
+
{
|
|
13666
|
+
const verdict = interfaceVerdict(framework);
|
|
13667
|
+
if (!baseIsUsable(verdict)) {
|
|
13668
|
+
warn(options.log, `${OWN_PREFIX} WARNING — interface: ${verdict.reason ?? "the loaded base implements another interface generation"}; the base's shared capabilities are NOT used (own prompt defaults, gate skipped, legacy provisioning) and the plugin mounts anyway`);
|
|
13669
|
+
return;
|
|
13251
13670
|
}
|
|
13252
|
-
|
|
13253
|
-
|
|
13671
|
+
if (verdict.status === "cannot-tell") warn(options.log, `${OWN_PREFIX} WARNING — interface: ${verdict.reason ?? "the interface generation cannot be told"}; using the loaded base anyway ("cannot tell" is never "incompatible")`);
|
|
13672
|
+
}
|
|
13673
|
+
const disposers = [];
|
|
13254
13674
|
let compat;
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
else try {
|
|
13258
|
-
const module = options.compatModule ?? await importCompat(state.handle.dir);
|
|
13259
|
-
if (module === void 0) warn(options.log, `${OWN_PREFIX} WARNING — ${COMPAT_PACKAGE} has no readable entry at ${state.handle.dir}; the compatibility gate is SKIPPED`);
|
|
13260
|
-
else compat = runtimeFromCompat(module);
|
|
13675
|
+
try {
|
|
13676
|
+
compat = runtimeFromCompat(options.compatModule ?? framework);
|
|
13261
13677
|
} catch (error) {
|
|
13262
|
-
warn(options.log, `${OWN_PREFIX} WARNING — ${COMPAT_PACKAGE} could not be
|
|
13678
|
+
warn(options.log, `${OWN_PREFIX} WARNING — the ${COMPAT_PACKAGE} compatibility gate could not be initialised (${reasonOf(error)}); the compatibility gate is SKIPPED and the plugin will mount anyway`);
|
|
13263
13679
|
}
|
|
13264
13680
|
let disposed = false;
|
|
13265
13681
|
return {
|
|
13266
13682
|
home,
|
|
13683
|
+
kit: framework,
|
|
13267
13684
|
compat,
|
|
13268
13685
|
roots: {
|
|
13269
13686
|
tools: join(home, "tools"),
|
|
@@ -13316,19 +13733,6 @@ async function loadOnce(options) {
|
|
|
13316
13733
|
}
|
|
13317
13734
|
};
|
|
13318
13735
|
}
|
|
13319
|
-
/** One line describing why a non-ready item is not ready, safe to put in a WARNING. */
|
|
13320
|
-
function describeState(state) {
|
|
13321
|
-
if (state.state === "ready") return `ready (${state.handle.source})`;
|
|
13322
|
-
if (state.state === "failed" || state.state === "skipped") return `${state.state}: ${state.code}${state.detail === void 0 ? "" : ` (${state.detail})`}`;
|
|
13323
|
-
if (state.state === "pending") return "pending (still being fetched)";
|
|
13324
|
-
return state.state;
|
|
13325
|
-
}
|
|
13326
|
-
/** Load the managed copy's entry by file URL; `undefined` when it has no readable entry. */
|
|
13327
|
-
async function importCompat(packageDir) {
|
|
13328
|
-
const url = await entryUrlOf(packageDir);
|
|
13329
|
-
if (url === void 0) return void 0;
|
|
13330
|
-
return await import(url);
|
|
13331
|
-
}
|
|
13332
13736
|
//#endregion
|
|
13333
13737
|
//#region lib/types/open.js
|
|
13334
13738
|
/**
|
|
@@ -13497,7 +13901,7 @@ function zodDef(schema) {
|
|
|
13497
13901
|
function jsonSchemaOf(schema) {
|
|
13498
13902
|
if (typeof schema?.safeParse !== "function") return void 0;
|
|
13499
13903
|
try {
|
|
13500
|
-
return z
|
|
13904
|
+
return z.toJSONSchema(schema, {
|
|
13501
13905
|
io: "input",
|
|
13502
13906
|
unrepresentable: "any"
|
|
13503
13907
|
});
|
|
@@ -13663,9 +14067,9 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
|
|
|
13663
14067
|
done = true;
|
|
13664
14068
|
};
|
|
13665
14069
|
const name = "avantf-mem";
|
|
13666
|
-
const Config = z.object({
|
|
13667
|
-
mode: z.union([z.const("cordis"), z.const("mcp")]).default("cordis"),
|
|
13668
|
-
dataHome: z.string()
|
|
14070
|
+
const Config = z$1.object({
|
|
14071
|
+
mode: z$1.union([z$1.const("cordis"), z$1.const("mcp")]).default("cordis"),
|
|
14072
|
+
dataHome: z$1.string()
|
|
13669
14073
|
});
|
|
13670
14074
|
/**
|
|
13671
14075
|
* Facts whose entity/triple rows the heartbeat may rebuild per beat.
|
|
@@ -13864,6 +14268,16 @@ let AvantfMemGateway = (() => {
|
|
|
13864
14268
|
super(ctx, "avantfMem");
|
|
13865
14269
|
this.rt = rt;
|
|
13866
14270
|
}
|
|
14271
|
+
/**
|
|
14272
|
+
* Run one already-validated tool call and wrap the answer in the contract envelope.
|
|
14273
|
+
*
|
|
14274
|
+
* `T` is the payload the tool key is DECLARED to answer, spelled at each `@Remote` below; this used
|
|
14275
|
+
* to be `unknown` everywhere, which made the host half the one seam in the type chain where nothing
|
|
14276
|
+
* said what crosses the wire. The single `as T` is unavoidable and is the honest part: the dispatch
|
|
14277
|
+
* table is keyed by `ToolSpec.key` (a `string`), so its result is `unknown` by construction. Naming
|
|
14278
|
+
* the type is what turns "the client assumed a shape" into "the engine's own type is what the host
|
|
14279
|
+
* claims to send".
|
|
14280
|
+
*/
|
|
13867
14281
|
async call(key, spec, payload) {
|
|
13868
14282
|
const parsed = spec.input.safeParse(payload);
|
|
13869
14283
|
if (!parsed.success) return validationError(spec.name, parsed.error.issues);
|
|
@@ -13879,6 +14293,11 @@ let AvantfMemGateway = (() => {
|
|
|
13879
14293
|
};
|
|
13880
14294
|
}
|
|
13881
14295
|
}
|
|
14296
|
+
/**
|
|
14297
|
+
* Each payload is written as the ENGINE type that produces it (`StoreResult<…>`, or a contract
|
|
14298
|
+
* interface), never as a hand-copied shape: if a store method's result changes, the declaration
|
|
14299
|
+
* here stops compiling instead of quietly misdescribing the wire.
|
|
14300
|
+
*/
|
|
13882
14301
|
remember(args) {
|
|
13883
14302
|
const payload = defined(args);
|
|
13884
14303
|
return this.call("remember", REMEMBER_TOOL, {
|
|
@@ -13900,6 +14319,12 @@ let AvantfMemGateway = (() => {
|
|
|
13900
14319
|
...payload
|
|
13901
14320
|
});
|
|
13902
14321
|
}
|
|
14322
|
+
/**
|
|
14323
|
+
* The one method that keeps `unknown`, and deliberately: its payload is whichever of the seven
|
|
14324
|
+
* corpus actions the caller asked for (a page, one document, an ingest report, a conflict report, a
|
|
14325
|
+
* reindex plan …), so there is no single engine type to name. The engine's `KbDispatch` spells all
|
|
14326
|
+
* seven out; the client names its expectation at each call site (`callRemote<…>(…)`).
|
|
14327
|
+
*/
|
|
13903
14328
|
kb(args) {
|
|
13904
14329
|
return this.call("kb", KB_TOOL, defined(args));
|
|
13905
14330
|
}
|
|
@@ -14105,8 +14530,8 @@ function startManagedResources(env, rt, logger, waitFor) {
|
|
|
14105
14530
|
if (entry.action === "failed" || entry.action === "skipped") {
|
|
14106
14531
|
logger.warn(`envinit: ${entry.id} is ${entry.action} (${String(entry.code)}${entry.reason === void 0 ? "" : ` — ${entry.reason}`})`);
|
|
14107
14532
|
if (entry.id === "mem:pandoc") {
|
|
14108
|
-
const fallback =
|
|
14109
|
-
logger.warn(`envinit:
|
|
14533
|
+
const fallback = resolveToolsDir(parseToolsConfig(rt.config.common.tools));
|
|
14534
|
+
logger.warn(`envinit: pandoc unavailable from the framework; the converter will use ${fallback} (override with tools.dir or AVANTF_PANDOC)`);
|
|
14110
14535
|
setPandocProvisioning({
|
|
14111
14536
|
toolsDir: fallback,
|
|
14112
14537
|
mirror: rt.config.common.tools.mirror,
|
|
@@ -14120,21 +14545,6 @@ function startManagedResources(env, rt, logger, waitFor) {
|
|
|
14120
14545
|
logger.info(`envinit: ${entry.id} ${entry.action} (${entry.source}${entry.version === void 0 ? "" : ` ${entry.version}`})`);
|
|
14121
14546
|
});
|
|
14122
14547
|
}
|
|
14123
|
-
/**
|
|
14124
|
-
* The tools directory to use when the framework did NOT deliver pandoc.
|
|
14125
|
-
*
|
|
14126
|
-
* Resolved through the provisioning layer's own `resolveToolsDir`, so the environment hatch
|
|
14127
|
-
* (`AVANTF_TOOLS_DIR`) and the `~` expansion behave here exactly as they do everywhere else — the
|
|
14128
|
-
* converter hands `settings.toolsDir` to `ensurePandoc` VERBATIM (`pandocBinary`), so whatever this
|
|
14129
|
-
* returns is the final word for the process. `managedRoots` replaced the built-in default, so
|
|
14130
|
-
* `config.common.tools.dir` equals the family root even when the operator never touched it; only a
|
|
14131
|
-
* resolved value that is STILL the family root means "nobody chose one", and only then may the
|
|
14132
|
-
* legacy default apply.
|
|
14133
|
-
*/
|
|
14134
|
-
function legacyToolsDir(rt, env) {
|
|
14135
|
-
const resolved = resolveToolsDir(parseToolsConfig(rt.config.common.tools));
|
|
14136
|
-
return resolved === env.roots.tools ? defaultToolsDir() : resolved;
|
|
14137
|
-
}
|
|
14138
14548
|
async function apply(ctx, config) {
|
|
14139
14549
|
const logger = hostLogger(ctx);
|
|
14140
14550
|
const env = await loadEnvinit({ log: logger });
|
|
@@ -14144,7 +14554,7 @@ async function apply(ctx, config) {
|
|
|
14144
14554
|
}
|
|
14145
14555
|
const compat = env?.compat;
|
|
14146
14556
|
let verdict;
|
|
14147
|
-
if (compat === void 0) logger.warn("plugin mount: the dsh compatibility gate is ABSENT (@avantf/dsh-
|
|
14557
|
+
if (compat === void 0) logger.warn("plugin mount: the dsh compatibility gate is ABSENT (@avantf/dsh-plugin-base unavailable — see the WARNING above); mounting the full plugin anyway");
|
|
14148
14558
|
else {
|
|
14149
14559
|
const run = provision(ctx, logger, compat);
|
|
14150
14560
|
if (!run.verdict.load) {
|
|
@@ -14156,11 +14566,12 @@ async function apply(ctx, config) {
|
|
|
14156
14566
|
}
|
|
14157
14567
|
const toolSpecs = TOOL_SPECS;
|
|
14158
14568
|
if (config.mode === "mcp") logger.warn("config mode=mcp is not served in-process — mounting the cordis toolset; run @avantf/mem-mcp standalone for MCP");
|
|
14159
|
-
|
|
14569
|
+
const dataHome = configDataHome(config.dataHome);
|
|
14570
|
+
logger.info(`plugin mount: mode=${config.mode ?? "cordis"} dataHome=${dataHome} (configured ${config.dataHome ?? "unset"})`);
|
|
14160
14571
|
let rt;
|
|
14161
14572
|
try {
|
|
14162
14573
|
rt = buildRuntime({
|
|
14163
|
-
dataHome
|
|
14574
|
+
dataHome,
|
|
14164
14575
|
logger,
|
|
14165
14576
|
...env === void 0 ? {} : { managedRoots: {
|
|
14166
14577
|
tools: env.roots.tools,
|
|
@@ -14217,44 +14628,46 @@ async function apply(ctx, config) {
|
|
|
14217
14628
|
logger.info(`trust heartbeat: every ${heartbeatMinutes}min (active-day clock + lifecycle sweep)`);
|
|
14218
14629
|
return () => clearInterval(timer);
|
|
14219
14630
|
});
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
|
|
14631
|
+
const promptDir = join(rt.config.home, "prompts");
|
|
14632
|
+
const kit = env?.kit;
|
|
14633
|
+
const loadedPrompts = kit?.PromptFiles === void 0 ? promptFileSpecs().map((spec) => ({
|
|
14634
|
+
file: spec.file,
|
|
14635
|
+
path: join(promptDir, spec.file),
|
|
14636
|
+
text: spec.fallback,
|
|
14637
|
+
source: "default",
|
|
14638
|
+
wrote: false
|
|
14639
|
+
})) : new kit.PromptFiles({
|
|
14640
|
+
dir: promptDir,
|
|
14641
|
+
logger
|
|
14642
|
+
}).load(promptFileSpecs());
|
|
14643
|
+
const promptSections = buildPromptSections(loadedPrompts);
|
|
14644
|
+
for (const section of promptSections) {
|
|
14225
14645
|
ctx.systemPrompt.section(section);
|
|
14226
14646
|
logger.info(`prompt section registered: ${section.name} (order ${String(section.order)})`);
|
|
14227
14647
|
}
|
|
14648
|
+
for (const warning of promptTextWarnings(promptSections)) logger.warn(`prompt text: ${warning}`);
|
|
14649
|
+
logger.info(`prompt files: ${promptDir} (${loadedPrompts.map((entry) => `${entry.file}:${entry.source}`).join(", ")})`);
|
|
14228
14650
|
const hints = /* @__PURE__ */ new WeakMap();
|
|
14229
14651
|
ctx.on("agent/inbox/inserted", (payload) => {
|
|
14230
14652
|
const agent = payload.agent;
|
|
14231
14653
|
if (agent === void 0) return;
|
|
14654
|
+
if (payload.message?.source?.kind !== "user") return;
|
|
14232
14655
|
const text = messageText(payload.message);
|
|
14233
14656
|
try {
|
|
14234
|
-
hints.set(agent, text
|
|
14657
|
+
hints.set(agent, text !== "" && rt.relevance(text));
|
|
14235
14658
|
} catch {
|
|
14236
|
-
hints.set(agent,
|
|
14659
|
+
hints.set(agent, false);
|
|
14237
14660
|
}
|
|
14238
14661
|
});
|
|
14239
|
-
|
|
14240
|
-
"avantf:
|
|
14241
|
-
130,
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
ctx.systemPrompt.context({
|
|
14249
|
-
name,
|
|
14250
|
-
order,
|
|
14251
|
-
text: (context) => {
|
|
14252
|
-
const hit = context.scope === void 0 ? void 0 : hints.get(context.scope);
|
|
14253
|
-
return hit === void 0 ? "" : hintLines(hit)[store];
|
|
14254
|
-
}
|
|
14255
|
-
});
|
|
14256
|
-
logger.info(`prompt context registered: ${name} (order ${String(order)})`);
|
|
14257
|
-
}
|
|
14662
|
+
ctx.systemPrompt.context({
|
|
14663
|
+
name: "avantf:mem-hint",
|
|
14664
|
+
order: 130,
|
|
14665
|
+
text: (context) => {
|
|
14666
|
+
const hit = context.scope === void 0 ? void 0 : hints.get(context.scope);
|
|
14667
|
+
return hit === void 0 ? "" : hintText(hit);
|
|
14668
|
+
}
|
|
14669
|
+
});
|
|
14670
|
+
logger.info("prompt context registered: avantf:mem-hint (order 130)");
|
|
14258
14671
|
let reconciling = false;
|
|
14259
14672
|
let lastDriftAt = 0;
|
|
14260
14673
|
let trailingDrift = null;
|