composable_agents 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0b4d0aa586936486335008d4a263e03e882150495932ca50da5a49f6266b940
4
- data.tar.gz: 6dcd208bf7884791fa2ef783312207feee0525e77e1363fbe88068dc260d0e88
3
+ metadata.gz: af07644d98f8997653ead8dc4b2707fe3ff6338480373cbc6fa5c5cc9aa5d82a
4
+ data.tar.gz: 2824ad225970ce7f3af6ff6e3d64d4a4200638af65bda0263e592466e7793e8d
5
5
  SHA512:
6
- metadata.gz: 57b00fc7d8861359dbc399fd580f86e51f579bc40544b6bceada3cbb18c38d7ca0b895ec26a091295c5004fd43f37639b496898dfd9b8ac5de79326b16fb58ed
7
- data.tar.gz: 56b80e997cecf2ceba654865946e8ec915a8bf9553d79738e4760709b2056430400af52fc96efea3043116732dcdc20e38ba3c578b31430c5c284afba10aae33
6
+ metadata.gz: 154a31e282dccb4b9c2f02a45b4ec2322b6dd269e63f9e21d0f7f9d239c5a087307d62c905a23ed5ca7981cd6fce8bf4bd4005ba80165f251d0c5b5d36d57440
7
+ data.tar.gz: 4f0d0b0ad45ab9a72ab206fd41d6d420dc167f0b3a66b3fd485f76ebece746d47eff3a9d17e7d2d66570369cda52eb1036eb44b1a59459b8626c063cf62eaa69
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # [v2.1.0](https://github.com/Muriel-Salvan/composable_agents/compare/v2.0.0...v2.1.0) (2026-09-06 16:41:14)
2
+
3
+ ## Global changes
4
+ ### Patches
5
+
6
+ * [fix: return nil on JSON parse failure to skip artifact save](https://github.com/Muriel-Salvan/composable_agents/commit/2f70822a9142d97aa9d5fa9424a8f9c97f98208e)
7
+ * [[Feature(Agent)] Added configurable logger for all agents.](https://github.com/Muriel-Salvan/composable_agents/commit/2979d6de1ea73bb259147f3778d6a51481c79f22)
8
+
9
+ ## Changes for Agent
10
+ ### Features
11
+
12
+ * [[Feature(Agent)] Added configurable logger for all agents.](https://github.com/Muriel-Salvan/composable_agents/commit/2979d6de1ea73bb259147f3778d6a51481c79f22)
13
+
1
14
  # [v2.0.0](https://github.com/Muriel-Salvan/composable_agents/compare/v1.1.2...v2.0.0) (2026-09-04 12:17:53)
2
15
 
3
16
  ## Global changes
data/README.md CHANGED
@@ -49,7 +49,7 @@ Whether you're building a code review assistant, a document summarizer, or a mul
49
49
  - [`ComposableAgents::Cline::Agent < PromptDrivenAgent`](#composableagentsclineagent--promptdrivenagent)
50
50
  - [`ComposableAgents::Cline::MissingSkillError < RuntimeError`](#composableagentsclinemissingskillerror--runtimeerror)
51
51
  - [Mixins](#mixins)
52
- - [`ComposableAgents::Mixins::Logger`](#composableagentsmixinslogger)
52
+ - [`ComposableAgents::Logger < ::Logger`](#composableagentslogger--logger)
53
53
  - [`ComposableAgents::Mixins::Resumable`](#composableagentsmixinsresumable)
54
54
  - [`ComposableAgents::Mixins::UserInteraction`](#composableagentsmixinsuserinteraction)
55
55
  - [`ComposableAgents::Mixins::ArtifactContract`](#composableagentsmixinsartifactcontract)
@@ -365,18 +365,16 @@ The project is a Ruby library (not a CLI) — users install the gem and use its
365
365
 
366
366
  Mixins are `prepend`-ed into an agent class to add specific capabilities.
367
367
 
368
- #### `ComposableAgents::Mixins::Logger`
368
+ #### `ComposableAgents::Logger < ::Logger`
369
369
 
370
- - **Description:** Provides debug and info logging to agents. Debug mode is enabled by setting `COMPOSABLE_AGENTS_DEBUG=1` in the environment.
371
- - **Public methods (class-level):**
372
- - `self.debug?` — Returns `true` if debug mode is enabled (`ENV['COMPOSABLE_AGENTS_DEBUG'] == '1'`).
370
+ - **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`.
373
371
  - **Usage example:**
374
372
  ```ruby
375
373
  # Enable debug logs
376
374
  ENV['COMPOSABLE_AGENTS_DEBUG'] = '1'
377
- puts ComposableAgents::Mixins::Logger.debug? # => true
375
+ puts ComposableAgents::Logger.instance.level == Logger::DEBUG # => true
378
376
  ```
379
- - **Full documentation:** [Mixins::Logger on RubyDoc](https://www.rubydoc.info/gems/composable_agents/ComposableAgents/Mixins/Logger) | [logger.rb on GitHub](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/mixins/logger.rb)
377
+ - **Full documentation:** ComposableAgents::Logger on RubyDoc | [logger.rb on GitHub](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/logger.rb)
380
378
 
381
379
  #### `ComposableAgents::Mixins::Resumable`
382
380
 
@@ -479,7 +477,7 @@ Mixins are `prepend`-ed into an agent class to add specific capabilities.
479
477
  - [`Mixins::Resumable`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/mixins/resumable.rb) — Resumable workflow mixin
480
478
  - [`Mixins::ArtifactContract`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/mixins/artifact_contract.rb) — Artifact validation mixin
481
479
  - [`Mixins::UserInteraction`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/mixins/user_interaction.rb) — User question-asking mixin
482
- - [`Mixins::Logger`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/mixins/logger.rb) — Debug logging mixin
480
+ - [`Logger`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/logger.rb) — Standard logger class
483
481
  - [`PromptRenderingStrategy::Markdown`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/prompt_rendering_strategy/markdown.rb) — Markdown prompt strategy
484
482
  - [`PromptRenderingStrategy::MarkdownHeavy`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/prompt_rendering_strategy/markdown_heavy.rb) — Heavy Markdown prompt strategy
485
483
  - [`Utils::Markdown`](https://github.com/Muriel-Salvan/composable_agents/blob/main/lib/composable_agents/utils/markdown.rb) — Markdown header alignment utilities
@@ -524,7 +522,7 @@ classDiagram
524
522
 
525
523
  The framework provides a clean **4-class hierarchy**:
526
524
 
527
- - **`Agent`** — Abstract base class. Defines the `run(**input_artifacts)` contract and includes the [`Mixins::Logger`](lib/composable_agents/mixins/logger.rb) for debug/info logging.
525
+ - **`Agent`** — Abstract base class. Defines the `run(**input_artifacts)` contract and includes the [`Logger`](lib/composable_agents/logger.rb) for debug/info logging.
528
526
  - **`RubyAgent`** — Wraps any Ruby `Proc` as an agent. No LLM involved: call `proc.call(input_artifacts)` and return a hash. Ideal for deterministic logic.
529
527
  - **`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.
530
528
  - **`AiAgents::Agent` / `Cline::Agent`** — Concrete LLM backends: one wraps the [`ai-agents`](https://github.com/nicbarker/ai-agents) gem, the other wraps [`cline-rb`](https://github.com/Muriel-Salvan/cline-rb). Both implement `#prompt(user_prompt)` to send the rendered prompt to the LLM.
@@ -584,7 +582,6 @@ Mixins are **prepended** (using `prepend`) or **included** (using `include`) to
584
582
  - **[`Mixins::ArtifactContract`](lib/composable_agents/mixins/artifact_contract.rb)** — Wraps `#run` to validate inputs before and outputs after execution against declared contracts. Raises `MissingInputArtifactError`, `MissingOutputArtifactError`, or `ArtifactTypeError` on violations.
585
583
  - **[`Mixins::Resumable`](lib/composable_agents/mixins/resumable.rb)** — 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`.
586
584
  - **[`Mixins::UserInteraction`](lib/composable_agents/mixins/user_interaction.rb)** — Adds an `#ask(question)` method. By default prompts the terminal; override `#answer_to` for custom behavior.
587
- - **[`Mixins::Logger`](lib/composable_agents/mixins/logger.rb)** — Provides `log_debug`/`log_info` methods. Debug output is toggled via the `COMPOSABLE_AGENTS_DEBUG=1` environment variable.
588
585
 
589
586
  ### Instruction system 📋
590
587
 
@@ -3,8 +3,6 @@ module ComposableAgents
3
3
  # Agents may internally use LLMs, tools, or other agents.
4
4
  # Agents are stateless: they take input artifacts and return output artifacts.
5
5
  class Agent
6
- include Mixins::Logger
7
-
8
6
  # @!group Public API
9
7
 
10
8
  # @return [String, nil] The agent name, if any.
@@ -17,12 +15,18 @@ module ComposableAgents
17
15
  #
18
16
  # @param name [String, nil] Agent name, or nil if none
19
17
  # @param composable_agents_dir [String] Base directory where composable agents can store data
18
+ # @param logger [::Logger] Logger to be used by this agent. Any Ruby-like logger is accepted.
19
+ # Defaults to ComposableAgents::Logger's singleton instance.
20
+ # This agent's log fields (see #log_fields) are prefixed to logged messages when the logger supports it
21
+ # (see ComposableAgents::Logger#tagged), and are left as-is otherwise.
20
22
  def initialize(
21
23
  name: nil,
22
- composable_agents_dir: '.composable_agents'
24
+ composable_agents_dir: '.composable_agents',
25
+ logger: ComposableAgents::Logger.instance
23
26
  )
24
27
  @name = name
25
28
  @composable_agents_dir = composable_agents_dir
29
+ @provided_logger = logger
26
30
  @runs_info = []
27
31
  @current_run_info = nil
28
32
  end
@@ -52,6 +56,15 @@ module ComposableAgents
52
56
  # @return [RunInfo, nil] The current run info, or nil if no run has started yet
53
57
  attr_reader :current_run_info
54
58
 
59
+ # Get the logger to be used by this agent, with this agent's log fields (see #log_fields) prefixed to messages.
60
+ # If the provided logger does not support fields tagging, it is used as-is (fields are ignored).
61
+ # Memoized.
62
+ #
63
+ # @return [::Logger] The logger to be used by this agent
64
+ def logger
65
+ @logger ||= @provided_logger.respond_to?(:tagged) ? @provided_logger.tagged(*log_fields) : @provided_logger
66
+ end
67
+
55
68
  # Fields to be logged
56
69
  #
57
70
  # @return [Array<String>] Fields to log
@@ -94,8 +94,8 @@ module ComposableAgents
94
94
  params: @params,
95
95
  instructions: @system_prompt,
96
96
  tools: [
97
- Tools::CreateArtifactTool.new(self),
98
- Tools::GetArtifactTool.new(@input_artifacts)
97
+ Tools::CreateArtifactTool.new(self, logger:),
98
+ Tools::GetArtifactTool.new(@input_artifacts, logger:)
99
99
  ] + agent_tools
100
100
  )
101
101
  ] + @handoff_agents
@@ -1,13 +1,10 @@
1
1
  module ComposableAgents
2
2
  module AiAgents
3
- # Collection of tools that can be used with ai-agents' agents
4
3
  module Tools
5
4
  # Tool that is used to ask the user for some input
6
- class AskUserTool < Agents::Tool
5
+ class AskUserTool < BaseTool
7
6
  # @!group Internal
8
7
 
9
- include Mixins::Logger
10
-
11
8
  description <<~EO_DESCRIPTION
12
9
  Ask the user a question and wait for their response.
13
10
  If you need information from the user, call this tool instead of asking directly.
@@ -18,8 +15,10 @@ module ComposableAgents
18
15
  # Constructor
19
16
  #
20
17
  # @param agent [Agent] The agent that is using this tool
21
- def initialize(agent)
22
- super()
18
+ # @param logger [::Logger] Logger to be used by this tool. Any Ruby-like logger is accepted.
19
+ # Defaults to ComposableAgents::Logger's singleton instance.
20
+ def initialize(agent, logger: ComposableAgents::Logger.instance)
21
+ super(logger:)
23
22
  @agent = agent
24
23
  end
25
24
 
@@ -0,0 +1,22 @@
1
+ module ComposableAgents
2
+ module AiAgents
3
+ # Collection of tools that can be used with ai-agents' agents
4
+ module Tools
5
+ # Base class for all tools used by agents.
6
+ # It provides a logger to all tools, that can be set to any Ruby-like logger
7
+ # and defaults to ComposableAgents::Logger's singleton instance.
8
+ class BaseTool < Agents::Tool
9
+ # @!group Internal
10
+
11
+ # Constructor
12
+ #
13
+ # @param logger [::Logger] Logger to be used by this tool. Any Ruby-like logger is accepted.
14
+ # Defaults to ComposableAgents::Logger's singleton instance.
15
+ def initialize(logger: ComposableAgents::Logger.instance)
16
+ super()
17
+ @logger = logger
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,11 +2,9 @@ module ComposableAgents
2
2
  module AiAgents
3
3
  module Tools
4
4
  # Tool that is used to create a new artifact's content
5
- class CreateArtifactTool < Agents::Tool
5
+ class CreateArtifactTool < BaseTool
6
6
  # @!group Internal
7
7
 
8
- include Mixins::Logger
9
-
10
8
  description <<~EO_DESCRIPTION
11
9
  Create an output artifact.
12
10
  Only create artifacts when the user is asking you to create an artifact named `{name}`.
@@ -18,8 +16,10 @@ module ComposableAgents
18
16
  # Constructor
19
17
  #
20
18
  # @param agent [PromptDrivenAgent] The agent that will receive output artifacts
21
- def initialize(agent)
22
- super()
19
+ # @param logger [::Logger] Logger to be used by this tool. Any Ruby-like logger is accepted.
20
+ # Defaults to ComposableAgents::Logger's singleton instance.
21
+ def initialize(agent, logger: ComposableAgents::Logger.instance)
22
+ super(logger:)
23
23
  @agent = agent
24
24
  end
25
25
 
@@ -2,19 +2,19 @@ module ComposableAgents
2
2
  module AiAgents
3
3
  module Tools
4
4
  # Tool that is used to read an artifact's content
5
- class GetArtifactTool < Agents::Tool
5
+ class GetArtifactTool < BaseTool
6
6
  # @!group Internal
7
7
 
8
- include Mixins::Logger
9
-
10
8
  description 'Get an input artifact'
11
9
  param :name, type: 'string', desc: 'Artifact name'
12
10
 
13
11
  # Constructor
14
12
  #
15
13
  # @param artifacts [Hash{Symbol => Object}>] The artifacts store
16
- def initialize(artifacts)
17
- super()
14
+ # @param logger [::Logger] Logger to be used by this tool. Any Ruby-like logger is accepted.
15
+ # Defaults to ComposableAgents::Logger's singleton instance.
16
+ def initialize(artifacts, logger: ComposableAgents::Logger.instance)
17
+ super(logger:)
18
18
  @artifacts = artifacts
19
19
  end
20
20
 
@@ -27,7 +27,7 @@ module ComposableAgents
27
27
  def perform(_tool_context, name:)
28
28
  name_sym = name.to_sym
29
29
  if @artifacts.key?(name_sym)
30
- log_debug "Artifact `#{name}` read successfully."
30
+ @logger.debug "Artifact `#{name}` read successfully."
31
31
  @artifacts[name_sym].to_s.strip
32
32
  else
33
33
  "[ERROR] No artifact named #{name}, don't call this tool with the name #{name} anymore."
@@ -209,7 +209,7 @@ module ComposableAgents
209
209
  # Setup the temporary Cline global config dir
210
210
  agent_tmp_dir = "#{@composable_agents_dir}/tmp/#{Time.now.utc.strftime('%F-%H-%M-%S')}#{"-#{name.gsub(/[^\w.]/, '_')}" if name}"
211
211
  ::Cline.configure do |config|
212
- config.debug = Mixins::Logger.debug?
212
+ config.debug = logger_debug?
213
213
  config.temp_dir_root = "#{agent_tmp_dir}/cline-rb"
214
214
  end
215
215
  cline_config = ::Cline::Config.open("#{agent_tmp_dir}/cline_config", create: true)
@@ -219,7 +219,7 @@ module ComposableAgents
219
219
  new_skill = cline_config.skills.new(skill_name)
220
220
  new_skill.files.replace(::Cline::Config.global.skills[skill_name].files)
221
221
  new_skill.enable
222
- log_debug "[Cline] - Enable global skill #{skill_name}"
222
+ logger.debug "[Cline] - Enable global skill #{skill_name}"
223
223
  new_skill.save
224
224
  end
225
225
  end
@@ -229,10 +229,10 @@ module ComposableAgents
229
229
  next if skill.enabled? == selected_skill
230
230
 
231
231
  if selected_skill
232
- log_debug "[Cline] - Enable project skill #{skill_name}"
232
+ logger.debug "[Cline] - Enable project skill #{skill_name}"
233
233
  skill.enable
234
234
  else
235
- log_debug "[Cline] - Disable project skill #{skill_name}"
235
+ logger.debug "[Cline] - Disable project skill #{skill_name}"
236
236
  skill.disable
237
237
  end
238
238
  skill.save
@@ -261,10 +261,17 @@ module ComposableAgents
261
261
  global_settings.auto_update_enabled = false
262
262
  @configure_global&.call(global_settings)
263
263
  global_settings.save
264
- cline_config.cli(stdout_echo: Mixins::Logger.debug?, verbose: Mixins::Logger.debug?)
264
+ cline_config.cli(stdout_echo: logger_debug?, verbose: logger_debug?)
265
265
  end
266
266
  end
267
267
 
268
+ # Check if the agent's logger is set to a debug level.
269
+ #
270
+ # @return [Boolean] True if the logger level is DEBUG or lower
271
+ def logger_debug?
272
+ @provided_logger.respond_to?(:level) && @provided_logger.level <= ::Logger::DEBUG
273
+ end
274
+
268
275
  # Find a skill among the current Cline environment (global and project) and recursively finds all its dependencies.
269
276
  #
270
277
  # @param skill [String] The skill name we are looking for
@@ -0,0 +1,110 @@
1
+ require 'logger'
2
+
3
+ module ComposableAgents
4
+ # Proxy around any Ruby-like logger that prefixes fields to the logged messages.
5
+ # This is a standard ::Logger that inherits all the severity methods (debug, info, warn, error, fatal, unknown),
6
+ # but overrides the add/log methods to delegate the actual logging to the wrapped logger,
7
+ # with the given fields prefixed to the message.
8
+ # Messages can be given as a String or a Proc returning the message (for lazy evaluation).
9
+ class Logger::Tagged < ::Logger
10
+ # Mapping of severity values to the corresponding severity methods on the wrapped logger
11
+ SEVERITY_METHODS = {
12
+ ::Logger::DEBUG => :debug,
13
+ ::Logger::INFO => :info,
14
+ ::Logger::WARN => :warn,
15
+ ::Logger::ERROR => :error,
16
+ ::Logger::FATAL => :fatal,
17
+ ::Logger::UNKNOWN => :unknown
18
+ }.freeze
19
+
20
+ # @!group Public API
21
+
22
+ # Constructor
23
+ #
24
+ # @param logger [Object] Any Ruby-like logger responding to the standard severity methods
25
+ # @param fields [Array<String>] Fields to be prefixed to each logged message
26
+ def initialize(logger, fields)
27
+ @logger = logger
28
+ @fields = fields
29
+ # This proxy does not log on its own: the wrapped logger does. Its level is kept in sync so that
30
+ # the inherited level predicates (debug?, info?...) reflect the wrapped logger's level.
31
+ super(nil, level: logger.respond_to?(:level) ? logger.level : ::Logger::DEBUG)
32
+ end
33
+
34
+ # Log a message with the given severity, delegating to the wrapped logger with the fields prefixed.
35
+ # This is the implementation used by all severity methods, that all call #add in ::Logger.
36
+ #
37
+ # @param severity [Integer, nil] Severity of the message
38
+ # @param message [String, #call => String, nil] Message string, Proc returning the message for lazy evaluation,
39
+ # or nil if the message is given by the progname or a block
40
+ # @param progname [String, nil] Program name to be logged if the message is nil
41
+ # @yield The optional code returning the message to log
42
+ # @yieldreturn [String] The message to log
43
+ # @return [Boolean] True, as ::Logger#add does
44
+ def add(severity, message = nil, progname = nil, &)
45
+ log_message(severity, message, progname, &)
46
+ end
47
+
48
+ # Log a message with the given severity.
49
+ # Defined because ::Logger defines #log as an alias of #add, and aliases are bound at definition time:
50
+ # without redefining it here, #log would not use this class' #add override.
51
+ #
52
+ # @param severity [Integer, nil] Severity of the message
53
+ # @param message [String, #call => String, nil] Message string, Proc returning the message for lazy evaluation,
54
+ # or nil if the message is given by the progname or a block
55
+ # @param progname [String, nil] Program name to be logged if the message is nil
56
+ # @yield The optional code returning the message to log
57
+ # @yieldreturn [String] The message to log
58
+ # @return [Boolean] True, as ::Logger#add does
59
+ def log(severity, message = nil, progname = nil, &)
60
+ add(severity, message, progname, &)
61
+ end
62
+
63
+ private
64
+
65
+ # Delegate the message to the wrapped logger, with the fields prefixed.
66
+ # The message is evaluated lazily (only when the wrapped logger's severity level allows it),
67
+ # by passing a block to the wrapped logger's severity method.
68
+ #
69
+ # @param severity [Integer, nil] Severity of the message
70
+ # @param message [String, #call => String, nil] Message string, Proc returning the message for lazy evaluation,
71
+ # or nil if the message is given by the progname or a block
72
+ # @param progname [String, nil] Program name to be logged if the message is nil
73
+ # @yield The optional code returning the message to log
74
+ # @yieldreturn [String] The message to log
75
+ def log_message(severity, message, progname, &)
76
+ @logger.public_send(SEVERITY_METHODS.fetch(severity || ::Logger::UNKNOWN, :unknown)) do
77
+ resolved = resolve_message(message, progname, &)
78
+ prefix = fields_prefix
79
+ prefix.empty? ? resolved.to_s : "#{prefix} - #{resolved}"
80
+ end
81
+ end
82
+
83
+ # Get the fields to be prefixed to logged messages, formatted
84
+ #
85
+ # @return [String] The formatted fields prefix
86
+ def fields_prefix
87
+ @fields.map { |field| "[#{field}]" }.join(' ')
88
+ end
89
+
90
+ # Resolve the message to be logged.
91
+ # The block has priority over the message and progname parameters, and Procs are called to get their result.
92
+ #
93
+ # @param message [String, #call => String, nil] Message string or Proc returning the message
94
+ # @param progname [String, nil] Program name to be logged if the message is nil
95
+ # @yield The optional code returning the message to log
96
+ # @yieldreturn [String] The message to log
97
+ # @return [String] The message to be logged
98
+ def resolve_message(message, progname, &)
99
+ resolved =
100
+ if block_given?
101
+ yield
102
+ elsif message.nil?
103
+ progname
104
+ else
105
+ message
106
+ end
107
+ resolved.respond_to?(:call) ? resolved.call : resolved
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,48 @@
1
+ require 'logger'
2
+
3
+ module ComposableAgents
4
+ # Logger to be used by agents.
5
+ # This is a standard Ruby Logger (inheriting from ::Logger) that formats messages with a UTC timestamp and severity.
6
+ # It also supports prefixing fields to messages (like the agent's full name) using the #tagged method,
7
+ # which returns a proxy compatible with any Ruby-like logger: fields are then prepended to the logged messages
8
+ # themselves, and silently ignored by loggers not supporting them.
9
+ class Logger < ::Logger
10
+ # @!group Public API
11
+
12
+ class << self
13
+ # Get the default singleton logger instance
14
+ #
15
+ # @return [Logger] The default logger instance
16
+ def instance
17
+ @instance ||= new
18
+ end
19
+ end
20
+
21
+ # Constructor.
22
+ # Unless overridden by the caller, the default level follows the COMPOSABLE_AGENTS_DEBUG environment variable
23
+ # (DEBUG when it is set to 1, INFO otherwise), and the default formatter outputs messages as
24
+ # `[UTC timestamp] [SEVERITY] message`.
25
+ #
26
+ # @param logdev [IO, String, #write, nil] Log device to be used, defaulting to standard output
27
+ # @param args [Array<Object>] Positional arguments to be given to ::Logger (shifting parameters...)
28
+ # @param kwargs [Hash{Symbol => Object}] Optional parameters to be given to ::Logger (level, formatter...)
29
+ def initialize(logdev = $stdout, *args, **kwargs)
30
+ super
31
+ self.level = ENV['COMPOSABLE_AGENTS_DEBUG'] == '1' ? DEBUG : INFO unless kwargs.key?(:level)
32
+ return if kwargs.key?(:formatter)
33
+
34
+ self.formatter = proc do |severity, _datetime, _progname, message|
35
+ "[#{Time.now.utc.strftime('%F %T')}] [#{severity}] #{message}\n"
36
+ end
37
+ end
38
+
39
+ # Get a logger proxy that prefixes the given fields to logged messages.
40
+ # The returned proxy is compatible with any Ruby-like logger.
41
+ #
42
+ # @param fields [Array<String>] Fields to be prefixed to each logged message
43
+ # @return [Tagged] The fields-prefixing logger proxy
44
+ def tagged(*fields)
45
+ Tagged.new(self, fields)
46
+ end
47
+ end
48
+ end
@@ -16,7 +16,7 @@ module ComposableAgents
16
16
  #
17
17
  # @return [Array<Agents::Tool>] List of tools
18
18
  def agent_tools
19
- super + [AiAgents::Tools::AskUserTool.new(self)]
19
+ super + [AiAgents::Tools::AskUserTool.new(self, logger:)]
20
20
  end
21
21
  end
22
22
  end
@@ -2,6 +2,7 @@ require 'json'
2
2
  require 'fileutils'
3
3
 
4
4
  module ComposableAgents
5
+ # Collection of useful mixins that add functionality to agents prepending them.
5
6
  module Mixins
6
7
  # Mixin adding resumable step capabilities to agents.
7
8
  # An agent prepending this mixin can use the following:
@@ -139,14 +140,14 @@ module ComposableAgents
139
140
  if step_state == saved_input_state
140
141
  @current_step_node[:status] = :cached
141
142
  set_current_step_state(saved_output_state, agent:)
142
- log_debug "[Step #{step_full_name}] - Already executed - Got #{@artifacts.size} artifacts from persistence: #{@artifacts.keys.join(', ')}"
143
+ logger.debug "[Step #{step_full_name}] - Already executed - Got #{@artifacts.size} artifacts from persistence: #{@artifacts.keys.join(', ')}"
143
144
  else
144
145
  # Clone state before yielding because it will certainly be modified
145
146
  input_step_state = clone_step_state(step_state)
146
147
  yield
147
148
  @current_step_node[:status] = :executed
148
149
  store_step_states(step_full_name, input: input_step_state, output: current_step_state(agent:))
149
- log_debug "[Step #{step_full_name}] - Executed - Stored #{@artifacts.size} artifacts in persistence: #{@artifacts.keys.join(', ')}"
150
+ logger.debug "[Step #{step_full_name}] - Executed - Stored #{@artifacts.size} artifacts in persistence: #{@artifacts.keys.join(', ')}"
150
151
  end
151
152
  end
152
153
  end
@@ -73,7 +73,7 @@ module ComposableAgents
73
73
  @output_artifacts = {}
74
74
  @output_artifacts_errors = {}
75
75
  @system_prompt = render_system_prompt(render_instructions(@system_instructions))
76
- log_debug "System prompt: #{@system_prompt}"
76
+ logger.debug "System prompt: #{@system_prompt}"
77
77
  converse(user_instructions, input_artifacts: @input_artifacts, author: 'User')
78
78
  if respond_to?(:normalized_output_artifacts_contracts, true)
79
79
  # We know which output artifacts we are expecting.
@@ -148,7 +148,7 @@ module ComposableAgents
148
148
  def save_output_artifact(artifact_name, content)
149
149
  @output_artifacts[artifact_name] = content
150
150
  @output_artifacts_errors.delete(artifact_name)
151
- log_debug "[Artifact] - Received output artifact #{artifact_name}"
151
+ logger.debug "[Artifact] - Received output artifact #{artifact_name}"
152
152
  end
153
153
 
154
154
  # Report an error on an output artifact.
@@ -162,7 +162,7 @@ module ComposableAgents
162
162
  @output_artifacts.delete(artifact_name)
163
163
  @output_artifacts_errors[artifact_name] = error
164
164
  # TODO: Make this as a warning message
165
- log_debug "[Artifact] - Should have received content for output artifact `#{artifact_name}` " \
165
+ logger.debug "[Artifact] - Should have received content for output artifact `#{artifact_name}` " \
166
166
  "but the following error occurred: #{error}"
167
167
  end
168
168
 
@@ -191,10 +191,10 @@ module ComposableAgents
191
191
  def converse(instructions, input_artifacts: {}, author: 'Orchestrator')
192
192
  rendered_instructions = render_instructions(instructions)
193
193
  rendered_user_prompt = render_user_prompt(rendered_instructions, input_artifacts:)
194
- log_debug "Rendered User prompt: #{rendered_user_prompt}"
194
+ logger.debug "Rendered User prompt: #{rendered_user_prompt}"
195
195
  track_message(message: rendered_instructions, author:)
196
196
  response = prompt(rendered_user_prompt)
197
- log_debug "Raw Agent #{full_name} response: #{response}"
197
+ logger.debug "Raw Agent #{full_name} response: #{response}"
198
198
  track_message(message: response, author: "Agent #{full_name}")
199
199
  end
200
200
 
@@ -169,7 +169,8 @@ module ComposableAgents
169
169
  # - error [String, nil] An error message related to this missing artifact.
170
170
  # @return [Object] The user instructions (see Instructions#initialize)
171
171
  def missing_output_user_instructions(missing_output_artifacts)
172
- log_debug "[Artifact] - Asking assistant for missing output artifacts `#{missing_output_artifacts.keys.join(', ')}` to be returned in its next answer."
172
+ logger.debug '[Artifact] - Asking assistant for missing output artifacts ' \
173
+ "`#{missing_output_artifacts.keys.join(', ')}` to be returned in its next answer."
173
174
  <<~EO_PROMPT
174
175
  The following output artifacts are missing from your previous responses:
175
176
  #{
@@ -2,5 +2,5 @@ module ComposableAgents
2
2
  # @!group Public API
3
3
 
4
4
  # Gem version
5
- VERSION = '2.0.0'
5
+ VERSION = '2.1.0'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: composable_agents
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
@@ -77,13 +77,15 @@ files:
77
77
  - lib/composable_agents/agent.rb
78
78
  - lib/composable_agents/ai_agents/agent.rb
79
79
  - lib/composable_agents/ai_agents/tools/ask_user_tool.rb
80
+ - lib/composable_agents/ai_agents/tools/base_tool.rb
80
81
  - lib/composable_agents/ai_agents/tools/create_artifact_tool.rb
81
82
  - lib/composable_agents/ai_agents/tools/get_artifact_tool.rb
82
83
  - lib/composable_agents/cline/agent.rb
83
84
  - lib/composable_agents/instructions.rb
85
+ - lib/composable_agents/logger.rb
86
+ - lib/composable_agents/logger/tagged.rb
84
87
  - lib/composable_agents/mixins/ai_agent_user_interaction.rb
85
88
  - lib/composable_agents/mixins/artifact_contract.rb
86
- - lib/composable_agents/mixins/logger.rb
87
89
  - lib/composable_agents/mixins/resumable.rb
88
90
  - lib/composable_agents/mixins/user_interaction.rb
89
91
  - lib/composable_agents/prompt_driven_agent.rb
@@ -1,47 +0,0 @@
1
- module ComposableAgents
2
- # Collection of useful mixins that add functionality to agents prepending them.
3
- module Mixins
4
- # Logging mixin for agents
5
- module Logger
6
- # @!group Public API
7
-
8
- # Check if debug mode is enabled
9
- #
10
- # @return [Boolean] True if debug mode is enabled
11
- def self.debug?
12
- ENV['COMPOSABLE_AGENTS_DEBUG'] == '1'
13
- end
14
-
15
- private
16
-
17
- # Log debug message only if debug mode is enabled
18
- #
19
- # @param message [String, Proc] Message string or Proc returning message for lazy evaluation
20
- def log_debug(message)
21
- return unless Logger.debug?
22
-
23
- log(message, severity: :debug)
24
- end
25
-
26
- # Log info message
27
- #
28
- # @param message [String, #call => String] Message string or Proc returning message for lazy evaluation
29
- def log_info(message)
30
- log(message, severity: :info)
31
- end
32
-
33
- # Log a message with severity
34
- #
35
- # @param message [String, #call => String] Message string or Proc returning message for lazy evaluation
36
- # @param severity [Symbol] Severity
37
- def log(message, severity: :info)
38
- fields = [
39
- Time.now.utc.strftime('%F %T'),
40
- severity.to_s.upcase
41
- ]
42
- fields.concat(log_fields) if respond_to?(:log_fields, true)
43
- puts "#{fields.map { |field| "[#{field}]" }.join(' ')} - #{message.is_a?(String) ? message : message.call}"
44
- end
45
- end
46
- end
47
- end