@desplega.ai/agent-swarm 1.64.1 → 1.66.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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.64.0",
5
+ "version": "1.66.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.64.1",
3
+ "version": "1.66.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>",
@@ -122,6 +122,7 @@
122
122
  "openai": "^6.22.0",
123
123
  "react": "^19.2.3",
124
124
  "react-devtools-core": "^7.0.1",
125
+ "sqlite-vec": "^0.1.9",
125
126
  "svix": "^1.62.0",
126
127
  "viem": "^2.46.3",
127
128
  "zod": "^4.2.1",
package/src/be/db.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
+ import pkg from "../../package.json";
2
3
  import { addEyesReactionOnTaskStart } from "../github/task-reactions";
3
4
  import { configureDbResolver } from "../prompts/resolver";
4
5
  import type {
@@ -7,9 +8,6 @@ import type {
7
8
  AgentLog,
8
9
  AgentLogEventType,
9
10
  AgentMcpServer,
10
- AgentMemory,
11
- AgentMemoryScope,
12
- AgentMemorySource,
13
11
  AgentSkill,
14
12
  AgentStatus,
15
13
  AgentTask,
@@ -64,6 +62,11 @@ import { runMigrations } from "./migrations/runner";
64
62
  import { seedDefaultTemplates } from "./seed";
65
63
 
66
64
  let db: Database | null = null;
65
+ let sqliteVecAvailable = false;
66
+
67
+ export function isSqliteVecAvailable(): boolean {
68
+ return sqliteVecAvailable;
69
+ }
67
70
 
68
71
  export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
69
72
  if (db) {
@@ -88,6 +91,19 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
88
91
  database.run("PRAGMA journal_mode = WAL;");
89
92
  database.run("PRAGMA foreign_keys = ON;");
90
93
 
94
+ // Load sqlite-vec extension for vector search
95
+ try {
96
+ const sqliteVec = require("sqlite-vec");
97
+ sqliteVec.load(database);
98
+ sqliteVecAvailable = true;
99
+ console.log("[db] sqlite-vec loaded");
100
+ } catch (err) {
101
+ console.warn(
102
+ "[db] sqlite-vec not available, falling back to in-memory cosine:",
103
+ (err as Error).message,
104
+ );
105
+ }
106
+
91
107
  // Run database migrations (schema creation + incremental changes)
92
108
  runMigrations(database);
93
109
 
@@ -749,6 +765,7 @@ type AgentTaskRow = {
749
765
  credentialKeySuffix: string | null;
750
766
  credentialKeyType: string | null;
751
767
  requestedByUserId: string | null;
768
+ swarmVersion: string | null;
752
769
  };
753
770
 
754
771
  function rowToAgentTask(row: AgentTaskRow): AgentTask {
@@ -808,6 +825,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
808
825
  credentialKeySuffix: row.credentialKeySuffix ?? undefined,
809
826
  credentialKeyType: row.credentialKeyType ?? undefined,
810
827
  requestedByUserId: row.requestedByUserId ?? undefined,
828
+ swarmVersion: row.swarmVersion ?? undefined,
811
829
  };
812
830
  }
813
831
 
@@ -824,10 +842,11 @@ export const taskQueries = {
824
842
  string | null,
825
843
  string | null,
826
844
  string | null,
845
+ string,
827
846
  ]
828
847
  >(
829
- `INSERT INTO agent_tasks (id, agentId, task, status, source, slackChannelId, slackThreadTs, slackUserId, createdAt, lastUpdatedAt)
830
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
848
+ `INSERT INTO agent_tasks (id, agentId, task, status, source, slackChannelId, slackThreadTs, slackUserId, swarmVersion, createdAt, lastUpdatedAt)
849
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) RETURNING *`,
831
850
  ),
832
851
 
833
852
  getById: () => getDb().prepare<AgentTaskRow, [string]>("SELECT * FROM agent_tasks WHERE id = ?"),
@@ -898,6 +917,7 @@ export function createTask(
898
917
  options?.slackChannelId ?? null,
899
918
  options?.slackThreadTs ?? null,
900
919
  options?.slackUserId ?? null,
920
+ pkg.version,
901
921
  );
902
922
  if (!row) throw new Error("Failed to create task");
903
923
  try {
@@ -1937,8 +1957,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
1937
1957
  vcsInstallationId, vcsNodeId,
1938
1958
  agentmailInboxId, agentmailMessageId, agentmailThreadId,
1939
1959
  mentionMessageId, mentionChannelId, dir, parentTaskId, model, scheduleId,
1940
- workflowRunId, workflowRunStepId, outputSchema, requestedByUserId, createdAt, lastUpdatedAt
1941
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
1960
+ workflowRunId, workflowRunStepId, outputSchema, requestedByUserId, swarmVersion, createdAt, lastUpdatedAt
1961
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
1942
1962
  )
1943
1963
  .get(
1944
1964
  id,
@@ -1978,6 +1998,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
1978
1998
  options?.workflowRunStepId ?? null,
1979
1999
  options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
1980
2000
  options?.requestedByUserId ?? null,
2001
+ pkg.version,
1981
2002
  now,
1982
2003
  now,
1983
2004
  );
@@ -4684,293 +4705,6 @@ export function deleteSwarmRepo(id: string): boolean {
4684
4705
  return result.changes > 0;
4685
4706
  }
4686
4707
 
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
4708
  // ============================================================================
4975
4709
  // AgentMail Inbox Mapping Queries
4976
4710
  // ============================================================================
@@ -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
+ }