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 < RuntimeError
- Mixins
ComposableAgents::Mixins::LoggerComposableAgents::Mixins::ResumableComposableAgents::Mixins::UserInteractionComposableAgents::Mixins::ArtifactContract
- 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). - ๐ Two prompt rendering strategies โ Choose between clean Markdown for simple agents, or MarkdownHeavy with structured output parsing, execution checklists, and typed artifact support for complex agentic systems.
- ๐ฆ 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. - ๐ฃ๏ธ 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 or MarkdownHeavy) 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
Mixins
Mixins are prepend-ed into an agent class to add specific capabilities.
ComposableAgents::Mixins::Logger
-
Description: Provides debug and info logging to agents. Debug mode is enabled by setting
COMPOSABLE_AGENTS_DEBUG=1in the environment. -
Public methods (class-level):
-
self.debug?โ Returnstrueif debug mode is enabled (ENV['COMPOSABLE_AGENTS_DEBUG'] == '1').
-
-
Usage example:
# Enable debug logs ENV['COMPOSABLE_AGENTS_DEBUG'] = '1' puts ComposableAgents::Mixins::Logger.debug? # => true
- Full documentation: Mixins::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
Additional notes
-
No executables / CLI โ This gem is a library only; there are no scripts in
bin/. -
Prompt rendering strategies (
PromptRenderingStrategy::MarkdownandPromptRenderingStrategy::MarkdownHeavy) are not part of the public API themselves โ they are included automatically by the agent'sstrategy:parameter. -
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::Loggerโ Debug logging mixin -
PromptRenderingStrategy::Markdownโ Markdown prompt strategy -
PromptRenderingStrategy::MarkdownHeavyโ Heavy Markdown prompt strategy -
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 theMixins::Loggerfor 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 ๐
Two 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).
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::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. -
Mixins::Loggerโ Provideslog_debug/log_infomethods. Debug output is toggled via theCOMPOSABLE_AGENTS_DEBUG=1environment variable.
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 (@steps_idx) that mirrors 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/ # Resumable, ArtifactContract, UserInteraction, Logger
โ โโโ prompt_rendering_strategy/ # Markdown & MarkdownHeavy 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.