riffer-rig 0.8.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 +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +13 -2
- data/lib/riffer/rig/cli/flags.rb +28 -2
- data/lib/riffer/rig/cli.rb +39 -7
- data/lib/riffer/rig/commands/auth.rb +79 -0
- data/lib/riffer/rig/commands/model.rb +70 -21
- data/lib/riffer/rig/commands/reload.rb +23 -0
- data/lib/riffer/rig/credentials.rb +13 -1
- data/lib/riffer/rig/extension.rb +12 -0
- data/lib/riffer/rig/loader.rb +383 -32
- data/lib/riffer/rig/provider_setup.rb +36 -5
- data/lib/riffer/rig/providers.rb +48 -0
- data/lib/riffer/rig/registrar.rb +8 -0
- data/lib/riffer/rig/runtime.rb +124 -21
- data/lib/riffer/rig/sdk.rb +62 -0
- data/lib/riffer/rig/settings/document.rb +9 -1
- data/lib/riffer/rig/settings.rb +5 -4
- data/lib/riffer/rig/stores/jsonl.rb +128 -0
- data/lib/riffer/rig/stores/recorder.rb +124 -0
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +7 -1
- data/sig/generated/riffer/rig/cli/flags.rbs +11 -2
- data/sig/generated/riffer/rig/cli.rbs +15 -0
- data/sig/generated/riffer/rig/commands/auth.rbs +35 -0
- data/sig/generated/riffer/rig/commands/model.rbs +21 -9
- data/sig/generated/riffer/rig/commands/reload.rbs +12 -0
- data/sig/generated/riffer/rig/credentials.rbs +8 -0
- data/sig/generated/riffer/rig/extension.rbs +4 -0
- data/sig/generated/riffer/rig/loader.rbs +167 -8
- data/sig/generated/riffer/rig/provider_setup.rbs +15 -4
- data/sig/generated/riffer/rig/providers.rbs +28 -0
- data/sig/generated/riffer/rig/registrar.rbs +6 -0
- data/sig/generated/riffer/rig/runtime.rbs +73 -10
- data/sig/generated/riffer/rig/sdk.rbs +33 -0
- data/sig/generated/riffer/rig/settings/document.rbs +7 -1
- data/sig/generated/riffer/rig/settings.rbs +2 -3
- data/sig/generated/riffer/rig/stores/jsonl.rbs +69 -0
- data/sig/generated/riffer/rig/stores/recorder.rbs +72 -0
- data/sig/generated/riffer/rig.rbs +4 -0
- data/sig/manual/riffer/rig/commands/auth.rbs +6 -0
- data/sig/manual/riffer/rig/commands/reload.rbs +6 -0
- data/sig/manual/riffer/rig/providers.rbs +6 -0
- data/sig/manual/riffer/rig/sdk.rbs +6 -0
- data/sig/manual/riffer/rig/stores.rbs +12 -0
- metadata +18 -29
data/lib/riffer/rig/loader.rb
CHANGED
|
@@ -3,24 +3,25 @@
|
|
|
3
3
|
class Riffer::Rig::Loader
|
|
4
4
|
class ConfigurationError < StandardError; end
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
ONBOARDING_QUESTION = 'Which model should riffer use? Enter provider/name, ' \
|
|
9
|
-
"with a provider from: #{PROVIDER_LIST}".freeze #: String
|
|
10
|
-
|
|
11
|
-
NO_MODEL = 'No model is set: pass --model provider/name, set RIFFER_MODEL, or set "model" in ' \
|
|
12
|
-
"~/.riffer/settings.json, with a provider from: #{PROVIDER_LIST}".freeze #: String
|
|
6
|
+
# Raised by a strict load: the reload abandons instead of continuing.
|
|
7
|
+
class AbandonedError < StandardError; end
|
|
13
8
|
|
|
14
9
|
EXTENSION_FEATURE = 'riffer/rig/extension' #: String
|
|
15
10
|
|
|
16
|
-
private_constant :EXTENSION_FEATURE
|
|
11
|
+
private_constant :EXTENSION_FEATURE
|
|
17
12
|
|
|
18
13
|
# @rbs @cwd: String
|
|
19
14
|
# @rbs @host: Riffer::Rig::Hosts::_Host
|
|
20
15
|
# @rbs @env: Riffer::Rig::Env
|
|
21
16
|
# @rbs @home: String
|
|
22
17
|
# @rbs @riffer_config: Riffer::Config
|
|
18
|
+
# @rbs @store: Riffer::Rig::Stores::_Store | nil
|
|
23
19
|
# @rbs @tracked_files: Array[String]
|
|
20
|
+
# @rbs @stripped: Array[Symbol]
|
|
21
|
+
# @rbs @extensions_enabled: bool
|
|
22
|
+
# @rbs @gem_extensions: Array[Riffer::Rig::Extension]
|
|
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]
|
|
@@ -36,6 +37,9 @@ class Riffer::Rig::Loader
|
|
|
36
37
|
# @rbs agents_md: bool
|
|
37
38
|
# @rbs tools: Array[String]?
|
|
38
39
|
# @rbs max_steps: Integer?
|
|
40
|
+
# @rbs store: Riffer::Rig::Stores::_Store | nil
|
|
41
|
+
# @rbs reload: (:auto | :manual | nil)
|
|
42
|
+
# @rbs snapshot: Hash[Symbol, untyped]?
|
|
39
43
|
# @rbs return: Riffer::Rig::Runtime
|
|
40
44
|
def self.runtime(
|
|
41
45
|
cwd:,
|
|
@@ -48,9 +52,21 @@ class Riffer::Rig::Loader
|
|
|
48
52
|
skills: true,
|
|
49
53
|
agents_md: true,
|
|
50
54
|
tools: nil,
|
|
51
|
-
max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
|
|
55
|
+
max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS,
|
|
56
|
+
store: Riffer::Rig::Stores::JSONL.new,
|
|
57
|
+
reload: nil,
|
|
58
|
+
snapshot: nil
|
|
52
59
|
)
|
|
53
|
-
new(cwd:, host:, env:, home:, riffer_config:).runtime(
|
|
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
|
+
)
|
|
54
70
|
end
|
|
55
71
|
|
|
56
72
|
# @rbs cwd: String
|
|
@@ -58,8 +74,16 @@ class Riffer::Rig::Loader
|
|
|
58
74
|
# @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
|
|
59
75
|
# @rbs home: String
|
|
60
76
|
# @rbs riffer_config: Riffer::Config
|
|
77
|
+
# @rbs store: Riffer::Rig::Stores::_Store | nil
|
|
61
78
|
# @rbs return: void
|
|
62
|
-
def initialize(
|
|
79
|
+
def initialize(
|
|
80
|
+
cwd:,
|
|
81
|
+
host:,
|
|
82
|
+
store: Riffer::Rig::Stores::JSONL.new,
|
|
83
|
+
env: Riffer::Rig::Env.load,
|
|
84
|
+
home: Dir.home,
|
|
85
|
+
riffer_config: Riffer.config
|
|
86
|
+
)
|
|
63
87
|
raise ConfigurationError, env.message if env.is_a?(Riffer::Rig::Env::Invalid)
|
|
64
88
|
|
|
65
89
|
@cwd = cwd
|
|
@@ -67,7 +91,13 @@ class Riffer::Rig::Loader
|
|
|
67
91
|
@env = env
|
|
68
92
|
@home = home
|
|
69
93
|
@riffer_config = riffer_config
|
|
94
|
+
@store = store
|
|
70
95
|
@tracked_files = []
|
|
96
|
+
@stripped = []
|
|
97
|
+
@extensions_enabled = true
|
|
98
|
+
@gem_extensions = []
|
|
99
|
+
@file_state = nil
|
|
100
|
+
@reload_mode = nil
|
|
71
101
|
end
|
|
72
102
|
|
|
73
103
|
# @rbs model: String?
|
|
@@ -76,6 +106,8 @@ class Riffer::Rig::Loader
|
|
|
76
106
|
# @rbs agents_md: bool
|
|
77
107
|
# @rbs tools: Array[String]?
|
|
78
108
|
# @rbs max_steps: Integer?
|
|
109
|
+
# @rbs reload: (:auto | :manual | nil)
|
|
110
|
+
# @rbs snapshot: Hash[Symbol, untyped]?
|
|
79
111
|
# @rbs return: Riffer::Rig::Runtime
|
|
80
112
|
def runtime(
|
|
81
113
|
model: nil,
|
|
@@ -83,21 +115,28 @@ class Riffer::Rig::Loader
|
|
|
83
115
|
skills: true,
|
|
84
116
|
agents_md: true,
|
|
85
117
|
tools: nil,
|
|
86
|
-
max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
|
|
118
|
+
max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS,
|
|
119
|
+
reload: nil,
|
|
120
|
+
snapshot: nil
|
|
87
121
|
)
|
|
88
|
-
settings =
|
|
89
|
-
Riffer::Rig::Settings.read(home_settings_path),
|
|
90
|
-
Riffer::Rig::Settings.read(File.join(@cwd, '.riffer', 'settings.json'))
|
|
91
|
-
)
|
|
122
|
+
settings = merged_settings
|
|
92
123
|
document = Riffer::Rig::Settings::Document.new(settings)
|
|
93
124
|
selected = select_model(model || @env.model || document.model)
|
|
94
125
|
provider = Riffer::Rig::Settings.provider_for(selected).to_s
|
|
126
|
+
ensure_sdk(provider)
|
|
95
127
|
credentials = { provider.to_sym => credentials_for(provider) }
|
|
96
|
-
|
|
97
|
-
|
|
128
|
+
@extensions_enabled = extensions
|
|
129
|
+
@reload_mode = reload
|
|
130
|
+
@stripped = { skills: skills, agents_md: agents_md }.reject { |_name, kept| kept }.keys
|
|
131
|
+
loaded = Riffer::Rig::Bundled::BY_NAME.except(*document.disabled.map(&:to_sym), *@stripped).values
|
|
132
|
+
@tracked_files = []
|
|
133
|
+
@gem_extensions = []
|
|
98
134
|
loaded += load_rig_files(document.autoload) if extensions
|
|
99
135
|
|
|
100
|
-
|
|
136
|
+
recorder = recorder_for(document, resumed: !snapshot.nil?)
|
|
137
|
+
loaded << Riffer::Rig::Stores::Recorder.extension(recorder) if recorder
|
|
138
|
+
|
|
139
|
+
built = Riffer::Rig::Runtime.new(
|
|
101
140
|
selected,
|
|
102
141
|
extensions: loaded,
|
|
103
142
|
tools: tools,
|
|
@@ -108,20 +147,271 @@ class Riffer::Rig::Loader
|
|
|
108
147
|
credentials: credentials,
|
|
109
148
|
pricing: document.models,
|
|
110
149
|
riffer_config: @riffer_config,
|
|
111
|
-
model_options: Riffer::Rig::Settings.model_options(selected, document.reasoning)
|
|
150
|
+
model_options: Riffer::Rig::Settings.model_options(selected, document.reasoning),
|
|
151
|
+
native_tools: document.native_tools,
|
|
152
|
+
snapshot: snapshot
|
|
112
153
|
)
|
|
154
|
+
built.install_command(Riffer::Rig::Commands::Reload.command(self))
|
|
155
|
+
built.install_reload_check(->(runtime) { automatic_reload(runtime) }) unless reload_mode(document) == :manual
|
|
156
|
+
@file_state = file_state
|
|
157
|
+
recorder&.attach(built)
|
|
158
|
+
built
|
|
159
|
+
end
|
|
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
|
+
|
|
232
|
+
# Without force, a no-op while the tracked file set and the two rig.rb paths
|
|
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.
|
|
238
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
239
|
+
# @rbs force: bool
|
|
240
|
+
# @rbs return: (Riffer::Rig::Runtime | String)
|
|
241
|
+
def reload(runtime, force: false)
|
|
242
|
+
return runtime if !force && file_state == @file_state
|
|
243
|
+
return runtime if !force && manual_reload?
|
|
244
|
+
|
|
245
|
+
outcome = perform_reload(runtime)
|
|
246
|
+
@file_state = file_state
|
|
247
|
+
outcome
|
|
113
248
|
end
|
|
114
249
|
|
|
115
250
|
private
|
|
116
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
|
+
|
|
273
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
274
|
+
def merged_settings
|
|
275
|
+
Riffer::Rig::Settings.merge(
|
|
276
|
+
Riffer::Rig::Settings.read(home_settings_path),
|
|
277
|
+
Riffer::Rig::Settings.read(File.join(@cwd, '.riffer', 'settings.json'))
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
|
|
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.
|
|
285
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
286
|
+
# @rbs return: (Riffer::Rig::Runtime | String)
|
|
287
|
+
def perform_reload(runtime)
|
|
288
|
+
settings = merged_settings
|
|
289
|
+
document = Riffer::Rig::Settings::Document.new(settings)
|
|
290
|
+
extensions = reload_extensions(document, settings)
|
|
291
|
+
|
|
292
|
+
failure = reload_credentials(runtime)
|
|
293
|
+
return notify_failure(failure) if failure
|
|
294
|
+
|
|
295
|
+
runtime.rebuild(extensions: validated(extensions, settings), settings: settings)
|
|
296
|
+
runtime
|
|
297
|
+
rescue StandardError => e
|
|
298
|
+
notify_failure(e.message)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# @rbs message: String
|
|
302
|
+
# @rbs return: String
|
|
303
|
+
def notify_failure(message)
|
|
304
|
+
@host.notify(message, level: :error)
|
|
305
|
+
message
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# @rbs document: Riffer::Rig::Settings::Document
|
|
309
|
+
# @rbs settings: Hash[Symbol, untyped]
|
|
310
|
+
# @rbs return: Array[Riffer::Rig::Extension]
|
|
311
|
+
def reload_extensions(document, settings)
|
|
312
|
+
candidates = Riffer::Rig::Bundled::BY_NAME.except(*document.disabled.map(&:to_sym), *@stripped).values
|
|
313
|
+
return validated(candidates, settings) unless @extensions_enabled
|
|
314
|
+
|
|
315
|
+
scrub_tracked_features
|
|
316
|
+
@tracked_files = []
|
|
317
|
+
candidates.concat(@gem_extensions, rig_recordings)
|
|
318
|
+
validated(candidates, settings)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# @rbs return: Array[Riffer::Rig::Extension]
|
|
322
|
+
def rig_recordings
|
|
323
|
+
recorded = [] #: Array[Riffer::Rig::Extension]
|
|
324
|
+
recorded.concat(load_rig_file(home_rig_path, confirm_trust: false))
|
|
325
|
+
recorded.concat(load_rig_file(project_rig_path, confirm_trust: true))
|
|
326
|
+
recorded
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# @rbs path: String
|
|
330
|
+
# @rbs confirm_trust: bool
|
|
331
|
+
# @rbs return: Array[Riffer::Rig::Extension]
|
|
332
|
+
def load_rig_file(path, confirm_trust:)
|
|
333
|
+
return [] unless File.file?(path)
|
|
334
|
+
return [] if confirm_trust && !trusted?(path)
|
|
335
|
+
|
|
336
|
+
load_file(path, track: true, strict: true)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# @rbs return: void
|
|
340
|
+
def scrub_tracked_features
|
|
341
|
+
@tracked_files.each { |path| $LOADED_FEATURES.delete(path) }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Runs each extension against a throwaway registrar so that a failing block
|
|
345
|
+
# is skipped and reported instead of aborting the rebuild.
|
|
346
|
+
# @rbs extensions: Array[Riffer::Rig::Extension]
|
|
347
|
+
# @rbs settings: Hash[Symbol, untyped]
|
|
348
|
+
# @rbs return: Array[Riffer::Rig::Extension]
|
|
349
|
+
def validated(extensions, settings)
|
|
350
|
+
extensions.select do |extension|
|
|
351
|
+
registrar = Riffer::Rig::Registrar.new(extension.name, settings[extension.name.to_sym] || {})
|
|
352
|
+
error = extension.load_into(registrar)
|
|
353
|
+
next true unless error
|
|
354
|
+
|
|
355
|
+
@host.notify("Extension #{extension.name} failed to load: #{error.message}", level: :error)
|
|
356
|
+
false
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
361
|
+
# @rbs return: String?
|
|
362
|
+
def reload_credentials(runtime)
|
|
363
|
+
provider = runtime.model.partition('/').first
|
|
364
|
+
resolution = Riffer::Rig::Credentials.resolve(
|
|
365
|
+
provider,
|
|
366
|
+
host: asking_host,
|
|
367
|
+
env: @env,
|
|
368
|
+
auth_path: File.join(@home, '.riffer', 'auth.json'),
|
|
369
|
+
settings_path: home_settings_path
|
|
370
|
+
)
|
|
371
|
+
return missing_credentials(provider, resolution.missing) unless resolution.missing.empty?
|
|
372
|
+
|
|
373
|
+
Riffer::Rig::Credentials.apply(provider, resolution.values, config: @riffer_config)
|
|
374
|
+
runtime.merge_credentials(provider.to_sym, resolution.values)
|
|
375
|
+
nil
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# @rbs return: Hash[String, Time?]
|
|
379
|
+
def file_state
|
|
380
|
+
(@tracked_files + rig_paths).uniq.to_h { |path| [path, stamp(path)] }
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# @rbs return: Array[String]
|
|
384
|
+
def rig_paths
|
|
385
|
+
return [] unless @extensions_enabled
|
|
386
|
+
|
|
387
|
+
[home_rig_path, project_rig_path]
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# @rbs path: String
|
|
391
|
+
# @rbs return: Time?
|
|
392
|
+
def stamp(path)
|
|
393
|
+
File.file?(path) ? File.mtime(path) : nil
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# @rbs return: String
|
|
397
|
+
def home_rig_path
|
|
398
|
+
File.expand_path(File.join(@home, '.riffer', 'rig.rb'))
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# @rbs return: String
|
|
402
|
+
def project_rig_path
|
|
403
|
+
File.expand_path(File.join(@cwd, '.riffer', 'rig.rb'))
|
|
404
|
+
end
|
|
405
|
+
|
|
117
406
|
# @rbs autoload: bool
|
|
118
407
|
# @rbs return: Array[Riffer::Rig::Extension]
|
|
119
408
|
def load_rig_files(autoload)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
recorded.concat(load_file(
|
|
123
|
-
|
|
124
|
-
|
|
409
|
+
@gem_extensions = gem_extension_files(autoload).flat_map { |file| load_file(File.expand_path(file), track: false) }
|
|
410
|
+
recorded = @gem_extensions.dup
|
|
411
|
+
recorded.concat(load_file(home_rig_path, track: true)) if File.file?(home_rig_path)
|
|
412
|
+
if File.file?(project_rig_path) && trusted?(project_rig_path)
|
|
413
|
+
recorded.concat(load_file(project_rig_path, track: true))
|
|
414
|
+
end
|
|
125
415
|
recorded
|
|
126
416
|
end
|
|
127
417
|
|
|
@@ -150,17 +440,25 @@ class Riffer::Rig::Loader
|
|
|
150
440
|
File.join(@home, '.riffer', 'trust.json')
|
|
151
441
|
end
|
|
152
442
|
|
|
443
|
+
# Loads one file and returns the extensions it recorded. A tracked load adds
|
|
444
|
+
# the features the load added to the tracked file set. A failure is reported
|
|
445
|
+
# to the host and yields nothing, unless strict: then it propagates as
|
|
446
|
+
# AbandonedError for the reload to abandon on.
|
|
153
447
|
# @rbs path: String
|
|
154
448
|
# @rbs track: bool
|
|
449
|
+
# @rbs strict: bool
|
|
155
450
|
# @rbs return: Array[Riffer::Rig::Extension]
|
|
156
|
-
def load_file(path, track:)
|
|
451
|
+
def load_file(path, track:, strict: false)
|
|
157
452
|
before = Riffer::Rig.extensions
|
|
158
453
|
before_features = $LOADED_FEATURES.dup
|
|
159
454
|
begin
|
|
160
455
|
load path
|
|
161
456
|
rescue StandardError => e
|
|
162
457
|
track_loaded(before_features, path) if track
|
|
163
|
-
|
|
458
|
+
message = "#{path} failed to load: #{e.message}"
|
|
459
|
+
raise AbandonedError, message if strict
|
|
460
|
+
|
|
461
|
+
@host.notify(message, level: :error)
|
|
164
462
|
return []
|
|
165
463
|
end
|
|
166
464
|
track_loaded(before_features, path) if track
|
|
@@ -195,12 +493,40 @@ class Riffer::Rig::Loader
|
|
|
195
493
|
|
|
196
494
|
# @rbs return: String
|
|
197
495
|
def ask_for_model
|
|
198
|
-
answer = asking_host.ask(
|
|
199
|
-
raise ConfigurationError,
|
|
496
|
+
answer = asking_host.ask(onboarding_question).to_s.strip
|
|
497
|
+
raise ConfigurationError, no_model if answer.empty?
|
|
200
498
|
|
|
201
499
|
answer
|
|
202
500
|
end
|
|
203
501
|
|
|
502
|
+
# @rbs return: String
|
|
503
|
+
def onboarding_question
|
|
504
|
+
'Which model should riffer use? Enter provider/name, ' \
|
|
505
|
+
"with a provider from: #{provider_list}"
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# @rbs return: String
|
|
509
|
+
def no_model
|
|
510
|
+
'No model is set: pass --model provider/name, set RIFFER_MODEL, or set "model" in ' \
|
|
511
|
+
"~/.riffer/settings.json, with a provider from: #{provider_list}"
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# @rbs return: String
|
|
515
|
+
def provider_list
|
|
516
|
+
Riffer::Rig::Settings.providers.join(', ')
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# @rbs provider: String
|
|
520
|
+
# @rbs return: void
|
|
521
|
+
def ensure_sdk(provider)
|
|
522
|
+
sdk = Riffer::Rig::ProviderSetup.for(provider).sdk
|
|
523
|
+
return unless sdk
|
|
524
|
+
|
|
525
|
+
gem, requirement = sdk
|
|
526
|
+
message = Riffer::Rig::SDK.ensure(gem, requirement, host: asking_host)
|
|
527
|
+
raise ConfigurationError, message if message
|
|
528
|
+
end
|
|
529
|
+
|
|
204
530
|
# @rbs provider: String
|
|
205
531
|
# @rbs return: Hash[Symbol, String]
|
|
206
532
|
def credentials_for(provider)
|
|
@@ -222,9 +548,8 @@ class Riffer::Rig::Loader
|
|
|
222
548
|
# @rbs return: String
|
|
223
549
|
def missing_credentials(provider, missing)
|
|
224
550
|
setup = Riffer::Rig::ProviderSetup.for(provider)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
"#{provider} has no #{wanted}; set it in the environment or run riffer interactively to paste it" \
|
|
551
|
+
"#{provider} has no #{setup.missing_fields(missing)}; " \
|
|
552
|
+
'set it in the environment or run riffer interactively to paste it' \
|
|
228
553
|
"#{" (create one at #{setup.url})" if setup.url}"
|
|
229
554
|
end
|
|
230
555
|
|
|
@@ -237,4 +562,30 @@ class Riffer::Rig::Loader
|
|
|
237
562
|
def home_settings_path
|
|
238
563
|
File.join(@home, '.riffer', 'settings.json')
|
|
239
564
|
end
|
|
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
|
+
|
|
579
|
+
# The store records unless it was declined with store: nil or globally with
|
|
580
|
+
# "sessions": {"save": false}.
|
|
581
|
+
# @rbs document: Riffer::Rig::Settings::Document
|
|
582
|
+
# @rbs resumed: bool
|
|
583
|
+
# @rbs return: Riffer::Rig::Stores::Recorder?
|
|
584
|
+
def recorder_for(document, resumed:)
|
|
585
|
+
store = @store
|
|
586
|
+
return nil unless document.save
|
|
587
|
+
return nil unless store
|
|
588
|
+
|
|
589
|
+
Riffer::Rig::Stores::Recorder.new(store: store, resumed: resumed)
|
|
590
|
+
end
|
|
240
591
|
end
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Riffer::Rig::ProviderSetup
|
|
4
|
-
# @dynamic url, chain, fields
|
|
4
|
+
# @dynamic url, chain, sdk, fields
|
|
5
5
|
attr_reader :url #: String?
|
|
6
6
|
attr_reader :chain #: bool
|
|
7
|
+
attr_reader :sdk #: [String, String]?
|
|
7
8
|
attr_reader :fields #: Array[Riffer::Rig::ProviderSetup::Field]
|
|
8
9
|
|
|
9
10
|
# @rbs fields: Array[Riffer::Rig::ProviderSetup::Field]
|
|
10
11
|
# @rbs url: String?
|
|
11
12
|
# @rbs chain: bool
|
|
13
|
+
# @rbs sdk: [String, String]?
|
|
12
14
|
# @rbs return: void
|
|
13
|
-
def initialize(fields:, url: nil, chain: false)
|
|
15
|
+
def initialize(fields:, url: nil, chain: false, sdk: nil)
|
|
14
16
|
@url = url
|
|
15
17
|
@chain = chain
|
|
18
|
+
@sdk = sdk&.freeze
|
|
16
19
|
@fields = fields.freeze
|
|
17
20
|
freeze
|
|
18
21
|
end
|
|
@@ -24,15 +27,17 @@ class Riffer::Rig::ProviderSetup
|
|
|
24
27
|
nil
|
|
25
28
|
end #: ^() -> String?
|
|
26
29
|
|
|
27
|
-
# Upstream candidate:
|
|
28
|
-
# fields, and this table would go.
|
|
30
|
+
# Upstream candidate: riffer's providers could declare their own SDK gem and
|
|
31
|
+
# credential fields, and this table would go.
|
|
29
32
|
TABLE = {
|
|
30
33
|
anthropic: new(
|
|
31
34
|
url: 'https://console.anthropic.com/settings/keys',
|
|
35
|
+
sdk: ['anthropic', '~> 1.69'],
|
|
32
36
|
fields: [Field.new(name: :api_key, env: ['ANTHROPIC_API_KEY'], secret: true, required: true)]
|
|
33
37
|
),
|
|
34
38
|
openai: new(
|
|
35
39
|
url: 'https://platform.openai.com/api-keys',
|
|
40
|
+
sdk: ['openai', '~> 0.80'],
|
|
36
41
|
fields: [
|
|
37
42
|
Field.new(name: :api_key, env: ['OPENAI_API_KEY'], secret: true, required: true),
|
|
38
43
|
Field.new(name: :base_url, env: ['OPENAI_BASE_URL'], secret: false, required: false)
|
|
@@ -44,10 +49,12 @@ class Riffer::Rig::ProviderSetup
|
|
|
44
49
|
),
|
|
45
50
|
openrouter: new(
|
|
46
51
|
url: 'https://openrouter.ai/keys',
|
|
52
|
+
sdk: ['openai', '~> 0.80'],
|
|
47
53
|
fields: [Field.new(name: :api_key, env: ['OPENROUTER_API_KEY'], secret: true, required: true)]
|
|
48
54
|
),
|
|
49
55
|
azure_openai: new(
|
|
50
56
|
url: 'https://portal.azure.com',
|
|
57
|
+
sdk: ['openai', '~> 0.80'],
|
|
51
58
|
fields: [
|
|
52
59
|
Field.new(name: :endpoint, env: ['AZURE_OPENAI_ENDPOINT'], secret: false, required: true),
|
|
53
60
|
Field.new(name: :api_key, env: ['AZURE_OPENAI_API_KEY'], secret: true, required: true)
|
|
@@ -56,6 +63,7 @@ class Riffer::Rig::ProviderSetup
|
|
|
56
63
|
amazon_bedrock: new(
|
|
57
64
|
url: 'https://console.aws.amazon.com/bedrock',
|
|
58
65
|
chain: true,
|
|
66
|
+
sdk: ['aws-sdk-bedrockruntime', '~> 1.0'],
|
|
59
67
|
fields: [
|
|
60
68
|
Field.new(
|
|
61
69
|
name: :region,
|
|
@@ -69,10 +77,33 @@ class Riffer::Rig::ProviderSetup
|
|
|
69
77
|
)
|
|
70
78
|
}.freeze #: Hash[Symbol, Riffer::Rig::ProviderSetup]
|
|
71
79
|
|
|
80
|
+
# @rbs missing: Array[Symbol]
|
|
81
|
+
# @rbs return: String
|
|
82
|
+
def missing_fields(missing)
|
|
83
|
+
fields.select { |field| missing.include?(field.name) }
|
|
84
|
+
.map { |field| "#{field.name} (#{field.env.join(' or ')})" }
|
|
85
|
+
.join(', ')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @rbs hash: Hash[Symbol, untyped]
|
|
89
|
+
# @rbs return: Riffer::Rig::ProviderSetup
|
|
90
|
+
def self.from(hash)
|
|
91
|
+
fields = hash.fetch(:fields).map do |field|
|
|
92
|
+
Field.new(
|
|
93
|
+
name: field.fetch(:name),
|
|
94
|
+
env: field.fetch(:env),
|
|
95
|
+
secret: field.fetch(:secret, false),
|
|
96
|
+
required: field.fetch(:required, false),
|
|
97
|
+
fallback: field[:fallback]
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
new(fields: fields, url: hash[:url], chain: hash.fetch(:chain, false), sdk: hash[:sdk])
|
|
101
|
+
end
|
|
102
|
+
|
|
72
103
|
# @rbs identifier: String | Symbol
|
|
73
104
|
# @rbs return: Riffer::Rig::ProviderSetup?
|
|
74
105
|
def self.[](identifier)
|
|
75
|
-
TABLE[identifier.to_sym]
|
|
106
|
+
Riffer::Rig::Providers.setup(identifier) || TABLE[identifier.to_sym]
|
|
76
107
|
end
|
|
77
108
|
|
|
78
109
|
# @rbs identifier: String | Symbol
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Riffer::Rig::Providers
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
# Upstream candidate: riffer's Repository exposes no listing of extension
|
|
7
|
+
# registrations and stores no credential setups, so this registry mirrors
|
|
8
|
+
# what the provider seam registers.
|
|
9
|
+
# @rbs @setups: Hash[Symbol, Riffer::Rig::ProviderSetup?]
|
|
10
|
+
@setups = {} #: Hash[Symbol, Riffer::Rig::ProviderSetup?]
|
|
11
|
+
|
|
12
|
+
# @rbs identifier: String | Symbol
|
|
13
|
+
# @rbs setup: Riffer::Rig::ProviderSetup | Hash[Symbol, untyped]?
|
|
14
|
+
# @rbs &block: () -> singleton(::Riffer::Providers::Base)
|
|
15
|
+
# @rbs return: void
|
|
16
|
+
def register(identifier, setup: nil, &)
|
|
17
|
+
::Riffer::Providers::Repository.register(identifier, &)
|
|
18
|
+
@setups[identifier.to_sym] = setup && normalize(setup)
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs identifier: String | Symbol
|
|
23
|
+
# @rbs return: void
|
|
24
|
+
def unregister(identifier)
|
|
25
|
+
::Riffer::Providers::Repository.unregister(identifier)
|
|
26
|
+
@setups.delete(identifier.to_sym)
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @rbs identifier: String | Symbol
|
|
31
|
+
# @rbs return: Riffer::Rig::ProviderSetup?
|
|
32
|
+
def setup(identifier)
|
|
33
|
+
@setups[identifier.to_sym]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @rbs return: Array[Symbol]
|
|
37
|
+
def identifiers
|
|
38
|
+
(::Riffer::Providers::Repository::REPO.keys - [:mock]) | @setups.keys
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# @rbs setup: Riffer::Rig::ProviderSetup | Hash[Symbol, untyped]
|
|
44
|
+
# @rbs return: Riffer::Rig::ProviderSetup
|
|
45
|
+
def normalize(setup)
|
|
46
|
+
setup.is_a?(::Riffer::Rig::ProviderSetup) ? setup : ::Riffer::Rig::ProviderSetup.from(setup)
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/riffer/rig/registrar.rb
CHANGED
|
@@ -88,6 +88,14 @@ class Riffer::Rig::Registrar
|
|
|
88
88
|
@mcp_servers[name] = Riffer::Rig::Mcp::Declaration.new(url: url, headers: headers)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# @rbs prefix: String | Symbol
|
|
92
|
+
# @rbs setup: Riffer::Rig::ProviderSetup | Hash[Symbol, untyped]?
|
|
93
|
+
# @rbs &block: () -> singleton(::Riffer::Providers::Base)
|
|
94
|
+
# @rbs return: void
|
|
95
|
+
def provider(prefix, setup: nil, &)
|
|
96
|
+
Riffer::Rig::Providers.register(prefix, setup:, &)
|
|
97
|
+
end
|
|
98
|
+
|
|
91
99
|
# @rbs event: Symbol
|
|
92
100
|
# @rbs &block: (Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base) -> untyped
|
|
93
101
|
# @rbs return: void
|