swarm_sdk 2.7.14 → 3.0.0.alpha2

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.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
  4. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  5. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  6. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  7. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  8. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  9. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  10. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  11. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  12. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  13. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  14. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  15. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  16. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  17. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  18. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  19. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  20. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  24. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  25. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  26. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  27. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  28. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  29. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  30. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  31. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  32. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  33. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  34. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  35. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  36. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  37. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  38. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  39. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  40. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  41. data/lib/swarm_sdk/v3/tools/document_converters/base.rb +84 -0
  42. data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
  43. data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
  45. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  46. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  47. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  48. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  49. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  50. data/lib/swarm_sdk/v3/tools/read.rb +213 -0
  51. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  52. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  53. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  54. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  55. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  56. data/lib/swarm_sdk/v3.rb +145 -0
  57. metadata +88 -149
  58. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  59. data/lib/swarm_sdk/agent/builder.rb +0 -705
  60. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  61. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  62. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  63. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  64. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  65. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  66. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  67. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  68. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  69. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  70. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  71. data/lib/swarm_sdk/agent/context.rb +0 -115
  72. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  73. data/lib/swarm_sdk/agent/definition.rb +0 -588
  74. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  75. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  76. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  77. data/lib/swarm_sdk/agent_registry.rb +0 -146
  78. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  79. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  80. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  81. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  82. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  83. data/lib/swarm_sdk/config.rb +0 -368
  84. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  85. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  86. data/lib/swarm_sdk/configuration.rb +0 -165
  87. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  88. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  89. data/lib/swarm_sdk/context_compactor.rb +0 -335
  90. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  91. data/lib/swarm_sdk/context_management/context.rb +0 -328
  92. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  93. data/lib/swarm_sdk/defaults.rb +0 -251
  94. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  95. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  96. data/lib/swarm_sdk/hooks/context.rb +0 -197
  97. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  98. data/lib/swarm_sdk/hooks/error.rb +0 -29
  99. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  100. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  101. data/lib/swarm_sdk/hooks/result.rb +0 -150
  102. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  103. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  104. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  105. data/lib/swarm_sdk/log_collector.rb +0 -227
  106. data/lib/swarm_sdk/log_stream.rb +0 -127
  107. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  108. data/lib/swarm_sdk/model_aliases.json +0 -8
  109. data/lib/swarm_sdk/models.json +0 -44002
  110. data/lib/swarm_sdk/models.rb +0 -161
  111. data/lib/swarm_sdk/node_context.rb +0 -245
  112. data/lib/swarm_sdk/observer/builder.rb +0 -81
  113. data/lib/swarm_sdk/observer/config.rb +0 -45
  114. data/lib/swarm_sdk/observer/manager.rb +0 -248
  115. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  116. data/lib/swarm_sdk/permissions/config.rb +0 -239
  117. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  118. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  119. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  120. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  121. data/lib/swarm_sdk/plugin.rb +0 -309
  122. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  123. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  124. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  125. data/lib/swarm_sdk/restore_result.rb +0 -65
  126. data/lib/swarm_sdk/result.rb +0 -241
  127. data/lib/swarm_sdk/snapshot.rb +0 -156
  128. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  129. data/lib/swarm_sdk/state_restorer.rb +0 -476
  130. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  131. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  132. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  133. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  134. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  135. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  136. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  141. data/lib/swarm_sdk/swarm.rb +0 -973
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/base.rb +0 -63
  145. data/lib/swarm_sdk/tools/bash.rb +0 -280
  146. data/lib/swarm_sdk/tools/clock.rb +0 -46
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  160. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  161. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  162. data/lib/swarm_sdk/tools/read.rb +0 -261
  163. data/lib/swarm_sdk/tools/registry.rb +0 -205
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  166. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  167. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  168. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  169. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  170. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  171. data/lib/swarm_sdk/tools/think.rb +0 -100
  172. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  173. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  174. data/lib/swarm_sdk/tools/write.rb +0 -112
  175. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  176. data/lib/swarm_sdk/utils.rb +0 -68
  177. data/lib/swarm_sdk/validation_result.rb +0 -33
  178. data/lib/swarm_sdk/version.rb +0 -5
  179. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  180. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  181. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  182. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  183. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  184. data/lib/swarm_sdk/workflow.rb +0 -589
  185. data/lib/swarm_sdk.rb +0 -721
