riffer-rig 0.10.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 +8 -0
- data/README.md +15 -1
- data/lib/riffer/rig/cli/flags.rb +18 -5
- data/lib/riffer/rig/cli.rb +63 -17
- data/lib/riffer/rig/headless/host.rb +47 -0
- data/lib/riffer/rig/headless.rb +142 -0
- data/lib/riffer/rig/loader.rb +7 -9
- 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/version.rb +1 -1
- data/sig/generated/riffer/rig/cli/flags.rbs +10 -3
- data/sig/generated/riffer/rig/cli.rbs +26 -1
- 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 +4 -4
- 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/manual/riffer/rig/stores.rbs +7 -5
- 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,13 @@
|
|
|
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
|
+
|
|
3
11
|
## [0.10.0](https://github.com/bottrall/riffer-rig/compare/v0.9.0...v0.10.0) (2026-10-05)
|
|
4
12
|
|
|
5
13
|
|
data/README.md
CHANGED
|
@@ -51,7 +51,21 @@ The model is the first of these that is set: `--model`, `RIFFER_MODEL`, `model`
|
|
|
51
51
|
| `-r ID`, `--resume ID` | resumes the session with id `ID` ([Sessions](docs/SESSIONS.md#resuming)) |
|
|
52
52
|
| `-h`, `--help` | prints the usage |
|
|
53
53
|
|
|
54
|
-
`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).
|
|
55
69
|
|
|
56
70
|
### In the session
|
|
57
71
|
|
data/lib/riffer/rig/cli/flags.rb
CHANGED
|
@@ -5,25 +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, continue, resume
|
|
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?
|
|
18
19
|
attr_reader :continue #: bool
|
|
19
20
|
attr_reader :resume #: String?
|
|
20
21
|
|
|
21
22
|
# @rbs argv: Array[String]
|
|
23
|
+
# @rbs prompt: bool
|
|
22
24
|
# @rbs return: Riffer::Rig::CLI::Flags | String
|
|
23
|
-
def self.parse(argv)
|
|
25
|
+
def self.parse(argv, prompt: false)
|
|
24
26
|
values = {} #: Hash[Symbol, untyped]
|
|
25
27
|
rest = parser.parse(argv, into: values)
|
|
26
|
-
|
|
28
|
+
allowed = rest.empty? || (prompt && rest.size == 1)
|
|
29
|
+
return "unexpected argument: #{rest.join(' ')}" unless allowed
|
|
27
30
|
|
|
28
31
|
new(
|
|
29
32
|
model: values[:model],
|
|
@@ -33,6 +36,8 @@ class Riffer::Rig::CLI::Flags
|
|
|
33
36
|
tools: values[:tools],
|
|
34
37
|
max_steps: values[:'max-steps'],
|
|
35
38
|
save: !values.key?(:'no-save'),
|
|
39
|
+
verbose: values.key?(:verbose),
|
|
40
|
+
prompt: prompt ? rest.first : nil,
|
|
36
41
|
continue: values.key?(:continue),
|
|
37
42
|
resume: values[:resume],
|
|
38
43
|
help: values.key?(:help)
|
|
@@ -60,6 +65,8 @@ class Riffer::Rig::CLI::Flags
|
|
|
60
65
|
parser.on('--no-save', 'Do not save this session')
|
|
61
66
|
parser.on('-c', '--continue', 'Continue the most recent session in this directory')
|
|
62
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')
|
|
63
70
|
parser.on('-h', '--help', 'Show this help')
|
|
64
71
|
end
|
|
65
72
|
end
|
|
@@ -72,6 +79,8 @@ class Riffer::Rig::CLI::Flags
|
|
|
72
79
|
# @rbs tools: Array[String]?
|
|
73
80
|
# @rbs max_steps: Integer?
|
|
74
81
|
# @rbs save: bool
|
|
82
|
+
# @rbs verbose: bool
|
|
83
|
+
# @rbs prompt: String?
|
|
75
84
|
# @rbs continue: bool
|
|
76
85
|
# @rbs resume: String?
|
|
77
86
|
# @rbs help: bool
|
|
@@ -84,6 +93,8 @@ class Riffer::Rig::CLI::Flags
|
|
|
84
93
|
tools: nil,
|
|
85
94
|
max_steps: nil,
|
|
86
95
|
save: true,
|
|
96
|
+
verbose: false,
|
|
97
|
+
prompt: nil,
|
|
87
98
|
continue: false,
|
|
88
99
|
resume: nil,
|
|
89
100
|
help: false
|
|
@@ -95,6 +106,8 @@ class Riffer::Rig::CLI::Flags
|
|
|
95
106
|
@tools = tools&.freeze
|
|
96
107
|
@max_steps = max_steps
|
|
97
108
|
@save = save
|
|
109
|
+
@verbose = verbose
|
|
110
|
+
@prompt = prompt
|
|
98
111
|
@continue = continue
|
|
99
112
|
@resume = resume
|
|
100
113
|
@help = help
|
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,33 +78,53 @@ 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
|
-
loader =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
store: flags.save ? Riffer::Rig::Stores::JSONL.new : nil
|
|
61
|
-
)
|
|
62
|
-
resumed(flags, host, loader) || build(loader, flags)
|
|
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)
|
|
63
86
|
end
|
|
64
87
|
end
|
|
65
88
|
|
|
66
89
|
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
67
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
|
|
68
106
|
# @rbs loader: Riffer::Rig::Loader
|
|
69
107
|
# @rbs return: Riffer::Rig::Runtime?
|
|
70
|
-
def
|
|
108
|
+
def find_session(flags, loader)
|
|
71
109
|
if flags.resume
|
|
72
|
-
|
|
73
|
-
host.notify("No saved session #{flags.resume}.") unless started
|
|
74
|
-
started
|
|
110
|
+
loader.resume(flags.resume, **keywords(flags))
|
|
75
111
|
elsif flags.continue
|
|
76
|
-
|
|
77
|
-
host.notify('No saved session in this directory.') unless started
|
|
78
|
-
started
|
|
112
|
+
loader.continue(**keywords(flags))
|
|
79
113
|
end
|
|
80
114
|
end
|
|
81
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
|
+
|
|
82
128
|
# @rbs loader: Riffer::Rig::Loader
|
|
83
129
|
# @rbs flags: Riffer::Rig::CLI::Flags
|
|
84
130
|
# @rbs return: Riffer::Rig::Runtime
|
|
@@ -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
|
data/lib/riffer/rig/loader.rb
CHANGED
|
@@ -176,11 +176,10 @@ class Riffer::Rig::Loader
|
|
|
176
176
|
store = @store
|
|
177
177
|
return nil unless store
|
|
178
178
|
|
|
179
|
-
latest = store.list(cwd: @cwd).max_by { |header|
|
|
179
|
+
latest = store.list(cwd: @cwd).max_by { |header| header.updated || Time.at(0) }
|
|
180
180
|
return nil unless latest
|
|
181
181
|
|
|
182
|
-
id
|
|
183
|
-
resume(id, model:, extensions:, skills:, agents_md:, tools:, max_steps:)
|
|
182
|
+
resume(latest.id, model:, extensions:, skills:, agents_md:, tools:, max_steps:)
|
|
184
183
|
end
|
|
185
184
|
|
|
186
185
|
# A resume carries the history over and nothing else: the settings,
|
|
@@ -212,7 +211,7 @@ class Riffer::Rig::Loader
|
|
|
212
211
|
end
|
|
213
212
|
|
|
214
213
|
# @rbs all: bool
|
|
215
|
-
# @rbs return: Array[
|
|
214
|
+
# @rbs return: Array[::Riffer::Rig::Stores::Header]
|
|
216
215
|
def list(all: false)
|
|
217
216
|
store = @store
|
|
218
217
|
return [] unless store
|
|
@@ -563,16 +562,15 @@ class Riffer::Rig::Loader
|
|
|
563
562
|
File.join(@home, '.riffer', 'settings.json')
|
|
564
563
|
end
|
|
565
564
|
|
|
566
|
-
# @rbs entries: Array[
|
|
565
|
+
# @rbs entries: Array[Riffer::Rig::Stores::entry]
|
|
567
566
|
# @rbs id: String
|
|
568
567
|
# @rbs return: Hash[Symbol, untyped]
|
|
569
568
|
def snapshot_of(entries, id)
|
|
570
|
-
models = entries.select { |entry| entry[:type] == 'model' }
|
|
571
569
|
{
|
|
572
570
|
id: id,
|
|
573
|
-
messages: entries.
|
|
574
|
-
model:
|
|
575
|
-
skills: entries.
|
|
571
|
+
messages: entries.filter_map { |entry| entry.message if entry.is_a?(Riffer::Rig::Stores::MessageEntry) },
|
|
572
|
+
model: entries.filter_map { |entry| entry.model if entry.is_a?(Riffer::Rig::Stores::ModelEntry) }.last,
|
|
573
|
+
skills: entries.filter_map { |entry| entry.skill if entry.is_a?(Riffer::Rig::Stores::SkillEntry) }
|
|
576
574
|
}
|
|
577
575
|
end
|
|
578
576
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Stores::Header
|
|
4
|
+
TYPE = 'header' #: String
|
|
5
|
+
|
|
6
|
+
include Riffer::Rig::Support::Equatable
|
|
7
|
+
|
|
8
|
+
# @rbs @schema_version: Integer
|
|
9
|
+
# @rbs @id: String
|
|
10
|
+
# @rbs @cwd: String
|
|
11
|
+
# @rbs @created_at: String
|
|
12
|
+
# @rbs @model: String
|
|
13
|
+
# @rbs @riffer_rig_version: String
|
|
14
|
+
# @rbs @riffer_version: String
|
|
15
|
+
# @rbs @title: String
|
|
16
|
+
# @rbs @updated: Time?
|
|
17
|
+
|
|
18
|
+
# @dynamic schema_version, id, cwd, created_at, model, riffer_rig_version, riffer_version, title, updated
|
|
19
|
+
attr_reader :schema_version #: Integer
|
|
20
|
+
attr_reader :id #: String
|
|
21
|
+
attr_reader :cwd #: String
|
|
22
|
+
attr_reader :created_at #: String
|
|
23
|
+
attr_reader :model #: String
|
|
24
|
+
attr_reader :riffer_rig_version #: String
|
|
25
|
+
attr_reader :riffer_version #: String
|
|
26
|
+
attr_reader :title #: String
|
|
27
|
+
# Store-derived metadata, not entry data: stamped by the store while it
|
|
28
|
+
# holds the physical artifact (the JSONL file's mtime), so it is excluded
|
|
29
|
+
# from to_h.
|
|
30
|
+
attr_reader :updated #: Time?
|
|
31
|
+
|
|
32
|
+
# @rbs hash: Hash[Symbol, untyped]
|
|
33
|
+
# @rbs updated: Time?
|
|
34
|
+
# @rbs return: ::Riffer::Rig::Stores::Header?
|
|
35
|
+
def self.from_hash(hash, updated: nil)
|
|
36
|
+
schema_version = hash[:schema_version]
|
|
37
|
+
id = hash[:id]
|
|
38
|
+
cwd = hash[:cwd]
|
|
39
|
+
created_at = hash[:created_at]
|
|
40
|
+
model = hash[:model]
|
|
41
|
+
riffer_rig_version = hash[:riffer_rig_version]
|
|
42
|
+
riffer_version = hash[:riffer_version]
|
|
43
|
+
title = hash[:title]
|
|
44
|
+
return nil unless schema_version.is_a?(Integer)
|
|
45
|
+
return nil unless [id, cwd, created_at, model, riffer_rig_version, riffer_version, title].all?(String)
|
|
46
|
+
|
|
47
|
+
new(
|
|
48
|
+
schema_version: schema_version,
|
|
49
|
+
id: id,
|
|
50
|
+
cwd: cwd,
|
|
51
|
+
created_at: created_at,
|
|
52
|
+
model: model,
|
|
53
|
+
riffer_rig_version: riffer_rig_version,
|
|
54
|
+
riffer_version: riffer_version,
|
|
55
|
+
title: title,
|
|
56
|
+
updated: updated
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @rbs schema_version: Integer
|
|
61
|
+
# @rbs id: String
|
|
62
|
+
# @rbs cwd: String
|
|
63
|
+
# @rbs created_at: String
|
|
64
|
+
# @rbs model: String
|
|
65
|
+
# @rbs riffer_rig_version: String
|
|
66
|
+
# @rbs riffer_version: String
|
|
67
|
+
# @rbs title: String
|
|
68
|
+
# @rbs updated: Time?
|
|
69
|
+
# @rbs return: void
|
|
70
|
+
def initialize(
|
|
71
|
+
schema_version:,
|
|
72
|
+
id:,
|
|
73
|
+
cwd:,
|
|
74
|
+
created_at:,
|
|
75
|
+
model:,
|
|
76
|
+
riffer_rig_version:,
|
|
77
|
+
riffer_version:,
|
|
78
|
+
title:,
|
|
79
|
+
updated: nil
|
|
80
|
+
)
|
|
81
|
+
@schema_version = schema_version
|
|
82
|
+
@id = id
|
|
83
|
+
@cwd = cwd
|
|
84
|
+
@created_at = created_at
|
|
85
|
+
@model = model
|
|
86
|
+
@riffer_rig_version = riffer_rig_version
|
|
87
|
+
@riffer_version = riffer_version
|
|
88
|
+
@title = title
|
|
89
|
+
@updated = updated
|
|
90
|
+
freeze
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
94
|
+
def to_h
|
|
95
|
+
{
|
|
96
|
+
type: TYPE,
|
|
97
|
+
schema_version: @schema_version,
|
|
98
|
+
id: @id,
|
|
99
|
+
cwd: @cwd,
|
|
100
|
+
created_at: @created_at,
|
|
101
|
+
model: @model,
|
|
102
|
+
riffer_rig_version: @riffer_rig_version,
|
|
103
|
+
riffer_version: @riffer_version,
|
|
104
|
+
title: @title
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|