The project is in a healthy, maintained state
Composable AI agents framework
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.10
~> 1.1
~> 2.7
 Project Readme

composable_agents

A Ruby framework for building composable, prompt-driven AI agent pipelines โ€” mix, match, and orchestrate agents into reusable workflows.

Build Test Coverage GitHub stars License Gem Version Gem Total Downloads

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::Agent
      • ComposableAgents::RubyAgent < Agent
      • ComposableAgents::Instructions
      • ComposableAgents::PromptDrivenAgent < Agent
    • LLM backend agent classes
      • ComposableAgents::AiAgents::Agent < PromptDrivenAgent
      • ComposableAgents::Cline::Agent < PromptDrivenAgent
      • ComposableAgents::Cline::MissingSkillError < RuntimeError
    • Mixins
      • ComposableAgents::Mixins::Logger
      • ComposableAgents::Mixins::Resumable
      • ComposableAgents::Mixins::UserInteraction
      • ComposableAgents::Mixins::ArtifactContract
    • Additional notes
  • 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_agents

Or install it globally:

gem install composable_agents

Requires 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 ArtifactContract mixin to validate inputs/outputs.
  • Use the Resumable mixin to persist and resume long-running workflows.
  • Use the AiAgentUserInteraction mixin 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-rb backend for pseudo-terminal (PTY) support via node-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_KEY environment variable when using the AiAgents backend.
    • Cline API key โ€” Set via the CLINE_API_KEY environment variable when using the Cline backend.

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 with RubyAgent, or compose complex multi-step workflows using the Resumable mixin.
  • ๐Ÿ”„ 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-agents gem (OpenRouter) or cline-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 clear MissingInputArtifactError, MissingOutputArtifactError, or ArtifactTypeError on violations.
  • ๐Ÿ’พ Resumable execution โ€” Persist step-by-step state to disk (via the Resumable mixin). 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-agents tool integration for LLM-controlled workflows.
  • ๐Ÿ“‹ Automatic conversation tracking โ€” Every prompt and response is automatically recorded with timestamps in a structured conversation store, 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=1 environment 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


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, or nil).
    • #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) โ€” The processor is a #call-able object (e.g. a Proc) 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_list keys). Includes Enumerable.
  • Public methods:
    • #initialize(instructions) โ€” Accepts a String, an Array, or a Hash{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) โ€” The strategy defaults to PromptRenderingStrategy::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-agents gem as its LLM backend. Requires an OpenRouter API key configured via Agents.configure.
  • Public methods:
    • #initialize(*args, model:, params:, handoff_agents:, **kwargs) โ€” Specify the model (e.g. 'openai/gpt-4o-mini'), optional params for model configuration, and a list of handoff_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-rb gem as its LLM backend. Requires a CLINE_API_KEY environment variable. Automatically prepends the ArtifactContract mixin.
  • 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::Agent when 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=1 in the environment.
  • Public methods (class-level):
    • self.debug? โ€” Returns true if 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) โ€” The run_id identifies 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_to for 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 are Hash{Symbol => String} (simple description) or Hash{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::Markdown and PromptRenderingStrategy::MarkdownHeavy) are not part of the public API themselves โ€” they are included automatically by the agent's strategy: parameter.
  • The AiAgentUserInteraction mixin (ComposableAgents::Mixins::AiAgentUserInteraction) is an internal bridge that combines UserInteraction with AiAgents::Agent; it is used by prepending it to an AiAgents::Agent subclass.
  • See the examples/ directory for complete, runnable scripts demonstrating all the above APIs.

Documentation

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
Loading

The framework provides a clean 4-class hierarchy:

  • Agent โ€” Abstract base class. Defines the run(**input_artifacts) contract and includes the Mixins::Logger for debug/info logging.
  • RubyAgent โ€” Wraps any Ruby Proc as an agent. No LLM involved: call proc.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 a conversation array.
  • AiAgents::Agent / Cline::Agent โ€” Concrete LLM backends: one wraps the ai-agents gem, the other wraps cline-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]
Loading
  1. render_system_prompt โ€” Assembles role, objective, instructions, and constraints into a structured Markdown document (via the chosen strategy).
  2. #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.
  3. Retry loop โ€” If expected output artifacts are missing, a retry prompt is generated and sent again.
  4. 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 with output_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 #run to validate inputs before and outputs after execution against declared contracts. Raises MissingInputArtifactError, MissingOutputArtifactError, or ArtifactTypeError on violations.
  • Mixins::Resumable โ€” Overrides #run with a step-based execution model. Each step block 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 via export_state/import_state.
  • Mixins::UserInteraction โ€” Adds an #ask(question) method. By default prompts the terminal; override #answer_to for custom behavior.
  • Mixins::Logger โ€” Provides log_debug/log_info methods. Debug output is toggled via the COMPOSABLE_AGENTS_DEBUG=1 environment 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
Loading

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-rb backend for pseudo-terminal support (node-pty)

Clone the repository

git clone https://github.com/Muriel-Salvan/composable_agents.git
cd composable_agents

Install dependencies

bundle install

Additionally, install the Node.js pseudo-terminal dependency that the cline-rb backend expects:

npm install node-pty

Project 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 rspec

Run a specific test file:

bundle exec rspec spec/scenarios/composable_agents/cline/agent_spec.rb

Run tests with verbose documentation output:

bundle exec rspec --format documentation

Test debugging

Set the TEST_DEBUG=1 environment variable to enable verbose debug output during test execution:

TEST_DEBUG=1 bundle exec rspec

Code 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 rubocop

To auto-correct fixable offenses:

bundle exec rubocop -a

Linting 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-warning

Check documentation coverage stats:

bundle exec yard stats --list-undoc --fail-on-warning

Documentation 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.gemspec

This 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

  1. 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.rb for ComposableAgents::MyFeature).
  2. Add RSpec tests under spec/scenarios/composable_agents/ following the existing patterns.
  3. Document all public methods with YARD annotations.
  4. Run the full test suite and linting before committing:
bundle exec rspec && bundle exec rubocop

Adding 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.rb

Some 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.rb

CI pipeline

The project uses GitHub Actions (defined in .github/workflows/continuous_integration.yml):

  • test job โ€” Runs the full RSpec suite on push (Ruby 3.4, with Bundler cache and node-pty installed). Coverage is uploaded to Codecov.
  • package job โ€” 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:

  1. Builds the gem
  2. Publishes it to RubyGems.org
  3. Creates a GitHub release with changelog

Manual gem publishing can also be done with:

GEM_HOST_API_KEY=your_key gem push composable_agents-*.gem

Contributing

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
  • 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

  1. Fork the repository on GitHub.
  2. Clone your fork locally:
    git clone https://github.com/<your-username>/composable_agents.git
    cd composable_agents
  3. Create a feature branch from main:
    git checkout -b feat/my-awesome-feature
    We follow a semantic-release workflow, so branch names like 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 rubocop

Configuration 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.
  • 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 that semantic-release can 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.