composable_agents
A Ruby framework for building composable, prompt-driven AI agent pipelines โ mix, match, and orchestrate agents into reusable workflows.
composable_agents is a Ruby gem that lets you build modular AI agent pipelines ๐งฉ โ compose simple agents together into complex, resumable workflows.
Think of it as LEGOยฎ for AI agents: each agent is a self-contained unit that takes input artifacts, processes them (via an LLM, custom Ruby code, or a sub-agent), and produces output artifacts. You can:
- ๐ง Create prompt-driven agents with role, objective, instructions, and constraints
- ๐ Chain agents together so the output of one becomes the input of another
- ๐ฆ Define typed artifact contracts with validation for inputs/outputs
- ๐พ Resume interrupted runs โ long workflows keep their state between executions
- ๐ฃ๏ธ Let agents ask users questions when they need clarification
- ๐ฏ Integrate with multiple LLM backends via cline-rb or ai-agents
- ๐ Use flexible prompt rendering (Markdown, or heavy Markdown with structured outputs)
Whether you're building a code review assistant, a document summarizer, or a multi-step research pipeline, composable_agents gives you the building blocks to design, test, and run AI agent systems โ all from Ruby.
Table of contents
- Quick start
- Installation
- Basic usage: create a composed pipeline of agents
- Using the Cline backend instead
- Next steps
- Requirements
- Features
- Public API
- Module constant
ComposableAgents::VERSION
- Core agent classes
ComposableAgents::AgentComposableAgents::RubyAgent < AgentComposableAgents::InstructionsComposableAgents::PromptDrivenAgent < Agent
- LLM backend agent classes
ComposableAgents::AiAgents::Agent < PromptDrivenAgentComposableAgents::Cline::Agent < PromptDrivenAgentComposableAgents::Cline::MissingSkillError < RuntimeErrorComposableAgents::Cline::ClineError < RuntimeError
- Mixins
ComposableAgents::Logger < ::LoggerComposableAgents::Mixins::ResumableComposableAgents::Mixins::UserInteractionComposableAgents::Mixins::ArtifactContractComposableAgents::Mixins::RetriableComposableAgents::Mixins::RetriableArtifacts
- Additional notes
- Module constant
- Documentation
- How it works
- Architecture overview ๐๏ธ
- Composition: the pipeline pattern ๐
- Prompt-driven execution flow ๐ง
- Prompt rendering strategies ๐
- LLM backends: ai-agents vs cline-rb ๐ฏ
- Mixin system โ augment agents with capabilities ๐งฐ
- Instruction system ๐
- Code loading โก
- State persistence for resumable workflows ๐พ
- Development
- Prerequisites
- Clone the repository
- Install dependencies
- Project structure (high-level)
- Run tests
- Test debugging
- Code coverage
- Code linting
- Generate documentation
- Package the gem
- Common development tasks
- Adding a new feature
- Adding a test helper
- Running examples
- CI pipeline
- Release process
- Contributing
- ๐ Issues
- ๐ด Fork & Branch
- ๐งช Setting up test dependencies & running tests
- โ Linting & code style
- ๐ CI / Build pipeline
- ๐ Pull request guidelines
- ๐ License
- License
Quick start
Installation
Add the gem to your application's Gemfile:
bundle add composable_agentsOr install it globally:
gem install composable_agentsRequires Ruby >= 3.1.
Basic usage: create a composed pipeline of agents
Here's a minimal example that chains three agents together to build a holiday planner.
require 'composable_agents'
# --- 1. Define the agents ---
# An LLM-powered agent (uses ai-agents gem, needs an API key)
class ItineraryAgent < ComposableAgents::AiAgents::Agent
def initialize
super(
role: 'You are a travel planner',
objective: 'Find cities matching the user preferences',
system_instructions: <<~EO_INSTRUCTIONS,
Get the user preferences from the artifact named `preferences`.
Find the best cities.
Create an artifact named `cities` as a JSON list of city names.
EO_INSTRUCTIONS
model: 'openai/gpt-4o-mini' # or any model supported by your provider
)
end
end
# A plain Ruby agent (no LLM needed)
class BudgetAgent < ComposableAgents::RubyAgent
def initialize
super(proc do |input_artifacts|
cities = JSON.parse(input_artifacts[:cities])
{ budget: cities.size * 1000 }
end)
end
end
# --- 2. Configure the LLM provider (for ai-agents backend) ---
require 'agents'
Agents.configure do |config|
config.openrouter_api_key = ENV.fetch('OPENROUTER_API_KEY', nil)
end
# --- 3. Compose and run them ---
preferences = { preferences: 'Cultural city trips in Europe' }
itinerary_outputs = ItineraryAgent.new.run(**preferences)
budget_outputs = BudgetAgent.new.run(**itinerary_outputs)
puts "Cities: #{itinerary_outputs[:cities]}"
puts "Budget: $#{budget_outputs[:budget]}"Using the Cline backend instead
If you prefer the cline-rb backend, set the CLINE_API_KEY environment variable:
# Use Cline-powered agents instead
itinerary_agent = ComposableAgents::Cline::Agent.new(
role: 'You are a travel planner',
objective: 'Find cities matching the user preferences',
model: 'anthropic/claude-sonnet-4.6',
api_key: ENV.fetch('CLINE_API_KEY', nil),
input_artifacts_contracts: { preferences: 'User travel preferences' },
output_artifacts_contracts: { cities: 'List of best cities' }
)Next steps
- Browse the examples/ directory for full working scripts.
- Use the
ArtifactContractmixin to validate inputs/outputs. - Use the
Resumablemixin to persist and resume long-running workflows. - Use the
AiAgentUserInteractionmixin to let agents ask the user questions.
Requirements
- Ruby >= 3.1 โ The gem requires Ruby 3.1 or newer.
- Bundler โ Used to install the gem and manage its dependencies (comes with Ruby).
-
Node.js โ Required at runtime by the
cline-rbbackend for pseudo-terminal (PTY) support vianode-pty. -
An LLM provider API key โ One of the following (depending on the agent backend you use):
-
OpenRouter API key โ Set via the
OPENROUTER_API_KEYenvironment variable when using theAiAgentsbackend. -
Cline API key โ Set via the
CLINE_API_KEYenvironment variable when using theClinebackend.
-
OpenRouter API key โ Set via the
Features
composable_agents is a Ruby framework for building modular, prompt-driven AI agent pipelines ๐งฉ. Here are its key capabilities:
- ๐ง Three agent types โ Create LLM-powered agents via
PromptDrivenAgent, wrap plain Ruby logic withRubyAgent, or compose complex multi-step workflows using theResumablemixin. - ๐ Composable pipelines โ Pass output artifacts from one agent directly as input to another, forming reusable, chainable workflows.
- ๐ฏ Multiple LLM backends โ Plug into different AI providers via the
ai-agentsgem (OpenRouter) orcline-rb(Claude, GPT, and many more). - ๐ Three prompt rendering strategies โ Choose between clean Markdown for simple agents, MarkdownHeavy with structured output parsing, execution checklists, and typed artifact support for complex agentic systems, or MarkdownHeavyFiles which persists input and output artifacts as files on disk.
- ๐ฆ Typed artifact contracts โ Define and validate input/output schemas with descriptions, optional flags, and types (
:text,:markdown,:json). The framework raises clearMissingInputArtifactError,MissingOutputArtifactError, orArtifactTypeErroron violations. - ๐พ Resumable execution โ Persist step-by-step state to disk (via the
Resumablemixin). Interrupted runs can be resumed seamlessly โ previously completed steps are skipped, saving time and API costs. - ๐ Conditional retries โ Wrap an agent's
#runwith theRetriablemixin to retry on transient failures. Register condition procs via#retry_onthat receive the agent and any raised exception (ornil) and return a policy hash (optional:wait_secs, optional:modifier) to wait-and-retry, ornilto skip. Unhandled exceptions are re-raised when no policy applies. - ๐ *Automatic artifact retry โ Prepend the RetriableArtifacts mixin to a PromptDrivenAgent subclass to automatically re-run when output artifacts declared in contracts are missing or in error. Configurable via
artifacts_errors_max_retries(default: 3); raisesArtifactsErrorsMaxRetriesExceededErrorwhen the limit is exceeded. - ๐ฃ๏ธ User interaction โ Agents can ask users clarifying questions mid-execution. Works out of the box via the terminal, or through an
ai-agentstool integration for LLM-controlled workflows. - ๐ Automatic conversation tracking โ Every prompt and response is automatically recorded with timestamps in a structured
conversationstore, ready for debugging or replay. - โ๏ธ Flexible instruction system โ Use raw text, structured ordered-lists, or a mix of both to define agent instructions, rendered consistently by the chosen strategy.
- ๐ ๏ธ Cline skill support โ Select and enable specific Cline skills per agent, with automatic dependency resolution.
- ๐ State export/import โ Agents can serialize and restore their internal state via
export_state/import_state, enabling deep integration with the resumable workflow system. - ๐ Debug logging โ Toggle verbose debug output with the
COMPOSABLE_AGENTS_DEBUG=1environment variable. - ๐ Markdown header alignment โ A built-in utility normalizes Markdown header levels across composed prompts, maintaining a clean document hierarchy.
Public API
This section documents all public entry points of the composable_agents gem. The project is a Ruby library (not a CLI) โ users install the gem and use its classes and mixins in their own Ruby code.
Module constant
ComposableAgents::VERSION
-
Description: The current version of the gem (
'0.1.0'). - Full documentation: version.rb on GitHub
Core agent classes
ComposableAgents::Agent
- Description: Abstract base class for all agents. An agent is a computational unit that transforms input artifacts into output artifacts. Agents are stateless by default.
-
Public methods:
-
#initialize(name: nil, composable_agents_dir: '.composable_agents')โ Create a new agent with an optional name and a working directory. -
#nameโ Return the agent's name (String, ornil). -
#full_nameโ Return a human-readable full name for logs and traces (can be overridden by subclasses).
-
-
Usage example:
class MyCustomAgent < ComposableAgents::Agent def run(**input_artifacts) # Process input_artifacts and return output artifacts { result: input_artifacts[:data].upcase } end end agent = MyCustomAgent.new(name: 'uppercaser') output = agent.run(data: 'hello') puts output[:result] # => "HELLO"
- Full documentation: Agent on RubyDoc | agent.rb on GitHub
ComposableAgents::RubyAgent < Agent
-
Description: An agent that wraps arbitrary Ruby logic as a
Proc. No LLM is needed โ ideal for deterministic or simple processing steps. -
Public methods:
-
#initialize(processor, *args, **kwargs)โ Theprocessoris a#call-able object (e.g. aProc) that receives input artifacts and returns output artifacts. -
#run(**input_artifacts)โ Execute the proc with the given input artifacts.
-
-
Usage example:
# A simple agent that doubles a number double_agent = ComposableAgents::RubyAgent.new( proc { |inputs| { double: inputs[:number] * 2 } } ) result = double_agent.run(number: 21) puts result[:double] # => 42
- Full documentation: RubyAgent on RubyDoc | ruby_agent.rb on GitHub
ComposableAgents::Instructions
-
Description: Normalizes instructions (system prompts, user prompts) into a canonical list format. Supports plain text strings and structured hashes (e.g. with
ordered_listkeys). IncludesEnumerable. -
Public methods:
-
#initialize(instructions)โ Accepts aString, anArray, or aHash{text:, ordered_list:}. -
#each(&)โ Iterate over each instruction as(type, content)pairs.
-
-
Usage example:
instructions = ComposableAgents::Instructions.new({ ordered_list: ['Step one', 'Step two'] }) instructions.each do |type, content| puts "#{type}: #{content}" end
- Full documentation: Instructions on RubyDoc | instructions.rb on GitHub
ComposableAgents::PromptDrivenAgent < Agent
- Description: An agent that uses a prompt rendering strategy (Markdown, MarkdownHeavy, or MarkdownHeavyFiles) to build prompts for an LLM. It manages role, objective, instructions, constraints, and a conversation history.
-
Public methods:
-
#role/#role=โ Agent's role description. -
#objective/#objective=โ Agent's objective. -
#system_instructions/#system_instructions=โ Instructions for the agent. -
#constraints/#constraints=โ Constraints the agent must respect. -
#conversationโ Read the conversation history (array of message hashes). -
#initialize(*args, role:, objective:, system_instructions:, constraints:, strategy:, **kwargs)โ Thestrategydefaults toPromptRenderingStrategy::Markdown. -
#full_nameโ Human-readable name for logs. -
#run(user_instructions: nil, **input_artifacts)โ Execute the agent and produce output artifacts.
-
-
Usage example:
agent = ComposableAgents::PromptDrivenAgent.new( role: 'A helpful assistant', objective: 'Answer user questions' ) # Subclass and implement #prompt(user_prompt) to provide the LLM backend.
- Full documentation: PromptDrivenAgent on RubyDoc | prompt_driven_agent.rb on GitHub
LLM backend agent classes
ComposableAgents::AiAgents::Agent < PromptDrivenAgent
-
Description: An agent that uses the
ai-agentsgem as its LLM backend. Requires an OpenRouter API key configured viaAgents.configure. -
Public methods:
-
#initialize(*args, model:, params:, handoff_agents:, **kwargs)โ Specify themodel(e.g.'openai/gpt-4o-mini'), optionalparamsfor model configuration, and a list ofhandoff_agents. -
#full_nameโ Returns"<name> (AiAgent <model>)".
-
-
Usage example:
require 'agents' Agents.configure { |c| c.openrouter_api_key = ENV['OPENROUTER_API_KEY'] } agent = ComposableAgents::AiAgents::Agent.new( role: 'Travel planner', objective: 'Suggest destinations', system_instructions: 'Create an artifact named `cities` with city names.', model: 'openai/gpt-4o-mini' ) result = agent.run(preferences: 'beach holidays') puts result[:cities]
- Full documentation: AiAgents::Agent on RubyDoc | ai_agents/agent.rb on GitHub
ComposableAgents::Cline::Agent < PromptDrivenAgent
-
Description: An agent that uses the
cline-rbgem as its LLM backend. Requires aCLINE_API_KEYenvironment variable. Automatically prepends theArtifactContractmixin. -
Public methods:
-
#initialize(*args, strategy:, provider:, model:, api_key:, configure_provider:, configure_global:, skills:, cli_options:, **kwargs)โ Configure provider, model, optional skill list, and CLI options. Defaults to'cline'provider,'anthropic/claude-sonnet-4.6'model. -
#full_nameโ Returns"<name> (Cline <provider>/<model>)".
-
-
Usage example:
agent = ComposableAgents::Cline::Agent.new( role: 'Travel planner', objective: 'Suggest destinations', model: 'deepseek/deepseek-v4-flash', api_key: ENV['CLINE_API_KEY'], input_artifacts_contracts: { preferences: 'User preferences' }, output_artifacts_contracts: { cities: 'City list' } ) result = agent.run(preferences: 'cultural trips in Italy') puts result[:cities]
- Full documentation: Cline::Agent on RubyDoc | cline/agent.rb on GitHub
ComposableAgents::Cline::MissingSkillError < RuntimeError
-
Description: Raised by
Cline::Agentwhen a referenced skill is not found in the global or project Cline configuration. - Full documentation: cline/agent.rb on GitHub
ComposableAgents::Cline::ClineError < RuntimeError
-
Description: Raised by
Cline::Agentwhen a Cline CLI run fails. - Full documentation: cline/cline_error.rb on GitHub
Mixins
Mixins are prepend-ed into an agent class to add specific capabilities.
ComposableAgents::Logger < ::Logger
-
Description: Standard Ruby
::Logger-based logger for agents. Debug logging is enabled by settingCOMPOSABLE_AGENTS_DEBUG=1in the environment (which sets the logger level toDEBUG), and log fields (like the agent full name) can be prefixed to messages via#tagged. -
Usage example:
# Enable debug logs ENV['COMPOSABLE_AGENTS_DEBUG'] = '1' puts ComposableAgents::Logger.instance.level == Logger::DEBUG # => true
- Full documentation: ComposableAgents::Logger on RubyDoc | logger.rb on GitHub
ComposableAgents::Mixins::Resumable
- Description: Adds step-level persistence and resumption capabilities to agents. Steps and their artifacts are serialized to JSON on disk so that long-running workflows can be interrupted and resumed.
-
Public methods:
-
#initialize(*args, run_id:, **kwargs)โ Therun_ididentifies the persisted run.
-
-
Usage example:
class WorkflowAgent < ComposableAgents::Agent prepend ComposableAgents::Mixins::Resumable def run(**inputs) @artifacts = inputs step(:process_data) { @artifacts[:result] = @artifacts[:data].upcase } step(:finalize) { @artifacts[:done] = true } @artifacts end end # If interrupted between steps, re-running with the same run_id skips completed steps WorkflowAgent.new(run_id: 'my_workflow').run(data: 'hello')
- Full documentation: Mixins::Resumable on RubyDoc | resumable.rb on GitHub
ComposableAgents::Mixins::UserInteraction
-
Description: Adds a simple question-and-answer interface for agents. By default, questions are printed to the terminal and answers are read from
$stdin. Override#answer_tofor custom behavior. -
Public methods:
-
#ask(question)โ Ask the user a question and return the answer.
-
-
Usage example:
class InteractiveAgent < ComposableAgents::Agent include ComposableAgents::Mixins::UserInteraction def run(**) name = ask('What is your name?') { greeting: "Hello, #{name}!" } end end
- Full documentation: Mixins::UserInteraction on RubyDoc | user_interaction.rb on GitHub
ComposableAgents::Mixins::ArtifactContract
-
Description: Validates input and output artifacts against declared contracts before and after running an agent. Contracts specify description, optionality, and expected type (
:text,:markdown,:json). -
Public error classes:
-
MissingInputArtifactError < RuntimeErrorโ Raised when required input artifacts are missing. -
MissingOutputArtifactError < RuntimeErrorโ Raised when expected output artifacts are missing after execution. -
ArtifactTypeError < RuntimeErrorโ Raised when an artifact's content does not match its declared type.
-
-
Public methods:
-
#initialize(*args, input_artifacts_contracts:, output_artifacts_contracts:, **kwargs)โ Contracts areHash{Symbol => String}(simple description) orHash{Symbol => Hash{description:, optional:, type:}}.
-
-
Usage example:
class ValidatedAgent < ComposableAgents::Agent prepend ComposableAgents::Mixins::ArtifactContract def input_artifacts_contracts { name: { description: 'User name', type: :text } } end def output_artifacts_contracts { greeting: { description: 'Greeting message', type: :text } } end def run(**inputs) { greeting: "Hello, #{inputs[:name]}!" } end end agent = ValidatedAgent.new agent.run(name: 'World') # => { greeting: "Hello, World!" } agent.run(foo: 'bar') # Raises MissingInputArtifactError
-
Full documentation: Mixins::ArtifactContract on RubyDoc | artifact_contract.rb on GitHub
ComposableAgents::Mixins::Retriable
-
Description: Adds conditional retry capabilities to agents. Retry condition procs are registered with
#retry_on(or#with_retry_onfor temporary activation) and are evaluated at the end of each run. A proc receives the agent instance and the exception raised byrun(ornilif none) and returns eithernil(no retry) or a policy hash with optional keys to describe the policy:-
:wait_secsโ Wait time in seconds before retrying (defaults to 0). -
:modifierโ Proc called with the agent before retrying ( defaults to doing nothing). -
:new_input_artifactsโ Hash of input artifacts to add to the retried run (defaults to{}). When a proc returns a policy, the agent waitswait_secsseconds, applies themodifierproc (if any), merges thenew_input_artifactsinto the retry input, and re-runs. Ifrunraised an exception and no proc returned a retry policy, the exception is re-raised to the caller.
-
-
Public methods:
-
#retry_on(proc)โ Register a retry condition proc. -
#with_retry_on(proc, &block)โ Register a retry proc for the duration of a block, then unregister it when the block returns (even if it raises).
-
-
Usage example:
class MyAgent < ComposableAgents::Agent prepend ComposableAgents::Mixins::Retriable def run(**inputs) # Your processing here, may raise transient errors { result: inputs[:input].upcase } end end agent = MyAgent.new # Retry once with a short wait when the first run fails. agent.retry_on(lambda do |agent_instance, exception| agent_instance.run_count == 1 ? { wait_secs: 1 } : nil end) agent.run(input: 'hello')
- Full documentation: Mixins::Retriable on RubyDoc | retriable.rb on GitHub
ComposableAgents::Mixins::RetriableArtifacts
-
Description: Automatically retries the run when output artifacts declared in contracts are missing or in error on prompt-driven agents. Requires both the ArtifactContract and Retriable mixins to be prepended, and must be prepended on an agent descending from PromptDrivenAgent.
-
Optional artifacts: Artifacts can be marked as
optional: truein their contract. When an optional artifact is missing, the run will not retry โ it will complete successfully. This is useful for artifacts that are nice-to-have but not required for the agent to function.Example contract with optional artifact:
output_artifacts_contracts: { result: 'Final result', # required artifact logs: { description: 'Execution logs', optional: true } # optional artifact }
-
Configuration: Configurable via
artifacts_errors_max_retries(default: 3); raisesArtifactsErrorsMaxRetriesExceededErrorwhen the limit is exceeded. -
Public methods:
-
#initialize(*args, artifacts_errors_max_retries: 3, **kwargs)โ Registers a retry policy that re-executes the run with a prompt asking for missing artifacts when output artifacts are missing or in error. The artifacts_errors_max_retries parameter (default: 3) sets the maximum number of retries before raising ArtifactsErrorsMaxRetriesExceededError.
-
-
Usage example:
class MyAgent < ComposableAgents::PromptDrivenAgent prepend ComposableAgents::Mixins::ArtifactContract prepend ComposableAgents::Mixins::Retriable prepend ComposableAgents::Mixins::RetriableArtifacts def output_artifacts_contracts { result: "Final result", logs: "Execution logs" } end def run(**inputs) # Processing logic that may produce incomplete output artifacts super end end agent = MyAgent.new(artifacts_errors_max_retries: 3) agent.run # Automatically retries if result or logs are missing or in error
-
Raises: ArtifactsErrorsMaxRetriesExceededError when the maximum number of retries is exceeded.
ComposableAgents::Mixins::RetriableCline
-
Description: Automatically retries the run when Cline raises errors (such as
UnexpectedExitStatusErrororUnexpectedInteractiveSessionError) using exponential backoff with jitter. Requires theRetriablemixin to be prepended first, and must be prepended on an agent descending fromComposableAgents::Cline::Agent. -
Public methods:
-
#initialize(*args, cline_errors_max_retries: 3, cline_errors_retry_initial_delay_secs: 5, cline_errors_retry_backoff_factor: 2, cline_errors_retry_max_delay_secs: 60, cline_errors_retry_jitter: 0.1, **kwargs)โ Registers a retry policy that re-executes the run with exponential backoff when Cline errors occur. Configuration options include:-
cline_errors_max_retries: Maximum number of retries (default: 3) -
cline_errors_retry_initial_delay_secs: Initial delay in seconds before first retry (default: 5) -
cline_errors_retry_backoff_factor: Multiplier for each subsequent retry (default: 2) -
cline_errors_retry_max_delay_secs: Maximum delay between retries (default: 60) -
cline_errors_retry_jitter: Amount of random jitter to add (default: 0.1 = 10%)
-
-
#run(user_instructions: nil, **input_artifacts)โ Re-executes the run, using stored user instructions from interactive session errors if applicable.
-
-
Usage example:
class MyClineAgent < ComposableAgents::Cline::Agent prepend ComposableAgents::Mixins::Retriable prepend ComposableAgents::Mixins::RetriableCline def run(**inputs) super end end agent = MyClineAgent.new(cline_errors_max_retries: 3, cline_errors_retry_initial_delay_secs: 2) agent.run # Automatically retries on Cline errors with exponential backoff
-
Raises: Various Cline error classes (e.g.,
ComposableAgents::Cline::ClineError,::Cline::Cli::UnexpectedExitStatusError,::Cline::Cli::UnexpectedInteractiveSessionError) when the maximum number of retries is exceeded.
Additional notes
-
No executables / CLI โ This gem is a library only; there are no scripts in
bin/. -
Prompt rendering strategies (
PromptRenderingStrategy::Markdown,PromptRenderingStrategy::MarkdownHeavy, andPromptRenderingStrategy::MarkdownHeavyFiles) are not part of the public API themselves โ they are included automatically by the agent'sstrategy:parameter. A strategy never definesinitialize: when it can be customized, it exposes public accessors on the agent, resolved lazily and set at any time after construction (e.g.PromptRenderingStrategy::MarkdownHeavyFiles#artifacts_directory=). -
The
AiAgentUserInteractionmixin (ComposableAgents::Mixins::AiAgentUserInteraction) is an internal bridge that combinesUserInteractionwithAiAgents::Agent; it is used by prepending it to anAiAgents::Agentsubclass. - See the examples/ directory for complete, runnable scripts demonstrating all the above APIs.
Documentation
- ๐ Main README โ README.md โ Overview, installation, usage, and development instructions.
- ๐ RubyDoc.info (API reference) โ composable_agents on RubyDoc โ Auto-generated YARD documentation for all public classes, modules, and methods. Covers the full API with 100% documented coverage.
- ๐ GitHub Repository โ github.com/Muriel-Salvan/composable_agents โ Source code, issue tracker, and pull requests.
- ๐ License โ BSD-3-Clause License โ The gem is available as open source under the BSD 3-Clause License.
-
๐ก Examples โ
examples/directory โ Runnable Ruby scripts demonstrating simple pipelines, resumable workflows, and user interaction patterns. - โ๏ธ CI / Build โ Continuous Integration workflow โ GitHub Actions configuration for running tests and publishing releases.
-
๐ Source Code โ Browse the
lib/directory for inline YARD-annotated source documentation of all agents, mixins, and rendering strategies:-
Agentโ Abstract base class -
PromptDrivenAgentโ LLM-prompted agent -
RubyAgentโ Plain Ruby logic agent -
AiAgents::Agentโ ai-agents backend -
Cline::Agentโ cline-rb backend -
Instructionsโ Instruction system -
Mixins::Resumableโ Resumable workflow mixin -
Mixins::ArtifactContractโ Artifact validation mixin -
Mixins::UserInteractionโ User question-asking mixin -
Mixins::Retriableโ Conditional retry mixin -
Loggerโ Standard logger class -
PromptRenderingStrategy::Markdownโ Markdown prompt strategy -
PromptRenderingStrategy::MarkdownHeavyโ Heavy Markdown prompt strategy -
PromptRenderingStrategy::MarkdownHeavyFilesโ Heavy Markdown prompt strategy persisting artifacts as files -
Utils::Markdownโ Markdown header alignment utilities
-
How it works
composable_agents is built on a simple principle: every agent is a stateless function that takes input artifacts (a Hash{Symbol => Object}) and returns output artifacts. Chain them โ one agent's outputs become the next agent's inputs.
Architecture overview ๐๏ธ
classDiagram
class Agent {
+run(**input_artifacts)~Hash~
+full_name()~String~
}
class PromptDrivenAgent {
+String role
+String objective
+String system_instructions
+String constraints
+Array conversation
#prompt(user_prompt)~String~
}
class RubyAgent {
-Proc processor
+run(**input_artifacts)~Hash~
}
class AiAgents_Agent {
-AgentRunner agent_runner
#prompt(user_prompt)~String~
}
class Cline_Agent {
-Cline::Config cline_config
#prompt(user_prompt)~String~
}
Agent <|-- PromptDrivenAgent : extends
Agent <|-- RubyAgent : extends
PromptDrivenAgent <|-- AiAgents_Agent : extends
PromptDrivenAgent <|-- Cline_Agent : extends
The framework provides a clean 4-class hierarchy:
-
Agentโ Abstract base class. Defines therun(**input_artifacts)contract and includes theLoggerfor debug/info logging. -
RubyAgentโ Wraps any RubyProcas an agent. No LLM involved: callproc.call(input_artifacts)and return a hash. Ideal for deterministic logic. -
PromptDrivenAgentโ Base for LLM-powered agents. Holds a role, objective, system_instructions, and constraints. Renders them via a pluggable prompt rendering strategy and records every prompt/response in aconversationarray. -
AiAgents::Agent/Cline::Agentโ Concrete LLM backends: one wraps theai-agentsgem, the other wrapscline-rb. Both implement#prompt(user_prompt)to send the rendered prompt to the LLM.
Composition: the pipeline pattern ๐
Agents communicate exclusively through artifacts โ named key/value pairs in a Ruby Hash:
outputs = agent.run(**inputs)
# outputs[:city] can feed into next_agent.run(city: outputs[:city])๐ก Agents are stateless by design โ no hidden mutable state. This makes them easy to test, debug, and reorder in pipelines.
Prompt-driven execution flow ๐ง
Here's what happens when you call run(**inputs) on a PromptDrivenAgent:
flowchart TD
A[run] --> B[Render system prompt]
B --> C[Call #prompt with system prompt]
C --> D{Missing output\nartifacts?}
D -->|Yes| E[Render retry prompt]
E --> F[Call #prompt again]
F --> D
D -->|No| G[Return output artifacts]
-
render_system_promptโ Assembles role, objective, instructions, and constraints into a structured Markdown document (via the chosen strategy). -
#prompt(user_prompt)โ Sends the rendered prompt to the LLM backend. The backend (ai-agents or cline-rb) manages tool calls, context, and the LLM conversation. - Retry loop โ If expected output artifacts are missing, a retry prompt is generated and sent again.
- Returns the collected
{ artifact_name => content }hash.
Prompt rendering strategies ๐
Three strategies are included, mixed into the agent at initialization via singleton_class.include strategy:
-
PromptRenderingStrategy::Markdownโ Clean, minimal Markdown. Simple instructions and artifact references. -
PromptRenderingStrategy::MarkdownHeavy(default for Cline) โ Elaborate prompts with execution checklists, structured artifact definition sections, and JSON-based output parsing. Agents format their artifacts as JSON blocks tagged withoutput_artifact=NAME, which the strategy parses back into the output hash. Includes type-aware parsing (:text,:markdown,:json). -
PromptRenderingStrategy::MarkdownHeavyFilesโ Same heavy Markdown behavior asMarkdownHeavy, but artifacts are persisted as files on disk instead of being in-lined or returned as JSON. Input artifacts are written to files that the assistant is asked to open, and output artifacts are expected as files that are read back from disk. It exposes a public#artifacts_directoryaccessor (defaulting to#{composable_agents_dir}/artifacts) that can be set on the agent at any time; its artifacts directories are resolved lazily and created on first use. Example:agent.artifacts_directory = File.join(composable_agents_dir, 'my_artifacts').
LLM backends: ai-agents vs cline-rb ๐ฏ
| Feature | AiAgents::Agent |
Cline::Agent |
|---|---|---|
| Underlying gem | ai-agents | cline-rb |
| Tools | Exposes CreateArtifactTool, GetArtifactTool to the LLM |
Uses Cline's skill system |
| State persistence | Marshal + Base64 via export_state/import_state
|
Direct JSON serialization of context array |
| Default rendering | Markdown |
MarkdownHeavy (with structured output parsing) |
| User interaction | Optional AskUserTool via AiAgentUserInteraction
|
N/A (uses Cline's own interaction) |
Mixin system โ augment agents with capabilities ๐งฐ
Mixins are prepended (using prepend) or included (using include) to override #run or add new methods:
-
Mixins::Retriableโ Wraps#runwith conditional retry logic. Register condition procs via#retry_onthat receive the agent and any exception raised byrun(ornil) and return a policy hash (:wait_secs,:modifier) to trigger a wait-and-retry, ornilto skip retrying. Ifrunraises and no policy is returned, the exception is re-raised. -
Mixins::ArtifactContractโ Wraps#runto validate inputs before and outputs after execution against declared contracts. RaisesMissingInputArtifactError,MissingOutputArtifactError, orArtifactTypeErroron violations. -
Mixins::Resumableโ Overrides#runwith a step-based execution model. Eachstepblock is persisted to.composable_agents/runs/{run_id}/as JSON. On re-run, completed steps are skipped โ only new steps execute. Supports nested steps and agent state serialization viaexport_state/import_state. -
Mixins::UserInteractionโ Adds an#ask(question)method. By default prompts the terminal; override#answer_tofor custom behavior.
Instruction system ๐
The Instructions class normalizes instructions into a standard list format. Each instruction can be:
-
{ text: "..." }โ Free-form text -
{ ordered_list: ["Step 1", "Step 2"] }โ Sequential steps
The rendering strategy then renders each type appropriately (#render_instruction_text, #render_instruction_ordered_list).
Code loading โก
Uses zeitwerk for automatic, thread-safe code autoloading โ no manual require calls needed beyond the top-level entry point.
State persistence for resumable workflows ๐พ
flowchart LR
A[Step: fetch_data] --> B{Step JSON\nexists?}
B -->|Yes| C[Deserialize state\nSkip execution]
B -->|No| D[Execute block]
D --> E[Serialize artifacts\n+ agent state to JSON]
C --> F[Continue with\nrestored artifacts]
E --> F
The Resumable mixin tracks a hierarchical step index for each step, stored as the index property of each node in the steps_run hierarchy, mirroring the nesting of step blocks. Each step's input/output state is saved as a JSON file. On re-execution with the same run_id, the framework loads the saved state instead of re-running completed steps โ saving both time and API costs.
Development
Prerequisites
- Ruby >= 3.1
- Bundler (comes with Ruby)
-
Node.js โ required by the
cline-rbbackend for pseudo-terminal support (node-pty)
Clone the repository
git clone https://github.com/Muriel-Salvan/composable_agents.git
cd composable_agentsInstall dependencies
bundle installAdditionally, install the Node.js pseudo-terminal dependency that the cline-rb backend expects:
npm install node-ptyProject structure (high-level)
.
โโโ lib/ # Source code (autoloaded via Zeitwerk)
โ โโโ composable_agents/ # Main library modules
โ โโโ agent.rb # Abstract base agent
โ โโโ prompt_driven_agent.rb # LLM-prompted agent
โ โโโ ruby_agent.rb # Plain Ruby logic agent
โ โโโ instructions.rb # Instruction system
โ โโโ ai_agents/ # ai-agents backend
โ โโโ cline/ # cline-rb backend
โ โโโ mixins/ # ArtifactContract, Logger, Resumable, Retriable, RetriableArtifacts, RetriableCline, UserInteraction
โ โโโ prompt_rendering_strategy/ # Markdown, MarkdownHeavy & MarkdownHeavyFiles strategies
โ โโโ utils/ # Markdown utilities
โโโ spec/ # Test suite
โ โโโ scenarios/ # RSpec test cases
โ โโโ composable_agents_test/ # Test helpers, spies, stubs
โโโ examples/ # Runnable usage examples
โโโ Gemfile # Dependencies
โโโ composable_agents.gemspec # Gem specification
โโโ .rubocop.yml # RuboCop configuration
โโโ .github/workflows/ # CI pipeline
Run tests
Run the full test suite with RSpec:
bundle exec rspecRun a specific test file:
bundle exec rspec spec/scenarios/composable_agents/cline/agent_spec.rbRun tests with verbose documentation output:
bundle exec rspec --format documentationTest debugging
Set the TEST_DEBUG=1 environment variable to enable verbose debug output during test execution:
TEST_DEBUG=1 bundle exec rspecCode coverage
The test suite enforces 99% minimum code coverage via SimpleCov. Coverage reports are generated in Cobertura format and automatically uploaded to Codecov in CI.
Code linting
This project uses RuboCop with the rubocop-rspec and rubocop-yard plugins. Run the linter:
bundle exec rubocopTo auto-correct fixable offenses:
bundle exec rubocop -aLinting is also verified as part of the test suite via the Code Quality spec (spec/scenarios/code_quality_spec.rb), which runs rubocop and asserts that no offenses are detected.
Generate documentation
API documentation is generated with YARD. The project enforces 100% documented code:
bundle exec yard doc --fail-on-warningCheck documentation coverage stats:
bundle exec yard stats --list-undoc --fail-on-warningDocumentation generation and coverage are also verified as part of the test suite via the Documentation generation spec.
Package the gem
Build the gem locally:
gem build composable_agents.gemspecThis produces a .gem file (e.g., composable_agents-0.1.0.gem) in the current directory. The packaging process is also verified by the Gem packaging spec.
Common development tasks
Adding a new feature
- Write the feature code under
lib/composable_agents/โ files are autoloaded by Zeitwerk, so name them according to the module/class namespace (e.g.,lib/composable_agents/my_feature.rbforComposableAgents::MyFeature). - Add RSpec tests under
spec/scenarios/composable_agents/following the existing patterns. - Document all public methods with YARD annotations.
- Run the full test suite and linting before committing:
bundle exec rspec && bundle exec rubocopAdding a test helper
Place reusable test helpers, spies, or stubs under spec/composable_agents_test/. They are autoloaded via Zeitwerk under the ComposableAgentsTest namespace and included automatically via the spec helper.
Running examples
Example scripts are located in the examples/ directory. Run any example directly with Ruby:
bundle exec ruby examples/compose_without_ai.rbSome examples require an LLM provider API key. Set the appropriate environment variable before running:
OPENROUTER_API_KEY=your_key bundle exec ruby examples/compose_with_ai.rb
# or
CLINE_API_KEY=your_key bundle exec ruby examples/compose_with_cline.rbCI pipeline
The project uses GitHub Actions (defined in .github/workflows/continuous_integration.yml):
-
testjob โ Runs the full RSpec suite on push (Ruby 3.4, with Bundler cache andnode-ptyinstalled). Coverage is uploaded to Codecov. -
packagejob โ Runs after tests pass, using semantic-release to publish the gem to RubyGems.org when a new version is tagged.
Release process
Releases are automated via semantic-release. Pushing a tag with a commit message following conventional commits format triggers the CI package job, which:
- Builds the gem
- Publishes it to RubyGems.org
- Creates a GitHub release with changelog
Manual gem publishing can also be done with:
GEM_HOST_API_KEY=your_key gem push composable_agents-*.gemContributing
Bug reports, feature suggestions, and pull requests are warmly welcomed on GitHub at github.com/Muriel-Salvan/composable_agents. Please follow the guidelines below to keep things running smoothly.
๐ Issues
- Before opening an issue, search the existing tracker to avoid duplicates.
- For a bug report, include:
- Ruby version (
ruby -v) - gem version
- a minimal code snippet that reproduces the problem
- the full error output or unexpected behaviour
- Ruby version (
- For a feature request, describe what you'd like to do and why, and if possible sketch how it could fit into the existing agent/mixin architecture.
๐ด Fork & Branch
- Fork the repository on GitHub.
- Clone your fork locally:
git clone https://github.com/<your-username>/composable_agents.git cd composable_agents
- Create a feature branch from
main:We follow a semantic-release workflow, so branch names likegit checkout -b feat/my-awesome-feature
feat/โฆ,fix/โฆ,chore/โฆhelp the CI determine the next version bump.
๐งช Setting up test dependencies & running tests
After checking out the repo, install all dependencies with bundle install. Then run the full test suite with bundle exec rspec (add --format documentation for verbose output). To run a single spec file, point to it directly, e.g. bundle exec rspec spec/composable_agents_test/prompt_driven_agent_spies.rb; to run a specific example, append :line_number, e.g. bundle exec rspec spec/composable_agents_test/agent_spec.rb:42. The CI (GitHub Actions, see .github/workflows/continuous_integration.yml) runs bundle exec rspec --format documentation on Ruby 3.4 โ your changes must pass all tests and should not drop code coverage below 99 % (enforced via SimpleCov).
โ Linting & code style
The project uses RuboCop with the rubocop-rspec plugin. Run the linter before pushing:
bundle exec rubocopConfiguration lives in .rubocop.yml. Key allowances (long methods, nested RSpec groups, etc.) are already tuned to the codebase โ please keep them as they are.
๐ CI / Build pipeline
| Job | When | What it does |
|---|---|---|
| test | Every push | Installs Ruby 3.4 + Node (for node-pty), runs bundle exec rspec, uploads coverage to Codecov
|
| package | After tests pass | Runs semantic-release to auto-publish the gem to RubyGems and create a GitHub release with a generated changelog |
Pull requests must pass the test job before they can be merged.
๐ Pull request guidelines
- Keep PRs focused โ one feature or fix per pull request.
- Write a clear title and description. Reference any related issues (e.g. "Closes #42").
- Ensure all existing tests still pass (
bundle exec rspec) and add new specs for your changes.- Unit specs go under
spec/composable_agents_test/. - Scenario specs (integration, documentation, packaging) go under
spec/scenarios/.
- Unit specs go under
- Run RuboCop and address any offenses.
- If your change adds a new public API method, document it with YARD โ the project enforces 100 % documented coverage.
- Commits should follow the Conventional Commits style (e.g.
feat:,fix:,chore:,docs:) so thatsemantic-releasecan determine the next version number automatically.
๐ License
By contributing, you agree that your contributions will be licensed under the BSD-3-Clause License that covers the project.
Questions? Open a Discussion or tag @Muriel-Salvan in an issue. ๐ฌ
License
The project is licensed under the BSD 3-Clause License.
See the LICENSE file for the full terms.
Copyright ยฉ 2026, Muriel Salvan. All rights reserved.