riffer-rig 0.16.0 → 0.17.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.
Files changed (120) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +3 -18
  4. data/lib/riffer/rig/acp/mapper.rb +14 -2
  5. data/lib/riffer/rig/acp/servers.rb +42 -0
  6. data/lib/riffer/rig/acp.rb +17 -56
  7. data/lib/riffer/rig/cli/flags.rb +5 -0
  8. data/lib/riffer/rig/cli/sessions.rb +0 -3
  9. data/lib/riffer/rig/cli.rb +0 -3
  10. data/lib/riffer/rig/commands/auth.rb +28 -20
  11. data/lib/riffer/rig/commands/model.rb +37 -34
  12. data/lib/riffer/rig/commands/reasoning.rb +10 -10
  13. data/lib/riffer/rig/commands/reload.rb +6 -6
  14. data/lib/riffer/rig/credentials.rb +46 -14
  15. data/lib/riffer/rig/directories.rb +7 -0
  16. data/lib/riffer/rig/extension.rb +1 -0
  17. data/lib/riffer/rig/headless.rb +3 -2
  18. data/lib/riffer/rig/loader/extension_source.rb +4 -4
  19. data/lib/riffer/rig/loader/reasoning.rb +25 -0
  20. data/lib/riffer/rig/loader/reloader.rb +91 -0
  21. data/lib/riffer/rig/loader.rb +52 -171
  22. data/lib/riffer/rig/mcp/auth.rb +11 -22
  23. data/lib/riffer/rig/mcp/declaration.rb +3 -3
  24. data/lib/riffer/rig/mcp.rb +0 -8
  25. data/lib/riffer/rig/prompts/agents_md.rb +1 -1
  26. data/lib/riffer/rig/provider_setup.rb +22 -3
  27. data/lib/riffer/rig/runtime/assembly.rb +128 -0
  28. data/lib/riffer/rig/{tools/bash → runtime}/job.rb +4 -5
  29. data/lib/riffer/rig/{tools/bash → runtime}/job_log.rb +1 -1
  30. data/lib/riffer/rig/runtime/job_notices.rb +89 -0
  31. data/lib/riffer/rig/{tools/bash → runtime}/jobs.rb +38 -29
  32. data/lib/riffer/rig/runtime/mcp_registrar.rb +4 -3
  33. data/lib/riffer/rig/runtime/snapshot.rb +18 -0
  34. data/lib/riffer/rig/runtime/stop_reason.rb +22 -0
  35. data/lib/riffer/rig/runtime/turn.rb +151 -0
  36. data/lib/riffer/rig/runtime.rb +108 -347
  37. data/lib/riffer/rig/sdk.rb +8 -10
  38. data/lib/riffer/rig/settings/file.rb +3 -3
  39. data/lib/riffer/rig/settings/model_options.rb +23 -0
  40. data/lib/riffer/rig/settings.rb +2 -1
  41. data/lib/riffer/rig/stores/entries.rb +4 -4
  42. data/lib/riffer/rig/stores/jsonl.rb +1 -1
  43. data/lib/riffer/rig/stores/message_entry.rb +1 -1
  44. data/lib/riffer/rig/stores/model_entry.rb +1 -1
  45. data/lib/riffer/rig/stores/reasoning_entry.rb +1 -1
  46. data/lib/riffer/rig/stores/recorder.rb +7 -11
  47. data/lib/riffer/rig/stores/skill_entry.rb +1 -1
  48. data/lib/riffer/rig/stores/timestamp.rb +6 -0
  49. data/lib/riffer/rig/terminal/banner.rb +9 -2
  50. data/lib/riffer/rig/terminal/count.rb +6 -0
  51. data/lib/riffer/rig/terminal/frame.rb +1 -3
  52. data/lib/riffer/rig/terminal/message_queue.rb +4 -4
  53. data/lib/riffer/rig/terminal/picker.rb +5 -2
  54. data/lib/riffer/rig/terminal/repl.rb +1 -1
  55. data/lib/riffer/rig/terminal/session.rb +0 -2
  56. data/lib/riffer/rig/terminal/status_bar.rb +2 -2
  57. data/lib/riffer/rig/terminal/theme.rb +1 -0
  58. data/lib/riffer/rig/terminal/ticker.rb +3 -1
  59. data/lib/riffer/rig/terminal/turn_report.rb +1 -1
  60. data/lib/riffer/rig/tools/bash/kill.rb +1 -1
  61. data/lib/riffer/rig/tools/bash/output.rb +3 -2
  62. data/lib/riffer/rig/tools/bash.rb +3 -4
  63. data/lib/riffer/rig/version.rb +1 -1
  64. data/sig/generated/riffer/rig/acp/mapper.rbs +4 -0
  65. data/sig/generated/riffer/rig/acp/servers.rbs +21 -0
  66. data/sig/generated/riffer/rig/acp.rbs +4 -19
  67. data/sig/generated/riffer/rig/cli/flags.rbs +2 -0
  68. data/sig/generated/riffer/rig/commands/auth.rbs +18 -5
  69. data/sig/generated/riffer/rig/commands/model.rbs +19 -13
  70. data/sig/generated/riffer/rig/commands/reasoning.rbs +4 -6
  71. data/sig/generated/riffer/rig/commands/reload.rbs +4 -4
  72. data/sig/generated/riffer/rig/credentials.rbs +18 -1
  73. data/sig/generated/riffer/rig/directories.rbs +5 -0
  74. data/sig/generated/riffer/rig/loader/reasoning.rbs +13 -0
  75. data/sig/generated/riffer/rig/loader/reloader.rbs +50 -0
  76. data/sig/generated/riffer/rig/loader.rbs +13 -88
  77. data/sig/generated/riffer/rig/mcp/auth.rbs +6 -9
  78. data/sig/generated/riffer/rig/mcp/declaration.rbs +0 -3
  79. data/sig/generated/riffer/rig/mcp.rbs +0 -4
  80. data/sig/generated/riffer/rig/provider_setup.rbs +10 -5
  81. data/sig/generated/riffer/rig/runtime/assembly.rbs +74 -0
  82. data/sig/generated/riffer/rig/{tools/bash → runtime}/job.rbs +8 -8
  83. data/sig/generated/riffer/rig/{tools/bash → runtime}/job_log.rbs +2 -2
  84. data/sig/generated/riffer/rig/runtime/job_notices.rbs +46 -0
  85. data/sig/generated/riffer/rig/runtime/jobs.rbs +86 -0
  86. data/sig/generated/riffer/rig/runtime/snapshot.rbs +12 -0
  87. data/sig/generated/riffer/rig/runtime/stop_reason.rbs +13 -0
  88. data/sig/generated/riffer/rig/runtime/turn.rbs +74 -0
  89. data/sig/generated/riffer/rig/runtime.rbs +45 -148
  90. data/sig/generated/riffer/rig/sdk.rbs +5 -5
  91. data/sig/generated/riffer/rig/settings/file.rbs +2 -2
  92. data/sig/generated/riffer/rig/settings/model_options.rbs +12 -0
  93. data/sig/generated/riffer/rig/stores/entries.rbs +2 -2
  94. data/sig/generated/riffer/rig/stores/recorder.rbs +2 -8
  95. data/sig/generated/riffer/rig/stores/timestamp.rbs +4 -0
  96. data/sig/generated/riffer/rig/terminal/banner.rbs +4 -0
  97. data/sig/generated/riffer/rig/terminal/count.rbs +4 -0
  98. data/sig/generated/riffer/rig/terminal/frame.rbs +0 -2
  99. data/sig/generated/riffer/rig/terminal/message_queue.rbs +5 -5
  100. data/sig/generated/riffer/rig/terminal/picker.rbs +4 -1
  101. data/sig/generated/riffer/rig/terminal/ticker.rbs +2 -0
  102. data/sig/generated/riffer/rig/tools/bash.rbs +0 -2
  103. data/sig/manual/riffer/rig/{terminal/tally.rbs → acp/servers.rbs} +2 -2
  104. data/sig/manual/riffer/rig/{terminal/spinner.rbs → loader/reasoning.rbs} +2 -2
  105. data/sig/manual/riffer/rig/mcp.rbs +0 -5
  106. data/sig/manual/riffer/rig/{loader/credentials.rbs → runtime/stop_reason.rbs} +2 -2
  107. metadata +24 -22
  108. data/lib/riffer/rig/loader/credentials.rb +0 -23
  109. data/lib/riffer/rig/terminal/message_queue/message.rb +0 -13
  110. data/lib/riffer/rig/terminal/reasoning.rb +0 -5
  111. data/lib/riffer/rig/terminal/spinner.rb +0 -13
  112. data/lib/riffer/rig/terminal/tally.rb +0 -12
  113. data/lib/riffer/rig/tools/bash/monotonic_clock.rb +0 -8
  114. data/sig/generated/riffer/rig/loader/credentials.rbs +0 -12
  115. data/sig/generated/riffer/rig/terminal/message_queue/message.rbs +0 -10
  116. data/sig/generated/riffer/rig/terminal/reasoning.rbs +0 -5
  117. data/sig/generated/riffer/rig/terminal/spinner.rbs +0 -9
  118. data/sig/generated/riffer/rig/terminal/tally.rbs +0 -7
  119. data/sig/generated/riffer/rig/tools/bash/jobs.rbs +0 -86
  120. data/sig/generated/riffer/rig/tools/bash/monotonic_clock.rbs +0 -6
