mocode-ai 1.6.4 → 1.6.6
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.md +382 -365
- package/README.zh-CN.md +19 -2
- package/dist/agent/model-turn.js +14 -1
- package/dist/agent/run-coordinator.js +7 -1
- package/dist/agent/spawn.js +38 -5
- package/dist/agent/stages/model-runner.js +1 -1
- package/dist/agent/stages/tool-dispatcher.js +1 -0
- package/dist/agent/tool-turn.js +2 -0
- package/dist/agent/work-discipline.js +3 -1
- package/dist/bots/bus.js +117 -0
- package/dist/bots/cli.js +102 -0
- package/dist/bots/store.js +100 -0
- package/dist/config/index.js +53 -2
- package/dist/config/presets.js +40 -1
- package/dist/config/profiles.js +9 -1
- package/dist/context/clearing.js +53 -0
- package/dist/context/relevance.js +6 -0
- package/dist/context/text-search.js +113 -0
- package/dist/headless.js +302 -0
- package/dist/index.js +171 -3
- package/dist/jobs/approval.js +153 -0
- package/dist/jobs/approve-cli.js +30 -0
- package/dist/jobs/attach.js +61 -0
- package/dist/jobs/checkpoint.js +33 -0
- package/dist/jobs/launch.js +107 -0
- package/dist/jobs/runner.js +59 -0
- package/dist/jobs/store.js +101 -0
- package/dist/jobs/worktree.js +63 -0
- package/dist/llm/index.js +35 -8
- package/dist/llm/providers/anthropic.js +14 -4
- package/dist/llm/reasoning.js +141 -0
- package/dist/memory/reflect.js +1 -1
- package/dist/memory/store.js +31 -28
- package/dist/models/budgets.js +11 -0
- package/dist/models/catalog.js +130 -0
- package/dist/models/map-preset.js +50 -0
- package/dist/models/model-panel.js +227 -0
- package/dist/models/protocol.js +44 -0
- package/dist/models/reasoning-cap.js +88 -0
- package/dist/models/search.js +58 -0
- package/dist/models/types.js +8 -0
- package/dist/notify/index.js +144 -0
- package/dist/repl/commands/bots.js +32 -0
- package/dist/repl/commands/effort.js +81 -0
- package/dist/repl/commands/jobs.js +109 -0
- package/dist/repl/commands/model-actions.js +138 -0
- package/dist/repl/commands/model-catalog.js +67 -0
- package/dist/repl/commands/model-panel-ui.js +107 -0
- package/dist/repl/commands/model.js +22 -20
- package/dist/repl/commands/registry.js +12 -0
- package/dist/repl/commands/schedule.js +37 -0
- package/dist/repl/commands/session.js +54 -0
- package/dist/repl/commands/stats.js +28 -0
- package/dist/repl/commands.js +6 -58
- package/dist/repl/runtime.js +5 -1
- package/dist/repl/status-bar.js +4 -2
- package/dist/rollback/store.js +17 -0
- package/dist/runtime/runtime.js +31 -1
- package/dist/schedule/cli.js +114 -0
- package/dist/schedule/cron.js +79 -0
- package/dist/schedule/daemon.js +166 -0
- package/dist/schedule/store.js +98 -0
- package/dist/session/compact.js +84 -3
- package/dist/session/index.js +2 -0
- package/dist/session/retention.js +297 -0
- package/dist/session/scheduler.js +30 -3
- package/dist/session/store.js +108 -6
- package/dist/session/usage-stats.js +90 -0
- package/dist/skills/activation.js +2 -0
- package/dist/skills/builtin-skills.js +153 -0
- package/dist/skills/discover.js +3 -1
- package/dist/skills/runner.js +4 -1
- package/dist/tools/builtins/arena.js +156 -0
- package/dist/tools/builtins/index.js +8 -0
- package/dist/tools/builtins/message-bus.js +98 -0
- package/dist/tools/builtins/read-file.js +19 -1
- package/dist/tools/builtins/run-command.js +2 -1
- package/dist/tools/builtins/session-search.js +33 -0
- package/dist/tools/builtins/task.js +2 -0
- package/dist/tools/constants.js +8 -0
- package/dist/tools/read-dedup.js +62 -0
- package/dist/tools/tool-runtime.js +1 -0
- package/dist/ui/batch.js +140 -104
- package/dist/ui/composer.js +4 -18
- package/dist/ui/fuzzy-picker.js +227 -0
- package/dist/ui/hierarchical-picker.js +302 -0
- package/dist/ui/intervention.js +17 -1
- package/dist/ui/layout-internal/statusbar.js +18 -7
- package/dist/ui/prompt-internal/editor.js +10 -27
- package/dist/ui/render.js +2 -0
- package/package.json +1 -1
package/dist/config/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
|
+
import { parseReasoningEffort, getScopedEffort } from '../llm/reasoning.js';
|
|
6
|
+
import { getActiveSkill } from '../skills/activation.js';
|
|
5
7
|
import { getCurrentSessionId } from '../session/state.js';
|
|
6
8
|
import { getNotesFilePath, extractActiveNotesSections } from '../session/notes.js';
|
|
7
9
|
import { buildGuiActionsSection } from '../session/gui-actions.js';
|
|
@@ -43,7 +45,7 @@ function loadEnvFiles() {
|
|
|
43
45
|
process.env[k] = v;
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
// 在 loadEnvFiles
|
|
48
|
+
// 在 loadEnvFiles 回填前先捕获:MOCODE_THEME / MOCODE_LANGUAGE 是否由 shell 显式 export(决定后续优先级提示)。
|
|
47
49
|
const themeFromShell = process.env.MOCODE_THEME !== undefined;
|
|
48
50
|
export const languageFromShell = process.env.MOCODE_LANGUAGE !== undefined;
|
|
49
51
|
// 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
|
|
@@ -56,6 +58,7 @@ const LLM_ENV_KEYS = [
|
|
|
56
58
|
'LLM_MODEL',
|
|
57
59
|
'CONTEXT_WINDOW_TOKENS',
|
|
58
60
|
'ANTHROPIC_PROMPT_CACHE',
|
|
61
|
+
'REASONING_EFFORT',
|
|
59
62
|
];
|
|
60
63
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 256000;
|
|
61
64
|
const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
|
|
@@ -492,7 +495,8 @@ ${buildVoiceSection()}
|
|
|
492
495
|
- Stop immediately when no more tools are needed; give conclusions directly.
|
|
493
496
|
- **Do not stop prematurely during exploration**: if you started investigating but haven't gathered enough information to answer the user's question, keep calling tools. Only stop when you have sufficient evidence or hit a dead end.
|
|
494
497
|
- **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
|
|
495
|
-
- Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise
|
|
498
|
+
- Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.
|
|
499
|
+
- Definition of done: never declare a multi-feature request finished just because the code exists. Each distinct user request listed in the plan must be actually verified (the relevant check run / observed result), not merely implemented; mark plan steps \`[x]\` only after that verification. If the user supplied an acceptance list, every item on it must pass. Leave unverified items explicitly listed as pending instead of collapsing them into "done".`;
|
|
496
500
|
// 动态段(置于末尾):AGENTS.md 项目记忆 + notepad 索引/说明。工具簇特定指导由
|
|
497
501
|
// ToolPolicyController.reminder() 按当前 turn 的 route 注入,避免旧全局 profile 与真实 schema 分裂。
|
|
498
502
|
// 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
|
|
@@ -582,10 +586,17 @@ export const config = {
|
|
|
582
586
|
? process.env.ANTHROPIC_PROMPT_CACHE !== 'false'
|
|
583
587
|
: (__activePreset?.anthropicPromptCache ?? process.env.ANTHROPIC_PROMPT_CACHE !== 'false'),
|
|
584
588
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
589
|
+
reasoningEffort: llmKeysFromShell.includes('REASONING_EFFORT')
|
|
590
|
+
? (parseReasoningEffort(process.env.REASONING_EFFORT) ?? 'auto')
|
|
591
|
+
: (__activePreset?.reasoningEffort ?? parseReasoningEffort(process.env.REASONING_EFFORT) ?? 'auto'),
|
|
592
|
+
compactFork: process.env.MOCODE_COMPACT_FORK !== 'false',
|
|
593
|
+
readDedup: process.env.MOCODE_READ_DEDUP !== 'false',
|
|
585
594
|
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
|
|
586
595
|
contextRelprune: process.env.MOCODE_CONTEXT_RELPRUNE !== 'false',
|
|
587
596
|
contextLifecycle: process.env.MOCODE_LIFECYCLE !== 'false',
|
|
588
597
|
contextBudget: process.env.MOCODE_BUDGET_SCHEDULER !== 'false',
|
|
598
|
+
lowPressureRatio: Math.min(0.95, Math.max(0.1, Number(process.env.MOCODE_LOW_PRESSURE_RATIO) || 0.6)),
|
|
599
|
+
toolClearing: process.env.MOCODE_TOOL_CLEARING !== 'false',
|
|
589
600
|
autoReflect: process.env.AUTO_REFLECT === 'true',
|
|
590
601
|
memoryEnabled: process.env.MEMORY_ENABLED === 'true',
|
|
591
602
|
reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
|
|
@@ -593,6 +604,7 @@ export const config = {
|
|
|
593
604
|
subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
|
|
594
605
|
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || Number(process.env.MAX_STEPS) || 1000,
|
|
595
606
|
subAgentConcurrency: Math.max(1, Number(process.env.SUB_AGENT_CONCURRENCY) || 5),
|
|
607
|
+
subAgentMaxDepth: Math.max(1, Number(process.env.SUB_AGENT_MAX_DEPTH) || 3),
|
|
596
608
|
frontendToolsEnabled: process.env.MOCODE_FRONTEND_TOOLS_ENABLED === 'true',
|
|
597
609
|
computerUseEnabled: process.env.MOCODE_COMPUTER_USE_ENABLED === 'true',
|
|
598
610
|
mcpEnabled: process.env.MOCODE_MCP_ENABLED !== 'false',
|
|
@@ -606,6 +618,7 @@ export const config = {
|
|
|
606
618
|
llmKeysFromShell,
|
|
607
619
|
permissionEnabled: process.env.MOCODE_PERMISSION !== 'false',
|
|
608
620
|
permissionNonInteractiveAllow: process.env.MOCODE_PERMISSION_NON_INTERACTIVE_ALLOW === 'true',
|
|
621
|
+
notifyWebhook: process.env.MOCODE_NOTIFY_WEBHOOK || undefined,
|
|
609
622
|
};
|
|
610
623
|
/**
|
|
611
624
|
* 创建一份独立 Config 快照,不重新读取环境变量、配置文件或 preset。
|
|
@@ -636,6 +649,38 @@ export function pinSessionModel() {
|
|
|
636
649
|
export function getActiveModel() {
|
|
637
650
|
return sessionModel ?? config.model;
|
|
638
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* 会话级思考强度钉死(P3),与 sessionModel 同构:窗口启动 pinSessionEffort() 捕获,
|
|
654
|
+
* /effort 显式修改同步更新钉死值;其它窗口的设置互不影响。
|
|
655
|
+
*/
|
|
656
|
+
let sessionEffort = null;
|
|
657
|
+
/** REPL 启动时调用一次。 */
|
|
658
|
+
export function pinSessionEffort() {
|
|
659
|
+
sessionEffort = config.reasoningEffort;
|
|
660
|
+
}
|
|
661
|
+
/** 运行中 agent 实际使用的思考强度。 */
|
|
662
|
+
export function getActiveEffort() {
|
|
663
|
+
return sessionEffort ?? config.reasoningEffort;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* 生效思考强度统一解析(P3 §4.8):
|
|
667
|
+
* 显式 per-request 参数(compact/reflect 的 low)> fork ALS scope(skill 子树)
|
|
668
|
+
* > inline 激活 skill 的 effort > 会话级。
|
|
669
|
+
* shell env 显式设置 REASONING_EFFORT 时,skill effort 不参与(对齐 Claude Code:env 最高)。
|
|
670
|
+
*/
|
|
671
|
+
export function effectiveReasoningEffort(explicit) {
|
|
672
|
+
if (explicit !== undefined)
|
|
673
|
+
return explicit;
|
|
674
|
+
const scoped = getScopedEffort();
|
|
675
|
+
if (scoped)
|
|
676
|
+
return scoped;
|
|
677
|
+
if (!config.llmKeysFromShell.includes('REASONING_EFFORT')) {
|
|
678
|
+
const skillEffort = getActiveSkill()?.effort;
|
|
679
|
+
if (skillEffort)
|
|
680
|
+
return skillEffort;
|
|
681
|
+
}
|
|
682
|
+
return getActiveEffort();
|
|
683
|
+
}
|
|
639
684
|
/**
|
|
640
685
|
* 运行时更新模型相关配置(/model 命令调)。
|
|
641
686
|
* - 更新 config 对象字段(即时生效:chat() 读 config.model,reconfigureClient 读 config.baseURL/apiKey)。
|
|
@@ -673,6 +718,12 @@ export function updateModelConfig(opts) {
|
|
|
673
718
|
config.anthropicPromptCache = opts.anthropicPromptCache;
|
|
674
719
|
process.env.ANTHROPIC_PROMPT_CACHE = opts.anthropicPromptCache ? 'true' : 'false';
|
|
675
720
|
}
|
|
721
|
+
if (opts.reasoningEffort !== undefined) {
|
|
722
|
+
config.reasoningEffort = opts.reasoningEffort;
|
|
723
|
+
if (sessionEffort !== null)
|
|
724
|
+
sessionEffort = opts.reasoningEffort;
|
|
725
|
+
process.env.REASONING_EFFORT = opts.reasoningEffort;
|
|
726
|
+
}
|
|
676
727
|
}
|
|
677
728
|
/** legacy 嵌入路径的 profile 查询;官方主 Agent 不读取。 */
|
|
678
729
|
export function getActiveProfile() {
|
package/dist/config/presets.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { parseReasoningEffort } from '../llm/reasoning.js';
|
|
4
5
|
/**
|
|
5
6
|
* 多模型预设(`/model save <name>` 保存的命名配置)的纯 I/O 叶子。
|
|
6
7
|
*
|
|
@@ -78,6 +79,41 @@ export function parsePreset(raw) {
|
|
|
78
79
|
}
|
|
79
80
|
const provider = obj.provider === 'anthropic' ? 'anthropic' : 'openai';
|
|
80
81
|
const anthropicPromptCache = provider === 'anthropic' && obj.anthropicPromptCache !== false;
|
|
82
|
+
// reasoningEffort 缺省 = auto;显式写了非法值按现有校验风格报错(坏文件由 listPresets 跳过)。
|
|
83
|
+
let reasoningEffort;
|
|
84
|
+
if (obj.reasoningEffort !== undefined) {
|
|
85
|
+
reasoningEffort = parseReasoningEffort(obj.reasoningEffort);
|
|
86
|
+
if (!reasoningEffort)
|
|
87
|
+
throw new Error(`预设 ${name}: reasoningEffort 非法(off|low|medium|high|auto)`);
|
|
88
|
+
}
|
|
89
|
+
// ── 目录可选字段:只在「存在且类型正确」时采纳,缺失/错误即 undefined,不报错 ──
|
|
90
|
+
const extra = {};
|
|
91
|
+
if (typeof obj.catalogProvider === 'string' && obj.catalogProvider)
|
|
92
|
+
extra.catalogProvider = obj.catalogProvider;
|
|
93
|
+
if (typeof obj.catalogModel === 'string' && obj.catalogModel)
|
|
94
|
+
extra.catalogModel = obj.catalogModel;
|
|
95
|
+
if (obj.capabilities && typeof obj.capabilities === 'object') {
|
|
96
|
+
const c = obj.capabilities;
|
|
97
|
+
const caps = {};
|
|
98
|
+
if (typeof c.reasoning === 'boolean')
|
|
99
|
+
caps.reasoning = c.reasoning;
|
|
100
|
+
if (typeof c.toolCall === 'boolean')
|
|
101
|
+
caps.toolCall = c.toolCall;
|
|
102
|
+
if (typeof c.attachment === 'boolean')
|
|
103
|
+
caps.attachment = c.attachment;
|
|
104
|
+
if (Array.isArray(c.reasoningOptions))
|
|
105
|
+
caps.reasoningOptions = c.reasoningOptions;
|
|
106
|
+
extra.capabilities = caps;
|
|
107
|
+
}
|
|
108
|
+
if (obj.pricing && typeof obj.pricing === 'object') {
|
|
109
|
+
const p = obj.pricing;
|
|
110
|
+
const pricing = {};
|
|
111
|
+
for (const k of ['input', 'output', 'cacheRead']) {
|
|
112
|
+
if (typeof p[k] === 'number' && Number.isFinite(p[k]))
|
|
113
|
+
pricing[k] = p[k];
|
|
114
|
+
}
|
|
115
|
+
extra.pricing = pricing;
|
|
116
|
+
}
|
|
81
117
|
return {
|
|
82
118
|
name,
|
|
83
119
|
provider,
|
|
@@ -86,6 +122,8 @@ export function parsePreset(raw) {
|
|
|
86
122
|
model,
|
|
87
123
|
contextWindow: Math.floor(contextWindow),
|
|
88
124
|
anthropicPromptCache,
|
|
125
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
126
|
+
...extra,
|
|
89
127
|
};
|
|
90
128
|
}
|
|
91
129
|
/** 读单个预设;不存在抛错。 */
|
|
@@ -217,7 +255,8 @@ export function migrateCurrentToPreset(input) {
|
|
|
217
255
|
p.apiKey === input.apiKey &&
|
|
218
256
|
p.model === input.model &&
|
|
219
257
|
p.contextWindow === input.contextWindow &&
|
|
220
|
-
p.anthropicPromptCache === anthropicPromptCache
|
|
258
|
+
p.anthropicPromptCache === anthropicPromptCache &&
|
|
259
|
+
(p.reasoningEffort ?? 'auto') === 'auto');
|
|
221
260
|
if (dup)
|
|
222
261
|
return null;
|
|
223
262
|
// 'default' 已被占 → 用户已显式起过预设,无需老数据迁入;返回 null 让调用方跳过即可。
|
package/dist/config/profiles.js
CHANGED
|
@@ -19,7 +19,15 @@ export const TOOL_GROUPS = {
|
|
|
19
19
|
web: ['web_search', 'web_fetch'],
|
|
20
20
|
frontend: ['browser', 'screenshot'],
|
|
21
21
|
computer: ['computer'],
|
|
22
|
-
memory: [
|
|
22
|
+
memory: [
|
|
23
|
+
'memory_save',
|
|
24
|
+
'memory_search',
|
|
25
|
+
'memory_list',
|
|
26
|
+
'memory_update',
|
|
27
|
+
'memory_forget',
|
|
28
|
+
'memory_graph',
|
|
29
|
+
'session_search',
|
|
30
|
+
],
|
|
23
31
|
subagent: ['sub-agent'],
|
|
24
32
|
};
|
|
25
33
|
// ── LLM 自动工具路由 ──────────────────────────────────────────────────────
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Tool-result clearing: 可重取结果的低成本清除(Anthropic 三原语之一)。
|
|
2
|
+
//
|
|
3
|
+
// 与 relevance pruner 的区别:pruner 只在有「精确的新替代」时 stub;clearing 更宽——
|
|
4
|
+
// 冷区中来自可重取工具的结果,无论有没有新替代,内容都可丢弃(需要时重新调用即可),
|
|
5
|
+
// 只保留「调用发生过」的 tombstone。不删消息、不改 tool_call_id 配对。
|
|
6
|
+
//
|
|
7
|
+
// 在低压阶段(60%)运行:零 LLM 成本、不依赖摘要质量。热区(最近 hotTurnWindow 个
|
|
8
|
+
// user turn)不动。
|
|
9
|
+
import { toText } from './utils.js';
|
|
10
|
+
/** 结果可随时重取的工具:纯读、无副作用。 */
|
|
11
|
+
export const REFETCHABLE_TOOLS = new Set(['read_file', 'grep', 'glob', 'web_search', 'web_fetch']);
|
|
12
|
+
/** Tombstone 前缀,与 relevance stub 同属 ⌦[ 家族,其余子系统据此识别已清除内容。 */
|
|
13
|
+
const CLEARED_PREFIX = '⌦[已清除:';
|
|
14
|
+
/** 回溯 tool_call_id → 产生该结果的工具名(同 relevance.ts callAt 的最小版本)。 */
|
|
15
|
+
function toolNameAt(history, idx) {
|
|
16
|
+
const tcId = history[idx]?.tool_call_id;
|
|
17
|
+
if (!tcId)
|
|
18
|
+
return null;
|
|
19
|
+
for (let j = idx - 1; j >= 1; j--) {
|
|
20
|
+
if (history[j].role !== 'assistant')
|
|
21
|
+
continue;
|
|
22
|
+
const calls = history[j].tool_calls;
|
|
23
|
+
const hit = calls?.find((tc) => tc?.id === tcId);
|
|
24
|
+
if (hit?.function?.name)
|
|
25
|
+
return hit.function.name;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 清除冷区可重取工具的结果内容。
|
|
31
|
+
* @param coldBoundary 仅处理 index < coldBoundary 的消息(热区保留)。
|
|
32
|
+
* @returns 被清除的消息数。
|
|
33
|
+
*/
|
|
34
|
+
export function clearRetrievableResults(history, coldBoundary) {
|
|
35
|
+
let cleared = 0;
|
|
36
|
+
const end = Math.min(coldBoundary, history.length);
|
|
37
|
+
for (let idx = 1; idx < end; idx++) {
|
|
38
|
+
const message = history[idx];
|
|
39
|
+
if (message.role !== 'tool' || !message.tool_call_id)
|
|
40
|
+
continue;
|
|
41
|
+
const content = toText(message.content);
|
|
42
|
+
if (!content || content.startsWith('⌦['))
|
|
43
|
+
continue;
|
|
44
|
+
const name = toolNameAt(history, idx);
|
|
45
|
+
if (!name || !REFETCHABLE_TOOLS.has(name))
|
|
46
|
+
continue;
|
|
47
|
+
message.content =
|
|
48
|
+
`${CLEARED_PREFIX}${name}] 原结果 ${content.length} 字符已清除(可重新调用获取)` +
|
|
49
|
+
` · id …${message.tool_call_id.slice(-6)}⌫`;
|
|
50
|
+
cleared++;
|
|
51
|
+
}
|
|
52
|
+
return cleared;
|
|
53
|
+
}
|
|
@@ -258,10 +258,15 @@ export function computePruneStats(history) {
|
|
|
258
258
|
let stubbed = 0;
|
|
259
259
|
let originalChars = 0;
|
|
260
260
|
let stubChars = 0;
|
|
261
|
+
let cleared = 0;
|
|
261
262
|
for (const message of history) {
|
|
262
263
|
if (message.role !== 'tool')
|
|
263
264
|
continue;
|
|
264
265
|
const content = toText(message.content);
|
|
266
|
+
if (content.startsWith('⌦[已清除:')) {
|
|
267
|
+
cleared++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
265
270
|
const isPruneStub = content.startsWith(STUB_PREFIX);
|
|
266
271
|
const isDigest = content.startsWith('⌦[摘要:');
|
|
267
272
|
if (!isPruneStub && !isDigest)
|
|
@@ -280,5 +285,6 @@ export function computePruneStats(history) {
|
|
|
280
285
|
originalTokens,
|
|
281
286
|
stubChars,
|
|
282
287
|
freedTokens: Math.max(0, originalTokens - stubTokens),
|
|
288
|
+
cleared,
|
|
283
289
|
};
|
|
284
290
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// 共享全文检索:CJK 感知分词 + BM25。零依赖纯函数,供 memory 与 archive 搜索复用。
|
|
2
|
+
//
|
|
3
|
+
// 分词:ASCII/数字按词;CJK 字符同时生成 unigram 与相邻 bigram——中文词多为两字,
|
|
4
|
+
// 单字召回噪声大、纯 bigram 又漏掉单字成词的情况,unigram+bigram 是 Elasticsearch
|
|
5
|
+
// cjk analyzer 的成熟折中。
|
|
6
|
+
//
|
|
7
|
+
// 多字段加权 BM25:每个字段独立长度归一,字段分按 weight 加权求和。
|
|
8
|
+
const CJK_RE = /[㐀-䶿一-鿿豈--ヿ가-]/;
|
|
9
|
+
/** CJK 感知分词(小写)。 */
|
|
10
|
+
export function tokenize(input) {
|
|
11
|
+
const text = (input ?? '').toLowerCase();
|
|
12
|
+
const tokens = [];
|
|
13
|
+
const chars = Array.from(text);
|
|
14
|
+
let word = '';
|
|
15
|
+
const flushWord = () => {
|
|
16
|
+
if (word) {
|
|
17
|
+
tokens.push(word);
|
|
18
|
+
word = '';
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
for (let i = 0; i < chars.length; i++) {
|
|
22
|
+
const ch = chars[i];
|
|
23
|
+
if (/[a-z0-9]/.test(ch)) {
|
|
24
|
+
word += ch;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
flushWord();
|
|
28
|
+
if (CJK_RE.test(ch)) {
|
|
29
|
+
tokens.push(ch); // unigram
|
|
30
|
+
if (i + 1 < chars.length && CJK_RE.test(chars[i + 1])) {
|
|
31
|
+
tokens.push(ch + chars[i + 1]); // bigram
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
flushWord();
|
|
36
|
+
return tokens;
|
|
37
|
+
}
|
|
38
|
+
/** BM25 多字段索引。文档量小(memory ≤百条、archive 索引),构建成本可忽略。 */
|
|
39
|
+
export class TextSearchIndex {
|
|
40
|
+
k1;
|
|
41
|
+
b;
|
|
42
|
+
ids;
|
|
43
|
+
fieldStats;
|
|
44
|
+
fieldWeights;
|
|
45
|
+
constructor(docs, k1 = 1.5, b = 0.75) {
|
|
46
|
+
this.k1 = k1;
|
|
47
|
+
this.b = b;
|
|
48
|
+
this.ids = docs.map((d) => d.id);
|
|
49
|
+
const fieldCount = Math.max(1, ...docs.map((d) => d.fields.length));
|
|
50
|
+
this.fieldWeights = Array.from({ length: fieldCount }, (_, f) => docs[0]?.fields[f]?.weight ?? 1);
|
|
51
|
+
this.fieldStats = Array.from({ length: fieldCount }, () => ({
|
|
52
|
+
df: new Map(),
|
|
53
|
+
tf: new Map(),
|
|
54
|
+
dl: new Map(),
|
|
55
|
+
avgdl: 0,
|
|
56
|
+
}));
|
|
57
|
+
docs.forEach((doc, docId) => {
|
|
58
|
+
doc.fields.forEach((field, f) => {
|
|
59
|
+
const stats = this.fieldStats[f];
|
|
60
|
+
const toks = tokenize(field.text);
|
|
61
|
+
stats.dl.set(docId, toks.length);
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
for (const t of toks) {
|
|
64
|
+
let perDoc = stats.tf.get(t);
|
|
65
|
+
if (!perDoc) {
|
|
66
|
+
perDoc = new Map();
|
|
67
|
+
stats.tf.set(t, perDoc);
|
|
68
|
+
}
|
|
69
|
+
perDoc.set(docId, (perDoc.get(docId) ?? 0) + 1);
|
|
70
|
+
seen.add(t);
|
|
71
|
+
}
|
|
72
|
+
for (const t of seen)
|
|
73
|
+
stats.df.set(t, (stats.df.get(t) ?? 0) + 1);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const n = docs.length || 1;
|
|
77
|
+
for (const stats of this.fieldStats) {
|
|
78
|
+
let total = 0;
|
|
79
|
+
for (const len of stats.dl.values())
|
|
80
|
+
total += len;
|
|
81
|
+
stats.avgdl = total / n;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** 检索;无有效查询词时返回空数组(调用方自行决定是否回退全量)。 */
|
|
85
|
+
search(query, limit = 10) {
|
|
86
|
+
const terms = tokenize(query);
|
|
87
|
+
if (terms.length === 0 || this.ids.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
const n = this.ids.length;
|
|
90
|
+
const hits = [];
|
|
91
|
+
for (let docId = 0; docId < n; docId++) {
|
|
92
|
+
let score = 0;
|
|
93
|
+
this.fieldStats.forEach((stats, f) => {
|
|
94
|
+
const dl = stats.dl.get(docId);
|
|
95
|
+
if (dl === undefined)
|
|
96
|
+
return;
|
|
97
|
+
const norm = this.k1 * (1 - this.b + (this.b * dl) / Math.max(stats.avgdl, 1));
|
|
98
|
+
for (const term of terms) {
|
|
99
|
+
const tf = stats.tf.get(term)?.get(docId);
|
|
100
|
+
if (!tf)
|
|
101
|
+
continue;
|
|
102
|
+
const df = stats.df.get(term) ?? 0;
|
|
103
|
+
const idf = Math.log(1 + (n - df + 0.5) / (df + 0.5));
|
|
104
|
+
score += ((this.fieldWeights[f] ?? 1) * (idf * (tf * (this.k1 + 1)))) / (tf + norm);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
if (score > 0)
|
|
108
|
+
hits.push({ id: this.ids[docId], score });
|
|
109
|
+
}
|
|
110
|
+
hits.sort((a, b) => b.score - a.score);
|
|
111
|
+
return hits.slice(0, Math.max(1, limit));
|
|
112
|
+
}
|
|
113
|
+
}
|