agent_c 2.9
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 +7 -0
- data/.rubocop.yml +10 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +21 -0
- data/README.md +360 -0
- data/Rakefile +16 -0
- data/TODO.md +105 -0
- data/agent_c.gemspec +38 -0
- data/docs/batch.md +503 -0
- data/docs/chat-methods.md +156 -0
- data/docs/cost-reporting.md +86 -0
- data/docs/pipeline-tips-and-tricks.md +453 -0
- data/docs/session-configuration.md +274 -0
- data/docs/testing.md +747 -0
- data/docs/tools.md +103 -0
- data/docs/versioned-store.md +840 -0
- data/lib/agent_c/agent/chat.rb +211 -0
- data/lib/agent_c/agent/chat_response.rb +38 -0
- data/lib/agent_c/agent/chats/anthropic_bedrock.rb +48 -0
- data/lib/agent_c/batch.rb +102 -0
- data/lib/agent_c/configs/repo.rb +90 -0
- data/lib/agent_c/context.rb +56 -0
- data/lib/agent_c/costs/data.rb +39 -0
- data/lib/agent_c/costs/report.rb +219 -0
- data/lib/agent_c/db/store.rb +162 -0
- data/lib/agent_c/errors.rb +19 -0
- data/lib/agent_c/pipeline.rb +152 -0
- data/lib/agent_c/pipelines/agent.rb +219 -0
- data/lib/agent_c/processor.rb +98 -0
- data/lib/agent_c/prompts.yml +53 -0
- data/lib/agent_c/schema.rb +71 -0
- data/lib/agent_c/session.rb +206 -0
- data/lib/agent_c/store.rb +72 -0
- data/lib/agent_c/test_helpers.rb +173 -0
- data/lib/agent_c/tools/dir_glob.rb +46 -0
- data/lib/agent_c/tools/edit_file.rb +114 -0
- data/lib/agent_c/tools/file_metadata.rb +43 -0
- data/lib/agent_c/tools/git_status.rb +30 -0
- data/lib/agent_c/tools/grep.rb +119 -0
- data/lib/agent_c/tools/paths.rb +36 -0
- data/lib/agent_c/tools/read_file.rb +94 -0
- data/lib/agent_c/tools/run_rails_test.rb +87 -0
- data/lib/agent_c/tools.rb +61 -0
- data/lib/agent_c/utils/git.rb +87 -0
- data/lib/agent_c/utils/shell.rb +58 -0
- data/lib/agent_c/version.rb +5 -0
- data/lib/agent_c.rb +32 -0
- data/lib/versioned_store/base.rb +314 -0
- data/lib/versioned_store/config.rb +26 -0
- data/lib/versioned_store/stores/schema.rb +127 -0
- data/lib/versioned_store/version.rb +5 -0
- data/lib/versioned_store.rb +5 -0
- data/template/Gemfile +9 -0
- data/template/Gemfile.lock +152 -0
- data/template/README.md +61 -0
- data/template/Rakefile +50 -0
- data/template/bin/rake +27 -0
- data/template/lib/autoload.rb +10 -0
- data/template/lib/config.rb +59 -0
- data/template/lib/pipeline.rb +19 -0
- data/template/lib/prompts.yml +57 -0
- data/template/lib/store.rb +17 -0
- data/template/test/pipeline_test.rb +221 -0
- data/template/test/test_helper.rb +18 -0
- metadata +194 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
module Costs
|
|
5
|
+
class Report
|
|
6
|
+
|
|
7
|
+
# Anthropic models Price per 1,000 input tokens Price per 1,000 output tokens Price per 1,000 input tokens (batch) Price per 1,000 output tokens (batch) Price per 1,000 input tokens (5m cache write) Price per 1,000 input tokens (1h cache write) Price per 1,000 input tokens (cache read)
|
|
8
|
+
# Claude Sonnet 4.5 $0.003 $0.015 $0.0015 $0.0075 $0.00375 $0.006 $0.0003
|
|
9
|
+
# Claude Sonnet 4.5 - Long Context $0.006 $0.0225 $0.003 $0.01125 $0.0075 $0.012 $0.0006
|
|
10
|
+
|
|
11
|
+
PRICING = {
|
|
12
|
+
normal: {
|
|
13
|
+
input: 3.0, # $0.003 per 1k tokens
|
|
14
|
+
output: 15.0, # $0.015 per 1k tokens
|
|
15
|
+
cached_input: 0.3, # $0.0003 per 1k tokens (cache read)
|
|
16
|
+
cache_creation: 3.75 # $0.00375 per 1k tokens (5m cache write)
|
|
17
|
+
},
|
|
18
|
+
long: {
|
|
19
|
+
input: 6.0, # $0.006 per 1k tokens
|
|
20
|
+
output: 22.5, # $0.0225 per 1k tokens
|
|
21
|
+
cached_input: 0.6, # $0.0006 per 1k tokens (cache read)
|
|
22
|
+
cache_creation: 7.5 # $0.0075 per 1k tokens (5m cache write)
|
|
23
|
+
}
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
LONG_CONTEXT_THRESHOLD = 200_000 # tokens
|
|
27
|
+
|
|
28
|
+
def self.call(...)
|
|
29
|
+
new(...).call
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(agent_store:, project: nil, run_id: nil, out: $stdout)
|
|
33
|
+
@agent_store = agent_store
|
|
34
|
+
@project = project
|
|
35
|
+
@run_id = run_id
|
|
36
|
+
@out = out
|
|
37
|
+
@calculator = Calculator.new
|
|
38
|
+
@printer = Printer.new(out: @out)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def call
|
|
42
|
+
# Gather all computations based on hierarchy
|
|
43
|
+
computations = []
|
|
44
|
+
|
|
45
|
+
# Always compute totals for all projects
|
|
46
|
+
all_messages = fetch_all_messages
|
|
47
|
+
if all_messages.any?
|
|
48
|
+
computations << {
|
|
49
|
+
label: "All Projects",
|
|
50
|
+
stats: @calculator.calculate(all_messages),
|
|
51
|
+
messages: all_messages
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# If project is specified, add project-level computation
|
|
56
|
+
if @project
|
|
57
|
+
project_messages = fetch_project_messages
|
|
58
|
+
if project_messages.any?
|
|
59
|
+
computations << {
|
|
60
|
+
label: "Project: #{@project}",
|
|
61
|
+
stats: @calculator.calculate(project_messages),
|
|
62
|
+
messages: project_messages
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# If run_id is also specified, add run-level computation
|
|
67
|
+
if @run_id
|
|
68
|
+
run_messages = fetch_run_messages
|
|
69
|
+
if run_messages.any?
|
|
70
|
+
computations << {
|
|
71
|
+
label: "Project: #{@project}, Run ID: #{@run_id}",
|
|
72
|
+
stats: @calculator.calculate(run_messages),
|
|
73
|
+
messages: run_messages
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if computations.empty?
|
|
80
|
+
@out.puts "No messages found" if @out
|
|
81
|
+
return []
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Print to output if provided
|
|
85
|
+
if @out
|
|
86
|
+
@printer.print_hierarchical_report(computations)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Return structured data
|
|
90
|
+
computations
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def fetch_all_messages
|
|
96
|
+
@agent_store.message
|
|
97
|
+
.joins(:chat)
|
|
98
|
+
.includes(:model, :chat)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def fetch_project_messages
|
|
102
|
+
@agent_store.message
|
|
103
|
+
.joins(:chat)
|
|
104
|
+
.where(chats: { project: @project })
|
|
105
|
+
.includes(:model, :chat)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def fetch_run_messages
|
|
109
|
+
@agent_store.message
|
|
110
|
+
.joins(:chat)
|
|
111
|
+
.where(chats: { project: @project, run_id: @run_id })
|
|
112
|
+
.includes(:model, :chat)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Calculator class for computing costs
|
|
116
|
+
class Calculator
|
|
117
|
+
def calculate(messages)
|
|
118
|
+
stats = {
|
|
119
|
+
input_tokens: 0,
|
|
120
|
+
output_tokens: 0,
|
|
121
|
+
cached_tokens: 0,
|
|
122
|
+
cache_creation_tokens: 0,
|
|
123
|
+
input_cost: 0.0,
|
|
124
|
+
output_cost: 0.0,
|
|
125
|
+
cached_cost: 0.0,
|
|
126
|
+
cache_creation_cost: 0.0,
|
|
127
|
+
total_cost: 0.0,
|
|
128
|
+
message_count: messages.count
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
messages.each do |message|
|
|
132
|
+
stats[:input_tokens] += message.input_tokens || 0
|
|
133
|
+
stats[:output_tokens] += message.output_tokens || 0
|
|
134
|
+
stats[:cached_tokens] += message.cached_tokens || 0
|
|
135
|
+
stats[:cache_creation_tokens] += message.cache_creation_tokens || 0
|
|
136
|
+
|
|
137
|
+
# Determine which pricing tier to use based on total token count
|
|
138
|
+
total_message_tokens = (message.input_tokens || 0) +
|
|
139
|
+
(message.cached_tokens || 0) +
|
|
140
|
+
(message.cache_creation_tokens || 0)
|
|
141
|
+
pricing = total_message_tokens > LONG_CONTEXT_THRESHOLD ? PRICING[:long] : PRICING[:normal]
|
|
142
|
+
|
|
143
|
+
# Calculate cost using appropriate pricing tier
|
|
144
|
+
# Input tokens cost
|
|
145
|
+
if message.input_tokens && message.input_tokens > 0
|
|
146
|
+
cost = (message.input_tokens / 1_000_000.0) * pricing[:input]
|
|
147
|
+
stats[:input_cost] += cost
|
|
148
|
+
stats[:total_cost] += cost
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Output tokens cost
|
|
152
|
+
if message.output_tokens && message.output_tokens > 0
|
|
153
|
+
cost = (message.output_tokens / 1_000_000.0) * pricing[:output]
|
|
154
|
+
stats[:output_cost] += cost
|
|
155
|
+
stats[:total_cost] += cost
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Cached tokens cost (cache read)
|
|
159
|
+
if message.cached_tokens && message.cached_tokens > 0
|
|
160
|
+
cost = (message.cached_tokens / 1_000_000.0) * pricing[:cached_input]
|
|
161
|
+
stats[:cached_cost] += cost
|
|
162
|
+
stats[:total_cost] += cost
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Cache creation tokens cost
|
|
166
|
+
if message.cache_creation_tokens && message.cache_creation_tokens > 0
|
|
167
|
+
cost = (message.cache_creation_tokens / 1_000_000.0) * pricing[:cache_creation]
|
|
168
|
+
stats[:cache_creation_cost] += cost
|
|
169
|
+
stats[:total_cost] += cost
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
stats
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Printer class for formatting output
|
|
178
|
+
class Printer
|
|
179
|
+
def initialize(out: $stdout)
|
|
180
|
+
@out = out
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def print_hierarchical_report(computations)
|
|
184
|
+
computations.each_with_index do |computation, index|
|
|
185
|
+
print_section(computation[:label], computation[:stats], computation[:messages])
|
|
186
|
+
@out.puts "" if index < computations.length - 1
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def print_section(label, stats, messages)
|
|
193
|
+
total_tokens = stats[:input_tokens] + stats[:output_tokens] + stats[:cached_tokens] + stats[:cache_creation_tokens]
|
|
194
|
+
|
|
195
|
+
@out.puts <<~REPORT
|
|
196
|
+
============================================================
|
|
197
|
+
Cost Report: #{label}
|
|
198
|
+
============================================================
|
|
199
|
+
Messages: #{stats[:message_count]}
|
|
200
|
+
Chats: #{messages.map(&:chat).uniq.count}
|
|
201
|
+
------------------------------------------------------------
|
|
202
|
+
Token Usage:
|
|
203
|
+
Input tokens: #{format_number(stats[:input_tokens]).rjust(15)} $#{format('%.4f', stats[:input_cost]).rjust(8)}
|
|
204
|
+
Output tokens: #{format_number(stats[:output_tokens]).rjust(15)} $#{format('%.4f', stats[:output_cost]).rjust(8)}
|
|
205
|
+
Cached tokens: #{format_number(stats[:cached_tokens]).rjust(15)} $#{format('%.4f', stats[:cached_cost]).rjust(8)}
|
|
206
|
+
Cache creation tokens: #{format_number(stats[:cache_creation_tokens]).rjust(15)} $#{format('%.4f', stats[:cache_creation_cost]).rjust(8)}
|
|
207
|
+
------------------------------------------------------------
|
|
208
|
+
Total: #{format_number(total_tokens).rjust(15)} $#{format('%.4f', stats[:total_cost]).rjust(8)}
|
|
209
|
+
============================================================
|
|
210
|
+
REPORT
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def format_number(num)
|
|
214
|
+
num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require "ruby_llm/active_record/acts_as"
|
|
5
|
+
|
|
6
|
+
module AgentC
|
|
7
|
+
module Db
|
|
8
|
+
class Store < VersionedStore::Base
|
|
9
|
+
record(:model) do
|
|
10
|
+
schema :models do |t|
|
|
11
|
+
t.string :model_id, null: false
|
|
12
|
+
t.string :name, null: false
|
|
13
|
+
t.string :provider, null: false
|
|
14
|
+
t.string :family
|
|
15
|
+
t.datetime :model_created_at
|
|
16
|
+
t.integer :context_window
|
|
17
|
+
t.integer :max_output_tokens
|
|
18
|
+
t.date :knowledge_cutoff
|
|
19
|
+
t.json :modalities, default: {}
|
|
20
|
+
t.json :capabilities, default: []
|
|
21
|
+
t.json :pricing, default: {}
|
|
22
|
+
t.json :metadata, default: {}
|
|
23
|
+
t.timestamps
|
|
24
|
+
|
|
25
|
+
t.index [:provider, :model_id], unique: true
|
|
26
|
+
t.index :provider
|
|
27
|
+
t.index :family
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
include RubyLLM::ActiveRecord::ActsAs
|
|
31
|
+
acts_as_model chats: :chats, chat_class: class_name(:chat)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
record(:chat) do
|
|
35
|
+
schema :chats do |t|
|
|
36
|
+
t.references :model, foreign_key: true
|
|
37
|
+
t.string :project
|
|
38
|
+
t.string :run_id
|
|
39
|
+
t.timestamps
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
include RubyLLM::ActiveRecord::ActsAs
|
|
43
|
+
|
|
44
|
+
acts_as_chat(
|
|
45
|
+
messages: :messages,
|
|
46
|
+
message_class: class_name(:message),
|
|
47
|
+
messages_foreign_key: :chat_id,
|
|
48
|
+
model: :model,
|
|
49
|
+
model_class: class_name(:model),
|
|
50
|
+
model_foreign_key: :model_id
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
belongs_to(
|
|
54
|
+
:model,
|
|
55
|
+
class_name: class_name(:model),
|
|
56
|
+
required: false
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
validates :model, presence: true
|
|
60
|
+
|
|
61
|
+
def messages_hash
|
|
62
|
+
messages
|
|
63
|
+
.map {
|
|
64
|
+
hash = _1.to_llm.to_h
|
|
65
|
+
|
|
66
|
+
if hash.key?(:tool_calls)
|
|
67
|
+
hash[:tool_calls] = hash.fetch(:tool_calls).values.map(&:to_h)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if hash.key?(:content) && !hash[:content].is_a?(String)
|
|
71
|
+
hash[:content] = hash[:content].to_h
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
hash
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
record(:message) do
|
|
80
|
+
schema :messages do |t|
|
|
81
|
+
t.references :chat, null: false, foreign_key: true
|
|
82
|
+
t.references :model, foreign_key: true
|
|
83
|
+
t.references :tool_call, foreign_key: true
|
|
84
|
+
t.string :role, null: false
|
|
85
|
+
t.text :content
|
|
86
|
+
t.json :content_raw
|
|
87
|
+
t.integer :input_tokens
|
|
88
|
+
t.integer :output_tokens
|
|
89
|
+
t.integer :cached_tokens
|
|
90
|
+
t.integer :cache_creation_tokens
|
|
91
|
+
t.timestamps
|
|
92
|
+
|
|
93
|
+
t.index :role
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
include RubyLLM::ActiveRecord::ActsAs
|
|
97
|
+
|
|
98
|
+
acts_as_message(
|
|
99
|
+
chat: :chat,
|
|
100
|
+
chat_class: class_name(:chat),
|
|
101
|
+
chat_foreign_key: :chat_id,
|
|
102
|
+
tool_calls: :tool_calls,
|
|
103
|
+
tool_call_class: class_name(:tool_call),
|
|
104
|
+
tool_calls_foreign_key: :message_id,
|
|
105
|
+
model: :model,
|
|
106
|
+
model_class: class_name(:model),
|
|
107
|
+
model_foreign_key: :model_id
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
belongs_to(
|
|
111
|
+
:chat,
|
|
112
|
+
class_name: class_name(:chat),
|
|
113
|
+
required: false
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
belongs_to(
|
|
117
|
+
:model,
|
|
118
|
+
class_name: class_name(:model),
|
|
119
|
+
required: false
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
belongs_to(
|
|
123
|
+
:tool_call,
|
|
124
|
+
class_name: class_name(:tool_call),
|
|
125
|
+
required: false
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
validates :role, presence: true
|
|
129
|
+
validates :chat, presence: true
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
record(:tool_call) do
|
|
133
|
+
schema :tool_calls do |t|
|
|
134
|
+
t.references :message, null: false, foreign_key: true
|
|
135
|
+
t.string :tool_call_id, null: false
|
|
136
|
+
t.string :name, null: false
|
|
137
|
+
t.json :arguments, default: {}
|
|
138
|
+
t.timestamps
|
|
139
|
+
|
|
140
|
+
t.index :tool_call_id, unique: true
|
|
141
|
+
t.index :name
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
include RubyLLM::ActiveRecord::ActsAs
|
|
145
|
+
|
|
146
|
+
acts_as_tool_call(
|
|
147
|
+
message: :message,
|
|
148
|
+
message_class: class_name(:message),
|
|
149
|
+
message_foreign_key: :message_id,
|
|
150
|
+
result: :result,
|
|
151
|
+
result_class: class_name(:message)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
belongs_to(
|
|
155
|
+
:message,
|
|
156
|
+
class_name: class_name(:message),
|
|
157
|
+
required: false
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
module Errors
|
|
5
|
+
class Base < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class AbortCostExceeded < Base
|
|
9
|
+
attr_reader :cost_type, :current_cost, :threshold
|
|
10
|
+
|
|
11
|
+
def initialize(cost_type:, current_cost:, threshold:)
|
|
12
|
+
@cost_type = cost_type
|
|
13
|
+
@current_cost = current_cost
|
|
14
|
+
@threshold = threshold
|
|
15
|
+
super("Abort: #{cost_type} cost $#{current_cost.round(2)} exceeds threshold $#{threshold.round(2)}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentC
|
|
4
|
+
class Pipeline
|
|
5
|
+
def self.call(...)
|
|
6
|
+
new(...).tap(&:call)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :session, :task
|
|
10
|
+
def initialize(
|
|
11
|
+
session:,
|
|
12
|
+
task:,
|
|
13
|
+
git: ->(dir) { Utils::Git.new(dir) }
|
|
14
|
+
)
|
|
15
|
+
@session = session
|
|
16
|
+
@task = task
|
|
17
|
+
@git = git
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Step = Data.define(:name, :block)
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def on_failures
|
|
24
|
+
@on_failures ||= []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def steps
|
|
28
|
+
@steps ||= []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def on_failure(&block)
|
|
32
|
+
self.on_failures << block
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def step(name, &block)
|
|
36
|
+
self.steps << Step.new(name:, block:)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def agent_review_loop(name, **params)
|
|
40
|
+
step(name) do
|
|
41
|
+
agent = Pipelines::Agent.new(self)
|
|
42
|
+
agent.agent_review_loop(name, **params)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def agent_step(name, **params, &block)
|
|
47
|
+
raise ArgumentError.new("Can't pass block and params") if params.any? && block
|
|
48
|
+
|
|
49
|
+
step(name) do
|
|
50
|
+
agent = Pipelines::Agent.new(self)
|
|
51
|
+
agent.agent_step(name, **params, &block)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def call
|
|
57
|
+
raise "Task.workspace is nil" unless task.workspace
|
|
58
|
+
|
|
59
|
+
log("start")
|
|
60
|
+
|
|
61
|
+
while(task.pending?)
|
|
62
|
+
break if task.failed?
|
|
63
|
+
|
|
64
|
+
step = self.class.steps.find { !task.completed_steps.include?(_1.name.to_s) }
|
|
65
|
+
break if step.nil?
|
|
66
|
+
|
|
67
|
+
@rewind_to = nil
|
|
68
|
+
|
|
69
|
+
store.transaction do
|
|
70
|
+
log_prefix = "step: '#{step.name}'"
|
|
71
|
+
|
|
72
|
+
log("#{log_prefix} start")
|
|
73
|
+
|
|
74
|
+
instance_exec(&step.block)
|
|
75
|
+
|
|
76
|
+
if task.failed?
|
|
77
|
+
log("#{log_prefix} failed, executing on_failures")
|
|
78
|
+
self.class.on_failures.each { instance_exec(&_1)}
|
|
79
|
+
elsif @rewind_to
|
|
80
|
+
matching_steps = task.completed_steps.select { _1 == @rewind_to }
|
|
81
|
+
|
|
82
|
+
if matching_steps.count == 0
|
|
83
|
+
raise ArgumentError, <<~TXT
|
|
84
|
+
Cannot rewind to a step that's not been completed yet:
|
|
85
|
+
|
|
86
|
+
rewind_to!(#{@rewind_to.inspect})
|
|
87
|
+
completed_steps: #{task.completed_steps.inspect}
|
|
88
|
+
TXT
|
|
89
|
+
elsif matching_steps.count > 1
|
|
90
|
+
raise ArgumentError, <<~TXT
|
|
91
|
+
Cannot rewind to a step with a non-distinct name. The step
|
|
92
|
+
name appears multiple times:
|
|
93
|
+
|
|
94
|
+
rewind_to!(#{@rewind_to.inspect})
|
|
95
|
+
completed_steps: #{task.completed_steps.inspect}
|
|
96
|
+
TXT
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
log("#{log_prefix} rewind_to! #{@rewind_to.inspect}")
|
|
100
|
+
task
|
|
101
|
+
.completed_steps
|
|
102
|
+
.index(@rewind_to)
|
|
103
|
+
.then { task.update!(completed_steps: task.completed_steps[0..._1]) }
|
|
104
|
+
else
|
|
105
|
+
log("#{log_prefix} done")
|
|
106
|
+
task.completed_steps << step.name.to_s
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
store.transaction do
|
|
112
|
+
log("done")
|
|
113
|
+
task.done! unless task.failed?
|
|
114
|
+
end
|
|
115
|
+
rescue => e
|
|
116
|
+
store.transaction do
|
|
117
|
+
log("Exception raised, running on_failures")
|
|
118
|
+
task.fail!(["#{e.class.name}:#{e.message}", e.backtrace].join("\n"))
|
|
119
|
+
self.class.on_failures.each { instance_exec(&_1) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def workspace
|
|
124
|
+
task.workspace
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def record
|
|
128
|
+
task.record
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def store
|
|
132
|
+
task.store
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def rewind_to!(step)
|
|
136
|
+
@rewind_to = step.to_s
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def git
|
|
140
|
+
@_git ||= @git.call(workspace.dir)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def log(msg)
|
|
144
|
+
logger.info("task: #{task.id}: #{msg}")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def logger
|
|
148
|
+
session.logger
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
end
|
|
152
|
+
end
|