ralph.rb 1.2.435535439 → 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/.github/workflows/gem-push.yml +2 -2
- data/Gemfile +1 -1
- data/Gemfile.lock +53 -0
- data/lib/ralph/cli.rb +67 -186
- data/lib/ralph/display.rb +105 -0
- data/lib/ralph/events.rb +117 -0
- data/lib/ralph/loop.rb +113 -170
- data/lib/ralph/metrics.rb +88 -0
- data/lib/ralph/opencode.rb +66 -0
- data/lib/ralph/version.rb +1 -1
- data/lib/ralph.rb +0 -3
- data/plans/00-complete-implementation.md +120 -0
- data/plans/01-cli-implementation.md +53 -0
- data/plans/02-loop-implementation.md +78 -0
- data/plans/03-agents-implementation.md +76 -0
- data/plans/04-metrics-implementation.md +98 -0
- data/plans/README.md +63 -0
- data/specs/README.md +4 -15
- data/specs/__templates__/API_TEMPLATE.md +0 -0
- data/specs/__templates__/AUTOMATION_ACTION_TEMPLATE.md +0 -0
- data/specs/__templates__/AUTOMATION_TRIGGER_TEMPLATE.md +0 -0
- data/specs/__templates__/CONTROLLER_TEMPLATE.md +32 -0
- data/specs/__templates__/INTEGRATION_TEMPLATE.md +0 -0
- data/specs/__templates__/MODEL_TEMPLATE.md +0 -0
- data/specs/agents.md +426 -120
- data/specs/cli.md +11 -218
- data/specs/lib/todo_item.rb +144 -0
- data/specs/log +15 -0
- data/specs/loop.md +42 -0
- data/specs/metrics.md +51 -0
- metadata +23 -39
- data/lib/ralph/agents/base.rb +0 -132
- data/lib/ralph/agents/claude_code.rb +0 -24
- data/lib/ralph/agents/codex.rb +0 -25
- data/lib/ralph/agents/open_code.rb +0 -30
- data/lib/ralph/agents.rb +0 -24
- data/lib/ralph/config.rb +0 -40
- data/lib/ralph/git/file_snapshot.rb +0 -60
- data/lib/ralph/helpers.rb +0 -76
- data/lib/ralph/iteration.rb +0 -220
- data/lib/ralph/output/active_loop_error.rb +0 -13
- data/lib/ralph/output/banner.rb +0 -29
- data/lib/ralph/output/completion_deferred.rb +0 -12
- data/lib/ralph/output/completion_detected.rb +0 -17
- data/lib/ralph/output/config_summary.rb +0 -31
- data/lib/ralph/output/context_consumed.rb +0 -11
- data/lib/ralph/output/iteration.rb +0 -45
- data/lib/ralph/output/max_iterations_reached.rb +0 -16
- data/lib/ralph/output/no_plugin_warning.rb +0 -14
- data/lib/ralph/output/nonzero_exit_warning.rb +0 -11
- data/lib/ralph/output/plugin_error.rb +0 -12
- data/lib/ralph/output/status.rb +0 -176
- data/lib/ralph/output/struggle_warning.rb +0 -18
- data/lib/ralph/output/task_completion.rb +0 -12
- data/lib/ralph/output/tasks_file_created.rb +0 -11
- data/lib/ralph/prompt_template.rb +0 -183
- data/lib/ralph/storage/context.rb +0 -58
- data/lib/ralph/storage/history.rb +0 -117
- data/lib/ralph/storage/state.rb +0 -178
- data/lib/ralph/storage/tasks.rb +0 -244
- data/lib/ralph/threads/heartbeat.rb +0 -44
- data/lib/ralph/threads/stream_reader.rb +0 -50
- data/original/bin/ralph.js +0 -13
- data/original/ralph.ts +0 -1706
- data/specs/iteration.md +0 -173
- data/specs/output.md +0 -104
- data/specs/storage/local-data-structure.md +0 -246
- data/specs/tasks.md +0 -295
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3950370724ceca76366a10ef15914ab10abc209595d1a5c8ddc466c071ea032
|
|
4
|
+
data.tar.gz: 0f85bc6a1f1a00d98ee6940928ce3ba506f6591536e14e4eb0cd30c3e6e914db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 181d38c7c1e72fd04ac4b22118a09caac7f262d7037d4e1c42f4f3440bac0b6a3c3b08d48934b2083ec3972a241457a18770c3f6c0038d82bd3ae33afd790416
|
|
7
|
+
data.tar.gz: 1feee430c07868a4877f351c1dcef388f42b99cb1db33b1c7515e22aeda33dba7ac0ce1abd5568c1315ded580f674f48f394909db2267d26bc9a64092e8d62e9
|
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
ralph.rb (1.2.435535439)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
ast (2.4.3)
|
|
10
|
+
json (2.18.1)
|
|
11
|
+
language_server-protocol (3.17.0.5)
|
|
12
|
+
lint_roller (1.1.0)
|
|
13
|
+
minitest (5.27.0)
|
|
14
|
+
parallel (1.27.0)
|
|
15
|
+
parser (3.3.10.1)
|
|
16
|
+
ast (~> 2.4.1)
|
|
17
|
+
racc
|
|
18
|
+
prism (1.9.0)
|
|
19
|
+
racc (1.8.1)
|
|
20
|
+
rainbow (3.1.1)
|
|
21
|
+
rake (13.3.1)
|
|
22
|
+
regexp_parser (2.11.3)
|
|
23
|
+
rubocop (1.84.1)
|
|
24
|
+
json (~> 2.3)
|
|
25
|
+
language_server-protocol (~> 3.17.0.2)
|
|
26
|
+
lint_roller (~> 1.1.0)
|
|
27
|
+
parallel (~> 1.10)
|
|
28
|
+
parser (>= 3.3.0.2)
|
|
29
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
30
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
31
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
32
|
+
ruby-progressbar (~> 1.7)
|
|
33
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
34
|
+
rubocop-ast (1.49.0)
|
|
35
|
+
parser (>= 3.3.7.2)
|
|
36
|
+
prism (~> 1.7)
|
|
37
|
+
ruby-progressbar (1.13.0)
|
|
38
|
+
unicode-display_width (3.2.0)
|
|
39
|
+
unicode-emoji (~> 4.1)
|
|
40
|
+
unicode-emoji (4.2.0)
|
|
41
|
+
|
|
42
|
+
PLATFORMS
|
|
43
|
+
ruby
|
|
44
|
+
x86_64-linux
|
|
45
|
+
|
|
46
|
+
DEPENDENCIES
|
|
47
|
+
minitest (~> 5.0)
|
|
48
|
+
rake (~> 13.0)
|
|
49
|
+
ralph.rb!
|
|
50
|
+
rubocop (~> 1.21)
|
|
51
|
+
|
|
52
|
+
BUNDLED WITH
|
|
53
|
+
2.7.2
|
data/lib/ralph/cli.rb
CHANGED
|
@@ -1,222 +1,103 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ralph
|
|
4
|
+
# Command-line interface for ralph. Parses arguments, reads stdin when piped,
|
|
5
|
+
# combines everything into a prompt, and launches the Loop.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# cat prompt.md | ralph "extra instructions" --model=opus-4.5 --max-iterations=10
|
|
4
9
|
class CLI
|
|
5
|
-
def initialize(argv
|
|
6
|
-
@
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Commands:
|
|
16
|
-
--status Show current Ralph loop status and history
|
|
17
|
-
--add-context TEXT Add context for the next iteration
|
|
18
|
-
--clear-context Clear any pending context
|
|
19
|
-
--list-tasks Display the current task list with indices
|
|
20
|
-
--add-task "desc" Add a new task to the list
|
|
21
|
-
--remove-task N Remove task at index N (including subtasks)
|
|
22
|
-
|
|
23
|
-
Options:
|
|
24
|
-
BANNER
|
|
25
|
-
|
|
26
|
-
o.on("--agent AGENT", Agents.valid_agent_names, "AI agent: #{Agents.valid_agent_names.join(', ')} (default: opencode)") do |v|
|
|
27
|
-
@config.chosen_agent = v
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
o.on("--min-iterations N", Integer, "Minimum iterations before completion (default: 1)") do |v|
|
|
31
|
-
@config.min_iterations = v
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
o.on("--max-iterations N", Integer, "Maximum iterations before stopping (default: unlimited)") do |v|
|
|
35
|
-
@config.max_iterations = v
|
|
36
|
-
end
|
|
10
|
+
def initialize(argv)
|
|
11
|
+
@argv = argv.dup
|
|
12
|
+
@options = {
|
|
13
|
+
model: nil,
|
|
14
|
+
max_iterations: nil,
|
|
15
|
+
duration: nil,
|
|
16
|
+
max_context: nil,
|
|
17
|
+
completion: nil
|
|
18
|
+
}
|
|
19
|
+
end
|
|
37
20
|
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
def run
|
|
22
|
+
parse_options
|
|
23
|
+
build_prompt.then do |prompt|
|
|
24
|
+
if prompt.nil? || prompt.strip.empty?
|
|
25
|
+
$stderr.puts "Error: no prompt provided."
|
|
26
|
+
$stderr.puts "Usage: ralph \"your prompt\" [options]"
|
|
27
|
+
$stderr.puts " cat prompt.md | ralph [options]"
|
|
28
|
+
$stderr.puts
|
|
29
|
+
$stderr.puts parser.help
|
|
30
|
+
exit 1
|
|
31
|
+
else
|
|
32
|
+
@options[:prompt] = prompt
|
|
33
|
+
Ralph::Loop.new(@options).run
|
|
40
34
|
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
@config.tasks_mode = true
|
|
44
|
-
end
|
|
38
|
+
private
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
def parse_options
|
|
41
|
+
parser.parse!(@argv)
|
|
42
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => error
|
|
43
|
+
$stderr.puts "Error: #{error.message}"
|
|
44
|
+
$stderr.puts parser.help
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
def parser
|
|
49
|
+
@parser ||= OptionParser.new do |option_parser|
|
|
50
|
+
option_parser.banner = "Usage: ralph [prompt] [options]"
|
|
51
|
+
option_parser.separator ""
|
|
52
|
+
option_parser.separator "Options:"
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
@
|
|
54
|
+
option_parser.on("--model=MODEL", "Model to use (e.g. opus-4.5)") do |value|
|
|
55
|
+
@options[:model] = value
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
@
|
|
58
|
+
option_parser.on("--max-iterations=N", Integer, "Maximum number of iterations") do |value|
|
|
59
|
+
@options[:max_iterations] = value
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
@
|
|
62
|
+
option_parser.on("--duration=SECONDS", Integer, "Maximum total duration in seconds") do |value|
|
|
63
|
+
@options[:duration] = value
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
@
|
|
66
|
+
option_parser.on("--max-context=N", Integer, "Maximum context tokens before iteration restart") do |value|
|
|
67
|
+
@options[:max_context] = value
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
puts "ralph #{VERSION}"
|
|
73
|
-
exit 0
|
|
70
|
+
option_parser.on("--completion=STRING", "Completion string the agent emits when done") do |value|
|
|
71
|
+
@options[:completion] = value
|
|
74
72
|
end
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
option_parser.on("-h", "--help", "Show this help message") do
|
|
75
|
+
puts option_parser
|
|
78
76
|
exit 0
|
|
79
77
|
end
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Storage::Context.new.append(
|
|
84
|
-
"\n## Context added at #{Time.now.utc.iso8601}\n#{context_text}\n"
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
puts "✅ Context added for next iteration"
|
|
88
|
-
puts " File: #{Storage::Context.new.path}"
|
|
89
|
-
|
|
90
|
-
Storage::State.load.then do |state|
|
|
91
|
-
if state&.active
|
|
92
|
-
puts " Will be picked up in iteration #{state.iteration + 1}"
|
|
93
|
-
else
|
|
94
|
-
puts " Will be used when loop starts"
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
79
|
+
option_parser.on("-v", "--version", "Show version") do
|
|
80
|
+
puts "ralph #{Ralph::VERSION}"
|
|
98
81
|
exit 0
|
|
99
82
|
end
|
|
100
|
-
|
|
101
|
-
o.on("--clear-context", "Clear any pending context") do
|
|
102
|
-
Storage::Context.new.then do |context|
|
|
103
|
-
if context.present?
|
|
104
|
-
context.clear
|
|
105
|
-
puts "✅ Context cleared"
|
|
106
|
-
else
|
|
107
|
-
puts "ℹ️ No pending context to clear"
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
exit 0
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
o.on("--list-tasks", "Display the current task list") do
|
|
115
|
-
begin
|
|
116
|
-
Storage::Tasks.new.load_tasks.then do |tasks|
|
|
117
|
-
if tasks
|
|
118
|
-
tasks.display_with_indices
|
|
119
|
-
else
|
|
120
|
-
puts "No tasks file found. Use --add-task to create your first task."
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
rescue StandardError => e
|
|
124
|
-
$stderr.puts "Error reading tasks file: #{e}"
|
|
125
|
-
exit 1
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
exit 0
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
o.on("--add-task DESC", "Add a new task to the list") do |description|
|
|
132
|
-
begin
|
|
133
|
-
Storage::Tasks.new.add_task(description)
|
|
134
|
-
puts "✅ Task added: \"#{description}\""
|
|
135
|
-
rescue StandardError => e
|
|
136
|
-
$stderr.puts "Error adding task: #{e}"
|
|
137
|
-
exit 1
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
exit 0
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
o.on("--remove-task N", Integer, "Remove task at index N") do |task_index|
|
|
144
|
-
begin
|
|
145
|
-
Storage::Tasks.new.remove_task(task_index)
|
|
146
|
-
puts "✅ Removed task #{task_index} and its subtasks"
|
|
147
|
-
rescue IndexError => e
|
|
148
|
-
$stderr.puts "Error: #{e.message}"
|
|
149
|
-
exit 1
|
|
150
|
-
rescue RuntimeError => e
|
|
151
|
-
$stderr.puts "Error: #{e.message}"
|
|
152
|
-
exit 1
|
|
153
|
-
rescue StandardError => e
|
|
154
|
-
$stderr.puts "Error removing task: #{e}"
|
|
155
|
-
exit 1
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
exit 0
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
o.separator ""
|
|
162
|
-
o.separator "Examples:"
|
|
163
|
-
o.separator ' ralph "Build a REST API for todos"'
|
|
164
|
-
o.separator ' ralph "Fix the auth bug" --max-iterations 10'
|
|
165
|
-
o.separator ' ralph "Add tests" --completion-promise "ALL TESTS PASS" --model openai/gpt-5.1'
|
|
166
|
-
o.separator ' ralph --status'
|
|
167
|
-
o.separator ' ralph --add-context "Focus on the auth module first"'
|
|
168
|
-
o.separator ""
|
|
169
|
-
o.separator "How it works:"
|
|
170
|
-
o.separator " 1. Sends your prompt to the selected AI agent"
|
|
171
|
-
o.separator " 2. AI agent works on the task"
|
|
172
|
-
o.separator " 3. Checks output for completion promise"
|
|
173
|
-
o.separator " 4. If not complete, repeats with same prompt"
|
|
174
|
-
o.separator " 5. AI sees its previous work in files"
|
|
175
|
-
o.separator " 6. Continues until promise detected or max iterations"
|
|
176
|
-
o.separator ""
|
|
177
|
-
o.separator "To stop manually: Ctrl+C"
|
|
178
|
-
o.separator "Learn more: https://ghuntley.com/ralph/"
|
|
179
83
|
end
|
|
180
|
-
|
|
181
84
|
end
|
|
182
85
|
|
|
183
|
-
def
|
|
184
|
-
|
|
185
|
-
if $stdin.tty?
|
|
186
|
-
remaining_args.join(" ").strip
|
|
187
|
-
else
|
|
188
|
-
[$stdin.read, remaining_args.join(" ")].join("\n").strip
|
|
189
|
-
end
|
|
190
|
-
end
|
|
86
|
+
def build_prompt
|
|
87
|
+
parts = []
|
|
191
88
|
|
|
192
|
-
if
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
Run 'ralph --help' for more information
|
|
197
|
-
"
|
|
89
|
+
# Read from stdin if data is being piped in
|
|
90
|
+
unless $stdin.tty?
|
|
91
|
+
stdin_content = $stdin.read
|
|
92
|
+
parts << stdin_content if stdin_content && !stdin_content.strip.empty?
|
|
198
93
|
end
|
|
199
94
|
|
|
200
|
-
|
|
201
|
-
|
|
95
|
+
# Remaining positional arguments are the inline prompt
|
|
96
|
+
parts << @argv.join(" ") if @argv.any?
|
|
202
97
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if @config.max_iterations > 0 && @config.min_iterations > @config.max_iterations
|
|
207
|
-
abort "Error: --min-iterations (#{@config.min_iterations}) cannot be greater than --max-iterations (#{@config.max_iterations})"
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
state = Storage::State.from_config(@config, prompt: prompt)
|
|
211
|
-
history = Storage::History.new
|
|
212
|
-
|
|
213
|
-
Ralph::Loop.new(@config, state, history, context, tasks).run
|
|
98
|
+
if parts.any?
|
|
99
|
+
parts.join("\n\n")
|
|
214
100
|
end
|
|
215
|
-
|
|
216
|
-
rescue StandardError => e
|
|
217
|
-
$stderr.puts "Fatal error: #{e}"
|
|
218
|
-
Storage::State.clear
|
|
219
|
-
exit 1
|
|
220
101
|
end
|
|
221
102
|
end
|
|
222
103
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ralph
|
|
4
|
+
# Handles all terminal output for the loop -- iteration status, metrics,
|
|
5
|
+
# agent text, and summary information.
|
|
6
|
+
class Display
|
|
7
|
+
SEPARATOR = "-" * 60
|
|
8
|
+
|
|
9
|
+
def initialize(loop_engine)
|
|
10
|
+
@loop_engine = loop_engine
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show_start(prompt)
|
|
14
|
+
puts SEPARATOR
|
|
15
|
+
puts "ralph -- autonomous agentic loop"
|
|
16
|
+
puts SEPARATOR
|
|
17
|
+
puts "Prompt: #{prompt[0, 200]}#{"..." if prompt.length > 200}"
|
|
18
|
+
puts SEPARATOR
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show_iteration_start
|
|
22
|
+
puts
|
|
23
|
+
puts "#{SEPARATOR}"
|
|
24
|
+
puts "Iteration #{@loop_engine.iteration_number} | " \
|
|
25
|
+
"elapsed: #{format_duration(@loop_engine.elapsed_seconds)} | " \
|
|
26
|
+
"context: #{format_tokens(@loop_engine.metrics.current_context)} | " \
|
|
27
|
+
"tokens: #{format_tokens(@loop_engine.metrics.tokens_consumed)}"
|
|
28
|
+
puts SEPARATOR
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def show_event(event)
|
|
32
|
+
if event.is_a?(Events::Text) && event.text
|
|
33
|
+
print event.text
|
|
34
|
+
$stdout.flush
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if event.is_a?(Events::ToolUse)
|
|
38
|
+
puts
|
|
39
|
+
puts " [tool] #{event.tool} (#{event.status})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if event.is_a?(Events::StepFinish)
|
|
43
|
+
puts
|
|
44
|
+
puts " [step] context=#{format_tokens(event.context_size)} " \
|
|
45
|
+
"output=#{format_tokens(event.output_tokens)} " \
|
|
46
|
+
"cache_r=#{format_tokens(event.cache_read)} " \
|
|
47
|
+
"cache_w=#{format_tokens(event.cache_write)}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def show_iteration_end
|
|
52
|
+
puts
|
|
53
|
+
puts " Iteration #{@loop_engine.iteration_number} complete | " \
|
|
54
|
+
"steps: #{@loop_engine.metrics.step_count} | " \
|
|
55
|
+
"iteration tokens: #{format_tokens(@loop_engine.metrics.iteration_tokens)}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def show_iteration_cancelled(reason)
|
|
59
|
+
puts
|
|
60
|
+
puts " ** Iteration cancelled: #{reason}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def show_termination(reason)
|
|
64
|
+
puts
|
|
65
|
+
puts " ** Loop terminated: #{reason}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def show_summary
|
|
69
|
+
metrics = @loop_engine.metrics
|
|
70
|
+
puts
|
|
71
|
+
puts SEPARATOR
|
|
72
|
+
puts "SUMMARY"
|
|
73
|
+
puts SEPARATOR
|
|
74
|
+
puts " Status: #{@loop_engine.completed ? "COMPLETED" : "TERMINATED"}"
|
|
75
|
+
puts " Iterations: #{@loop_engine.iteration_number}"
|
|
76
|
+
puts " Duration: #{format_duration(@loop_engine.elapsed_seconds)}"
|
|
77
|
+
puts " Steps: #{metrics.step_count}"
|
|
78
|
+
puts " Tokens: #{format_tokens(metrics.tokens_consumed)}"
|
|
79
|
+
puts " Context: #{format_tokens(metrics.current_context)}"
|
|
80
|
+
puts SEPARATOR
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def format_duration(seconds)
|
|
86
|
+
minutes = (seconds / 60).to_i
|
|
87
|
+
remaining_seconds = (seconds % 60).to_i
|
|
88
|
+
if minutes > 0
|
|
89
|
+
"#{minutes}m #{remaining_seconds}s"
|
|
90
|
+
else
|
|
91
|
+
"#{remaining_seconds}s"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def format_tokens(count)
|
|
96
|
+
if count >= 1_000_000
|
|
97
|
+
format("%.1fM", count / 1_000_000.0)
|
|
98
|
+
elsif count >= 1_000
|
|
99
|
+
format("%.1fk", count / 1_000.0)
|
|
100
|
+
else
|
|
101
|
+
count.to_s
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/ralph/events.rb
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ralph
|
|
4
|
+
module Events
|
|
5
|
+
# Base event wrapping the common fields from opencode JSON stream lines
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :type, :timestamp, :session_id, :part
|
|
8
|
+
|
|
9
|
+
def initialize(data)
|
|
10
|
+
@type = data["type"]
|
|
11
|
+
@timestamp = data["timestamp"]
|
|
12
|
+
@session_id = data["sessionID"]
|
|
13
|
+
@part = data["part"] || {}
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Emitted when a new LLM step begins
|
|
18
|
+
class StepStart < Base
|
|
19
|
+
def snapshot
|
|
20
|
+
part["snapshot"]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Emitted when the model streams text content
|
|
25
|
+
class Text < Base
|
|
26
|
+
def text
|
|
27
|
+
part["text"]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Emitted when the model invokes a tool
|
|
32
|
+
class ToolUse < Base
|
|
33
|
+
def tool
|
|
34
|
+
part["tool"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call_id
|
|
38
|
+
part["callID"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def state
|
|
42
|
+
part["state"] || {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def status
|
|
46
|
+
state["status"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def input
|
|
50
|
+
state["input"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def output
|
|
54
|
+
state["output"]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Emitted when a step completes -- carries the token usage data
|
|
59
|
+
class StepFinish < Base
|
|
60
|
+
def reason
|
|
61
|
+
part["reason"]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def tokens
|
|
65
|
+
part["tokens"] || {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def input_tokens
|
|
69
|
+
tokens["input"] || 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def output_tokens
|
|
73
|
+
tokens["output"] || 0
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def reasoning_tokens
|
|
77
|
+
tokens["reasoning"] || 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def cache
|
|
81
|
+
tokens["cache"] || {}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def cache_read
|
|
85
|
+
cache["read"] || 0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def cache_write
|
|
89
|
+
cache["write"] || 0
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Total context size for this step: input + cache.read + cache.write
|
|
93
|
+
def context_size
|
|
94
|
+
input_tokens + cache_read + cache_write
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
EVENT_TYPES = {
|
|
99
|
+
"step_start" => StepStart,
|
|
100
|
+
"text" => Text,
|
|
101
|
+
"tool_use" => ToolUse,
|
|
102
|
+
"step_finish" => StepFinish
|
|
103
|
+
}.freeze
|
|
104
|
+
|
|
105
|
+
# Parse a single JSON line into the appropriate event object.
|
|
106
|
+
# Returns nil for unknown event types or malformed JSON.
|
|
107
|
+
def self.parse(line)
|
|
108
|
+
data = JSON.parse(line)
|
|
109
|
+
event_class = EVENT_TYPES[data["type"]]
|
|
110
|
+
if event_class
|
|
111
|
+
event_class.new(data)
|
|
112
|
+
end
|
|
113
|
+
rescue JSON::ParserError
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|