riffer-rig 0.6.0 → 0.7.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 (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +37 -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 +163 -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 +27 -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/{ui → terminal}/renderer.rb +115 -66
  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.rb +207 -0
  66. data/lib/riffer/rig/tools/bash.rb +6 -4
  67. data/lib/riffer/rig/tools/edit.rb +3 -2
  68. data/lib/riffer/rig/tools/read.rb +3 -2
  69. data/lib/riffer/rig/tools/write.rb +3 -2
  70. data/lib/riffer/rig/version.rb +1 -1
  71. data/lib/riffer/rig.rb +8 -8
  72. data/sig/generated/riffer/rig/bundled/agents_md.rbs +5 -0
  73. data/sig/generated/riffer/rig/bundled/bash.rbs +5 -0
  74. data/sig/generated/riffer/rig/bundled/edit.rbs +5 -0
  75. data/sig/generated/riffer/rig/bundled/mcp.rbs +5 -0
  76. data/sig/generated/riffer/rig/bundled/read.rbs +5 -0
  77. data/sig/generated/riffer/rig/bundled/skills.rbs +5 -0
  78. data/sig/generated/riffer/rig/bundled/write.rbs +5 -0
  79. data/sig/generated/riffer/rig/bundled.rbs +5 -0
  80. data/sig/generated/riffer/rig/cli/flags.rbs +40 -0
  81. data/sig/generated/riffer/rig/cli.rbs +30 -35
  82. data/sig/generated/riffer/rig/command/context.rbs +50 -0
  83. data/sig/generated/riffer/rig/command.rbs +23 -0
  84. data/sig/generated/riffer/rig/commands/model.rbs +27 -0
  85. data/sig/generated/riffer/rig/commands/skill.rbs +12 -0
  86. data/sig/generated/riffer/rig/commands.rbs +4 -0
  87. data/sig/generated/riffer/rig/credentials.rbs +4 -4
  88. data/sig/generated/riffer/rig/directories.rbs +11 -0
  89. data/sig/generated/riffer/rig/env/invalid.rbs +10 -0
  90. data/sig/generated/riffer/rig/env.rbs +7 -1
  91. data/sig/generated/riffer/rig/events/after_response.rbs +18 -0
  92. data/sig/generated/riffer/rig/events/after_tool_call.rbs +24 -0
  93. data/sig/generated/riffer/rig/events/before_prompt.rbs +18 -0
  94. data/sig/generated/riffer/rig/events/before_request.rbs +18 -0
  95. data/sig/generated/riffer/rig/events/before_tool_call.rbs +21 -0
  96. data/sig/generated/riffer/rig/events/command_output.rbs +3 -1
  97. data/sig/generated/riffer/rig/events/notify.rbs +3 -1
  98. data/sig/generated/riffer/rig/events/session_end.rbs +3 -1
  99. data/sig/generated/riffer/rig/events/session_start.rbs +3 -1
  100. data/sig/generated/riffer/rig/events/skill_activated.rbs +3 -1
  101. data/sig/generated/riffer/rig/events/turn_end.rbs +3 -1
  102. data/sig/generated/riffer/rig/extension/failure.rbs +13 -0
  103. data/sig/generated/riffer/rig/extension.rbs +14 -1
  104. data/sig/generated/riffer/rig/hosts/mirror.rbs +9 -9
  105. data/sig/generated/riffer/rig/hosts/null.rbs +1 -1
  106. data/sig/generated/riffer/rig/loader.rbs +77 -0
  107. data/sig/generated/riffer/rig/mcp/declaration.rbs +18 -0
  108. data/sig/generated/riffer/rig/mcp/server.rbs +13 -0
  109. data/sig/generated/riffer/rig/mcp.rbs +8 -0
  110. data/sig/generated/riffer/rig/prompts/agents_md.rbs +19 -0
  111. data/sig/generated/riffer/rig/prompts.rbs +4 -0
  112. data/sig/generated/riffer/rig/registrar.rbs +78 -4
  113. data/sig/generated/riffer/rig/runtime/blocked.rbs +10 -0
  114. data/sig/generated/riffer/rig/runtime/hooks.rbs +49 -0
  115. data/sig/generated/riffer/rig/runtime/request_guardrail.rbs +16 -0
  116. data/sig/generated/riffer/rig/runtime/tool_runtime.rbs +23 -0
  117. data/sig/generated/riffer/rig/runtime.rbs +225 -22
  118. data/sig/generated/riffer/rig/settings/document.rbs +11 -11
  119. data/sig/generated/riffer/rig/settings/pricing.rbs +7 -2
  120. data/sig/generated/riffer/rig/settings.rbs +36 -23
  121. data/sig/generated/riffer/rig/skills/sources.rbs +18 -0
  122. data/sig/generated/riffer/rig/skills.rbs +7 -0
  123. data/sig/generated/riffer/rig/support/equatable.rbs +15 -0
  124. data/sig/generated/riffer/rig/{ui → terminal}/animator.rbs +5 -5
  125. data/sig/generated/riffer/rig/terminal/banner.rbs +39 -0
  126. data/sig/generated/riffer/rig/{ui → terminal}/cursor.rbs +5 -5
  127. data/sig/generated/riffer/rig/terminal/host.rbs +50 -0
  128. data/sig/generated/riffer/rig/{ui → terminal}/palette.rbs +2 -2
  129. data/sig/generated/riffer/rig/terminal/renderer.rbs +126 -0
  130. data/sig/generated/riffer/rig/{ui → terminal}/smoother.rbs +5 -5
  131. data/sig/generated/riffer/rig/{ui → terminal}/theme.rbs +5 -5
  132. data/sig/generated/riffer/rig/terminal.rbs +85 -0
  133. data/sig/generated/riffer/rig/tools/bash.rbs +2 -1
  134. data/sig/generated/riffer/rig.rbs +4 -0
  135. data/sig/manual/riffer/rig/commands/model.rbs +6 -0
  136. data/sig/manual/riffer/rig/commands/skill.rbs +6 -0
  137. data/sig/manual/riffer/rig/directories.rbs +6 -0
  138. data/sig/manual/riffer/rig/events.rbs +6 -0
  139. data/sig/manual/riffer/rig/hosts.rbs +7 -0
  140. data/sig/manual/riffer/rig/mcp.rbs +11 -0
  141. data/sig/manual/riffer/rig/prompts/agents_md.rbs +6 -0
  142. data/sig/manual/riffer/rig/{ui/banner.rbs → skills.rbs} +2 -2
  143. data/sig/manual/riffer/rig/support.rbs +8 -0
  144. data/sig/manual/riffer/rig/terminal/banner.rbs +6 -0
  145. metadata +140 -27
  146. data/lib/riffer/rig/coding_agent.rb +0 -53
  147. data/lib/riffer/rig/hosts/base.rb +0 -36
  148. data/lib/riffer/rig/repl.rb +0 -188
  149. data/lib/riffer/rig/token_tally.rb +0 -54
  150. data/sig/generated/riffer/rig/coding_agent.rbs +0 -9
  151. data/sig/generated/riffer/rig/events/event.rbs +0 -20
  152. data/sig/generated/riffer/rig/hosts/base.rbs +0 -26
  153. data/sig/generated/riffer/rig/repl.rbs +0 -69
  154. data/sig/generated/riffer/rig/token_tally.rbs +0 -42
  155. data/sig/generated/riffer/rig/ui/banner.rbs +0 -42
  156. data/sig/generated/riffer/rig/ui/renderer.rbs +0 -94
  157. data/sig/manual/riffer/rig/ui.rbs +0 -5
@@ -40,27 +40,42 @@ class Riffer::Rig::Runtime
40
40
  # @rbs @busy: bool
41
41
  # @rbs @closed: bool
42
42
  # @rbs @session_start_pending: bool
43
- # @rbs @registrar: Riffer::Rig::Registrar
43
+ # @rbs @session_start_reason: Symbol
44
+ # @rbs @model_override: String?
45
+ # @rbs @message_observers: Array[^(Riffer::Messages::Base) -> void]
46
+ # @rbs @prompts: Hash[Symbol, ^(Riffer::Rig::Runtime) -> String?]
47
+ # @rbs @commands: Hash[String, Riffer::Rig::Command]
48
+ # @rbs @declared_settings: Hash[String, Hash[Symbol, untyped]]
49
+ # @rbs @errors: Array[Riffer::Rig::Extension::Failure]
50
+ # @rbs @hooks: Riffer::Rig::Runtime::Hooks
51
+ # @rbs @tool_allowlist: Array[String]?
52
+ # @rbs @mcp_registry: Riffer::Rig::Mcp::_Registry
53
+ # @rbs @mcp_servers: Hash[String, Riffer::Rig::Mcp::Server]
54
+ # @rbs @model_options: Hash[Symbol, untyped]
44
55
 
45
- # @dynamic agent, credentials, cwd, host, id, settings
56
+ # @dynamic agent, credentials, cwd, host, id, settings, declared_settings
46
57
  attr_reader :agent #: Riffer::Agent
47
58
  attr_reader :credentials #: Hash[Symbol, Hash[Symbol, String]]
48
59
  attr_reader :cwd #: String
49
60
  attr_reader :host #: Riffer::Rig::Hosts::Mirror
50
61
  attr_reader :id #: String
51
62
  attr_reader :settings #: Hash[Symbol, untyped]
63
+ attr_reader :declared_settings #: Hash[String, Hash[Symbol, untyped]]
52
64
 
53
65
  # @rbs model: String
54
66
  # @rbs extensions: Array[Riffer::Rig::Extension]
55
67
  # @rbs tools: Array[String]?
56
68
  # @rbs settings: Hash[Symbol, untyped]
57
- # @rbs host: Riffer::Rig::Hosts::Base
69
+ # @rbs host: Riffer::Rig::Hosts::_Host
58
70
  # @rbs cwd: String?
59
71
  # @rbs name: String
60
72
  # @rbs instructions: String?
61
73
  # @rbs credentials: Hash[Symbol, Hash[Symbol, String]]
62
74
  # @rbs pricing: Hash[String, Riffer::Rig::Settings::Pricing]
75
+ # @rbs riffer_config: Riffer::Config
76
+ # @rbs mcp_registry: Riffer::Rig::Mcp::_Registry
63
77
  # @rbs max_steps: Integer?
78
+ # @rbs model_options: Hash[Symbol, untyped]
64
79
  # @rbs snapshot: Hash[Symbol, untyped]?
65
80
  # @rbs return: void
66
81
  def initialize(
@@ -74,68 +89,140 @@ class Riffer::Rig::Runtime
74
89
  instructions: nil,
75
90
  credentials: {},
76
91
  pricing: {},
92
+ riffer_config: Riffer.config,
93
+ mcp_registry: Riffer::Mcp,
77
94
  max_steps: DEFAULT_MAX_STEPS,
95
+ model_options: {},
78
96
  snapshot: nil
79
97
  )
80
98
  # Doubles as the snapshot id and ACP sessionId.
81
- @id = ::SecureRandom.uuid_v7
99
+ @id = snapshot ? snapshot.fetch(:id) : ::SecureRandom.uuid_v7
82
100
  @host = Riffer::Rig::Hosts::Mirror.new(host)
83
101
  @cwd = cwd || Dir.pwd
84
- @settings = settings
85
102
  @credentials = credentials
86
103
 
87
104
  @busy = false
88
105
  @closed = false
89
106
  @cancel_flag = Riffer::Rig::Runtime::CancelFlag.new
90
107
  @session_start_pending = true
91
- @registrar = build_registrar(extensions)
92
- tool_classes = select_tools(@registrar.tools, tools)
108
+ @session_start_reason = snapshot ? :restore : :new
109
+ @message_observers = []
110
+ Riffer::Rig::Settings::Pricing.register(pricing, riffer_config.pricing)
111
+ @errors = []
112
+ @tool_allowlist = tools
113
+ @model_options = model_options
93
114
  @base_prompt = instructions || format(BASE_PROMPT_TEMPLATE, name: name)
94
-
95
- @agent = Riffer::Agent.new(
96
- context: { cancel_flag: @cancel_flag },
97
- config: Riffer::Agent::Config.new(
98
- model: model,
99
- instructions: system_prompt([]),
100
- tools_config: tool_classes,
101
- max_steps: max_steps
102
- )
103
- )
104
- @agent.session.on_message { |_message| interrupt_if_cancelled }
115
+ @mcp_registry = mcp_registry
116
+ @mcp_servers = {}
117
+ registrars = build_registrars(extensions, settings) { |extension, error| record_error(extension, error) }
118
+ mcp_servers = register_mcp_servers(registrars)
119
+ hooks = Riffer::Rig::Runtime::Hooks.new(merge_hooks(registrars), @host)
120
+ config = agent_config(registrars, hooks, max_steps)
121
+ agent = snapshot ? restore(snapshot, model, config) : build_agent(model, config)
122
+ install(registrars, mcp_servers, settings, hooks, agent)
123
+ @agent.session.on_message { |message| observe_message(message) }
105
124
  end
106
125
 
107
126
  # @rbs text: String
108
- # @rbs &block: ?(::Riffer::StreamEvents::Base | Riffer::Rig::Events::Event) -> void
109
- # @rbs return: (nil | Enumerator[::Riffer::StreamEvents::Base | Riffer::Rig::Events::Event, Riffer::Agent::Response])
127
+ # @rbs &block: ?(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void
128
+ # @rbs return: (nil | Enumerator[::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event, Riffer::Agent::Response])
110
129
  def prompt(text, &block)
111
- raise BusyError, 'a prompt is already running on this Runtime' if @busy
112
- raise ClosedError, 'this Runtime is closed' if @closed
113
-
114
- @busy = true
115
- @cancel_flag.clear
116
- refresh_system_message
117
- if block
118
- wrap_stream(@agent.stream(text)).each(&block)
119
- nil
120
- else
121
- wrap_stream(@agent.stream(text))
130
+ claim
131
+ begin
132
+ if block
133
+ turn(text).each(&block)
134
+ nil
135
+ else
136
+ turn(text)
137
+ end
138
+ ensure
139
+ @busy = false
122
140
  end
123
- ensure
124
- @busy = false
125
141
  end
126
142
 
127
143
  # @rbs text: String
128
144
  # @rbs return: Riffer::Agent::Response
129
145
  def ask(text)
130
- raise BusyError, 'a prompt is already running on this Runtime' if @busy
131
- raise ClosedError, 'this Runtime is closed' if @closed
146
+ claim
147
+ begin
148
+ turn(text).each { |event| event }
149
+ ensure
150
+ @busy = false
151
+ end
152
+ end
132
153
 
133
- @busy = true
134
- @cancel_flag.clear
135
- refresh_system_message
136
- @agent.stream(text).each { |event| event }
137
- ensure
138
- @busy = false
154
+ # @rbs return: Array[Riffer::Rig::Command]
155
+ def commands
156
+ @commands.values
157
+ end
158
+
159
+ # @rbs name: String
160
+ # @rbs args: String
161
+ # @rbs &block: ?(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void
162
+ # @rbs return: nil
163
+ def run_command(name, args = '', &block)
164
+ claim
165
+ emit = block || ->(_event) {} #: ^(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void
166
+ begin
167
+ execute(name, args, emit)
168
+ ensure
169
+ @busy = false
170
+ end
171
+ @host.drain.each(&emit)
172
+ nil
173
+ end
174
+
175
+ # @rbs return: Array[Riffer::Rig::Extension::Failure]
176
+ def errors
177
+ @errors.dup
178
+ end
179
+
180
+ # @rbs return: String
181
+ def model
182
+ "#{@agent.provider_name}/#{@agent.model_name}"
183
+ end
184
+
185
+ # @rbs model: String
186
+ # @rbs return: void
187
+ def model=(model)
188
+ # Upstream candidate: riffer resolves the model Proc once, in Agent.new, so
189
+ # a switch rebuilds the agent over the same session and config.
190
+ @agent = successor(model, @agent.config)
191
+ @model_override = model
192
+ end
193
+
194
+ # @rbs extensions: Array[Riffer::Rig::Extension]
195
+ # @rbs settings: Hash[Symbol, untyped]
196
+ # @rbs return: nil
197
+ def rebuild(extensions:, settings:)
198
+ claim
199
+ begin
200
+ registrars = build_registrars(extensions, settings) { |_extension, error| raise error }
201
+ mcp_servers = register_mcp_servers(registrars)
202
+ hooks = Riffer::Rig::Runtime::Hooks.new(merge_hooks(registrars), @host)
203
+ agent = successor(model, agent_config(registrars, hooks, @agent.config.max_steps))
204
+ lifecycle(:session_end, Riffer::Rig::Events::SessionEnd.new(:reload))
205
+ @errors = []
206
+ dropped = @mcp_servers.except(*mcp_servers.keys)
207
+ install(registrars, mcp_servers, settings, hooks, agent)
208
+ unregister_mcp_servers(dropped)
209
+ lifecycle(:session_start, Riffer::Rig::Events::SessionStart.new(@id, :reload))
210
+ ensure
211
+ @busy = false
212
+ end
213
+ nil
214
+ end
215
+
216
+ # @rbs return: Hash[Symbol, untyped]
217
+ def to_h
218
+ { id: @id, messages: @agent.session.messages.map(&:to_h), model: @model_override, skills: activated_skills }
219
+ end
220
+
221
+ # @rbs &block: (Riffer::Messages::Base) -> void
222
+ # @rbs return: nil
223
+ def on_message(&block)
224
+ @message_observers << block
225
+ nil
139
226
  end
140
227
 
141
228
  # @rbs return: nil
@@ -144,28 +231,236 @@ class Riffer::Rig::Runtime
144
231
  nil
145
232
  end
146
233
 
234
+ # @rbs return: Riffer::Providers::TokenUsage?
235
+ def tally
236
+ @agent.context.token_usage
237
+ end
238
+
147
239
  # @rbs return: void
148
240
  def close
149
- # TODO: emit Riffer::Rig::Events::SessionEnd once the rebuild ticket settles
150
- # the stream's session_end reasons.
241
+ return if @closed
242
+
243
+ # TODO: emit Riffer::Rig::Events::SessionEnd on the stream once the rebuild
244
+ # ticket settles the stream's session_end reasons.
151
245
  @closed = true
246
+ unregister_mcp_servers(@mcp_servers)
247
+ @hooks.observe(:session_end, Riffer::Rig::Events::SessionEnd.new(:close)) unless @session_start_pending
152
248
  end
153
249
 
154
250
  private
155
251
 
156
- # @rbs stream: Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
157
- # @rbs return: Enumerator[::Riffer::StreamEvents::Base | Riffer::Rig::Events::Event, Riffer::Agent::Response]
158
- def wrap_stream(stream)
252
+ # @rbs return: void
253
+ def claim
254
+ raise BusyError, 'a prompt is already running on this Runtime' if @busy
255
+ raise ClosedError, 'this Runtime is closed' if @closed
256
+
257
+ @busy = true
258
+ end
259
+
260
+ # @rbs model: String
261
+ # @rbs config: Riffer::Agent::Config
262
+ # @rbs session: Riffer::Agent::Session?
263
+ # @rbs return: Riffer::Agent
264
+ def build_agent(model, config, session: nil)
265
+ Riffer::Agent.new(session: session, context: { cancel_flag: @cancel_flag, cwd: @cwd, model: model }, config: config)
266
+ end
267
+
268
+ # @rbs model: String
269
+ # @rbs config: Riffer::Agent::Config
270
+ # @rbs return: Riffer::Agent
271
+ def successor(model, config)
272
+ agent = build_agent(model, config, session: @agent.session)
273
+ agent.context.token_usage = @agent.context.token_usage
274
+ reactivate(agent.context.skills, activated_skills)
275
+ agent
276
+ end
277
+
278
+ # @rbs registrars: Array[Riffer::Rig::Registrar]
279
+ # @rbs hooks: Riffer::Rig::Runtime::Hooks
280
+ # @rbs max_steps: Numeric?
281
+ # @rbs return: Riffer::Agent::Config
282
+ def agent_config(registrars, hooks, max_steps)
283
+ config = Riffer::Agent::Config.new(
284
+ model: ->(context) { context[:model] },
285
+ model_options: @model_options,
286
+ instructions: system_prompt([]),
287
+ tools_config: select_tools(registrars.flat_map { |registrar| registrar.tools.to_a }.to_h.values, @tool_allowlist),
288
+ max_steps: max_steps,
289
+ tool_runtime: Riffer::Rig::Runtime::ToolRuntime.new(hooks),
290
+ skills_config: skills_config(registrars.flat_map(&:skill_sources))
291
+ )
292
+ config.add_guardrail(:before, klass: Riffer::Rig::Runtime::RequestGuardrail, options: { hooks: hooks })
293
+ # Upstream candidate: riffer resolves MCP tools inside the agent, after
294
+ # tools_config, so the tools: allowlist never sees them.
295
+ config.add_mcp(mcp_tag, progressive: false)
296
+ config
297
+ end
298
+
299
+ # @rbs sources: Array[^(Riffer::Rig::Runtime) -> Riffer::Skills::Backend]
300
+ # @rbs return: Riffer::Skills::Config?
301
+ def skills_config(sources)
302
+ return if sources.empty?
303
+
304
+ config = Riffer::Skills::Config.new
305
+ config.backend(Riffer::Rig::Skills::Sources.new(sources.map { |source| source.call(self) }))
306
+ config
307
+ end
308
+
309
+ # @rbs registrars: Array[Riffer::Rig::Registrar]
310
+ # @rbs mcp_servers: Hash[String, Riffer::Rig::Mcp::Server]
311
+ # @rbs settings: Hash[Symbol, untyped]
312
+ # @rbs hooks: Riffer::Rig::Runtime::Hooks
313
+ # @rbs agent: Riffer::Agent
314
+ # @rbs return: void
315
+ def install(registrars, mcp_servers, settings, hooks, agent)
316
+ overrides(registrars).each { |message| @host.notify(message, level: :info) }
317
+ @mcp_servers = mcp_servers
318
+ @declared_settings = registrars.to_h { |registrar| [registrar.extension, registrar.declared_settings] }
319
+ .reject { |_extension, declared| declared.empty? }
320
+ @settings = with_declared_defaults(settings)
321
+ @prompts = registrars.flat_map { |registrar| registrar.prompts.to_a }.to_h
322
+ skills = agent.context.skills&.skills&.values || []
323
+ commands = [
324
+ Riffer::Rig::Commands::Model.command,
325
+ *skills.map { |skill| Riffer::Rig::Commands::Skill.command(skill) },
326
+ *registrars.flat_map { |registrar| registrar.commands.values }
327
+ ]
328
+ @commands = commands.to_h { |command| [command.name, command] }
329
+ @hooks = hooks
330
+ @agent = agent
331
+ end
332
+
333
+ # A session that has not started yet opens with session_start(:new) on its
334
+ # first turn instead.
335
+ # @rbs name: Symbol
336
+ # @rbs event: Riffer::Rig::Events::_Event
337
+ # @rbs return: void
338
+ def lifecycle(name, event)
339
+ return if @session_start_pending
340
+
341
+ @hooks.observe(name, event)
342
+ @host.queue(event)
343
+ end
344
+
345
+ # @rbs text: String
346
+ # @rbs return: Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
347
+ def start_turn(text)
348
+ @cancel_flag.clear
349
+ refresh_system_message
350
+ prompt = @hooks.before_prompt(text)
351
+ return blocked_turn(prompt.reason) if prompt.is_a?(Riffer::Rig::Runtime::Blocked)
352
+
353
+ stream = @agent.stream(prompt)
354
+ # Upstream candidate: riffer adds the prompt to the session silently, so
355
+ # its on_message never sees the user message a store has to keep.
356
+ deliver(@agent.session.messages.last)
357
+ stream
358
+ end
359
+
360
+ # @rbs reason: String
361
+ # @rbs return: Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
362
+ def blocked_turn(reason)
363
+ response = Riffer::Agent::Response.new(
364
+ '',
365
+ outcome: Riffer::Agent::Outcome.new(reason: :guardrail_blocked, detail: reason),
366
+ messages: @agent.session.messages.dup.freeze
367
+ )
368
+ Enumerator.new { |_yielder| response }
369
+ end
370
+
371
+ # @rbs name: String
372
+ # @rbs args: String
373
+ # @rbs emit: ^(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void
374
+ # @rbs return: void
375
+ def execute(name, args, emit)
376
+ command = @commands.fetch(name, nil)
377
+ return @host.notify("Unknown command: #{name}", level: :error) unless command
378
+
379
+ begin
380
+ command.call(command_context(command, args, emit))
381
+ rescue StandardError => e
382
+ @host.notify("Command #{name} failed: #{e.message}", level: :error)
383
+ end
384
+ end
385
+
386
+ # @rbs command: Riffer::Rig::Command
387
+ # @rbs args: String
388
+ # @rbs emit: ^(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void
389
+ # @rbs return: Riffer::Rig::Command::Context
390
+ def command_context(command, args, emit)
391
+ settings = @settings[command.extension.to_sym] || {} #: Hash[Symbol, untyped]
392
+ Riffer::Rig::Command::Context.new(
393
+ command.name,
394
+ args,
395
+ runtime: self,
396
+ host: @host,
397
+ settings: settings,
398
+ emit: emit,
399
+ turn: ->(text) { turn(text).each(&emit) }
400
+ )
401
+ end
402
+
403
+ # @rbs text: String
404
+ # @rbs return: Enumerator[::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event, Riffer::Agent::Response]
405
+ def turn(text)
159
406
  Enumerator.new do |yielder|
160
- yielder << Riffer::Rig::Events::SessionStart.new(@id, :new) if @session_start_pending
161
- @session_start_pending = false
162
- @host.drain.each { |event| yielder << event }
163
- response = stream.each { |event| yielder << event }
164
- yielder << Riffer::Rig::Events::TurnEnd.new(stop_reason(response.outcome), response.token_usage)
407
+ if @session_start_pending
408
+ @session_start_pending = false
409
+ session_start = Riffer::Rig::Events::SessionStart.new(@id, @session_start_reason)
410
+ @hooks.observe(:session_start, session_start)
411
+ yielder << session_start
412
+ end
413
+ @host.drain.each { |queued| yielder << queued }
414
+ response = start_turn(text).each { |event| pass_through(yielder, event) }
415
+ turn_end = Riffer::Rig::Events::TurnEnd.new(stop_reason(response.outcome), response.token_usage)
416
+ @hooks.observe(:turn_end, turn_end)
417
+ @host.drain.each { |queued| yielder << queued }
418
+ yielder << turn_end
165
419
  response
166
420
  end
167
421
  end
168
422
 
423
+ # @rbs yielder: Enumerator::Yielder
424
+ # @rbs event: ::Riffer::StreamEvents::Base
425
+ # @rbs return: void
426
+ def pass_through(yielder, event)
427
+ @hooks.observe(:stream, event)
428
+ yielder << event
429
+ # Mid-turn notifies (a failing hook, a blocked request) reach the stream
430
+ # next to the event that raised them, not at the next turn.
431
+ @host.drain.each { |queued| yielder << queued }
432
+ end
433
+
434
+ # @rbs message: Riffer::Messages::Base
435
+ # @rbs return: void
436
+ def observe_message(message)
437
+ deliver(message)
438
+ interrupt_if_cancelled
439
+ case message
440
+ when Riffer::Messages::Assistant
441
+ @hooks.observe(:after_response, Riffer::Rig::Events::AfterResponse.new(message))
442
+ when Riffer::Messages::Tool
443
+ before_next_request if @agent.session.pending_tool_calls.last.empty?
444
+ end
445
+ end
446
+
447
+ # @rbs message: Riffer::Messages::Base
448
+ # @rbs return: void
449
+ def deliver(message)
450
+ @message_observers.each { |observer| observer.call(message) }
451
+ end
452
+
453
+ # @rbs return: void
454
+ def before_next_request
455
+ messages = @agent.session.messages
456
+ # Upstream candidate: riffer has no hook between tool results and the next
457
+ # request, so the last tool result's on_message stands in for one.
458
+ verdict = @hooks.before_request(messages)
459
+ return @agent.interrupt!(verdict.reason) if verdict.is_a?(Riffer::Rig::Runtime::Blocked)
460
+
461
+ @agent.session.set(verdict.dup) unless verdict.equal?(messages)
462
+ end
463
+
169
464
  # @rbs return: void
170
465
  def interrupt_if_cancelled
171
466
  return unless @cancel_flag.set?
@@ -185,16 +480,170 @@ class Riffer::Rig::Runtime
185
480
  cancelled ? INTERRUPT_CANCELLED : outcome.reason
186
481
  end
187
482
 
483
+ # @rbs snapshot: Hash[Symbol, untyped]
484
+ # @rbs model: String
485
+ # @rbs config: Riffer::Agent::Config
486
+ # @rbs return: Riffer::Agent
487
+ def restore(snapshot, model, config)
488
+ @model_override = restorable_model(snapshot.fetch(:model), model)
489
+ # Upstream candidate: riffer's Serializer carries an agent's config, never
490
+ # its history, so this and to_h round-trip the session message by message;
491
+ # a Session.from_h could also heal orphaned tool calls on load.
492
+ messages = snapshot.fetch(:messages).map { |message| Riffer::Messages::Base.from_hash(message) }
493
+ session = Riffer::Agent::Session.new(messages: messages)
494
+ session.discard_pending_tool_calls
495
+ agent = build_agent(@model_override || model, config, session: session)
496
+ agent.context.token_usage = usage_of(messages)
497
+ reactivate(agent.context.skills, snapshot.fetch(:skills))
498
+ agent
499
+ end
500
+
501
+ # @rbs saved: String?
502
+ # @rbs model: String
503
+ # @rbs return: String?
504
+ def restorable_model(saved, model)
505
+ return nil unless saved
506
+
507
+ provider = saved.split('/', 2).first.to_s
508
+ return saved if @credentials.key?(provider.to_sym)
509
+
510
+ @host.notify("Not restoring model #{saved}: #{provider} has no credentials; using #{model}", level: :warning)
511
+ nil
512
+ end
513
+
514
+ # @rbs messages: Array[Riffer::Messages::Base]
515
+ # @rbs return: Riffer::Providers::TokenUsage?
516
+ def usage_of(messages)
517
+ # Upstream candidate: a nil-aware TokenUsage.sum, which riffer's own
518
+ # Agent::Run and evals also hand-roll; a loaded session could restore its
519
+ # own tally.
520
+ messages.filter_map { |message| message.token_usage if message.is_a?(Riffer::Messages::Assistant) }
521
+ .reduce { |total, usage| total + usage } # rubocop:disable Performance/Sum -- TokenUsage has no zero, and no usage must stay nil
522
+ end
523
+
524
+ # @rbs skills: Riffer::Skills::Context?
525
+ # @rbs names: Array[String]
526
+ # @rbs return: void
527
+ def reactivate(skills, names)
528
+ return unless skills
529
+
530
+ names.select { |name| skills.skills.key?(name) }.each { |name| skills.activate(name) }
531
+ end
532
+
533
+ # @rbs return: Array[String]
534
+ def activated_skills
535
+ # Upstream candidate: Skills::Context keeps its activated list private, so
536
+ # the catalog is filtered through activated? instead.
537
+ skills = @agent.context.skills
538
+ skills ? skills.skills.keys.select { |name| skills.activated?(name) } : []
539
+ end
540
+
188
541
  # @rbs extensions: Array[Riffer::Rig::Extension]
189
- # @rbs return: Riffer::Rig::Registrar
190
- def build_registrar(extensions)
191
- raise BusyError, 'a prompt is already running on this Runtime' if @busy
542
+ # @rbs settings: Hash[Symbol, untyped]
543
+ # @rbs &: (Riffer::Rig::Extension, StandardError) -> void
544
+ # @rbs return: Array[Riffer::Rig::Registrar]
545
+ def build_registrars(extensions, settings)
546
+ extensions.filter_map do |extension|
547
+ registrar = Riffer::Rig::Registrar.new(extension.name, settings[extension.name.to_sym] || {})
548
+ error = load_extension(extension, registrar)
549
+ next registrar unless error
550
+
551
+ yield(extension, error)
552
+ nil
553
+ end
554
+ end
555
+
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
+ # @rbs extension: Riffer::Rig::Extension
570
+ # @rbs error: StandardError
571
+ # @rbs return: void
572
+ def record_error(extension, error)
573
+ @errors << Riffer::Rig::Extension::Failure.new(extension: extension, error: error)
574
+ @host.notify("Extension #{extension.name} failed to load: #{error.message}", level: :error)
575
+ end
192
576
 
193
- extensions.each_with_object(Riffer::Rig::Registrar.new) do |extension, registrar|
194
- extension.run(registrar)
577
+ # @rbs registrars: Array[Riffer::Rig::Registrar]
578
+ # @rbs return: Array[String]
579
+ def overrides(registrars)
580
+ registrars
581
+ .flat_map { |registrar| registrar.registrations.map { |registration| [registration, registrar.extension] } }
582
+ .group_by(&:first)
583
+ .flat_map do |registration, claims|
584
+ claims.map(&:last).each_cons(2).map do |earlier, later|
585
+ "Extension #{later} replaces #{registration} from #{earlier}"
586
+ end
587
+ end
588
+ end
589
+
590
+ # @rbs settings: Hash[Symbol, untyped]
591
+ # @rbs return: Hash[Symbol, untyped]
592
+ def with_declared_defaults(settings)
593
+ namespaces = @declared_settings.to_h do |extension, defaults|
594
+ given = settings[extension.to_sym] || {} #: Hash[Symbol, untyped]
595
+ [extension.to_sym, defaults.merge(given)]
596
+ end
597
+ settings.merge(namespaces)
598
+ end
599
+
600
+ # @rbs return: Hash[Symbol, Array[^(Riffer::Rig::Events::_Event | ::Riffer::StreamEvents::Base) -> untyped]]
601
+ def merge_hooks(registrars)
602
+ Riffer::Rig::Registrar::EVENTS.to_h do |event|
603
+ [event, registrars.flat_map { |registrar| registrar.hooks.fetch(event) }]
195
604
  end
196
605
  end
197
606
 
607
+ # Upstream candidate: riffer's MCP registry is process-wide and keyed by
608
+ # server name alone, so each Runtime tags its registrations, and two
609
+ # Runtimes declaring the same name replace each other's.
610
+ # @rbs return: Symbol
611
+ def mcp_tag
612
+ :"riffer_rig_#{@id}"
613
+ end
614
+
615
+ # @rbs registrars: Array[Riffer::Rig::Registrar]
616
+ # @rbs return: Hash[String, Riffer::Rig::Mcp::Server]
617
+ def register_mcp_servers(registrars)
618
+ declarations = registrars.flat_map { |registrar| registrar.mcp_servers.to_a }.to_h
619
+ declarations.filter_map do |name, declaration|
620
+ live = @mcp_servers[name]
621
+ server = live && live.declaration == declaration ? live : register_mcp_server(name, declaration)
622
+ [name, server] if server
623
+ end.to_h
624
+ end
625
+
626
+ # @rbs name: String
627
+ # @rbs declaration: Riffer::Rig::Mcp::Declaration
628
+ # @rbs return: Riffer::Rig::Mcp::Server?
629
+ def register_mcp_server(name, declaration)
630
+ registration = @mcp_registry.register(
631
+ name: name, endpoint: declaration.url, tags: [mcp_tag], discovery_headers: declaration.headers
632
+ )
633
+ Riffer::Rig::Mcp::Server.new(declaration: declaration, registration: registration)
634
+ rescue StandardError => e
635
+ @host.notify("MCP server #{name} failed to register: #{e.message}", level: :error)
636
+ nil
637
+ end
638
+
639
+ # A retired registration was replaced under the same name, by another
640
+ # Runtime or a later declaration, and is no longer this Runtime's to remove.
641
+ # @rbs servers: Hash[String, Riffer::Rig::Mcp::Server]
642
+ # @rbs return: void
643
+ def unregister_mcp_servers(servers)
644
+ servers.each { |name, server| @mcp_registry.unregister(name) unless server.registration.retired? }
645
+ end
646
+
198
647
  # @rbs registered: Array[singleton(Riffer::Tool)]
199
648
  # @rbs allowlist: Array[String]?
200
649
  # @rbs return: Array[singleton(Riffer::Tool)]
@@ -214,7 +663,7 @@ class Riffer::Rig::Runtime
214
663
 
215
664
  # @rbs return: Array[String]
216
665
  def rendered_sections
217
- @registrar.prompts.each_value.map { |section| section.call(self).to_s }.reject(&:empty?)
666
+ @prompts.each_value.map { |section| section.call(self).to_s }.reject(&:empty?)
218
667
  end
219
668
 
220
669
  # @rbs sections: Array[String]