riffer-rig 0.9.0 → 0.11.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 +16 -0
- data/README.md +17 -1
- data/lib/riffer/rig/cli/flags.rb +30 -5
- data/lib/riffer/rig/cli.rb +92 -15
- data/lib/riffer/rig/headless/host.rb +47 -0
- data/lib/riffer/rig/headless.rb +142 -0
- data/lib/riffer/rig/loader.rb +147 -25
- data/lib/riffer/rig/runtime.rb +14 -0
- data/lib/riffer/rig/settings/document.rb +3 -1
- data/lib/riffer/rig/stores/header.rb +107 -0
- data/lib/riffer/rig/stores/jsonl.rb +43 -17
- 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 +16 -12
- data/lib/riffer/rig/stores/skill_entry.rb +31 -0
- data/lib/riffer/rig/version.rb +1 -1
- data/sig/generated/riffer/rig/cli/flags.rbs +16 -3
- data/sig/generated/riffer/rig/cli.rbs +40 -0
- data/sig/generated/riffer/rig/headless/host.rbs +34 -0
- data/sig/generated/riffer/rig/headless.rbs +69 -0
- data/sig/generated/riffer/rig/loader.rbs +68 -10
- data/sig/generated/riffer/rig/runtime.rbs +11 -2
- data/sig/generated/riffer/rig/settings/document.rbs +3 -1
- data/sig/generated/riffer/rig/stores/header.rbs +67 -0
- data/sig/generated/riffer/rig/stores/jsonl.rbs +22 -13
- 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 +12 -7
- data/sig/generated/riffer/rig/stores/skill_entry.rbs +21 -0
- data/sig/manual/riffer/rig/stores.rbs +7 -4
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f79d98c522224290937a01b701fa7f4ac72f2be08fbbb39abf32f06691880f7
|
|
4
|
+
data.tar.gz: 3c66dbd81e98e86b1c1132094431e0803d23c757a950684c6663ad5abd8c3369
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6087cb7527548ca826bf04ab3b214c36ab29e18c676c62f7df84377010bb47fddae9e88b1354d1fa8815efa4967d4599ccc63e022c57660a567b58e226554679
|
|
7
|
+
data.tar.gz: 2aae83bea0978c35b33674149527c0281baeb33f7bf186ba38d9b6abdeb905a3649989eae97430d7eae6284a80a253152d0532c8ba3d75f3ae3d7adee6f6a757
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0](https://github.com/bottrall/riffer-rig/compare/v0.10.0...v0.11.0) (2026-10-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* riffer -p runs one prompt headless with exit codes ([#226](https://github.com/bottrall/riffer-rig/issues/226)) ([eaa91d6](https://github.com/bottrall/riffer-rig/commit/eaa91d625eb303e4dc8a08c317ea38fc136a729d)), closes [#137](https://github.com/bottrall/riffer-rig/issues/137)
|
|
9
|
+
* store entries as typed POROs at the store boundary ([#225](https://github.com/bottrall/riffer-rig/issues/225)) ([05431e5](https://github.com/bottrall/riffer-rig/commit/05431e583c412ee7c3551dadd61db1089af465a9))
|
|
10
|
+
|
|
11
|
+
## [0.10.0](https://github.com/bottrall/riffer-rig/compare/v0.9.0...v0.10.0) (2026-10-05)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* continue and resume sessions with -c and -r ([#220](https://github.com/bottrall/riffer-rig/issues/220)) ([e43d3ac](https://github.com/bottrall/riffer-rig/commit/e43d3ac44678e56016791545ea241779569687c9))
|
|
17
|
+
* reload automatically before each request ([#222](https://github.com/bottrall/riffer-rig/issues/222)) ([fe96aa4](https://github.com/bottrall/riffer-rig/commit/fe96aa4476b0765e58824a8eb59301f0986e397c))
|
|
18
|
+
|
|
3
19
|
## [0.9.0](https://github.com/bottrall/riffer-rig/compare/v0.8.0...v0.9.0) (2026-10-04)
|
|
4
20
|
|
|
5
21
|
|
data/README.md
CHANGED
|
@@ -47,9 +47,25 @@ The model is the first of these that is set: `--model`, `RIFFER_MODEL`, `model`
|
|
|
47
47
|
| `--tools read,bash` | only these tools, by identifier ([Tools](docs/TOOLS.md)) |
|
|
48
48
|
| `--max-steps N` | stops a turn after `N` model calls |
|
|
49
49
|
| `--no-save` | does not save this session to the [store](docs/SESSIONS.md) |
|
|
50
|
+
| `-c`, `--continue` | continues the most recent session in this directory ([Sessions](docs/SESSIONS.md#resuming)) |
|
|
51
|
+
| `-r ID`, `--resume ID` | resumes the session with id `ID` ([Sessions](docs/SESSIONS.md#resuming)) |
|
|
50
52
|
| `-h`, `--help` | prints the usage |
|
|
51
53
|
|
|
52
|
-
`riffer
|
|
54
|
+
`riffer acp` (an editor's agent over stdio) is reserved: for now it prints the usage and exits with status 2.
|
|
55
|
+
|
|
56
|
+
### Headless
|
|
57
|
+
|
|
58
|
+
Run one prompt with no terminal ([Headless](docs/HEADLESS.md)):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
riffer -p "what does this repo do"
|
|
62
|
+
git diff | riffer -p "review this diff" # piped stdin appended as context
|
|
63
|
+
echo "explain this diff" | riffer -p # piped stdin alone is the prompt
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Everything loads exactly as the REPL does, the shared flags work the same — `-c` and `-r` included: `riffer -p -c "now run the tests"` continues the most recent session in this directory, and a missing session exits 2 ([Headless](docs/HEADLESS.md)). The session is saved like any other unless `--no-save`. Only the assistant's text is printed, streamed to stdout; errors and warnings go to stderr, and `--verbose` adds a one-line trace of each tool call there. Headless never prompts: a missing model, credential, or SDK prints the reason on stderr and exits 2.
|
|
67
|
+
|
|
68
|
+
The exit code says how the turn ended: `0` finished, `1` runtime error, `2` usage or configuration, `3` the turn ended without completing (`--max-steps`, a full context window, a provider content filter), `130` SIGINT (the turn is cancelled first).
|
|
53
69
|
|
|
54
70
|
### In the session
|
|
55
71
|
|
data/lib/riffer/rig/cli/flags.rb
CHANGED
|
@@ -5,23 +5,28 @@ 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
|
-
# @dynamic model, extensions, skills, agents_md, tools, max_steps, help, save
|
|
12
|
+
# @dynamic model, extensions, skills, agents_md, tools, max_steps, help, save, verbose, prompt, continue, resume
|
|
13
13
|
attr_reader :model #: String?
|
|
14
|
-
attr_reader :extensions, :skills, :agents_md, :help #: bool
|
|
14
|
+
attr_reader :extensions, :skills, :agents_md, :help, :verbose #: bool
|
|
15
15
|
attr_reader :tools #: Array[String]?
|
|
16
16
|
attr_reader :max_steps #: Integer?
|
|
17
17
|
attr_reader :save #: bool
|
|
18
|
+
attr_reader :prompt #: String?
|
|
19
|
+
attr_reader :continue #: bool
|
|
20
|
+
attr_reader :resume #: String?
|
|
18
21
|
|
|
19
22
|
# @rbs argv: Array[String]
|
|
23
|
+
# @rbs prompt: bool
|
|
20
24
|
# @rbs return: Riffer::Rig::CLI::Flags | String
|
|
21
|
-
def self.parse(argv)
|
|
25
|
+
def self.parse(argv, prompt: false)
|
|
22
26
|
values = {} #: Hash[Symbol, untyped]
|
|
23
27
|
rest = parser.parse(argv, into: values)
|
|
24
|
-
|
|
28
|
+
allowed = rest.empty? || (prompt && rest.size == 1)
|
|
29
|
+
return "unexpected argument: #{rest.join(' ')}" unless allowed
|
|
25
30
|
|
|
26
31
|
new(
|
|
27
32
|
model: values[:model],
|
|
@@ -31,6 +36,10 @@ class Riffer::Rig::CLI::Flags
|
|
|
31
36
|
tools: values[:tools],
|
|
32
37
|
max_steps: values[:'max-steps'],
|
|
33
38
|
save: !values.key?(:'no-save'),
|
|
39
|
+
verbose: values.key?(:verbose),
|
|
40
|
+
prompt: prompt ? rest.first : nil,
|
|
41
|
+
continue: values.key?(:continue),
|
|
42
|
+
resume: values[:resume],
|
|
34
43
|
help: values.key?(:help)
|
|
35
44
|
)
|
|
36
45
|
rescue OptionParser::ParseError => e
|
|
@@ -54,6 +63,10 @@ class Riffer::Rig::CLI::Flags
|
|
|
54
63
|
parser.on('--tools NAME,NAME', Array, 'Only these tools, by identifier')
|
|
55
64
|
parser.on('--max-steps N', Integer, 'Stop a turn after N model calls')
|
|
56
65
|
parser.on('--no-save', 'Do not save this session')
|
|
66
|
+
parser.on('-c', '--continue', 'Continue the most recent session in this directory')
|
|
67
|
+
parser.on('-r ID', '--resume ID', String, 'Resume the session with this id')
|
|
68
|
+
parser.on('-p', '--print', 'Run one prompt headless and exit; the prompt is the argument, else stdin')
|
|
69
|
+
parser.on('--verbose', 'With -p, trace each tool call to stderr')
|
|
57
70
|
parser.on('-h', '--help', 'Show this help')
|
|
58
71
|
end
|
|
59
72
|
end
|
|
@@ -66,6 +79,10 @@ class Riffer::Rig::CLI::Flags
|
|
|
66
79
|
# @rbs tools: Array[String]?
|
|
67
80
|
# @rbs max_steps: Integer?
|
|
68
81
|
# @rbs save: bool
|
|
82
|
+
# @rbs verbose: bool
|
|
83
|
+
# @rbs prompt: String?
|
|
84
|
+
# @rbs continue: bool
|
|
85
|
+
# @rbs resume: String?
|
|
69
86
|
# @rbs help: bool
|
|
70
87
|
# @rbs return: void
|
|
71
88
|
def initialize(
|
|
@@ -76,6 +93,10 @@ class Riffer::Rig::CLI::Flags
|
|
|
76
93
|
tools: nil,
|
|
77
94
|
max_steps: nil,
|
|
78
95
|
save: true,
|
|
96
|
+
verbose: false,
|
|
97
|
+
prompt: nil,
|
|
98
|
+
continue: false,
|
|
99
|
+
resume: nil,
|
|
79
100
|
help: false
|
|
80
101
|
)
|
|
81
102
|
@model = model
|
|
@@ -85,6 +106,10 @@ class Riffer::Rig::CLI::Flags
|
|
|
85
106
|
@tools = tools&.freeze
|
|
86
107
|
@max_steps = max_steps
|
|
87
108
|
@save = save
|
|
109
|
+
@verbose = verbose
|
|
110
|
+
@prompt = prompt
|
|
111
|
+
@continue = continue
|
|
112
|
+
@resume = resume
|
|
88
113
|
@help = help
|
|
89
114
|
freeze
|
|
90
115
|
end
|
data/lib/riffer/rig/cli.rb
CHANGED
|
@@ -27,17 +27,43 @@ module Riffer::Rig::CLI
|
|
|
27
27
|
home: Dir.home
|
|
28
28
|
)
|
|
29
29
|
return unavailable("riffer #{ACP_COMMAND}", error) if argv.first == ACP_COMMAND
|
|
30
|
-
return unavailable('riffer -p', error) if argv.intersect?(PRINT_FLAGS)
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
headless_mode = argv.intersect?(PRINT_FLAGS)
|
|
32
|
+
flags = Flags.parse(argv, prompt: headless_mode)
|
|
33
33
|
return refuse(flags, error) if flags.is_a?(String)
|
|
34
34
|
return help(output) if flags.help
|
|
35
|
+
return refuse('--verbose is only available with -p', error) if flags.verbose && !headless_mode
|
|
36
|
+
|
|
37
|
+
return headless(flags, input:, output:, error:, env:, cwd:, home:) if headless_mode
|
|
35
38
|
|
|
36
39
|
terminal(flags, input:, output:, env:, cwd:, home:)
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
private
|
|
40
43
|
|
|
44
|
+
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
45
|
+
# @rbs input: IO
|
|
46
|
+
# @rbs output: IO
|
|
47
|
+
# @rbs error: IO
|
|
48
|
+
# @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
|
|
49
|
+
# @rbs cwd: String
|
|
50
|
+
# @rbs home: String
|
|
51
|
+
# @rbs return: Integer
|
|
52
|
+
def headless(flags, input:, output:, error:, env:, cwd:, home:)
|
|
53
|
+
Riffer::Rig::Headless.for(
|
|
54
|
+
input: input,
|
|
55
|
+
output: output,
|
|
56
|
+
error: error,
|
|
57
|
+
verbose: flags.verbose
|
|
58
|
+
).run(prompt: flags.prompt) do |host|
|
|
59
|
+
loader = new_loader(flags, host: host, env: env, cwd: cwd, home: home)
|
|
60
|
+
started = find_session(flags, loader)
|
|
61
|
+
raise Riffer::Rig::Loader::ConfigurationError, missing_session(flags) if started.nil? && session_flags?(flags)
|
|
62
|
+
|
|
63
|
+
started || build(loader, flags)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
41
67
|
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
42
68
|
# @rbs input: IO
|
|
43
69
|
# @rbs output: IO
|
|
@@ -52,22 +78,73 @@ module Riffer::Rig::CLI
|
|
|
52
78
|
version: Riffer::Rig::VERSION,
|
|
53
79
|
no_color: env.is_a?(Riffer::Rig::Env) && env.no_color
|
|
54
80
|
).run do |host|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
loader = new_loader(flags, host: host, env: env, cwd: cwd, home: home)
|
|
82
|
+
started = find_session(flags, loader)
|
|
83
|
+
host.notify(missing_session(flags)) if started.nil? && session_flags?(flags)
|
|
84
|
+
|
|
85
|
+
started || build(loader, flags)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
90
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
91
|
+
# @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
|
|
92
|
+
# @rbs cwd: String
|
|
93
|
+
# @rbs home: String
|
|
94
|
+
# @rbs return: Riffer::Rig::Loader
|
|
95
|
+
def new_loader(flags, host:, env:, cwd:, home:)
|
|
96
|
+
Riffer::Rig::Loader.new(
|
|
97
|
+
cwd: cwd,
|
|
98
|
+
host: host,
|
|
99
|
+
env: env,
|
|
100
|
+
home: home,
|
|
101
|
+
store: flags.save ? Riffer::Rig::Stores::JSONL.new : nil
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
106
|
+
# @rbs loader: Riffer::Rig::Loader
|
|
107
|
+
# @rbs return: Riffer::Rig::Runtime?
|
|
108
|
+
def find_session(flags, loader)
|
|
109
|
+
if flags.resume
|
|
110
|
+
loader.resume(flags.resume, **keywords(flags))
|
|
111
|
+
elsif flags.continue
|
|
112
|
+
loader.continue(**keywords(flags))
|
|
68
113
|
end
|
|
69
114
|
end
|
|
70
115
|
|
|
116
|
+
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
117
|
+
# @rbs return: bool
|
|
118
|
+
def session_flags?(flags)
|
|
119
|
+
flags.resume ? true : flags.continue
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
123
|
+
# @rbs return: String
|
|
124
|
+
def missing_session(flags)
|
|
125
|
+
flags.resume ? "No saved session #{flags.resume}." : 'No saved session in this directory.'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @rbs loader: Riffer::Rig::Loader
|
|
129
|
+
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
130
|
+
# @rbs return: Riffer::Rig::Runtime
|
|
131
|
+
def build(loader, flags)
|
|
132
|
+
loader.runtime(**keywords(flags))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
136
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
137
|
+
def keywords(flags)
|
|
138
|
+
{
|
|
139
|
+
model: flags.model,
|
|
140
|
+
extensions: flags.extensions,
|
|
141
|
+
skills: flags.skills,
|
|
142
|
+
agents_md: flags.agents_md,
|
|
143
|
+
tools: flags.tools,
|
|
144
|
+
max_steps: flags.max_steps
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
71
148
|
# @rbs name: String
|
|
72
149
|
# @rbs error: IO
|
|
73
150
|
# @rbs return: Integer
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
|
|
8
|
+
# @rbs error: IO
|
|
9
|
+
# @rbs return: void
|
|
10
|
+
def initialize(error: $stderr)
|
|
11
|
+
@error = error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @rbs return: Set[Symbol]
|
|
15
|
+
def capabilities
|
|
16
|
+
CAPABILITIES
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @rbs question: String?
|
|
20
|
+
# @rbs options: Array[String]?
|
|
21
|
+
# @rbs secret: bool
|
|
22
|
+
# @rbs return: String?
|
|
23
|
+
def ask(question = nil, options: nil, secret: false)
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @rbs question: String?
|
|
28
|
+
# @rbs return: bool
|
|
29
|
+
def confirm(question = nil)
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @rbs message: String?
|
|
34
|
+
# @rbs level: Symbol
|
|
35
|
+
# @rbs return: void
|
|
36
|
+
def notify(message = nil, level: :info)
|
|
37
|
+
@error.puts(message.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @rbs label: String?
|
|
41
|
+
# @rbs &block: ? () -> void
|
|
42
|
+
# @rbs return: void
|
|
43
|
+
def progress(label = nil, &block)
|
|
44
|
+
@error.puts(label) if label
|
|
45
|
+
block&.call
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
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 @streamed_text: bool
|
|
19
|
+
|
|
20
|
+
# @rbs input: IO
|
|
21
|
+
# @rbs output: IO
|
|
22
|
+
# @rbs error: IO
|
|
23
|
+
# @rbs verbose: bool
|
|
24
|
+
# @rbs return: Riffer::Rig::Headless
|
|
25
|
+
def self.for(input:, output:, error:, verbose: false)
|
|
26
|
+
host = Riffer::Rig::Headless::Host.new(error: error)
|
|
27
|
+
new(input: input, output: output, error: error, host: host, verbose: verbose)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @rbs input: IO
|
|
31
|
+
# @rbs output: IO
|
|
32
|
+
# @rbs error: IO
|
|
33
|
+
# @rbs host: Riffer::Rig::Headless::Host
|
|
34
|
+
# @rbs verbose: bool
|
|
35
|
+
# @rbs return: void
|
|
36
|
+
def initialize(input:, output:, error:, host:, verbose: false)
|
|
37
|
+
@input = input
|
|
38
|
+
@output = output
|
|
39
|
+
@error = error
|
|
40
|
+
@host = host
|
|
41
|
+
@verbose = verbose
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @rbs prompt: String?
|
|
45
|
+
# @rbs &build: (Riffer::Rig::Hosts::_Host) -> Riffer::Rig::Runtime
|
|
46
|
+
# @rbs return: Integer
|
|
47
|
+
def run(prompt:, &build)
|
|
48
|
+
text = prompt_text(prompt)
|
|
49
|
+
return usage_error if text.empty?
|
|
50
|
+
|
|
51
|
+
drive(yield(@host), text)
|
|
52
|
+
rescue Riffer::Rig::Loader::ConfigurationError => e
|
|
53
|
+
@error.puts("riffer: #{e.message}")
|
|
54
|
+
USAGE_ERROR
|
|
55
|
+
rescue Interrupt
|
|
56
|
+
130
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
@error.puts("riffer: #{e.message}")
|
|
59
|
+
1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# @rbs prompt: String?
|
|
65
|
+
# @rbs return: String
|
|
66
|
+
def prompt_text(prompt)
|
|
67
|
+
piped = @input.tty? ? '' : @input.read.to_s.strip
|
|
68
|
+
[prompt, piped].compact.reject(&:empty?).join("\n\n")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @rbs return: Integer
|
|
72
|
+
def usage_error
|
|
73
|
+
@error.puts('riffer: no prompt: pass one as an argument, or pipe it to stdin')
|
|
74
|
+
USAGE_ERROR
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
78
|
+
# @rbs text: String
|
|
79
|
+
# @rbs return: Integer
|
|
80
|
+
def drive(runtime, text)
|
|
81
|
+
turn(text, runtime)
|
|
82
|
+
ensure
|
|
83
|
+
runtime.close
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @rbs text: String
|
|
87
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
88
|
+
# @rbs return: Integer
|
|
89
|
+
def turn(text, runtime)
|
|
90
|
+
# @type var stop_reason: Symbol?
|
|
91
|
+
stop_reason = nil
|
|
92
|
+
# Runtime#cancel takes a Mutex, which Ruby refuses inside a trap handler,
|
|
93
|
+
# so the handler hands the cancel to a thread.
|
|
94
|
+
previous = Signal.trap('INT') { Thread.new { runtime.cancel } }
|
|
95
|
+
begin
|
|
96
|
+
runtime.prompt(text) do |event|
|
|
97
|
+
case event
|
|
98
|
+
when Riffer::Rig::Events::TurnEnd
|
|
99
|
+
stop_reason = event.stop_reason
|
|
100
|
+
else
|
|
101
|
+
handle(event)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
@error.puts("riffer: #{e.message}")
|
|
106
|
+
return 1
|
|
107
|
+
ensure
|
|
108
|
+
Signal.trap('INT', previous)
|
|
109
|
+
end
|
|
110
|
+
finish(stop_reason)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# @rbs event: ::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event
|
|
114
|
+
# @rbs return: void
|
|
115
|
+
def handle(event)
|
|
116
|
+
case event
|
|
117
|
+
when Riffer::StreamEvents::TextDelta
|
|
118
|
+
@output.write(event.content)
|
|
119
|
+
@streamed_text = true
|
|
120
|
+
when Riffer::StreamEvents::TextDone
|
|
121
|
+
# A provider that streams nothing hands over the whole text here; one
|
|
122
|
+
# that streamed deltas has already written it.
|
|
123
|
+
@output.write(event.content) if !@streamed_text && !event.content.empty?
|
|
124
|
+
@output.puts if @streamed_text || !event.content.empty?
|
|
125
|
+
@streamed_text = false
|
|
126
|
+
when Riffer::StreamEvents::ToolCallDone
|
|
127
|
+
@error.puts("tool_call #{event.name} #{event.arguments}") if @verbose
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @rbs stop_reason: Symbol?
|
|
132
|
+
# @rbs return: Integer
|
|
133
|
+
def finish(stop_reason)
|
|
134
|
+
return 0 if stop_reason == :completed
|
|
135
|
+
|
|
136
|
+
@error.puts("turn ended: #{stop_reason}") if stop_reason
|
|
137
|
+
return 130 if stop_reason == Riffer::Rig::Runtime::INTERRUPT_CANCELLED
|
|
138
|
+
return 3 if stop_reason && STOPPED_WITHOUT_COMPLETING.include?(stop_reason)
|
|
139
|
+
|
|
140
|
+
1
|
|
141
|
+
end
|
|
142
|
+
end
|