A Ruby gem for building and running AI agents in a plan-and-execute fashion. Agentic provides a simple command-line tool and library to build, manage, deploy, and run purpose-driven AI agents using OpenAI's LLM API.
🚀 Agent Self-Assembly System - Agents can now dynamically construct themselves based on task requirements
🎯 Capability System - Rich specification and versioning system for agent capabilities
💾 Persistent Agent Store - Save and reuse agents across sessions
🔧 Enhanced CLI - Comprehensive command-line interface with agent and capability management
📊 Learning System - Agents improve over time through execution history analysis
🎨 Real-time Feedback - Progress bars, spinners, and colorized output for better user experience
Install the gem and add to the application's Gemfile by executing:
$ bundle add agenticIf bundler is not being used to manage dependencies, install the gem by executing:
$ gem install agenticAgentic comes with a rich command-line interface featuring colored output, spinners for long-running tasks, and progress tracking:
# Display version information
$ agentic version
# Configuration Management
$ agentic config init # Initialize configuration
$ agentic config init --global # Initialize global configuration
$ agentic config list # List all configuration settings
$ agentic config get model # Get a specific configuration value
$ agentic config set api_token=your_openai_api_key # Set your OpenAI API token
$ agentic config set model=gpt-4o-mini # Configure the default model# Plan Creation and Execution
$ agentic plan "Generate a market research report on AI trends"# Create a plan
$ agentic plan "Write a blog post about Ruby" --save plan.json # Save plan to file
$ agentic plan "Research quantum computing" --output json # Output in JSON format
$ agentic plan "Analyze market trends" --model gpt-4o # Use a specific model
$ agentic plan "Create documentation" --execute # Create and execute immediately
$ agentic plan "Research topics" --no-interactive # Skip interactive prompts
$ agentic execute --plan plan.json # Execute a saved plan
$ agentic execute --from-stdin # Read plan from stdin
$ agentic execute --max-concurrency 5 # Limit concurrent tasks
$ agentic execute --file results.json # Save results to specific file# Agent Management
$ agentic agent list # List available agents
$ agentic agent list --detailed # List with detailed information
$ agentic agent create "ResearchAgent"\ # Create a new agent
--role="Research Assistant" \
--purpose="Conduct thorough research" \
--capabilities=text_generation,web_search
$ agentic agent show "ResearchAgent"# Show agent details
$ agentic agent build "ResearchAgent"# Build an agent from storage
$ agentic agent delete "ResearchAgent"# Delete an agent# Capability Management
$ agentic capabilities list # List available capabilities
$ agentic capabilities show text_generation # Show capability details
$ agentic capabilities search generation # Search for capabilitiesThe CLI provides real-time feedback with spinners for long-running operations, colored status updates, and detailed progress tracking. It supports both global and project-specific configuration, allowing you to customize the behavior for different projects.
To use Agentic in your Ruby code:
require'agentic'# Configure your OpenAI API keyAgentic.configuredo |config|
config.access_token='your_openai_api_key'end# Create a TaskPlanner instance with a goalplanner=Agentic::TaskPlanner.new("Write a blog post about Ruby on Rails")# Generate the planplan=planner.plan# Display the planputsplan.to_s# Or access the structured dataplan.tasks.eachdo |task|
puts"Task: #{task.description}"puts"Agent: #{task.agent.name}"end# Create an agent with specific configurationagent=Agentic::Agent.builddo |a|
a.name="ResearchAgent"a.role="Research Assistant"a.instructions="Conduct thorough research on the given topic"end# Configure the LLMconfig=Agentic::LlmConfig.new(model: "gpt-4o-mini",temperature: 0.7)# Create a tasktask=Agentic::Task.new(description: "Research the latest trends in Ruby on Rails",agent_spec: {"name"=>"ResearchAgent","description"=>"An agent that performs research","instructions"=>"Research the given topic thoroughly"})# Execute the task with the agentresult=task.perform(agent)# Check the resultifresult.successful?puts"Task completed successfully!"putsresult.outputelseputs"Task failed: #{result.failure.message}"end# Create an orchestratororchestrator=Agentic::PlanOrchestrator.new(concurrency_limit: 5)# Add tasks (each with its own agent - see the next section for providers)collect=Agentic::Task.new(description: "collect entries",agent_spec: {"name"=>"Collector","instructions"=>"collect"})report=Agentic::Task.new(description: "write the report",agent_spec: {"name"=>"Reporter","instructions"=>"report"})orchestrator.add_task(collect,agent: ->(_task){%w[alphabetagamma]})orchestrator.add_task(report,[collect],agent: ->(task){"#{task.previous_output.size} entries reported"})# Execute the planresult=orchestrator.execute_plan# Process the resultsifresult.successful?puts"Plan executed successfully!"elseputs"Plan execution had issues: #{result.status}"result.results.eachdo |task_id,task_result|
iftask_result.failed?puts"Task #{task_id} failed: #{task_result.failure.message}"endendendYou don't need an agent provider to run a plan. Tasks carry an arbitrary
payload, accept their agent (or a bare callable) directly, and receive the
outputs of the tasks they depend on:
# Stand-ins for your app's objects:moduleOrderApidefself.fetch(id)={id: id,status: "shipped"}endmoduleMailerdefself.deliver(order)="notified customer for order #{order[:id]}"endorder_id=42orchestrator=Agentic::PlanOrchestrator.newfetch=Agentic::Task.new(description: "fetch the order",agent_spec: {"name"=>"Fetcher","instructions"=>"fetch"},payload: order_id# any domain object, opaque to the framework)notify=Agentic::Task.new(description: "notify the customer",agent_spec: {"name"=>"Notifier","instructions"=>"notify"})# A callable receives the Task itself; its return value becomes the outputorchestrator.add_task(fetch,agent: ->(task){OrderApi.fetch(task.payload)})# Dependencies pipe their outputs into dependentsorchestrator.add_task(notify,[fetch],agent: ->(task){Mailer.deliver(task.output_of(fetch))# or task.dependency_outputs})result=orchestrator.execute_plan# no provider neededA plan-wide block acts as an agent factory when you do want one place that
builds agents: orchestrator.execute_plan { |task| build_agent_for(task) }.
Dependencies can be named, so they're declared and consumed under one word, and single-dependency chains have a shorthand:
orchestrator=Agentic::PlanOrchestrator.newnew_task=->(name){Agentic::Task.new(description: name,agent_spec: {"name"=>name,"instructions"=>name})}commits,debt,digest=new_task["commits"],new_task["debt"],new_task["digest"]orchestrator.add_task(commits,agent: ->(_t){12})orchestrator.add_task(debt,agent: ->(_t){3})orchestrator.add_task(digest,needs: {shipped: commits,owed: debt},agent: ->(task){"#{task.needs.shipped} commits shipped, #{task.needs.owed} TODOs owed"})previous_verse,next_verse=new_task["first verse"],new_task["second verse"]orchestrator.add_task(previous_verse,agent: ->(_t){"an old pond"})orchestrator.add_task(next_verse,[previous_verse],agent: ->(task){"#{task.previous_output} / a frog leaps in"# the sole dependency's output})Capability contracts can constrain values beyond type and presence —
enum: %w[standard express], min:/max: bounds, non_empty: true for
strings and arrays, and cross-field rules: checked over the whole input
(rules: {"express max 10 items" => ->(i) { i[:speed] != "express" || i[:quantity] <= 10 }}).
ValidationError#expectations carries the declared contract for each
violated key, so error messages can name what would have been legal.
The task_slot_acquired lifecycle hook fires when a task obtains a
concurrency slot (with waited: time), separating queue wait from run
time; retry policies consult an error's own retryable? verdict before
falling back to the retryable_errors list, and backoff jitter is on by
default so fleets don't retry in lockstep (backoff_jitter: :full draws
uniformly from [0, delay] for the hardest herd-flattening).
PlanOrchestrator#graph returns a frozen snapshot of the plan's topology
— including graph[:order] (topological sort), graph[:edges] (labeled
by needs: names), and graph[:stats] (per-task depth, max depth, max
fan-in) — for tools that render, review, or analyze plans.
Agentic::RateLimit is a credential-scoped ceiling shareable across
plans and clients (Agentic::LlmClient.new(config, limiter: rate_limit)):
concurrent by default, a rolling-window quota with
RateLimit.new(30, per: 60), or both laws at once via composition —
quota.and(pool). Cross-field rules may be structured —
rules: {air_weight_limit: {message: "...", fields: [:mode, :weight], check: ->(i) {...}}} —
and ValidationError#rule_violations reports each broken rule with its
identifier, message, and the fields it reads.
CapabilitySpecification#to_json_schema emits a contract side as
draft-07 JSON Schema for OpenAPI and validator toolchains; journal
replays expose per-task durations keyed by description (performance
baselines for free); and retry policies accept an injected rng: for
reproducible jitter timing.
Tasks run as fibers inside an async reactor. That means:
- IO-bound tasks scale nearly perfectly. LLM calls, HTTP requests,
sleep, database queries — anything that waits on IO yields to other tasks. Twenty 200ms API calls atconcurrency_limit: 20take ~200ms of wall clock, not 4 seconds (seeexamples/latency_lab.rbfor the measured curve). - CPU-bound tasks do not parallelize. Fibers share one thread; parsing, hashing, and number crunching gain nothing from a higher concurrency limit. If your tasks are compute, the limit is a queue, not a speedup.
execute_plancomposes with a running reactor: called insideAsync(e.g. under Falcon), it joins the current event loop instead of nesting a new one; standalone, it creates its own and blocks until done.
Start with capabilities — lambdas with declared contracts — and call
them directly. Add the orchestrator when you have an actual queue:
many independent items, or tasks with dependencies. Add the planner
(Agentic.run / TaskPlanner) when the task list itself should come from
an LLM. Each layer is optional and composes with the ones below it.
Agentic includes a powerful Extension System to help integrate with external systems and customize the framework's behavior. The Extension System consists of three main components:
Domain Adapters let you customize Agentic's behavior for specific domains like healthcare, finance, or legal:
# Create a domain adapter for healthcareadapter=Agentic::Extension.domain_adapter("healthcare")# Add domain-specific knowledgeadapter.add_knowledge(:terminology,{terms: ["patient","diagnosis","treatment"],specialty: "cardiology"})# Create a custom prompt adapterprompt_adapter=->(prompt,context){specialty=context[:domain_knowledge][:terminology][:specialty]"#{prompt} [Adapted for #{context[:domain]}#{specialty}]"}# Register the adapteradapter.register_adapter(:prompt,prompt_adapter)# Apply adaptation to a promptadapted_prompt=adapter.adapt(:prompt,"Describe the symptoms")# => "Describe the symptoms [Adapted for healthcare cardiology]"Protocol Handlers standardize connections to external systems and APIs:
# Get the protocol handlerhandler=Agentic::Extension.protocol_handler(default_headers: {"User-Agent"=>"AgenticApp/1.0"})# Create and register a protocol implementationhttp_protocol=Object.newdefhttp_protocol.send_request(endpoint,options)# Implement HTTP request logic{status: 200,body: "Response data"}end# Register the protocolhandler.register_protocol(:http,http_protocol,{timeout: 30})# Send a requestresponse=handler.send_request(:http,"/api/data",{method: "GET",headers: {"Accept"=>"application/json"}})The Plugin Manager handles third-party extensions and their lifecycle:
# Get the plugin managermanager=Agentic::Extension.plugin_manager# A plugin is any object with #initialize_plugin and #callclassGreeterPlugindefinitialize_plugin(context={})=truedefcall(name)="hello, #{name}"end# Register a plugin (register! replaces an existing registration)manager.register!("greeter",GreeterPlugin.new,{version: "1.0.0"})# Get and use a pluginif(plugin=manager.get("greeter"))result=plugin.call("agentic")end# Disable a pluginmanager.disable("greeter")Agentic provides structured representations for agents and tasks through several key value objects:
The AgentSpecification defines the requirements for an agent:
# Create an agent specificationagent_spec=Agentic::AgentSpecification.new(name: "ResearchAgent",description: "An agent that performs research",instructions: "Research the given topic thoroughly")# Convert to a hash representationhash=agent_spec.to_h# Create from a hash representationagent_spec_from_hash=Agentic::AgentSpecification.from_hash(hash)The TaskDefinition defines a task to be performed by an agent:
agent_spec=Agentic::AgentSpecification.new(name: "ResearchAgent",description: "Researches topics",instructions: "Research thoroughly")# Create a task definitiontask_def=Agentic::TaskDefinition.new(description: "Research AI trends",agent: agent_spec)# Convert to a hash representationhash=task_def.to_h# Create from a hash representationtask_def_from_hash=Agentic::TaskDefinition.from_hash(hash)The ExecutionPlan represents a plan with tasks and expected answer format:
agent_spec=Agentic::AgentSpecification.new(name: "ResearchAgent",description: "Researches topics",instructions: "Research thoroughly")task_def=Agentic::TaskDefinition.new(description: "Research AI trends",agent: agent_spec)# Create an expected answer formatexpected_answer=Agentic::ExpectedAnswerFormat.new(format: "PDF",sections: ["Summary","Trends","Conclusion"],length: "10 pages")# Create an execution planplan=Agentic::ExecutionPlan.new([task_def],expected_answer)# Convert to a formatted stringputsplan.to_s# Convert to a hash representationhash=plan.to_hThe AgentConfig provides a configuration object for agents:
# Create an agent configurationconfig=Agentic::AgentConfig.new(name: "ResearchAgent",role: "Research Assistant",backstory: "You are an expert researcher with decades of experience",tools: ["web_search","document_processor"],llm_config: Agentic::LlmConfig.new(model: "gpt-4o-mini"))# Access configuration propertiesputsconfig.nameputsconfig.role# Convert to a hash representationhash=config.to_hAgentic features a powerful Capability System that enables dynamic agent composition, self-assembly, and persistence. The system allows agents to be constructed with precisely the capabilities they need for specific tasks, and to be stored for future reuse.
The capability registry manages capability specifications and providers:
# Define a capability specificationtext_gen_capability=Agentic::CapabilitySpecification.new(name: "text_generation",description: "Generates text based on a prompt",version: "1.0.0",inputs: {prompt: {type: "string",required: true,description: "The prompt to generate text from"}},outputs: {response: {type: "string",description: "The generated text"}})# Create a provider for the capabilitytext_gen_provider=Agentic::CapabilityProvider.new(capability: text_gen_capability,implementation: ->(inputs){{response: "Generated text for: #{inputs[:prompt]}"}})# Register the capability with the systemAgentic.register_capability(text_gen_capability,text_gen_provider)The assembly engine dynamically constructs agents based on task requirements:
# Define a tasktask=Agentic::Task.new(description: "Generate a report on AI trends",agent_spec: Agentic::AgentSpecification.new(name: "Report Generator",description: "An agent that generates reports",instructions: "Generate a comprehensive report on the topic"),input: {topic: "AI trends in 2023",format: "markdown"})# Assemble an agent for the taskagent=Agentic.assemble_agent(task)# The agent will automatically have capabilities like "text_generation"# based on the task requirementsAgents can be stored and retrieved for future use:
# Store an agent for future useagent_id=Agentic.agent_store.store(agent,name: "report_generator")# Later, retrieve the agent by namestored_agent=Agentic.agent_store.build_agent("report_generator")# Or retrieve by IDstored_agent=Agentic.agent_store.build_agent(agent_id)# List all stored agentsagents=Agentic.agent_store.allCapabilities can be composed into higher-level capabilities:
registry=Agentic::AgentCapabilityRegistry.instance# Register the two base capabilities (bare lambdas are providers)[["text_generation",->(i){{response: "report on: #{i[:prompt]}"}}],["data_analysis",->(i){{summary: "avg sales #{i[:data][:sales].sum / i[:data][:sales].size}"}}]].eachdo |name,impl|
spec=Agentic::CapabilitySpecification.new(name: name,description: name,version: "1.0.0")Agentic.register_capability(spec,Agentic::CapabilityProvider.new(capability: spec,implementation: impl))end# Compose multiple capabilities into a new oneregistry.compose("comprehensive_report","Generates a comprehensive report with research and formatting","1.0.0",[{name: "text_generation",version: "1.0.0"},{name: "data_analysis",version: "1.0.0"}],->(providers,inputs){analysis=providers[1].execute(data: inputs[:data])report=providers[0].execute(prompt: "Generate a report on: #{analysis[:summary]}"){report: report[:response],analysis: analysis}})# Use the composed capability like any otherprovider=registry.get_provider("comprehensive_report")result=provider.execute(data: {sales: [120,90,143]})Agentic features a Learning System that enables agents to improve over time by capturing execution metrics, recognizing patterns, and optimizing strategies. The Learning System consists of three main components:
The ExecutionHistoryStore captures and stores execution metrics and performance data:
# Create a history storehistory_store=Agentic::Learning::ExecutionHistoryStore.new(storage_path: "~/.agentic/history",anonymize: true,retention_days: 30)# Record task execution metricshistory_store.record_execution(task_id: "task-123",agent_type: "research_agent",duration_ms: 1500,success: true,metrics: {tokens_used: 2000,quality_score: 0.85})# Query execution historyresearch_tasks=history_store.get_history(agent_type: "research_agent",success: true)# Calculate metricsavg_tokens=history_store.get_metric(:tokens_used,{agent_type: "research_agent"},:avg)The PatternRecognizer analyzes execution history to identify patterns and optimization opportunities:
history_store=Agentic::Learning::ExecutionHistoryStore.new(storage_path: "history")15.timesdo |i|
history_store.record_execution(task_id: "t#{i}",agent_type: "research_agent",duration_ms: 1000 + i * 20,success: i % 4 != 0,metrics: {tokens_used: 1500 + i})end# Create a pattern recognizerrecognizer=Agentic::Learning::PatternRecognizer.new(history_store: history_store,min_sample_size: 10)# Analyze agent performancepatterns=recognizer.analyze_agent_performance("research_agent")# Analyze correlations between propertiescorrelation=recognizer.analyze_correlation(:duration_ms,:tokens_used)# Get optimization recommendationsrecommendations=recognizer.recommend_optimizations("research_agent")The StrategyOptimizer generates improvements for prompts, parameters, and task sequences:
history_store=Agentic::Learning::ExecutionHistoryStore.new(storage_path: "history")15.timesdo |i|
history_store.record_execution(task_id: "t#{i}",agent_type: "research_agent",duration_ms: 1000 + i * 20,success: i % 4 != 0,metrics: {tokens_used: 1500 + i})endrecognizer=Agentic::Learning::PatternRecognizer.new(history_store: history_store)# Create a strategy optimizer (add llm_client: for LLM-enhanced optimizations)optimizer=Agentic::Learning::StrategyOptimizer.new(pattern_recognizer: recognizer,history_store: history_store)# Optimize a prompt templateimproved_prompt=optimizer.optimize_prompt_template("Research the topic: {topic}","research_agent")# Optimize LLM parametersimproved_params=optimizer.optimize_llm_parameters({temperature: 0.7,max_tokens: 2000},"research_agent",optimization_strategy: :balanced)# Generate performance reportreport=optimizer.generate_performance_report("research_agent")The Learning System can be automatically integrated with the PlanOrchestrator:
# Create the learning systemlearning_system=Agentic::Learning.create(storage_path: "history",auto_optimize: false)# Hooks are a construction-time seam: pass the learning system's hooks# to the orchestrator (chaining any hooks you already have)orchestrator=Agentic::PlanOrchestrator.new(lifecycle_hooks: Agentic::Learning.lifecycle_hooks(learning_system))# The orchestrator now records execution metrics automaticallytask=Agentic::Task.new(description: "work",agent_spec: {"name"=>"w","instructions"=>"w"})orchestrator.add_task(task,agent: ->(_t){:done})orchestrator.execute_planlearning_system[:history_store].get_history.size# => 1You can configure the OpenAI API key in several ways:
Environment variable:
export OPENAI_ACCESS_TOKEN="your_api_key"
Configuration file: Create
.agentic.ymlin your project directory or home directory:api_token: "your_api_key"model: "gpt-4o-mini"
In your Ruby code:
Agentic.configuredo |config| config.access_token="your_api_key"end
You can customize the LLM behavior:
config=Agentic::LlmConfig.new(model: "gpt-4o-mini",max_tokens: 500,temperature: 0.7,top_p: 1.0,frequency_penalty: 0,presence_penalty: 0)# Use in TaskPlannerplanner=Agentic::TaskPlanner.new("Write a blog post",config)# Or with LlmClient directlyclient=Agentic::LlmClient.new(config)After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
Agentic includes a comprehensive test suite with both unit tests and integration tests. To run all tests:
rake specTo run specific test categories:
# Run just integration tests
rspec spec/integration/
# Run a specific integration test file
rspec spec/integration/plan_orchestrator_integration_spec.rb
# Run with verbose output
rspec spec/integration/ --format documentationThe integration tests cover real-world scenarios including:
- Complex orchestration patterns with dependencies
- Learning system's pattern recognition capabilities
- Agent specification and task definition interactions
- Timeout and retry behaviors
- Error handling in complex scenarios
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/codenamev/agentic. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the Agentic project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.