@kenz1117/dsh-engram 0.7.4 → 0.7.5
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/README.en.md +7 -3
- package/README.md +8 -4
- package/lib/client.js +1 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +295 -22
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -3708,31 +3708,156 @@ function truncateItem(text, maxChars = RECALL_PER_ITEM_CHARS) {
|
|
|
3708
3708
|
return text.length <= maxChars ? text : `${text.slice(0, maxChars)}…`;
|
|
3709
3709
|
}
|
|
3710
3710
|
/**
|
|
3711
|
-
*
|
|
3712
|
-
*
|
|
3713
|
-
* @param
|
|
3714
|
-
* @param totalBudget -
|
|
3715
|
-
* @returns
|
|
3711
|
+
* 预算内的贪心装填(保序):按 cost 逐个判断,装不下的计入 dropped 并继续试后续更短的项。
|
|
3712
|
+
* @param items - 候选(已按相关性排序)。
|
|
3713
|
+
* @param cost - 单项占用预算(字符数等)。
|
|
3714
|
+
* @param totalBudget - 总预算。
|
|
3715
|
+
* @returns 保留项与丢弃数。
|
|
3716
3716
|
*/
|
|
3717
|
-
function
|
|
3717
|
+
function fitWithinBudget(items, cost, totalBudget) {
|
|
3718
3718
|
const kept = [];
|
|
3719
3719
|
let used = 0;
|
|
3720
3720
|
let dropped = 0;
|
|
3721
|
-
for (const
|
|
3722
|
-
|
|
3721
|
+
for (const item of items) {
|
|
3722
|
+
const size = cost(item);
|
|
3723
|
+
if (used + size > totalBudget) {
|
|
3723
3724
|
dropped += 1;
|
|
3724
3725
|
continue;
|
|
3725
3726
|
}
|
|
3726
|
-
kept.push(
|
|
3727
|
-
used +=
|
|
3727
|
+
kept.push(item);
|
|
3728
|
+
used += size;
|
|
3728
3729
|
}
|
|
3730
|
+
return {
|
|
3731
|
+
kept,
|
|
3732
|
+
dropped
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* 总量预算内贪心装填行(保序;某行装不下时继续尝试更短的后续行),
|
|
3737
|
+
* 被跳过的行计数并在末尾追加提示行。
|
|
3738
|
+
* @param lines - 候选行(已按相关性排序)。
|
|
3739
|
+
* @param totalBudget - 总字符预算,默认 RECALL_TOTAL_CHARS。
|
|
3740
|
+
* @returns 装填后的行;有丢弃时末尾含「另有 N 条…」提示。
|
|
3741
|
+
*/
|
|
3742
|
+
function enforceBudget(lines, totalBudget = RECALL_TOTAL_CHARS) {
|
|
3743
|
+
const { kept, dropped } = fitWithinBudget(lines, (line) => line.length, totalBudget);
|
|
3729
3744
|
if (dropped > 0) kept.push(`(另有 ${dropped} 条未展示:缩小查询范围或降低 limit 后重试)`);
|
|
3730
3745
|
return kept;
|
|
3731
3746
|
}
|
|
3747
|
+
/** 判定后可选的下一步策略。 */
|
|
3748
|
+
const NEXT_STRATEGIES = [
|
|
3749
|
+
"answer",
|
|
3750
|
+
"search_keyword",
|
|
3751
|
+
"search_room",
|
|
3752
|
+
"search_timeline",
|
|
3753
|
+
"ask_user",
|
|
3754
|
+
"stop"
|
|
3755
|
+
];
|
|
3756
|
+
/**
|
|
3757
|
+
* 判断字符串是否为合法策略。
|
|
3758
|
+
* @param value - 待判定字符串。
|
|
3759
|
+
*/
|
|
3760
|
+
function isNextStrategy(value) {
|
|
3761
|
+
return NEXT_STRATEGIES.includes(value);
|
|
3762
|
+
}
|
|
3763
|
+
/**
|
|
3764
|
+
* 构造批次内唯一的证据 ref:已排桩时用「scope/房间#桩位」(与检索行里的宫殿坐标一致),
|
|
3765
|
+
* 未排桩时用「scope/#id 前 8 位」。带 scope 前缀,跨作用域的同名房间不会撞号。
|
|
3766
|
+
* @param scope - 条目所在作用域。
|
|
3767
|
+
* @param id - 条目 id。
|
|
3768
|
+
* @param slot - 宫殿坐标;未排桩传 undefined。
|
|
3769
|
+
*/
|
|
3770
|
+
function evidenceRefOf(scope, id, slot) {
|
|
3771
|
+
return slot === void 0 ? `${scope}/#${id.slice(0, 8)}` : `${scope}/${slot.room}#${slot.index}`;
|
|
3772
|
+
}
|
|
3773
|
+
/** 批次注册表:按会话隔离,超出上限丢最旧的批次。 */
|
|
3774
|
+
var EvidenceBatches = class {
|
|
3775
|
+
bySession = /* @__PURE__ */ new Map();
|
|
3776
|
+
counter = 0;
|
|
3777
|
+
/**
|
|
3778
|
+
* 注册一个批次。
|
|
3779
|
+
* @param sessionId - 会话标识。
|
|
3780
|
+
* @param refs - 本次输出中可引用的证据 ref(重复项只留一次)。
|
|
3781
|
+
* @param now - 当前时间(测试注入)。
|
|
3782
|
+
* @returns 新批次。
|
|
3783
|
+
*/
|
|
3784
|
+
register(sessionId, refs, now = Date.now()) {
|
|
3785
|
+
this.counter += 1;
|
|
3786
|
+
const batch = {
|
|
3787
|
+
batchId: `batch-${String(this.counter)}`,
|
|
3788
|
+
refs: new Set(refs),
|
|
3789
|
+
createdAt: now
|
|
3790
|
+
};
|
|
3791
|
+
const batches = this.bySession.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
3792
|
+
batches.set(batch.batchId, batch);
|
|
3793
|
+
while (batches.size > 20) {
|
|
3794
|
+
const oldest = batches.keys().next().value;
|
|
3795
|
+
if (oldest === void 0) break;
|
|
3796
|
+
batches.delete(oldest);
|
|
3797
|
+
}
|
|
3798
|
+
this.bySession.set(sessionId, batches);
|
|
3799
|
+
return batch;
|
|
3800
|
+
}
|
|
3801
|
+
/**
|
|
3802
|
+
* 取会话内指定批次。
|
|
3803
|
+
* @param sessionId - 会话标识。
|
|
3804
|
+
* @param batchId - 批次 id。
|
|
3805
|
+
* @returns 批次;不存在或已被上限淘汰时返回 undefined。
|
|
3806
|
+
*/
|
|
3807
|
+
get(sessionId, batchId) {
|
|
3808
|
+
return this.bySession.get(sessionId)?.get(batchId);
|
|
3809
|
+
}
|
|
3810
|
+
/**
|
|
3811
|
+
* 清理会话的全部批次(会话销毁时调用,避免长驻进程累积)。
|
|
3812
|
+
* @param sessionId - 会话标识。
|
|
3813
|
+
*/
|
|
3814
|
+
clear(sessionId) {
|
|
3815
|
+
this.bySession.delete(sessionId);
|
|
3816
|
+
}
|
|
3817
|
+
};
|
|
3818
|
+
/** 进程内批次注册表:插件为单实例,会话销毁时调用 clear 释放该会话的批次。 */
|
|
3819
|
+
const evidenceBatches = new EvidenceBatches();
|
|
3820
|
+
/**
|
|
3821
|
+
* 校验判定:ref 必须属于批次,sufficient 由代码强制。
|
|
3822
|
+
* 强制规则:sufficient 需同时满足「模型声称充足」「至少一条本批次有效证据」「策略为 answer」;
|
|
3823
|
+
* 三者缺一即判为不充足,并把策略改写为继续检索(模型声称充足时)或尊重其非作答策略。
|
|
3824
|
+
* @param batch - 被引用的批次。
|
|
3825
|
+
* @param request - 模型提交的判定。
|
|
3826
|
+
* @returns 校验后的判定结果(含被拒绝与超限的 ref 明细)。
|
|
3827
|
+
*/
|
|
3828
|
+
function assessEvidence(batch, request) {
|
|
3829
|
+
const submitted = request.evidenceRefs.filter((ref) => typeof ref === "string").map((ref) => ref.trim()).filter((ref) => ref !== "");
|
|
3830
|
+
const unique = [...new Set(submitted)];
|
|
3831
|
+
const limited = unique.slice(0, 8);
|
|
3832
|
+
const droppedRefs = unique.slice(8);
|
|
3833
|
+
const accepted = [];
|
|
3834
|
+
const rejectedRefs = [];
|
|
3835
|
+
for (const ref of limited) if (batch.refs.has(ref)) accepted.push(ref);
|
|
3836
|
+
else rejectedRefs.push(ref);
|
|
3837
|
+
const rawMissing = typeof request.missing === "string" ? request.missing.trim() : "";
|
|
3838
|
+
const missingTruncated = rawMissing.length > 160;
|
|
3839
|
+
const missing = missingTruncated ? `${rawMissing.slice(0, 160)}…` : rawMissing;
|
|
3840
|
+
const requestedStrategy = typeof request.nextStrategy === "string" ? request.nextStrategy.trim() : "";
|
|
3841
|
+
const strategy = isNextStrategy(requestedStrategy) ? requestedStrategy : "search_keyword";
|
|
3842
|
+
const claimed = request.sufficient === true;
|
|
3843
|
+
const sufficient = claimed && accepted.length > 0 && strategy === "answer";
|
|
3844
|
+
const nextStrategy = sufficient || strategy !== "answer" ? strategy : "search_keyword";
|
|
3845
|
+
return {
|
|
3846
|
+
sufficient,
|
|
3847
|
+
evidenceRefs: accepted,
|
|
3848
|
+
rejectedRefs,
|
|
3849
|
+
droppedRefs,
|
|
3850
|
+
missing,
|
|
3851
|
+
missingTruncated,
|
|
3852
|
+
nextStrategy,
|
|
3853
|
+
requestedStrategy,
|
|
3854
|
+
forced: claimed !== sufficient || strategy !== requestedStrategy || nextStrategy !== strategy
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3732
3857
|
//#endregion
|
|
3733
3858
|
//#region src/tools/create.ts
|
|
3734
3859
|
/**
|
|
3735
|
-
*
|
|
3860
|
+
* 17 个 engram_ 工具的定义与执行器。工具 schema 保持窄参数;
|
|
3736
3861
|
* scope 决定读写哪个分库;嵌入缺失时检索结果显式标记降级。
|
|
3737
3862
|
* @module @kenz1117/dsh-engram/tools/create
|
|
3738
3863
|
*/
|
|
@@ -3770,6 +3895,26 @@ function renderHistoryRun(result) {
|
|
|
3770
3895
|
lines.push("同一批可重复执行:已完成的轮次按幂等键跳过,只补未完成的部分。");
|
|
3771
3896
|
return lines.join("\n");
|
|
3772
3897
|
}
|
|
3898
|
+
/** 证据门策略的可读提示。 */
|
|
3899
|
+
const STRATEGY_HINT = {
|
|
3900
|
+
answer: "可以据此作答",
|
|
3901
|
+
search_keyword: "换关键词再检索(engram_search)",
|
|
3902
|
+
search_room: "收窄到具体房间再检索(engram_search 带 room)",
|
|
3903
|
+
search_timeline: "按时间线找(engram_timeline)",
|
|
3904
|
+
ask_user: "向用户澄清缺失的信息",
|
|
3905
|
+
stop: "不回答,并向用户说明缺少什么"
|
|
3906
|
+
};
|
|
3907
|
+
/** 证据门判定的模型可读文本(含被拒绝的 ref 与强制改写说明)。 */
|
|
3908
|
+
function renderAssessText(verdict, batchId) {
|
|
3909
|
+
const lines = [verdict.sufficient ? `证据判定:充足(批次 ${batchId},${String(verdict.evidenceRefs.length)} 条有效证据)——可以据此作答。` : `证据判定:不足(批次 ${batchId})。`];
|
|
3910
|
+
if (verdict.evidenceRefs.length > 0) lines.push(`有效证据:${verdict.evidenceRefs.join("、")}`);
|
|
3911
|
+
if (verdict.rejectedRefs.length > 0) lines.push(`无效 ref(不属于该批次,已忽略):${verdict.rejectedRefs.join("、")}`);
|
|
3912
|
+
if (verdict.droppedRefs.length > 0) lines.push(`超出上限被丢弃(单次最多 ${String(8)} 条):${verdict.droppedRefs.join("、")}`);
|
|
3913
|
+
lines.push(`缺口:${verdict.missing === "" ? "(未填写)" : verdict.missing}${verdict.missingTruncated ? "(已截断)" : ""}`);
|
|
3914
|
+
lines.push(`下一步:${verdict.nextStrategy} —— ${STRATEGY_HINT[verdict.nextStrategy]}`);
|
|
3915
|
+
if (verdict.forced) lines.push("注意:判定由代码修正——sufficient 需同时满足「你声称充足」「至少一条属于本批次的有效证据」「nextStrategy=answer」。");
|
|
3916
|
+
return lines.join("\n");
|
|
3917
|
+
}
|
|
3773
3918
|
/** 从模型参数收敛 scope(非法值或缺失回退 fallback)。 */
|
|
3774
3919
|
function scopeOf(raw, fallback) {
|
|
3775
3920
|
if (raw === "user" || raw === "project" || raw === "shared") return raw;
|
|
@@ -3839,7 +3984,7 @@ async function rewriteQueries(deps, exec, query) {
|
|
|
3839
3984
|
}
|
|
3840
3985
|
}
|
|
3841
3986
|
/**
|
|
3842
|
-
* 构造
|
|
3987
|
+
* 构造 17 个工具定义(engram_save/search/assess/timeline/update/forget/report/review/review_queue/
|
|
3843
3988
|
* stats/export/distill/examine/neighbors/audit_forgotten/tour/ingest_history)。
|
|
3844
3989
|
* @param deps - 分库打开器、嵌入器、辅助 LLM、导出目录。
|
|
3845
3990
|
* @returns 可直接 register 的工具定义数组。
|
|
@@ -4219,16 +4364,106 @@ function createEngramTools(deps) {
|
|
|
4219
4364
|
};
|
|
4220
4365
|
}));
|
|
4221
4366
|
const degraded = retrievals.some((retrieval) => retrieval.degraded);
|
|
4222
|
-
const
|
|
4367
|
+
const { kept, dropped } = fitWithinBudget(mergeQueryResults(retrievals, limit, 60, Math.floor(Math.max(0, limit) / Math.max(1, rewrite.queries.length))).map((hit, index) => {
|
|
4368
|
+
const ref = evidenceRefOf(hit.record.scope, hit.record.id, hit.record.slot);
|
|
4223
4369
|
const edge = hit.viaEdge === void 0 ? "" : `(经 ${hit.viaEdge.type} 关联自 ${hit.viaEdge.from})`;
|
|
4224
4370
|
const slot = hit.record.slot === void 0 ? "" : ` ${hit.record.slot.room}#${hit.record.slot.index}`;
|
|
4225
4371
|
const date = ` 刻于 ${new Date(hit.record.createdAt).toISOString().slice(0, 10)}`;
|
|
4226
4372
|
const cues = hit.cues === void 0 ? "" : ` 相邻桩位: ${hit.cues.neighbors.join(", ")}`;
|
|
4227
|
-
return
|
|
4228
|
-
|
|
4373
|
+
return {
|
|
4374
|
+
ref,
|
|
4375
|
+
line: `${index + 1}. [${hit.record.scope}/${hit.record.kind}]${slot}${date} ${truncateItem(hit.record.content)}(id=${hit.record.id}, ref=${ref})${edge}${cues}`
|
|
4376
|
+
};
|
|
4377
|
+
}), (entry) => entry.line.length, RECALL_TOTAL_CHARS);
|
|
4378
|
+
const lines = kept.map((entry) => entry.line);
|
|
4379
|
+
if (dropped > 0) lines.push(`(另有 ${dropped} 条未展示:缩小查询范围或降低 limit 后重试)`);
|
|
4380
|
+
const sessionId = exec.agent === void 0 ? void 0 : String(exec.agent.session.id);
|
|
4381
|
+
const batch = sessionId === void 0 || kept.length === 0 ? void 0 : evidenceBatches.register(sessionId, kept.map((entry) => entry.ref));
|
|
4382
|
+
const prefix = degraded && lines.length > 0 ? "(语义嵌入不可用,仅关键词检索)\n" : "";
|
|
4383
|
+
const roomNote = rooms === void 0 ? "" : `(房间路由:${rooms.join("、")})\n`;
|
|
4384
|
+
const batchNote = batch === void 0 ? "" : `\n批次 ${batch.batchId}(${String(batch.refs.size)} 条可引用证据):作答前用 engram_assess 判定证据是否充分,evidenceRefs 只能引用上面的 ref。`;
|
|
4229
4385
|
return {
|
|
4230
4386
|
degraded,
|
|
4231
|
-
text: renderMemoryPacket(`${
|
|
4387
|
+
text: renderMemoryPacket(`${prefix}${roomNote}${lines.join("\n") || "无命中"}${batchNote}`, "tool_search", input.query)
|
|
4388
|
+
};
|
|
4389
|
+
}
|
|
4390
|
+
});
|
|
4391
|
+
const assess = defineTool({
|
|
4392
|
+
name: "engram_assess",
|
|
4393
|
+
description: "证据门:检索(engram_search)之后、作答之前,判定「检索到的内容是否足以回答当前问题」。提交 batchId、sufficient、最多 8 条 evidenceRefs(只能引用该批次输出里的 ref=…)、缺口说明 missing(≤160 字符)与下一步 nextStrategy。代码强制校验:sufficient 需同时满足「你声称充足」「至少一条属于本批次的有效证据」「nextStrategy=answer」,否则判为不足并把策略改回继续检索;不属于该批次的 ref 会被拒绝并列出。判词与拒绝明细写入审计日志。",
|
|
4394
|
+
parameters: {
|
|
4395
|
+
batchId: {
|
|
4396
|
+
type: "string",
|
|
4397
|
+
required: true,
|
|
4398
|
+
description: "engram_search 输出末尾给出的批次 id(如 batch-3)"
|
|
4399
|
+
},
|
|
4400
|
+
sufficient: {
|
|
4401
|
+
type: "boolean",
|
|
4402
|
+
required: true,
|
|
4403
|
+
description: "证据是否足以回答当前问题(true 时 nextStrategy 必须为 answer)"
|
|
4404
|
+
},
|
|
4405
|
+
evidenceRefs: {
|
|
4406
|
+
type: "array",
|
|
4407
|
+
items: { type: "string" },
|
|
4408
|
+
description: "引用的证据 ref(最多 8 条,只能取该批次输出里的 ref=…)"
|
|
4409
|
+
},
|
|
4410
|
+
missing: {
|
|
4411
|
+
type: "string",
|
|
4412
|
+
description: "缺少什么维度的信息(≤160 字符)"
|
|
4413
|
+
},
|
|
4414
|
+
nextStrategy: {
|
|
4415
|
+
type: "string",
|
|
4416
|
+
enum: [...NEXT_STRATEGIES],
|
|
4417
|
+
description: "下一步:answer 作答 / search_keyword 换关键词 / search_room 换房间 / search_timeline 查时间线 / ask_user 问用户 / stop 不回答"
|
|
4418
|
+
}
|
|
4419
|
+
},
|
|
4420
|
+
output: {
|
|
4421
|
+
schema: {
|
|
4422
|
+
type: "object",
|
|
4423
|
+
additionalProperties: false,
|
|
4424
|
+
properties: {
|
|
4425
|
+
sufficient: {
|
|
4426
|
+
type: "boolean",
|
|
4427
|
+
required: true
|
|
4428
|
+
},
|
|
4429
|
+
nextStrategy: {
|
|
4430
|
+
type: "string",
|
|
4431
|
+
required: true
|
|
4432
|
+
},
|
|
4433
|
+
text: {
|
|
4434
|
+
type: "string",
|
|
4435
|
+
required: true
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
},
|
|
4439
|
+
render: (_args, value) => [{
|
|
4440
|
+
type: "text",
|
|
4441
|
+
text: value.text
|
|
4442
|
+
}]
|
|
4443
|
+
},
|
|
4444
|
+
async execute(args, exec) {
|
|
4445
|
+
const input = args;
|
|
4446
|
+
if (typeof input.batchId !== "string" || input.batchId.trim() === "") throw new Error("engram_assess: batchId 必填(取自 engram_search 输出末尾)");
|
|
4447
|
+
const sessionId = exec.agent === void 0 ? void 0 : String(exec.agent.session.id);
|
|
4448
|
+
const batch = sessionId === void 0 ? void 0 : evidenceBatches.get(sessionId, input.batchId.trim());
|
|
4449
|
+
if (batch === void 0) throw new Error(`engram_assess: 批次 ${input.batchId} 不存在或已过期(每个会话保留最近 ${String(20)} 个批次)——请重新 engram_search 取新批次`);
|
|
4450
|
+
const verdict = assessEvidence(batch, {
|
|
4451
|
+
sufficient: input.sufficient === true,
|
|
4452
|
+
evidenceRefs: Array.isArray(input.evidenceRefs) ? input.evidenceRefs.filter((ref) => typeof ref === "string") : [],
|
|
4453
|
+
missing: typeof input.missing === "string" ? input.missing : "",
|
|
4454
|
+
nextStrategy: typeof input.nextStrategy === "string" ? input.nextStrategy : ""
|
|
4455
|
+
});
|
|
4456
|
+
await (await deps.openStore("user")).audit("assess", batch.batchId, JSON.stringify({
|
|
4457
|
+
batchId: batch.batchId,
|
|
4458
|
+
sufficient: verdict.sufficient,
|
|
4459
|
+
refs: verdict.evidenceRefs,
|
|
4460
|
+
rejected: verdict.rejectedRefs,
|
|
4461
|
+
strategy: verdict.nextStrategy
|
|
4462
|
+
}));
|
|
4463
|
+
return {
|
|
4464
|
+
sufficient: verdict.sufficient,
|
|
4465
|
+
nextStrategy: verdict.nextStrategy,
|
|
4466
|
+
text: renderAssessText(verdict, batch.batchId)
|
|
4232
4467
|
};
|
|
4233
4468
|
}
|
|
4234
4469
|
});
|
|
@@ -4622,6 +4857,7 @@ function createEngramTools(deps) {
|
|
|
4622
4857
|
return [
|
|
4623
4858
|
save,
|
|
4624
4859
|
search,
|
|
4860
|
+
assess,
|
|
4625
4861
|
timeline,
|
|
4626
4862
|
update,
|
|
4627
4863
|
forget,
|
|
@@ -5270,6 +5506,38 @@ function reasonsLabel(reasons) {
|
|
|
5270
5506
|
return reasons.map((r) => LABELS[r]).join(" / ");
|
|
5271
5507
|
}
|
|
5272
5508
|
//#endregion
|
|
5509
|
+
//#region src/token.ts
|
|
5510
|
+
/**
|
|
5511
|
+
* token 估算:CJK 感知的上下文预算口径。
|
|
5512
|
+
*
|
|
5513
|
+
* 汉字、假名、韩文与全角标点按 1.5 token/字计,其余字符按 4 字符/token 计。
|
|
5514
|
+
* 统一的 `长度 / 4` 会把中文低估四倍以上,使按 token 计的注入预算与实际占用脱钩
|
|
5515
|
+
* (中文 1024 token 预算实际会注入四千余 token)。
|
|
5516
|
+
* @module @kenz1117/dsh-engram/token
|
|
5517
|
+
*/
|
|
5518
|
+
/** 单个 CJK 字符的 token 成本。 */
|
|
5519
|
+
const WIDE_TOKENS_PER_CHAR = 1.5;
|
|
5520
|
+
/**
|
|
5521
|
+
* 判断码点是否属于 CJK 文字与全角符号区间(含扩展平面)。
|
|
5522
|
+
* @param codePoint - Unicode 码点。
|
|
5523
|
+
*/
|
|
5524
|
+
function isWideCodePoint(codePoint) {
|
|
5525
|
+
return codePoint >= 12288 && codePoint <= 12351 || codePoint >= 12352 && codePoint <= 12543 || codePoint >= 13312 && codePoint <= 19903 || codePoint >= 19968 && codePoint <= 40959 || codePoint >= 43360 && codePoint <= 43391 || codePoint >= 44032 && codePoint <= 55295 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 131072 && codePoint <= 262143;
|
|
5526
|
+
}
|
|
5527
|
+
/**
|
|
5528
|
+
* 估算文本占用的 token 数(向上取整)。
|
|
5529
|
+
* @param text - 待估算文本。
|
|
5530
|
+
* @returns token 估算值;空串为 0。
|
|
5531
|
+
*/
|
|
5532
|
+
function estimateTokens(text) {
|
|
5533
|
+
if (text === "") return 0;
|
|
5534
|
+
let wide = 0;
|
|
5535
|
+
let narrow = 0;
|
|
5536
|
+
for (const char of text) if (isWideCodePoint(char.codePointAt(0))) wide += 1;
|
|
5537
|
+
else narrow += 1;
|
|
5538
|
+
return Math.ceil(wide * WIDE_TOKENS_PER_CHAR + narrow / 4);
|
|
5539
|
+
}
|
|
5540
|
+
//#endregion
|
|
5273
5541
|
//#region src/index.ts
|
|
5274
5542
|
/**
|
|
5275
5543
|
* dsh-engram:DeepSeek Harness 跨会话长期记忆插件(host 半)。
|
|
@@ -5280,19 +5548,20 @@ function reasonsLabel(reasons) {
|
|
|
5280
5548
|
/** Cordis 插件名(loader 诊断与注入 source 使用)。 */
|
|
5281
5549
|
const name = "dsh-engram";
|
|
5282
5550
|
/** 插件版本(与 package.json 同步,写进备份 _meta.json)。 */
|
|
5283
|
-
const VERSION = "0.7.
|
|
5551
|
+
const VERSION = "0.7.5";
|
|
5284
5552
|
/** 必需服务:工具注册表与 LLM 流式端点(摄取/蒸馏的辅助调用)。 */
|
|
5285
5553
|
const inject = ["tools", "llm"];
|
|
5286
5554
|
/**
|
|
5287
|
-
* 会话开始注入的画像渲染:按重要性降序在 token
|
|
5288
|
-
*
|
|
5289
|
-
*
|
|
5555
|
+
* 会话开始注入的画像渲染:按重要性降序在 token 预算内整行装填(估算见
|
|
5556
|
+
* estimateTokens:中文按 1.5 token/字、其余按 4 字符/token,超预算的行跳过不截断、
|
|
5557
|
+
* 继续试更短行);装不下的条目降级为索引行(#id + 前 40 字),
|
|
5558
|
+
* 索引行也装不下的折成末尾 `+N more; use engram_search` 计数行;计数行同样占用预算。
|
|
5290
5559
|
* @param records - 候选条目(调用方已按重要性排序、按条数截断)。
|
|
5291
5560
|
* @param tokenBudget - 整段画像的 token 预算(含首尾固定行)。
|
|
5292
5561
|
* @returns 渲染文本与溢出条目(调用方可用辅助 LLM 压缩后重渲染)。
|
|
5293
5562
|
*/
|
|
5294
5563
|
function renderProfileDetailed(records, tokenBudget) {
|
|
5295
|
-
const estimate =
|
|
5564
|
+
const estimate = estimateTokens;
|
|
5296
5565
|
const header = "User memory profile (dsh-engram, cross-session) — Grand Hall (always present):";
|
|
5297
5566
|
const footer = "Use engram_search to recall details (pass room to search inside one room); use engram_save to persist new facts.";
|
|
5298
5567
|
let remaining = Math.max(0, tokenBudget - estimate(header) - estimate(footer));
|
|
@@ -5308,6 +5577,7 @@ function renderProfileDetailed(records, tokenBudget) {
|
|
|
5308
5577
|
} else overflow.push(record);
|
|
5309
5578
|
}
|
|
5310
5579
|
let more = 0;
|
|
5580
|
+
remaining -= overflow.length === 0 ? 0 : estimate(`+${overflow.length} more; use engram_search`);
|
|
5311
5581
|
for (const record of overflow) {
|
|
5312
5582
|
const line = `- [${record.kind}] #${record.id} ${record.content.slice(0, 40)}…`;
|
|
5313
5583
|
const cost = estimate(line);
|
|
@@ -5316,7 +5586,7 @@ function renderProfileDetailed(records, tokenBudget) {
|
|
|
5316
5586
|
remaining -= cost;
|
|
5317
5587
|
} else more += 1;
|
|
5318
5588
|
}
|
|
5319
|
-
if (more > 0) lines.push(`+${more} more; use engram_search`);
|
|
5589
|
+
if (more > 0 && remaining >= 0) lines.push(`+${more} more; use engram_search`);
|
|
5320
5590
|
return {
|
|
5321
5591
|
text: [
|
|
5322
5592
|
header,
|
|
@@ -5727,6 +5997,9 @@ function apply(ctx, config = {}) {
|
|
|
5727
5997
|
});
|
|
5728
5998
|
});
|
|
5729
5999
|
if (resolved.injectProfile || resolved.ingest !== "off") ctx.on("agent/pre-step", (payload, next) => preStep(ctx, openStore, resolved, embedder, preStepState, logIngestRequest, payload, next), { prepend: true });
|
|
6000
|
+
ctx.on("session/disposed", (session) => {
|
|
6001
|
+
evidenceBatches.clear(String(session.id));
|
|
6002
|
+
});
|
|
5730
6003
|
if (resolved.ingest !== "off") ctx.on("session/disposed", (session) => {
|
|
5731
6004
|
const mode = resolved.ingest;
|
|
5732
6005
|
if (mode === "off") return;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenz1117/dsh-engram",
|
|
3
3
|
"description": "Cross-session long-term memory for DeepSeek Harness (memory palace · AGI Architecture Exploration on dsh-market): dual-scope SQLite memory graph, hybrid retrieval, provenance audit, and a knowledge flywheel.",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.5",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"logo": "icon-256.svg",
|
|
7
7
|
"publishConfig": {
|