Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

215 Commits

Repository files navigation

Agentic

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.

What's New in v0.2.0

🚀 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

Demo Video

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add agentic

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install agentic

Usage

Command-Line Interface

Agentic 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 capabilities

The 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.

Programmatic API

To use Agentic in your Ruby code:

Basic Usage

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

Working with Agents

# 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

Plan Orchestration

# 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}"endendend

Passing work to the orchestrator directly

You 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 needed

A 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.

The concurrency contract

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 at concurrency_limit: 20 take ~200ms of wall clock, not 4 seconds (see examples/latency_lab.rb for 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_plan composes with a running reactor: called inside Async (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.

When to reach for which layer

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.

Extension System

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

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

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"}})

Plugin Manager

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")

Agent Specification and Task Definition

Agentic provides structured representations for agents and tasks through several key value objects:

Agent Specification

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)

Task Definition

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)

Execution Plan

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_h

Agent Configuration

The 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_h

Agent Capabilities System

Agentic 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.

Capability Registry

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)

Agent Assembly Engine

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 requirements

Agent Persistence

Agents 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.all

Capability Composition

Capabilities 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]})

Learning System

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:

Execution History Store

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)

Pattern Recognizer

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")

Strategy Optimizer

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")

Integrating with Plan Orchestrator

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# => 1

Configuration

Setting up the OpenAI API Key

You can configure the OpenAI API key in several ways:

  1. Environment variable:

    export OPENAI_ACCESS_TOKEN="your_api_key"
  2. Configuration file: Create .agentic.yml in your project directory or home directory:

    api_token: "your_api_key"model: "gpt-4o-mini"
  3. In your Ruby code:

    Agentic.configuredo |config|
    config.access_token="your_api_key"end

LLM Configuration

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)

Development

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.

Running Tests

Agentic includes a comprehensive test suite with both unit tests and integration tests. To run all tests:

rake spec

To 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 documentation

The 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.

Contributing

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.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Agentic project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

About

A simple Ruby command-line tool to build and run AI Agents in a plan-and-execute fashion.

Resources

Code of conduct

Contributing

Stars

27 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages