recallnest 2.3.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +187 -0
- package/README.md +49 -38
- package/README_CN.md +46 -31
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +25 -4
- package/src/__tests__/access-tracker-novelty.test.ts +304 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/api-server-security.test.ts +102 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/belief-history.test.ts +220 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1328 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +116 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +945 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +418 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +430 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +262 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +129 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +780 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/freshness.test.ts +186 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +283 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-client-pivot.test.ts +628 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +189 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +282 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/noise-prototype-bank.test.ts +280 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +333 -0
- package/src/__tests__/pivot-apply.test.ts +373 -0
- package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
- package/src/__tests__/pivot-distill.test.ts +1178 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +282 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-gate.test.ts +266 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/resolve-db-path.test.ts +130 -0
- package/src/__tests__/resolve-tier.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-eval.test.ts +163 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +654 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/stable-context-length-contract.test.ts +62 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-read-consistency.test.ts +122 -0
- package/src/__tests__/store-schema-migration.test.ts +87 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +201 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/ui-server-security.test.ts +93 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +90 -34
- package/src/activity-counter.ts +46 -18
- package/src/adaptive-retrieval.ts +11 -0
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +36 -9
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/auto-recall.ts +61 -1
- package/src/belief-history.ts +164 -0
- package/src/capture-engine.ts +142 -32
- package/src/cli.ts +268 -41
- package/src/confidence-tracker.ts +36 -18
- package/src/conflict-engine.ts +24 -3
- package/src/consolidation-engine.ts +255 -23
- package/src/context-composer-stable-selection.ts +7 -1
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +96 -7
- package/src/decay-engine.ts +59 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +13 -4
- package/src/dream-pipeline.ts +473 -41
- package/src/env-config.ts +118 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +402 -14
- package/src/freshness.ts +192 -0
- package/src/http-request-policy.ts +148 -0
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +308 -12
- package/src/kg-extractor.ts +13 -3
- package/src/kg-store.ts +236 -44
- package/src/llm-client.ts +106 -11
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +87 -1800
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +954 -0
- package/src/mcp-tools-core.ts +637 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +26 -3
- package/src/memory-evolution.ts +73 -1
- package/src/memory-health-rebalance.ts +4 -0
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +129 -4
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +46 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/noise-prototype-bank.ts +408 -0
- package/src/pii-detector.ts +207 -5
- package/src/pivot-apply.ts +399 -0
- package/src/preference-matcher.ts +37 -3
- package/src/query-expander.ts +90 -9
- package/src/recall-gate.ts +305 -0
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retrieval-profiles.ts +46 -4
- package/src/retriever.ts +354 -157
- package/src/retroactive-boost.ts +6 -2
- package/src/runtime-config.ts +78 -6
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +1 -0
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +304 -48
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +321 -57
- package/src/term-registry.ts +35 -0
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +9 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/version.ts +19 -0
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -60
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/ingest-2026-04-13.log +0 -87
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
package/src/skill-promotion.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { MemoryEntry, MemoryStore } from "./store.js";
|
|
13
13
|
import { cosineSimilarity } from "./multi-vector.js";
|
|
14
14
|
import { isActiveMemory } from "./memory-evolution.js";
|
|
15
|
+
import { verifyDraft, type VerifyResult } from "./skill-verifier.js";
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Types
|
|
@@ -25,12 +26,20 @@ export interface PromotionCandidate {
|
|
|
25
26
|
/** For pattern_to_skill: extracted implementation steps */
|
|
26
27
|
suggestedImplementation?: string;
|
|
27
28
|
confidence: number; // 0-1
|
|
29
|
+
/** A2: zero-LLM verifier verdict (pattern_to_skill only). 失败候选保留披露、排后、不静默丢。 */
|
|
30
|
+
verification?: VerifyResult;
|
|
31
|
+
/** Read-signal evidence aggregated from source entries(Artel 读驱动升华:被真实读过的候选提权)。 */
|
|
32
|
+
readEvidence?: { totalAccess: number; distinctReaders: number };
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
export interface PromotionScanResult {
|
|
31
36
|
candidates: PromotionCandidate[];
|
|
32
37
|
scannedCases: number;
|
|
33
38
|
scannedPatterns: number;
|
|
39
|
+
/** No-silent-caps 披露:被桶上限截断、未参与聚类的 case 数。 */
|
|
40
|
+
truncatedCases: number;
|
|
41
|
+
/** 向量回填失败(库中无向量)而被跳过的条目数。 */
|
|
42
|
+
vectorlessSkipped: number;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export interface PromotionConfig {
|
|
@@ -40,24 +49,138 @@ export interface PromotionConfig {
|
|
|
40
49
|
caseSimilarityThreshold: number;
|
|
41
50
|
/** Max candidates to return (default: 5) */
|
|
42
51
|
maxCandidates: number;
|
|
52
|
+
/** 全量扫描上限(防 OOM,default: 20000)。cases/patterns 各自独立适用。 */
|
|
53
|
+
maxScanEntries: number;
|
|
54
|
+
/** 单桶聚类上限(default: 2000)。桶内按 recency 保留,截断量计入 truncatedCases。 */
|
|
55
|
+
maxBucketSize: number;
|
|
56
|
+
/** listPage 翻页大小 (default: 1000)。 */
|
|
57
|
+
pageSize: number;
|
|
58
|
+
/** pattern_to_skill 通道最多探测的 structured patterns 数(每个一次 vectorSearch,
|
|
59
|
+
* default: 1000,按 recency 优先)。 */
|
|
60
|
+
maxPatternProbes: number;
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
const DEFAULT_CONFIG: PromotionConfig = {
|
|
46
64
|
minCaseOccurrences: 3,
|
|
47
65
|
caseSimilarityThreshold: 0.75,
|
|
48
66
|
maxCandidates: 5,
|
|
67
|
+
maxScanEntries: 20_000,
|
|
68
|
+
maxBucketSize: 2_000,
|
|
69
|
+
pageSize: 1_000,
|
|
70
|
+
maxPatternProbes: 1_000,
|
|
49
71
|
};
|
|
50
72
|
|
|
51
73
|
// ---------------------------------------------------------------------------
|
|
52
74
|
// Helpers
|
|
53
75
|
// ---------------------------------------------------------------------------
|
|
54
76
|
|
|
55
|
-
type PromotionStore = Pick<MemoryStore, "
|
|
77
|
+
type PromotionStore = Pick<MemoryStore, "listPage" | "getVectors" | "vectorSearch">;
|
|
56
78
|
|
|
57
79
|
function isActive(entry: MemoryEntry): boolean {
|
|
58
80
|
return isActiveMemory(entry.metadata);
|
|
59
81
|
}
|
|
60
82
|
|
|
83
|
+
/**
|
|
84
|
+
* 翻页拉取一个 category 的全部条目(轻列,不含向量)。
|
|
85
|
+
* listPage 在 DB 层下推 where/limit/offset,不会像 list() 那样每页全表拉取。
|
|
86
|
+
*/
|
|
87
|
+
async function listAllByCategory(
|
|
88
|
+
store: PromotionStore,
|
|
89
|
+
scope: string,
|
|
90
|
+
category: string,
|
|
91
|
+
maxEntries: number,
|
|
92
|
+
pageSize: number,
|
|
93
|
+
truncateTextTo?: number,
|
|
94
|
+
): Promise<MemoryEntry[]> {
|
|
95
|
+
const out: MemoryEntry[] = [];
|
|
96
|
+
for (let offset = 0; out.length < maxEntries; offset += pageSize) {
|
|
97
|
+
const requested = Math.min(pageSize, maxEntries - out.length);
|
|
98
|
+
const page = await store.listPage({
|
|
99
|
+
scopeFilter: [scope],
|
|
100
|
+
category,
|
|
101
|
+
limit: requested,
|
|
102
|
+
offset,
|
|
103
|
+
});
|
|
104
|
+
for (const e of page) {
|
|
105
|
+
// 扫描只用 text 的头部(extractName 首行 / summarize 前 80 字 / 聚类靠向量),
|
|
106
|
+
// bridge 对话 case 全文很长,2 万条常驻能到 GB 级——截断防内存膨胀。
|
|
107
|
+
out.push(
|
|
108
|
+
truncateTextTo !== undefined && e.text.length > truncateTextTo
|
|
109
|
+
? { ...e, text: e.text.slice(0, truncateTextTo) }
|
|
110
|
+
: e,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (page.length < requested) break;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 从 metadata JSON 读 topicTag(缺失/解析失败 → undefined)。 */
|
|
119
|
+
function readTopicTag(entry: MemoryEntry): string | undefined {
|
|
120
|
+
try {
|
|
121
|
+
const parsed: unknown = JSON.parse(entry.metadata || "{}");
|
|
122
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
123
|
+
const tag = (parsed as Record<string, unknown>).topicTag;
|
|
124
|
+
return typeof tag === "string" && tag.length > 0 ? tag : undefined;
|
|
125
|
+
}
|
|
126
|
+
} catch { /* fallthrough */ }
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A2: 抽 entry 的工具名供 verifier tool-coverage 用。
|
|
132
|
+
* metadata.{caseMemory|workflowPattern}.tools 优先;fallback 解析正文 `Tools: a, b, c` 行。
|
|
133
|
+
*/
|
|
134
|
+
function extractToolsFromEntry(entry: MemoryEntry): string[] {
|
|
135
|
+
try {
|
|
136
|
+
const parsed: unknown = JSON.parse(entry.metadata || "{}");
|
|
137
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
138
|
+
const obj = parsed as Record<string, unknown>;
|
|
139
|
+
for (const key of ["caseMemory", "workflowPattern"]) {
|
|
140
|
+
const sub = obj[key];
|
|
141
|
+
if (sub !== null && typeof sub === "object") {
|
|
142
|
+
const tools = (sub as Record<string, unknown>).tools;
|
|
143
|
+
if (Array.isArray(tools)) {
|
|
144
|
+
return tools.filter((t): t is string => typeof t === "string" && t.length > 0);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch { /* fallthrough to text parse */ }
|
|
150
|
+
const m = entry.text.match(/(?:^|\n)Tools:\s*(.+)/);
|
|
151
|
+
if (m) return m[1].split(",").map(t => t.trim()).filter(Boolean);
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 按 topicTag 分桶(无 tag → "untagged" 桶),桶内按 recency 保留 maxBucketSize 条。
|
|
157
|
+
* 分桶把 greedyCluster 的 O(n²) 最坏复杂度限制在桶内,同时避免全库向量常驻。
|
|
158
|
+
*/
|
|
159
|
+
function bucketByTopic(
|
|
160
|
+
entries: MemoryEntry[],
|
|
161
|
+
maxBucketSize: number,
|
|
162
|
+
): { buckets: Map<string, MemoryEntry[]>; truncated: number } {
|
|
163
|
+
const buckets = new Map<string, MemoryEntry[]>();
|
|
164
|
+
for (const e of entries) {
|
|
165
|
+
const key = readTopicTag(e) ?? "untagged";
|
|
166
|
+
let bucket = buckets.get(key);
|
|
167
|
+
if (!bucket) {
|
|
168
|
+
bucket = [];
|
|
169
|
+
buckets.set(key, bucket);
|
|
170
|
+
}
|
|
171
|
+
bucket.push(e);
|
|
172
|
+
}
|
|
173
|
+
let truncated = 0;
|
|
174
|
+
for (const [key, bucket] of buckets) {
|
|
175
|
+
if (bucket.length > maxBucketSize) {
|
|
176
|
+
bucket.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
177
|
+
truncated += bucket.length - maxBucketSize;
|
|
178
|
+
buckets.set(key, bucket.slice(0, maxBucketSize));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { buckets, truncated };
|
|
182
|
+
}
|
|
183
|
+
|
|
61
184
|
/** Extract a short name from the first case's text (first line or first ~60 chars). */
|
|
62
185
|
function extractName(text: string): string {
|
|
63
186
|
const firstLine = text.split("\n")[0].trim();
|
|
@@ -95,12 +218,12 @@ function extractSteps(text: string): string {
|
|
|
95
218
|
// Greedy Clustering (same approach as consolidation-engine's C-2)
|
|
96
219
|
// ---------------------------------------------------------------------------
|
|
97
220
|
|
|
98
|
-
interface Cluster {
|
|
221
|
+
export interface Cluster {
|
|
99
222
|
seed: MemoryEntry;
|
|
100
223
|
members: MemoryEntry[];
|
|
101
224
|
}
|
|
102
225
|
|
|
103
|
-
function greedyCluster(
|
|
226
|
+
export function greedyCluster(
|
|
104
227
|
entries: MemoryEntry[],
|
|
105
228
|
threshold: number,
|
|
106
229
|
): Cluster[] {
|
|
@@ -145,6 +268,47 @@ function greedyCluster(
|
|
|
145
268
|
return clusters;
|
|
146
269
|
}
|
|
147
270
|
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Read evidence (Artel-style read-driven promotion)
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
/** Max confidence boost from read evidence. Saturates at 4 distinct readers. */
|
|
276
|
+
const READ_BOOST_MAX = 0.15;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Aggregate read signals (accessCount / readerIds / distinctReaderCount, written
|
|
280
|
+
* by AccessTracker on retrieval) across a candidate's source entries.
|
|
281
|
+
* distinctReaders = max(union of readerIds, per-entry saturated count) — the
|
|
282
|
+
* union undercounts once READER_ID_CAP saturates, the per-entry count covers that.
|
|
283
|
+
*/
|
|
284
|
+
export function aggregateReadEvidence(
|
|
285
|
+
entries: Array<{ metadata?: string }>,
|
|
286
|
+
): { totalAccess: number; distinctReaders: number } {
|
|
287
|
+
let totalAccess = 0;
|
|
288
|
+
const readers = new Set<string>();
|
|
289
|
+
let saturatedMax = 0;
|
|
290
|
+
for (const e of entries) {
|
|
291
|
+
try {
|
|
292
|
+
const meta = JSON.parse(e.metadata || "{}") as Record<string, unknown>;
|
|
293
|
+
if (typeof meta.accessCount === "number") totalAccess += meta.accessCount;
|
|
294
|
+
if (Array.isArray(meta.readerIds)) {
|
|
295
|
+
for (const r of meta.readerIds) if (typeof r === "string") readers.add(r);
|
|
296
|
+
}
|
|
297
|
+
if (typeof meta.distinctReaderCount === "number") {
|
|
298
|
+
saturatedMax = Math.max(saturatedMax, meta.distinctReaderCount);
|
|
299
|
+
}
|
|
300
|
+
} catch { /* broken metadata → contributes nothing */ }
|
|
301
|
+
}
|
|
302
|
+
return { totalAccess, distinctReaders: Math.max(readers.size, saturatedMax) };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Blend read evidence into base confidence: +0..READ_BOOST_MAX, capped at 0.99.
|
|
306
|
+
* 提权不设门槛(observe-before-enforce:先积累读证据,硬门槛等有生产数据再定)。 */
|
|
307
|
+
export function applyReadBoost(base: number, distinctReaders: number): number {
|
|
308
|
+
const boost = Math.min(1, distinctReaders / 4) * READ_BOOST_MAX;
|
|
309
|
+
return Math.min(0.99, base + boost);
|
|
310
|
+
}
|
|
311
|
+
|
|
148
312
|
// ---------------------------------------------------------------------------
|
|
149
313
|
// Main scan
|
|
150
314
|
// ---------------------------------------------------------------------------
|
|
@@ -165,78 +329,150 @@ export async function scanForPromotions(
|
|
|
165
329
|
): Promise<PromotionScanResult> {
|
|
166
330
|
const cfg: PromotionConfig = { ...DEFAULT_CONFIG, ...config };
|
|
167
331
|
|
|
168
|
-
// 1.
|
|
169
|
-
|
|
170
|
-
|
|
332
|
+
// 1. Paged full scan, category pushed down to the DB layer.
|
|
333
|
+
// 旧实现 store.list([scope], undefined, 500, 0) 双重失效:① limit=500 在
|
|
334
|
+
// 34K+ 库上覆盖率 ~1.4%;② list() 性能优化恒返回 vector:[],聚类全部跳过,
|
|
335
|
+
// 管线产出恒为零(单测 mock 带向量所以从未暴露)。
|
|
336
|
+
// cases/patterns 各自独立预算——共享预算时大库的 cases 会把 patterns 饿死
|
|
337
|
+
// (首次生产 dry-run 实测:20000 cases 拉满后 patterns scanned=0,
|
|
338
|
+
// pattern_to_skill 通道整个没跑)。
|
|
339
|
+
const rawCases = await listAllByCategory(store, scope, "cases", cfg.maxScanEntries, cfg.pageSize, 2_000);
|
|
340
|
+
const rawPatterns = await listAllByCategory(store, scope, "patterns", cfg.maxScanEntries, cfg.pageSize);
|
|
171
341
|
|
|
172
|
-
const cases =
|
|
173
|
-
const patterns =
|
|
342
|
+
const cases = rawCases.filter(isActive);
|
|
343
|
+
const patterns = rawPatterns.filter(isActive);
|
|
174
344
|
|
|
175
345
|
const result: PromotionScanResult = {
|
|
176
346
|
candidates: [],
|
|
177
347
|
scannedCases: cases.length,
|
|
178
348
|
scannedPatterns: patterns.length,
|
|
349
|
+
truncatedCases: 0,
|
|
350
|
+
vectorlessSkipped: 0,
|
|
179
351
|
};
|
|
180
352
|
|
|
181
|
-
// 2. Cluster cases by
|
|
353
|
+
// 2. Cluster cases bucket-by-bucket (topicTag, untagged fallback)。
|
|
354
|
+
// 向量按桶回填、桶毕即释,全库向量绝不常驻;分桶同时把 greedyCluster 的
|
|
355
|
+
// 最坏 O(n²) 限制在 maxBucketSize 内。
|
|
356
|
+
// 配额 per-channel:case 通道最多产 maxCandidates 个,pattern 通道同;
|
|
357
|
+
// 最后合并按 confidence 排序截断——否则 case 候选多的库会让
|
|
358
|
+
// pattern_to_skill 通道在全局 break 处永远轮空(首次生产 dry-run 实测)。
|
|
359
|
+
let caseCandidates = 0;
|
|
182
360
|
if (cases.length >= cfg.minCaseOccurrences) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
361
|
+
const { buckets, truncated } = bucketByTopic(cases, cfg.maxBucketSize);
|
|
362
|
+
result.truncatedCases = truncated;
|
|
363
|
+
|
|
364
|
+
for (const bucket of buckets.values()) {
|
|
365
|
+
if (bucket.length < cfg.minCaseOccurrences) continue;
|
|
366
|
+
if (caseCandidates >= cfg.maxCandidates) break;
|
|
367
|
+
|
|
368
|
+
const vectorMap = await store.getVectors(bucket.map(e => e.id));
|
|
369
|
+
const withVectors = bucket
|
|
370
|
+
.map(e => ({ ...e, vector: vectorMap.get(e.id) ?? [] }))
|
|
371
|
+
.filter(e => e.vector.length > 0);
|
|
372
|
+
result.vectorlessSkipped += bucket.length - withVectors.length;
|
|
373
|
+
if (withVectors.length < cfg.minCaseOccurrences) continue;
|
|
374
|
+
|
|
375
|
+
const clusters = greedyCluster(withVectors, cfg.caseSimilarityThreshold);
|
|
376
|
+
|
|
377
|
+
for (const cluster of clusters) {
|
|
378
|
+
if (cluster.members.length < cfg.minCaseOccurrences) continue;
|
|
379
|
+
if (caseCandidates >= cfg.maxCandidates) break;
|
|
380
|
+
|
|
381
|
+
// Compute average intra-cluster similarity for scoring
|
|
382
|
+
const avgSim = computeAverageIntraClusterSimilarity(cluster.members);
|
|
383
|
+
|
|
384
|
+
caseCandidates++;
|
|
385
|
+
const caseReadEvidence = aggregateReadEvidence(cluster.members);
|
|
386
|
+
result.candidates.push({
|
|
387
|
+
type: "case_to_pattern",
|
|
388
|
+
sourceEntries: cluster.members.map(m => ({
|
|
389
|
+
id: m.id,
|
|
390
|
+
text: m.text,
|
|
391
|
+
score: avgSim,
|
|
392
|
+
})),
|
|
393
|
+
suggestedName: extractName(cluster.seed.text),
|
|
394
|
+
suggestedDescription: summarizeCluster(cluster.members),
|
|
395
|
+
confidence: applyReadBoost(
|
|
396
|
+
cluster.members.length / (cluster.members.length + 2), // Bayesian smoothing
|
|
397
|
+
caseReadEvidence.distinctReaders,
|
|
398
|
+
),
|
|
399
|
+
readEvidence: caseReadEvidence,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
203
402
|
}
|
|
204
403
|
}
|
|
205
404
|
|
|
206
|
-
// 3. Detect pattern_to_skill candidates
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
405
|
+
// 3. Detect pattern_to_skill candidates — similarity search pushed down to the
|
|
406
|
+
// vector index (one vectorSearch per structured pattern) instead of pairwise
|
|
407
|
+
// cosine against the full in-memory case corpus.
|
|
408
|
+
// vectorSearch score = 1/(1+cosineDistance) = 1/(2-cosineSim) is monotonic in
|
|
409
|
+
// cosineSim, so minScore = 1/(2-threshold) filters EXACTLY the same set as
|
|
410
|
+
// `cosineSim >= threshold`; per-hit cosine 反算 = 2 - 1/score.
|
|
411
|
+
const structuredPatterns = patterns
|
|
412
|
+
.filter(p => hasStructuredSteps(p.text))
|
|
413
|
+
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
|
|
414
|
+
.slice(0, cfg.maxPatternProbes);
|
|
415
|
+
const patternVectors = structuredPatterns.length > 0
|
|
416
|
+
? await store.getVectors(structuredPatterns.map(p => p.id))
|
|
417
|
+
: new Map<string, number[]>();
|
|
418
|
+
const minSearchScore = 1 / (2 - cfg.caseSimilarityThreshold);
|
|
419
|
+
|
|
420
|
+
let patternCandidates = 0;
|
|
421
|
+
for (const pattern of structuredPatterns) {
|
|
422
|
+
if (patternCandidates >= cfg.maxCandidates) break;
|
|
423
|
+
const vec = patternVectors.get(pattern.id);
|
|
424
|
+
if (!vec || vec.length === 0) {
|
|
425
|
+
result.vectorlessSkipped++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const hits = await store.vectorSearch(vec, 20, minSearchScore, [scope]);
|
|
430
|
+
const similarCases = hits.filter(
|
|
431
|
+
h => h.entry.category === "cases" && h.entry.id !== pattern.id && isActive(h.entry),
|
|
432
|
+
);
|
|
218
433
|
|
|
219
434
|
if (similarCases.length < 2) continue;
|
|
220
435
|
|
|
436
|
+
// A2: zero-LLM verifier — pattern 当 draft,supporting cases 当 evidence。
|
|
437
|
+
// steps 传 [] 不是漏传:pattern.text 已含完整 Steps 块,resonance 用整段文本即可;
|
|
438
|
+
// extractSteps(pattern.text) 仅用于下面的 suggestedImplementation 字段。
|
|
439
|
+
const verification = verifyDraft(
|
|
440
|
+
{ tools: extractToolsFromEntry(pattern), summary: pattern.text, steps: [] },
|
|
441
|
+
similarCases.map(h => ({ text: h.entry.text, tools: extractToolsFromEntry(h.entry) })),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
patternCandidates++;
|
|
445
|
+
const patternReadEvidence = aggregateReadEvidence([pattern, ...similarCases.map(h => h.entry)]);
|
|
221
446
|
result.candidates.push({
|
|
222
447
|
type: "pattern_to_skill",
|
|
223
448
|
sourceEntries: [
|
|
224
449
|
{ id: pattern.id, text: pattern.text, score: 1.0 },
|
|
225
|
-
...similarCases.map(
|
|
226
|
-
id:
|
|
227
|
-
text:
|
|
228
|
-
score:
|
|
450
|
+
...similarCases.map(h => ({
|
|
451
|
+
id: h.entry.id,
|
|
452
|
+
text: h.entry.text,
|
|
453
|
+
score: 2 - 1 / h.score, // back-convert to cosine similarity
|
|
229
454
|
})),
|
|
230
455
|
],
|
|
231
456
|
suggestedName: extractName(pattern.text),
|
|
232
457
|
suggestedDescription: `Skill derived from pattern with ${similarCases.length} supporting cases`,
|
|
233
458
|
suggestedImplementation: extractSteps(pattern.text),
|
|
234
|
-
confidence:
|
|
459
|
+
confidence: applyReadBoost(
|
|
460
|
+
similarCases.length / (similarCases.length + 2),
|
|
461
|
+
patternReadEvidence.distinctReaders,
|
|
462
|
+
),
|
|
463
|
+
verification,
|
|
464
|
+
readEvidence: patternReadEvidence,
|
|
235
465
|
});
|
|
236
466
|
}
|
|
237
467
|
|
|
238
|
-
// Sort
|
|
239
|
-
|
|
468
|
+
// Sort: A2 verifier-passed first, then confidence desc, then truncate.
|
|
469
|
+
// 失败候选保留披露但排后;截断优先砍失败候选,通过的不被挤掉(Codex 验证补充)。
|
|
470
|
+
result.candidates.sort((a, b) => {
|
|
471
|
+
const aOk = a.verification?.ok ?? true;
|
|
472
|
+
const bOk = b.verification?.ok ?? true;
|
|
473
|
+
if (aOk !== bOk) return aOk ? -1 : 1;
|
|
474
|
+
return b.confidence - a.confidence;
|
|
475
|
+
});
|
|
240
476
|
result.candidates = result.candidates.slice(0, cfg.maxCandidates);
|
|
241
477
|
|
|
242
478
|
return result;
|
|
@@ -252,6 +488,14 @@ export function formatPromotionResult(result: PromotionScanResult): string {
|
|
|
252
488
|
`Promotion scan: ${result.scannedCases} cases, ${result.scannedPatterns} patterns scanned.`,
|
|
253
489
|
];
|
|
254
490
|
|
|
491
|
+
// No-silent-caps:截断与向量缺失必须可见,否则"0 候选"会被误读成"扫全了没东西"。
|
|
492
|
+
if (result.truncatedCases > 0) {
|
|
493
|
+
lines.push(`⚠️ ${result.truncatedCases} cases truncated by bucket cap (kept most recent per topic bucket).`);
|
|
494
|
+
}
|
|
495
|
+
if (result.vectorlessSkipped > 0) {
|
|
496
|
+
lines.push(`⚠️ ${result.vectorlessSkipped} entries skipped (no vector in store).`);
|
|
497
|
+
}
|
|
498
|
+
|
|
255
499
|
if (result.candidates.length === 0) {
|
|
256
500
|
lines.push("No promotion candidates found.");
|
|
257
501
|
return lines.join("\n");
|
|
@@ -264,6 +508,18 @@ export function formatPromotionResult(result: PromotionScanResult): string {
|
|
|
264
508
|
lines.push(`Confidence: ${(c.confidence * 100).toFixed(1)}%`);
|
|
265
509
|
lines.push(`Description: ${c.suggestedDescription}`);
|
|
266
510
|
lines.push(`Sources: ${c.sourceEntries.length} entries`);
|
|
511
|
+
if (c.readEvidence) {
|
|
512
|
+
lines.push(`Read evidence: ${c.readEvidence.totalAccess} total reads, ${c.readEvidence.distinctReaders} distinct reader(s)`);
|
|
513
|
+
}
|
|
514
|
+
if (c.verification) {
|
|
515
|
+
const v = c.verification;
|
|
516
|
+
if (v.ok) {
|
|
517
|
+
lines.push(`Verifier: ✓ pass (coverage ${(v.coverage * 100).toFixed(0)}%, resonance ${(v.resonance * 100).toFixed(0)}%)`);
|
|
518
|
+
} else {
|
|
519
|
+
const unmapped = v.unmappedTools.length > 0 ? `; unmapped tools: ${v.unmappedTools.join(", ")}` : "";
|
|
520
|
+
lines.push(`Verifier: ⚠️ FAILED — ${v.reason ?? "?"} (coverage ${(v.coverage * 100).toFixed(0)}%, resonance ${(v.resonance * 100).toFixed(0)}%${unmapped})`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
267
523
|
if (c.suggestedImplementation) {
|
|
268
524
|
lines.push(`Implementation:\n\`\`\`\n${c.suggestedImplementation}\n\`\`\``);
|
|
269
525
|
}
|
package/src/skill-schema.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D-1: Skill Schema — defines the shape of
|
|
2
|
+
* D-1: Skill Schema — defines the shape of agent-readable skill runbooks stored in memory.
|
|
3
3
|
*
|
|
4
4
|
* Skills are "what an agent can do" (vs. memories which are "what an agent learned").
|
|
5
5
|
* Alita/Alita-G showed MCP itself is a natural skill representation format.
|
|
6
|
+
*
|
|
7
|
+
* v2.5 (2026-05-27) — schema 收缩:
|
|
8
|
+
* 原 SkillImplementationTypeSchema 含 "bash" / "python" / "mcp_tool_chain" / "instruction_sequence"
|
|
9
|
+
* 四种,**implementation 字段从未真执行**(无 evaluator,仅作 context 给 agent 读)——是 schema
|
|
10
|
+
* 撒谎暗示可执行。源自 brgsk《agent memory: an anatomy》借鉴审计 + Codex trio 二审建议
|
|
11
|
+
* "P1 选收缩/删承诺,不接 evaluator"。
|
|
12
|
+
*
|
|
13
|
+
* 现在 enum 只保留 "instruction_sequence" 唯一类型,明确 skill 是 **agent-readable runbook**
|
|
14
|
+
* 而非可执行物。未来如真接 evaluator(产品决策另开),再展开 enum 加 "bash" / "python" 等。
|
|
15
|
+
*
|
|
16
|
+
* 兼容性:parseSkillFromEntry 读取老 metadata 用 type cast 不走 schema 校验,所以历史 bash/python
|
|
17
|
+
* skill records 仍可 retrieve;只是新写入受新 schema 约束。production 库 2026-05-27 实测无真实
|
|
18
|
+
* skill 数据,破坏面接近 zero。
|
|
6
19
|
*/
|
|
7
20
|
|
|
8
21
|
import { z } from "zod";
|
|
9
22
|
|
|
10
23
|
export const SkillImplementationTypeSchema = z.enum([
|
|
11
|
-
"
|
|
24
|
+
"instruction_sequence",
|
|
12
25
|
]);
|
|
13
26
|
|
|
14
27
|
export type SkillImplementationType = z.infer<typeof SkillImplementationTypeSchema>;
|
|
@@ -18,7 +31,7 @@ export const SkillInputSchema = z.object({
|
|
|
18
31
|
description: z.string().min(1).max(500).describe("Natural language description (used for retrieval)"),
|
|
19
32
|
triggerPattern: z.string().min(1).max(300).describe("When to suggest this skill"),
|
|
20
33
|
implementationType: SkillImplementationTypeSchema,
|
|
21
|
-
implementation: z.string().min(1).max(5000).describe("
|
|
34
|
+
implementation: z.string().min(1).max(5000).describe("Agent-readable runbook content: markdown steps, natural language workflow, or structured procedure. RecallNest does NOT execute this — it stores runbooks for agents to read and follow as context."),
|
|
22
35
|
inputSchema: z.record(z.string(), z.unknown()).optional().describe("Parameter definition (JSON Schema)"),
|
|
23
36
|
verification: z.string().max(500).optional().describe("How to verify execution success"),
|
|
24
37
|
scope: z.string().min(1).max(160).describe("Project scope"),
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2 · Skill promotion verifier — zero-LLM consistency gate.
|
|
3
|
+
*
|
|
4
|
+
* 借鉴 MemOS local-plugin `core/skill/verifier.ts`(repo-insight 借鉴审计 2026-06-13)。
|
|
5
|
+
* 用在 `scan_skill_promotions` 的 pattern_to_skill 候选生成处:pattern 当 draft,
|
|
6
|
+
* supporting cases 当 evidence。两个确定性检查,**无 LLM 调用**:
|
|
7
|
+
*
|
|
8
|
+
* 1. **tool coverage**:draft 声称的 tools 必须 ⊆ evidence 里真实出现过的 tools
|
|
9
|
+
* (覆盖率 ≥ COVERAGE_THRESHOLD)。专治 LLM 蒸馏 skill 时编造证据里没有的命令/工具名。
|
|
10
|
+
* 2. **evidence resonance**:≥ minResonance 比例的 evidence 与 draft 的 summary/steps
|
|
11
|
+
* 共享 ≥2 个 token。专治"叙述跟例子对不上"。
|
|
12
|
+
*
|
|
13
|
+
* `tokensOf` 对中文做 2-gram bigram(RecallNest case/skill 大量中文,不能只认 ASCII token)。
|
|
14
|
+
* verdict 只描述、不决策——调用方(skill-promotion)据此附 `verification` 字段披露,
|
|
15
|
+
* 失败候选保留排后、不静默丢弃(no-silent-caps)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface VerifyDraft {
|
|
19
|
+
/** draft 声称用到的工具/命令名 */
|
|
20
|
+
tools: string[];
|
|
21
|
+
/** draft 摘要文本(如 pattern 的 title + trigger / 正文) */
|
|
22
|
+
summary: string;
|
|
23
|
+
/** draft 步骤文本 */
|
|
24
|
+
steps: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface VerifyEvidence {
|
|
28
|
+
/** 证据条目正文(如 supporting case 的 text) */
|
|
29
|
+
text: string;
|
|
30
|
+
/** 证据条目里真实出现过的工具/命令名 */
|
|
31
|
+
tools: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface VerifyResult {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
/** draft.tools ∩ evidenceTools / draft.tools(无声称工具时为 1) */
|
|
37
|
+
coverage: number;
|
|
38
|
+
/** 与 draft 共享 ≥2 token 的 evidence 比例 */
|
|
39
|
+
resonance: number;
|
|
40
|
+
/** draft 声称但任何 evidence 都没出现过的工具(疑似幻觉) */
|
|
41
|
+
unmappedTools: string[];
|
|
42
|
+
/** 失败原因;ok=true 时省略 */
|
|
43
|
+
reason?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_MIN_RESONANCE = 0.5;
|
|
47
|
+
export const COVERAGE_THRESHOLD = 0.5;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 对一份 draft + 其证据做零 LLM 一致性校验。确定性、可单测、读写无副作用。
|
|
51
|
+
*/
|
|
52
|
+
export function verifyDraft(
|
|
53
|
+
draft: VerifyDraft,
|
|
54
|
+
evidence: VerifyEvidence[],
|
|
55
|
+
options: { minResonance?: number } = {},
|
|
56
|
+
): VerifyResult {
|
|
57
|
+
const minResonance = options.minResonance ?? DEFAULT_MIN_RESONANCE;
|
|
58
|
+
|
|
59
|
+
if (evidence.length === 0) {
|
|
60
|
+
return { ok: false, coverage: 0, resonance: 0, unmappedTools: [], reason: "no-evidence" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// --- tool coverage(集合包含检查)---
|
|
64
|
+
const evidenceTools = new Set<string>();
|
|
65
|
+
for (const ev of evidence) {
|
|
66
|
+
for (const t of ev.tools) {
|
|
67
|
+
const norm = t.trim().toLowerCase();
|
|
68
|
+
if (norm) evidenceTools.add(norm);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const draftTools = draft.tools.map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
72
|
+
const matched: string[] = [];
|
|
73
|
+
const unmapped: string[] = [];
|
|
74
|
+
for (const tok of draftTools) {
|
|
75
|
+
if (evidenceTools.has(tok)) matched.push(tok);
|
|
76
|
+
else unmapped.push(tok);
|
|
77
|
+
}
|
|
78
|
+
// 无声称工具 → 无可证伪,coverage 视为满分(只看 resonance)。
|
|
79
|
+
const coverage = draftTools.length === 0 ? 1 : matched.length / draftTools.length;
|
|
80
|
+
|
|
81
|
+
// --- evidence resonance ---
|
|
82
|
+
const resonance = computeResonance(draft, evidence);
|
|
83
|
+
|
|
84
|
+
if (coverage < COVERAGE_THRESHOLD && draftTools.length > 0) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
coverage,
|
|
88
|
+
resonance,
|
|
89
|
+
unmappedTools: unmapped,
|
|
90
|
+
reason: `coverage=${coverage.toFixed(2)}<${COVERAGE_THRESHOLD}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (resonance < minResonance) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
coverage,
|
|
97
|
+
resonance,
|
|
98
|
+
unmappedTools: unmapped,
|
|
99
|
+
reason: `resonance=${resonance.toFixed(2)}<${minResonance}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { ok: true, coverage, resonance, unmappedTools: unmapped };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function computeResonance(draft: VerifyDraft, evidence: VerifyEvidence[]): number {
|
|
106
|
+
const needle = [draft.summary, ...draft.steps].join(" ").toLowerCase();
|
|
107
|
+
const draftTokens = tokensOf(needle);
|
|
108
|
+
if (draftTokens.size === 0) return 0;
|
|
109
|
+
let hit = 0;
|
|
110
|
+
for (const ev of evidence) {
|
|
111
|
+
const toks = tokensOf(ev.text.toLowerCase());
|
|
112
|
+
let overlap = 0;
|
|
113
|
+
for (const tok of draftTokens) {
|
|
114
|
+
if (toks.has(tok)) {
|
|
115
|
+
overlap += 1;
|
|
116
|
+
if (overlap >= 2) break; // 只需判定 ≥2,命中即可停
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (overlap >= 2) hit += 1;
|
|
120
|
+
}
|
|
121
|
+
return hit / evidence.length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 分词:ASCII 标识符(≥4 字符、去停用词)+ CJK 2-gram bigram。
|
|
126
|
+
* CJK 部分不走停用词(2-gram 本身已足够稀疏)。
|
|
127
|
+
*/
|
|
128
|
+
export function tokensOf(s: string): Set<string> {
|
|
129
|
+
const out = new Set<string>();
|
|
130
|
+
const asciiMatches = s.match(/[a-z0-9_][a-z0-9_./-]{3,}/g) ?? [];
|
|
131
|
+
for (const m of asciiMatches) {
|
|
132
|
+
const tok = m.toLowerCase();
|
|
133
|
+
if (RESONANCE_STOPWORDS.has(tok)) continue;
|
|
134
|
+
out.add(tok);
|
|
135
|
+
}
|
|
136
|
+
const cjkRuns = s.match(/[一-鿿-ヿ㐀-䶿]{2,}/g) ?? [];
|
|
137
|
+
for (const run of cjkRuns) {
|
|
138
|
+
for (let i = 0; i + 1 < run.length; i++) {
|
|
139
|
+
out.add(run.slice(i, i + 2));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const RESONANCE_STOPWORDS = new Set<string>([
|
|
146
|
+
"the", "and", "for", "with", "that", "this", "from", "will", "then",
|
|
147
|
+
"into", "when", "what", "where", "your", "user", "agent", "null", "true",
|
|
148
|
+
"false", "none", "let", "new", "old", "use", "used", "have", "has", "its",
|
|
149
|
+
"not", "any", "can", "does", "only", "just", "like", "please", "step",
|
|
150
|
+
"steps", "body", "title", "summary", "task", "tasks", "run", "see", "end",
|
|
151
|
+
"our", "their", "them", "being", "make", "made", "thing", "things",
|
|
152
|
+
]);
|