riffer 0.27.2 → 0.29.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/.agents/architecture.md +18 -11
- data/.agents/code-style.md +1 -1
- data/.agents/rbs-inline.md +2 -2
- data/.agents/testing.md +9 -5
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +17 -10
- data/CHANGELOG.md +31 -0
- data/README.md +17 -18
- data/Steepfile +7 -1
- data/docs/03_AGENTS.md +34 -3
- data/docs/04_AGENT_LIFECYCLE.md +134 -86
- data/docs/05_AGENT_LOOP.md +2 -2
- data/docs/06_TOOLS.md +9 -4
- data/docs/07_TOOL_ADVANCED.md +23 -19
- data/docs/08_MESSAGES.md +28 -31
- data/docs/09_STREAM_EVENTS.md +1 -1
- data/docs/10_CONFIGURATION.md +25 -15
- data/docs/providers/01_PROVIDERS.md +6 -0
- data/docs/providers/06_MOCK_PROVIDER.md +2 -1
- data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
- data/docs/providers/08_GEMINI.md +2 -2
- data/docs/providers/09_OPENROUTER.md +242 -0
- data/lib/riffer/agent/config.rb +173 -0
- data/lib/riffer/agent/context.rb +125 -0
- data/lib/riffer/agent/response.rb +11 -2
- data/lib/riffer/agent/run.rb +308 -0
- data/lib/riffer/agent/session/repair.rb +112 -0
- data/lib/riffer/agent/session.rb +268 -0
- data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
- data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
- data/lib/riffer/agent.rb +246 -684
- data/lib/riffer/config.rb +56 -7
- data/lib/riffer/evals/evaluator.rb +13 -3
- data/lib/riffer/evals/judge.rb +2 -2
- data/lib/riffer/evals/run_result.rb +2 -1
- data/lib/riffer/evals/scenario_result.rb +2 -1
- data/lib/riffer/guardrails/runner.rb +3 -2
- data/lib/riffer/helpers/call_or_value.rb +16 -0
- data/lib/riffer/helpers.rb +0 -1
- data/lib/riffer/mcp/authenticated_tool.rb +4 -0
- data/lib/riffer/mcp/client.rb +1 -1
- data/lib/riffer/mcp/registration.rb +2 -3
- data/lib/riffer/mcp/registry.rb +3 -1
- data/lib/riffer/mcp/tool_factory.rb +5 -0
- data/lib/riffer/messages/assistant.rb +9 -3
- data/lib/riffer/messages/base.rb +22 -0
- data/lib/riffer/messages/converter.rb +6 -6
- data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
- data/lib/riffer/messages/tool.rb +1 -1
- data/lib/riffer/messages/user.rb +4 -4
- data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
- data/lib/riffer/{param.rb → params/param.rb} +6 -6
- data/lib/riffer/params.rb +27 -21
- data/lib/riffer/providers/amazon_bedrock.rb +19 -20
- data/lib/riffer/providers/anthropic.rb +27 -28
- data/lib/riffer/providers/base.rb +10 -9
- data/lib/riffer/providers/gemini.rb +15 -12
- data/lib/riffer/providers/mock.rb +41 -13
- data/lib/riffer/providers/open_ai.rb +24 -22
- data/lib/riffer/providers/open_router.rb +318 -0
- data/lib/riffer/providers/repository.rb +1 -0
- data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
- data/lib/riffer/providers.rb +1 -0
- data/lib/riffer/runner/fibers.rb +4 -3
- data/lib/riffer/runner/sequential.rb +1 -1
- data/lib/riffer/runner/threaded.rb +1 -1
- data/lib/riffer/runner.rb +1 -1
- data/lib/riffer/skills/activate_tool.rb +4 -3
- data/lib/riffer/skills/config.rb +1 -1
- data/lib/riffer/skills/context.rb +3 -3
- data/lib/riffer/skills/filesystem_backend.rb +7 -5
- data/lib/riffer/skills/markdown_adapter.rb +1 -1
- data/lib/riffer/skills/xml_adapter.rb +1 -1
- data/lib/riffer/stream_events/interrupt.rb +10 -3
- data/lib/riffer/stream_events/token_usage_done.rb +2 -2
- data/lib/riffer/stream_events/web_search_status.rb +1 -1
- data/lib/riffer/tool.rb +3 -3
- data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
- data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
- data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
- data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +21 -15
- data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -1
- data/sig/generated/riffer/agent/config.rbs +119 -0
- data/sig/generated/riffer/agent/context.rbs +91 -0
- data/sig/generated/riffer/agent/response.rbs +10 -2
- data/sig/generated/riffer/agent/run.rbs +144 -0
- data/sig/generated/riffer/agent/session/repair.rbs +51 -0
- data/sig/generated/riffer/agent/session.rbs +145 -0
- data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
- data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
- data/sig/generated/riffer/agent.rbs +154 -225
- data/sig/generated/riffer/config.rbs +50 -5
- data/sig/generated/riffer/evals/judge.rbs +2 -2
- data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
- data/sig/generated/riffer/helpers.rbs +0 -1
- data/sig/generated/riffer/messages/assistant.rbs +7 -3
- data/sig/generated/riffer/messages/base.rbs +18 -0
- data/sig/generated/riffer/messages/converter.rbs +4 -4
- data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
- data/sig/generated/riffer/messages/user.rbs +4 -4
- data/sig/generated/riffer/params/boolean.rbs +10 -0
- data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
- data/sig/generated/riffer/params.rbs +15 -15
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
- data/sig/generated/riffer/providers/anthropic.rbs +4 -4
- data/sig/generated/riffer/providers/base.rbs +10 -10
- data/sig/generated/riffer/providers/gemini.rbs +4 -4
- data/sig/generated/riffer/providers/mock.rbs +25 -5
- data/sig/generated/riffer/providers/open_ai.rbs +4 -4
- data/sig/generated/riffer/providers/open_router.rbs +85 -0
- data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
- data/sig/generated/riffer/providers.rbs +1 -0
- data/sig/generated/riffer/runner/fibers.rbs +2 -2
- data/sig/generated/riffer/runner/sequential.rbs +2 -2
- data/sig/generated/riffer/runner/threaded.rbs +2 -2
- data/sig/generated/riffer/runner.rbs +2 -2
- data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
- data/sig/generated/riffer/skills/config.rbs +1 -1
- data/sig/generated/riffer/skills/context.rbs +2 -2
- data/sig/generated/riffer/stream_events/interrupt.rbs +7 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
- data/sig/generated/riffer/tool.rbs +5 -5
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
- data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
- data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +19 -13
- data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
- data/sig/stubs/agent_ivars.rbs +7 -0
- data/sig/stubs/async.rbs +24 -0
- data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
- data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
- data/sig/stubs/extend_self.rbs +11 -0
- data/sig/stubs/lib_ivars.rbs +101 -0
- data/sig/stubs/mcp_sdk.rbs +22 -0
- data/sig/stubs/provider_ivars.rbs +36 -0
- data/sig/stubs/provider_sdk_methods.rbs +50 -0
- data/sig/stubs/zeitwerk.rbs +12 -0
- metadata +54 -33
- data/lib/riffer/core.rb +0 -28
- data/lib/riffer/helpers/validations.rb +0 -18
- data/sig/generated/riffer/boolean.rbs +0 -10
- data/sig/generated/riffer/core.rbs +0 -19
- data/sig/generated/riffer/helpers/validations.rbs +0 -12
|
@@ -25,10 +25,21 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
25
25
|
|
|
26
26
|
# Initializes the mock provider.
|
|
27
27
|
#
|
|
28
|
+
# +responses:+ accepts an array of response hashes in the same shape
|
|
29
|
+
# +#stub_response+ takes — raw +tool_calls:+ hashes are normalised to
|
|
30
|
+
# +Riffer::Messages::Assistant::ToolCall+ instances. This is the canonical
|
|
31
|
+
# way to pre-configure canned LLM responses on an agent via
|
|
32
|
+
# +provider_options responses: [...]+.
|
|
33
|
+
#
|
|
34
|
+
# Riffer::Providers::Mock.new(responses: [
|
|
35
|
+
# {content: "", tool_calls: [{name: "tool_a", arguments: "{}"}]},
|
|
36
|
+
# {content: "Final answer"}
|
|
37
|
+
# ])
|
|
38
|
+
#
|
|
28
39
|
#--
|
|
29
40
|
#: (**untyped) -> void
|
|
30
41
|
def initialize(**options)
|
|
31
|
-
@responses = options[:responses] || []
|
|
42
|
+
@responses = (options[:responses] || []).map { |r| normalize_response(r) }
|
|
32
43
|
@current_index = 0
|
|
33
44
|
@calls = []
|
|
34
45
|
@stubbed_responses = []
|
|
@@ -40,19 +51,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
40
51
|
#
|
|
41
52
|
# provider.stub_response("Hello")
|
|
42
53
|
# provider.stub_response("", tool_calls: [{name: "my_tool", arguments: '{"key":"value"}'}])
|
|
43
|
-
# provider.stub_response("Final response", token_usage: Riffer::TokenUsage.new(input_tokens: 10, output_tokens: 5))
|
|
54
|
+
# provider.stub_response("Final response", token_usage: Riffer::Providers::TokenUsage.new(input_tokens: 10, output_tokens: 5))
|
|
44
55
|
#
|
|
45
56
|
#--
|
|
46
|
-
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::TokenUsage?) -> void
|
|
57
|
+
#: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?) -> void
|
|
47
58
|
def stub_response(content, tool_calls: [], token_usage: nil)
|
|
48
|
-
|
|
49
|
-
Riffer::Messages::Assistant::ToolCall.new(
|
|
50
|
-
call_id: tc[:call_id] || tc[:id] || "mock_call_#{idx}",
|
|
51
|
-
name: tc[:name],
|
|
52
|
-
arguments: tc[:arguments].is_a?(String) ? tc[:arguments] : tc[:arguments].to_json
|
|
53
|
-
)
|
|
54
|
-
end
|
|
55
|
-
@stubbed_responses << {role: "assistant", content: content, tool_calls: formatted_tool_calls, token_usage: token_usage}
|
|
59
|
+
@stubbed_responses << normalize_response(content: content, tool_calls: tool_calls, token_usage: token_usage)
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
# Clears all stubbed responses.
|
|
@@ -65,6 +69,30 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
65
69
|
|
|
66
70
|
private
|
|
67
71
|
|
|
72
|
+
# Normalises a response hash into Mock's internal format. Accepts the
|
|
73
|
+
# +#stub_response+ kwargs shape (+content:+, +tool_calls:+, +token_usage:+)
|
|
74
|
+
# or a pre-built hash with already-converted ToolCall instances. Raw
|
|
75
|
+
# +tool_calls:+ hashes are wrapped in +Riffer::Messages::Assistant::ToolCall+.
|
|
76
|
+
#
|
|
77
|
+
#--
|
|
78
|
+
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
79
|
+
def normalize_response(response)
|
|
80
|
+
formatted_tool_calls = (response[:tool_calls] || []).map.with_index do |tc, idx|
|
|
81
|
+
next tc if tc.is_a?(Riffer::Messages::Assistant::ToolCall)
|
|
82
|
+
Riffer::Messages::Assistant::ToolCall.new(
|
|
83
|
+
call_id: tc[:call_id] || tc[:id] || "mock_call_#{idx}",
|
|
84
|
+
name: tc[:name],
|
|
85
|
+
arguments: tc[:arguments].is_a?(String) ? tc[:arguments] : tc[:arguments].to_json
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
{
|
|
89
|
+
role: response[:role] || "assistant",
|
|
90
|
+
content: response[:content] || "",
|
|
91
|
+
tool_calls: formatted_tool_calls,
|
|
92
|
+
token_usage: response[:token_usage]
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
68
96
|
#--
|
|
69
97
|
#: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
70
98
|
def build_request_params(messages, model, options)
|
|
@@ -82,7 +110,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
82
110
|
end
|
|
83
111
|
|
|
84
112
|
#--
|
|
85
|
-
#: (untyped) -> Riffer::TokenUsage?
|
|
113
|
+
#: (untyped) -> Riffer::Providers::TokenUsage?
|
|
86
114
|
def extract_token_usage(response)
|
|
87
115
|
response[:token_usage]
|
|
88
116
|
end
|
|
@@ -146,7 +174,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
|
|
|
146
174
|
def next_response
|
|
147
175
|
if @stubbed_responses.any?
|
|
148
176
|
@stubbed_responses.shift
|
|
149
|
-
elsif @responses
|
|
177
|
+
elsif @current_index < @responses.size
|
|
150
178
|
response = @responses[@current_index]
|
|
151
179
|
@current_index += 1
|
|
152
180
|
response
|
|
@@ -36,9 +36,9 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
36
36
|
summary: "auto"
|
|
37
37
|
},
|
|
38
38
|
**options.except(:reasoning, :tools, :structured_output, :web_search)
|
|
39
|
-
}
|
|
39
|
+
} #: Hash[Symbol, untyped]
|
|
40
40
|
|
|
41
|
-
openai_tools = []
|
|
41
|
+
openai_tools = [] #: Array[Hash[Symbol, untyped]]
|
|
42
42
|
openai_tools.concat(tools.map { |t| convert_tool_to_openai_format(t) }) if tools && !tools.empty?
|
|
43
43
|
|
|
44
44
|
if web_search
|
|
@@ -72,12 +72,12 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
#--
|
|
75
|
-
#: (OpenAI::Models::Responses::Response) -> Riffer::TokenUsage?
|
|
75
|
+
#: (OpenAI::Models::Responses::Response) -> Riffer::Providers::TokenUsage?
|
|
76
76
|
def extract_token_usage(response)
|
|
77
77
|
usage = response.usage
|
|
78
78
|
return nil unless usage
|
|
79
79
|
|
|
80
|
-
Riffer::TokenUsage.new(
|
|
80
|
+
Riffer::Providers::TokenUsage.new(
|
|
81
81
|
input_tokens: usage.input_tokens,
|
|
82
82
|
output_tokens: usage.output_tokens
|
|
83
83
|
)
|
|
@@ -89,10 +89,10 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
89
89
|
text_content = ""
|
|
90
90
|
|
|
91
91
|
response.output.each do |item|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
next unless item.is_a?(::OpenAI::Models::Responses::ResponseOutputMessage)
|
|
93
|
+
|
|
94
|
+
text_block = item.content.find { |c| c.is_a?(::OpenAI::Models::Responses::ResponseOutputText) }
|
|
95
|
+
text_content = text_block.text if text_block.is_a?(::OpenAI::Models::Responses::ResponseOutputText)
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
text_content
|
|
@@ -101,16 +101,16 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
101
101
|
#--
|
|
102
102
|
#: (OpenAI::Models::Responses::Response) -> Array[Riffer::Messages::Assistant::ToolCall]
|
|
103
103
|
def extract_tool_calls(response)
|
|
104
|
-
tool_calls = []
|
|
104
|
+
tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
|
|
105
105
|
|
|
106
106
|
response.output.each do |item|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
next unless item.is_a?(::OpenAI::Models::Responses::ResponseFunctionToolCall)
|
|
108
|
+
|
|
109
|
+
tool_calls << Riffer::Messages::Assistant::ToolCall.new(
|
|
110
|
+
call_id: item.call_id,
|
|
111
|
+
name: decode_tool_name(item.name, tools: @current_tools),
|
|
112
|
+
arguments: item.arguments
|
|
113
|
+
)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
tool_calls
|
|
@@ -121,7 +121,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
121
121
|
def execute_stream(params, yielder)
|
|
122
122
|
current_state = {
|
|
123
123
|
tool_info: {}
|
|
124
|
-
}
|
|
124
|
+
} #: Hash[Symbol, untyped]
|
|
125
125
|
|
|
126
126
|
stream = @client.responses.stream(params)
|
|
127
127
|
begin
|
|
@@ -224,7 +224,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
224
224
|
return unless usage
|
|
225
225
|
|
|
226
226
|
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
227
|
-
token_usage: Riffer::TokenUsage.new(
|
|
227
|
+
token_usage: Riffer::Providers::TokenUsage.new(
|
|
228
228
|
input_tokens: usage.input_tokens,
|
|
229
229
|
output_tokens: usage.output_tokens
|
|
230
230
|
)
|
|
@@ -242,11 +242,11 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
242
242
|
def handle_output_item_done_web_search(event, yielder:)
|
|
243
243
|
action = event.item.action
|
|
244
244
|
case action
|
|
245
|
-
when OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::OpenPage
|
|
245
|
+
when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::OpenPage
|
|
246
246
|
# OpenPage carries a url but no query or sources, so it doesn't fit
|
|
247
247
|
# WebSearchDone — emit as a status notification instead.
|
|
248
248
|
yielder << Riffer::StreamEvents::WebSearchStatus.new("open_page", url: action.url)
|
|
249
|
-
when OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::Search
|
|
249
|
+
when ::OpenAI::Models::Responses::ResponseFunctionWebSearch::Action::Search
|
|
250
250
|
sources = (action.sources || []).map { |s| {title: nil, url: s.url} }
|
|
251
251
|
yielder << Riffer::StreamEvents::WebSearchDone.new(action.query, sources: sources)
|
|
252
252
|
end
|
|
@@ -275,6 +275,8 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
275
275
|
call_id: message.tool_call_id,
|
|
276
276
|
output: message.content
|
|
277
277
|
}
|
|
278
|
+
else
|
|
279
|
+
raise Riffer::ArgumentError, "unsupported message type: #{message.class}"
|
|
278
280
|
end
|
|
279
281
|
end
|
|
280
282
|
end
|
|
@@ -285,7 +287,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
285
287
|
if message.tool_calls.empty?
|
|
286
288
|
{role: "assistant", content: message.content}
|
|
287
289
|
else
|
|
288
|
-
items = []
|
|
290
|
+
items = [] #: Array[Hash[Symbol, untyped]]
|
|
289
291
|
items << {type: "message", role: "assistant", content: message.content} if message.content && !message.content.empty?
|
|
290
292
|
message.tool_calls.each do |tc|
|
|
291
293
|
items << {
|
|
@@ -300,7 +302,7 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
|
|
|
300
302
|
end
|
|
301
303
|
|
|
302
304
|
#--
|
|
303
|
-
#: (Riffer::FilePart) -> Hash[Symbol, untyped]
|
|
305
|
+
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
304
306
|
def convert_file_part_to_openai_format(file)
|
|
305
307
|
if file.image?
|
|
306
308
|
image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
# OpenRouter provider for the OpenRouter unified gateway (https://openrouter.ai).
|
|
7
|
+
#
|
|
8
|
+
# Requires the +openai+ gem to be installed. OpenRouter exposes an
|
|
9
|
+
# OpenAI-compatible Chat Completions endpoint, so this provider reuses
|
|
10
|
+
# the OpenAI Ruby SDK with a +base_url+ override.
|
|
11
|
+
#
|
|
12
|
+
# The +api_key+ falls back to <tt>Riffer.config.openrouter.api_key</tt>
|
|
13
|
+
# and then to +OPENROUTER_API_KEY+.
|
|
14
|
+
class Riffer::Providers::OpenRouter < Riffer::Providers::Base
|
|
15
|
+
BASE_URL = "https://openrouter.ai/api/v1" #: String
|
|
16
|
+
|
|
17
|
+
# Initializes the OpenRouter provider.
|
|
18
|
+
#
|
|
19
|
+
#--
|
|
20
|
+
#: (?api_key: String?, **untyped) -> void
|
|
21
|
+
def initialize(api_key: nil, **options)
|
|
22
|
+
depends_on "openai"
|
|
23
|
+
|
|
24
|
+
api_key ||= Riffer.config.openrouter.api_key || ENV["OPENROUTER_API_KEY"]
|
|
25
|
+
|
|
26
|
+
@client = ::OpenAI::Client.new(api_key: api_key, base_url: BASE_URL, **options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
#--
|
|
32
|
+
#: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
33
|
+
def build_request_params(messages, model, options)
|
|
34
|
+
reasoning = options[:reasoning]
|
|
35
|
+
tools = options[:tools]
|
|
36
|
+
structured_output = options[:structured_output]
|
|
37
|
+
|
|
38
|
+
params = {
|
|
39
|
+
model: model,
|
|
40
|
+
messages: convert_messages_to_chat_completions_format(messages),
|
|
41
|
+
**options.except(:reasoning, :tools, :structured_output)
|
|
42
|
+
} #: Hash[Symbol, untyped]
|
|
43
|
+
|
|
44
|
+
if reasoning
|
|
45
|
+
params[:reasoning] = reasoning.is_a?(String) ? {effort: reasoning} : reasoning
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if tools && !tools.empty?
|
|
49
|
+
params[:tools] = tools.map { |t| convert_tool_to_chat_completions_format(t) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if structured_output
|
|
53
|
+
params[:response_format] = {
|
|
54
|
+
type: "json_schema",
|
|
55
|
+
json_schema: {
|
|
56
|
+
name: "response",
|
|
57
|
+
schema: structured_output.json_schema(strict: true),
|
|
58
|
+
strict: true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
params.compact
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#--
|
|
67
|
+
#: (Hash[Symbol, untyped]) -> OpenAI::Models::Chat::ChatCompletion
|
|
68
|
+
def execute_generate(params)
|
|
69
|
+
@client.chat.completions.create(**params)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#--
|
|
73
|
+
#: (OpenAI::Models::Chat::ChatCompletion) -> Riffer::Providers::TokenUsage?
|
|
74
|
+
def extract_token_usage(response)
|
|
75
|
+
usage = response.usage
|
|
76
|
+
return nil unless usage
|
|
77
|
+
|
|
78
|
+
Riffer::Providers::TokenUsage.new(
|
|
79
|
+
input_tokens: usage.prompt_tokens,
|
|
80
|
+
output_tokens: usage.completion_tokens
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#--
|
|
85
|
+
#: (OpenAI::Models::Chat::ChatCompletion) -> String
|
|
86
|
+
def extract_content(response)
|
|
87
|
+
response.choices.first&.message&.content || ""
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
#--
|
|
91
|
+
#: (OpenAI::Models::Chat::ChatCompletion) -> Array[Riffer::Messages::Assistant::ToolCall]
|
|
92
|
+
def extract_tool_calls(response)
|
|
93
|
+
message = response.choices.first&.message
|
|
94
|
+
return [] unless message
|
|
95
|
+
|
|
96
|
+
tool_calls = message.tool_calls
|
|
97
|
+
return [] if tool_calls.nil? || tool_calls.empty?
|
|
98
|
+
|
|
99
|
+
tool_calls.filter_map do |tc|
|
|
100
|
+
next unless tc.is_a?(::OpenAI::Models::Chat::ChatCompletionMessageFunctionToolCall)
|
|
101
|
+
|
|
102
|
+
Riffer::Messages::Assistant::ToolCall.new(
|
|
103
|
+
call_id: tc.id,
|
|
104
|
+
name: decode_tool_name(tc.function.name, tools: @current_tools),
|
|
105
|
+
arguments: tc.function.arguments
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
#--
|
|
111
|
+
#: (Hash[Symbol, untyped], Enumerator::Yielder) -> void
|
|
112
|
+
def execute_stream(params, yielder)
|
|
113
|
+
# OpenRouter omits usage from streams unless explicitly opted in.
|
|
114
|
+
stream_options = (params[:stream_options] || {}).merge(include_usage: true)
|
|
115
|
+
stream_params = params.merge(stream_options: stream_options)
|
|
116
|
+
|
|
117
|
+
state = {
|
|
118
|
+
text: +"",
|
|
119
|
+
reasoning: +"",
|
|
120
|
+
tool_calls: {}
|
|
121
|
+
} #: Hash[Symbol, untyped]
|
|
122
|
+
|
|
123
|
+
# Use stream_raw (not stream) — the latter yields a higher-level
|
|
124
|
+
# ChatChunkEvent helper that aggregates content/tool calls into typed
|
|
125
|
+
# events. We want raw ChatCompletionChunk objects with
|
|
126
|
+
# +choices.first.delta+ so we can map deltas to Riffer::StreamEvents
|
|
127
|
+
# ourselves.
|
|
128
|
+
stream = @client.chat.completions.stream_raw(**stream_params)
|
|
129
|
+
begin
|
|
130
|
+
stream.each do |chunk|
|
|
131
|
+
handle_stream_chunk(chunk, state: state, yielder: yielder)
|
|
132
|
+
end
|
|
133
|
+
ensure
|
|
134
|
+
# The OpenAI SDK does not auto-close the SSE socket on iteration
|
|
135
|
+
# interrupt, so close explicitly. Idempotent and a no-op after EOF.
|
|
136
|
+
stream.close
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Chat Completions has no per-tool terminal event, so flush any leftover
|
|
140
|
+
# tool calls here in case finish_reason is missing or not "tool_calls".
|
|
141
|
+
emit_tool_call_done_events(state: state, yielder: yielder) unless state[:tool_calls].empty?
|
|
142
|
+
|
|
143
|
+
yielder << Riffer::StreamEvents::TextDone.new(state[:text]) unless state[:text].empty?
|
|
144
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(state[:reasoning]) unless state[:reasoning].empty?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
#--
|
|
148
|
+
#: (OpenAI::Models::Chat::ChatCompletionChunk, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
149
|
+
def handle_stream_chunk(chunk, state:, yielder:)
|
|
150
|
+
choice = chunk.choices&.first
|
|
151
|
+
delta = choice&.delta
|
|
152
|
+
|
|
153
|
+
if delta
|
|
154
|
+
handle_text_delta(delta, state: state, yielder: yielder)
|
|
155
|
+
handle_reasoning_delta(delta, state: state, yielder: yielder)
|
|
156
|
+
handle_tool_call_deltas(delta, state: state, yielder: yielder)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
if choice && finish_reason_is_tool_calls?(choice)
|
|
160
|
+
emit_tool_call_done_events(state: state, yielder: yielder)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
return unless chunk.usage
|
|
164
|
+
|
|
165
|
+
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
166
|
+
token_usage: Riffer::Providers::TokenUsage.new(
|
|
167
|
+
input_tokens: chunk.usage.prompt_tokens,
|
|
168
|
+
output_tokens: chunk.usage.completion_tokens
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
#--
|
|
174
|
+
#: (OpenAI::Models::Chat::ChatCompletionChunk::Choice::Delta, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
175
|
+
def handle_text_delta(delta, state:, yielder:)
|
|
176
|
+
content = delta.content
|
|
177
|
+
return if content.nil? || content.empty?
|
|
178
|
+
|
|
179
|
+
state[:text] << content
|
|
180
|
+
yielder << Riffer::StreamEvents::TextDelta.new(content)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
#--
|
|
184
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
185
|
+
def handle_reasoning_delta(delta, state:, yielder:)
|
|
186
|
+
# The openai gem's typed Delta model strips fields not in OpenAI's spec
|
|
187
|
+
# (so +delta.reasoning+ raises NoMethodError), but the underlying data
|
|
188
|
+
# hash retains them. Access via +#[]+ which reads from BaseModel#@data.
|
|
189
|
+
reasoning = delta[:reasoning] if delta.respond_to?(:[])
|
|
190
|
+
return if reasoning.nil? || reasoning.empty?
|
|
191
|
+
|
|
192
|
+
state[:reasoning] << reasoning
|
|
193
|
+
yielder << Riffer::StreamEvents::ReasoningDelta.new(reasoning)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
#--
|
|
197
|
+
#: (OpenAI::Models::Chat::ChatCompletionChunk::Choice::Delta, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
198
|
+
def handle_tool_call_deltas(delta, state:, yielder:)
|
|
199
|
+
tool_calls = delta.tool_calls
|
|
200
|
+
return if tool_calls.nil? || tool_calls.empty?
|
|
201
|
+
|
|
202
|
+
tool_calls.each do |tc|
|
|
203
|
+
entry = state[:tool_calls][tc.index] ||= {id: nil, name: nil, arguments: +""}
|
|
204
|
+
entry[:id] = tc.id if tc.id
|
|
205
|
+
|
|
206
|
+
fn = tc.function
|
|
207
|
+
next unless fn
|
|
208
|
+
|
|
209
|
+
entry[:name] = decode_tool_name(fn.name, tools: @current_tools) if fn.name
|
|
210
|
+
|
|
211
|
+
args_delta = fn.arguments
|
|
212
|
+
next if args_delta.nil? || args_delta.empty?
|
|
213
|
+
|
|
214
|
+
entry[:arguments] << args_delta
|
|
215
|
+
yielder << Riffer::StreamEvents::ToolCallDelta.new(
|
|
216
|
+
item_id: entry[:id] || "tool_#{tc.index}",
|
|
217
|
+
name: entry[:name],
|
|
218
|
+
arguments_delta: args_delta
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
#--
|
|
224
|
+
#: (state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
225
|
+
def emit_tool_call_done_events(state:, yielder:)
|
|
226
|
+
state[:tool_calls].each do |index, entry|
|
|
227
|
+
fallback = "tool_#{index}"
|
|
228
|
+
yielder << Riffer::StreamEvents::ToolCallDone.new(
|
|
229
|
+
item_id: entry[:id] || fallback,
|
|
230
|
+
call_id: entry[:id] || fallback,
|
|
231
|
+
name: entry[:name],
|
|
232
|
+
arguments: entry[:arguments]
|
|
233
|
+
)
|
|
234
|
+
end
|
|
235
|
+
state[:tool_calls] = {}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
#--
|
|
239
|
+
#: (OpenAI::Models::Chat::ChatCompletionChunk::Choice) -> bool
|
|
240
|
+
def finish_reason_is_tool_calls?(choice)
|
|
241
|
+
choice.finish_reason.to_s == "tool_calls"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
#--
|
|
245
|
+
#: (Array[Riffer::Messages::Base]) -> Array[Hash[Symbol, untyped]]
|
|
246
|
+
def convert_messages_to_chat_completions_format(messages)
|
|
247
|
+
messages.flat_map do |message|
|
|
248
|
+
case message
|
|
249
|
+
when Riffer::Messages::System
|
|
250
|
+
{role: "system", content: message.content}
|
|
251
|
+
when Riffer::Messages::User
|
|
252
|
+
if message.files.empty?
|
|
253
|
+
{role: "user", content: message.content}
|
|
254
|
+
else
|
|
255
|
+
content = [{type: "text", text: message.content}]
|
|
256
|
+
message.files.each { |file| content << convert_file_part_to_chat_completions_format(file) }
|
|
257
|
+
{role: "user", content: content}
|
|
258
|
+
end
|
|
259
|
+
when Riffer::Messages::Assistant
|
|
260
|
+
convert_assistant_to_chat_completions_format(message)
|
|
261
|
+
when Riffer::Messages::Tool
|
|
262
|
+
{role: "tool", tool_call_id: message.tool_call_id, content: message.content}
|
|
263
|
+
else
|
|
264
|
+
raise Riffer::ArgumentError, "unsupported message type: #{message.class}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
#--
|
|
270
|
+
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
271
|
+
def convert_assistant_to_chat_completions_format(message)
|
|
272
|
+
msg = {role: "assistant"} #: Hash[Symbol, untyped]
|
|
273
|
+
msg[:content] = message.content if message.content && !message.content.empty?
|
|
274
|
+
|
|
275
|
+
unless message.tool_calls.empty?
|
|
276
|
+
msg[:tool_calls] = message.tool_calls.map do |tc|
|
|
277
|
+
{
|
|
278
|
+
id: tc.call_id,
|
|
279
|
+
type: "function",
|
|
280
|
+
function: {
|
|
281
|
+
name: encode_tool_name(tc.name),
|
|
282
|
+
arguments: tc.arguments.is_a?(String) ? tc.arguments : tc.arguments.to_json
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
msg
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
#--
|
|
292
|
+
#: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
|
|
293
|
+
def convert_file_part_to_chat_completions_format(file)
|
|
294
|
+
if file.image?
|
|
295
|
+
image_url = file.url? ? file.url : "data:#{file.media_type};base64,#{file.data}"
|
|
296
|
+
{type: "image_url", image_url: {url: image_url}}
|
|
297
|
+
else
|
|
298
|
+
data_uri = "data:#{file.media_type};base64,#{file.data}"
|
|
299
|
+
block = {type: "file", file: {file_data: data_uri}} #: Hash[Symbol, untyped]
|
|
300
|
+
block[:file][:filename] = file.filename if file.filename
|
|
301
|
+
block
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
#--
|
|
306
|
+
#: (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
|
|
307
|
+
def convert_tool_to_chat_completions_format(tool)
|
|
308
|
+
{
|
|
309
|
+
type: "function",
|
|
310
|
+
function: {
|
|
311
|
+
name: encode_tool_name(tool.name),
|
|
312
|
+
description: tool.description,
|
|
313
|
+
parameters: tool.parameters_schema(strict: true),
|
|
314
|
+
strict: true
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
end
|
|
318
|
+
end
|
|
@@ -10,6 +10,7 @@ class Riffer::Providers::Repository
|
|
|
10
10
|
azure_openai: -> { Riffer::Providers::AzureOpenAI },
|
|
11
11
|
gemini: -> { Riffer::Providers::Gemini },
|
|
12
12
|
openai: -> { Riffer::Providers::OpenAI },
|
|
13
|
+
openrouter: -> { Riffer::Providers::OpenRouter },
|
|
13
14
|
mock: -> { Riffer::Providers::Mock }
|
|
14
15
|
}.freeze #: Hash[Symbol, ^() -> singleton(Riffer::Providers::Base)]
|
|
15
16
|
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Tracks input tokens, output tokens, and optional cache statistics.
|
|
7
7
|
#
|
|
8
|
-
# token_usage = Riffer::TokenUsage.new(input_tokens: 100, output_tokens: 50)
|
|
8
|
+
# token_usage = Riffer::Providers::TokenUsage.new(input_tokens: 100, output_tokens: 50)
|
|
9
9
|
# token_usage.total_tokens # => 150
|
|
10
10
|
#
|
|
11
11
|
# combined = token_usage1 + token_usage2 # Combine multiple token usage objects
|
|
12
12
|
#
|
|
13
|
-
class Riffer::TokenUsage
|
|
13
|
+
class Riffer::Providers::TokenUsage
|
|
14
14
|
# Number of tokens in the input/prompt.
|
|
15
15
|
attr_reader :input_tokens #: Integer
|
|
16
16
|
|
|
@@ -43,9 +43,9 @@ class Riffer::TokenUsage
|
|
|
43
43
|
# Combines two TokenUsage objects for cumulative tracking.
|
|
44
44
|
#
|
|
45
45
|
#--
|
|
46
|
-
#: (Riffer::TokenUsage) -> Riffer::TokenUsage
|
|
46
|
+
#: (Riffer::Providers::TokenUsage) -> Riffer::Providers::TokenUsage
|
|
47
47
|
def +(other)
|
|
48
|
-
Riffer::TokenUsage.new(
|
|
48
|
+
Riffer::Providers::TokenUsage.new(
|
|
49
49
|
input_tokens: input_tokens + other.input_tokens,
|
|
50
50
|
output_tokens: output_tokens + other.output_tokens,
|
|
51
51
|
cache_creation_tokens: add_nullable(cache_creation_tokens, other.cache_creation_tokens),
|
data/lib/riffer/providers.rb
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
# - Riffer::Providers::OpenAI - OpenAI GPT models
|
|
8
8
|
# - Riffer::Providers::AzureOpenAI - Azure OpenAI GPT models
|
|
9
9
|
# - Riffer::Providers::AmazonBedrock - AWS Bedrock models
|
|
10
|
+
# - Riffer::Providers::OpenRouter - OpenRouter unified gateway
|
|
10
11
|
# - Riffer::Providers::Mock - Mock provider for testing
|
|
11
12
|
module Riffer::Providers
|
|
12
13
|
end
|
data/lib/riffer/runner/fibers.rb
CHANGED
|
@@ -28,7 +28,7 @@ class Riffer::Runner::Fibers < Riffer::Runner
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
#--
|
|
31
|
-
#: (Array[untyped], context:
|
|
31
|
+
#: (Array[untyped], context: Riffer::Agent::Context?) { (untyped) -> untyped } -> Array[untyped]
|
|
32
32
|
def map(items, context:, &block)
|
|
33
33
|
return [] if items.empty?
|
|
34
34
|
|
|
@@ -37,8 +37,9 @@ class Riffer::Runner::Fibers < Riffer::Runner
|
|
|
37
37
|
|
|
38
38
|
Async do
|
|
39
39
|
barrier = Async::Barrier.new
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
max = @max_concurrency
|
|
41
|
+
parent = if max
|
|
42
|
+
Async::Semaphore.new(max, parent: barrier)
|
|
42
43
|
else
|
|
43
44
|
barrier
|
|
44
45
|
end
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
#
|
|
8
8
|
class Riffer::Runner::Sequential < Riffer::Runner
|
|
9
9
|
#--
|
|
10
|
-
#: (Array[untyped], context:
|
|
10
|
+
#: (Array[untyped], context: Riffer::Agent::Context?) { (untyped) -> untyped } -> Array[untyped]
|
|
11
11
|
def map(items, context:, &block)
|
|
12
12
|
items.map(&block)
|
|
13
13
|
end
|
|
@@ -25,7 +25,7 @@ class Riffer::Runner::Threaded < Riffer::Runner
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
#--
|
|
28
|
-
#: (Array[untyped], context:
|
|
28
|
+
#: (Array[untyped], context: Riffer::Agent::Context?) { (untyped) -> untyped } -> Array[untyped]
|
|
29
29
|
def map(items, context:, &block)
|
|
30
30
|
return [] if items.empty?
|
|
31
31
|
|
data/lib/riffer/runner.rb
CHANGED
|
@@ -19,7 +19,7 @@ class Riffer::Runner
|
|
|
19
19
|
# Raises NotImplementedError if not implemented by subclass.
|
|
20
20
|
#
|
|
21
21
|
#--
|
|
22
|
-
#: (Array[untyped], context:
|
|
22
|
+
#: (Array[untyped], context: Riffer::Agent::Context?) { (untyped) -> untyped } -> Array[untyped]
|
|
23
23
|
def map(items, context:, &block)
|
|
24
24
|
raise NotImplementedError, "#{self.class} must implement #map"
|
|
25
25
|
end
|
|
@@ -19,13 +19,14 @@ class Riffer::Skills::ActivateTool < Riffer::Tool
|
|
|
19
19
|
|
|
20
20
|
# Activates a skill by name and returns its body.
|
|
21
21
|
#
|
|
22
|
-
# [context]
|
|
22
|
+
# [context] the agent's +Riffer::Agent::Context+, exposing +#skills+
|
|
23
|
+
# (a +Riffer::Skills::Context+).
|
|
23
24
|
# [name] the skill name to activate.
|
|
24
25
|
#
|
|
25
26
|
#--
|
|
26
|
-
#: (context:
|
|
27
|
+
#: (context: Riffer::Agent::Context?, name: String) -> Riffer::Tools::Response
|
|
27
28
|
def call(context:, name:)
|
|
28
|
-
skills_context = context&.
|
|
29
|
+
skills_context = context&.skills
|
|
29
30
|
return error("Skills not configured") unless skills_context
|
|
30
31
|
|
|
31
32
|
text(skills_context.activate(name))
|
data/lib/riffer/skills/config.rb
CHANGED
|
@@ -66,7 +66,7 @@ class Riffer::Skills::Config
|
|
|
66
66
|
# Returns the configured override when set, or +nil+ when unset. The
|
|
67
67
|
# global fallback to <tt>Riffer.config.skills.default_activate_tool</tt>
|
|
68
68
|
# is applied by the agent at resolution time (see
|
|
69
|
-
# Riffer::Agent
|
|
69
|
+
# Riffer::Agent#resolve_tools), not by this getter.
|
|
70
70
|
#
|
|
71
71
|
# The override must be a subclass of Riffer::Tool.
|
|
72
72
|
#
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
# Coordinates skill discovery, activation, and prompt rendering.
|
|
7
7
|
# Tracks activations with caching to avoid redundant backend reads.
|
|
8
8
|
#
|
|
9
|
-
# Built by the agent
|
|
10
|
-
#
|
|
9
|
+
# Built by the agent during +Agent.new+ and exposed to tools via
|
|
10
|
+
# <tt>context.skills</tt> on the agent's +Riffer::Agent::Context+.
|
|
11
11
|
#
|
|
12
12
|
# See Riffer::Skills::Backend, Riffer::Skills::Frontmatter.
|
|
13
13
|
class Riffer::Skills::Context
|
|
@@ -66,7 +66,7 @@ class Riffer::Skills::Context
|
|
|
66
66
|
#: () -> String
|
|
67
67
|
def system_prompt
|
|
68
68
|
available = available_skills
|
|
69
|
-
parts = []
|
|
69
|
+
parts = [] #: Array[String]
|
|
70
70
|
parts << @adapter.render_catalog(available) unless available.empty?
|
|
71
71
|
@activated.each_value { |body| parts << body }
|
|
72
72
|
parts.join("\n\n")
|