@4da/mcp-server 4.6.1 → 4.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/db.d.ts +21 -2
- package/dist/db.js +66 -16
- package/dist/doctor.js +5 -3
- package/dist/embeddings.d.ts +88 -0
- package/dist/embeddings.js +292 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.js +21 -15
- package/dist/live/cvss.d.ts +16 -0
- package/dist/live/cvss.js +62 -0
- package/dist/live/index.d.ts +9 -1
- package/dist/live/index.js +40 -3
- package/dist/live/npm-registry.js +29 -6
- package/dist/live/osv-scanner.d.ts +8 -0
- package/dist/live/osv-scanner.js +115 -30
- package/dist/live/platform.d.ts +32 -0
- package/dist/live/platform.js +156 -0
- package/dist/live/semver-utils.d.ts +8 -0
- package/dist/live/semver-utils.js +24 -0
- package/dist/live/types.d.ts +31 -9
- package/dist/live/version-resolver.d.ts +12 -2
- package/dist/live/version-resolver.js +63 -20
- package/dist/llm.js +1 -1
- package/dist/project-scanner.d.ts +2 -0
- package/dist/project-scanner.js +34 -0
- package/dist/scheduler/core.d.ts +25 -0
- package/dist/scheduler/core.js +61 -0
- package/dist/scheduler/freshness.d.ts +17 -0
- package/dist/scheduler/freshness.js +41 -0
- package/dist/scheduler/index.d.ts +21 -0
- package/dist/scheduler/index.js +32 -0
- package/dist/scheduler/refreshers.d.ts +21 -0
- package/dist/scheduler/refreshers.js +21 -0
- package/dist/schemas/get-relevant-content.json +1 -1
- package/dist/schemas/upgrade-planner.json +1 -1
- package/dist/schemas/what-should-i-know.json +1 -1
- package/dist/tools/agent-memory.d.ts +11 -1
- package/dist/tools/agent-memory.js +120 -14
- package/dist/tools/decision-enforcement.d.ts +17 -1
- package/dist/tools/decision-enforcement.js +109 -81
- package/dist/tools/decision-memory.d.ts +1 -1
- package/dist/tools/decision-memory.js +58 -28
- package/dist/tools/decision-recall.d.ts +59 -0
- package/dist/tools/decision-recall.js +87 -0
- package/dist/tools/dependency-health.js +6 -4
- package/dist/tools/developer-dna.js +4 -2
- package/dist/tools/knowledge-gaps.js +16 -4
- package/dist/tools/recall.d.ts +33 -0
- package/dist/tools/recall.js +134 -0
- package/dist/tools/upgrade-planner.d.ts +25 -1
- package/dist/tools/upgrade-planner.js +111 -30
- package/dist/tools/vulnerability-scan.d.ts +46 -0
- package/dist/tools/vulnerability-scan.js +64 -24
- package/dist/tools/what-should-i-know.d.ts +3 -1
- package/dist/tools/what-should-i-know.js +210 -106
- package/dist/types.d.ts +9 -0
- package/package.json +8 -3
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js.map +0 -1
- package/dist/doctor.d.ts.map +0 -1
- package/dist/doctor.js.map +0 -1
- package/dist/http-transport.d.ts.map +0 -1
- package/dist/http-transport.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/live/cache.d.ts.map +0 -1
- package/dist/live/cache.js.map +0 -1
- package/dist/live/crates-registry.d.ts.map +0 -1
- package/dist/live/crates-registry.js.map +0 -1
- package/dist/live/go-registry.d.ts.map +0 -1
- package/dist/live/go-registry.js.map +0 -1
- package/dist/live/hn-fetcher.d.ts.map +0 -1
- package/dist/live/hn-fetcher.js.map +0 -1
- package/dist/live/http-utils.d.ts.map +0 -1
- package/dist/live/http-utils.js.map +0 -1
- package/dist/live/index.d.ts.map +0 -1
- package/dist/live/index.js.map +0 -1
- package/dist/live/npm-registry.d.ts.map +0 -1
- package/dist/live/npm-registry.js.map +0 -1
- package/dist/live/osv-scanner.d.ts.map +0 -1
- package/dist/live/osv-scanner.js.map +0 -1
- package/dist/live/pypi-registry.d.ts.map +0 -1
- package/dist/live/pypi-registry.js.map +0 -1
- package/dist/live/rate-limiter.d.ts.map +0 -1
- package/dist/live/rate-limiter.js.map +0 -1
- package/dist/live/semver-utils.d.ts.map +0 -1
- package/dist/live/semver-utils.js.map +0 -1
- package/dist/live/types.d.ts.map +0 -1
- package/dist/live/types.js.map +0 -1
- package/dist/live/version-resolver.d.ts.map +0 -1
- package/dist/live/version-resolver.js.map +0 -1
- package/dist/live-singleton.d.ts.map +0 -1
- package/dist/live-singleton.js.map +0 -1
- package/dist/llm.d.ts.map +0 -1
- package/dist/llm.js.map +0 -1
- package/dist/output-manager.d.ts.map +0 -1
- package/dist/output-manager.js.map +0 -1
- package/dist/project-scanner.d.ts.map +0 -1
- package/dist/project-scanner.js.map +0 -1
- package/dist/schema-registry.d.ts.map +0 -1
- package/dist/schema-registry.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/tool-dispatch.d.ts.map +0 -1
- package/dist/tool-dispatch.js.map +0 -1
- package/dist/tools/agent-memory.d.ts.map +0 -1
- package/dist/tools/agent-memory.js.map +0 -1
- package/dist/tools/decision-enforcement.d.ts.map +0 -1
- package/dist/tools/decision-enforcement.js.map +0 -1
- package/dist/tools/decision-memory.d.ts.map +0 -1
- package/dist/tools/decision-memory.js.map +0 -1
- package/dist/tools/dependency-health.d.ts.map +0 -1
- package/dist/tools/dependency-health.js.map +0 -1
- package/dist/tools/developer-dna.d.ts.map +0 -1
- package/dist/tools/developer-dna.js.map +0 -1
- package/dist/tools/ecosystem-pulse.d.ts.map +0 -1
- package/dist/tools/ecosystem-pulse.js.map +0 -1
- package/dist/tools/get-actionable-signals.d.ts.map +0 -1
- package/dist/tools/get-actionable-signals.js.map +0 -1
- package/dist/tools/get-context.d.ts.map +0 -1
- package/dist/tools/get-context.js.map +0 -1
- package/dist/tools/get-relevant-content.d.ts.map +0 -1
- package/dist/tools/get-relevant-content.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/knowledge-gaps.d.ts.map +0 -1
- package/dist/tools/knowledge-gaps.js.map +0 -1
- package/dist/tools/record-feedback.d.ts.map +0 -1
- package/dist/tools/record-feedback.js.map +0 -1
- package/dist/tools/upgrade-planner.d.ts.map +0 -1
- package/dist/tools/upgrade-planner.js.map +0 -1
- package/dist/tools/vulnerability-scan.d.ts.map +0 -1
- package/dist/tools/vulnerability-scan.js.map +0 -1
- package/dist/tools/what-should-i-know.d.ts.map +0 -1
- package/dist/tools/what-should-i-know.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler composition (S4) — wire core + freshness + refreshers into one switchable unit.
|
|
3
|
+
*
|
|
4
|
+
* The whole scheduler is OFF unless explicitly enabled (fail-safe: an MCP server must never
|
|
5
|
+
* grow background network activity by surprise). Enabled, it registers every refresh unit on
|
|
6
|
+
* the cadence engine, starts the timers, and exposes a status view the server (or a future
|
|
7
|
+
* `scheduler_status` tool) can render: every unit, its cadence, and when it last ran.
|
|
8
|
+
*/
|
|
9
|
+
import { type SchedulerJobView } from "./core.js";
|
|
10
|
+
export interface InitSchedulerOpts {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
pull: Record<string, () => Promise<void>>;
|
|
13
|
+
freshnessPath?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RunningScheduler {
|
|
16
|
+
started: boolean;
|
|
17
|
+
status(): SchedulerJobView[];
|
|
18
|
+
stop(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function initScheduler(opts: InitSchedulerOpts): RunningScheduler;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler composition (S4) — wire core + freshness + refreshers into one switchable unit.
|
|
3
|
+
*
|
|
4
|
+
* The whole scheduler is OFF unless explicitly enabled (fail-safe: an MCP server must never
|
|
5
|
+
* grow background network activity by surprise). Enabled, it registers every refresh unit on
|
|
6
|
+
* the cadence engine, starts the timers, and exposes a status view the server (or a future
|
|
7
|
+
* `scheduler_status` tool) can render: every unit, its cadence, and when it last ran.
|
|
8
|
+
*/
|
|
9
|
+
import { createScheduler } from "./core.js";
|
|
10
|
+
import { createFreshness } from "./freshness.js";
|
|
11
|
+
import { buildRefreshers } from "./refreshers.js";
|
|
12
|
+
export function initScheduler(opts) {
|
|
13
|
+
if (!opts.enabled) {
|
|
14
|
+
return { started: false, status: () => [], stop: () => { } };
|
|
15
|
+
}
|
|
16
|
+
const freshness = createFreshness(opts.freshnessPath);
|
|
17
|
+
freshness.load();
|
|
18
|
+
const scheduler = createScheduler();
|
|
19
|
+
for (const unit of buildRefreshers({ pull: opts.pull, freshness })) {
|
|
20
|
+
scheduler.register(unit.name, unit.intervalMs, () => unit.run());
|
|
21
|
+
}
|
|
22
|
+
scheduler.start();
|
|
23
|
+
return {
|
|
24
|
+
started: true,
|
|
25
|
+
status: () => scheduler.jobs(),
|
|
26
|
+
stop: () => {
|
|
27
|
+
scheduler.stop();
|
|
28
|
+
freshness.save();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refresh adapters (S3) — wrap each named data pull into a schedulable refresh unit.
|
|
3
|
+
*
|
|
4
|
+
* Pure dependency injection: the pulls and the freshness sink are handed in, nothing is
|
|
5
|
+
* imported from the fetcher layer and nothing touches the network here. A refresh unit runs
|
|
6
|
+
* its pull and only THEN marks freshness — a failing pull rethrows and leaves the watermark
|
|
7
|
+
* untouched, so staleness stays honest.
|
|
8
|
+
*/
|
|
9
|
+
export interface RefreshDeps {
|
|
10
|
+
pull: Record<string, () => Promise<void>>;
|
|
11
|
+
freshness: {
|
|
12
|
+
markFresh(name: string, at?: number): void;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface RefreshUnit {
|
|
16
|
+
name: string;
|
|
17
|
+
intervalMs: number;
|
|
18
|
+
run(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export declare function buildRefreshers(deps: RefreshDeps): RefreshUnit[];
|
|
21
|
+
//# sourceMappingURL=refreshers.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refresh adapters (S3) — wrap each named data pull into a schedulable refresh unit.
|
|
3
|
+
*
|
|
4
|
+
* Pure dependency injection: the pulls and the freshness sink are handed in, nothing is
|
|
5
|
+
* imported from the fetcher layer and nothing touches the network here. A refresh unit runs
|
|
6
|
+
* its pull and only THEN marks freshness — a failing pull rethrows and leaves the watermark
|
|
7
|
+
* untouched, so staleness stays honest.
|
|
8
|
+
*/
|
|
9
|
+
/** Default cadence per refresh unit; callers can re-register with their own interval. */
|
|
10
|
+
const DEFAULT_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes — fresh enough for feed data, kind to upstreams
|
|
11
|
+
export function buildRefreshers(deps) {
|
|
12
|
+
return Object.entries(deps.pull).map(([name, pull]) => ({
|
|
13
|
+
name,
|
|
14
|
+
intervalMs: DEFAULT_INTERVAL_MS,
|
|
15
|
+
async run() {
|
|
16
|
+
await pull(); // a throw propagates — freshness must NOT advance on a failed pull
|
|
17
|
+
deps.freshness.markFresh(name);
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=refreshers.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "get_relevant_content",
|
|
3
|
-
"description": "Get filtered relevant content from 4DA's personalized feed.\n\nReturns content items that match the user's interests, tech stack, and learned preferences.\nItems are scored based on:\n- Explicit interests declared by the user\n- Tech stack and domains\n- ACE-detected context (recent files, projects, git activity)\n- Learned preferences from past interactions\n\nCall when the user asks what to read, what is relevant to them, or for content on a topic.",
|
|
3
|
+
"description": "Get filtered relevant content from 4DA's personalized feed.\n\nReturns content items that match the user's interests, tech stack, and learned preferences.\nItems are scored based on:\n- Explicit interests declared by the user\n- Tech stack and domains\n- ACE-detected context (recent files, projects, git activity)\n- Learned preferences from past interactions\n\nIMPORTANT for autonomous use: each item carries an `evidence_class` describing how it is grounded. The relevance_score float is NOT comparable across classes — gate actions on evidence_class, not the number:\n- \"osv_verified\": a security advisory matching one of the user's INSTALLED dependencies — safe to act on\n- \"dependency_grounded\": matched one of the user's declared dependencies — trustworthy\n- \"semantic_only\": topical/embedding relevance with NO dependency edge — surface to the human, verify before acting\n- \"keyword_heuristic\": standalone-mode keyword score (no Rust pipeline) — weakest signal\n\nCall when the user asks what to read, what is relevant to them, or for content on a topic.",
|
|
4
4
|
"inputSchema": {
|
|
5
5
|
"type": "object",
|
|
6
6
|
"properties": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "upgrade_planner",
|
|
3
|
-
"description": "Ranked upgrade recommendations for project dependencies. Combines vulnerability severity, deprecation status, and version distance
|
|
3
|
+
"description": "Ranked standalone upgrade recommendations for project dependencies — call before upgrading, adding, or auditing any dependency to pick the safest order. Combines vulnerability severity, deprecation status, and version distance into a prioritized plan.\n\nSplits recommendations three ways: quick wins (safe patch/minor upgrades), breaking changes (major version bumps), and transitive CVEs marked waiting_on_upstream (not declared in your manifest — the fix arrives via a parent package update or a lockfile refresh). Call dependency_health first for registry context and vulnerability_scan first for a CVE-aware plan; without a scan the output says it is CVE-blind.\n\nProvenance: standalone heuristic — a point-in-time plan from local lockfiles plus live registry/OSV lookups, not the 4DA desktop app's cross-project, version-confirmed plan.\n\nPrivacy: live mode may query public registries and OSV with package names and versions. It never sends source code.",
|
|
4
4
|
"inputSchema": {
|
|
5
5
|
"type": "object",
|
|
6
6
|
"properties": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what_should_i_know",
|
|
3
|
-
"description": "Pre-task intelligence briefing. Given a task description and optional file paths, returns filtered advisories (security alerts, breaking changes), active decision windows, signal chains, relevant past decisions/memories, and a delegation assessment. Call this before starting any non-trivial task to get caught up on what matters.",
|
|
3
|
+
"description": "Pre-task intelligence briefing. Given a task description and optional file paths, returns filtered advisories (security alerts, breaking changes), active decision windows, signal chains, relevant past decisions/memories, and a delegation assessment. Call this before starting any non-trivial task to get caught up on what matters. If the task involves upgrading, adding, or auditing dependencies, follow with upgrade_planner for the ranked plan.",
|
|
4
4
|
"inputSchema": {
|
|
5
5
|
"type": "object",
|
|
6
6
|
"properties": {
|
|
@@ -90,5 +90,15 @@ export declare const agentMemoryTool: {
|
|
|
90
90
|
required: string[];
|
|
91
91
|
};
|
|
92
92
|
};
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* The text embedded for a memory. Exported so every surface that embeds a memory
|
|
95
|
+
* row (this tool AND the what_should_i_know briefing) uses IDENTICAL text —
|
|
96
|
+
* otherwise they would write conflicting vectors into the shared
|
|
97
|
+
* agent_memory.embedding column.
|
|
98
|
+
*/
|
|
99
|
+
export declare function memoryEmbedText(row: {
|
|
100
|
+
subject: string;
|
|
101
|
+
content: string;
|
|
102
|
+
}): string;
|
|
103
|
+
export declare function executeAgentMemory(db: FourDADatabase, params: AgentMemoryParams): object | Promise<object>;
|
|
94
104
|
//# sourceMappingURL=agent-memory.d.ts.map
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Cross-agent persistent memory. What Claude Code learns, Cursor can access.
|
|
6
6
|
* Enables AI agents to store and recall memories across sessions and tools.
|
|
7
7
|
*/
|
|
8
|
+
import { rankRowsByRecall } from "./recall.js";
|
|
9
|
+
import { getEmbeddingConfig, semanticScores, } from "../embeddings.js";
|
|
8
10
|
// ============================================================================
|
|
9
11
|
// Tool Definition
|
|
10
12
|
// ============================================================================
|
|
@@ -81,6 +83,24 @@ export const agentMemoryTool = {
|
|
|
81
83
|
const MAX_CONTENT_BYTES = 10 * 1024;
|
|
82
84
|
/** Maximum entries per agent */
|
|
83
85
|
const MAX_ENTRIES_PER_AGENT = 1000;
|
|
86
|
+
/** Weighted text fields for lexical recall ranking (shared by lexical + hybrid paths). */
|
|
87
|
+
const MEMORY_RECALL_FIELDS = [
|
|
88
|
+
{ name: "subject", weight: 4, value: (row) => row.subject },
|
|
89
|
+
{ name: "tags", weight: 3, value: (row) => row.context_tags },
|
|
90
|
+
{ name: "content", weight: 2, value: (row) => row.content },
|
|
91
|
+
{ name: "type", weight: 1, value: (row) => row.memory_type },
|
|
92
|
+
];
|
|
93
|
+
/** Blend weight for semantic cosine vs normalized lexical score in hybrid recall. */
|
|
94
|
+
const SEMANTIC_BLEND = 0.65;
|
|
95
|
+
/**
|
|
96
|
+
* The text embedded for a memory. Exported so every surface that embeds a memory
|
|
97
|
+
* row (this tool AND the what_should_i_know briefing) uses IDENTICAL text —
|
|
98
|
+
* otherwise they would write conflicting vectors into the shared
|
|
99
|
+
* agent_memory.embedding column.
|
|
100
|
+
*/
|
|
101
|
+
export function memoryEmbedText(row) {
|
|
102
|
+
return `${row.subject}\n${row.content}`;
|
|
103
|
+
}
|
|
84
104
|
// ============================================================================
|
|
85
105
|
// Helpers
|
|
86
106
|
// ============================================================================
|
|
@@ -92,12 +112,21 @@ function parseMemoryRow(row) {
|
|
|
92
112
|
memory_type: row.memory_type,
|
|
93
113
|
subject: row.subject,
|
|
94
114
|
content: row.content,
|
|
95
|
-
context_tags:
|
|
115
|
+
context_tags: parseStringList(row.context_tags),
|
|
96
116
|
created_at: row.created_at,
|
|
97
117
|
expires_at: row.expires_at,
|
|
98
118
|
promoted_to_decision_id: row.promoted_to_decision_id,
|
|
99
119
|
};
|
|
100
120
|
}
|
|
121
|
+
function parseStringList(json) {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(json || "[]");
|
|
124
|
+
return Array.isArray(parsed) ? parsed.map(String) : [];
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
101
130
|
// ============================================================================
|
|
102
131
|
// Execute
|
|
103
132
|
// ============================================================================
|
|
@@ -161,24 +190,28 @@ export function executeAgentMemory(db, params) {
|
|
|
161
190
|
return { error: "query is required for recall action" };
|
|
162
191
|
}
|
|
163
192
|
try {
|
|
164
|
-
const pattern = `%${params.query.toLowerCase()}%`;
|
|
165
193
|
let sql = `SELECT id, session_id, agent_type, memory_type, subject, content,
|
|
166
194
|
context_tags, created_at, expires_at, promoted_to_decision_id
|
|
167
195
|
FROM agent_memory
|
|
168
|
-
WHERE (
|
|
169
|
-
|
|
170
|
-
const sqlParams = [pattern, pattern];
|
|
196
|
+
WHERE (expires_at IS NULL OR expires_at > datetime('now'))`;
|
|
197
|
+
const sqlParams = [];
|
|
171
198
|
if (params.filter_agent) {
|
|
172
199
|
sql += ` AND agent_type = ?`;
|
|
173
200
|
sqlParams.push(params.filter_agent);
|
|
174
201
|
}
|
|
175
202
|
sql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
176
|
-
sqlParams.push(
|
|
203
|
+
sqlParams.push(MAX_ENTRIES_PER_AGENT);
|
|
177
204
|
const rows = rawDb.prepare(sql).all(...sqlParams);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
205
|
+
const limit = params.limit || 20;
|
|
206
|
+
// Optional semantic recall — only when an embedding provider is configured.
|
|
207
|
+
// Returns a Promise (dispatch awaits uniformly); every other path stays
|
|
208
|
+
// synchronous, so callers without a provider behave exactly as before.
|
|
209
|
+
const embedConfig = getEmbeddingConfig();
|
|
210
|
+
if (embedConfig) {
|
|
211
|
+
return enhanceRecallWithSemantics(db, params.query, rows, limit, embedConfig);
|
|
212
|
+
}
|
|
213
|
+
const ranked = rankRowsByRecall(rows, params.query, MEMORY_RECALL_FIELDS, limit);
|
|
214
|
+
return lexicalRecallResult(ranked, rows.length);
|
|
182
215
|
}
|
|
183
216
|
catch (error) {
|
|
184
217
|
return {
|
|
@@ -196,7 +229,7 @@ export function executeAgentMemory(db, params) {
|
|
|
196
229
|
return { error: "tags array is required for recall_by_tags action" };
|
|
197
230
|
}
|
|
198
231
|
try {
|
|
199
|
-
//
|
|
232
|
+
// Prefilter to entries whose tags literally contain ANY requested tag...
|
|
200
233
|
const conditions = params.tags.map(() => `LOWER(context_tags) LIKE ?`);
|
|
201
234
|
const tagParams = params.tags.map((t) => `%${t.toLowerCase()}%`);
|
|
202
235
|
const sql = `SELECT id, session_id, agent_type, memory_type, subject, content,
|
|
@@ -207,10 +240,23 @@ export function executeAgentMemory(db, params) {
|
|
|
207
240
|
ORDER BY created_at DESC LIMIT ?`;
|
|
208
241
|
const rows = rawDb
|
|
209
242
|
.prepare(sql)
|
|
210
|
-
.all(...tagParams,
|
|
243
|
+
.all(...tagParams, MAX_ENTRIES_PER_AGENT);
|
|
244
|
+
// ...then rank by relevance so the strongest tag/subject/content matches
|
|
245
|
+
// lead, returning the same shape as `recall` (matched_fields, recall_score).
|
|
246
|
+
const ranked = rankRowsByRecall(rows, params.tags.join(" "), [
|
|
247
|
+
{ name: "tags", weight: 4, value: (row) => row.context_tags },
|
|
248
|
+
{ name: "subject", weight: 2, value: (row) => row.subject },
|
|
249
|
+
{ name: "content", weight: 1, value: (row) => row.content },
|
|
250
|
+
], params.limit || 20);
|
|
211
251
|
return {
|
|
212
|
-
memories:
|
|
213
|
-
|
|
252
|
+
memories: ranked.map((item) => ({
|
|
253
|
+
...parseMemoryRow(item.row),
|
|
254
|
+
matched_fields: item.matched_fields,
|
|
255
|
+
recall_score: item.score,
|
|
256
|
+
})),
|
|
257
|
+
count: ranked.length,
|
|
258
|
+
candidate_count: rows.length,
|
|
259
|
+
recall_mode: "ranked_lexical",
|
|
214
260
|
};
|
|
215
261
|
}
|
|
216
262
|
catch (error) {
|
|
@@ -259,4 +305,64 @@ export function executeAgentMemory(db, params) {
|
|
|
259
305
|
return { error: `Unknown action: ${params.action}` };
|
|
260
306
|
}
|
|
261
307
|
}
|
|
308
|
+
/** Shape a lexical-only recall response (the default, provider-free path). */
|
|
309
|
+
function lexicalRecallResult(ranked, candidateCount) {
|
|
310
|
+
return {
|
|
311
|
+
memories: ranked.map((item) => ({
|
|
312
|
+
...parseMemoryRow(item.row),
|
|
313
|
+
matched_fields: item.matched_fields,
|
|
314
|
+
recall_score: item.score,
|
|
315
|
+
})),
|
|
316
|
+
count: ranked.length,
|
|
317
|
+
candidate_count: candidateCount,
|
|
318
|
+
recall_mode: "ranked_lexical",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Hybrid recall: blends alias-aware lexical scoring with cosine similarity over
|
|
323
|
+
* provider-backed embeddings. Embeddings are cached in the agent_memory table and
|
|
324
|
+
* backfilled lazily (capped per call). ANY failure to embed the query degrades to
|
|
325
|
+
* pure lexical recall, and recall_mode reports exactly which path produced the result.
|
|
326
|
+
*/
|
|
327
|
+
async function enhanceRecallWithSemantics(db, query, rows, limit, config) {
|
|
328
|
+
// Lexical baseline over ALL candidates — used for blending and as the fallback.
|
|
329
|
+
const lexical = rankRowsByRecall(rows, query, MEMORY_RECALL_FIELDS, rows.length);
|
|
330
|
+
const lexById = new Map();
|
|
331
|
+
for (const item of lexical)
|
|
332
|
+
lexById.set(item.row.id, item);
|
|
333
|
+
const maxLex = lexical.length ? lexical[0].score : 0;
|
|
334
|
+
const sem = await semanticScores(db, "agent_memory", query, rows.map((row) => ({ id: row.id, text: memoryEmbedText(row) })), config);
|
|
335
|
+
// Query could not be embedded -> the provider is effectively unavailable.
|
|
336
|
+
if (!sem.queryEmbedded) {
|
|
337
|
+
return {
|
|
338
|
+
...lexicalRecallResult(lexical.slice(0, limit), rows.length),
|
|
339
|
+
semantic_note: "embedding provider unreachable — used lexical recall",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
const blended = rows
|
|
343
|
+
.map((row) => {
|
|
344
|
+
const semantic = Math.max(0, sem.semanticById.get(row.id) ?? 0);
|
|
345
|
+
const lex = lexById.get(row.id);
|
|
346
|
+
const lexNorm = maxLex > 0 && lex ? lex.score / maxLex : 0;
|
|
347
|
+
const score = SEMANTIC_BLEND * semantic + (1 - SEMANTIC_BLEND) * lexNorm;
|
|
348
|
+
return { row, semantic, matched: lex?.matched_fields ?? [], score };
|
|
349
|
+
})
|
|
350
|
+
.filter((x) => x.score > 0)
|
|
351
|
+
.sort((a, b) => b.score - a.score)
|
|
352
|
+
.slice(0, limit);
|
|
353
|
+
return {
|
|
354
|
+
memories: blended.map((x) => ({
|
|
355
|
+
...parseMemoryRow(x.row),
|
|
356
|
+
matched_fields: x.matched,
|
|
357
|
+
recall_score: Math.round(x.score * 1000) / 1000,
|
|
358
|
+
semantic_score: Math.round(x.semantic * 1000) / 1000,
|
|
359
|
+
})),
|
|
360
|
+
count: blended.length,
|
|
361
|
+
candidate_count: rows.length,
|
|
362
|
+
embedded_candidates: sem.embeddedCount,
|
|
363
|
+
newly_embedded: sem.newlyEmbedded,
|
|
364
|
+
embedding_model: sem.model,
|
|
365
|
+
recall_mode: sem.embeddedCount > 0 ? "hybrid" : "ranked_lexical",
|
|
366
|
+
};
|
|
367
|
+
}
|
|
262
368
|
//# sourceMappingURL=agent-memory.js.map
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* The key tool AI agents call BEFORE suggesting major changes.
|
|
5
5
|
* Checks if a technology or pattern aligns with the developer's active decisions.
|
|
6
6
|
* Returns alignment status, relevant decisions, and any conflicts.
|
|
7
|
+
*
|
|
8
|
+
* Retrieval of "relevant decisions" is lexical by default and OPTIONALLY hybrid
|
|
9
|
+
* (semantic) when an embedding provider is configured — so a decision that is a
|
|
10
|
+
* paraphrase of the proposal surfaces even with no shared words. Hard conflict
|
|
11
|
+
* detection ("X was explicitly rejected") stays lexical/alias-aware so semantic
|
|
12
|
+
* similarity can never fabricate a grounded conflict; paraphrase risks are
|
|
13
|
+
* reported separately as `possible_conflicts`.
|
|
7
14
|
*/
|
|
8
15
|
import type { FourDADatabase } from "../db.js";
|
|
9
16
|
export interface CheckDecisionAlignmentParams {
|
|
@@ -24,13 +31,22 @@ interface DecisionConflict {
|
|
|
24
31
|
subject: string;
|
|
25
32
|
reason: string;
|
|
26
33
|
}
|
|
34
|
+
interface PossibleConflict {
|
|
35
|
+
decision_id: number;
|
|
36
|
+
subject: string;
|
|
37
|
+
similarity: number;
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
27
40
|
interface AlignmentResult {
|
|
28
41
|
aligned: boolean;
|
|
29
42
|
technology: string;
|
|
30
43
|
relevant_decisions: RelevantDecision[];
|
|
31
44
|
conflicts: DecisionConflict[];
|
|
45
|
+
/** Paraphrase risks: semantically close to a rejected alternative, not grounded. */
|
|
46
|
+
possible_conflicts: PossibleConflict[];
|
|
32
47
|
confidence: number;
|
|
33
48
|
recommendation: string;
|
|
49
|
+
recall_mode: "hybrid" | "ranked_lexical";
|
|
34
50
|
}
|
|
35
51
|
export declare const checkDecisionAlignmentTool: {
|
|
36
52
|
name: string;
|
|
@@ -54,6 +70,6 @@ export declare const checkDecisionAlignmentTool: {
|
|
|
54
70
|
required: string[];
|
|
55
71
|
};
|
|
56
72
|
};
|
|
57
|
-
export declare function executeCheckDecisionAlignment(db: FourDADatabase, params: CheckDecisionAlignmentParams): AlignmentResult
|
|
73
|
+
export declare function executeCheckDecisionAlignment(db: FourDADatabase, params: CheckDecisionAlignmentParams): AlignmentResult | Promise<AlignmentResult>;
|
|
58
74
|
export {};
|
|
59
75
|
//# sourceMappingURL=decision-enforcement.d.ts.map
|
|
@@ -5,7 +5,17 @@
|
|
|
5
5
|
* The key tool AI agents call BEFORE suggesting major changes.
|
|
6
6
|
* Checks if a technology or pattern aligns with the developer's active decisions.
|
|
7
7
|
* Returns alignment status, relevant decisions, and any conflicts.
|
|
8
|
+
*
|
|
9
|
+
* Retrieval of "relevant decisions" is lexical by default and OPTIONALLY hybrid
|
|
10
|
+
* (semantic) when an embedding provider is configured — so a decision that is a
|
|
11
|
+
* paraphrase of the proposal surfaces even with no shared words. Hard conflict
|
|
12
|
+
* detection ("X was explicitly rejected") stays lexical/alias-aware so semantic
|
|
13
|
+
* similarity can never fabricate a grounded conflict; paraphrase risks are
|
|
14
|
+
* reported separately as `possible_conflicts`.
|
|
8
15
|
*/
|
|
16
|
+
import { matchesRecallQuery } from "./recall.js";
|
|
17
|
+
import { getEmbeddingConfig } from "../embeddings.js";
|
|
18
|
+
import { rankDecisionsLexical, hybridDecisionRecall, POSSIBLE_CONFLICT_THRESHOLD, } from "./decision-recall.js";
|
|
9
19
|
// ============================================================================
|
|
10
20
|
// Tool Definition
|
|
11
21
|
// ============================================================================
|
|
@@ -34,100 +44,71 @@ export const checkDecisionAlignmentTool = {
|
|
|
34
44
|
// ============================================================================
|
|
35
45
|
// Helpers
|
|
36
46
|
// ============================================================================
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
* context_tags, and alternatives_rejected columns.
|
|
40
|
-
*/
|
|
41
|
-
function searchActiveDecisions(rawDb, searchTerm) {
|
|
42
|
-
const pattern = `%${searchTerm.toLowerCase()}%`;
|
|
47
|
+
/** Load active decisions (unranked); ranking/retrieval is layered on top. */
|
|
48
|
+
function loadActiveDecisions(rawDb) {
|
|
43
49
|
return rawDb
|
|
44
50
|
.prepare(`SELECT id, decision_type, subject, decision, rationale,
|
|
45
51
|
alternatives_rejected, context_tags, confidence,
|
|
46
52
|
status, superseded_by, created_at, updated_at
|
|
47
53
|
FROM developer_decisions
|
|
48
54
|
WHERE status = 'active'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
ORDER BY updated_at DESC
|
|
56
|
+
LIMIT 500`)
|
|
57
|
+
.all();
|
|
58
|
+
}
|
|
59
|
+
function parseAlts(row) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(row.alternatives_rejected || "[]");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
53
66
|
}
|
|
54
67
|
/**
|
|
55
|
-
* Classify a decision row's relationship to the queried technology.
|
|
68
|
+
* Classify a decision row's relationship to the queried technology. Hard
|
|
69
|
+
* conflicts require a literal/alias-aware match against rejected alternatives.
|
|
56
70
|
*/
|
|
57
71
|
function classifyRelationship(row, tech) {
|
|
58
72
|
const techLower = tech.toLowerCase();
|
|
59
|
-
const alts = JSON.parse(row.alternatives_rejected || "[]");
|
|
60
73
|
// If the technology appears in rejected alternatives, it's a conflict
|
|
61
|
-
const isRejected =
|
|
74
|
+
const isRejected = parseAlts(row).some((alt) => matchesRecallQuery(alt, techLower));
|
|
62
75
|
if (isRejected) {
|
|
63
76
|
return "conflict";
|
|
64
77
|
}
|
|
65
78
|
// If the technology matches the decision subject, it's aligned
|
|
66
|
-
if (row.subject
|
|
79
|
+
if (matchesRecallQuery(row.subject, techLower) ||
|
|
80
|
+
matchesRecallQuery(row.decision, techLower)) {
|
|
67
81
|
return "aligned";
|
|
68
82
|
}
|
|
69
83
|
// Otherwise it's tangentially related
|
|
70
84
|
return "related";
|
|
71
85
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return `No existing decisions found for '${technology}'. No conflicts detected — proceed with caution.`;
|
|
79
|
-
}
|
|
80
|
-
return `No conflicts found. Proceed with ${technology}.`;
|
|
86
|
+
/** Build a human-readable recommendation string. */
|
|
87
|
+
function buildRecommendation(technology, conflicts, relevantCount, possibleCount) {
|
|
88
|
+
if (conflicts.length > 0) {
|
|
89
|
+
const first = conflicts[0];
|
|
90
|
+
return (`CONFLICT: ${technology} was explicitly rejected. ` +
|
|
91
|
+
`Active decision: '${first.subject}' — ${first.reason}`);
|
|
81
92
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`Active decision: '${first.subject}' — ${first.reason}`);
|
|
86
|
-
}
|
|
87
|
-
// ============================================================================
|
|
88
|
-
// Execute
|
|
89
|
-
// ============================================================================
|
|
90
|
-
export function executeCheckDecisionAlignment(db, params) {
|
|
91
|
-
const rawDb = db.getRawDb();
|
|
92
|
-
const { technology, pattern, context } = params;
|
|
93
|
-
// Collect all matching decision rows (deduplicated by id)
|
|
94
|
-
const seenIds = new Set();
|
|
95
|
-
const allRows = [];
|
|
96
|
-
try {
|
|
97
|
-
// Primary search: technology name
|
|
98
|
-
const techRows = searchActiveDecisions(rawDb, technology);
|
|
99
|
-
for (const row of techRows) {
|
|
100
|
-
if (!seenIds.has(row.id)) {
|
|
101
|
-
seenIds.add(row.id);
|
|
102
|
-
allRows.push(row);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// Secondary search: pattern (if provided)
|
|
106
|
-
if (pattern) {
|
|
107
|
-
const patternRows = searchActiveDecisions(rawDb, pattern);
|
|
108
|
-
for (const row of patternRows) {
|
|
109
|
-
if (!seenIds.has(row.id)) {
|
|
110
|
-
seenIds.add(row.id);
|
|
111
|
-
allRows.push(row);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
93
|
+
if (possibleCount > 0) {
|
|
94
|
+
return (`No explicit conflict, but ${possibleCount} prior decision(s) are semantically ` +
|
|
95
|
+
`close to '${technology}' and rejected alternatives — review before proceeding.`);
|
|
115
96
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
aligned: true,
|
|
120
|
-
technology,
|
|
121
|
-
relevant_decisions: [],
|
|
122
|
-
conflicts: [],
|
|
123
|
-
confidence: 0.5,
|
|
124
|
-
recommendation: `No decision history available. The developer_decisions table may not exist yet. Proceed with ${technology}.`,
|
|
125
|
-
};
|
|
97
|
+
if (relevantCount === 0) {
|
|
98
|
+
return `No existing decisions found for '${technology}'. No conflicts detected — proceed with caution.`;
|
|
126
99
|
}
|
|
127
|
-
|
|
100
|
+
return `No conflicts found. Proceed with ${technology}.`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Classify the retrieved decisions into aligned/conflict/related, derive hard and
|
|
104
|
+
* possible (paraphrase) conflicts, and assemble the alignment result. Shared by
|
|
105
|
+
* the lexical and hybrid retrieval paths.
|
|
106
|
+
*/
|
|
107
|
+
function assembleAlignment(params, rows, semanticById, recallMode) {
|
|
108
|
+
const { technology, pattern } = params;
|
|
128
109
|
const relevantDecisions = [];
|
|
129
110
|
const conflicts = [];
|
|
130
|
-
for (const row of
|
|
111
|
+
for (const row of rows) {
|
|
131
112
|
const relationship = classifyRelationship(row, technology);
|
|
132
113
|
relevantDecisions.push({
|
|
133
114
|
id: row.id,
|
|
@@ -138,8 +119,7 @@ export function executeCheckDecisionAlignment(db, params) {
|
|
|
138
119
|
relationship,
|
|
139
120
|
});
|
|
140
121
|
if (relationship === "conflict") {
|
|
141
|
-
const
|
|
142
|
-
const rejectedAlt = alts.find((alt) => alt.toLowerCase().includes(technology.toLowerCase()));
|
|
122
|
+
const rejectedAlt = parseAlts(row).find((alt) => matchesRecallQuery(alt, technology));
|
|
143
123
|
conflicts.push({
|
|
144
124
|
decision_id: row.id,
|
|
145
125
|
subject: row.subject,
|
|
@@ -147,21 +127,17 @@ export function executeCheckDecisionAlignment(db, params) {
|
|
|
147
127
|
});
|
|
148
128
|
}
|
|
149
129
|
}
|
|
150
|
-
//
|
|
151
|
-
// also check pattern against decision relationships
|
|
130
|
+
// Pattern rejected-alternative check (if a pattern was supplied separately).
|
|
152
131
|
if (pattern) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
if (patternRejected &&
|
|
158
|
-
!conflicts.some((c) => c.decision_id === row.id)) {
|
|
132
|
+
const patternLower = pattern.toLowerCase();
|
|
133
|
+
for (const row of rows) {
|
|
134
|
+
const patternRejected = parseAlts(row).some((alt) => matchesRecallQuery(alt, patternLower));
|
|
135
|
+
if (patternRejected && !conflicts.some((c) => c.decision_id === row.id)) {
|
|
159
136
|
conflicts.push({
|
|
160
137
|
decision_id: row.id,
|
|
161
138
|
subject: row.subject,
|
|
162
139
|
reason: `Pattern '${pattern}' was rejected in favor of '${row.decision}' (rationale: ${row.rationale || "none"})`,
|
|
163
140
|
});
|
|
164
|
-
// Update the relationship for this decision
|
|
165
141
|
const existing = relevantDecisions.find((d) => d.id === row.id);
|
|
166
142
|
if (existing && existing.relationship !== "conflict") {
|
|
167
143
|
existing.relationship = "conflict";
|
|
@@ -169,17 +145,69 @@ export function executeCheckDecisionAlignment(db, params) {
|
|
|
169
145
|
}
|
|
170
146
|
}
|
|
171
147
|
}
|
|
148
|
+
// Possible (paraphrase) conflicts: semantically close to a decision that rejected
|
|
149
|
+
// alternatives, but NOT a literal match. Advisory only — never flips `aligned`.
|
|
150
|
+
const possibleConflicts = [];
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
const sim = semanticById.get(row.id) ?? 0;
|
|
153
|
+
if (sim < POSSIBLE_CONFLICT_THRESHOLD)
|
|
154
|
+
continue;
|
|
155
|
+
const alts = parseAlts(row);
|
|
156
|
+
if (alts.length === 0)
|
|
157
|
+
continue;
|
|
158
|
+
if (conflicts.some((c) => c.decision_id === row.id))
|
|
159
|
+
continue;
|
|
160
|
+
possibleConflicts.push({
|
|
161
|
+
decision_id: row.id,
|
|
162
|
+
subject: row.subject,
|
|
163
|
+
similarity: Math.round(sim * 1000) / 1000,
|
|
164
|
+
reason: `'${technology}' is semantically close (${Math.round(sim * 100)}%) to a decision that rejected: ${alts.join(", ")}. Review before proceeding.`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
172
167
|
const maxConfidence = relevantDecisions.length > 0
|
|
173
168
|
? Math.max(...relevantDecisions.map((d) => d.confidence))
|
|
174
169
|
: 0.5;
|
|
175
|
-
const recommendation = buildRecommendation(technology, conflicts, relevantDecisions.length);
|
|
176
170
|
return {
|
|
177
171
|
aligned: conflicts.length === 0,
|
|
178
172
|
technology,
|
|
179
173
|
relevant_decisions: relevantDecisions,
|
|
180
174
|
conflicts,
|
|
175
|
+
possible_conflicts: possibleConflicts,
|
|
181
176
|
confidence: maxConfidence,
|
|
182
|
-
recommendation,
|
|
177
|
+
recommendation: buildRecommendation(technology, conflicts, relevantDecisions.length, possibleConflicts.length),
|
|
178
|
+
recall_mode: recallMode,
|
|
183
179
|
};
|
|
184
180
|
}
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Execute
|
|
183
|
+
// ============================================================================
|
|
184
|
+
export function executeCheckDecisionAlignment(db, params) {
|
|
185
|
+
const rawDb = db.getRawDb();
|
|
186
|
+
let candidates;
|
|
187
|
+
try {
|
|
188
|
+
candidates = loadActiveDecisions(rawDb);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// developer_decisions table may not exist yet — safe default.
|
|
192
|
+
return {
|
|
193
|
+
aligned: true,
|
|
194
|
+
technology: params.technology,
|
|
195
|
+
relevant_decisions: [],
|
|
196
|
+
conflicts: [],
|
|
197
|
+
possible_conflicts: [],
|
|
198
|
+
confidence: 0.5,
|
|
199
|
+
recommendation: `No decision history available. The developer_decisions table may not exist yet. Proceed with ${params.technology}.`,
|
|
200
|
+
recall_mode: "ranked_lexical",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const query = [params.technology, params.pattern].filter(Boolean).join(" ");
|
|
204
|
+
const config = getEmbeddingConfig();
|
|
205
|
+
// No provider -> synchronous lexical retrieval (classic behaviour).
|
|
206
|
+
if (!config) {
|
|
207
|
+
const rows = rankDecisionsLexical(candidates, query, 50);
|
|
208
|
+
return assembleAlignment(params, rows, new Map(), "ranked_lexical");
|
|
209
|
+
}
|
|
210
|
+
// Provider configured -> hybrid retrieval (returns a Promise; dispatch awaits).
|
|
211
|
+
return hybridDecisionRecall(db, query, candidates, 50, config).then(({ ranked, semanticById, recall_mode }) => assembleAlignment(params, ranked, semanticById, recall_mode));
|
|
212
|
+
}
|
|
185
213
|
//# sourceMappingURL=decision-enforcement.js.map
|