mackin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/embeddings.d.ts +3 -0
- package/dist/embeddings.js +93 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +40 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +161 -0
- package/dist/retrieval.d.ts +15 -0
- package/dist/retrieval.js +191 -0
- package/dist/storage.d.ts +23 -0
- package/dist/storage.js +147 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
const clients = new Map();
|
|
3
|
+
function getClient(apiKey) {
|
|
4
|
+
const key = apiKey || process.env.ANTHROPIC_API_KEY;
|
|
5
|
+
if (!key)
|
|
6
|
+
return null;
|
|
7
|
+
let client = clients.get(key);
|
|
8
|
+
if (!client) {
|
|
9
|
+
client = new Anthropic({ apiKey: key });
|
|
10
|
+
clients.set(key, client);
|
|
11
|
+
}
|
|
12
|
+
return client;
|
|
13
|
+
}
|
|
14
|
+
const EMBED_DIMS = 64;
|
|
15
|
+
export async function embed(text, apiKey) {
|
|
16
|
+
const anthropic = getClient(apiKey);
|
|
17
|
+
if (anthropic) {
|
|
18
|
+
try {
|
|
19
|
+
const response = await anthropic.messages.create({
|
|
20
|
+
model: "claude-haiku-4-5-20251001",
|
|
21
|
+
max_tokens: 512,
|
|
22
|
+
messages: [
|
|
23
|
+
{
|
|
24
|
+
role: "user",
|
|
25
|
+
content: `Generate a semantic embedding for the following text. Respond with ONLY a valid JSON array of exactly ${EMBED_DIMS} floating point numbers between -1 and 1. No explanation, no markdown, just the array.\n\nText: "${text.slice(0, 1000)}"`,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
const content = response.content[0];
|
|
30
|
+
if (content.type === "text") {
|
|
31
|
+
const cleaned = content.text.trim().replace(/^```json?\s*/, "").replace(/\s*```$/, "");
|
|
32
|
+
const parsed = JSON.parse(cleaned);
|
|
33
|
+
if (Array.isArray(parsed) && parsed.length === EMBED_DIMS && parsed.every((v) => typeof v === "number" && isFinite(v))) {
|
|
34
|
+
const magnitude = Math.sqrt(parsed.reduce((sum, v) => sum + v * v, 0));
|
|
35
|
+
if (magnitude > 0) {
|
|
36
|
+
return parsed.map((v) => v / magnitude);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// API unavailable or malformed response — fall back to hash-based embedding
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return hashEmbed(text);
|
|
46
|
+
}
|
|
47
|
+
export function hashEmbed(text) {
|
|
48
|
+
const dims = 64;
|
|
49
|
+
const embedding = new Array(dims).fill(0);
|
|
50
|
+
const normalized = text.toLowerCase().replace(/[^\w\s]/g, " ");
|
|
51
|
+
const words = normalized.split(/\s+/).filter(Boolean);
|
|
52
|
+
for (let i = 0; i < words.length; i++) {
|
|
53
|
+
const word = words[i];
|
|
54
|
+
let hash = 0;
|
|
55
|
+
for (let j = 0; j < word.length; j++) {
|
|
56
|
+
hash = ((hash << 5) - hash + word.charCodeAt(j)) | 0;
|
|
57
|
+
}
|
|
58
|
+
const idx1 = Math.abs(hash) % dims;
|
|
59
|
+
const idx2 = Math.abs((hash * 31 + i) | 0) % dims;
|
|
60
|
+
const idx3 = Math.abs((hash * 17 + word.length) | 0) % dims;
|
|
61
|
+
embedding[idx1] += 1.0;
|
|
62
|
+
embedding[idx2] += 0.5;
|
|
63
|
+
embedding[idx3] += 0.25;
|
|
64
|
+
// bigram context
|
|
65
|
+
if (i > 0) {
|
|
66
|
+
const bigramHash = ((hash << 3) + words[i - 1].length) | 0;
|
|
67
|
+
const idx4 = Math.abs(bigramHash) % dims;
|
|
68
|
+
embedding[idx4] += 0.3;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Normalize to unit vector
|
|
72
|
+
const magnitude = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
|
|
73
|
+
if (magnitude > 0) {
|
|
74
|
+
for (let i = 0; i < dims; i++) {
|
|
75
|
+
embedding[i] /= magnitude;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return embedding;
|
|
79
|
+
}
|
|
80
|
+
export function cosineSimilarity(a, b) {
|
|
81
|
+
if (a.length !== b.length || a.length === 0)
|
|
82
|
+
return 0;
|
|
83
|
+
let dot = 0;
|
|
84
|
+
let magA = 0;
|
|
85
|
+
let magB = 0;
|
|
86
|
+
for (let i = 0; i < a.length; i++) {
|
|
87
|
+
dot += a[i] * b[i];
|
|
88
|
+
magA += a[i] * a[i];
|
|
89
|
+
magB += b[i] * b[i];
|
|
90
|
+
}
|
|
91
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
92
|
+
return denom === 0 ? 0 : dot / denom;
|
|
93
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MackinStoreOptions, WriteInput, ReadQuery, ConflictsQuery, ConflictPair, ReasoningEntry, Session } from "./types.js";
|
|
2
|
+
export declare class MackinStore {
|
|
3
|
+
private storage;
|
|
4
|
+
private retrieval;
|
|
5
|
+
private sessionId;
|
|
6
|
+
private apiKey?;
|
|
7
|
+
constructor(options: MackinStoreOptions);
|
|
8
|
+
write(input: WriteInput): Promise<ReasoningEntry>;
|
|
9
|
+
read(query: ReadQuery): Promise<ReasoningEntry[]>;
|
|
10
|
+
conflicts(query?: ConflictsQuery): Promise<ConflictPair[]>;
|
|
11
|
+
trace(): Promise<ReasoningEntry[]>;
|
|
12
|
+
getSession(): Session | null;
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|
|
15
|
+
export { Storage } from "./storage.js";
|
|
16
|
+
export { Retrieval } from "./retrieval.js";
|
|
17
|
+
export { embed, cosineSimilarity, hashEmbed } from "./embeddings.js";
|
|
18
|
+
export type { MackinStoreOptions, WriteInput, ReadQuery, ReadFilter, ConflictsQuery, ConflictPair, ReasoningEntry, Session, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Storage } from "./storage.js";
|
|
2
|
+
import { Retrieval } from "./retrieval.js";
|
|
3
|
+
import { embed } from "./embeddings.js";
|
|
4
|
+
export class MackinStore {
|
|
5
|
+
storage;
|
|
6
|
+
retrieval;
|
|
7
|
+
sessionId;
|
|
8
|
+
apiKey;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
const dbPath = options.dbPath || "./mackin.db";
|
|
11
|
+
this.sessionId = options.sessionId;
|
|
12
|
+
this.apiKey = options.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
13
|
+
this.storage = new Storage(dbPath);
|
|
14
|
+
this.retrieval = new Retrieval(this.storage, this.apiKey);
|
|
15
|
+
this.storage.ensureSession(this.sessionId);
|
|
16
|
+
}
|
|
17
|
+
async write(input) {
|
|
18
|
+
const text = `${input.decision} ${input.reasoning}`;
|
|
19
|
+
const embedding = await embed(text, this.apiKey);
|
|
20
|
+
return this.storage.writeEntry(this.sessionId, input.agent, input.decision, input.reasoning, input.alternatives_considered, input.confidence ?? 0.5, input.flags, input.tags, embedding);
|
|
21
|
+
}
|
|
22
|
+
async read(query) {
|
|
23
|
+
return this.retrieval.read(this.sessionId, query);
|
|
24
|
+
}
|
|
25
|
+
async conflicts(query) {
|
|
26
|
+
return this.retrieval.conflicts(this.sessionId, query?.topic);
|
|
27
|
+
}
|
|
28
|
+
async trace() {
|
|
29
|
+
return this.storage.getEntriesBySession(this.sessionId);
|
|
30
|
+
}
|
|
31
|
+
getSession() {
|
|
32
|
+
return this.storage.getSession(this.sessionId);
|
|
33
|
+
}
|
|
34
|
+
close() {
|
|
35
|
+
this.storage.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export { Storage } from "./storage.js";
|
|
39
|
+
export { Retrieval } from "./retrieval.js";
|
|
40
|
+
export { embed, cosineSimilarity, hashEmbed } from "./embeddings.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { test, describe, beforeEach, afterEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import { MackinStore } from "./index.js";
|
|
5
|
+
const TEST_DB = "./test-mackin.db";
|
|
6
|
+
function cleanup() {
|
|
7
|
+
if (fs.existsSync(TEST_DB)) {
|
|
8
|
+
fs.unlinkSync(TEST_DB);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
describe("MackinStore", () => {
|
|
12
|
+
let store;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
cleanup();
|
|
15
|
+
store = new MackinStore({ sessionId: "test-session", dbPath: TEST_DB });
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
store.close();
|
|
19
|
+
cleanup();
|
|
20
|
+
});
|
|
21
|
+
test("write and read a reasoning entry", async () => {
|
|
22
|
+
const entry = await store.write({
|
|
23
|
+
agent: "researcher",
|
|
24
|
+
decision: "use source A",
|
|
25
|
+
reasoning: "it is more recent and authoritative",
|
|
26
|
+
confidence: 0.9,
|
|
27
|
+
tags: ["source-selection"],
|
|
28
|
+
});
|
|
29
|
+
assert.strictEqual(entry.agentId, "researcher");
|
|
30
|
+
assert.strictEqual(entry.decision, "use source A");
|
|
31
|
+
assert.strictEqual(entry.confidence, 0.9);
|
|
32
|
+
assert.ok(entry.id);
|
|
33
|
+
assert.ok(entry.embedding);
|
|
34
|
+
assert.ok(entry.embedding.length > 0);
|
|
35
|
+
});
|
|
36
|
+
test("read with structured filter by tags", async () => {
|
|
37
|
+
await store.write({
|
|
38
|
+
agent: "a1",
|
|
39
|
+
decision: "decision 1",
|
|
40
|
+
reasoning: "reason 1",
|
|
41
|
+
tags: ["finance", "data"],
|
|
42
|
+
confidence: 0.8,
|
|
43
|
+
});
|
|
44
|
+
await store.write({
|
|
45
|
+
agent: "a2",
|
|
46
|
+
decision: "decision 2",
|
|
47
|
+
reasoning: "reason 2",
|
|
48
|
+
tags: ["tech"],
|
|
49
|
+
confidence: 0.7,
|
|
50
|
+
});
|
|
51
|
+
await store.write({
|
|
52
|
+
agent: "a3",
|
|
53
|
+
decision: "decision 3",
|
|
54
|
+
reasoning: "reason 3",
|
|
55
|
+
tags: ["finance"],
|
|
56
|
+
confidence: 0.6,
|
|
57
|
+
});
|
|
58
|
+
const results = await store.read({
|
|
59
|
+
agent: "reader",
|
|
60
|
+
filter: { tags: ["finance"] },
|
|
61
|
+
});
|
|
62
|
+
assert.strictEqual(results.length, 2);
|
|
63
|
+
assert.ok(results.every((r) => r.tags?.includes("finance")));
|
|
64
|
+
});
|
|
65
|
+
test("read with confidence_min filter", async () => {
|
|
66
|
+
await store.write({
|
|
67
|
+
agent: "a1",
|
|
68
|
+
decision: "high confidence",
|
|
69
|
+
reasoning: "very sure",
|
|
70
|
+
confidence: 0.95,
|
|
71
|
+
tags: ["test"],
|
|
72
|
+
});
|
|
73
|
+
await store.write({
|
|
74
|
+
agent: "a2",
|
|
75
|
+
decision: "low confidence",
|
|
76
|
+
reasoning: "not sure",
|
|
77
|
+
confidence: 0.3,
|
|
78
|
+
tags: ["test"],
|
|
79
|
+
});
|
|
80
|
+
const results = await store.read({
|
|
81
|
+
agent: "reader",
|
|
82
|
+
filter: { tags: ["test"], confidence_min: 0.8 },
|
|
83
|
+
});
|
|
84
|
+
assert.strictEqual(results.length, 1);
|
|
85
|
+
assert.strictEqual(results[0].decision, "high confidence");
|
|
86
|
+
});
|
|
87
|
+
test("semantic search returns relevant results", async () => {
|
|
88
|
+
await store.write({
|
|
89
|
+
agent: "a1",
|
|
90
|
+
decision: "use PostgreSQL for storage",
|
|
91
|
+
reasoning: "relational data with complex joins",
|
|
92
|
+
tags: ["database"],
|
|
93
|
+
});
|
|
94
|
+
await store.write({
|
|
95
|
+
agent: "a2",
|
|
96
|
+
decision: "deploy to AWS us-east-1",
|
|
97
|
+
reasoning: "lowest latency for east coast users",
|
|
98
|
+
tags: ["infrastructure"],
|
|
99
|
+
});
|
|
100
|
+
const results = await store.read({
|
|
101
|
+
agent: "reader",
|
|
102
|
+
semantic_query: "database choice for storage",
|
|
103
|
+
limit: 1,
|
|
104
|
+
});
|
|
105
|
+
assert.strictEqual(results.length, 1);
|
|
106
|
+
assert.strictEqual(results[0].decision, "use PostgreSQL for storage");
|
|
107
|
+
});
|
|
108
|
+
test("conflict detection finds contradictions", async () => {
|
|
109
|
+
await store.write({
|
|
110
|
+
agent: "agent-a",
|
|
111
|
+
decision: "the market size is $47 billion",
|
|
112
|
+
reasoning: "based on IEA report",
|
|
113
|
+
confidence: 0.9,
|
|
114
|
+
tags: ["market-size"],
|
|
115
|
+
});
|
|
116
|
+
await store.write({
|
|
117
|
+
agent: "agent-b",
|
|
118
|
+
decision: "the market size is $52 billion",
|
|
119
|
+
reasoning: "based on bottom-up analysis",
|
|
120
|
+
confidence: 0.75,
|
|
121
|
+
tags: ["market-size"],
|
|
122
|
+
});
|
|
123
|
+
const conflicts = await store.conflicts({ topic: "market size" });
|
|
124
|
+
assert.ok(conflicts.length >= 1);
|
|
125
|
+
const c = conflicts[0];
|
|
126
|
+
assert.ok(c.entry_a.agentId !== c.entry_b.agentId);
|
|
127
|
+
assert.ok(c.reason.length > 0);
|
|
128
|
+
});
|
|
129
|
+
test("trace returns entries in chronological order", async () => {
|
|
130
|
+
await store.write({ agent: "a1", decision: "first", reasoning: "r1" });
|
|
131
|
+
await store.write({ agent: "a2", decision: "second", reasoning: "r2" });
|
|
132
|
+
await store.write({ agent: "a3", decision: "third", reasoning: "r3" });
|
|
133
|
+
const trace = await store.trace();
|
|
134
|
+
assert.strictEqual(trace.length, 3);
|
|
135
|
+
assert.strictEqual(trace[0].decision, "first");
|
|
136
|
+
assert.strictEqual(trace[1].decision, "second");
|
|
137
|
+
assert.strictEqual(trace[2].decision, "third");
|
|
138
|
+
});
|
|
139
|
+
test("works without API key (hash embeddings)", async () => {
|
|
140
|
+
const entry = await store.write({
|
|
141
|
+
agent: "test",
|
|
142
|
+
decision: "test decision",
|
|
143
|
+
reasoning: "test reasoning",
|
|
144
|
+
});
|
|
145
|
+
assert.ok(entry.embedding);
|
|
146
|
+
assert.strictEqual(entry.embedding.length, 64);
|
|
147
|
+
const magnitude = Math.sqrt(entry.embedding.reduce((sum, v) => sum + v * v, 0));
|
|
148
|
+
assert.ok(Math.abs(magnitude - 1.0) < 0.01, "embedding should be unit normalized");
|
|
149
|
+
});
|
|
150
|
+
test("alternatives and flags are stored correctly", async () => {
|
|
151
|
+
const entry = await store.write({
|
|
152
|
+
agent: "test",
|
|
153
|
+
decision: "chose option A",
|
|
154
|
+
reasoning: "better fit",
|
|
155
|
+
alternatives_considered: ["option B", "option C"],
|
|
156
|
+
flags: ["needs_review", "uncertain"],
|
|
157
|
+
});
|
|
158
|
+
assert.deepStrictEqual(entry.alternativesConsidered, ["option B", "option C"]);
|
|
159
|
+
assert.deepStrictEqual(entry.flags, ["needs_review", "uncertain"]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Storage } from "./storage.js";
|
|
2
|
+
import type { ReasoningEntry, ReadQuery, ConflictPair } from "./types.js";
|
|
3
|
+
export declare class Retrieval {
|
|
4
|
+
private storage;
|
|
5
|
+
private apiKey?;
|
|
6
|
+
constructor(storage: Storage, apiKey?: string | undefined);
|
|
7
|
+
read(sessionId: string, query: ReadQuery): Promise<ReasoningEntry[]>;
|
|
8
|
+
private structuredFilter;
|
|
9
|
+
private semanticSearch;
|
|
10
|
+
private mergeResults;
|
|
11
|
+
conflicts(sessionId: string, topic?: string, maxResults?: number): Promise<ConflictPair[]>;
|
|
12
|
+
private detectConflict;
|
|
13
|
+
private hasTagOverlap;
|
|
14
|
+
private stringSimilarity;
|
|
15
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { embed, cosineSimilarity } from "./embeddings.js";
|
|
2
|
+
const SEMANTIC_FALLBACK_THRESHOLD = 3;
|
|
3
|
+
const CONFLICT_SIMILARITY_HIGH = 0.95;
|
|
4
|
+
const CONFLICT_DECISION_SIMILARITY_LOW = 0.5;
|
|
5
|
+
const CONFLICT_CONFIDENCE_DELTA = 0.3;
|
|
6
|
+
export class Retrieval {
|
|
7
|
+
storage;
|
|
8
|
+
apiKey;
|
|
9
|
+
constructor(storage, apiKey) {
|
|
10
|
+
this.storage = storage;
|
|
11
|
+
this.apiKey = apiKey;
|
|
12
|
+
}
|
|
13
|
+
async read(sessionId, query) {
|
|
14
|
+
const limit = query.limit || 10;
|
|
15
|
+
if (query.filter) {
|
|
16
|
+
const results = this.structuredFilter(sessionId, query.filter, limit);
|
|
17
|
+
if (results.length >= SEMANTIC_FALLBACK_THRESHOLD) {
|
|
18
|
+
return results.slice(0, limit);
|
|
19
|
+
}
|
|
20
|
+
if (query.semantic_query) {
|
|
21
|
+
const semanticResults = await this.semanticSearch(sessionId, query.semantic_query, limit);
|
|
22
|
+
const merged = this.mergeResults(results, semanticResults, limit);
|
|
23
|
+
return merged;
|
|
24
|
+
}
|
|
25
|
+
return results;
|
|
26
|
+
}
|
|
27
|
+
if (query.semantic_query) {
|
|
28
|
+
return this.semanticSearch(sessionId, query.semantic_query, limit);
|
|
29
|
+
}
|
|
30
|
+
return this.storage.getEntriesBySession(sessionId).slice(0, limit);
|
|
31
|
+
}
|
|
32
|
+
structuredFilter(sessionId, filter, limit) {
|
|
33
|
+
return this.storage.queryEntries(sessionId, {
|
|
34
|
+
tags: filter.tags,
|
|
35
|
+
confidenceMin: filter.confidence_min,
|
|
36
|
+
agentId: filter.agent_id,
|
|
37
|
+
createdAfter: filter.created_after,
|
|
38
|
+
createdBefore: filter.created_before,
|
|
39
|
+
limit,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async semanticSearch(sessionId, query, limit) {
|
|
43
|
+
const queryEmbedding = await embed(query, this.apiKey);
|
|
44
|
+
const allEntries = this.storage.getEntriesBySession(sessionId);
|
|
45
|
+
const scored = allEntries
|
|
46
|
+
.filter((entry) => entry.embedding && entry.embedding.length > 0)
|
|
47
|
+
.map((entry) => ({
|
|
48
|
+
entry,
|
|
49
|
+
score: cosineSimilarity(queryEmbedding, entry.embedding),
|
|
50
|
+
}))
|
|
51
|
+
.sort((a, b) => b.score - a.score);
|
|
52
|
+
return scored.slice(0, limit).map((s) => s.entry);
|
|
53
|
+
}
|
|
54
|
+
mergeResults(structured, semantic, limit) {
|
|
55
|
+
const seen = new Set(structured.map((e) => e.id));
|
|
56
|
+
const merged = [...structured];
|
|
57
|
+
for (const entry of semantic) {
|
|
58
|
+
if (!seen.has(entry.id)) {
|
|
59
|
+
merged.push(entry);
|
|
60
|
+
seen.add(entry.id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return merged.slice(0, limit);
|
|
64
|
+
}
|
|
65
|
+
async conflicts(sessionId, topic, maxResults = 50) {
|
|
66
|
+
let entries = this.storage.getEntriesBySession(sessionId);
|
|
67
|
+
if (topic) {
|
|
68
|
+
const topicEmbedding = await embed(topic, this.apiKey);
|
|
69
|
+
const scored = entries
|
|
70
|
+
.filter((entry) => entry.embedding)
|
|
71
|
+
.map((entry) => ({
|
|
72
|
+
entry,
|
|
73
|
+
score: cosineSimilarity(topicEmbedding, entry.embedding),
|
|
74
|
+
}))
|
|
75
|
+
.sort((a, b) => b.score - a.score);
|
|
76
|
+
if (scored.length > 0) {
|
|
77
|
+
const maxScore = scored[0].score;
|
|
78
|
+
const threshold = maxScore * 0.6;
|
|
79
|
+
entries = scored
|
|
80
|
+
.filter((s) => s.score >= threshold)
|
|
81
|
+
.map((s) => s.entry);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Build tag index to avoid full O(n^2) when possible
|
|
85
|
+
const tagIndex = new Map();
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (entry.tags) {
|
|
88
|
+
for (const tag of entry.tags) {
|
|
89
|
+
let group = tagIndex.get(tag);
|
|
90
|
+
if (!group) {
|
|
91
|
+
group = [];
|
|
92
|
+
tagIndex.set(tag, group);
|
|
93
|
+
}
|
|
94
|
+
group.push(entry);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const conflicts = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
// Check tag-grouped pairs first (covers path 1 and path 3)
|
|
101
|
+
for (const group of tagIndex.values()) {
|
|
102
|
+
for (let i = 0; i < group.length && conflicts.length < maxResults; i++) {
|
|
103
|
+
for (let j = i + 1; j < group.length && conflicts.length < maxResults; j++) {
|
|
104
|
+
const pairKey = group[i].id < group[j].id
|
|
105
|
+
? `${group[i].id}:${group[j].id}`
|
|
106
|
+
: `${group[j].id}:${group[i].id}`;
|
|
107
|
+
if (seen.has(pairKey))
|
|
108
|
+
continue;
|
|
109
|
+
seen.add(pairKey);
|
|
110
|
+
const conflict = this.detectConflict(group[i], group[j]);
|
|
111
|
+
if (conflict)
|
|
112
|
+
conflicts.push(conflict);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Check embedding-based conflicts (path 2) for entries with embeddings
|
|
117
|
+
if (conflicts.length < maxResults) {
|
|
118
|
+
const withEmbeddings = entries.filter((e) => e.embedding && e.embedding.length > 0);
|
|
119
|
+
for (let i = 0; i < withEmbeddings.length && conflicts.length < maxResults; i++) {
|
|
120
|
+
for (let j = i + 1; j < withEmbeddings.length && conflicts.length < maxResults; j++) {
|
|
121
|
+
const pairKey = withEmbeddings[i].id < withEmbeddings[j].id
|
|
122
|
+
? `${withEmbeddings[i].id}:${withEmbeddings[j].id}`
|
|
123
|
+
: `${withEmbeddings[j].id}:${withEmbeddings[i].id}`;
|
|
124
|
+
if (seen.has(pairKey))
|
|
125
|
+
continue;
|
|
126
|
+
seen.add(pairKey);
|
|
127
|
+
const conflict = this.detectConflict(withEmbeddings[i], withEmbeddings[j]);
|
|
128
|
+
if (conflict)
|
|
129
|
+
conflicts.push(conflict);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return conflicts;
|
|
134
|
+
}
|
|
135
|
+
detectConflict(a, b) {
|
|
136
|
+
const tagsOverlap = this.hasTagOverlap(a.tags, b.tags);
|
|
137
|
+
const confidenceDelta = Math.abs(a.confidence - b.confidence);
|
|
138
|
+
const decisionSim = this.stringSimilarity(a.decision, b.decision);
|
|
139
|
+
// Path 1: Same topic, different decisions, significant confidence gap
|
|
140
|
+
if (tagsOverlap && confidenceDelta > CONFLICT_CONFIDENCE_DELTA) {
|
|
141
|
+
if (decisionSim < CONFLICT_DECISION_SIMILARITY_LOW) {
|
|
142
|
+
return {
|
|
143
|
+
entry_a: a,
|
|
144
|
+
entry_b: b,
|
|
145
|
+
reason: `Same topic area (overlapping tags) but different decisions with confidence delta of ${confidenceDelta.toFixed(2)}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Path 2: High embedding similarity but different decisions
|
|
150
|
+
if (a.embedding && b.embedding) {
|
|
151
|
+
const embeddingSim = cosineSimilarity(a.embedding, b.embedding);
|
|
152
|
+
if (embeddingSim > CONFLICT_SIMILARITY_HIGH) {
|
|
153
|
+
if (decisionSim < CONFLICT_DECISION_SIMILARITY_LOW) {
|
|
154
|
+
return {
|
|
155
|
+
entry_a: a,
|
|
156
|
+
entry_b: b,
|
|
157
|
+
reason: `High semantic similarity (${embeddingSim.toFixed(2)}) but different decisions (similarity: ${decisionSim.toFixed(2)})`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Path 3: Same topic, very similar phrasing but different specific values
|
|
163
|
+
// (e.g. "$47B" vs "$52B" — high word overlap but not identical)
|
|
164
|
+
if (tagsOverlap && a.agentId !== b.agentId && decisionSim > 0.7 && decisionSim < 1.0) {
|
|
165
|
+
if (a.decision !== b.decision) {
|
|
166
|
+
return {
|
|
167
|
+
entry_a: a,
|
|
168
|
+
entry_b: b,
|
|
169
|
+
reason: `Same topic, different agents, nearly identical framing but different conclusions (similarity: ${decisionSim.toFixed(2)})`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
hasTagOverlap(tagsA, tagsB) {
|
|
176
|
+
if (!tagsA || !tagsB)
|
|
177
|
+
return false;
|
|
178
|
+
return tagsA.some((t) => tagsB.includes(t));
|
|
179
|
+
}
|
|
180
|
+
stringSimilarity(a, b) {
|
|
181
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/));
|
|
182
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/));
|
|
183
|
+
let intersection = 0;
|
|
184
|
+
for (const word of wordsA) {
|
|
185
|
+
if (wordsB.has(word))
|
|
186
|
+
intersection++;
|
|
187
|
+
}
|
|
188
|
+
const union = new Set([...wordsA, ...wordsB]).size;
|
|
189
|
+
return union === 0 ? 0 : intersection / union;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReasoningEntry, Session } from "./types.js";
|
|
2
|
+
export declare class Storage {
|
|
3
|
+
private db;
|
|
4
|
+
constructor(dbPath: string);
|
|
5
|
+
private initialize;
|
|
6
|
+
createSession(id?: string, name?: string, metadata?: Record<string, unknown>): Session;
|
|
7
|
+
getSession(id: string): Session | null;
|
|
8
|
+
deleteSession(id: string): void;
|
|
9
|
+
ensureSession(id: string): void;
|
|
10
|
+
writeEntry(sessionId: string, agentId: string, decision: string, reasoning: string, alternativesConsidered?: string[], confidence?: number, flags?: string[], tags?: string[], embedding?: number[]): ReasoningEntry;
|
|
11
|
+
getEntry(id: string): ReasoningEntry | null;
|
|
12
|
+
getEntriesBySession(sessionId: string): ReasoningEntry[];
|
|
13
|
+
queryEntries(sessionId: string, opts: {
|
|
14
|
+
tags?: string[];
|
|
15
|
+
confidenceMin?: number;
|
|
16
|
+
agentId?: string;
|
|
17
|
+
createdAfter?: string;
|
|
18
|
+
createdBefore?: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
}): ReasoningEntry[];
|
|
21
|
+
private rowToEntry;
|
|
22
|
+
close(): void;
|
|
23
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
export class Storage {
|
|
4
|
+
db;
|
|
5
|
+
constructor(dbPath) {
|
|
6
|
+
this.db = new Database(dbPath);
|
|
7
|
+
this.db.pragma("journal_mode = WAL");
|
|
8
|
+
this.db.pragma("foreign_keys = ON");
|
|
9
|
+
this.initialize();
|
|
10
|
+
}
|
|
11
|
+
initialize() {
|
|
12
|
+
this.db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
name TEXT,
|
|
16
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
17
|
+
metadata TEXT
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS reasoning_entries (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
session_id TEXT NOT NULL,
|
|
23
|
+
agent_id TEXT NOT NULL,
|
|
24
|
+
decision TEXT NOT NULL,
|
|
25
|
+
reasoning TEXT NOT NULL,
|
|
26
|
+
alternatives_considered TEXT,
|
|
27
|
+
confidence REAL DEFAULT 0.5,
|
|
28
|
+
flags TEXT,
|
|
29
|
+
tags TEXT,
|
|
30
|
+
embedding TEXT,
|
|
31
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
32
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_session ON reasoning_entries(session_id);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_agent ON reasoning_entries(agent_id);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_confidence ON reasoning_entries(confidence);
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
createSession(id, name, metadata) {
|
|
41
|
+
const sessionId = id || uuidv4();
|
|
42
|
+
const stmt = this.db.prepare("INSERT INTO sessions (id, name, metadata) VALUES (?, ?, ?)");
|
|
43
|
+
stmt.run(sessionId, name || null, metadata ? JSON.stringify(metadata) : null);
|
|
44
|
+
return this.getSession(sessionId);
|
|
45
|
+
}
|
|
46
|
+
getSession(id) {
|
|
47
|
+
const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
48
|
+
if (!row)
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
id: row.id,
|
|
52
|
+
name: row.name,
|
|
53
|
+
createdAt: row.created_at,
|
|
54
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
deleteSession(id) {
|
|
58
|
+
this.db.prepare("DELETE FROM reasoning_entries WHERE session_id = ?").run(id);
|
|
59
|
+
this.db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
60
|
+
}
|
|
61
|
+
ensureSession(id) {
|
|
62
|
+
const exists = this.db.prepare("SELECT 1 FROM sessions WHERE id = ?").get(id);
|
|
63
|
+
if (!exists) {
|
|
64
|
+
this.createSession(id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
writeEntry(sessionId, agentId, decision, reasoning, alternativesConsidered, confidence = 0.5, flags, tags, embedding) {
|
|
68
|
+
const id = uuidv4();
|
|
69
|
+
this.ensureSession(sessionId);
|
|
70
|
+
const stmt = this.db.prepare(`
|
|
71
|
+
INSERT INTO reasoning_entries
|
|
72
|
+
(id, session_id, agent_id, decision, reasoning, alternatives_considered, confidence, flags, tags, embedding)
|
|
73
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
74
|
+
`);
|
|
75
|
+
stmt.run(id, sessionId, agentId, decision, reasoning, alternativesConsidered ? JSON.stringify(alternativesConsidered) : null, confidence, flags ? JSON.stringify(flags) : null, tags ? JSON.stringify(tags) : null, embedding ? JSON.stringify(embedding) : null);
|
|
76
|
+
return this.getEntry(id);
|
|
77
|
+
}
|
|
78
|
+
getEntry(id) {
|
|
79
|
+
const row = this.db.prepare("SELECT * FROM reasoning_entries WHERE id = ?").get(id);
|
|
80
|
+
if (!row)
|
|
81
|
+
return null;
|
|
82
|
+
return this.rowToEntry(row);
|
|
83
|
+
}
|
|
84
|
+
getEntriesBySession(sessionId) {
|
|
85
|
+
const rows = this.db
|
|
86
|
+
.prepare("SELECT * FROM reasoning_entries WHERE session_id = ? ORDER BY created_at ASC")
|
|
87
|
+
.all(sessionId);
|
|
88
|
+
return rows.map(this.rowToEntry);
|
|
89
|
+
}
|
|
90
|
+
queryEntries(sessionId, opts) {
|
|
91
|
+
let sql = "SELECT * FROM reasoning_entries WHERE session_id = ?";
|
|
92
|
+
const params = [sessionId];
|
|
93
|
+
if (opts.confidenceMin !== undefined) {
|
|
94
|
+
sql += " AND confidence >= ?";
|
|
95
|
+
params.push(opts.confidenceMin);
|
|
96
|
+
}
|
|
97
|
+
if (opts.agentId) {
|
|
98
|
+
sql += " AND agent_id = ?";
|
|
99
|
+
params.push(opts.agentId);
|
|
100
|
+
}
|
|
101
|
+
if (opts.createdAfter) {
|
|
102
|
+
sql += " AND created_at > ?";
|
|
103
|
+
params.push(opts.createdAfter);
|
|
104
|
+
}
|
|
105
|
+
if (opts.createdBefore) {
|
|
106
|
+
sql += " AND created_at < ?";
|
|
107
|
+
params.push(opts.createdBefore);
|
|
108
|
+
}
|
|
109
|
+
sql += " ORDER BY created_at DESC";
|
|
110
|
+
// Don't apply SQL LIMIT when tags filter is active (tags are filtered in-memory)
|
|
111
|
+
if (opts.limit && !(opts.tags && opts.tags.length > 0)) {
|
|
112
|
+
sql += " LIMIT ?";
|
|
113
|
+
params.push(opts.limit);
|
|
114
|
+
}
|
|
115
|
+
let rows = this.db.prepare(sql).all(...params);
|
|
116
|
+
if (opts.tags && opts.tags.length > 0) {
|
|
117
|
+
rows = rows.filter((row) => {
|
|
118
|
+
const entryTags = row.tags ? JSON.parse(row.tags) : [];
|
|
119
|
+
return opts.tags.some((t) => entryTags.includes(t));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (opts.limit && opts.tags && opts.tags.length > 0) {
|
|
123
|
+
rows = rows.slice(0, opts.limit);
|
|
124
|
+
}
|
|
125
|
+
return rows.map(this.rowToEntry);
|
|
126
|
+
}
|
|
127
|
+
rowToEntry(row) {
|
|
128
|
+
return {
|
|
129
|
+
id: row.id,
|
|
130
|
+
sessionId: row.session_id,
|
|
131
|
+
agentId: row.agent_id,
|
|
132
|
+
decision: row.decision,
|
|
133
|
+
reasoning: row.reasoning,
|
|
134
|
+
alternativesConsidered: row.alternatives_considered
|
|
135
|
+
? JSON.parse(row.alternatives_considered)
|
|
136
|
+
: undefined,
|
|
137
|
+
confidence: row.confidence,
|
|
138
|
+
flags: row.flags ? JSON.parse(row.flags) : undefined,
|
|
139
|
+
tags: row.tags ? JSON.parse(row.tags) : undefined,
|
|
140
|
+
embedding: row.embedding ? JSON.parse(row.embedding) : undefined,
|
|
141
|
+
createdAt: row.created_at,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
close() {
|
|
145
|
+
this.db.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface MackinStoreOptions {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
dbPath?: string;
|
|
4
|
+
anthropicApiKey?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ReasoningEntry {
|
|
7
|
+
id: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
agentId: string;
|
|
10
|
+
decision: string;
|
|
11
|
+
reasoning: string;
|
|
12
|
+
alternativesConsidered?: string[];
|
|
13
|
+
confidence: number;
|
|
14
|
+
flags?: string[];
|
|
15
|
+
tags?: string[];
|
|
16
|
+
embedding?: number[];
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
export interface WriteInput {
|
|
20
|
+
agent: string;
|
|
21
|
+
decision: string;
|
|
22
|
+
reasoning: string;
|
|
23
|
+
alternatives_considered?: string[];
|
|
24
|
+
confidence?: number;
|
|
25
|
+
flags?: string[];
|
|
26
|
+
tags?: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface ReadFilter {
|
|
29
|
+
tags?: string[];
|
|
30
|
+
confidence_min?: number;
|
|
31
|
+
agent_id?: string;
|
|
32
|
+
created_after?: string;
|
|
33
|
+
created_before?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ReadQuery {
|
|
36
|
+
agent: string;
|
|
37
|
+
filter?: ReadFilter;
|
|
38
|
+
semantic_query?: string;
|
|
39
|
+
limit?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface ConflictsQuery {
|
|
42
|
+
topic?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ConflictPair {
|
|
45
|
+
entry_a: ReasoningEntry;
|
|
46
|
+
entry_b: ReasoningEntry;
|
|
47
|
+
reason: string;
|
|
48
|
+
}
|
|
49
|
+
export interface Session {
|
|
50
|
+
id: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
metadata?: Record<string, unknown>;
|
|
54
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mackin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared reasoning memory for parallel multi-agent systems",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"dev": "tsc --watch",
|
|
22
|
+
"test": "node --import tsx --test src/**/*.test.ts",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"ai",
|
|
27
|
+
"agents",
|
|
28
|
+
"multi-agent",
|
|
29
|
+
"memory",
|
|
30
|
+
"reasoning",
|
|
31
|
+
"llm",
|
|
32
|
+
"rag",
|
|
33
|
+
"embeddings",
|
|
34
|
+
"sqlite"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/HarrisSagiris/mackin.git",
|
|
40
|
+
"directory": "sdk/typescript"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
44
|
+
"better-sqlite3": "^11.0.0",
|
|
45
|
+
"uuid": "^10.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"@types/uuid": "^10.0.0",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.6.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@anthropic-ai/sdk": ">=0.30.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@anthropic-ai/sdk": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|