├── .agentic.yml ├── .rspec ├── lib ├── agentic │ ├── version.rb │ ├── capabilities.rb │ ├── verification │ │ ├── verification_strategy.rb │ │ ├── schema_verification_strategy.rb │ │ ├── verification_result.rb │ │ ├── verification_hub.rb │ │ ├── llm_verification_strategy.rb │ │ └── critic_framework.rb │ ├── cli │ │ ├── agent.rb │ │ └── config.rb │ ├── logger.rb │ ├── default_agent_provider.rb │ ├── task_definition.rb │ ├── agent_specification.rb │ ├── expected_answer_format.rb │ ├── factory_methods.rb │ ├── observable.rb │ ├── task_result.rb │ ├── extension.rb │ ├── agent_config.rb │ ├── execution_plan.rb │ ├── plan_orchestrator_config.rb │ ├── generation_stats.rb │ ├── task_failure.rb │ ├── retry_config.rb │ ├── llm_config.rb │ ├── learning │ │ └── README.md │ ├── task_execution_result.rb │ ├── execution_result.rb │ ├── extension │ │ ├── domain_adapter.rb │ │ └── protocol_handler.rb │ ├── structured_outputs.rb │ ├── task_output_schemas.rb │ ├── capability_specification.rb │ ├── plan_execution_result.rb │ ├── retry_handler.rb │ └── adaptation_engine.rb └── agentic.rb ├── .standard.yml ├── lefthook.yml ├── bin ├── setup └── console ├── sig └── agentic │ └── builder.rbs ├── exe └── agentic ├── spec ├── agentic_spec.rb ├── agentic │ ├── llm_client_spec.rb │ ├── factory_methods_spec.rb │ ├── task_planner_spec.rb │ ├── agent_spec.rb │ ├── expected_answer_format_spec.rb │ ├── agent_specification_spec.rb │ ├── execution_plan_spec.rb │ ├── task_failure_spec.rb │ ├── task_definition_spec.rb │ ├── task_result_spec.rb │ ├── retry_config_spec.rb │ ├── generation_stats_spec.rb │ ├── errors │ │ ├── llm_refusal_error_spec.rb │ │ └── llm_error_spec.rb │ ├── task_execution_result_spec.rb │ ├── llm_config_spec.rb │ ├── retry_handler_spec.rb │ ├── observable_spec.rb │ └── extension │ │ └── domain_adapter_spec.rb ├── support │ └── mock_agent.rb ├── spec_helper.rb ├── integration │ └── cli_plan_command_spec.rb └── vcr_cassettes │ └── gpt4o_mini_completion.yml ├── .gitignore ├── Rakefile ├── Gemfile ├── .claude └── settings.local.json ├── .rubocop.yml ├── .github └── workflows │ └── main.yml ├── LICENSE.txt ├── plugins └── README.md ├── .claude-sessions ├── architecture-review-session.md └── architecture-governance-implementation.md ├── .architecture ├── planning │ └── session_compaction_rule.md ├── templates │ ├── adr.md │ ├── recalibration_plan.md │ ├── version_comparison.md │ ├── progress_tracking.md │ └── implementation_roadmap.md ├── decisions │ ├── cli_command_structure.md │ └── adrs │ │ ├── ADR-014-agent-capability-registry.md │ │ ├── ADR-015-persistent-agent-store.md │ │ └── ADR-016-agent-assembly-engine.md ├── reviews │ └── cli_command_duplication.md └── recalibration │ └── cli_command_structure.md ├── agentic.gemspec ├── CHANGELOG.md └── CLAUDE.md /.agentic.yml: -------------------------------------------------------------------------------- 1 | --- 2 | model: gpt-4o-mini 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/agentic/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | VERSION = "0.2.0" 5 | end 6 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | # For available configuration options, see: 2 | # https://github.com/standardrb/standard 3 | ruby_version: 3.0 4 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | commands: 3 | standardrb: 4 | glob: "*.{rb}" 5 | run: bundle exec standardrb --fix {staged_files} -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /sig/agentic/builder.rbs: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | VERSION: String 5 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 6 | end 7 | -------------------------------------------------------------------------------- /exe/agentic: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative "../lib/agentic" 5 | require "fileutils" 6 | 7 | # Execute the CLI 8 | Agentic::CLI.start(ARGV) 9 | -------------------------------------------------------------------------------- /spec/agentic_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Agentic do 4 | it "has a version number" do 5 | expect(Agentic::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | # environment variables 13 | .env 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | require "standard/rake" 9 | 10 | task default: %i[spec standard] 11 | -------------------------------------------------------------------------------- /spec/agentic/llm_client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | # Skip LlmClient tests since they depend on API connectivity 6 | # TODO: Add proper mocks/VCR cassettes for these tests 7 | RSpec.describe Agentic::LlmClient do 8 | # Empty for now 9 | end 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in agentic.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | 10 | gem "rspec", "~> 3.0" 11 | 12 | gem "standard", "~> 1.3" 13 | 14 | gem "vcr" 15 | gem "webmock" 16 | gem "timecop" 17 | -------------------------------------------------------------------------------- /.claude/settings.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(bundle exec rspec:*)", 5 | "Bash(bundle exec rake:*)", 6 | "Bash(bundle exec:*)", 7 | "Bash(ls:*)", 8 | "Bash(touch:*)" 9 | ], 10 | "deny": [] 11 | }, 12 | "enableAllProjectMcpServers": false 13 | } -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "agentic" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - standard 3 | - rubocop-rspec 4 | 5 | inherit_gem: 6 | standard: config/base.yml 7 | 8 | AllCops: 9 | NewCops: enable 10 | Exclude: 11 | - vendor/**/* 12 | 13 | RSpec: 14 | Enabled: true # enable rubocop-rspec cops 15 | RSpec/DescribeClass: 16 | Enabled: false # ignore missing comments on classes 17 | -------------------------------------------------------------------------------- /lib/agentic/capabilities.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "capabilities/examples" 4 | 5 | module Agentic 6 | # Namespace for capability-related functionality 7 | module Capabilities 8 | # Register standard capabilities 9 | # @return [void] 10 | def self.register_standard_capabilities 11 | Examples.register_all 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/mock_agent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | class MockAgent 5 | include FactoryMethods 6 | 7 | configurable :role, :goal, :backstory 8 | 9 | assembly do |agent| 10 | agent.role ||= "Mock" 11 | agent.goal ||= "Default Goal" 12 | agent.backstory ||= "Default Backstory" 13 | end 14 | 15 | def execute(task) 16 | task.perform(self) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '3.2.4' 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - name: Run the default task 27 | run: bundle exec rake 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "agentic" 4 | 5 | require "vcr" 6 | 7 | VCR.configure do |config| 8 | config.cassette_library_dir = "spec/vcr_cassettes" 9 | config.hook_into :webmock 10 | config.filter_sensitive_data("") { Agentic.configuration.access_token } 11 | config.allow_http_connections_when_no_cassette = true 12 | end 13 | 14 | RSpec.configure do |config| 15 | # Enable flags like --only-failures and --next-failure 16 | config.example_status_persistence_file_path = ".rspec_status" 17 | 18 | # Disable RSpec exposing methods globally on `Module` and `main` 19 | config.disable_monkey_patching! 20 | 21 | config.expect_with :rspec do |c| 22 | c.syntax = :expect 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/agentic/factory_methods_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rspec" 4 | require_relative "../support/mock_agent" 5 | 6 | RSpec.describe Agentic::FactoryMethods do 7 | describe ".build" do 8 | it "configures and builds an agent with default values" do 9 | agent = Agentic::MockAgent.build 10 | 11 | expect(agent.role).to eq("Mock") 12 | expect(agent.goal).to eq("Default Goal") 13 | expect(agent.backstory).to eq("Default Backstory") 14 | end 15 | 16 | it "configures and builds an agent with custom values" do 17 | agent = Agentic::MockAgent.build do |builder| 18 | builder.role = "Custom Role" 19 | builder.goal = "Custom Goal" 20 | builder.backstory = "Custom Backstory" 21 | end 22 | 23 | expect(agent.role).to eq("Custom Role") 24 | expect(agent.goal).to eq("Custom Goal") 25 | expect(agent.backstory).to eq("Custom Backstory") 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/agentic/task_planner_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::TaskPlanner do 6 | let(:goal) { "Generate a market research report on the latest trends in AI technology." } 7 | let(:llm_config) { Agentic::LlmConfig.new } 8 | let(:planner) { described_class.new(goal, llm_config) } 9 | 10 | describe "#initialize" do 11 | it "sets the goal and llm_config" do 12 | expect(planner.goal).to eq(goal) 13 | expect(planner.llm_config).to eq(llm_config) 14 | end 15 | 16 | it "initializes tasks and expected_answer as empty" do 17 | expect(planner.tasks).to be_empty 18 | expect(planner.expected_answer).to be_a(Agentic::ExpectedAnswerFormat) 19 | expect(planner.expected_answer.format).to eq("Undetermined") 20 | expect(planner.expected_answer.sections).to be_empty 21 | expect(planner.expected_answer.length).to eq("Undetermined") 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/agentic/verification/verification_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | module Verification 5 | # Base class for verification strategies 6 | class VerificationStrategy 7 | # @return [Hash] Configuration options for the strategy 8 | attr_reader :config 9 | 10 | # Initializes a new VerificationStrategy 11 | # @param config [Hash] Configuration options for the strategy 12 | def initialize(config = {}) 13 | @config = config 14 | end 15 | 16 | # Verifies a task result 17 | # @param task [Task] The task to verify 18 | # @param result [TaskResult] The result to verify 19 | # @return [VerificationResult] The verification result 20 | # @raise [NotImplementedError] This method must be implemented by subclasses 21 | def verify(task, result) 22 | raise NotImplementedError, "Subclasses must implement verify" 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/agentic/agent_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::Agent do 6 | describe ".build" do 7 | it "configures and builds an agent with default values" do 8 | agent = Agentic::Agent.build 9 | 10 | expect(agent.role).to be_nil 11 | expect(agent.purpose).to be_nil 12 | expect(agent.backstory).to be_nil 13 | expect(agent.tools).to eq(Set.new) 14 | end 15 | 16 | it "configures and builds an agent with custom values" do 17 | agent = Agentic::Agent.build do |builder| 18 | builder.role = "Custom Role" 19 | builder.purpose = "Custom Purpose" 20 | builder.backstory = "Custom Backstory" 21 | builder.tools = ["Custom Tool"] 22 | end 23 | 24 | expect(agent.role).to eq("Custom Role") 25 | expect(agent.purpose).to eq("Custom Purpose") 26 | expect(agent.backstory).to eq("Custom Backstory") 27 | expect(agent.tools).to eq(["Custom Tool"]) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Valentino Stoll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/agentic/cli/agent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | class CLI < Thor 5 | # CLI commands for managing agents 6 | class Agent < Thor 7 | desc "list", "List available agents" 8 | def list 9 | puts "Available agents:" 10 | # In a future implementation, this would list agents from a registry 11 | puts " - No custom agents registered yet" 12 | end 13 | 14 | desc "create NAME", "Create a new agent" 15 | option :role, type: :string, required: true, desc: "Role of the agent" 16 | option :instructions, type: :string, required: true, desc: "Instructions for the agent" 17 | def create(name) 18 | puts "Creating agent: #{name}" 19 | # In a future implementation, this would create and register an agent 20 | puts "Agent created successfully." 21 | end 22 | 23 | desc "delete NAME", "Delete an agent" 24 | def delete(name) 25 | puts "Deleting agent: #{name}" 26 | # In a future implementation, this would delete an agent from a registry 27 | puts "Agent deleted successfully." 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Agentic Plugins 2 | 3 | This directory contains plugins for the Agentic framework. Plugins provide additional functionality that can be loaded by the `Agentic::Extension::PluginManager`. 4 | 5 | ## Creating a Plugin 6 | 7 | A valid plugin must implement the following interface: 8 | 9 | ```ruby 10 | class ExamplePlugin 11 | # Initialize the plugin with configuration options 12 | def initialize_plugin(config = {}) 13 | # Initialize plugin with config 14 | end 15 | 16 | # Execute the plugin functionality 17 | def call(*args) 18 | # Plugin functionality 19 | end 20 | end 21 | ``` 22 | 23 | ## Loading Plugins 24 | 25 | Plugins in this directory are automatically discovered and loaded by the `PluginManager` if auto-discovery is enabled (which is the default behavior). 26 | 27 | You can also manually register plugins: 28 | 29 | ```ruby 30 | plugin_manager = Agentic::Extension.plugin_manager 31 | plugin_manager.register("my_plugin", MyPlugin.new, { version: "1.0.0" }) 32 | ``` 33 | 34 | ## Using Plugins 35 | 36 | To use a registered plugin: 37 | 38 | ```ruby 39 | plugin = Agentic::Extension.plugin_manager.get("my_plugin") 40 | plugin.call(arg1, arg2) if plugin 41 | ``` -------------------------------------------------------------------------------- /lib/agentic/logger.rb: -------------------------------------------------------------------------------- 1 | # lib/agentic/logger.rb 2 | # frozen_string_literal: true 3 | 4 | require "logger" 5 | 6 | module Agentic 7 | class Logger < ::Logger 8 | COLORS = { 9 | "FATAL" => :red, 10 | "ERROR" => :red, 11 | "WARN" => :orange, 12 | "INFO" => :yellow, 13 | "DEBUG" => :white 14 | } 15 | 16 | # Simple formatter which only displays the message. 17 | class SimpleFormatter < ::Logger::Formatter 18 | # This method is invoked when a log event occurs 19 | def call(severity, timestamp, progname, msg) 20 | if $stdout.tty? && severity.respond_to?(:colorize) 21 | "#{severity.colorize(COLORS[severity])}: #{(String === msg) ? msg : msg.inspect}\n" 22 | else 23 | "#{severity}: #{(String === msg) ? msg : msg.inspect}\n" 24 | end 25 | end 26 | end 27 | 28 | def initialize(*args) 29 | super 30 | @formatter = SimpleFormatter.new 31 | end 32 | 33 | def self.info(message) 34 | instance.info(message) 35 | end 36 | 37 | def self.error(message) 38 | instance.error(message) 39 | end 40 | 41 | def self.debug(message) 42 | instance.debug(message) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/agentic/default_agent_provider.rb: -------------------------------------------------------------------------------- 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 36 | -------------------------------------------------------------------------------- /lib/agentic/task_definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Value object representing a task definition 5 | class TaskDefinition 6 | # @return [String] A description of the task 7 | attr_reader :description 8 | 9 | # @return [AgentSpecification] The agent specification for this task 10 | attr_reader :agent 11 | 12 | # Initializes a new task definition 13 | # @param description [String] A description of the task 14 | # @param agent [AgentSpecification] The agent specification for this task 15 | def initialize(description:, agent:) 16 | @description = description 17 | @agent = agent 18 | end 19 | 20 | # Returns a serializable representation of the task definition 21 | # @return [Hash] The task definition as a hash 22 | def to_h 23 | { 24 | "description" => @description, 25 | "agent" => @agent.to_h 26 | } 27 | end 28 | 29 | # Creates a TaskDefinition from a hash 30 | # @param hash [Hash] The hash representation 31 | # @return [TaskDefinition] A new task definition 32 | def self.from_hash(hash) 33 | new( 34 | description: hash["description"], 35 | agent: AgentSpecification.from_hash(hash["agent"]) 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /.claude-sessions/architecture-review-session.md: -------------------------------------------------------------------------------- 1 | # Architecture Review Practice Implementation 2 | 3 | This session documented the implementation of a new architectural design review practice for the Agentic gem, including: 4 | 5 | 1. Directory Structure: 6 | - Created `.architecture` with `decisions` and `reviews` subdirectories 7 | - Moved existing architectural files to `.architecture/decisions` 8 | 9 | 2. Review File Format: 10 | - Version-based naming (e.g., `0-2-0.md`) 11 | - Template includes sections for team member reviews and collaborative analysis 12 | 13 | 3. Architecture Members System: 14 | - Defined 5 specialized roles in `.architecture/members.yml` 15 | - Members include Systems Architect, Domain Expert, Security Specialist, Maintainability Expert, and Performance Specialist 16 | - Each member has defined specialties and domains of expertise 17 | 18 | 4. Review Process: 19 | - Three phases: individual member reviews, collaborative discussion, final consolidated report 20 | - Process initiated with "Start architecture review" command 21 | - Results in comprehensive analysis across multiple architectural perspectives 22 | 23 | 5. Documentation: 24 | - Updated CLAUDE.md with the architecture review process details 25 | - Created templates for review documents 26 | 27 | This implementation establishes a structured approach to architecture reviews that leverages multiple specialized perspectives for thorough analysis. -------------------------------------------------------------------------------- /lib/agentic/agent_specification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Value object representing requirements for an agent 5 | class AgentSpecification 6 | # @return [String] The name of the agent 7 | attr_reader :name 8 | 9 | # @return [String] A description of the agent 10 | attr_reader :description 11 | 12 | # @return [String] Instructions for the agent 13 | attr_reader :instructions 14 | 15 | # Initializes a new agent specification 16 | # @param name [String] The name of the agent 17 | # @param description [String] A description of the agent 18 | # @param instructions [String] Instructions for the agent 19 | def initialize(name:, description:, instructions:) 20 | @name = name 21 | @description = description 22 | @instructions = instructions 23 | end 24 | 25 | # Returns a serializable representation of the agent specification 26 | # @return [Hash] The agent specification as a hash 27 | def to_h 28 | { 29 | "name" => @name, 30 | "description" => @description, 31 | "instructions" => @instructions 32 | } 33 | end 34 | 35 | # Creates an AgentSpecification from a hash 36 | # @param hash [Hash] The hash representation 37 | # @return [AgentSpecification] A new agent specification 38 | def self.from_hash(hash) 39 | new( 40 | name: hash["name"], 41 | description: hash["description"], 42 | instructions: hash["instructions"] 43 | ) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/agentic/expected_answer_format.rb: -------------------------------------------------------------------------------- 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] 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] 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 47 | -------------------------------------------------------------------------------- /spec/agentic/expected_answer_format_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::ExpectedAnswerFormat do 6 | let(:expected_answer_format) do 7 | described_class.new( 8 | format: "PDF", 9 | sections: ["Summary", "Trends", "Conclusion"], 10 | length: "10 pages" 11 | ) 12 | end 13 | 14 | describe "#initialize" do 15 | it "sets the format, sections, and length" do 16 | expect(expected_answer_format.format).to eq("PDF") 17 | expect(expected_answer_format.sections).to eq(["Summary", "Trends", "Conclusion"]) 18 | expect(expected_answer_format.length).to eq("10 pages") 19 | end 20 | end 21 | 22 | describe "#to_h" do 23 | it "returns a hash representation of the expected answer format" do 24 | expect(expected_answer_format.to_h).to eq({ 25 | "format" => "PDF", 26 | "sections" => ["Summary", "Trends", "Conclusion"], 27 | "length" => "10 pages" 28 | }) 29 | end 30 | end 31 | 32 | describe ".from_hash" do 33 | let(:hash) do 34 | { 35 | "format" => "PDF", 36 | "sections" => ["Summary", "Trends", "Conclusion"], 37 | "length" => "10 pages" 38 | } 39 | end 40 | 41 | it "creates an ExpectedAnswerFormat from a hash" do 42 | format = described_class.from_hash(hash) 43 | expect(format).to be_a(described_class) 44 | expect(format.format).to eq("PDF") 45 | expect(format.sections).to eq(["Summary", "Trends", "Conclusion"]) 46 | expect(format.length).to eq("10 pages") 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/agentic/factory_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | module FactoryMethods 5 | def self.included(base) 6 | base.extend(ClassMethods) 7 | base.instance_variable_set(:@configurable_attributes, Set.new) 8 | base.instance_variable_set(:@assembly_instructions, Set.new) 9 | end 10 | 11 | module ClassMethods 12 | def build 13 | agent = new 14 | yield(agent) if block_given? 15 | configure(agent) 16 | run_assembly_instructions(agent) 17 | agent 18 | end 19 | 20 | private 21 | 22 | def configure(agent) 23 | configurable_attributes.each do |attr| 24 | unless agent.public_send(attr) 25 | default_value = case attr 26 | when :tools 27 | Set.new 28 | when :capabilities 29 | {} 30 | end 31 | agent.public_send(:"#{attr}=", default_value) 32 | end 33 | end 34 | end 35 | 36 | def run_assembly_instructions(agent) 37 | assembly_instructions.each do |instruction| 38 | instruction.call(agent) 39 | end 40 | end 41 | 42 | def configurable(*attrs) 43 | @configurable_attributes.merge(attrs) 44 | attr_accessor(*attrs) 45 | end 46 | 47 | def assembly(&block) 48 | @assembly_instructions << block 49 | end 50 | 51 | def configurable_attributes 52 | @configurable_attributes 53 | end 54 | 55 | def assembly_instructions 56 | @assembly_instructions 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/agentic/observable.rb: -------------------------------------------------------------------------------- 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 52 | -------------------------------------------------------------------------------- /lib/agentic/verification/schema_verification_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "verification_strategy" 4 | require_relative "verification_result" 5 | 6 | module Agentic 7 | module Verification 8 | # Verifies task results against a schema 9 | class SchemaVerificationStrategy < VerificationStrategy 10 | # Verifies a task result against a schema 11 | # @param task [Task] The task to verify 12 | # @param result [TaskResult] The result to verify 13 | # @return [VerificationResult] The verification result 14 | def verify(task, result) 15 | unless result.successful? 16 | return VerificationResult.new( 17 | task_id: task.id, 18 | verified: false, 19 | confidence: 0.0, 20 | messages: ["Task failed, skipping schema verification"] 21 | ) 22 | end 23 | 24 | # Extracting schema from task if available 25 | schema = task.input["output_schema"] if task.input.is_a?(Hash) 26 | 27 | unless schema 28 | return VerificationResult.new( 29 | task_id: task.id, 30 | verified: true, 31 | confidence: 0.5, 32 | messages: ["No schema specified for verification, passing by default"] 33 | ) 34 | end 35 | 36 | # In a real implementation, we would validate the output against the schema 37 | # For this stub, we'll assume validation passes 38 | VerificationResult.new( 39 | task_id: task.id, 40 | verified: true, 41 | confidence: 0.9, 42 | messages: ["Output matches expected schema"] 43 | ) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/agentic/agent_specification_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::AgentSpecification do 6 | let(:agent_spec) do 7 | described_class.new( 8 | name: "ResearchAgent", 9 | description: "An agent that performs research", 10 | instructions: "Research the given topic thoroughly" 11 | ) 12 | end 13 | 14 | describe "#initialize" do 15 | it "sets the name, description, and instructions" do 16 | expect(agent_spec.name).to eq("ResearchAgent") 17 | expect(agent_spec.description).to eq("An agent that performs research") 18 | expect(agent_spec.instructions).to eq("Research the given topic thoroughly") 19 | end 20 | end 21 | 22 | describe "#to_h" do 23 | it "returns a hash representation of the agent specification" do 24 | expect(agent_spec.to_h).to eq({ 25 | "name" => "ResearchAgent", 26 | "description" => "An agent that performs research", 27 | "instructions" => "Research the given topic thoroughly" 28 | }) 29 | end 30 | end 31 | 32 | describe ".from_hash" do 33 | let(:hash) do 34 | { 35 | "name" => "ResearchAgent", 36 | "description" => "An agent that performs research", 37 | "instructions" => "Research the given topic thoroughly" 38 | } 39 | end 40 | 41 | it "creates an AgentSpecification from a hash" do 42 | agent = described_class.from_hash(hash) 43 | expect(agent).to be_a(described_class) 44 | expect(agent.name).to eq("ResearchAgent") 45 | expect(agent.description).to eq("An agent that performs research") 46 | expect(agent.instructions).to eq("Research the given topic thoroughly") 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/agentic/task_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Represents the result of a task execution 5 | # @attr_reader [String] task_id The ID of the task that produced this result 6 | # @attr_reader [Boolean] success Whether the task execution was successful 7 | # @attr_reader [Hash, nil] output The output produced by the task, nil if unsuccessful 8 | # @attr_reader [TaskFailure, nil] failure The failure information, nil if successful 9 | class TaskResult 10 | attr_reader :task_id, :success, :output, :failure 11 | 12 | # Initializes a new task result 13 | # @param task_id [String] The ID of the task that produced this result 14 | # @param success [Boolean] Whether the task execution was successful 15 | # @param output [Hash, nil] The output produced by the task 16 | # @param failure [TaskFailure, nil] The failure information 17 | # @return [TaskResult] A new task result instance 18 | def initialize(task_id:, success:, output: nil, failure: nil) 19 | @task_id = task_id 20 | @success = success 21 | @output = output 22 | @failure = failure 23 | end 24 | 25 | # Checks if the task execution was successful 26 | # @return [Boolean] True if successful, false otherwise 27 | def successful? 28 | @success 29 | end 30 | 31 | # Checks if the task execution failed 32 | # @return [Boolean] True if failed, false otherwise 33 | def failed? 34 | !@success 35 | end 36 | 37 | # Returns a serializable representation of the result 38 | # @return [Hash] The result as a hash 39 | def to_h 40 | { 41 | task_id: @task_id, 42 | success: @success, 43 | output: @output, 44 | failure: @failure&.to_h 45 | } 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/agentic/extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "extension/domain_adapter" 4 | require_relative "extension/protocol_handler" 5 | require_relative "extension/plugin_manager" 6 | 7 | module Agentic 8 | # The Extension module provides extensibility points for the Agentic framework. 9 | # It includes three main components: 10 | # 11 | # 1. DomainAdapter - Adapts the framework for specific domains (e.g., healthcare, finance) 12 | # 2. ProtocolHandler - Standardizes connections to external systems 13 | # 3. PluginManager - Coordinates third-party extension loading and registration 14 | # 15 | # These components allow users to customize and extend Agentic's capabilities 16 | # while maintaining a consistent interface. 17 | module Extension 18 | class << self 19 | # Get or create a plugin manager instance 20 | # 21 | # @param [Hash] options Configuration options 22 | # @return [PluginManager] The plugin manager instance 23 | def plugin_manager(options = {}) 24 | @plugin_manager ||= PluginManager.new(options) 25 | end 26 | 27 | # Get or create a protocol handler instance 28 | # 29 | # @param [Hash] options Configuration options 30 | # @return [ProtocolHandler] The protocol handler instance 31 | def protocol_handler(options = {}) 32 | @protocol_handler ||= ProtocolHandler.new(options) 33 | end 34 | 35 | # Create a domain adapter for a specific domain 36 | # 37 | # @param [String] domain The domain identifier 38 | # @param [Hash] options Configuration options 39 | # @return [DomainAdapter] A new domain adapter instance 40 | def domain_adapter(domain, options = {}) 41 | DomainAdapter.new(domain, options) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /.claude-sessions/architecture-governance-implementation.md: -------------------------------------------------------------------------------- 1 | # Architecture Governance Implementation 2 | 3 | ## Session Summary 4 | 5 | The session focused on implementing architectural governance practices for the Agentic Ruby gem, including: 6 | 7 | 1. **Architectural Review System**: 8 | - Architecture review for version 0.2.0 with multi-perspective analysis 9 | - Team of specialized architecture members evaluating different aspects 10 | - Collaborative findings and prioritized improvement suggestions 11 | 12 | 2. **AI Engineer Role**: 13 | - Added specialized role focused on AI/ML product development 14 | - Expanded capabilities to include evaluations, metrics, observability, and agent orchestration 15 | - Provided unique user-centered perspective on the architecture 16 | 17 | 3. **Post-Review Recalibration Process**: 18 | - 5-phase process to translate findings into action 19 | - Standardized templates for documentation 20 | - Sample recalibration plan based on 0.2.0 review 21 | - CLAUDE.md updates for process documentation 22 | 23 | ## Key Files Modified 24 | 25 | - `.architecture/members.yml`: Added AI Engineer role with specialized capabilities 26 | - `.architecture/reviews/0-2-0.md`: Comprehensive architectural review 27 | - `.architecture/recalibration_process.md`: Detailed 5-phase process 28 | - `.architecture/templates/`: Standardized documentation templates 29 | - `.architecture/recalibration/0-2-0.md`: Sample implementation plan 30 | - `CLAUDE.md`: Updated with recalibration process information 31 | 32 | ## Next Steps 33 | 34 | All requested implementations have been completed. Potential next steps include: 35 | - Assigning owners to action items in the recalibration plan 36 | - Drafting ADRs for high-priority architectural changes 37 | - Creating implementation roadmap for the next version -------------------------------------------------------------------------------- /spec/agentic/execution_plan_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::ExecutionPlan do 6 | let(:agent_spec) do 7 | Agentic::AgentSpecification.new( 8 | name: "Researcher", 9 | description: "Research expert", 10 | instructions: "Search latest AI trends" 11 | ) 12 | end 13 | 14 | let(:task_definition) do 15 | Agentic::TaskDefinition.new( 16 | description: "Research AI trends", 17 | agent: agent_spec 18 | ) 19 | end 20 | 21 | let(:tasks) do 22 | [task_definition] 23 | end 24 | 25 | let(:expected_answer) do 26 | Agentic::ExpectedAnswerFormat.new( 27 | format: "PDF", 28 | sections: ["Summary", "Trends"], 29 | length: "10 pages" 30 | ) 31 | end 32 | 33 | let(:execution_plan) { described_class.new(tasks, expected_answer) } 34 | 35 | describe "#initialize" do 36 | it "sets the tasks and expected_answer" do 37 | expect(execution_plan.tasks).to eq(tasks) 38 | expect(execution_plan.expected_answer).to eq(expected_answer) 39 | end 40 | end 41 | 42 | describe "#to_h" do 43 | it "returns a hash representation of the execution plan" do 44 | hash = execution_plan.to_h 45 | expect(hash).to be_a(Hash) 46 | expect(hash[:tasks]).to be_an(Array) 47 | expect(hash[:tasks].first).to eq(task_definition.to_h) 48 | expect(hash[:expected_answer]).to eq(expected_answer.to_h) 49 | end 50 | end 51 | 52 | describe "#to_s" do 53 | it "returns a formatted string representation of the execution plan" do 54 | string = execution_plan.to_s 55 | expect(string).to include("Research AI trends") 56 | expect(string).to include("Format: PDF") 57 | expect(string).to include("Sections: Summary, Trends") 58 | expect(string).to include("Length: 10 pages") 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/agentic/verification/verification_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | module Verification 5 | # Represents the result of a verification process 6 | class VerificationResult 7 | # @return [String] The ID of the task that was verified 8 | attr_reader :task_id 9 | 10 | # @return [Boolean] Whether the verification passed 11 | attr_reader :verified 12 | 13 | # @return [Float] The confidence score (0.0-1.0) of the verification 14 | attr_reader :confidence 15 | 16 | # @return [Array] Messages from the verification process 17 | attr_reader :messages 18 | 19 | # Initializes a new VerificationResult 20 | # @param task_id [String] The ID of the task that was verified 21 | # @param verified [Boolean] Whether the verification passed 22 | # @param confidence [Float] The confidence score (0.0-1.0) of the verification 23 | # @param messages [Array] Messages from the verification process 24 | def initialize(task_id:, verified:, confidence:, messages: []) 25 | @task_id = task_id 26 | @verified = verified 27 | @confidence = confidence 28 | @messages = messages 29 | end 30 | 31 | # Checks if the verification passed with high confidence 32 | # @param threshold [Float] The confidence threshold 33 | # @return [Boolean] Whether verification passed with confidence above the threshold 34 | def verified_with_confidence?(threshold: 0.8) 35 | @verified && @confidence >= threshold 36 | end 37 | 38 | # Converts the verification result to a hash 39 | # @return [Hash] The verification result as a hash 40 | def to_h 41 | { 42 | task_id: @task_id, 43 | verified: @verified, 44 | confidence: @confidence, 45 | messages: @messages 46 | } 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/agentic/task_failure_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::TaskFailure do 6 | let(:message) { "Test failure message" } 7 | let(:type) { "TestErrorType" } 8 | let(:context) { {"key" => "value"} } 9 | 10 | describe "#initialize" do 11 | it "sets the attributes correctly" do 12 | failure = described_class.new(message: message, type: type, context: context) 13 | 14 | expect(failure.message).to eq(message) 15 | expect(failure.type).to eq(type) 16 | expect(failure.context).to eq(context) 17 | expect(failure.timestamp).to be_a(Time) 18 | end 19 | 20 | it "defaults to empty context" do 21 | failure = described_class.new(message: message, type: type) 22 | expect(failure.context).to eq({}) 23 | end 24 | end 25 | 26 | describe "#to_h" do 27 | it "returns a hash representation" do 28 | freeze_time = Time.now 29 | allow(Time).to receive(:now).and_return(freeze_time) 30 | 31 | failure = described_class.new(message: message, type: type, context: context) 32 | hash = failure.to_h 33 | 34 | expect(hash).to be_a(Hash) 35 | expect(hash[:message]).to eq(message) 36 | expect(hash[:type]).to eq(type) 37 | expect(hash[:context]).to eq(context) 38 | expect(hash[:timestamp]).to eq(freeze_time.iso8601) 39 | end 40 | end 41 | 42 | describe ".from_exception" do 43 | it "creates a failure from an exception" do 44 | exception = StandardError.new("Exception message") 45 | allow(exception).to receive(:backtrace).and_return(["line1", "line2"]) 46 | 47 | failure = described_class.from_exception(exception, {agent_id: "agent-123"}) 48 | 49 | expect(failure.message).to eq("Exception message") 50 | expect(failure.type).to eq("StandardError") 51 | expect(failure.context[:agent_id]).to eq("agent-123") 52 | expect(failure.context[:backtrace]).to eq(["line1", "line2"]) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/agentic/agent_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Configuration object for an Agent 5 | class AgentConfig 6 | # @return [String] The name of the agent 7 | attr_accessor :name 8 | 9 | # @return [String] The role of the agent 10 | attr_accessor :role 11 | 12 | # @return [String] The backstory or additional context for the agent 13 | attr_accessor :backstory 14 | 15 | # @return [Array] The tools available to the agent 16 | attr_accessor :tools 17 | 18 | # @return [LlmConfig] The LLM configuration for the agent 19 | attr_accessor :llm_config 20 | 21 | # @return [Hash] Additional options for the agent 22 | attr_accessor :options 23 | 24 | # Initializes a new agent configuration 25 | # @param name [String] The name of the agent 26 | # @param role [String] The role of the agent 27 | # @param backstory [String, nil] The backstory or additional context for the agent 28 | # @param tools [Array] The tools available to the agent 29 | # @param llm_config [LlmConfig, nil] The LLM configuration for the agent 30 | # @param options [Hash] Additional options for the agent 31 | def initialize( 32 | name:, 33 | role:, 34 | backstory: nil, 35 | tools: [], 36 | llm_config: nil, 37 | options: {} 38 | ) 39 | @name = name 40 | @role = role 41 | @backstory = backstory 42 | @tools = tools 43 | @llm_config = llm_config || LlmConfig.new 44 | @options = options 45 | end 46 | 47 | # Returns a hash representation of the agent configuration 48 | # @return [Hash] The agent configuration as a hash 49 | def to_h 50 | { 51 | name: @name, 52 | role: @role, 53 | backstory: @backstory, 54 | tools: @tools, 55 | llm_config: { 56 | model: @llm_config.model, 57 | temperature: @llm_config.temperature 58 | }, 59 | options: @options 60 | } 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /.architecture/planning/session_compaction_rule.md: -------------------------------------------------------------------------------- 1 | # Session Compaction Rule 2 | 3 | ## Purpose 4 | Store session history summaries in a structured format to maintain an accessible record of design decisions, implementation activities, and architectural evolution across the project. 5 | 6 | ## Rules 7 | 8 | 1. **Directory Structure** 9 | - All session summaries are stored in the `.claude-sessions/` directory 10 | - Directory is created if it doesn't exist 11 | 12 | 2. **Naming Convention** 13 | - Files follow the format: `NNN-descriptive-session-name.md` 14 | - Where NNN is a sequential number (e.g., 001, 002) 15 | - The descriptive name should concisely capture the session's main focus 16 | 17 | 3. **Content Structure** 18 | - Each file includes: 19 | - Primary request and intent 20 | - Key technical concepts 21 | - Files and code sections impacted 22 | - Problem-solving approach 23 | - Pending tasks and next steps 24 | 25 | 4. **When to Compact** 26 | - After completing significant architectural or implementation work 27 | - When switching to a new major feature or component 28 | - Periodically during long sessions to maintain continuity 29 | 30 | 5. **Process** 31 | - Generate a comprehensive summary of the conversation 32 | - Save to a new file in the `.claude-sessions/` directory 33 | - Ensure all key decisions and implementation details are captured 34 | 35 | ## Integration with Architecture Documentation 36 | 37 | Session compaction summaries should cross-reference with other architectural documents to maintain a coherent history of the project's evolution. Key architectural decisions identified during sessions should be properly documented in: 38 | 39 | - Architecture Decision Records (ADRs) in the `.architecture-review/` directory 40 | - Updates to `ArchitectureConsiderations.md` for high-level changes 41 | - Component-specific documentation files 42 | 43 | This ensures that the project maintains both a record of what was done (session summaries) and the rationale behind architectural decisions (formal documentation). -------------------------------------------------------------------------------- /lib/agentic/execution_plan.rb: -------------------------------------------------------------------------------- 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] 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] 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 54 | -------------------------------------------------------------------------------- /spec/agentic/task_definition_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::TaskDefinition do 6 | let(:agent) do 7 | Agentic::AgentSpecification.new( 8 | name: "ResearchAgent", 9 | description: "An agent that performs research", 10 | instructions: "Research the given topic thoroughly" 11 | ) 12 | end 13 | 14 | let(:task_definition) do 15 | described_class.new( 16 | description: "Research AI trends", 17 | agent: agent 18 | ) 19 | end 20 | 21 | describe "#initialize" do 22 | it "sets the description and agent" do 23 | expect(task_definition.description).to eq("Research AI trends") 24 | expect(task_definition.agent).to eq(agent) 25 | end 26 | end 27 | 28 | describe "#to_h" do 29 | it "returns a hash representation of the task definition" do 30 | expect(task_definition.to_h).to eq({ 31 | "description" => "Research AI trends", 32 | "agent" => { 33 | "name" => "ResearchAgent", 34 | "description" => "An agent that performs research", 35 | "instructions" => "Research the given topic thoroughly" 36 | } 37 | }) 38 | end 39 | end 40 | 41 | describe ".from_hash" do 42 | let(:hash) do 43 | { 44 | "description" => "Research AI trends", 45 | "agent" => { 46 | "name" => "ResearchAgent", 47 | "description" => "An agent that performs research", 48 | "instructions" => "Research the given topic thoroughly" 49 | } 50 | } 51 | end 52 | 53 | it "creates a TaskDefinition from a hash" do 54 | task = described_class.from_hash(hash) 55 | expect(task).to be_a(described_class) 56 | expect(task.description).to eq("Research AI trends") 57 | expect(task.agent).to be_a(Agentic::AgentSpecification) 58 | expect(task.agent.name).to eq("ResearchAgent") 59 | expect(task.agent.description).to eq("An agent that performs research") 60 | expect(task.agent.instructions).to eq("Research the given topic thoroughly") 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/agentic/plan_orchestrator_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Configuration object for the PlanOrchestrator 5 | class PlanOrchestratorConfig 6 | # @return [Integer] Maximum number of concurrent tasks 7 | attr_accessor :concurrency_limit 8 | 9 | # @return [Hash] Lifecycle hooks for the orchestrator 10 | attr_accessor :lifecycle_hooks 11 | 12 | # @return [Boolean] Whether to continue execution after a task failure 13 | attr_accessor :continue_on_failure 14 | 15 | # @return [RetryConfig] Retry configuration for tasks 16 | attr_accessor :retry_config 17 | 18 | # @return [Boolean] Whether to execute tasks asynchronously 19 | attr_accessor :async 20 | 21 | # Initializes a new plan orchestrator configuration 22 | # @param concurrency_limit [Integer] Maximum number of concurrent tasks 23 | # @param lifecycle_hooks [Hash] Lifecycle hooks for the orchestrator 24 | # @param continue_on_failure [Boolean] Whether to continue execution after a task failure 25 | # @param retry_config [RetryConfig, nil] Retry configuration for tasks 26 | # @param async [Boolean] Whether to execute tasks asynchronously 27 | def initialize( 28 | concurrency_limit: 10, 29 | lifecycle_hooks: {}, 30 | continue_on_failure: true, 31 | retry_config: nil, 32 | async: true 33 | ) 34 | @concurrency_limit = concurrency_limit 35 | @lifecycle_hooks = lifecycle_hooks 36 | @continue_on_failure = continue_on_failure 37 | @retry_config = retry_config || RetryConfig.new 38 | @async = async 39 | end 40 | 41 | # Returns a hash of configuration options 42 | # @return [Hash] The configuration options 43 | def to_h 44 | { 45 | concurrency_limit: @concurrency_limit, 46 | lifecycle_hooks: @lifecycle_hooks, 47 | continue_on_failure: @continue_on_failure, 48 | retry_config: { 49 | max_retries: @retry_config.max_retries, 50 | backoff_strategy: @retry_config.backoff_strategy, 51 | backoff_options: @retry_config.backoff_options 52 | }, 53 | async: @async 54 | } 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /agentic.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/agentic/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "agentic" 7 | spec.version = Agentic::VERSION 8 | spec.authors = ["Valentino Stoll"] 9 | spec.email = ["v@codenamev.com"] 10 | 11 | spec.summary = "An AI Agent builder and orchestrator" 12 | spec.description = "Easily build, manage, deploy, and run self-contained purpose-driven AI Agents." 13 | spec.homepage = "https://github.com/codenamev/agentic" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 3.0.0" 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = "https://github.com/codenamev/agentic" 19 | spec.metadata["changelog_uri"] = "https://github.com/codenamev/agentic/tree/main/CHANGELOG.md" 20 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 21 | 22 | # Specify which files should be added to the gem when it is released. 23 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 24 | gemspec = File.basename(__FILE__) 25 | spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| 26 | ls.readlines("\x0", chomp: true).reject do |f| 27 | (f == gemspec) || 28 | f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) 29 | end 30 | end 31 | spec.bindir = "exe" 32 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 33 | spec.require_paths = ["lib"] 34 | 35 | spec.add_dependency "dry-schema" 36 | spec.add_dependency "ruby-openai" 37 | spec.add_dependency "zeitwerk" 38 | spec.add_dependency "async", "~> 2.0" 39 | spec.add_dependency "thor", "~> 1.2" 40 | spec.add_dependency "tty-spinner", "~> 0.9" 41 | spec.add_dependency "tty-progressbar", "~> 0.18" 42 | spec.add_dependency "tty-box", "~> 0.7" 43 | spec.add_dependency "tty-table", "~> 0.12" 44 | spec.add_dependency "tty-cursor", "~> 0.7" 45 | spec.add_dependency "pastel", "~> 0.8" 46 | spec.add_dependency "ostruct" 47 | 48 | # For more information and examples about making a new gem, check out our 49 | # guide at: https://bundler.io/guides/creating_gem.html 50 | end 51 | -------------------------------------------------------------------------------- /lib/agentic/generation_stats.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Value object representing statistics for an LLM generation 5 | class GenerationStats 6 | # @return [String] The ID of the generation 7 | attr_reader :id 8 | 9 | # @return [Integer] The number of prompt tokens 10 | attr_reader :prompt_tokens 11 | 12 | # @return [Integer] The number of completion tokens 13 | attr_reader :completion_tokens 14 | 15 | # @return [Integer] The total number of tokens 16 | attr_reader :total_tokens 17 | 18 | # @return [Hash] The raw statistics from the API 19 | attr_reader :raw_stats 20 | 21 | # Initializes a new generation statistics object 22 | # @param id [String] The ID of the generation 23 | # @param prompt_tokens [Integer] The number of prompt tokens 24 | # @param completion_tokens [Integer] The number of completion tokens 25 | # @param total_tokens [Integer] The total number of tokens 26 | # @param raw_stats [Hash] The raw statistics from the API 27 | def initialize(id:, prompt_tokens:, completion_tokens:, total_tokens:, raw_stats: {}) 28 | @id = id 29 | @prompt_tokens = prompt_tokens 30 | @completion_tokens = completion_tokens 31 | @total_tokens = total_tokens 32 | @raw_stats = raw_stats 33 | end 34 | 35 | # Returns a hash representation of the generation statistics 36 | # @return [Hash] The generation statistics as a hash 37 | def to_h 38 | { 39 | id: @id, 40 | prompt_tokens: @prompt_tokens, 41 | completion_tokens: @completion_tokens, 42 | total_tokens: @total_tokens 43 | } 44 | end 45 | 46 | # Creates a GenerationStats object from an API response 47 | # @param response [Hash] The API response 48 | # @return [GenerationStats] A new generation statistics object 49 | def self.from_response(response) 50 | usage = response&.dig("usage") || {} 51 | 52 | new( 53 | id: response&.dig("id") || "", 54 | prompt_tokens: usage["prompt_tokens"] || 0, 55 | completion_tokens: usage["completion_tokens"] || 0, 56 | total_tokens: usage["total_tokens"] || 0, 57 | raw_stats: response || {} 58 | ) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/agentic/verification/verification_hub.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | module Verification 5 | # Coordinates verification strategies and manages the verification process 6 | class VerificationHub 7 | # @return [Array] The registered verification strategies 8 | attr_reader :strategies 9 | 10 | # @return [Hash] Configuration options for the verification hub 11 | attr_reader :config 12 | 13 | # Initializes a new VerificationHub 14 | # @param strategies [Array] The verification strategies to use 15 | # @param config [Hash] Configuration options for the verification hub 16 | def initialize(strategies: [], config: {}) 17 | @strategies = strategies 18 | @config = config 19 | end 20 | 21 | # Adds a verification strategy 22 | # @param strategy [VerificationStrategy] The strategy to add 23 | # @return [void] 24 | def add_strategy(strategy) 25 | @strategies << strategy 26 | end 27 | 28 | # Verifies a task result using the registered strategies 29 | # @param task [Task] The task to verify 30 | # @param result [TaskResult] The result to verify 31 | # @return [VerificationResult] The verification result 32 | def verify(task, result) 33 | # Skip verification for failed tasks 34 | if result.failed? 35 | return VerificationResult.new( 36 | task_id: task.id, 37 | verified: false, 38 | confidence: 0.0, 39 | messages: ["Task failed, skipping verification"] 40 | ) 41 | end 42 | 43 | # Apply all strategies 44 | strategy_results = @strategies.map do |strategy| 45 | strategy.verify(task, result) 46 | end 47 | 48 | # Combine results 49 | verified = strategy_results.all?(&:verified) 50 | confidence = strategy_results.map(&:confidence).sum / strategy_results.size.to_f 51 | messages = strategy_results.flat_map(&:messages) 52 | 53 | VerificationResult.new( 54 | task_id: task.id, 55 | verified: verified, 56 | confidence: confidence, 57 | messages: messages 58 | ) 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /.architecture/templates/adr.md: -------------------------------------------------------------------------------- 1 | # ADR-XXX: [Title] 2 | 3 | ## Status 4 | 5 | [Draft | Proposed | Accepted | Deprecated | Superseded] 6 | 7 | If superseded, link to the new ADR: [New ADR Link] 8 | 9 | ## Context 10 | 11 | [Describe the context and problem statement that led to this decision. Include any relevant constraints, requirements, or background information. Reference the architectural review findings if applicable.] 12 | 13 | ## Decision Drivers 14 | 15 | * [Driver 1: Briefly describe a factor influencing the decision] 16 | * [Driver 2: ...] 17 | * [Driver n: ...] 18 | 19 | ## Decision 20 | 21 | [Describe the decision that was made. Be clear and precise about the architectural change being implemented.] 22 | 23 | **Architectural Components Affected:** 24 | * [Component 1] 25 | * [Component 2] 26 | * [...] 27 | 28 | **Interface Changes:** 29 | * [Detail any changes to public interfaces] 30 | 31 | ## Consequences 32 | 33 | ### Positive 34 | 35 | * [Positive consequence 1] 36 | * [Positive consequence 2] 37 | * [...] 38 | 39 | ### Negative 40 | 41 | * [Negative consequence 1] 42 | * [Negative consequence 2] 43 | * [...] 44 | 45 | ### Neutral 46 | 47 | * [Neutral consequence 1] 48 | * [Neutral consequence 2] 49 | * [...] 50 | 51 | ## Implementation 52 | 53 | [Provide a high-level implementation plan, including any phasing or migration strategies.] 54 | 55 | **Phase 1: [Phase Name]** 56 | * [Implementation step 1] 57 | * [Implementation step 2] 58 | * [...] 59 | 60 | **Phase 2: [Phase Name]** 61 | * [Implementation step 1] 62 | * [Implementation step 2] 63 | * [...] 64 | 65 | ## Alternatives Considered 66 | 67 | ### [Alternative 1] 68 | 69 | [Describe alternative approach] 70 | 71 | **Pros:** 72 | * [Pro 1] 73 | * [Pro 2] 74 | 75 | **Cons:** 76 | * [Con 1] 77 | * [Con 2] 78 | 79 | ### [Alternative 2] 80 | 81 | [Describe alternative approach] 82 | 83 | **Pros:** 84 | * [Pro 1] 85 | * [Pro 2] 86 | 87 | **Cons:** 88 | * [Con 1] 89 | * [Con 2] 90 | 91 | ## Validation 92 | 93 | **Acceptance Criteria:** 94 | - [ ] [Criterion 1] 95 | - [ ] [Criterion 2] 96 | - [ ] [...] 97 | 98 | **Testing Approach:** 99 | * [Describe how the implementation of this decision will be tested] 100 | 101 | ## References 102 | 103 | * [Architectural Review X.Y.Z](link-to-review) 104 | * [Reference 1](link) 105 | * [Reference 2](link) -------------------------------------------------------------------------------- /lib/agentic/task_failure.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Represents a failure that occurred during task execution 5 | # @attr_reader [String] message The failure message 6 | # @attr_reader [String] type The type of failure 7 | # @attr_reader [Time] timestamp When the failure occurred 8 | # @attr_reader [Hash] context Additional context about the failure 9 | class TaskFailure 10 | attr_reader :message, :type, :timestamp, :context 11 | 12 | # Initializes a new task failure 13 | # @param message [String] The failure message 14 | # @param type [String] The type of failure 15 | # @param context [Hash] Additional context about the failure 16 | # @return [TaskFailure] A new task failure instance 17 | def initialize(message:, type:, context: {}) 18 | @message = message 19 | @type = type 20 | @timestamp = Time.now 21 | @context = context 22 | end 23 | 24 | # Returns a serializable representation of the failure 25 | # @return [Hash] The failure as a hash 26 | def to_h 27 | { 28 | message: @message, 29 | type: @type, 30 | timestamp: @timestamp.iso8601, 31 | context: @context 32 | } 33 | end 34 | 35 | # Creates a task failure from an exception 36 | # @param exception [Exception] The exception 37 | # @param context [Hash] Additional context about the failure 38 | # @return [TaskFailure] A new task failure instance 39 | def self.from_exception(exception, context = {}) 40 | new( 41 | message: exception.message, 42 | type: exception.class.name, 43 | context: context.merge( 44 | backtrace: exception.backtrace&.first(10) 45 | ) 46 | ) 47 | end 48 | 49 | # Creates a task failure from a hash 50 | # @param hash [Hash] The hash representation of a task failure 51 | # @return [TaskFailure] A new task failure instance 52 | def self.from_hash(hash) 53 | # Handle the case where hash is not actually a hash 54 | return new(message: "Unknown error", type: "UnknownError") unless hash.is_a?(Hash) 55 | 56 | # Convert string keys to symbols if necessary 57 | hash = hash.transform_keys(&:to_sym) if hash.keys.first.is_a?(String) 58 | 59 | new( 60 | message: hash[:message] || "Unknown error", 61 | type: hash[:type] || "UnknownError", 62 | context: hash[:context] || {} 63 | ) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/agentic/retry_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Configuration object for the RetryHandler 5 | class RetryConfig 6 | # @return [Integer] The maximum number of retry attempts 7 | attr_accessor :max_retries 8 | 9 | # @return [Array] List of retryable error types/names 10 | attr_accessor :retryable_errors 11 | 12 | # @return [Symbol] The backoff strategy to use 13 | attr_accessor :backoff_strategy 14 | 15 | # @return [Hash] Options for the backoff strategy 16 | attr_accessor :backoff_options 17 | 18 | # @return [Proc, nil] Optional block to run before each retry 19 | attr_accessor :before_retry 20 | 21 | # @return [Proc, nil] Optional block to run after each retry 22 | attr_accessor :after_retry 23 | 24 | # Initializes a new retry configuration 25 | # @param max_retries [Integer] The maximum number of retry attempts 26 | # @param retryable_errors [Array] List of retryable error types/names 27 | # @param backoff_strategy [Symbol] The backoff strategy (:constant, :linear, :exponential) 28 | # @param backoff_options [Hash] Options for the backoff strategy 29 | # @param before_retry [Proc, nil] Optional block to run before each retry 30 | # @param after_retry [Proc, nil] Optional block to run after each retry 31 | def initialize( 32 | max_retries: 3, 33 | retryable_errors: [Errors::LlmTimeoutError, Errors::LlmRateLimitError, Errors::LlmServerError, Errors::LlmNetworkError], 34 | backoff_strategy: :exponential, 35 | backoff_options: {}, 36 | before_retry: nil, 37 | after_retry: nil 38 | ) 39 | @max_retries = max_retries 40 | @retryable_errors = retryable_errors 41 | @backoff_strategy = backoff_strategy 42 | @backoff_options = { 43 | base_delay: 1.0, 44 | jitter_factor: 0.25 45 | }.merge(backoff_options) 46 | @before_retry = before_retry 47 | @after_retry = after_retry 48 | end 49 | 50 | # Creates a RetryHandler from this configuration 51 | # @return [RetryHandler] A new retry handler 52 | def to_handler 53 | RetryHandler.new( 54 | max_retries: @max_retries, 55 | retryable_errors: @retryable_errors, 56 | backoff_strategy: @backoff_strategy, 57 | backoff_options: @backoff_options, 58 | before_retry: @before_retry, 59 | after_retry: @after_retry 60 | ) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/agentic/verification/llm_verification_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "verification_strategy" 4 | require_relative "verification_result" 5 | 6 | module Agentic 7 | module Verification 8 | # Verifies task results using an LLM 9 | class LlmVerificationStrategy < VerificationStrategy 10 | # Initializes a new LlmVerificationStrategy 11 | # @param llm_client [LlmClient] The LLM client to use for verification 12 | # @param config [Hash] Configuration options for the strategy 13 | def initialize(llm_client, config = {}) 14 | super(config) 15 | @llm_client = llm_client 16 | end 17 | 18 | # Verifies a task result using an LLM 19 | # @param task [Task] The task to verify 20 | # @param result [TaskResult] The result to verify 21 | # @return [VerificationResult] The verification result 22 | def verify(task, result) 23 | unless result.successful? 24 | return VerificationResult.new( 25 | task_id: task.id, 26 | verified: false, 27 | confidence: 0.0, 28 | messages: ["Task failed, skipping LLM verification"] 29 | ) 30 | end 31 | 32 | # In a real implementation, we would send the task and result to the LLM 33 | # and analyze the LLM's assessment 34 | # For this stub, we'll simulate a response 35 | 36 | # Example verification prompt 37 | # Task Description: #{task.description} 38 | # Task Input: #{task.input.inspect} 39 | # Task Result: #{result.output.inspect} 40 | # 41 | # Verify if the result satisfies the task requirements. 42 | # Consider correctness, completeness, and alignment with the task description. 43 | # Provide your assessment with a boolean verdict (verified: true/false) and a confidence score (0.0-1.0). 44 | 45 | # In a real implementation, we would use the LLM client here 46 | # For this stub, we'll return a simulated verification result 47 | verified = rand > 0.1 # 90% chance of success for simulation purposes 48 | confidence = verified ? (0.8 + rand * 0.2) : (0.3 + rand * 0.3) 49 | message = verified ? "Result meets task requirements" : "Result does not fully satisfy task requirements" 50 | 51 | VerificationResult.new( 52 | task_id: task.id, 53 | verified: verified, 54 | confidence: confidence, 55 | messages: [message] 56 | ) 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/agentic/llm_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Configuration object for LLM API calls 5 | class LlmConfig 6 | # @return [String] The model to use for LLM requests 7 | attr_accessor :model 8 | 9 | # @return [Integer] The maximum number of tokens to generate 10 | attr_accessor :max_tokens 11 | 12 | # @return [Float] The temperature parameter (controls randomness) 13 | attr_accessor :temperature 14 | 15 | # @return [Float] The top_p parameter (nucleus sampling) 16 | attr_accessor :top_p 17 | 18 | # @return [Integer] The frequency penalty 19 | attr_accessor :frequency_penalty 20 | 21 | # @return [Integer] The presence penalty 22 | attr_accessor :presence_penalty 23 | 24 | # @return [Hash] Additional options to pass to the API 25 | attr_accessor :additional_options 26 | 27 | # Initializes a new LLM configuration 28 | # @param model [String] The model to use 29 | # @param max_tokens [Integer, nil] The maximum number of tokens to generate 30 | # @param temperature [Float] The temperature parameter (0.0-2.0) 31 | # @param top_p [Float] The top_p parameter (0.0-1.0) 32 | # @param frequency_penalty [Float] The frequency penalty (-2.0-2.0) 33 | # @param presence_penalty [Float] The presence penalty (-2.0-2.0) 34 | # @param additional_options [Hash] Additional options to pass to the API 35 | def initialize( 36 | model: "gpt-4o-2024-08-06", 37 | max_tokens: nil, 38 | temperature: 0.7, 39 | top_p: 1.0, 40 | frequency_penalty: 0.0, 41 | presence_penalty: 0.0, 42 | additional_options: {} 43 | ) 44 | @model = model 45 | @max_tokens = max_tokens 46 | @temperature = temperature 47 | @top_p = top_p 48 | @frequency_penalty = frequency_penalty 49 | @presence_penalty = presence_penalty 50 | @additional_options = additional_options 51 | end 52 | 53 | # Returns a hash of parameters for the API call 54 | # @param base_params [Hash] Base parameters to include 55 | # @return [Hash] Parameters for the API call 56 | def to_api_parameters(base_params = {}) 57 | params = { 58 | model: @model, 59 | temperature: @temperature, 60 | top_p: @top_p, 61 | frequency_penalty: @frequency_penalty, 62 | presence_penalty: @presence_penalty 63 | } 64 | 65 | # Add max_tokens if specified 66 | params[:max_tokens] = @max_tokens if @max_tokens 67 | 68 | # Merge any additional options 69 | params.merge!(@additional_options) 70 | 71 | # Merge with base parameters 72 | base_params.merge(params) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /.architecture/decisions/cli_command_structure.md: -------------------------------------------------------------------------------- 1 | # Architectural Decision Record: CLI Command Structure 2 | 3 | ## Context 4 | 5 | The Agentic CLI currently has duplicate command definitions appearing in the help output. Commands related to agent management and configuration appear both as top-level commands and as subcommands. This is causing confusion for users trying to understand the proper command structure. 6 | 7 | Investigation revealed that two implementations of the same functionality exist: 8 | 1. Nested classes within the main CLI class (`AgentCommands` and `ConfigCommands` in cli.rb) with enhanced UI 9 | 2. Standalone files in the cli/ directory (`agent.rb` and `config.rb`) with simpler implementations 10 | 11 | Both implementations are being loaded and registered, resulting in duplicate commands in the help output. 12 | 13 | ## Decision 14 | 15 | We will standardize on the nested class implementation and remove the standalone file implementations. This approach: 16 | 17 | 1. Provides enhanced UI with colorization and box output for a better user experience 18 | 2. Maintains a clear hierarchy of commands 19 | 3. Eliminates duplicate commands in the help output 20 | 21 | We will: 22 | 1. Remove or disable the standalone CLI command files (agent.rb, config.rb) 23 | 2. Update requires in agentic.rb to reflect this change 24 | 3. Ensure proper Thor subcommand registration 25 | 26 | ## Rationale 27 | 28 | The nested class implementation provides a superior user experience with colorized output and formatted boxes. While moving to standalone files might provide better modularity in the long term, the current priority is to fix the command duplication and maintain the enhanced UI. 29 | 30 | ## Consequences 31 | 32 | ### Positive 33 | - Elimination of duplicate commands in CLI help output 34 | - Consistent, enhanced UI for all commands 35 | - Clear command hierarchy 36 | 37 | ### Negative 38 | - The CLI implementation remains in a single file, which may be less modular 39 | - Future changes to the CLI may require more coordination 40 | - Long-term maintenance might be more challenging 41 | 42 | ### Neutral 43 | - This approach prioritizes user experience over code modularity 44 | - A future refactoring to a hybrid approach may still be desirable 45 | 46 | ## Future Considerations 47 | 48 | In future versions, we may want to consider: 49 | 1. Refactoring to a hybrid approach that maintains the enhanced UI while improving modularity 50 | 2. Separating UI presentation from command logic 51 | 3. Creating a more comprehensive testing framework for CLI commands 52 | 53 | ## Implementation Notes 54 | 55 | The implementation will: 56 | 1. Remove or comment out requires for standalone CLI files in agentic.rb 57 | 2. Ensure only nested class implementations are being registered 58 | 3. Test all CLI commands to ensure they work as expected 59 | 4. Update documentation to reflect the command structure 60 | 61 | Date: May 22, 2024 -------------------------------------------------------------------------------- /lib/agentic/learning/README.md: -------------------------------------------------------------------------------- 1 | # Learning System Components 2 | 3 | The Learning System in Agentic provides capabilities for capturing and analyzing execution data, identifying patterns, and optimizing strategies based on historical performance. 4 | 5 | ## Overview 6 | 7 | The Learning System consists of three main components: 8 | 9 | 1. **ExecutionHistoryStore**: Captures and stores task and plan execution data. 10 | 2. **PatternRecognizer**: Analyzes execution history to identify patterns and optimization opportunities. 11 | 3. **StrategyOptimizer**: Uses insights from pattern analysis to optimize prompts, parameters, and task sequences. 12 | 13 | ## Usage 14 | 15 | ```ruby 16 | # Initialize components 17 | history_store = Agentic::Learning::ExecutionHistoryStore.new 18 | recognizer = Agentic::Learning::PatternRecognizer.new(history_store: history_store) 19 | optimizer = Agentic::Learning::StrategyOptimizer.new( 20 | pattern_recognizer: recognizer, 21 | history_store: history_store, 22 | llm_client: llm_client # Optional 23 | ) 24 | 25 | # Record execution data 26 | history_store.record_execution( 27 | task_id: "task-123", 28 | agent_type: "research_agent", 29 | duration_ms: 1500, 30 | success: true, 31 | metrics: { tokens_used: 2000 } 32 | ) 33 | 34 | # Analyze patterns 35 | patterns = recognizer.analyze_agent_performance("research_agent") 36 | 37 | # Get optimization recommendations 38 | recommendations = recognizer.recommend_optimizations("research_agent") 39 | 40 | # Optimize a prompt template 41 | improved_prompt = optimizer.optimize_prompt_template( 42 | original_template: "Please research the topic: {topic}", 43 | agent_type: "research_agent" 44 | ) 45 | 46 | # Optimize LLM parameters 47 | improved_params = optimizer.optimize_llm_parameters( 48 | original_params: { temperature: 0.7, max_tokens: 2000 }, 49 | agent_type: "research_agent" 50 | ) 51 | 52 | # Generate a performance report 53 | report = optimizer.generate_performance_report("research_agent") 54 | ``` 55 | 56 | ## Factory Method 57 | 58 | The Learning module provides a factory method to create all components at once: 59 | 60 | ```ruby 61 | learning_system = Agentic::Learning.create( 62 | storage_path: "/path/to/history", 63 | llm_client: llm_client, 64 | auto_optimize: false 65 | ) 66 | 67 | # Access individual components 68 | history_store = learning_system[:history_store] 69 | recognizer = learning_system[:pattern_recognizer] 70 | optimizer = learning_system[:strategy_optimizer] 71 | ``` 72 | 73 | ## Integration with PlanOrchestrator 74 | 75 | The Learning System can be integrated with the PlanOrchestrator to automatically record execution data: 76 | 77 | ```ruby 78 | orchestrator = Agentic::PlanOrchestrator.new 79 | learning_system = Agentic::Learning.create 80 | 81 | Agentic::Learning.register_with_orchestrator(orchestrator, learning_system) 82 | ``` 83 | 84 | This will register event handlers to automatically record task and plan executions. -------------------------------------------------------------------------------- /.architecture/templates/recalibration_plan.md: -------------------------------------------------------------------------------- 1 | # Architectural Recalibration Plan: Version X.Y.Z 2 | 3 | ## Overview 4 | 5 | This document outlines the action plan derived from the architectural review of version X.Y.Z. It categorizes and prioritizes recommendations to guide implementation across upcoming releases. 6 | 7 | ## Review Summary 8 | 9 | - Review Date: [DATE] 10 | - Review Document: [LINK TO REVIEW] 11 | - Participants: [LIST OF PARTICIPANTS] 12 | 13 | ## Action Items 14 | 15 | ### Architectural Changes 16 | 17 | | ID | Recommendation | Priority | Owner | Target Version | Dependencies | Notes | 18 | |----|---------------|----------|-------|----------------|--------------|-------| 19 | | A1 | [Brief description] | [Critical/High/Medium/Low] | [Owner] | [Version] | [Dependencies] | [Additional context] | 20 | 21 | ### Implementation Improvements 22 | 23 | | ID | Recommendation | Priority | Owner | Target Version | Dependencies | Notes | 24 | |----|---------------|----------|-------|----------------|--------------|-------| 25 | | I1 | [Brief description] | [Critical/High/Medium/Low] | [Owner] | [Version] | [Dependencies] | [Additional context] | 26 | 27 | ### Documentation Enhancements 28 | 29 | | ID | Recommendation | Priority | Owner | Target Version | Dependencies | Notes | 30 | |----|---------------|----------|-------|----------------|--------------|-------| 31 | | D1 | [Brief description] | [Critical/High/Medium/Low] | [Owner] | [Version] | [Dependencies] | [Additional context] | 32 | 33 | ### Process Adjustments 34 | 35 | | ID | Recommendation | Priority | Owner | Target Version | Dependencies | Notes | 36 | |----|---------------|----------|-------|----------------|--------------|-------| 37 | | P1 | [Brief description] | [Critical/High/Medium/Low] | [Owner] | [Version] | [Dependencies] | [Additional context] | 38 | 39 | ## Technical Debt Items 40 | 41 | Items identified in the review that won't be addressed immediately but should be tracked: 42 | 43 | | ID | Description | Impact | Potential Resolution Timeframe | 44 | |----|-------------|--------|--------------------------------| 45 | | TD1 | [Description] | [Low/Medium/High] | [Timeframe] | 46 | 47 | ## Decision Records 48 | 49 | List of Architectural Decision Records (ADRs) that need to be created or updated based on the review: 50 | 51 | | ADR ID | Title | Status | Owner | Target Completion | 52 | |--------|-------|--------|-------|-------------------| 53 | | ADR-XXX | [Title] | [Draft/Proposed/Accepted] | [Owner] | [Date] | 54 | 55 | ## Timeline 56 | 57 | Overview of the recalibration implementation timeline: 58 | 59 | - Analysis & Prioritization: [Start Date] - [End Date] 60 | - Architectural Plan Update: [Start Date] - [End Date] 61 | - Documentation Refresh: [Start Date] - [End Date] 62 | - Implementation Roadmapping: [Start Date] - [End Date] 63 | 64 | ## Next Steps 65 | 66 | Immediate next actions to be taken: 67 | 68 | 1. [Action 1] 69 | 2. [Action 2] 70 | 3. [Action 3] -------------------------------------------------------------------------------- /spec/agentic/task_result_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::TaskResult do 6 | let(:task_id) { "test-task-id" } 7 | let(:output) { {"result" => "test_result"} } 8 | let(:failure) { Agentic::TaskFailure.new(message: "Test failure", type: "TestError") } 9 | 10 | describe "#initialize" do 11 | context "with success result" do 12 | let(:result) { described_class.new(task_id: task_id, success: true, output: output) } 13 | 14 | it "sets the attributes correctly" do 15 | expect(result.task_id).to eq(task_id) 16 | expect(result.success).to be true 17 | expect(result.output).to eq(output) 18 | expect(result.failure).to be_nil 19 | end 20 | end 21 | 22 | context "with failure result" do 23 | let(:result) { described_class.new(task_id: task_id, success: false, failure: failure) } 24 | 25 | it "sets the attributes correctly" do 26 | expect(result.task_id).to eq(task_id) 27 | expect(result.success).to be false 28 | expect(result.output).to be_nil 29 | expect(result.failure).to eq(failure) 30 | end 31 | end 32 | end 33 | 34 | describe "#successful?" do 35 | it "returns true when success is true" do 36 | result = described_class.new(task_id: task_id, success: true) 37 | expect(result.successful?).to be true 38 | end 39 | 40 | it "returns false when success is false" do 41 | result = described_class.new(task_id: task_id, success: false) 42 | expect(result.successful?).to be false 43 | end 44 | end 45 | 46 | describe "#failed?" do 47 | it "returns false when success is true" do 48 | result = described_class.new(task_id: task_id, success: true) 49 | expect(result.failed?).to be false 50 | end 51 | 52 | it "returns true when success is false" do 53 | result = described_class.new(task_id: task_id, success: false) 54 | expect(result.failed?).to be true 55 | end 56 | end 57 | 58 | describe "#to_h" do 59 | it "returns a hash representation for success result" do 60 | result = described_class.new(task_id: task_id, success: true, output: output) 61 | hash = result.to_h 62 | 63 | expect(hash).to be_a(Hash) 64 | expect(hash[:task_id]).to eq(task_id) 65 | expect(hash[:success]).to be true 66 | expect(hash[:output]).to eq(output) 67 | expect(hash[:failure]).to be_nil 68 | end 69 | 70 | it "returns a hash representation for failure result" do 71 | result = described_class.new(task_id: task_id, success: false, failure: failure) 72 | hash = result.to_h 73 | 74 | expect(hash).to be_a(Hash) 75 | expect(hash[:task_id]).to eq(task_id) 76 | expect(hash[:success]).to be false 77 | expect(hash[:output]).to be_nil 78 | expect(hash[:failure]).to be_a(Hash) 79 | expect(hash[:failure][:message]).to eq("Test failure") 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.2.0] - 2025-05-29 2 | 3 | ### Added 4 | - Agent Self-Assembly System for dynamic agent construction 5 | - AgentCapabilityRegistry for managing capability specifications and providers 6 | - PersistentAgentStore for saving and retrieving agent configurations 7 | - AgentAssemblyEngine for analyzing tasks and assembling appropriate agents 8 | - CapabilityOptimizer for improving capability implementations 9 | - LLM-assisted capability selection strategy 10 | - Capability System with rich specification and versioning 11 | - Clear distinction between capabilities and tools 12 | - Semantic versioning for capability evolution 13 | - Capability composition for building complex capabilities 14 | - Dependency management for capabilities 15 | - CLI commands for capability and agent management 16 | - Listing and filtering capabilities 17 | - Viewing capability details 18 | - Searching for capabilities 19 | - Agent creation and management 20 | - Example capabilities for common tasks 21 | - Integration with learning system for capability optimization 22 | - Comprehensive integration tests for agent assembly workflow 23 | - Documentation for capability API and agent self-assembly 24 | - Comprehensive CLI implementation with subcommands for plan, execute, agent, and config 25 | - Real-time feedback with progress bars, spinners, and colorized output 26 | - Per-user and per-project configuration support 27 | - Enhanced LLM error and refusal handling with categorization 28 | - First-class configuration objects for LLM, retry handling, and orchestration 29 | - Value objects for task definitions, agent specifications, and execution results 30 | - Expanded test coverage for core components 31 | 32 | ### Improved 33 | - Test coverage across all major features 34 | - Documentation for integration testing 35 | - Stability in edge cases like timeouts and partial failures 36 | - Metrics collection for learning system analysis 37 | - Agent reusability through persistent storage 38 | - Decoupled data from presentation throughout the codebase 39 | - Improved error handling with specific error types and recovery strategies 40 | - Enhanced documentation with CLI examples and API snippets 41 | 42 | ## [0.1.0] - 2024-06-27 43 | 44 | ### Added 45 | - Comprehensive CLI implementation with subcommands for plan, execute, agent, and config 46 | - Real-time feedback with progress bars, spinners, and colorized output 47 | - Per-user and per-project configuration support 48 | - Enhanced LLM error and refusal handling with categorization 49 | - First-class configuration objects for LLM, retry handling, and orchestration 50 | - Value objects for task definitions, agent specifications, and execution results 51 | - Expanded test coverage for core components 52 | 53 | ### Changed 54 | - Decoupled data from presentation throughout the codebase 55 | - Improved error handling with specific error types and recovery strategies 56 | - Enhanced documentation with CLI examples and API snippets 57 | 58 | ## [0.1.0] - 2024-06-27 59 | 60 | - Initial release -------------------------------------------------------------------------------- /spec/agentic/retry_config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::RetryConfig do 6 | let(:default_config) { described_class.new } 7 | let(:custom_config) do 8 | described_class.new( 9 | max_retries: 5, 10 | retryable_errors: [Agentic::Errors::LlmTimeoutError], 11 | backoff_strategy: :linear, 12 | backoff_options: {base_delay: 2.0, jitter_factor: 0.3}, 13 | before_retry: lambda { |attempt:, **_| puts "Retrying (#{attempt})" }, 14 | after_retry: lambda { |attempt:, **_| puts "Retried (#{attempt})" } 15 | ) 16 | end 17 | 18 | describe "#initialize" do 19 | it "sets default values" do 20 | expect(default_config.max_retries).to eq(3) 21 | expect(default_config.retryable_errors).to include(Agentic::Errors::LlmTimeoutError) 22 | expect(default_config.retryable_errors).to include(Agentic::Errors::LlmRateLimitError) 23 | expect(default_config.retryable_errors).to include(Agentic::Errors::LlmServerError) 24 | expect(default_config.retryable_errors).to include(Agentic::Errors::LlmNetworkError) 25 | expect(default_config.backoff_strategy).to eq(:exponential) 26 | expect(default_config.backoff_options[:base_delay]).to eq(1.0) 27 | expect(default_config.backoff_options[:jitter_factor]).to eq(0.25) 28 | expect(default_config.before_retry).to be_nil 29 | expect(default_config.after_retry).to be_nil 30 | end 31 | 32 | it "allows custom values" do 33 | expect(custom_config.max_retries).to eq(5) 34 | expect(custom_config.retryable_errors).to eq([Agentic::Errors::LlmTimeoutError]) 35 | expect(custom_config.backoff_strategy).to eq(:linear) 36 | expect(custom_config.backoff_options[:base_delay]).to eq(2.0) 37 | expect(custom_config.backoff_options[:jitter_factor]).to eq(0.3) 38 | expect(custom_config.before_retry).to be_a(Proc) 39 | expect(custom_config.after_retry).to be_a(Proc) 40 | end 41 | 42 | it "merges backoff options with defaults" do 43 | config = described_class.new(backoff_options: {base_delay: 2.0}) 44 | expect(config.backoff_options[:base_delay]).to eq(2.0) 45 | expect(config.backoff_options[:jitter_factor]).to eq(0.25) 46 | end 47 | end 48 | 49 | describe "#to_handler" do 50 | it "creates a RetryHandler with the configuration" do 51 | handler = custom_config.to_handler 52 | expect(handler).to be_a(Agentic::RetryHandler) 53 | expect(handler.max_retries).to eq(5) 54 | expect(handler.retryable_errors).to eq([Agentic::Errors::LlmTimeoutError]) 55 | expect(handler.backoff_strategy).to eq(:linear) 56 | expect(handler.instance_variable_get(:@backoff_options)[:base_delay]).to eq(2.0) 57 | expect(handler.instance_variable_get(:@backoff_options)[:jitter_factor]).to eq(0.3) 58 | expect(handler.instance_variable_get(:@before_retry)).to be_a(Proc) 59 | expect(handler.instance_variable_get(:@after_retry)).to be_a(Proc) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/agentic/generation_stats_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::GenerationStats do 6 | let(:generation_stats) do 7 | described_class.new( 8 | id: "gen-123", 9 | prompt_tokens: 50, 10 | completion_tokens: 150, 11 | total_tokens: 200, 12 | raw_stats: {"usage" => {"prompt_tokens" => 50, "completion_tokens" => 150, "total_tokens" => 200}} 13 | ) 14 | end 15 | 16 | describe "#initialize" do 17 | it "sets the id, prompt_tokens, completion_tokens, and total_tokens" do 18 | expect(generation_stats.id).to eq("gen-123") 19 | expect(generation_stats.prompt_tokens).to eq(50) 20 | expect(generation_stats.completion_tokens).to eq(150) 21 | expect(generation_stats.total_tokens).to eq(200) 22 | end 23 | 24 | it "sets the raw_stats" do 25 | expect(generation_stats.raw_stats).to eq({ 26 | "usage" => { 27 | "prompt_tokens" => 50, 28 | "completion_tokens" => 150, 29 | "total_tokens" => 200 30 | } 31 | }) 32 | end 33 | end 34 | 35 | describe "#to_h" do 36 | it "returns a hash representation of the generation statistics" do 37 | expect(generation_stats.to_h).to eq({ 38 | id: "gen-123", 39 | prompt_tokens: 50, 40 | completion_tokens: 150, 41 | total_tokens: 200 42 | }) 43 | end 44 | end 45 | 46 | describe ".from_response" do 47 | let(:response) do 48 | { 49 | "id" => "gen-123", 50 | "usage" => { 51 | "prompt_tokens" => 50, 52 | "completion_tokens" => 150, 53 | "total_tokens" => 200 54 | } 55 | } 56 | end 57 | 58 | it "creates a GenerationStats from an API response" do 59 | stats = described_class.from_response(response) 60 | expect(stats).to be_a(described_class) 61 | expect(stats.id).to eq("gen-123") 62 | expect(stats.prompt_tokens).to eq(50) 63 | expect(stats.completion_tokens).to eq(150) 64 | expect(stats.total_tokens).to eq(200) 65 | expect(stats.raw_stats).to eq(response) 66 | end 67 | 68 | it "handles missing or incomplete data gracefully" do 69 | incomplete_response = {"id" => "gen-123"} 70 | stats = described_class.from_response(incomplete_response) 71 | expect(stats).to be_a(described_class) 72 | expect(stats.id).to eq("gen-123") 73 | expect(stats.prompt_tokens).to eq(0) 74 | expect(stats.completion_tokens).to eq(0) 75 | expect(stats.total_tokens).to eq(0) 76 | expect(stats.raw_stats).to eq(incomplete_response) 77 | end 78 | 79 | it "handles nil response gracefully" do 80 | stats = described_class.from_response(nil) 81 | expect(stats).to be_a(described_class) 82 | expect(stats.id).to eq("") 83 | expect(stats.prompt_tokens).to eq(0) 84 | expect(stats.completion_tokens).to eq(0) 85 | expect(stats.total_tokens).to eq(0) 86 | expect(stats.raw_stats).to eq({}) 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/agentic/errors/llm_refusal_error_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::Errors::LlmRefusalError do 6 | describe "#initialize" do 7 | it "sets the refusal message and response" do 8 | error = described_class.new( 9 | "I cannot provide that information", 10 | response: {"id" => "test-response"} 11 | ) 12 | 13 | expect(error.message).to eq("LLM refused to respond: I cannot provide that information") 14 | expect(error.refusal_message).to eq("I cannot provide that information") 15 | expect(error.response).to eq({"id" => "test-response"}) 16 | end 17 | 18 | it "detects refusal category from message" do 19 | harmful_error = described_class.new("This content is harmful") 20 | expect(harmful_error.refusal_category).to eq(:harmful_content) 21 | 22 | clarify_error = described_class.new("This request needs clarification") 23 | expect(clarify_error.refusal_category).to eq(:needs_clarification) 24 | 25 | format_error = described_class.new("The format of your request is incorrect") 26 | expect(format_error.refusal_category).to eq(:format_error) 27 | 28 | unclear_error = described_class.new("Your instructions are unclear") 29 | expect(unclear_error.refusal_category).to eq(:unclear_instructions) 30 | 31 | capability_error = described_class.new("I don't have the capability to do that") 32 | expect(capability_error.refusal_category).to eq(:capability_limitation) 33 | 34 | general_error = described_class.new("I cannot do that") 35 | expect(general_error.refusal_category).to eq(:general_refusal) 36 | end 37 | 38 | it "allows explicit refusal category specification" do 39 | error = described_class.new( 40 | "I cannot do that", 41 | refusal_category: :custom_category 42 | ) 43 | 44 | expect(error.refusal_category).to eq(:custom_category) 45 | end 46 | end 47 | 48 | describe "#retryable_with_modifications?" do 49 | it "returns true for retryable refusal categories" do 50 | unclear_error = described_class.new("Your instructions are unclear") 51 | expect(unclear_error.retryable_with_modifications?).to be true 52 | 53 | clarify_error = described_class.new("This request needs clarification") 54 | expect(clarify_error.retryable_with_modifications?).to be true 55 | 56 | format_error = described_class.new("The format of your request is incorrect") 57 | expect(format_error.retryable_with_modifications?).to be true 58 | end 59 | 60 | it "returns false for non-retryable refusal categories" do 61 | harmful_error = described_class.new("This content is harmful") 62 | expect(harmful_error.retryable_with_modifications?).to be false 63 | 64 | capability_error = described_class.new("I don't have the capability to do that") 65 | expect(capability_error.retryable_with_modifications?).to be false 66 | 67 | general_error = described_class.new("I cannot do that") 68 | expect(general_error.retryable_with_modifications?).to be false 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/agentic/task_execution_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "task_failure" 4 | 5 | module Agentic 6 | # Value object representing the execution result of a task 7 | class TaskExecutionResult 8 | # @return [Symbol] The status of the task execution (:completed, :failed, :canceled) 9 | attr_reader :status 10 | 11 | # @return [Hash, nil] The output produced by the task (only if successful) 12 | attr_reader :output 13 | 14 | # @return [TaskFailure, nil] The failure details (only if failed) 15 | attr_reader :failure 16 | 17 | # @param status [Symbol] The status of the task execution 18 | # @param output [Hash, nil] The output produced by the task 19 | # @param failure [TaskFailure, nil] The failure details 20 | def initialize(status:, output: nil, failure: nil) 21 | @status = status 22 | @output = output 23 | @failure = failure 24 | end 25 | 26 | # Creates a successful execution result 27 | # @param output [Hash] The task output 28 | # @return [TaskExecutionResult] A successful execution result 29 | def self.success(output) 30 | new(status: :completed, output: output) 31 | end 32 | 33 | # Creates a failed execution result 34 | # @param failure [TaskFailure] The failure details 35 | # @return [TaskExecutionResult] A failed execution result 36 | def self.failure(failure) 37 | new(status: :failed, failure: failure) 38 | end 39 | 40 | # Creates a canceled execution result 41 | # @return [TaskExecutionResult] A canceled execution result 42 | def self.canceled 43 | new(status: :canceled) 44 | end 45 | 46 | # Creates a task execution result from a hash 47 | # @param hash [Hash] The hash representation of a task execution result 48 | # @return [TaskExecutionResult] A task execution result 49 | def self.from_hash(hash) 50 | # Handle the case where hash is not actually a hash (could be nil, Integer, etc.) 51 | return new(status: :completed) unless hash.is_a?(Hash) 52 | 53 | # Convert string keys to symbols if necessary 54 | hash = hash.transform_keys(&:to_sym) if hash.keys.first.is_a?(String) 55 | 56 | failure = hash[:failure] ? TaskFailure.from_hash(hash[:failure]) : nil 57 | new( 58 | status: hash[:status] || :completed, 59 | output: hash[:output], 60 | failure: failure 61 | ) 62 | end 63 | 64 | # Checks if the task execution was successful 65 | # @return [Boolean] True if successful, false otherwise 66 | def successful? 67 | @status == :completed 68 | end 69 | 70 | # Checks if the task execution failed 71 | # @return [Boolean] True if failed, false otherwise 72 | def failed? 73 | @status == :failed 74 | end 75 | 76 | # Checks if the task execution was canceled 77 | # @return [Boolean] True if canceled, false otherwise 78 | def canceled? 79 | @status == :canceled 80 | end 81 | 82 | # Returns a hash representation of the execution result 83 | # @return [Hash] The execution result as a hash 84 | def to_h 85 | { 86 | status: @status, 87 | output: @output, 88 | failure: @failure&.to_h 89 | } 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/agentic/execution_result.rb: -------------------------------------------------------------------------------- 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 92 | -------------------------------------------------------------------------------- /spec/integration/cli_plan_command_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "stringio" 5 | 6 | RSpec.describe "CLI plan command integration", :vcr do 7 | let(:output) { StringIO.new } 8 | 9 | # Capture stdout for testing CLI output 10 | around do |example| 11 | original_stdout = $stdout 12 | $stdout = output 13 | example.run 14 | $stdout = original_stdout 15 | end 16 | 17 | describe "agentic plan command" do 18 | # This is our main end-to-end test for the plan command 19 | it "creates a plan for building a Ruby coding agent" do 20 | # Stub check_api_token! to not raise an error 21 | allow_any_instance_of(Agentic::CLI).to receive(:check_api_token!).and_return(true) 22 | 23 | # Set up VCR to record the API call 24 | VCR.use_cassette("plan_ruby_coding_agent") do 25 | # Execute the plan command 26 | Agentic::CLI.start(["plan", "Create a Ruby coding agent"]) 27 | end 28 | 29 | # Verify output contains expected elements 30 | expect(output.string).to include("Creating plan for goal: Create a Ruby coding agent") 31 | 32 | # Verify that the plan contains tasks 33 | expect(output.string).to include("Tasks:") 34 | 35 | # Verify that agents are assigned to tasks 36 | expect(output.string).to include("Agent:") 37 | 38 | # Verify that expected answer format is present 39 | expect(output.string).to include("Expected Answer:") 40 | end 41 | 42 | it "saves the plan to a file when requested" do 43 | # Stub check_api_token! to not raise an error 44 | allow_any_instance_of(Agentic::CLI).to receive(:check_api_token!).and_return(true) 45 | 46 | # Create a temporary file for the test 47 | require "tempfile" 48 | temp_file = Tempfile.new(["test_plan", ".json"]) 49 | file_path = temp_file.path 50 | temp_file.close 51 | 52 | begin 53 | # Simply expect File.write to be called with the right path 54 | expect(File).to receive(:write).with(file_path, anything) 55 | 56 | # Set up VCR to record the API call 57 | VCR.use_cassette("plan_ruby_coding_agent") do 58 | # Execute the plan command with save option 59 | Agentic::CLI.start(["plan", "Create a Ruby coding agent", "--save=#{file_path}", "--output=json"]) 60 | end 61 | 62 | # Verify output indicates the plan was saved 63 | expect(output.string).to include("Plan saved to #{file_path}") 64 | ensure 65 | # Clean up temporary file 66 | File.unlink(file_path) if File.exist?(file_path) 67 | end 68 | end 69 | 70 | it "formats output as JSON when requested" do 71 | # Stub check_api_token! to not raise an error 72 | allow_any_instance_of(Agentic::CLI).to receive(:check_api_token!).and_return(true) 73 | 74 | # Set up VCR to record the API call 75 | VCR.use_cassette("plan_ruby_coding_agent") do 76 | # Execute the plan command with JSON output 77 | Agentic::CLI.start(["plan", "Create a Ruby coding agent", "--output=json"]) 78 | end 79 | 80 | # Verify output includes JSON markers 81 | expect(output.string).to include('"tasks":') 82 | expect(output.string).to include('"expected_answer":') 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/agentic/task_execution_result_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::TaskExecutionResult do 6 | let(:output) { {key: "value"} } 7 | let(:failure) { Agentic::TaskFailure.new(message: "Something went wrong", type: "ErrorType") } 8 | 9 | describe "#initialize" do 10 | it "initializes with status, output, and failure" do 11 | result = described_class.new(status: :completed, output: output, failure: nil) 12 | expect(result.status).to eq(:completed) 13 | expect(result.output).to eq(output) 14 | expect(result.failure).to be_nil 15 | end 16 | end 17 | 18 | describe ".success" do 19 | it "creates a successful result" do 20 | result = described_class.success(output) 21 | expect(result.status).to eq(:completed) 22 | expect(result.output).to eq(output) 23 | expect(result.failure).to be_nil 24 | expect(result.successful?).to be true 25 | expect(result.failed?).to be false 26 | expect(result.canceled?).to be false 27 | end 28 | end 29 | 30 | describe ".failure" do 31 | it "creates a failed result" do 32 | result = described_class.failure(failure) 33 | expect(result.status).to eq(:failed) 34 | expect(result.output).to be_nil 35 | expect(result.failure).to eq(failure) 36 | expect(result.successful?).to be false 37 | expect(result.failed?).to be true 38 | expect(result.canceled?).to be false 39 | end 40 | end 41 | 42 | describe ".canceled" do 43 | it "creates a canceled result" do 44 | result = described_class.canceled 45 | expect(result.status).to eq(:canceled) 46 | expect(result.output).to be_nil 47 | expect(result.failure).to be_nil 48 | expect(result.successful?).to be false 49 | expect(result.failed?).to be false 50 | expect(result.canceled?).to be true 51 | end 52 | end 53 | 54 | describe ".from_hash" do 55 | let(:hash) { {status: :completed, output: output, failure: nil} } 56 | let(:failure_hash) { {status: :failed, output: nil, failure: failure.to_h} } 57 | 58 | it "creates a result from a success hash" do 59 | result = described_class.from_hash(hash) 60 | expect(result.status).to eq(:completed) 61 | expect(result.output).to eq(output) 62 | expect(result.failure).to be_nil 63 | end 64 | 65 | it "creates a result from a failure hash" do 66 | result = described_class.from_hash(failure_hash) 67 | expect(result.status).to eq(:failed) 68 | expect(result.output).to be_nil 69 | expect(result.failure).to be_a(Agentic::TaskFailure) 70 | expect(result.failure.message).to eq("Something went wrong") 71 | expect(result.failure.type).to eq("ErrorType") 72 | end 73 | end 74 | 75 | describe "#to_h" do 76 | it "returns a hash for a successful result" do 77 | result = described_class.success(output) 78 | expect(result.to_h).to eq({ 79 | status: :completed, 80 | output: output, 81 | failure: nil 82 | }) 83 | end 84 | 85 | it "returns a hash for a failed result" do 86 | result = described_class.failure(failure) 87 | expect(result.to_h).to eq({ 88 | status: :failed, 89 | output: nil, 90 | failure: failure.to_h 91 | }) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/vcr_cassettes/gpt4o_mini_completion.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.openai.com/v1/chat/completions 6 | body: 7 | encoding: UTF-8 8 | string: '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"What is 9 | the capital of France?"}]}' 10 | headers: 11 | Content-Type: 12 | - application/json 13 | Authorization: 14 | - Bearer 15 | Accept-Encoding: 16 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 17 | Accept: 18 | - "*/*" 19 | User-Agent: 20 | - Ruby 21 | response: 22 | status: 23 | code: 200 24 | message: OK 25 | headers: 26 | Date: 27 | - Wed, 28 Aug 2024 14:45:19 GMT 28 | Content-Type: 29 | - application/json 30 | Transfer-Encoding: 31 | - chunked 32 | Connection: 33 | - keep-alive 34 | Access-Control-Expose-Headers: 35 | - X-Request-ID 36 | Openai-Organization: 37 | - user-dup9zcwkwpbrgftk7mms7ssr 38 | Openai-Processing-Ms: 39 | - '270' 40 | Openai-Version: 41 | - '2020-10-01' 42 | Strict-Transport-Security: 43 | - max-age=15552000; includeSubDomains; preload 44 | X-Ratelimit-Limit-Requests: 45 | - '5000' 46 | X-Ratelimit-Limit-Tokens: 47 | - '2000000' 48 | X-Ratelimit-Remaining-Requests: 49 | - '4999' 50 | X-Ratelimit-Remaining-Tokens: 51 | - '1999975' 52 | X-Ratelimit-Reset-Requests: 53 | - 12ms 54 | X-Ratelimit-Reset-Tokens: 55 | - 0s 56 | X-Request-Id: 57 | - req_a9f6403b1690f81a0c25b35547663f09 58 | Cf-Cache-Status: 59 | - DYNAMIC 60 | Set-Cookie: 61 | - __cf_bm=xc6W4jCDiyxFi87vsU0cDNggizEDFy.NPMOke5vHoyg-1724856319-1.0.1.1-jbH.nDEw6e6276oQGUJX536Us6EwoW3nguJbNSqIILqlMeChEupMmtinpkrylbGwompEZvmWBUjkf15qGEJScQ; 62 | path=/; expires=Wed, 28-Aug-24 15:15:19 GMT; domain=.api.openai.com; HttpOnly; 63 | Secure; SameSite=None 64 | - _cfuvid=gPM6dlMt_32uEkAIZuRAGQ39rP6_SMYIrFUSTRkeHhY-1724856319835-0.0.1.1-604800000; 65 | path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None 66 | X-Content-Type-Options: 67 | - nosniff 68 | Server: 69 | - cloudflare 70 | Cf-Ray: 71 | - 8ba5159a2d39181d-EWR 72 | Alt-Svc: 73 | - h3=":443"; ma=86400 74 | body: 75 | encoding: ASCII-8BIT 76 | string: | 77 | { 78 | "id": "chatcmpl-A1ED9Jo7KElxBvt1NKvXu3diSSUS9", 79 | "object": "chat.completion", 80 | "created": 1724856319, 81 | "model": "gpt-4o-mini-2024-07-18", 82 | "choices": [ 83 | { 84 | "index": 0, 85 | "message": { 86 | "role": "assistant", 87 | "content": "The capital of France is Paris.", 88 | "refusal": null 89 | }, 90 | "logprobs": null, 91 | "finish_reason": "stop" 92 | } 93 | ], 94 | "usage": { 95 | "prompt_tokens": 14, 96 | "completion_tokens": 7, 97 | "total_tokens": 21 98 | }, 99 | "system_fingerprint": "fp_f33667828e" 100 | } 101 | recorded_at: Wed, 28 Aug 2024 14:45:19 GMT 102 | recorded_with: VCR 6.3.1 103 | -------------------------------------------------------------------------------- /.architecture/decisions/adrs/ADR-014-agent-capability-registry.md: -------------------------------------------------------------------------------- 1 | # ADR-014: Agent Capability Registry 2 | 3 | ## Status 4 | 5 | Accepted 6 | 7 | ## Context 8 | 9 | As we develop the Agentic framework, we need a centralized system to manage, discover, and utilize agent capabilities. Capabilities are richer than simple tools, with versioning, dependencies, and composition possibilities. The system must support: 10 | 11 | 1. Registration of capabilities with semantic versioning 12 | 2. Discovery of capabilities based on name, version, or functionality 13 | 3. Composition of capabilities into higher-order capabilities 14 | 4. Runtime validation of capability inputs and outputs 15 | 5. Management of capability providers (implementations) 16 | 17 | Previously, we had a more limited "tool" concept which lacked versioning, composition, and formal interface definitions. We need to evolve this concept while maintaining backward compatibility. 18 | 19 | ## Decision 20 | 21 | We will implement an `AgentCapabilityRegistry` that serves as the central repository for all agent capabilities in the system. The registry will: 22 | 23 | 1. Be implemented as a singleton to provide global access 24 | 2. Maintain a collection of capability specifications and their providers 25 | 3. Support semantic versioning for capability evolution 26 | 4. Enable capability composition through dependency management 27 | 5. Provide a rich API for capability discovery and filtering 28 | 29 | Key classes: 30 | 31 | - `CapabilitySpecification`: Defines the interface for a capability 32 | - `CapabilityProvider`: Implements a capability interface 33 | - `AgentCapabilityRegistry`: Manages capability specifications and providers 34 | 35 | The registry will provide these core operations: 36 | 37 | ```ruby 38 | # Registration 39 | registry.register(specification, provider) 40 | 41 | # Discovery 42 | registry.get(name, version = nil) 43 | registry.list(filter = {}) 44 | 45 | # Provider access 46 | registry.get_provider(name, version = nil) 47 | 48 | # Composition 49 | registry.compose(name, description, version, dependencies, implementation) 50 | ``` 51 | 52 | ## Consequences 53 | 54 | ### Positive 55 | 56 | 1. Centralized management of capabilities improves discovery and reuse 57 | 2. Semantic versioning enables capability evolution while maintaining compatibility 58 | 3. Capability composition facilitates building complex capabilities from simpler ones 59 | 4. Formal interface definitions improve capability documentation and usage 60 | 5. Provider abstraction allows multiple implementations of the same capability 61 | 62 | ### Negative 63 | 64 | 1. Singleton pattern creates global state that may complicate testing 65 | 2. Centralized registry could become a performance bottleneck at scale 66 | 3. Version management adds complexity compared to simpler approaches 67 | 4. Requires migration from previous tool-based approach 68 | 69 | ### Neutral 70 | 71 | 1. Requires agents to adopt capability-aware execution model 72 | 2. Necessitates formal capability specification development 73 | 74 | ## Implementation Notes 75 | 76 | The registry will be implemented as a singleton class with thread-safe operations. Capability specifications will include name, version, description, inputs, outputs, and dependencies. Providers will encapsulate the actual implementation logic. 77 | 78 | The registry will maintain an index of capabilities by name and version, allowing efficient lookups. It will also support finding the latest version of a capability when no specific version is requested. 79 | 80 | For backward compatibility, existing tools will be automatically wrapped as capabilities with appropriate adapters. -------------------------------------------------------------------------------- /spec/agentic/llm_config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::LlmConfig do 6 | let(:default_config) { described_class.new } 7 | let(:custom_config) do 8 | described_class.new( 9 | model: "gpt-4", 10 | max_tokens: 500, 11 | temperature: 0.8, 12 | top_p: 0.9, 13 | frequency_penalty: 0.1, 14 | presence_penalty: 0.2, 15 | additional_options: {stop: ["END"]} 16 | ) 17 | end 18 | 19 | describe "#initialize" do 20 | it "sets default values" do 21 | expect(default_config.model).to eq("gpt-4o-2024-08-06") 22 | expect(default_config.max_tokens).to be_nil 23 | expect(default_config.temperature).to eq(0.7) 24 | expect(default_config.top_p).to eq(1.0) 25 | expect(default_config.frequency_penalty).to eq(0.0) 26 | expect(default_config.presence_penalty).to eq(0.0) 27 | expect(default_config.additional_options).to eq({}) 28 | end 29 | 30 | it "allows custom values" do 31 | expect(custom_config.model).to eq("gpt-4") 32 | expect(custom_config.max_tokens).to eq(500) 33 | expect(custom_config.temperature).to eq(0.8) 34 | expect(custom_config.top_p).to eq(0.9) 35 | expect(custom_config.frequency_penalty).to eq(0.1) 36 | expect(custom_config.presence_penalty).to eq(0.2) 37 | expect(custom_config.additional_options).to eq({stop: ["END"]}) 38 | end 39 | end 40 | 41 | describe "attr_accessors" do 42 | it "allows reading and writing all attributes" do 43 | config = described_class.new 44 | 45 | config.model = "new-model" 46 | expect(config.model).to eq("new-model") 47 | 48 | config.max_tokens = 100 49 | expect(config.max_tokens).to eq(100) 50 | 51 | config.temperature = 0.5 52 | expect(config.temperature).to eq(0.5) 53 | 54 | config.top_p = 0.8 55 | expect(config.top_p).to eq(0.8) 56 | 57 | config.frequency_penalty = 0.3 58 | expect(config.frequency_penalty).to eq(0.3) 59 | 60 | config.presence_penalty = 0.4 61 | expect(config.presence_penalty).to eq(0.4) 62 | 63 | config.additional_options = {logit_bias: {123 => -100}} 64 | expect(config.additional_options).to eq({logit_bias: {123 => -100}}) 65 | end 66 | end 67 | 68 | describe "#to_api_parameters" do 69 | it "returns a hash with all parameters for API calls" do 70 | base_params = {messages: [{role: "user", content: "Hello"}]} 71 | params = custom_config.to_api_parameters(base_params) 72 | 73 | expect(params).to include( 74 | messages: [{role: "user", content: "Hello"}], 75 | model: "gpt-4", 76 | max_tokens: 500, 77 | temperature: 0.8, 78 | top_p: 0.9, 79 | frequency_penalty: 0.1, 80 | presence_penalty: 0.2, 81 | stop: ["END"] 82 | ) 83 | end 84 | 85 | it "omits max_tokens if not specified" do 86 | params = default_config.to_api_parameters({}) 87 | expect(params).not_to include(:max_tokens) 88 | end 89 | 90 | it "merges with base parameters" do 91 | base_params = {stream: true} 92 | params = default_config.to_api_parameters(base_params) 93 | expect(params[:stream]).to be true 94 | expect(params[:model]).to eq("gpt-4o-2024-08-06") 95 | end 96 | 97 | it "allows additional options to override default parameters" do 98 | config = described_class.new( 99 | model: "gpt-4", 100 | additional_options: {model: "custom-model"} 101 | ) 102 | 103 | params = config.to_api_parameters({}) 104 | expect(params[:model]).to eq("custom-model") 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/agentic/retry_handler_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::RetryHandler do 6 | describe "#initialize" do 7 | it "sets default values" do 8 | handler = described_class.new 9 | expect(handler.max_retries).to eq(3) 10 | expect(handler.backoff_strategy).to eq(:exponential) 11 | expect(handler.retryable_errors).to include(Agentic::Errors::LlmTimeoutError) 12 | end 13 | 14 | it "accepts custom values" do 15 | handler = described_class.new( 16 | max_retries: 5, 17 | backoff_strategy: :linear, 18 | retryable_errors: [RuntimeError] 19 | ) 20 | expect(handler.max_retries).to eq(5) 21 | expect(handler.backoff_strategy).to eq(:linear) 22 | expect(handler.retryable_errors).to eq([RuntimeError]) 23 | end 24 | end 25 | 26 | describe "#with_retry" do 27 | let(:handler) { described_class.new(backoff_options: {base_delay: 0.001}) } 28 | 29 | it "executes the block successfully" do 30 | result = handler.with_retry { 42 } 31 | expect(result).to eq(42) 32 | end 33 | 34 | it "retries when a retryable error occurs" do 35 | attempt = 0 36 | 37 | result = handler.with_retry do 38 | attempt += 1 39 | if attempt == 1 40 | raise Agentic::Errors::LlmTimeoutError.new("Timeout") 41 | else 42 | "success" 43 | end 44 | end 45 | 46 | expect(result).to eq("success") 47 | expect(attempt).to eq(2) 48 | end 49 | 50 | it "gives up after max retries" do 51 | handler = described_class.new(max_retries: 2, backoff_options: {base_delay: 0.001}) 52 | attempt = 0 53 | 54 | expect do 55 | handler.with_retry do 56 | attempt += 1 57 | raise Agentic::Errors::LlmTimeoutError.new("Timeout") 58 | end 59 | end.to raise_error(Agentic::Errors::LlmTimeoutError) 60 | 61 | expect(attempt).to eq(3) # Initial + 2 retries 62 | end 63 | end 64 | 65 | describe "backoff strategies" do 66 | let(:error) { Agentic::Errors::LlmTimeoutError.new("Timeout") } 67 | 68 | it "calculates constant backoff" do 69 | handler = described_class.new( 70 | backoff_strategy: :constant, 71 | backoff_options: {base_delay: 1.0, jitter_factor: 0} 72 | ) 73 | 74 | delay = handler.send(:calculate_backoff_delay, 1) 75 | expect(delay).to eq(1.0) 76 | 77 | delay = handler.send(:calculate_backoff_delay, 3) 78 | expect(delay).to eq(1.0) 79 | end 80 | 81 | it "calculates linear backoff" do 82 | handler = described_class.new( 83 | backoff_strategy: :linear, 84 | backoff_options: {base_delay: 1.0, jitter_factor: 0} 85 | ) 86 | 87 | delay = handler.send(:calculate_backoff_delay, 1) 88 | expect(delay).to eq(1.0) 89 | 90 | delay = handler.send(:calculate_backoff_delay, 3) 91 | expect(delay).to eq(3.0) 92 | end 93 | 94 | it "calculates exponential backoff" do 95 | handler = described_class.new( 96 | backoff_strategy: :exponential, 97 | backoff_options: {base_delay: 1.0, jitter_factor: 0} 98 | ) 99 | 100 | delay = handler.send(:calculate_backoff_delay, 1) 101 | expect(delay).to eq(1.0) 102 | 103 | delay = handler.send(:calculate_backoff_delay, 3) 104 | expect(delay).to eq(4.0) # 1.0 * 2^(3-1) 105 | end 106 | 107 | it "applies jitter" do 108 | handler = described_class.new( 109 | backoff_strategy: :constant, 110 | backoff_options: {base_delay: 1.0, jitter_factor: 0.5} 111 | ) 112 | 113 | # Run multiple times to ensure jitter is working 114 | delays = 10.times.map { handler.send(:calculate_backoff_delay, 1) } 115 | 116 | # All delays should be between 0.5 and 1.5 117 | expect(delays.min).to be >= 0.5 118 | expect(delays.max).to be <= 1.5 119 | 120 | # There should be some variation 121 | expect(delays.uniq.size).to be > 1 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /.architecture/decisions/adrs/ADR-015-persistent-agent-store.md: -------------------------------------------------------------------------------- 1 | # ADR-015: Persistent Agent Store 2 | 3 | ## Status 4 | 5 | Accepted 6 | 7 | ## Context 8 | 9 | To enable more efficient agent reuse and evolution, we need a mechanism to persistently store and retrieve agent configurations. This system should support: 10 | 11 | 1. Storing complete agent configurations, including capabilities 12 | 2. Retrieving agents by ID or name 13 | 3. Versioning agent configurations as they evolve 14 | 4. Filtering and searching for suitable agents based on capabilities or metadata 15 | 5. Tracking agent provenance and usage history 16 | 17 | Without a persistent store, agents must be recreated from scratch for each task, even when similar agents have been previously assembled. This leads to inefficiency and missed opportunities for improvement through agent evolution. 18 | 19 | ## Decision 20 | 21 | We will implement a `PersistentAgentStore` class that handles agent storage and retrieval. The store will: 22 | 23 | 1. Use a file-based storage system with a configurable storage path 24 | 2. Support semantic versioning for agent configurations 25 | 3. Maintain an in-memory index for efficient lookup 26 | 4. Store agent configurations as structured JSON 27 | 5. Support rich filtering and querying capabilities 28 | 29 | Key operations: 30 | 31 | ```ruby 32 | # Store an agent 33 | store.store(agent, name: "my_agent", metadata: { category: "research" }) 34 | 35 | # Build an agent from stored configuration 36 | store.build_agent("my_agent") # By name 37 | store.build_agent(agent_id) # By ID 38 | store.build_agent(id, version: "1.2.0") # Specific version 39 | 40 | # List and filter agents 41 | store.list_all() # All agents 42 | store.all(filter: { capability: "web_search" }) # With specific capability 43 | store.all(filter: { metadata: { category: "research" } }) # With metadata 44 | 45 | # Get version history 46 | store.version_history(agent_id) 47 | 48 | # Delete agents 49 | store.delete(agent_id) 50 | store.delete(agent_id, version: "1.0.0") # Specific version 51 | ``` 52 | 53 | We will use the following design: 54 | - File-based storage with one directory per agent 55 | - Each version stored as a separate JSON file 56 | - An index file for quick lookup without loading all agent files 57 | - Agent data will include capabilities, metadata, and configuration 58 | 59 | ## Consequences 60 | 61 | ### Positive 62 | 63 | 1. Enables reuse of previously assembled agents for similar tasks 64 | 2. Supports agent evolution through versioning 65 | 3. Provides rich filtering for agent discovery 66 | 4. Maintains complete agent provenance for auditing 67 | 5. Simplifies integration with agent assembly system 68 | 69 | ### Negative 70 | 71 | 1. File-based storage has scalability limitations 72 | 2. Limited transactional support compared to databases 73 | 3. Requires additional synchronization in multi-process scenarios 74 | 4. Increases code complexity with versioning logic 75 | 76 | ### Neutral 77 | 78 | 1. Requires filesystem access for storage 79 | 2. Creates dependency on JSON serialization format 80 | 3. May require migration strategies for major version changes 81 | 82 | ## Implementation Notes 83 | 84 | The store will create a directory structure like: 85 | 86 | ``` 87 | storage_path/ 88 | index.json # Main index with all agents 89 | agent_id_1/ 90 | 1.0.0.json # First version 91 | 1.0.1.json # Second version 92 | agent_id_2/ 93 | 1.0.0.json # First version 94 | ``` 95 | 96 | The index will contain minimal information for quick lookups, while the full agent configuration will be stored in the version-specific files. The store will automatically rebuild the index if it becomes out of sync with the filesystem. 97 | 98 | For thread safety, file operations will use appropriate locking mechanisms. The store will gracefully handle partial or corrupted data through validation and error recovery. 99 | 100 | To support multiple storage backends in the future, the implementation will follow a repository pattern with a clear interface for storage operations. -------------------------------------------------------------------------------- /.architecture/templates/version_comparison.md: -------------------------------------------------------------------------------- 1 | # Architectural Comparison: Version X.Y.Z to A.B.C 2 | 3 | ## Overview 4 | 5 | This document provides a comprehensive comparison of the architectural changes between version X.Y.Z and version A.B.C of the Agentic gem. It highlights key changes, their impact, and provides guidance for adapting existing implementations. 6 | 7 | ## Summary of Changes 8 | 9 | ### Major Architectural Changes 10 | 11 | * [Change 1: Brief description of a significant architectural change] 12 | * [Change 2: ...] 13 | * [...] 14 | 15 | ### New Components 16 | 17 | | Component | Purpose | Key Features | Related ADRs | 18 | |-----------|---------|--------------|-------------| 19 | | [Component Name] | [Brief description] | [List key features] | [ADR-XXX] | 20 | 21 | ### Modified Components 22 | 23 | | Component | Type of Change | Before | After | Rationale | 24 | |-----------|----------------|--------|-------|-----------| 25 | | [Component Name] | [Interface/Implementation/Behavior] | [Previous state] | [New state] | [Why changed] | 26 | 27 | ### Removed Components 28 | 29 | | Component | Replacement (if any) | Migration Path | Rationale | 30 | |-----------|----------------------|----------------|-----------| 31 | | [Component Name] | [Replacement component] | [How to migrate] | [Why removed] | 32 | 33 | ## Architectural Diagrams 34 | 35 | ### Before (Version X.Y.Z) 36 | 37 | [Include before diagram or link to it] 38 | 39 | ### After (Version A.B.C) 40 | 41 | [Include after diagram or link to it] 42 | 43 | ## Impact Analysis 44 | 45 | ### Developer Experience 46 | 47 | * [Impact 1: How the changes affect developers using the gem] 48 | * [Impact 2: ...] 49 | * [...] 50 | 51 | **Migration Effort:** [Low/Medium/High] 52 | 53 | ### Performance Characteristics 54 | 55 | | Aspect | Before | After | Change Impact | 56 | |--------|--------|-------|---------------| 57 | | [Performance metric] | [Previous value] | [New value] | [Better/Worse/Neutral] | 58 | 59 | ### Security Posture 60 | 61 | | Aspect | Before | After | Change Impact | 62 | |--------|--------|-------|---------------| 63 | | [Security aspect] | [Previous state] | [New state] | [Better/Worse/Neutral] | 64 | 65 | ### Maintainability Metrics 66 | 67 | | Metric | Before | After | Change Impact | 68 | |--------|--------|-------|---------------| 69 | | [Maintainability metric] | [Previous value] | [New value] | [Better/Worse/Neutral] | 70 | 71 | ### Observability Capabilities 72 | 73 | | Capability | Before | After | Change Impact | 74 | |------------|--------|-------|---------------| 75 | | [Observability feature] | [Previous state] | [New state] | [Better/Worse/Neutral] | 76 | 77 | ## Implementation Details 78 | 79 | ### Review Recommendations Addressed 80 | 81 | | Review ID | Recommendation | Implementation Approach | Deviation (if any) | 82 | |-----------|----------------|-------------------------|--------------------| 83 | | [ID from review] | [Original recommendation] | [How it was implemented] | [How implementation differed from recommendation] | 84 | 85 | ### Review Recommendations Deferred 86 | 87 | | Review ID | Recommendation | Rationale for Deferral | Planned Version | 88 | |-----------|----------------|------------------------|-----------------| 89 | | [ID from review] | [Original recommendation] | [Why deferred] | [Future version] | 90 | 91 | ## Migration Guide 92 | 93 | ### Breaking Changes 94 | 95 | * [Breaking change 1: Description and mitigation] 96 | * [Breaking change 2: ...] 97 | * [...] 98 | 99 | ### Upgrade Steps 100 | 101 | 1. [Step 1: What to do first when upgrading] 102 | 2. [Step 2: ...] 103 | 3. [...] 104 | 105 | ### Code Examples 106 | 107 | #### Before (Version X.Y.Z) 108 | 109 | ```ruby 110 | # Example code showing usage in previous version 111 | ``` 112 | 113 | #### After (Version A.B.C) 114 | 115 | ```ruby 116 | # Example code showing equivalent usage in new version 117 | ``` 118 | 119 | ## References 120 | 121 | * [Architectural Review for X.Y.Z](link-to-review) 122 | * [Recalibration Plan](link-to-plan) 123 | * [ADR-XXX: Title](link-to-adr) 124 | * [ADR-YYY: Title](link-to-adr) -------------------------------------------------------------------------------- /.architecture/templates/progress_tracking.md: -------------------------------------------------------------------------------- 1 | # Architectural Changes Progress Tracking 2 | 3 | ## Overview 4 | 5 | This document tracks the implementation progress of architectural changes identified in the recalibration plan for version X.Y.Z. It is updated regularly to reflect current status and any adjustments to the implementation approach. 6 | 7 | **Last Updated**: [DATE] 8 | 9 | ## Executive Summary 10 | 11 | | Category | Total Items | Completed | In Progress | Not Started | Deferred | 12 | |----------|-------------|-----------|-------------|-------------|----------| 13 | | Architectural Changes | [Number] | [Number] | [Number] | [Number] | [Number] | 14 | | Implementation Improvements | [Number] | [Number] | [Number] | [Number] | [Number] | 15 | | Documentation Enhancements | [Number] | [Number] | [Number] | [Number] | [Number] | 16 | | Process Adjustments | [Number] | [Number] | [Number] | [Number] | [Number] | 17 | | **TOTAL** | [Number] | [Number] | [Number] | [Number] | [Number] | 18 | 19 | **Completion Percentage**: [X%] 20 | 21 | ## Detailed Status 22 | 23 | ### Architectural Changes 24 | 25 | | ID | Recommendation | Priority | Status | Target Version | Actual Version | Notes | 26 | |----|---------------|----------|--------|----------------|----------------|-------| 27 | | A1 | [Brief description] | [Priority] | [Not Started/In Progress/Completed/Deferred] | [Version] | [Actual version or N/A] | [Current status notes] | 28 | 29 | ### Implementation Improvements 30 | 31 | | ID | Recommendation | Priority | Status | Target Version | Actual Version | Notes | 32 | |----|---------------|----------|--------|----------------|----------------|-------| 33 | | I1 | [Brief description] | [Priority] | [Not Started/In Progress/Completed/Deferred] | [Version] | [Actual version or N/A] | [Current status notes] | 34 | 35 | ### Documentation Enhancements 36 | 37 | | ID | Recommendation | Priority | Status | Target Version | Actual Version | Notes | 38 | |----|---------------|----------|--------|----------------|----------------|-------| 39 | | D1 | [Brief description] | [Priority] | [Not Started/In Progress/Completed/Deferred] | [Version] | [Actual version or N/A] | [Current status notes] | 40 | 41 | ### Process Adjustments 42 | 43 | | ID | Recommendation | Priority | Status | Target Version | Actual Version | Notes | 44 | |----|---------------|----------|--------|----------------|----------------|-------| 45 | | P1 | [Brief description] | [Priority] | [Not Started/In Progress/Completed/Deferred] | [Version] | [Actual version or N/A] | [Current status notes] | 46 | 47 | ## Implementation Adjustments 48 | 49 | This section documents any adjustments made to the implementation approach since the original recalibration plan. 50 | 51 | | ID | Original Approach | Adjusted Approach | Rationale | Impact | 52 | |----|-------------------|-------------------|-----------|--------| 53 | | [ID] | [Description] | [Description] | [Reason for change] | [Impact of change] | 54 | 55 | ## Milestone Progress 56 | 57 | | Milestone | Target Date | Status | Actual/Projected Completion | Notes | 58 | |-----------|-------------|--------|---------------------------|-------| 59 | | [Milestone] | [Date] | [Not Started/In Progress/Completed/Delayed] | [Date] | [Notes] | 60 | 61 | ## Blocked Items 62 | 63 | | ID | Blocker Description | Impact | Owner | Resolution Plan | Projected Resolution Date | 64 | |----|---------------------|--------|-------|-----------------|---------------------------| 65 | | [ID] | [Description] | [Impact] | [Owner] | [Plan] | [Date] | 66 | 67 | ## Recently Completed Items 68 | 69 | | ID | Description | Completion Date | Implemented In | Implementation Notes | 70 | |----|-------------|-----------------|----------------|----------------------| 71 | | [ID] | [Description] | [Date] | [Version] | [Notes] | 72 | 73 | ## Next Check-in 74 | 75 | The next progress check-in meeting is scheduled for [DATE]. 76 | 77 | ## Appendices 78 | 79 | ### A. Test Coverage Report 80 | 81 | [Summary of test coverage for implemented architectural changes] 82 | 83 | ### B. Documentation Status 84 | 85 | [Summary of documentation updates status] 86 | 87 | ### C. Quality Metrics 88 | 89 | [Key quality metrics for implemented architectural changes] -------------------------------------------------------------------------------- /spec/agentic/observable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | class ObservableTest 6 | include Agentic::Observable 7 | 8 | attr_reader :value 9 | 10 | def initialize 11 | @value = 0 12 | end 13 | 14 | def increment 15 | old_value = @value 16 | @value += 1 17 | notify_observers(:value_changed, old_value, @value) 18 | end 19 | end 20 | 21 | class TestObserver 22 | attr_reader :events 23 | 24 | def initialize 25 | @events = [] 26 | end 27 | 28 | def update(event_type, observable, *args) 29 | @events << { 30 | type: event_type, 31 | observable: observable, 32 | args: args 33 | } 34 | end 35 | end 36 | 37 | RSpec.describe Agentic::Observable do 38 | let(:observable) { ObservableTest.new } 39 | let(:observer) { TestObserver.new } 40 | 41 | describe "#add_observer" do 42 | it "adds an observer" do 43 | observable.add_observer(observer) 44 | expect(observable.count_observers).to eq(1) 45 | end 46 | 47 | it "doesn't add the same observer twice" do 48 | observable.add_observer(observer) 49 | observable.add_observer(observer) 50 | expect(observable.count_observers).to eq(1) 51 | end 52 | end 53 | 54 | describe "#delete_observer" do 55 | it "removes an observer" do 56 | observable.add_observer(observer) 57 | observable.delete_observer(observer) 58 | expect(observable.count_observers).to eq(0) 59 | end 60 | 61 | it "doesn't raise an error when observer is not present" do 62 | expect { observable.delete_observer(observer) }.not_to raise_error 63 | end 64 | end 65 | 66 | describe "#delete_observers" do 67 | it "removes all observers" do 68 | observable.add_observer(observer) 69 | observable.add_observer(TestObserver.new) 70 | observable.delete_observers 71 | expect(observable.count_observers).to eq(0) 72 | end 73 | end 74 | 75 | describe "#notify_observers" do 76 | before do 77 | observable.add_observer(observer) 78 | end 79 | 80 | it "notifies observers with event and arguments" do 81 | observable.increment 82 | 83 | expect(observer.events.size).to eq(1) 84 | expect(observer.events.first[:type]).to eq(:value_changed) 85 | expect(observer.events.first[:observable]).to eq(observable) 86 | expect(observer.events.first[:args]).to eq([0, 1]) 87 | end 88 | 89 | it "handles multiple observers" do 90 | second_observer = TestObserver.new 91 | observable.add_observer(second_observer) 92 | 93 | observable.increment 94 | 95 | expect(observer.events.size).to eq(1) 96 | expect(second_observer.events.size).to eq(1) 97 | end 98 | 99 | it "doesn't call observers that don't implement update" do 100 | non_conforming_observer = Object.new 101 | observable.add_observer(non_conforming_observer) 102 | 103 | expect { observable.increment }.not_to raise_error 104 | end 105 | 106 | it "is thread-safe when observers are added/removed during notification" do 107 | # Observer that removes itself during notification 108 | self_removing_observer = Class.new do 109 | def initialize(observable) 110 | @observable = observable 111 | end 112 | 113 | def update(*) 114 | @observable.delete_observer(self) 115 | end 116 | end.new(observable) 117 | 118 | # Observer that adds a new observer during notification 119 | observer_adding_observer = Class.new do 120 | def initialize(observable, new_observer) 121 | @observable = observable 122 | @new_observer = new_observer 123 | end 124 | 125 | def update(*) 126 | @observable.add_observer(@new_observer) 127 | end 128 | end.new(observable, TestObserver.new) 129 | 130 | observable.add_observer(self_removing_observer) 131 | observable.add_observer(observer_adding_observer) 132 | 133 | # This should not raise errors even though observers are modified during notification 134 | expect { observable.increment }.not_to raise_error 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/agentic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "zeitwerk" 4 | loader = Zeitwerk::Loader.for_gem 5 | 6 | # Configure Zeitwerk to handle the CLI class name properly 7 | loader.inflector.inflect( 8 | "cli" => "CLI" 9 | ) 10 | 11 | # Configure paths that need to be eager loaded or excluded from Zeitwerk 12 | loader.do_not_eager_load("#{__dir__}/agentic/cli") 13 | 14 | loader.setup 15 | 16 | # Explicitly require Thor-related components to avoid Zeitwerk issues with Thor 17 | # Thor requires subcommands to be loaded before they're referenced 18 | require_relative "agentic/ui" 19 | require_relative "agentic/default_agent_provider" 20 | require_relative "agentic/cli" 21 | require_relative "agentic/cli/execution_observer" 22 | require_relative "agentic/extension" 23 | require_relative "agentic/capabilities" 24 | require_relative "agentic/agent_assembly_engine" 25 | require_relative "agentic/llm_assisted_composition_strategy" 26 | require_relative "agentic/task_output_schemas" 27 | 28 | module Agentic 29 | class Error < StandardError; end 30 | 31 | class << self 32 | attr_accessor :logger 33 | end 34 | 35 | self.logger ||= Logger.new($stdout, level: :debug) 36 | 37 | class Configuration 38 | attr_accessor :access_token, :agent_store_path, :api_base_url 39 | 40 | def initialize 41 | @access_token = ENV["OPENAI_ACCESS_TOKEN"] || ENV["AGENTIC_API_TOKEN"] || "ollama" 42 | @agent_store_path = ENV["AGENTIC_AGENT_STORE_PATH"] || File.join(Dir.home, ".agentic", "agents") 43 | @api_base_url = ENV["AGENTIC_API_BASE_URL"] || ENV["OPENAI_BASE_URL"] 44 | end 45 | end 46 | 47 | class << self 48 | attr_writer :configuration 49 | attr_reader :agent_capability_registry, :agent_assembly_engine, :agent_store 50 | end 51 | 52 | def self.configuration 53 | @configuration ||= Configuration.new 54 | end 55 | 56 | def self.configure 57 | yield(configuration) 58 | end 59 | 60 | def self.client(config) 61 | LlmClient.new(config) 62 | end 63 | 64 | # Initialize the core agent self-assembly components 65 | def self.initialize_agent_assembly 66 | # Create registry, store, and assembly engine if not already initialized 67 | unless @agent_capability_registry 68 | @agent_capability_registry = AgentCapabilityRegistry.instance 69 | @agent_store = PersistentAgentStore.new(configuration.agent_store_path, @agent_capability_registry) 70 | @agent_assembly_engine = AgentAssemblyEngine.new(@agent_capability_registry, @agent_store) 71 | 72 | # Register standard capabilities 73 | Capabilities.register_standard_capabilities 74 | 75 | logger.info("Initialized agent assembly system") 76 | end 77 | end 78 | 79 | # Register a capability with the system 80 | # @param capability [CapabilitySpecification] The capability to register 81 | # @param provider [CapabilityProvider] The provider for the capability 82 | # @return [CapabilitySpecification] The registered capability 83 | def self.register_capability(capability, provider) 84 | initialize_agent_assembly 85 | @agent_capability_registry.register(capability, provider) 86 | end 87 | 88 | # Assemble an agent for a task 89 | # @param task [Task] The task to assemble an agent for 90 | # @param strategy [AgentCompositionStrategy, nil] The strategy to use 91 | # @param store [Boolean] Whether to use the agent store 92 | # @param use_llm [Boolean] Whether to use LLM-assisted strategy if no strategy provided 93 | # @return [Agent] The assembled agent 94 | def self.assemble_agent(task, strategy: nil, store: true, use_llm: false) 95 | initialize_agent_assembly 96 | 97 | # Use LLM-assisted strategy if requested and no strategy provided 98 | if use_llm && strategy.nil? 99 | strategy = LlmAssistedCompositionStrategy.new 100 | end 101 | 102 | @agent_assembly_engine.assemble_agent(task, strategy: strategy, store: store) 103 | end 104 | 105 | # Create an LLM-assisted composition strategy 106 | # @param llm_config [LlmConfig, nil] The LLM config to use 107 | # @return [LlmAssistedCompositionStrategy] The strategy 108 | def self.llm_assisted_strategy(llm_config = nil) 109 | LlmAssistedCompositionStrategy.new(llm_config) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/agentic/extension/domain_adapter.rb: -------------------------------------------------------------------------------- 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 110 | -------------------------------------------------------------------------------- /lib/agentic/cli/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "yaml" 4 | require "fileutils" 5 | 6 | module Agentic 7 | class CLI < Thor 8 | # CLI commands for managing configuration 9 | class Config < Thor 10 | CONFIG_FILE_NAME = ".agentic.yml" 11 | USER_CONFIG_PATH = File.join(Dir.home, CONFIG_FILE_NAME) 12 | PROJECT_CONFIG_PATH = File.join(Dir.pwd, CONFIG_FILE_NAME) 13 | 14 | desc "list", "List configuration settings" 15 | def list 16 | user_config = load_config(USER_CONFIG_PATH) 17 | project_config = load_config(PROJECT_CONFIG_PATH) 18 | 19 | puts "User configuration (#{USER_CONFIG_PATH}):" 20 | print_config(user_config) 21 | 22 | puts "\nProject configuration (#{PROJECT_CONFIG_PATH}):" 23 | print_config(project_config) 24 | 25 | puts "\nActive configuration:" 26 | print_config(active_config) 27 | 28 | puts "\nEnvironment variables:" 29 | puts " OPENAI_ACCESS_TOKEN: #{ENV["OPENAI_ACCESS_TOKEN"] ? "[SET]" : "[NOT SET]"}" 30 | end 31 | 32 | desc "get KEY", "Get a configuration setting" 33 | def get(key) 34 | config = active_config 35 | value = config[key] 36 | 37 | if value 38 | puts value 39 | else 40 | puts "Key '#{key}' not found in configuration" 41 | exit 1 42 | end 43 | end 44 | 45 | desc "set KEY=VALUE", "Set a configuration setting" 46 | option :global, type: :boolean, aliases: "-g", 47 | desc: "Set in global user config instead of project config" 48 | def set(key_value) 49 | key, value = key_value.split("=", 2) 50 | 51 | unless value 52 | puts "Error: Invalid format. Use KEY=VALUE" 53 | exit 1 54 | end 55 | 56 | path = options[:global] ? USER_CONFIG_PATH : PROJECT_CONFIG_PATH 57 | config = load_config(path) || {} 58 | 59 | # Convert string values to appropriate types 60 | value = case value.downcase 61 | when "true" then true 62 | when "false" then false 63 | when /^\d+$/ then value.to_i 64 | when /^\d+\.\d+$/ then value.to_f 65 | else value 66 | end 67 | 68 | config[key] = value 69 | save_config(path, config) 70 | 71 | puts "Configuration updated successfully." 72 | end 73 | 74 | desc "init", "Initialize configuration" 75 | option :global, type: :boolean, aliases: "-g", 76 | desc: "Initialize global user config instead of project config" 77 | def init 78 | path = options[:global] ? USER_CONFIG_PATH : PROJECT_CONFIG_PATH 79 | 80 | if File.exist?(path) 81 | puts "Configuration already exists at #{path}" 82 | return 83 | end 84 | 85 | config = { 86 | "model" => "gpt-4o-mini" 87 | # Add other default configuration options here 88 | } 89 | 90 | save_config(path, config) 91 | puts "Configuration initialized at #{path}" 92 | end 93 | 94 | private 95 | 96 | def load_config(path) 97 | return unless File.exist?(path) 98 | 99 | begin 100 | YAML.load_file(path) 101 | rescue => e 102 | puts "Error loading configuration from #{path}: #{e.message}" 103 | nil 104 | end 105 | end 106 | 107 | def save_config(path, config) 108 | # Create directory if it doesn't exist 109 | dir = File.dirname(path) 110 | FileUtils.mkdir_p(dir) unless File.directory?(dir) 111 | 112 | File.write(path, YAML.dump(config)) 113 | end 114 | 115 | def active_config 116 | # Combine user and project configs, with project taking precedence 117 | user_config = load_config(USER_CONFIG_PATH) || {} 118 | project_config = load_config(PROJECT_CONFIG_PATH) || {} 119 | 120 | user_config.merge(project_config) 121 | end 122 | 123 | def print_config(config) 124 | if config.nil? || config.empty? 125 | puts " [empty]" 126 | else 127 | config.each do |key, value| 128 | puts " #{key}: #{value}" 129 | end 130 | end 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /spec/agentic/extension/domain_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Agentic::Extension::DomainAdapter do 4 | let(:domain) { "healthcare" } 5 | let(:adapter) { described_class.new(domain) } 6 | 7 | describe "#initialize" do 8 | it "initializes with the specified domain" do 9 | expect(adapter.domain).to eq(domain) 10 | end 11 | 12 | it "initializes default adapters" do 13 | expect(adapter.supports?(:prompt)).to be true 14 | expect(adapter.supports?(:task)).to be true 15 | expect(adapter.supports?(:verification)).to be true 16 | end 17 | 18 | it "accepts custom configuration" do 19 | domain_config = {terminology: "medical"} 20 | custom_adapter = described_class.new(domain, domain_config: domain_config) 21 | 22 | expect(custom_adapter.configuration).to eq(domain_config) 23 | end 24 | end 25 | 26 | describe "#register_adapter" do 27 | it "registers a valid adapter" do 28 | custom_adapter = ->(target, context) { "#{target} adapted for #{context[:domain]}" } 29 | result = adapter.register_adapter(:custom, custom_adapter) 30 | 31 | expect(result).to be true 32 | expect(adapter.supports?(:custom)).to be true 33 | end 34 | 35 | it "rejects non-callable adapters" do 36 | result = adapter.register_adapter(:invalid, "not a callable") 37 | expect(result).to be false 38 | end 39 | end 40 | 41 | describe "#add_knowledge and #get_knowledge" do 42 | it "stores and retrieves domain knowledge" do 43 | knowledge = {terms: ["patient", "diagnosis", "treatment"]} 44 | adapter.add_knowledge(:terminology, knowledge) 45 | 46 | result = adapter.get_knowledge(:terminology) 47 | expect(result).to eq(knowledge) 48 | end 49 | 50 | it "returns nil for non-existent knowledge" do 51 | expect(adapter.get_knowledge(:non_existent)).to be_nil 52 | end 53 | end 54 | 55 | describe "#adapt" do 56 | context "with default adapters" do 57 | it "returns the input unchanged" do 58 | input = "This is a medical prompt" 59 | output = adapter.adapt(:prompt, input) 60 | 61 | expect(output).to eq(input) 62 | end 63 | end 64 | 65 | context "with custom adapters" do 66 | let(:custom_prompt_adapter) do 67 | ->(prompt, context) { "#{prompt} [Adapted for #{context[:domain]}]" } 68 | end 69 | 70 | before do 71 | adapter.register_adapter(:prompt, custom_prompt_adapter) 72 | end 73 | 74 | it "applies the adaptation to the input" do 75 | input = "Describe the symptoms" 76 | output = adapter.adapt(:prompt, input) 77 | 78 | expect(output).to eq("Describe the symptoms [Adapted for healthcare]") 79 | end 80 | 81 | it "passes domain knowledge to the adapter" do 82 | adapter.add_knowledge(:terminology, {specialty: "cardiology"}) 83 | 84 | custom_adapter_with_knowledge = lambda do |prompt, context| 85 | specialty = context[:domain_knowledge][:terminology][:specialty] 86 | "#{prompt} [Adapted for #{context[:domain]} #{specialty}]" 87 | end 88 | 89 | adapter.register_adapter(:prompt, custom_adapter_with_knowledge) 90 | 91 | input = "Describe the symptoms" 92 | output = adapter.adapt(:prompt, input) 93 | 94 | expect(output).to eq("Describe the symptoms [Adapted for healthcare cardiology]") 95 | end 96 | end 97 | 98 | context "with error-raising adapters" do 99 | let(:error_adapter) do 100 | ->(_, _) { raise "Adaptation error" } 101 | end 102 | 103 | before do 104 | adapter.register_adapter(:prompt, error_adapter) 105 | end 106 | 107 | it "returns the original input on error" do 108 | input = "Original prompt" 109 | output = adapter.adapt(:prompt, input) 110 | 111 | expect(output).to eq(input) 112 | end 113 | end 114 | end 115 | 116 | describe "#supports?" do 117 | it "returns true for supported components" do 118 | expect(adapter.supports?(:prompt)).to be true 119 | end 120 | 121 | it "returns false for unsupported components" do 122 | expect(adapter.supports?(:non_existent)).to be false 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/agentic/structured_outputs.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "dry/schema" 3 | require "openai" 4 | require "ostruct" 5 | 6 | # Credit: https://github.com/alexrudall/ruby-openai/issues/508#issuecomment-2291916913 7 | module Agentic 8 | module StructuredOutputs 9 | # Schema class for defining JSON schemas 10 | class Schema 11 | MAX_OBJECT_PROPERTIES = 100 12 | MAX_NESTING_DEPTH = 5 13 | 14 | def initialize(name = nil, &block) 15 | # Use the provided name or derive from class name 16 | @name = name || self.class.name.split("::").last.downcase 17 | # Initialize the base schema structure 18 | @schema = { 19 | type: "object", 20 | properties: {}, 21 | required: [], 22 | additionalProperties: false, 23 | strict: true 24 | } 25 | @definitions = {} 26 | # Execute the provided block to define the schema 27 | yield(self) if block 28 | validate_schema 29 | end 30 | 31 | # Convert the schema to a hash format 32 | def to_hash 33 | { 34 | name: @name, 35 | description: "Schema for the structured response", 36 | schema: @schema 37 | } 38 | end 39 | 40 | # Define a string property 41 | def string(name, enum: nil) 42 | add_property(name, enum ? {type: "string", enum: enum} : {type: "string"}) 43 | end 44 | 45 | # Define a number property 46 | def number(name) 47 | add_property(name, {type: "number"}) 48 | end 49 | 50 | # Define a boolean property 51 | def boolean(name) 52 | add_property(name, {type: "boolean"}) 53 | end 54 | 55 | # Define an object property 56 | def object(name, &block) 57 | properties = {} 58 | required = [] 59 | Schema.new.tap do |s| 60 | s.instance_eval(&block) 61 | properties = s.instance_variable_get(:@schema)[:properties] 62 | required = s.instance_variable_get(:@schema)[:required] 63 | end 64 | add_property(name, {type: "object", properties: properties, required: required, additionalProperties: false}) 65 | end 66 | 67 | # Define an array property 68 | def array(name, items:) 69 | add_property(name, {type: "array", items: items}) 70 | end 71 | 72 | # Define an anyOf property 73 | def any_of(name, schemas) 74 | add_property(name, {anyOf: schemas}) 75 | end 76 | 77 | # Define a reusable schema component 78 | def define(name, &block) 79 | @definitions[name] = Schema.new(&block).instance_variable_get(:@schema) 80 | end 81 | 82 | # Reference a defined schema component 83 | def ref(name) 84 | {"$ref" => "#/$defs/#{name}"} 85 | end 86 | 87 | private 88 | 89 | # Add a property to the schema 90 | def add_property(name, definition) 91 | @schema[:properties][name] = definition 92 | @schema[:required] << name 93 | end 94 | 95 | # Validate the schema against defined limits 96 | def validate_schema 97 | properties_count = count_properties(@schema) 98 | raise "Exceeded maximum number of object properties" if properties_count > MAX_OBJECT_PROPERTIES 99 | 100 | max_depth = calculate_max_depth(@schema) 101 | raise "Exceeded maximum nesting depth" if max_depth > MAX_NESTING_DEPTH 102 | end 103 | 104 | # Count the total number of properties in the schema 105 | def count_properties(schema) 106 | return 0 unless schema.is_a?(Hash) && schema[:properties] 107 | count = schema[:properties].size 108 | schema[:properties].each_value do |prop| 109 | count += count_properties(prop) 110 | end 111 | count 112 | end 113 | 114 | # Calculate the maximum nesting depth of the schema 115 | def calculate_max_depth(schema, current_depth = 1) 116 | return current_depth unless schema.is_a?(Hash) && schema[:properties] 117 | max_child_depth = schema[:properties].values.map do |prop| 118 | calculate_max_depth(prop, current_depth + 1) 119 | end.max 120 | max_child_depth ? [current_depth, max_child_depth].max : current_depth 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/agentic/verification/critic_framework.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | module Verification 5 | # Framework for multi-perspective evaluation of task results 6 | class CriticFramework 7 | # @return [Array] The critics registered with this framework 8 | attr_reader :critics 9 | 10 | # @return [Hash] Configuration options for the framework 11 | attr_reader :config 12 | 13 | # Initializes a new CriticFramework 14 | # @param critics [Array] The critics to register 15 | # @param config [Hash] Configuration options for the framework 16 | def initialize(critics: [], config: {}) 17 | @critics = critics 18 | @config = config 19 | end 20 | 21 | # Adds a critic to the framework 22 | # @param critic [Critic] The critic to add 23 | # @return [void] 24 | def add_critic(critic) 25 | @critics << critic 26 | end 27 | 28 | # Evaluates a task result using all registered critics 29 | # @param task [Task] The task to evaluate 30 | # @param result [TaskResult] The result to evaluate 31 | # @return [CriticResult] The combined evaluation result 32 | def evaluate(task, result) 33 | evaluations = @critics.map { |critic| critic.critique(task, result) } 34 | 35 | # Aggregate critic evaluations 36 | positive_critiques = evaluations.count(&:positive?) 37 | total_critiques = evaluations.size 38 | confidence = (total_critiques > 0) ? positive_critiques.to_f / total_critiques : 0.5 39 | 40 | comments = evaluations.flat_map(&:comments) 41 | 42 | CriticResult.new( 43 | task_id: task.id, 44 | confidence: confidence, 45 | verdict: confidence >= 0.7, # Pass if 70% or more critics give positive evaluation 46 | comments: comments 47 | ) 48 | end 49 | end 50 | 51 | # Represents the result of a critic's evaluation 52 | class CriticResult 53 | # @return [String] The ID of the task that was evaluated 54 | attr_reader :task_id 55 | 56 | # @return [Float] The confidence of the evaluation (0.0-1.0) 57 | attr_reader :confidence 58 | 59 | # @return [Boolean] The verdict of the evaluation (true = pass, false = fail) 60 | attr_reader :verdict 61 | 62 | # @return [Array] Comments from the critic 63 | attr_reader :comments 64 | 65 | # Initializes a new CriticResult 66 | # @param task_id [String] The ID of the task that was evaluated 67 | # @param confidence [Float] The confidence of the evaluation (0.0-1.0) 68 | # @param verdict [Boolean] The verdict of the evaluation (true = pass, false = fail) 69 | # @param comments [Array] Comments from the critic 70 | def initialize(task_id:, confidence:, verdict:, comments: []) 71 | @task_id = task_id 72 | @confidence = confidence 73 | @verdict = verdict 74 | @comments = comments 75 | end 76 | 77 | # Checks if the evaluation is positive 78 | # @return [Boolean] Whether the evaluation is positive 79 | def positive? 80 | @verdict 81 | end 82 | 83 | # Converts the critic result to a hash 84 | # @return [Hash] The critic result as a hash 85 | def to_h 86 | { 87 | task_id: @task_id, 88 | confidence: @confidence, 89 | verdict: @verdict, 90 | comments: @comments 91 | } 92 | end 93 | end 94 | 95 | # Base class for critics 96 | class Critic 97 | # @return [Hash] Configuration options for the critic 98 | attr_reader :config 99 | 100 | # Initializes a new Critic 101 | # @param config [Hash] Configuration options for the critic 102 | def initialize(config = {}) 103 | @config = config 104 | end 105 | 106 | # Critiques a task result 107 | # @param task [Task] The task to critique 108 | # @param result [TaskResult] The result to critique 109 | # @return [CriticResult] The critique result 110 | # @raise [NotImplementedError] This method must be implemented by subclasses 111 | def critique(task, result) 112 | raise NotImplementedError, "Subclasses must implement critique" 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/agentic/errors/llm_error_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Agentic::Errors::LlmError do 6 | describe "base error class" do 7 | it "initializes with a message" do 8 | error = described_class.new("An error occurred") 9 | expect(error.message).to eq("An error occurred") 10 | expect(error.response).to be_nil 11 | expect(error.context).to eq({}) 12 | end 13 | 14 | it "initializes with a response" do 15 | response = {"error" => {"message" => "Something went wrong"}} 16 | error = described_class.new("An error occurred", response: response) 17 | expect(error.response).to eq(response) 18 | end 19 | 20 | it "initializes with context" do 21 | context = {request_id: "req123"} 22 | error = described_class.new("An error occurred", context: context) 23 | expect(error.context).to eq(context) 24 | end 25 | end 26 | 27 | describe "subclasses" do 28 | describe Agentic::Errors::LlmRefusalError do 29 | it "initializes with a refusal message" do 30 | error = described_class.new("I can't do that") 31 | expect(error.message).to eq("LLM refused to respond: I can't do that") 32 | expect(error.refusal_message).to eq("I can't do that") 33 | end 34 | end 35 | 36 | describe Agentic::Errors::LlmParseError do 37 | it "initializes with a parse exception" do 38 | parse_exception = JSON::ParserError.new("Invalid JSON") 39 | error = described_class.new("Failed to parse JSON", parse_exception: parse_exception) 40 | expect(error.message).to eq("Failed to parse JSON") 41 | expect(error.parse_exception).to eq(parse_exception) 42 | end 43 | end 44 | 45 | describe Agentic::Errors::LlmNetworkError do 46 | it "initializes with a network exception" do 47 | network_exception = StandardError.new("Connection failed") 48 | error = described_class.new("Network error", network_exception: network_exception) 49 | expect(error.message).to eq("Network error") 50 | expect(error.network_exception).to eq(network_exception) 51 | end 52 | 53 | it "is retryable" do 54 | error = described_class.new("Network error") 55 | expect(error.retryable?).to be true 56 | end 57 | end 58 | 59 | describe Agentic::Errors::LlmRateLimitError do 60 | it "initializes with a retry_after value" do 61 | error = described_class.new("Rate limit exceeded", retry_after: 30) 62 | expect(error.message).to eq("Rate limit exceeded") 63 | expect(error.retry_after).to eq(30) 64 | end 65 | 66 | it "is retryable" do 67 | error = described_class.new("Rate limit exceeded") 68 | expect(error.retryable?).to be true 69 | end 70 | end 71 | 72 | describe Agentic::Errors::LlmAuthenticationError do 73 | it "initializes with a message" do 74 | error = described_class.new("Invalid API key") 75 | expect(error.message).to eq("Invalid API key") 76 | end 77 | 78 | it "is not retryable" do 79 | error = described_class.new("Invalid API key") 80 | expect(error.retryable?).to be false 81 | end 82 | end 83 | 84 | describe Agentic::Errors::LlmServerError do 85 | it "initializes with a message" do 86 | error = described_class.new("Server error") 87 | expect(error.message).to eq("Server error") 88 | end 89 | 90 | it "is retryable" do 91 | error = described_class.new("Server error") 92 | expect(error.retryable?).to be true 93 | end 94 | end 95 | 96 | describe Agentic::Errors::LlmTimeoutError do 97 | it "initializes with a message" do 98 | error = described_class.new("Request timed out") 99 | expect(error.message).to eq("Request timed out") 100 | end 101 | 102 | it "is retryable" do 103 | error = described_class.new("Request timed out") 104 | expect(error.retryable?).to be true 105 | end 106 | end 107 | 108 | describe Agentic::Errors::LlmInvalidRequestError do 109 | it "initializes with a message" do 110 | error = described_class.new("Invalid request") 111 | expect(error.message).to eq("Invalid request") 112 | end 113 | 114 | it "is not retryable" do 115 | error = described_class.new("Invalid request") 116 | expect(error.retryable?).to be false 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /.architecture/templates/implementation_roadmap.md: -------------------------------------------------------------------------------- 1 | # Implementation Roadmap for Version X.Y.Z 2 | 3 | ## Overview 4 | 5 | This document outlines the implementation plan for architectural changes identified in the recalibration plan for version X.Y.Z. It breaks down high-level architectural changes into implementable tasks, assigns them to specific versions, and establishes acceptance criteria. 6 | 7 | ## Target Versions 8 | 9 | This roadmap covers the following versions: 10 | - **X.Y.Z**: [Brief description of focus] 11 | - **X.Y.(Z+1)**: [Brief description of focus] 12 | - **X.(Y+1).0**: [Brief description of focus] 13 | 14 | ## Implementation Areas 15 | 16 | ### [Area 1: e.g., Component Decomposition] 17 | 18 | **Overall Goal**: [Describe the high-level architectural goal for this area] 19 | 20 | #### Tasks for Version X.Y.Z 21 | 22 | | Task ID | Description | Dependencies | Complexity | Owner | Tests Required | 23 | |---------|-------------|--------------|------------|-------|----------------| 24 | | [A1.1] | [Detailed task description] | [Dependencies] | [Low/Medium/High] | [Owner] | [Test requirements] | 25 | | [A1.2] | [...] | [...] | [...] | [...] | [...] | 26 | 27 | **Acceptance Criteria**: 28 | - [ ] [Criterion 1] 29 | - [ ] [Criterion 2] 30 | - [ ] [...] 31 | 32 | #### Tasks for Version X.Y.(Z+1) 33 | 34 | | Task ID | Description | Dependencies | Complexity | Owner | Tests Required | 35 | |---------|-------------|--------------|------------|-------|----------------| 36 | | [A1.3] | [Detailed task description] | [Dependencies] | [Low/Medium/High] | [Owner] | [Test requirements] | 37 | | [A1.4] | [...] | [...] | [...] | [...] | [...] | 38 | 39 | **Acceptance Criteria**: 40 | - [ ] [Criterion 1] 41 | - [ ] [Criterion 2] 42 | - [ ] [...] 43 | 44 | ### [Area 2: e.g., Security Enhancements] 45 | 46 | **Overall Goal**: [Describe the high-level architectural goal for this area] 47 | 48 | #### Tasks for Version X.Y.Z 49 | 50 | | Task ID | Description | Dependencies | Complexity | Owner | Tests Required | 51 | |---------|-------------|--------------|------------|-------|----------------| 52 | | [B1.1] | [Detailed task description] | [Dependencies] | [Low/Medium/High] | [Owner] | [Test requirements] | 53 | | [B1.2] | [...] | [...] | [...] | [...] | [...] | 54 | 55 | **Acceptance Criteria**: 56 | - [ ] [Criterion 1] 57 | - [ ] [Criterion 2] 58 | - [ ] [...] 59 | 60 | ## Implementation Approach 61 | 62 | ### Breaking vs. Non-Breaking Changes 63 | 64 | [Describe the approach to handling breaking changes, including deprecation policy, backward compatibility strategies, etc.] 65 | 66 | ### Feature Flags 67 | 68 | [Document any feature flags that will be used to control the rollout of new architectural components] 69 | 70 | | Flag Name | Purpose | Default Value | Removal Version | 71 | |-----------|---------|---------------|-----------------| 72 | | [Flag name] | [Purpose] | [true/false] | [Version] | 73 | 74 | ### Migration Support 75 | 76 | [Detail any migration utilities, scripts, or guidance that will be provided to help users adapt to architectural changes] 77 | 78 | ## Testing Strategy 79 | 80 | ### Component Tests 81 | 82 | [Describe the approach to testing individual architectural components] 83 | 84 | ### Integration Tests 85 | 86 | [Describe the approach to testing integration between components] 87 | 88 | ### Migration Tests 89 | 90 | [Describe the approach to testing migration paths from previous versions] 91 | 92 | ## Documentation Plan 93 | 94 | | Document | Update Required | Responsible | Deadline | 95 | |----------|-----------------|-------------|----------| 96 | | [Document name] | [Description of update needed] | [Responsible person] | [Date] | 97 | 98 | ## Risk Assessment 99 | 100 | | Risk | Impact | Likelihood | Mitigation Strategy | 101 | |------|--------|------------|---------------------| 102 | | [Risk description] | [High/Medium/Low] | [High/Medium/Low] | [Mitigation approach] | 103 | 104 | ## Timeline 105 | 106 | | Milestone | Target Date | Dependencies | Owner | 107 | |-----------|-------------|--------------|-------| 108 | | [Milestone description] | [Date] | [Dependencies] | [Owner] | 109 | 110 | ## Progress Tracking 111 | 112 | Progress on this implementation roadmap will be tracked in: 113 | - [Link to tracking tool/document] 114 | - [Link to relevant GitHub projects/issues] 115 | 116 | ## Appendices 117 | 118 | ### A. Architecture Diagrams 119 | 120 | [Include or link to relevant architecture diagrams] 121 | 122 | ### B. Relevant ADRs 123 | 124 | - [ADR-XXX: Title](link-to-adr) 125 | - [ADR-YYY: Title](link-to-adr) -------------------------------------------------------------------------------- /.architecture/decisions/adrs/ADR-016-agent-assembly-engine.md: -------------------------------------------------------------------------------- 1 | # ADR-016: Agent Assembly Engine 2 | 3 | ## Status 4 | 5 | Accepted 6 | 7 | ## Context 8 | 9 | A key requirement for the Agentic framework is the ability to dynamically assemble agents based on task requirements. Currently, agents are constructed manually or through predefined configurations, which: 10 | 11 | 1. Requires foreknowledge of task requirements 12 | 2. Results in either overly generic or overly specialized agents 13 | 3. Misses opportunities for capability reuse and optimization 14 | 4. Does not leverage knowledge from previous similar tasks 15 | 16 | We need a system that can analyze task requirements, select appropriate capabilities, and assemble agents optimized for specific tasks. This system should also integrate with our persistent storage to reuse existing agents when appropriate. 17 | 18 | ## Decision 19 | 20 | We will implement an `AgentAssemblyEngine` that handles the dynamic construction of agents based on task analysis. The engine will: 21 | 22 | 1. Analyze task requirements to determine needed capabilities 23 | 2. Select appropriate capabilities using pluggable selection strategies 24 | 3. Build agents with the selected capabilities 25 | 4. Store assembled agents for future reuse 26 | 5. Find and reuse suitable existing agents when possible 27 | 28 | Key components: 29 | 30 | ```ruby 31 | # Main engine 32 | engine = AgentAssemblyEngine.new(registry, agent_store) 33 | 34 | # Assemble an agent for a task 35 | agent = engine.assemble_agent(task, strategy: strategy, store: true) 36 | 37 | # Analyze task requirements 38 | requirements = engine.analyze_requirements(task) 39 | 40 | # Select capabilities based on requirements 41 | capabilities = engine.select_capabilities(requirements, strategy) 42 | 43 | # Build an agent with selected capabilities 44 | agent = engine.build_agent(task, capabilities) 45 | ``` 46 | 47 | The engine will support multiple composition strategies through a Strategy pattern: 48 | 49 | 1. `DefaultCompositionStrategy`: Basic capability selection based on requirements 50 | 2. `LlmAssistedCompositionStrategy`: Uses an LLM to suggest optimal capabilities 51 | 3. Custom strategies can be implemented for specialized domains 52 | 53 | ## Consequences 54 | 55 | ### Positive 56 | 57 | 1. Enables task-optimized agent assembly 58 | 2. Supports agent reuse through integration with persistent store 59 | 3. Pluggable strategies allow domain-specific optimization 60 | 4. Reduces manual configuration requirements 61 | 5. Promotes capability-based agent design 62 | 63 | ### Negative 64 | 65 | 1. Increases system complexity 66 | 2. Requires accurate task requirement analysis 67 | 3. May lead to capability explosion without governance 68 | 4. LLM-assisted strategies add external dependencies 69 | 70 | ### Neutral 71 | 72 | 1. Changes agent construction paradigm from static to dynamic 73 | 2. Requires integration with the capability registry and agent store 74 | 3. May require adjustment of existing agent usage patterns 75 | 76 | ## Implementation Notes 77 | 78 | ### Task Requirement Analysis 79 | 80 | The engine will analyze tasks using multiple approaches: 81 | 82 | 1. Task description analysis using pattern matching or NLP 83 | 2. Agent specification analysis for explicit capability requirements 84 | 3. Task input analysis for capability hints 85 | 4. LLM-assisted analysis for complex requirements 86 | 87 | ### Capability Selection 88 | 89 | Selection strategies will consider: 90 | - Capability relevance to the task 91 | - Capability performance metrics from learning system 92 | - Capability dependencies and compatibility 93 | - Agent resource constraints 94 | 95 | ### Agent Matching 96 | 97 | When finding existing agents for a task, the engine will: 98 | 1. Identify primary capabilities required for the task 99 | 2. Find agents with these capabilities 100 | 3. Score agents based on capability match 101 | 4. Return the best matching agent if score exceeds threshold 102 | 103 | ### Integration with Learning System 104 | 105 | The engine will integrate with the learning system to: 106 | 1. Record capability selection decisions 107 | 2. Track assembled agent performance 108 | 3. Improve selection strategies based on outcomes 109 | 4. Optimize capability compositions 110 | 111 | ## Future Considerations 112 | 113 | 1. Support for agent composition constraints (e.g., maximum capabilities) 114 | 2. Advanced capability compatibility checking 115 | 3. Agent performance benchmarking for selection decisions 116 | 4. Multi-agent assembly for complex tasks 117 | 5. Capability governance and approval workflows -------------------------------------------------------------------------------- /lib/agentic/task_output_schemas.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Registry for managing task output schemas 5 | # Provides a centralized location for defining and accessing 6 | # structured output schemas used by tasks 7 | class TaskOutputSchemas 8 | @schemas = {} 9 | 10 | class << self 11 | # Registers a new output schema 12 | # @param name [Symbol] The schema name/identifier 13 | # @param schema [Agentic::StructuredOutputs::Schema] The schema definition 14 | def register(name, schema) 15 | @schemas[name] = schema 16 | end 17 | 18 | # Retrieves a registered schema 19 | # @param name [Symbol] The schema name/identifier 20 | # @return [Agentic::StructuredOutputs::Schema, nil] The schema or nil if not found 21 | def get(name) 22 | @schemas[name] || ((name == :default) ? default_task_schema : nil) 23 | end 24 | 25 | # Lists all registered schema names 26 | # @return [Array] Array of schema names 27 | def list_schemas 28 | (@schemas.keys + [:default]).uniq 29 | end 30 | 31 | # Checks if a schema is registered 32 | # @param name [Symbol] The schema name/identifier 33 | # @return [Boolean] True if schema exists 34 | def exists?(name) 35 | @schemas.key?(name) || name == :default 36 | end 37 | 38 | # Returns the default task output schema for general task responses 39 | # @return [Agentic::StructuredOutputs::Schema] Default schema for task outputs 40 | def default_task_schema 41 | @default_schema ||= StructuredOutputs::Schema.new("task_output") do |schema| 42 | # Simple, flexible schema for task results 43 | schema.string(:status, enum: ["completed", "partial", "failed"]) 44 | schema.object(:result) do |result_schema| 45 | result_schema.string(:summary) 46 | # Additional properties will be allowed for flexible task outputs 47 | end 48 | schema.array(:steps, items: {type: "string"}) 49 | end 50 | end 51 | 52 | # Returns a simple object schema for maximum flexibility 53 | # @return [Agentic::StructuredOutputs::Schema] Simple object schema 54 | def simple_object_schema 55 | @simple_object_schema ||= StructuredOutputs::Schema.new("simple_object") do |schema| 56 | # Minimal schema that accepts any structured JSON object 57 | # This is useful when we want structured JSON but maximum flexibility 58 | schema.string(:type) 59 | schema.object(:data) do 60 | # Flexible data structure 61 | end 62 | end 63 | end 64 | 65 | # Returns a schema for code generation tasks 66 | # @return [Agentic::StructuredOutputs::Schema] Code generation schema 67 | def code_generation_schema 68 | @code_generation_schema ||= StructuredOutputs::Schema.new("code_generation") do |schema| 69 | schema.string(:language) 70 | schema.string(:filename) 71 | schema.string(:code) 72 | schema.string(:description) 73 | schema.array(:dependencies, items: {type: "string"}) 74 | end 75 | end 76 | 77 | # Returns a schema for analysis/research tasks 78 | # @return [Agentic::StructuredOutputs::Schema] Analysis task schema 79 | def analysis_schema 80 | @analysis_schema ||= StructuredOutputs::Schema.new("analysis_result") do |schema| 81 | schema.string(:summary) 82 | schema.array(:key_findings, items: {type: "string"}) 83 | schema.object(:data) do 84 | # Flexible analysis data 85 | end 86 | schema.array(:recommendations, items: {type: "string"}) 87 | schema.string(:confidence_level, enum: ["high", "medium", "low"]) 88 | end 89 | end 90 | 91 | # Resets all registered schemas (useful for testing) 92 | def reset! 93 | @schemas = {} 94 | @default_schema = nil 95 | @simple_object_schema = nil 96 | @code_generation_schema = nil 97 | @analysis_schema = nil 98 | end 99 | 100 | # Registers default schemas 101 | def register_defaults! 102 | register(:default, default_task_schema) 103 | register(:simple_object, simple_object_schema) 104 | register(:code_generation, code_generation_schema) 105 | register(:analysis, analysis_schema) 106 | end 107 | end 108 | 109 | # Register defaults when the class is loaded 110 | register_defaults! 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/agentic/capability_specification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Defines the specification for an agent capability 5 | # @attr_reader [String] name The name of the capability 6 | # @attr_reader [String] description Description of the capability 7 | # @attr_reader [String] version The version of the capability 8 | # @attr_reader [Hash] inputs The required inputs for the capability 9 | # @attr_reader [Hash] outputs The expected outputs from the capability 10 | # @attr_reader [Array] dependencies The dependencies of the capability 11 | class CapabilitySpecification 12 | attr_reader :name, :description, :version, :inputs, :outputs, :dependencies 13 | 14 | # Initialize a new capability specification 15 | # @param name [String] The name of the capability 16 | # @param description [String] Description of the capability 17 | # @param version [String] The version of the capability 18 | # @param inputs [Hash] The required inputs for the capability 19 | # @param outputs [Hash] The expected outputs from the capability 20 | # @param dependencies [Array] The dependencies of the capability 21 | def initialize(name:, description:, version:, inputs: {}, outputs: {}, dependencies: []) 22 | @name = name 23 | @description = description 24 | @version = version 25 | @inputs = inputs 26 | @outputs = outputs 27 | @dependencies = dependencies 28 | end 29 | 30 | # Check if this capability is compatible with another capability 31 | # @param other [CapabilitySpecification] The other capability 32 | # @return [Boolean] True if compatible 33 | def compatible_with?(other) 34 | return false unless other.is_a?(CapabilitySpecification) 35 | return false unless name == other.name 36 | 37 | # Compare versions using semantic versioning rules 38 | # For now, just check for exact match or higher minor version 39 | return true if version == other.version 40 | 41 | begin 42 | my_parts = version.split(".").map(&:to_i) 43 | other_parts = other.version.split(".").map(&:to_i) 44 | 45 | # Major version must match 46 | return false unless my_parts[0] == other_parts[0] 47 | 48 | # Our minor version should be >= other's minor version 49 | my_parts[1] >= other_parts[1] 50 | rescue 51 | # If version parsing fails, require exact match 52 | false 53 | end 54 | end 55 | 56 | # Convert to a hash representation 57 | # @return [Hash] The hash representation 58 | def to_h 59 | { 60 | name: @name, 61 | description: @description, 62 | version: @version, 63 | inputs: @inputs, 64 | outputs: @outputs, 65 | dependencies: @dependencies 66 | } 67 | end 68 | 69 | # Create from a hash representation 70 | # @param hash [Hash] The hash representation 71 | # @return [CapabilitySpecification] The capability specification 72 | def self.from_h(hash) 73 | new( 74 | name: hash[:name] || hash["name"], 75 | description: hash[:description] || hash["description"], 76 | version: hash[:version] || hash["version"], 77 | inputs: hash[:inputs] || hash["inputs"] || {}, 78 | outputs: hash[:outputs] || hash["outputs"] || {}, 79 | dependencies: hash[:dependencies] || hash["dependencies"] || [] 80 | ) 81 | end 82 | 83 | # Get the capability requirements as a human-readable string 84 | # @return [String] The capability requirements 85 | def requirements_description 86 | result = "Capability: #{name} (v#{version})\n" 87 | result += "Description: #{description}\n" 88 | 89 | unless inputs.empty? 90 | result += "\nInputs:\n" 91 | inputs.each do |name, spec| 92 | result += " #{name}: #{spec[:type] || "any"}" 93 | result += " (required)" if spec[:required] 94 | result += " - #{spec[:description]}" if spec[:description] 95 | result += "\n" 96 | end 97 | end 98 | 99 | unless outputs.empty? 100 | result += "\nOutputs:\n" 101 | outputs.each do |name, spec| 102 | result += " #{name}: #{spec[:type] || "any"}" 103 | result += " - #{spec[:description]}" if spec[:description] 104 | result += "\n" 105 | end 106 | end 107 | 108 | unless dependencies.empty? 109 | result += "\nDependencies:\n" 110 | dependencies.each do |dep| 111 | result += " #{dep[:name]} (v#{dep[:version] || "any"})\n" 112 | end 113 | end 114 | 115 | result 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/agentic/extension/protocol_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | module Extension 5 | # The ProtocolHandler standardizes connections to external systems through consistent 6 | # interface definitions. It enables integration with various APIs, data sources, 7 | # and services while providing a uniform access pattern regardless of the underlying 8 | # protocol or system specifics. 9 | class ProtocolHandler 10 | # Initialize a new ProtocolHandler 11 | # 12 | # @param [Hash] options Configuration options 13 | # @option options [Logger] :logger Custom logger instance 14 | # @option options [Hash] :default_headers Default headers for all protocol requests 15 | def initialize(options = {}) 16 | @logger = options[:logger] || Agentic.logger 17 | @default_headers = options[:default_headers] || {} 18 | @protocols = {} 19 | end 20 | 21 | # Register a protocol implementation 22 | # 23 | # @param [Symbol] protocol_name The name of the protocol (e.g., :http, :websocket, :grpc) 24 | # @param [Object] implementation The protocol implementation 25 | # @param [Hash] config Protocol-specific configuration 26 | # @return [Boolean] True if registration was successful 27 | def register_protocol(protocol_name, implementation, config = {}) 28 | if !valid_protocol?(implementation) 29 | @logger.error("Protocol implementation for '#{protocol_name}' is invalid") 30 | return false 31 | end 32 | 33 | @protocols[protocol_name] = { 34 | implementation: implementation, 35 | config: config 36 | } 37 | 38 | @logger.info("Protocol '#{protocol_name}' registered successfully") 39 | true 40 | end 41 | 42 | # Send a request using a registered protocol 43 | # 44 | # @param [Symbol] protocol_name The protocol to use 45 | # @param [String] endpoint The endpoint to send the request to 46 | # @param [Hash] options Request options including :method, :headers, :body, etc. 47 | # @return [Hash, nil] The response or nil if the protocol is not registered 48 | def send_request(protocol_name, endpoint, options = {}) 49 | unless @protocols.key?(protocol_name) 50 | @logger.error("Protocol '#{protocol_name}' is not registered") 51 | return nil 52 | end 53 | 54 | protocol = @protocols[protocol_name] 55 | options[:headers] = @default_headers.merge(options[:headers] || {}) 56 | 57 | begin 58 | response = protocol[:implementation].send_request(endpoint, options.merge(protocol[:config])) 59 | @logger.debug("Request sent using '#{protocol_name}' protocol to '#{endpoint}'") 60 | response 61 | rescue => e 62 | @logger.error("Failed to send request using '#{protocol_name}' protocol: #{e.message}") 63 | nil 64 | end 65 | end 66 | 67 | # Get protocol configuration 68 | # 69 | # @param [Symbol] protocol_name The protocol to get configuration for 70 | # @return [Hash, nil] The protocol configuration or nil if not registered 71 | def protocol_config(protocol_name) 72 | return nil unless @protocols.key?(protocol_name) 73 | 74 | @protocols[protocol_name][:config] 75 | end 76 | 77 | # Update protocol configuration 78 | # 79 | # @param [Symbol] protocol_name The protocol to update 80 | # @param [Hash] config The new configuration to merge 81 | # @return [Boolean] True if update was successful 82 | def update_protocol_config(protocol_name, config) 83 | return false unless @protocols.key?(protocol_name) 84 | 85 | @protocols[protocol_name][:config].merge!(config) 86 | true 87 | end 88 | 89 | # Check if a protocol is registered 90 | # 91 | # @param [Symbol] protocol_name The protocol to check 92 | # @return [Boolean] True if the protocol is registered 93 | def protocol_registered?(protocol_name) 94 | @protocols.key?(protocol_name) 95 | end 96 | 97 | # List all registered protocols 98 | # 99 | # @return [Array] List of registered protocol names 100 | def list_protocols 101 | @protocols.keys 102 | end 103 | 104 | private 105 | 106 | # Check if a protocol implementation is valid 107 | # 108 | # @param [Object] implementation The implementation to validate 109 | # @return [Boolean] True if the implementation is valid 110 | def valid_protocol?(implementation) 111 | # Check if implementation has required methods 112 | implementation.respond_to?(:send_request) 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/agentic/plan_execution_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "task_execution_result" 4 | 5 | module Agentic 6 | # Value object representing the execution result of a plan 7 | class PlanExecutionResult 8 | # @return [String] The unique identifier for the plan 9 | attr_reader :plan_id 10 | 11 | # @return [Symbol] The overall status of the plan (:completed, :in_progress, :partial_failure) 12 | attr_reader :status 13 | 14 | # @return [Float] The execution time in seconds 15 | attr_reader :execution_time 16 | 17 | # @return [Hash] Map of task ids to serialized Task objects 18 | attr_reader :tasks 19 | 20 | # @return [Hash] The execution results for each task 21 | attr_reader :results 22 | 23 | # Initializes a new plan execution result 24 | # @param plan_id [String] The unique identifier for the plan 25 | # @param status [Symbol] The overall status of the plan 26 | # @param execution_time [Float] The execution time in seconds 27 | # @param tasks [Hash] Map of task ids to serialized Task objects 28 | # @param results [Hash] Map of task ids to raw execution results 29 | def initialize(plan_id:, status:, execution_time:, tasks:, results:) 30 | @plan_id = plan_id 31 | @status = status 32 | @execution_time = execution_time 33 | @tasks = tasks 34 | @results = convert_raw_results(results) 35 | end 36 | 37 | # Creates a plan execution result from a hash 38 | # @param hash [Hash] The hash representation of a plan execution result 39 | # @return [PlanExecutionResult] A plan execution result 40 | def self.from_hash(hash) 41 | new( 42 | plan_id: hash[:plan_id], 43 | status: hash[:status], 44 | execution_time: hash[:execution_time], 45 | tasks: hash[:tasks], 46 | results: hash[:results] 47 | ) 48 | end 49 | 50 | # Checks if the plan execution was successful 51 | # @return [Boolean] True if successful, false otherwise 52 | def successful? 53 | @status == :completed 54 | end 55 | 56 | # Checks if the plan execution failed partially 57 | # @return [Boolean] True if partially failed, false otherwise 58 | def partial_failure? 59 | @status == :partial_failure 60 | end 61 | 62 | # Checks if the plan execution is still in progress 63 | # @return [Boolean] True if in progress, false otherwise 64 | def in_progress? 65 | @status == :in_progress 66 | end 67 | 68 | # Gets the result for a specific task 69 | # @param task_id [String] The ID of the task 70 | # @return [TaskExecutionResult, nil] The execution result for the task, or nil if not found 71 | def task_result(task_id) 72 | @results[task_id] 73 | end 74 | 75 | # Gets the serialized task data for a specific task 76 | # @param task_id [String] The ID of the task 77 | # @return [Hash, nil] The serialized task data, or nil if not found 78 | def task_data(task_id) 79 | @tasks[task_id] 80 | end 81 | 82 | # Gets the number of completed tasks 83 | # @return [Integer] The number of completed tasks 84 | def completed_tasks_count 85 | @results.count { |_, result| result.successful? } 86 | end 87 | 88 | # Gets the number of failed tasks 89 | # @return [Integer] The number of failed tasks 90 | def failed_tasks_count 91 | @results.count { |_, result| result.failed? } 92 | end 93 | 94 | # Gets the successful task results 95 | # @return [Hash] The successful task results 96 | def successful_task_results 97 | @results.select { |_, result| result.successful? } 98 | end 99 | 100 | # Gets the failed task results 101 | # @return [Hash] The failed task results 102 | def failed_task_results 103 | @results.select { |_, result| result.failed? } 104 | end 105 | 106 | # Returns a hash representation of the plan execution result 107 | # @return [Hash] The plan execution result as a hash 108 | def to_h 109 | { 110 | plan_id: @plan_id, 111 | status: @status, 112 | execution_time: @execution_time, 113 | tasks: @tasks, 114 | results: @results.transform_values(&:to_h) 115 | } 116 | end 117 | 118 | private 119 | 120 | # Converts raw results to TaskExecutionResult objects 121 | # @param raw_results [Hash] Map of task ids to raw execution results 122 | # @return [Hash] The converted results 123 | def convert_raw_results(raw_results) 124 | raw_results.transform_values do |result| 125 | result.is_a?(TaskExecutionResult) ? result : TaskExecutionResult.from_hash(result) 126 | end 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /.architecture/reviews/cli_command_duplication.md: -------------------------------------------------------------------------------- 1 | # Architecture Review: CLI Command Duplication Issue 2 | 3 | ## Review Purpose 4 | To analyze the current CLI implementation which has duplicate command definitions leading to confusion in the command line help output. 5 | 6 | ## System Context 7 | Agentic is a Ruby gem providing a command-line tool for building and running AI agents in a plan-and-execute fashion. The CLI is built using Thor and provides commands for creating plans, executing them, and managing configuration. 8 | 9 | ## Current Issue 10 | The CLI implementation has duplicate command definitions appearing in the help output. Commands related to agent management and configuration appear both as top-level commands and as subcommands, causing confusion for users. 11 | 12 | ## Individual Member Reviews 13 | 14 | ### API Design Specialist 15 | 16 | #### Findings 17 | - Duplicate command definitions create a confusing developer experience 18 | - The CLI help output shows multiple instances of the same commands with different paths 19 | - There is inconsistency in the command output formatting between implementations 20 | - The duplicates send conflicting signals about the proper command organization 21 | 22 | #### Recommendations 23 | - Select a single, consistent approach to command organization 24 | - Standardize on subcommands for logically grouped functionality 25 | - Ensure commands follow a clear hierarchy that reflects their relationships 26 | - Consider user expectations and mental models when organizing commands 27 | 28 | ### Ruby Systems Architect 29 | 30 | #### Findings 31 | - Two implementations of the same functionality exist in the codebase: 32 | 1. Nested classes within the main CLI class (enhanced UI with colorization) 33 | 2. Standalone files in the cli/ directory (simpler implementation) 34 | - Both implementations are being loaded and registered as commands 35 | - The implementation is not DRY (Don't Repeat Yourself) 36 | - Zeitwerk autoloading may be contributing to the issue by loading both implementations 37 | 38 | #### Recommendations 39 | - Choose one implementation approach and remove the duplicate 40 | - Refactor to properly utilize Thor's subcommand functionality 41 | - Ensure Zeitwerk loading is properly configured for Thor classes 42 | - Consider moving all command implementations to standalone files for maintainability 43 | 44 | ### CLI Implementation Expert 45 | 46 | #### Findings 47 | - Thor's subcommand registration is functioning correctly, but multiple command sources exist 48 | - Both standalone files and nested classes are being recognized by Thor 49 | - The enhanced UI implementations have better user experience with colorization and box output 50 | - The duplicate registrations are likely causing confusion in command discovery 51 | 52 | #### Recommendations 53 | - Keep the enhanced UI implementation as it provides better user experience 54 | - Remove or deactivate the simpler implementations in standalone files 55 | - Review the Thor documentation to ensure proper subcommand registration 56 | - Consider adding command namespaces to better organize related commands 57 | 58 | ## Consolidated Analysis 59 | 60 | ### Key Findings 61 | 1. The CLI is registering two implementations of the same commands - one from nested classes within CLI.rb and another from standalone files 62 | 2. The nested implementations have enhanced UI with colorization and box output 63 | 3. The standalone implementations have simpler output 64 | 4. Both are being loaded due to how Thor processes command registration and Zeitwerk's autoloading 65 | 66 | ### Trade-offs Analysis 67 | **Option 1: Keep nested classes only** 68 | - Pros: Enhanced UI, centralized code location 69 | - Cons: Larger file size, less modular 70 | 71 | **Option 2: Use standalone files only** 72 | - Pros: Better modularity, separation of concerns 73 | - Cons: Currently has simpler UI, would need enhancement 74 | 75 | **Option 3: Hybrid approach with delegation** 76 | - Pros: Clean architecture, separation of UI from logic 77 | - Cons: More complex, requires additional refactoring 78 | 79 | ### Recommendations 80 | 1. **Short-term fix**: Choose the nested class implementation and remove or disable the standalone files 81 | 2. **Long-term solution**: Refactor to a hybrid approach where: 82 | - Standalone files contain the core command logic 83 | - UI presentation layer is separated 84 | - Commands are clearly organized in a hierarchical structure 85 | 86 | ## Action Items 87 | 1. Remove or disable the standalone CLI command files (agent.rb, config.rb) 88 | 2. Update the requires in agentic.rb to reflect this change 89 | 3. Consider renaming the nested classes for clarity 90 | 4. Document the CLI command structure in the README or documentation 91 | 5. Add comprehensive tests for CLI command functionality 92 | 93 | ## Review Participants 94 | - API Design Specialist 95 | - Ruby Systems Architect 96 | - CLI Implementation Expert 97 | 98 | Date: May 22, 2024 -------------------------------------------------------------------------------- /.architecture/recalibration/cli_command_structure.md: -------------------------------------------------------------------------------- 1 | # Architectural Recalibration: CLI Command Structure 2 | 3 | ## Review Analysis & Prioritization 4 | 5 | Based on the architectural review of the CLI command duplication issue, we have identified the following key areas for recalibration: 6 | 7 | ### High Priority 8 | 1. **Eliminate command duplication** - Address the immediate issue of duplicate commands appearing in the CLI help output 9 | 2. **Standardize on a single implementation approach** - Choose between nested classes or standalone files 10 | 3. **Ensure proper Thor configuration** - Fix the way commands are registered and loaded 11 | 12 | ### Medium Priority 13 | 1. **Improve CLI command organization** - Establish a clear hierarchy for commands 14 | 2. **Enhance user experience** - Maintain the enhanced UI with colorization and box output 15 | 3. **Document CLI structure** - Create clear documentation for the command structure 16 | 17 | ### Low Priority 18 | 1. **Refactor to a hybrid approach** - Consider a long-term solution with better separation of concerns 19 | 2. **Add comprehensive testing** - Ensure all CLI commands are thoroughly tested 20 | 21 | ## Architectural Plan Update 22 | 23 | ### Selected Approach 24 | After careful consideration of the trade-offs, we have decided to **standardize on the nested class implementation** in the short term. This approach provides the enhanced UI that creates a better user experience, while still maintaining the proper command hierarchy. 25 | 26 | ### Technical Implementation Plan 27 | 28 | 1. **Command Structure** 29 | - Keep the hierarchical command structure with top-level commands and logical subcommands 30 | - Maintain the enhanced UI with colorization and box output 31 | - Ensure consistent command naming and behavior 32 | 33 | 2. **Implementation Details** 34 | - Remove the standalone CLI command files (agent.rb, config.rb) 35 | - Update requires in agentic.rb to not load these files 36 | - Ensure the nested classes (AgentCommands, ConfigCommands) handle all functionality 37 | - Verify Thor's subcommand registration is properly configured 38 | 39 | 3. **Long-term Considerations** 40 | - Consider a future refactoring to a more modular approach 41 | - Evaluate the possibility of separating UI presentation from command logic 42 | - Maintain backward compatibility with existing command structure 43 | 44 | ## Documentation Refresh 45 | 46 | We will update the following documentation to reflect the changes: 47 | 48 | 1. **README.md** 49 | - Update the CLI usage section to clearly show the command hierarchy 50 | - Add examples of all available commands and their usage 51 | 52 | 2. **CLAUDE.md** 53 | - Document the CLI command structure and implementation approach 54 | - Provide guidance for future developers working on CLI commands 55 | 56 | 3. **Code Documentation** 57 | - Add thorough YARD comments to all CLI-related classes and methods 58 | - Document the intended command hierarchy and organization 59 | 60 | ## Implementation Roadmap 61 | 62 | ### Phase 1: Immediate Fix (Current Version 0.2.0) 63 | 1. Remove or comment out requires for standalone CLI files in agentic.rb 64 | 2. Verify that only the nested class implementations are being registered 65 | 3. Test all CLI commands to ensure they work as expected 66 | 4. Update basic documentation to reflect current command structure 67 | 68 | ### Phase 2: Cleanup (Next Minor Version) 69 | 1. Completely remove the standalone CLI files if they are no longer needed 70 | 2. Refactor nested classes for improved readability and maintenance 71 | 3. Add comprehensive tests for all CLI commands 72 | 4. Update all documentation with detailed CLI usage information 73 | 74 | ### Phase 3: Long-term Refactoring (Future Major Version) 75 | 1. Evaluate a hybrid approach with better separation of concerns 76 | 2. Consider moving to standalone files with enhanced UI capabilities 77 | 3. Implement a more modular architecture for the CLI components 78 | 4. Ensure backward compatibility with existing command structure 79 | 80 | ## Progress Tracking 81 | 82 | We will track progress on this recalibration using the following metrics: 83 | 84 | 1. **Command Duplication**: Verify that duplicate commands no longer appear in CLI help output 85 | 2. **Test Coverage**: Ensure all CLI commands have appropriate test coverage 86 | 3. **Documentation Completeness**: Check that all CLI commands are properly documented 87 | 4. **User Experience**: Collect feedback on the clarity and usability of the CLI 88 | 89 | ## Conclusion 90 | 91 | This recalibration plan addresses the immediate issue of CLI command duplication while setting the stage for longer-term improvements to the CLI architecture. By standardizing on the nested class implementation in the short term, we maintain the enhanced user experience while eliminating confusion. The longer-term plan allows for a more modular approach that better separates concerns while maintaining backward compatibility. -------------------------------------------------------------------------------- /lib/agentic/retry_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # Handles retrying operations with configurable backoff strategies 5 | class RetryHandler 6 | # @return [Integer] The maximum number of retry attempts 7 | attr_reader :max_retries 8 | 9 | # @return [Array] List of retryable error types/names 10 | attr_reader :retryable_errors 11 | 12 | # @return [Symbol] The backoff strategy to use 13 | attr_reader :backoff_strategy 14 | 15 | # @return [Proc, nil] Optional block to run before each retry 16 | attr_reader :before_retry 17 | 18 | # @return [Proc, nil] Optional block to run after each retry 19 | attr_reader :after_retry 20 | 21 | # Initializes a new RetryHandler 22 | # @param max_retries [Integer] The maximum number of retry attempts 23 | # @param retryable_errors [Array] List of retryable error types/names 24 | # @param backoff_strategy [Symbol] The backoff strategy (:constant, :linear, :exponential) 25 | # @param backoff_options [Hash] Options for the backoff strategy 26 | # @param before_retry [Proc, nil] Optional block to run before each retry 27 | # @param after_retry [Proc, nil] Optional block to run after each retry 28 | # @option backoff_options [Float] :base_delay The base delay in seconds 29 | # @option backoff_options [Float] :jitter_factor The jitter factor (0.0-1.0) 30 | def initialize( 31 | max_retries: 3, 32 | retryable_errors: [Errors::LlmTimeoutError, Errors::LlmRateLimitError, Errors::LlmServerError, Errors::LlmNetworkError], 33 | backoff_strategy: :exponential, 34 | backoff_options: {}, 35 | before_retry: nil, 36 | after_retry: nil 37 | ) 38 | @max_retries = max_retries 39 | @retryable_errors = retryable_errors 40 | @backoff_strategy = backoff_strategy 41 | @backoff_options = { 42 | base_delay: 1.0, 43 | jitter_factor: 0.25 44 | }.merge(backoff_options) 45 | @before_retry = before_retry 46 | @after_retry = after_retry 47 | end 48 | 49 | # Executes the given block with retries 50 | # @param block [Proc] The block to execute with retries 51 | # @return [Object] The return value of the block 52 | # @raise [StandardError] If the block failed after all retries 53 | def with_retry(&block) 54 | attempt = 0 55 | 56 | begin 57 | attempt += 1 58 | block.call 59 | rescue => e 60 | error = e.is_a?(Errors::LlmError) ? e : Errors::LlmError.new(e.message, context: {original_error: e.class.name}) 61 | 62 | if retryable?(error) && attempt <= max_retries 63 | delay = calculate_backoff_delay(attempt) 64 | Agentic.logger.info("Retry #{attempt}/#{max_retries} for error: #{error.message}. Waiting #{delay.round(2)}s before retrying.") 65 | 66 | @before_retry&.call(attempt: attempt, error: error, delay: delay) 67 | sleep(delay) 68 | @after_retry&.call(attempt: attempt, error: error, delay: delay) 69 | 70 | retry 71 | else 72 | if attempt > max_retries 73 | Agentic.logger.error("Max retries (#{max_retries}) exceeded for error: #{error.message}") 74 | else 75 | Agentic.logger.error("Non-retryable error: #{error.message}") 76 | end 77 | 78 | raise error 79 | end 80 | end 81 | end 82 | 83 | private 84 | 85 | # Determines if an error is retryable 86 | # @param error [StandardError] The error to check 87 | # @return [Boolean] True if the error is retryable 88 | def retryable?(error) 89 | return true if error.respond_to?(:retryable?) && error.retryable? 90 | 91 | @retryable_errors.any? do |retryable| 92 | if retryable.is_a?(Class) 93 | error.is_a?(retryable) 94 | else 95 | error.instance_of?(retryable).to_s 96 | end 97 | end 98 | end 99 | 100 | # Calculates the backoff delay for a given attempt 101 | # @param attempt [Integer] The current attempt number (1-based) 102 | # @return [Float] The delay in seconds 103 | def calculate_backoff_delay(attempt) 104 | base_delay = @backoff_options[:base_delay] 105 | 106 | delay = case @backoff_strategy 107 | when :constant 108 | base_delay 109 | when :linear 110 | base_delay * attempt 111 | when :exponential 112 | base_delay * (2**(attempt - 1)) 113 | else 114 | base_delay 115 | end 116 | 117 | if @backoff_options[:jitter_factor] && @backoff_options[:jitter_factor] > 0 118 | jitter = rand(-delay * @backoff_options[:jitter_factor]..delay * @backoff_options[:jitter_factor]) 119 | delay += jitter 120 | end 121 | 122 | [delay, 0].max # Ensure positive delay 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/agentic/adaptation_engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Agentic 4 | # The AdaptationEngine enables feedback-driven adjustments to agents and tasks. 5 | # It is part of the Verification Layer and focuses on applying learned improvements 6 | # based on feedback, outcomes, and performance metrics. 7 | class AdaptationEngine 8 | # Initialize a new AdaptationEngine. 9 | # 10 | # @param [Hash] options Configuration options for the adaptation engine 11 | # @option options [Logger] :logger Custom logger instance 12 | # @option options [Integer] :adaptation_threshold Minimum confidence score to trigger adaptation (0-100) 13 | # @option options [Boolean] :auto_adapt Whether to automatically apply adaptations 14 | def initialize(options = {}) 15 | @logger = options[:logger] || Agentic.logger 16 | @adaptation_threshold = options[:adaptation_threshold] || 75 17 | @auto_adapt = options.fetch(:auto_adapt, false) 18 | @adaptation_registry = {} 19 | @feedback_history = [] 20 | end 21 | 22 | # Register an adaptation strategy for a specific component/context 23 | # 24 | # @param [Symbol] component The component or context to adapt (e.g., :agent, :task, :prompt) 25 | # @param [Proc] strategy A callable that implements the adaptation logic 26 | # @return [Boolean] True if registration was successful 27 | def register_adaptation_strategy(component, strategy) 28 | return false unless strategy.respond_to?(:call) 29 | 30 | @adaptation_registry[component] = strategy 31 | true 32 | end 33 | 34 | # Process feedback and determine if adaptation is needed 35 | # 36 | # @param [Hash] feedback The feedback data to process 37 | # @option feedback [Symbol] :component The component receiving feedback 38 | # @option feedback [Object] :target The specific instance to adapt 39 | # @option feedback [Hash] :metrics Performance metrics 40 | # @option feedback [String, Symbol] :outcome Success/failure indicator 41 | # @option feedback [String] :suggestion Suggested improvement 42 | # @return [Hash] Result of the adaptation process 43 | def process_feedback(feedback) 44 | record_feedback(feedback) 45 | 46 | adaptation_needed = determine_if_adaptation_needed(feedback) 47 | return {adapted: false, reason: "Adaptation threshold not met"} unless adaptation_needed 48 | 49 | if @auto_adapt 50 | apply_adaptation(feedback) 51 | else 52 | { 53 | adapted: false, 54 | adaptation_suggested: true, 55 | suggestion: feedback[:suggestion] 56 | } 57 | end 58 | end 59 | 60 | # Apply an adaptation based on feedback 61 | # 62 | # @param [Hash] feedback The feedback data to use for adaptation 63 | # @return [Hash] Result of the adaptation attempt 64 | def apply_adaptation(feedback) 65 | component = feedback[:component] 66 | 67 | unless @adaptation_registry.key?(component) 68 | return {adapted: false, reason: "No adaptation strategy registered for #{component}"} 69 | end 70 | 71 | strategy = @adaptation_registry[component] 72 | 73 | begin 74 | result = strategy.call(feedback) 75 | { 76 | adapted: true, 77 | component: component, 78 | target: feedback[:target], 79 | result: result 80 | } 81 | rescue => e 82 | @logger.error("Adaptation failed: #{e.message}") 83 | { 84 | adapted: false, 85 | error: e.message, 86 | component: component 87 | } 88 | end 89 | end 90 | 91 | # Retrieve adaptation history for a specific component 92 | # 93 | # @param [Symbol] component The component to get history for 94 | # @return [Array] History of adaptations for the component 95 | def adaptation_history(component = nil) 96 | if component 97 | @feedback_history.select { |f| f[:component] == component } 98 | else 99 | @feedback_history 100 | end 101 | end 102 | 103 | private 104 | 105 | # Record feedback in the history 106 | # 107 | # @param [Hash] feedback The feedback to record 108 | def record_feedback(feedback) 109 | @feedback_history << feedback.merge(timestamp: Time.now) 110 | end 111 | 112 | # Determine if adaptation is needed based on feedback 113 | # 114 | # @param [Hash] feedback The feedback to analyze 115 | # @return [Boolean] True if adaptation is needed 116 | def determine_if_adaptation_needed(feedback) 117 | # Simple implementation - can be expanded with more sophisticated logic 118 | return false unless feedback[:metrics] && feedback[:metrics][:confidence] 119 | 120 | confidence_score = feedback[:metrics][:confidence] 121 | confidence_score < @adaptation_threshold 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | Agentic is a Ruby gem for building and running AI agents in a plan-and-execute fashion. It provides a simple command-line tool and library to build, manage, deploy, and run purpose-driven AI agents, using OpenAI's LLM API. 8 | 9 | ## Architecture Documentation 10 | 11 | This project follows a rigorous architectural design approach inspired by the [ai-software-architect](https://github.com/codenamev/ai-software-architect) framework. Before implementing new features or making significant changes: 12 | 13 | 1. **Consult Architectural Documents**: 14 | - @ArchitectureConsiderations.md - Core architectural vision and system layers 15 | - @ArchitecturalFeatureBuilder.md - Feature implementation guidelines and checklist 16 | - @.architecture/ folder (when available) - Detailed architectural decision records and reviews 17 | 18 | 2. **Follow Architectural Design Process**: 19 | - Design Phase: Reference existing architecture, identify component placement, define interfaces 20 | - Implementation Phase: Follow established patterns, maintain separation of concerns 21 | - Verification Phase: Implement comprehensive testing and manual verification 22 | 23 | 3. **Architectural Principles**: 24 | - Domain-agnostic, self-improving framework design 25 | - Progressive automation with human oversight 26 | - Learning capability through execution history 27 | - Extensibility through well-defined interfaces 28 | - Clear separation of concerns across system layers 29 | 30 | ## Key Commands 31 | 32 | ### Setup and Installation 33 | 34 | ```bash 35 | # Install dependencies 36 | bin/setup 37 | 38 | # Install the gem locally 39 | bundle exec rake install 40 | ``` 41 | 42 | ### Testing and Linting 43 | 44 | ```bash 45 | # Run the test suite 46 | bundle exec rake spec 47 | 48 | # Run a specific test file 49 | bundle exec rspec spec/path/to/file_spec.rb 50 | 51 | # Run a specific test 52 | bundle exec rspec spec/path/to/file_spec.rb:LINE_NUMBER 53 | 54 | # Run linting (StandardRB) 55 | bundle exec rake standard 56 | 57 | # Run both tests and linting (default task) 58 | bundle exec rake 59 | 60 | # Autofix linter issues with StandardRB 61 | standardrb --fix 62 | ``` 63 | 64 | ### Release 65 | 66 | ```bash 67 | # Release a new version (after updating version.rb) 68 | bundle exec rake release 69 | ``` 70 | 71 | ## Development Guidelines 72 | 73 | You are an experienced Ruby on Rails developer, very accurate for details. The 74 | last 10 years you've spent managing open source Ruby gems and architecting 75 | object oriented solutions. 76 | 77 | You must keep your answers very short, concise, simple and informative. 78 | 79 | ### Architectural Rigor 80 | 81 | Before implementing any feature: 82 | 1. **Review Architecture**: Consult `ArchitectureConsiderations.md` and `ArchitecturalFeatureBuilder.md` 83 | 2. **System Layer Identification**: Determine which architectural layer(s) your feature affects: 84 | - Foundation Layer (core abstractions, registries) 85 | - Runtime Layer (task execution, orchestration) 86 | - Verification Layer (quality assurance, validation) 87 | - Extension System (plugins, domain adapters) 88 | 3. **Interface Design**: Define clear interfaces following established patterns 89 | 4. **Implementation Checklist**: Use the checklist in `ArchitecturalFeatureBuilder.md` 90 | 91 | ### Code Standards 92 | 93 | 1. Prepend all Ruby commands with "bundle exec" 94 | 2. Use the project's .rubocop.yml for formatting of all Ruby code. 95 | 3. Use YARD comments for properly documenting all generated Ruby code. 96 | 4. **Testing with VCR**: The project uses VCR to record and replay HTTP interactions for tests. When adding new API interactions, ensure that they are properly recorded in cassettes. 97 | 5. **Structured Outputs**: When working with LLM responses, use the StructuredOutputs module to define schemas and validate responses. 98 | 6. **Factory Pattern**: Follow the established factory pattern when extending or creating new agents. 99 | 7. **API Key Handling**: Never hardcode API keys. Use the configuration system or environment variables. 100 | 8. **Ruby Style**: The project follows StandardRB conventions. Ensure your code passes `rake standard`. 101 | 9. **Documentation**: Document new classes and methods using YARD-style comments. 102 | 103 | ### Architectural Decision Documentation 104 | 105 | When making significant architectural decisions: 106 | 1. Document rationale in relevant architectural files 107 | 2. Update @ArchitectureConsiderations.md if system design changes 108 | 3. Consider creating @.architecture/decisions/adrs/ entries for major decisions 109 | 4. Ensure decisions align with multi-perspective review principles (systems, domain, security, performance, maintainability) 110 | 111 | (Rest of the document remains the same as the previous content) 112 | --------------------------------------------------------------------------------