composable_agents 1.1.2 → 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: 83d046d554869aa540c97465182338129f2aefbf14f92b20fc0aaf81ef54b632
4
- data.tar.gz: 0b86bd8489a1b4b2f66a1ae20c33c543b055c559e07cafddc18924628a58ef01
3
+ metadata.gz: af07644d98f8997653ead8dc4b2707fe3ff6338480373cbc6fa5c5cc9aa5d82a
4
+ data.tar.gz: 2824ad225970ce7f3af6ff6e3d64d4a4200638af65bda0263e592466e7793e8d
5
5
  SHA512:
6
- metadata.gz: 313bf41f9c1b24ce950ef3071adbf79a1433e4dea0d1b586964ef30854153c4f06e9a44ccbf852c2d798d8bd90e6bf2328f5f35beb799e433b0712250ad076c9
7
- data.tar.gz: 94dfd245657963c6566b796d99bd7be1d5609d30c04b68d0ee0a6845f9b1082a33ed6494a3c0b45447120f20ef0f498d064ad3c89e5d7be09f873554de1bb0f4
6
+ metadata.gz: 154a31e282dccb4b9c2f02a45b4ec2322b6dd269e63f9e21d0f7f9d239c5a087307d62c905a23ed5ca7981cd6fce8bf4bd4005ba80165f251d0c5b5d36d57440
7
+ data.tar.gz: 4f0d0b0ad45ab9a72ab206fd41d6d420dc167f0b3a66b3fd485f76ebece746d47eff3a9d17e7d2d66570369cda52eb1036eb44b1a59459b8626c063cf62eaa69
data/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
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
+
14
+ # [v2.0.0](https://github.com/Muriel-Salvan/composable_agents/compare/v1.1.2...v2.0.0) (2026-09-04 12:17:53)
15
+
16
+ ## Global changes
17
+ ### Patches
18
+
19
+ * [[Feature(AiAgents)] Publish realtime LLM usage (cost, tokens) in run info](https://github.com/Muriel-Salvan/composable_agents/commit/e6302ae30ec4a8c54b449c23ebf795f6b6a3e055)
20
+ * [feat(cline): track and publish realtime API usage in run info](https://github.com/Muriel-Salvan/composable_agents/commit/c07eed20e81eda7fd08c32fb28426ca262844aaa)
21
+ * [[Breaking(Resumable)] track steps per run and document resumable run info](https://github.com/Muriel-Salvan/composable_agents/commit/ba839759e24e6a1ca9eb4ee14e552c6b7b8da017)
22
+ * [[Feature(Agent)]: add run tracking and info publishing to agents](https://github.com/Muriel-Salvan/composable_agents/commit/67567699bdfd66d6cf65bd3095e8b08e04ca301e)
23
+
24
+ ## Changes for Agent
25
+ ### Features
26
+
27
+ * [[Feature(Agent)]: add run tracking and info publishing to agents](https://github.com/Muriel-Salvan/composable_agents/commit/67567699bdfd66d6cf65bd3095e8b08e04ca301e)
28
+
29
+ ## Changes for AiAgents
30
+ ### Features
31
+
32
+ * [[Feature(AiAgents)] Publish realtime LLM usage (cost, tokens) in run info](https://github.com/Muriel-Salvan/composable_agents/commit/e6302ae30ec4a8c54b449c23ebf795f6b6a3e055)
33
+
34
+ ## Changes for Resumable
35
+ ### Breaking changes
36
+
37
+ * [[Breaking(Resumable)] track steps per run and document resumable run info](https://github.com/Muriel-Salvan/composable_agents/commit/ba839759e24e6a1ca9eb4ee14e552c6b7b8da017)
38
+
1
39
  # [v1.1.2](https://github.com/Muriel-Salvan/composable_agents/compare/v1.1.1...v1.1.2) (2026-09-03 16:08:18)
2
40
 
3
41
  ### Patches
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,23 +3,32 @@ 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
- # @return [String, nil] The agent name, if any
8
+ # @return [String, nil] The agent name, if any.
11
9
  attr_reader :name
12
10
 
11
+ # @return [Array<RunInfo>] The list of run information of this agent.
12
+ attr_reader :runs_info
13
+
13
14
  # Constructor
14
15
  #
15
16
  # @param name [String, nil] Agent name, or nil if none
16
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.
17
22
  def initialize(
18
23
  name: nil,
19
- composable_agents_dir: '.composable_agents'
24
+ composable_agents_dir: '.composable_agents',
25
+ logger: ComposableAgents::Logger.instance
20
26
  )
21
27
  @name = name
22
28
  @composable_agents_dir = composable_agents_dir
29
+ @provided_logger = logger
30
+ @runs_info = []
31
+ @current_run_info = nil
23
32
  end
24
33
 
25
34
  # Return the full name of the agent.
@@ -31,23 +40,43 @@ module ComposableAgents
31
40
  "#{name || 'Unnamed'}#{" (#{self.class.name.split('::').last})" if self.class != Agent && self.class.name}"
32
41
  end
33
42
 
34
- # @!group Internal
35
-
36
43
  # Execute the agent to generate some output artifacts based on some input artifacts.
37
44
  #
38
- # @param input_artifacts [Hash{Symbol => Object}] The input artifacts content
45
+ # @param _input_artifacts [Hash{Symbol => Object}] The input artifacts content
39
46
  # @return [Hash{Symbol => Object}] Output artifacts content
40
- def run(**input_artifacts)
41
- raise NotImplementedError, 'This method should be implemented by an Agent subclass'
47
+ def run(**_input_artifacts)
48
+ @current_run_info = RunInfo.new
49
+ @runs_info << @current_run_info
50
+ publish_run_info(started_at: Time.now)
51
+ {}
42
52
  end
43
53
 
44
54
  private
45
55
 
56
+ # @return [RunInfo, nil] The current run info, or nil if no run has started yet
57
+ attr_reader :current_run_info
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
+
46
68
  # Fields to be logged
47
69
  #
48
70
  # @return [Array<String>] Fields to log
49
71
  def log_fields
50
72
  [full_name]
51
73
  end
74
+
75
+ # Publish run information for the current run
76
+ #
77
+ # @param kwargs [Hash{Symbol => Object}] Set of properties to publish
78
+ def publish_run_info(**kwargs)
79
+ current_run_info.publish(**kwargs)
80
+ end
52
81
  end
53
82
  end
@@ -5,6 +5,20 @@ module ComposableAgents
5
5
  # All agents from this module work with the awesome ai-agents Rubygem
6
6
  module AiAgents
7
7
  # Agent implementation that uses an ai-agent's AgentRunner.
8
+ # This agent publishes the following run information:
9
+ # - usage [Hash] Cumulative usage (cost and tokens) of the LLM calls made during the run.
10
+ # This information is published in realtime, as soon as the AgentRunner completes LLM calls.
11
+ # Here are the properties it contains:
12
+ # - cost [Float] Cumulative monetary cost of all LLM calls of the run.
13
+ # This cost is computed by RubyLLM from the model's pricing in its registry: it is best-effort,
14
+ # as models with incomplete pricing information can have their cost underestimated.
15
+ # - input_tokens [Integer] Cumulative number of input tokens of all LLM calls of the run.
16
+ # - output_tokens [Integer] Cumulative number of output tokens of all LLM calls of the run.
17
+ # - cache_read_tokens [Integer] Cumulative number of tokens read from cache by all LLM calls of the run.
18
+ # - cache_write_tokens [Integer] Cumulative number of tokens written to cache by all LLM calls of the run.
19
+ # - context_tokens [Integer, nil] Number of context tokens consumed by the last LLM call of the run.
20
+ # This property is not cumulative, as each LLM call contains the full context.
21
+ # - context_tokens_limit [Integer, nil] Maximum context window limit of the model used, or nil if unknown.
8
22
  class Agent < PromptDrivenAgent
9
23
  # @!group Public API
10
24
 
@@ -36,6 +50,18 @@ module ComposableAgents
36
50
  "#{name || 'Unnamed'} (AiAgent #{@model})"
37
51
  end
38
52
 
53
+ # Execute the agent to generate some output artifacts based on some input artifacts.
54
+ # Also reset the usage statistics accumulated for this run (published realtime in the run info).
55
+ #
56
+ # @param user_instructions [Object, nil] Instructions for the user prompt, that will be rendered.
57
+ # The kind of instructions that can be given are defined by the Instructions's constructor (see Instructions#initialize).
58
+ # @param input_artifacts [Hash{Symbol => Object}] The input artifacts content, per artifact name
59
+ # @return [Hash{Symbol => Object}] The output artifacts
60
+ def run(user_instructions: nil, **input_artifacts)
61
+ @llm_call_responses = []
62
+ super
63
+ end
64
+
39
65
  # @!group Internal
40
66
 
41
67
  # Export the agent state for persistence
@@ -68,14 +94,70 @@ module ComposableAgents
68
94
  params: @params,
69
95
  instructions: @system_prompt,
70
96
  tools: [
71
- Tools::CreateArtifactTool.new(self),
72
- Tools::GetArtifactTool.new(@input_artifacts)
97
+ Tools::CreateArtifactTool.new(self, logger:),
98
+ Tools::GetArtifactTool.new(@input_artifacts, logger:)
73
99
  ] + agent_tools
74
100
  )
75
101
  ] + @handoff_agents
102
+ ).on_llm_call_complete { |_agent_name, _model, response| track_llm_usage(response) }
103
+ end
104
+
105
+ # Track usage (cost and tokens) of an LLM call, and publish it realtime in the run info.
106
+ # The usage of all LLM calls of the run is summed to give the run's cumulative usage.
107
+ #
108
+ # @param response [RubyLLM::Message] The response of the LLM call
109
+ def track_llm_usage(response)
110
+ # Some responses have no usage metrics (eg. RubyLLM::Tool::Halt responses, or responses without token data)
111
+ return unless response.respond_to?(:input_tokens) && response.tokens
112
+
113
+ @llm_call_responses << response
114
+ usages = @llm_call_responses
115
+ last_response = usages.last
116
+ publish_run_info(
117
+ usage: {
118
+ cost: usages.sum { |run_usage| llm_call_cost(run_usage) },
119
+ input_tokens: usages.sum { |run_usage| run_usage.input_tokens || 0 },
120
+ output_tokens: usages.sum { |run_usage| run_usage.output_tokens || 0 },
121
+ cache_read_tokens: usages.sum { |run_usage| run_usage.cached_tokens || 0 },
122
+ cache_write_tokens: usages.sum { |run_usage| run_usage.cache_creation_tokens || 0 },
123
+ # Context tokens are not summed, as each LLM call contains the full context: keep the last call's value.
124
+ context_tokens: llm_call_context_tokens(last_response),
125
+ context_tokens_limit: llm_call_context_tokens_limit(last_response)
126
+ }
76
127
  )
77
128
  end
78
129
 
130
+ # Compute the monetary cost of a single LLM call.
131
+ # The cost is computed by RubyLLM from the model's pricing in its registry.
132
+ # Best-effort: components with unknown pricing are not counted (their value is nil).
133
+ #
134
+ # @param response [RubyLLM::Message] The response of the LLM call
135
+ # @return [Float] The monetary cost of the LLM call
136
+ def llm_call_cost(response)
137
+ return 0.0 unless response.respond_to?(:cost)
138
+
139
+ cost = response.cost
140
+ %i[input output cache_read cache_write thinking].sum(0.0) { |component| cost.public_send(component) || 0.0 }
141
+ end
142
+
143
+ # Compute the number of context tokens consumed by a single LLM call.
144
+ # Each LLM call contains the full context of the conversation, plus the tokens generated in its response.
145
+ #
146
+ # @param response [RubyLLM::Message] The response of the LLM call
147
+ # @return [Integer] Number of context tokens consumed by the LLM call
148
+ def llm_call_context_tokens(response)
149
+ tokens = response.tokens
150
+ %i[input output cached cache_creation thinking].sum { |component| tokens.public_send(component) || 0 }
151
+ end
152
+
153
+ # Get the context window limit of the model used by a single LLM call.
154
+ #
155
+ # @param response [RubyLLM::Message] The response of the LLM call
156
+ # @return [Integer, nil] Maximum context window limit of the model, or nil if unknown
157
+ def llm_call_context_tokens_limit(response)
158
+ response.model_info&.context_window
159
+ end
160
+
79
161
  # Process a user prompt.
80
162
  #
81
163
  # @param user_prompt [String] The rendered user prompt
@@ -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."
@@ -8,6 +8,18 @@ module ComposableAgents
8
8
  end
9
9
 
10
10
  # Agent implementation that uses an ai-agent's AgentRunner.
11
+ # This agent publishes the following run information:
12
+ # - usage [Hash] Cumulative usage of the API requests (cost and tokens) made during the run.
13
+ # This information is published in realtime, as soon as the Cline CLI reports API requests.
14
+ # Here are the properties it contains:
15
+ # - cost [Float] Cumulative monetary cost of all API requests of the run.
16
+ # - input_tokens [Integer] Cumulative number of input tokens of all API requests of the run.
17
+ # - output_tokens [Integer] Cumulative number of output tokens of all API requests of the run.
18
+ # - cache_read_tokens [Integer] Cumulative number of tokens read from cache by all API requests of the run.
19
+ # - cache_write_tokens [Integer] Cumulative number of tokens written to cache by all API requests of the run.
20
+ # - context_tokens [Integer, nil] Number of context tokens consumed by the last API request of the run.
21
+ # This property is not cumulative, as each API request contains the full context.
22
+ # - context_tokens_limit [Integer, nil] Maximum context window limit of the model used, or nil if unknown.
11
23
  class Agent < PromptDrivenAgent
12
24
  # @!group Public API
13
25
 
@@ -57,6 +69,18 @@ module ComposableAgents
57
69
  "#{name || 'Unnamed'} (Cline #{@provider}/#{@model})"
58
70
  end
59
71
 
72
+ # Execute the agent to generate some output artifacts based on some input artifacts.
73
+ # Also reset the usage statistics accumulated for this run (published realtime in the run info).
74
+ #
75
+ # @param user_instructions [Object, nil] Instructions for the user prompt, that will be rendered.
76
+ # The kind of instructions that can be given are defined by the Instructions's constructor (see Instructions#initialize).
77
+ # @param input_artifacts [Hash{Symbol => Object}] The input artifacts content, per artifact name
78
+ # @return [Hash{Symbol => Object}] The output artifacts
79
+ def run(user_instructions: nil, **input_artifacts)
80
+ @usage_by_ts = {}
81
+ super
82
+ end
83
+
60
84
  # @!group Internal
61
85
 
62
86
  # Export the agent state for persistence
@@ -112,6 +136,25 @@ module ComposableAgents
112
136
  parse_output_artifacts(content.text) if content.type == 'text' && content.text
113
137
  end
114
138
  end
139
+ # Track usage (cost and tokens) of API requests, and publish it realtime in the run info.
140
+ # Messages are keyed by their timestamp, as the same message can be updated (last version wins),
141
+ # and the usage of all API requests of the session is summed to give the run's cumulative usage.
142
+ next unless (usage = message.usage)
143
+
144
+ @usage_by_ts[message.ts] = usage
145
+ usages = @usage_by_ts.values
146
+ publish_run_info(
147
+ usage: {
148
+ cost: usages.sum { |run_usage| run_usage.cost || 0.0 },
149
+ input_tokens: usages.sum { |run_usage| run_usage.input_tokens || 0 },
150
+ output_tokens: usages.sum { |run_usage| run_usage.output_tokens || 0 },
151
+ cache_read_tokens: usages.sum { |run_usage| run_usage.cache_read_tokens || 0 },
152
+ cache_write_tokens: usages.sum { |run_usage| run_usage.cache_write_tokens || 0 },
153
+ # Context tokens are not summed, as each API request contains the full context: keep the last request's value.
154
+ context_tokens: usages.last&.context_tokens,
155
+ context_tokens_limit: usages.last&.context_tokens_limit
156
+ }
157
+ )
115
158
  end,
116
159
  **@cli_options
117
160
  )
@@ -166,7 +209,7 @@ module ComposableAgents
166
209
  # Setup the temporary Cline global config dir
167
210
  agent_tmp_dir = "#{@composable_agents_dir}/tmp/#{Time.now.utc.strftime('%F-%H-%M-%S')}#{"-#{name.gsub(/[^\w.]/, '_')}" if name}"
168
211
  ::Cline.configure do |config|
169
- config.debug = Mixins::Logger.debug?
212
+ config.debug = logger_debug?
170
213
  config.temp_dir_root = "#{agent_tmp_dir}/cline-rb"
171
214
  end
172
215
  cline_config = ::Cline::Config.open("#{agent_tmp_dir}/cline_config", create: true)
@@ -176,7 +219,7 @@ module ComposableAgents
176
219
  new_skill = cline_config.skills.new(skill_name)
177
220
  new_skill.files.replace(::Cline::Config.global.skills[skill_name].files)
178
221
  new_skill.enable
179
- log_debug "[Cline] - Enable global skill #{skill_name}"
222
+ logger.debug "[Cline] - Enable global skill #{skill_name}"
180
223
  new_skill.save
181
224
  end
182
225
  end
@@ -186,10 +229,10 @@ module ComposableAgents
186
229
  next if skill.enabled? == selected_skill
187
230
 
188
231
  if selected_skill
189
- log_debug "[Cline] - Enable project skill #{skill_name}"
232
+ logger.debug "[Cline] - Enable project skill #{skill_name}"
190
233
  skill.enable
191
234
  else
192
- log_debug "[Cline] - Disable project skill #{skill_name}"
235
+ logger.debug "[Cline] - Disable project skill #{skill_name}"
193
236
  skill.disable
194
237
  end
195
238
  skill.save
@@ -218,10 +261,17 @@ module ComposableAgents
218
261
  global_settings.auto_update_enabled = false
219
262
  @configure_global&.call(global_settings)
220
263
  global_settings.save
221
- cline_config.cli(stdout_echo: Mixins::Logger.debug?, verbose: Mixins::Logger.debug?)
264
+ cline_config.cli(stdout_echo: logger_debug?, verbose: logger_debug?)
222
265
  end
223
266
  end
224
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
+
225
275
  # Find a skill among the current Cline environment (global and project) and recursively finds all its dependencies.
226
276
  #
227
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:
@@ -15,6 +16,50 @@ module ComposableAgents
15
16
  # This mixin uses the following methods from the agent:
16
17
  # - `#export_state -> Object` Optional method returning the current JSON-serializable state of the agent.
17
18
  # - `#import_state(state)` Optional method that sets the agent state from a JSON-serializable object.
19
+ #
20
+ # This mixin publishes the following run information:
21
+ # - steps [Array<Hash>>] Hierarchy of the steps executed during the run.
22
+ # Each step node has those properties:
23
+ # - step_name [Symbol] The step name.
24
+ # - index [Array<Integer>] Position of the step in the hierarchy of recursive step calls.
25
+ # It is the succession of indices from the root of the hierarchy to this step node,
26
+ # each index being the position of the node among its sibling nodes (children of the same parent).
27
+ # For example here are the values of the index property of the step nodes if we have this code:
28
+ # # index == [0]
29
+ # step(:a) do
30
+ # # index == [0, 0]
31
+ # step(:a1) do
32
+ # # index == [0, 0, 0]
33
+ # end
34
+ # # index == [0, 1]
35
+ # step(:a2) do
36
+ # # index == [0, 1, 0]
37
+ # step(:a21) do
38
+ # # index == [0, 1, 0, 0]
39
+ # end
40
+ # # index == [0, 1, 1]
41
+ # step(:a22) do
42
+ # # index == [0, 1, 1, 0]
43
+ # end
44
+ # # index == [0, 1, 2]
45
+ # end
46
+ # # index == [0, 2]
47
+ # end
48
+ # # index == [1]
49
+ # step(:b) do
50
+ # # index == [1, 0]
51
+ # end
52
+ # - status [Symbol] The status of the step:
53
+ # - `started``: The step has started its execution.
54
+ # - `cached``: The step was skipped because of a previous execution.
55
+ # - `executed``: The step has completed its execution.
56
+ # - `error``: The step has raised an exception during its execution.
57
+ # - created_at [Time] Timestamp of the creation of this step node.
58
+ # - extra_input_artifacts [Hash] Additional artifacts given as input to this step.
59
+ # - agent [Agent, nil] The agent that is invoked in this step, or nil if none.
60
+ # - error [String, nil] The message of the exception raised during the step execution.
61
+ # This property is only present when the status is `error`.
62
+ # - children [Array<Hash>] Sub-steps that are invoked from this step.
18
63
  module Resumable
19
64
  # @!group Public API
20
65
 
@@ -23,7 +68,6 @@ module ComposableAgents
23
68
  # @param run_id [String, nil] ID identifying this run to reuse previously executed steps, or nil if there is no resumability needed
24
69
  def initialize(*args, run_id: nil, **kwargs)
25
70
  super(*args, **kwargs)
26
- @steps_run = []
27
71
  @run_id = run_id
28
72
  end
29
73
 
@@ -36,59 +80,11 @@ module ComposableAgents
36
80
  def run(**input_artifacts)
37
81
  # The artifacts store, JSON serializable
38
82
  @artifacts = input_artifacts.dup
39
- # List of the steps_run hierarchies, one per run. Each run appends its own new list.
40
- @steps_run << []
83
+ @current_run_steps = []
41
84
  @current_step_node = nil
42
85
  super
43
86
  end
44
87
 
45
- # Hierarchies of the steps executed during each run of this agent.
46
- #
47
- # @return [Array<Array<Hash{Symbol => Object}>>>] List of per-run step hierarchies.
48
- # Each element of this array corresponds to a run, and is the ordered list of top-level step nodes of that run.
49
- # Each step node has those properties:
50
- # - step_name [Symbol] The step name.
51
- # - index [Array<Integer>] Position of the step in the hierarchy of recursive step calls.
52
- # It is the succession of indices from the root of the hierarchy to this step node,
53
- # each index being the position of the node among its sibling nodes (children of the same parent).
54
- # For example here are the values of the index property of the step nodes if we have this code:
55
- # # index == [0]
56
- # step(:a) do
57
- # # index == [0, 0]
58
- # step(:a1) do
59
- # # index == [0, 0, 0]
60
- # end
61
- # # index == [0, 1]
62
- # step(:a2) do
63
- # # index == [0, 1, 0]
64
- # step(:a21) do
65
- # # index == [0, 1, 0, 0]
66
- # end
67
- # # index == [0, 1, 1]
68
- # step(:a22) do
69
- # # index == [0, 1, 1, 0]
70
- # end
71
- # # index == [0, 1, 2]
72
- # end
73
- # # index == [0, 2]
74
- # end
75
- # # index == [1]
76
- # step(:b) do
77
- # # index == [1, 0]
78
- # end
79
- # - status [Symbol] The status of the step:
80
- # - `started``: The step has started its execution.
81
- # - `cached``: The step was skipped because of a previous execution.
82
- # - `executed``: The step has completed its execution.
83
- # - `error``: The step has raised an exception during its execution.
84
- # - created_at [Time] Timestamp of the creation of this step node.
85
- # - extra_input_artifacts [Hash] Additional artifacts given as input to this step.
86
- # - agent [Agent, nil] The agent that is invoked in this step, or nil if none.
87
- # - error [String, nil] The message of the exception raised during the step execution.
88
- # This property is only present when the status is `error`.
89
- # - children [Array<Hash>] Sub-steps that are invoked from this step.
90
- attr_reader :steps_run
91
-
92
88
  private
93
89
 
94
90
  # Define a step that can be serialized and resumed.
@@ -144,14 +140,14 @@ module ComposableAgents
144
140
  if step_state == saved_input_state
145
141
  @current_step_node[:status] = :cached
146
142
  set_current_step_state(saved_output_state, agent:)
147
- 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(', ')}"
148
144
  else
149
145
  # Clone state before yielding because it will certainly be modified
150
146
  input_step_state = clone_step_state(step_state)
151
147
  yield
152
148
  @current_step_node[:status] = :executed
153
149
  store_step_states(step_full_name, input: input_step_state, output: current_step_state(agent:))
154
- 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(', ')}"
155
151
  end
156
152
  end
157
153
  end
@@ -165,13 +161,13 @@ module ComposableAgents
165
161
  # @param extra_input_artifacts [Hash{Symbol => Object}] Input artifacts given to the step.
166
162
  # @yield The code of the step to be executed
167
163
  def record_step(step_name:, agent:, extra_input_artifacts:)
168
- @steps_run ||= []
164
+ publish_run_info(steps: @current_run_steps) unless current_run_info.respond_to?(:steps)
169
165
  parent_node = @current_step_node
170
166
  node = {
171
167
  step_name:,
172
168
  # Compute the index of this step in the hierarchy of recursive step calls:
173
169
  # the indices of the ancestor step nodes, followed by this node's position among its sibling nodes.
174
- index: (parent_node ? parent_node[:index] : []) + [(parent_node ? parent_node[:children] : @steps_run.last).size],
170
+ index: (parent_node ? parent_node[:index] : []) + [(parent_node ? parent_node[:children] : @current_run_steps).size],
175
171
  status: :started,
176
172
  created_at: Time.now,
177
173
  agent:,
@@ -181,7 +177,7 @@ module ComposableAgents
181
177
  if parent_node
182
178
  parent_node[:children] << node
183
179
  else
184
- @steps_run.last << node
180
+ @current_run_steps << node
185
181
  end
186
182
  @current_step_node = node
187
183
  begin
@@ -68,11 +68,12 @@ module ComposableAgents
68
68
  # @param input_artifacts [Hash{Symbol => Object}] The input artifacts content, per artifact name
69
69
  # @return [Hash{Symbol => Object}] The output artifacts
70
70
  def run(user_instructions: nil, **input_artifacts)
71
+ super
71
72
  @input_artifacts = input_artifacts
72
73
  @output_artifacts = {}
73
74
  @output_artifacts_errors = {}
74
75
  @system_prompt = render_system_prompt(render_instructions(@system_instructions))
75
- log_debug "System prompt: #{@system_prompt}"
76
+ logger.debug "System prompt: #{@system_prompt}"
76
77
  converse(user_instructions, input_artifacts: @input_artifacts, author: 'User')
77
78
  if respond_to?(:normalized_output_artifacts_contracts, true)
78
79
  # We know which output artifacts we are expecting.
@@ -147,7 +148,7 @@ module ComposableAgents
147
148
  def save_output_artifact(artifact_name, content)
148
149
  @output_artifacts[artifact_name] = content
149
150
  @output_artifacts_errors.delete(artifact_name)
150
- log_debug "[Artifact] - Received output artifact #{artifact_name}"
151
+ logger.debug "[Artifact] - Received output artifact #{artifact_name}"
151
152
  end
152
153
 
153
154
  # Report an error on an output artifact.
@@ -161,7 +162,7 @@ module ComposableAgents
161
162
  @output_artifacts.delete(artifact_name)
162
163
  @output_artifacts_errors[artifact_name] = error
163
164
  # TODO: Make this as a warning message
164
- 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}` " \
165
166
  "but the following error occurred: #{error}"
166
167
  end
167
168
 
@@ -190,10 +191,10 @@ module ComposableAgents
190
191
  def converse(instructions, input_artifacts: {}, author: 'Orchestrator')
191
192
  rendered_instructions = render_instructions(instructions)
192
193
  rendered_user_prompt = render_user_prompt(rendered_instructions, input_artifacts:)
193
- log_debug "Rendered User prompt: #{rendered_user_prompt}"
194
+ logger.debug "Rendered User prompt: #{rendered_user_prompt}"
194
195
  track_message(message: rendered_instructions, author:)
195
196
  response = prompt(rendered_user_prompt)
196
- log_debug "Raw Agent #{full_name} response: #{response}"
197
+ logger.debug "Raw Agent #{full_name} response: #{response}"
197
198
  track_message(message: response, author: "Agent #{full_name}")
198
199
  end
199
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
  #{
@@ -22,6 +22,7 @@ module ComposableAgents
22
22
  # @param input_artifacts [Hash{Symbol => Object}] The input artifacts content, per artifact name
23
23
  # @return [Hash{Symbol => Object}] The output artifacts returned by the Proc
24
24
  def run(**input_artifacts)
25
+ super
25
26
  @processor.call(input_artifacts)
26
27
  end
27
28
  end
@@ -0,0 +1,19 @@
1
+ module ComposableAgents
2
+ # Store runtime information about a run
3
+ class RunInfo
4
+ # Constructor
5
+ def initialize
6
+ @properties = {}
7
+ end
8
+
9
+ # Publish properties
10
+ #
11
+ # @param kwargs [Hash{Symbol => Object}] Set of properties to publish
12
+ def publish(**kwargs)
13
+ kwargs.each do |name, value|
14
+ @properties[name] = value
15
+ singleton_class.define_method(name) { @properties.fetch(name) } unless singleton_class.respond_to?(name)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -2,5 +2,5 @@ module ComposableAgents
2
2
  # @!group Public API
3
3
 
4
4
  # Gem version
5
- VERSION = '1.1.2'
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: 1.1.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
@@ -77,19 +77,22 @@ 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
90
92
  - lib/composable_agents/prompt_rendering_strategy/markdown.rb
91
93
  - lib/composable_agents/prompt_rendering_strategy/markdown_heavy.rb
92
94
  - lib/composable_agents/ruby_agent.rb
95
+ - lib/composable_agents/run_info.rb
93
96
  - lib/composable_agents/utils/markdown.rb
94
97
  - lib/composable_agents/version.rb
95
98
  homepage: https://github.com/Muriel-Salvan/composable_agents
@@ -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