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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +37 -4
- data/exe/riffer +1 -1
- data/lib/riffer/rig/bundled/agents_md.rb +7 -0
- data/lib/riffer/rig/bundled/bash.rb +5 -0
- data/lib/riffer/rig/bundled/edit.rb +5 -0
- data/lib/riffer/rig/bundled/mcp.rb +9 -0
- data/lib/riffer/rig/bundled/read.rb +5 -0
- data/lib/riffer/rig/bundled/skills.rb +10 -0
- data/lib/riffer/rig/bundled/write.rb +5 -0
- data/lib/riffer/rig/bundled.rb +7 -0
- data/lib/riffer/rig/cli/flags.rb +77 -0
- data/lib/riffer/rig/cli.rb +69 -106
- data/lib/riffer/rig/command/context.rb +70 -0
- data/lib/riffer/rig/command.rb +29 -0
- data/lib/riffer/rig/commands/model.rb +54 -0
- data/lib/riffer/rig/commands/skill.rb +27 -0
- data/lib/riffer/rig/commands.rb +4 -0
- data/lib/riffer/rig/credentials.rb +2 -2
- data/lib/riffer/rig/directories.rb +20 -0
- data/lib/riffer/rig/env/invalid.rb +13 -0
- data/lib/riffer/rig/env.rb +12 -1
- data/lib/riffer/rig/events/after_response.rb +25 -0
- data/lib/riffer/rig/events/after_tool_call.rb +31 -0
- data/lib/riffer/rig/events/before_prompt.rb +25 -0
- data/lib/riffer/rig/events/before_request.rb +25 -0
- data/lib/riffer/rig/events/before_tool_call.rb +28 -0
- data/lib/riffer/rig/events/command_output.rb +3 -2
- data/lib/riffer/rig/events/notify.rb +3 -2
- data/lib/riffer/rig/events/session_end.rb +3 -2
- data/lib/riffer/rig/events/session_start.rb +3 -2
- data/lib/riffer/rig/events/skill_activated.rb +3 -2
- data/lib/riffer/rig/events/turn_end.rb +3 -2
- data/lib/riffer/rig/extension/failure.rb +16 -0
- data/lib/riffer/rig/extension.rb +16 -1
- data/lib/riffer/rig/hosts/mirror.rb +6 -7
- data/lib/riffer/rig/hosts/null.rb +1 -1
- data/lib/riffer/rig/loader.rb +163 -0
- data/lib/riffer/rig/mcp/declaration.rb +23 -0
- data/lib/riffer/rig/mcp/server.rb +16 -0
- data/lib/riffer/rig/mcp.rb +12 -0
- data/lib/riffer/rig/prompts/agents_md.rb +32 -0
- data/lib/riffer/rig/prompts.rb +4 -0
- data/lib/riffer/rig/registrar.rb +120 -5
- data/lib/riffer/rig/runtime/blocked.rb +13 -0
- data/lib/riffer/rig/runtime/hooks.rb +78 -0
- data/lib/riffer/rig/runtime/request_guardrail.rb +24 -0
- data/lib/riffer/rig/runtime/tool_runtime.rb +44 -0
- data/lib/riffer/rig/runtime.rb +506 -57
- data/lib/riffer/rig/settings/document.rb +27 -16
- data/lib/riffer/rig/settings/pricing.rb +16 -5
- data/lib/riffer/rig/settings.rb +73 -43
- data/lib/riffer/rig/skills/sources.rb +28 -0
- data/lib/riffer/rig/skills.rb +12 -0
- data/lib/riffer/rig/{events/event.rb → support/equatable.rb} +2 -11
- data/lib/riffer/rig/{ui → terminal}/animator.rb +4 -4
- data/lib/riffer/rig/{ui → terminal}/banner.rb +17 -21
- data/lib/riffer/rig/{ui → terminal}/cursor.rb +4 -4
- data/lib/riffer/rig/terminal/host.rb +85 -0
- data/lib/riffer/rig/{ui → terminal}/palette.rb +1 -1
- data/lib/riffer/rig/{ui → terminal}/renderer.rb +115 -66
- data/lib/riffer/rig/{ui → terminal}/smoother.rb +4 -4
- data/lib/riffer/rig/{ui → terminal}/theme.rb +12 -12
- data/lib/riffer/rig/terminal.rb +207 -0
- data/lib/riffer/rig/tools/bash.rb +6 -4
- data/lib/riffer/rig/tools/edit.rb +3 -2
- data/lib/riffer/rig/tools/read.rb +3 -2
- data/lib/riffer/rig/tools/write.rb +3 -2
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +8 -8
- data/sig/generated/riffer/rig/bundled/agents_md.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/bash.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/edit.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/mcp.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/read.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/skills.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/write.rbs +5 -0
- data/sig/generated/riffer/rig/bundled.rbs +5 -0
- data/sig/generated/riffer/rig/cli/flags.rbs +40 -0
- data/sig/generated/riffer/rig/cli.rbs +30 -35
- data/sig/generated/riffer/rig/command/context.rbs +50 -0
- data/sig/generated/riffer/rig/command.rbs +23 -0
- data/sig/generated/riffer/rig/commands/model.rbs +27 -0
- data/sig/generated/riffer/rig/commands/skill.rbs +12 -0
- data/sig/generated/riffer/rig/commands.rbs +4 -0
- data/sig/generated/riffer/rig/credentials.rbs +4 -4
- data/sig/generated/riffer/rig/directories.rbs +11 -0
- data/sig/generated/riffer/rig/env/invalid.rbs +10 -0
- data/sig/generated/riffer/rig/env.rbs +7 -1
- data/sig/generated/riffer/rig/events/after_response.rbs +18 -0
- data/sig/generated/riffer/rig/events/after_tool_call.rbs +24 -0
- data/sig/generated/riffer/rig/events/before_prompt.rbs +18 -0
- data/sig/generated/riffer/rig/events/before_request.rbs +18 -0
- data/sig/generated/riffer/rig/events/before_tool_call.rbs +21 -0
- data/sig/generated/riffer/rig/events/command_output.rbs +3 -1
- data/sig/generated/riffer/rig/events/notify.rbs +3 -1
- data/sig/generated/riffer/rig/events/session_end.rbs +3 -1
- data/sig/generated/riffer/rig/events/session_start.rbs +3 -1
- data/sig/generated/riffer/rig/events/skill_activated.rbs +3 -1
- data/sig/generated/riffer/rig/events/turn_end.rbs +3 -1
- data/sig/generated/riffer/rig/extension/failure.rbs +13 -0
- data/sig/generated/riffer/rig/extension.rbs +14 -1
- data/sig/generated/riffer/rig/hosts/mirror.rbs +9 -9
- data/sig/generated/riffer/rig/hosts/null.rbs +1 -1
- data/sig/generated/riffer/rig/loader.rbs +77 -0
- data/sig/generated/riffer/rig/mcp/declaration.rbs +18 -0
- data/sig/generated/riffer/rig/mcp/server.rbs +13 -0
- data/sig/generated/riffer/rig/mcp.rbs +8 -0
- data/sig/generated/riffer/rig/prompts/agents_md.rbs +19 -0
- data/sig/generated/riffer/rig/prompts.rbs +4 -0
- data/sig/generated/riffer/rig/registrar.rbs +78 -4
- data/sig/generated/riffer/rig/runtime/blocked.rbs +10 -0
- data/sig/generated/riffer/rig/runtime/hooks.rbs +49 -0
- data/sig/generated/riffer/rig/runtime/request_guardrail.rbs +16 -0
- data/sig/generated/riffer/rig/runtime/tool_runtime.rbs +23 -0
- data/sig/generated/riffer/rig/runtime.rbs +225 -22
- data/sig/generated/riffer/rig/settings/document.rbs +11 -11
- data/sig/generated/riffer/rig/settings/pricing.rbs +7 -2
- data/sig/generated/riffer/rig/settings.rbs +36 -23
- data/sig/generated/riffer/rig/skills/sources.rbs +18 -0
- data/sig/generated/riffer/rig/skills.rbs +7 -0
- data/sig/generated/riffer/rig/support/equatable.rbs +15 -0
- data/sig/generated/riffer/rig/{ui → terminal}/animator.rbs +5 -5
- data/sig/generated/riffer/rig/terminal/banner.rbs +39 -0
- data/sig/generated/riffer/rig/{ui → terminal}/cursor.rbs +5 -5
- data/sig/generated/riffer/rig/terminal/host.rbs +50 -0
- data/sig/generated/riffer/rig/{ui → terminal}/palette.rbs +2 -2
- data/sig/generated/riffer/rig/terminal/renderer.rbs +126 -0
- data/sig/generated/riffer/rig/{ui → terminal}/smoother.rbs +5 -5
- data/sig/generated/riffer/rig/{ui → terminal}/theme.rbs +5 -5
- data/sig/generated/riffer/rig/terminal.rbs +85 -0
- data/sig/generated/riffer/rig/tools/bash.rbs +2 -1
- data/sig/generated/riffer/rig.rbs +4 -0
- data/sig/manual/riffer/rig/commands/model.rbs +6 -0
- data/sig/manual/riffer/rig/commands/skill.rbs +6 -0
- data/sig/manual/riffer/rig/directories.rbs +6 -0
- data/sig/manual/riffer/rig/events.rbs +6 -0
- data/sig/manual/riffer/rig/hosts.rbs +7 -0
- data/sig/manual/riffer/rig/mcp.rbs +11 -0
- data/sig/manual/riffer/rig/prompts/agents_md.rbs +6 -0
- data/sig/manual/riffer/rig/{ui/banner.rbs → skills.rbs} +2 -2
- data/sig/manual/riffer/rig/support.rbs +8 -0
- data/sig/manual/riffer/rig/terminal/banner.rbs +6 -0
- metadata +140 -27
- data/lib/riffer/rig/coding_agent.rb +0 -53
- data/lib/riffer/rig/hosts/base.rb +0 -36
- data/lib/riffer/rig/repl.rb +0 -188
- data/lib/riffer/rig/token_tally.rb +0 -54
- data/sig/generated/riffer/rig/coding_agent.rbs +0 -9
- data/sig/generated/riffer/rig/events/event.rbs +0 -20
- data/sig/generated/riffer/rig/hosts/base.rbs +0 -26
- data/sig/generated/riffer/rig/repl.rbs +0 -69
- data/sig/generated/riffer/rig/token_tally.rbs +0 -42
- data/sig/generated/riffer/rig/ui/banner.rbs +0 -42
- data/sig/generated/riffer/rig/ui/renderer.rbs +0 -94
- data/sig/manual/riffer/rig/ui.rbs +0 -5
data/lib/riffer/rig/runtime.rb
CHANGED
|
@@ -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 @
|
|
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::
|
|
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
|
-
@
|
|
92
|
-
|
|
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
|
-
@
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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::
|
|
109
|
-
# @rbs return: (nil | Enumerator[::Riffer::StreamEvents::Base | Riffer::Rig::Events::
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
131
|
-
|
|
146
|
+
claim
|
|
147
|
+
begin
|
|
148
|
+
turn(text).each { |event| event }
|
|
149
|
+
ensure
|
|
150
|
+
@busy = false
|
|
151
|
+
end
|
|
152
|
+
end
|
|
132
153
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
@
|
|
666
|
+
@prompts.each_value.map { |section| section.call(self).to_s }.reject(&:empty?)
|
|
218
667
|
end
|
|
219
668
|
|
|
220
669
|
# @rbs sections: Array[String]
|