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 +4 -4
- data/CHANGELOG.md +56 -0
- data/README.md +1 -1
- data/lib/composable_agents/agent.rb +22 -6
- data/lib/composable_agents/ai_agents/agent.rb +82 -0
- data/lib/composable_agents/cline/agent.rb +43 -0
- data/lib/composable_agents/mixins/resumable.rb +108 -50
- data/lib/composable_agents/prompt_driven_agent.rb +1 -0
- data/lib/composable_agents/ruby_agent.rb +1 -0
- data/lib/composable_agents/run_info.rb +19 -0
- data/lib/composable_agents/version.rb +1 -1
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f0b4d0aa586936486335008d4a263e03e882150495932ca50da5a49f6266b940
|
|
4
|
+
data.tar.gz: 6dcd208bf7884791fa2ef783312207feee0525e77e1363fbe88068dc260d0e88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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**
|
|
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
|
|
41
|
+
# @param _input_artifacts [Hash{Symbol => Object}] The input artifacts content
|
|
39
42
|
# @return [Hash{Symbol => Object}] Output artifacts content
|
|
40
|
-
def run(**
|
|
41
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
@
|
|
128
|
-
|
|
129
|
-
|
|
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
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|