roast-ai 1.0.0 → 1.0.1
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/Gemfile.lock +2 -1
- data/examples/console_output.rb +13 -0
- data/examples/custom_logging.rb +27 -0
- data/examples/demo/Gemfile.lock +3 -1
- data/examples/plugin-gem-example/Gemfile.lock +2 -1
- data/examples/shell_sanitization.rb +37 -0
- data/lib/roast/cog.rb +17 -3
- data/lib/roast/cogs/agent/providers/claude/claude_invocation.rb +2 -0
- data/lib/roast/cogs/agent/providers/claude/message.rb +6 -0
- data/lib/roast/cogs/agent/providers/claude/messages/result_message.rb +1 -0
- data/lib/roast/cogs/agent/providers/claude/messages/system_message.rb +16 -12
- data/lib/roast/cogs/agent/providers/claude/messages/thinking_message.rb +36 -0
- data/lib/roast/cogs/agent/providers/claude/messages/tool_use_message.rb +1 -0
- data/lib/roast/event.rb +75 -0
- data/lib/roast/event_monitor.rb +163 -0
- data/lib/roast/execution_manager.rb +22 -2
- data/lib/roast/log.rb +68 -45
- data/lib/roast/log_formatter.rb +56 -0
- data/lib/roast/output_router.rb +76 -0
- data/lib/roast/system_cog/params.rb +8 -0
- data/lib/roast/system_cog.rb +3 -3
- data/lib/roast/system_cogs/call.rb +1 -1
- data/lib/roast/system_cogs/map.rb +1 -1
- data/lib/roast/system_cogs/repeat.rb +1 -1
- data/lib/roast/task_context.rb +53 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow.rb +14 -6
- data/lib/roast.rb +3 -2
- data/roast-ai.gemspec +1 -0
- data/sorbet/tapioca/require.rb +3 -2
- metadata +24 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5ba7cc0a49bb69453853fcd6f4e2976d06260a9fc0c5c949007f313fba05a24
|
|
4
|
+
data.tar.gz: bd2a41c501b808ccea6970c7cb2af195848708fc23acb2c35aae6fa3a143b05d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 475cd2ca97a13f701fd4c699257090b2b89b559bde22b1391506daa8c876dbbd6a3d2711d6e46a2a22f34db1c83477a373728575eeb9178c113447ec624048d5
|
|
7
|
+
data.tar.gz: 4a68745484d9aa4da868cf99340eb27578d507c632a2d935c5e6aa5dc037501db56ed271042f295d0e3d8a4e8247cef9eec5aaa66dad02295a9e020cd2d3085f
|
data/Gemfile.lock
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::Workflow
|
|
5
|
+
|
|
6
|
+
# By default, Roast logs to standard error at the WARN level. You can easily override this with the ROAST_LOG_LEVEL
|
|
7
|
+
# environment variable, without having to touch the logger's configuration itself.
|
|
8
|
+
# Valid levels are DEBUG, INFO, WARN, ERROR, or FATAL (not case-sensitive).
|
|
9
|
+
# For more advanced configuration, such as configuring a custom log formatter, or logging to a custom output location,
|
|
10
|
+
# you can configure ore replace the logger instance used by Roast (Roast::Log.logger)
|
|
11
|
+
|
|
12
|
+
# Log to standard output, always at the DEBUG level
|
|
13
|
+
Roast::Log.logger = Logger.new($stdout).tap { |logger| logger.level = ::Logger::DEBUG }
|
|
14
|
+
|
|
15
|
+
# Format log lines in a particular way
|
|
16
|
+
Roast::Log.logger.formatter = proc do |severity, time, progname, msg|
|
|
17
|
+
"#{severity[0..0]}, #{msg.strip} (at #{time})\n"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
config do
|
|
22
|
+
cmd(:echo) { display! }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
execute do
|
|
26
|
+
cmd(:echo) { "echo hello world" }
|
|
27
|
+
end
|
data/examples/demo/Gemfile.lock
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ../..
|
|
3
3
|
specs:
|
|
4
|
-
roast-ai (1.0.
|
|
4
|
+
roast-ai (1.0.1)
|
|
5
5
|
activesupport (~> 8.0)
|
|
6
6
|
async (>= 2.34)
|
|
7
|
+
rainbow (>= 3.0.0)
|
|
7
8
|
ruby_llm (>= 1.8)
|
|
8
9
|
zeitwerk (>= 2.6)
|
|
9
10
|
|
|
@@ -71,6 +72,7 @@ GEM
|
|
|
71
72
|
multipart-post (2.4.1)
|
|
72
73
|
net-http (0.9.1)
|
|
73
74
|
uri (>= 0.11.1)
|
|
75
|
+
rainbow (3.1.1)
|
|
74
76
|
ruby_llm (1.8.2)
|
|
75
77
|
base64
|
|
76
78
|
event_stream_parser (~> 1)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#: self as Roast::Workflow
|
|
5
|
+
|
|
6
|
+
# If you have untrusted strings you want to use in `cmd` cog shell commands,
|
|
7
|
+
# you have several built-in options for safety
|
|
8
|
+
|
|
9
|
+
config do
|
|
10
|
+
cmd { display! }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
execute do
|
|
14
|
+
cmd(:attack_succeeds) do
|
|
15
|
+
bad_param = "; echo bad"
|
|
16
|
+
"echo hello world #{bad_param}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
cmd(:attack_fails_because_of_sanitization) do
|
|
20
|
+
# Use `.shellescape` on any untrusted strings you're interpolating into shell command strings
|
|
21
|
+
bad_param = "; echo bad"
|
|
22
|
+
"echo hello world #{bad_param.shellescape}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
cmd(:attack_fails_because_of_explicit_args) do |my|
|
|
26
|
+
# Use an array of arguments instead of string interpolation. This skips the shell entirely and just run the command.
|
|
27
|
+
bad_param = "; echo bad"
|
|
28
|
+
my.command = "echo"
|
|
29
|
+
my.args = ["hello", "world", bad_param]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
cmd(:attack_fails_because_of_explicit_args_shorthand) do
|
|
33
|
+
# Simply returning an array instead of a string is convenient shorthand
|
|
34
|
+
bad_param = "; echo bad"
|
|
35
|
+
["echo", "hello", "world", bad_param]
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/roast/cog.rb
CHANGED
|
@@ -44,9 +44,10 @@ module Roast
|
|
|
44
44
|
#: Cog::Output?
|
|
45
45
|
attr_reader :output
|
|
46
46
|
|
|
47
|
-
#: (Symbol, ^(Cog::Input) -> untyped) -> void
|
|
48
|
-
def initialize(name, cog_input_proc)
|
|
47
|
+
#: (Symbol, ^(Cog::Input) -> untyped, ?anonymous: bool) -> void
|
|
48
|
+
def initialize(name, cog_input_proc, anonymous: false)
|
|
49
49
|
@name = name
|
|
50
|
+
@anonymous = anonymous
|
|
50
51
|
@cog_input_proc = cog_input_proc #: ^(Cog::Input) -> untyped
|
|
51
52
|
@output = nil #: Cog::Output?
|
|
52
53
|
@skipped = false #: bool
|
|
@@ -56,12 +57,23 @@ module Roast
|
|
|
56
57
|
@config = self.class.config_class.new #: untyped
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
#: () -> bool
|
|
61
|
+
def anonymous?
|
|
62
|
+
@anonymous
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#: () -> String
|
|
66
|
+
def type
|
|
67
|
+
self.class.name.not_nil!.demodulize.underscore
|
|
68
|
+
end
|
|
69
|
+
|
|
59
70
|
#: (Async::Barrier, Cog::Config, CogInputContext, untyped, Integer) -> Async::Task
|
|
60
71
|
def run!(barrier, config, input_context, executor_scope_value, executor_scope_index)
|
|
61
72
|
raise CogAlreadyStartedError if @task
|
|
62
73
|
|
|
63
74
|
@task = barrier.async(finished: false) do |task|
|
|
64
|
-
task.annotate("
|
|
75
|
+
task.annotate("Cog #{type}(:#{@name})")
|
|
76
|
+
TaskContext.begin_cog(self)
|
|
65
77
|
@config = config
|
|
66
78
|
input_instance = self.class.input_class.new
|
|
67
79
|
input_return = input_context.instance_exec(
|
|
@@ -84,6 +96,8 @@ module Roast
|
|
|
84
96
|
rescue StandardError => e
|
|
85
97
|
@failed = true
|
|
86
98
|
raise e
|
|
99
|
+
ensure
|
|
100
|
+
TaskContext.end
|
|
87
101
|
end
|
|
88
102
|
end
|
|
89
103
|
|
|
@@ -31,6 +31,8 @@ module Roast
|
|
|
31
31
|
Messages::SystemMessage.new(type:, hash:)
|
|
32
32
|
when :text
|
|
33
33
|
Messages::TextMessage.new(type:, hash:)
|
|
34
|
+
when :thinking
|
|
35
|
+
Messages::ThinkingMessage.new(type:, hash:)
|
|
34
36
|
when :tool_result
|
|
35
37
|
Messages::ToolResultMessage.new(type:, hash:)
|
|
36
38
|
when :tool_use
|
|
@@ -49,6 +51,9 @@ module Roast
|
|
|
49
51
|
#: Symbol
|
|
50
52
|
attr_reader :type
|
|
51
53
|
|
|
54
|
+
#: String?
|
|
55
|
+
attr_reader :error
|
|
56
|
+
|
|
52
57
|
#: Hash[Symbol, untyped]
|
|
53
58
|
attr_reader :unparsed
|
|
54
59
|
|
|
@@ -56,6 +61,7 @@ module Roast
|
|
|
56
61
|
def initialize(type:, hash:)
|
|
57
62
|
@session_id = hash.delete(:session_id)
|
|
58
63
|
@type = type
|
|
64
|
+
@error = hash.delete(:error)
|
|
59
65
|
hash.except!(*IGNORED_FIELDS)
|
|
60
66
|
@unparsed = hash
|
|
61
67
|
end
|
|
@@ -9,23 +9,27 @@ module Roast
|
|
|
9
9
|
module Messages
|
|
10
10
|
class SystemMessage < Message
|
|
11
11
|
IGNORED_FIELDS = [
|
|
12
|
-
:
|
|
13
|
-
:cwd,
|
|
14
|
-
:tools,
|
|
15
|
-
:mcp_servers,
|
|
16
|
-
:permissionMode,
|
|
17
|
-
:slash_commands,
|
|
12
|
+
:agents,
|
|
18
13
|
:apiKeySource,
|
|
14
|
+
:compact_metadata,
|
|
19
15
|
:claude_code_version,
|
|
16
|
+
:cwd,
|
|
17
|
+
:exit_code,
|
|
18
|
+
:fast_mode_state,
|
|
19
|
+
:hook_event,
|
|
20
|
+
:hook_name,
|
|
21
|
+
:mcp_servers,
|
|
20
22
|
:output_style,
|
|
21
|
-
:
|
|
22
|
-
:skills,
|
|
23
|
+
:permissionMode,
|
|
23
24
|
:plugins,
|
|
24
|
-
:
|
|
25
|
-
:
|
|
26
|
-
:
|
|
25
|
+
:skills,
|
|
26
|
+
:slash_commands,
|
|
27
|
+
# TODO: "status": "compacting" indicates compaction in progress. We might want to handle that someday
|
|
28
|
+
:status,
|
|
27
29
|
:stderr,
|
|
28
|
-
:
|
|
30
|
+
:stdout,
|
|
31
|
+
:subtype,
|
|
32
|
+
:tools,
|
|
29
33
|
].freeze
|
|
30
34
|
|
|
31
35
|
#: String?
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module Cogs
|
|
6
|
+
class Agent < Cog
|
|
7
|
+
module Providers
|
|
8
|
+
class Claude < Provider
|
|
9
|
+
module Messages
|
|
10
|
+
class ThinkingMessage < Message
|
|
11
|
+
IGNORED_FIELDS = [
|
|
12
|
+
:signature,
|
|
13
|
+
:role,
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
#: String
|
|
17
|
+
attr_reader :thinking
|
|
18
|
+
|
|
19
|
+
#: (type: Symbol, hash: Hash[Symbol, untyped]) -> void
|
|
20
|
+
def initialize(type:, hash:)
|
|
21
|
+
@thinking = hash.delete(:thinking) || ""
|
|
22
|
+
hash.except!(*IGNORED_FIELDS)
|
|
23
|
+
super(type:, hash:)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: (ClaudeInvocation::Context) -> String?
|
|
27
|
+
def format(context)
|
|
28
|
+
@thinking
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/roast/event.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
class Event
|
|
6
|
+
class << self
|
|
7
|
+
#: (Hash[Symbol, untyped]) -> void
|
|
8
|
+
def <<(event)
|
|
9
|
+
EventMonitor.accept(Event.new(TaskContext.path, event))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
LOG_TYPE_KEYS = [
|
|
14
|
+
:fatal,
|
|
15
|
+
:error,
|
|
16
|
+
:warn,
|
|
17
|
+
:info,
|
|
18
|
+
:debug,
|
|
19
|
+
:unknown,
|
|
20
|
+
].freeze #: Array[Symbol]
|
|
21
|
+
|
|
22
|
+
OTHER_TYPE_KEYS = [
|
|
23
|
+
:begin,
|
|
24
|
+
:end,
|
|
25
|
+
:stdout,
|
|
26
|
+
:stderr,
|
|
27
|
+
].freeze #: Array[Symbol]
|
|
28
|
+
|
|
29
|
+
#: Array[TaskContext::PathElement]
|
|
30
|
+
attr_reader :path
|
|
31
|
+
|
|
32
|
+
#: Hash[Symbol, untyped] :payload
|
|
33
|
+
attr_reader :payload
|
|
34
|
+
|
|
35
|
+
#: Time
|
|
36
|
+
attr_reader :time
|
|
37
|
+
|
|
38
|
+
delegate :[], :key?, :keys, to: :payload
|
|
39
|
+
|
|
40
|
+
#: (Array[TaskContext::PathElement] path, Hash[Symbol, untyped]) -> void
|
|
41
|
+
def initialize(path, payload)
|
|
42
|
+
@path = path
|
|
43
|
+
@payload = payload
|
|
44
|
+
@time = Time.now
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#: () -> Symbol
|
|
48
|
+
def type
|
|
49
|
+
return :log if (LOG_TYPE_KEYS & @payload.keys).present?
|
|
50
|
+
|
|
51
|
+
(OTHER_TYPE_KEYS & @payload.keys).first || :unknown
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#: () -> Integer
|
|
55
|
+
def log_severity
|
|
56
|
+
severity = case type
|
|
57
|
+
when :log
|
|
58
|
+
(LOG_TYPE_KEYS & @payload.keys).first || :unknown
|
|
59
|
+
when :stderr
|
|
60
|
+
:warn
|
|
61
|
+
else
|
|
62
|
+
:info
|
|
63
|
+
end
|
|
64
|
+
Logger::Severity.const_get(:LEVELS)[severity.to_s] # rubocop:disable Sorbet/ConstantsFromStrings
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#: () -> String
|
|
68
|
+
def log_message
|
|
69
|
+
key = (LOG_TYPE_KEYS & @payload.keys).first
|
|
70
|
+
return "" unless key.present?
|
|
71
|
+
|
|
72
|
+
payload[key] || ""
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module EventMonitor
|
|
6
|
+
extend self
|
|
7
|
+
include Kernel
|
|
8
|
+
|
|
9
|
+
class EventMonitorError < StandardError; end
|
|
10
|
+
|
|
11
|
+
class EventMonitorAlreadyStartedError < EventMonitorError; end
|
|
12
|
+
|
|
13
|
+
class EventMonitorNotRunningError < EventMonitorError; end
|
|
14
|
+
|
|
15
|
+
@queue = Async::Queue.new.tap(&:close) #: Async::Queue
|
|
16
|
+
@task = nil #: Async::Task?
|
|
17
|
+
|
|
18
|
+
#: () -> bool
|
|
19
|
+
def running?
|
|
20
|
+
!@queue.closed?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#: () -> Async::Task
|
|
24
|
+
def start!
|
|
25
|
+
raise EventMonitorAlreadyStartedError if running?
|
|
26
|
+
|
|
27
|
+
OutputRouter.enable!
|
|
28
|
+
@queue = Async::Queue.new
|
|
29
|
+
@task = Async(transient: true) do
|
|
30
|
+
OutputRouter.mark_as_output_fiber!
|
|
31
|
+
loop do
|
|
32
|
+
event = @queue.pop #: as Event?
|
|
33
|
+
break if event.nil?
|
|
34
|
+
|
|
35
|
+
handle_event(event)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#: () -> void
|
|
41
|
+
def stop!
|
|
42
|
+
raise EventMonitorNotRunningError unless running?
|
|
43
|
+
|
|
44
|
+
OutputRouter.disable!
|
|
45
|
+
@queue.close
|
|
46
|
+
@task&.wait
|
|
47
|
+
@task = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#: () -> void
|
|
51
|
+
def reset!
|
|
52
|
+
OutputRouter.disable!
|
|
53
|
+
@queue.close
|
|
54
|
+
@task = nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
#: (Event) -> void
|
|
58
|
+
def accept(event)
|
|
59
|
+
if running?
|
|
60
|
+
@queue.push(event)
|
|
61
|
+
else
|
|
62
|
+
handle_event(event)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
#: (Event) -> void
|
|
69
|
+
def handle_event(event)
|
|
70
|
+
with_stubbed_class_method_returning(Time, :now, event.time) do
|
|
71
|
+
OutputRouter.mark_as_output_fiber!
|
|
72
|
+
handler_method_name = "handle_#{event.type}_event".to_sym
|
|
73
|
+
if respond_to?(handler_method_name, true)
|
|
74
|
+
send(handler_method_name, event)
|
|
75
|
+
else
|
|
76
|
+
handle_unknown_event(event)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#: (Event) -> void
|
|
82
|
+
def handle_begin_event(event)
|
|
83
|
+
# The first path element is always the top-level ExecutionManager
|
|
84
|
+
handle_begin_workflow_event(event) if event.path.length == 1
|
|
85
|
+
return unless event[:begin].cog.present?
|
|
86
|
+
|
|
87
|
+
Roast::Log.logger.info { "#{format_path(event)} Starting" }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def handle_begin_workflow_event(event)
|
|
91
|
+
execution_manager = event[:begin].execution_manager.not_nil!
|
|
92
|
+
workflow_context = execution_manager.workflow_context
|
|
93
|
+
Roast::Log.logger.info("🔥🔥🔥 Workflow Starting")
|
|
94
|
+
Roast::Log.logger.debug do
|
|
95
|
+
message = <<~MESSAGE
|
|
96
|
+
Workflow Context:
|
|
97
|
+
Targets: #{workflow_context.params.targets}
|
|
98
|
+
Args: #{workflow_context.params.args}
|
|
99
|
+
Kwargs: #{workflow_context.params.kwargs}
|
|
100
|
+
Temporary Directory: #{workflow_context.tmpdir}
|
|
101
|
+
Workflow Directory: #{workflow_context.workflow_dir}
|
|
102
|
+
Working Directory: #{Dir.pwd}
|
|
103
|
+
MESSAGE
|
|
104
|
+
message.strip
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#: (Event) -> void
|
|
109
|
+
def handle_end_event(event)
|
|
110
|
+
# The first path element is always the top-level ExecutionManager
|
|
111
|
+
Roast::Log.logger.info("🔥🔥🔥 Workflow Complete") if event.path.length == 1
|
|
112
|
+
return unless event[:end].cog.present?
|
|
113
|
+
|
|
114
|
+
Roast::Log.logger.info { "#{format_path(event)} Complete" }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
#: (Event) -> void
|
|
118
|
+
def handle_log_event(event)
|
|
119
|
+
Roast::Log.logger.add(event.log_severity, event.log_message)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
#: (Event) -> void
|
|
123
|
+
def handle_stderr_event(event)
|
|
124
|
+
Roast::Log.logger.warn { "#{format_path(event)} ❯❯ #{event[:stderr]}" }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
#: (Event) -> void
|
|
128
|
+
def handle_stdout_event(event)
|
|
129
|
+
Roast::Log.logger.info { "#{format_path(event)} ❯ #{event[:stdout]}" }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
#: (Event) -> void
|
|
133
|
+
def handle_unknown_event(event)
|
|
134
|
+
Roast::Log.logger.unknown(event.inspect)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
#: (Event) -> String
|
|
138
|
+
def format_path(event)
|
|
139
|
+
event.path.map do |element|
|
|
140
|
+
cog = element.cog
|
|
141
|
+
execution_manager = element.execution_manager
|
|
142
|
+
if cog.present?
|
|
143
|
+
"#{cog.type}#{cog.anonymous? ? "" : "(:#{cog.name})"}"
|
|
144
|
+
elsif execution_manager&.scope
|
|
145
|
+
"{:#{execution_manager.scope}}[#{execution_manager.scope_index}]"
|
|
146
|
+
end
|
|
147
|
+
end.compact.join(" -> ")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
#: [T] (Class, Symbol, untyped) { () -> T } -> T
|
|
151
|
+
def with_stubbed_class_method_returning(klass, method_name, return_value, &blk)
|
|
152
|
+
original_method = klass.singleton_class.instance_method(method_name)
|
|
153
|
+
klass.singleton_class.silence_redefinition_of_method(method_name)
|
|
154
|
+
klass.define_singleton_method(method_name, proc { return_value })
|
|
155
|
+
blk.call
|
|
156
|
+
ensure
|
|
157
|
+
if original_method
|
|
158
|
+
klass.singleton_class.silence_redefinition_of_method(method_name)
|
|
159
|
+
klass.define_singleton_method(method_name, original_method)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -24,6 +24,18 @@ module Roast
|
|
|
24
24
|
|
|
25
25
|
class OutputsAlreadyDefinedError < ExecutionManagerError; end
|
|
26
26
|
|
|
27
|
+
#: WorkflowContext
|
|
28
|
+
attr_reader :workflow_context
|
|
29
|
+
|
|
30
|
+
#: Symbol?
|
|
31
|
+
attr_reader :scope
|
|
32
|
+
|
|
33
|
+
#: untyped
|
|
34
|
+
attr_reader :scope_value
|
|
35
|
+
|
|
36
|
+
#: Integer
|
|
37
|
+
attr_reader :scope_index
|
|
38
|
+
|
|
27
39
|
#: untyped
|
|
28
40
|
attr_reader :final_output
|
|
29
41
|
|
|
@@ -79,6 +91,7 @@ module Roast
|
|
|
79
91
|
@running = true
|
|
80
92
|
Sync do |sync_task|
|
|
81
93
|
sync_task.annotate("ExecutionManager #{@scope}")
|
|
94
|
+
TaskContext.begin_execution_manager(self)
|
|
82
95
|
@cog_stack.each do |cog|
|
|
83
96
|
cog_config = @config_manager.config_for(cog.class, cog.name)
|
|
84
97
|
cog_task = cog.run!(
|
|
@@ -97,6 +110,7 @@ module Roast
|
|
|
97
110
|
ensure
|
|
98
111
|
@barrier.stop
|
|
99
112
|
compute_final_output
|
|
113
|
+
TaskContext.end
|
|
100
114
|
@running = false
|
|
101
115
|
end
|
|
102
116
|
end
|
|
@@ -199,8 +213,14 @@ module Roast
|
|
|
199
213
|
raise NotImplementedError, "No system cog manager defined for #{cog_class}"
|
|
200
214
|
end
|
|
201
215
|
else
|
|
202
|
-
cog_name = Array.wrap(cog_args).shift
|
|
203
|
-
|
|
216
|
+
cog_name = Array.wrap(cog_args).shift
|
|
217
|
+
if cog_name
|
|
218
|
+
anonymous = false
|
|
219
|
+
else
|
|
220
|
+
anonymous = true
|
|
221
|
+
cog_name = Cog.generate_fallback_name
|
|
222
|
+
end
|
|
223
|
+
cog_instance = cog_class.new(cog_name, cog_input_proc, anonymous:)
|
|
204
224
|
end
|
|
205
225
|
add_cog_instance(cog_instance)
|
|
206
226
|
end
|
data/lib/roast/log.rb
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
module Roast
|
|
5
5
|
# Central logging interface for Roast.
|
|
6
6
|
#
|
|
7
|
-
# Provides a simple, testable logging API that wraps the standard library Logger
|
|
8
|
-
#
|
|
7
|
+
# Provides a simple, testable logging API that wraps the standard library Logger
|
|
8
|
+
# and leverages Roast's Event framework for clean async task integration with proper task hierarchy attribution.
|
|
9
|
+
# Outputs to STDERR by default.
|
|
9
10
|
#
|
|
10
11
|
# @example Basic usage
|
|
11
12
|
# Roast::Log.info("Processing file...")
|
|
@@ -17,61 +18,83 @@ module Roast
|
|
|
17
18
|
# Roast::Log.logger = Rails.logger
|
|
18
19
|
#
|
|
19
20
|
module Log
|
|
21
|
+
extend self
|
|
22
|
+
include Kernel
|
|
23
|
+
|
|
20
24
|
LOG_LEVELS = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}.freeze
|
|
27
|
-
|
|
28
|
-
class << self
|
|
29
|
-
attr_writer :logger
|
|
30
|
-
|
|
31
|
-
def debug(message)
|
|
32
|
-
logger.debug(message)
|
|
33
|
-
end
|
|
25
|
+
DEBUG: ::Logger::DEBUG,
|
|
26
|
+
INFO: ::Logger::INFO,
|
|
27
|
+
WARN: ::Logger::WARN,
|
|
28
|
+
ERROR: ::Logger::ERROR,
|
|
29
|
+
FATAL: ::Logger::FATAL,
|
|
30
|
+
}.freeze #: Hash[Symbol, Integer]
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
logger.info(message)
|
|
37
|
-
end
|
|
32
|
+
attr_writer :logger
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
#: (String) -> void
|
|
35
|
+
def debug(message)
|
|
36
|
+
Roast::Event << { debug: message }
|
|
37
|
+
end
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
#: (String) -> void
|
|
40
|
+
def info(message)
|
|
41
|
+
Roast::Event << { info: message }
|
|
42
|
+
end
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
#: (String) -> void
|
|
45
|
+
def warn(message)
|
|
46
|
+
Roast::Event << { warn: message }
|
|
47
|
+
end
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
#: (String) -> void
|
|
50
|
+
def error(message)
|
|
51
|
+
Roast::Event << { error: message }
|
|
52
|
+
end
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
#: (String) -> void
|
|
55
|
+
def fatal(message)
|
|
56
|
+
Roast::Event << { fatal: message }
|
|
57
|
+
end
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
#: (String) -> void
|
|
60
|
+
def unknown(message)
|
|
61
|
+
Roast::Event << { unknown: message }
|
|
62
|
+
end
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
#: () -> Logger
|
|
65
|
+
def logger
|
|
66
|
+
@logger ||= create_logger
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#: () -> void
|
|
70
|
+
def reset!
|
|
71
|
+
@logger = nil
|
|
72
|
+
end
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
raise ArgumentError, "Invalid log level: #{level_str}. Valid levels are: #{LOG_LEVELS.keys.join(", ")}"
|
|
71
|
-
end
|
|
74
|
+
#: () -> bool
|
|
75
|
+
def tty?
|
|
76
|
+
return false unless @logger
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
logdev = @logger.instance_variable_get(:@logdev)&.dev
|
|
79
|
+
logdev&.respond_to?(:isatty) && logdev&.isatty
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
#: () -> Logger
|
|
85
|
+
def create_logger
|
|
86
|
+
::Logger.new($stderr, progname: "Roast").tap do |l|
|
|
87
|
+
l.level = LOG_LEVELS.fetch(log_level("INFO"))
|
|
88
|
+
l.formatter = Roast::LogFormatter.new(tty: $stderr.tty?)
|
|
74
89
|
end
|
|
75
90
|
end
|
|
91
|
+
|
|
92
|
+
#: (String) -> Symbol
|
|
93
|
+
def log_level(default_level)
|
|
94
|
+
level = (ENV["ROAST_LOG_LEVEL"] || default_level).upcase.to_sym
|
|
95
|
+
raise ArgumentError, "Invalid log level: #{level}. Valid levels are: #{LOG_LEVELS.keys.join(", ")}" unless LOG_LEVELS.key?(level)
|
|
96
|
+
|
|
97
|
+
level
|
|
98
|
+
end
|
|
76
99
|
end
|
|
77
100
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
class LogFormatter < ::Logger::Formatter
|
|
6
|
+
TTY_FORMAT = "• %.1s, %s\n" #: String
|
|
7
|
+
NON_TTY_FORMAT = "%.1s, [%s] %5s -- %s\n" #: String
|
|
8
|
+
DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N" #: String
|
|
9
|
+
|
|
10
|
+
#: (tty: bool) -> void
|
|
11
|
+
def initialize(tty:)
|
|
12
|
+
super()
|
|
13
|
+
@tty = tty
|
|
14
|
+
@rainbow = Rainbow.new.tap { |r| r.enabled = tty }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call(severity, time, _progname, msg)
|
|
18
|
+
line = if @tty
|
|
19
|
+
format(TTY_FORMAT, severity, msg2str(msg))
|
|
20
|
+
else
|
|
21
|
+
format(NON_TTY_FORMAT, severity, time.strftime(DATETIME_FORMAT), severity, msg2str(msg))
|
|
22
|
+
end
|
|
23
|
+
colourize(severity, line)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
#: (String, String) -> String
|
|
29
|
+
def colourize(severity, line)
|
|
30
|
+
if line.include?("❯❯") # standard error lines
|
|
31
|
+
@rainbow.wrap(line).yellow
|
|
32
|
+
elsif line.include?("❯") # standard output lines
|
|
33
|
+
@rainbow.wrap(line)
|
|
34
|
+
else
|
|
35
|
+
case severity
|
|
36
|
+
when "ERROR", "FATAL" then @rainbow.wrap(line).red
|
|
37
|
+
when "WARN" then @rainbow.wrap(line).color("#FF8C00") # orange
|
|
38
|
+
when "INFO" then @rainbow.wrap(line).bright
|
|
39
|
+
when "DEBUG" then @rainbow.wrap(line).faint
|
|
40
|
+
else line
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#: (String | Exception | untyped) -> String
|
|
46
|
+
def msg2str(msg)
|
|
47
|
+
msg = case msg
|
|
48
|
+
when ::String
|
|
49
|
+
msg.strip
|
|
50
|
+
else
|
|
51
|
+
msg
|
|
52
|
+
end
|
|
53
|
+
super(msg)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
class OutputRouter
|
|
6
|
+
# This is the name of the alias methods for `:write` on the $stdout and $stderr objects
|
|
7
|
+
# that bypass OutputRouter's wrapper. Calling this method will write to those output stream directly.
|
|
8
|
+
WRITE_WITHOUT_ROAST = :write_without_roast
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
#: () -> bool
|
|
12
|
+
def enable!
|
|
13
|
+
return false if enabled?
|
|
14
|
+
|
|
15
|
+
activate($stdout, :stdout)
|
|
16
|
+
activate($stderr, :stderr)
|
|
17
|
+
mark_as_output_fiber!
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#: () -> bool
|
|
22
|
+
def disable!
|
|
23
|
+
return false unless enabled?
|
|
24
|
+
|
|
25
|
+
deactivate($stdout)
|
|
26
|
+
deactivate($stderr)
|
|
27
|
+
@output_fiber = nil
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: () -> bool
|
|
32
|
+
def enabled?
|
|
33
|
+
$stdout.respond_to?(WRITE_WITHOUT_ROAST)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#: () -> bool
|
|
37
|
+
def output_fiber?
|
|
38
|
+
@output_fiber == Fiber.current
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#: () -> void
|
|
42
|
+
def mark_as_output_fiber!
|
|
43
|
+
@output_fiber = Fiber.current
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
#: (IO stream, Symbol name) -> void
|
|
49
|
+
def activate(stream, name)
|
|
50
|
+
router = self
|
|
51
|
+
stream.singleton_class.send(:alias_method, WRITE_WITHOUT_ROAST, :write)
|
|
52
|
+
stream.define_singleton_method(:write) do |*args|
|
|
53
|
+
if router.output_fiber?
|
|
54
|
+
self #: as untyped # rubocop:disable Style/RedundantSelf
|
|
55
|
+
.send(WRITE_WITHOUT_ROAST, *args)
|
|
56
|
+
else
|
|
57
|
+
str = args.map(&:to_s).join
|
|
58
|
+
Event << case name
|
|
59
|
+
when :stdout then { stdout: str }
|
|
60
|
+
when :stderr then { stderr: str }
|
|
61
|
+
else { unknown: str }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#: (IO stream) -> void
|
|
68
|
+
def deactivate(stream)
|
|
69
|
+
sc = stream.singleton_class
|
|
70
|
+
sc.send(:remove_method, :write)
|
|
71
|
+
sc.send(:alias_method, :write, WRITE_WITHOUT_ROAST)
|
|
72
|
+
sc.send(:remove_method, WRITE_WITHOUT_ROAST)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -23,8 +23,16 @@ module Roast
|
|
|
23
23
|
#
|
|
24
24
|
#: (Symbol?) -> void
|
|
25
25
|
def initialize(name)
|
|
26
|
+
@anonymous = name.nil? #: bool
|
|
26
27
|
@name = name || Cog.generate_fallback_name
|
|
27
28
|
end
|
|
29
|
+
|
|
30
|
+
# Whether the cog is using a fallback name, or was given one explicitly.
|
|
31
|
+
#
|
|
32
|
+
#: () -> bool
|
|
33
|
+
def anonymous?
|
|
34
|
+
@anonymous
|
|
35
|
+
end
|
|
28
36
|
end
|
|
29
37
|
end
|
|
30
38
|
end
|
data/lib/roast/system_cog.rb
CHANGED
|
@@ -18,9 +18,9 @@ module Roast
|
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
#: (Symbol, ^(Cog::Input) -> untyped) { (Cog::Input, Cog::Config) -> Cog::Output } -> void
|
|
22
|
-
def initialize(name, cog_input_proc, &on_execute)
|
|
23
|
-
super(name, cog_input_proc)
|
|
21
|
+
#: (Symbol, ^(Cog::Input) -> untyped, anonymous: bool) { (Cog::Input, Cog::Config) -> Cog::Output } -> void
|
|
22
|
+
def initialize(name, cog_input_proc, anonymous:, &on_execute)
|
|
23
|
+
super(name, cog_input_proc, anonymous:)
|
|
24
24
|
@on_execute = on_execute
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -89,7 +89,7 @@ module Roast
|
|
|
89
89
|
|
|
90
90
|
#: (Params, ^(Cog::Input) -> untyped) -> SystemCogs::Call
|
|
91
91
|
def create_call_system_cog(params, input_proc)
|
|
92
|
-
SystemCogs::Call.new(params.name, input_proc) do |input|
|
|
92
|
+
SystemCogs::Call.new(params.name, input_proc, anonymous: params.anonymous?) do |input|
|
|
93
93
|
input = input #: as Input
|
|
94
94
|
raise ExecutionManager::ExecutionScopeNotSpecifiedError unless params.run.present?
|
|
95
95
|
|
|
@@ -257,7 +257,7 @@ module Roast
|
|
|
257
257
|
|
|
258
258
|
#: (Params, ^(Cog::Input) -> untyped) -> SystemCogs::Map
|
|
259
259
|
def create_map_system_cog(params, input_proc)
|
|
260
|
-
SystemCogs::Map.new(params.name, input_proc) do |input, config|
|
|
260
|
+
SystemCogs::Map.new(params.name, input_proc, anonymous: params.anonymous?) do |input, config|
|
|
261
261
|
raise ExecutionManager::ExecutionScopeNotSpecifiedError unless params.run.present?
|
|
262
262
|
|
|
263
263
|
input = input #: as Input
|
|
@@ -206,7 +206,7 @@ module Roast
|
|
|
206
206
|
|
|
207
207
|
#: (Params, ^(Cog::Input) -> untyped) -> SystemCogs::Repeat
|
|
208
208
|
def create_repeat_system_cog(params, input_proc)
|
|
209
|
-
SystemCogs::Repeat.new(params.name, input_proc) do |input|
|
|
209
|
+
SystemCogs::Repeat.new(params.name, input_proc, anonymous: params.anonymous?) do |input|
|
|
210
210
|
input = input #: as Input
|
|
211
211
|
raise ExecutionManager::ExecutionScopeNotSpecifiedError unless params.run.present?
|
|
212
212
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Roast
|
|
5
|
+
module TaskContext
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
class PathElement
|
|
9
|
+
#: Cog?
|
|
10
|
+
attr_reader :cog
|
|
11
|
+
|
|
12
|
+
#: ExecutionManager?
|
|
13
|
+
attr_reader :execution_manager
|
|
14
|
+
|
|
15
|
+
#: (?cog: Cog?, ?execution_manager: ExecutionManager?) -> void
|
|
16
|
+
def initialize(cog: nil, execution_manager: nil)
|
|
17
|
+
@cog = cog
|
|
18
|
+
@execution_manager = execution_manager
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: () -> Array[PathElement]
|
|
23
|
+
def path
|
|
24
|
+
Fiber[:path]&.deep_dup || []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#: (Cog) -> Array[PathElement]
|
|
28
|
+
def begin_cog(cog)
|
|
29
|
+
begin_element(PathElement.new(cog:))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#: (ExecutionManager) -> Array[PathElement]
|
|
33
|
+
def begin_execution_manager(execution_manager)
|
|
34
|
+
begin_element(PathElement.new(execution_manager:))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#: () -> [PathElement, Array[PathElement]]
|
|
38
|
+
def end
|
|
39
|
+
Event << { end: Fiber[:path]&.last }
|
|
40
|
+
el = Fiber[:path]&.pop
|
|
41
|
+
[el, path]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
#: (PathElement) -> Array[PathElement]
|
|
47
|
+
def begin_element(element)
|
|
48
|
+
Fiber[:path] = (Fiber[:path] || []) + [element]
|
|
49
|
+
Event << { begin: element }
|
|
50
|
+
path
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/roast/version.rb
CHANGED
data/lib/roast/workflow.rb
CHANGED
|
@@ -4,20 +4,28 @@
|
|
|
4
4
|
module Roast
|
|
5
5
|
class Workflow
|
|
6
6
|
class WorkflowError < Roast::Error; end
|
|
7
|
+
|
|
7
8
|
class WorkflowNotPreparedError < WorkflowError; end
|
|
9
|
+
|
|
8
10
|
class WorkflowAlreadyPreparedError < WorkflowError; end
|
|
11
|
+
|
|
9
12
|
class WorkflowAlreadyStartedError < WorkflowError; end
|
|
13
|
+
|
|
10
14
|
class InvalidLoadableReference < WorkflowError; end
|
|
11
15
|
|
|
12
16
|
class << self
|
|
13
17
|
#: (String | Pathname, WorkflowParams) -> void
|
|
14
18
|
def from_file(workflow_path, params)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
Sync do
|
|
20
|
+
Dir.mktmpdir("roast-") do |tmpdir|
|
|
21
|
+
EventMonitor.start!
|
|
22
|
+
workflow_dir = Pathname.new(workflow_path).dirname
|
|
23
|
+
workflow_context = WorkflowContext.new(params: params, tmpdir: tmpdir, workflow_dir: workflow_dir)
|
|
24
|
+
workflow = new(workflow_path, workflow_context)
|
|
25
|
+
workflow.prepare!
|
|
26
|
+
workflow.start!
|
|
27
|
+
EventMonitor.stop!
|
|
28
|
+
end
|
|
21
29
|
end
|
|
22
30
|
end
|
|
23
31
|
end
|
data/lib/roast.rb
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
# Standard library requires
|
|
5
|
-
require "digest"
|
|
6
5
|
require "English"
|
|
6
|
+
require "digest"
|
|
7
7
|
require "erb"
|
|
8
8
|
require "fileutils"
|
|
9
9
|
require "json"
|
|
@@ -24,14 +24,15 @@ require "active_support"
|
|
|
24
24
|
require "active_support/cache"
|
|
25
25
|
require "active_support/core_ext/array"
|
|
26
26
|
require "active_support/core_ext/hash"
|
|
27
|
-
require "active_support/core_ext/object/deep_dup"
|
|
28
27
|
require "active_support/core_ext/module/delegation"
|
|
28
|
+
require "active_support/core_ext/object/deep_dup"
|
|
29
29
|
require "active_support/core_ext/string"
|
|
30
30
|
require "active_support/core_ext/string/inflections"
|
|
31
31
|
require "active_support/isolated_execution_state"
|
|
32
32
|
require "active_support/notifications"
|
|
33
33
|
require "async"
|
|
34
34
|
require "async/semaphore"
|
|
35
|
+
require "rainbow"
|
|
35
36
|
require "ruby_llm"
|
|
36
37
|
|
|
37
38
|
# Require project components that will not get automatically loaded
|
data/roast-ai.gemspec
CHANGED
|
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
33
33
|
|
|
34
34
|
spec.add_dependency("activesupport", "~> 8.0")
|
|
35
35
|
spec.add_dependency("async", ">= 2.34")
|
|
36
|
+
spec.add_dependency("rainbow", ">= 3.0.0")
|
|
36
37
|
spec.add_dependency("ruby_llm", ">= 1.8")
|
|
37
38
|
spec.add_dependency("zeitwerk", ">= 2.6")
|
|
38
39
|
end
|
data/sorbet/tapioca/require.rb
CHANGED
|
@@ -6,8 +6,8 @@ require "active_support"
|
|
|
6
6
|
require "active_support/cache"
|
|
7
7
|
require "active_support/core_ext/array"
|
|
8
8
|
require "active_support/core_ext/hash"
|
|
9
|
-
require "active_support/core_ext/object/deep_dup"
|
|
10
9
|
require "active_support/core_ext/module/delegation"
|
|
10
|
+
require "active_support/core_ext/object/deep_dup"
|
|
11
11
|
require "active_support/core_ext/string"
|
|
12
12
|
require "active_support/core_ext/string/inflections"
|
|
13
13
|
require "active_support/isolated_execution_state"
|
|
@@ -17,8 +17,9 @@ require "async/semaphore"
|
|
|
17
17
|
require "bundler/setup"
|
|
18
18
|
require "json"
|
|
19
19
|
require "open3"
|
|
20
|
+
require "optparse"
|
|
21
|
+
require "rainbow"
|
|
20
22
|
require "shellwords"
|
|
21
23
|
require "sqlite3"
|
|
22
|
-
require "optparse"
|
|
23
24
|
require "timeout"
|
|
24
25
|
require "zeitwerk"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: roast-ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shopify
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '2.34'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rainbow
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 3.0.0
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 3.0.0
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: ruby_llm
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -107,6 +121,8 @@ files:
|
|
|
107
121
|
- examples/async_cogs_complex.rb
|
|
108
122
|
- examples/call.rb
|
|
109
123
|
- examples/collect_from.rb
|
|
124
|
+
- examples/console_output.rb
|
|
125
|
+
- examples/custom_logging.rb
|
|
110
126
|
- examples/demo/Gemfile
|
|
111
127
|
- examples/demo/Gemfile.lock
|
|
112
128
|
- examples/demo/cogs/local.rb
|
|
@@ -133,6 +149,7 @@ files:
|
|
|
133
149
|
- examples/prototype.rb
|
|
134
150
|
- examples/repeat_loop_results.rb
|
|
135
151
|
- examples/ruby_cog.rb
|
|
152
|
+
- examples/shell_sanitization.rb
|
|
136
153
|
- examples/simple_agent.rb
|
|
137
154
|
- examples/simple_chat.rb
|
|
138
155
|
- examples/simple_repeat.rb
|
|
@@ -171,6 +188,7 @@ files:
|
|
|
171
188
|
- lib/roast/cogs/agent/providers/claude/messages/result_message.rb
|
|
172
189
|
- lib/roast/cogs/agent/providers/claude/messages/system_message.rb
|
|
173
190
|
- lib/roast/cogs/agent/providers/claude/messages/text_message.rb
|
|
191
|
+
- lib/roast/cogs/agent/providers/claude/messages/thinking_message.rb
|
|
174
192
|
- lib/roast/cogs/agent/providers/claude/messages/tool_result_message.rb
|
|
175
193
|
- lib/roast/cogs/agent/providers/claude/messages/tool_use_message.rb
|
|
176
194
|
- lib/roast/cogs/agent/providers/claude/messages/unknown_message.rb
|
|
@@ -191,15 +209,20 @@ files:
|
|
|
191
209
|
- lib/roast/config_manager.rb
|
|
192
210
|
- lib/roast/control_flow.rb
|
|
193
211
|
- lib/roast/error.rb
|
|
212
|
+
- lib/roast/event.rb
|
|
213
|
+
- lib/roast/event_monitor.rb
|
|
194
214
|
- lib/roast/execution_context.rb
|
|
195
215
|
- lib/roast/execution_manager.rb
|
|
196
216
|
- lib/roast/log.rb
|
|
217
|
+
- lib/roast/log_formatter.rb
|
|
197
218
|
- lib/roast/nil_assertions.rb
|
|
219
|
+
- lib/roast/output_router.rb
|
|
198
220
|
- lib/roast/system_cog.rb
|
|
199
221
|
- lib/roast/system_cog/params.rb
|
|
200
222
|
- lib/roast/system_cogs/call.rb
|
|
201
223
|
- lib/roast/system_cogs/map.rb
|
|
202
224
|
- lib/roast/system_cogs/repeat.rb
|
|
225
|
+
- lib/roast/task_context.rb
|
|
203
226
|
- lib/roast/version.rb
|
|
204
227
|
- lib/roast/workflow.rb
|
|
205
228
|
- lib/roast/workflow_context.rb
|