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,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "llm_client"
4
+ require_relative "llm_config"
5
+
6
+ module Agentic
7
+ # Default implementation of an agent provider for use in the CLI
8
+ # This provider creates agents based on agent specs in tasks
9
+ class DefaultAgentProvider
10
+ # Initialize with optional LLM configuration
11
+ # @param llm_config [LlmConfig, nil] Configuration for the LLM client
12
+ def initialize(llm_config = nil)
13
+ @llm_config = llm_config || LlmConfig.new
14
+ end
15
+
16
+ # Creates and returns an agent for a task
17
+ # @param task [Task] The task that needs an agent
18
+ # @return [Agent] The agent created for the task
19
+ def get_agent_for_task(task)
20
+ agent_spec = task.agent_spec
21
+
22
+ # Create LLM client for this agent
23
+ llm_client = LlmClient.new(@llm_config)
24
+
25
+ # Create a new agent using the factory methods
26
+ Agent.build do |a|
27
+ a.name = agent_spec.name
28
+ a.role = agent_spec.name # Use name as role for simplicity
29
+ a.purpose = agent_spec.description
30
+ a.instructions = agent_spec.instructions
31
+ a.llm_client = llm_client
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ module Errors
5
+ # Base class for all LLM-related errors
6
+ class LlmError < StandardError
7
+ # @return [Hash, nil] The raw response from the LLM API, if available
8
+ attr_reader :response
9
+
10
+ # @return [Hash, nil] Additional context about the error
11
+ attr_reader :context
12
+
13
+ # @param message [String] The error message
14
+ # @param response [Hash, nil] The raw response from the LLM API
15
+ # @param context [Hash, nil] Additional context about the error
16
+ def initialize(message, response: nil, context: nil)
17
+ super(message)
18
+ @response = response
19
+ @context = context || {}
20
+ end
21
+ end
22
+
23
+ # Error raised when the LLM refuses to respond
24
+ class LlmRefusalError < LlmError
25
+ # @return [String] The refusal message from the LLM
26
+ attr_reader :refusal_message
27
+
28
+ # @return [Symbol] The category of refusal
29
+ attr_reader :refusal_category
30
+
31
+ # @param refusal_message [String] The refusal message from the LLM
32
+ # @param refusal_category [Symbol, nil] The category of refusal
33
+ # @param response [Hash, nil] The raw response from the LLM API
34
+ # @param context [Hash, nil] Additional context about the error
35
+ def initialize(refusal_message, refusal_category: nil, response: nil, context: nil)
36
+ super("LLM refused to respond: #{refusal_message}", response: response, context: context)
37
+ @refusal_message = refusal_message
38
+ @refusal_category = refusal_category || determine_refusal_category(refusal_message)
39
+ end
40
+
41
+ # Determines whether this refusal is retryable with modifications
42
+ # @return [Boolean] True if the refusal can be retried with modifications
43
+ def retryable_with_modifications?
44
+ [:unclear_instructions, :needs_clarification, :ambiguous_request, :format_error].include?(@refusal_category)
45
+ end
46
+
47
+ private
48
+
49
+ # Determines the category of refusal from the message
50
+ # @param message [String] The refusal message
51
+ # @return [Symbol] The category of refusal
52
+ def determine_refusal_category(message)
53
+ message = message.to_s.downcase
54
+
55
+ if message.include?("harmful") || message.include?("offensive") || message.include?("illegal")
56
+ :harmful_content
57
+ elsif message.include?("clarif") || message.include?("ambiguous")
58
+ :needs_clarification
59
+ elsif message.include?("format") || message.include?("structure")
60
+ :format_error
61
+ elsif message.include?("unclear") || message.include?("specific")
62
+ :unclear_instructions
63
+ elsif message.include?("capability") || message.include?("unable")
64
+ :capability_limitation
65
+ else
66
+ :general_refusal
67
+ end
68
+ end
69
+ end
70
+
71
+ # Error raised when the LLM response cannot be parsed
72
+ class LlmParseError < LlmError
73
+ # @return [Exception] The original parsing exception
74
+ attr_reader :parse_exception
75
+
76
+ # @param message [String] The error message
77
+ # @param parse_exception [Exception] The original parsing exception
78
+ # @param response [Hash, nil] The raw response from the LLM API
79
+ # @param context [Hash, nil] Additional context about the error
80
+ def initialize(message, parse_exception: nil, response: nil, context: nil)
81
+ super(message, response: response, context: context)
82
+ @parse_exception = parse_exception
83
+ end
84
+ end
85
+
86
+ # Error raised when there's a connection or network issue
87
+ class LlmNetworkError < LlmError
88
+ # @return [Exception] The original network exception
89
+ attr_reader :network_exception
90
+
91
+ # @param message [String] The error message
92
+ # @param network_exception [Exception] The original network exception
93
+ # @param context [Hash, nil] Additional context about the error
94
+ def initialize(message, network_exception: nil, context: nil)
95
+ super(message, context: context)
96
+ @network_exception = network_exception
97
+ end
98
+
99
+ # @return [Boolean] Whether this error is retryable
100
+ def retryable?
101
+ true
102
+ end
103
+ end
104
+
105
+ # Error raised when the API returns a rate limit error
106
+ class LlmRateLimitError < LlmError
107
+ # @return [Integer, nil] The number of seconds to wait before retrying
108
+ attr_reader :retry_after
109
+
110
+ # @param message [String] The error message
111
+ # @param retry_after [Integer, nil] The number of seconds to wait before retrying
112
+ # @param response [Hash, nil] The raw response from the LLM API
113
+ # @param context [Hash, nil] Additional context about the error
114
+ def initialize(message, retry_after: nil, response: nil, context: nil)
115
+ super(message, response: response, context: context)
116
+ @retry_after = retry_after
117
+ end
118
+
119
+ # @return [Boolean] Whether this error is retryable
120
+ def retryable?
121
+ true
122
+ end
123
+ end
124
+
125
+ # Error raised when the API returns an authentication error
126
+ class LlmAuthenticationError < LlmError
127
+ # @param message [String] The error message
128
+ # @param response [Hash, nil] The raw response from the LLM API
129
+ # @param context [Hash, nil] Additional context about the error
130
+ def initialize(message, response: nil, context: nil)
131
+ super(message, response: response, context: context)
132
+ end
133
+
134
+ # @return [Boolean] Whether this error is retryable
135
+ def retryable?
136
+ false
137
+ end
138
+ end
139
+
140
+ # Error raised when the API returns a server error
141
+ class LlmServerError < LlmError
142
+ # @param message [String] The error message
143
+ # @param response [Hash, nil] The raw response from the LLM API
144
+ # @param context [Hash, nil] Additional context about the error
145
+ def initialize(message, response: nil, context: nil)
146
+ super(message, response: response, context: context)
147
+ end
148
+
149
+ # @return [Boolean] Whether this error is retryable
150
+ def retryable?
151
+ true
152
+ end
153
+ end
154
+
155
+ # Error raised when the request to the LLM times out
156
+ class LlmTimeoutError < LlmError
157
+ # @param message [String] The error message
158
+ # @param context [Hash, nil] Additional context about the error
159
+ def initialize(message, context: nil)
160
+ super(message, context: context)
161
+ end
162
+
163
+ # @return [Boolean] Whether this error is retryable
164
+ def retryable?
165
+ true
166
+ end
167
+ end
168
+
169
+ # Error raised when an invalid request is made to the LLM API
170
+ class LlmInvalidRequestError < LlmError
171
+ # @param message [String] The error message
172
+ # @param response [Hash, nil] The raw response from the LLM API
173
+ # @param context [Hash, nil] Additional context about the error
174
+ def initialize(message, response: nil, context: nil)
175
+ super(message, response: response, context: context)
176
+ end
177
+
178
+ # @return [Boolean] Whether this error is retryable
179
+ def retryable?
180
+ false
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ # Value object representing an execution plan with tasks and expected answer
5
+ #
6
+ # This class is part of the data-presentation separation pattern:
7
+ # 1. TaskPlanner generates the core plan data
8
+ # 2. ExecutionPlan serves as a structured value object to hold this data
9
+ # 3. The to_s method provides presentation capabilities when needed
10
+ #
11
+ # Using a value object instead of raw hashes provides:
12
+ # - Type safety
13
+ # - Domain-specific methods
14
+ # - Encapsulation of presentation logic
15
+ # - Clearer interfaces between components
16
+ class ExecutionPlan
17
+ # @return [Array<TaskDefinition>] The list of tasks to accomplish the goal
18
+ attr_reader :tasks
19
+
20
+ # @return [ExpectedAnswerFormat] The expected answer format
21
+ attr_reader :expected_answer
22
+
23
+ # @param tasks [Array<TaskDefinition>] The list of tasks to accomplish the goal
24
+ # @param expected_answer [ExpectedAnswerFormat] The expected answer format
25
+ def initialize(tasks, expected_answer)
26
+ @tasks = tasks
27
+ @expected_answer = expected_answer
28
+ end
29
+
30
+ # Returns a hash representation of the execution plan
31
+ # @return [Hash] The execution plan as a hash
32
+ def to_h
33
+ {
34
+ tasks: @tasks.map(&:to_h),
35
+ expected_answer: @expected_answer.to_h
36
+ }
37
+ end
38
+
39
+ # Returns a formatted string representation of the execution plan
40
+ # @return [String] The formatted execution plan
41
+ def to_s
42
+ plan = "Execution Plan:\n\n"
43
+ @tasks.each_with_index do |task, index|
44
+ plan += "#{index + 1}. #{task.description} (Agent: #{task.agent.name})\n"
45
+ end
46
+ plan += "\nExpected Answer:\n"
47
+ plan += "Format: #{@expected_answer.format}\n"
48
+ plan += "Sections: #{@expected_answer.sections.join(", ")}\n"
49
+ plan += "Length: #{@expected_answer.length}\n"
50
+ plan
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ # Value object representing the results of plan execution
5
+ class ExecutionResult
6
+ # @return [String] The ID of the plan
7
+ attr_reader :plan_id
8
+
9
+ # @return [Symbol] The status of the execution (:completed, :partial_failure, :failed)
10
+ attr_reader :status
11
+
12
+ # @return [Float] The total execution time in seconds
13
+ attr_reader :execution_time
14
+
15
+ # @return [Hash] The tasks that were executed, keyed by ID
16
+ attr_reader :tasks
17
+
18
+ # @return [Hash] The results of the tasks, keyed by task ID
19
+ attr_reader :results
20
+
21
+ # Initializes a new execution result
22
+ # @param plan_id [String] The ID of the plan
23
+ # @param status [Symbol] The status of the execution
24
+ # @param execution_time [Float] The total execution time in seconds
25
+ # @param tasks [Hash] The tasks that were executed, keyed by ID
26
+ # @param results [Hash] The results of the tasks, keyed by task ID
27
+ def initialize(plan_id:, status:, execution_time:, tasks:, results:)
28
+ @plan_id = plan_id
29
+ @status = status
30
+ @execution_time = execution_time
31
+ @tasks = tasks
32
+ @results = results
33
+ end
34
+
35
+ # Returns the result for a specific task
36
+ # @param task_id [String] The ID of the task
37
+ # @return [TaskResult, nil] The result of the task, or nil if not found
38
+ def task_result(task_id)
39
+ @results[task_id]
40
+ end
41
+
42
+ # Checks if the execution was fully successful
43
+ # @return [Boolean] True if all tasks succeeded
44
+ def successful?
45
+ @status == :completed
46
+ end
47
+
48
+ # Checks if the execution partially failed
49
+ # @return [Boolean] True if some tasks failed but the plan completed
50
+ def partial_failure?
51
+ @status == :partial_failure
52
+ end
53
+
54
+ # Checks if the execution completely failed
55
+ # @return [Boolean] True if the plan failed to complete
56
+ def failed?
57
+ @status == :failed
58
+ end
59
+
60
+ # Returns a hash representation of the execution result
61
+ # @return [Hash] The execution result as a hash
62
+ def to_h
63
+ {
64
+ plan_id: @plan_id,
65
+ status: @status,
66
+ execution_time: @execution_time,
67
+ tasks: @tasks.transform_values { |task| task.is_a?(Task) ? task.to_h : task },
68
+ results: @results.transform_values { |result| result.is_a?(TaskResult) ? result.to_h : result }
69
+ }
70
+ end
71
+
72
+ # Returns a summary of the execution result
73
+ # @return [Hash] A summary of the execution result
74
+ def summary
75
+ total_tasks = @tasks.size
76
+ successful_tasks = @results.count { |_, result| result.successful? }
77
+ failed_tasks = @results.count { |_, result| result.failed? }
78
+
79
+ {
80
+ plan_id: @plan_id,
81
+ status: @status,
82
+ execution_time: @execution_time,
83
+ task_counts: {
84
+ total: total_tasks,
85
+ successful: successful_tasks,
86
+ failed: failed_tasks
87
+ }
88
+ }
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ # Value object representing expected answer format
5
+ class ExpectedAnswerFormat
6
+ # @return [String] The format of the expected answer
7
+ attr_reader :format
8
+
9
+ # @return [Array<String>] The sections expected in the answer
10
+ attr_reader :sections
11
+
12
+ # @return [String] The expected length of the answer
13
+ attr_reader :length
14
+
15
+ # Initializes a new expected answer format
16
+ # @param format [String] The format of the expected answer
17
+ # @param sections [Array<String>] The sections expected in the answer
18
+ # @param length [String] The expected length of the answer
19
+ def initialize(format:, sections:, length:)
20
+ @format = format
21
+ @sections = sections
22
+ @length = length
23
+ end
24
+
25
+ # Returns a serializable representation of the expected answer format
26
+ # @return [Hash] The expected answer format as a hash
27
+ def to_h
28
+ {
29
+ "format" => @format,
30
+ "sections" => @sections,
31
+ "length" => @length
32
+ }
33
+ end
34
+
35
+ # Creates an ExpectedAnswerFormat from a hash
36
+ # @param hash [Hash] The hash representation
37
+ # @return [ExpectedAnswerFormat] A new expected answer format
38
+ def self.from_hash(hash)
39
+ new(
40
+ format: hash["format"],
41
+ sections: hash["sections"],
42
+ length: hash["length"]
43
+ )
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ module Extension
5
+ # The DomainAdapter integrates domain-specific knowledge into the general agent framework.
6
+ # It provides mechanisms for adapting prompts, tasks, and verification strategies
7
+ # to specific domains like healthcare, finance, legal, etc.
8
+ class DomainAdapter
9
+ # Initialize a new DomainAdapter
10
+ #
11
+ # @param [String] domain The identifier for the domain (e.g., "healthcare", "finance")
12
+ # @param [Hash] options Configuration options
13
+ # @option options [Logger] :logger Custom logger instance
14
+ # @option options [Hash] :domain_config Domain-specific configuration
15
+ def initialize(domain, options = {})
16
+ @domain = domain
17
+ @logger = options[:logger] || Agentic.logger
18
+ @domain_config = options[:domain_config] || {}
19
+ @adapters = {}
20
+ @domain_knowledge = {}
21
+
22
+ initialize_default_adapters
23
+ end
24
+
25
+ # Register an adapter for a specific component
26
+ #
27
+ # @param [Symbol] component The component to adapt (e.g., :prompt, :task, :verification)
28
+ # @param [Proc] adapter A callable that performs the adaptation
29
+ # @return [Boolean] True if registration was successful
30
+ def register_adapter(component, adapter)
31
+ return false unless adapter.respond_to?(:call)
32
+
33
+ @adapters[component] = adapter
34
+ true
35
+ end
36
+
37
+ # Add domain-specific knowledge
38
+ #
39
+ # @param [Symbol] key The knowledge identifier
40
+ # @param [Object] knowledge The domain knowledge to store
41
+ def add_knowledge(key, knowledge)
42
+ @domain_knowledge[key] = knowledge
43
+ end
44
+
45
+ # Get domain-specific knowledge
46
+ #
47
+ # @param [Symbol] key The knowledge identifier
48
+ # @return [Object, nil] The stored knowledge or nil if not found
49
+ def get_knowledge(key)
50
+ @domain_knowledge[key]
51
+ end
52
+
53
+ # Apply domain-specific adaptation to a component
54
+ #
55
+ # @param [Symbol] component The component to adapt
56
+ # @param [Object] target The target to apply adaptation to
57
+ # @param [Hash] context Additional context for adaptation
58
+ # @return [Object] The adapted target
59
+ def adapt(component, target, context = {})
60
+ return target unless @adapters.key?(component)
61
+
62
+ adapter = @adapters[component]
63
+ context = context.merge(domain: @domain, domain_knowledge: @domain_knowledge)
64
+
65
+ begin
66
+ result = adapter.call(target, context)
67
+ @logger.debug("Applied #{@domain} domain adaptation to #{component}")
68
+ result
69
+ rescue => e
70
+ @logger.error("Failed to apply #{@domain} domain adaptation to #{component}: #{e.message}")
71
+ target # Return original if adaptation fails
72
+ end
73
+ end
74
+
75
+ # Check if the adapter supports a specific component
76
+ #
77
+ # @param [Symbol] component The component to check
78
+ # @return [Boolean] True if an adapter exists for the component
79
+ def supports?(component)
80
+ @adapters.key?(component)
81
+ end
82
+
83
+ # Get the domain identifier
84
+ #
85
+ # @return [String] The domain identifier
86
+ attr_reader :domain
87
+
88
+ # Get domain configuration
89
+ #
90
+ # @return [Hash] The domain configuration
91
+ def configuration
92
+ @domain_config
93
+ end
94
+
95
+ private
96
+
97
+ # Initialize default adapters for common components
98
+ def initialize_default_adapters
99
+ # Identity adapter (returns input unchanged) as fallback
100
+ identity_adapter = ->(target, _context) { target }
101
+
102
+ # Register default adapters for common components
103
+ register_adapter(:prompt, identity_adapter)
104
+ register_adapter(:task, identity_adapter)
105
+ register_adapter(:verification, identity_adapter)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ module Extension
5
+ # The PluginManager coordinates third-party extension loading, registration,
6
+ # and lifecycle management. It provides a central registry for all extensions
7
+ # and ensures they conform to the extension contracts.
8
+ class PluginManager
9
+ # Initialize a new PluginManager
10
+ #
11
+ # @param [Hash] options Configuration options for the plugin manager
12
+ # @option options [Logger] :logger Custom logger instance
13
+ # @option options [Boolean] :auto_discovery Whether to automatically discover plugins
14
+ # @option options [Array<String>] :plugin_paths Additional paths to search for plugins
15
+ def initialize(options = {})
16
+ @logger = options[:logger] || Agentic.logger
17
+ @auto_discovery = options.fetch(:auto_discovery, true)
18
+ @plugin_paths = options[:plugin_paths] || []
19
+ @plugin_paths << default_plugin_path
20
+ @plugins = {}
21
+ @hooks = Hash.new { |h, k| h[k] = [] }
22
+
23
+ discover_plugins if @auto_discovery
24
+ end
25
+
26
+ # Register a plugin with the manager
27
+ #
28
+ # @param [String] name The unique name of the plugin
29
+ # @param [Object] plugin The plugin instance
30
+ # @param [Hash] metadata Additional information about the plugin
31
+ # @return [Boolean] True if registration was successful
32
+ def register(name, plugin, metadata = {})
33
+ if @plugins.key?(name)
34
+ @logger.warn("Plugin '#{name}' is already registered. Use force: true to override.")
35
+ return false
36
+ end
37
+
38
+ unless valid_plugin?(plugin)
39
+ @logger.error("Plugin '#{name}' does not conform to the plugin contract")
40
+ return false
41
+ end
42
+
43
+ @plugins[name] = {
44
+ instance: plugin,
45
+ metadata: metadata.merge(registered_at: Time.now),
46
+ enabled: true
47
+ }
48
+
49
+ @logger.info("Plugin '#{name}' registered successfully")
50
+ true
51
+ end
52
+
53
+ # Register a plugin with the manager, overriding any existing plugin with the same name
54
+ #
55
+ # @param [String] name The unique name of the plugin
56
+ # @param [Object] plugin The plugin instance
57
+ # @param [Hash] metadata Additional information about the plugin
58
+ # @return [Boolean] True if registration was successful
59
+ def register!(name, plugin, metadata = {})
60
+ @plugins.delete(name) if @plugins.key?(name)
61
+ register(name, plugin, metadata)
62
+ end
63
+
64
+ # Enable a registered plugin
65
+ #
66
+ # @param [String] name The name of the plugin to enable
67
+ # @return [Boolean] True if the plugin was enabled
68
+ def enable(name)
69
+ return false unless @plugins.key?(name)
70
+
71
+ @plugins[name][:enabled] = true
72
+ @logger.info("Plugin '#{name}' enabled")
73
+ true
74
+ end
75
+
76
+ # Disable a registered plugin
77
+ #
78
+ # @param [String] name The name of the plugin to disable
79
+ # @return [Boolean] True if the plugin was disabled
80
+ def disable(name)
81
+ return false unless @plugins.key?(name)
82
+
83
+ @plugins[name][:enabled] = false
84
+ @logger.info("Plugin '#{name}' disabled")
85
+ true
86
+ end
87
+
88
+ # Get a registered plugin by name
89
+ #
90
+ # @param [String] name The name of the plugin to retrieve
91
+ # @return [Object, nil] The plugin or nil if not found or disabled
92
+ def get(name)
93
+ return nil unless @plugins.key?(name) && @plugins[name][:enabled]
94
+
95
+ @plugins[name][:instance]
96
+ end
97
+
98
+ # List all registered plugins
99
+ #
100
+ # @param [Boolean] only_enabled Only return enabled plugins
101
+ # @return [Hash] A hash of all registered plugins and their metadata
102
+ def list(only_enabled: false)
103
+ if only_enabled
104
+ @plugins.select { |_, data| data[:enabled] }
105
+ else
106
+ @plugins
107
+ end
108
+ end
109
+
110
+ # Register a hook for plugin events
111
+ #
112
+ # @param [Symbol] event The event to hook into (:after_register, :before_enable, :after_enable, :before_disable, :after_disable)
113
+ # @yield [name, plugin] The callback to execute when the event occurs
114
+ # @yieldparam [String] name The name of the plugin
115
+ # @yieldparam [Object] plugin The plugin instance
116
+ # @return [Boolean] True if the hook was registered
117
+ def register_hook(event, &callback)
118
+ return false unless callback
119
+
120
+ @hooks[event] << callback
121
+ true
122
+ end
123
+
124
+ # Discover plugins in configured paths
125
+ #
126
+ # @return [Integer] The number of plugins discovered
127
+ def discover_plugins
128
+ return 0 unless @auto_discovery
129
+
130
+ discovered = 0
131
+ @plugin_paths.each do |path|
132
+ Dir.glob(File.join(path, "*.rb")).each do |file|
133
+ require file
134
+ discovered += 1
135
+ rescue => e
136
+ @logger.error("Failed to load plugin from #{file}: #{e.message}")
137
+ end
138
+ end
139
+
140
+ @logger.info("Discovered #{discovered} plugins")
141
+ discovered
142
+ end
143
+
144
+ private
145
+
146
+ # Get the default plugin path
147
+ #
148
+ # @return [String] The default path for plugins
149
+ def default_plugin_path
150
+ File.join(File.dirname(__FILE__), "../../../plugins")
151
+ end
152
+
153
+ # Check if a plugin conforms to the plugin contract
154
+ #
155
+ # @param [Object] plugin The plugin to validate
156
+ # @return [Boolean] True if the plugin is valid
157
+ def valid_plugin?(plugin)
158
+ # Check that the plugin implements both required methods
159
+ plugin.respond_to?(:initialize_plugin) && plugin.respond_to?(:call)
160
+ end
161
+ end
162
+ end
163
+ end