roast-ai 0.4.8 → 0.4.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 +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +1 -0
- data/Gemfile.lock +3 -3
- data/README.md +9 -5
- data/dsl/less_simple.rb +112 -0
- data/dsl/prototype.rb +17 -0
- data/dsl/simple.rb +5 -7
- data/dsl/step_communication.rb +18 -0
- data/examples/grading/README.md +46 -0
- data/examples/grading/analyze_coverage/prompt.md +52 -0
- data/examples/grading/calculate_final_grade.rb +64 -0
- data/examples/grading/format_result.rb +61 -0
- data/examples/grading/generate_grades/prompt.md +105 -0
- data/examples/grading/generate_recommendations/output.txt +17 -0
- data/examples/grading/generate_recommendations/prompt.md +60 -0
- data/examples/grading/read_dependencies/prompt.md +15 -0
- data/examples/grading/verify_mocks_and_stubs/prompt.md +12 -0
- data/examples/grading/verify_test_helpers/prompt.md +53 -0
- data/examples/grading/workflow.md +5 -0
- data/examples/grading/workflow.yml +28 -0
- data/lib/roast/dsl/cog/config.rb +31 -0
- data/lib/roast/dsl/cog/stack.rb +21 -0
- data/lib/roast/dsl/cog/store.rb +26 -0
- data/lib/roast/dsl/cog.rb +70 -0
- data/lib/roast/dsl/cog_execution_context.rb +29 -0
- data/lib/roast/dsl/cogs/cmd.rb +55 -0
- data/lib/roast/dsl/cogs/graph.rb +53 -0
- data/lib/roast/dsl/cogs.rb +65 -0
- data/lib/roast/dsl/config_context.rb +54 -0
- data/lib/roast/dsl/executor.rb +62 -7
- data/lib/roast/dsl/workflow_execution_context.rb +47 -0
- data/lib/roast/error.rb +7 -0
- data/lib/roast/errors.rb +3 -3
- data/lib/roast/graph/edge.rb +25 -0
- data/lib/roast/graph/node.rb +40 -0
- data/lib/roast/graph/quantum_edge.rb +27 -0
- data/lib/roast/graph/threaded_exec.rb +93 -0
- data/lib/roast/graph.rb +233 -0
- data/lib/roast/resources/api_resource.rb +2 -2
- data/lib/roast/resources/url_resource.rb +2 -2
- data/lib/roast/tools/apply_diff.rb +1 -1
- data/lib/roast/tools/ask_user.rb +1 -1
- data/lib/roast/tools/bash.rb +1 -1
- data/lib/roast/tools/cmd.rb +2 -2
- data/lib/roast/tools/coding_agent.rb +2 -2
- data/lib/roast/tools/grep.rb +1 -1
- data/lib/roast/tools/read_file.rb +1 -1
- data/lib/roast/tools/search_file.rb +1 -1
- data/lib/roast/tools/swarm.rb +1 -1
- data/lib/roast/tools/update_files.rb +2 -2
- data/lib/roast/tools/write_file.rb +1 -1
- data/lib/roast/tools.rb +1 -1
- data/lib/roast/value_objects/api_token.rb +1 -1
- data/lib/roast/value_objects/uri_base.rb +1 -1
- data/lib/roast/value_objects/workflow_path.rb +1 -1
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_workflow.rb +38 -2
- data/lib/roast/workflow/command_executor.rb +1 -1
- data/lib/roast/workflow/configuration_loader.rb +1 -1
- data/lib/roast/workflow/error_handler.rb +1 -1
- data/lib/roast/workflow/step_executor_registry.rb +1 -1
- data/lib/roast/workflow/step_loader.rb +1 -1
- data/lib/roast/workflow/workflow_executor.rb +1 -1
- data/lib/roast.rb +1 -1
- data/sorbet/config +2 -0
- data/sorbet/rbi/annotations/.gitattributes +1 -0
- data/sorbet/rbi/annotations/activesupport.rbi +495 -0
- data/sorbet/rbi/annotations/faraday.rbi +17 -0
- data/sorbet/rbi/annotations/minitest.rbi +119 -0
- data/sorbet/rbi/annotations/mocha.rbi +34 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/annotations/webmock.rbi +9 -0
- data/sorbet/rbi/gems/rbs-inline@0.12.0.rbi +2170 -0
- data/sorbet/rbi/gems/{rexml@3.4.1.rbi → rexml@3.4.2.rbi} +284 -239
- data/sorbet/rbi/shims/lib/roast/dsl/config_context.rbi +11 -0
- data/sorbet/rbi/shims/lib/roast/dsl/workflow_execution_context.rbi +11 -0
- data/sorbet/rbi/todo.rbi +7 -0
- metadata +46 -5
- data/package-lock.json +0 -6
- /data/sorbet/rbi/gems/{rack@2.2.17.rbi → rack@2.2.18.rbi} +0 -0
data/lib/roast/errors.rb
CHANGED
@@ -4,12 +4,12 @@
|
|
4
4
|
module Roast
|
5
5
|
module Errors
|
6
6
|
# Custom error for API resource not found (404) responses
|
7
|
-
class ResourceNotFoundError <
|
7
|
+
class ResourceNotFoundError < Roast::Error; end
|
8
8
|
|
9
9
|
# Custom error for when API authentication fails
|
10
|
-
class AuthenticationError <
|
10
|
+
class AuthenticationError < Roast::Error; end
|
11
11
|
|
12
12
|
# Exit the app, for instance via Ctrl-C during an InputStep
|
13
|
-
class ExitEarly <
|
13
|
+
class ExitEarly < Roast::Error; end
|
14
14
|
end
|
15
15
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
class Graph
|
6
|
+
class Edge
|
7
|
+
#: (Node) -> void
|
8
|
+
attr_writer :join_node
|
9
|
+
|
10
|
+
attr_reader :from_node, :to_node
|
11
|
+
|
12
|
+
#: (Node, Node, ?proc: Proc?) -> void
|
13
|
+
def initialize(from_node, to_node, proc: nil)
|
14
|
+
@from_node = from_node
|
15
|
+
@to_node = to_node
|
16
|
+
@proc = proc # TODO: Shadowing proc builtin here
|
17
|
+
end
|
18
|
+
|
19
|
+
#: () -> String
|
20
|
+
def to_s
|
21
|
+
"#{@from_node} -> #{@to_node}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
class Graph
|
6
|
+
class Node
|
7
|
+
# class InvalidExecutableError < Roast::Error; end
|
8
|
+
|
9
|
+
attr_reader :name, :executable
|
10
|
+
|
11
|
+
#: (Symbol, ?executable: Proc | Graph | nil) -> void
|
12
|
+
def initialize(name, executable: nil)
|
13
|
+
@name = name
|
14
|
+
@executable = executable
|
15
|
+
end
|
16
|
+
|
17
|
+
#: () -> T::Boolean
|
18
|
+
def subgraph?
|
19
|
+
@executable.is_a?(Graph)
|
20
|
+
end
|
21
|
+
|
22
|
+
#: () -> T::Boolean
|
23
|
+
def done?
|
24
|
+
@name == :DONE
|
25
|
+
end
|
26
|
+
|
27
|
+
#: (Hash) -> void
|
28
|
+
def execute(state)
|
29
|
+
return if @executable.nil?
|
30
|
+
|
31
|
+
executable = @executable
|
32
|
+
if executable.is_a?(Proc)
|
33
|
+
executable.call(state)
|
34
|
+
elsif executable.is_a?(Graph)
|
35
|
+
executable.execute(state)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
class Graph
|
6
|
+
class QuantumEdge
|
7
|
+
#: (Roast::Graph::Node, Proc) -> void
|
8
|
+
def initialize(from_node, to_proc)
|
9
|
+
@from_node = from_node
|
10
|
+
@to_proc = to_proc
|
11
|
+
end
|
12
|
+
|
13
|
+
#: (Hash[untyped, untyped], Hash[Symbol, Roast::Graph::Node]) -> Array[Roast::Graph::Edge]
|
14
|
+
def collapse(state, nodes)
|
15
|
+
to_node_names = @to_proc.call(state)
|
16
|
+
to_node_names = to_node_names.is_a?(Array) ? to_node_names : [to_node_names]
|
17
|
+
|
18
|
+
to_node_names.map do |to_node_name|
|
19
|
+
to_node = nodes[to_node_name]
|
20
|
+
raise Error, "No node found with name #{to_node_name.inspect}" if to_node.nil?
|
21
|
+
|
22
|
+
Edge.new(@from_node, to_node)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
class Graph
|
6
|
+
class StateConflictError < Error; end
|
7
|
+
|
8
|
+
class ThreadedExec
|
9
|
+
def initialize(nodes, og_state)
|
10
|
+
@nodes = nodes
|
11
|
+
@og_state = og_state
|
12
|
+
end
|
13
|
+
|
14
|
+
#: () -> Hash[untyped, untyped]
|
15
|
+
def async_execute
|
16
|
+
states = threaded_execute(@nodes, @og_state)
|
17
|
+
merge_states!(@og_state, states)
|
18
|
+
@og_state
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a hash of the new states for each node.
|
22
|
+
#: (Array[Node], Hash[untyped, untyped]) -> Hash[Symbol, Hash[untyped, untyped]]
|
23
|
+
def threaded_execute(nodes, og_state)
|
24
|
+
states = {}
|
25
|
+
threads = nodes.map do |current_node|
|
26
|
+
states[current_node.name] = og_state.dup
|
27
|
+
Thread.new do
|
28
|
+
current_node.execute(states[current_node.name])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
threads.map(&:value)
|
33
|
+
|
34
|
+
states
|
35
|
+
end
|
36
|
+
|
37
|
+
#: (Hash, Hash) -> void
|
38
|
+
def merge_states!(orig_state, new_states)
|
39
|
+
orig_state.merge!(merge_new_states(orig_state, new_states))
|
40
|
+
end
|
41
|
+
|
42
|
+
# We merge all the new states together first so we can catch any keys that were modified by multiple threads.
|
43
|
+
#: (Hash[untyped, untyped], Hash[Symbol, Hash[untyped, untyped]]) -> Hash[untyped, untyped]
|
44
|
+
def merge_new_states(orig_state, new_states)
|
45
|
+
# We only care about what the threads have changes from the original state.
|
46
|
+
changed_states = changed_states(orig_state, new_states)
|
47
|
+
|
48
|
+
return {} if changed_states.empty?
|
49
|
+
|
50
|
+
# Grab some entry to be the one we merge all the other changes into.
|
51
|
+
base_node_name, base_changed_state = T.must(changed_states.shift)
|
52
|
+
base_node_name = T.cast(base_node_name, Symbol)
|
53
|
+
base_changed_state = T.cast(base_changed_state, T::Hash[T.untyped, T.untyped])
|
54
|
+
|
55
|
+
changed_states.each do |node_name, changed_state|
|
56
|
+
base_changed_state.merge!(changed_state) do |key, old_value, new_value|
|
57
|
+
raise_state_conflict(key, old_value, new_value, [base_node_name, node_name])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
base_changed_state
|
62
|
+
end
|
63
|
+
|
64
|
+
#: (Hash, Hash[Symbol, Hash]) -> Hash[Symbol, Hash]
|
65
|
+
def changed_states(orig_state, new_states)
|
66
|
+
new_states.transform_values do |new_state|
|
67
|
+
changed_state_entries(orig_state, new_state)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Filter new states content to only include keys that were modified by the new states.
|
72
|
+
#: (Hash, Hash) -> Hash
|
73
|
+
def changed_state_entries(orig_state, new_state)
|
74
|
+
new_state.reject do |key, value|
|
75
|
+
orig_state[key] == value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
#: (Symbol, untyped, untyped, Array[Symbol]) -> void
|
80
|
+
def raise_state_conflict(key, old_value, new_value, conflicting_nodes)
|
81
|
+
raise StateConflictError, <<~CONFLICT.chomp
|
82
|
+
Parallel nodes modified the same state key.
|
83
|
+
Conflicting nodes: #{conflicting_nodes.join(", ")}
|
84
|
+
Key: :#{key}
|
85
|
+
Old value:
|
86
|
+
#{old_value.inspect}
|
87
|
+
New value:
|
88
|
+
#{new_value.inspect}
|
89
|
+
CONFLICT
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/roast/graph.rb
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Roast
|
5
|
+
class Graph
|
6
|
+
class Error < StandardError; end
|
7
|
+
class AddEdgeError < Error; end
|
8
|
+
class EdgeTopologyError < Error; end
|
9
|
+
|
10
|
+
#: (Symbol) { (Graph) -> void } -> void
|
11
|
+
def subgraph(name, &block)
|
12
|
+
subgraph = Graph.new
|
13
|
+
block.call(subgraph)
|
14
|
+
nodes[name] = Node.new(name, executable: subgraph)
|
15
|
+
end
|
16
|
+
|
17
|
+
#: (Symbol) { () -> void } -> void
|
18
|
+
def node(name, &block)
|
19
|
+
nodes[name] = Node.new(name, executable: block)
|
20
|
+
end
|
21
|
+
|
22
|
+
#: (from: Symbol | Array[Symbol], ?to: Symbol | Array[Symbol] | nil) ?{ () -> void } -> void
|
23
|
+
def edge(from:, to: nil, &block)
|
24
|
+
from_nodes = from.is_a?(Array) ? from.map { |from_node| nodes[from_node] } : [nodes[from]].compact
|
25
|
+
|
26
|
+
if from_nodes.empty?
|
27
|
+
raise AddEdgeError, "Cannot create edge from #{from.inspect} to #{to.inspect} because #{from.inspect} does not exist"
|
28
|
+
end
|
29
|
+
|
30
|
+
if block.nil? && !to.nil?
|
31
|
+
to_nodes = to.is_a?(Array) ? to.map { |to_node| nodes[to_node] } : [nodes[to]].compact
|
32
|
+
|
33
|
+
if to_nodes.empty?
|
34
|
+
raise AddEdgeError, "Cannot create edge from #{from.inspect} to #{to.inspect} because #{to.inspect} does not exist"
|
35
|
+
end
|
36
|
+
|
37
|
+
from_nodes.each do |from_node|
|
38
|
+
to_nodes.each do |to_node|
|
39
|
+
insert_edge(Edge.new(T.must(from_node), T.must(to_node)))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
elsif !block.nil? && to.nil?
|
43
|
+
from_nodes.each do |from_node|
|
44
|
+
quantum_edges[T.must(from_node).name] = QuantumEdge.new(T.must(from_node), T.must(block))
|
45
|
+
end
|
46
|
+
elsif !block.nil? && !to.nil?
|
47
|
+
raise AddEdgeError, "Must provide either a to node or a block, not both"
|
48
|
+
else
|
49
|
+
raise AddEdgeError, "Must provide either a to node or a block"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#: (?Hash[untyped, untyped]?) -> void
|
54
|
+
def execute(init_state = nil)
|
55
|
+
return if nodes.empty?
|
56
|
+
|
57
|
+
# HACK: Move the DONE node to the end of the nodes array.
|
58
|
+
# In reality we should have a separate "ordered" representation of the nodes, and we store the
|
59
|
+
# main thing as a set.
|
60
|
+
nodes[:DONE] = T.must(nodes.delete(:DONE))
|
61
|
+
|
62
|
+
self.state = init_state unless init_state.nil?
|
63
|
+
|
64
|
+
current_nodes = T.let([T.must(nodes.values.first)], T::Array[Roast::Graph::Node])
|
65
|
+
|
66
|
+
until current_nodes.any? { |node| T.must(node).done? }
|
67
|
+
if current_nodes.size == 1
|
68
|
+
T.must(current_nodes.first).execute(state)
|
69
|
+
else
|
70
|
+
ThreadedExec.new(current_nodes, state).async_execute
|
71
|
+
end
|
72
|
+
|
73
|
+
current_nodes = find_next(current_nodes)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
#: () -> Hash[Symbol, Node]
|
80
|
+
def nodes
|
81
|
+
@nodes ||= { START: Node.new(:START), DONE: Node.new(:DONE) }
|
82
|
+
end
|
83
|
+
|
84
|
+
#: () -> Hash[Symbol, Array[Edge]]
|
85
|
+
def edges
|
86
|
+
@edges ||= {}
|
87
|
+
end
|
88
|
+
|
89
|
+
#: () -> Hash[Symbol, Roast::Graph::QuantumEdge]
|
90
|
+
def quantum_edges
|
91
|
+
@quantum_edges ||= {}
|
92
|
+
end
|
93
|
+
|
94
|
+
#: () -> Hash
|
95
|
+
def state
|
96
|
+
@state ||= {}
|
97
|
+
end
|
98
|
+
|
99
|
+
#: (Hash) -> void
|
100
|
+
def state=(new_state)
|
101
|
+
raise Error, "State already set, cannot set it again" if !@state.nil? && !new_state.nil?
|
102
|
+
|
103
|
+
@state = new_state
|
104
|
+
end
|
105
|
+
|
106
|
+
#: (Array[Node]) -> Array[Node]
|
107
|
+
def find_next(current_nodes)
|
108
|
+
raise Error, "Somehow got an empty array of nodes" if current_nodes.empty?
|
109
|
+
|
110
|
+
collapse_quantum_edges(current_nodes)
|
111
|
+
|
112
|
+
next_edges = if current_nodes.size == 1
|
113
|
+
next_edges_for_node(T.must(current_nodes.first))
|
114
|
+
elsif current_nodes.size > 1
|
115
|
+
next_edges_for_nodes(current_nodes)
|
116
|
+
end
|
117
|
+
|
118
|
+
if next_edges.nil?
|
119
|
+
raise EdgeTopologyError, "No next edges found for #{current_nodes.map(&:name).join(", ")}, please define edges for this node"
|
120
|
+
end
|
121
|
+
|
122
|
+
next_nodes = next_edges.map(&:to_node).uniq
|
123
|
+
|
124
|
+
# If we're doing paralell nodes, we need to lookahead to ensuer we join back at the same node.
|
125
|
+
if next_nodes.size > 1
|
126
|
+
raise EdgeTopologyError, "Parallel execution many to many nodes is not supported" if current_nodes.size != 1
|
127
|
+
|
128
|
+
collapse_quantum_edges(next_nodes)
|
129
|
+
raise_unless_all_point_to_same_next_node?(next_nodes)
|
130
|
+
end
|
131
|
+
|
132
|
+
next_nodes
|
133
|
+
end
|
134
|
+
|
135
|
+
#: (Array[Node]) -> bool
|
136
|
+
def raise_unless_all_point_to_same_next_node?(nodes)
|
137
|
+
next_nodes = nodes.map { |node| edges_from(node) }.compact.flatten.map(&:to_node).uniq
|
138
|
+
# TODO: Deal with when next_nodes here is empty, should be generic "if you define any edges, you must define them all"
|
139
|
+
if next_nodes.size > 1
|
140
|
+
Roast::Helpers::Logger.info("Next nodes: #{next_nodes.map(&:name).join(", ")}")
|
141
|
+
raise EdgeTopologyError, "Parallel nodes #{nodes.map(&:name).join(", ")} have different next nodes: #{next_nodes.inspect}"
|
142
|
+
end
|
143
|
+
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
147
|
+
#: (Array[Node]) -> void
|
148
|
+
def collapse_quantum_edges(current_nodes)
|
149
|
+
curr_quantum_edges = current_nodes.map do |node|
|
150
|
+
quantum_edges[node.name]
|
151
|
+
end.compact
|
152
|
+
|
153
|
+
return if curr_quantum_edges.empty?
|
154
|
+
|
155
|
+
if curr_quantum_edges.size > 1
|
156
|
+
raise EdgeTopologyError, <<~MANY_Q_EDGES
|
157
|
+
Multiple quantum edges for nodes:
|
158
|
+
Nodes: #{current_nodes.map(&:name).join(", ")}"
|
159
|
+
Quantum Edges: #{quantum_edges.inspect}
|
160
|
+
MANY_Q_EDGES
|
161
|
+
end
|
162
|
+
|
163
|
+
edges = curr_quantum_edges.map do |quantum_edge|
|
164
|
+
quantum_edge.collapse(state, nodes)
|
165
|
+
end
|
166
|
+
|
167
|
+
edges.flatten.each do |edge|
|
168
|
+
insert_edge(edge)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
#: (Node) -> Array[Edge]
|
173
|
+
def next_edges_for_node(current_node)
|
174
|
+
next_edges = edges_from(current_node)
|
175
|
+
# If the user never defined any edges, we'll just use the next node in the file.
|
176
|
+
next_edges ||= [edge_from_next_loaded(current_node.name)].compact if edges.empty?
|
177
|
+
T.must(next_edges)
|
178
|
+
end
|
179
|
+
|
180
|
+
#: (Array[Node]) -> Array[Edge]
|
181
|
+
def next_edges_for_nodes(current_nodes)
|
182
|
+
maybe_next_edges = current_nodes.map { |node| edges_from(node) }.compact.flatten
|
183
|
+
|
184
|
+
# Verify there are same number of edges as nodes.
|
185
|
+
if maybe_next_edges.size != current_nodes.size
|
186
|
+
raise EdgeTopologyError, <<~WRONG_NUM_EDGES
|
187
|
+
Parallel nodes have different numbers of edges:
|
188
|
+
Next Edges: #{maybe_next_edges}
|
189
|
+
Parallel nodes: #{current_nodes.map(&:name).join(", ")}
|
190
|
+
WRONG_NUM_EDGES
|
191
|
+
end
|
192
|
+
|
193
|
+
# Verify all the edges go to the same place.
|
194
|
+
uniq_to_nodes = maybe_next_edges.map(&:to_node).uniq!
|
195
|
+
if T.must(uniq_to_nodes).size != 1
|
196
|
+
# TODO: Present which edges are going to different places.
|
197
|
+
raise EdgeTopologyError, <<~WRONG_NUM_EDGES
|
198
|
+
Parallel nodes end up at different nodes:
|
199
|
+
Next Edges: #{maybe_next_edges}
|
200
|
+
Parallel nodes: #{current_nodes.map(&:name).join(", ")}
|
201
|
+
WRONG_NUM_EDGES
|
202
|
+
end
|
203
|
+
|
204
|
+
maybe_next_edges
|
205
|
+
end
|
206
|
+
|
207
|
+
#: (Node) -> Array[Edge]?
|
208
|
+
def edges_from(from_node)
|
209
|
+
edges[from_node.name]
|
210
|
+
end
|
211
|
+
|
212
|
+
#: (Edge) -> void
|
213
|
+
def insert_edge(edge)
|
214
|
+
edges[edge.from_node.name] ||= []
|
215
|
+
T.must(edges[edge.from_node.name]) << edge
|
216
|
+
end
|
217
|
+
|
218
|
+
#: (Symbol) -> Edge?
|
219
|
+
def edge_from_next_loaded(current_node_name)
|
220
|
+
next_node = next_loaded_node(current_node_name)
|
221
|
+
return if next_node.nil?
|
222
|
+
|
223
|
+
Edge.new(T.must(nodes[current_node_name]), next_node)
|
224
|
+
end
|
225
|
+
|
226
|
+
#: (Symbol) -> Node?
|
227
|
+
def next_loaded_node(current_node_name)
|
228
|
+
current_index = nodes.keys.index(current_node_name)
|
229
|
+
next_index = (T.must(current_index) + 1)
|
230
|
+
nodes.values[next_index]
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -74,7 +74,7 @@ module Roast
|
|
74
74
|
|
75
75
|
# Consider 2xx and 3xx as success
|
76
76
|
response.code.to_i < 400
|
77
|
-
rescue
|
77
|
+
rescue Roast::Error => e
|
78
78
|
# Log the error but don't crash
|
79
79
|
Roast::Helpers::Logger.error("Error checking API existence: #{e.message}")
|
80
80
|
false
|
@@ -96,7 +96,7 @@ module Roast
|
|
96
96
|
begin
|
97
97
|
uri = URI.parse(target)
|
98
98
|
Net::HTTP.get(uri)
|
99
|
-
rescue
|
99
|
+
rescue Roast::Error => e
|
100
100
|
# Log the error but don't crash
|
101
101
|
Roast::Helpers::Logger.error("Error fetching API contents: #{e.message}")
|
102
102
|
nil
|
@@ -23,7 +23,7 @@ module Roast
|
|
23
23
|
|
24
24
|
# Consider 2xx and 3xx as success
|
25
25
|
response.code.to_i < 400
|
26
|
-
rescue
|
26
|
+
rescue Roast::Error => e
|
27
27
|
# Log the error but don't crash
|
28
28
|
Roast::Helpers::Logger.error("Error checking URL existence: #{e.message}")
|
29
29
|
false
|
@@ -36,7 +36,7 @@ module Roast
|
|
36
36
|
begin
|
37
37
|
uri = URI.parse(target)
|
38
38
|
Net::HTTP.get(uri)
|
39
|
-
rescue
|
39
|
+
rescue Roast::Error => e
|
40
40
|
# Log the error but don't crash
|
41
41
|
Roast::Helpers::Logger.error("Error fetching URL contents: #{e.message}")
|
42
42
|
nil
|
@@ -64,7 +64,7 @@ module Roast
|
|
64
64
|
Roast::Helpers::Logger.info(cancel_msg + "\n")
|
65
65
|
cancel_msg
|
66
66
|
end
|
67
|
-
rescue
|
67
|
+
rescue Roast::Error => e
|
68
68
|
error_message = "Error applying diff: #{e.message}"
|
69
69
|
Roast::Helpers::Logger.error(error_message + "\n")
|
70
70
|
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
data/lib/roast/tools/ask_user.rb
CHANGED
@@ -28,7 +28,7 @@ module Roast
|
|
28
28
|
|
29
29
|
Roast::Helpers::Logger.info("User responded: #{response}\n")
|
30
30
|
response
|
31
|
-
rescue
|
31
|
+
rescue Roast::Error => e
|
32
32
|
"Error getting user input: #{e.message}".tap do |error_message|
|
33
33
|
Roast::Helpers::Logger.error(error_message + "\n")
|
34
34
|
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
data/lib/roast/tools/bash.rb
CHANGED
data/lib/roast/tools/cmd.rb
CHANGED
@@ -93,7 +93,7 @@ module Roast
|
|
93
93
|
Roast::Helpers::Logger.info("🔧 Running command: #{full_command}\n")
|
94
94
|
|
95
95
|
execute_command(full_command, command_prefix, timeout)
|
96
|
-
rescue
|
96
|
+
rescue Roast::Error => e
|
97
97
|
handle_error(e)
|
98
98
|
end
|
99
99
|
|
@@ -108,7 +108,7 @@ module Roast
|
|
108
108
|
command_prefix = command.split(" ").first
|
109
109
|
|
110
110
|
execute_command(command, command_prefix, timeout)
|
111
|
-
rescue
|
111
|
+
rescue Roast::Error => e
|
112
112
|
handle_error(e)
|
113
113
|
end
|
114
114
|
|
@@ -7,7 +7,7 @@ module Roast
|
|
7
7
|
extend self
|
8
8
|
include Roast::Helpers::MetadataAccess
|
9
9
|
|
10
|
-
class CodingAgentError <
|
10
|
+
class CodingAgentError < Roast::Error; end
|
11
11
|
|
12
12
|
CONFIG_CODING_AGENT_COMMAND = "coding_agent_command"
|
13
13
|
private_constant :CONFIG_CODING_AGENT_COMMAND
|
@@ -59,7 +59,7 @@ module Roast
|
|
59
59
|
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
60
60
|
end
|
61
61
|
Roast::Helpers::Logger.error("🤖 CodingAgent did not complete successfully after multiple retries")
|
62
|
-
rescue
|
62
|
+
rescue Roast::Error => e
|
63
63
|
"🤖 Error running CodingAgent: #{e.message}".tap do |error_message|
|
64
64
|
Roast::Helpers::Logger.error(error_message + "\n")
|
65
65
|
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
data/lib/roast/tools/grep.rb
CHANGED
@@ -47,7 +47,7 @@ module Roast
|
|
47
47
|
else
|
48
48
|
stdout
|
49
49
|
end
|
50
|
-
rescue
|
50
|
+
rescue Roast::Error => e
|
51
51
|
"Error grepping for string: #{e.message}".tap do |error_message|
|
52
52
|
Roast::Helpers::Logger.error(error_message + "\n")
|
53
53
|
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
@@ -39,7 +39,7 @@ module Roast
|
|
39
39
|
else
|
40
40
|
File.read(path)
|
41
41
|
end
|
42
|
-
rescue
|
42
|
+
rescue Roast::Error => e
|
43
43
|
"Error reading file: #{e.message}".tap do |error_message|
|
44
44
|
Roast::Helpers::Logger.error(error_message + "\n")
|
45
45
|
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
@@ -44,7 +44,7 @@ module Roast
|
|
44
44
|
|
45
45
|
results.map { |result| File.join(path, result) }.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
|
46
46
|
end
|
47
|
-
rescue
|
47
|
+
rescue Roast::Error => e
|
48
48
|
"Error searching for '#{glob_pattern}' in '#{path}': #{e.message}".tap do |error_message|
|
49
49
|
Roast::Helpers::Logger.error(error_message + "\n")
|
50
50
|
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
data/lib/roast/tools/swarm.rb
CHANGED
@@ -70,7 +70,7 @@ module Roast
|
|
70
70
|
|
71
71
|
# Apply changes atomically
|
72
72
|
apply_changes(file_changes, base_path, create_files)
|
73
|
-
rescue
|
73
|
+
rescue Roast::Error => e
|
74
74
|
"Error applying patch: #{e.message}".tap do |error_message|
|
75
75
|
Roast::Helpers::Logger.error(error_message + "\n")
|
76
76
|
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
@@ -316,7 +316,7 @@ module Roast
|
|
316
316
|
end
|
317
317
|
|
318
318
|
"Successfully applied patch to #{modified_files.size} file(s): #{modified_files.join(", ")}"
|
319
|
-
rescue
|
319
|
+
rescue Roast::Error => e
|
320
320
|
# Restore backups if any change fails
|
321
321
|
backup_files.each do |path, content|
|
322
322
|
File.write(path, content) if File.exist?(path)
|
@@ -49,7 +49,7 @@ module Roast
|
|
49
49
|
Roast::Helpers::Logger.error(restriction_message)
|
50
50
|
"Error: Path must start with '#{restrict_path}' to use the write_file tool, try again."
|
51
51
|
end
|
52
|
-
rescue
|
52
|
+
rescue Roast::Error => e
|
53
53
|
"Error writing file: #{e.message}".tap do |error_message|
|
54
54
|
Roast::Helpers::Logger.error(error_message + "\n")
|
55
55
|
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
data/lib/roast/tools.rb
CHANGED
data/lib/roast/version.rb
CHANGED