agentic 0.1.0 → 0.2.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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.agentic.yml +2 -0
  3. data/.architecture/decisions/ArchitecturalFeatureBuilder.md +136 -0
  4. data/.architecture/decisions/ArchitectureConsiderations.md +200 -0
  5. data/.architecture/decisions/adr_001_observer_pattern_implementation.md +196 -0
  6. data/.architecture/decisions/adr_002_plan_orchestrator.md +320 -0
  7. data/.architecture/decisions/adr_003_plan_orchestrator_interface.md +179 -0
  8. data/.architecture/decisions/adrs/ADR-001-dependency-management.md +147 -0
  9. data/.architecture/decisions/adrs/ADR-002-system-boundaries.md +162 -0
  10. data/.architecture/decisions/adrs/ADR-003-content-safety.md +158 -0
  11. data/.architecture/decisions/adrs/ADR-004-agent-permissions.md +161 -0
  12. data/.architecture/decisions/adrs/ADR-005-adaptation-engine.md +127 -0
  13. data/.architecture/decisions/adrs/ADR-006-extension-system.md +273 -0
  14. data/.architecture/decisions/adrs/ADR-007-learning-system.md +156 -0
  15. data/.architecture/decisions/adrs/ADR-008-prompt-generation.md +325 -0
  16. data/.architecture/decisions/adrs/ADR-009-task-failure-handling.md +353 -0
  17. data/.architecture/decisions/adrs/ADR-010-task-input-handling.md +251 -0
  18. data/.architecture/decisions/adrs/ADR-011-task-observable-pattern.md +391 -0
  19. data/.architecture/decisions/adrs/ADR-012-task-output-handling.md +205 -0
  20. data/.architecture/decisions/adrs/ADR-013-architecture-alignment.md +211 -0
  21. data/.architecture/decisions/adrs/ADR-014-agent-capability-registry.md +80 -0
  22. data/.architecture/decisions/adrs/ADR-015-persistent-agent-store.md +100 -0
  23. data/.architecture/decisions/adrs/ADR-016-agent-assembly-engine.md +117 -0
  24. data/.architecture/decisions/adrs/ADR-017-streaming-observability.md +171 -0
  25. data/.architecture/decisions/capability_tools_distinction.md +150 -0
  26. data/.architecture/decisions/cli_command_structure.md +61 -0
  27. data/.architecture/implementation/agent_self_assembly_implementation.md +267 -0
  28. data/.architecture/implementation/agent_self_assembly_summary.md +138 -0
  29. data/.architecture/members.yml +187 -0
  30. data/.architecture/planning/self_implementation_exercise.md +295 -0
  31. data/.architecture/planning/session_compaction_rule.md +43 -0
  32. data/.architecture/planning/streaming_observability_feature.md +223 -0
  33. data/.architecture/principles.md +151 -0
  34. data/.architecture/recalibration/0-2-0.md +92 -0
  35. data/.architecture/recalibration/agent_self_assembly.md +238 -0
  36. data/.architecture/recalibration/cli_command_structure.md +91 -0
  37. data/.architecture/recalibration/implementation_roadmap_0-2-0.md +301 -0
  38. data/.architecture/recalibration/progress_tracking_0-2-0.md +114 -0
  39. data/.architecture/recalibration_process.md +127 -0
  40. data/.architecture/reviews/0-2-0.md +181 -0
  41. data/.architecture/reviews/cli_command_duplication.md +98 -0
  42. data/.architecture/templates/adr.md +105 -0
  43. data/.architecture/templates/implementation_roadmap.md +125 -0
  44. data/.architecture/templates/progress_tracking.md +89 -0
  45. data/.architecture/templates/recalibration_plan.md +70 -0
  46. data/.architecture/templates/version_comparison.md +124 -0
  47. data/.claude/settings.local.json +13 -0
  48. data/.claude-sessions/001-task-class-architecture-implementation.md +129 -0
  49. data/.claude-sessions/002-plan-orchestrator-interface-review.md +105 -0
  50. data/.claude-sessions/architecture-governance-implementation.md +37 -0
  51. data/.claude-sessions/architecture-review-session.md +27 -0
  52. data/ArchitecturalFeatureBuilder.md +136 -0
  53. data/ArchitectureConsiderations.md +229 -0
  54. data/CHANGELOG.md +57 -2
  55. data/CLAUDE.md +111 -0
  56. data/CONTRIBUTING.md +286 -0
  57. data/MAINTAINING.md +301 -0
  58. data/README.md +582 -28
  59. data/docs/agent_capabilities_api.md +259 -0
  60. data/docs/artifact_extension_points.md +757 -0
  61. data/docs/artifact_generation_architecture.md +323 -0
  62. data/docs/artifact_implementation_plan.md +596 -0
  63. data/docs/artifact_integration_points.md +345 -0
  64. data/docs/artifact_verification_strategies.md +581 -0
  65. data/docs/streaming_observability_architecture.md +510 -0
  66. data/exe/agentic +6 -1
  67. data/lefthook.yml +5 -0
  68. data/lib/agentic/adaptation_engine.rb +124 -0
  69. data/lib/agentic/agent.rb +181 -4
  70. data/lib/agentic/agent_assembly_engine.rb +442 -0
  71. data/lib/agentic/agent_capability_registry.rb +260 -0
  72. data/lib/agentic/agent_config.rb +63 -0
  73. data/lib/agentic/agent_specification.rb +46 -0
  74. data/lib/agentic/capabilities/examples.rb +530 -0
  75. data/lib/agentic/capabilities.rb +14 -0
  76. data/lib/agentic/capability_provider.rb +146 -0
  77. data/lib/agentic/capability_specification.rb +118 -0
  78. data/lib/agentic/cli/agent.rb +31 -0
  79. data/lib/agentic/cli/capabilities.rb +191 -0
  80. data/lib/agentic/cli/config.rb +134 -0
  81. data/lib/agentic/cli/execution_observer.rb +796 -0
  82. data/lib/agentic/cli.rb +1068 -0
  83. data/lib/agentic/default_agent_provider.rb +35 -0
  84. data/lib/agentic/errors/llm_error.rb +184 -0
  85. data/lib/agentic/execution_plan.rb +53 -0
  86. data/lib/agentic/execution_result.rb +91 -0
  87. data/lib/agentic/expected_answer_format.rb +46 -0
  88. data/lib/agentic/extension/domain_adapter.rb +109 -0
  89. data/lib/agentic/extension/plugin_manager.rb +163 -0
  90. data/lib/agentic/extension/protocol_handler.rb +116 -0
  91. data/lib/agentic/extension.rb +45 -0
  92. data/lib/agentic/factory_methods.rb +9 -1
  93. data/lib/agentic/generation_stats.rb +61 -0
  94. data/lib/agentic/learning/README.md +84 -0
  95. data/lib/agentic/learning/capability_optimizer.rb +613 -0
  96. data/lib/agentic/learning/execution_history_store.rb +251 -0
  97. data/lib/agentic/learning/pattern_recognizer.rb +500 -0
  98. data/lib/agentic/learning/strategy_optimizer.rb +706 -0
  99. data/lib/agentic/learning.rb +131 -0
  100. data/lib/agentic/llm_assisted_composition_strategy.rb +188 -0
  101. data/lib/agentic/llm_client.rb +215 -15
  102. data/lib/agentic/llm_config.rb +65 -1
  103. data/lib/agentic/llm_response.rb +163 -0
  104. data/lib/agentic/logger.rb +1 -1
  105. data/lib/agentic/observable.rb +51 -0
  106. data/lib/agentic/persistent_agent_store.rb +385 -0
  107. data/lib/agentic/plan_execution_result.rb +129 -0
  108. data/lib/agentic/plan_orchestrator.rb +464 -0
  109. data/lib/agentic/plan_orchestrator_config.rb +57 -0
  110. data/lib/agentic/retry_config.rb +63 -0
  111. data/lib/agentic/retry_handler.rb +125 -0
  112. data/lib/agentic/structured_outputs.rb +1 -1
  113. data/lib/agentic/task.rb +193 -0
  114. data/lib/agentic/task_definition.rb +39 -0
  115. data/lib/agentic/task_execution_result.rb +92 -0
  116. data/lib/agentic/task_failure.rb +66 -0
  117. data/lib/agentic/task_output_schemas.rb +112 -0
  118. data/lib/agentic/task_planner.rb +54 -19
  119. data/lib/agentic/task_result.rb +48 -0
  120. data/lib/agentic/ui.rb +244 -0
  121. data/lib/agentic/verification/critic_framework.rb +116 -0
  122. data/lib/agentic/verification/llm_verification_strategy.rb +60 -0
  123. data/lib/agentic/verification/schema_verification_strategy.rb +47 -0
  124. data/lib/agentic/verification/verification_hub.rb +62 -0
  125. data/lib/agentic/verification/verification_result.rb +50 -0
  126. data/lib/agentic/verification/verification_strategy.rb +26 -0
  127. data/lib/agentic/version.rb +1 -1
  128. data/lib/agentic.rb +74 -2
  129. data/plugins/README.md +41 -0
  130. metadata +245 -6
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors/llm_error"
4
+ require_relative "generation_stats"
5
+
6
+ module Agentic
7
+ # Value object representing a response from an LLM
8
+ class LlmResponse
9
+ # @return [Hash, String, nil] The parsed content from the LLM response
10
+ attr_reader :content
11
+
12
+ # @return [String, nil] The refusal message, if the LLM refused the request
13
+ attr_reader :refusal
14
+
15
+ # @return [Hash] The raw response from the LLM API
16
+ attr_reader :raw_response
17
+
18
+ # @return [Agentic::Errors::LlmError, nil] The error object, if an error occurred
19
+ attr_reader :error
20
+
21
+ # @return [Agentic::Errors::LlmRefusalError, nil] The refusal error object, if the LLM refused the request
22
+ attr_reader :refusal_error
23
+
24
+ # @return [GenerationStats, nil] Statistics about the generation
25
+ attr_reader :stats
26
+
27
+ # Initializes a new LlmResponse
28
+ # @param raw_response [Hash] The raw response from the LLM API
29
+ # @param parsed_content [Hash, String, nil] Optional pre-parsed content
30
+ # @param error [Agentic::Errors::LlmError, nil] Optional error object
31
+ def initialize(raw_response, parsed_content: nil, error: nil, refusal_error: nil)
32
+ @raw_response = raw_response
33
+ @refusal = raw_response&.dig("choices", 0, "message", "refusal")
34
+ @content = parsed_content || parse_content(raw_response)
35
+ @error = error
36
+ @refusal_error = refusal_error
37
+ @stats = raw_response ? GenerationStats.from_response(raw_response) : nil
38
+ end
39
+
40
+ # Creates a successful response
41
+ # @param raw_response [Hash] The raw response from the LLM API
42
+ # @param content [Hash, String] The parsed content
43
+ # @return [LlmResponse] A successful response
44
+ def self.success(raw_response, content)
45
+ new(raw_response, parsed_content: content)
46
+ end
47
+
48
+ # Creates a refusal response
49
+ # @param raw_response [Hash] The raw response from the LLM API
50
+ # @param refusal [String] The refusal message
51
+ # @param refusal_error [Agentic::Errors::LlmRefusalError, nil] The refusal error object
52
+ # @return [LlmResponse] A refusal response
53
+ def self.refusal(raw_response, refusal, refusal_error = nil)
54
+ new(
55
+ raw_response,
56
+ parsed_content: nil,
57
+ refusal_error: refusal_error
58
+ )
59
+ end
60
+
61
+ # Creates an error response
62
+ # @param error [Agentic::Errors::LlmError] The error that occurred
63
+ # @param raw_response [Hash, nil] The raw response from the LLM API, if available
64
+ # @return [LlmResponse] An error response
65
+ def self.error(error, raw_response = nil)
66
+ new(raw_response, error: error)
67
+ end
68
+
69
+ # Checks if the response was successful
70
+ # @return [Boolean] True if the response was successful
71
+ def successful?
72
+ !refused? && !error?
73
+ end
74
+
75
+ # Checks if the request was refused
76
+ # @return [Boolean] True if the request was refused
77
+ def refused?
78
+ !@refusal.nil? || !@refusal_error.nil?
79
+ end
80
+
81
+ # Gets the refusal category if available
82
+ # @return [Symbol, nil] The refusal category, or nil if not refused
83
+ def refusal_category
84
+ @refusal_error&.refusal_category
85
+ end
86
+
87
+ # Checks if the refusal can be retried with modifications
88
+ # @return [Boolean] True if the refusal can be retried with modifications, false otherwise
89
+ def retryable_refusal?
90
+ @refusal_error&.retryable_with_modifications? || false
91
+ end
92
+
93
+ # Checks if an error occurred
94
+ # @return [Boolean] True if an error occurred
95
+ def error?
96
+ !@error.nil?
97
+ end
98
+
99
+ # Raises the error if one occurred
100
+ # @return [void]
101
+ # @raise [Agentic::Errors::LlmError] If an error occurred
102
+ def raise_if_error!
103
+ raise @error if error?
104
+ end
105
+
106
+ # Converts the response to a hash
107
+ # @return [Hash] The response as a hash
108
+ def to_h
109
+ base = {stats: @stats&.to_h}
110
+
111
+ if error?
112
+ base.merge({
113
+ error: {
114
+ message: @error.message,
115
+ type: @error.class.name
116
+ },
117
+ content: nil,
118
+ refusal: nil,
119
+ refusal_category: nil
120
+ })
121
+ elsif refused?
122
+ refusal_info = {
123
+ refusal: @refusal,
124
+ content: nil,
125
+ error: nil
126
+ }
127
+
128
+ # Add refusal category if available
129
+ if @refusal_error
130
+ refusal_info[:refusal_category] = @refusal_error.refusal_category
131
+ refusal_info[:retryable] = @refusal_error.retryable_with_modifications?
132
+ end
133
+
134
+ base.merge(refusal_info)
135
+ else
136
+ base.merge({
137
+ content: @content,
138
+ refusal: nil,
139
+ error: nil,
140
+ refusal_category: nil
141
+ })
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ # Parses the content from the raw response
148
+ # @param response [Hash] The raw response from the LLM API
149
+ # @return [Hash, String, nil] The parsed content
150
+ def parse_content(response)
151
+ return nil if response.nil?
152
+
153
+ content_text = response.dig("choices", 0, "message", "content")
154
+ return nil if content_text.nil?
155
+
156
+ begin
157
+ JSON.parse(content_text)
158
+ rescue JSON::ParserError
159
+ content_text
160
+ end
161
+ end
162
+ end
163
+ end
@@ -27,7 +27,7 @@ module Agentic
27
27
 
