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
data/lib/riffer/rig/loader.rb
CHANGED
|
@@ -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
|
-
|
|
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,98 @@ 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| header.updated || Time.at(0) }
|
|
180
|
+
return nil unless latest
|
|
181
|
+
|
|
182
|
+
resume(latest.id, model:, extensions:, skills:, agents_md:, tools:, max_steps:)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# A resume carries the history over and nothing else: the settings,
|
|
186
|
+
# credentials, extensions and tools are today's.
|
|
187
|
+
# @rbs id: String
|
|
188
|
+
# @rbs model: String?
|
|
189
|
+
# @rbs extensions: bool
|
|
190
|
+
# @rbs skills: bool
|
|
191
|
+
# @rbs agents_md: bool
|
|
192
|
+
# @rbs tools: Array[String]?
|
|
193
|
+
# @rbs max_steps: Integer?
|
|
194
|
+
# @rbs return: Riffer::Rig::Runtime?
|
|
195
|
+
def resume(
|
|
196
|
+
id,
|
|
197
|
+
model: nil,
|
|
198
|
+
extensions: true,
|
|
199
|
+
skills: true,
|
|
200
|
+
agents_md: true,
|
|
201
|
+
tools: nil,
|
|
202
|
+
max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
|
|
203
|
+
)
|
|
204
|
+
store = @store
|
|
205
|
+
return nil unless store
|
|
206
|
+
|
|
207
|
+
entries = store.read(id)
|
|
208
|
+
return nil if entries.empty?
|
|
209
|
+
|
|
210
|
+
runtime(model:, extensions:, skills:, agents_md:, tools:, max_steps:, snapshot: snapshot_of(entries, id))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# @rbs all: bool
|
|
214
|
+
# @rbs return: Array[::Riffer::Rig::Stores::Header]
|
|
215
|
+
def list(all: false)
|
|
216
|
+
store = @store
|
|
217
|
+
return [] unless store
|
|
218
|
+
|
|
219
|
+
all ? store.list : store.list(cwd: @cwd)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# @rbs id: String
|
|
223
|
+
# @rbs return: void
|
|
224
|
+
def delete(id)
|
|
225
|
+
store = @store
|
|
226
|
+
return unless store
|
|
227
|
+
|
|
228
|
+
store.delete(id)
|
|
229
|
+
end
|
|
230
|
+
|
|
140
231
|
# Without force, a no-op while the tracked file set and the two rig.rb paths
|
|
141
|
-
# look as the last discovery left them
|
|
142
|
-
# trigger (#133) calls. The snapshot advances
|
|
143
|
-
# included, so a failure does not retry every
|
|
144
|
-
# once and reported as the message, the way
|
|
145
|
-
# rejection; the Runtime is the success value.
|
|
232
|
+
# look as the last discovery left them and the reload mode is not manual —
|
|
233
|
+
# the entry point the automatic trigger (#133) calls. The snapshot advances
|
|
234
|
+
# on every attempt, failed ones included, so a failure does not retry every
|
|
235
|
+
# request. Failures are notified once and reported as the message, the way
|
|
236
|
+
# Settings.rejection reports a rejection; the Runtime is the success value.
|
|
146
237
|
# @rbs runtime: Riffer::Rig::Runtime
|
|
147
238
|
# @rbs force: bool
|
|
148
239
|
# @rbs return: (Riffer::Rig::Runtime | String)
|
|
149
240
|
def reload(runtime, force: false)
|
|
150
241
|
return runtime if !force && file_state == @file_state
|
|
242
|
+
return runtime if !force && manual_reload?
|
|
151
243
|
|
|
152
244
|
outcome = perform_reload(runtime)
|
|
153
245
|
@file_state = file_state
|
|
@@ -156,6 +248,27 @@ class Riffer::Rig::Loader
|
|
|
156
248
|
|
|
157
249
|
private
|
|
158
250
|
|
|
251
|
+
# The reload mode a build runs under: the keyword wins over the setting,
|
|
252
|
+
# the setting over the default.
|
|
253
|
+
# @rbs document: Riffer::Rig::Settings::Document
|
|
254
|
+
# @rbs return: (:auto | :manual)
|
|
255
|
+
def reload_mode(document)
|
|
256
|
+
@reload_mode || document.reload
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# @rbs return: bool
|
|
260
|
+
def manual_reload?
|
|
261
|
+
reload_mode(Riffer::Rig::Settings::Document.new(merged_settings)) == :manual
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The automatic trigger: runs at every before_request boundary, so the
|
|
265
|
+
# change check decides and a no-op costs the stat alone.
|
|
266
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
267
|
+
# @rbs return: void
|
|
268
|
+
def automatic_reload(runtime)
|
|
269
|
+
_ = reload(runtime)
|
|
270
|
+
end
|
|
271
|
+
|
|
159
272
|
# @rbs return: Hash[Symbol, untyped]
|
|
160
273
|
def merged_settings
|
|
161
274
|
Riffer::Rig::Settings.merge(
|
|
@@ -164,28 +277,24 @@ class Riffer::Rig::Loader
|
|
|
164
277
|
)
|
|
165
278
|
end
|
|
166
279
|
|
|
167
|
-
# Notifies every failure once and reports its message
|
|
168
|
-
# the
|
|
280
|
+
# Notifies every failure once and reports its message, unexpected raises
|
|
281
|
+
# included — the automatic check runs mid-turn with no other isolation, and a
|
|
282
|
+
# raised attempt must still advance the snapshot in reload. A rebuilt Runtime
|
|
283
|
+
# is the success value.
|
|
169
284
|
# @rbs runtime: Riffer::Rig::Runtime
|
|
170
285
|
# @rbs return: (Riffer::Rig::Runtime | String)
|
|
171
286
|
def perform_reload(runtime)
|
|
172
287
|
settings = merged_settings
|
|
173
288
|
document = Riffer::Rig::Settings::Document.new(settings)
|
|
174
|
-
|
|
175
|
-
extensions = reload_extensions(document, settings)
|
|
176
|
-
rescue AbandonedError => e
|
|
177
|
-
return notify_failure(e.message)
|
|
178
|
-
end
|
|
289
|
+
extensions = reload_extensions(document, settings)
|
|
179
290
|
|
|
180
291
|
failure = reload_credentials(runtime)
|
|
181
292
|
return notify_failure(failure) if failure
|
|
182
293
|
|
|
183
|
-
|
|
184
|
-
runtime.rebuild(extensions: validated(extensions, settings), settings: settings)
|
|
185
|
-
rescue StandardError => e
|
|
186
|
-
return notify_failure(e.message)
|
|
187
|
-
end
|
|
294
|
+
runtime.rebuild(extensions: validated(extensions, settings), settings: settings)
|
|
188
295
|
runtime
|
|
296
|
+
rescue StandardError => e
|
|
297
|
+
notify_failure(e.message)
|
|
189
298
|
end
|
|
190
299
|
|
|
191
300
|
# @rbs message: String
|
|
@@ -453,15 +562,28 @@ class Riffer::Rig::Loader
|
|
|
453
562
|
File.join(@home, '.riffer', 'settings.json')
|
|
454
563
|
end
|
|
455
564
|
|
|
565
|
+
# @rbs entries: Array[Riffer::Rig::Stores::entry]
|
|
566
|
+
# @rbs id: String
|
|
567
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
568
|
+
def snapshot_of(entries, id)
|
|
569
|
+
{
|
|
570
|
+
id: id,
|
|
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) }
|
|
574
|
+
}
|
|
575
|
+
end
|
|
576
|
+
|
|
456
577
|
# The store records unless it was declined with store: nil or globally with
|
|
457
578
|
# "sessions": {"save": false}.
|
|
458
579
|
# @rbs document: Riffer::Rig::Settings::Document
|
|
580
|
+
# @rbs resumed: bool
|
|
459
581
|
# @rbs return: Riffer::Rig::Stores::Recorder?
|
|
460
|
-
def recorder_for(document)
|
|
582
|
+
def recorder_for(document, resumed:)
|
|
461
583
|
store = @store
|
|
462
584
|
return nil unless document.save
|
|
463
585
|
return nil unless store
|
|
464
586
|
|
|
465
|
-
Riffer::Rig::Stores::Recorder.new(store: store)
|
|
587
|
+
Riffer::Rig::Stores::Recorder.new(store: store, resumed: resumed)
|
|
466
588
|
end
|
|
467
589
|
end
|
data/lib/riffer/rig/runtime.rb
CHANGED
|
@@ -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
|
|
@@ -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
|
|
@@ -6,6 +6,13 @@ require 'json'
|
|
|
6
6
|
class Riffer::Rig::Stores::JSONL
|
|
7
7
|
HEADER_VERSION = 1 #: Integer
|
|
8
8
|
|
|
9
|
+
ENTRY_TYPES = {
|
|
10
|
+
Riffer::Rig::Stores::Header::TYPE => Riffer::Rig::Stores::Header,
|
|
11
|
+
Riffer::Rig::Stores::MessageEntry::TYPE => Riffer::Rig::Stores::MessageEntry,
|
|
12
|
+
Riffer::Rig::Stores::ModelEntry::TYPE => Riffer::Rig::Stores::ModelEntry,
|
|
13
|
+
Riffer::Rig::Stores::SkillEntry::TYPE => Riffer::Rig::Stores::SkillEntry
|
|
14
|
+
}.freeze #: Hash[String, Riffer::Rig::Stores::entry_type]
|
|
15
|
+
|
|
9
16
|
# @rbs @home: String
|
|
10
17
|
# @rbs @paths: Hash[String, String]
|
|
11
18
|
|
|
@@ -17,26 +24,31 @@ class Riffer::Rig::Stores::JSONL
|
|
|
17
24
|
end
|
|
18
25
|
|
|
19
26
|
# @rbs id: String
|
|
20
|
-
# @rbs entry:
|
|
27
|
+
# @rbs entry: Riffer::Rig::Stores::entry
|
|
21
28
|
# @rbs return: void
|
|
22
29
|
def append(id, entry)
|
|
23
|
-
File.write(path_for(id, entry), "#{JSON.generate(entry)}\n", mode: 'a')
|
|
30
|
+
File.write(path_for(id, entry), "#{JSON.generate(entry.to_h)}\n", mode: 'a')
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
# @rbs id: String
|
|
27
|
-
# @rbs return: Array[
|
|
34
|
+
# @rbs return: Array[Riffer::Rig::Stores::entry]
|
|
28
35
|
def read(id)
|
|
29
36
|
path = find(id)
|
|
30
37
|
return [] unless path
|
|
31
38
|
|
|
32
|
-
File.foreach(path).filter_map { |line|
|
|
39
|
+
File.foreach(path).filter_map { |line| entry_of(line) }
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
# @rbs cwd: String?
|
|
36
|
-
# @rbs return: Array[
|
|
43
|
+
# @rbs return: Array[::Riffer::Rig::Stores::Header]
|
|
37
44
|
def list(cwd: nil)
|
|
38
|
-
headers = Dir.glob(File.join(root, '*', '*.jsonl')).filter_map
|
|
39
|
-
|
|
45
|
+
headers = Dir.glob(File.join(root, '*', '*.jsonl')).filter_map do |path|
|
|
46
|
+
# Seeding the path cache here keeps picking the most recently updated
|
|
47
|
+
# session one mtime per header instead of one glob.
|
|
48
|
+
@paths[File.basename(path, '.jsonl')] = path
|
|
49
|
+
header_of(path)
|
|
50
|
+
end
|
|
51
|
+
cwd ? headers.select { |header| header.cwd == cwd } : headers
|
|
40
52
|
end
|
|
41
53
|
|
|
42
54
|
# @rbs id: String
|
|
@@ -57,11 +69,11 @@ class Riffer::Rig::Stores::JSONL
|
|
|
57
69
|
end
|
|
58
70
|
|
|
59
71
|
# @rbs id: String
|
|
60
|
-
# @rbs entry:
|
|
72
|
+
# @rbs entry: Riffer::Rig::Stores::entry
|
|
61
73
|
# @rbs return: String
|
|
62
74
|
def path_for(id, entry)
|
|
63
75
|
@paths.fetch(id) do
|
|
64
|
-
path = entry
|
|
76
|
+
path = entry.is_a?(Riffer::Rig::Stores::Header) ? new_path(id, entry.cwd) : find(id)
|
|
65
77
|
raise ArgumentError, "no session file for #{id}" unless path
|
|
66
78
|
|
|
67
79
|
@paths[id] = path
|
|
@@ -77,14 +89,6 @@ class Riffer::Rig::Stores::JSONL
|
|
|
77
89
|
File.join(dir, "#{id}.jsonl")
|
|
78
90
|
end
|
|
79
91
|
|
|
80
|
-
# @rbs path: String
|
|
81
|
-
# @rbs return: Hash[Symbol, untyped]?
|
|
82
|
-
def header_of(path)
|
|
83
|
-
line = File.foreach(path).first
|
|
84
|
-
entry = parse(line) if line
|
|
85
|
-
entry if entry.is_a?(Hash) && entry[:type] == 'header'
|
|
86
|
-
end
|
|
87
|
-
|
|
88
92
|
# @rbs id: String
|
|
89
93
|
# @rbs return: String?
|
|
90
94
|
def find(id)
|
|
@@ -105,6 +109,28 @@ class Riffer::Rig::Stores::JSONL
|
|
|
105
109
|
slug.empty? || slug.match?(/\A\.{1,2}\z/) ? '-' : slug
|
|
106
110
|
end
|
|
107
111
|
|
|
112
|
+
# A session's last update is its file's mtime: appends are the only writes.
|
|
113
|
+
# The store stamps it while it holds the artifact, so the listed header
|
|
114
|
+
# carries it.
|
|
115
|
+
# @rbs path: String
|
|
116
|
+
# @rbs return: ::Riffer::Rig::Stores::Header?
|
|
117
|
+
def header_of(path)
|
|
118
|
+
line = File.foreach(path).first
|
|
119
|
+
hash = parse(line) if line
|
|
120
|
+
return nil unless hash && hash[:type] == Riffer::Rig::Stores::Header::TYPE
|
|
121
|
+
|
|
122
|
+
Riffer::Rig::Stores::Header.from_hash(hash, updated: File.mtime(path))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @rbs line: String
|
|
126
|
+
# @rbs return: Riffer::Rig::Stores::entry?
|
|
127
|
+
def entry_of(line)
|
|
128
|
+
hash = parse(line)
|
|
129
|
+
return nil unless hash
|
|
130
|
+
|
|
131
|
+
ENTRY_TYPES[hash[:type]]&.from_hash(hash)
|
|
132
|
+
end
|
|
133
|
+
|
|
108
134
|
# @rbs line: String
|
|
109
135
|
# @rbs return: Hash[Symbol, untyped]?
|
|
110
136
|
def parse(line)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Stores::MessageEntry
|
|
4
|
+
TYPE = 'message' #: String
|
|
5
|
+
|
|
6
|
+
include Riffer::Rig::Support::Equatable
|
|
7
|
+
|
|
8
|
+
# @dynamic message
|
|
9
|
+
attr_reader :message #: Hash[Symbol, untyped]
|
|
10
|
+
|
|
11
|
+
# @rbs hash: Hash[Symbol, untyped]
|
|
12
|
+
# @rbs return: ::Riffer::Rig::Stores::MessageEntry?
|
|
13
|
+
def self.from_hash(hash)
|
|
14
|
+
message = hash[:message]
|
|
15
|
+
return nil unless message.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
new(message: message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs message: Hash[Symbol, untyped]
|
|
21
|
+
# @rbs return: void
|
|
22
|
+
def initialize(message:)
|
|
23
|
+
@message = message
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
28
|
+
def to_h
|
|
29
|
+
{ type: TYPE, message: @message }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Stores::ModelEntry
|
|
4
|
+
TYPE = 'model' #: String
|
|
5
|
+
|
|
6
|
+
include Riffer::Rig::Support::Equatable
|
|
7
|
+
|
|
8
|
+
# @dynamic model
|
|
9
|
+
attr_reader :model #: String
|
|
10
|
+
|
|
11
|
+
# @rbs hash: Hash[Symbol, untyped]
|
|
12
|
+
# @rbs return: ::Riffer::Rig::Stores::ModelEntry?
|
|
13
|
+
def self.from_hash(hash)
|
|
14
|
+
model = hash[:model]
|
|
15
|
+
return nil unless model.is_a?(String)
|
|
16
|
+
|
|
17
|
+
new(model: model)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs model: String
|
|
21
|
+
# @rbs return: void
|
|
22
|
+
def initialize(model:)
|
|
23
|
+
@model = model
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
28
|
+
def to_h
|
|
29
|
+
{ type: TYPE, model: @model }
|
|
30
|
+
end
|
|
31
|
+
end
|