recallnest 2.2.1 → 2.5.4
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 +150 -0
- package/README.md +46 -35
- package/README_CN.md +41 -26
- 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/connectors/examples/email-adapter.ts +73 -0
- package/connectors/examples/logs-adapter.ts +103 -0
- package/connectors/examples/rss-adapter.ts +95 -0
- 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 +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -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__/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__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -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 +33 -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 +782 -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 +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -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 +216 -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 +84 -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 +321 -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__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -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-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -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 +250 -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__/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 +314 -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 +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -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-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.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-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -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 +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -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.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__/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 +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +199 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/connector-types.ts +57 -0
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/conversation-importer.ts +25 -1
- package/src/data-checkup.ts +126 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +38 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +234 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/obsidian-connector.ts +282 -0
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- 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 +3 -2
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/source-heartbeat.ts +108 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- 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 -30
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
package/src/mcp-server.ts
CHANGED
|
@@ -7,14 +7,32 @@
|
|
|
7
7
|
* can search your indexed conversations.
|
|
8
8
|
*
|
|
9
9
|
* Tool tiers:
|
|
10
|
-
* - core: Always exposed (
|
|
11
|
-
* - advanced: Exposed by default, includes core (
|
|
12
|
-
* - full: All tools including governance (
|
|
10
|
+
* - core: Always exposed (7 tools)
|
|
11
|
+
* - advanced: Exposed by default, includes core (30 tools)
|
|
12
|
+
* - full: All tools including governance (43 tools)
|
|
13
13
|
*
|
|
14
14
|
* Control: RECALLNEST_MCP_TIER=core|advanced|full
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import * as envConfig from "./env-config.js";
|
|
20
|
+
import { autoRegisterBabelMemory } from "./language-hook.js";
|
|
21
|
+
import { createComponentResolver, loadConfig, loadDotEnv } from "./runtime-config.js";
|
|
22
|
+
import { SessionCheckpointStore } from "./session-store.js";
|
|
23
|
+
import { ConflictCandidateStore } from "./conflict-store.js";
|
|
24
|
+
import { WorkflowObservationStore } from "./workflow-observation-store.js";
|
|
25
|
+
import { createAuditLogger } from "./audit-log.js";
|
|
26
|
+
import { KGStore } from "./kg-store.js";
|
|
27
|
+
import { createKGExtractor, isKGModeEnabled, type KGExtractor } from "./kg-extractor.js";
|
|
28
|
+
import type { MemoryStore } from "./store.js";
|
|
29
|
+
import type { LLMClient } from "./llm-client.js";
|
|
30
|
+
import { runToolSafely } from "./error-taxonomy.js";
|
|
31
|
+
|
|
32
|
+
import { registerCoreTools } from "./mcp-tools-core.js";
|
|
33
|
+
import { registerAdvancedTools } from "./mcp-tools-advanced.js";
|
|
34
|
+
import { registerGovernanceTools } from "./mcp-tools-governance.js";
|
|
35
|
+
import type { ToolRegistryDeps } from "./mcp-tool-deps.js";
|
|
18
36
|
|
|
19
37
|
// ============================================================================
|
|
20
38
|
// Tier Configuration
|
|
@@ -22,7 +40,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
22
40
|
|
|
23
41
|
type ToolTier = "core" | "advanced" | "governance";
|
|
24
42
|
|
|
25
|
-
const MCP_TIER = (
|
|
43
|
+
const MCP_TIER = envConfig.mcpTier();
|
|
26
44
|
|
|
27
45
|
const TOOL_TIERS: Record<string, ToolTier> = {
|
|
28
46
|
// Core (always)
|
|
@@ -59,6 +77,8 @@ const TOOL_TIERS: Record<string, ToolTier> = {
|
|
|
59
77
|
import_conversations: "advanced",
|
|
60
78
|
distill_session: "advanced",
|
|
61
79
|
scan_skill_promotions: "governance",
|
|
80
|
+
promote_scan: "governance",
|
|
81
|
+
manage_alias: "governance",
|
|
62
82
|
forget_memory: "advanced",
|
|
63
83
|
|
|
64
84
|
// Governance (CLI-only, not in MCP by default)
|
|
@@ -82,74 +102,7 @@ function shouldRegisterTool(toolName: string): boolean {
|
|
|
82
102
|
if (MCP_TIER === "core") return tier === "core";
|
|
83
103
|
return true;
|
|
84
104
|
}
|
|
85
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
86
|
-
import { autoRegisterBabelMemory } from "./language-hook.js";
|
|
87
|
-
import { z } from "zod";
|
|
88
|
-
import type { RetrievalResult } from "./retriever.js";
|
|
89
|
-
import type { MemoryStore } from "./store.js";
|
|
90
|
-
import { distillResults, formatExplainResults, formatSearchResults, formatBriefResults, formatFullResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
|
|
91
|
-
import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsset, listDirtyBriefAssets, listMemoryAssets, listPinAssets, saveBriefAsset, savePinAsset, writeExportArtifact } from "./memory-assets.js";
|
|
92
|
-
import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
|
|
93
|
-
import { createComponentResolver, loadConfig, loadDotEnv, resolveRecallMode } from "./runtime-config.js";
|
|
94
|
-
import { DurableMemoryCategorySchema, StoreMemorySourceSchema, PrivacyTierSchema, isPredictiveMemoryEnabled } from "./memory-schema.js";
|
|
95
|
-
import { persistCaseMemory, persistMemory, persistMemoryBatch, persistWorkflowPattern, promoteMemory } from "./capture-engine.js";
|
|
96
|
-
import { persistSkill, retrieveSkills } from "./skill-engine.js";
|
|
97
|
-
import { SkillImplementationTypeSchema } from "./skill-schema.js";
|
|
98
|
-
import { scanForPromotions, formatPromotionResult } from "./skill-promotion.js";
|
|
99
|
-
import { autoCapture } from "./capture-heuristic.js";
|
|
100
|
-
import { ConsolidationEngine, formatConsolidationResult } from "./consolidation-engine.js";
|
|
101
|
-
import { renderMemories, type RenderMode } from "./context-renderer.js";
|
|
102
|
-
import { buildSessionCheckpointResult } from "./session-engine.js";
|
|
103
|
-
import { SessionCheckpointStore } from "./session-store.js";
|
|
104
|
-
import { composeLightResumeContext, composeResumeContext } from "./context-composer.js";
|
|
105
|
-
import { formatCheckpointSaved, formatCheckpointSummary, formatResumeContext } from "./session-output.js";
|
|
106
|
-
import { ConflictStatusSchema } from "./conflict-schema.js";
|
|
107
|
-
import { KGStore } from "./kg-store.js";
|
|
108
|
-
import { createKGExtractor, isKGModeEnabled, type KGExtractor } from "./kg-extractor.js";
|
|
109
|
-
import { resolveConflictCandidate } from "./conflict-engine.js";
|
|
110
|
-
import { escalateConflicts } from "./conflict-escalation.js";
|
|
111
|
-
import { ConflictCandidateStore } from "./conflict-store.js";
|
|
112
|
-
import { runDataCheckup, formatCheckupReport } from "./data-checkup.js";
|
|
113
|
-
import { runMemoryLint, formatMemoryLintReport } from "./memory-lint.js";
|
|
114
|
-
import { exportMemoryGraph, formatGraphExportResult } from "./graph-export.js";
|
|
115
|
-
import { runDream, formatDreamResult } from "./dream-pipeline.js";
|
|
116
|
-
import { formatConflictAudit, formatConflictClusters, formatConflictEscalation, formatConflictList, formatConflictRecord, formatConflictResolution } from "./conflict-output.js";
|
|
117
|
-
import { CONFLICT_ATTENTION_LEVELS, summarizeConflictLifecycle } from "./conflict-lifecycle.js";
|
|
118
|
-
import { buildConflictAuditSummary, clusterConflicts } from "./conflict-advisor.js";
|
|
119
|
-
import { WorkflowObservationOutcomeSchema } from "./workflow-observation-schema.js";
|
|
120
|
-
import { buildWorkflowEvidence, buildWorkflowObservationRecord, inspectWorkflowDashboard, inspectWorkflowHealth } from "./workflow-observation-engine.js";
|
|
121
|
-
import { formatWorkflowEvidencePack, formatWorkflowHealthDashboard, formatWorkflowHealthReport, formatWorkflowObservationSaved } from "./workflow-observation-output.js";
|
|
122
|
-
import { WorkflowObservationStore } from "./workflow-observation-store.js";
|
|
123
|
-
import { buildManagedCheckpointObservation, buildManagedResumeObservation } from "./workflow-observation-managed.js";
|
|
124
|
-
import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
|
|
125
|
-
import { matchesTemporalConstraint, type TemporalConstraint } from "./temporal-parser.js";
|
|
126
|
-
import { setReminder, checkTriggers, fireReminder, formatReminders, suggestPredictedReminders, formatSuggestedReminders, acceptPredictedReminder, demotePredictedReminder } from "./prospective-memory.js";
|
|
127
|
-
import type { PredictionContext } from "./prediction-engine.js";
|
|
128
|
-
import { forgetMemory, forgetByScope } from "./forget-engine.js";
|
|
129
|
-
import { createAuditLogger } from "./audit-log.js";
|
|
130
|
-
// distill_session uses dynamic import("./session-distiller.js") at call time
|
|
131
|
-
|
|
132
|
-
function entryToRetrievalResult(entry: Awaited<ReturnType<MemoryStore["get"]>>): RetrievalResult {
|
|
133
|
-
if (!entry) {
|
|
134
|
-
throw new Error("Memory entry not found.");
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
entry,
|
|
138
|
-
score: entry.importance || 0.7,
|
|
139
|
-
sources: {
|
|
140
|
-
fused: { score: entry.importance || 0.7 },
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
105
|
|
|
145
|
-
async function saveManagedObservation(observation: Parameters<typeof buildWorkflowObservationRecord>[0]): Promise<void> {
|
|
146
|
-
try {
|
|
147
|
-
const record = buildWorkflowObservationRecord(observation);
|
|
148
|
-
await workflowObservationStore.save(record);
|
|
149
|
-
} catch (error) {
|
|
150
|
-
console.error("[RecallNest MCP] Failed to persist managed workflow observation:", error);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
106
|
|
|
154
107
|
// ============================================================================
|
|
155
108
|
// MCP Server
|
|
@@ -158,7 +111,13 @@ async function saveManagedObservation(observation: Parameters<typeof buildWorkfl
|
|
|
158
111
|
loadDotEnv();
|
|
159
112
|
const config = loadConfig();
|
|
160
113
|
const getComponents = createComponentResolver(config);
|
|
161
|
-
|
|
114
|
+
// Lazy: components (embedder/store/retriever/LLM client) are heavy (~8s incl.
|
|
115
|
+
// LanceDB + LLM client). Constructing at module top-level blocked the stdio
|
|
116
|
+
// `initialize` handshake (server.connect is at EOF), making headless `claude -p`
|
|
117
|
+
// cron runs fail with "✗ Failed to connect". getComponents() is a memoized
|
|
118
|
+
// factory (runtime-config.ts), so deferring to first tool call is free.
|
|
119
|
+
let store!: MemoryStore;
|
|
120
|
+
let llm: LLMClient | null = null;
|
|
162
121
|
const checkpointStore = new SessionCheckpointStore();
|
|
163
122
|
const conflictStore = new ConflictCandidateStore();
|
|
164
123
|
const workflowObservationStore = new WorkflowObservationStore();
|
|
@@ -166,17 +125,29 @@ const workflowObservationStore = new WorkflowObservationStore();
|
|
|
166
125
|
// Tier 4.1: Knowledge Graph triple extraction (gated by RECALLNEST_KG_MODE=true)
|
|
167
126
|
let kgExtractor: KGExtractor | null = null;
|
|
168
127
|
let kgStoreInstance: KGStore | null = null;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
128
|
+
|
|
129
|
+
// Idempotent + concurrency-safe lazy component init. Runs on first tool call
|
|
130
|
+
// (via registerTool wrapper), NOT during the MCP handshake — so server.connect()
|
|
131
|
+
// answers `initialize` immediately and headless cron no longer times out.
|
|
132
|
+
let componentsReady: Promise<void> | null = null;
|
|
133
|
+
function ensureComponents(): Promise<void> {
|
|
134
|
+
if (componentsReady) return componentsReady;
|
|
135
|
+
componentsReady = (async () => {
|
|
136
|
+
({ store, llm } = getComponents());
|
|
137
|
+
if (isKGModeEnabled() && llm) {
|
|
138
|
+
try {
|
|
139
|
+
kgStoreInstance = new KGStore({ dbPath: store.dbPath });
|
|
140
|
+
kgExtractor = createKGExtractor({ llmClient: llm, kgStore: kgStoreInstance });
|
|
141
|
+
// Attach KG store to default retriever for PPR graph traversal
|
|
142
|
+
const { retriever } = getComponents();
|
|
143
|
+
retriever.setKGStore(kgStoreInstance);
|
|
144
|
+
console.error("[RecallNest] KG triple extraction + graph traversal enabled");
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error("[RecallNest] KG init failed:", err);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
return componentsReady;
|
|
180
151
|
}
|
|
181
152
|
|
|
182
153
|
const server = new McpServer({
|
|
@@ -201,1728 +172,43 @@ function registerTool(name: string, description: string, schema: ToolSchema, han
|
|
|
201
172
|
return;
|
|
202
173
|
}
|
|
203
174
|
TOOL_DESCRIPTIONS.set(name, description);
|
|
204
|
-
|
|
175
|
+
// Lazy-init guard: defer heavy component construction to first tool call so the
|
|
176
|
+
// MCP handshake (initialize / tools/list) isn't blocked. tools/call enters here.
|
|
177
|
+
const lazyHandler = (async (...args: unknown[]) => {
|
|
178
|
+
// P-error-taxonomy: 把 ensureComponents(lazy-init) + handler 调用整体交给 runToolSafely,
|
|
179
|
+
// 任意抛错都分类 + 结构化日志 + 返回 isError result(不裸抛)。lazy-init 失败(缺 env /
|
|
180
|
+
// LanceDB load / 无效 storage path)同样被覆盖(Codex 复审 P2-1)。
|
|
181
|
+
// 已知分层边界(P2-2):SDK 在调本 handler 前已跑 zod schema 验证,参数校验失败由 SDK
|
|
182
|
+
// 返回标准 MCP validation error、不经此处;taxonomy 只覆盖执行层错误。
|
|
183
|
+
return runToolSafely(name, async () => {
|
|
184
|
+
await ensureComponents();
|
|
185
|
+
return (handler as (...a: unknown[]) => unknown)(...args);
|
|
186
|
+
});
|
|
187
|
+
}) as ToolHandler;
|
|
188
|
+
server.tool(name, description, schema, lazyHandler);
|
|
205
189
|
}
|
|
206
190
|
|
|
207
|
-
registerTool(
|
|
208
|
-
"workflow_observe",
|
|
209
|
-
"Store an append-only workflow observation for self-evolution. Use this to record whether a continuity primitive or reusable workflow succeeded, failed, was corrected by the user, or was missed entirely.",
|
|
210
|
-
{
|
|
211
|
-
workflowId: z.string().min(1).max(120).describe("Workflow primitive id, such as resume_context or checkpoint_session"),
|
|
212
|
-
outcome: WorkflowObservationOutcomeSchema.default("success").describe("success | failure | corrected | missed"),
|
|
213
|
-
summary: z.string().min(1).max(400).describe("Short description of what happened"),
|
|
214
|
-
scope: z.string().min(1).max(160).optional().describe("Optional scope such as project:recallnest"),
|
|
215
|
-
source: z.string().min(1).max(40).default("agent").describe("Source label such as agent, smoke, eval, or manual"),
|
|
216
|
-
signal: z.string().min(1).max(120).optional().describe("Optional failure/correction signal tag"),
|
|
217
|
-
task: z.string().min(1).max(240).optional().describe("Optional related task"),
|
|
218
|
-
tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
|
|
219
|
-
tools: z.array(z.string().min(1).max(60)).max(6).default([]).describe("Optional tools involved"),
|
|
220
|
-
},
|
|
221
|
-
async ({ workflowId, outcome, summary, scope, source, signal, task, tags, tools }) => {
|
|
222
|
-
const record = buildWorkflowObservationRecord({
|
|
223
|
-
workflowId,
|
|
224
|
-
outcome,
|
|
225
|
-
summary,
|
|
226
|
-
scope,
|
|
227
|
-
source,
|
|
228
|
-
signal,
|
|
229
|
-
task,
|
|
230
|
-
tags,
|
|
231
|
-
tools,
|
|
232
|
-
});
|
|
233
|
-
const stored = await workflowObservationStore.save(record);
|
|
234
|
-
return {
|
|
235
|
-
content: [{
|
|
236
|
-
type: "text" as const,
|
|
237
|
-
text: formatWorkflowObservationSaved(stored),
|
|
238
|
-
}],
|
|
239
|
-
};
|
|
240
|
-
},
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
registerTool(
|
|
244
|
-
"workflow_health",
|
|
245
|
-
"Inspect workflow observation health: 7d/30d report for one workflow or dashboard of degraded workflows. Read-only. Use when checking if continuity primitives are succeeding or degrading.",
|
|
246
|
-
{
|
|
247
|
-
workflowId: z.string().min(1).max(120).optional().describe("Optional workflow primitive id"),
|
|
248
|
-
scope: z.string().min(1).max(160).optional().describe("Optional scope filter"),
|
|
249
|
-
limit: z.number().int().min(1).max(30).default(10).describe("Dashboard result limit when workflowId is omitted"),
|
|
250
|
-
},
|
|
251
|
-
async ({ workflowId, scope, limit }) => {
|
|
252
|
-
const text = workflowId
|
|
253
|
-
? formatWorkflowHealthReport(await inspectWorkflowHealth(workflowObservationStore, { workflowId, scope }))
|
|
254
|
-
: formatWorkflowHealthDashboard(await inspectWorkflowDashboard(workflowObservationStore, { scope, limit }), scope);
|
|
255
|
-
return {
|
|
256
|
-
content: [{
|
|
257
|
-
type: "text" as const,
|
|
258
|
-
text,
|
|
259
|
-
}],
|
|
260
|
-
};
|
|
261
|
-
},
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
registerTool(
|
|
265
|
-
"workflow_evidence",
|
|
266
|
-
"Generate an evidence pack for a workflow primitive with recent issues, top signals, and suggested actions. Read-only. Use when investigating why a workflow is degraded and you need concrete failure examples.",
|
|
267
|
-
{
|
|
268
|
-
workflowId: z.string().min(1).max(120).describe("Workflow primitive id, e.g. 'resume_context'"),
|
|
269
|
-
scope: z.string().min(1).max(160).optional().describe("Scope filter, e.g. 'project:recallnest'"),
|
|
270
|
-
limit: z.number().int().min(1).max(20).default(5).describe("Max recent issue observations to include"),
|
|
271
|
-
},
|
|
272
|
-
async ({ workflowId, scope, limit }) => {
|
|
273
|
-
const pack = await buildWorkflowEvidence(workflowObservationStore, {
|
|
274
|
-
workflowId,
|
|
275
|
-
scope,
|
|
276
|
-
limit,
|
|
277
|
-
});
|
|
278
|
-
return {
|
|
279
|
-
content: [{
|
|
280
|
-
type: "text" as const,
|
|
281
|
-
text: formatWorkflowEvidencePack(pack),
|
|
282
|
-
}],
|
|
283
|
-
};
|
|
284
|
-
},
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
registerTool(
|
|
288
|
-
"store_memory",
|
|
289
|
-
"Store a durable memory when the user shares a stable preference, identity fact, project entity, reusable pattern, or solved case that should survive future windows. Do not use this for transient task state; use it only for memory worth keeping.",
|
|
290
|
-
{
|
|
291
|
-
text: z.string().min(1).max(4000).describe("Memory text to store"),
|
|
292
|
-
category: DurableMemoryCategorySchema.default("events").describe("Durable memory category"),
|
|
293
|
-
importance: z.number().min(0).max(1).default(0.7).describe("Importance score from 0 to 1"),
|
|
294
|
-
scope: z.string().min(1).max(160).describe("Required scope such as project:recallnest or session:abc123"),
|
|
295
|
-
source: StoreMemorySourceSchema.default("manual").describe("How this memory was captured"),
|
|
296
|
-
tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
|
|
297
|
-
canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
|
|
298
|
-
topicTag: z.string().min(1).max(60).optional().describe("Optional topic tag for intra-scope partitioning (e.g. 'auth', 'deploy', 'testing'). Auto-detected if omitted."),
|
|
299
|
-
privacyTier: PrivacyTierSchema.default("durable").describe("Privacy tier: ephemeral (auto-expire, no KG), private (persist, no KG), durable (default), shared (cross-scope)"),
|
|
300
|
-
validUntil: z.union([z.string(), z.number()]).optional().describe("Optional expiration: ISO date string or ms timestamp. Memory will be deprioritized after this time."),
|
|
301
|
-
eventTime: z.union([z.string(), z.number()]).optional().describe("Optional event time: when the event actually happened (ISO date or ms), distinct from storage time."),
|
|
302
|
-
confidence: z.union([
|
|
303
|
-
z.number().min(0).max(1),
|
|
304
|
-
z.object({
|
|
305
|
-
score: z.number().min(0).max(1),
|
|
306
|
-
reliability: z.enum(["direct", "inferred", "hearsay"]).optional(),
|
|
307
|
-
}),
|
|
308
|
-
]).optional().describe("Optional confidence override: number (0-1) or {score, reliability}. Auto-assigned from source if omitted."),
|
|
309
|
-
},
|
|
310
|
-
async ({ text, category, importance, scope, source, tags, canonicalKey, topicTag, privacyTier, validUntil, eventTime, confidence }) => {
|
|
311
|
-
const { store, embedder } = getComponents();
|
|
312
|
-
const stored = await persistMemory({
|
|
313
|
-
store,
|
|
314
|
-
embedder,
|
|
315
|
-
conflictStore,
|
|
316
|
-
kgExtractor,
|
|
317
|
-
}, {
|
|
318
|
-
text,
|
|
319
|
-
category,
|
|
320
|
-
importance,
|
|
321
|
-
scope,
|
|
322
|
-
source,
|
|
323
|
-
tags,
|
|
324
|
-
canonicalKey,
|
|
325
|
-
topicTag,
|
|
326
|
-
privacyTier,
|
|
327
|
-
// F3: Pass temporal validity params (extracted by persistMemory before Zod parse)
|
|
328
|
-
validUntil,
|
|
329
|
-
eventTime,
|
|
330
|
-
// F1: Pass confidence override (extracted by persistMemory before Zod parse)
|
|
331
|
-
confidence,
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
content: [{
|
|
336
|
-
type: "text" as const,
|
|
337
|
-
text: [
|
|
338
|
-
`Stored memory ${stored.id.slice(0, 8)}`,
|
|
339
|
-
`Disposition: ${stored.disposition}`,
|
|
340
|
-
`Category: ${stored.category}`,
|
|
341
|
-
`Scope: ${stored.resolvedScope}`,
|
|
342
|
-
`Canonical key: ${stored.canonicalKey}`,
|
|
343
|
-
...(stored.conflictId ? [`Conflict: ${stored.conflictId.slice(0, 8)}`] : []),
|
|
344
|
-
`Stored at: ${stored.storedAt}`,
|
|
345
|
-
].join("\n"),
|
|
346
|
-
}],
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
registerTool(
|
|
352
|
-
"store_workflow_pattern",
|
|
353
|
-
"Store a reusable workflow pattern as durable memory. Use this when you identify a repeatable process worth reusing across fresh windows, such as startup continuity, debugging routines, review flows, or handoff steps.",
|
|
354
|
-
{
|
|
355
|
-
title: z.string().min(1).max(120).describe("Short pattern title"),
|
|
356
|
-
trigger: z.string().min(1).max(240).describe("When this workflow should be used"),
|
|
357
|
-
steps: z.array(z.string().min(1).max(220)).min(1).max(8).describe("Ordered workflow steps"),
|
|
358
|
-
outcome: z.string().min(1).max(240).optional().describe("Optional expected outcome"),
|
|
359
|
-
tools: z.array(z.string().min(1).max(60)).max(6).default([]).describe("Optional tools, commands, or interfaces involved"),
|
|
360
|
-
importance: z.number().min(0).max(1).default(0.82).describe("Importance score from 0 to 1"),
|
|
361
|
-
scope: z.string().min(1).max(160).describe("Required scope such as project:recallnest or session:abc123"),
|
|
362
|
-
source: StoreMemorySourceSchema.default("agent").describe("How this pattern was captured"),
|
|
363
|
-
tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
|
|
364
|
-
canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
|
|
365
|
-
},
|
|
366
|
-
async ({ title, trigger, steps, outcome, tools, importance, scope, source, tags, canonicalKey }) => {
|
|
367
|
-
const { store, embedder } = getComponents();
|
|
368
|
-
const stored = await persistWorkflowPattern({
|
|
369
|
-
store,
|
|
370
|
-
embedder,
|
|
371
|
-
conflictStore,
|
|
372
|
-
kgExtractor,
|
|
373
|
-
}, {
|
|
374
|
-
title,
|
|
375
|
-
trigger,
|
|
376
|
-
steps,
|
|
377
|
-
outcome,
|
|
378
|
-
tools,
|
|
379
|
-
importance,
|
|
380
|
-
scope,
|
|
381
|
-
source,
|
|
382
|
-
tags,
|
|
383
|
-
canonicalKey,
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
content: [{
|
|
388
|
-
type: "text" as const,
|
|
389
|
-
text: [
|
|
390
|
-
`Stored workflow pattern ${stored.id.slice(0, 8)}`,
|
|
391
|
-
`Disposition: ${stored.disposition}`,
|
|
392
|
-
`Title: ${stored.title}`,
|
|
393
|
-
`Scope: ${stored.resolvedScope}`,
|
|
394
|
-
`Canonical key: ${stored.canonicalKey}`,
|
|
395
|
-
`Tags: ${stored.tags.join(", ") || "-"}`,
|
|
396
|
-
...(stored.conflictId ? [`Conflict: ${stored.conflictId.slice(0, 8)}`] : []),
|
|
397
|
-
`Stored at: ${stored.storedAt}`,
|
|
398
|
-
].join("\n"),
|
|
399
|
-
}],
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
);
|
|
403
|
-
|
|
404
|
-
registerTool(
|
|
405
|
-
"set_reminder",
|
|
406
|
-
"Set a prospective memory reminder that auto-triggers during future search_memory calls when the trigger keywords match. Side effect: stores a reminder entry. Use when you need a future nudge tied to a specific context.",
|
|
407
|
-
{
|
|
408
|
-
trigger: z.string().min(1).max(200).describe("Trigger condition — keywords that should activate this reminder"),
|
|
409
|
-
action: z.string().min(1).max(500).describe("What to remind about when the trigger fires"),
|
|
410
|
-
scope: z.string().min(1).max(160).describe("Required scope"),
|
|
411
|
-
expiresInDays: z.number().min(1).max(365).optional().describe("Optional: auto-expire after N days"),
|
|
412
|
-
},
|
|
413
|
-
async ({ trigger, action, scope, expiresInDays }) => {
|
|
414
|
-
const { store, embedder } = getComponents();
|
|
415
|
-
const entry = await setReminder(store, embedder, { trigger, action, scope, expiresInDays });
|
|
416
|
-
|
|
417
|
-
return {
|
|
418
|
-
content: [{
|
|
419
|
-
type: "text" as const,
|
|
420
|
-
text: [
|
|
421
|
-
`Reminder set: ${entry.id.slice(0, 8)}`,
|
|
422
|
-
`Trigger: "${trigger}"`,
|
|
423
|
-
`Action: ${action}`,
|
|
424
|
-
`Scope: ${scope}`,
|
|
425
|
-
...(expiresInDays ? [`Expires in: ${expiresInDays} days`] : []),
|
|
426
|
-
].join("\n"),
|
|
427
|
-
}],
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
);
|
|
431
|
-
|
|
432
|
-
registerTool(
|
|
433
|
-
"auto_capture",
|
|
434
|
-
"Extract memory-worthy items from a conversation turn using lightweight heuristics (zero LLM calls). Detects preferences, identity facts, decisions, corrections, explicit memory instructions, and workflow patterns. Items that pass salience filtering are stored as durable memories. Use this when you want to analyze a block of conversation text and automatically capture any signals worth remembering.",
|
|
435
|
-
{
|
|
436
|
-
text: z.string().min(1).max(8000).describe("Conversation text to analyze for memory-worthy signals"),
|
|
437
|
-
scope: z.string().min(1).max(160).describe("Required scope such as project:recallnest or session:abc123"),
|
|
438
|
-
source: StoreMemorySourceSchema.default("agent").describe("How this memory was captured"),
|
|
439
|
-
},
|
|
440
|
-
async ({ text, scope, source }) => {
|
|
441
|
-
const result = autoCapture(text);
|
|
442
|
-
|
|
443
|
-
if (result.skippedSalience) {
|
|
444
|
-
return {
|
|
445
|
-
content: [{
|
|
446
|
-
type: "text" as const,
|
|
447
|
-
text: "Skipped: text did not pass salience filter (too short, noise, or greeting)",
|
|
448
|
-
}],
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (result.items.length === 0) {
|
|
453
|
-
return {
|
|
454
|
-
content: [{
|
|
455
|
-
type: "text" as const,
|
|
456
|
-
text: "No memory-worthy signals detected in this text",
|
|
457
|
-
}],
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const { store, embedder } = getComponents();
|
|
462
|
-
const stored = await persistMemoryBatch({
|
|
463
|
-
store,
|
|
464
|
-
embedder,
|
|
465
|
-
conflictStore,
|
|
466
|
-
kgExtractor,
|
|
467
|
-
}, {
|
|
468
|
-
scope,
|
|
469
|
-
source,
|
|
470
|
-
defaultImportance: 0.7,
|
|
471
|
-
memories: result.items.map((item) => ({
|
|
472
|
-
text: item.text,
|
|
473
|
-
category: item.category,
|
|
474
|
-
importance: item.importance,
|
|
475
|
-
tags: [`auto-capture:${item.sourceContext.replace(/\s+/g, "-")}`],
|
|
476
|
-
})),
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
const lines = stored.map((r, i) => {
|
|
480
|
-
const item = result.items[i];
|
|
481
|
-
return `${i + 1}. [${item.sourceContext}] ${r.disposition} → ${r.category} (${r.id.slice(0, 8)})`;
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
return {
|
|
485
|
-
content: [{
|
|
486
|
-
type: "text" as const,
|
|
487
|
-
text: [
|
|
488
|
-
`Auto-captured ${stored.length} item(s) from ${result.items.length} signal(s):`,
|
|
489
|
-
...lines,
|
|
490
|
-
`Scope: ${scope}`,
|
|
491
|
-
].join("\n"),
|
|
492
|
-
}],
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
);
|
|
496
|
-
|
|
497
|
-
registerTool(
|
|
498
|
-
"store_case",
|
|
499
|
-
"Store a reusable case as durable memory. Use this when you identify a concrete problem-and-solution pair worth reusing across future windows, such as a debugging fix, continuity cleanup, migration lesson, or implementation recovery.",
|
|
500
|
-
{
|
|
501
|
-
title: z.string().min(1).max(120).describe("Short case title"),
|
|
502
|
-
problem: z.string().min(1).max(320).describe("What problem happened"),
|
|
503
|
-
context: z.string().min(1).max(240).optional().describe("Optional context or preconditions"),
|
|
504
|
-
solutionSteps: z.array(z.string().min(1).max(220)).min(1).max(8).describe("Ordered solution steps"),
|
|
505
|
-
outcome: z.string().min(1).max(240).optional().describe("Optional result or resolution"),
|
|
506
|
-
tools: z.array(z.string().min(1).max(60)).max(6).default([]).describe("Optional tools, commands, or interfaces involved"),
|
|
507
|
-
importance: z.number().min(0).max(1).default(0.84).describe("Importance score from 0 to 1"),
|
|
508
|
-
scope: z.string().min(1).max(160).describe("Required scope such as project:recallnest or session:abc123"),
|
|
509
|
-
source: StoreMemorySourceSchema.default("agent").describe("How this case was captured"),
|
|
510
|
-
tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
|
|
511
|
-
canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
|
|
512
|
-
},
|
|
513
|
-
async ({ title, problem, context, solutionSteps, outcome, tools, importance, scope, source, tags, canonicalKey }) => {
|
|
514
|
-
const { store, embedder } = getComponents();
|
|
515
|
-
const stored = await persistCaseMemory({
|
|
516
|
-
store,
|
|
517
|
-
embedder,
|
|
518
|
-
conflictStore,
|
|
519
|
-
kgExtractor,
|
|
520
|
-
}, {
|
|
521
|
-
title,
|
|
522
|
-
problem,
|
|
523
|
-
context,
|
|
524
|
-
solutionSteps,
|
|
525
|
-
outcome,
|
|
526
|
-
tools,
|
|
527
|
-
importance,
|
|
528
|
-
scope,
|
|
529
|
-
source,
|
|
530
|
-
tags,
|
|
531
|
-
canonicalKey,
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
return {
|
|
535
|
-
content: [{
|
|
536
|
-
type: "text" as const,
|
|
537
|
-
text: [
|
|
538
|
-
`Stored case ${stored.id.slice(0, 8)}`,
|
|
539
|
-
`Disposition: ${stored.disposition}`,
|
|
540
|
-
`Title: ${stored.title}`,
|
|
541
|
-
`Scope: ${stored.resolvedScope}`,
|
|
542
|
-
`Canonical key: ${stored.canonicalKey}`,
|
|
543
|
-
`Tags: ${stored.tags.join(", ") || "-"}`,
|
|
544
|
-
...(stored.conflictId ? [`Conflict: ${stored.conflictId.slice(0, 8)}`] : []),
|
|
545
|
-
`Stored at: ${stored.storedAt}`,
|
|
546
|
-
].join("\n"),
|
|
547
|
-
}],
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
registerTool(
|
|
553
|
-
"promote_memory",
|
|
554
|
-
"Promote an evidence memory into durable memory with an authority upgrade. Side effect: creates a new durable entry linked to the source evidence. Use when a transcript snippet or imported artifact contains a fact worth keeping across windows.",
|
|
555
|
-
{
|
|
556
|
-
memoryId: z.string().min(1).max(128).describe("Existing evidence memory ID or unique prefix"),
|
|
557
|
-
text: z.string().min(1).max(4000).optional().describe("Optional cleaned durable text; defaults to the source entry text"),
|
|
558
|
-
category: DurableMemoryCategorySchema.optional().describe("Optional target durable category; defaults to the source evidence category or its originalCategory"),
|
|
559
|
-
importance: z.number().min(0).max(1).default(0.78).describe("Importance score from 0 to 1"),
|
|
560
|
-
scope: z.string().min(1).max(160).describe("Required target scope such as project:recallnest or session:abc123"),
|
|
561
|
-
source: StoreMemorySourceSchema.default("agent").describe("How this promotion was captured"),
|
|
562
|
-
tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
|
|
563
|
-
canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
|
|
564
|
-
},
|
|
565
|
-
async ({ memoryId, text, category, importance, scope, source, tags, canonicalKey }) => {
|
|
566
|
-
const { store, embedder } = getComponents();
|
|
567
|
-
const stored = await promoteMemory({
|
|
568
|
-
store,
|
|
569
|
-
embedder,
|
|
570
|
-
conflictStore,
|
|
571
|
-
kgExtractor,
|
|
572
|
-
}, {
|
|
573
|
-
memoryId,
|
|
574
|
-
text,
|
|
575
|
-
category,
|
|
576
|
-
importance,
|
|
577
|
-
scope,
|
|
578
|
-
source,
|
|
579
|
-
tags,
|
|
580
|
-
canonicalKey,
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
return {
|
|
584
|
-
content: [{
|
|
585
|
-
type: "text" as const,
|
|
586
|
-
text: stored.disposition === "conflict" && stored.conflictId
|
|
587
|
-
? `Promotion conflict ${stored.conflictId.slice(0, 8)}\nIncoming: ${stored.sourceMemoryId.slice(0, 8)} (${stored.sourceCategory})\nExisting durable: ${stored.id.slice(0, 8)}\nCategory: ${stored.category}\nCanonical key: ${stored.canonicalKey}\nStatus: manual review required`
|
|
588
|
-
: `Promoted memory ${stored.id.slice(0, 8)}\nFrom: ${stored.sourceMemoryId.slice(0, 8)} (${stored.sourceCategory})\nDisposition: ${stored.disposition}\nCategory: ${stored.category}\nScope: ${stored.resolvedScope}\nCanonical key: ${stored.canonicalKey}\nStored at: ${stored.storedAt}`,
|
|
589
|
-
}],
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
registerTool(
|
|
595
|
-
"list_conflicts",
|
|
596
|
-
"List or inspect conflict candidates where promoted evidence disagrees with existing durable memory. Read-only. Use when reviewing pending conflicts before resolution.",
|
|
597
|
-
{
|
|
598
|
-
conflictId: z.string().min(1).max(128).optional().describe("Conflict ID to inspect a single record, e.g. 'c1d2e3f4'"),
|
|
599
|
-
status: ConflictStatusSchema.optional().describe("Optional status filter"),
|
|
600
|
-
attention: z.enum(CONFLICT_ATTENTION_LEVELS).optional().describe("Optional lifecycle filter: fresh / aging / stale / escalated / resolved"),
|
|
601
|
-
canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
|
|
602
|
-
groupBy: z.enum(["record", "cluster"]).default("record").describe("Whether to list individual conflicts or grouped clusters"),
|
|
603
|
-
limit: z.number().int().min(1).max(50).default(20).describe("Max conflicts to list"),
|
|
604
|
-
},
|
|
605
|
-
async ({ conflictId, status, attention, canonicalKey, groupBy, limit }) => {
|
|
606
|
-
if (conflictId) {
|
|
607
|
-
const record = await conflictStore.getById(conflictId);
|
|
608
|
-
return {
|
|
609
|
-
content: [{
|
|
610
|
-
type: "text" as const,
|
|
611
|
-
text: record ? formatConflictRecord(record) : `Conflict not found: ${conflictId}`,
|
|
612
|
-
}],
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const records = (await conflictStore.listRecent({ status, canonicalKey, limit: Math.max(limit * 2, limit) }))
|
|
617
|
-
.filter((record) => !attention || summarizeConflictLifecycle(record).attention === attention)
|
|
618
|
-
.slice(0, limit);
|
|
619
|
-
return {
|
|
620
|
-
content: [{
|
|
621
|
-
type: "text" as const,
|
|
622
|
-
text: groupBy === "cluster"
|
|
623
|
-
? formatConflictClusters(clusterConflicts(records))
|
|
624
|
-
: formatConflictList(records),
|
|
625
|
-
}],
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
registerTool(
|
|
631
|
-
"resolve_conflict",
|
|
632
|
-
"Resolve a conflict candidate by keeping existing, accepting incoming, or merging texts. Side effect: updates conflict status and may modify durable memory. Use when list_conflicts shows open conflicts that need a decision.",
|
|
633
|
-
{
|
|
634
|
-
conflictId: z.string().min(1).max(128).describe("Conflict ID to resolve"),
|
|
635
|
-
resolution: z.enum(["accept_incoming", "keep_existing", "merge"]).describe("How to resolve the conflict"),
|
|
636
|
-
mergedText: z.string().min(1).max(2000).optional().describe("Optional merged text override when resolution is merge"),
|
|
637
|
-
notes: z.string().min(1).max(320).optional().describe("Optional operator notes"),
|
|
638
|
-
},
|
|
639
|
-
async ({ conflictId, resolution, mergedText, notes }) => {
|
|
640
|
-
const { store, embedder } = getComponents();
|
|
641
|
-
const result = await resolveConflictCandidate({
|
|
642
|
-
store,
|
|
643
|
-
embedder,
|
|
644
|
-
conflictStore,
|
|
645
|
-
}, {
|
|
646
|
-
conflictId,
|
|
647
|
-
resolution,
|
|
648
|
-
...(mergedText ? { mergedText } : {}),
|
|
649
|
-
notes,
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
return {
|
|
653
|
-
content: [{
|
|
654
|
-
type: "text" as const,
|
|
655
|
-
text: formatConflictResolution(result),
|
|
656
|
-
}],
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
);
|
|
660
|
-
|
|
661
|
-
registerTool(
|
|
662
|
-
"audit_conflicts",
|
|
663
|
-
"Generate a conflict audit summary showing priority clusters by staleness and escalation level. Read-only. Use when triaging which conflict clusters to resolve first.",
|
|
664
|
-
{
|
|
665
|
-
status: ConflictStatusSchema.optional().describe("Optional status filter"),
|
|
666
|
-
canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
|
|
667
|
-
limit: z.number().int().min(1).max(500).default(100).describe("How many conflict records to scan"),
|
|
668
|
-
top: z.number().int().min(1).max(20).default(5).describe("How many priority clusters to show"),
|
|
669
|
-
},
|
|
670
|
-
async ({ status, canonicalKey, limit, top }) => {
|
|
671
|
-
const records = await conflictStore.listRecent({
|
|
672
|
-
status,
|
|
673
|
-
canonicalKey,
|
|
674
|
-
limit,
|
|
675
|
-
});
|
|
676
|
-
const summary = buildConflictAuditSummary(records, top);
|
|
677
|
-
return {
|
|
678
|
-
content: [{
|
|
679
|
-
type: "text" as const,
|
|
680
|
-
text: formatConflictAudit(summary),
|
|
681
|
-
}],
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
);
|
|
685
|
-
|
|
686
|
-
registerTool(
|
|
687
|
-
"escalate_conflicts",
|
|
688
|
-
"Preview or apply conflict aging policy to mark stale conflicts for operator review. Side effect: when apply=true, persists escalation metadata. Use when conflicts have aged past their attention threshold.",
|
|
689
|
-
{
|
|
690
|
-
attention: z.enum(["stale", "escalated"]).default("stale").describe("Only consider stale or escalated conflicts"),
|
|
691
|
-
canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
|
|
692
|
-
limit: z.number().int().min(1).max(500).default(100).describe("How many open conflicts to scan"),
|
|
693
|
-
top: z.number().int().min(1).max(20).default(10).describe("How many eligible conflicts to include"),
|
|
694
|
-
apply: z.boolean().default(false).describe("When false, preview only. When true, persist escalation metadata."),
|
|
695
|
-
notes: z.string().min(1).max(320).optional().describe("Optional operator note when applying escalation"),
|
|
696
|
-
},
|
|
697
|
-
async ({ attention, canonicalKey, limit, top, apply, notes }) => {
|
|
698
|
-
const result = await escalateConflicts({
|
|
699
|
-
conflictStore,
|
|
700
|
-
}, {
|
|
701
|
-
attention,
|
|
702
|
-
canonicalKey,
|
|
703
|
-
limit,
|
|
704
|
-
top,
|
|
705
|
-
apply,
|
|
706
|
-
notes,
|
|
707
|
-
});
|
|
708
|
-
return {
|
|
709
|
-
content: [{
|
|
710
|
-
type: "text" as const,
|
|
711
|
-
text: formatConflictEscalation(result),
|
|
712
|
-
}],
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
);
|
|
716
|
-
|
|
717
|
-
registerTool(
|
|
718
|
-
"checkpoint_session",
|
|
719
|
-
"Store a compact checkpoint of the current work state. Use this when a task spans windows or terminals and you need the next session to recover decisions, open loops, and next actions without polluting durable memory.",
|
|
720
|
-
{
|
|
721
|
-
sessionId: z.string().min(1).max(160).describe("Current session identifier"),
|
|
722
|
-
scope: z.string().min(1).max(160).optional().describe("Optional shared scope; defaults to session:<sessionId>"),
|
|
723
|
-
summary: z.string().min(1).max(600).describe("Compact summary of the current work state"),
|
|
724
|
-
task: z.string().min(1).max(240).optional().describe("Optional task label"),
|
|
725
|
-
decisions: z.array(z.string().min(1).max(200)).max(6).default([]).describe("Key decisions already made"),
|
|
726
|
-
openLoops: z.array(z.string().min(1).max(200)).max(6).default([]).describe("Unresolved questions or pending items"),
|
|
727
|
-
nextActions: z.array(z.string().min(1).max(200)).max(6).default([]).describe("Next actions to take"),
|
|
728
|
-
entities: z.array(z.string().min(1).max(120)).max(8).default([]).describe("Relevant projects, tools, or people"),
|
|
729
|
-
files: z.array(z.string().min(1).max(220)).max(12).default([]).describe("Relevant files or paths"),
|
|
730
|
-
updatedAt: z.string().datetime().optional().describe("Optional override; defaults to now"),
|
|
731
|
-
},
|
|
732
|
-
async ({ sessionId, scope, summary, task, decisions, openLoops, nextActions, entities, files, updatedAt }) => {
|
|
733
|
-
const result = buildSessionCheckpointResult({
|
|
734
|
-
sessionId,
|
|
735
|
-
scope,
|
|
736
|
-
summary,
|
|
737
|
-
task,
|
|
738
|
-
decisions,
|
|
739
|
-
openLoops,
|
|
740
|
-
nextActions,
|
|
741
|
-
entities,
|
|
742
|
-
files,
|
|
743
|
-
...(updatedAt ? { updatedAt } : {}),
|
|
744
|
-
});
|
|
745
|
-
const storedRecord = await checkpointStore.save(result.record);
|
|
746
|
-
await saveManagedObservation(buildManagedCheckpointObservation({
|
|
747
|
-
...result,
|
|
748
|
-
record: storedRecord,
|
|
749
|
-
}));
|
|
750
|
-
return {
|
|
751
|
-
content: [{
|
|
752
|
-
type: "text" as const,
|
|
753
|
-
text: formatCheckpointSaved(storedRecord),
|
|
754
|
-
}],
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
);
|
|
758
|
-
|
|
759
|
-
registerTool(
|
|
760
|
-
"latest_checkpoint",
|
|
761
|
-
"Fetch the most recent saved checkpoint for a session or shared scope. Read-only. Use when you need to inspect current work state without running a full resume_context.",
|
|
762
|
-
{
|
|
763
|
-
sessionId: z.string().min(1).max(160).optional().describe("Session identifier filter, e.g. 'abc123'"),
|
|
764
|
-
scope: z.string().min(1).max(160).optional().describe("Shared scope filter, e.g. 'project:recallnest'"),
|
|
765
|
-
},
|
|
766
|
-
async ({ sessionId, scope }) => {
|
|
767
|
-
const latest = await checkpointStore.getLatest({ sessionId, scope });
|
|
768
|
-
return {
|
|
769
|
-
content: [{
|
|
770
|
-
type: "text" as const,
|
|
771
|
-
text: formatCheckpointSummary(latest),
|
|
772
|
-
}],
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
);
|
|
776
|
-
|
|
777
|
-
registerTool(
|
|
778
|
-
"resume_context",
|
|
779
|
-
"Compose startup context for a fresh window by combining durable memory, patterns, cases, and the latest checkpoint. Read-only. Use when entering a new session and you need to recover prior decisions, open loops, and next actions.",
|
|
780
|
-
{
|
|
781
|
-
task: z.string().min(1).max(500).optional().describe("Optional current task or question to bias recall"),
|
|
782
|
-
scope: z.string().min(1).max(160).optional().describe("Optional shared scope for project or terminal continuity"),
|
|
783
|
-
sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to recover the latest checkpoint"),
|
|
784
|
-
limitPerSection: z.number().int().min(1).max(6).default(3).describe("Max items per section"),
|
|
785
|
-
includeLatestCheckpoint: z.boolean().default(true).describe("Whether to include the latest checkpoint summary"),
|
|
786
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
|
|
787
|
-
mode: z.enum(["full", "light", "summary", "off"]).optional().describe("Override recall mode: 'full' (default), 'light' (<300 tokens), 'summary' (checkpoint only), 'off'"),
|
|
788
|
-
},
|
|
789
|
-
async ({ task, scope, sessionId, limitPerSection, includeLatestCheckpoint, profile: profileName, mode: modeOverride }) => {
|
|
790
|
-
const effectiveMode = resolveRecallMode(config, modeOverride);
|
|
791
|
-
|
|
792
|
-
// --- off mode: no recall, guide agent to use search_memory ---
|
|
793
|
-
if (effectiveMode === "off") {
|
|
794
|
-
return {
|
|
795
|
-
content: [{
|
|
796
|
-
type: "text" as const,
|
|
797
|
-
text: "Recall mode is off. Use search_memory to retrieve specific memories on demand.",
|
|
798
|
-
}],
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// --- summary mode: checkpoint only, lightweight ---
|
|
803
|
-
if (effectiveMode === "summary") {
|
|
804
|
-
const scopeSelection = resolveScopeSelection({
|
|
805
|
-
scope,
|
|
806
|
-
sessionId,
|
|
807
|
-
operation: "resume_context",
|
|
808
|
-
allowUnscoped: true,
|
|
809
|
-
});
|
|
810
|
-
const latest = await checkpointStore.getLatest({
|
|
811
|
-
sessionId,
|
|
812
|
-
scope: scopeSelection.resolvedScope,
|
|
813
|
-
});
|
|
814
|
-
const summaryText = formatCheckpointSummary(latest) +
|
|
815
|
-
"\n\nFor detailed recall, use search_memory with specific queries.";
|
|
816
|
-
await saveManagedObservation({
|
|
817
|
-
workflowId: "resume_context",
|
|
818
|
-
outcome: "success",
|
|
819
|
-
summary: `Managed resume_context returned summary-mode checkpoint${latest ? "" : " (none found)"}.`,
|
|
820
|
-
scope: scopeSelection.resolvedScope || scope || "global",
|
|
821
|
-
source: "managed:recallnest",
|
|
822
|
-
signal: "managed-resume-summary",
|
|
823
|
-
task,
|
|
824
|
-
tags: ["managed", "recallnest", "summary-mode"],
|
|
825
|
-
});
|
|
826
|
-
return {
|
|
827
|
-
content: [{
|
|
828
|
-
type: "text" as const,
|
|
829
|
-
text: summaryText,
|
|
830
|
-
}],
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// --- light mode: <300 token ultra-light wake-up ---
|
|
835
|
-
if (effectiveMode === "light") {
|
|
836
|
-
const { retriever: lightRetriever } = getComponents(profileName);
|
|
837
|
-
const lightScope = resolveScopeSelection({
|
|
838
|
-
scope,
|
|
839
|
-
sessionId,
|
|
840
|
-
operation: "resume_context",
|
|
841
|
-
allowUnscoped: true,
|
|
842
|
-
});
|
|
843
|
-
const lightResult = await composeLightResumeContext({
|
|
844
|
-
retriever: lightRetriever,
|
|
845
|
-
checkpointStore,
|
|
846
|
-
}, {
|
|
847
|
-
task,
|
|
848
|
-
scope: lightScope.resolvedScope,
|
|
849
|
-
sessionId,
|
|
850
|
-
limitPerSection: limitPerSection,
|
|
851
|
-
includeLatestCheckpoint,
|
|
852
|
-
profile: profileName,
|
|
853
|
-
});
|
|
854
|
-
await saveManagedObservation({
|
|
855
|
-
workflowId: "resume_context",
|
|
856
|
-
outcome: "success",
|
|
857
|
-
summary: `Managed resume_context returned light-mode context (~${lightResult.text.length} chars).`,
|
|
858
|
-
scope: lightScope.resolvedScope || scope || "global",
|
|
859
|
-
source: "managed:recallnest",
|
|
860
|
-
signal: "managed-resume-light",
|
|
861
|
-
task,
|
|
862
|
-
tags: ["managed", "recallnest", "light-mode"],
|
|
863
|
-
});
|
|
864
|
-
return {
|
|
865
|
-
content: [{
|
|
866
|
-
type: "text" as const,
|
|
867
|
-
text: lightResult.text,
|
|
868
|
-
}],
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
// --- full mode: existing compose behavior ---
|
|
873
|
-
const { retriever, profile } = getComponents(profileName);
|
|
874
|
-
const scopeSelection = resolveScopeSelection({
|
|
875
|
-
scope,
|
|
876
|
-
sessionId,
|
|
877
|
-
operation: "resume_context",
|
|
878
|
-
allowUnscoped: true,
|
|
879
|
-
});
|
|
880
|
-
const context = await composeResumeContext({
|
|
881
|
-
retriever,
|
|
882
|
-
checkpointStore,
|
|
883
|
-
}, {
|
|
884
|
-
task,
|
|
885
|
-
scope: scopeSelection.resolvedScope,
|
|
886
|
-
sessionId,
|
|
887
|
-
limitPerSection,
|
|
888
|
-
includeLatestCheckpoint,
|
|
889
|
-
profile: profile.name,
|
|
890
|
-
});
|
|
891
|
-
await saveManagedObservation(buildManagedResumeObservation({
|
|
892
|
-
task,
|
|
893
|
-
scope,
|
|
894
|
-
sessionId,
|
|
895
|
-
}, context));
|
|
896
|
-
|
|
897
|
-
return {
|
|
898
|
-
content: [{
|
|
899
|
-
type: "text" as const,
|
|
900
|
-
text: formatResumeContext(context),
|
|
901
|
-
}],
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
);
|
|
905
|
-
|
|
906
|
-
// --- search_memory tool ---
|
|
907
|
-
registerTool(
|
|
908
|
-
"search_memory",
|
|
909
|
-
"Search indexed memories by semantic similarity and return ranked results with optional temporal filtering. Read-only, but may fire stored reminders as a side effect. Use proactively at the start of tasks, when debugging, writing, or when the user references past work.",
|
|
910
|
-
{
|
|
911
|
-
query: z.string().describe("Search query — natural language or keywords"),
|
|
912
|
-
limit: z.number().min(1).max(20).default(5).describe("Max results to return"),
|
|
913
|
-
scope: z.string().optional().describe("Optional explicit scope"),
|
|
914
|
-
sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to infer session:<id> scope"),
|
|
915
|
-
allScopes: z.boolean().default(false).describe("When true, explicitly allow cross-scope search"),
|
|
916
|
-
category: DurableMemoryCategorySchema.optional().describe("Filter by memory category: profile (identity/background), preferences (habits/style), entities (projects/tools/people), events (past happenings), cases (problem-solution pairs), patterns (reusable workflows)"),
|
|
917
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
|
|
918
|
-
render: z.enum(["verbatim", "highlight"]).default("verbatim").optional().describe("Result rendering mode: verbatim (default, original order) or highlight (reorder by contextual relevance to query)"),
|
|
919
|
-
after: z.string().optional().describe("Filter memories stored after this date (ISO format YYYY-MM-DD, or relative like '最近30天', 'last 7 days')"),
|
|
920
|
-
before: z.string().optional().describe("Filter memories stored before this date (ISO format YYYY-MM-DD, or relative)"),
|
|
921
|
-
graph: z.boolean().default(false).optional().describe("Enable KG graph traversal (PPR) for relationship-aware search. Use when query involves entity relationships (e.g. 'what tools does Alice use', 'Bob的朋友')."),
|
|
922
|
-
includeArchived: z.boolean().default(false).optional().describe("When true, also return archived/superseded/consolidated memories (default: only active)"),
|
|
923
|
-
detail_level: z.enum(["brief", "normal", "full"]).default("normal").optional()
|
|
924
|
-
.describe("Result detail level: brief (ID+score+one-liner), normal (default, current behavior), full (include metadata)"),
|
|
925
|
-
topicTag: z.string().min(1).max(60).optional()
|
|
926
|
-
.describe("Filter by topic tag (e.g. 'auth', 'deploy', 'testing'). Only returns memories tagged with this topic."),
|
|
927
|
-
reconstruct: z.boolean().default(false).describe(
|
|
928
|
-
"Return LLM-synthesized reconstruction alongside raw results. Requires RECALLNEST_CONSTRUCTIVE_RETRIEVAL=true."
|
|
929
|
-
),
|
|
930
|
-
validAt: z.string().optional().describe("Query memories valid at a specific point in time (ISO date, e.g. '2025-06-15'). Returns only memories whose validity window covers this date."),
|
|
931
|
-
includeExpired: z.boolean().default(false).optional().describe("When true, include expired memories in results (demoted 80%). Default: only active/non-expired."),
|
|
932
|
-
},
|
|
933
|
-
async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName, render, after, before, graph, includeArchived, detail_level, topicTag, reconstruct, validAt, includeExpired }) => {
|
|
934
|
-
const { retriever, profile } = getComponents(profileName);
|
|
935
|
-
// Ensure KG store is attached to non-default profile retrievers for PPR
|
|
936
|
-
if (graph && kgStoreInstance) retriever.setKGStore(kgStoreInstance);
|
|
937
|
-
// Attach LLM client for constructive retrieval if available
|
|
938
|
-
if (reconstruct && llm) retriever.setLLMClient(llm);
|
|
939
|
-
let results = await retriever.retrieve(buildRetrievalContext({
|
|
940
|
-
query,
|
|
941
|
-
limit: (after || before || topicTag) ? limit * 3 : limit,
|
|
942
|
-
category,
|
|
943
|
-
scope,
|
|
944
|
-
sessionId,
|
|
945
|
-
allScopes,
|
|
946
|
-
graph,
|
|
947
|
-
includeArchived,
|
|
948
|
-
topicTag,
|
|
949
|
-
reconstruct,
|
|
950
|
-
// F3: Temporal validity filtering
|
|
951
|
-
validAt: validAt ? new Date(validAt).getTime() : undefined,
|
|
952
|
-
includeExpired: includeExpired ?? undefined,
|
|
953
|
-
}, {
|
|
954
|
-
operation: "search_memory",
|
|
955
|
-
}));
|
|
956
|
-
|
|
957
|
-
// Explicit temporal filtering from after/before params
|
|
958
|
-
if (after || before) {
|
|
959
|
-
const constraint: TemporalConstraint = {
|
|
960
|
-
type: (after && before) ? "range" : (after ? "after" : "before"),
|
|
961
|
-
startMs: after ? new Date(after).getTime() || undefined : undefined,
|
|
962
|
-
endMs: before ? new Date(before).getTime() || undefined : undefined,
|
|
963
|
-
anchor: `${after || ""}..${before || ""}`,
|
|
964
|
-
};
|
|
965
|
-
if (constraint.startMs || constraint.endMs) {
|
|
966
|
-
results = results
|
|
967
|
-
.filter(r => matchesTemporalConstraint(r.entry.timestamp, constraint))
|
|
968
|
-
.slice(0, limit);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// Apply context-aware rendering when requested
|
|
973
|
-
if (render === "highlight" && results.length > 0) {
|
|
974
|
-
const rendered = renderMemories(
|
|
975
|
-
results.map(r => ({ id: r.entry.id, text: r.entry.text, score: r.score, category: r.entry.category })),
|
|
976
|
-
query,
|
|
977
|
-
"highlight",
|
|
978
|
-
);
|
|
979
|
-
// Reorder results to match rendered order
|
|
980
|
-
const idOrder = new Map(rendered.memories.map((m, i) => [m.id, i]));
|
|
981
|
-
results.sort((a, b) => (idOrder.get(a.entry.id) ?? 999) - (idOrder.get(b.entry.id) ?? 999));
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// Tier 3.4: Check for triggered reminders alongside search results
|
|
985
|
-
const { store, embedder } = getComponents();
|
|
986
|
-
const scopeFilter = scope ? [scope] : undefined;
|
|
987
|
-
const triggered = await checkTriggers(store, embedder, query, scopeFilter);
|
|
988
|
-
let reminderText = "";
|
|
989
|
-
if (triggered.length > 0) {
|
|
990
|
-
const firedActions: string[] = [];
|
|
991
|
-
for (const reminder of triggered) {
|
|
992
|
-
const action = await fireReminder(store, reminder.entryId, scopeFilter);
|
|
993
|
-
if (action) firedActions.push(action);
|
|
994
|
-
}
|
|
995
|
-
if (firedActions.length > 0) {
|
|
996
|
-
reminderText = "\n\n--- Triggered Reminders ---\n" +
|
|
997
|
-
firedActions.map(a => `- ${a}`).join("\n");
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// HP-predictive: Surface predicted reminders alongside search results
|
|
1002
|
-
let suggestedText = "";
|
|
1003
|
-
if (isPredictiveMemoryEnabled()) {
|
|
1004
|
-
try {
|
|
1005
|
-
const recentCheckpoints = await checkpointStore.listRecent({ scope, limit: 5 });
|
|
1006
|
-
const recentObservations = await workflowObservationStore.listRecent({ scope, limit: 20 });
|
|
1007
|
-
const predictionCtx: PredictionContext = {
|
|
1008
|
-
checkpoints: recentCheckpoints,
|
|
1009
|
-
workflowObservations: recentObservations,
|
|
1010
|
-
frequentMemories: [], // Populated by access tracker in future iteration
|
|
1011
|
-
uncoveredTopics: results.length === 0 && query ? [query] : [],
|
|
1012
|
-
};
|
|
1013
|
-
const suggestions = await suggestPredictedReminders(store, embedder, predictionCtx, scope ?? "global");
|
|
1014
|
-
suggestedText = formatSuggestedReminders(suggestions);
|
|
1015
|
-
} catch {
|
|
1016
|
-
// Prediction failure is non-critical — silently skip
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
const level = detail_level ?? "normal";
|
|
1021
|
-
const sections: string[] = [];
|
|
1022
|
-
|
|
1023
|
-
// Phase 4: Read reconstruction from first-class field (no metadata hack)
|
|
1024
|
-
const reconstruction = (results as import("./retriever.js").RetrievalResultSet).reconstruction;
|
|
1025
|
-
if (reconstruction?.reconstructed) {
|
|
1026
|
-
const sourceIds = reconstruction.sources.map(s => s.id).join(", ");
|
|
1027
|
-
const sourceTypes = [...new Set(reconstruction.sources.map(s => s.source.type))].join(", ");
|
|
1028
|
-
sections.push(
|
|
1029
|
-
`## Reconstructed Context (confidence: ${reconstruction.confidence.toFixed(2)}, coverage: ${reconstruction.coverage.toFixed(2)})\n${reconstruction.reconstructed}\n\nSources (${sourceTypes}): ${sourceIds}`
|
|
1030
|
-
);
|
|
1031
|
-
// Render contradictions if detected
|
|
1032
|
-
if (reconstruction.contradictions.length > 0) {
|
|
1033
|
-
const conflictLines = reconstruction.contradictions.map(c =>
|
|
1034
|
-
`- \u26a0\ufe0f ${c.description} [${c.memoryIds.join(" vs ")}]`
|
|
1035
|
-
);
|
|
1036
|
-
sections.push(`### Contradictions Detected\n${conflictLines.join("\n")}`);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
let body: string;
|
|
1041
|
-
if (level === "brief") {
|
|
1042
|
-
body = formatBriefResults(results, { query });
|
|
1043
|
-
} else if (level === "full") {
|
|
1044
|
-
body = formatFullResults(results, { query, profile: profile.name });
|
|
1045
|
-
} else {
|
|
1046
|
-
body = formatSearchResults(results, { query, profile: profile.name });
|
|
1047
|
-
}
|
|
1048
|
-
sections.push(body);
|
|
1049
|
-
|
|
1050
|
-
return {
|
|
1051
|
-
content: [{
|
|
1052
|
-
type: "text" as const,
|
|
1053
|
-
text: sections.join("\n\n") + reminderText + suggestedText,
|
|
1054
|
-
}],
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
);
|
|
1058
|
-
|
|
1059
|
-
registerTool(
|
|
1060
|
-
"explain_memory",
|
|
1061
|
-
"Explain why memories matched a query: retrieval path, freshness, scope, and matched terms. Read-only. Use when search results seem unexpected and you need to debug ranking or scope filtering.",
|
|
1062
|
-
{
|
|
1063
|
-
query: z.string().describe("Search query to explain — natural language or keywords, e.g. 'auth migration'"),
|
|
1064
|
-
limit: z.number().min(1).max(20).default(5).describe("Maximum number of matched results to analyze and explain (default: 5)"),
|
|
1065
|
-
scope: z.string().optional().describe("Restrict to a specific scope, e.g. 'project:myapp'. Omit to use default scope"),
|
|
1066
|
-
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
|
|
1067
|
-
allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
|
|
1068
|
-
category: DurableMemoryCategorySchema.optional().describe("Filter results by memory category, e.g. 'preference', 'decision', 'fact'"),
|
|
1069
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile that tunes ranking: 'debug' for technical, 'fact-check' for precision"),
|
|
1070
|
-
},
|
|
1071
|
-
async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName }) => {
|
|
1072
|
-
const { retriever, profile } = getComponents(profileName);
|
|
1073
|
-
const results = await retriever.retrieve(buildRetrievalContext({
|
|
1074
|
-
query,
|
|
1075
|
-
limit,
|
|
1076
|
-
category,
|
|
1077
|
-
scope,
|
|
1078
|
-
sessionId,
|
|
1079
|
-
allScopes,
|
|
1080
|
-
}, {
|
|
1081
|
-
operation: "explain_memory",
|
|
1082
|
-
}));
|
|
1083
|
-
return {
|
|
1084
|
-
content: [{
|
|
1085
|
-
type: "text" as const,
|
|
1086
|
-
text: formatExplainResults(results, { query, profile: profile.name }),
|
|
1087
|
-
}],
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
);
|
|
1091
|
-
|
|
1092
|
-
registerTool(
|
|
1093
|
-
"distill_memory",
|
|
1094
|
-
"Distill retrieved memories into a compact briefing with source map, key takeaways, and reusable evidence. Use this when you need a synthesized summary of stored knowledge on a topic rather than raw search results. Returns a structured briefing with citations. Read-only — does not modify stored memories.",
|
|
1095
|
-
{
|
|
1096
|
-
query: z.string().describe("Natural language topic or task to distill, e.g. 'authentication migration decisions'"),
|
|
1097
|
-
limit: z.number().min(1).max(20).default(8).describe("Maximum number of retrieved memories to include in the distillation (default: 8)"),
|
|
1098
|
-
scope: z.string().optional().describe("Restrict search to a specific scope, e.g. 'project:myapp'. Omit to use the default scope"),
|
|
1099
|
-
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
|
|
1100
|
-
allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
|
|
1101
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile that tunes ranking weights: 'writing' for narrative, 'debug' for technical, 'fact-check' for high-precision"),
|
|
1102
|
-
},
|
|
1103
|
-
async ({ query, limit, scope, sessionId, allScopes, profile: profileName }) => {
|
|
1104
|
-
const { retriever, profile } = getComponents(profileName || "writing");
|
|
1105
|
-
const results = await retriever.retrieve(buildRetrievalContext({
|
|
1106
|
-
query,
|
|
1107
|
-
limit,
|
|
1108
|
-
scope,
|
|
1109
|
-
sessionId,
|
|
1110
|
-
allScopes,
|
|
1111
|
-
}, {
|
|
1112
|
-
operation: "distill_memory",
|
|
1113
|
-
}));
|
|
1114
|
-
return {
|
|
1115
|
-
content: [{
|
|
1116
|
-
type: "text" as const,
|
|
1117
|
-
text: distillResults(results, { query, profile: profile.name }),
|
|
1118
|
-
}],
|
|
1119
|
-
};
|
|
1120
|
-
}
|
|
1121
|
-
);
|
|
1122
|
-
|
|
1123
|
-
registerTool(
|
|
1124
|
-
"brief_memory",
|
|
1125
|
-
"Create a structured memory brief by retrieving and summarizing relevant memories, then persist it as a reusable asset indexed for future recall. Use this when you want to consolidate scattered knowledge on a topic into a single retrievable document. Side effect: writes a new brief asset to disk and indexes it in the vector store for future search.",
|
|
1126
|
-
{
|
|
1127
|
-
query: z.string().describe("Natural language topic or task to brief, e.g. 'deployment pipeline architecture decisions'"),
|
|
1128
|
-
limit: z.number().min(1).max(20).default(8).describe("Maximum number of source memories to include in the brief (default: 8)"),
|
|
1129
|
-
scope: z.string().optional().describe("Restrict search to a specific scope, e.g. 'project:myapp'. Omit to use the default scope"),
|
|
1130
|
-
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
|
|
1131
|
-
allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
|
|
1132
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile that tunes ranking weights: 'writing' for narrative, 'debug' for technical, 'fact-check' for high-precision"),
|
|
1133
|
-
title: z.string().optional().describe("Human-readable title for the brief asset, e.g. 'Q1 Auth Migration Summary'. Auto-generated if omitted"),
|
|
1134
|
-
},
|
|
1135
|
-
async ({ query, limit, scope, sessionId, allScopes, profile: profileName, title }) => {
|
|
1136
|
-
const { retriever, profile, store, embedder } = getComponents(profileName || "writing");
|
|
1137
|
-
const results = await retriever.retrieve(buildRetrievalContext({
|
|
1138
|
-
query,
|
|
1139
|
-
limit,
|
|
1140
|
-
scope,
|
|
1141
|
-
sessionId,
|
|
1142
|
-
allScopes,
|
|
1143
|
-
}, {
|
|
1144
|
-
operation: "brief_memory",
|
|
1145
|
-
}));
|
|
1146
|
-
if (results.length === 0) {
|
|
1147
|
-
return { content: [{ type: "text" as const, text: `No results found for: ${query}` }] };
|
|
1148
|
-
}
|
|
1149
|
-
const briefSeedResults = selectBriefSeedResults(results);
|
|
1150
|
-
const summary = summarizeResults(briefSeedResults, { query, profile: profile.name });
|
|
1151
|
-
const asset = buildBriefAsset(summary, { title });
|
|
1152
|
-
const path = saveBriefAsset(asset);
|
|
1153
|
-
await indexAsset(store, embedder, asset);
|
|
1154
|
-
|
|
1155
|
-
return {
|
|
1156
|
-
content: [{
|
|
1157
|
-
type: "text" as const,
|
|
1158
|
-
text: `Created brief ${asset.id.slice(0, 8)}\nTitle: ${asset.title}\nHits: ${asset.hits}\nPath: ${path}`,
|
|
1159
|
-
}],
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
);
|
|
1163
|
-
|
|
1164
|
-
registerTool(
|
|
1165
|
-
"pin_memory",
|
|
1166
|
-
"Pin a retrieved memory as a high-importance reusable asset on disk. Side effect: boosts importance to 0.95, writes pin asset file, and indexes it. Use when a search result is critical and should be surfaced in future recalls.",
|
|
1167
|
-
{
|
|
1168
|
-
memory_id: z.string().describe("Memory ID or unique prefix from search/explain output, e.g. 'a1b2c3d4'"),
|
|
1169
|
-
scope: z.string().optional().describe("Explicit scope filter, e.g. 'project:recallnest'"),
|
|
1170
|
-
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session:<id> scope, e.g. 'abc123'"),
|
|
1171
|
-
allScopes: z.boolean().default(false).describe("When true, allow cross-scope reads to find the memory"),
|
|
1172
|
-
title: z.string().optional().describe("Human-readable title for the pin, e.g. 'Auth migration decision'"),
|
|
1173
|
-
summary: z.string().optional().describe("Short summary override for the pinned asset"),
|
|
1174
|
-
query: z.string().optional().describe("Original query that led to this pin, e.g. 'auth decisions'"),
|
|
1175
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile for ranking, e.g. 'debug'"),
|
|
1176
|
-
},
|
|
1177
|
-
async ({ memory_id, scope, sessionId, allScopes, title, summary, query, profile: profileName }) => {
|
|
1178
|
-
const { store, embedder } = getComponents(profileName);
|
|
1179
|
-
const scopeSelection = resolveScopeSelection({
|
|
1180
|
-
scope,
|
|
1181
|
-
sessionId,
|
|
1182
|
-
allScopes,
|
|
1183
|
-
operation: "pin_memory",
|
|
1184
|
-
});
|
|
1185
|
-
const entry = await store.get(memory_id, scopeSelection.scopeFilter);
|
|
1186
|
-
if (!entry) {
|
|
1187
|
-
return { content: [{ type: "text" as const, text: `Memory not found: ${memory_id}` }] };
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
await store.update(entry.id, { importance: Math.max(entry.importance || 0.7, 0.95) }, scopeSelection.scopeFilter);
|
|
1191
|
-
const asset = buildPinAsset(entryToRetrievalResult(entry), {
|
|
1192
|
-
title,
|
|
1193
|
-
summary,
|
|
1194
|
-
query,
|
|
1195
|
-
profile: profileName || "default",
|
|
1196
|
-
});
|
|
1197
|
-
const path = savePinAsset(asset);
|
|
1198
|
-
await indexPinnedAsset(store, embedder, asset);
|
|
1199
|
-
|
|
1200
|
-
return {
|
|
1201
|
-
content: [{
|
|
1202
|
-
type: "text" as const,
|
|
1203
|
-
text: `Pinned ${asset.id.slice(0, 8)} from memory ${entry.id.slice(0, 8)}\nTitle: ${asset.title}\nPath: ${path}`,
|
|
1204
|
-
}],
|
|
1205
|
-
};
|
|
1206
|
-
}
|
|
1207
|
-
);
|
|
1208
|
-
|
|
1209
|
-
registerTool(
|
|
1210
|
-
"export_memory",
|
|
1211
|
-
"Export a distilled memory briefing to a markdown or JSON file on disk. Side effect: writes an export artifact file. Use when you need an offline-readable snapshot of knowledge on a topic.",
|
|
1212
|
-
{
|
|
1213
|
-
query: z.string().describe("Topic or task to export, e.g. 'auth migration decisions'"),
|
|
1214
|
-
limit: z.number().min(1).max(20).default(8).describe("Maximum number of source memories to include in the export (default: 8)"),
|
|
1215
|
-
scope: z.string().optional().describe("Restrict to a specific scope, e.g. 'project:recallnest'. Omit to use default scope"),
|
|
1216
|
-
sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
|
|
1217
|
-
allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
|
|
1218
|
-
profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile for ranking, e.g. 'writing'"),
|
|
1219
|
-
format: z.enum(["md", "json"]).default("md").describe("Export format: 'md' for markdown, 'json' for structured JSON"),
|
|
1220
|
-
},
|
|
1221
|
-
async ({ query, limit, scope, sessionId, allScopes, profile: profileName, format }) => {
|
|
1222
|
-
const { retriever, profile } = getComponents(profileName || "writing");
|
|
1223
|
-
const results = await retriever.retrieve(buildRetrievalContext({
|
|
1224
|
-
query,
|
|
1225
|
-
limit,
|
|
1226
|
-
scope,
|
|
1227
|
-
sessionId,
|
|
1228
|
-
allScopes,
|
|
1229
|
-
}, {
|
|
1230
|
-
operation: "export_memory",
|
|
1231
|
-
}));
|
|
1232
|
-
const summary = distillResults(results, { query, profile: profile.name });
|
|
1233
|
-
const artifact = writeExportArtifact({
|
|
1234
|
-
query,
|
|
1235
|
-
profile: profile.name,
|
|
1236
|
-
results,
|
|
1237
|
-
summary,
|
|
1238
|
-
format,
|
|
1239
|
-
});
|
|
1240
|
-
|
|
1241
|
-
return {
|
|
1242
|
-
content: [{
|
|
1243
|
-
type: "text" as const,
|
|
1244
|
-
text: `Exported ${artifact.id.slice(0, 8)}\nFormat: ${artifact.format}\nPath: ${artifact.outputPath}`,
|
|
1245
|
-
}],
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
);
|
|
1249
|
-
|
|
1250
|
-
registerTool(
|
|
1251
|
-
"list_assets",
|
|
1252
|
-
"List recent structured memory assets (pinned memories and distilled briefs) sorted by creation date. Read-only. Use when you need an inventory of persisted knowledge artifacts — for example, before creating a new brief to avoid duplicates. Returns asset type, title, scope, creation date, and file path for each entry.",
|
|
1253
|
-
{
|
|
1254
|
-
limit: z.number().min(1).max(50).default(12).describe("Maximum number of assets to return, sorted most-recent-first (default: 12, max: 50)"),
|
|
1255
|
-
},
|
|
1256
|
-
async ({ limit }) => {
|
|
1257
|
-
const rows = listMemoryAssets(limit);
|
|
1258
|
-
if (rows.length === 0) {
|
|
1259
|
-
return { content: [{ type: "text" as const, text: "No assets yet." }] };
|
|
1260
|
-
}
|
|
1261
|
-
const lines = [
|
|
1262
|
-
"Asset ID Kind Title Scope / Sources Date",
|
|
1263
|
-
"-------- ----- ----- --------------- ----------",
|
|
1264
|
-
...rows.map(row => assetSummaryLine(row)),
|
|
1265
|
-
];
|
|
1266
|
-
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
1267
|
-
}
|
|
1268
|
-
);
|
|
1269
|
-
|
|
1270
|
-
registerTool(
|
|
1271
|
-
"list_dirty_briefs",
|
|
1272
|
-
"List memory briefs generated before current cleanup rules that may need re-indexing. Read-only. Use when auditing brief quality or before running clean_dirty_briefs.",
|
|
1273
|
-
{},
|
|
1274
|
-
async () => {
|
|
1275
|
-
const rows = listDirtyBriefAssets();
|
|
1276
|
-
if (rows.length === 0) {
|
|
1277
|
-
return { content: [{ type: "text" as const, text: "No dirty briefs found." }] };
|
|
1278
|
-
}
|
|
1279
|
-
const lines = [
|
|
1280
|
-
"Brief ID Title Scope Reasons",
|
|
1281
|
-
"-------- ----- ----- ----------------------------------------",
|
|
1282
|
-
...rows.map(row => `${row.id.slice(0, 8)} ${row.title} [${row.scope}] ${row.reasons.join("; ")}`),
|
|
1283
|
-
];
|
|
1284
|
-
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
1285
|
-
}
|
|
1286
|
-
);
|
|
1287
|
-
|
|
1288
|
-
registerTool(
|
|
1289
|
-
"clean_dirty_briefs",
|
|
1290
|
-
"Archive dirty briefs and remove their indexed asset entries. Side effect: when apply=true, moves briefs to archive and deletes index rows. Use when list_dirty_briefs shows stale briefs that need cleanup.",
|
|
1291
|
-
{
|
|
1292
|
-
apply: z.boolean().default(false).describe("When false, preview only (no writes). When true, archive briefs and delete indexed rows."),
|
|
1293
|
-
},
|
|
1294
|
-
async ({ apply }) => {
|
|
1295
|
-
const rows = listDirtyBriefAssets();
|
|
1296
|
-
if (rows.length === 0) {
|
|
1297
|
-
return { content: [{ type: "text" as const, text: "No dirty briefs found." }] };
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
if (!apply) {
|
|
1301
|
-
const preview = rows.map(row => `${row.id.slice(0, 8)} ${row.title} [${row.scope}] ${row.reasons.join("; ")}`).join("\n");
|
|
1302
|
-
return {
|
|
1303
|
-
content: [{
|
|
1304
|
-
type: "text" as const,
|
|
1305
|
-
text: `Dirty briefs detected: ${rows.length}\n\n${preview}\n\nCall clean_dirty_briefs with apply=true to archive them.`,
|
|
1306
|
-
}],
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
let archived = 0;
|
|
1311
|
-
let deleted = 0;
|
|
1312
|
-
for (const row of rows) {
|
|
1313
|
-
try {
|
|
1314
|
-
archiveDirtyBriefAsset(row);
|
|
1315
|
-
archived += 1;
|
|
1316
|
-
} catch (err) {
|
|
1317
|
-
console.error("[recallnest] Failed to archive dirty brief:", err instanceof Error ? err.message : String(err));
|
|
1318
|
-
}
|
|
1319
|
-
try {
|
|
1320
|
-
deleted += await store.bulkDelete([row.scope]);
|
|
1321
|
-
} catch (err) {
|
|
1322
|
-
console.error("[recallnest] Failed to delete dirty brief index rows:", err instanceof Error ? err.message : String(err));
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
return {
|
|
1327
|
-
content: [{
|
|
1328
|
-
type: "text" as const,
|
|
1329
|
-
text: `Dirty briefs: ${rows.length}\nArchived: ${archived}\nIndex rows deleted: ${deleted}`,
|
|
1330
|
-
}],
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
);
|
|
1334
|
-
|
|
1335
|
-
registerTool(
|
|
1336
|
-
"consolidate_memories",
|
|
1337
|
-
"Run semantic consolidation: cluster similar memories, merge near-duplicates, and detect contradictions. Side effect: when apply=true, archives merged entries. Use when a scope has grown large and needs deduplication.",
|
|
1338
|
-
{
|
|
1339
|
-
scope: z.string().min(1).max(160).describe("Scope to consolidate (e.g. project:recallnest)"),
|
|
1340
|
-
clusterThreshold: z.number().min(0.5).max(1.0).default(0.82).describe("Min similarity to form a cluster (default 0.82)"),
|
|
1341
|
-
mergeThreshold: z.number().min(0.5).max(1.0).default(0.92).describe("Min similarity to merge/archive (default 0.92)"),
|
|
1342
|
-
maxEntries: z.number().min(10).max(2000).default(500).describe("Max entries to scan (default 500)"),
|
|
1343
|
-
apply: z.boolean().default(false).describe("When false, preview only (scan + report without archiving). When true, actually merge/archive."),
|
|
1344
|
-
},
|
|
1345
|
-
async ({ scope, clusterThreshold, mergeThreshold, maxEntries, apply }) => {
|
|
1346
|
-
const { store } = getComponents();
|
|
1347
|
-
|
|
1348
|
-
if (!apply) {
|
|
1349
|
-
// Dry-run: use a read-only wrapper that blocks writes
|
|
1350
|
-
const readOnlyStore = {
|
|
1351
|
-
list: store.list.bind(store),
|
|
1352
|
-
getById: store.getById.bind(store),
|
|
1353
|
-
vectorSearch: store.vectorSearch.bind(store),
|
|
1354
|
-
update: async () => null, // no-op in dry-run
|
|
1355
|
-
};
|
|
1356
|
-
const engine = new ConsolidationEngine(readOnlyStore, { clusterThreshold, mergeThreshold, maxEntriesPerRun: maxEntries });
|
|
1357
|
-
const result = await engine.run(scope);
|
|
1358
|
-
return {
|
|
1359
|
-
content: [{
|
|
1360
|
-
type: "text" as const,
|
|
1361
|
-
text: `[DRY-RUN] ${formatConsolidationResult(result)}\n\nRe-run with apply=true to execute merges.`,
|
|
1362
|
-
}],
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
const engine = new ConsolidationEngine(store, { clusterThreshold, mergeThreshold, maxEntriesPerRun: maxEntries });
|
|
1367
|
-
const result = await engine.run(scope);
|
|
1368
|
-
return {
|
|
1369
|
-
content: [{
|
|
1370
|
-
type: "text" as const,
|
|
1371
|
-
text: formatConsolidationResult(result),
|
|
1372
|
-
}],
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
);
|
|
1376
|
-
|
|
1377
|
-
// --- forget_memory tool (Ethics Layer) ---
|
|
1378
|
-
registerTool(
|
|
1379
|
-
"forget_memory",
|
|
1380
|
-
"Permanently forget a memory with full cascade: delete primary entry, remove KG triples, demote related memories, and log an audit trail. Requires confirm=true for durable-tier memories. Use when the user explicitly requests a memory be forgotten, or to clean up sensitive/incorrect data.",
|
|
1381
|
-
{
|
|
1382
|
-
memoryId: z.string().min(1).max(128).describe("Memory ID to forget (full UUID or 8+ hex prefix)"),
|
|
1383
|
-
confirm: z.boolean().default(false).describe("Required confirmation — must be true for durable-tier memories"),
|
|
1384
|
-
reason: z.string().min(1).max(200).optional().describe("Reason for forgetting (recorded in audit trail)"),
|
|
1385
|
-
scope: z.string().min(1).max(160).optional().describe("Optional scope filter for permission check"),
|
|
1386
|
-
},
|
|
1387
|
-
async ({ memoryId, confirm, reason, scope }) => {
|
|
1388
|
-
const { store } = getComponents();
|
|
1389
|
-
const auditLogger = createAuditLogger();
|
|
1390
|
-
const scopeFilter = scope ? [scope] : undefined;
|
|
1391
|
-
|
|
1392
|
-
const result = await forgetMemory(
|
|
1393
|
-
{ store, kgStore: kgStoreInstance, auditLogger },
|
|
1394
|
-
{ memoryId, confirm, reason, scopeFilter },
|
|
1395
|
-
);
|
|
1396
|
-
|
|
1397
|
-
if (!result.success) {
|
|
1398
|
-
return {
|
|
1399
|
-
content: [{
|
|
1400
|
-
type: "text" as const,
|
|
1401
|
-
text: `❌ Forget failed: ${result.error}`,
|
|
1402
|
-
}],
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
const lines = [
|
|
1407
|
-
`✅ Memory ${result.memoryId.slice(0, 8)} forgotten.`,
|
|
1408
|
-
`Privacy tier: ${result.evidence?.privacyTier || "unknown"}`,
|
|
1409
|
-
`KG triples removed: ${result.kgTriplesRemoved ? "yes" : "no/N/A"}`,
|
|
1410
|
-
`Cascade demoted: ${result.cascadeResult.demotedCount} related memories`,
|
|
1411
|
-
];
|
|
1412
|
-
if (result.evidence?.reason) {
|
|
1413
|
-
lines.push(`Reason: ${result.evidence.reason}`);
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
return {
|
|
1417
|
-
content: [{
|
|
1418
|
-
type: "text" as const,
|
|
1419
|
-
text: lines.join("\n"),
|
|
1420
|
-
}],
|
|
1421
|
-
};
|
|
1422
|
-
},
|
|
1423
|
-
);
|
|
1424
|
-
|
|
1425
|
-
registerTool(
|
|
1426
|
-
"list_pins",
|
|
1427
|
-
"List pinned memory assets sorted by creation date, showing title, scope, importance score, and file path. Read-only. Use when you need to review high-value memories that were explicitly pinned via pin_memory, or to check if a topic already has a pinned reference before creating a new one.",
|
|
1428
|
-
{
|
|
1429
|
-
limit: z.number().min(1).max(50).default(10).describe("Maximum number of pinned assets to return, sorted most-recent-first (default: 10, max: 50)"),
|
|
1430
|
-
},
|
|
1431
|
-
async ({ limit }) => {
|
|
1432
|
-
const rows = listPinAssets(limit);
|
|
1433
|
-
if (rows.length === 0) {
|
|
1434
|
-
return { content: [{ type: "text" as const, text: "No pinned assets yet." }] };
|
|
1435
|
-
}
|
|
1436
|
-
const lines = [
|
|
1437
|
-
"Pin ID Title Scope Date",
|
|
1438
|
-
"-------- ----- ----- ----------",
|
|
1439
|
-
...rows.map(row => `${row.id.slice(0, 8)} ${row.title} [${row.source.scope}] ${row.createdAt.slice(0, 10)}`),
|
|
1440
|
-
];
|
|
1441
|
-
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
1442
|
-
}
|
|
1443
|
-
);
|
|
1444
|
-
|
|
1445
|
-
// --- memory_stats tool ---
|
|
1446
|
-
registerTool(
|
|
1447
|
-
"memory_stats",
|
|
1448
|
-
"Show aggregate statistics of the memory database: total entries, counts by source and category. Read-only. Use when you need an overview of memory store health or size.",
|
|
1449
|
-
{},
|
|
1450
|
-
async () => {
|
|
1451
|
-
const stats = await store.stats();
|
|
1452
|
-
|
|
1453
|
-
// Aggregate by source prefix
|
|
1454
|
-
const sourceCounts: Record<string, number> = {};
|
|
1455
|
-
for (const [scope, count] of Object.entries(stats.scopeCounts)) {
|
|
1456
|
-
const prefix = scope.split(":")[0];
|
|
1457
|
-
sourceCounts[prefix] = (sourceCounts[prefix] || 0) + count;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
const lines = [
|
|
1461
|
-
`Total entries: ${stats.totalCount}`,
|
|
1462
|
-
"",
|
|
1463
|
-
"By source:",
|
|
1464
|
-
...Object.entries(sourceCounts)
|
|
1465
|
-
.sort((a, b) => b[1] - a[1])
|
|
1466
|
-
.map(([src, count]) => ` ${src}: ${count}`),
|
|
1467
|
-
"",
|
|
1468
|
-
"By category:",
|
|
1469
|
-
...Object.entries(stats.categoryCounts || {})
|
|
1470
|
-
.sort((a, b) => b[1] - a[1])
|
|
1471
|
-
.map(([cat, count]) => ` ${cat}: ${count}`),
|
|
1472
|
-
];
|
|
1473
|
-
|
|
1474
|
-
return {
|
|
1475
|
-
content: [{ type: "text" as const, text: lines.join("\n") }],
|
|
1476
|
-
};
|
|
1477
|
-
}
|
|
1478
|
-
);
|
|
1479
|
-
|
|
1480
|
-
// ============================================================================
|
|
1481
|
-
// LC-P4: Data Checkup Tool
|
|
1482
|
-
// ============================================================================
|
|
1483
|
-
|
|
1484
|
-
registerTool(
|
|
1485
|
-
"data_checkup",
|
|
1486
|
-
"Run health checks on the memory database: vector dimensions, orphans, tier distribution, and conflict backlog. Read-only. Use when diagnosing data quality issues or before a consolidation run.",
|
|
1487
|
-
{},
|
|
1488
|
-
async () => {
|
|
1489
|
-
const openConflicts = (await conflictStore.listRecent({ status: "open", limit: 200 })).length;
|
|
1490
|
-
const report = await runDataCheckup({ store, openConflictCount: openConflicts });
|
|
1491
|
-
return {
|
|
1492
|
-
content: [{ type: "text" as const, text: formatCheckupReport(report) }],
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
);
|
|
1496
|
-
|
|
1497
191
|
// ============================================================================
|
|
1498
|
-
//
|
|
192
|
+
// Tool Registration — tier modules (P3-B split; see mcp-tools-{core,advanced,governance}.ts)
|
|
1499
193
|
// ============================================================================
|
|
1500
194
|
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
);
|
|
1515
|
-
|
|
1516
|
-
// ============================================================================
|
|
1517
|
-
// Knowledge Graph Export Tool
|
|
1518
|
-
// ============================================================================
|
|
1519
|
-
|
|
1520
|
-
registerTool(
|
|
1521
|
-
"export_graph",
|
|
1522
|
-
"Export memories as an interactive HTML knowledge graph. Creates a self-contained HTML file with a force-directed visualization. Open in any browser. Use when the user wants to visualize their memory network.",
|
|
1523
|
-
{
|
|
1524
|
-
scope: z.string().min(1).max(160).optional().describe("Optional scope filter"),
|
|
1525
|
-
maxNodes: z.number().int().min(10).max(500).default(200).describe("Maximum nodes to include (default 200)"),
|
|
1526
|
-
},
|
|
1527
|
-
async ({ scope, maxNodes }) => {
|
|
1528
|
-
const { path, graph } = await exportMemoryGraph(store, { scope, maxNodes });
|
|
1529
|
-
return {
|
|
1530
|
-
content: [{ type: "text" as const, text: formatGraphExportResult(path, graph) }],
|
|
1531
|
-
};
|
|
1532
|
-
}
|
|
1533
|
-
);
|
|
1534
|
-
|
|
1535
|
-
// ============================================================================
|
|
1536
|
-
// AD-1: Dream Pipeline Tool
|
|
1537
|
-
// ============================================================================
|
|
1538
|
-
|
|
1539
|
-
registerTool(
|
|
1540
|
-
"dream",
|
|
1541
|
-
"Run a full memory consolidation cycle (Orient, Gather, Consolidate, Prune). Side effect: may archive low-value entries and generate insight memories. Use when memory count is high and you need periodic maintenance.",
|
|
1542
|
-
{
|
|
1543
|
-
scope: z.string().min(1).max(160).optional().describe("Scope to consolidate, e.g. 'project:myapp'. Omit to consolidate across all scopes"),
|
|
1544
|
-
force: z.boolean().default(false).describe("Set to true to force consolidation even if recent write count is below the automatic threshold"),
|
|
1545
|
-
},
|
|
1546
|
-
async ({ scope, force }) => {
|
|
1547
|
-
const resolvedScope = scope || "project:default";
|
|
1548
|
-
const components = getComponents();
|
|
1549
|
-
const result = await runDream({
|
|
1550
|
-
store: components.store,
|
|
1551
|
-
llm: components.llm,
|
|
1552
|
-
embedder: components.embedder,
|
|
1553
|
-
scope: resolvedScope,
|
|
1554
|
-
force,
|
|
1555
|
-
});
|
|
1556
|
-
return {
|
|
1557
|
-
content: [{ type: "text" as const, text: formatDreamResult(result) }],
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
);
|
|
1561
|
-
|
|
1562
|
-
// ============================================================================
|
|
1563
|
-
// Memory Drill-Down Tool
|
|
1564
|
-
// ============================================================================
|
|
1565
|
-
|
|
1566
|
-
registerTool(
|
|
1567
|
-
"memory_drill_down",
|
|
1568
|
-
"Retrieve the full or overview-level content of a single memory entry. Read-only. Use when search returned compact summaries and you need the complete text or L1 overview.",
|
|
1569
|
-
{
|
|
1570
|
-
id: z.string().describe("Memory ID or unique prefix (at least 8 hex chars), e.g. 'a1b2c3d4'"),
|
|
1571
|
-
level: z.enum(["overview", "full"]).optional().default("full")
|
|
1572
|
-
.describe("Content depth: 'overview' (L1) or 'full' (L2, default)"),
|
|
1573
|
-
},
|
|
1574
|
-
async ({ id, level }) => {
|
|
1575
|
-
try {
|
|
1576
|
-
const entry = await store.getById(id);
|
|
1577
|
-
if (!entry) {
|
|
1578
|
-
return {
|
|
1579
|
-
content: [{ type: "text" as const, text: `No memory found with ID: ${id}` }],
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// Parse metadata for L0/L1/L2 content
|
|
1584
|
-
let meta: Record<string, unknown> = {};
|
|
1585
|
-
try {
|
|
1586
|
-
meta = JSON.parse(entry.metadata || "{}");
|
|
1587
|
-
} catch { /* malformed metadata, use raw text */ }
|
|
1588
|
-
|
|
1589
|
-
// Support both legacy short names (l0/l1) and current long names (l0_abstract/l1_overview/l2_content)
|
|
1590
|
-
const l0 = typeof meta.l0_abstract === "string" ? meta.l0_abstract : typeof meta.l0 === "string" ? meta.l0 : null;
|
|
1591
|
-
const l1 = typeof meta.l1_overview === "string" ? meta.l1_overview : typeof meta.l1 === "string" ? meta.l1 : null;
|
|
1592
|
-
const l2 = typeof meta.l2_content === "string" ? meta.l2_content : entry.text;
|
|
1593
|
-
|
|
1594
|
-
let content: string;
|
|
1595
|
-
if (level === "overview" && l1) {
|
|
1596
|
-
content = `## ${entry.category} (L1 Overview)\n\n${l1}`;
|
|
1597
|
-
} else {
|
|
1598
|
-
content = `## ${entry.category} (Full Content)\n\n${l2}`;
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
const header = [
|
|
1602
|
-
`**ID**: ${entry.id}`,
|
|
1603
|
-
`**Category**: ${entry.category}`,
|
|
1604
|
-
`**Scope**: ${entry.scope}`,
|
|
1605
|
-
`**Importance**: ${entry.importance}`,
|
|
1606
|
-
`**Created**: ${new Date(entry.timestamp).toISOString()}`,
|
|
1607
|
-
l0 ? `**Abstract**: ${l0}` : null,
|
|
1608
|
-
].filter(Boolean).join("\n");
|
|
1609
|
-
|
|
1610
|
-
return {
|
|
1611
|
-
content: [{ type: "text" as const, text: `${header}\n\n${content}` }],
|
|
1612
|
-
};
|
|
1613
|
-
} catch (err) {
|
|
1614
|
-
return {
|
|
1615
|
-
content: [{ type: "text" as const, text: `Error drilling down: ${String(err)}` }],
|
|
1616
|
-
};
|
|
1617
|
-
}
|
|
1618
|
-
},
|
|
1619
|
-
);
|
|
1620
|
-
|
|
1621
|
-
// ============================================================================
|
|
1622
|
-
// Skill Memory Tools (D-1)
|
|
1623
|
-
// ============================================================================
|
|
1624
|
-
|
|
1625
|
-
registerTool(
|
|
1626
|
-
"store_skill",
|
|
1627
|
-
"Store an executable skill with trigger conditions, implementation, and verification steps. Side effect: persists a new skill entry and indexes it. Use when you identify a reusable procedure worth automating across sessions.",
|
|
1628
|
-
{
|
|
1629
|
-
name: z.string().min(1).max(120).describe("Unique skill identifier, e.g. 'deploy_production' or 'run_migrations'"),
|
|
1630
|
-
description: z.string().min(1).max(500).describe("Natural language description of what the skill does (used for semantic retrieval matching)"),
|
|
1631
|
-
triggerPattern: z.string().min(1).max(300).describe("Natural language pattern describing when to suggest this skill, e.g. 'user asks to deploy to production'"),
|
|
1632
|
-
implementationType: SkillImplementationTypeSchema.describe("Execution type: 'bash' for shell scripts, 'python' for Python code, 'mcp_tool_chain' for MCP sequences, 'instruction_sequence' for step-by-step instructions"),
|
|
1633
|
-
implementation: z.string().min(1).max(5000).describe("Executable content: the actual script, code, or instruction steps to run"),
|
|
1634
|
-
inputSchema: z.record(z.string(), z.unknown()).optional().describe("JSON Schema defining the skill's input parameters, e.g. {\"env\": {\"type\": \"string\"}}"),
|
|
1635
|
-
verification: z.string().max(500).optional().describe("Steps to verify the skill executed correctly, e.g. 'check deployment URL returns 200'"),
|
|
1636
|
-
scope: z.string().min(1).max(160).describe("Scope to store the skill under, e.g. 'project:recallnest'"),
|
|
1637
|
-
source: z.enum(["manual", "agent", "api"]).default("agent").describe("How this skill was captured: 'manual' by user, 'agent' by AI, or 'api' programmatically"),
|
|
1638
|
-
tags: z.array(z.string().max(60)).max(6).default([]).describe("Optional categorization tags, e.g. ['deployment', 'production']"),
|
|
1639
|
-
},
|
|
1640
|
-
async ({ name, description, triggerPattern, implementationType, implementation, inputSchema, verification, scope, source, tags }) => {
|
|
1641
|
-
const { store, embedder } = getComponents();
|
|
1642
|
-
const stored = await persistSkill(store, embedder, {
|
|
1643
|
-
name,
|
|
1644
|
-
description,
|
|
1645
|
-
triggerPattern,
|
|
1646
|
-
implementationType,
|
|
1647
|
-
implementation,
|
|
1648
|
-
inputSchema,
|
|
1649
|
-
verification,
|
|
1650
|
-
scope,
|
|
1651
|
-
source,
|
|
1652
|
-
tags,
|
|
1653
|
-
});
|
|
1654
|
-
|
|
1655
|
-
return {
|
|
1656
|
-
content: [{
|
|
1657
|
-
type: "text" as const,
|
|
1658
|
-
text: [
|
|
1659
|
-
`Stored skill ${stored.id.slice(0, 8)}`,
|
|
1660
|
-
`Name: ${stored.name}`,
|
|
1661
|
-
`Type: ${stored.implementationType}`,
|
|
1662
|
-
`Scope: ${stored.scope}`,
|
|
1663
|
-
`Tags: ${stored.tags.join(", ") || "-"}`,
|
|
1664
|
-
`Stored at: ${stored.storedAt}`,
|
|
1665
|
-
].join("\n"),
|
|
1666
|
-
}],
|
|
1667
|
-
};
|
|
1668
|
-
},
|
|
1669
|
-
);
|
|
1670
|
-
|
|
1671
|
-
registerTool(
|
|
1672
|
-
"retrieve_skill",
|
|
1673
|
-
"Retrieve executable skills matching a task description by semantic similarity. Read-only. Use when you need a stored procedure to act on, not just recall knowledge.",
|
|
1674
|
-
{
|
|
1675
|
-
query: z.string().min(1).max(300).describe("Natural language task description to match, e.g. 'deploy the app to production'"),
|
|
1676
|
-
scope: z.string().min(1).max(160).optional().describe("Restrict to skills in a specific scope, e.g. 'project:myapp'. Omit to search all scopes"),
|
|
1677
|
-
limit: z.number().min(1).max(10).default(3).describe("Maximum number of matching skills to return, sorted by relevance (default: 3)"),
|
|
1678
|
-
},
|
|
1679
|
-
async ({ query, scope, limit }) => {
|
|
1680
|
-
const { store, embedder } = getComponents();
|
|
1681
|
-
const results = await retrieveSkills(store, embedder, query, scope, limit);
|
|
1682
|
-
|
|
1683
|
-
if (results.length === 0) {
|
|
1684
|
-
return {
|
|
1685
|
-
content: [{
|
|
1686
|
-
type: "text" as const,
|
|
1687
|
-
text: "No matching skills found.",
|
|
1688
|
-
}],
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
const formatted = results.map(({ skill, score }, index) => [
|
|
1693
|
-
`## ${index + 1}. ${skill.name} (score: ${score.toFixed(3)})`,
|
|
1694
|
-
`**Description**: ${skill.description}`,
|
|
1695
|
-
`**Trigger**: ${skill.triggerPattern}`,
|
|
1696
|
-
`**Type**: ${skill.implementationType}`,
|
|
1697
|
-
skill.verification ? `**Verification**: ${skill.verification}` : null,
|
|
1698
|
-
`**Tags**: ${skill.tags.join(", ") || "-"}`,
|
|
1699
|
-
"",
|
|
1700
|
-
"```",
|
|
1701
|
-
skill.implementation,
|
|
1702
|
-
"```",
|
|
1703
|
-
].filter((line): line is string => line !== null).join("\n")).join("\n\n---\n\n");
|
|
1704
|
-
|
|
1705
|
-
return {
|
|
1706
|
-
content: [{
|
|
1707
|
-
type: "text" as const,
|
|
1708
|
-
text: `Found ${results.length} skill(s):\n\n${formatted}`,
|
|
1709
|
-
}],
|
|
1710
|
-
};
|
|
1711
|
-
},
|
|
1712
|
-
);
|
|
1713
|
-
|
|
1714
|
-
// --- D-2: scan_skill_promotions tool ---
|
|
1715
|
-
registerTool(
|
|
1716
|
-
"scan_skill_promotions",
|
|
1717
|
-
"Scan cases and patterns in a scope for potential promotion to reusable skills. Read-only. Use when you want to discover recurring procedures that deserve formalization as skills.",
|
|
1718
|
-
{
|
|
1719
|
-
scope: z.string().min(1).max(160).describe("Project scope to scan for promotion candidates"),
|
|
1720
|
-
minOccurrences: z.number().min(2).max(20).default(3).describe("Minimum similar cases to trigger a promotion suggestion"),
|
|
1721
|
-
},
|
|
1722
|
-
async ({ scope, minOccurrences }) => {
|
|
1723
|
-
const { store } = getComponents();
|
|
1724
|
-
const result = await scanForPromotions(store, scope, {
|
|
1725
|
-
minCaseOccurrences: minOccurrences,
|
|
1726
|
-
});
|
|
1727
|
-
|
|
1728
|
-
return {
|
|
1729
|
-
content: [{
|
|
1730
|
-
type: "text" as const,
|
|
1731
|
-
text: formatPromotionResult(result),
|
|
1732
|
-
}],
|
|
1733
|
-
};
|
|
1734
|
-
},
|
|
1735
|
-
);
|
|
1736
|
-
|
|
1737
|
-
// --- MCP-1: list_tools tool ---
|
|
1738
|
-
registerTool(
|
|
1739
|
-
"list_tools",
|
|
1740
|
-
"List available RecallNest tools with one-line descriptions, filtered by tier. Read-only. Use when you need to discover advanced or governance tools beyond the core set.",
|
|
1741
|
-
{
|
|
1742
|
-
tier: z.enum(["core", "advanced", "full"]).default("advanced").optional()
|
|
1743
|
-
.describe("Which tier of tools to list. Returns tools at this tier and below."),
|
|
1744
|
-
},
|
|
1745
|
-
async ({ tier }) => {
|
|
1746
|
-
const requestedTier = tier ?? "advanced";
|
|
1747
|
-
const tierOrder: Record<string, number> = { core: 0, advanced: 1, governance: 2 };
|
|
1748
|
-
const maxOrder = requestedTier === "full" ? 2 : tierOrder[requestedTier] ?? 1;
|
|
1749
|
-
|
|
1750
|
-
const lines: string[] = [`Available tools (tier: ${requestedTier}):`];
|
|
1751
|
-
for (const [toolName, toolTier] of Object.entries(TOOL_TIERS)) {
|
|
1752
|
-
if ((tierOrder[toolTier] ?? 999) > maxOrder) continue;
|
|
1753
|
-
const desc = TOOL_DESCRIPTIONS.get(toolName);
|
|
1754
|
-
const oneLiner = desc
|
|
1755
|
-
? desc.split(/[.!]\s/)[0]?.slice(0, 100) ?? desc.slice(0, 100)
|
|
1756
|
-
: "(no description)";
|
|
1757
|
-
lines.push(`- ${toolName}: ${oneLiner}`);
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
return {
|
|
1761
|
-
content: [{
|
|
1762
|
-
type: "text" as const,
|
|
1763
|
-
text: lines.join("\n"),
|
|
1764
|
-
}],
|
|
1765
|
-
};
|
|
1766
|
-
},
|
|
1767
|
-
);
|
|
1768
|
-
|
|
1769
|
-
// --- MCP-3: batch_store tool ---
|
|
1770
|
-
registerTool(
|
|
1771
|
-
"batch_store",
|
|
1772
|
-
"Store multiple memories in a single call with deduplication. Side effect: persists up to 20 entries. Use when you have several facts to store at once, more efficient than repeated store_memory calls.",
|
|
1773
|
-
{
|
|
1774
|
-
memories: z.array(z.object({
|
|
1775
|
-
text: z.string().min(1),
|
|
1776
|
-
category: DurableMemoryCategorySchema.default("events"),
|
|
1777
|
-
importance: z.number().min(0).max(1).default(0.7),
|
|
1778
|
-
tags: z.array(z.string()).max(6).default([]),
|
|
1779
|
-
})).min(1).max(20),
|
|
1780
|
-
scope: z.string().min(1).max(160),
|
|
1781
|
-
source: z.enum(["manual", "agent", "api"]).default("agent"),
|
|
1782
|
-
},
|
|
1783
|
-
async ({ memories, scope, source }) => {
|
|
1784
|
-
const { store, embedder } = getComponents();
|
|
1785
|
-
const stored = await persistMemoryBatch({
|
|
1786
|
-
store,
|
|
1787
|
-
embedder,
|
|
1788
|
-
conflictStore,
|
|
1789
|
-
kgExtractor,
|
|
1790
|
-
}, {
|
|
1791
|
-
scope,
|
|
1792
|
-
source,
|
|
1793
|
-
defaultImportance: 0.7,
|
|
1794
|
-
memories: memories.map((m) => ({
|
|
1795
|
-
text: m.text,
|
|
1796
|
-
category: m.category,
|
|
1797
|
-
importance: m.importance,
|
|
1798
|
-
tags: m.tags,
|
|
1799
|
-
})),
|
|
1800
|
-
});
|
|
1801
|
-
|
|
1802
|
-
const counts = { new: 0, deduped: 0, updated: 0 };
|
|
1803
|
-
for (const r of stored) {
|
|
1804
|
-
if (r.disposition === "deduped") counts.deduped++;
|
|
1805
|
-
else if (r.disposition === "updated") counts.updated++;
|
|
1806
|
-
else counts.new++;
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
return {
|
|
1810
|
-
content: [{
|
|
1811
|
-
type: "text" as const,
|
|
1812
|
-
text: `Stored ${stored.length} memories (${counts.new} new, ${counts.deduped} deduped, ${counts.updated} updated)`,
|
|
1813
|
-
}],
|
|
1814
|
-
};
|
|
1815
|
-
},
|
|
1816
|
-
);
|
|
1817
|
-
|
|
1818
|
-
// --- MP-2: import_conversations tool ---
|
|
1819
|
-
|
|
1820
|
-
registerTool(
|
|
1821
|
-
"import_conversations",
|
|
1822
|
-
"Import a conversation file (Claude Code JSONL, Claude.ai JSON, ChatGPT JSON, Slack JSON, or plaintext) into memory. Auto-detects format or use explicit format parameter. Messages are normalized and stored via the standard persistMemory pipeline.",
|
|
1823
|
-
{
|
|
1824
|
-
content: z.string().min(1).max(500_000).describe("Raw file content to import"),
|
|
1825
|
-
scope: z.string().min(1).max(160).describe("Target scope for imported memories, e.g. 'project:myapp'"),
|
|
1826
|
-
format: z.enum(["auto", "claude-code", "claude-ai", "chatgpt", "slack", "plaintext"]).default("auto").describe("Conversation format. Use 'auto' to detect automatically."),
|
|
1827
|
-
},
|
|
1828
|
-
async ({ content, scope, format }) => {
|
|
1829
|
-
const { detectFormat, normalizeConversation, ingestNormalizedMessages } = await import("./conversation-importer.js");
|
|
1830
|
-
const { embedder } = getComponents();
|
|
1831
|
-
|
|
1832
|
-
const resolvedFormat = format === "auto" ? detectFormat(content) : format;
|
|
1833
|
-
const messages = normalizeConversation(content, resolvedFormat);
|
|
1834
|
-
|
|
1835
|
-
if (messages.length === 0) {
|
|
1836
|
-
return {
|
|
1837
|
-
content: [{
|
|
1838
|
-
type: "text" as const,
|
|
1839
|
-
text: `No messages found (detected format: ${resolvedFormat})`,
|
|
1840
|
-
}],
|
|
1841
|
-
};
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
const result = await ingestNormalizedMessages(
|
|
1845
|
-
{ store, embedder, llm, conflictStore, kgExtractor },
|
|
1846
|
-
messages,
|
|
1847
|
-
scope,
|
|
1848
|
-
);
|
|
1849
|
-
|
|
1850
|
-
return {
|
|
1851
|
-
content: [{
|
|
1852
|
-
type: "text" as const,
|
|
1853
|
-
text: [
|
|
1854
|
-
`Import complete (format: ${resolvedFormat})`,
|
|
1855
|
-
`Total: ${result.total}`,
|
|
1856
|
-
`Stored: ${result.stored}`,
|
|
1857
|
-
`Rejected: ${result.rejected}`,
|
|
1858
|
-
result.errors.length > 0 ? `Errors: ${result.errors.join("; ")}` : null,
|
|
1859
|
-
].filter(Boolean).join("\n"),
|
|
1860
|
-
}],
|
|
1861
|
-
};
|
|
1862
|
-
},
|
|
1863
|
-
);
|
|
1864
|
-
|
|
1865
|
-
// --- S15: distill_session tool ---
|
|
1866
|
-
registerTool(
|
|
1867
|
-
"distill_session",
|
|
1868
|
-
"Distill a conversation session into structured knowledge and persist to long-term memory. Three layers: (1) microcompact clears old tool results at zero cost, (2) LLM summarizes into 9 dimensions, (3) extracts durable knowledge into RecallNest. Use when a session is ending or context is getting large. Side effect: persists extracted memories.",
|
|
1869
|
-
{
|
|
1870
|
-
messages: z.array(z.object({
|
|
1871
|
-
role: z.enum(["user", "assistant", "system"]),
|
|
1872
|
-
content: z.union([
|
|
1873
|
-
z.string(),
|
|
1874
|
-
z.array(z.object({
|
|
1875
|
-
type: z.enum(["text", "tool_use", "tool_result"]),
|
|
1876
|
-
name: z.string().optional(),
|
|
1877
|
-
id: z.string().optional(),
|
|
1878
|
-
input: z.record(z.unknown()).optional(),
|
|
1879
|
-
content: z.string().optional(),
|
|
1880
|
-
text: z.string().optional(),
|
|
1881
|
-
tool_use_id: z.string().optional(),
|
|
1882
|
-
})),
|
|
1883
|
-
]),
|
|
1884
|
-
})).min(1).max(500).describe("Conversation messages to distill"),
|
|
1885
|
-
scope: z.string().min(1).max(160).describe("Memory scope for persisted knowledge, e.g. 'project:recallnest'"),
|
|
1886
|
-
preserveRecent: z.number().min(0).max(20).default(6).describe("Keep the N most recent messages verbatim (default: 6)"),
|
|
1887
|
-
keepRecentTools: z.number().min(0).max(20).default(5).describe("Keep the N most recent tool results during microcompact (default: 5)"),
|
|
1888
|
-
persist: z.boolean().default(true).describe("Whether to persist extracted knowledge to RecallNest (default: true)"),
|
|
1889
|
-
},
|
|
1890
|
-
async ({ messages, scope, preserveRecent, keepRecentTools, persist }) => {
|
|
1891
|
-
const { store, embedder } = getComponents();
|
|
1892
|
-
const { distillSession } = await import("./session-distiller.js");
|
|
1893
|
-
|
|
1894
|
-
const result = await distillSession(
|
|
1895
|
-
{ messages, scope, preserveRecent, keepRecentTools, persist },
|
|
1896
|
-
{
|
|
1897
|
-
llm: llmClient,
|
|
1898
|
-
persistMemory: async (input) => {
|
|
1899
|
-
const stored = await persistMemory({
|
|
1900
|
-
store, embedder, conflictStore, kgExtractor,
|
|
1901
|
-
}, input);
|
|
1902
|
-
return { disposition: stored.disposition, id: stored.id };
|
|
1903
|
-
},
|
|
1904
|
-
},
|
|
1905
|
-
);
|
|
1906
|
-
|
|
1907
|
-
const lines = [
|
|
1908
|
-
`Microcompact: ${result.microcompact.toolsCleared} tool results cleared, ~${result.microcompact.tokensFreed} tokens freed`,
|
|
1909
|
-
];
|
|
1910
|
-
if (result.summary) {
|
|
1911
|
-
lines.push(`Summary: 9-dimension structured summary generated`);
|
|
1912
|
-
}
|
|
1913
|
-
if (result.persisted) {
|
|
1914
|
-
const p = result.persisted;
|
|
1915
|
-
lines.push(`Persisted: ${p.memoriesStored} stored, ${p.memoriesDeduped} deduped, ${p.memoriesConflicted} conflicted, ${p.memoriesRejected} rejected`);
|
|
1916
|
-
}
|
|
195
|
+
const toolDeps: ToolRegistryDeps = {
|
|
196
|
+
registerTool,
|
|
197
|
+
getComponents,
|
|
198
|
+
auditLogger: createAuditLogger(),
|
|
199
|
+
config,
|
|
200
|
+
checkpointStore,
|
|
201
|
+
conflictStore,
|
|
202
|
+
workflowObservationStore,
|
|
203
|
+
toolDescriptions: TOOL_DESCRIPTIONS,
|
|
204
|
+
toolTiers: TOOL_TIERS,
|
|
205
|
+
getKGExtractor: () => kgExtractor,
|
|
206
|
+
getKGStore: () => kgStoreInstance,
|
|
207
|
+
};
|
|
1917
208
|
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
text: lines.join("\n"),
|
|
1922
|
-
}],
|
|
1923
|
-
};
|
|
1924
|
-
},
|
|
1925
|
-
);
|
|
209
|
+
registerCoreTools(toolDeps);
|
|
210
|
+
registerAdvancedTools(toolDeps);
|
|
211
|
+
registerGovernanceTools(toolDeps);
|
|
1926
212
|
|
|
1927
213
|
// ============================================================================
|
|
1928
214
|
// Global error handlers — prevent silent crashes from unhandled async errors
|