composable_agents 1.1.2 → 2.0.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: f0b4d0aa586936486335008d4a263e03e882150495932ca50da5a49f6266b940
4
+ data.tar.gz: 6dcd208bf7884791fa2ef783312207feee0525e77e1363fbe88068dc260d0e88
5
5
  SHA512:
6
- metadata.gz: 313bf41f9c1b24ce950ef3071adbf79a1433e4dea0d1b586964ef30854153c4f06e9a44ccbf852c2d798d8bd90e6bf2328f5f35beb799e433b0712250ad076c9
7
- data.tar.gz: 94dfd245657963c6566b796d99bd7be1d5609d30c04b68d0ee0a6845f9b1082a33ed6494a3c0b45447120f20ef0f498d064ad3c89e5d7be09f873554de1bb0f4
6
+ metadata.gz: 57b00fc7d8861359dbc399fd580f86e51f579bc40544b6bceada3cbb18c38d7ca0b895ec26a091295c5004fd43f37639b496898dfd9b8ac5de79326b16fb58ed
7
+ data.tar.gz: 56b80e997cecf2ceba654865946e8ec915a8bf9553d79738e4760709b2056430400af52fc96efea3043116732dcdc20e38ba3c578b31430c5c284afba10aae33
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ # [v2.0.0](https://github.com/Muriel-Salvan/composable_agents/compare/v1.1.2...v2.0.0) (2026-09-04 12:17:53)
2
+
3
+ ## Global changes
4
+ ### Patches
5
+
6
+ * [[Feature(AiAgents)] Publish realtime LLM usage (cost, tokens) in run info](https://github.com/Muriel-Salvan/composable_agents/commit/e6302ae30ec4a8c54b449c23ebf795f6b6a3e055)
7
+ * [feat(cline): track and publish realtime API usage in run info](https://github.com/Muriel-Salvan/composable_agents/commit/c07eed20e81eda7fd08c32fb28426ca262844aaa)
8
+ * [[Breaking(Resumable)] track steps per run and document resumable run info](https://github.com/Muriel-Salvan/composable_agents/commit/ba839759e24e6a1ca9eb4ee14e552c6b7b8da017)
9
+ * [[Feature(Agent)]: add run tracking and info publishing to agents](https://github.com/Muriel-Salvan/composable_agents/commit/67567699bdfd66d6cf65bd3095e8b08e04ca301e)
10
+
11
+ ## Changes for Agent
12
+ ### Features
13
+
14
+ * [[Feature(Agent)]: add run tracking and info publishing to agents](https://github.com/Muriel-Salvan/composable_agents/commit/67567699bdfd66d6cf65bd3095e8b08e04ca301e)
15
+
16
+ ## Changes for AiAgents
17
+ ### Features
18
+
19
+ * [[Feature(AiAgents)] Publish realtime LLM usage (cost, tokens) in run info](https://github.com/Muriel-Salvan/composable_agents/commit/e6302ae30ec4a8c54b449c23ebf795f6b6a3e055)
20
+
21
+ ## Changes for Resumable
22
+ ### Breaking changes
23
+
24
+ * [[Breaking(Resumable)] track steps per run and document resumable run info](https://github.com/Muriel-Salvan/composable_agents/commit/ba839759e24e6a1ca9eb4ee14e552c6b7b8da017)
25
+
1
26
  # [v1.1.2](https://github.com/Muriel-Salvan/composable_agents/compare/v1.1.1...v1.1.2) (2026-09-03 16:08:18)
2
27
 
3
28
  ### Patches
@@ -7,9 +7,12 @@ module ComposableAgents
7
7
 
8
8
  # @!group Public API
9
9
 
10
- # @return [String, nil] The agent name, if any
10
+ # @return [String, nil] The agent name, if any.
11
11
  attr_reader :name
12
12
 
13
+ # @return [Array<RunInfo>] The list of run information of this agent.
14
+ attr_reader :runs_info
15
+
13
16
  # Constructor
14
17
  #
15
18
  # @param name [String, nil] Agent name, or nil if none
@@ -20,6 +23,8 @@ module ComposableAgents
20
23
  )
21
24
  @name = name
22
25
  @composable_agents_dir = composable_agents_dir
26
+ @runs_info = []
27
+ @current_run_info = nil
23
28
  end
24
29
 
25
30
  # Return the full name of the agent.
@@ -31,23 +36,34 @@ module ComposableAgents
31
36
  "#{name || 'Unnamed'}#{" (#{self.class.name.split('::').last})" if self.class != Agent && self.class.name}"
32
37
  end
33
38
 
34
- # @!group Internal
35
-
36
39
  # Execute the agent to generate some output artifacts based on some input artifacts.
37
40
  #
38
- # @param input_artifacts [Hash{Symbol => Object}] The input artifacts content
41
+ # @param _input_artifacts [Hash{Symbol => Object}] The input artifacts content
39
42
  # @return [Hash{Symbol => Object}] Output artifacts content
40
- def run(**input_artifacts)
41
- raise NotImplementedError, 'This method should be implemented by an Agent subclass'
43
+ def run(**_input_artifacts)
44
+ @current_run_info = RunInfo.new
45
+ @runs_info << @current_run_info
46
+ publish_run_info(started_at: Time.now)
47
+ {}
42
48
  end
43
49
 
44
50
  private
45
51
 
52
+ # @return [RunInfo, nil] The current run info, or nil if no run has started yet
53
+ attr_reader :current_run_info
54
+
46
55
  # Fields to be logged
47
56
  #
48
57
  # @return [Array<String>] Fields to log
49
58
  def log_fields
50
59
  [full_name]
51
60
  end
61
+
62
+ # Publish run information for the current run
63
+ #
64
+ # @param kwargs [Hash{Symbol => Object}] Set of properties to publish
65
+ def publish_run_info(**kwargs)
66
+ current_run_info.publish(**kwargs)
67
+ end
52
68
  end
53
69
  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
@@ -73,9 +99,65 @@ module ComposableAgents
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
@@ -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
  )
@@ -15,6 +15,50 @@ module ComposableAgents
15
15
  # This mixin uses the following methods from the agent:
16
16
  # - `#export_state -> Object` Optional method returning the current JSON-serializable state of the agent.
17
17
  # - `#import_state(state)` Optional method that sets the agent state from a JSON-serializable object.
18
+ #
19
+ # This mixin publishes the following run information:
20
+ # - steps [Array<Hash>>] Hierarchy of the steps executed during the run.
21
+ # Each step node has those properties:
22
+ # - step_name [Symbol] The step name.
23
+ # - index [Array<Integer>] Position of the step in the hierarchy of recursive step calls.
24
+ # It is the succession of indices from the root of the hierarchy to this step node,
25
+ # each index being the position of the node among its sibling nodes (children of the same parent).
26
+ # For example here are the values of the index property of the step nodes if we have this code:
27
+ # # index == [0]
28
+ # step(:a) do
29
+ # # index == [0, 0]
30
+ # step(:a1) do
31
+ # # index == [0, 0, 0]
32
+ # end
33
+ # # index == [0, 1]
34
+ # step(:a2) do
35
+ # # index == [0, 1, 0]
36
+ # step(:a21) do
37
+ # # index == [0, 1, 0, 0]
38
+ # end
39
+ # # index == [0, 1, 1]
40
+ # step(:a22) do
41
+ # # index == [0, 1, 1, 0]
42
+ # end
43
+ # # index == [0, 1, 2]
44
+ # end
45
+ # # index == [0, 2]
46
+ # end
47
+ # # index == [1]
48
+ # step(:b) do
49
+ # # index == [1, 0]
50
+ # end
51
+ # - status [Symbol] The status of the step:
52
+ # - `started``: The step has started its execution.
53
+ # - `cached``: The step was skipped because of a previous execution.
54
+ # - `executed``: The step has completed its execution.
55
+ # - `error``: The step has raised an exception during its execution.
56
+ # - created_at [Time] Timestamp of the creation of this step node.
57
+ # - extra_input_artifacts [Hash] Additional artifacts given as input to this step.
58
+ # - agent [Agent, nil] The agent that is invoked in this step, or nil if none.
59
+ # - error [String, nil] The message of the exception raised during the step execution.
60
+ # This property is only present when the status is `error`.
61
+ # - children [Array<Hash>] Sub-steps that are invoked from this step.
18
62
  module Resumable
19
63
  # @!group Public API
20
64
 
@@ -23,7 +67,6 @@ module ComposableAgents
23
67
  # @param run_id [String, nil] ID identifying this run to reuse previously executed steps, or nil if there is no resumability needed
24
68
  def initialize(*args, run_id: nil, **kwargs)
25
69
  super(*args, **kwargs)
26
- @steps_run = []
27
70
  @run_id = run_id
28
71
  end
29
72
 
@@ -36,59 +79,11 @@ module ComposableAgents
36
79
  def run(**input_artifacts)
37
80
  # The artifacts store, JSON serializable
38
81
  @artifacts = input_artifacts.dup
39
- # List of the steps_run hierarchies, one per run. Each run appends its own new list.
40
- @steps_run << []
82
+ @current_run_steps = []
41
83
  @current_step_node = nil
42
84
  super
43
85
  end
44
86
 
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
87
  private
93
88
 
94
89
  # Define a step that can be serialized and resumed.
@@ -165,13 +160,13 @@ module ComposableAgents
165
160
  # @param extra_input_artifacts [Hash{Symbol => Object}] Input artifacts given to the step.
166
161
  # @yield The code of the step to be executed
167
162
  def record_step(step_name:, agent:, extra_input_artifacts:)
168
- @steps_run ||= []
163
+ publish_run_info(steps: @current_run_steps) unless current_run_info.respond_to?(:steps)
169
164
  parent_node = @current_step_node
170
165
  node = {
171
166
  step_name:,
172
167
  # Compute the index of this step in the hierarchy of recursive step calls:
173
168
  # 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],
169
+ index: (parent_node ? parent_node[:index] : []) + [(parent_node ? parent_node[:children] : @current_run_steps).size],
175
170
  status: :started,
176
171
  created_at: Time.now,
177
172
  agent:,
@@ -181,7 +176,7 @@ module ComposableAgents
181
176
  if parent_node
182
177
  parent_node[:children] << node
183
178
  else
184
- @steps_run.last << node
179
+ @current_run_steps << node
185
180
  end
186
181
  @current_step_node = node
187
182
  begin
@@ -68,6 +68,7 @@ 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 = {}
@@ -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.0.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.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
@@ -90,6 +90,7 @@ files:
90
90
  - lib/composable_agents/prompt_rendering_strategy/markdown.rb
91
91
  - lib/composable_agents/prompt_rendering_strategy/markdown_heavy.rb
92
92
  - lib/composable_agents/ruby_agent.rb
93
+ - lib/composable_agents/run_info.rb
93
94
  - lib/composable_agents/utils/markdown.rb
94
95
  - lib/composable_agents/version.rb
95
96
  homepage: https://github.com/Muriel-Salvan/composable_agents