riffer-rig 0.6.0 → 0.8.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 (163) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +38 -4
  4. data/exe/riffer +1 -1
  5. data/lib/riffer/rig/bundled/agents_md.rb +7 -0
  6. data/lib/riffer/rig/bundled/bash.rb +5 -0
  7. data/lib/riffer/rig/bundled/edit.rb +5 -0
  8. data/lib/riffer/rig/bundled/mcp.rb +9 -0
  9. data/lib/riffer/rig/bundled/read.rb +5 -0
  10. data/lib/riffer/rig/bundled/skills.rb +10 -0
  11. data/lib/riffer/rig/bundled/write.rb +5 -0
  12. data/lib/riffer/rig/bundled.rb +7 -0
  13. data/lib/riffer/rig/cli/flags.rb +77 -0
  14. data/lib/riffer/rig/cli.rb +69 -106
  15. data/lib/riffer/rig/command/context.rb +70 -0
  16. data/lib/riffer/rig/command.rb +29 -0
  17. data/lib/riffer/rig/commands/model.rb +54 -0
  18. data/lib/riffer/rig/commands/skill.rb +27 -0
  19. data/lib/riffer/rig/commands.rb +4 -0
  20. data/lib/riffer/rig/credentials.rb +2 -2
  21. data/lib/riffer/rig/directories.rb +20 -0
  22. data/lib/riffer/rig/env/invalid.rb +13 -0
  23. data/lib/riffer/rig/env.rb +12 -1
  24. data/lib/riffer/rig/events/after_response.rb +25 -0
  25. data/lib/riffer/rig/events/after_tool_call.rb +31 -0
  26. data/lib/riffer/rig/events/before_prompt.rb +25 -0
  27. data/lib/riffer/rig/events/before_request.rb +25 -0
  28. data/lib/riffer/rig/events/before_tool_call.rb +28 -0
  29. data/lib/riffer/rig/events/command_output.rb +3 -2
  30. data/lib/riffer/rig/events/notify.rb +3 -2
  31. data/lib/riffer/rig/events/session_end.rb +3 -2
  32. data/lib/riffer/rig/events/session_start.rb +3 -2
  33. data/lib/riffer/rig/events/skill_activated.rb +3 -2
  34. data/lib/riffer/rig/events/turn_end.rb +3 -2
  35. data/lib/riffer/rig/extension/failure.rb +16 -0
  36. data/lib/riffer/rig/extension.rb +16 -1
  37. data/lib/riffer/rig/hosts/mirror.rb +6 -7
  38. data/lib/riffer/rig/hosts/null.rb +1 -1
  39. data/lib/riffer/rig/loader.rb +240 -0
  40. data/lib/riffer/rig/mcp/declaration.rb +23 -0
  41. data/lib/riffer/rig/mcp/server.rb +16 -0
  42. data/lib/riffer/rig/mcp.rb +12 -0
  43. data/lib/riffer/rig/prompts/agents_md.rb +32 -0
  44. data/lib/riffer/rig/prompts.rb +4 -0
  45. data/lib/riffer/rig/registrar.rb +120 -5
  46. data/lib/riffer/rig/runtime/blocked.rb +13 -0
  47. data/lib/riffer/rig/runtime/hooks.rb +78 -0
  48. data/lib/riffer/rig/runtime/request_guardrail.rb +24 -0
  49. data/lib/riffer/rig/runtime/tool_runtime.rb +44 -0
  50. data/lib/riffer/rig/runtime.rb +506 -57
  51. data/lib/riffer/rig/settings/document.rb +29 -16
  52. data/lib/riffer/rig/settings/pricing.rb +16 -5
  53. data/lib/riffer/rig/settings.rb +73 -43
  54. data/lib/riffer/rig/skills/sources.rb +28 -0
  55. data/lib/riffer/rig/skills.rb +12 -0
  56. data/lib/riffer/rig/{events/event.rb → support/equatable.rb} +2 -11
  57. data/lib/riffer/rig/{ui → terminal}/animator.rb +4 -4
  58. data/lib/riffer/rig/{ui → terminal}/banner.rb +17 -21
  59. data/lib/riffer/rig/{ui → terminal}/cursor.rb +4 -4
  60. data/lib/riffer/rig/terminal/host.rb +85 -0
  61. data/lib/riffer/rig/{ui → terminal}/palette.rb +1 -1
  62. data/lib/riffer/rig/terminal/renderer.rb +313 -0
  63. data/lib/riffer/rig/{ui → terminal}/smoother.rb +4 -4
  64. data/lib/riffer/rig/{ui → terminal}/theme.rb +12 -12
  65. data/lib/riffer/rig/terminal/wrapper.rb +103 -0
  66. data/lib/riffer/rig/terminal.rb +207 -0
  67. data/lib/riffer/rig/tools/bash.rb +6 -4
  68. data/lib/riffer/rig/tools/edit.rb +3 -2
  69. data/lib/riffer/rig/tools/read.rb +3 -2
  70. data/lib/riffer/rig/tools/write.rb +3 -2
  71. data/lib/riffer/rig/trust.rb +37 -0
  72. data/lib/riffer/rig/version.rb +1 -1
  73. data/lib/riffer/rig.rb +8 -8
  74. data/sig/generated/riffer/rig/bundled/agents_md.rbs +5 -0
  75. data/sig/generated/riffer/rig/bundled/bash.rbs +5 -0
  76. data/sig/generated/riffer/rig/bundled/edit.rbs +5 -0
  77. data/sig/generated/riffer/rig/bundled/mcp.rbs +5 -0
  78. data/sig/generated/riffer/rig/bundled/read.rbs +5 -0
  79. data/sig/generated/riffer/rig/bundled/skills.rbs +5 -0
  80. data/sig/generated/riffer/rig/bundled/write.rbs +5 -0
  81. data/sig/generated/riffer/rig/bundled.rbs +5 -0
  82. data/sig/generated/riffer/rig/cli/flags.rbs +40 -0
  83. data/sig/generated/riffer/rig/cli.rbs +30 -35
  84. data/sig/generated/riffer/rig/command/context.rbs +50 -0
  85. data/sig/generated/riffer/rig/command.rbs +23 -0
  86. data/sig/generated/riffer/rig/commands/model.rbs +27 -0
  87. data/sig/generated/riffer/rig/commands/skill.rbs +12 -0
  88. data/sig/generated/riffer/rig/commands.rbs +4 -0
  89. data/sig/generated/riffer/rig/credentials.rbs +4 -4
  90. data/sig/generated/riffer/rig/directories.rbs +11 -0
  91. data/sig/generated/riffer/rig/env/invalid.rbs +10 -0
  92. data/sig/generated/riffer/rig/env.rbs +7 -1
  93. data/sig/generated/riffer/rig/events/after_response.rbs +18 -0
  94. data/sig/generated/riffer/rig/events/after_tool_call.rbs +24 -0
  95. data/sig/generated/riffer/rig/events/before_prompt.rbs +18 -0
  96. data/sig/generated/riffer/rig/events/before_request.rbs +18 -0
  97. data/sig/generated/riffer/rig/events/before_tool_call.rbs +21 -0
  98. data/sig/generated/riffer/rig/events/command_output.rbs +3 -1
  99. data/sig/generated/riffer/rig/events/notify.rbs +3 -1
  100. data/sig/generated/riffer/rig/events/session_end.rbs +3 -1
  101. data/sig/generated/riffer/rig/events/session_start.rbs +3 -1
  102. data/sig/generated/riffer/rig/events/skill_activated.rbs +3 -1
  103. data/sig/generated/riffer/rig/events/turn_end.rbs +3 -1
  104. data/sig/generated/riffer/rig/extension/failure.rbs +13 -0
  105. data/sig/generated/riffer/rig/extension.rbs +14 -1
  106. data/sig/generated/riffer/rig/hosts/mirror.rbs +9 -9
  107. data/sig/generated/riffer/rig/hosts/null.rbs +1 -1
  108. data/sig/generated/riffer/rig/loader.rbs +113 -0
  109. data/sig/generated/riffer/rig/mcp/declaration.rbs +18 -0
  110. data/sig/generated/riffer/rig/mcp/server.rbs +13 -0
  111. data/sig/generated/riffer/rig/mcp.rbs +8 -0
  112. data/sig/generated/riffer/rig/prompts/agents_md.rbs +19 -0
  113. data/sig/generated/riffer/rig/prompts.rbs +4 -0
  114. data/sig/generated/riffer/rig/registrar.rbs +78 -4
  115. data/sig/generated/riffer/rig/runtime/blocked.rbs +10 -0
  116. data/sig/generated/riffer/rig/runtime/hooks.rbs +49 -0
  117. data/sig/generated/riffer/rig/runtime/request_guardrail.rbs +16 -0
  118. data/sig/generated/riffer/rig/runtime/tool_runtime.rbs +23 -0
  119. data/sig/generated/riffer/rig/runtime.rbs +225 -22
  120. data/sig/generated/riffer/rig/settings/document.rbs +13 -11
  121. data/sig/generated/riffer/rig/settings/pricing.rbs +7 -2
  122. data/sig/generated/riffer/rig/settings.rbs +36 -23
  123. data/sig/generated/riffer/rig/skills/sources.rbs +18 -0
  124. data/sig/generated/riffer/rig/skills.rbs +7 -0
  125. data/sig/generated/riffer/rig/support/equatable.rbs +15 -0
  126. data/sig/generated/riffer/rig/{ui → terminal}/animator.rbs +5 -5
  127. data/sig/generated/riffer/rig/terminal/banner.rbs +39 -0
  128. data/sig/generated/riffer/rig/{ui → terminal}/cursor.rbs +5 -5
  129. data/sig/generated/riffer/rig/terminal/host.rbs +50 -0
  130. data/sig/generated/riffer/rig/{ui → terminal}/palette.rbs +2 -2
  131. data/sig/generated/riffer/rig/terminal/renderer.rbs +166 -0
  132. data/sig/generated/riffer/rig/{ui → terminal}/smoother.rbs +5 -5
  133. data/sig/generated/riffer/rig/{ui → terminal}/theme.rbs +5 -5
  134. data/sig/generated/riffer/rig/terminal/wrapper.rbs +43 -0
  135. data/sig/generated/riffer/rig/terminal.rbs +85 -0
  136. data/sig/generated/riffer/rig/tools/bash.rbs +2 -1
  137. data/sig/generated/riffer/rig/trust.rbs +20 -0
  138. data/sig/generated/riffer/rig.rbs +4 -0
  139. data/sig/manual/riffer/rig/commands/model.rbs +6 -0
  140. data/sig/manual/riffer/rig/commands/skill.rbs +6 -0
  141. data/sig/manual/riffer/rig/directories.rbs +6 -0
  142. data/sig/manual/riffer/rig/events.rbs +6 -0
  143. data/sig/manual/riffer/rig/hosts.rbs +7 -0
  144. data/sig/manual/riffer/rig/mcp.rbs +11 -0
  145. data/sig/manual/riffer/rig/prompts/agents_md.rbs +6 -0
  146. data/sig/manual/riffer/rig/{ui/banner.rbs → skills.rbs} +2 -2
  147. data/sig/manual/riffer/rig/support.rbs +8 -0
  148. data/sig/manual/riffer/rig/terminal/banner.rbs +6 -0
  149. data/sig/manual/riffer/rig/trust.rbs +6 -0
  150. metadata +145 -27
  151. data/lib/riffer/rig/coding_agent.rb +0 -53
  152. data/lib/riffer/rig/hosts/base.rb +0 -36
  153. data/lib/riffer/rig/repl.rb +0 -188
  154. data/lib/riffer/rig/token_tally.rb +0 -54
  155. data/lib/riffer/rig/ui/renderer.rb +0 -191
  156. data/sig/generated/riffer/rig/coding_agent.rbs +0 -9
  157. data/sig/generated/riffer/rig/events/event.rbs +0 -20
  158. data/sig/generated/riffer/rig/hosts/base.rbs +0 -26
  159. data/sig/generated/riffer/rig/repl.rbs +0 -69
  160. data/sig/generated/riffer/rig/token_tally.rbs +0 -42
  161. data/sig/generated/riffer/rig/ui/banner.rbs +0 -42
  162. data/sig/generated/riffer/rig/ui/renderer.rbs +0 -94
  163. data/sig/manual/riffer/rig/ui.rbs +0 -5
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Loader
4
+ class ConfigurationError < StandardError; end
5
+
6
+ PROVIDER_LIST = Riffer::Rig::Settings::PROVIDERS.join(', ') #: String
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
13
+
14
+ EXTENSION_FEATURE = 'riffer/rig/extension' #: String
15
+
16
+ private_constant :EXTENSION_FEATURE, :PROVIDER_LIST, :ONBOARDING_QUESTION, :NO_MODEL
17
+
18
+ # @rbs @cwd: String
19
+ # @rbs @host: Riffer::Rig::Hosts::_Host
20
+ # @rbs @env: Riffer::Rig::Env
21
+ # @rbs @home: String
22
+ # @rbs @riffer_config: Riffer::Config
23
+ # @rbs @tracked_files: Array[String]
24
+
25
+ # @dynamic tracked_files
26
+ attr_reader :tracked_files #: Array[String]
27
+
28
+ # @rbs cwd: String
29
+ # @rbs host: Riffer::Rig::Hosts::_Host
30
+ # @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
31
+ # @rbs home: String
32
+ # @rbs riffer_config: Riffer::Config
33
+ # @rbs model: String?
34
+ # @rbs extensions: bool
35
+ # @rbs skills: bool
36
+ # @rbs agents_md: bool
37
+ # @rbs tools: Array[String]?
38
+ # @rbs max_steps: Integer?
39
+ # @rbs return: Riffer::Rig::Runtime
40
+ def self.runtime(
41
+ cwd:,
42
+ host:,
43
+ env: Riffer::Rig::Env.load,
44
+ home: Dir.home,
45
+ riffer_config: Riffer.config,
46
+ model: nil,
47
+ extensions: true,
48
+ skills: true,
49
+ agents_md: true,
50
+ tools: nil,
51
+ max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
52
+ )
53
+ new(cwd:, host:, env:, home:, riffer_config:).runtime(model:, extensions:, skills:, agents_md:, tools:, max_steps:)
54
+ end
55
+
56
+ # @rbs cwd: String
57
+ # @rbs host: Riffer::Rig::Hosts::_Host
58
+ # @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
59
+ # @rbs home: String
60
+ # @rbs riffer_config: Riffer::Config
61
+ # @rbs return: void
62
+ def initialize(cwd:, host:, env: Riffer::Rig::Env.load, home: Dir.home, riffer_config: Riffer.config)
63
+ raise ConfigurationError, env.message if env.is_a?(Riffer::Rig::Env::Invalid)
64
+
65
+ @cwd = cwd
66
+ @host = host
67
+ @env = env
68
+ @home = home
69
+ @riffer_config = riffer_config
70
+ @tracked_files = []
71
+ end
72
+
73
+ # @rbs model: String?
74
+ # @rbs extensions: bool
75
+ # @rbs skills: bool
76
+ # @rbs agents_md: bool
77
+ # @rbs tools: Array[String]?
78
+ # @rbs max_steps: Integer?
79
+ # @rbs return: Riffer::Rig::Runtime
80
+ def runtime(
81
+ model: nil,
82
+ extensions: true,
83
+ skills: true,
84
+ agents_md: true,
85
+ tools: nil,
86
+ max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
87
+ )
88
+ settings = Riffer::Rig::Settings.merge(
89
+ Riffer::Rig::Settings.read(home_settings_path),
90
+ Riffer::Rig::Settings.read(File.join(@cwd, '.riffer', 'settings.json'))
91
+ )
92
+ document = Riffer::Rig::Settings::Document.new(settings)
93
+ selected = select_model(model || @env.model || document.model)
94
+ provider = Riffer::Rig::Settings.provider_for(selected).to_s
95
+ credentials = { provider.to_sym => credentials_for(provider) }
96
+ stripped = { skills: skills, agents_md: agents_md }.reject { |_name, kept| kept }.keys
97
+ loaded = Riffer::Rig::Bundled::BY_NAME.except(*document.disabled.map(&:to_sym), *stripped).values
98
+ loaded += load_rig_files(document.autoload) if extensions
99
+
100
+ Riffer::Rig::Runtime.new(
101
+ selected,
102
+ extensions: loaded,
103
+ tools: tools,
104
+ settings: settings,
105
+ host: @host,
106
+ cwd: @cwd,
107
+ max_steps: max_steps,
108
+ credentials: credentials,
109
+ pricing: document.models,
110
+ riffer_config: @riffer_config,
111
+ model_options: Riffer::Rig::Settings.model_options(selected, document.reasoning)
112
+ )
113
+ end
114
+
115
+ private
116
+
117
+ # @rbs autoload: bool
118
+ # @rbs return: Array[Riffer::Rig::Extension]
119
+ def load_rig_files(autoload)
120
+ recorded = gem_extension_files(autoload).flat_map { |file| load_file(File.expand_path(file), track: false) }
121
+ home = File.expand_path(File.join(@home, '.riffer', 'rig.rb'))
122
+ recorded.concat(load_file(home, track: true)) if File.file?(home)
123
+ project = File.expand_path(File.join(@cwd, '.riffer', 'rig.rb'))
124
+ recorded.concat(load_file(project, track: true)) if File.file?(project) && trusted?(project)
125
+ recorded
126
+ end
127
+
128
+ # @rbs autoload: bool
129
+ # @rbs return: Array[String]
130
+ def gem_extension_files(autoload)
131
+ return [] unless autoload
132
+
133
+ Gem.find_files(EXTENSION_FEATURE)
134
+ end
135
+
136
+ # @rbs path: String
137
+ # @rbs return: bool
138
+ def trusted?(path)
139
+ stored = Riffer::Rig::Trust.read(trust_path)[path]
140
+ return stored unless stored.nil?
141
+
142
+ asked = @host.capabilities.include?(:confirm)
143
+ decision = asked ? @host.confirm("Trust #{path}?") : false
144
+ Riffer::Rig::Trust.store(trust_path, path, decision) if asked
145
+ decision
146
+ end
147
+
148
+ # @rbs return: String
149
+ def trust_path
150
+ File.join(@home, '.riffer', 'trust.json')
151
+ end
152
+
153
+ # @rbs path: String
154
+ # @rbs track: bool
155
+ # @rbs return: Array[Riffer::Rig::Extension]
156
+ def load_file(path, track:)
157
+ before = Riffer::Rig.extensions
158
+ before_features = $LOADED_FEATURES.dup
159
+ begin
160
+ load path
161
+ rescue StandardError => e
162
+ track_loaded(before_features, path) if track
163
+ @host.notify("#{path} failed to load: #{e.message}", level: :error)
164
+ return []
165
+ end
166
+ track_loaded(before_features, path) if track
167
+ Riffer::Rig.extensions.values.reject { |extension| before[extension.name] == extension }
168
+ end
169
+
170
+ # @rbs before_features: Array[String]
171
+ # @rbs path: String
172
+ # @rbs return: void
173
+ def track_loaded(before_features, path)
174
+ recorded = ($LOADED_FEATURES - before_features) + [path] #: Array[String]
175
+ @tracked_files.concat(recorded.select { |feature| local?(feature) })
176
+ end
177
+
178
+ # @rbs feature: String
179
+ # @rbs return: bool
180
+ def local?(feature)
181
+ roots = [File.expand_path(File.join(@home, '.riffer')), File.expand_path(@cwd)] #: Array[String]
182
+ roots.any? { |root| feature.start_with?("#{root}#{File::SEPARATOR}") }
183
+ end
184
+
185
+ # @rbs configured: String?
186
+ # @rbs return: String
187
+ def select_model(configured)
188
+ model = configured || ask_for_model
189
+ rejection = Riffer::Rig::Settings.rejection(model)
190
+ raise ConfigurationError, rejection if rejection
191
+
192
+ Riffer::Rig::Settings.store_model(model, path: home_settings_path) unless configured
193
+ model
194
+ end
195
+
196
+ # @rbs return: String
197
+ def ask_for_model
198
+ answer = asking_host.ask(ONBOARDING_QUESTION).to_s.strip
199
+ raise ConfigurationError, NO_MODEL if answer.empty?
200
+
201
+ answer
202
+ end
203
+
204
+ # @rbs provider: String
205
+ # @rbs return: Hash[Symbol, String]
206
+ def credentials_for(provider)
207
+ resolution = Riffer::Rig::Credentials.resolve(
208
+ provider,
209
+ host: asking_host,
210
+ env: @env,
211
+ auth_path: File.join(@home, '.riffer', 'auth.json'),
212
+ settings_path: home_settings_path
213
+ )
214
+ raise ConfigurationError, missing_credentials(provider, resolution.missing) unless resolution.missing.empty?
215
+
216
+ Riffer::Rig::Credentials.apply(provider, resolution.values, config: @riffer_config)
217
+ resolution.values
218
+ end
219
+
220
+ # @rbs provider: String
221
+ # @rbs missing: Array[Symbol]
222
+ # @rbs return: String
223
+ def missing_credentials(provider, missing)
224
+ setup = Riffer::Rig::ProviderSetup.for(provider)
225
+ fields = setup.fields.select { |field| missing.include?(field.name) }
226
+ wanted = fields.map { |field| "#{field.name} (#{field.env.join(' or ')})" }.join(', ')
227
+ "#{provider} has no #{wanted}; set it in the environment or run riffer interactively to paste it" \
228
+ "#{" (create one at #{setup.url})" if setup.url}"
229
+ end
230
+
231
+ # @rbs return: Riffer::Rig::Hosts::_Host
232
+ def asking_host
233
+ @host.capabilities.include?(:ask) ? @host : Riffer::Rig::Hosts::Null.new
234
+ end
235
+
236
+ # @rbs return: String
237
+ def home_settings_path
238
+ File.join(@home, '.riffer', 'settings.json')
239
+ end
240
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Mcp::Declaration
4
+ include Riffer::Rig::Support::Equatable
5
+
6
+ # @dynamic url, headers
7
+ attr_reader :url #: String
8
+ attr_reader :headers #: Hash[String, String]
9
+
10
+ # @rbs url: String
11
+ # @rbs headers: Hash[String, String]
12
+ # @rbs return: void
13
+ def initialize(url:, headers:)
14
+ @url = url
15
+ @headers = headers
16
+ freeze
17
+ end
18
+
19
+ # @rbs return: Hash[Symbol, untyped]
20
+ def to_h
21
+ { url: url, headers: headers }
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Mcp::Server
4
+ # @dynamic declaration, registration
5
+ attr_reader :declaration #: Riffer::Rig::Mcp::Declaration
6
+ attr_reader :registration #: Riffer::Mcp::Registration
7
+
8
+ # @rbs declaration: Riffer::Rig::Mcp::Declaration
9
+ # @rbs registration: Riffer::Mcp::Registration
10
+ # @rbs return: void
11
+ def initialize(declaration:, registration:)
12
+ @declaration = declaration
13
+ @registration = registration
14
+ freeze
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Mcp
4
+ extend self
5
+
6
+ # @rbs home: Hash[Symbol, untyped]
7
+ # @rbs project: Hash[Symbol, untyped]
8
+ # @rbs return: Hash[Symbol, untyped]
9
+ def merge(home, project)
10
+ home.merge(project, servers: home[:servers].to_h.merge(project[:servers].to_h))
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Prompts::AgentsMd
4
+ extend self
5
+
6
+ FRAMING = 'The user wrote the following instructions in AGENTS.md files, ordered from the most general to ' \
7
+ 'the one closest to the working directory. Follow them; where they conflict with the guidance ' \
8
+ 'above, they take precedence, and where they conflict with each other, the later file wins.' #: String
9
+
10
+ # @rbs cwd: String
11
+ # @rbs return: Array[String]
12
+ def paths(cwd)
13
+ global = File.join(Dir.home, '.riffer', 'AGENTS.md')
14
+ project = Riffer::Rig::Directories.ancestors(File.expand_path(cwd)).reverse.map { |dir| File.join(dir, 'AGENTS.md') }
15
+ [global, *project].uniq.select { |path| File.file?(path) }
16
+ end
17
+
18
+ # @rbs cwd: String
19
+ # @rbs return: String?
20
+ def section(cwd)
21
+ blocks = paths(cwd).map { |path| block(path) }
22
+ [FRAMING, *blocks].join("\n\n") unless blocks.empty?
23
+ end
24
+
25
+ private
26
+
27
+ # @rbs path: String
28
+ # @rbs return: String
29
+ def block(path)
30
+ "<project_instructions path=\"#{path}\">\n#{File.read(path)}\n</project_instructions>"
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Prompts
4
+ end
@@ -1,19 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Riffer::Rig::Registrar
4
- # @rbs @tools: Array[singleton(Riffer::Tool)]
4
+ class NameCollisionError < StandardError; end
5
+
6
+ CORE_SETTINGS_KEYS = %w[model reasoning models reload extensions sessions providers tools].freeze #: Array[String]
7
+
8
+ EVENTS = %i[
9
+ session_start session_end
10
+ before_prompt before_tool_call before_request
11
+ after_tool_call after_response turn_end
12
+ stream
13
+ ].freeze #: Array[Symbol]
14
+
15
+ # @rbs @tools: Hash[String, singleton(Riffer::Tool)]
5
16
  # @rbs @prompts: Hash[Symbol, ^(Riffer::Rig::Runtime) -> String?]
17
+ # @rbs @commands: Hash[String, Riffer::Rig::Command]
18
+ # @rbs @skill_sources: Array[^(Riffer::Rig::Runtime) -> Riffer::Skills::Backend]
19
+ # @rbs @declared_settings: Hash[Symbol, untyped]
20
+ # @rbs @given_settings: Hash[Symbol, untyped]
21
+ # @rbs @mcp_servers: Hash[String, Riffer::Rig::Mcp::Declaration]
22
+ # @rbs @hooks: Hash[Symbol, Array[^(Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base) -> untyped]]
6
23
 
24
+ # @dynamic extension
25
+ attr_reader :extension #: String
26
+
27
+ # @rbs extension: String
28
+ # @rbs settings: Hash[Symbol, untyped]
7
29
  # @rbs return: void
8
- def initialize
9
- @tools = []
30
+ def initialize(extension, settings = {})
31
+ @extension = extension
32
+ @tools = {}
10
33
  @prompts = {}
34
+ @commands = {}
35
+ @skill_sources = []
36
+ @declared_settings = {}
37
+ @given_settings = settings
38
+ @mcp_servers = {}
39
+ @hooks = EVENTS.to_h { |event| [event, []] }
40
+ end
41
+
42
+ # @rbs return: NameCollisionError?
43
+ def collision
44
+ return unless CORE_SETTINGS_KEYS.include?(@extension)
45
+
46
+ NameCollisionError.new("extension name #{@extension} collides with a core settings key")
11
47
  end
12
48
 
13
49
  # @rbs klass: singleton(Riffer::Tool)
14
50
  # @rbs return: void
15
51
  def tool(klass)
16
- @tools << klass
52
+ @tools[klass.identifier] = klass
17
53
  end
18
54
 
19
55
  # @rbs name: Symbol
@@ -23,7 +59,46 @@ class Riffer::Rig::Registrar
23
59
  @prompts[name] = block
24
60
  end
25
61
 
26
- # @rbs return: Array[singleton(Riffer::Tool)]
62
+ # @rbs name: String
63
+ # @rbs description: String
64
+ # @rbs &block: (Riffer::Rig::Command::Context) -> void
65
+ # @rbs return: void
66
+ def command(name, description:, &)
67
+ @commands[name] = Riffer::Rig::Command.new(name, description: description, extension: @extension, &)
68
+ end
69
+
70
+ # @rbs &block: (Riffer::Rig::Runtime) -> Riffer::Skills::Backend
71
+ # @rbs return: void
72
+ def skills(&block)
73
+ @skill_sources << block
74
+ end
75
+
76
+ # @rbs key: Symbol
77
+ # @rbs default: untyped
78
+ # @rbs return: void
79
+ def setting(key, default:)
80
+ @declared_settings[key] = default
81
+ end
82
+
83
+ # @rbs name: String
84
+ # @rbs url: String
85
+ # @rbs headers: Hash[String, String]
86
+ # @rbs return: void
87
+ def mcp(name, url:, headers: {})
88
+ @mcp_servers[name] = Riffer::Rig::Mcp::Declaration.new(url: url, headers: headers)
89
+ end
90
+
91
+ # @rbs event: Symbol
92
+ # @rbs &block: (Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base) -> untyped
93
+ # @rbs return: void
94
+ def on(event, &block)
95
+ hooks = @hooks.fetch(event) do
96
+ raise Riffer::ArgumentError, "unknown event #{event.inspect}; expected one of #{EVENTS.join(', ')}"
97
+ end
98
+ hooks << block
99
+ end
100
+
101
+ # @rbs return: Hash[String, singleton(Riffer::Tool)]
27
102
  def tools
28
103
  @tools.dup
29
104
  end
@@ -32,4 +107,44 @@ class Riffer::Rig::Registrar
32
107
  def prompts
33
108
  @prompts.dup
34
109
  end
110
+
111
+ # @rbs return: Hash[String, Riffer::Rig::Command]
112
+ def commands
113
+ @commands.dup
114
+ end
115
+
116
+ # @rbs return: Array[^(Riffer::Rig::Runtime) -> Riffer::Skills::Backend]
117
+ def skill_sources
118
+ @skill_sources.dup
119
+ end
120
+
121
+ # @rbs return: Hash[Symbol, untyped]
122
+ def settings
123
+ @declared_settings.merge(@given_settings)
124
+ end
125
+
126
+ # @rbs return: Hash[Symbol, untyped]
127
+ def declared_settings
128
+ @declared_settings.dup
129
+ end
130
+
131
+ # @rbs return: Hash[String, Riffer::Rig::Mcp::Declaration]
132
+ def mcp_servers
133
+ @mcp_servers.dup
134
+ end
135
+
136
+ # @rbs return: Array[String]
137
+ def registrations
138
+ [
139
+ *@tools.keys.map { |identifier| "tool #{identifier}" },
140
+ *@prompts.keys.map { |name| "prompt section #{name}" },
141
+ *@commands.keys.map { |name| "command #{name}" },
142
+ *@mcp_servers.keys.map { |name| "MCP server #{name}" }
143
+ ]
144
+ end
145
+
146
+ # @rbs return: Hash[Symbol, Array[^(Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base) -> untyped]]
147
+ def hooks
148
+ @hooks.transform_values(&:dup)
149
+ end
35
150
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Runtime::Blocked
4
+ # @dynamic reason
5
+ attr_reader :reason #: String
6
+
7
+ # @rbs reason: String
8
+ # @rbs return: void
9
+ def initialize(reason)
10
+ @reason = reason
11
+ freeze
12
+ end
13
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Runtime::Hooks
4
+ # @rbs @hooks: Hash[Symbol, Array[^(Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base) -> untyped]]
5
+ # @rbs @host: Riffer::Rig::Hosts::_Host
6
+
7
+ # @rbs hooks: Hash[Symbol, Array[^(Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base) -> untyped]]
8
+ # @rbs host: Riffer::Rig::Hosts::_Host
9
+ # @rbs return: void
10
+ def initialize(hooks, host)
11
+ @hooks = hooks
12
+ @host = host
13
+ end
14
+
15
+ # @rbs name: Symbol
16
+ # @rbs event: Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base
17
+ # @rbs return: void
18
+ def observe(name, event)
19
+ @hooks.fetch(name).each { |hook| call(name, hook, event) }
20
+ end
21
+
22
+ # @rbs text: String
23
+ # @rbs return: String | Riffer::Rig::Runtime::Blocked
24
+ def before_prompt(text)
25
+ accepts = ->(value) { value.is_a?(String) }
26
+ announce(veto(:before_prompt, text, accepts) { |current| Riffer::Rig::Events::BeforePrompt.new(current) })
27
+ end
28
+
29
+ # @rbs tool: String
30
+ # @rbs args: Hash[Symbol, untyped]
31
+ # @rbs return: Hash[Symbol, untyped] | Riffer::Rig::Runtime::Blocked
32
+ def before_tool_call(tool, args)
33
+ accepts = ->(value) { value.is_a?(Hash) }
34
+ veto(:before_tool_call, args, accepts) { |current| Riffer::Rig::Events::BeforeToolCall.new(tool, current) }
35
+ end
36
+
37
+ # @rbs messages: Array[::Riffer::Messages::Base]
38
+ # @rbs return: Array[::Riffer::Messages::Base] | Riffer::Rig::Runtime::Blocked
39
+ def before_request(messages)
40
+ accepts = ->(value) { value.is_a?(Array) && value.all?(::Riffer::Messages::Base) }
41
+ announce(veto(:before_request, messages, accepts) { |current| Riffer::Rig::Events::BeforeRequest.new(current) })
42
+ end
43
+
44
+ private
45
+
46
+ # @rbs name: Symbol
47
+ # @rbs payload: untyped
48
+ # @rbs accepts: ^(untyped) -> bool
49
+ # @rbs &: (untyped) -> Riffer::Rig::Events::_Event
50
+ # @rbs return: untyped
51
+ def veto(name, payload, accepts)
52
+ @hooks.fetch(name).reduce(payload) do |current, hook|
53
+ result = call(name, hook, yield(current))
54
+ verdict, reason = result
55
+ break Riffer::Rig::Runtime::Blocked.new(reason&.to_s || "blocked by a #{name} hook") if verdict == :block
56
+
57
+ accepts.call(result) ? result : current
58
+ end
59
+ end
60
+
61
+ # @rbs outcome: untyped
62
+ # @rbs return: untyped
63
+ def announce(outcome)
64
+ @host.notify(outcome.reason, level: :warning) if outcome.is_a?(Riffer::Rig::Runtime::Blocked)
65
+ outcome
66
+ end
67
+
68
+ # @rbs name: Symbol
69
+ # @rbs hook: ^(Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base) -> untyped
70
+ # @rbs event: Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base
71
+ # @rbs return: untyped
72
+ def call(name, hook, event)
73
+ hook.call(event)
74
+ rescue StandardError => e
75
+ @host.notify("#{name} hook failed: #{e.message}", level: :error)
76
+ nil
77
+ end
78
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # riffer runs its before guardrails once per run, just ahead of the first
4
+ # request; the Runtime's on_message observer covers the requests after it.
5
+ class Riffer::Rig::Runtime::RequestGuardrail < Riffer::Guardrail
6
+ # @rbs @hooks: Riffer::Rig::Runtime::Hooks
7
+
8
+ # @rbs hooks: Riffer::Rig::Runtime::Hooks
9
+ # @rbs return: void
10
+ def initialize(hooks:)
11
+ super()
12
+ @hooks = hooks
13
+ end
14
+
15
+ # @rbs messages: Array[Riffer::Messages::Base]
16
+ # @rbs context: untyped
17
+ # @rbs return: Riffer::Guardrails::Result
18
+ def process_input(messages, context:)
19
+ verdict = @hooks.before_request(messages)
20
+ return block(verdict.reason) if verdict.is_a?(Riffer::Rig::Runtime::Blocked)
21
+
22
+ verdict.equal?(messages) ? pass(messages) : transform(verdict.dup)
23
+ end
24
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ class Riffer::Rig::Runtime::ToolRuntime < Riffer::Tools::Runtime
6
+ # @rbs @hooks: Riffer::Rig::Runtime::Hooks
7
+
8
+ # @rbs hooks: Riffer::Rig::Runtime::Hooks
9
+ # @rbs return: void
10
+ def initialize(hooks)
11
+ # Sequential, so hooks never run concurrently.
12
+ super(runner: Riffer::Runner::Sequential.new)
13
+ @hooks = hooks
14
+ end
15
+
16
+ private
17
+
18
+ # @rbs tool_call: Riffer::Messages::Assistant::ToolCall
19
+ # @rbs tools: Array[singleton(Riffer::Tool)]
20
+ # @rbs context: Riffer::Agent::Context?
21
+ # @rbs assistant_message: Riffer::Messages::Assistant?
22
+ # @rbs return: Riffer::Tools::Response
23
+ def dispatch_tool_call(tool_call, tools:, context:, assistant_message: nil)
24
+ args = parse_arguments(tool_call.arguments)
25
+ return super unless args.is_a?(Hash)
26
+
27
+ verdict = @hooks.before_tool_call(tool_call.name, args)
28
+ return Riffer::Tools::Response.error(verdict.reason, type: :blocked) if verdict.is_a?(Riffer::Rig::Runtime::Blocked)
29
+
30
+ call = verdict.equal?(args) ? tool_call : replace_arguments(tool_call, verdict)
31
+ response = super(call, tools: tools, context: context, assistant_message: assistant_message)
32
+ @hooks.observe(:after_tool_call, Riffer::Rig::Events::AfterToolCall.new(tool_call.name, verdict, response))
33
+ response
34
+ rescue JSON::ParserError
35
+ super
36
+ end
37
+
38
+ # @rbs tool_call: Riffer::Messages::Assistant::ToolCall
39
+ # @rbs args: Hash[Symbol, untyped]
40
+ # @rbs return: Riffer::Messages::Assistant::ToolCall
41
+ def replace_arguments(tool_call, args)
42
+ Riffer::Messages::Assistant::ToolCall.new(call_id: tool_call.call_id, name: tool_call.name, arguments: args.to_json)
43
+ end
44
+ end