@@ -5,7 +5,7 @@ require 'open3'
5
5
  module Riffer::Rig::Credentials
6
6
  extend self
7
7
 
8
- PATH = File.expand_path('~/.riffer/auth.json') #: String
8
+ PATH = Riffer::Rig::Directories.riffer(Dir.home, 'auth.json') #: String
9
9
 
10
10
  ENTRY_TYPE = 'api_key'
11
11
 
@@ -39,6 +39,36 @@ module Riffer::Rig::Credentials
39
39
  Resolution.new(values: resolution.values, missing: missing)
40
40
  end
41
41
 
42
+ # @rbs identifier: String | Symbol
43
+ # @rbs host: Riffer::Rig::Hosts::_Host
44
+ # @rbs env: Riffer::Rig::Env
45
+ # @rbs auth_path: String
46
+ # @rbs label: String
47
+ # @rbs setup: Riffer::Rig::ProviderSetup
48
+ # @rbs config: Riffer::Config?
49
+ # @rbs settings_path: String
50
+ # @rbs known: Hash[Symbol, String]
51
+ # @rbs return: (Hash[Symbol, String] | String)
52
+ def install_values(
53
+ identifier,
54
+ host:,
55
+ env:,
56
+ auth_path:,
57
+ label: identifier.to_s,
58
+ setup: Riffer::Rig::ProviderSetup.for(identifier),
59
+ config: Riffer.config,
60
+ settings_path: Riffer::Rig::Settings::File::PATH,
61
+ known: {}
62
+ )
63
+ resolution = install(identifier, host:, setup:, env:, config:, auth_path:, settings_path:, known:)
64
+ return resolution if resolution.is_a?(String)
65
+ return resolution.values if resolution.missing.empty?
66
+
67
+ "#{label} has no #{setup.missing_fields(resolution.missing)}; " \
68
+ 'set it in the environment or run riffer interactively to paste it' \
69
+ "#{" (create one at #{setup.url})" if setup.url}"
70
+ end
71
+
42
72
  # A registered extension provider has no Riffer::Config member, so the
43
73
  # assignment stays built-ins-only.
44
74
  # @rbs identifier: String | Symbol
@@ -107,7 +137,7 @@ module Riffer::Rig::Credentials
107
137
  return stored if stored.is_a?(String)
108
138
  return :stored if secret_fields.any? { |field| stored.key?(field.name.to_s) }
109
139
 
110
- setup.chain ? :chain : :missing
140
+ setup.chain? ? :chain : :missing
111
141
  end
112
142
 
113
143
  private
@@ -119,14 +149,7 @@ module Riffer::Rig::Credentials
119
149
  # @rbs auth_path: String
120
150
  # @rbs settings_path: String
121
151
  # @rbs return: (Resolution | String)
122
- def resolve(
123
- identifier,
124
- host:,
125
- setup: Riffer::Rig::ProviderSetup.for(identifier),
126
- env: Riffer::Rig::Env.new,
127
- auth_path: PATH,
128
- settings_path: Riffer::Rig::Settings::File::PATH
129
- )
152
+ def resolve(identifier, host:, setup:, env:, auth_path:, settings_path:)
130
153
  secrets = stored_secrets(identifier, auth_path)
131
154
  return secrets if secrets.is_a?(String)
132
155
 
@@ -136,13 +159,22 @@ module Riffer::Rig::Credentials
136
159
  found = setup.fields.to_h do |field|
137
160
  [field.name, env.value_for(field) || stored_value(field, secrets, plain, env) || field.fallback&.call]
138
161
  end.compact
139
- required = setup.fields.select(&:required)
140
- answers = ask_for(required.reject { |field| found.key?(field.name) }, identifier, host)
162
+ unresolved = setup.fields.select(&:required).reject { |field| found.key?(field.name) }
163
+ pending = outside_chain(unresolved, setup)
164
+ answers = ask_for(pending, identifier, host)
141
165
  refusal = store(identifier, answers, setup:, auth_path:, settings_path:)
142
166
  return refusal if refusal
143
167
 
144
- values = found.merge(answers)
145
- Resolution.new(values:, missing: required.map(&:name) - values.keys)
168
+ Resolution.new(values: found.merge(answers), missing: pending.map(&:name) - answers.keys)
169
+ end
170
+
171
+ # @rbs fields: Array[Riffer::Rig::ProviderSetup::Field]
172
+ # @rbs setup: Riffer::Rig::ProviderSetup
173
+ # @rbs return: Array[Riffer::Rig::ProviderSetup::Field]
174
+ def outside_chain(fields, setup)
175
+ return fields unless fields.any?(&:secret) && setup.chain?
176
+
177
+ fields.reject(&:secret)
146
178
  end
147
179
 
148
180
  # @rbs fields: Array[Riffer::Rig::ProviderSetup::Field]
@@ -17,4 +17,11 @@ module Riffer::Rig::Directories
17
17
  root = dirs.index { |dir| File.exist?(File.join(dir, '.git')) } || 0
18
18
  dirs.take(root + 1)
19
19
  end
20
+
21
+ # @rbs root: String
22
+ # @rbs *names: String
23
+ # @rbs return: String
24
+ def riffer(root, *names)
25
+ File.join(root, '.riffer', *names)
26
+ end
20
27
  end
@@ -22,6 +22,7 @@ class Riffer::Rig::Extension
22
22
  @requires = requires && Gem::Requirement.new(requires)
23
23
  @mismatch = check(@requires)
24
24
  @block = block
25
+ freeze
25
26
  end
26
27
 
27
28
  # @rbs registrar: Riffer::Rig::Registrar
@@ -76,6 +76,7 @@ class Riffer::Rig::Headless
76
76
  @json = json
77
77
  @job_idle_timeout = job_idle_timeout
78
78
  @stdin_timeout = stdin_timeout
79
+ @streamed_text = false
79
80
  @wrote_text = false
80
81
  @break_before_text = false
81
82
  end
@@ -245,8 +246,8 @@ class Riffer::Rig::Headless
245
246
  return 0 if stop_reason == :completed
246
247
 
247
248
  @error.puts("turn ended: #{stop_reason}") if stop_reason
248
- return 130 if stop_reason == Riffer::Rig::Runtime::INTERRUPT_CANCELLED
249
- return 3 if stop_reason && Riffer::Rig::Runtime::STOPPED_WITHOUT_COMPLETING.include?(stop_reason)
249
+ return 130 if stop_reason == Riffer::Rig::Runtime::StopReason::INTERRUPT_CANCELLED
250
+ return 3 if stop_reason && Riffer::Rig::Runtime::StopReason::STOPPED_WITHOUT_COMPLETING.include?(stop_reason)
250
251
 
251
252
  1
252
253
  end
@@ -80,12 +80,12 @@ class Riffer::Rig::Loader::ExtensionSource
80
80
 
81
81
  # @rbs return: String
82
82
  def home_rig_path
83
- File.expand_path(File.join(@home, '.riffer', 'rig.rb'))
83
+ File.expand_path(Riffer::Rig::Directories.riffer(@home, 'rig.rb'))
84
84
  end
85
85
 
86
86
  # @rbs return: String
87
87
  def project_rig_path
88
- File.expand_path(File.join(@cwd, '.riffer', 'rig.rb'))
88
+ File.expand_path(Riffer::Rig::Directories.riffer(@cwd, 'rig.rb'))
89
89
  end
90
90
 
91
91
  # @rbs autoload: bool
@@ -135,7 +135,7 @@ class Riffer::Rig::Loader::ExtensionSource
135
135
 
136
136
  # @rbs return: String
137
137
  def trust_path
138
- File.join(@home, '.riffer', 'trust.json')
138
+ Riffer::Rig::Directories.riffer(@home, 'trust.json')
139
139
  end
140
140
 
141
141
  # @rbs path: String
@@ -171,7 +171,7 @@ class Riffer::Rig::Loader::ExtensionSource
171
171
  # @rbs feature: String
172
172
  # @rbs return: bool
173
173
  def local?(feature)
174
- roots = [File.expand_path(File.join(@home, '.riffer')), File.expand_path(@cwd)] #: Array[String]
174
+ roots = [File.expand_path(Riffer::Rig::Directories.riffer(@home)), File.expand_path(@cwd)] #: Array[String]
175
175
  roots.any? { |root| feature.start_with?("#{root}#{File::SEPARATOR}") }
176
176
  end
177
177
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Loader::Reasoning
4
+ extend self
5
+
6
+ # @rbs flag: String?
7
+ # @rbs env: String?
8
+ # @rbs return: String?
9
+ def forced(flag, env)
10
+ flag || env
11
+ end
12
+
13
+ # @rbs flag: String?
14
+ # @rbs env: String?
15
+ # @rbs return: String?
16
+ def rejection(flag, env)
17
+ level = forced(flag, env)
18
+ return unless level
19
+
20
+ rejection = Riffer::Rig::Settings::ModelOptions.reasoning_rejection(level)
21
+ return unless rejection
22
+
23
+ flag ? rejection : "RIFFER_REASONING: #{rejection}"
24
+ end
25
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Loader::Reloader
4
+ # @rbs @source: Riffer::Rig::Loader::ExtensionSource
5
+ # @rbs @host: Riffer::Rig::Hosts::_Host
6
+ # @rbs @settings: ^() -> (Hash[Symbol, untyped] | String)
7
+ # @rbs @credentials: ^(String) -> (Hash[Symbol, String] | String)
8
+ # @rbs @extensions: ^(Riffer::Rig::Settings::Document, Array[Riffer::Rig::Extension]) -> Array[Riffer::Rig::Extension]
9
+ # @rbs @rediscovers: bool
10
+ # @rbs @mode: (:auto | :manual | nil)
11
+ # @rbs @file_state: Hash[String, Time?]
12
+
13
+ # @rbs source: Riffer::Rig::Loader::ExtensionSource
14
+ # @rbs host: Riffer::Rig::Hosts::_Host
15
+ # @rbs settings: ^() -> (Hash[Symbol, untyped] | String)
16
+ # @rbs credentials: ^(String) -> (Hash[Symbol, String] | String)
17
+ # @rbs extensions: ^(Riffer::Rig::Settings::Document, Array[Riffer::Rig::Extension]) -> Array[Riffer::Rig::Extension]
18
+ # @rbs rediscovers: bool
19
+ # @rbs mode: (:auto | :manual | nil)
20
+ # @rbs return: void
21
+ def initialize(source:, host:, settings:, credentials:, extensions:, rediscovers:, mode:)
22
+ @source = source
23
+ @host = host
24
+ @settings = settings
25
+ @credentials = credentials
26
+ @extensions = extensions
27
+ @rediscovers = rediscovers
28
+ @mode = mode
29
+ @file_state = file_state
30
+ end
31
+
32
+ # @rbs runtime: Riffer::Rig::Runtime
33
+ # @rbs force: bool
34
+ # @rbs return: (Riffer::Rig::Runtime | String)
35
+ def reload(runtime, force: false)
36
+ return runtime if !force && file_state == @file_state
37
+ return runtime if !force && manual?
38
+
39
+ outcome = perform(runtime)
40
+ # Every attempt advances the state, failed ones included, so a failure is
41
+ # not retried at every request.
42
+ @file_state = file_state
43
+ outcome
44
+ end
45
+
46
+ private
47
+
48
+ # @rbs return: bool
49
+ def manual?
50
+ return @mode == :manual if @mode
51
+
52
+ settings = @settings.call
53
+ settings.is_a?(Hash) && Riffer::Rig::Settings::Document.new(settings).reload == :manual
54
+ end
55
+
56
+ # @rbs runtime: Riffer::Rig::Runtime
57
+ # @rbs return: (Riffer::Rig::Runtime | String)
58
+ def perform(runtime)
59
+ settings = @settings.call
60
+ return notify_failure(settings) if settings.is_a?(String)
61
+
62
+ rediscovered = @rediscovers ? @source.rediscover : [] #: Array[Riffer::Rig::Extension] | String
63
+ return notify_failure(rediscovered) if rediscovered.is_a?(String)
64
+
65
+ candidates = @extensions.call(Riffer::Rig::Settings::Document.new(settings), rediscovered)
66
+ extensions = @source.validated(candidates, settings)
67
+ provider = runtime.model.partition('/').first
68
+ credentials = @credentials.call(provider)
69
+ return notify_failure(credentials) if credentials.is_a?(String)
70
+
71
+ runtime.merge_credentials(provider.to_sym, credentials)
72
+ runtime.rebuild(extensions: extensions, settings: settings)
73
+ runtime
74
+ rescue StandardError => e
75
+ # The automatic check runs mid-turn with nothing else to catch a raise,
76
+ # and a raised attempt must still advance the file state.
77
+ notify_failure(e.message)
78
+ end
79
+
80
+ # @rbs message: String
81
+ # @rbs return: String
82
+ def notify_failure(message)
83
+ @host.notify(message, level: :error)
84
+ message
85
+ end
86
+
87
+ # @rbs return: Hash[String, Time?]
88
+ def file_state
89
+ @rediscovers ? @source.file_state : {}
90
+ end
91
+ end
@@ -10,12 +10,8 @@ class Riffer::Rig::Loader
10
10
  # @rbs @riffer_config: Riffer::Config
11
11
  # @rbs @store: Riffer::Rig::Stores::_Store | nil
12
12
  # @rbs @source: Riffer::Rig::Loader::ExtensionSource
13
- # @rbs @stripped: Array[Symbol]
14
- # @rbs @extensions_enabled: bool
15
- # @rbs @extra_extensions: Array[Riffer::Rig::Extension]
16
- # @rbs @recorder_extensions: Array[Riffer::Rig::Extension]
17
- # @rbs @file_state: Hash[String, Time?]?
18
- # @rbs @reload_mode: (:auto | :manual | nil)
13
+ # @rbs @reloaders: ObjectSpace::WeakKeyMap[Riffer::Rig::Runtime, Riffer::Rig::Loader::Reloader]
14
+ # @rbs @sdk: singleton(Riffer::Rig::SDK)
19
15
 
20
16
  # @rbs cwd: String
21
17
  # @rbs host: Riffer::Rig::Hosts::_Host
@@ -26,7 +22,7 @@ class Riffer::Rig::Loader
26
22
  # @rbs options: Riffer::Rig::Loader::Options
27
23
  # @rbs reload: (:auto | :manual | nil)
28
24
  # @rbs extra_extensions: Array[Riffer::Rig::Extension]
29
- # @rbs snapshot: Hash[Symbol, untyped]?
25
+ # @rbs snapshot: (Riffer::Rig::Runtime::Snapshot | Hash[Symbol, untyped])?
30
26
  # @rbs return: Riffer::Rig::Runtime
31
27
  def self.runtime(
32
28
  cwd:,
@@ -49,6 +45,7 @@ class Riffer::Rig::Loader
49
45
  # @rbs home: String
50
46
  # @rbs riffer_config: Riffer::Config
51
47
  # @rbs store: Riffer::Rig::Stores::_Store | nil
48
+ # @rbs sdk: singleton(Riffer::Rig::SDK)
52
49
  # @rbs return: void
53
50
  def initialize(
54
51
  cwd:,
@@ -56,7 +53,8 @@ class Riffer::Rig::Loader
56
53
  env: Riffer::Rig::Env.load,
57
54
  home: Dir.home,
58
55
  riffer_config: Riffer.config,
59
- store: Riffer::Rig::Stores::JSONL.new(home: home)
56
+ store: Riffer::Rig::Stores::JSONL.new(home: home),
57
+ sdk: Riffer::Rig::SDK
60
58
  )
61
59
  raise ConfigurationError, env.message if env.is_a?(Riffer::Rig::Env::Invalid)
62
60
 
@@ -66,18 +64,15 @@ class Riffer::Rig::Loader
66
64
  @home = home
67
65
  @riffer_config = riffer_config
68
66
  @store = store
67
+ @sdk = sdk
69
68
  @source = Riffer::Rig::Loader::ExtensionSource.new(cwd: cwd, home: home, host: host)
70
- @stripped = []
71
- @extensions_enabled = true
72
- @recorder_extensions = []
73
- @file_state = nil
74
- @reload_mode = nil
69
+ @reloaders = ObjectSpace::WeakKeyMap.new
75
70
  end
76
71
 
77
72
  # @rbs options: Riffer::Rig::Loader::Options
78
73
  # @rbs reload: (:auto | :manual | nil)
79
74
  # @rbs extra_extensions: Array[Riffer::Rig::Extension]
80
- # @rbs snapshot: Hash[Symbol, untyped]?
75
+ # @rbs snapshot: (Riffer::Rig::Runtime::Snapshot | Hash[Symbol, untyped])?
81
76
  # @rbs return: Riffer::Rig::Runtime
82
77
  def runtime(options = Options.new, reload: nil, extra_extensions: [], snapshot: nil)
83
78
  settings = merged_settings
@@ -85,31 +80,27 @@ class Riffer::Rig::Loader
85
80
 
86
81
  document = Riffer::Rig::Settings::Document.new(settings)
87
82
  selected = select_model(options.model || @env.model || document.model)
88
- check_reasoning(options)
83
+ rejection = Riffer::Rig::Loader::Reasoning.rejection(options.reasoning, @env.reasoning)
84
+ raise ConfigurationError, rejection if rejection
85
+
89
86
  provider = Riffer::Rig::Settings::ModelOptions.provider_for(selected).to_s
90
87
  ensure_sdk(provider)
91
88
  credentials = install_credentials(provider)
92
89
  raise ConfigurationError, credentials if credentials.is_a?(String)
93
90
 
94
91
  @riffer_config.catalog_files = document.catalog_files
95
- @extensions_enabled = options.extensions
96
- @extra_extensions = extra_extensions
97
- @reload_mode = reload
98
- @stripped = { skills: options.skills, agents_md: options.agents_md }.reject { |_name, kept| kept }.keys
99
- loaded = bundled(document)
100
- loaded += @source.discover(autoload: document.autoload) if options.extensions
101
- loaded += @extra_extensions
92
+ stripped = { skills: options.skills, agents_md: options.agents_md }.reject { |_name, kept| kept }.keys
93
+ discovered = options.extensions ? @source.discover(autoload: document.autoload) : [] #: Array[Riffer::Rig::Extension]
102
94
 
103
- recorder = recorder_for(document, resumed: !snapshot.nil?)
104
- @recorder_extensions = recorder ? [Riffer::Rig::Stores::Recorder.extension(recorder)] : []
105
- loaded += @recorder_extensions
95
+ saved = snapshot && Riffer::Rig::Runtime::Snapshot.of(snapshot)
96
+ recorder = recorder_for(document, resumed: !saved.nil?)
97
+ recording = recorder ? [Riffer::Rig::Stores::Recorder.extension(recorder)] : [] #: Array[Riffer::Rig::Extension]
106
98
 
107
- snapshot = snapshot_without_forced(snapshot, options)
108
- reasoning = forced_reasoning(options, snapshot)
99
+ saved = snapshot_without_forced(saved, options)
109
100
 
110
101
  built = Riffer::Rig::Runtime.new(
111
102
  selected,
112
- extensions: loaded,
103
+ extensions: bundled(document, stripped) + discovered + extra_extensions + recording,
113
104
  tools: options.tools,
114
105
  settings: settings,
115
106
  host: @host,
@@ -118,17 +109,29 @@ class Riffer::Rig::Loader
118
109
  credentials: { provider.to_sym => credentials },
119
110
  env: @env,
120
111
  auth_path: home_auth_path,
121
- reasoning: reasoning || settings_reasoning(document),
112
+ settings_path: home_settings_path,
113
+ reasoning: Riffer::Rig::Loader::Reasoning.forced(options.reasoning, @env.reasoning),
122
114
  riffer_config: @riffer_config,
123
115
  model_options: Riffer::Rig::Settings::ModelOptions.model_options(selected),
124
116
  native_tools: document.native_tools,
125
- snapshot: snapshot
117
+ snapshot: saved
126
118
  )
127
- built.install_command(Riffer::Rig::Commands::Reload.command(self))
119
+ reloader = Riffer::Rig::Loader::Reloader.new(
120
+ source: @source,
121
+ host: @host,
122
+ settings: -> { merged_settings },
123
+ credentials: ->(name) { install_credentials(name) },
124
+ extensions: ->(current, found) { bundled(current, stripped) + extra_extensions + found + recording },
125
+ rediscovers: options.extensions,
126
+ mode: reload
127
+ )
128
+ @reloaders[built] = reloader
129
+ built.install_command(Riffer::Rig::Commands::Reload.command(reloader))
128
130
  store = @store
129
131
  built.install_command(Riffer::Rig::Commands::Trace.command(store)) if store && recorder
130
- built.install_reload_check(->(runtime) { automatic_reload(runtime) }) unless reload_mode(document) == :manual
131
- @file_state = file_state
132
+ unless (reload || document.reload) == :manual
133
+ built.install_reload_check(->(runtime) { _ = reloader.reload(runtime) })
134
+ end
132
135
  recorder&.attach(built)
133
136
  built
134
137
  end
@@ -145,8 +148,6 @@ class Riffer::Rig::Loader
145
148
  resume(latest.id, options)
146
149
  end
147
150
 
148
- # A resume carries the history over and nothing else: the settings,
149
- # credentials, extensions and tools are today's.
150
151
  # @rbs id: String
151
152
  # @rbs options: Riffer::Rig::Loader::Options
152
153
  # @rbs extra_extensions: Array[Riffer::Rig::Extension]
@@ -190,56 +191,22 @@ class Riffer::Rig::Loader
190
191
  store.delete(id)
191
192
  end
192
193
 
193
- # Without force, a no-op while the tracked file set and the two rig.rb paths
194
- # look as the last discovery left them and the reload mode is not manual —
195
- # the entry point the automatic trigger (#133) calls. The snapshot advances
196
- # on every attempt, failed ones included, so a failure does not retry every
197
- # request. Failures are notified once and reported as the message, the way
198
- # Settings::ModelOptions.rejection reports a rejection; the Runtime is the success value.
199
194
  # @rbs runtime: Riffer::Rig::Runtime
200
195
  # @rbs force: bool
201
196
  # @rbs return: (Riffer::Rig::Runtime | String)
202
197
  def reload(runtime, force: false)
203
- return runtime if !force && file_state == @file_state
204
- return runtime if !force && manual_reload?
205
-
206
- outcome = perform_reload(runtime)
207
- @file_state = file_state
208
- outcome
198
+ reloader = @reloaders[runtime]
199
+ reloader ? reloader.reload(runtime, force: force) : runtime
209
200
  end
210
201
 
211
202
  private
212
203
 
213
- # The reload mode a build runs under: the keyword wins over the setting,
214
- # the setting over the default.
215
- # @rbs document: Riffer::Rig::Settings::Document
216
- # @rbs return: (:auto | :manual)
217
- def reload_mode(document)
218
- @reload_mode || document.reload
219
- end
220
-
221
- # @rbs return: bool
222
- def manual_reload?
223
- return true if @reload_mode == :manual
224
-
225
- settings = merged_settings
226
- settings.is_a?(Hash) && reload_mode(Riffer::Rig::Settings::Document.new(settings)) == :manual
227
- end
228
-
229
- # The automatic trigger: runs at every before_request boundary, so the
230
- # change check decides and a no-op costs the stat alone.
231
- # @rbs runtime: Riffer::Rig::Runtime
232
- # @rbs return: void
233
- def automatic_reload(runtime)
234
- _ = reload(runtime)
235
- end
236
-
237
204
  # @rbs return: (Hash[Symbol, untyped] | String)
238
205
  def merged_settings
239
206
  home = read_settings(home_settings_path)
240
207
  return home if home.is_a?(String)
241
208
 
242
- project = read_settings(File.join(@cwd, '.riffer', 'settings.json'))
209
+ project = read_settings(Riffer::Rig::Directories.riffer(@cwd, 'settings.json'))
243
210
  return project if project.is_a?(String)
244
211
 
245
212
  Riffer::Rig::Settings.merge(home, project)
@@ -254,57 +221,11 @@ class Riffer::Rig::Loader
254
221
  "#{path} sets \"model\" to a value that is not a string"
255
222
  end
256
223
 
257
- # Notifies every failure once and reports its message, unexpected raises
258
- # included — the automatic check runs mid-turn with no other isolation, and a
259
- # raised attempt must still advance the snapshot in reload. A rebuilt Runtime
260
- # is the success value.
261
- # @rbs runtime: Riffer::Rig::Runtime
262
- # @rbs return: (Riffer::Rig::Runtime | String)
263
- def perform_reload(runtime)
264
- settings = merged_settings
265
- return notify_failure(settings) if settings.is_a?(String)
266
-
267
- candidates = reload_candidates(Riffer::Rig::Settings::Document.new(settings))
268
- return notify_failure(candidates) if candidates.is_a?(String)
269
-
270
- extensions = @source.validated(candidates + @recorder_extensions, settings)
271
- provider = runtime.model.partition('/').first
272
- credentials = install_credentials(provider)
273
- return notify_failure(credentials) if credentials.is_a?(String)
274
-
275
- runtime.merge_credentials(provider.to_sym, credentials)
276
- runtime.rebuild(extensions: extensions, settings: settings)
277
- runtime
278
- rescue StandardError => e
279
- notify_failure(e.message)
280
- end
281
-
282
- # @rbs message: String
283
- # @rbs return: String
284
- def notify_failure(message)
285
- @host.notify(message, level: :error)
286
- message
287
- end
288
-
289
- # @rbs document: Riffer::Rig::Settings::Document
290
- # @rbs return: (Array[Riffer::Rig::Extension] | String)
291
- def reload_candidates(document)
292
- candidates = bundled(document) + @extra_extensions
293
- return candidates unless @extensions_enabled
294
-
295
- rediscovered = @source.rediscover
296
- rediscovered.is_a?(String) ? rediscovered : candidates + rediscovered
297
- end
298
-
299
224
  # @rbs document: Riffer::Rig::Settings::Document
225
+ # @rbs stripped: Array[Symbol]
300
226
  # @rbs return: Array[Riffer::Rig::Extension]
301
- def bundled(document)
302
- Riffer::Rig::Bundled::BY_NAME.except(*document.disabled.map(&:to_sym), *@stripped).values
303
- end
304
-
305
- # @rbs return: Hash[String, Time?]
306
- def file_state
307
- @extensions_enabled ? @source.file_state : {}
227
+ def bundled(document, stripped)
228
+ Riffer::Rig::Bundled::BY_NAME.except(*document.disabled.map(&:to_sym), *stripped).values
308
229
  end
309
230
 
310
231
  # @rbs configured: String?
@@ -348,14 +269,14 @@ class Riffer::Rig::Loader
348
269
  return unless sdk
349
270
 
350
271
  gem, requirement = sdk
351
- message = Riffer::Rig::SDK.ensure(gem, requirement, host: @host)
272
+ message = @sdk.ensure(gem, requirement, host: @host)
352
273
  raise ConfigurationError, message if message
353
274
  end
354
275
 
355
276
  # @rbs provider: String
356
277
  # @rbs return: (Hash[Symbol, String] | String)
357
278
  def install_credentials(provider)
358
- Riffer::Rig::Loader::Credentials.install(
279
+ Riffer::Rig::Credentials.install_values(
359
280
  provider,
360
281
  host: @host,
361
282
  env: @env,
@@ -367,65 +288,25 @@ class Riffer::Rig::Loader
367
288
 
368
289
  # @rbs return: String
369
290
  def home_settings_path
370
- File.join(@home, '.riffer', 'settings.json')
291
+ Riffer::Rig::Directories.riffer(@home, 'settings.json')
371
292
  end
372
293
 
373
294
  # @rbs return: String
374
295
  def home_auth_path
375
- File.join(@home, '.riffer', 'auth.json')
376
- end
377
-
378
- # A reasoning level from the flag or the environment is refused, the way a
379
- # bad model string is; one from the settings document or a session's recorded
380
- # switch falls back to the model's default instead.
381
- # @rbs options: Riffer::Rig::Loader::Options
382
- # @rbs return: void
383
- def check_reasoning(options)
384
- forced = options.reasoning || @env.reasoning
385
- return unless forced
386
-
387
- rejection = Riffer::Rig::Settings::ModelOptions.reasoning_rejection(forced)
388
- return unless rejection
389
-
390
- raise ConfigurationError, options.reasoning ? rejection : "RIFFER_REASONING: #{rejection}"
391
- end
392
-
393
- # The reasoning in effect for the build, over the settings document: the flag,
394
- # then the session's recorded switch, then RIFFER_REASONING.
395
- # @rbs options: Riffer::Rig::Loader::Options
396
- # @rbs snapshot: Hash[Symbol, untyped]?
397
- # @rbs return: String?
398
- def forced_reasoning(options, snapshot)
399
- options.reasoning || recorded_reasoning(snapshot) || @env.reasoning
400
- end
401
-
402
- # @rbs snapshot: Hash[Symbol, untyped]?
403
- # @rbs return: String?
404
- def recorded_reasoning(snapshot)
405
- saved = snapshot && snapshot[:reasoning]
406
- Riffer::Rig::Settings::ModelOptions.reasoning_level(saved) if saved.is_a?(String)
407
- end
408
-
409
- # A settings level riffer does not accept silently keeps the model's default.
410
- # @rbs document: Riffer::Rig::Settings::Document
411
- # @rbs return: String?
412
- def settings_reasoning(document)
413
- Riffer::Rig::Settings::ModelOptions.reasoning_level(document.reasoning)
296
+ Riffer::Rig::Directories.riffer(@home, 'auth.json')
414
297
  end
415
298
 
416
- # A model or reasoning level chosen for the run wins for that run only: it is
417
- # not recorded, so a later resume restores the session's own switches.
418
- # @rbs snapshot: Hash[Symbol, untyped]?
299
+ # @rbs snapshot: Riffer::Rig::Runtime::Snapshot?
419
300
  # @rbs options: Riffer::Rig::Loader::Options
420
- # @rbs return: Hash[Symbol, untyped]?
301
+ # @rbs return: Riffer::Rig::Runtime::Snapshot?
421
302
  def snapshot_without_forced(snapshot, options)
422
- snapshot = snapshot&.merge(model: nil) if options.model
423
- snapshot = snapshot&.merge(reasoning: nil) if options.reasoning
303
+ # The flags beat the session's recorded switches, which the Runtime would
304
+ # otherwise restore over them.
305
+ snapshot = snapshot&.without_model if options.model
306
+ snapshot = snapshot&.without_reasoning if options.reasoning
424
307
  snapshot
425
308
  end
426
309
 
427
- # The store records unless it was declined with store: nil or globally with
428
- # "sessions": {"save": false}.
429
310
  # @rbs document: Riffer::Rig::Settings::Document
430
311
  # @rbs resumed: bool
431
312
  # @rbs return: Riffer::Rig::Stores::Recorder?