riffer-rig 0.9.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d49bc7e8176b7eec0bc8cc2249bc39668f9637557db92bdd34c1f4baf9ee7ce0
4
- data.tar.gz: c9d563f1ff2de6cbece63dc6735107537bc33fd1b8cefde8541e7e2fc3262f3a
3
+ metadata.gz: 0bc260756e56f8ed0d89556ef071a3ecb1922b637eb5b10cc0268416e0ceb26d
4
+ data.tar.gz: b7e6d6314a893ff23f289336b5e3df25f11e032f060272d7656695f1e96e584d
5
5
  SHA512:
6
- metadata.gz: 7b89243587acc30185f2a0f16ebc5adfb43c860f537ec5f1cab6b7fae116322109f74f49b6e990c7d4d0b48dab1a8db3ec61f804c84b69bb06f3fa14cba2f0d7
7
- data.tar.gz: cdb0fdd4d2354fd7ed1625c022e6f0fff02f2a34b3f37babfdf1a443a57427e45bb88fab58edf07cd3cfc9e1ef14ad492589ca847df7ff9339c0695728d21290
6
+ metadata.gz: edda5ec3d1675966361db52a009328703f0f62340d8d18ea3f5e689f3e9194762fec33804072989f1cb92d357038e03ca9481e7ab9f34eb123964f3f025b8749
7
+ data.tar.gz: c1953f30afa3a7871291852d3aaeba52a256006732582c82f71ad3245234f1e2a974524b174a801df81096af6effc37186e3a10e11ace5f5be644edbe4ed1c49
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.0](https://github.com/bottrall/riffer-rig/compare/v0.9.0...v0.10.0) (2026-10-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * 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))
9
+ * reload automatically before each request ([#222](https://github.com/bottrall/riffer-rig/issues/222)) ([fe96aa4](https://github.com/bottrall/riffer-rig/commit/fe96aa4476b0765e58824a8eb59301f0986e397c))
10
+
3
11
  ## [0.9.0](https://github.com/bottrall/riffer-rig/compare/v0.8.0...v0.9.0) (2026-10-04)
4
12
 
5
13
 
data/README.md CHANGED
@@ -47,6 +47,8 @@ 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
54
  `riffer -p` (one prompt, no terminal) and `riffer acp` (an editor's agent over stdio) are reserved: for now each prints the usage and exits with status 2.
@@ -9,12 +9,14 @@ class Riffer::Rig::CLI::Flags
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, continue, resume
13
13
  attr_reader :model #: String?
14
14
  attr_reader :extensions, :skills, :agents_md, :help #: bool
15
15
  attr_reader :tools #: Array[String]?
16
16
  attr_reader :max_steps #: Integer?
17
17
  attr_reader :save #: bool
18
+ attr_reader :continue #: bool
19
+ attr_reader :resume #: String?
18
20
 
19
21
  # @rbs argv: Array[String]
20
22
  # @rbs return: Riffer::Rig::CLI::Flags | String
@@ -31,6 +33,8 @@ class Riffer::Rig::CLI::Flags
31
33
  tools: values[:tools],
32
34
  max_steps: values[:'max-steps'],
33
35
  save: !values.key?(:'no-save'),
36
+ continue: values.key?(:continue),
37
+ resume: values[:resume],
34
38
  help: values.key?(:help)
35
39
  )
36
40
  rescue OptionParser::ParseError => e
@@ -54,6 +58,8 @@ class Riffer::Rig::CLI::Flags
54
58
  parser.on('--tools NAME,NAME', Array, 'Only these tools, by identifier')
55
59
  parser.on('--max-steps N', Integer, 'Stop a turn after N model calls')
56
60
  parser.on('--no-save', 'Do not save this session')
61
+ 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')
57
63
  parser.on('-h', '--help', 'Show this help')
58
64
  end
59
65
  end
@@ -66,6 +72,8 @@ class Riffer::Rig::CLI::Flags
66
72
  # @rbs tools: Array[String]?
67
73
  # @rbs max_steps: Integer?
68
74
  # @rbs save: bool
75
+ # @rbs continue: bool
76
+ # @rbs resume: String?
69
77
  # @rbs help: bool
70
78
  # @rbs return: void
71
79
  def initialize(
@@ -76,6 +84,8 @@ class Riffer::Rig::CLI::Flags
76
84
  tools: nil,
77
85
  max_steps: nil,
78
86
  save: true,
87
+ continue: false,
88
+ resume: nil,
79
89
  help: false
80
90
  )
81
91
  @model = model
@@ -85,6 +95,8 @@ class Riffer::Rig::CLI::Flags
85
95
  @tools = tools&.freeze
86
96
  @max_steps = max_steps
87
97
  @save = save
98
+ @continue = continue
99
+ @resume = resume
88
100
  @help = help
89
101
  freeze
90
102
  end
@@ -52,22 +52,53 @@ module Riffer::Rig::CLI
52
52
  version: Riffer::Rig::VERSION,
53
53
  no_color: env.is_a?(Riffer::Rig::Env) && env.no_color
54
54
  ).run do |host|
55
- Riffer::Rig::Loader.runtime(
55
+ loader = Riffer::Rig::Loader.new(
56
56
  cwd: cwd,
57
57
  host: host,
58
58
  env: env,
59
59
  home: home,
60
- model: flags.model,
61
- extensions: flags.extensions,
62
- skills: flags.skills,
63
- agents_md: flags.agents_md,
64
- tools: flags.tools,
65
- max_steps: flags.max_steps,
66
60
  store: flags.save ? Riffer::Rig::Stores::JSONL.new : nil
67
61
  )
62
+ resumed(flags, host, loader) || build(loader, flags)
68
63
  end
69
64
  end
70
65
 
66
+ # @rbs flags: Riffer::Rig::CLI::Flags
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
79
+ end
80
+ end
81
+
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
+ # @rbs flags: Riffer::Rig::CLI::Flags
90
+ # @rbs return: Hash[Symbol, untyped]
91
+ def keywords(flags)
92
+ {
93
+ model: flags.model,
94
+ extensions: flags.extensions,
95
+ skills: flags.skills,
96
+ agents_md: flags.agents_md,
97
+ tools: flags.tools,
98
+ max_steps: flags.max_steps
99
+ }
100
+ end
101
+
71
102
  # @rbs name: String
72
103
  # @rbs error: IO
73
104
  # @rbs return: Integer
@@ -21,6 +21,7 @@ class Riffer::Rig::Loader
21
21
  # @rbs @extensions_enabled: bool
22
22
  # @rbs @gem_extensions: Array[Riffer::Rig::Extension]
23
23
  # @rbs @file_state: Hash[String, Time?]?
24
+ # @rbs @reload_mode: (:auto | :manual | nil)
24
25
 
25
26
  # @dynamic tracked_files
26
27
  attr_reader :tracked_files #: Array[String]
@@ -37,6 +38,8 @@ class Riffer::Rig::Loader
37
38
  # @rbs tools: Array[String]?
38
39
  # @rbs max_steps: Integer?
39
40
  # @rbs store: Riffer::Rig::Stores::_Store | nil
41
+ # @rbs reload: (:auto | :manual | nil)
42
+ # @rbs snapshot: Hash[Symbol, untyped]?
40
43
  # @rbs return: Riffer::Rig::Runtime
41
44
  def self.runtime(
42
45
  cwd:,
@@ -50,10 +53,20 @@ class Riffer::Rig::Loader
50
53
  agents_md: true,
51
54
  tools: nil,
52
55
  max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS,
53
- store: Riffer::Rig::Stores::JSONL.new
56
+ store: Riffer::Rig::Stores::JSONL.new,
57
+ reload: nil,
58
+ snapshot: nil
54
59
  )
55
- new(cwd:, host:, env:, home:, riffer_config:, store:)
56
- .runtime(model:, extensions:, skills:, agents_md:, tools:, max_steps:)
60
+ new(cwd:, host:, env:, home:, riffer_config:, store:).runtime(
61
+ model:,
62
+ extensions:,
63
+ skills:,
64
+ agents_md:,
65
+ tools:,
66
+ max_steps:,
67
+ reload:,
68
+ snapshot:
69
+ )
57
70
  end
58
71
 
59
72
  # @rbs cwd: String
@@ -84,6 +97,7 @@ class Riffer::Rig::Loader
84
97
  @extensions_enabled = true
85
98
  @gem_extensions = []
86
99
  @file_state = nil
100
+ @reload_mode = nil
87
101
  end
88
102
 
89
103
  # @rbs model: String?
@@ -92,6 +106,8 @@ class Riffer::Rig::Loader
92
106
  # @rbs agents_md: bool
93
107
  # @rbs tools: Array[String]?
94
108
  # @rbs max_steps: Integer?
109
+ # @rbs reload: (:auto | :manual | nil)
110
+ # @rbs snapshot: Hash[Symbol, untyped]?
95
111
  # @rbs return: Riffer::Rig::Runtime
96
112
  def runtime(
97
113
  model: nil,
@@ -99,7 +115,9 @@ class Riffer::Rig::Loader
99
115
  skills: true,
100
116
  agents_md: true,
101
117
  tools: nil,
102
- max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
118
+ max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS,
119
+ reload: nil,
120
+ snapshot: nil
103
121
  )
104
122
  settings = merged_settings
105
123
  document = Riffer::Rig::Settings::Document.new(settings)
@@ -108,13 +126,14 @@ class Riffer::Rig::Loader
108
126
  ensure_sdk(provider)
109
127
  credentials = { provider.to_sym => credentials_for(provider) }
110
128
  @extensions_enabled = extensions
129
+ @reload_mode = reload
111
130
  @stripped = { skills: skills, agents_md: agents_md }.reject { |_name, kept| kept }.keys
112
131
  loaded = Riffer::Rig::Bundled::BY_NAME.except(*document.disabled.map(&:to_sym), *@stripped).values
113
132
  @tracked_files = []
114
133
  @gem_extensions = []
115
134
  loaded += load_rig_files(document.autoload) if extensions
116
135
 
117
- recorder = recorder_for(document)
136
+ recorder = recorder_for(document, resumed: !snapshot.nil?)
118
137
  loaded << Riffer::Rig::Stores::Recorder.extension(recorder) if recorder
119
138
 
120
139
  built = Riffer::Rig::Runtime.new(
@@ -129,25 +148,99 @@ class Riffer::Rig::Loader
129
148
  pricing: document.models,
130
149
  riffer_config: @riffer_config,
131
150
  model_options: Riffer::Rig::Settings.model_options(selected, document.reasoning),
132
- native_tools: document.native_tools
151
+ native_tools: document.native_tools,
152
+ snapshot: snapshot
133
153
  )
134
154
  built.install_command(Riffer::Rig::Commands::Reload.command(self))
155
+ built.install_reload_check(->(runtime) { automatic_reload(runtime) }) unless reload_mode(document) == :manual
135
156
  @file_state = file_state
136
157
  recorder&.attach(built)
137
158
  built
138
159
  end
139
160
 
161
+ # @rbs model: String?
162
+ # @rbs extensions: bool
163
+ # @rbs skills: bool
164
+ # @rbs agents_md: bool
165
+ # @rbs tools: Array[String]?
166
+ # @rbs max_steps: Integer?
167
+ # @rbs return: Riffer::Rig::Runtime?
168
+ def continue(
169
+ model: nil,
170
+ extensions: true,
171
+ skills: true,
172
+ agents_md: true,
173
+ tools: nil,
174
+ max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
175
+ )
176
+ store = @store
177
+ return nil unless store
178
+
179
+ latest = store.list(cwd: @cwd).max_by { |header| store.updated(header.fetch(:id)) || Time.at(0) }
180
+ return nil unless latest
181
+
182
+ id = latest.fetch(:id) #: String
183
+ resume(id, model:, extensions:, skills:, agents_md:, tools:, max_steps:)
184
+ end
185
+
186
+ # A resume carries the history over and nothing else: the settings,
187
+ # credentials, extensions and tools are today's.
188
+ # @rbs id: String
189
+ # @rbs model: String?
190
+ # @rbs extensions: bool
191
+ # @rbs skills: bool
192
+ # @rbs agents_md: bool
193
+ # @rbs tools: Array[String]?
194
+ # @rbs max_steps: Integer?
195
+ # @rbs return: Riffer::Rig::Runtime?
196
+ def resume(
197
+ id,
198
+ model: nil,
199
+ extensions: true,
200
+ skills: true,
201
+ agents_md: true,
202
+ tools: nil,
203
+ max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
204
+ )
205
+ store = @store
206
+ return nil unless store
207
+
208
+ entries = store.read(id)
209
+ return nil if entries.empty?
210
+
211
+ runtime(model:, extensions:, skills:, agents_md:, tools:, max_steps:, snapshot: snapshot_of(entries, id))
212
+ end
213
+
214
+ # @rbs all: bool
215
+ # @rbs return: Array[Hash[Symbol, untyped]]
216
+ def list(all: false)
217
+ store = @store
218
+ return [] unless store
219
+
220
+ all ? store.list : store.list(cwd: @cwd)
221
+ end
222
+
223
+ # @rbs id: String
224
+ # @rbs return: void
225
+ def delete(id)
226
+ store = @store
227
+ return unless store
228
+
229
+ store.delete(id)
230
+ end
231
+
140
232
  # Without force, a no-op while the tracked file set and the two rig.rb paths
141
- # look as the last discovery left them — the entry point the automatic
142
- # trigger (#133) calls. The snapshot advances on every attempt, failed ones
143
- # included, so a failure does not retry every request. Failures are notified
144
- # once and reported as the message, the way Settings.rejection reports a
145
- # rejection; the Runtime is the success value.
233
+ # look as the last discovery left them and the reload mode is not manual —
234
+ # the entry point the automatic trigger (#133) calls. The snapshot advances
235
+ # on every attempt, failed ones included, so a failure does not retry every
236
+ # request. Failures are notified once and reported as the message, the way
237
+ # Settings.rejection reports a rejection; the Runtime is the success value.
146
238
  # @rbs runtime: Riffer::Rig::Runtime
147
239
  # @rbs force: bool
148
240
  # @rbs return: (Riffer::Rig::Runtime | String)
149
241
  def reload(runtime, force: false)
150
242
  return runtime if !force && file_state == @file_state
243
+ return runtime if !force && manual_reload?
151
244
 
152
245
  outcome = perform_reload(runtime)
153
246
  @file_state = file_state
@@ -156,6 +249,27 @@ class Riffer::Rig::Loader
156
249
 
157
250
  private
158
251
 
252
+ # The reload mode a build runs under: the keyword wins over the setting,
253
+ # the setting over the default.
254
+ # @rbs document: Riffer::Rig::Settings::Document
255
+ # @rbs return: (:auto | :manual)
256
+ def reload_mode(document)
257
+ @reload_mode || document.reload
258
+ end
259
+
260
+ # @rbs return: bool
261
+ def manual_reload?
262
+ reload_mode(Riffer::Rig::Settings::Document.new(merged_settings)) == :manual
263
+ end
264
+
265
+ # The automatic trigger: runs at every before_request boundary, so the
266
+ # change check decides and a no-op costs the stat alone.
267
+ # @rbs runtime: Riffer::Rig::Runtime
268
+ # @rbs return: void
269
+ def automatic_reload(runtime)
270
+ _ = reload(runtime)
271
+ end
272
+
159
273
  # @rbs return: Hash[Symbol, untyped]
160
274
  def merged_settings
161
275
  Riffer::Rig::Settings.merge(
@@ -164,28 +278,24 @@ class Riffer::Rig::Loader
164
278
  )
165
279
  end
166
280
 
167
- # Notifies every failure once and reports its message; a rebuilt Runtime is
168
- # the success value.
281
+ # Notifies every failure once and reports its message, unexpected raises
282
+ # included — the automatic check runs mid-turn with no other isolation, and a
283
+ # raised attempt must still advance the snapshot in reload. A rebuilt Runtime
284
+ # is the success value.
169
285
  # @rbs runtime: Riffer::Rig::Runtime
170
286
  # @rbs return: (Riffer::Rig::Runtime | String)
171
287
  def perform_reload(runtime)
172
288
  settings = merged_settings
173
289
  document = Riffer::Rig::Settings::Document.new(settings)
174
- begin
175
- extensions = reload_extensions(document, settings)
176
- rescue AbandonedError => e
177
- return notify_failure(e.message)
178
- end
290
+ extensions = reload_extensions(document, settings)
179
291
 
180
292
  failure = reload_credentials(runtime)
181
293
  return notify_failure(failure) if failure
182
294
 
183
- begin
184
- runtime.rebuild(extensions: validated(extensions, settings), settings: settings)
185
- rescue StandardError => e
186
- return notify_failure(e.message)
187
- end
295
+ runtime.rebuild(extensions: validated(extensions, settings), settings: settings)
188
296
  runtime
297
+ rescue StandardError => e
298
+ notify_failure(e.message)
189
299
  end
190
300
 
191
301
  # @rbs message: String
@@ -453,15 +563,29 @@ class Riffer::Rig::Loader
453
563
  File.join(@home, '.riffer', 'settings.json')
454
564
  end
455
565
 
566
+ # @rbs entries: Array[Hash[Symbol, untyped]]
567
+ # @rbs id: String
568
+ # @rbs return: Hash[Symbol, untyped]
569
+ def snapshot_of(entries, id)
570
+ models = entries.select { |entry| entry[:type] == 'model' }
571
+ {
572
+ id: id,
573
+ messages: entries.select { |entry| entry[:type] == 'message' }.map { |entry| entry.fetch(:message) },
574
+ model: models.last&.fetch(:model),
575
+ skills: entries.select { |entry| entry[:type] == 'skill' }.map { |entry| entry.fetch(:skill) }
576
+ }
577
+ end
578
+
456
579
  # The store records unless it was declined with store: nil or globally with
457
580
  # "sessions": {"save": false}.
458
581
  # @rbs document: Riffer::Rig::Settings::Document
582
+ # @rbs resumed: bool
459
583
  # @rbs return: Riffer::Rig::Stores::Recorder?
460
- def recorder_for(document)
584
+ def recorder_for(document, resumed:)
461
585
  store = @store
462
586
  return nil unless document.save
463
587
  return nil unless store
464
588
 
465
- Riffer::Rig::Stores::Recorder.new(store: store)
589
+ Riffer::Rig::Stores::Recorder.new(store: store, resumed: resumed)
466
590
  end
467
591
  end
@@ -66,6 +66,7 @@ class Riffer::Rig::Runtime
66
66
  # @rbs @mcp_servers: Hash[String, Riffer::Rig::Mcp::Server]
67
67
  # @rbs @model_options: Hash[Symbol, untyped]
68
68
  # @rbs @native_tools: Hash[Symbol, untyped]
69
+ # @rbs @reload_check: (^(::Riffer::Rig::Runtime) -> void)?
69
70
 
70
71
  # @dynamic agent, credentials, cwd, host, id, settings, declared_settings
71
72
  attr_reader :agent #: Riffer::Agent
@@ -122,6 +123,7 @@ class Riffer::Rig::Runtime
122
123
  @claim_depth = 0
123
124
  @claim_thread = nil
124
125
  @core_commands = []
126
+ @reload_check = nil
125
127
  @cancel_flag = Riffer::Rig::Runtime::CancelFlag.new
126
128
  @session_start_pending = true
127
129
  @session_start_reason = snapshot ? :restore : :new
@@ -187,6 +189,16 @@ class Riffer::Rig::Runtime
187
189
  nil
188
190
  end
189
191
 
192
+ # Installs a host-of-the-runtime check (the Loader's automatic reload) so it
193
+ # survives every rebuild. The Runtime calls it at both before_request
194
+ # boundaries, before any handler runs, and ignores the result.
195
+ # @rbs check: ^(::Riffer::Rig::Runtime) -> void
196
+ # @rbs return: nil
197
+ def install_reload_check(check)
198
+ @reload_check = check
199
+ nil
200
+ end
201
+
190
202
  # @rbs name: String
191
203
  # @rbs args: String
192
204
  # @rbs &block: ?(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void
@@ -448,6 +460,7 @@ class Riffer::Rig::Runtime
448
460
  # @rbs return: Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
449
461
  def start_turn(text)
450
462
  @cancel_flag.clear
463
+ @reload_check&.call(self)
451
464
  refresh_system_message
452
465
  prompt = @hooks.before_prompt(text)
453
466
  return blocked_turn(prompt.reason) if prompt.is_a?(Riffer::Rig::Runtime::Blocked)
@@ -554,6 +567,7 @@ class Riffer::Rig::Runtime
554
567
 
555
568
  # @rbs return: void
556
569
  def before_next_request
570
+ @reload_check&.call(self)
557
571
  messages = @agent.session.messages
558
572
  # Upstream candidate: riffer has no hook between tool results and the next
559
573
  # request, so the last tool result's on_message stands in for one.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Riffer::Rig::Settings::Document
4
- # @dynamic model, reasoning, models, providers, disabled, autoload, native_tools, save
4
+ # @dynamic model, reasoning, models, providers, disabled, autoload, native_tools, save, reload
5
5
  attr_reader :model #: String?
6
6
  attr_reader :reasoning #: String?
7
7
  attr_reader :models #: Hash[String, Riffer::Rig::Settings::Pricing]
@@ -10,6 +10,7 @@ class Riffer::Rig::Settings::Document
10
10
  attr_reader :autoload #: bool
11
11
  attr_reader :native_tools #: Hash[Symbol, bool | Hash[Symbol, untyped]]
12
12
  attr_reader :save #: bool
13
+ attr_reader :reload #: (:auto | :manual)
13
14
 
14
15
  # @rbs source: Hash[Symbol, untyped]
15
16
  # @rbs return: void
@@ -31,6 +32,7 @@ class Riffer::Rig::Settings::Document
31
32
  @disabled = names.freeze
32
33
  @autoload = hash_or_empty(source[:extensions])[:autoload] == true
33
34
  @save = hash_or_empty(source[:sessions])[:save] != false
35
+ @reload = source[:reload] == 'manual' ? :manual : :auto
34
36
  @native_tools = hash_or_empty(hash_or_empty(source[:tools])[:native])
35
37
  .select { |_tool, switch| switch == true || switch.is_a?(Hash) }
36
38
  .transform_keys(&:to_sym).freeze
@@ -35,10 +35,23 @@ class Riffer::Rig::Stores::JSONL
35
35
  # @rbs cwd: String?
36
36
  # @rbs return: Array[Hash[Symbol, untyped]]
37
37
  def list(cwd: nil)
38
- headers = Dir.glob(File.join(root, '*', '*.jsonl')).filter_map { |path| header_of(path) }
38
+ headers = Dir.glob(File.join(root, '*', '*.jsonl')).filter_map do |path|
39
+ # Seeding the path cache here keeps picking the most recently updated
40
+ # session one mtime per header instead of one glob.
41
+ @paths[File.basename(path, '.jsonl')] = path
42
+ header_of(path)
43
+ end
39
44
  cwd ? headers.select { |header| header[:cwd] == cwd } : headers
40
45
  end
41
46
 
47
+ # A session's last update is its file's mtime: appends are the only writes.
48
+ # @rbs id: String
49
+ # @rbs return: Time?
50
+ def updated(id)
51
+ path = find(id)
52
+ path ? File.mtime(path) : nil
53
+ end
54
+
42
55
  # @rbs id: String
43
56
  # @rbs return: void
44
57
  def delete(id)
@@ -6,6 +6,7 @@ class Riffer::Rig::Stores::Recorder
6
6
  EXTENSION_NAME = 'session-store' #: String
7
7
 
8
8
  # @rbs @store: Riffer::Rig::Stores::_Store
9
+ # @rbs @resumed: bool
9
10
  # @rbs @runtime: Riffer::Rig::Runtime?
10
11
  # @rbs @header_written: bool
11
12
  # @rbs @pending: Array[Hash[Symbol, untyped]]
@@ -14,11 +15,15 @@ class Riffer::Rig::Stores::Recorder
14
15
  # Records one Loader-built session into the store: the header on the first
15
16
  # message (its title needs the first prompt), one entry per message, one per
16
17
  # model switch and one per skill activation. Entries that land before the
17
- # first message (a /model before the first prompt) wait for the header.
18
+ # first message (a /model before the first prompt) wait for the header. A
19
+ # resumed session appends to the file its first run wrote, so it rewrites
20
+ # no header.
18
21
  # @rbs store: Riffer::Rig::Stores::_Store
22
+ # @rbs resumed: bool
19
23
  # @rbs return: void
20
- def initialize(store:)
24
+ def initialize(store:, resumed: false)
21
25
  @store = store
26
+ @resumed = resumed
22
27
  @runtime = nil
23
28
  @header_written = false
24
29
  @pending = []
@@ -84,7 +89,7 @@ class Riffer::Rig::Stores::Recorder
84
89
  # @rbs return: void
85
90
  def write_header
86
91
  @header_written = true
87
- @store.append(runtime.id, header_entry)
92
+ @store.append(runtime.id, header_entry) unless @resumed
88
93
  @pending.each { |entry| @store.append(runtime.id, entry) }
89
94
  @pending.clear
90
95
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Riffer
4
4
  module Rig
5
- VERSION = '0.9.0'
5
+ VERSION = '0.10.0'
6
6
  end
7
7
  end
@@ -3,7 +3,7 @@
3
3
  class Riffer::Rig::CLI::Flags
4
4
  BANNER: String
5
5
 
6
- # @dynamic model, extensions, skills, agents_md, tools, max_steps, help, save
6
+ # @dynamic model, extensions, skills, agents_md, tools, max_steps, help, save, continue, resume
7
7
  attr_reader model: String?
8
8
 
9
9
  attr_reader extensions: bool
@@ -20,6 +20,10 @@ class Riffer::Rig::CLI::Flags
20
20
 
21
21
  attr_reader save: bool
22
22
 
23
+ attr_reader continue: bool
24
+
25
+ attr_reader resume: String?
26
+
23
27
  # @rbs argv: Array[String]
24
28
  # @rbs return: Riffer::Rig::CLI::Flags | String
25
29
  def self.parse: (Array[String] argv) -> (Riffer::Rig::CLI::Flags | String)
@@ -37,7 +41,9 @@ class Riffer::Rig::CLI::Flags
37
41
  # @rbs tools: Array[String]?
38
42
  # @rbs max_steps: Integer?
39
43
  # @rbs save: bool
44
+ # @rbs continue: bool
45
+ # @rbs resume: String?
40
46
  # @rbs help: bool
41
47
  # @rbs return: void
42
- def initialize: (?model: String?, ?extensions: bool, ?skills: bool, ?agents_md: bool, ?tools: Array[String]?, ?max_steps: Integer?, ?save: bool, ?help: bool) -> void
48
+ def initialize: (?model: String?, ?extensions: bool, ?skills: bool, ?agents_md: bool, ?tools: Array[String]?, ?max_steps: Integer?, ?save: bool, ?continue: bool, ?resume: String?, ?help: bool) -> void
43
49
  end
@@ -28,6 +28,21 @@ module Riffer::Rig::CLI
28
28
  # @rbs return: Integer
29
29
  def terminal: (Riffer::Rig::CLI::Flags flags, input: IO, output: IO, env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid, cwd: String, home: String) -> Integer
30
30
 
31
+ # @rbs flags: Riffer::Rig::CLI::Flags
32
+ # @rbs host: Riffer::Rig::Hosts::_Host
33
+ # @rbs loader: Riffer::Rig::Loader
34
+ # @rbs return: Riffer::Rig::Runtime?
35
+ def resumed: (Riffer::Rig::CLI::Flags flags, Riffer::Rig::Hosts::_Host host, Riffer::Rig::Loader loader) -> Riffer::Rig::Runtime?
36
+
37
+ # @rbs loader: Riffer::Rig::Loader
38
+ # @rbs flags: Riffer::Rig::CLI::Flags
39
+ # @rbs return: Riffer::Rig::Runtime
40
+ def build: (Riffer::Rig::Loader loader, Riffer::Rig::CLI::Flags flags) -> Riffer::Rig::Runtime
41
+
42
+ # @rbs flags: Riffer::Rig::CLI::Flags
43
+ # @rbs return: Hash[Symbol, untyped]
44
+ def keywords: (Riffer::Rig::CLI::Flags flags) -> Hash[Symbol, untyped]
45
+
31
46
  # @rbs name: String
32
47
  # @rbs error: IO
33
48
  # @rbs return: Integer
@@ -10,6 +10,8 @@ class Riffer::Rig::Loader
10
10
 
11
11
  EXTENSION_FEATURE: String
12
12
 
13
+ @reload_mode: :auto | :manual | nil
14
+
13
15
  @file_state: Hash[String, Time?]?
14
16
 
15
17
  @gem_extensions: Array[Riffer::Rig::Extension]
@@ -47,8 +49,10 @@ class Riffer::Rig::Loader
47
49
  # @rbs tools: Array[String]?
48
50
  # @rbs max_steps: Integer?
49
51
  # @rbs store: Riffer::Rig::Stores::_Store | nil
52
+ # @rbs reload: (:auto | :manual | nil)
53
+ # @rbs snapshot: Hash[Symbol, untyped]?
50
54
  # @rbs return: Riffer::Rig::Runtime
51
- def self.runtime: (cwd: String, host: Riffer::Rig::Hosts::_Host, ?env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid, ?home: String, ?riffer_config: Riffer::Config, ?model: String?, ?extensions: bool, ?skills: bool, ?agents_md: bool, ?tools: Array[String]?, ?max_steps: Integer?, ?store: Riffer::Rig::Stores::_Store | nil) -> Riffer::Rig::Runtime
55
+ def self.runtime: (cwd: String, host: Riffer::Rig::Hosts::_Host, ?env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid, ?home: String, ?riffer_config: Riffer::Config, ?model: String?, ?extensions: bool, ?skills: bool, ?agents_md: bool, ?tools: Array[String]?, ?max_steps: Integer?, ?store: Riffer::Rig::Stores::_Store | nil, ?reload: :auto | :manual | nil, ?snapshot: Hash[Symbol, untyped]?) -> Riffer::Rig::Runtime
52
56
 
53
57
  # @rbs cwd: String
54
58
  # @rbs host: Riffer::Rig::Hosts::_Host
@@ -65,15 +69,46 @@ class Riffer::Rig::Loader
65
69
  # @rbs agents_md: bool
66
70
  # @rbs tools: Array[String]?
67
71
  # @rbs max_steps: Integer?
72
+ # @rbs reload: (:auto | :manual | nil)
73
+ # @rbs snapshot: Hash[Symbol, untyped]?
68
74
  # @rbs return: Riffer::Rig::Runtime
69
- def runtime: (?model: String?, ?extensions: bool, ?skills: bool, ?agents_md: bool, ?tools: Array[String]?, ?max_steps: Integer?) -> Riffer::Rig::Runtime
75
+ def runtime: (?model: String?, ?extensions: bool, ?skills: bool, ?agents_md: bool, ?tools: Array[String]?, ?max_steps: Integer?, ?reload: :auto | :manual | nil, ?snapshot: Hash[Symbol, untyped]?) -> Riffer::Rig::Runtime
76
+
77
+ # @rbs model: String?
78
+ # @rbs extensions: bool
79
+ # @rbs skills: bool
80
+ # @rbs agents_md: bool
81
+ # @rbs tools: Array[String]?
82
+ # @rbs max_steps: Integer?
83
+ # @rbs return: Riffer::Rig::Runtime?
84
+ def continue: (?model: String?, ?extensions: bool, ?skills: bool, ?agents_md: bool, ?tools: Array[String]?, ?max_steps: Integer?) -> Riffer::Rig::Runtime?
85
+
86
+ # A resume carries the history over and nothing else: the settings,
87
+ # credentials, extensions and tools are today's.
88
+ # @rbs id: String
89
+ # @rbs model: String?
90
+ # @rbs extensions: bool
91
+ # @rbs skills: bool
92
+ # @rbs agents_md: bool
93
+ # @rbs tools: Array[String]?
94
+ # @rbs max_steps: Integer?
95
+ # @rbs return: Riffer::Rig::Runtime?
96
+ def resume: (String id, ?model: String?, ?extensions: bool, ?skills: bool, ?agents_md: bool, ?tools: Array[String]?, ?max_steps: Integer?) -> Riffer::Rig::Runtime?
97
+
98
+ # @rbs all: bool
99
+ # @rbs return: Array[Hash[Symbol, untyped]]
100
+ def list: (?all: bool) -> Array[Hash[Symbol, untyped]]
101
+
102
+ # @rbs id: String
103
+ # @rbs return: void
104
+ def delete: (String id) -> void
70
105
 
71
106
  # Without force, a no-op while the tracked file set and the two rig.rb paths
72
- # look as the last discovery left them — the entry point the automatic
73
- # trigger (#133) calls. The snapshot advances on every attempt, failed ones
74
- # included, so a failure does not retry every request. Failures are notified
75
- # once and reported as the message, the way Settings.rejection reports a
76
- # rejection; the Runtime is the success value.
107
+ # look as the last discovery left them and the reload mode is not manual —
108
+ # the entry point the automatic trigger (#133) calls. The snapshot advances
109
+ # on every attempt, failed ones included, so a failure does not retry every
110
+ # request. Failures are notified once and reported as the message, the way
111
+ # Settings.rejection reports a rejection; the Runtime is the success value.
77
112
  # @rbs runtime: Riffer::Rig::Runtime
78
113
  # @rbs force: bool
79
114
  # @rbs return: (Riffer::Rig::Runtime | String)
@@ -81,11 +116,28 @@ class Riffer::Rig::Loader
81
116
 
82
117
  private
83
118
 
119
+ # The reload mode a build runs under: the keyword wins over the setting,
120
+ # the setting over the default.
121
+ # @rbs document: Riffer::Rig::Settings::Document
122
+ # @rbs return: (:auto | :manual)
123
+ def reload_mode: (Riffer::Rig::Settings::Document document) -> (:auto | :manual)
124
+
125
+ # @rbs return: bool
126
+ def manual_reload?: () -> bool
127
+
128
+ # The automatic trigger: runs at every before_request boundary, so the
129
+ # change check decides and a no-op costs the stat alone.
130
+ # @rbs runtime: Riffer::Rig::Runtime
131
+ # @rbs return: void
132
+ def automatic_reload: (Riffer::Rig::Runtime runtime) -> void
133
+
84
134
  # @rbs return: Hash[Symbol, untyped]
85
135
  def merged_settings: () -> Hash[Symbol, untyped]
86
136
 
87
- # Notifies every failure once and reports its message; a rebuilt Runtime is
88
- # the success value.
137
+ # Notifies every failure once and reports its message, unexpected raises
138
+ # included — the automatic check runs mid-turn with no other isolation, and a
139
+ # raised attempt must still advance the snapshot in reload. A rebuilt Runtime
140
+ # is the success value.
89
141
  # @rbs runtime: Riffer::Rig::Runtime
90
142
  # @rbs return: (Riffer::Rig::Runtime | String)
91
143
  def perform_reload: (Riffer::Rig::Runtime runtime) -> (Riffer::Rig::Runtime | String)
@@ -206,9 +258,15 @@ class Riffer::Rig::Loader
206
258
  # @rbs return: String
207
259
  def home_settings_path: () -> String
208
260
 
261
+ # @rbs entries: Array[Hash[Symbol, untyped]]
262
+ # @rbs id: String
263
+ # @rbs return: Hash[Symbol, untyped]
264
+ def snapshot_of: (Array[Hash[Symbol, untyped]] entries, String id) -> Hash[Symbol, untyped]
265
+
209
266
  # The store records unless it was declined with store: nil or globally with
210
267
  # "sessions": {"save": false}.
211
268
  # @rbs document: Riffer::Rig::Settings::Document
269
+ # @rbs resumed: bool
212
270
  # @rbs return: Riffer::Rig::Stores::Recorder?
213
- def recorder_for: (Riffer::Rig::Settings::Document document) -> Riffer::Rig::Stores::Recorder?
271
+ def recorder_for: (Riffer::Rig::Settings::Document document, resumed: bool) -> Riffer::Rig::Stores::Recorder?
214
272
  end
@@ -26,6 +26,8 @@ class Riffer::Rig::Runtime
26
26
  # tests and embedders can exercise the switches.
27
27
  NATIVE_TOOLS_BY_PROVIDER: Hash[Symbol, Hash[Symbol, bool]]
28
28
 
29
+ @model_observers: Array[^(String) -> void]
30
+
29
31
  @message_observers: Array[^(Riffer::Messages::Base) -> void]
30
32
 
31
33
  @model_override: String?
@@ -54,8 +56,6 @@ class Riffer::Rig::Runtime
54
56
 
55
57
  @agent: Riffer::Agent
56
58
 
57
- @model_observers: Array[^(String) -> void]
58
-
59
59
  @prompts: Hash[Symbol, ^(Riffer::Rig::Runtime) -> String?]
60
60
 
61
61
  @commands: Hash[String, Riffer::Rig::Command]
@@ -82,6 +82,8 @@ class Riffer::Rig::Runtime
82
82
 
83
83
  @native_tools: Hash[Symbol, untyped]
84
84
 
85
+ @reload_check: (^(::Riffer::Rig::Runtime) -> void)?
86
+
85
87
  # @dynamic agent, credentials, cwd, host, id, settings, declared_settings
86
88
  attr_reader agent: Riffer::Agent
87
89
 
@@ -134,6 +136,13 @@ class Riffer::Rig::Runtime
134
136
  # @rbs return: nil
135
137
  def install_command: (Riffer::Rig::Command command) -> nil
136
138
 
139
+ # Installs a host-of-the-runtime check (the Loader's automatic reload) so it
140
+ # survives every rebuild. The Runtime calls it at both before_request
141
+ # boundaries, before any handler runs, and ignores the result.
142
+ # @rbs check: ^(::Riffer::Rig::Runtime) -> void
143
+ # @rbs return: nil
144
+ def install_reload_check: (^(::Riffer::Rig::Runtime) -> void check) -> nil
145
+
137
146
  # @rbs name: String
138
147
  # @rbs args: String
139
148
  # @rbs &block: ?(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void
@@ -1,7 +1,7 @@
1
1
  # Generated from lib/riffer/rig/settings/document.rb with RBS::Inline
2
2
 
3
3
  class Riffer::Rig::Settings::Document
4
- # @dynamic model, reasoning, models, providers, disabled, autoload, native_tools, save
4
+ # @dynamic model, reasoning, models, providers, disabled, autoload, native_tools, save, reload
5
5
  attr_reader model: String?
6
6
 
7
7
  attr_reader reasoning: String?
@@ -18,6 +18,8 @@ class Riffer::Rig::Settings::Document
18
18
 
19
19
  attr_reader save: bool
20
20
 
21
+ attr_reader reload: :auto | :manual
22
+
21
23
  # @rbs source: Hash[Symbol, untyped]
22
24
  # @rbs return: void
23
25
  def initialize: (Hash[Symbol, untyped] source) -> void
@@ -3,10 +3,10 @@
3
3
  class Riffer::Rig::Stores::JSONL
4
4
  HEADER_VERSION: Integer
5
5
 
6
- @home: String
7
-
8
6
  @paths: Hash[String, String]
9
7
 
8
+ @home: String
9
+
10
10
  # @rbs home: String
11
11
  # @rbs return: void
12
12
  def initialize: (?home: String) -> void
@@ -24,6 +24,11 @@ class Riffer::Rig::Stores::JSONL
24
24
  # @rbs return: Array[Hash[Symbol, untyped]]
25
25
  def list: (?cwd: String?) -> Array[Hash[Symbol, untyped]]
26
26
 
27
+ # A session's last update is its file's mtime: appends are the only writes.
28
+ # @rbs id: String
29
+ # @rbs return: Time?
30
+ def updated: (String id) -> Time?
31
+
27
32
  # @rbs id: String
28
33
  # @rbs return: void
29
34
  def delete: (String id) -> void
@@ -11,15 +11,20 @@ class Riffer::Rig::Stores::Recorder
11
11
 
12
12
  @runtime: Riffer::Rig::Runtime?
13
13
 
14
+ @resumed: bool
15
+
14
16
  @store: Riffer::Rig::Stores::_Store
15
17
 
16
18
  # Records one Loader-built session into the store: the header on the first
17
19
  # message (its title needs the first prompt), one entry per message, one per
18
20
  # model switch and one per skill activation. Entries that land before the
19
- # first message (a /model before the first prompt) wait for the header.
21
+ # first message (a /model before the first prompt) wait for the header. A
22
+ # resumed session appends to the file its first run wrote, so it rewrites
23
+ # no header.
20
24
  # @rbs store: Riffer::Rig::Stores::_Store
25
+ # @rbs resumed: bool
21
26
  # @rbs return: void
22
- def initialize: (store: Riffer::Rig::Stores::_Store) -> void
27
+ def initialize: (store: Riffer::Rig::Stores::_Store, ?resumed: bool) -> void
23
28
 
24
29
  # @rbs runtime: Riffer::Rig::Runtime
25
30
  # @rbs return: void
@@ -6,6 +6,7 @@ module Riffer::Rig::Stores
6
6
  def append: (String id, Hash[Symbol, untyped] entry) -> void
7
7
  def read: (String id) -> Array[Hash[Symbol, untyped]]
8
8
  def list: (?cwd: String?) -> Array[Hash[Symbol, untyped]]
9
+ def updated: (String id) -> Time?
9
10
  def delete: (String id) -> void
10
11
  end
11
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riffer-rig
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall