swarm_sdk 2.7.14 → 3.0.0.alpha1

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 (181) 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/edit.rb +111 -0
  42. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  43. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  44. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  45. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  46. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  47. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  48. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  49. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  50. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  51. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  52. data/lib/swarm_sdk/v3.rb +145 -0
  53. metadata +83 -148
  54. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  55. data/lib/swarm_sdk/agent/builder.rb +0 -705
  56. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  57. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  58. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  59. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  60. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  61. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  62. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  63. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  64. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  65. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  66. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  67. data/lib/swarm_sdk/agent/context.rb +0 -115
  68. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  69. data/lib/swarm_sdk/agent/definition.rb +0 -588
  70. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  71. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  72. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  73. data/lib/swarm_sdk/agent_registry.rb +0 -146
  74. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  75. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  76. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  77. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  78. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  79. data/lib/swarm_sdk/config.rb +0 -368
  80. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  81. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  82. data/lib/swarm_sdk/configuration.rb +0 -165
  83. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  84. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  85. data/lib/swarm_sdk/context_compactor.rb +0 -335
  86. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  87. data/lib/swarm_sdk/context_management/context.rb +0 -328
  88. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  89. data/lib/swarm_sdk/defaults.rb +0 -251
  90. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  91. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  92. data/lib/swarm_sdk/hooks/context.rb +0 -197
  93. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  94. data/lib/swarm_sdk/hooks/error.rb +0 -29
  95. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  96. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  97. data/lib/swarm_sdk/hooks/result.rb +0 -150
  98. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  99. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  100. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  101. data/lib/swarm_sdk/log_collector.rb +0 -227
  102. data/lib/swarm_sdk/log_stream.rb +0 -127
  103. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  104. data/lib/swarm_sdk/model_aliases.json +0 -8
  105. data/lib/swarm_sdk/models.json +0 -44002
  106. data/lib/swarm_sdk/models.rb +0 -161
  107. data/lib/swarm_sdk/node_context.rb +0 -245
  108. data/lib/swarm_sdk/observer/builder.rb +0 -81
  109. data/lib/swarm_sdk/observer/config.rb +0 -45
  110. data/lib/swarm_sdk/observer/manager.rb +0 -248
  111. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  112. data/lib/swarm_sdk/permissions/config.rb +0 -239
  113. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  114. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  115. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  116. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  117. data/lib/swarm_sdk/plugin.rb +0 -309
  118. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  119. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  120. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  121. data/lib/swarm_sdk/restore_result.rb +0 -65
  122. data/lib/swarm_sdk/result.rb +0 -241
  123. data/lib/swarm_sdk/snapshot.rb +0 -156
  124. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  125. data/lib/swarm_sdk/state_restorer.rb +0 -476
  126. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  127. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  128. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  129. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  130. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  131. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  132. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  133. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  134. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  135. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  136. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  137. data/lib/swarm_sdk/swarm.rb +0 -973
  138. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  139. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  140. data/lib/swarm_sdk/tools/base.rb +0 -63
  141. data/lib/swarm_sdk/tools/bash.rb +0 -280
  142. data/lib/swarm_sdk/tools/clock.rb +0 -46
  143. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  144. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  145. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  146. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  147. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  148. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  149. data/lib/swarm_sdk/tools/edit.rb +0 -145
  150. data/lib/swarm_sdk/tools/glob.rb +0 -166
  151. data/lib/swarm_sdk/tools/grep.rb +0 -235
  152. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  153. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  154. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  155. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  156. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  157. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  158. data/lib/swarm_sdk/tools/read.rb +0 -261
  159. data/lib/swarm_sdk/tools/registry.rb +0 -205
  160. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  161. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  163. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  164. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  165. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  166. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  167. data/lib/swarm_sdk/tools/think.rb +0 -100
  168. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  169. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  170. data/lib/swarm_sdk/tools/write.rb +0 -112
  171. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  172. data/lib/swarm_sdk/utils.rb +0 -68
  173. data/lib/swarm_sdk/validation_result.rb +0 -33
  174. data/lib/swarm_sdk/version.rb +0 -5
  175. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  176. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  177. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  178. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  179. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  180. data/lib/swarm_sdk/workflow.rb +0 -589
  181. data/lib/swarm_sdk.rb +0 -721
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Memory
6
+ module Adapters
7
+ # Shared FAISS vector index support for adapters
8
+ #
9
+ # Provides FAISS-based vector search and index management that can
10
+ # be included by any adapter that stores embeddings as Ruby arrays.
11
+ # The FAISS index (IndexFlatIP with L2-normalized vectors) handles
12
+ # efficient top-k nearest neighbor search.
13
+ #
14
+ # ## Requirements for including classes
15
+ #
16
+ # The including class must:
17
+ # - Call {#initialize_faiss!} from its constructor
18
+ # - Define a `faiss_directory` method returning the path for FAISS files
19
+ # - Implement `list_cards` (used by {#rebuild_index})
20
+ #
21
+ # @example Including in a custom adapter
22
+ # class MyAdapter < Base
23
+ # include VectorUtils # pairwise similarity
24
+ # include FaissSupport # FAISS vector index
25
+ #
26
+ # def initialize(directory)
27
+ # super()
28
+ # @directory = directory
29
+ # initialize_faiss!
30
+ # end
31
+ #
32
+ # private
33
+ #
34
+ # def faiss_directory
35
+ # @directory
36
+ # end
37
+ # end
38
+ module FaissSupport
39
+ # Embedding dimensions for MiniLM-L6-v2 model
40
+ EMBEDDING_DIMENSIONS = 384
41
+
42
+ # Search the FAISS index for similar vectors
43
+ #
44
+ # @param embedding [Array<Float>] Query embedding
45
+ # @param top_k [Integer] Maximum number of results
46
+ # @param threshold [Float] Minimum cosine similarity to include
47
+ # @return [Array<Hash>] Array of `{ id: String, similarity: Float }`
48
+ def vector_search(embedding, top_k:, threshold: 0.0)
49
+ return [] unless @faiss_index && @faiss_index.ntotal > 0
50
+
51
+ normalized = normalize_vector(embedding)
52
+ effective_k = [top_k, @faiss_index.ntotal].min
53
+ # Explicit Numo conversion for safe FAISS C extension boundary crossing
54
+ query_vector = Numo::SFloat.cast([normalized])
55
+ distances, ids = @faiss_index.search(query_vector, effective_k)
56
+
57
+ index_to_id = @id_to_index.invert
58
+ # FAISS returns Numo::NArray — convert to Ruby arrays
59
+ id_row = ids.to_a.flatten
60
+ dist_row = distances.to_a.flatten
61
+ results = []
62
+ id_row.each_with_index do |idx, i|
63
+ next if idx < 0
64
+
65
+ card_id = index_to_id[idx]
66
+ next unless card_id
67
+
68
+ similarity = dist_row[i]
69
+ next if similarity < threshold
70
+
71
+ results << { id: card_id, similarity: similarity }
72
+ end
73
+
74
+ results
75
+ end
76
+
77
+ # Rebuild the FAISS index from all stored cards
78
+ #
79
+ # @return [void]
80
+ def rebuild_index
81
+ @faiss_index = create_faiss_index
82
+ @id_to_index = {}
83
+ @next_index_id = 0
84
+
85
+ list_cards.each do |card|
86
+ add_to_vector_index(card) if card.embedding
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ # Initialize FAISS instance variables
93
+ #
94
+ # Must be called from the including class's constructor.
95
+ #
96
+ # @return [void]
97
+ def initialize_faiss!
98
+ @id_to_index = {}
99
+ @next_index_id = 0
100
+ @faiss_index = nil
101
+ end
102
+
103
+ # Create a new FAISS IndexIDMap backed by IndexFlatIP
104
+ #
105
+ # @return [Faiss::IndexIDMap] New FAISS index
106
+ def create_faiss_index
107
+ require "faiss"
108
+ # Keep a reference to the inner index to prevent GC from collecting
109
+ # it while IndexIDMap holds a raw C pointer to it. Without this,
110
+ # Ruby's GC can free the inner index, leaving IndexIDMap with a
111
+ # dangling pointer that causes segfaults on add/search.
112
+ @faiss_inner_index = Faiss::IndexFlatIP.new(EMBEDDING_DIMENSIONS)
113
+ Faiss::IndexIDMap.new(@faiss_inner_index)
114
+ end
115
+
116
+ # Add a card's embedding to the FAISS index
117
+ #
118
+ # @param card [Card] Card with embedding
119
+ # @return [void]
120
+ def add_to_vector_index(card)
121
+ @faiss_index ||= create_faiss_index
122
+ normalized = normalize_vector(card.embedding)
123
+ index_id = @next_index_id
124
+ @next_index_id += 1
125
+ # Explicit Numo conversion avoids segfaults from implicit
126
+ # Ruby Array → Numo cast at the FAISS C extension boundary.
127
+ vectors = Numo::SFloat.cast([normalized])
128
+ ids = Numo::Int64.cast([index_id])
129
+ @faiss_index.add_with_ids(vectors, ids)
130
+ @id_to_index[card.id] = index_id
131
+ end
132
+
133
+ # Remove a card from the vector index
134
+ #
135
+ # Removes the ID mapping. Full FAISS rebuild needed for actual removal.
136
+ #
137
+ # @param card_id [String] Card ID
138
+ # @return [void]
139
+ def remove_from_vector_index(card_id)
140
+ @id_to_index.delete(card_id)
141
+ end
142
+
143
+ # L2-normalize a vector for cosine similarity via inner product
144
+ #
145
+ # @param vec [Array<Float>] Input vector
146
+ # @return [Array<Float>] Normalized vector
147
+ def normalize_vector(vec)
148
+ magnitude = Math.sqrt(vec.sum { |v| v * v })
149
+ return vec if magnitude.zero?
150
+
151
+ vec.map { |v| v / magnitude }
152
+ end
153
+
154
+ # Save the FAISS index and ID mapping to disk
155
+ #
156
+ # @return [void]
157
+ def save_faiss_index
158
+ return unless @faiss_index
159
+
160
+ path = File.join(faiss_directory, "index.faiss")
161
+ @faiss_index.save(path)
162
+
163
+ # Save ID mapping separately
164
+ mapping_path = File.join(faiss_directory, "index_mapping.json")
165
+ File.write(mapping_path, JSON.generate(
166
+ id_to_index: @id_to_index,
167
+ next_index_id: @next_index_id,
168
+ ))
169
+ end
170
+
171
+ # Load the FAISS index and ID mapping from disk
172
+ #
173
+ # @return [void]
174
+ def load_faiss_index
175
+ path = File.join(faiss_directory, "index.faiss")
176
+ mapping_path = File.join(faiss_directory, "index_mapping.json")
177
+
178
+ if File.exist?(path) && File.exist?(mapping_path)
179
+ require "faiss"
180
+ @faiss_index = Faiss::Index.load(path)
181
+ mapping = JSON.parse(File.read(mapping_path))
182
+ @id_to_index = mapping["id_to_index"] || {}
183
+ @next_index_id = mapping["next_index_id"] || 0
184
+ else
185
+ @faiss_index = nil
186
+ @id_to_index = {}
187
+ @next_index_id = 0
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Memory
6
+ module Adapters
7
+ # Filesystem-based storage adapter with FAISS vector index
8
+ #
9
+ # Stores cards as individual JSON files, edges and clusters in
10
+ # aggregate JSON files, and vectors in a FAISS IndexFlatIP index
11
+ # (cosine similarity via L2-normalized vectors + inner product).
12
+ #
13
+ # Directory structure:
14
+ # .swarm/memory/
15
+ # ├── cards/
16
+ # │ ├── card_a1b2c3.json
17
+ # │ └── ...
18
+ # ├── edges.json
19
+ # ├── clusters.json
20
+ # └── index.faiss
21
+ #
22
+ # @example
23
+ # adapter = FilesystemAdapter.new("/path/to/.swarm/memory")
24
+ # adapter.load
25
+ # adapter.write_card(card)
26
+ # adapter.save
27
+ class FilesystemAdapter < Base
28
+ include VectorUtils
29
+ include FaissSupport
30
+
31
+ # @return [String] Root directory for storage
32
+ attr_reader :directory
33
+
34
+ # @param directory [String] Root directory for memory storage
35
+ def initialize(directory)
36
+ super()
37
+ @directory = File.expand_path(directory)
38
+ @edges = []
39
+ @clusters = []
40
+ initialize_faiss!
41
+ end
42
+
43
+ # --- Card CRUD ---
44
+
45
+ # @param card [Card] Card to write
46
+ # @return [void]
47
+ def write_card(card)
48
+ ensure_directories!
49
+ path = card_path(card.id)
50
+ File.write(path, JSON.pretty_generate(card.to_h))
51
+ add_to_vector_index(card) if card.embedding
52
+ end
53
+
54
+ # @param id [String] Card ID
55
+ # @return [Card, nil]
56
+ def read_card(id)
57
+ path = card_path(id)
58
+ return unless File.exist?(path)
59
+
60
+ data = JSON.parse(File.read(path))
61
+ Card.from_h(data)
62
+ end
63
+
64
+ # @param id [String] Card ID
65
+ # @return [void]
66
+ def delete_card(id)
67
+ path = card_path(id)
68
+ File.delete(path) if File.exist?(path)
69
+ remove_from_vector_index(id)
70
+ end
71
+
72
+ # @param prefix [String, nil] ID prefix filter
73
+ # @return [Array<Card>]
74
+ def list_cards(prefix: nil)
75
+ ensure_directories!
76
+ pattern = prefix ? "#{prefix}*.json" : "*.json"
77
+ Dir.glob(File.join(cards_dir, pattern)).map do |path|
78
+ data = JSON.parse(File.read(path))
79
+ Card.from_h(data)
80
+ end
81
+ end
82
+
83
+ # --- Edge CRUD ---
84
+
85
+ # @param edge [Edge] Edge to write
86
+ # @return [void]
87
+ def write_edge(edge)
88
+ @edges << edge
89
+ end
90
+
91
+ # @param card_id [String] Card ID
92
+ # @param type [Symbol, nil] Filter by edge type
93
+ # @return [Array<Edge>]
94
+ def edges_for(card_id, type: nil)
95
+ results = @edges.select { |e| e.from_id == card_id || e.to_id == card_id }
96
+ results = results.select { |e| e.type == type } if type
97
+ results
98
+ end
99
+
100
+ # @param card_id [String] Card ID
101
+ # @return [void]
102
+ def delete_edges_for(card_id)
103
+ @edges.reject! { |e| e.from_id == card_id || e.to_id == card_id }
104
+ end
105
+
106
+ # --- Cluster CRUD ---
107
+
108
+ # @param cluster [Cluster] Cluster to write
109
+ # @return [void]
110
+ def write_cluster(cluster)
111
+ idx = @clusters.index { |c| c.id == cluster.id }
112
+ if idx
113
+ @clusters[idx] = cluster
114
+ else
115
+ @clusters << cluster
116
+ end
117
+ end
118
+
119
+ # @param id [String] Cluster ID
120
+ # @return [Cluster, nil]
121
+ def read_cluster(id)
122
+ @clusters.find { |c| c.id == id }
123
+ end
124
+
125
+ # @return [Array<Cluster>]
126
+ def list_clusters
127
+ @clusters.dup
128
+ end
129
+
130
+ # --- Persistence ---
131
+
132
+ # Save edges, clusters, and FAISS index to disk
133
+ #
134
+ # @return [void]
135
+ def save
136
+ ensure_directories!
137
+ save_edges
138
+ save_clusters
139
+ save_faiss_index
140
+ end
141
+
142
+ # Load edges, clusters, and FAISS index from disk
143
+ #
144
+ # @return [void]
145
+ def load
146
+ ensure_directories!
147
+ load_edges
148
+ load_clusters
149
+ load_faiss_index
150
+ end
151
+
152
+ private
153
+
154
+ # @return [String] Path to the FAISS index directory
155
+ def faiss_directory
156
+ @directory
157
+ end
158
+
159
+ # @return [String] Path to cards directory
160
+ def cards_dir
161
+ File.join(@directory, "cards")
162
+ end
163
+
164
+ # @param id [String] Card ID
165
+ # @return [String] Path to card JSON file
166
+ def card_path(id)
167
+ File.join(cards_dir, "#{id}.json")
168
+ end
169
+
170
+ # Ensure required directories exist
171
+ #
172
+ # @return [void]
173
+ def ensure_directories!
174
+ FileUtils.mkdir_p(cards_dir)
175
+ end
176
+
177
+ # --- Serialization ---
178
+
179
+ # @return [void]
180
+ def save_edges
181
+ path = File.join(@directory, "edges.json")
182
+ File.write(path, JSON.pretty_generate(@edges.map(&:to_h)))
183
+ end
184
+
185
+ # @return [void]
186
+ def load_edges
187
+ path = File.join(@directory, "edges.json")
188
+ return unless File.exist?(path)
189
+
190
+ data = JSON.parse(File.read(path))
191
+ @edges = data.map { |h| Edge.from_h(h) }
192
+ end
193
+
194
+ # @return [void]
195
+ def save_clusters
196
+ path = File.join(@directory, "clusters.json")
197
+ File.write(path, JSON.pretty_generate(@clusters.map(&:to_h)))
198
+ end
199
+
200
+ # @return [void]
201
+ def load_clusters
202
+ path = File.join(@directory, "clusters.json")
203
+ return unless File.exist?(path)
204
+
205
+ data = JSON.parse(File.read(path))
206
+ @clusters = data.map { |h| Cluster.from_h(h) }
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end