@desplega.ai/agent-swarm 1.64.0 → 1.65.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/openapi.json +42 -1
- package/package.json +3 -1
- package/src/be/db.ts +18 -290
- package/src/be/embedding.ts +0 -38
- package/src/be/memory/constants.ts +26 -0
- package/src/be/memory/index.ts +22 -0
- package/src/be/memory/providers/openai-embedding.ts +94 -0
- package/src/be/memory/providers/sqlite-store.ts +530 -0
- package/src/be/memory/reranker.ts +59 -0
- package/src/be/memory/types.ts +83 -0
- package/src/be/migrations/036_memory_ttl_staleness.sql +8 -0
- package/src/http/memory.ts +99 -46
- package/src/server.ts +2 -0
- package/src/tests/artifact-sdk.test.ts +2 -1
- package/src/tests/memory-e2e.test.ts +453 -0
- package/src/tests/memory-reranker.test.ts +192 -0
- package/src/tests/memory-store.test.ts +330 -0
- package/src/tests/memory.test.ts +105 -121
- package/src/tests/package-publish.test.ts +47 -0
- package/src/tests/self-improvement.test.ts +18 -19
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tools/inject-learning.ts +7 -5
- package/src/tools/memory-delete.ts +89 -0
- package/src/tools/memory-get.ts +2 -2
- package/src/tools/memory-search.ts +13 -7
- package/src/tools/store-progress.ts +12 -11
- package/src/tools/tool-config.ts +1 -0
- package/src/types.ts +3 -0
- package/tsconfig.json +49 -0
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.65.0",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
|
@@ -2387,6 +2387,47 @@
|
|
|
2387
2387
|
}
|
|
2388
2388
|
}
|
|
2389
2389
|
},
|
|
2390
|
+
"/api/memory/re-embed": {
|
|
2391
|
+
"post": {
|
|
2392
|
+
"summary": "Re-embed all memories using the current embedding provider",
|
|
2393
|
+
"tags": [
|
|
2394
|
+
"Memory"
|
|
2395
|
+
],
|
|
2396
|
+
"security": [
|
|
2397
|
+
{
|
|
2398
|
+
"bearerAuth": []
|
|
2399
|
+
}
|
|
2400
|
+
],
|
|
2401
|
+
"requestBody": {
|
|
2402
|
+
"content": {
|
|
2403
|
+
"application/json": {
|
|
2404
|
+
"schema": {
|
|
2405
|
+
"type": "object",
|
|
2406
|
+
"properties": {
|
|
2407
|
+
"agentId": {
|
|
2408
|
+
"type": "string",
|
|
2409
|
+
"format": "uuid",
|
|
2410
|
+
"description": "Re-embed only this agent's memories. Omit for all."
|
|
2411
|
+
},
|
|
2412
|
+
"batchSize": {
|
|
2413
|
+
"type": "integer",
|
|
2414
|
+
"minimum": 1,
|
|
2415
|
+
"maximum": 100,
|
|
2416
|
+
"default": 20,
|
|
2417
|
+
"description": "Memories per batch"
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
},
|
|
2424
|
+
"responses": {
|
|
2425
|
+
"202": {
|
|
2426
|
+
"description": "Re-embedding started"
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
},
|
|
2390
2431
|
"/api/prompt-templates/resolved": {
|
|
2391
2432
|
"get": {
|
|
2392
2433
|
"summary": "Resolve a prompt template for a given event type and scope chain",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.65.0",
|
|
4
4
|
"description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "desplega.sh <contact@desplega.sh>",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"src/",
|
|
26
|
+
"tsconfig.json",
|
|
26
27
|
"plugin/commands/",
|
|
27
28
|
"plugin/agents/",
|
|
28
29
|
"plugin/skills/",
|
|
@@ -121,6 +122,7 @@
|
|
|
121
122
|
"openai": "^6.22.0",
|
|
122
123
|
"react": "^19.2.3",
|
|
123
124
|
"react-devtools-core": "^7.0.1",
|
|
125
|
+
"sqlite-vec": "^0.1.9",
|
|
124
126
|
"svix": "^1.62.0",
|
|
125
127
|
"viem": "^2.46.3",
|
|
126
128
|
"zod": "^4.2.1",
|
package/src/be/db.ts
CHANGED
|
@@ -7,9 +7,6 @@ import type {
|
|
|
7
7
|
AgentLog,
|
|
8
8
|
AgentLogEventType,
|
|
9
9
|
AgentMcpServer,
|
|
10
|
-
AgentMemory,
|
|
11
|
-
AgentMemoryScope,
|
|
12
|
-
AgentMemorySource,
|
|
13
10
|
AgentSkill,
|
|
14
11
|
AgentStatus,
|
|
15
12
|
AgentTask,
|
|
@@ -64,6 +61,11 @@ import { runMigrations } from "./migrations/runner";
|
|
|
64
61
|
import { seedDefaultTemplates } from "./seed";
|
|
65
62
|
|
|
66
63
|
let db: Database | null = null;
|
|
64
|
+
let sqliteVecAvailable = false;
|
|
65
|
+
|
|
66
|
+
export function isSqliteVecAvailable(): boolean {
|
|
67
|
+
return sqliteVecAvailable;
|
|
68
|
+
}
|
|
67
69
|
|
|
68
70
|
export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
69
71
|
if (db) {
|
|
@@ -88,6 +90,19 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
88
90
|
database.run("PRAGMA journal_mode = WAL;");
|
|
89
91
|
database.run("PRAGMA foreign_keys = ON;");
|
|
90
92
|
|
|
93
|
+
// Load sqlite-vec extension for vector search
|
|
94
|
+
try {
|
|
95
|
+
const sqliteVec = require("sqlite-vec");
|
|
96
|
+
sqliteVec.load(database);
|
|
97
|
+
sqliteVecAvailable = true;
|
|
98
|
+
console.log("[db] sqlite-vec loaded");
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn(
|
|
101
|
+
"[db] sqlite-vec not available, falling back to in-memory cosine:",
|
|
102
|
+
(err as Error).message,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
// Run database migrations (schema creation + incremental changes)
|
|
92
107
|
runMigrations(database);
|
|
93
108
|
|
|
@@ -4684,293 +4699,6 @@ export function deleteSwarmRepo(id: string): boolean {
|
|
|
4684
4699
|
return result.changes > 0;
|
|
4685
4700
|
}
|
|
4686
4701
|
|
|
4687
|
-
// ============================================================================
|
|
4688
|
-
// Agent Memory Functions
|
|
4689
|
-
// ============================================================================
|
|
4690
|
-
|
|
4691
|
-
type AgentMemoryRow = {
|
|
4692
|
-
id: string;
|
|
4693
|
-
agentId: string | null;
|
|
4694
|
-
scope: string;
|
|
4695
|
-
name: string;
|
|
4696
|
-
content: string;
|
|
4697
|
-
summary: string | null;
|
|
4698
|
-
embedding: Buffer | null;
|
|
4699
|
-
source: string;
|
|
4700
|
-
sourceTaskId: string | null;
|
|
4701
|
-
sourcePath: string | null;
|
|
4702
|
-
chunkIndex: number;
|
|
4703
|
-
totalChunks: number;
|
|
4704
|
-
tags: string;
|
|
4705
|
-
createdAt: string;
|
|
4706
|
-
accessedAt: string;
|
|
4707
|
-
};
|
|
4708
|
-
|
|
4709
|
-
function rowToAgentMemory(row: AgentMemoryRow): AgentMemory {
|
|
4710
|
-
return {
|
|
4711
|
-
id: row.id,
|
|
4712
|
-
agentId: row.agentId,
|
|
4713
|
-
scope: row.scope as AgentMemoryScope,
|
|
4714
|
-
name: row.name,
|
|
4715
|
-
content: row.content,
|
|
4716
|
-
summary: row.summary,
|
|
4717
|
-
source: row.source as AgentMemorySource,
|
|
4718
|
-
sourceTaskId: row.sourceTaskId,
|
|
4719
|
-
sourcePath: row.sourcePath,
|
|
4720
|
-
chunkIndex: row.chunkIndex,
|
|
4721
|
-
totalChunks: row.totalChunks,
|
|
4722
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
4723
|
-
createdAt: row.createdAt,
|
|
4724
|
-
accessedAt: row.accessedAt,
|
|
4725
|
-
};
|
|
4726
|
-
}
|
|
4727
|
-
|
|
4728
|
-
export interface CreateMemoryOptions {
|
|
4729
|
-
agentId?: string | null;
|
|
4730
|
-
scope: AgentMemoryScope;
|
|
4731
|
-
name: string;
|
|
4732
|
-
content: string;
|
|
4733
|
-
summary?: string | null;
|
|
4734
|
-
embedding?: Buffer | null;
|
|
4735
|
-
source: AgentMemorySource;
|
|
4736
|
-
sourceTaskId?: string | null;
|
|
4737
|
-
sourcePath?: string | null;
|
|
4738
|
-
chunkIndex?: number;
|
|
4739
|
-
totalChunks?: number;
|
|
4740
|
-
tags?: string[];
|
|
4741
|
-
}
|
|
4742
|
-
|
|
4743
|
-
export function createMemory(data: CreateMemoryOptions): AgentMemory {
|
|
4744
|
-
const id = crypto.randomUUID();
|
|
4745
|
-
const now = new Date().toISOString();
|
|
4746
|
-
const row = getDb()
|
|
4747
|
-
.prepare<
|
|
4748
|
-
AgentMemoryRow,
|
|
4749
|
-
[
|
|
4750
|
-
string,
|
|
4751
|
-
string | null,
|
|
4752
|
-
string,
|
|
4753
|
-
string,
|
|
4754
|
-
string,
|
|
4755
|
-
string | null,
|
|
4756
|
-
Buffer | null,
|
|
4757
|
-
string,
|
|
4758
|
-
string | null,
|
|
4759
|
-
string | null,
|
|
4760
|
-
number,
|
|
4761
|
-
number,
|
|
4762
|
-
string,
|
|
4763
|
-
string,
|
|
4764
|
-
string,
|
|
4765
|
-
]
|
|
4766
|
-
>(
|
|
4767
|
-
`INSERT INTO agent_memory (id, agentId, scope, name, content, summary, embedding, source, sourceTaskId, sourcePath, chunkIndex, totalChunks, tags, createdAt, accessedAt)
|
|
4768
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
4769
|
-
)
|
|
4770
|
-
.get(
|
|
4771
|
-
id,
|
|
4772
|
-
data.agentId ?? null,
|
|
4773
|
-
data.scope,
|
|
4774
|
-
data.name,
|
|
4775
|
-
data.content,
|
|
4776
|
-
data.summary ?? null,
|
|
4777
|
-
data.embedding ?? null,
|
|
4778
|
-
data.source,
|
|
4779
|
-
data.sourceTaskId ?? null,
|
|
4780
|
-
data.sourcePath ?? null,
|
|
4781
|
-
data.chunkIndex ?? 0,
|
|
4782
|
-
data.totalChunks ?? 1,
|
|
4783
|
-
JSON.stringify(data.tags ?? []),
|
|
4784
|
-
now,
|
|
4785
|
-
now,
|
|
4786
|
-
);
|
|
4787
|
-
|
|
4788
|
-
if (!row) throw new Error("Failed to create memory");
|
|
4789
|
-
return rowToAgentMemory(row);
|
|
4790
|
-
}
|
|
4791
|
-
|
|
4792
|
-
export function getMemoryById(id: string): AgentMemory | null {
|
|
4793
|
-
const row = getDb()
|
|
4794
|
-
.prepare<AgentMemoryRow, [string]>("SELECT * FROM agent_memory WHERE id = ?")
|
|
4795
|
-
.get(id);
|
|
4796
|
-
if (!row) return null;
|
|
4797
|
-
|
|
4798
|
-
// Update accessedAt
|
|
4799
|
-
getDb()
|
|
4800
|
-
.prepare("UPDATE agent_memory SET accessedAt = ? WHERE id = ?")
|
|
4801
|
-
.run(new Date().toISOString(), id);
|
|
4802
|
-
|
|
4803
|
-
return rowToAgentMemory(row);
|
|
4804
|
-
}
|
|
4805
|
-
|
|
4806
|
-
export function updateMemoryEmbedding(id: string, embedding: Buffer): void {
|
|
4807
|
-
getDb().prepare("UPDATE agent_memory SET embedding = ? WHERE id = ?").run(embedding, id);
|
|
4808
|
-
}
|
|
4809
|
-
|
|
4810
|
-
export interface SearchMemoriesOptions {
|
|
4811
|
-
scope?: "agent" | "swarm" | "all";
|
|
4812
|
-
limit?: number;
|
|
4813
|
-
source?: AgentMemorySource;
|
|
4814
|
-
isLead?: boolean;
|
|
4815
|
-
}
|
|
4816
|
-
|
|
4817
|
-
export function searchMemoriesByVector(
|
|
4818
|
-
queryEmbedding: Float32Array,
|
|
4819
|
-
agentId: string,
|
|
4820
|
-
options: SearchMemoriesOptions = {},
|
|
4821
|
-
): (AgentMemory & { similarity: number })[] {
|
|
4822
|
-
const { scope = "all", limit = 10, source, isLead = false } = options;
|
|
4823
|
-
|
|
4824
|
-
// Build WHERE clause
|
|
4825
|
-
const conditions: string[] = ["embedding IS NOT NULL"];
|
|
4826
|
-
const params: (string | null)[] = [];
|
|
4827
|
-
|
|
4828
|
-
if (!isLead) {
|
|
4829
|
-
// Workers see their own agent-scoped + all swarm-scoped
|
|
4830
|
-
if (scope === "agent") {
|
|
4831
|
-
conditions.push("agentId = ? AND scope = 'agent'");
|
|
4832
|
-
params.push(agentId);
|
|
4833
|
-
} else if (scope === "swarm") {
|
|
4834
|
-
conditions.push("scope = 'swarm'");
|
|
4835
|
-
} else {
|
|
4836
|
-
// "all" - own agent + swarm
|
|
4837
|
-
conditions.push("(agentId = ? OR scope = 'swarm')");
|
|
4838
|
-
params.push(agentId);
|
|
4839
|
-
}
|
|
4840
|
-
} else {
|
|
4841
|
-
// Leads see everything
|
|
4842
|
-
if (scope === "agent") {
|
|
4843
|
-
conditions.push("scope = 'agent'");
|
|
4844
|
-
} else if (scope === "swarm") {
|
|
4845
|
-
conditions.push("scope = 'swarm'");
|
|
4846
|
-
}
|
|
4847
|
-
// "all" for lead = no scope filter needed
|
|
4848
|
-
}
|
|
4849
|
-
|
|
4850
|
-
if (source) {
|
|
4851
|
-
conditions.push("source = ?");
|
|
4852
|
-
params.push(source);
|
|
4853
|
-
}
|
|
4854
|
-
|
|
4855
|
-
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4856
|
-
|
|
4857
|
-
const rows = getDb()
|
|
4858
|
-
.prepare<AgentMemoryRow, (string | null)[]>(`SELECT * FROM agent_memory ${whereClause}`)
|
|
4859
|
-
.all(...params);
|
|
4860
|
-
|
|
4861
|
-
// Import cosine similarity inline to avoid circular deps
|
|
4862
|
-
const { cosineSimilarity, deserializeEmbedding } = require("./embedding");
|
|
4863
|
-
|
|
4864
|
-
// Compute similarities and sort
|
|
4865
|
-
const results: (AgentMemory & { similarity: number })[] = [];
|
|
4866
|
-
for (const row of rows) {
|
|
4867
|
-
if (!row.embedding) continue;
|
|
4868
|
-
const embedding = deserializeEmbedding(row.embedding);
|
|
4869
|
-
// Skip embeddings with mismatched dimensions (can happen if embedding model changes)
|
|
4870
|
-
if (embedding.length !== queryEmbedding.length) continue;
|
|
4871
|
-
const similarity = cosineSimilarity(queryEmbedding, embedding) as number;
|
|
4872
|
-
results.push({ ...rowToAgentMemory(row), similarity });
|
|
4873
|
-
}
|
|
4874
|
-
|
|
4875
|
-
results.sort((a, b) => b.similarity - a.similarity);
|
|
4876
|
-
return results.slice(0, limit);
|
|
4877
|
-
}
|
|
4878
|
-
|
|
4879
|
-
export interface ListMemoriesOptions {
|
|
4880
|
-
scope?: "agent" | "swarm" | "all";
|
|
4881
|
-
limit?: number;
|
|
4882
|
-
offset?: number;
|
|
4883
|
-
isLead?: boolean;
|
|
4884
|
-
}
|
|
4885
|
-
|
|
4886
|
-
export function listMemoriesByAgent(
|
|
4887
|
-
agentId: string,
|
|
4888
|
-
options: ListMemoriesOptions = {},
|
|
4889
|
-
): AgentMemory[] {
|
|
4890
|
-
const { scope = "all", limit = 20, offset = 0, isLead = false } = options;
|
|
4891
|
-
|
|
4892
|
-
const conditions: string[] = [];
|
|
4893
|
-
const params: (string | number)[] = [];
|
|
4894
|
-
|
|
4895
|
-
if (!isLead) {
|
|
4896
|
-
if (scope === "agent") {
|
|
4897
|
-
conditions.push("agentId = ? AND scope = 'agent'");
|
|
4898
|
-
params.push(agentId);
|
|
4899
|
-
} else if (scope === "swarm") {
|
|
4900
|
-
conditions.push("scope = 'swarm'");
|
|
4901
|
-
} else {
|
|
4902
|
-
conditions.push("(agentId = ? OR scope = 'swarm')");
|
|
4903
|
-
params.push(agentId);
|
|
4904
|
-
}
|
|
4905
|
-
} else {
|
|
4906
|
-
if (scope === "agent") {
|
|
4907
|
-
conditions.push("scope = 'agent'");
|
|
4908
|
-
} else if (scope === "swarm") {
|
|
4909
|
-
conditions.push("scope = 'swarm'");
|
|
4910
|
-
}
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4913
|
-
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4914
|
-
|
|
4915
|
-
params.push(limit, offset);
|
|
4916
|
-
|
|
4917
|
-
const rows = getDb()
|
|
4918
|
-
.prepare<AgentMemoryRow, (string | number)[]>(
|
|
4919
|
-
`SELECT * FROM agent_memory ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
|
|
4920
|
-
)
|
|
4921
|
-
.all(...params);
|
|
4922
|
-
|
|
4923
|
-
return rows.map(rowToAgentMemory);
|
|
4924
|
-
}
|
|
4925
|
-
|
|
4926
|
-
export function deleteMemoriesBySourcePath(sourcePath: string, agentId: string): number {
|
|
4927
|
-
const result = getDb()
|
|
4928
|
-
.prepare("DELETE FROM agent_memory WHERE sourcePath = ? AND agentId = ?")
|
|
4929
|
-
.run(sourcePath, agentId);
|
|
4930
|
-
return result.changes;
|
|
4931
|
-
}
|
|
4932
|
-
|
|
4933
|
-
export function deleteMemory(id: string): boolean {
|
|
4934
|
-
const result = getDb().prepare("DELETE FROM agent_memory WHERE id = ?").run(id);
|
|
4935
|
-
return result.changes > 0;
|
|
4936
|
-
}
|
|
4937
|
-
|
|
4938
|
-
export function getMemoryStats(agentId: string): {
|
|
4939
|
-
total: number;
|
|
4940
|
-
bySource: Record<string, number>;
|
|
4941
|
-
byScope: Record<string, number>;
|
|
4942
|
-
} {
|
|
4943
|
-
const total = getDb()
|
|
4944
|
-
.prepare<{ count: number }, [string]>(
|
|
4945
|
-
"SELECT COUNT(*) as count FROM agent_memory WHERE agentId = ?",
|
|
4946
|
-
)
|
|
4947
|
-
.get(agentId);
|
|
4948
|
-
|
|
4949
|
-
const bySourceRows = getDb()
|
|
4950
|
-
.prepare<{ source: string; count: number }, [string]>(
|
|
4951
|
-
"SELECT source, COUNT(*) as count FROM agent_memory WHERE agentId = ? GROUP BY source",
|
|
4952
|
-
)
|
|
4953
|
-
.all(agentId);
|
|
4954
|
-
|
|
4955
|
-
const byScopeRows = getDb()
|
|
4956
|
-
.prepare<{ scope: string; count: number }, [string]>(
|
|
4957
|
-
"SELECT scope, COUNT(*) as count FROM agent_memory WHERE agentId = ? GROUP BY scope",
|
|
4958
|
-
)
|
|
4959
|
-
.all(agentId);
|
|
4960
|
-
|
|
4961
|
-
const bySource: Record<string, number> = {};
|
|
4962
|
-
for (const row of bySourceRows) {
|
|
4963
|
-
bySource[row.source] = row.count;
|
|
4964
|
-
}
|
|
4965
|
-
|
|
4966
|
-
const byScope: Record<string, number> = {};
|
|
4967
|
-
for (const row of byScopeRows) {
|
|
4968
|
-
byScope[row.scope] = row.count;
|
|
4969
|
-
}
|
|
4970
|
-
|
|
4971
|
-
return { total: total?.count ?? 0, bySource, byScope };
|
|
4972
|
-
}
|
|
4973
|
-
|
|
4974
4702
|
// ============================================================================
|
|
4975
4703
|
// AgentMail Inbox Mapping Queries
|
|
4976
4704
|
// ============================================================================
|
package/src/be/embedding.ts
CHANGED
|
@@ -1,41 +1,3 @@
|
|
|
1
|
-
import OpenAI from "openai";
|
|
2
|
-
|
|
3
|
-
let openai: OpenAI | null = null;
|
|
4
|
-
|
|
5
|
-
function getClient(): OpenAI | null {
|
|
6
|
-
if (!process.env.OPENAI_API_KEY) return null;
|
|
7
|
-
if (!openai) openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
8
|
-
return openai;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Generate an embedding vector for the given text using OpenAI text-embedding-3-small (512 dims).
|
|
13
|
-
* Returns null if OPENAI_API_KEY is not set or the API call fails.
|
|
14
|
-
*/
|
|
15
|
-
export async function getEmbedding(text: string): Promise<Float32Array | null> {
|
|
16
|
-
const client = getClient();
|
|
17
|
-
if (!client) return null;
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const cleaned = text.replace(/[\n\r]/g, " ").trim();
|
|
21
|
-
if (!cleaned) return null;
|
|
22
|
-
|
|
23
|
-
const response = await client.embeddings.create({
|
|
24
|
-
model: "text-embedding-3-small",
|
|
25
|
-
input: cleaned,
|
|
26
|
-
dimensions: 512,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const values = response.data[0]?.embedding;
|
|
30
|
-
if (!values) return null;
|
|
31
|
-
|
|
32
|
-
return new Float32Array(values);
|
|
33
|
-
} catch (err) {
|
|
34
|
-
console.error("[memory] Embedding failed:", (err as Error).message);
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
1
|
/**
|
|
40
2
|
* Compute cosine similarity between two Float32Array vectors.
|
|
41
3
|
* Returns a value between -1 and 1 (1 = identical, 0 = orthogonal, -1 = opposite).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentMemorySource } from "@/types";
|
|
2
|
+
|
|
3
|
+
function numEnv(key: string, fallback: number): number {
|
|
4
|
+
const val = process.env[key];
|
|
5
|
+
if (val === undefined) return fallback;
|
|
6
|
+
const parsed = Number(val);
|
|
7
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// TTL defaults (in days) — null means no expiry
|
|
11
|
+
export const TTL_DEFAULTS: Record<AgentMemorySource, number | null> = {
|
|
12
|
+
task_completion: 7,
|
|
13
|
+
session_summary: 3,
|
|
14
|
+
file_index: 30,
|
|
15
|
+
manual: null,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Reranking parameters
|
|
19
|
+
export const RECENCY_DECAY_HALF_LIFE_DAYS = numEnv("MEMORY_RECENCY_HALF_LIFE_DAYS", 14);
|
|
20
|
+
export const ACCESS_BOOST_MAX_MULTIPLIER = numEnv("MEMORY_ACCESS_BOOST_MAX", 1.5);
|
|
21
|
+
export const ACCESS_BOOST_RECENCY_WINDOW_HOURS = numEnv("MEMORY_ACCESS_RECENCY_HOURS", 48);
|
|
22
|
+
export const CANDIDATE_SET_MULTIPLIER = numEnv("MEMORY_CANDIDATE_MULTIPLIER", 3);
|
|
23
|
+
|
|
24
|
+
// Embedding defaults
|
|
25
|
+
export const DEFAULT_EMBEDDING_DIMENSIONS = 512;
|
|
26
|
+
export const DEFAULT_EMBEDDING_MODEL = "openai/text-embedding-3-small";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EmbeddingProvider, MemoryStore } from "./types";
|
|
2
|
+
|
|
3
|
+
let embeddingProvider: EmbeddingProvider | null = null;
|
|
4
|
+
let memoryStore: MemoryStore | null = null;
|
|
5
|
+
|
|
6
|
+
export function getEmbeddingProvider(): EmbeddingProvider {
|
|
7
|
+
if (!embeddingProvider) {
|
|
8
|
+
const { OpenAIEmbeddingProvider } =
|
|
9
|
+
require("./providers/openai-embedding") as typeof import("./providers/openai-embedding");
|
|
10
|
+
embeddingProvider = new OpenAIEmbeddingProvider();
|
|
11
|
+
}
|
|
12
|
+
return embeddingProvider;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getMemoryStore(): MemoryStore {
|
|
16
|
+
if (!memoryStore) {
|
|
17
|
+
const { SqliteMemoryStore } =
|
|
18
|
+
require("./providers/sqlite-store") as typeof import("./providers/sqlite-store");
|
|
19
|
+
memoryStore = new SqliteMemoryStore();
|
|
20
|
+
}
|
|
21
|
+
return memoryStore;
|
|
22
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { DEFAULT_EMBEDDING_DIMENSIONS, DEFAULT_EMBEDDING_MODEL } from "../constants";
|
|
3
|
+
import type { EmbeddingProvider } from "../types";
|
|
4
|
+
|
|
5
|
+
interface OpenAIEmbeddingConfig {
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
dimensions?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly dimensions: number;
|
|
14
|
+
|
|
15
|
+
private client: OpenAI | null = null;
|
|
16
|
+
private readonly model: string;
|
|
17
|
+
private readonly apiKey: string | undefined;
|
|
18
|
+
|
|
19
|
+
constructor(config?: OpenAIEmbeddingConfig) {
|
|
20
|
+
this.apiKey = config?.apiKey ?? process.env.OPENAI_API_KEY;
|
|
21
|
+
this.model = config?.model ?? "text-embedding-3-small";
|
|
22
|
+
this.dimensions = config?.dimensions ?? DEFAULT_EMBEDDING_DIMENSIONS;
|
|
23
|
+
this.name = config?.model ? `openai/${config.model}` : DEFAULT_EMBEDDING_MODEL;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private getClient(): OpenAI | null {
|
|
27
|
+
if (!this.apiKey) return null;
|
|
28
|
+
if (!this.client) this.client = new OpenAI({ apiKey: this.apiKey });
|
|
29
|
+
return this.client;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async embed(text: string): Promise<Float32Array | null> {
|
|
33
|
+
const client = this.getClient();
|
|
34
|
+
if (!client) return null;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const cleaned = text.replace(/[\n\r]/g, " ").trim();
|
|
38
|
+
if (!cleaned) return null;
|
|
39
|
+
|
|
40
|
+
const response = await client.embeddings.create({
|
|
41
|
+
model: this.model,
|
|
42
|
+
input: cleaned,
|
|
43
|
+
dimensions: this.dimensions,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const values = response.data[0]?.embedding;
|
|
47
|
+
if (!values) return null;
|
|
48
|
+
|
|
49
|
+
return new Float32Array(values);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error("[memory] Embedding failed:", (err as Error).message);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async embedBatch(texts: string[]): Promise<(Float32Array | null)[]> {
|
|
57
|
+
const client = this.getClient();
|
|
58
|
+
if (!client) return texts.map(() => null);
|
|
59
|
+
|
|
60
|
+
const cleaned = texts.map((t) => t.replace(/[\n\r]/g, " ").trim());
|
|
61
|
+
const nonEmptyIndices: number[] = [];
|
|
62
|
+
const nonEmptyTexts: string[] = [];
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
65
|
+
if (cleaned[i]) {
|
|
66
|
+
nonEmptyIndices.push(i);
|
|
67
|
+
nonEmptyTexts.push(cleaned[i]!);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (nonEmptyTexts.length === 0) return texts.map(() => null);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const response = await client.embeddings.create({
|
|
75
|
+
model: this.model,
|
|
76
|
+
input: nonEmptyTexts,
|
|
77
|
+
dimensions: this.dimensions,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const results: (Float32Array | null)[] = texts.map(() => null);
|
|
81
|
+
for (const item of response.data) {
|
|
82
|
+
const originalIndex = nonEmptyIndices[item.index];
|
|
83
|
+
if (originalIndex !== undefined && item.embedding) {
|
|
84
|
+
results[originalIndex] = new Float32Array(item.embedding);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return results;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error("[memory] Batch embedding failed:", (err as Error).message);
|
|
91
|
+
return texts.map(() => null);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|