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.
- checksums.yaml +4 -4
- data/.agentic.yml +2 -0
- data/.architecture/decisions/ArchitecturalFeatureBuilder.md +136 -0
- data/.architecture/decisions/ArchitectureConsiderations.md +200 -0
- data/.architecture/decisions/adr_001_observer_pattern_implementation.md +196 -0
- data/.architecture/decisions/adr_002_plan_orchestrator.md +320 -0
- data/.architecture/decisions/adr_003_plan_orchestrator_interface.md +179 -0
- data/.architecture/decisions/adrs/ADR-001-dependency-management.md +147 -0
- data/.architecture/decisions/adrs/ADR-002-system-boundaries.md +162 -0
- data/.architecture/decisions/adrs/ADR-003-content-safety.md +158 -0
- data/.architecture/decisions/adrs/ADR-004-agent-permissions.md +161 -0
- data/.architecture/decisions/adrs/ADR-005-adaptation-engine.md +127 -0
- data/.architecture/decisions/adrs/ADR-006-extension-system.md +273 -0
- data/.architecture/decisions/adrs/ADR-007-learning-system.md +156 -0
- data/.architecture/decisions/adrs/ADR-008-prompt-generation.md +325 -0
- data/.architecture/decisions/adrs/ADR-009-task-failure-handling.md +353 -0
- data/.architecture/decisions/adrs/ADR-010-task-input-handling.md +251 -0
- data/.architecture/decisions/adrs/ADR-011-task-observable-pattern.md +391 -0
- data/.architecture/decisions/adrs/ADR-012-task-output-handling.md +205 -0
- data/.architecture/decisions/adrs/ADR-013-architecture-alignment.md +211 -0
- data/.architecture/decisions/adrs/ADR-014-agent-capability-registry.md +80 -0
- data/.architecture/decisions/adrs/ADR-015-persistent-agent-store.md +100 -0
- data/.architecture/decisions/adrs/ADR-016-agent-assembly-engine.md +117 -0
- data/.architecture/decisions/adrs/ADR-017-streaming-observability.md +171 -0
- data/.architecture/decisions/capability_tools_distinction.md +150 -0
- data/.architecture/decisions/cli_command_structure.md +61 -0
- data/.architecture/implementation/agent_self_assembly_implementation.md +267 -0
- data/.architecture/implementation/agent_self_assembly_summary.md +138 -0
- data/.architecture/members.yml +187 -0
- data/.architecture/planning/self_implementation_exercise.md +295 -0
- data/.architecture/planning/session_compaction_rule.md +43 -0
- data/.architecture/planning/streaming_observability_feature.md +223 -0
- data/.architecture/principles.md +151 -0
- data/.architecture/recalibration/0-2-0.md +92 -0
- data/.architecture/recalibration/agent_self_assembly.md +238 -0
- data/.architecture/recalibration/cli_command_structure.md +91 -0
- data/.architecture/recalibration/implementation_roadmap_0-2-0.md +301 -0
- data/.architecture/recalibration/progress_tracking_0-2-0.md +114 -0
- data/.architecture/recalibration_process.md +127 -0
- data/.architecture/reviews/0-2-0.md +181 -0
- data/.architecture/reviews/cli_command_duplication.md +98 -0
- data/.architecture/templates/adr.md +105 -0
- data/.architecture/templates/implementation_roadmap.md +125 -0
- data/.architecture/templates/progress_tracking.md +89 -0
- data/.architecture/templates/recalibration_plan.md +70 -0
- data/.architecture/templates/version_comparison.md +124 -0
- data/.claude/settings.local.json +13 -0
- data/.claude-sessions/001-task-class-architecture-implementation.md +129 -0
- data/.claude-sessions/002-plan-orchestrator-interface-review.md +105 -0
- data/.claude-sessions/architecture-governance-implementation.md +37 -0
- data/.claude-sessions/architecture-review-session.md +27 -0
- data/ArchitecturalFeatureBuilder.md +136 -0
- data/ArchitectureConsiderations.md +229 -0
- data/CHANGELOG.md +57 -2
- data/CLAUDE.md +111 -0
- data/CONTRIBUTING.md +286 -0
- data/MAINTAINING.md +301 -0
- data/README.md +582 -28
- data/docs/agent_capabilities_api.md +259 -0
- data/docs/artifact_extension_points.md +757 -0
- data/docs/artifact_generation_architecture.md +323 -0
- data/docs/artifact_implementation_plan.md +596 -0
- data/docs/artifact_integration_points.md +345 -0
- data/docs/artifact_verification_strategies.md +581 -0
- data/docs/streaming_observability_architecture.md +510 -0
- data/exe/agentic +6 -1
- data/lefthook.yml +5 -0
- data/lib/agentic/adaptation_engine.rb +124 -0
- data/lib/agentic/agent.rb +181 -4
- data/lib/agentic/agent_assembly_engine.rb +442 -0
- data/lib/agentic/agent_capability_registry.rb +260 -0
- data/lib/agentic/agent_config.rb +63 -0
- data/lib/agentic/agent_specification.rb +46 -0
- data/lib/agentic/capabilities/examples.rb +530 -0
- data/lib/agentic/capabilities.rb +14 -0
- data/lib/agentic/capability_provider.rb +146 -0
- data/lib/agentic/capability_specification.rb +118 -0
- data/lib/agentic/cli/agent.rb +31 -0
- data/lib/agentic/cli/capabilities.rb +191 -0
- data/lib/agentic/cli/config.rb +134 -0
- data/lib/agentic/cli/execution_observer.rb +796 -0
- data/lib/agentic/cli.rb +1068 -0
- data/lib/agentic/default_agent_provider.rb +35 -0
- data/lib/agentic/errors/llm_error.rb +184 -0
- data/lib/agentic/execution_plan.rb +53 -0
- data/lib/agentic/execution_result.rb +91 -0
- data/lib/agentic/expected_answer_format.rb +46 -0
- data/lib/agentic/extension/domain_adapter.rb +109 -0
- data/lib/agentic/extension/plugin_manager.rb +163 -0
- data/lib/agentic/extension/protocol_handler.rb +116 -0
- data/lib/agentic/extension.rb +45 -0
- data/lib/agentic/factory_methods.rb +9 -1
- data/lib/agentic/generation_stats.rb +61 -0
- data/lib/agentic/learning/README.md +84 -0
- data/lib/agentic/learning/capability_optimizer.rb +613 -0
- data/lib/agentic/learning/execution_history_store.rb +251 -0
- data/lib/agentic/learning/pattern_recognizer.rb +500 -0
- data/lib/agentic/learning/strategy_optimizer.rb +706 -0
- data/lib/agentic/learning.rb +131 -0
- data/lib/agentic/llm_assisted_composition_strategy.rb +188 -0
- data/lib/agentic/llm_client.rb +215 -15
- data/lib/agentic/llm_config.rb +65 -1
- data/lib/agentic/llm_response.rb +163 -0
- data/lib/agentic/logger.rb +1 -1
- data/lib/agentic/observable.rb +51 -0
- data/lib/agentic/persistent_agent_store.rb +385 -0
- data/lib/agentic/plan_execution_result.rb +129 -0
- data/lib/agentic/plan_orchestrator.rb +464 -0
- data/lib/agentic/plan_orchestrator_config.rb +57 -0
- data/lib/agentic/retry_config.rb +63 -0
- data/lib/agentic/retry_handler.rb +125 -0
- data/lib/agentic/structured_outputs.rb +1 -1
- data/lib/agentic/task.rb +193 -0
- data/lib/agentic/task_definition.rb +39 -0
- data/lib/agentic/task_execution_result.rb +92 -0
- data/lib/agentic/task_failure.rb +66 -0
- data/lib/agentic/task_output_schemas.rb +112 -0
- data/lib/agentic/task_planner.rb +54 -19
- data/lib/agentic/task_result.rb +48 -0
- data/lib/agentic/ui.rb +244 -0
- data/lib/agentic/verification/critic_framework.rb +116 -0
- data/lib/agentic/verification/llm_verification_strategy.rb +60 -0
- data/lib/agentic/verification/schema_verification_strategy.rb +47 -0
- data/lib/agentic/verification/verification_hub.rb +62 -0
- data/lib/agentic/verification/verification_result.rb +50 -0
- data/lib/agentic/verification/verification_strategy.rb +26 -0
- data/lib/agentic/version.rb +1 -1
- data/lib/agentic.rb +74 -2
- data/plugins/README.md +41 -0
- 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
|
data/lib/agentic/logger.rb
CHANGED
@@ -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
|