@@ -0,0 +1,507 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Memory
6
+ module Adapters
7
+ # SQLite-based storage adapter with sqlite-vec vector index
8
+ #
9
+ # Stores cards, edges, clusters, and vector embeddings in a single
10
+ # SQLite database for durable, transactional, multi-process-safe storage.
11
+ # Vector search uses sqlite-vec's vec0 virtual table with cosine
12
+ # distance, eliminating external FAISS files and ID mapping tables.
13
+ #
14
+ # Database structure:
15
+ # .swarm/memory/
16
+ # └── memory.db (SQLite database with vec0 virtual table)
17
+ #
18
+ # ## Why UPSERT instead of INSERT OR REPLACE
19
+ #
20
+ # `INSERT OR REPLACE` deletes the old row then inserts a new one.
21
+ # With `ON DELETE CASCADE` on edges, this silently destroys all
22
+ # edges for the card on every update (including `record_access!`).
23
+ # `ON CONFLICT DO UPDATE` updates in-place without triggering CASCADE.
24
+ #
25
+ # @example
26
+ # adapter = SqliteAdapter.new("/path/to/.swarm/memory")
27
+ # adapter.load
28
+ # adapter.write_card(card)
29
+ # adapter.save
30
+ class SqliteAdapter < Base
31
+ include VectorUtils
32
+
33
+ # Embedding dimensions for MiniLM-L6-v2 model
34
+ EMBEDDING_DIMENSIONS = 384
35
+
36
+ # @return [String] Root directory for storage
37
+ attr_reader :directory
38
+
39
+ # SQL schema for the memory database
40
+ SCHEMA_SQL = <<~SQL
41
+ CREATE TABLE IF NOT EXISTS cards (
42
+ id TEXT PRIMARY KEY,
43
+ text TEXT NOT NULL,
44
+ type TEXT NOT NULL,
45
+ entities TEXT NOT NULL DEFAULT '[]',
46
+ source_turn_ids TEXT NOT NULL DEFAULT '[]',
47
+ embedding TEXT,
48
+ importance REAL NOT NULL DEFAULT 0.5,
49
+ confidence REAL NOT NULL DEFAULT 1.0,
50
+ access_count INTEGER NOT NULL DEFAULT 0,
51
+ last_accessed TEXT,
52
+ dwell REAL NOT NULL DEFAULT 0.0,
53
+ compression_level INTEGER NOT NULL DEFAULT 0,
54
+ canonical_id TEXT,
55
+ created_at TEXT NOT NULL,
56
+ updated_at TEXT NOT NULL
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS edges (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ from_id TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
62
+ to_id TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
63
+ type TEXT NOT NULL,
64
+ weight REAL NOT NULL DEFAULT 1.0,
65
+ created_at TEXT NOT NULL
66
+ );
67
+
68
+ CREATE INDEX IF NOT EXISTS idx_edges_from_id ON edges(from_id);
69
+ CREATE INDEX IF NOT EXISTS idx_edges_to_id ON edges(to_id);
70
+
71
+ CREATE TABLE IF NOT EXISTS clusters (
72
+ id TEXT PRIMARY KEY,
73
+ title TEXT NOT NULL,
74
+ embedding TEXT,
75
+ rolling_summary TEXT NOT NULL DEFAULT '',
76
+ decision_log TEXT NOT NULL DEFAULT '[]',
77
+ key_entities TEXT NOT NULL DEFAULT '[]',
78
+ card_ids TEXT NOT NULL DEFAULT '[]',
79
+ created_at TEXT NOT NULL,
80
+ updated_at TEXT NOT NULL
81
+ );
82
+ SQL
83
+
84
+ # @param directory [String] Root directory for memory storage
85
+ def initialize(directory)
86
+ super()
87
+ @directory = File.expand_path(directory)
88
+ @db = nil
89
+ end
90
+
91
+ # --- Card CRUD ---
92
+
93
+ # @param card [Card] Card to write (insert or update)
94
+ # @return [void]
95
+ def write_card(card)
96
+ @db.execute(<<~SQL, card_params(card))
97
+ INSERT INTO cards (id, text, type, entities, source_turn_ids, embedding,
98
+ importance, confidence, access_count, last_accessed,
99
+ dwell, compression_level, canonical_id, created_at, updated_at)
100
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
101
+ ON CONFLICT(id) DO UPDATE SET
102
+ text = excluded.text,
103
+ type = excluded.type,
104
+ entities = excluded.entities,
105
+ source_turn_ids = excluded.source_turn_ids,
106
+ embedding = excluded.embedding,
107
+ importance = excluded.importance,
108
+ confidence = excluded.confidence,
109
+ access_count = excluded.access_count,
110
+ last_accessed = excluded.last_accessed,
111
+ dwell = excluded.dwell,
112
+ compression_level = excluded.compression_level,
113
+ canonical_id = excluded.canonical_id,
114
+ updated_at = excluded.updated_at
115
+ SQL
116
+ upsert_vec_embedding(card.id, card.embedding) if card.embedding
117
+ end
118
+
119
+ # @param id [String] Card ID
120
+ # @return [Card, nil]
121
+ def read_card(id)
122
+ row = @db.execute("SELECT * FROM cards WHERE id = ?", [id]).first
123
+ return unless row
124
+
125
+ card_from_row(row)
126
+ end
127
+
128
+ # @param id [String] Card ID
129
+ # @return [void]
130
+ def delete_card(id)
131
+ @db.execute("DELETE FROM cards WHERE id = ?", [id])
132
+ @db.execute("DELETE FROM vec_cards WHERE card_id = ?", [id])
133
+ end
134
+
135
+ # @param prefix [String, nil] ID prefix filter
136
+ # @return [Array<Card>]
137
+ def list_cards(prefix: nil)
138
+ rows = if prefix
139
+ @db.execute("SELECT * FROM cards WHERE id LIKE ?", ["#{prefix}%"])
140
+ else
141
+ @db.execute("SELECT * FROM cards")
142
+ end
143
+ rows.map { |row| card_from_row(row) }
144
+ end
145
+
146
+ # @param max_level [Integer] Maximum compression level to include
147
+ # @return [Array<Card>] Cards eligible for compression
148
+ def list_cards_for_compression(max_level: 3)
149
+ rows = @db.execute("SELECT * FROM cards WHERE compression_level <= ?", [max_level])
150
+ rows.map { |row| card_from_row(row) }
151
+ end
152
+
153
+ # --- Edge CRUD ---
154
+
155
+ # @param edge [Edge] Edge to write
156
+ # @return [void]
157
+ def write_edge(edge)
158
+ @db.execute(
159
+ "INSERT INTO edges (from_id, to_id, type, weight, created_at) VALUES (?, ?, ?, ?, ?)",
160
+ [edge.from_id, edge.to_id, edge.type.to_s, edge.weight, edge.created_at.iso8601],
161
+ )
162
+ end
163
+
164
+ # @param card_id [String] Card ID
165
+ # @param type [Symbol, nil] Filter by edge type
166
+ # @return [Array<Edge>]
167
+ def edges_for(card_id, type: nil)
168
+ rows = if type
169
+ @db.execute(
170
+ "SELECT * FROM edges WHERE (from_id = ? OR to_id = ?) AND type = ?",
171
+ [card_id, card_id, type.to_s],
172
+ )
173
+ else
174
+ @db.execute(
175
+ "SELECT * FROM edges WHERE from_id = ? OR to_id = ?",
176
+ [card_id, card_id],
177
+ )
178
+ end
179
+ rows.map { |row| edge_from_row(row) }
180
+ end
181
+
182
+ # @param card_id [String] Card ID
183
+ # @return [void]
184
+ def delete_edges_for(card_id)
185
+ @db.execute("DELETE FROM edges WHERE from_id = ? OR to_id = ?", [card_id, card_id])
186
+ end
187
+
188
+ # --- Cluster CRUD ---
189
+
190
+ # @param cluster [Cluster] Cluster to write (insert or update)
191
+ # @return [void]
192
+ def write_cluster(cluster)
193
+ @db.execute(<<~SQL, cluster_params(cluster))
194
+ INSERT INTO clusters (id, title, embedding, rolling_summary, decision_log,
195
+ key_entities, card_ids, created_at, updated_at)
196
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
197
+ ON CONFLICT(id) DO UPDATE SET
198
+ title = excluded.title,
199
+ embedding = excluded.embedding,
200
+ rolling_summary = excluded.rolling_summary,
201
+ decision_log = excluded.decision_log,
202
+ key_entities = excluded.key_entities,
203
+ card_ids = excluded.card_ids,
204
+ updated_at = excluded.updated_at
205
+ SQL
206
+ end
207
+
208
+ # @param id [String] Cluster ID
209
+ # @return [Cluster, nil]
210
+ def read_cluster(id)
211
+ row = @db.execute("SELECT * FROM clusters WHERE id = ?", [id]).first
212
+ return unless row
213
+
214
+ cluster_from_row(row)
215
+ end
216
+
217
+ # @return [Array<Cluster>]
218
+ def list_clusters
219
+ rows = @db.execute("SELECT * FROM clusters")
220
+ rows.map { |row| cluster_from_row(row) }
221
+ end
222
+
223
+ # --- Vector Search ---
224
+
225
+ # Search the vec0 virtual table for similar vectors
226
+ #
227
+ # sqlite-vec's cosine distance returns values where 0 = identical
228
+ # and 2 = opposite. We convert to similarity: `1.0 - distance`.
229
+ #
230
+ # @param embedding [Array<Float>] Query embedding
231
+ # @param top_k [Integer] Maximum number of results
232
+ # @param threshold [Float] Minimum cosine similarity to include
233
+ # @return [Array<Hash>] Array of `{ id: String, similarity: Float }`
234
+ #
235
+ # @example
236
+ # results = adapter.vector_search(query_embedding, top_k: 5, threshold: 0.7)
237
+ # results.each { |r| puts "#{r[:id]}: #{r[:similarity]}" }
238
+ def vector_search(embedding, top_k:, threshold: 0.0)
239
+ rows = @db.execute(
240
+ "SELECT card_id, distance FROM vec_cards WHERE embedding MATCH ? AND k = ?",
241
+ [embedding.pack("f*"), top_k],
242
+ )
243
+
244
+ results = []
245
+ rows.each do |row|
246
+ sim = 1.0 - row["distance"]
247
+ next if sim < threshold
248
+
249
+ results << { id: row["card_id"], similarity: sim }
250
+ end
251
+ results
252
+ end
253
+
254
+ # Rebuild the vec0 index from all stored card embeddings
255
+ #
256
+ # Drops and recreates the vec_cards virtual table, then re-inserts
257
+ # all card embeddings from the cards table.
258
+ #
259
+ # @return [void]
260
+ def rebuild_index
261
+ @db.execute("DROP TABLE IF EXISTS vec_cards")
262
+ create_vec_table!
263
+ @db.execute("SELECT id, embedding FROM cards WHERE embedding IS NOT NULL").each do |row|
264
+ embedding = JSON.parse(row["embedding"])
265
+ @db.execute(
266
+ "INSERT INTO vec_cards(card_id, embedding) VALUES (?, ?)",
267
+ [row["id"], embedding.pack("f*")],
268
+ )
269
+ end
270
+ end
271
+
272
+ # --- Transactions ---
273
+
274
+ # Execute a block within a SQLite transaction
275
+ #
276
+ # Uses IMMEDIATE mode to acquire a write lock at the start,
277
+ # preventing deadlocks in WAL mode.
278
+ #
279
+ # @yield Block to execute within the transaction
280
+ # @return [Object] Return value of the block
281
+ def transaction(&block)
282
+ @db.transaction(:immediate, &block)
283
+ end
284
+
285
+ # --- Persistence ---
286
+
287
+ # Ensure the storage directory exists
288
+ #
289
+ # SQLite with WAL mode is already durable — no external
290
+ # index files need saving.
291
+ #
292
+ # @return [void]
293
+ def save
294
+ FileUtils.mkdir_p(@directory)
295
+ end
296
+
297
+ # Load state from SQLite and initialize vec0 index
298
+ #
299
+ # Opens the database, creates schema if needed, loads the
300
+ # sqlite-vec extension, and creates the vec0 virtual table.
301
+ # If vec_cards is empty but cards have embeddings, auto-rebuilds
302
+ # the index (handles FAISS migration and first-run).
303
+ #
304
+ # @return [void]
305
+ def load
306
+ load_sqlite!
307
+ auto_rebuild_vec_if_needed!
308
+ end
309
+
310
+ private
311
+
312
+ # Open SQLite database, initialize schema, and load sqlite-vec
313
+ #
314
+ # @return [void]
315
+ def load_sqlite!
316
+ begin
317
+ require "sqlite3"
318
+ rescue LoadError
319
+ raise LoadError,
320
+ "sqlite3 gem is required for SqliteAdapter. Add `gem 'sqlite3'` to your Gemfile."
321
+ end
322
+
323
+ begin
324
+ require "sqlite_vec"
325
+ rescue LoadError
326
+ raise LoadError,
327
+ "sqlite_vec gem is required for SqliteAdapter. Add `gem 'sqlite_vec'` to your Gemfile."
328
+ end
329
+
330
+ FileUtils.mkdir_p(@directory)
331
+ db_path = File.join(@directory, "memory.db")
332
+ @db = SQLite3::Database.new(db_path)
333
+ @db.results_as_hash = true
334
+ @db.execute("PRAGMA journal_mode=WAL")
335
+ @db.execute("PRAGMA foreign_keys=ON")
336
+ @db.execute("PRAGMA busy_timeout=5000")
337
+ @db.execute_batch(SCHEMA_SQL)
338
+
339
+ @db.enable_load_extension(true)
340
+ SqliteVec.load(@db)
341
+ @db.enable_load_extension(false)
342
+
343
+ create_vec_table!
344
+ end
345
+
346
+ # Create the vec0 virtual table for vector search
347
+ #
348
+ # @return [void]
349
+ def create_vec_table!
350
+ @db.execute(<<~SQL)
351
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_cards USING vec0(
352
+ card_id TEXT PRIMARY KEY,
353
+ embedding FLOAT[#{EMBEDDING_DIMENSIONS}] DISTANCE_METRIC=cosine
354
+ )
355
+ SQL
356
+ end
357
+
358
+ # Upsert a card's embedding into the vec0 virtual table
359
+ #
360
+ # vec0 does not support ON CONFLICT, so we DELETE then INSERT.
361
+ #
362
+ # @param card_id [String] Card ID
363
+ # @param embedding [Array<Float>] Embedding vector
364
+ # @return [void]
365
+ def upsert_vec_embedding(card_id, embedding)
366
+ @db.execute("DELETE FROM vec_cards WHERE card_id = ?", [card_id])
367
+ @db.execute(
368
+ "INSERT INTO vec_cards(card_id, embedding) VALUES (?, ?)",
369
+ [card_id, embedding.pack("f*")],
370
+ )
371
+ end
372
+
373
+ # Auto-rebuild vec0 index if cards have embeddings but vec_cards is empty
374
+ #
375
+ # Handles migration from FAISS and first-run scenarios.
376
+ #
377
+ # @return [void]
378
+ def auto_rebuild_vec_if_needed!
379
+ vec_count = @db.execute("SELECT COUNT(*) AS cnt FROM vec_cards").first["cnt"]
380
+ return if vec_count > 0
381
+
382
+ has_embeddings = @db.execute(
383
+ "SELECT COUNT(*) AS cnt FROM cards WHERE embedding IS NOT NULL",
384
+ ).first["cnt"] > 0
385
+ rebuild_index if has_embeddings
386
+ end
387
+
388
+ # --- Card Serialization ---
389
+
390
+ # Convert a Card to bind parameter array
391
+ #
392
+ # @param card [Card] Card to serialize
393
+ # @return [Array] Bind values for INSERT/UPSERT
394
+ def card_params(card)
395
+ [
396
+ card.id,
397
+ card.text,
398
+ card.type.to_s,
399
+ JSON.generate(card.entities),
400
+ JSON.generate(card.source_turn_ids),
401
+ card.embedding ? JSON.generate(card.embedding) : nil,
402
+ card.importance,
403
+ card.confidence,
404
+ card.access_count,
405
+ card.last_accessed&.iso8601,
406
+ card.dwell,
407
+ card.compression_level,
408
+ card.canonical_id,
409
+ card.created_at.iso8601,
410
+ card.updated_at.iso8601,
411
+ ]
412
+ end
413
+
414
+ # Convert a SQLite row hash to a Card
415
+ #
416
+ # @param row [Hash] Row from SQLite (results_as_hash mode)
417
+ # @return [Card]
418
+ def card_from_row(row)
419
+ Card.new(
420
+ id: row["id"],
421
+ text: row["text"],
422
+ type: row["type"]&.to_sym || :fact,
423
+ entities: parse_json_array(row["entities"]),
424
+ source_turn_ids: parse_json_array(row["source_turn_ids"]),
425
+ embedding: row["embedding"] ? JSON.parse(row["embedding"]) : nil,
426
+ importance: row["importance"] || 0.5,
427
+ confidence: row["confidence"] || 1.0,
428
+ access_count: row["access_count"] || 0,
429
+ last_accessed: row["last_accessed"] ? Time.parse(row["last_accessed"]) : nil,
430
+ dwell: row["dwell"] || 0.0,
431
+ compression_level: row["compression_level"] || 0,
432
+ canonical_id: row["canonical_id"],
433
+ created_at: row["created_at"] ? Time.parse(row["created_at"]) : nil,
434
+ updated_at: row["updated_at"] ? Time.parse(row["updated_at"]) : nil,
435
+ )
436
+ end
437
+
438
+ # --- Edge Serialization ---
439
+
440
+ # Convert a SQLite row hash to an Edge
441
+ #
442
+ # @param row [Hash] Row from SQLite
443
+ # @return [Edge]
444
+ def edge_from_row(row)
445
+ Edge.new(
446
+ from_id: row["from_id"],
447
+ to_id: row["to_id"],
448
+ type: row["type"]&.to_sym,
449
+ weight: row["weight"] || 1.0,
450
+ created_at: row["created_at"] ? Time.parse(row["created_at"]) : nil,
451
+ )
452
+ end
453
+
454
+ # --- Cluster Serialization ---
455
+
456
+ # Convert a Cluster to bind parameter array
457
+ #
458
+ # @param cluster [Cluster] Cluster to serialize
459
+ # @return [Array] Bind values for INSERT/UPSERT
460
+ def cluster_params(cluster)
461
+ [
462
+ cluster.id,
463
+ cluster.title,
464
+ cluster.embedding ? JSON.generate(cluster.embedding) : nil,
465
+ cluster.rolling_summary,
466
+ JSON.generate(cluster.decision_log),
467
+ JSON.generate(cluster.key_entities),
468
+ JSON.generate(cluster.card_ids),
469
+ cluster.created_at.iso8601,
470
+ cluster.updated_at.iso8601,
471
+ ]
472
+ end
473
+
474
+ # Convert a SQLite row hash to a Cluster
475
+ #
476
+ # @param row [Hash] Row from SQLite
477
+ # @return [Cluster]
478
+ def cluster_from_row(row)
479
+ Cluster.new(
480
+ id: row["id"],
481
+ title: row["title"],
482
+ embedding: row["embedding"] ? JSON.parse(row["embedding"]) : nil,
483
+ rolling_summary: row["rolling_summary"] || "",
484
+ decision_log: parse_json_array(row["decision_log"]),
485
+ key_entities: parse_json_array(row["key_entities"]),
486
+ card_ids: parse_json_array(row["card_ids"]),
487
+ created_at: row["created_at"] ? Time.parse(row["created_at"]) : nil,
488
+ updated_at: row["updated_at"] ? Time.parse(row["updated_at"]) : nil,
489
+ )
490
+ end
491
+
492
+ # Parse a JSON array string, returning empty array on nil/error
493
+ #
494
+ # @param json_str [String, nil] JSON string
495
+ # @return [Array]
496
+ def parse_json_array(json_str)
497
+ return [] if json_str.nil? || json_str.empty?
498
+
499
+ JSON.parse(json_str)
500
+ rescue JSON::ParserError
501
+ []
502
+ end
503
+ end
504
+ end
505
+ end
506
+ end
507
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Memory
6
+ module Adapters
7
+ # Default in-memory vector math for adapters
8
+ #
9
+ # Provides a pure-Ruby cosine similarity implementation that adapters
10
+ # can include when they don't have a native vector engine. The
11
+ # {FilesystemAdapter} includes this module because its FAISS index
12
+ # only handles indexed top-k search — it cannot compute pairwise
13
+ # similarity between two arbitrary vectors.
14
+ #
15
+ # **When to use this module:**
16
+ # Include it if your adapter stores embeddings as Ruby arrays and
17
+ # doesn't have a native way to compute pairwise similarity. This is
18
+ # the common case for file-based, SQLite, or simple database adapters.
19
+ #
20
+ # **When NOT to use this module:**
21
+ # If your storage backend has native vector operations, implement
22
+ # {Base#similarity} directly using the backend's operator. Examples:
23
+ #
24
+ # - **pgvector**: `SELECT 1 - (a <=> b)` (cosine distance operator)
25
+ # - **Qdrant/Pinecone**: Use the client's similarity API
26
+ # - **Redis with RediSearch**: `FT.SEARCH` with vector scoring
27
+ #
28
+ # In those cases you skip this module entirely and write your own
29
+ # `similarity` method that delegates to the backend.
30
+ #
31
+ # @example Including in a custom adapter
32
+ # class SqliteAdapter < SwarmSDK::V3::Memory::Adapters::Base
33
+ # include SwarmSDK::V3::Memory::Adapters::VectorUtils
34
+ #
35
+ # # similarity() is now available via VectorUtils
36
+ # # Override vector_search with your own indexed search
37
+ # end
38
+ #
39
+ # @example NOT including — pgvector adapter
40
+ # class PgvectorAdapter < SwarmSDK::V3::Memory::Adapters::Base
41
+ # def similarity(embedding_a, embedding_b)
42
+ # # Compute server-side using pgvector's cosine distance
43
+ # result = @db.exec_params(
44
+ # "SELECT 1 - ($1::vector <=> $2::vector) AS sim",
45
+ # [embedding_a.to_s, embedding_b.to_s]
46
+ # )
47
+ # result[0]["sim"].to_f
48
+ # end
49
+ # end
50
+ module VectorUtils
51
+ # Compute cosine similarity between two embedding vectors
52
+ #
53
+ # This is a pure-Ruby implementation suitable for in-memory use.
54
+ # It computes: dot(a, b) / (||a|| * ||b||)
55
+ #
56
+ # For production adapters with native vector engines, override
57
+ # this method to delegate to the backend instead.
58
+ #
59
+ # @param embedding_a [Array<Float>] First embedding vector
60
+ # @param embedding_b [Array<Float>] Second embedding vector
61
+ # @return [Float] Cosine similarity (-1.0 to 1.0)
62
+ #
63
+ # @example
64
+ # sim = adapter.similarity(card_a.embedding, card_b.embedding)
65
+ # puts "Cards are duplicates" if sim > 0.92
66
+ def similarity(embedding_a, embedding_b)
67
+ dot = 0.0
68
+ mag_a = 0.0
69
+ mag_b = 0.0
70
+
71
+ embedding_a.each_with_index do |va, i|
72
+ vb = embedding_b[i]
73
+ dot += va * vb
74
+ mag_a += va * va
75
+ mag_b += vb * vb
76
+ end
77
+
78
+ mag_a = Math.sqrt(mag_a)
79
+ mag_b = Math.sqrt(mag_b)
80
+ return 0.0 if mag_a.zero? || mag_b.zero?
81
+
82
+ dot / (mag_a * mag_b)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end