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 +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +7 -10
- data/lib/composable_agents/agent.rb +38 -9
- data/lib/composable_agents/ai_agents/agent.rb +84 -2
- data/lib/composable_agents/ai_agents/tools/ask_user_tool.rb +5 -6
- data/lib/composable_agents/ai_agents/tools/base_tool.rb +22 -0
- data/lib/composable_agents/ai_agents/tools/create_artifact_tool.rb +5 -5
- data/lib/composable_agents/ai_agents/tools/get_artifact_tool.rb +6 -6
- data/lib/composable_agents/cline/agent.rb +55 -5
- data/lib/composable_agents/logger/tagged.rb +110 -0
- data/lib/composable_agents/logger.rb +48 -0
- data/lib/composable_agents/mixins/ai_agent_user_interaction.rb +1 -1
- data/lib/composable_agents/mixins/resumable.rb +51 -55
- data/lib/composable_agents/prompt_driven_agent.rb +6 -5
- data/lib/composable_agents/prompt_rendering_strategy/markdown_heavy.rb +2 -1
- 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 +5 -2
- data/lib/composable_agents/mixins/logger.rb +0 -47
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af07644d98f8997653ead8dc4b2707fe3ff6338480373cbc6fa5c5cc9aa5d82a
|
|
4
|
+
data.tar.gz: 2824ad225970ce7f3af6ff6e3d64d4a4200638af65bda0263e592466e7793e8d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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::
|
|
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::
|
|
368
|
+
#### `ComposableAgents::Logger < ::Logger`
|
|
369
369
|
|
|
370
|
-
- **Description:**
|
|
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::
|
|
375
|
+
puts ComposableAgents::Logger.instance.level == Logger::DEBUG # => true
|
|
378
376
|
```
|
|
379
|
-
- **Full documentation:**
|
|
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
|
-
- [`
|
|
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 [`
|
|
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
|
|
45
|
+
# @param _input_artifacts [Hash{Symbol => Object}] The input artifacts content
|
|
39
46
|
# @return [Hash{Symbol => Object}] Output artifacts content
|
|
40
|
-
def run(**
|
|
41
|
-
|
|
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 <
|
|
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
|
-
|
|
22
|
-
|
|
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 <
|
|
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
|
-
|
|
22
|
-
|
|
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 <
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
232
|
+
logger.debug "[Cline] - Enable project skill #{skill_name}"
|
|
190
233
|
skill.enable
|
|
191
234
|
else
|
|
192
|
-
|
|
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:
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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] : @
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
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.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
|