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.13
~> 2.2
~> 2.8
 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
      • ComposableAgents::Cline::ClineError < RuntimeError
    • Mixins
      • ComposableAgents::Logger < ::Logger
      • ComposableAgents::Mixins::Resumable
      • ComposableAgents::Mixins::UserInteraction
      • ComposableAgents::Mixins::ArtifactContract
      • ComposableAgents::Mixins::Retriable
      • ComposableAgents::Mixins::RetriableArtifacts
    • 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).
  • ๐Ÿ“ 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 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.
  • ๐Ÿ” Conditional retries โ€” Wrap an agent's #run with the Retriable mixin to retry on transient failures. Register condition procs via #retry_on that receive the agent and any raised exception (or nil) and return a policy hash (optional :wait_secs, optional :modifier) to wait-and-retry, or nil to 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); raises ArtifactsErrorsMaxRetriesExceededError when 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-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, 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) โ€” 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

ComposableAgents::Cline::ClineError < RuntimeError


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 setting COMPOSABLE_AGENTS_DEBUG=1 in the environment (which sets the logger level to DEBUG), 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) โ€” 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

ComposableAgents::Mixins::Retriable

  • Description: Adds conditional retry capabilities to agents. Retry condition procs are registered with #retry_on (or #with_retry_on for temporary activation) and are evaluated at the end of each run. A proc receives the agent instance and the exception raised by run (or nil if none) and returns either nil (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 waits wait_secs seconds, applies the modifier proc (if any), merges the new_input_artifacts into the retry input, and re-runs. If run raised 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: true in 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); raises ArtifactsErrorsMaxRetriesExceededError when 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 UnexpectedExitStatusError or UnexpectedInteractiveSessionError) using exponential backoff with jitter. Requires the Retriable mixin to be prepended first, and must be prepended on an agent descending from ComposableAgents::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, and PromptRenderingStrategy::MarkdownHeavyFiles) are not part of the public API themselves โ€” they are included automatically by the agent's strategy: parameter. A strategy never defines initialize: 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 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 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 ๐Ÿ“

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 with output_artifact=NAME, which the strategy parses back into the output hash. Includes type-aware parsing (:text, :markdown, :json).
  • PromptRenderingStrategy::MarkdownHeavyFiles โ€” Same heavy Markdown behavior as MarkdownHeavy, 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_directory accessor (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 #run with conditional retry logic. Register condition procs via #retry_on that receive the agent and any exception raised by run (or nil) and return a policy hash (:wait_secs, :modifier) to trigger a wait-and-retry, or nil to skip retrying. If run raises and no policy is returned, the exception is re-raised.
  • 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.

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 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-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/               # 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 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.