riffer-rig 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +107 -30
- data/lib/riffer/rig/acp/host.rb +83 -0
- data/lib/riffer/rig/acp.rb +353 -0
- data/lib/riffer/rig/bundled/mcp.rb +6 -1
- data/lib/riffer/rig/cli/flags.rb +64 -18
- data/lib/riffer/rig/cli/sessions.rb +124 -0
- data/lib/riffer/rig/cli.rb +39 -52
- data/lib/riffer/rig/headless/host.rb +55 -0
- data/lib/riffer/rig/headless/ndjson.rb +49 -0
- data/lib/riffer/rig/headless.rb +157 -0
- data/lib/riffer/rig/loader.rb +39 -12
- data/lib/riffer/rig/mcp/auth.rb +59 -0
- data/lib/riffer/rig/mcp/declaration.rb +29 -5
- data/lib/riffer/rig/registrar.rb +11 -3
- data/lib/riffer/rig/runtime.rb +35 -1
- data/lib/riffer/rig/stores/header.rb +107 -0
- data/lib/riffer/rig/stores/jsonl.rb +37 -24
- data/lib/riffer/rig/stores/message_entry.rb +31 -0
- data/lib/riffer/rig/stores/model_entry.rb +31 -0
- data/lib/riffer/rig/stores/recorder.rb +8 -9
- data/lib/riffer/rig/stores/skill_entry.rb +31 -0
- data/lib/riffer/rig/terminal/picker.rb +144 -0
- data/lib/riffer/rig/terminal/session.rb +33 -0
- data/lib/riffer/rig/terminal.rb +113 -17
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +1 -1
- data/sig/generated/riffer/rig/acp/host.rbs +53 -0
- data/sig/generated/riffer/rig/acp.rbs +169 -0
- data/sig/generated/riffer/rig/cli/flags.rbs +31 -3
- data/sig/generated/riffer/rig/cli/sessions.rbs +72 -0
- data/sig/generated/riffer/rig/cli.rbs +14 -17
- data/sig/generated/riffer/rig/headless/host.rbs +37 -0
- data/sig/generated/riffer/rig/headless/ndjson.rbs +27 -0
- data/sig/generated/riffer/rig/headless.rbs +78 -0
- data/sig/generated/riffer/rig/loader.rbs +19 -6
- data/sig/generated/riffer/rig/mcp/auth.rbs +32 -0
- data/sig/generated/riffer/rig/mcp/declaration.rbs +23 -4
- data/sig/generated/riffer/rig/registrar.rbs +6 -2
- data/sig/generated/riffer/rig/runtime.rbs +16 -3
- data/sig/generated/riffer/rig/stores/header.rbs +67 -0
- data/sig/generated/riffer/rig/stores/jsonl.rbs +21 -17
- data/sig/generated/riffer/rig/stores/message_entry.rbs +21 -0
- data/sig/generated/riffer/rig/stores/model_entry.rbs +21 -0
- data/sig/generated/riffer/rig/stores/recorder.rbs +5 -5
- data/sig/generated/riffer/rig/stores/skill_entry.rbs +21 -0
- data/sig/generated/riffer/rig/terminal/picker.rbs +66 -0
- data/sig/generated/riffer/rig/terminal/session.rbs +32 -0
- data/sig/generated/riffer/rig/terminal.rbs +52 -8
- data/sig/manual/riffer/rig/events.rbs +1 -0
- data/sig/manual/riffer/rig/headless/ndjson.rbs +6 -0
- data/sig/manual/riffer/rig/headless.rbs +7 -0
- data/sig/manual/riffer/rig/mcp/auth.rbs +6 -0
- data/sig/manual/riffer/rig/stores.rbs +7 -5
- data/sig/manual/riffer/rig/terminal.rbs +12 -0
- metadata +45 -1
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
module Riffer::Rig::Bundled
|
|
4
4
|
Mcp = Riffer::Rig::Extension.new('mcp') do |rig|
|
|
5
5
|
rig.settings[:servers].to_h.each do |name, server|
|
|
6
|
-
rig.mcp(
|
|
6
|
+
rig.mcp(
|
|
7
|
+
name.to_s,
|
|
8
|
+
url: server.fetch(:url),
|
|
9
|
+
headers: server[:headers].to_h.transform_keys(&:to_s),
|
|
10
|
+
auth: Hash(server[:auth])
|
|
11
|
+
)
|
|
7
12
|
end
|
|
8
13
|
end #: Riffer::Rig::Extension
|
|
9
14
|
end
|
data/lib/riffer/rig/cli/flags.rb
CHANGED
|
@@ -5,38 +5,43 @@ require 'optparse'
|
|
|
5
5
|
class Riffer::Rig::CLI::Flags
|
|
6
6
|
BANNER = <<~TEXT.chomp #: String
|
|
7
7
|
Usage: riffer [options]
|
|
8
|
-
riffer -p [prompt]
|
|
8
|
+
riffer -p [prompt] run one prompt headless and exit
|
|
9
9
|
riffer acp (not available yet)
|
|
10
10
|
TEXT
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# The tokens OptionParser reports a missing argument for when -r is bare.
|
|
13
|
+
BARE_RESUME = %w[-r --resume].freeze #: Array[String]
|
|
14
|
+
|
|
15
|
+
# @dynamic model, extensions, skills, agents_md, tools, max_steps, help, save, verbose, json, prompt, continue, resume
|
|
13
16
|
attr_reader :model #: String?
|
|
14
|
-
attr_reader :extensions, :skills, :agents_md, :help #: bool
|
|
17
|
+
attr_reader :extensions, :skills, :agents_md, :help, :verbose, :json #: bool
|
|
15
18
|
attr_reader :tools #: Array[String]?
|
|
16
19
|
attr_reader :max_steps #: Integer?
|
|
17
20
|
attr_reader :save #: bool
|
|
21
|
+
attr_reader :prompt #: String?
|
|
18
22
|
attr_reader :continue #: bool
|
|
19
23
|
attr_reader :resume #: String?
|
|
20
24
|
|
|
25
|
+
# A bare -r (no id) only reaches here for the REPL: the parser requires the
|
|
26
|
+
# id, and the missing-argument error is translated to an empty resume so the
|
|
27
|
+
# terminal can open the picker instead. Headless keeps the error and prints
|
|
28
|
+
# it as the usage failure.
|
|
29
|
+
#
|
|
21
30
|
# @rbs argv: Array[String]
|
|
31
|
+
# @rbs prompt: bool
|
|
22
32
|
# @rbs return: Riffer::Rig::CLI::Flags | String
|
|
23
|
-
def self.parse(argv)
|
|
33
|
+
def self.parse(argv, prompt: false)
|
|
24
34
|
values = {} #: Hash[Symbol, untyped]
|
|
25
35
|
rest = parser.parse(argv, into: values)
|
|
26
|
-
|
|
36
|
+
allowed = rest.empty? || (prompt && rest.size == 1)
|
|
37
|
+
return "unexpected argument: #{rest.join(' ')}" unless allowed
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
max_steps: values[:'max-steps'],
|
|
35
|
-
save: !values.key?(:'no-save'),
|
|
36
|
-
continue: values.key?(:continue),
|
|
37
|
-
resume: values[:resume],
|
|
38
|
-
help: values.key?(:help)
|
|
39
|
-
)
|
|
39
|
+
assemble(values, rest.first, prompt: prompt)
|
|
40
|
+
rescue OptionParser::MissingArgument => e
|
|
41
|
+
bare = !prompt && BARE_RESUME.include?(e.args.first.to_s)
|
|
42
|
+
return assemble(values || {}, nil, prompt: prompt, bare_resume: bare) if bare
|
|
43
|
+
|
|
44
|
+
e.message
|
|
40
45
|
rescue OptionParser::ParseError => e
|
|
41
46
|
e.message
|
|
42
47
|
end
|
|
@@ -46,6 +51,11 @@ class Riffer::Rig::CLI::Flags
|
|
|
46
51
|
parser.help
|
|
47
52
|
end
|
|
48
53
|
|
|
54
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
55
|
+
def keywords
|
|
56
|
+
{ model: model, extensions: extensions, skills: skills, agents_md: agents_md, tools: tools, max_steps: max_steps }
|
|
57
|
+
end
|
|
58
|
+
|
|
49
59
|
# @rbs return: OptionParser
|
|
50
60
|
def self.parser
|
|
51
61
|
OptionParser.new do |parser|
|
|
@@ -59,12 +69,39 @@ class Riffer::Rig::CLI::Flags
|
|
|
59
69
|
parser.on('--max-steps N', Integer, 'Stop a turn after N model calls')
|
|
60
70
|
parser.on('--no-save', 'Do not save this session')
|
|
61
71
|
parser.on('-c', '--continue', 'Continue the most recent session in this directory')
|
|
62
|
-
parser.on('-r ID', '--resume ID', String, 'Resume the session with this id')
|
|
72
|
+
parser.on('-r ID', '--resume ID', String, 'Resume the session with this id; bare -r opens the picker in the REPL')
|
|
73
|
+
parser.on('-p', '--print', 'Run one prompt headless and exit; the prompt is the argument, else stdin')
|
|
74
|
+
parser.on('--verbose', 'With -p, trace each tool call to stderr')
|
|
75
|
+
parser.on('--json', 'With -p, stream every Runtime event as NDJSON to stdout')
|
|
63
76
|
parser.on('-h', '--help', 'Show this help')
|
|
64
77
|
end
|
|
65
78
|
end
|
|
66
79
|
private_class_method :parser
|
|
67
80
|
|
|
81
|
+
# @rbs values: Hash[Symbol, untyped]
|
|
82
|
+
# @rbs head: String?
|
|
83
|
+
# @rbs prompt: bool
|
|
84
|
+
# @rbs bare_resume: bool
|
|
85
|
+
# @rbs return: Riffer::Rig::CLI::Flags
|
|
86
|
+
def self.assemble(values, head, prompt:, bare_resume: false)
|
|
87
|
+
new(
|
|
88
|
+
model: values[:model],
|
|
89
|
+
extensions: !values.key?(:'no-extensions'),
|
|
90
|
+
skills: !values.key?(:'no-skills'),
|
|
91
|
+
agents_md: !values.key?(:'no-agents-md'),
|
|
92
|
+
tools: values[:tools],
|
|
93
|
+
max_steps: values[:'max-steps'],
|
|
94
|
+
save: !values.key?(:'no-save'),
|
|
95
|
+
verbose: values.key?(:verbose),
|
|
96
|
+
json: values.key?(:json),
|
|
97
|
+
prompt: prompt ? head : nil,
|
|
98
|
+
continue: values.key?(:continue),
|
|
99
|
+
resume: bare_resume ? '' : values[:resume],
|
|
100
|
+
help: values.key?(:help)
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
private_class_method :assemble
|
|
104
|
+
|
|
68
105
|
# @rbs model: String?
|
|
69
106
|
# @rbs extensions: bool
|
|
70
107
|
# @rbs skills: bool
|
|
@@ -72,6 +109,9 @@ class Riffer::Rig::CLI::Flags
|
|
|
72
109
|
# @rbs tools: Array[String]?
|
|
73
110
|
# @rbs max_steps: Integer?
|
|
74
111
|
# @rbs save: bool
|
|
112
|
+
# @rbs verbose: bool
|
|
113
|
+
# @rbs json: bool
|
|
114
|
+
# @rbs prompt: String?
|
|
75
115
|
# @rbs continue: bool
|
|
76
116
|
# @rbs resume: String?
|
|
77
117
|
# @rbs help: bool
|
|
@@ -84,6 +124,9 @@ class Riffer::Rig::CLI::Flags
|
|
|
84
124
|
tools: nil,
|
|
85
125
|
max_steps: nil,
|
|
86
126
|
save: true,
|
|
127
|
+
verbose: false,
|
|
128
|
+
json: false,
|
|
129
|
+
prompt: nil,
|
|
87
130
|
continue: false,
|
|
88
131
|
resume: nil,
|
|
89
132
|
help: false
|
|
@@ -95,6 +138,9 @@ class Riffer::Rig::CLI::Flags
|
|
|
95
138
|
@tools = tools&.freeze
|
|
96
139
|
@max_steps = max_steps
|
|
97
140
|
@save = save
|
|
141
|
+
@verbose = verbose
|
|
142
|
+
@json = json
|
|
143
|
+
@prompt = prompt
|
|
98
144
|
@continue = continue
|
|
99
145
|
@resume = resume
|
|
100
146
|
@help = help
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::CLI::Sessions
|
|
4
|
+
# The terminal's sessions collaborator: translates the parsed flags into
|
|
5
|
+
# Loader builds and store lookups, so the host never sees the flags.
|
|
6
|
+
#
|
|
7
|
+
# @rbs @flags: Riffer::Rig::CLI::Flags
|
|
8
|
+
# @rbs @env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
|
|
9
|
+
# @rbs @cwd: String
|
|
10
|
+
# @rbs @home: String
|
|
11
|
+
# @rbs @store: Riffer::Rig::Stores::_Store?
|
|
12
|
+
# @rbs @loader: Riffer::Rig::Loader?
|
|
13
|
+
|
|
14
|
+
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
15
|
+
# @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
|
|
16
|
+
# @rbs cwd: String
|
|
17
|
+
# @rbs home: String
|
|
18
|
+
# @rbs return: void
|
|
19
|
+
def initialize(flags:, env:, cwd:, home:)
|
|
20
|
+
@flags = flags
|
|
21
|
+
@env = env
|
|
22
|
+
@cwd = cwd
|
|
23
|
+
@home = home
|
|
24
|
+
@store = flags.save ? Riffer::Rig::Stores::JSONL.new(home: home) : nil
|
|
25
|
+
@loader = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
29
|
+
# @rbs return: Riffer::Rig::Runtime
|
|
30
|
+
def start(host)
|
|
31
|
+
started = find(host)
|
|
32
|
+
host.notify(missing) if started.nil? && asked?
|
|
33
|
+
|
|
34
|
+
started || loader(host).runtime(**@flags.keywords)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
38
|
+
# @rbs return: Riffer::Rig::Runtime?
|
|
39
|
+
def find(host)
|
|
40
|
+
id = resume_id
|
|
41
|
+
if id
|
|
42
|
+
loader(host).resume(id, **@flags.keywords)
|
|
43
|
+
elsif @flags.continue
|
|
44
|
+
loader(host).continue(**@flags.keywords)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
49
|
+
# @rbs return: Riffer::Rig::Runtime
|
|
50
|
+
def fresh(host)
|
|
51
|
+
loader(host).runtime(**@flags.keywords)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
55
|
+
# @rbs id: String
|
|
56
|
+
# @rbs return: Riffer::Rig::Runtime?
|
|
57
|
+
def resume(host, id)
|
|
58
|
+
loader(host).resume(id, **@flags.keywords)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
62
|
+
# @rbs all: bool
|
|
63
|
+
# @rbs return: Array[::Riffer::Rig::Terminal::Session]
|
|
64
|
+
def list(host, all: false)
|
|
65
|
+
loader(host)
|
|
66
|
+
.list(all: all)
|
|
67
|
+
# The id breaks mtime ties so the rows never shuffle under sort_by.
|
|
68
|
+
.sort_by { |header| [header.updated ? -header.updated.to_f : 0.0, header.id] }
|
|
69
|
+
.map { |header| row(header) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
73
|
+
# @rbs id: String
|
|
74
|
+
# @rbs return: void
|
|
75
|
+
def delete(host, id)
|
|
76
|
+
loader(host).delete(id)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @rbs return: bool
|
|
80
|
+
def asked?
|
|
81
|
+
resume_id ? true : @flags.continue
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @rbs return: String
|
|
85
|
+
def missing
|
|
86
|
+
id = resume_id
|
|
87
|
+
id ? "No saved session #{id}." : 'No saved session in this directory.'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# @rbs header: Riffer::Rig::Stores::Header
|
|
93
|
+
# @rbs return: ::Riffer::Rig::Terminal::Session
|
|
94
|
+
def row(header)
|
|
95
|
+
Riffer::Rig::Terminal::Session.new(
|
|
96
|
+
id: header.id,
|
|
97
|
+
title: header.title.gsub(/\s+/, ' ').strip,
|
|
98
|
+
updated: header.updated,
|
|
99
|
+
messages: message_count(header.id),
|
|
100
|
+
cwd: header.cwd
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @rbs id: String
|
|
105
|
+
# @rbs return: Integer
|
|
106
|
+
def message_count(id)
|
|
107
|
+
store = @store
|
|
108
|
+
return 0 unless store
|
|
109
|
+
|
|
110
|
+
store.read(id).count { |entry| entry.is_a?(Riffer::Rig::Stores::MessageEntry) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
114
|
+
# @rbs return: Riffer::Rig::Loader
|
|
115
|
+
def loader(host)
|
|
116
|
+
@loader ||= Riffer::Rig::Loader.new(cwd: @cwd, host: host, env: @env, home: @home, store: @store)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @rbs return: String?
|
|
120
|
+
def resume_id
|
|
121
|
+
id = @flags.resume
|
|
122
|
+
id unless id.nil? || id.empty?
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/riffer/rig/cli.rb
CHANGED
|
@@ -26,12 +26,17 @@ module Riffer::Rig::CLI
|
|
|
26
26
|
cwd: Dir.pwd,
|
|
27
27
|
home: Dir.home
|
|
28
28
|
)
|
|
29
|
-
return
|
|
30
|
-
return
|
|
29
|
+
return refuse("#{ACP_COMMAND} takes no arguments", error) if argv.first == ACP_COMMAND && argv.length > 1
|
|
30
|
+
return acp(input: input, output: output, env: env, home: home) if argv.first == ACP_COMMAND
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
headless_mode = argv.intersect?(PRINT_FLAGS)
|
|
33
|
+
flags = Flags.parse(argv, prompt: headless_mode)
|
|
33
34
|
return refuse(flags, error) if flags.is_a?(String)
|
|
34
35
|
return help(output) if flags.help
|
|
36
|
+
return refuse('--verbose is only available with -p', error) if flags.verbose && !headless_mode
|
|
37
|
+
return refuse('--json is only available with -p', error) if flags.json && !headless_mode
|
|
38
|
+
|
|
39
|
+
return headless(flags, input:, output:, error:, env:, cwd:, home:) if headless_mode
|
|
35
40
|
|
|
36
41
|
terminal(flags, input:, output:, env:, cwd:, home:)
|
|
37
42
|
end
|
|
@@ -41,69 +46,51 @@ module Riffer::Rig::CLI
|
|
|
41
46
|
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
42
47
|
# @rbs input: IO
|
|
43
48
|
# @rbs output: IO
|
|
49
|
+
# @rbs error: IO
|
|
44
50
|
# @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
|
|
45
51
|
# @rbs cwd: String
|
|
46
52
|
# @rbs home: String
|
|
47
53
|
# @rbs return: Integer
|
|
48
|
-
def
|
|
49
|
-
|
|
54
|
+
def headless(flags, input:, output:, error:, env:, cwd:, home:)
|
|
55
|
+
sessions = Sessions.new(flags: flags, env: env, cwd: cwd, home: home)
|
|
56
|
+
Riffer::Rig::Headless.for(
|
|
50
57
|
input: input,
|
|
51
58
|
output: output,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
env: env,
|
|
59
|
-
home: home,
|
|
60
|
-
store: flags.save ? Riffer::Rig::Stores::JSONL.new : nil
|
|
61
|
-
)
|
|
62
|
-
resumed(flags, host, loader) || build(loader, flags)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
59
|
+
error: error,
|
|
60
|
+
verbose: flags.verbose,
|
|
61
|
+
json: flags.json
|
|
62
|
+
).run(prompt: flags.prompt) do |host|
|
|
63
|
+
started = sessions.find(host)
|
|
64
|
+
raise Riffer::Rig::Loader::ConfigurationError, sessions.missing if started.nil? && sessions.asked?
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
68
|
-
# @rbs loader: Riffer::Rig::Loader
|
|
69
|
-
# @rbs return: Riffer::Rig::Runtime?
|
|
70
|
-
def resumed(flags, host, loader)
|
|
71
|
-
if flags.resume
|
|
72
|
-
started = loader.resume(flags.resume, **keywords(flags))
|
|
73
|
-
host.notify("No saved session #{flags.resume}.") unless started
|
|
74
|
-
started
|
|
75
|
-
elsif flags.continue
|
|
76
|
-
started = loader.continue(**keywords(flags))
|
|
77
|
-
host.notify('No saved session in this directory.') unless started
|
|
78
|
-
started
|
|
66
|
+
started || sessions.fresh(host)
|
|
79
67
|
end
|
|
80
68
|
end
|
|
81
69
|
|
|
82
|
-
# @rbs loader: Riffer::Rig::Loader
|
|
83
|
-
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
84
|
-
# @rbs return: Riffer::Rig::Runtime
|
|
85
|
-
def build(loader, flags)
|
|
86
|
-
loader.runtime(**keywords(flags))
|
|
87
|
-
end
|
|
88
|
-
|
|
89
70
|
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
90
|
-
# @rbs
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
71
|
+
# @rbs input: IO
|
|
72
|
+
# @rbs output: IO
|
|
73
|
+
# @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
|
|
74
|
+
# @rbs cwd: String
|
|
75
|
+
# @rbs home: String
|
|
76
|
+
# @rbs return: Integer
|
|
77
|
+
def terminal(flags, input:, output:, env:, cwd:, home:)
|
|
78
|
+
Riffer::Rig::Terminal.for(
|
|
79
|
+
input: input,
|
|
80
|
+
output: output,
|
|
81
|
+
version: Riffer::Rig::VERSION,
|
|
82
|
+
no_color: env.is_a?(Riffer::Rig::Env) && env.no_color,
|
|
83
|
+
sessions: Sessions.new(flags: flags, env: env, cwd: cwd, home: home)
|
|
84
|
+
).run(open_picker: flags.resume == '')
|
|
100
85
|
end
|
|
101
86
|
|
|
102
|
-
# @rbs
|
|
103
|
-
# @rbs
|
|
87
|
+
# @rbs input: IO
|
|
88
|
+
# @rbs output: IO
|
|
89
|
+
# @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
|
|
90
|
+
# @rbs home: String
|
|
104
91
|
# @rbs return: Integer
|
|
105
|
-
def
|
|
106
|
-
|
|
92
|
+
def acp(input:, output:, env:, home:)
|
|
93
|
+
Riffer::Rig::ACP.for(input: input, output: output, env: env, home: home).run
|
|
107
94
|
end
|
|
108
95
|
|
|
109
96
|
# @rbs message: String
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Headless::Host
|
|
4
|
+
CAPABILITIES = Set[:notify, :progress].freeze #: Set[Symbol]
|
|
5
|
+
|
|
6
|
+
# @rbs @error: IO
|
|
7
|
+
# @rbs @json: bool
|
|
8
|
+
|
|
9
|
+
# @rbs error: IO
|
|
10
|
+
# @rbs json: bool
|
|
11
|
+
# @rbs return: void
|
|
12
|
+
def initialize(error: $stderr, json: false)
|
|
13
|
+
@error = error
|
|
14
|
+
@json = json
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs return: Set[Symbol]
|
|
18
|
+
def capabilities
|
|
19
|
+
CAPABILITIES
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs question: String?
|
|
23
|
+
# @rbs options: Array[String]?
|
|
24
|
+
# @rbs secret: bool
|
|
25
|
+
# @rbs return: String?
|
|
26
|
+
def ask(question = nil, options: nil, secret: false)
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @rbs question: String?
|
|
31
|
+
# @rbs return: bool
|
|
32
|
+
def confirm(question = nil)
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @rbs message: String?
|
|
37
|
+
# @rbs level: Symbol
|
|
38
|
+
# @rbs return: void
|
|
39
|
+
def notify(message = nil, level: :info)
|
|
40
|
+
# In --json the Mirror queues the Notify event onto the Runtime's stream,
|
|
41
|
+
# where the printer emits it as a line; printing here would duplicate it
|
|
42
|
+
# on stderr.
|
|
43
|
+
return if @json
|
|
44
|
+
|
|
45
|
+
@error.puts(message.to_s)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs label: String?
|
|
49
|
+
# @rbs &block: ? () -> void
|
|
50
|
+
# @rbs return: void
|
|
51
|
+
def progress(label = nil, &block)
|
|
52
|
+
@error.puts(label) if label
|
|
53
|
+
block&.call
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Riffer::Rig::Headless::Ndjson
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# @rbs event: ::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event
|
|
9
|
+
# @rbs return: String
|
|
10
|
+
def line(event)
|
|
11
|
+
"#{JSON.generate(envelope(event))}\n"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @rbs message: String
|
|
15
|
+
# @rbs return: String
|
|
16
|
+
def error(message)
|
|
17
|
+
"#{JSON.generate({ 'type' => 'error', 'message' => message })}\n"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs event: ::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event
|
|
21
|
+
# @rbs return: Hash[String, Riffer::Rig::Headless::json]
|
|
22
|
+
def envelope(event)
|
|
23
|
+
json_hash(event.to_h).merge('type' => event_type(event))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @rbs hash: Hash[Symbol, untyped]
|
|
27
|
+
# @rbs return: Hash[String, Riffer::Rig::Headless::json]
|
|
28
|
+
def json_hash(hash)
|
|
29
|
+
hash.to_h { |key, item| [key.to_s, json_value(item)] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @rbs event: ::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event
|
|
33
|
+
# @rbs return: String
|
|
34
|
+
def event_type(event)
|
|
35
|
+
event.class.name.to_s.split('::').last.to_s.gsub(/(?<=[a-z\d])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/, '_').downcase
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @rbs value: untyped
|
|
39
|
+
# @rbs return: Riffer::Rig::Headless::json
|
|
40
|
+
def json_value(value)
|
|
41
|
+
case value
|
|
42
|
+
when String, Integer, Float, true, false, nil then value
|
|
43
|
+
when Symbol then value.to_s
|
|
44
|
+
when Hash then value.to_h { |key, item| [key.to_s, json_value(item)] }
|
|
45
|
+
when Array then value.map { |item| json_value(item) }
|
|
46
|
+
else value.respond_to?(:to_h) ? json_value(value.to_h) : value.to_s
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Headless
|
|
4
|
+
USAGE_ERROR = 2 #: Integer
|
|
5
|
+
|
|
6
|
+
# The plan's exit-3 set: the cap plus the provider stop reasons that mean the
|
|
7
|
+
# turn ended without completing. Riffer's remaining stop reasons (:error,
|
|
8
|
+
# :other, :guardrail_blocked) are runtime failures and map to 1.
|
|
9
|
+
STOPPED_WITHOUT_COMPLETING = %i[
|
|
10
|
+
max_steps context_window length content_filter malformed_output
|
|
11
|
+
].freeze #: Array[Symbol]
|
|
12
|
+
|
|
13
|
+
# @rbs @input: IO
|
|
14
|
+
# @rbs @output: IO
|
|
15
|
+
# @rbs @error: IO
|
|
16
|
+
# @rbs @host: Riffer::Rig::Headless::Host
|
|
17
|
+
# @rbs @verbose: bool
|
|
18
|
+
# @rbs @json: bool
|
|
19
|
+
# @rbs @streamed_text: bool
|
|
20
|
+
|
|
21
|
+
# @rbs input: IO
|
|
22
|
+
# @rbs output: IO
|
|
23
|
+
# @rbs error: IO
|
|
24
|
+
# @rbs verbose: bool
|
|
25
|
+
# @rbs json: bool
|
|
26
|
+
# @rbs return: Riffer::Rig::Headless
|
|
27
|
+
def self.for(input:, output:, error:, verbose: false, json: false)
|
|
28
|
+
host = Riffer::Rig::Headless::Host.new(error: error, json: json)
|
|
29
|
+
new(input: input, output: output, error: error, host: host, verbose: verbose, json: json)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @rbs input: IO
|
|
33
|
+
# @rbs output: IO
|
|
34
|
+
# @rbs error: IO
|
|
35
|
+
# @rbs host: Riffer::Rig::Headless::Host
|
|
36
|
+
# @rbs verbose: bool
|
|
37
|
+
# @rbs json: bool
|
|
38
|
+
# @rbs return: void
|
|
39
|
+
def initialize(input:, output:, error:, host:, verbose: false, json: false)
|
|
40
|
+
@input = input
|
|
41
|
+
@output = output
|
|
42
|
+
@error = error
|
|
43
|
+
@host = host
|
|
44
|
+
@verbose = verbose
|
|
45
|
+
@json = json
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs prompt: String?
|
|
49
|
+
# @rbs &build: (Riffer::Rig::Hosts::_Host) -> Riffer::Rig::Runtime
|
|
50
|
+
# @rbs return: Integer
|
|
51
|
+
def run(prompt:, &build)
|
|
52
|
+
text = prompt_text(prompt)
|
|
53
|
+
return usage_error if text.empty?
|
|
54
|
+
|
|
55
|
+
drive(yield(@host), text)
|
|
56
|
+
rescue Riffer::Rig::Loader::ConfigurationError => e
|
|
57
|
+
fatal(e.message, USAGE_ERROR)
|
|
58
|
+
rescue Interrupt
|
|
59
|
+
130
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
fatal(e.message, 1)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# @rbs prompt: String?
|
|
67
|
+
# @rbs return: String
|
|
68
|
+
def prompt_text(prompt)
|
|
69
|
+
piped = @input.tty? ? '' : @input.read.to_s.strip
|
|
70
|
+
[prompt, piped].compact.reject(&:empty?).join("\n\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @rbs return: Integer
|
|
74
|
+
def usage_error
|
|
75
|
+
fatal('no prompt: pass one as an argument, or pipe it to stdin', USAGE_ERROR)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @rbs message: String
|
|
79
|
+
# @rbs code: Integer
|
|
80
|
+
# @rbs return: Integer
|
|
81
|
+
def fatal(message, code)
|
|
82
|
+
if @json
|
|
83
|
+
@output.write(Ndjson.error(message))
|
|
84
|
+
else
|
|
85
|
+
@error.puts("riffer: #{message}")
|
|
86
|
+
end
|
|
87
|
+
code
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
91
|
+
# @rbs text: String
|
|
92
|
+
# @rbs return: Integer
|
|
93
|
+
def drive(runtime, text)
|
|
94
|
+
turn(text, runtime)
|
|
95
|
+
ensure
|
|
96
|
+
runtime.close
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @rbs text: String
|
|
100
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
101
|
+
# @rbs return: Integer
|
|
102
|
+
def turn(text, runtime)
|
|
103
|
+
# @type var stop_reason: Symbol?
|
|
104
|
+
stop_reason = nil
|
|
105
|
+
# Runtime#cancel takes a Mutex, which Ruby refuses inside a trap handler,
|
|
106
|
+
# so the handler hands the cancel to a thread.
|
|
107
|
+
previous = Signal.trap('INT') { Thread.new { runtime.cancel } }
|
|
108
|
+
begin
|
|
109
|
+
runtime.prompt(text) do |event|
|
|
110
|
+
case event
|
|
111
|
+
when Riffer::Rig::Events::TurnEnd
|
|
112
|
+
stop_reason = event.stop_reason
|
|
113
|
+
@output.write(Ndjson.line(event)) if @json
|
|
114
|
+
else
|
|
115
|
+
handle(event)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
return fatal(e.message, 1)
|
|
120
|
+
ensure
|
|
121
|
+
Signal.trap('INT', previous)
|
|
122
|
+
end
|
|
123
|
+
finish(stop_reason)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @rbs event: ::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event
|
|
127
|
+
# @rbs return: void
|
|
128
|
+
def handle(event)
|
|
129
|
+
return @output.write(Ndjson.line(event)) if @json
|
|
130
|
+
|
|
131
|
+
case event
|
|
132
|
+
when Riffer::StreamEvents::TextDelta
|
|
133
|
+
@output.write(event.content)
|
|
134
|
+
@streamed_text = true
|
|
135
|
+
when Riffer::StreamEvents::TextDone
|
|
136
|
+
# A provider that streams nothing hands over the whole text here; one
|
|
137
|
+
# that streamed deltas has already written it.
|
|
138
|
+
@output.write(event.content) if !@streamed_text && !event.content.empty?
|
|
139
|
+
@output.puts if @streamed_text || !event.content.empty?
|
|
140
|
+
@streamed_text = false
|
|
141
|
+
when Riffer::StreamEvents::ToolCallDone
|
|
142
|
+
@error.puts("tool_call #{event.name} #{event.arguments}") if @verbose
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @rbs stop_reason: Symbol?
|
|
147
|
+
# @rbs return: Integer
|
|
148
|
+
def finish(stop_reason)
|
|
149
|
+
return 0 if stop_reason == :completed
|
|
150
|
+
|
|
151
|
+
@error.puts("turn ended: #{stop_reason}") if stop_reason
|
|
152
|
+
return 130 if stop_reason == Riffer::Rig::Runtime::INTERRUPT_CANCELLED
|
|
153
|
+
return 3 if stop_reason && STOPPED_WITHOUT_COMPLETING.include?(stop_reason)
|
|
154
|
+
|
|
155
|
+
1
|
|
156
|
+
end
|
|
157
|
+
end
|