recallnest 1.4.1 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +4 -0
- package/CLAUDE.md +7 -3
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/bin/recallnest.mjs +62 -0
- package/config.json +55 -0
- package/daily-recall.sh +23 -0
- package/integrations/setup-all.sh +31 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +30 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +10 -3
- package/src/access-tracker.ts +3 -2
- package/src/activity-counter.ts +109 -0
- package/src/adaptive-retrieval.ts +6 -5
- package/src/admission-control.ts +163 -0
- package/src/anchor-generator.ts +80 -0
- package/src/api-server.ts +10 -0
- package/src/audit-log.ts +122 -0
- package/src/auto-gc.ts +98 -29
- package/src/auto-recall.ts +33 -4
- package/src/capture-engine.ts +372 -23
- package/src/cascade-forget.ts +3 -2
- package/src/cli.ts +107 -5
- package/src/compat.ts +22 -0
- package/src/confidence-tracker.ts +210 -47
- package/src/conflict-store.ts +12 -3
- package/src/consolidation-engine.ts +296 -8
- package/src/context-collapse-renderer.ts +222 -0
- package/src/context-composer-stable-selection.ts +5 -1
- package/src/context-composer.ts +239 -4
- package/src/context-reconstructor.ts +404 -0
- package/src/conversation-importer.ts +441 -0
- package/src/data-checkup.ts +232 -0
- package/src/decay-engine.ts +84 -0
- package/src/distill-lock.ts +122 -0
- package/src/doctor.ts +8 -10
- package/src/dream-pipeline.ts +227 -0
- package/src/emotion-detector.ts +90 -0
- package/src/eval.ts +3 -2
- package/src/forget-engine.ts +269 -0
- package/src/frequency-tracker.ts +175 -0
- package/src/graph-export.ts +644 -0
- package/src/ingest.ts +36 -6
- package/src/interference-detector.ts +187 -0
- package/src/kg-extractor.ts +15 -24
- package/src/kg-store.ts +2 -1
- package/src/language-hook.ts +67 -0
- package/src/llm-client.ts +289 -6
- package/src/mcp-server.ts +707 -80
- package/src/memory-assets.ts +22 -5
- package/src/memory-evolution.ts +365 -0
- package/src/memory-lint.ts +429 -0
- package/src/memory-output.ts +69 -0
- package/src/memory-schema.ts +74 -2
- package/src/narrative-schema.ts +79 -0
- package/src/narrative-tagger.ts +164 -0
- package/src/pii-detector.ts +98 -0
- package/src/post-retrieval-filter.ts +76 -0
- package/src/prediction-engine.ts +253 -0
- package/src/preference-slots.ts +65 -2
- package/src/prospective-memory.ts +215 -0
- package/src/query-expander.ts +67 -0
- package/src/recall-governor.ts +183 -0
- package/src/retention-policy.ts +90 -0
- package/src/retriever.ts +828 -55
- package/src/rif.ts +34 -8
- package/src/runtime-config.ts +23 -8
- package/src/scope-inventory.ts +4 -3
- package/src/scope-policy.ts +4 -0
- package/src/session-distiller.ts +307 -0
- package/src/session-output.ts +40 -0
- package/src/session-schema.ts +37 -0
- package/src/session-store.ts +7 -2
- package/src/skill-engine.ts +208 -0
- package/src/skill-promotion.ts +293 -0
- package/src/skill-schema.ts +39 -0
- package/src/store.ts +96 -10
- package/src/topic-tag.ts +66 -0
- package/src/tracker.ts +3 -1
- package/src/ui-server.ts +80 -8
- package/src/workflow-observation-scope-review.ts +2 -1
- package/src/workflow-observation-store.ts +2 -1
- package/src/write-verifier.ts +126 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -198
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +0 -807
- package/assets/ui/index.html +0 -154
- package/assets/ui/styles.css +0 -605
- package/assets/wechat-qr.jpg +0 -0
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/lm +0 -19
- package/plan-v1.2-distributable.md +0 -105
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1142
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -33
- package/src/__tests__/confidence-tracker.test.ts +0 -196
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -106
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4743
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D-1: Skill Memory — store and retrieve executable skills.
|
|
3
|
+
*
|
|
4
|
+
* Skills are stored as "patterns" category entries with structured metadata,
|
|
5
|
+
* using the same LanceDB vector search infrastructure as regular memories.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Embedder } from "./embedder.js";
|
|
9
|
+
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
10
|
+
import { deterministicId } from "./store.js";
|
|
11
|
+
import { SkillInputSchema, type SkillInput, type StoredSkillRecord } from "./skill-schema.js";
|
|
12
|
+
import { defaultEvolution } from "./memory-evolution.js";
|
|
13
|
+
import { generateAnchor } from "./anchor-generator.js";
|
|
14
|
+
import { buildStructuredMemoryBoundary } from "./memory-boundaries.js";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
type StoreDeps = Pick<MemoryStore, "store" | "update" | "getById">;
|
|
21
|
+
type EmbedDeps = Pick<Embedder, "embedPassage">;
|
|
22
|
+
type SearchDeps = Pick<MemoryStore, "vectorSearch">;
|
|
23
|
+
|
|
24
|
+
const SKILL_IMPORTANCE = 0.85;
|
|
25
|
+
const SKILL_CATEGORY = "patterns" as const;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function buildEmbeddingText(input: SkillInput): string {
|
|
32
|
+
return `${input.description} ${input.triggerPattern}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildFullText(input: SkillInput): string {
|
|
36
|
+
return [
|
|
37
|
+
`Skill: ${input.name}`,
|
|
38
|
+
`Description: ${input.description}`,
|
|
39
|
+
`Trigger: ${input.triggerPattern}`,
|
|
40
|
+
`Type: ${input.implementationType}`,
|
|
41
|
+
`Implementation: ${input.implementation}`,
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildSkillMetadata(input: SkillInput, anchor: string | undefined): string {
|
|
46
|
+
return JSON.stringify({
|
|
47
|
+
source: input.source,
|
|
48
|
+
tags: input.tags,
|
|
49
|
+
capture: "skill_schema_v1",
|
|
50
|
+
boundary: buildStructuredMemoryBoundary("patterns"),
|
|
51
|
+
canonicalKey: `patterns:skill:${input.name.toLowerCase().replace(/\s+/g, "-")}`,
|
|
52
|
+
evolution: defaultEvolution(),
|
|
53
|
+
...(anchor ? { anchor } : {}),
|
|
54
|
+
skill: {
|
|
55
|
+
name: input.name,
|
|
56
|
+
description: input.description,
|
|
57
|
+
triggerPattern: input.triggerPattern,
|
|
58
|
+
implementationType: input.implementationType,
|
|
59
|
+
implementation: input.implementation,
|
|
60
|
+
...(input.inputSchema ? { inputSchema: input.inputSchema } : {}),
|
|
61
|
+
...(input.verification ? { verification: input.verification } : {}),
|
|
62
|
+
successCount: 0,
|
|
63
|
+
failureCount: 0,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseSkillFromEntry(entry: MemoryEntry): StoredSkillRecord | null {
|
|
69
|
+
let meta: Record<string, unknown>;
|
|
70
|
+
try {
|
|
71
|
+
meta = JSON.parse(entry.metadata || "{}");
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const skill = meta.skill;
|
|
77
|
+
if (!skill || typeof skill !== "object") return null;
|
|
78
|
+
|
|
79
|
+
const s = skill as Record<string, unknown>;
|
|
80
|
+
if (typeof s.name !== "string" || typeof s.implementation !== "string") return null;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
name: s.name as string,
|
|
84
|
+
description: (s.description as string) || "",
|
|
85
|
+
triggerPattern: (s.triggerPattern as string) || "",
|
|
86
|
+
implementationType: (s.implementationType as string) as StoredSkillRecord["implementationType"],
|
|
87
|
+
implementation: s.implementation as string,
|
|
88
|
+
inputSchema: s.inputSchema as Record<string, unknown> | undefined,
|
|
89
|
+
verification: (s.verification as string) || undefined,
|
|
90
|
+
scope: entry.scope,
|
|
91
|
+
source: (meta.source as StoredSkillRecord["source"]) || "manual",
|
|
92
|
+
tags: Array.isArray(meta.tags) ? (meta.tags as string[]) : [],
|
|
93
|
+
id: entry.id,
|
|
94
|
+
storedAt: new Date(entry.timestamp).toISOString(),
|
|
95
|
+
successCount: typeof s.successCount === "number" ? s.successCount : 0,
|
|
96
|
+
failureCount: typeof s.failureCount === "number" ? s.failureCount : 0,
|
|
97
|
+
lastRefinedAt: typeof s.lastRefinedAt === "string" ? s.lastRefinedAt : undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Public API
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
export async function persistSkill(
|
|
106
|
+
store: StoreDeps,
|
|
107
|
+
embedder: EmbedDeps,
|
|
108
|
+
rawInput: unknown,
|
|
109
|
+
): Promise<StoredSkillRecord> {
|
|
110
|
+
const input = SkillInputSchema.parse(rawInput);
|
|
111
|
+
|
|
112
|
+
const embeddingText = buildEmbeddingText(input);
|
|
113
|
+
const vector = await embedder.embedPassage(embeddingText);
|
|
114
|
+
const fullText = buildFullText(input);
|
|
115
|
+
const id = deterministicId(input.scope, input.name);
|
|
116
|
+
const anchor = generateAnchor(fullText);
|
|
117
|
+
const metadata = buildSkillMetadata(input, anchor);
|
|
118
|
+
|
|
119
|
+
// Check for existing skill with same scope+name (idempotent upsert)
|
|
120
|
+
const existing = await store.getById(id);
|
|
121
|
+
|
|
122
|
+
if (existing) {
|
|
123
|
+
// Update existing skill — bump version in evolution metadata
|
|
124
|
+
let existingMeta: Record<string, unknown> = {};
|
|
125
|
+
try {
|
|
126
|
+
existingMeta = JSON.parse(existing.metadata || "{}");
|
|
127
|
+
} catch { /* use fresh metadata */ }
|
|
128
|
+
|
|
129
|
+
const existingEvolution = existingMeta.evolution as Record<string, unknown> | undefined;
|
|
130
|
+
const currentVersion = typeof existingEvolution?.version === "number" ? existingEvolution.version : 1;
|
|
131
|
+
|
|
132
|
+
const updatedMetadata = JSON.stringify({
|
|
133
|
+
...JSON.parse(metadata),
|
|
134
|
+
evolution: {
|
|
135
|
+
...defaultEvolution(),
|
|
136
|
+
version: currentVersion + 1,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const updated = await store.update(id, {
|
|
141
|
+
text: fullText,
|
|
142
|
+
vector,
|
|
143
|
+
metadata: updatedMetadata,
|
|
144
|
+
timestamp: Date.now(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!updated) {
|
|
148
|
+
throw new Error(`Failed to update skill "${input.name}" (id: ${id})`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...input,
|
|
153
|
+
id: updated.id,
|
|
154
|
+
storedAt: new Date(updated.timestamp).toISOString(),
|
|
155
|
+
successCount: 0,
|
|
156
|
+
failureCount: 0,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Store new skill
|
|
161
|
+
const stored = await store.store({
|
|
162
|
+
id,
|
|
163
|
+
text: fullText,
|
|
164
|
+
vector,
|
|
165
|
+
category: SKILL_CATEGORY,
|
|
166
|
+
scope: input.scope,
|
|
167
|
+
importance: SKILL_IMPORTANCE,
|
|
168
|
+
metadata,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...input,
|
|
173
|
+
id: stored.id,
|
|
174
|
+
storedAt: new Date(stored.timestamp).toISOString(),
|
|
175
|
+
successCount: 0,
|
|
176
|
+
failureCount: 0,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function retrieveSkills(
|
|
181
|
+
store: SearchDeps,
|
|
182
|
+
embedder: EmbedDeps,
|
|
183
|
+
query: string,
|
|
184
|
+
scope?: string,
|
|
185
|
+
limit?: number,
|
|
186
|
+
): Promise<Array<{ skill: StoredSkillRecord; score: number }>> {
|
|
187
|
+
const vector = await embedder.embedPassage(query);
|
|
188
|
+
const safeLimit = Math.min(Math.max(limit ?? 3, 1), 10);
|
|
189
|
+
|
|
190
|
+
// Over-fetch to account for filtering
|
|
191
|
+
const fetchLimit = safeLimit * 3;
|
|
192
|
+
const scopeFilter = scope ? [scope] : undefined;
|
|
193
|
+
const results = await store.vectorSearch(vector, fetchLimit, 0.3, scopeFilter);
|
|
194
|
+
|
|
195
|
+
const matched: Array<{ skill: StoredSkillRecord; score: number }> = [];
|
|
196
|
+
|
|
197
|
+
for (const result of results) {
|
|
198
|
+
if (result.entry.category !== SKILL_CATEGORY) continue;
|
|
199
|
+
|
|
200
|
+
const skill = parseSkillFromEntry(result.entry);
|
|
201
|
+
if (!skill) continue;
|
|
202
|
+
|
|
203
|
+
matched.push({ skill, score: result.score });
|
|
204
|
+
if (matched.length >= safeLimit) break;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return matched;
|
|
208
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D-2: Case -> Strategy -> Skill Promotion Pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Automatically detects promotion opportunities:
|
|
5
|
+
* - Same scope has N+ similar cases (store_case called repeatedly) -> suggest workflow_pattern
|
|
6
|
+
* - workflow_pattern with structured steps that correlates with multiple cases -> suggest skill
|
|
7
|
+
*
|
|
8
|
+
* Promotion suggestions are returned to the agent for review — never auto-executed,
|
|
9
|
+
* to avoid low-quality skills entering the store.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { MemoryEntry, MemoryStore } from "./store.js";
|
|
13
|
+
import { cosineSimilarity } from "./multi-vector.js";
|
|
14
|
+
import { isActiveMemory } from "./memory-evolution.js";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface PromotionCandidate {
|
|
21
|
+
type: "case_to_pattern" | "pattern_to_skill";
|
|
22
|
+
sourceEntries: Array<{ id: string; text: string; score: number }>;
|
|
23
|
+
suggestedName: string;
|
|
24
|
+
suggestedDescription: string;
|
|
25
|
+
/** For pattern_to_skill: extracted implementation steps */
|
|
26
|
+
suggestedImplementation?: string;
|
|
27
|
+
confidence: number; // 0-1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PromotionScanResult {
|
|
31
|
+
candidates: PromotionCandidate[];
|
|
32
|
+
scannedCases: number;
|
|
33
|
+
scannedPatterns: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PromotionConfig {
|
|
37
|
+
/** Minimum similar cases to suggest pattern promotion (default: 3) */
|
|
38
|
+
minCaseOccurrences: number;
|
|
39
|
+
/** Similarity threshold for case clustering (default: 0.75) */
|
|
40
|
+
caseSimilarityThreshold: number;
|
|
41
|
+
/** Max candidates to return (default: 5) */
|
|
42
|
+
maxCandidates: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_CONFIG: PromotionConfig = {
|
|
46
|
+
minCaseOccurrences: 3,
|
|
47
|
+
caseSimilarityThreshold: 0.75,
|
|
48
|
+
maxCandidates: 5,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Helpers
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
type PromotionStore = Pick<MemoryStore, "list" | "vectorSearch">;
|
|
56
|
+
|
|
57
|
+
function isActive(entry: MemoryEntry): boolean {
|
|
58
|
+
return isActiveMemory(entry.metadata);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Extract a short name from the first case's text (first line or first ~60 chars). */
|
|
62
|
+
function extractName(text: string): string {
|
|
63
|
+
const firstLine = text.split("\n")[0].trim();
|
|
64
|
+
return firstLine.length > 60 ? firstLine.slice(0, 57) + "..." : firstLine;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Summarize a cluster into a description from member texts. */
|
|
68
|
+
function summarizeCluster(members: MemoryEntry[]): string {
|
|
69
|
+
const snippets = members.slice(0, 3).map(m => {
|
|
70
|
+
const first = m.text.split("\n")[0].trim();
|
|
71
|
+
return first.length > 80 ? first.slice(0, 77) + "..." : first;
|
|
72
|
+
});
|
|
73
|
+
return `Recurring pattern across ${members.length} cases: ${snippets.join("; ")}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Check if text contains structured steps (numbered list or "Steps:" header). */
|
|
77
|
+
function hasStructuredSteps(text: string): boolean {
|
|
78
|
+
return /(?:^|\n)\s*(?:Steps?:|##?\s*Steps?)/i.test(text)
|
|
79
|
+
|| /(?:^|\n)\s*[1-9]\.\s+\S/.test(text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Extract the steps section from a pattern's text. */
|
|
83
|
+
function extractSteps(text: string): string {
|
|
84
|
+
// Try to find content after "Steps:" header
|
|
85
|
+
const stepsMatch = text.match(/(?:^|\n)\s*(?:Steps?:|##?\s*Steps?)\s*\n([\s\S]+)/i);
|
|
86
|
+
if (stepsMatch) return stepsMatch[1].trim();
|
|
87
|
+
|
|
88
|
+
// Fall back: extract all numbered list items
|
|
89
|
+
const lines = text.split("\n");
|
|
90
|
+
const numbered = lines.filter(l => /^\s*[1-9]\d*\.\s+\S/.test(l));
|
|
91
|
+
return numbered.length > 0 ? numbered.join("\n") : text;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Greedy Clustering (same approach as consolidation-engine's C-2)
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
interface Cluster {
|
|
99
|
+
seed: MemoryEntry;
|
|
100
|
+
members: MemoryEntry[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function greedyCluster(
|
|
104
|
+
entries: MemoryEntry[],
|
|
105
|
+
threshold: number,
|
|
106
|
+
): Cluster[] {
|
|
107
|
+
const clusters: Cluster[] = [];
|
|
108
|
+
const centroids: number[][] = [];
|
|
109
|
+
const assigned = new Set<string>();
|
|
110
|
+
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (assigned.has(entry.id) || !entry.vector?.length) continue;
|
|
113
|
+
|
|
114
|
+
let bestIdx = -1;
|
|
115
|
+
let bestSim = -1;
|
|
116
|
+
|
|
117
|
+
for (let ci = 0; ci < centroids.length; ci++) {
|
|
118
|
+
const sim = cosineSimilarity(entry.vector, centroids[ci]);
|
|
119
|
+
if (sim > threshold && sim > bestSim) {
|
|
120
|
+
bestSim = sim;
|
|
121
|
+
bestIdx = ci;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (bestIdx >= 0) {
|
|
126
|
+
clusters[bestIdx].members.push(entry);
|
|
127
|
+
assigned.add(entry.id);
|
|
128
|
+
// Update centroid as running average
|
|
129
|
+
const members = clusters[bestIdx].members;
|
|
130
|
+
const dim = centroids[bestIdx].length;
|
|
131
|
+
const newCentroid = new Array<number>(dim);
|
|
132
|
+
for (let d = 0; d < dim; d++) {
|
|
133
|
+
let sum = 0;
|
|
134
|
+
for (const m of members) sum += m.vector[d];
|
|
135
|
+
newCentroid[d] = sum / members.length;
|
|
136
|
+
}
|
|
137
|
+
centroids[bestIdx] = newCentroid;
|
|
138
|
+
} else {
|
|
139
|
+
clusters.push({ seed: entry, members: [entry] });
|
|
140
|
+
centroids.push([...entry.vector]);
|
|
141
|
+
assigned.add(entry.id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return clusters;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Main scan
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Scan for promotion candidates in a given scope.
|
|
154
|
+
*
|
|
155
|
+
* Algorithm:
|
|
156
|
+
* 1. Load all active entries in scope, split into cases and patterns
|
|
157
|
+
* 2. Cluster cases by embedding similarity (greedy clustering)
|
|
158
|
+
* 3. Clusters with >= minCaseOccurrences members -> case_to_pattern candidate
|
|
159
|
+
* 4. Patterns with structured steps that are similar to >= 2 cases -> pattern_to_skill candidate
|
|
160
|
+
*/
|
|
161
|
+
export async function scanForPromotions(
|
|
162
|
+
store: PromotionStore,
|
|
163
|
+
scope: string,
|
|
164
|
+
config?: Partial<PromotionConfig>,
|
|
165
|
+
): Promise<PromotionScanResult> {
|
|
166
|
+
const cfg: PromotionConfig = { ...DEFAULT_CONFIG, ...config };
|
|
167
|
+
|
|
168
|
+
// 1. Load all entries in scope (high limit to get everything meaningful)
|
|
169
|
+
const entries = await store.list([scope], undefined, 500, 0);
|
|
170
|
+
const active = entries.filter(isActive);
|
|
171
|
+
|
|
172
|
+
const cases = active.filter(e => e.category === "cases");
|
|
173
|
+
const patterns = active.filter(e => e.category === "patterns");
|
|
174
|
+
|
|
175
|
+
const result: PromotionScanResult = {
|
|
176
|
+
candidates: [],
|
|
177
|
+
scannedCases: cases.length,
|
|
178
|
+
scannedPatterns: patterns.length,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// 2. Cluster cases by vector similarity
|
|
182
|
+
if (cases.length >= cfg.minCaseOccurrences) {
|
|
183
|
+
const clusters = greedyCluster(cases, cfg.caseSimilarityThreshold);
|
|
184
|
+
|
|
185
|
+
for (const cluster of clusters) {
|
|
186
|
+
if (cluster.members.length < cfg.minCaseOccurrences) continue;
|
|
187
|
+
if (result.candidates.length >= cfg.maxCandidates) break;
|
|
188
|
+
|
|
189
|
+
// Compute average intra-cluster similarity for scoring
|
|
190
|
+
const avgSim = computeAverageIntraClusterSimilarity(cluster.members);
|
|
191
|
+
|
|
192
|
+
result.candidates.push({
|
|
193
|
+
type: "case_to_pattern",
|
|
194
|
+
sourceEntries: cluster.members.map(m => ({
|
|
195
|
+
id: m.id,
|
|
196
|
+
text: m.text,
|
|
197
|
+
score: avgSim,
|
|
198
|
+
})),
|
|
199
|
+
suggestedName: extractName(cluster.seed.text),
|
|
200
|
+
suggestedDescription: summarizeCluster(cluster.members),
|
|
201
|
+
confidence: cluster.members.length / (cluster.members.length + 2), // Bayesian smoothing
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 3. Detect pattern_to_skill candidates
|
|
207
|
+
for (const pattern of patterns) {
|
|
208
|
+
if (result.candidates.length >= cfg.maxCandidates) break;
|
|
209
|
+
if (!hasStructuredSteps(pattern.text)) continue;
|
|
210
|
+
if (!pattern.vector?.length) continue;
|
|
211
|
+
|
|
212
|
+
// Find cases similar to this pattern
|
|
213
|
+
const similarCases = cases.filter(c => {
|
|
214
|
+
if (!c.vector?.length) return false;
|
|
215
|
+
const sim = cosineSimilarity(pattern.vector, c.vector);
|
|
216
|
+
return sim >= cfg.caseSimilarityThreshold;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (similarCases.length < 2) continue;
|
|
220
|
+
|
|
221
|
+
result.candidates.push({
|
|
222
|
+
type: "pattern_to_skill",
|
|
223
|
+
sourceEntries: [
|
|
224
|
+
{ id: pattern.id, text: pattern.text, score: 1.0 },
|
|
225
|
+
...similarCases.map(c => ({
|
|
226
|
+
id: c.id,
|
|
227
|
+
text: c.text,
|
|
228
|
+
score: cosineSimilarity(pattern.vector, c.vector),
|
|
229
|
+
})),
|
|
230
|
+
],
|
|
231
|
+
suggestedName: extractName(pattern.text),
|
|
232
|
+
suggestedDescription: `Skill derived from pattern with ${similarCases.length} supporting cases`,
|
|
233
|
+
suggestedImplementation: extractSteps(pattern.text),
|
|
234
|
+
confidence: similarCases.length / (similarCases.length + 2),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Sort by confidence descending, then truncate
|
|
239
|
+
result.candidates.sort((a, b) => b.confidence - a.confidence);
|
|
240
|
+
result.candidates = result.candidates.slice(0, cfg.maxCandidates);
|
|
241
|
+
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Formatting
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
/** Format scan results for MCP tool output. */
|
|
250
|
+
export function formatPromotionResult(result: PromotionScanResult): string {
|
|
251
|
+
const lines = [
|
|
252
|
+
`Promotion scan: ${result.scannedCases} cases, ${result.scannedPatterns} patterns scanned.`,
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
if (result.candidates.length === 0) {
|
|
256
|
+
lines.push("No promotion candidates found.");
|
|
257
|
+
return lines.join("\n");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
lines.push(`Found ${result.candidates.length} candidate(s):\n`);
|
|
261
|
+
|
|
262
|
+
for (const [i, c] of result.candidates.entries()) {
|
|
263
|
+
lines.push(`### ${i + 1}. [${c.type}] ${c.suggestedName}`);
|
|
264
|
+
lines.push(`Confidence: ${(c.confidence * 100).toFixed(1)}%`);
|
|
265
|
+
lines.push(`Description: ${c.suggestedDescription}`);
|
|
266
|
+
lines.push(`Sources: ${c.sourceEntries.length} entries`);
|
|
267
|
+
if (c.suggestedImplementation) {
|
|
268
|
+
lines.push(`Implementation:\n\`\`\`\n${c.suggestedImplementation}\n\`\`\``);
|
|
269
|
+
}
|
|
270
|
+
lines.push("");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return lines.join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Internal helpers
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
function computeAverageIntraClusterSimilarity(members: MemoryEntry[]): number {
|
|
281
|
+
if (members.length < 2) return 1.0;
|
|
282
|
+
let total = 0;
|
|
283
|
+
let pairs = 0;
|
|
284
|
+
for (let i = 0; i < members.length; i++) {
|
|
285
|
+
for (let j = i + 1; j < members.length; j++) {
|
|
286
|
+
if (members[i].vector?.length && members[j].vector?.length) {
|
|
287
|
+
total += cosineSimilarity(members[i].vector, members[j].vector);
|
|
288
|
+
pairs++;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return pairs > 0 ? total / pairs : 0;
|
|
293
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D-1: Skill Schema — defines the shape of executable skills stored in memory.
|
|
3
|
+
*
|
|
4
|
+
* Skills are "what an agent can do" (vs. memories which are "what an agent learned").
|
|
5
|
+
* Alita/Alita-G showed MCP itself is a natural skill representation format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
export const SkillImplementationTypeSchema = z.enum([
|
|
11
|
+
"bash", "python", "mcp_tool_chain", "instruction_sequence",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export type SkillImplementationType = z.infer<typeof SkillImplementationTypeSchema>;
|
|
15
|
+
|
|
16
|
+
export const SkillInputSchema = z.object({
|
|
17
|
+
name: z.string().min(1).max(120).describe("Unique skill identifier (e.g. 'deploy_production')"),
|
|
18
|
+
description: z.string().min(1).max(500).describe("Natural language description (used for retrieval)"),
|
|
19
|
+
triggerPattern: z.string().min(1).max(300).describe("When to suggest this skill"),
|
|
20
|
+
implementationType: SkillImplementationTypeSchema,
|
|
21
|
+
implementation: z.string().min(1).max(5000).describe("Executable content"),
|
|
22
|
+
inputSchema: z.record(z.string(), z.unknown()).optional().describe("Parameter definition (JSON Schema)"),
|
|
23
|
+
verification: z.string().max(500).optional().describe("How to verify execution success"),
|
|
24
|
+
scope: z.string().min(1).max(160).describe("Project scope"),
|
|
25
|
+
source: z.enum(["manual", "agent", "api"]).default("manual"),
|
|
26
|
+
tags: z.array(z.string().max(60)).max(6).default([]),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type SkillInput = z.infer<typeof SkillInputSchema>;
|
|
30
|
+
|
|
31
|
+
export const StoredSkillRecordSchema = SkillInputSchema.extend({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
storedAt: z.string().datetime(),
|
|
34
|
+
successCount: z.number().default(0),
|
|
35
|
+
failureCount: z.number().default(0),
|
|
36
|
+
lastRefinedAt: z.string().datetime().optional(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type StoredSkillRecord = z.infer<typeof StoredSkillRecordSchema>;
|