composable_agents 1.0.1 → 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: 3f9996134748aa3bf3ad0893b706a99a9c4611adcf5455591ef427230cebd64f
4
- data.tar.gz: 283f258cf24bf3dfca6575a0fe29f5cee5d8096de2cf0b40c73ac6060a78f76d
3
+ metadata.gz: f0b4d0aa586936486335008d4a263e03e882150495932ca50da5a49f6266b940
4
+ data.tar.gz: 6dcd208bf7884791fa2ef783312207feee0525e77e1363fbe88068dc260d0e88
5
5
  SHA512:
6
- metadata.gz: e7009799753fdd7216b8ef26c7756da633aa17665e4ca9ae6fc4f586fcc2501c2eade1e2ab7ac906fba5ddfe42888f4317cdd1611e20649d93eb3ab624ca58bc
7
- data.tar.gz: 5f374253ab859627a3c3a745f12b003e53149c3a358808990d67daef639013c0f57b3cbb6687fc2366d784937b09d932afaac893887f1d14e9a9ea3529298d3e
6
+ metadata.gz: 57b00fc7d8861359dbc399fd580f86e51f579bc40544b6bceada3cbb18c38d7ca0b895ec26a091295c5004fd43f37639b496898dfd9b8ac5de79326b16fb58ed
7
+ data.tar.gz: 56b80e997cecf2ceba654865946e8ec915a8bf9553d79738e4760709b2056430400af52fc96efea3043116732dcdc20e38ba3c578b31430c5c284afba10aae33
data/CHANGELOG.md CHANGED
@@ -1,3 +1,59 @@
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
+
26
+ # [v1.1.2](https://github.com/Muriel-Salvan/composable_agents/compare/v1.1.1...v1.1.2) (2026-09-03 16:08:18)
27
+
28
+ ### Patches
29
+
30
+ * [ci: add RubyGems credential configuration workflow](https://github.com/Muriel-Salvan/composable_agents/commit/76219abd1356bf1fc070347a74aa4472c24b4fc6)
31
+
32
+ # [v1.1.1](https://github.com/Muriel-Salvan/composable_agents/compare/v1.1.0...v1.1.1) (2026-09-03 15:35:07)
33
+
34
+ ### Patches
35
+
36
+ * [build(deps): update ai-agents, commonmarker, and zeitwerk versions](https://github.com/Muriel-Salvan/composable_agents/commit/66fc281db2e68e6f07edcde15371a865bcf97d13)
37
+
38
+ # [v1.1.0](https://github.com/Muriel-Salvan/composable_agents/compare/v1.0.1...v1.1.0) (2026-09-03 14:36:17)
39
+
40
+ ## Global changes
41
+ ### Patches
42
+
43
+ * [docs(resumable): simplify type annotations in steps_run docs](https://github.com/Muriel-Salvan/composable_agents/commit/dc4ef8838791f0d62a65646b46cd2fc9600027c6)
44
+ * [feat: record error status and message for failed steps](https://github.com/Muriel-Salvan/composable_agents/commit/db54a74e92381012db19a5a00bb02b86c0885422)
45
+ * [feat(resumable): add created_at timestamp to step run tracking](https://github.com/Muriel-Salvan/composable_agents/commit/5158c186003ec8bbcde5fb5a10fad026f94f849c)
46
+ * [refactor: track step index in steps_run hierarchy](https://github.com/Muriel-Salvan/composable_agents/commit/8c0d73c1a3abb9b762d2d09be8430480eeb5f384)
47
+ * [[Feature(Resumable)] Implement steps_run with history of all runs.](https://github.com/Muriel-Salvan/composable_agents/commit/97e3f7477155aa6e2d3e6ee85e5389bb1d2ac70e)
48
+ * [[Feature(Resumable)] Add status to steps_run](https://github.com/Muriel-Salvan/composable_agents/commit/2abd016de83cee43a821674e219deba1f6b79a24)
49
+ * [feat: add steps_run hierarchy tracking to resumable mixin](https://github.com/Muriel-Salvan/composable_agents/commit/c656ea56a8ce81729075f8d98f0227c7402106e3)
50
+
51
+ ## Changes for Resumable
52
+ ### Features
53
+
54
+ * [[Feature(Resumable)] Implement steps_run with history of all runs.](https://github.com/Muriel-Salvan/composable_agents/commit/97e3f7477155aa6e2d3e6ee85e5389bb1d2ac70e)
55
+ * [[Feature(Resumable)] Add status to steps_run](https://github.com/Muriel-Salvan/composable_agents/commit/2abd016de83cee43a821674e219deba1f6b79a24)
56
+
1
57
  # [v1.0.1](https://github.com/Muriel-Salvan/composable_agents/compare/v1.0.0...v1.0.1) (2026-07-01 16:37:12)
2
58
 
3
59
  ### Patches
data/README.md CHANGED
@@ -610,7 +610,7 @@ flowchart LR
610
610
  E --> F
611
611
  ```
612
612
 
613
- The `Resumable` mixin tracks a **hierarchical step index** (`@steps_idx`) that mirrors the nesting of `step` blocks. Each step's input/output state is saved as a JSON file. On re-execution with the same `run_id`, the framework loads the saved state instead of re-running completed steps — saving both time and API costs.
613
+ The `Resumable` mixin tracks a **hierarchical step index** for each step, stored as the `index` property of each node in the `steps_run` hierarchy, mirroring the nesting of `step` blocks. Each step's input/output state is saved as a JSON file. On re-execution with the same `run_id`, the framework loads the saved state instead of re-running completed steps — saving both time and API costs.
614
614
 
615
615
  ## Development
616
616
 
@@ -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
 
@@ -35,34 +79,8 @@ module ComposableAgents
35
79
  def run(**input_artifacts)
36
80
  # The artifacts store, JSON serializable
37
81
  @artifacts = input_artifacts.dup
38
- # List of the levels' next step index, following the hierarchy of recusive step calls.
39
- # This is only used if there is a persistent run ID.
40
- # For example here are the values of this variable if we have this code:
41
- # # @steps_idx == [0]
42
- # step(:a) do
43
- # # @steps_idx == [0, 0]
44
- # step(:a1) do
45
- # # @steps_idx == [0, 0, 0]
46
- # end
47
- # # @steps_idx == [0, 1]
48
- # step(:a2) do
49
- # # @steps_idx == [0, 1, 0]
50
- # step(:a21) do
51
- # # @steps_idx == [0, 1, 0, 0]
52
- # end
53
- # # @steps_idx == [0, 1, 1]
54
- # step(:a22) do
55
- # # @steps_idx == [0, 1, 1, 0]
56
- # end
57
- # # @steps_idx == [0, 1, 2]
58
- # end
59
- # # @steps_idx == [0, 2]
60
- # end
61
- # # @steps_idx == [1]
62
- # step(:b) do
63
- # # @steps_idx == [1, 0]
64
- # end
65
- @steps_idx = [0] unless @run_id.nil?
82
+ @current_run_steps = []
83
+ @current_step_node = nil
66
84
  super
67
85
  end
68
86
 
@@ -105,33 +123,73 @@ module ComposableAgents
105
123
  # @param extra_input_artifacts [Hash{Symbol => Object}] Additional input artifacts to merge before the step executes.
106
124
  # @yield The code called for this step
107
125
  def internal_step(step_name:, agent:, extra_input_artifacts: {})
108
- @artifacts.merge!(extra_input_artifacts)
109
- if @run_id.nil?
110
- yield
111
- else
112
- # Compute the current step state
113
- step_state = current_step_state(agent:)
114
- # Read the persisted step state if any
115
- step_full_name = "#{@steps_idx.join('-')}-#{step_name}"
116
- saved_input_state, saved_output_state = saved_step_states(step_full_name)
117
- # If the input exists, it means the step was already executed.
118
- # If it is the same state as the current one, skip the step and set the current state to the stored output step state.
119
- if step_state == saved_input_state
120
- set_current_step_state(saved_output_state, agent:)
121
- log_debug "[Step #{step_full_name}] - Already executed - Got #{@artifacts.size} artifacts from persistence: #{@artifacts.keys.join(', ')}"
122
- else
123
- # Clone state before yielding because it will certainly be modified
124
- input_step_state = clone_step_state(step_state)
125
- @steps_idx << 0
126
+ record_step(step_name:, agent:, extra_input_artifacts:) do
127
+ @artifacts.merge!(extra_input_artifacts)
128
+ if @run_id.nil?
126
129
  yield
127
- @steps_idx.pop
128
- store_step_states(step_full_name, input: input_step_state, output: current_step_state(agent:))
129
- log_debug "[Step #{step_full_name}] - Executed - Stored #{@artifacts.size} artifacts in persistence: #{@artifacts.keys.join(', ')}"
130
+ @current_step_node[:status] = :executed
131
+ else
132
+ # Compute the current step state
133
+ step_state = current_step_state(agent:)
134
+ # Read the persisted step state if any
135
+ step_full_name = "#{@current_step_node[:index].join('-')}-#{step_name}"
136
+ saved_input_state, saved_output_state = saved_step_states(step_full_name)
137
+ # If the input exists, it means the step was already executed.
138
+ # If it is the same state as the current one, skip the step and set the current state to the stored output step state.
139
+ if step_state == saved_input_state
140
+ @current_step_node[:status] = :cached
141
+ 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
+ else
144
+ # Clone state before yielding because it will certainly be modified
145
+ input_step_state = clone_step_state(step_state)
146
+ yield
147
+ @current_step_node[:status] = :executed
148
+ 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
+ end
130
151
  end
131
- @steps_idx[-1] += 1
132
152
  end
133
153
  end
134
154
 
155
+ # Record a step node in the steps hierarchy, then execute the step's code.
156
+ # Steps executed while this node is the current one are recorded as its children.
157
+ #
158
+ # @param step_name [Symbol] Name of the step, mirroring the one used by the persisted steps.
159
+ # @param agent [ComposableAgents::Agent, nil] The agent run by this step, or nil for plain steps.
160
+ # @param extra_input_artifacts [Hash{Symbol => Object}] Input artifacts given to the step.
161
+ # @yield The code of the step to be executed
162
+ def record_step(step_name:, agent:, extra_input_artifacts:)
163
+ publish_run_info(steps: @current_run_steps) unless current_run_info.respond_to?(:steps)
164
+ parent_node = @current_step_node
165
+ node = {
166
+ step_name:,
167
+ # Compute the index of this step in the hierarchy of recursive step calls:
168
+ # the indices of the ancestor step nodes, followed by this node's position among its sibling nodes.
169
+ index: (parent_node ? parent_node[:index] : []) + [(parent_node ? parent_node[:children] : @current_run_steps).size],
170
+ status: :started,
171
+ created_at: Time.now,
172
+ agent:,
173
+ extra_input_artifacts: extra_input_artifacts.clone,
174
+ children: []
175
+ }
176
+ if parent_node
177
+ parent_node[:children] << node
178
+ else
179
+ @current_run_steps << node
180
+ end
181
+ @current_step_node = node
182
+ begin
183
+ yield
184
+ rescue Exception => e # rubocop:disable Lint/RescueException -- Record the error status of any failing step, including Ctrl-C interruptions (Interrupt)
185
+ node[:status] = :error
186
+ node[:error] = e.message
187
+ raise
188
+ end
189
+ ensure
190
+ @current_step_node = parent_node
191
+ end
192
+
135
193
  # Get the current step state.
136
194
  #
137
195
  # @param agent [Agent, nil] Agent that is used in this step, or nil if none.
@@ -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.0.1'
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.0.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.10'
18
+ version: '0.12'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.10'
25
+ version: '0.12'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: cline-rb
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -43,28 +43,28 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.7'
46
+ version: '2.10'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '2.7'
53
+ version: '2.10'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: zeitwerk
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '2.7'
60
+ version: '2.8'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '2.7'
67
+ version: '2.8'
68
68
  email: muriel@x-aeon.com
69
69
  executables: []
70
70
  extensions: []
@@ -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