riffer-rig 0.7.0 → 0.9.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 -3
- data/lib/riffer/rig/cli/flags.rb +16 -2
- data/lib/riffer/rig/cli.rb +2 -1
- 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 +327 -23
- 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 +110 -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 +115 -0
- data/lib/riffer/rig/stores/recorder.rb +119 -0
- data/lib/riffer/rig/terminal/renderer.rb +86 -13
- data/lib/riffer/rig/terminal/wrapper.rb +103 -0
- data/lib/riffer/rig/trust.rb +37 -0
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +7 -1
- data/sig/generated/riffer/rig/cli/flags.rbs +5 -2
- 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 +142 -5
- 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 +63 -9
- 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 +64 -0
- data/sig/generated/riffer/rig/stores/recorder.rbs +67 -0
- data/sig/generated/riffer/rig/terminal/renderer.rbs +41 -1
- data/sig/generated/riffer/rig/terminal/wrapper.rbs +43 -0
- data/sig/generated/riffer/rig/trust.rbs +20 -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 +11 -0
- data/sig/manual/riffer/rig/trust.rbs +6 -0
- metadata +23 -29
data/lib/riffer/rig/runtime.rb
CHANGED
|
@@ -29,6 +29,15 @@ class Riffer::Rig::Runtime
|
|
|
29
29
|
|
|
30
30
|
INTERRUPT_CANCELLED = :cancelled #: Symbol
|
|
31
31
|
|
|
32
|
+
# Upstream candidate: riffer's Anthropic, OpenAI and Azure OpenAI classes read
|
|
33
|
+
# the web_search option and OpenRouter passes unknown options through to its
|
|
34
|
+
# API, but the provider seam cannot yet answer which provider takes which
|
|
35
|
+
# option, so rig keeps the mapping until it can. Mock consumes web_search, so
|
|
36
|
+
# tests and embedders can exercise the switches.
|
|
37
|
+
NATIVE_TOOLS_BY_PROVIDER = {
|
|
38
|
+
web_search: { anthropic: true, openai: true, azure_openai: true, mock: true }.freeze
|
|
39
|
+
}.freeze #: Hash[Symbol, Hash[Symbol, bool]]
|
|
40
|
+
|
|
32
41
|
# @rbs @agent: Riffer::Agent
|
|
33
42
|
# @rbs @base_prompt: String
|
|
34
43
|
# @rbs @cancel_flag: Riffer::Rig::Runtime::CancelFlag
|
|
@@ -43,8 +52,12 @@ class Riffer::Rig::Runtime
|
|
|
43
52
|
# @rbs @session_start_reason: Symbol
|
|
44
53
|
# @rbs @model_override: String?
|
|
45
54
|
# @rbs @message_observers: Array[^(Riffer::Messages::Base) -> void]
|
|
55
|
+
# @rbs @model_observers: Array[^(String) -> void]
|
|
46
56
|
# @rbs @prompts: Hash[Symbol, ^(Riffer::Rig::Runtime) -> String?]
|
|
47
57
|
# @rbs @commands: Hash[String, Riffer::Rig::Command]
|
|
58
|
+
# @rbs @core_commands: Array[Riffer::Rig::Command]
|
|
59
|
+
# @rbs @claim_depth: Integer
|
|
60
|
+
# @rbs @claim_thread: Thread?
|
|
48
61
|
# @rbs @declared_settings: Hash[String, Hash[Symbol, untyped]]
|
|
49
62
|
# @rbs @errors: Array[Riffer::Rig::Extension::Failure]
|
|
50
63
|
# @rbs @hooks: Riffer::Rig::Runtime::Hooks
|
|
@@ -52,6 +65,7 @@ class Riffer::Rig::Runtime
|
|
|
52
65
|
# @rbs @mcp_registry: Riffer::Rig::Mcp::_Registry
|
|
53
66
|
# @rbs @mcp_servers: Hash[String, Riffer::Rig::Mcp::Server]
|
|
54
67
|
# @rbs @model_options: Hash[Symbol, untyped]
|
|
68
|
+
# @rbs @native_tools: Hash[Symbol, untyped]
|
|
55
69
|
|
|
56
70
|
# @dynamic agent, credentials, cwd, host, id, settings, declared_settings
|
|
57
71
|
attr_reader :agent #: Riffer::Agent
|
|
@@ -76,6 +90,7 @@ class Riffer::Rig::Runtime
|
|
|
76
90
|
# @rbs mcp_registry: Riffer::Rig::Mcp::_Registry
|
|
77
91
|
# @rbs max_steps: Integer?
|
|
78
92
|
# @rbs model_options: Hash[Symbol, untyped]
|
|
93
|
+
# @rbs native_tools: Hash[Symbol, untyped]
|
|
79
94
|
# @rbs snapshot: Hash[Symbol, untyped]?
|
|
80
95
|
# @rbs return: void
|
|
81
96
|
def initialize(
|
|
@@ -93,6 +108,7 @@ class Riffer::Rig::Runtime
|
|
|
93
108
|
mcp_registry: Riffer::Mcp,
|
|
94
109
|
max_steps: DEFAULT_MAX_STEPS,
|
|
95
110
|
model_options: {},
|
|
111
|
+
native_tools: {},
|
|
96
112
|
snapshot: nil
|
|
97
113
|
)
|
|
98
114
|
# Doubles as the snapshot id and ACP sessionId.
|
|
@@ -103,14 +119,19 @@ class Riffer::Rig::Runtime
|
|
|
103
119
|
|
|
104
120
|
@busy = false
|
|
105
121
|
@closed = false
|
|
122
|
+
@claim_depth = 0
|
|
123
|
+
@claim_thread = nil
|
|
124
|
+
@core_commands = []
|
|
106
125
|
@cancel_flag = Riffer::Rig::Runtime::CancelFlag.new
|
|
107
126
|
@session_start_pending = true
|
|
108
127
|
@session_start_reason = snapshot ? :restore : :new
|
|
109
128
|
@message_observers = []
|
|
129
|
+
@model_observers = []
|
|
110
130
|
Riffer::Rig::Settings::Pricing.register(pricing, riffer_config.pricing)
|
|
111
131
|
@errors = []
|
|
112
132
|
@tool_allowlist = tools
|
|
113
|
-
@
|
|
133
|
+
@native_tools = native_tools
|
|
134
|
+
@model_options = model_options.merge(native_options(model))
|
|
114
135
|
@base_prompt = instructions || format(BASE_PROMPT_TEMPLATE, name: name)
|
|
115
136
|
@mcp_registry = mcp_registry
|
|
116
137
|
@mcp_servers = {}
|
|
@@ -136,7 +157,7 @@ class Riffer::Rig::Runtime
|
|
|
136
157
|
turn(text)
|
|
137
158
|
end
|
|
138
159
|
ensure
|
|
139
|
-
|
|
160
|
+
release
|
|
140
161
|
end
|
|
141
162
|
end
|
|
142
163
|
|
|
@@ -147,7 +168,7 @@ class Riffer::Rig::Runtime
|
|
|
147
168
|
begin
|
|
148
169
|
turn(text).each { |event| event }
|
|
149
170
|
ensure
|
|
150
|
-
|
|
171
|
+
release
|
|
151
172
|
end
|
|
152
173
|
end
|
|
153
174
|
|
|
@@ -156,6 +177,16 @@ class Riffer::Rig::Runtime
|
|
|
156
177
|
@commands.values
|
|
157
178
|
end
|
|
158
179
|
|
|
180
|
+
# Installs a host-of-the-runtime command (the Loader's /reload) so it
|
|
181
|
+
# survives every rebuild; an extension command of the same name replaces it.
|
|
182
|
+
# @rbs command: Riffer::Rig::Command
|
|
183
|
+
# @rbs return: nil
|
|
184
|
+
def install_command(command)
|
|
185
|
+
@core_commands << command
|
|
186
|
+
@commands[command.name] = command
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
159
190
|
# @rbs name: String
|
|
160
191
|
# @rbs args: String
|
|
161
192
|
# @rbs &block: ?(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void
|
|
@@ -166,7 +197,7 @@ class Riffer::Rig::Runtime
|
|
|
166
197
|
begin
|
|
167
198
|
execute(name, args, emit)
|
|
168
199
|
ensure
|
|
169
|
-
|
|
200
|
+
release
|
|
170
201
|
end
|
|
171
202
|
@host.drain.each(&emit)
|
|
172
203
|
nil
|
|
@@ -187,15 +218,25 @@ class Riffer::Rig::Runtime
|
|
|
187
218
|
def model=(model)
|
|
188
219
|
# Upstream candidate: riffer resolves the model Proc once, in Agent.new, so
|
|
189
220
|
# a switch rebuilds the agent over the same session and config.
|
|
190
|
-
|
|
221
|
+
agent = successor(model, @agent.config)
|
|
222
|
+
rederive_options(model)
|
|
223
|
+
@agent = agent
|
|
191
224
|
@model_override = model
|
|
225
|
+
@model_observers.each { |observer| observer.call(model) }
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @rbs provider: Symbol
|
|
229
|
+
# @rbs values: Hash[Symbol, String]
|
|
230
|
+
# @rbs return: void
|
|
231
|
+
def merge_credentials(provider, values)
|
|
232
|
+
@credentials = @credentials.merge(provider => values)
|
|
192
233
|
end
|
|
193
234
|
|
|
194
235
|
# @rbs extensions: Array[Riffer::Rig::Extension]
|
|
195
236
|
# @rbs settings: Hash[Symbol, untyped]
|
|
196
237
|
# @rbs return: nil
|
|
197
238
|
def rebuild(extensions:, settings:)
|
|
198
|
-
|
|
239
|
+
claim_rebuild
|
|
199
240
|
begin
|
|
200
241
|
registrars = build_registrars(extensions, settings) { |_extension, error| raise error }
|
|
201
242
|
mcp_servers = register_mcp_servers(registrars)
|
|
@@ -208,7 +249,7 @@ class Riffer::Rig::Runtime
|
|
|
208
249
|
unregister_mcp_servers(dropped)
|
|
209
250
|
lifecycle(:session_start, Riffer::Rig::Events::SessionStart.new(@id, :reload))
|
|
210
251
|
ensure
|
|
211
|
-
|
|
252
|
+
release
|
|
212
253
|
end
|
|
213
254
|
nil
|
|
214
255
|
end
|
|
@@ -225,6 +266,13 @@ class Riffer::Rig::Runtime
|
|
|
225
266
|
nil
|
|
226
267
|
end
|
|
227
268
|
|
|
269
|
+
# @rbs &block: (String) -> void
|
|
270
|
+
# @rbs return: nil
|
|
271
|
+
def on_model_change(&block)
|
|
272
|
+
@model_observers << block
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
|
|
228
276
|
# @rbs return: nil
|
|
229
277
|
def cancel
|
|
230
278
|
@cancel_flag.set
|
|
@@ -255,6 +303,31 @@ class Riffer::Rig::Runtime
|
|
|
255
303
|
raise ClosedError, 'this Runtime is closed' if @closed
|
|
256
304
|
|
|
257
305
|
@busy = true
|
|
306
|
+
@claim_depth += 1
|
|
307
|
+
@claim_thread = Thread.current
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Rebuild is a configuration swap at a quiet boundary, not a turn entry, so
|
|
311
|
+
# it may run nested under the thread that holds the Runtime — the /reload
|
|
312
|
+
# command and the automatic reload check both do. Another thread is still
|
|
313
|
+
# refused while work runs.
|
|
314
|
+
# @rbs return: void
|
|
315
|
+
def claim_rebuild
|
|
316
|
+
raise BusyError, 'a prompt is already running on this Runtime' if @busy && @claim_thread != Thread.current
|
|
317
|
+
raise ClosedError, 'this Runtime is closed' if @closed
|
|
318
|
+
|
|
319
|
+
@busy = true
|
|
320
|
+
@claim_depth += 1
|
|
321
|
+
@claim_thread = Thread.current
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# @rbs return: void
|
|
325
|
+
def release
|
|
326
|
+
@claim_depth -= 1
|
|
327
|
+
return if @claim_depth.positive?
|
|
328
|
+
|
|
329
|
+
@busy = false
|
|
330
|
+
@claim_thread = nil
|
|
258
331
|
end
|
|
259
332
|
|
|
260
333
|
# @rbs model: String
|
|
@@ -275,6 +348,33 @@ class Riffer::Rig::Runtime
|
|
|
275
348
|
agent
|
|
276
349
|
end
|
|
277
350
|
|
|
351
|
+
# The options the settings built for the starting model are provider-shaped
|
|
352
|
+
# (cache control, per-provider reasoning levels), so a switch re-derives them.
|
|
353
|
+
# Runs after the successor is built, so a failed build leaves the running
|
|
354
|
+
# agent's options alone; riffer reads config.model_options per request, so the
|
|
355
|
+
# swap still reaches the new agent through the shared config.
|
|
356
|
+
# @rbs model: String
|
|
357
|
+
# @rbs return: void
|
|
358
|
+
def rederive_options(model)
|
|
359
|
+
reasoning = Riffer::Rig::Settings::Document.new(@settings).reasoning
|
|
360
|
+
@model_options = Riffer::Rig::Settings.model_options(model, reasoning).merge(native_options(model))
|
|
361
|
+
@agent.config.model_options = @model_options
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# The switches are on/off settings, so what the agent can do stays a function
|
|
365
|
+
# of the current provider: a switch whose provider cannot take the option
|
|
366
|
+
# contributes nothing, and a switch to such a provider drops it, both silently.
|
|
367
|
+
# @rbs model: String
|
|
368
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
369
|
+
def native_options(model)
|
|
370
|
+
provider = Riffer::Rig::Settings.provider_for(model)&.to_sym
|
|
371
|
+
@native_tools.filter_map do |tool, switch|
|
|
372
|
+
next unless provider && NATIVE_TOOLS_BY_PROVIDER[tool]&.key?(provider)
|
|
373
|
+
|
|
374
|
+
[tool, switch]
|
|
375
|
+
end.to_h
|
|
376
|
+
end
|
|
377
|
+
|
|
278
378
|
# @rbs registrars: Array[Riffer::Rig::Registrar]
|
|
279
379
|
# @rbs hooks: Riffer::Rig::Runtime::Hooks
|
|
280
380
|
# @rbs max_steps: Numeric?
|
|
@@ -321,6 +421,8 @@ class Riffer::Rig::Runtime
|
|
|
321
421
|
@prompts = registrars.flat_map { |registrar| registrar.prompts.to_a }.to_h
|
|
322
422
|
skills = agent.context.skills&.skills&.values || []
|
|
323
423
|
commands = [
|
|
424
|
+
*@core_commands,
|
|
425
|
+
Riffer::Rig::Commands::Auth.command,
|
|
324
426
|
Riffer::Rig::Commands::Model.command,
|
|
325
427
|
*skills.map { |skill| Riffer::Rig::Commands::Skill.command(skill) },
|
|
326
428
|
*registrars.flat_map { |registrar| registrar.commands.values }
|
|
@@ -545,7 +647,7 @@ class Riffer::Rig::Runtime
|
|
|
545
647
|
def build_registrars(extensions, settings)
|
|
546
648
|
extensions.filter_map do |extension|
|
|
547
649
|
registrar = Riffer::Rig::Registrar.new(extension.name, settings[extension.name.to_sym] || {})
|
|
548
|
-
error =
|
|
650
|
+
error = extension.load_into(registrar)
|
|
549
651
|
next registrar unless error
|
|
550
652
|
|
|
551
653
|
yield(extension, error)
|
|
@@ -553,19 +655,6 @@ class Riffer::Rig::Runtime
|
|
|
553
655
|
end
|
|
554
656
|
end
|
|
555
657
|
|
|
556
|
-
# @rbs extension: Riffer::Rig::Extension
|
|
557
|
-
# @rbs registrar: Riffer::Rig::Registrar
|
|
558
|
-
# @rbs return: StandardError?
|
|
559
|
-
def load_extension(extension, registrar)
|
|
560
|
-
rejection = extension.mismatch || registrar.collision
|
|
561
|
-
return rejection if rejection
|
|
562
|
-
|
|
563
|
-
extension.run(registrar)
|
|
564
|
-
nil
|
|
565
|
-
rescue StandardError => e
|
|
566
|
-
e
|
|
567
|
-
end
|
|
568
|
-
|
|
569
658
|
# @rbs extension: Riffer::Rig::Extension
|
|
570
659
|
# @rbs error: StandardError
|
|
571
660
|
# @rbs return: void
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Riffer::Rig::SDK
|
|
4
|
+
extend self
|
|
5
|
+
|
|
6
|
+
CONFIRM_QUESTION = 'Install the %s gem now?' #: String
|
|
7
|
+
|
|
8
|
+
PROGRESS_LABEL = 'Installing %s %s' #: String
|
|
9
|
+
|
|
10
|
+
BUNDLER_MESSAGE = "%s is not installed and Bundler manages the load path; add gem '%s', '%s' to the Gemfile" #: String
|
|
11
|
+
|
|
12
|
+
DECLINED_MESSAGE = "%s is not installed; install it with gem install %s -v '%s' or add it to a Gemfile" #: String
|
|
13
|
+
|
|
14
|
+
FAILED_MESSAGE = '%s was installed but could not be loaded' #: String
|
|
15
|
+
|
|
16
|
+
private_constant :CONFIRM_QUESTION, :PROGRESS_LABEL, :BUNDLER_MESSAGE, :DECLINED_MESSAGE, :FAILED_MESSAGE
|
|
17
|
+
|
|
18
|
+
# @rbs gem: String
|
|
19
|
+
# @rbs requirement: String
|
|
20
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
21
|
+
# @rbs return: String?
|
|
22
|
+
def ensure(gem, requirement, host:)
|
|
23
|
+
return nil if installed?(gem, requirement) && require_gem(gem)
|
|
24
|
+
return format(BUNDLER_MESSAGE, gem, gem, requirement) if bundler?
|
|
25
|
+
|
|
26
|
+
return format(DECLINED_MESSAGE, gem, gem, requirement) unless host.confirm(format(CONFIRM_QUESTION, gem))
|
|
27
|
+
|
|
28
|
+
host.progress(format(PROGRESS_LABEL, gem, requirement)) do
|
|
29
|
+
Gem.install(gem, Gem::Requirement.new(requirement))
|
|
30
|
+
Gem.clear_paths
|
|
31
|
+
end
|
|
32
|
+
return nil if require_gem(gem)
|
|
33
|
+
|
|
34
|
+
format(FAILED_MESSAGE, gem)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# @rbs gem: String
|
|
40
|
+
# @rbs requirement: String
|
|
41
|
+
# @rbs return: bool
|
|
42
|
+
def installed?(gem, requirement)
|
|
43
|
+
Gem::Specification.find_by_name(gem, requirement)
|
|
44
|
+
true
|
|
45
|
+
rescue Gem::LoadError
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @rbs gem: String
|
|
50
|
+
# @rbs return: bool
|
|
51
|
+
def require_gem(gem)
|
|
52
|
+
require gem
|
|
53
|
+
true
|
|
54
|
+
rescue ::LoadError
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @rbs return: bool
|
|
59
|
+
def bundler?
|
|
60
|
+
!defined?(Bundler).nil?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Riffer::Rig::Settings::Document
|
|
4
|
-
# @dynamic model, reasoning, models, providers, disabled
|
|
4
|
+
# @dynamic model, reasoning, models, providers, disabled, autoload, native_tools, save
|
|
5
5
|
attr_reader :model #: String?
|
|
6
6
|
attr_reader :reasoning #: String?
|
|
7
7
|
attr_reader :models #: Hash[String, Riffer::Rig::Settings::Pricing]
|
|
8
8
|
attr_reader :providers #: Hash[String, Hash[String, String]]
|
|
9
9
|
attr_reader :disabled #: Array[String]
|
|
10
|
+
attr_reader :autoload #: bool
|
|
11
|
+
attr_reader :native_tools #: Hash[Symbol, bool | Hash[Symbol, untyped]]
|
|
12
|
+
attr_reader :save #: bool
|
|
10
13
|
|
|
11
14
|
# @rbs source: Hash[Symbol, untyped]
|
|
12
15
|
# @rbs return: void
|
|
@@ -26,6 +29,11 @@ class Riffer::Rig::Settings::Document
|
|
|
26
29
|
disabled = hash_or_empty(source[:extensions])[:disabled]
|
|
27
30
|
names = disabled.is_a?(Array) ? disabled.grep(String) : [] #: Array[String]
|
|
28
31
|
@disabled = names.freeze
|
|
32
|
+
@autoload = hash_or_empty(source[:extensions])[:autoload] == true
|
|
33
|
+
@save = hash_or_empty(source[:sessions])[:save] != false
|
|
34
|
+
@native_tools = hash_or_empty(hash_or_empty(source[:tools])[:native])
|
|
35
|
+
.select { |_tool, switch| switch == true || switch.is_a?(Hash) }
|
|
36
|
+
.transform_keys(&:to_sym).freeze
|
|
29
37
|
freeze
|
|
30
38
|
end
|
|
31
39
|
|
data/lib/riffer/rig/settings.rb
CHANGED
|
@@ -16,9 +16,10 @@ module Riffer::Rig::Settings
|
|
|
16
16
|
|
|
17
17
|
MODEL_STRING = %r{\A(?<provider>[^/\s]+)/\S+\z} #: Regexp
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
# @rbs return: Array[Symbol]
|
|
20
|
+
def providers
|
|
21
|
+
Riffer::Rig::Providers.identifiers
|
|
22
|
+
end
|
|
22
23
|
|
|
23
24
|
# @rbs path: String
|
|
24
25
|
# @rbs return: Hash[Symbol, untyped]
|
|
@@ -62,7 +63,7 @@ module Riffer::Rig::Settings
|
|
|
62
63
|
provider = provider_for(model)
|
|
63
64
|
return if provider && Riffer::Providers::Repository.find(provider)
|
|
64
65
|
|
|
65
|
-
"#{model} is not a provider/name model string; the provider is one of: #{
|
|
66
|
+
"#{model} is not a provider/name model string; the provider is one of: #{providers.join(', ')}"
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
# @rbs model: String
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
class Riffer::Rig::Stores::JSONL
|
|
7
|
+
HEADER_VERSION = 1 #: Integer
|
|
8
|
+
|
|
9
|
+
# @rbs @home: String
|
|
10
|
+
# @rbs @paths: Hash[String, String]
|
|
11
|
+
|
|
12
|
+
# @rbs home: String
|
|
13
|
+
# @rbs return: void
|
|
14
|
+
def initialize(home: Dir.home)
|
|
15
|
+
@home = home
|
|
16
|
+
@paths = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @rbs id: String
|
|
20
|
+
# @rbs entry: Hash[Symbol, untyped]
|
|
21
|
+
# @rbs return: void
|
|
22
|
+
def append(id, entry)
|
|
23
|
+
File.write(path_for(id, entry), "#{JSON.generate(entry)}\n", mode: 'a')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @rbs id: String
|
|
27
|
+
# @rbs return: Array[Hash[Symbol, untyped]]
|
|
28
|
+
def read(id)
|
|
29
|
+
path = find(id)
|
|
30
|
+
return [] unless path
|
|
31
|
+
|
|
32
|
+
File.foreach(path).filter_map { |line| parse(line) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @rbs cwd: String?
|
|
36
|
+
# @rbs return: Array[Hash[Symbol, untyped]]
|
|
37
|
+
def list(cwd: nil)
|
|
38
|
+
headers = Dir.glob(File.join(root, '*', '*.jsonl')).filter_map { |path| header_of(path) }
|
|
39
|
+
cwd ? headers.select { |header| header[:cwd] == cwd } : headers
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @rbs id: String
|
|
43
|
+
# @rbs return: void
|
|
44
|
+
def delete(id)
|
|
45
|
+
path = find(id)
|
|
46
|
+
return unless path
|
|
47
|
+
|
|
48
|
+
File.delete(path)
|
|
49
|
+
@paths.delete(id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# @rbs return: String
|
|
55
|
+
def root
|
|
56
|
+
File.join(@home, '.riffer', 'sessions')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @rbs id: String
|
|
60
|
+
# @rbs entry: Hash[Symbol, untyped]
|
|
61
|
+
# @rbs return: String
|
|
62
|
+
def path_for(id, entry)
|
|
63
|
+
@paths.fetch(id) do
|
|
64
|
+
path = entry[:type] == 'header' ? new_path(id, entry.fetch(:cwd)) : find(id)
|
|
65
|
+
raise ArgumentError, "no session file for #{id}" unless path
|
|
66
|
+
|
|
67
|
+
@paths[id] = path
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @rbs id: String
|
|
72
|
+
# @rbs cwd: String
|
|
73
|
+
# @rbs return: String
|
|
74
|
+
def new_path(id, cwd)
|
|
75
|
+
dir = File.join(root, slug(cwd))
|
|
76
|
+
FileUtils.mkdir_p(dir)
|
|
77
|
+
File.join(dir, "#{id}.jsonl")
|
|
78
|
+
end
|
|
79
|
+
|
|
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
|
+
# @rbs id: String
|
|
89
|
+
# @rbs return: String?
|
|
90
|
+
def find(id)
|
|
91
|
+
@paths.fetch(id) do
|
|
92
|
+
found = Dir.glob(File.join(root, '*', "#{id}.jsonl")).first
|
|
93
|
+
@paths[id] = found if found
|
|
94
|
+
found
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The slug groups a cwd's sessions for listing; it is not the truth — the
|
|
99
|
+
# header's cwd is, so two cwd strings that slug alike share a directory. A
|
|
100
|
+
# bare "." or ".." is collapsed so the file stays inside the sessions tree.
|
|
101
|
+
# @rbs cwd: String
|
|
102
|
+
# @rbs return: String
|
|
103
|
+
def slug(cwd)
|
|
104
|
+
slug = cwd.gsub(/[^A-Za-z0-9._-]+/, '-').gsub(/\A-+|-+\z/, '')
|
|
105
|
+
slug.empty? || slug.match?(/\A\.{1,2}\z/) ? '-' : slug
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @rbs line: String
|
|
109
|
+
# @rbs return: Hash[Symbol, untyped]?
|
|
110
|
+
def parse(line)
|
|
111
|
+
JSON.parse(line, symbolize_names: true)
|
|
112
|
+
rescue JSON::ParserError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
class Riffer::Rig::Stores::Recorder
|
|
6
|
+
EXTENSION_NAME = 'session-store' #: String
|
|
7
|
+
|
|
8
|
+
# @rbs @store: Riffer::Rig::Stores::_Store
|
|
9
|
+
# @rbs @runtime: Riffer::Rig::Runtime?
|
|
10
|
+
# @rbs @header_written: bool
|
|
11
|
+
# @rbs @pending: Array[Hash[Symbol, untyped]]
|
|
12
|
+
# @rbs @title: String?
|
|
13
|
+
|
|
14
|
+
# Records one Loader-built session into the store: the header on the first
|
|
15
|
+
# message (its title needs the first prompt), one entry per message, one per
|
|
16
|
+
# 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
|
+
# @rbs store: Riffer::Rig::Stores::_Store
|
|
19
|
+
# @rbs return: void
|
|
20
|
+
def initialize(store:)
|
|
21
|
+
@store = store
|
|
22
|
+
@runtime = nil
|
|
23
|
+
@header_written = false
|
|
24
|
+
@pending = []
|
|
25
|
+
@title = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
29
|
+
# @rbs return: void
|
|
30
|
+
def attach(runtime)
|
|
31
|
+
@runtime = runtime
|
|
32
|
+
runtime.on_message { |message| message(message) }
|
|
33
|
+
runtime.on_model_change { |model| switched(model) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @rbs event: Riffer::StreamEvents::SkillActivation
|
|
37
|
+
# @rbs return: void
|
|
38
|
+
def skill(event)
|
|
39
|
+
record(type: 'skill', skill: event.name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @rbs message: Riffer::Messages::Base
|
|
43
|
+
# @rbs return: void
|
|
44
|
+
def message(message)
|
|
45
|
+
@title ||= one_line(message.content) if message.is_a?(Riffer::Messages::User)
|
|
46
|
+
write_header unless @header_written
|
|
47
|
+
record(type: 'message', message: message.to_h)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @rbs model: String
|
|
51
|
+
# @rbs return: void
|
|
52
|
+
def switched(model)
|
|
53
|
+
record(type: 'model', model: model)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The extension rides the build as its last extension, so its :stream hook
|
|
57
|
+
# sees the model's skill activations. Per build, unlike the extensions in
|
|
58
|
+
# the process registry: the hook closes over this build's recorder.
|
|
59
|
+
# @rbs recorder: Riffer::Rig::Stores::Recorder
|
|
60
|
+
# @rbs return: Riffer::Rig::Extension
|
|
61
|
+
def self.extension(recorder)
|
|
62
|
+
Riffer::Rig::Extension.new(EXTENSION_NAME) do |registrar|
|
|
63
|
+
registrar.on(:stream) do |event|
|
|
64
|
+
case event
|
|
65
|
+
when Riffer::StreamEvents::SkillActivation then recorder.skill(event)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# @rbs entry: Hash[Symbol, untyped]
|
|
74
|
+
# @rbs return: void
|
|
75
|
+
def record(entry)
|
|
76
|
+
unless @header_written
|
|
77
|
+
@pending << entry
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@store.append(runtime.id, entry)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @rbs return: void
|
|
85
|
+
def write_header
|
|
86
|
+
@header_written = true
|
|
87
|
+
@store.append(runtime.id, header_entry)
|
|
88
|
+
@pending.each { |entry| @store.append(runtime.id, entry) }
|
|
89
|
+
@pending.clear
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
93
|
+
def header_entry
|
|
94
|
+
{
|
|
95
|
+
type: 'header',
|
|
96
|
+
schema_version: Riffer::Rig::Stores::JSONL::HEADER_VERSION,
|
|
97
|
+
id: runtime.id,
|
|
98
|
+
cwd: runtime.cwd,
|
|
99
|
+
created_at: Time.now.utc.iso8601,
|
|
100
|
+
model: runtime.model,
|
|
101
|
+
riffer_rig_version: Riffer::Rig::VERSION,
|
|
102
|
+
riffer_version: Riffer::VERSION,
|
|
103
|
+
title: @title.to_s
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @rbs content: String
|
|
108
|
+
# @rbs return: String
|
|
109
|
+
def one_line(content)
|
|
110
|
+
content.lines.first.to_s.strip
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The recorder is built before the Runtime it records; every entry arrives
|
|
114
|
+
# after attach, so the runtime is always there by then.
|
|
115
|
+
# @rbs return: Riffer::Rig::Runtime
|
|
116
|
+
def runtime
|
|
117
|
+
@runtime || raise(StandardError, 'Recorder used before attach')
|
|
118
|
+
end
|
|
119
|
+
end
|