riffer-rig 0.5.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 +58 -0
- data/README.md +77 -7
- 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 +70 -102
- 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/resolution.rb +16 -0
- data/lib/riffer/rig/credentials.rb +151 -60
- data/lib/riffer/rig/directories.rb +20 -0
- data/lib/riffer/rig/env/invalid.rb +13 -0
- data/lib/riffer/rig/env.rb +40 -0
- 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 +28 -0
- data/lib/riffer/rig/events/notify.rb +28 -0
- data/lib/riffer/rig/events/session_end.rb +25 -0
- data/lib/riffer/rig/events/session_start.rb +28 -0
- data/lib/riffer/rig/events/skill_activated.rb +25 -0
- data/lib/riffer/rig/events/turn_end.rb +33 -0
- data/lib/riffer/rig/events.rb +4 -0
- data/lib/riffer/rig/extension/failure.rb +16 -0
- data/lib/riffer/rig/extension.rb +16 -5
- data/lib/riffer/rig/hosts/mirror.rb +59 -0
- data/lib/riffer/rig/hosts/null.rb +36 -0
- 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/provider_setup/field.rb +24 -0
- data/lib/riffer/rig/provider_setup.rb +84 -0
- data/lib/riffer/rig/registrar.rb +134 -7
- data/lib/riffer/rig/runtime/blocked.rb +13 -0
- data/lib/riffer/rig/runtime/cancel_flag.rb +27 -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 +582 -70
- data/lib/riffer/rig/settings/document.rb +28 -12
- data/lib/riffer/rig/settings/pricing.rb +16 -6
- data/lib/riffer/rig/settings.rb +108 -81
- data/lib/riffer/rig/skills/sources.rb +28 -0
- data/lib/riffer/rig/skills.rb +12 -0
- data/lib/riffer/rig/support/equatable.rb +21 -0
- data/lib/riffer/rig/{ui → terminal}/animator.rb +6 -13
- data/lib/riffer/rig/{ui → terminal}/banner.rb +17 -21
- data/lib/riffer/rig/{ui → terminal}/cursor.rb +4 -6
- 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 +121 -87
- data/lib/riffer/rig/{ui → terminal}/smoother.rb +15 -23
- data/lib/riffer/rig/{ui → terminal}/theme.rb +12 -14
- data/lib/riffer/rig/terminal.rb +207 -0
- data/lib/riffer/rig/tools/bash.rb +36 -13
- 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 +10 -15
- 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 -27
- 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/resolution.rbs +13 -0
- data/sig/generated/riffer/rig/credentials.rbs +70 -41
- 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 +26 -0
- 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 +21 -0
- data/sig/generated/riffer/rig/events/notify.rbs +21 -0
- data/sig/generated/riffer/rig/events/session_end.rbs +18 -0
- data/sig/generated/riffer/rig/events/session_start.rbs +21 -0
- data/sig/generated/riffer/rig/events/skill_activated.rbs +18 -0
- data/sig/generated/riffer/rig/events/turn_end.rbs +24 -0
- data/sig/generated/riffer/rig/events.rbs +4 -0
- data/sig/generated/riffer/rig/extension/failure.rbs +13 -0
- data/sig/generated/riffer/rig/extension.rbs +14 -5
- data/sig/generated/riffer/rig/hosts/mirror.rbs +41 -0
- data/sig/generated/riffer/rig/hosts/null.rbs +26 -0
- 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/provider_setup/field.rbs +22 -0
- data/sig/generated/riffer/rig/provider_setup.rbs +30 -0
- data/sig/generated/riffer/rig/registrar.rbs +88 -6
- data/sig/generated/riffer/rig/runtime/blocked.rbs +10 -0
- data/sig/generated/riffer/rig/runtime/cancel_flag.rbs +19 -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 +262 -40
- data/sig/generated/riffer/rig/settings/document.rbs +13 -9
- data/sig/generated/riffer/rig/settings/pricing.rbs +7 -3
- data/sig/generated/riffer/rig/settings.rbs +56 -64
- 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 -13
- data/sig/generated/riffer/rig/terminal/banner.rbs +39 -0
- data/sig/generated/riffer/rig/terminal/cursor.rbs +31 -0
- 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/terminal/smoother.rbs +59 -0
- data/sig/generated/riffer/rig/{ui → terminal}/theme.rbs +5 -7
- data/sig/generated/riffer/rig/terminal.rbs +85 -0
- data/sig/generated/riffer/rig/tools/bash.rbs +14 -1
- data/sig/generated/riffer/rig.rbs +4 -7
- data/sig/manual/riffer/rig/cli.rbs +2 -4
- 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/credentials.rbs +2 -4
- 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 +12 -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/settings.rbs +2 -4
- data/sig/manual/riffer/rig/skills.rbs +6 -0
- data/sig/manual/riffer/rig/support.rbs +8 -0
- data/sig/manual/riffer/rig/terminal/banner.rbs +6 -0
- metadata +173 -35
- data/lib/riffer/rig/coding_agent.rb +0 -53
- data/lib/riffer/rig/host.rb +0 -52
- data/lib/riffer/rig/repl.rb +0 -189
- data/lib/riffer/rig/token_tally.rb +0 -74
- data/sig/generated/riffer/rig/coding_agent.rbs +0 -9
- data/sig/generated/riffer/rig/host.rbs +0 -42
- data/sig/generated/riffer/rig/repl.rbs +0 -77
- data/sig/generated/riffer/rig/token_tally.rbs +0 -61
- data/sig/generated/riffer/rig/ui/banner.rbs +0 -42
- data/sig/generated/riffer/rig/ui/cursor.rbs +0 -33
- data/sig/generated/riffer/rig/ui/renderer.rbs +0 -118
- data/sig/generated/riffer/rig/ui/smoother.rbs +0 -72
- data/sig/manual/riffer/rig/ui/banner.rbs +0 -8
- data/sig/manual/riffer/rig/ui.rbs +0 -5
data/lib/riffer/rig/runtime.rb
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'date'
|
|
4
|
+
require 'securerandom'
|
|
4
5
|
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# completion and returned as riffer's Agent::Response.
|
|
8
|
-
#
|
|
9
|
-
# runtime = Riffer::Rig::Runtime.new('mock/x')
|
|
10
|
-
# runtime.prompt('hello') { |event| ... } # yields riffer StreamEvents
|
|
11
|
-
# runtime.prompt('hello').each { |event| } # an Enumerator without a block
|
|
12
|
-
# response = runtime.ask('hello') # a Riffer::Agent::Response
|
|
13
|
-
#
|
|
14
|
-
# Two Runtimes in one process share nothing but the process-wide extension
|
|
15
|
-
# registry and riffer's provider repository. One prompt runs at a time; a
|
|
16
|
-
# second while one is running raises Riffer::Rig::Runtime::BusyError. The
|
|
17
|
-
# Runtime never renders, never prints, never reads the filesystem.
|
|
6
|
+
# Never renders, prints or reads the filesystem: anything a host needs is a
|
|
7
|
+
# Runtime feature, so embedders get it too.
|
|
18
8
|
class Riffer::Rig::Runtime
|
|
19
|
-
# Raised when a second prompt, ask or registrar build runs while one is
|
|
20
|
-
# already running on this Runtime.
|
|
21
9
|
class BusyError < StandardError; end
|
|
22
10
|
|
|
11
|
+
class ClosedError < StandardError; end
|
|
12
|
+
|
|
23
13
|
BASE_PROMPT_TEMPLATE = <<~TEXT
|
|
24
14
|
You are %<name>s, a general-purpose agent. You work by using the tools you have
|
|
25
15
|
been given; each tool describes what it does and when to use it.
|
|
@@ -33,43 +23,59 @@ class Riffer::Rig::Runtime
|
|
|
33
23
|
|
|
34
24
|
DEFAULT_NAME = 'riffer'
|
|
35
25
|
|
|
36
|
-
#
|
|
37
|
-
#
|
|
26
|
+
# Unlimited: riffer's own default (16) is too small for a general-purpose
|
|
27
|
+
# harness.
|
|
38
28
|
DEFAULT_MAX_STEPS = nil #: Integer?
|
|
39
29
|
|
|
40
|
-
|
|
41
|
-
# interface _Host
|
|
42
|
-
# def ask: (?String, ?options: Array[String]?, ?secret: bool) -> String?
|
|
43
|
-
# def confirm: (?String) -> bool
|
|
44
|
-
# def notify: (?String, ?level: Symbol) -> void
|
|
45
|
-
# def progress: (?String) { () -> void } -> void
|
|
46
|
-
# def capabilities: () -> Set[Symbol]
|
|
47
|
-
# end
|
|
30
|
+
INTERRUPT_CANCELLED = :cancelled #: Symbol
|
|
48
31
|
|
|
49
32
|
# @rbs @agent: Riffer::Agent
|
|
33
|
+
# @rbs @base_prompt: String
|
|
34
|
+
# @rbs @cancel_flag: Riffer::Rig::Runtime::CancelFlag
|
|
35
|
+
# @rbs @credentials: Hash[Symbol, Hash[Symbol, String]]
|
|
50
36
|
# @rbs @cwd: String
|
|
51
|
-
# @rbs @host:
|
|
37
|
+
# @rbs @host: Riffer::Rig::Hosts::Mirror
|
|
38
|
+
# @rbs @id: String
|
|
52
39
|
# @rbs @settings: Hash[Symbol, untyped]
|
|
53
40
|
# @rbs @busy: bool
|
|
54
|
-
# @rbs @
|
|
41
|
+
# @rbs @closed: bool
|
|
42
|
+
# @rbs @session_start_pending: bool
|
|
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]
|
|
55
55
|
|
|
56
|
-
# @dynamic agent, cwd, host, settings
|
|
56
|
+
# @dynamic agent, credentials, cwd, host, id, settings, declared_settings
|
|
57
57
|
attr_reader :agent #: Riffer::Agent
|
|
58
|
+
attr_reader :credentials #: Hash[Symbol, Hash[Symbol, String]]
|
|
58
59
|
attr_reader :cwd #: String
|
|
59
|
-
attr_reader :host #:
|
|
60
|
+
attr_reader :host #: Riffer::Rig::Hosts::Mirror
|
|
61
|
+
attr_reader :id #: String
|
|
60
62
|
attr_reader :settings #: Hash[Symbol, untyped]
|
|
63
|
+
attr_reader :declared_settings #: Hash[String, Hash[Symbol, untyped]]
|
|
61
64
|
|
|
62
65
|
# @rbs model: String
|
|
63
66
|
# @rbs extensions: Array[Riffer::Rig::Extension]
|
|
64
67
|
# @rbs tools: Array[String]?
|
|
65
68
|
# @rbs settings: Hash[Symbol, untyped]
|
|
66
|
-
# @rbs host: _Host
|
|
69
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
67
70
|
# @rbs cwd: String?
|
|
68
71
|
# @rbs name: String
|
|
69
72
|
# @rbs instructions: String?
|
|
70
|
-
# @rbs credentials: Hash[
|
|
73
|
+
# @rbs credentials: Hash[Symbol, Hash[Symbol, String]]
|
|
71
74
|
# @rbs pricing: Hash[String, Riffer::Rig::Settings::Pricing]
|
|
75
|
+
# @rbs riffer_config: Riffer::Config
|
|
76
|
+
# @rbs mcp_registry: Riffer::Rig::Mcp::_Registry
|
|
72
77
|
# @rbs max_steps: Integer?
|
|
78
|
+
# @rbs model_options: Hash[Symbol, untyped]
|
|
73
79
|
# @rbs snapshot: Hash[Symbol, untyped]?
|
|
74
80
|
# @rbs return: void
|
|
75
81
|
def initialize(
|
|
@@ -77,72 +83,565 @@ class Riffer::Rig::Runtime
|
|
|
77
83
|
extensions: [],
|
|
78
84
|
tools: nil,
|
|
79
85
|
settings: {},
|
|
80
|
-
host: Riffer::Rig::
|
|
86
|
+
host: Riffer::Rig::Hosts::Null.new,
|
|
81
87
|
cwd: nil,
|
|
82
88
|
name: DEFAULT_NAME,
|
|
83
89
|
instructions: nil,
|
|
84
90
|
credentials: {},
|
|
85
91
|
pricing: {},
|
|
92
|
+
riffer_config: Riffer.config,
|
|
93
|
+
mcp_registry: Riffer::Mcp,
|
|
86
94
|
max_steps: DEFAULT_MAX_STEPS,
|
|
95
|
+
model_options: {},
|
|
87
96
|
snapshot: nil
|
|
88
97
|
)
|
|
89
|
-
|
|
98
|
+
# Doubles as the snapshot id and ACP sessionId.
|
|
99
|
+
@id = snapshot ? snapshot.fetch(:id) : ::SecureRandom.uuid_v7
|
|
100
|
+
@host = Riffer::Rig::Hosts::Mirror.new(host)
|
|
90
101
|
@cwd = cwd || Dir.pwd
|
|
91
|
-
@
|
|
102
|
+
@credentials = credentials
|
|
92
103
|
|
|
93
104
|
@busy = false
|
|
94
|
-
@
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
@closed = false
|
|
106
|
+
@cancel_flag = Riffer::Rig::Runtime::CancelFlag.new
|
|
107
|
+
@session_start_pending = true
|
|
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
|
|
114
|
+
@base_prompt = instructions || format(BASE_PROMPT_TEMPLATE, name: name)
|
|
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) }
|
|
106
124
|
end
|
|
107
125
|
|
|
108
126
|
# @rbs text: String
|
|
109
|
-
# @rbs &block: ?(Riffer::StreamEvents::Base) -> void
|
|
110
|
-
# @rbs return: (nil | Enumerator[Riffer::StreamEvents::Base,
|
|
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])
|
|
111
129
|
def prompt(text, &block)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
120
140
|
end
|
|
121
|
-
ensure
|
|
122
|
-
@busy = false
|
|
123
141
|
end
|
|
124
142
|
|
|
125
143
|
# @rbs text: String
|
|
126
144
|
# @rbs return: Riffer::Agent::Response
|
|
127
145
|
def ask(text)
|
|
146
|
+
claim
|
|
147
|
+
begin
|
|
148
|
+
turn(text).each { |event| event }
|
|
149
|
+
ensure
|
|
150
|
+
@busy = false
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
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
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# @rbs return: nil
|
|
229
|
+
def cancel
|
|
230
|
+
@cancel_flag.set
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# @rbs return: Riffer::Providers::TokenUsage?
|
|
235
|
+
def tally
|
|
236
|
+
@agent.context.token_usage
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# @rbs return: void
|
|
240
|
+
def close
|
|
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.
|
|
245
|
+
@closed = true
|
|
246
|
+
unregister_mcp_servers(@mcp_servers)
|
|
247
|
+
@hooks.observe(:session_end, Riffer::Rig::Events::SessionEnd.new(:close)) unless @session_start_pending
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
private
|
|
251
|
+
|
|
252
|
+
# @rbs return: void
|
|
253
|
+
def claim
|
|
128
254
|
raise BusyError, 'a prompt is already running on this Runtime' if @busy
|
|
255
|
+
raise ClosedError, 'this Runtime is closed' if @closed
|
|
129
256
|
|
|
130
257
|
@busy = true
|
|
131
|
-
@agent.stream(text).each { |event| event }
|
|
132
|
-
ensure
|
|
133
|
-
@busy = false
|
|
134
258
|
end
|
|
135
259
|
|
|
136
|
-
|
|
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)
|
|
406
|
+
Enumerator.new do |yielder|
|
|
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
|
|
419
|
+
response
|
|
420
|
+
end
|
|
421
|
+
end
|
|
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
|
+
|
|
464
|
+
# @rbs return: void
|
|
465
|
+
def interrupt_if_cancelled
|
|
466
|
+
return unless @cancel_flag.set?
|
|
467
|
+
|
|
468
|
+
@agent.session.discard_pending_tool_calls
|
|
469
|
+
# Upstream candidate: a cancel token on riffer's run loop. Until then the
|
|
470
|
+
# loop can only be stopped from inside, at a message boundary.
|
|
471
|
+
@agent.interrupt!(INTERRUPT_CANCELLED)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# @rbs outcome: Riffer::Agent::Outcome
|
|
475
|
+
# @rbs return: Symbol
|
|
476
|
+
def stop_reason(outcome)
|
|
477
|
+
# Upstream candidate: riffer's outcome vocabulary is closed, so a cancel
|
|
478
|
+
# reaches us as :interrupted with the reason in detail.
|
|
479
|
+
cancelled = outcome.reason == :interrupted && outcome.detail == INTERRUPT_CANCELLED.to_s
|
|
480
|
+
cancelled ? INTERRUPT_CANCELLED : outcome.reason
|
|
481
|
+
end
|
|
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
|
|
137
540
|
|
|
138
541
|
# @rbs extensions: Array[Riffer::Rig::Extension]
|
|
139
|
-
# @rbs
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
142
576
|
|
|
143
|
-
|
|
144
|
-
|
|
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)]
|
|
145
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) }]
|
|
604
|
+
end
|
|
605
|
+
end
|
|
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? }
|
|
146
645
|
end
|
|
147
646
|
|
|
148
647
|
# @rbs registered: Array[singleton(Riffer::Tool)]
|
|
@@ -154,9 +653,22 @@ class Riffer::Rig::Runtime
|
|
|
154
653
|
registered.select { |klass| allowlist.include?(klass.name) }
|
|
155
654
|
end
|
|
156
655
|
|
|
157
|
-
# @rbs
|
|
656
|
+
# @rbs return: void
|
|
657
|
+
def refresh_system_message
|
|
658
|
+
# Upstream candidate: riffer resolves `instructions` once, in Agent.new, so
|
|
659
|
+
# a per-turn system message has to be swapped into the session by hand.
|
|
660
|
+
session = @agent.session
|
|
661
|
+
session.set([Riffer::Messages::System.new(system_prompt(rendered_sections)), *session.messages.drop(1)])
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# @rbs return: Array[String]
|
|
665
|
+
def rendered_sections
|
|
666
|
+
@prompts.each_value.map { |section| section.call(self).to_s }.reject(&:empty?)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# @rbs sections: Array[String]
|
|
158
670
|
# @rbs return: String
|
|
159
|
-
def system_prompt(
|
|
160
|
-
|
|
671
|
+
def system_prompt(sections)
|
|
672
|
+
[@base_prompt, *sections, "Current date: #{Date.today}\nCurrent working directory: #{@cwd}"].join("\n\n")
|
|
161
673
|
end
|
|
162
674
|
end
|