28
28
  def initialize(*args)
29
29
  super
30
- @formatter = SimpleFormatter < ::Logger::Formatter
30
+ @formatter = SimpleFormatter.new
31
31
  end
32
32
 
33
33
  def self.info(message)
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ # Custom implementation of the Observer pattern
5
+ # Provides a thread-safe way for objects to notify observers of state changes
6
+ module Observable
7
+ # Add an observer to this object
8
+ # @param observer [Object] The observer object
9
+ # @return [void]
10
+ def add_observer(observer)
11
+ @_observers ||= []
12
+ @_observers << observer unless @_observers.include?(observer)
13
+ end
14
+
15
+ # Remove an observer from this object
16
+ # @param observer [Object] The observer object
17
+ # @return [void]
18
+ def delete_observer(observer)
19
+ @_observers&.delete(observer)
20
+ end
21
+
22
+ # Remove all observers from this object
23
+ # @return [void]
24
+ def delete_observers
25
+ @_observers = []
26
+ end
27
+
28
+ # Return the number of observers
29
+ # @return [Integer] The number of observers
30
+ def count_observers
31
+ @_observers ? @_observers.size : 0
32
+ end
33
+
34
+ # Notify all observers of an event
35
+ # @param event_type [Symbol] The type of event
36
+ # @param *args Arguments to pass to the observers
37
+ # @return [void]
38
+ def notify_observers(event_type, *args)
39
+ return unless @_observers
40
+
41
+ # Make a thread-safe copy of the observers array
42
+ observers = @_observers.dup
43
+
44
+ observers.each do |observer|
45
+ if observer.respond_to?(:update)
46
+ observer.update(event_type, self, *args)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,385 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "json"
5
+ require "fileutils"
6
+ require "securerandom"
7
+
8
+ module Agentic
9
+ # Responsible for storing and retrieving agent configurations persistently
10
+ # @attr_reader [String] storage_path Directory to store agent configurations
11
+ # @attr_reader [AgentCapabilityRegistry] registry The capability registry used for instantiation
12
+ class PersistentAgentStore
13
+ attr_reader :storage_path, :registry
14
+
15
+ # Initialize a new persistent agent store
16
+ # @param storage_path [String, nil] The path to the storage directory
17
+ # @param registry [AgentCapabilityRegistry] The capability registry for instantiation
18
+ # @param options [Hash] Additional options
19
+ # @option options [Logger] :logger Custom logger (defaults to Agentic.logger)
20
+ def initialize(storage_path = nil, registry = AgentCapabilityRegistry.instance, options = {})
21
+ @registry = registry
22
+ @storage_path = storage_path || default_storage_path
23
+ @logger = options[:logger] || Agentic.logger
24
+ @index = {}
25
+
26
+ # Create the storage directory if it doesn't exist
27
+ FileUtils.mkdir_p(@storage_path) unless File.directory?(@storage_path)
28
+
29
+ # Initialize the index
30
+ initialize_index
31
+ end
32
+
33
+ # Store an agent configuration
34
+ # @param agent [Agent] The agent to store
35
+ # @param name [String, nil] The name to use for the agent (generated if nil)
36
+ # @param metadata [Hash] Additional metadata to store with the agent
37
+ # @return [String] The ID of the stored agent
38
+ def store(agent, name: nil, metadata: {})
39
+ # Generate ID if agent doesn't have one
40
+ id = agent&.id || SecureRandom.uuid
41
+
42
+ # Generate version
43
+ version = generate_version(id)
44
+
45
+ # Create agent data structure
46
+ agent_data = {
47
+ id: id,
48
+ name: name || generate_name(agent),
49
+ version: version,
50
+ timestamp: Time.now.iso8601,
51
+ agent: agent.to_h,
52
+ capabilities: agent.capabilities.keys.map do |capability_name|
53
+ {
54
+ name: capability_name,
55
+ version: agent.capability_specification(capability_name)&.version
56
+ }
57
+ end,
58
+ metadata: metadata
59
+ }
60
+
61
+ # Save to storage
62
+ save_to_storage(id, version, agent_data)
63
+
64
+ # Update index
65
+ update_index(id, version, agent_data)
66
+
67
+ id
68
+ end
69
+
70
+ # Build an agent from a stored configuration
71
+ # @param id_or_name [String] The ID or name of the agent
72
+ # @param version [String, nil] The version to load (latest if nil)
73
+ # @return [Agent, nil] The built agent or nil if not found
74
+ def build_agent(id_or_name, version: nil)
75
+ # First try to find by ID
76
+ agent_data = find_agent_data(id_or_name, version)
77
+
78
+ # If not found by ID, try to find by name
79
+ unless agent_data
80
+ id = find_id_by_name(id_or_name)
81
+ agent_data = id ? find_agent_data(id, version) : nil
82
+ end
83
+
84
+ return nil unless agent_data
85
+
86
+ # Create a new agent
87
+ agent = Agent.build do |a|
88
+ a.role = agent_data[:agent][:role]
89
+ a.purpose = agent_data[:agent][:purpose]
90
+ a.backstory = agent_data[:agent][:backstory]
91
+ end
92
+
93
+ # Add capabilities
94
+ agent_data[:capabilities].each do |capability|
95
+ agent.add_capability(capability[:name], capability[:version])
96
+ rescue => e
97
+ @logger.warn("Failed to add capability: #{capability[:name]} v#{capability[:version]} - #{e.message}")
98
+ end
99
+
100
+ agent
101
+ end
102
+
103
+ # List all stored agent configurations
104
+ # @param filter [Hash] Filter criteria
105
+ # @option filter [String] :capability Filter by capability name
106
+ # @option filter [String] :capability_version Filter by capability name and version
107
+ # @option filter [Time, String] :after Filter agents stored after this time
108
+ # @option filter [Time, String] :before Filter agents stored before this time
109
+ # @option filter [Hash] :metadata Filter by metadata values
110
+ # @return [Array<Hash>] Array of agent configurations
111
+ def list_all(filter = {})
112
+ results = []
113
+
114
+ @index.each do |id, versions|
115
+ # Get the latest version for each agent by default
116
+ version = get_latest_version(id)
117
+
118
+ # Skip if no version found
119
+ next unless version
120
+
121
+ # Get the agent data
122
+ agent_data = versions[version]
123
+
124
+ # Skip if no data found
125
+ next unless agent_data
126
+
127
+ # Add ID and version to the data
128
+ full_data = agent_data.merge(
129
+ id: id,
130
+ version: version
131
+ )
132
+
133
+ # Apply filters
134
+ if matches_filter?(full_data, filter)
135
+ results << full_data
136
+ end
137
+ end
138
+
139
+ # Sort by timestamp (newest first)
140
+ results.sort_by { |data| data[:timestamp] }.reverse
141
+ end
142
+
143
+ # Alias to list_all for a more concise API
144
+ # @param filter [Hash] Filter criteria
145
+ # @return [Array<Hash>] Array of agent configurations
146
+ alias_method :all, :list_all
147
+
148
+ # Get the version history for an agent
149
+ # @param id [String] The ID of the agent
150
+ # @return [Array<Hash>] The version history or empty array if not found
151
+ def version_history(id)
152
+ # Check if the agent exists
153
+ return [] unless @index[id]
154
+
155
+ # Get all versions and sort by timestamp
156
+ versions = @index[id].map do |version, data|
157
+ {
158
+ id: id,
159
+ name: data[:name],
160
+ version: version,
161
+ timestamp: data[:timestamp],
162
+ capabilities: data[:capabilities],
163
+ metadata: data[:metadata]
164
+ }
165
+ end
166
+
167
+ # Sort by timestamp (newest first)
168
+ versions.sort_by { |v| v[:timestamp] }.reverse
169
+ end
170
+
171
+ # Delete an agent from the store
172
+ # @param id_or_name [String] The ID or name of the agent to delete
173
+ # @param version [String, nil] The version to delete (all versions if nil)
174
+ # @return [Boolean] True if successfully deleted
175
+ def delete(id_or_name, version: nil)
176
+ # First try to find by ID
177
+ id = id_or_name
178
+
179
+ # If not found in index, try to find by name
180
+ unless @index[id]
181
+ id = find_id_by_name(id_or_name)
182
+ return false unless id
183
+ end
184
+
185
+ # Check if the agent exists
186
+ return false unless @index[id]
187
+
188
+ if version
189
+ # Delete specific version
190
+ return false unless @index[id][version]
191
+
192
+ # Delete from storage
193
+ delete_from_storage(id, version)
194
+
195
+ # Update index
196
+ @index[id].delete(version)
197
+ @index.delete(id) if @index[id].empty?
198
+ else
199
+ # Delete all versions
200
+ @index[id].each_key do |ver|
201
+ delete_from_storage(id, ver)
202
+ end
203
+
204
+ # Update index
205
+ @index.delete(id)
206
+ end
207
+
208
+ # Save the index
209
+ save_index
210
+
211
+ true
212
+ end
213
+
214
+ private
215
+
216
+ def default_storage_path
217
+ # Use a default path within the user's home directory
218
+ File.join(Dir.home, ".agentic", "agents")
219
+ end
220
+
221
+ def initialize_index
222
+ # Load the index from storage if it exists
223
+ index_path = File.join(@storage_path, "index.json")
224
+ if File.exist?(index_path)
225
+ begin
226
+ @index = JSON.parse(File.read(index_path), symbolize_names: true)
227
+ rescue JSON::ParserError => e
228
+ @logger.error("Failed to parse agent store index: #{e.message}")
229
+ @index = {}
230
+ end
231
+ end
232
+ end
233
+
234
+ def save_index
235
+ # Save the index to storage
236
+ index_path = File.join(@storage_path, "index.json")
237
+ File.write(index_path, JSON.pretty_generate(@index))
238
+ end
239
+
240
+ def save_to_storage(id, version, agent_data)
241
+ # Create directory for the agent if it doesn't exist
242
+ agent_dir = File.join(@storage_path, id)
243
+ FileUtils.mkdir_p(agent_dir) unless File.directory?(agent_dir)
244
+
245
+ # Save the agent data
246
+ File.write(
247
+ File.join(agent_dir, "#{version}.json"),
248
+ JSON.pretty_generate(agent_data)
249
+ )
250
+ end
251
+
252
+ def update_index(id, version, agent_data)
253
+ # Add to the index
254
+ @index[id] ||= {}
255
+ @index[id][version] = {
256
+ name: agent_data[:name],
257
+ timestamp: agent_data[:timestamp],
258
+ capabilities: agent_data[:capabilities],
259
+ metadata: agent_data[:metadata]
260
+ }
261
+
262
+ # Save the index
263
+ save_index
264
+ end
265
+
266
+ def delete_from_storage(id, version)
267
+ # Delete the agent file
268
+ agent_path = File.join(@storage_path, id, "#{version}.json")
269
+ File.delete(agent_path) if File.exist?(agent_path)
270
+
271
+ # Delete the agent directory if it's empty
272
+ agent_dir = File.join(@storage_path, id)
273
+ Dir.rmdir(agent_dir) if Dir.empty?(agent_dir)
274
+ end
275
+
276
+ def find_agent_data(id, version = nil)
277
+ # Check if the agent exists
278
+ return nil unless @index[id]
279
+
280
+ # Determine which version to load
281
+ version ||= get_latest_version(id)
282
+ return nil unless version
283
+
284
+ # Load from storage
285
+ agent_path = File.join(@storage_path, id, "#{version}.json")
286
+ return nil unless File.exist?(agent_path)
287
+
288
+ begin
289
+ JSON.parse(File.read(agent_path), symbolize_names: true)
290
+ rescue JSON::ParserError => e
291
+ @logger.error("Failed to parse agent data: #{e.message}")
292
+ nil
293
+ end
294
+ end
295
+
296
+ def find_id_by_name(name)
297
+ # Find an agent ID by name
298
+ @index.each do |id, versions|
299
+ versions.each do |_, data|
300
+ return id if data[:name] == name
301
+ end
302
+ end
303
+
304
+ nil
305
+ end
306
+
307
+ def get_latest_version(id)
308
+ # Check if the agent exists
309
+ return nil unless @index[id]
310
+
311
+ # Get all versions
312
+ versions = @index[id].keys
313
+
314
+ # Sort versions semantically
315
+ versions.max do |a, b|
316
+ a_parts = a.split(".").map(&:to_i)
317
+ b_parts = b.split(".").map(&:to_i)
318
+
319
+ # Compare major version
320
+ major_comparison = a_parts[0] <=> b_parts[0]
321
+ next major_comparison unless major_comparison == 0
322
+
323
+ # Compare minor version
324
+ minor_comparison = a_parts[1] <=> b_parts[1]
325
+ next minor_comparison unless minor_comparison == 0
326
+
327
+ # Compare patch version
328
+ a_parts[2] <=> b_parts[2]
329
+ end
330
+ end
331
+
332
+ def generate_version(id)
333
+ # Get the current versions
334
+ versions = @index[id] ? @index[id].keys : []
335
+
336
+ if versions.empty?
337
+ # First version
338
+ "1.0.0"
339
+ else
340
+ # Get the latest version
341
+ latest = get_latest_version(id)
342
+
343
+ # Increment the patch version
344
+ parts = latest.split(".").map(&:to_i)
345
+ "#{parts[0]}.#{parts[1]}.#{parts[2] + 1}"
346
+ end
347
+ end
348
+
349
+ def generate_name(agent)
350
+ # Generate a name based on the agent's role
351
+ base = agent.role ? agent.role.downcase.gsub(/[^a-z0-9]/, "_") : "agent"
352
+ "#{base}_#{SecureRandom.hex(4)}"
353
+ end
354
+
355
+ def matches_filter?(agent_data, filter)
356
+ return true if filter.empty?
357
+
358
+ filter.all? do |key, value|
359
+ case key
360
+ when :capability, "capability"
361
+ agent_data[:capabilities].any? { |cap| cap[:name] == value }
362
+ when :capability_version, "capability_version"
363
+ name, version = value.split(":", 2)
364
+ agent_data[:capabilities].any? { |cap| cap[:name] == name && cap[:version] == version }
365
+ when :after, "after"
366
+ # Convert value to Time if it's a string
367
+ threshold = value.is_a?(String) ? Time.parse(value) : value
368
+ Time.parse(agent_data[:timestamp]) >= threshold
369
+ when :before, "before"
370
+ # Convert value to Time if it's a string
371
+ threshold = value.is_a?(String) ? Time.parse(value) : value
372
+ Time.parse(agent_data[:timestamp]) <= threshold
373
+ when :metadata, "metadata"
374
+ # Match all metadata criteria
375
+ value.all? do |meta_key, meta_value|
376
+ agent_data[:metadata] && agent_data[:metadata][meta_key] == meta_value
377
+ end
378
+ else
379
+ # For other criteria, check if the property matches
380
+ agent_data[key] == value
381
+ end
382
+ end
383
+ end
384
+ end
385
+ end