riffer 0.47.2 → 0.49.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/.claude/rules/comments.md +2 -4
- data/.claude/rules/rbs-inline.md +2 -12
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +42 -0
- data/docs/AGENTS.md +39 -7
- data/docs/AGENT_LIFECYCLE.md +14 -16
- data/docs/CONFIGURATION.md +23 -34
- data/docs/EVALS.md +2 -1
- data/docs/MCP.md +0 -4
- data/docs/MESSAGES.md +85 -17
- data/docs/STREAM_EVENTS.md +8 -4
- data/docs/TOOL_ADVANCED.md +1 -3
- data/docs/TRACING.md +2 -2
- data/docs/providers/AMAZON_BEDROCK.md +32 -1
- data/docs/providers/CUSTOM_PROVIDERS.md +56 -4
- data/docs/providers/GEMINI.md +1 -1
- data/docs/providers/MOCK_PROVIDER.md +17 -0
- data/docs/providers/OPENROUTER.md +18 -1
- data/docs-site/build.rb +0 -6
- data/docs-site/check.rb +1 -5
- data/lib/riffer/agent/config.rb +27 -44
- data/lib/riffer/agent/context.rb +2 -26
- data/lib/riffer/agent/outcome.rb +0 -20
- data/lib/riffer/agent/response.rb +4 -35
- data/lib/riffer/agent/run.rb +32 -48
- data/lib/riffer/agent/serializer.rb +10 -39
- data/lib/riffer/agent/session/repair.rb +3 -15
- data/lib/riffer/agent/session.rb +26 -38
- data/lib/riffer/agent/structured_output/result.rb +0 -8
- data/lib/riffer/agent/structured_output.rb +0 -7
- data/lib/riffer/agent.rb +31 -127
- data/lib/riffer/config/amazon_bedrock.rb +30 -0
- data/lib/riffer/config/anthropic.rb +21 -0
- data/lib/riffer/config/azure_open_ai.rb +30 -0
- data/lib/riffer/config/evals.rb +18 -0
- data/lib/riffer/config/files.rb +61 -0
- data/lib/riffer/config/gemini.rb +21 -0
- data/lib/riffer/config/mcp.rb +31 -0
- data/lib/riffer/config/open_ai.rb +30 -0
- data/lib/riffer/config/open_router.rb +21 -0
- data/lib/riffer/config/pricing/rates.rb +36 -0
- data/lib/riffer/config/pricing.rb +64 -0
- data/lib/riffer/config/skills.rb +38 -0
- data/lib/riffer/config/tracing.rb +43 -0
- data/lib/riffer/config.rb +3 -362
- data/lib/riffer/evals/evaluator.rb +22 -23
- data/lib/riffer/evals/evaluator_runner.rb +0 -15
- data/lib/riffer/evals/judge.rb +9 -11
- data/lib/riffer/evals/result.rb +1 -11
- data/lib/riffer/evals/run_result.rb +0 -12
- data/lib/riffer/evals/scenario_result.rb +0 -19
- data/lib/riffer/files/downloader.rb +2 -3
- data/lib/riffer/files/resolver.rb +5 -10
- data/lib/riffer/guardrail.rb +2 -22
- data/lib/riffer/guardrails/modification.rb +0 -8
- data/lib/riffer/guardrails/result.rb +0 -17
- data/lib/riffer/guardrails/runner.rb +2 -11
- data/lib/riffer/guardrails/tripwire.rb +0 -8
- data/lib/riffer/guardrails.rb +0 -2
- data/lib/riffer/helpers/boolean.rb +0 -4
- data/lib/riffer/helpers/call_or_value.rb +0 -3
- data/lib/riffer/helpers/deep_dup.rb +37 -0
- data/lib/riffer/helpers/dependencies.rb +0 -4
- data/lib/riffer/helpers/identifier.rb +3 -12
- data/lib/riffer/helpers/validate.rb +42 -0
- data/lib/riffer/mcp/authenticated_tool.rb +4 -12
- data/lib/riffer/mcp/client.rb +0 -7
- data/lib/riffer/mcp/manifest.rb +3 -7
- data/lib/riffer/mcp/registration.rb +0 -10
- data/lib/riffer/mcp/registry.rb +0 -9
- data/lib/riffer/mcp/search_tool.rb +0 -4
- data/lib/riffer/mcp/tool.rb +1 -4
- data/lib/riffer/mcp/tool_factory.rb +3 -6
- data/lib/riffer/mcp.rb +2 -23
- data/lib/riffer/messages/assistant/reasoning_part.rb +73 -0
- data/lib/riffer/messages/assistant/tool_call.rb +55 -0
- data/lib/riffer/messages/assistant.rb +43 -15
- data/lib/riffer/messages/base.rb +5 -33
- data/lib/riffer/messages/system.rb +8 -1
- data/lib/riffer/messages/tool.rb +15 -13
- data/lib/riffer/messages/{file_part.rb → user/file_part.rb} +16 -40
- data/lib/riffer/messages/user.rb +11 -4
- data/lib/riffer/params/boolean.rb +1 -5
- data/lib/riffer/params/param.rb +15 -31
- data/lib/riffer/params.rb +15 -39
- data/lib/riffer/providers/amazon_bedrock.rb +104 -44
- data/lib/riffer/providers/anthropic.rb +11 -26
- data/lib/riffer/providers/azure_open_ai.rb +3 -8
- data/lib/riffer/providers/base.rb +30 -32
- data/lib/riffer/providers/finish_reason.rb +0 -6
- data/lib/riffer/providers/gemini/client.rb +4 -17
- data/lib/riffer/providers/gemini.rb +6 -12
- data/lib/riffer/providers/mock.rb +18 -27
- data/lib/riffer/providers/open_ai.rb +12 -21
- data/lib/riffer/providers/open_router.rb +109 -33
- data/lib/riffer/providers/repository.rb +2 -14
- data/lib/riffer/providers/token_usage.rb +19 -12
- data/lib/riffer/registrable.rb +11 -45
- data/lib/riffer/runner/fibers.rb +1 -6
- data/lib/riffer/runner/sequential.rb +0 -1
- data/lib/riffer/runner/threaded.rb +0 -3
- data/lib/riffer/runner.rb +0 -3
- data/lib/riffer/skills/activate_tool.rb +0 -3
- data/lib/riffer/skills/adapter.rb +0 -8
- data/lib/riffer/skills/backend.rb +2 -7
- data/lib/riffer/skills/config.rb +13 -17
- data/lib/riffer/skills/context.rb +0 -29
- data/lib/riffer/skills/filesystem_backend.rb +0 -7
- data/lib/riffer/skills/frontmatter.rb +2 -16
- data/lib/riffer/skills/markdown_adapter.rb +3 -6
- data/lib/riffer/skills/xml_adapter.rb +0 -3
- data/lib/riffer/stream_events/base.rb +0 -3
- data/lib/riffer/stream_events/finish_reason_done.rb +1 -6
- data/lib/riffer/stream_events/guardrail_modification.rb +0 -10
- data/lib/riffer/stream_events/guardrail_tripwire.rb +0 -10
- data/lib/riffer/stream_events/interrupt.rb +2 -12
- data/lib/riffer/stream_events/reasoning_delta.rb +0 -3
- data/lib/riffer/stream_events/reasoning_done.rb +6 -8
- data/lib/riffer/stream_events/skill_activation.rb +0 -3
- data/lib/riffer/stream_events/text_delta.rb +0 -2
- data/lib/riffer/stream_events/text_done.rb +0 -2
- data/lib/riffer/stream_events/token_usage_done.rb +0 -2
- data/lib/riffer/stream_events/tool_call_delta.rb +1 -5
- data/lib/riffer/stream_events/tool_call_done.rb +0 -5
- data/lib/riffer/stream_events/web_search_done.rb +0 -3
- data/lib/riffer/stream_events/web_search_status.rb +1 -5
- data/lib/riffer/testing/minitest.rb +4 -5
- data/lib/riffer/testing.rb +5 -38
- data/lib/riffer/tool.rb +2 -28
- data/lib/riffer/tools/response.rb +3 -32
- data/lib/riffer/tools/runtime/fibers.rb +0 -6
- data/lib/riffer/tools/runtime/inline.rb +0 -1
- data/lib/riffer/tools/runtime/threaded.rb +0 -6
- data/lib/riffer/tools/runtime.rb +8 -30
- data/lib/riffer/tools/toolable.rb +0 -37
- data/lib/riffer/tracing/capture.rb +5 -9
- data/lib/riffer/tracing/no_op.rb +0 -7
- data/lib/riffer/tracing/otel.rb +7 -16
- data/lib/riffer/tracing/stream_recorder.rb +0 -7
- data/lib/riffer/tracing.rb +4 -27
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -30
- data/sig/generated/riffer/agent/config.rbs +24 -46
- data/sig/generated/riffer/agent/context.rbs +2 -26
- data/sig/generated/riffer/agent/outcome.rbs +0 -20
- data/sig/generated/riffer/agent/response.rbs +4 -27
- data/sig/generated/riffer/agent/run.rbs +12 -31
- data/sig/generated/riffer/agent/serializer.rbs +2 -27
- data/sig/generated/riffer/agent/session/repair.rbs +2 -10
- data/sig/generated/riffer/agent/session.rbs +10 -36
- data/sig/generated/riffer/agent/structured_output/result.rbs +0 -7
- data/sig/generated/riffer/agent/structured_output.rbs +0 -7
- data/sig/generated/riffer/agent.rbs +25 -125
- data/sig/generated/riffer/config/amazon_bedrock.rbs +21 -0
- data/sig/generated/riffer/config/anthropic.rbs +15 -0
- data/sig/generated/riffer/config/azure_open_ai.rbs +21 -0
- data/sig/generated/riffer/config/evals.rbs +13 -0
- data/sig/generated/riffer/config/files.rbs +43 -0
- data/sig/generated/riffer/config/gemini.rbs +15 -0
- data/sig/generated/riffer/config/mcp.rbs +19 -0
- data/sig/generated/riffer/config/open_ai.rbs +21 -0
- data/sig/generated/riffer/config/open_router.rbs +15 -0
- data/sig/generated/riffer/config/pricing/rates.rbs +19 -0
- data/sig/generated/riffer/config/pricing.rbs +31 -0
- data/sig/generated/riffer/config/skills.rbs +19 -0
- data/sig/generated/riffer/config/tracing.rbs +25 -0
- data/sig/generated/riffer/config.rbs +2 -307
- data/sig/generated/riffer/evals/evaluator.rbs +12 -21
- data/sig/generated/riffer/evals/evaluator_runner.rbs +0 -14
- data/sig/generated/riffer/evals/judge.rbs +4 -8
- data/sig/generated/riffer/evals/result.rbs +1 -11
- data/sig/generated/riffer/evals/run_result.rbs +0 -12
- data/sig/generated/riffer/evals/scenario_result.rbs +0 -14
- data/sig/generated/riffer/files/resolver.rbs +6 -13
- data/sig/generated/riffer/guardrail.rbs +2 -22
- data/sig/generated/riffer/guardrails/modification.rbs +0 -6
- data/sig/generated/riffer/guardrails/result.rbs +0 -15
- data/sig/generated/riffer/guardrails/runner.rbs +0 -11
- data/sig/generated/riffer/guardrails/tripwire.rbs +0 -8
- data/sig/generated/riffer/guardrails.rbs +0 -2
- data/sig/generated/riffer/helpers/boolean.rbs +0 -4
- data/sig/generated/riffer/helpers/call_or_value.rbs +0 -3
- data/sig/generated/riffer/helpers/deep_dup.rbs +13 -0
- data/sig/generated/riffer/helpers/dependencies.rbs +0 -4
- data/sig/generated/riffer/helpers/identifier.rbs +0 -12
- data/sig/generated/riffer/helpers/validate.rbs +21 -0
- data/sig/generated/riffer/mcp/authenticated_tool.rbs +0 -5
- data/sig/generated/riffer/mcp/client.rbs +0 -7
- data/sig/generated/riffer/mcp/manifest.rbs +3 -7
- data/sig/generated/riffer/mcp/registration.rbs +0 -10
- data/sig/generated/riffer/mcp/registry.rbs +0 -9
- data/sig/generated/riffer/mcp/search_tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool.rbs +0 -4
- data/sig/generated/riffer/mcp/tool_factory.rbs +0 -6
- data/sig/generated/riffer/mcp.rbs +2 -22
- data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +45 -0
- data/sig/generated/riffer/messages/assistant/tool_call.rbs +34 -0
- data/sig/generated/riffer/messages/assistant.rbs +25 -31
- data/sig/generated/riffer/messages/base.rbs +0 -12
- data/sig/generated/riffer/messages/system.rbs +4 -1
- data/sig/generated/riffer/messages/tool.rbs +4 -10
- data/sig/generated/riffer/messages/user/file_part.rbs +76 -0
- data/sig/generated/riffer/messages/user.rbs +7 -5
- data/sig/generated/riffer/params/boolean.rbs +1 -4
- data/sig/generated/riffer/params/param.rbs +7 -31
- data/sig/generated/riffer/params.rbs +8 -38
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +33 -29
- data/sig/generated/riffer/providers/anthropic.rbs +2 -12
- data/sig/generated/riffer/providers/azure_open_ai.rbs +0 -8
- data/sig/generated/riffer/providers/base.rbs +18 -34
- data/sig/generated/riffer/providers/finish_reason.rbs +0 -6
- data/sig/generated/riffer/providers/gemini/client.rbs +4 -17
- data/sig/generated/riffer/providers/gemini.rbs +4 -10
- data/sig/generated/riffer/providers/mock.rbs +6 -27
- data/sig/generated/riffer/providers/open_ai.rbs +6 -13
- data/sig/generated/riffer/providers/open_router.rbs +37 -18
- data/sig/generated/riffer/providers/repository.rbs +2 -14
- data/sig/generated/riffer/providers/token_usage.rbs +9 -12
- data/sig/generated/riffer/registrable.rbs +0 -45
- data/sig/generated/riffer/runner/fibers.rbs +0 -6
- data/sig/generated/riffer/runner/sequential.rbs +0 -1
- data/sig/generated/riffer/runner/threaded.rbs +0 -3
- data/sig/generated/riffer/runner.rbs +0 -3
- data/sig/generated/riffer/skills/activate_tool.rbs +0 -3
- data/sig/generated/riffer/skills/adapter.rbs +0 -8
- data/sig/generated/riffer/skills/backend.rbs +2 -7
- data/sig/generated/riffer/skills/config.rbs +6 -17
- data/sig/generated/riffer/skills/context.rbs +0 -29
- data/sig/generated/riffer/skills/filesystem_backend.rbs +0 -7
- data/sig/generated/riffer/skills/frontmatter.rbs +2 -16
- data/sig/generated/riffer/skills/markdown_adapter.rbs +0 -6
- data/sig/generated/riffer/skills/xml_adapter.rbs +0 -3
- data/sig/generated/riffer/stream_events/base.rbs +0 -3
- data/sig/generated/riffer/stream_events/finish_reason_done.rbs +1 -6
- data/sig/generated/riffer/stream_events/guardrail_modification.rbs +0 -10
- data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +0 -10
- data/sig/generated/riffer/stream_events/interrupt.rbs +2 -10
- data/sig/generated/riffer/stream_events/reasoning_delta.rbs +0 -3
- data/sig/generated/riffer/stream_events/reasoning_done.rbs +4 -6
- data/sig/generated/riffer/stream_events/skill_activation.rbs +0 -3
- data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
- data/sig/generated/riffer/stream_events/text_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/token_usage_done.rbs +0 -2
- data/sig/generated/riffer/stream_events/tool_call_delta.rbs +1 -5
- data/sig/generated/riffer/stream_events/tool_call_done.rbs +0 -5
- data/sig/generated/riffer/stream_events/web_search_done.rbs +0 -3
- data/sig/generated/riffer/stream_events/web_search_status.rbs +1 -5
- data/sig/generated/riffer/testing.rbs +0 -38
- data/sig/generated/riffer/tool.rbs +0 -27
- data/sig/generated/riffer/tools/response.rbs +3 -29
- data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -5
- data/sig/generated/riffer/tools/runtime/inline.rbs +0 -1
- data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -5
- data/sig/generated/riffer/tools/runtime.rbs +4 -26
- data/sig/generated/riffer/tools/toolable.rbs +0 -37
- data/sig/generated/riffer/tracing/capture.rbs +6 -9
- data/sig/generated/riffer/tracing/no_op.rbs +0 -7
- data/sig/generated/riffer/tracing/otel.rbs +7 -16
- data/sig/generated/riffer/tracing/stream_recorder.rbs +0 -2
- data/sig/generated/riffer/tracing.rbs +4 -23
- data/sig/generated/riffer.rbs +2 -29
- data/sig/manual/riffer/helpers/deep_dup.rbs +5 -0
- data/sig/manual/riffer/helpers/validate.rbs +5 -0
- metadata +39 -3
- data/sig/generated/riffer/messages/file_part.rbs +0 -101
data/docs/MESSAGES.md
CHANGED
|
@@ -32,9 +32,9 @@ msg.to_h # => {role: :user, content: "Hello, how are you?"}
|
|
|
32
32
|
User messages can include file attachments:
|
|
33
33
|
|
|
34
34
|
```ruby
|
|
35
|
-
file = Riffer::Messages::FilePart.from_path("photo.jpg")
|
|
35
|
+
file = Riffer::Messages::User::FilePart.from_path("photo.jpg")
|
|
36
36
|
msg = Riffer::Messages::User.new("Describe this image", files: [file])
|
|
37
|
-
msg.files # => [#<Riffer::Messages::FilePart ...>]
|
|
37
|
+
msg.files # => [#<Riffer::Messages::User::FilePart ...>]
|
|
38
38
|
msg.to_h # => {role: :user, content: "Describe this image", files: [{...}]}
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -67,6 +67,19 @@ if msg.token_usage
|
|
|
67
67
|
end
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
#### Tool Calls
|
|
71
|
+
|
|
72
|
+
`Riffer::Messages::Assistant::ToolCall` is the normalized container riffer stores a requested tool invocation in. Each call carries `call_id` (the provider's identifier, passed back as the tool result's `tool_call_id`), `name`, and `arguments` (the JSON-encoded argument string exactly as the provider emitted it); `to_h` serializes all three.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
tool_call = Riffer::Messages::Assistant::ToolCall.new(call_id: "call_123", name: "weather_tool", arguments: '{"city":"Tokyo"}')
|
|
76
|
+
msg = Riffer::Messages::Assistant.new("", tool_calls: [tool_call])
|
|
77
|
+
|
|
78
|
+
msg.has_tool_calls? # => true
|
|
79
|
+
msg.tool_calls.first.name # => "weather_tool"
|
|
80
|
+
tool_call.to_h # => {call_id: "call_123", name: "weather_tool", arguments: '{"city":"Tokyo"}'}
|
|
81
|
+
```
|
|
82
|
+
|
|
70
83
|
#### Token Usage Semantics
|
|
71
84
|
|
|
72
85
|
`TokenUsage` buckets carry the same meaning for every provider, regardless of how the provider reports its raw usage:
|
|
@@ -84,16 +97,16 @@ The cache buckets are subsets of `input_tokens`, never additions to it — summi
|
|
|
84
97
|
|
|
85
98
|
`finish_reason` carries the same meaning for every provider — each adapter maps its raw wire value (Anthropic's `end_turn`, OpenAI's response status, Gemini's `STOP`, …) into a normalized vocabulary:
|
|
86
99
|
|
|
87
|
-
| Value | Meaning
|
|
88
|
-
| ------------------- |
|
|
89
|
-
| `:stop` | The model finished its turn naturally (or hit a stop sequence).
|
|
90
|
-
| `:length` | Output was truncated at the max-token limit.
|
|
91
|
-
| `:tool_calls` | The model stopped to call tools.
|
|
92
|
-
| `:content_filter` | A provider safety system blocked or cut the response.
|
|
93
|
-
| `:context_window` | Input plus output hit the model's context window; trim or compact history rather than raising `max_tokens`.
|
|
100
|
+
| Value | Meaning |
|
|
101
|
+
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `:stop` | The model finished its turn naturally (or hit a stop sequence). |
|
|
103
|
+
| `:length` | Output was truncated at the max-token limit. |
|
|
104
|
+
| `:tool_calls` | The model stopped to call tools. |
|
|
105
|
+
| `:content_filter` | A provider safety system blocked or cut the response. |
|
|
106
|
+
| `:context_window` | Input plus output hit the model's context window; trim or compact history rather than raising `max_tokens`. |
|
|
94
107
|
| `:malformed_output` | The model emitted output the provider could not parse, such as an invalid tool call; retry or nudge rather than backing off. |
|
|
95
|
-
| `:error` | The provider reported an error finish.
|
|
96
|
-
| `:other` | A provider-specific value with no normalized equivalent.
|
|
108
|
+
| `:error` | The provider reported an error finish. |
|
|
109
|
+
| `:other` | A provider-specific value with no normalized equivalent. |
|
|
97
110
|
|
|
98
111
|
`finish_reason` is `nil` when the provider doesn't report one. The provider's raw wire value travels alongside as `finish_reason_raw` on the message (round-tripped through `to_h` / `from_hash`), on the `FinishReasonDone` stream event, and as the `riffer.finish_reason.raw` trace attribute — for OpenRouter that is the upstream model's `native_finish_reason`, and for a failed OpenAI response it is the error code. Use `finish_reason` to detect truncation without parsing provider responses:
|
|
99
112
|
|
|
@@ -124,6 +137,61 @@ msg = Riffer::Messages::Assistant.new('{"sentiment":"positive"}', structured_out
|
|
|
124
137
|
msg.to_h # => {role: :assistant, content: '{"sentiment":"positive"}', structured_output: {sentiment: "positive"}}
|
|
125
138
|
```
|
|
126
139
|
|
|
140
|
+
#### Reasoning
|
|
141
|
+
|
|
142
|
+
Reasoning models emit thinking blocks alongside their answer, and several providers require those blocks back on the next turn of a tool-calling loop. `Riffer::Messages::Assistant::ReasoningPart` is the normalized container riffer stores them in: a list of parts on the assistant message, in the order the provider emitted them.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
summary = Riffer::Messages::Assistant::ReasoningPart.new(type: :summary, text: "The user wants the answer.", format: "mock-v1")
|
|
146
|
+
opaque = Riffer::Messages::Assistant::ReasoningPart.new(type: :encrypted, data: "b3BhcXVl", format: "mock-v1")
|
|
147
|
+
msg = Riffer::Messages::Assistant.new("42", reasoning: [summary, opaque])
|
|
148
|
+
|
|
149
|
+
msg.reasoning? # => true
|
|
150
|
+
msg.reasoning_text # => "The user wants the answer."
|
|
151
|
+
msg.reasoning.first.type # => :summary
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
A readable part next to an opaque one is the common shape, not a contrived one: OpenAI's Responses API returns a reasoning item as summary text plus an encrypted payload, and Anthropic pairs a `thinking` block with a `redacted_thinking` block when it redacts part of the chain of thought.
|
|
155
|
+
|
|
156
|
+
`reasoning:` takes `ReasoningPart`s; `Riffer::Messages::Base.from_hash` is what turns persisted hashes back into parts.
|
|
157
|
+
|
|
158
|
+
Each part carries:
|
|
159
|
+
|
|
160
|
+
| Field | Type | Description |
|
|
161
|
+
| ----------- | --------- | --------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| `type` | `Symbol` | One of `:text` (readable reasoning), `:summary` (a provider-condensed digest), `:encrypted` (an opaque payload) |
|
|
163
|
+
| `text` | `String?` | The reasoning prose, for `:text` and `:summary` parts |
|
|
164
|
+
| `data` | `String?` | The opaque payload, for `:encrypted` parts |
|
|
165
|
+
| `signature` | `String?` | The provider's signature over the part, when it issues one |
|
|
166
|
+
| `id` | `String?` | The provider's identifier for the part, when it issues one |
|
|
167
|
+
| `format` | `String?` | The wire format, owned by the adapter that produced the part (e.g. `"anthropic-claude-v1"`) |
|
|
168
|
+
|
|
169
|
+
A `type` outside the three values raises `Riffer::ArgumentError`. `format` is a free string riffer never validates — it exists so an adapter can tell its own parts apart from another adapter's.
|
|
170
|
+
|
|
171
|
+
`reasoning?` is true when the message carries any part. `reasoning_text` joins the `text` of the `:text` and `:summary` parts with blank lines, skipping `:encrypted` parts, and is `nil` when there is nothing to join. The run's final assistant message projects its parts onto `response.reasoning` (see [Agent Lifecycle — Response Attributes](AGENT_LIFECYCLE.md#response-attributes)).
|
|
172
|
+
|
|
173
|
+
Parts round-trip through `to_h` / `from_hash` like every other message field, so an application that persists sessions can store and replay them. The `reasoning` key is absent from `to_h` when the message has no parts, and each part omits the fields it doesn't carry:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
msg.to_h
|
|
177
|
+
# => {role: :assistant, content: "42", reasoning: [
|
|
178
|
+
# {type: :summary, text: "The user wants the answer.", format: "mock-v1"},
|
|
179
|
+
# {type: :encrypted, data: "b3BhcXVl", format: "mock-v1"}
|
|
180
|
+
# ]}
|
|
181
|
+
|
|
182
|
+
Riffer::Messages::Base.from_hash(msg.to_h).reasoning # => [ReasoningPart, ReasoningPart]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**The replay contract.** A provider adapter replays only the parts whose `format` it recognizes and silently skips the rest, so history that travelled through another provider is never rejected. A part with no `format` is never replayed; adapters that surface reasoning text but cannot yet send it back emit their parts that way, so the text is kept for display without risking a rejected request. Parts are never reordered, merged, or edited — riffer treats them as opaque, because the provider's signature covers their exact bytes.
|
|
186
|
+
|
|
187
|
+
An application that would rather not store parts can leave the key out when it serializes:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
msg.to_h.except(:reasoning)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
To drop parts from the in-memory session mid-run instead, `Session#update(id:, reasoning: [])` rewrites the message in place; it needs [message ids](#ids) enabled to address it.
|
|
194
|
+
|
|
127
195
|
### Tool
|
|
128
196
|
|
|
129
197
|
Tool messages contain the results of tool executions:
|
|
@@ -155,7 +223,7 @@ msg.error_type # => :execution_error
|
|
|
155
223
|
|
|
156
224
|
## File Parts
|
|
157
225
|
|
|
158
|
-
`Riffer::Messages::FilePart` represents a file attachment (image or document) that can be included with user messages.
|
|
226
|
+
`Riffer::Messages::User::FilePart` represents a file attachment (image or document) that can be included with user messages.
|
|
159
227
|
|
|
160
228
|
### Supported Media Types
|
|
161
229
|
|
|
@@ -167,21 +235,21 @@ msg.error_type # => :execution_error
|
|
|
167
235
|
|
|
168
236
|
```ruby
|
|
169
237
|
# From a file path (reads eagerly, detects media type from extension)
|
|
170
|
-
file = Riffer::Messages::FilePart.from_path("photo.jpg")
|
|
238
|
+
file = Riffer::Messages::User::FilePart.from_path("photo.jpg")
|
|
171
239
|
file.media_type # => "image/jpeg"
|
|
172
240
|
file.filename # => "photo.jpg"
|
|
173
241
|
file.image? # => true
|
|
174
242
|
|
|
175
243
|
# From a URL (stored directly, resolved lazily if provider needs bytes)
|
|
176
|
-
file = Riffer::Messages::FilePart.from_url("https://example.com/doc.pdf")
|
|
244
|
+
file = Riffer::Messages::User::FilePart.from_url("https://example.com/doc.pdf")
|
|
177
245
|
file.url? # => true
|
|
178
246
|
file.document? # => true
|
|
179
247
|
|
|
180
248
|
# From raw base64 data
|
|
181
|
-
file = Riffer::Messages::FilePart.new(media_type: "image/png", data: base64_string, filename: "chart.png")
|
|
249
|
+
file = Riffer::Messages::User::FilePart.new(media_type: "image/png", data: base64_string, filename: "chart.png")
|
|
182
250
|
|
|
183
251
|
# With an expected sha256 checksum of the file's contents
|
|
184
|
-
file = Riffer::Messages::FilePart.from_url(
|
|
252
|
+
file = Riffer::Messages::User::FilePart.from_url(
|
|
185
253
|
"https://example.com/doc.pdf",
|
|
186
254
|
sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
187
255
|
)
|
|
@@ -243,7 +311,7 @@ agent = MyAgent.new(session: session)
|
|
|
243
311
|
response = agent.generate # session already carries the last user turn
|
|
244
312
|
```
|
|
245
313
|
|
|
246
|
-
`Riffer::Agent::Session.new(messages:)` accepts `Riffer::Messages::Base` objects. If your persistence layer hands back hashes, normalize them first via `Riffer::Messages::Base.from_hash` or your own adapter.
|
|
314
|
+
`Riffer::Agent::Session.new(messages:)` accepts `Riffer::Messages::Base` objects. If your persistence layer hands back hashes, normalize them first via `Riffer::Messages::Base.from_hash` (which dispatches on `:role`), a role's own `from_hash` such as `Riffer::Messages::User.from_hash` when the role is already known, or your own adapter.
|
|
247
315
|
|
|
248
316
|
### Accessing Message History
|
|
249
317
|
|
data/docs/STREAM_EVENTS.md
CHANGED
|
@@ -105,14 +105,18 @@ event.content # => "Let me think about "
|
|
|
105
105
|
|
|
106
106
|
### ReasoningDone
|
|
107
107
|
|
|
108
|
-
Emitted when reasoning is complete:
|
|
108
|
+
Emitted when one reasoning block is complete:
|
|
109
109
|
|
|
110
110
|
```ruby
|
|
111
|
-
|
|
112
|
-
event
|
|
113
|
-
event.
|
|
111
|
+
part = Riffer::Messages::Assistant::ReasoningPart.new(type: :text, text: "Let me think about this step by step...", format: "mock-v1")
|
|
112
|
+
event = Riffer::StreamEvents::ReasoningDone.new(part)
|
|
113
|
+
event.role # => :assistant
|
|
114
|
+
event.part # => the ReasoningPart
|
|
115
|
+
event.part.text # => "Let me think about this step by step..."
|
|
114
116
|
```
|
|
115
117
|
|
|
118
|
+
`part` is the [reasoning part](MESSAGES.md#reasoning) the preceding `ReasoningDelta` events added up to, and the agent loop accumulates it onto the assistant message. Adapters that cannot yet replay their reasoning emit it as a `:text` part with no `format`, so it is stored for display but never sent back to the provider.
|
|
119
|
+
|
|
116
120
|
### WebSearchStatus
|
|
117
121
|
|
|
118
122
|
Emitted during web search progress with status updates:
|
data/docs/TOOL_ADVANCED.md
CHANGED
|
@@ -112,9 +112,7 @@ For expected failures, return `error(...)` or raise `Riffer::ToolExecutionError`
|
|
|
112
112
|
|
|
113
113
|
The LLM receives the error message and can decide how to respond (retry, apologize, ask for different input, etc.).
|
|
114
114
|
|
|
115
|
-
## Tool Runtime
|
|
116
|
-
|
|
117
|
-
> **Warning:** This feature is experimental and may be removed or changed without warning in a future release.
|
|
115
|
+
## Tool Runtime
|
|
118
116
|
|
|
119
117
|
By default, tool calls are executed sequentially in the current thread using `Riffer::Tools::Runtime::Inline`. You can change how tool calls are executed by configuring a different tool runtime.
|
|
120
118
|
|
data/docs/TRACING.md
CHANGED
|
@@ -58,7 +58,7 @@ Riffer.configure do |config|
|
|
|
58
58
|
end
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
The backend is duck-typed — any object satisfying the contract works, and the setter validates
|
|
61
|
+
The backend is duck-typed — any object satisfying the contract works, and the setter validates that it responds to `in_span`, `current_context`, and `with_context` (otherwise it raises `Riffer::ArgumentError`). It must respond to:
|
|
62
62
|
|
|
63
63
|
- `in_span(name, attributes:, kind:) { |span| … }` — open a span around the block, yield a span object, and return the block's value.
|
|
64
64
|
- `current_context` — return the active trace context (for re-attaching across fiber/thread boundaries), or `nil` when there is none.
|
|
@@ -95,7 +95,7 @@ The contract promise is: **when present**, a key carries the documented meaning
|
|
|
95
95
|
|
|
96
96
|
### Per-call tags (`riffer.tag.*`)
|
|
97
97
|
|
|
98
|
-
Any tags passed to `#generate` / `#stream` via `tags:` are stamped on **all four** span types as `riffer.tag.<key>` (string), so the per-span tables below omit them. They appear on every span the tagged call emits and are absent otherwise. Example: `tags: {team: "growth"}` adds `riffer.tag.team` → `"growth"` to the `invoke_agent`, `chat`, `execute_tool`, and `execute_guardrail` spans. See [Per-Call Tags](AGENTS.md#per-call-tags) for the full surface and the per-provider request-metadata mapping.
|
|
98
|
+
Any tags passed to `#generate` / `#stream` via `tags:` are stamped on **all four** span types as `riffer.tag.<key>` (string), so the per-span tables below omit them. They appear on every span the tagged call emits and are absent otherwise. Example: `tags: {team: "growth"}` adds `riffer.tag.team` → `"growth"` to the `invoke_agent`, `chat`, `execute_tool`, and `execute_guardrail` spans. The [default tags](AGENTS.md#default-tags) are always present, e.g. `riffer.tag.kind` → `"agent"` and `riffer.tag.agent` → the agent identifier. See [Per-Call Tags](AGENTS.md#per-call-tags) for the full surface and the per-provider request-metadata mapping.
|
|
99
99
|
|
|
100
100
|
## `invoke_agent {agent}` — the run span
|
|
101
101
|
|
|
@@ -166,12 +166,43 @@ class AWSAgent < Riffer::Agent
|
|
|
166
166
|
end
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
## Reasoning Models
|
|
170
|
+
|
|
171
|
+
Claude models on Bedrock can think before they answer. Enable extended thinking through `additional_model_request_fields`:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
class ThinkAgent < Riffer::Agent
|
|
175
|
+
model 'amazon_bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0'
|
|
176
|
+
model_options inference_config: {max_tokens: 2048},
|
|
177
|
+
additional_model_request_fields: {thinking: {type: "enabled", budget_tokens: 1024}}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
ThinkAgent.new.stream('What is 2+2? Think step by step.').each do |event|
|
|
181
|
+
case event
|
|
182
|
+
when Riffer::StreamEvents::ReasoningDelta
|
|
183
|
+
print "[reasoning] #{event.content}"
|
|
184
|
+
when Riffer::StreamEvents::TextDelta
|
|
185
|
+
print event.content
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The reasoning is kept on the assistant message as [reasoning parts](../MESSAGES.md#reasoning), whether you call `generate` or `stream`. Read it with `response.reasoning`, or as plain text with `reasoning_text` on the message. Bedrock sometimes redacts part of Claude's reasoning. Those parts are stored and sent back like any other, but they have no readable text, so `reasoning_text` leaves them out.
|
|
191
|
+
|
|
192
|
+
### Reasoning Replay
|
|
193
|
+
|
|
194
|
+
Riffer sends the reasoning back to Bedrock on every later turn. How much of it Claude uses depends on the model; some only use the reasoning from the current tool-calling turn. For tool calls, sending it back is required: when thinking is enabled, Claude needs its earlier reasoning back to carry on after a tool result. You don't have to do anything; it happens as long as the assistant messages stay in the history.
|
|
195
|
+
|
|
196
|
+
If you persist sessions, keep the `reasoning` key when you store messages (see [Messages — Reasoning](../MESSAGES.md#reasoning)). If you drop it, later turns lose the model's earlier reasoning, and a tool-calling turn with thinking enabled may be rejected.
|
|
197
|
+
|
|
198
|
+
Only reasoning that Bedrock produced is sent back to Bedrock. A conversation that switches providers midway still works: reasoning from other providers is kept on the messages but left out of Bedrock requests.
|
|
199
|
+
|
|
169
200
|
## File Support
|
|
170
201
|
|
|
171
202
|
Bedrock accepts file attachments either as raw bytes, or as `s3://` URIs passed straight through to Converse — Bedrock fetches the S3 object itself:
|
|
172
203
|
|
|
173
204
|
```ruby
|
|
174
|
-
file = Riffer::Messages::FilePart.from_url("s3://my-bucket/document.pdf", media_type: "application/pdf")
|
|
205
|
+
file = Riffer::Messages::User::FilePart.from_url("s3://my-bucket/document.pdf", media_type: "application/pdf")
|
|
175
206
|
response = provider.generate_text(
|
|
176
207
|
prompt: "Summarize this document",
|
|
177
208
|
model: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
@@ -24,7 +24,7 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
|
24
24
|
params = {
|
|
25
25
|
model: model,
|
|
26
26
|
messages: convert_messages(messages),
|
|
27
|
-
**options.except(:tools)
|
|
27
|
+
**options.except(:tools, :tags)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
if tools && !tools.empty?
|
|
@@ -220,9 +220,9 @@ Riffer::StreamEvents::ToolCallDone.new(
|
|
|
220
220
|
arguments: '{"complete":"args"}'
|
|
221
221
|
)
|
|
222
222
|
|
|
223
|
-
# Reasoning (if supported)
|
|
223
|
+
# Reasoning (if supported); see the Reasoning section for building the part
|
|
224
224
|
Riffer::StreamEvents::ReasoningDelta.new("thinking...")
|
|
225
|
-
Riffer::StreamEvents::ReasoningDone.new(
|
|
225
|
+
Riffer::StreamEvents::ReasoningDone.new(part)
|
|
226
226
|
|
|
227
227
|
# Web search (if supported)
|
|
228
228
|
Riffer::StreamEvents::WebSearchStatus.new("searching", query: "search query")
|
|
@@ -276,6 +276,58 @@ yielder << Riffer::StreamEvents::FinishReasonDone.new(finish_reason: :stop, raw_
|
|
|
276
276
|
|
|
277
277
|
Also have `execute_stream` raise `Riffer::IncompleteStreamError` when the stream ends without the provider's terminal event, rather than returning normally. Otherwise a connection that drops mid-response looks identical to a finished one, and the agent loop accepts a truncated message as complete.
|
|
278
278
|
|
|
279
|
+
## Reasoning
|
|
280
|
+
|
|
281
|
+
`extract_reasoning` is the optional hook for reasoning models — return the response's thinking blocks as [`Riffer::Messages::Assistant::ReasoningPart`s](../MESSAGES.md#reasoning) and the base class attaches them to the assistant message, where your application can persist them and hand them back on the next turn:
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
def extract_reasoning(response)
|
|
285
|
+
response.thinking_blocks.map do |block|
|
|
286
|
+
Riffer::Messages::Assistant::ReasoningPart.new(
|
|
287
|
+
type: :encrypted,
|
|
288
|
+
data: block.data,
|
|
289
|
+
signature: block.signature,
|
|
290
|
+
format: "my-provider-v1"
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The base class defaults to `[]`, so a provider without reasoning stays valid.
|
|
297
|
+
|
|
298
|
+
Your adapter owns its `format` string: pick one value per wire shape, replay only the parts carrying a value you recognize, and skip the rest — history that travelled through another provider must never make a request fail. Never reorder or edit a part; the provider's signature covers its exact bytes.
|
|
299
|
+
|
|
300
|
+
For streaming, emit one `ReasoningDone` per block, carrying the part so the agent loop can accumulate it:
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
part = Riffer::Messages::Assistant::ReasoningPart.new(type: :text, text: "complete reasoning", format: "my-provider-v1")
|
|
304
|
+
|
|
305
|
+
yielder << Riffer::StreamEvents::ReasoningDelta.new("thinking...")
|
|
306
|
+
yielder << Riffer::StreamEvents::ReasoningDone.new(part)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
If your adapter surfaces reasoning text but cannot yet replay it, call `yield_reasoning_done(yielder, text)` instead. It wraps the text in a `:text` part with no `format`, which persists for display and is skipped on replay.
|
|
310
|
+
|
|
311
|
+
## Tags
|
|
312
|
+
|
|
313
|
+
Every agent and judge call passes a `:tags` option: a flat `String => String` hash of the caller's [per-call tags](../AGENTS.md#per-call-tags) plus the [default tags](../AGENTS.md#default-tags). `kind` (`"agent"` or `"judge"`) and `agent` (the agent or evaluator identifier) tell you who the call is on behalf of:
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
def build_request_params(messages, model, options)
|
|
317
|
+
tags = options[:tags] || {}
|
|
318
|
+
|
|
319
|
+
{
|
|
320
|
+
agent: tags["agent"],
|
|
321
|
+
user: tags["user_id"],
|
|
322
|
+
messages: convert_messages(messages),
|
|
323
|
+
model: model,
|
|
324
|
+
**options.except(:tools, :tags),
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Map tags to your service's native request field, or drop them, but don't pass `:tags` on to an SDK verbatim.
|
|
330
|
+
|
|
279
331
|
## Trace Provider Name
|
|
280
332
|
|
|
281
333
|
LLM-call and agent-run spans stamp `gen_ai.provider.name` from the `semconv_provider_name` class method. The default is your snake_cased class name; override it when a [GenAI semconv well-known value](https://opentelemetry.io/docs/specs/semconv/gen-ai/) exists for your provider:
|
|
@@ -330,7 +382,7 @@ class Riffer::Providers::MyProvider < Riffer::Providers::Base
|
|
|
330
382
|
messages: convert_messages(conversation),
|
|
331
383
|
system: system_message,
|
|
332
384
|
max_tokens: options[:max_tokens] || 4096,
|
|
333
|
-
**options.except(:tools, :max_tokens)
|
|
385
|
+
**options.except(:tools, :max_tokens, :tags)
|
|
334
386
|
}
|
|
335
387
|
|
|
336
388
|
if tools && !tools.empty?
|
data/docs/providers/GEMINI.md
CHANGED
|
@@ -148,7 +148,7 @@ response = provider.generate_text(
|
|
|
148
148
|
Gemini's API only accepts inline base64-encoded files (images and documents), never a URL reference:
|
|
149
149
|
|
|
150
150
|
```ruby
|
|
151
|
-
file = Riffer::Messages::FilePart.new(data: base64_data, media_type: "image/png")
|
|
151
|
+
file = Riffer::Messages::User::FilePart.new(data: base64_data, media_type: "image/png")
|
|
152
152
|
response = provider.generate_text(
|
|
153
153
|
prompt: "Describe this image",
|
|
154
154
|
model: "gemini-2.5-flash-lite",
|
|
@@ -47,6 +47,23 @@ provider.stub_response("Based on the tool result, here's my answer.")
|
|
|
47
47
|
response = agent.generate("Use the tool")
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Stubbing Reasoning
|
|
51
|
+
|
|
52
|
+
Stub [reasoning parts](../MESSAGES.md#reasoning) to exercise your application's persistence of them. Hashes are normalized into `Riffer::Messages::Assistant::ReasoningPart`s:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
provider.stub_response("42", reasoning: [
|
|
56
|
+
{type: :text, text: "The user wants the answer.", format: "mock-v1"},
|
|
57
|
+
{type: :encrypted, data: "b3BhcXVl", signature: "sig", format: "mock-v1"}
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
response = agent.generate("What is the answer?")
|
|
61
|
+
response.reasoning.map(&:type) # => [:text, :encrypted]
|
|
62
|
+
agent.session.messages.last.reasoning_text # => "The user wants the answer."
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
When streaming, each part is emitted as a `ReasoningDelta` (only when it carries `text`) followed by a `ReasoningDone` carrying the part, ahead of the text events.
|
|
66
|
+
|
|
50
67
|
## Queueing Multiple Responses
|
|
51
68
|
|
|
52
69
|
Responses are consumed in order:
|
|
@@ -173,7 +173,7 @@ end
|
|
|
173
173
|
|
|
174
174
|
## Reasoning Models
|
|
175
175
|
|
|
176
|
-
Reasoning models surface their thought process via OpenRouter's normalised `
|
|
176
|
+
Reasoning models surface their thought process via OpenRouter's normalised `reasoning_details` field. Enable it with the `reasoning` option:
|
|
177
177
|
|
|
178
178
|
```ruby
|
|
179
179
|
class ThinkAgent < Riffer::Agent
|
|
@@ -191,6 +191,23 @@ ThinkAgent.new.stream('What is 2+2? Think step by step.').each do |event|
|
|
|
191
191
|
end
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
+
### Reasoning Replay
|
|
195
|
+
|
|
196
|
+
Each entry in OpenRouter's `reasoning_details` becomes a [`ReasoningPart`](../MESSAGES.md#reasoning) on the assistant message, on both `generate_text` and `stream_text`, with nothing dropped:
|
|
197
|
+
|
|
198
|
+
| `reasoning_details` field | `ReasoningPart` field |
|
|
199
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
200
|
+
| `type` | `type`: `reasoning.text` → `:text`, `reasoning.summary` → `:summary`, `reasoning.encrypted` → `:encrypted` |
|
|
201
|
+
| `text` / `summary` | `text` |
|
|
202
|
+
| `data` | `data` |
|
|
203
|
+
| `signature` | `signature` |
|
|
204
|
+
| `id` | `id` |
|
|
205
|
+
| `format` | `format` (e.g. `"anthropic-claude-v1"`, `"openai-responses-v1"`, `"unknown"`) |
|
|
206
|
+
|
|
207
|
+
When streaming, OpenRouter splits one block into many fragments that share an `index`. The provider concatenates their `text`, `summary`, and `data` and keeps the `signature`, `id`, and `format` that arrive along the way, so each block ends as one `ReasoningDone` part. Each non-empty `text` or `summary` fragment is also yielded as a `ReasoningDelta`. Detail types riffer doesn't know are skipped.
|
|
208
|
+
|
|
209
|
+
On the next request, the assistant message's parts go back as `reasoning_details` in their original order and unchanged. This is what lets Anthropic and Gemini models continue signed thinking across a tool-call loop, and lets OpenAI models reuse their encrypted reasoning. Following the [replay contract](../MESSAGES.md#reasoning), only parts whose `format` is one OpenRouter documents are sent: `unknown`, `openai-responses-v1`, `azure-openai-responses-v1`, `bedrock-openai-responses-v1`, `bedrock-xai-responses-v1`, `xai-responses-v1`, `meta-responses-v1`, `anthropic-claude-v1`, and `google-gemini-v1` (listed in `Riffer::Providers::OpenRouter::REASONING_FORMATS`). Parts with no `format`, or one produced by another adapter such as `mock-v1`, are skipped. The `index` is not stored, since the parts' order already carries it.
|
|
210
|
+
|
|
194
211
|
## Routing & Fallbacks
|
|
195
212
|
|
|
196
213
|
Survive an upstream outage by chaining models:
|
data/docs-site/build.rb
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Builds the docs site — landing page, guide pages, 404, and assets — into
|
|
5
|
-
# _site/ at the repo root. Pages are declared in manifest.yml; the build fails
|
|
6
|
-
# if the manifest and docs/**/*.md ever disagree.
|
|
7
|
-
|
|
8
4
|
require "erb"
|
|
9
5
|
require "fileutils"
|
|
10
6
|
require "yaml"
|
|
@@ -89,8 +85,6 @@ def build_groups(manifest)
|
|
|
89
85
|
end
|
|
90
86
|
end
|
|
91
87
|
|
|
92
|
-
# Numbering restarts per docs/ subdirectory, so the providers pages read as
|
|
93
|
-
# their own sequence rather than continuing the main chapters.
|
|
94
88
|
def chapter_numbers(manifest)
|
|
95
89
|
manifest.
|
|
96
90
|
flat_map { |group| group[:pages] }.
|
data/docs-site/check.rb
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Validates every internal link and anchor in the built _site/ HTML. Exits
|
|
5
|
-
# non-zero with a list of broken links on failure.
|
|
6
|
-
|
|
7
4
|
SITE = Pathname(__dir__).join("../_site").expand_path
|
|
8
5
|
|
|
9
6
|
EXTERNAL = %r{\A(?:https?:|mailto:|//)}
|
|
@@ -33,11 +30,10 @@ def ids(html)
|
|
|
33
30
|
html.scan(/\bid="([^"]+)"/).flatten
|
|
34
31
|
end
|
|
35
32
|
|
|
36
|
-
# Links into /api/ are skipped: RDoc builds that tree in a separate task, so
|
|
37
|
-
# it is absent when only the site has been built.
|
|
38
33
|
def page_errors(file, id_index)
|
|
39
34
|
links(file.read).
|
|
40
35
|
grep_v(EXTERNAL).
|
|
36
|
+
# RDoc builds /api/ in a separate task, so it is absent when only the site has been built.
|
|
41
37
|
reject { |link| link.start_with?("/api/") }.
|
|
42
38
|
filter_map { |link| link_error(file, link, id_index) }
|
|
43
39
|
end
|
data/lib/riffer/agent/config.rb
CHANGED
|
@@ -1,47 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
-
# Typed configuration object holding every class-level DSL setting on a
|
|
5
|
-
# Riffer::Agent subclass. Procs are stored unresolved and resolved per-instance
|
|
6
|
-
# later.
|
|
7
4
|
class Riffer::Agent::Config
|
|
8
5
|
DEFAULT_MAX_STEPS = 16 #: Integer
|
|
9
6
|
|
|
10
|
-
#
|
|
11
|
-
attr_reader :identifier #: String? # @dynamic identifier
|
|
7
|
+
# @rbs @tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?
|
|
12
8
|
|
|
13
|
-
#
|
|
9
|
+
attr_reader :identifier #: String? # @dynamic identifier
|
|
14
10
|
attr_reader :model #: (String | Proc)? # @dynamic model
|
|
15
|
-
|
|
16
|
-
# The configured instructions.
|
|
17
11
|
attr_reader :instructions #: (String | Proc)? # @dynamic instructions
|
|
18
|
-
|
|
19
|
-
# Options passed to generate_text/stream_text.
|
|
20
12
|
attr_accessor :model_options #: Hash[Symbol, untyped] # @dynamic model_options, model_options=
|
|
21
|
-
|
|
22
|
-
# The configured structured-output schema.
|
|
23
13
|
attr_reader :structured_output #: Riffer::Params? # @dynamic structured_output
|
|
24
|
-
|
|
25
|
-
# The maximum number of LLM call steps in the tool-use loop.
|
|
26
14
|
attr_accessor :max_steps #: Numeric? # @dynamic max_steps, max_steps=
|
|
27
|
-
|
|
28
|
-
# The configured tools.
|
|
29
15
|
attr_accessor :tools_config #: (Array[singleton(Riffer::Tool)] | Proc)? # @dynamic tools_config, tools_config=
|
|
30
|
-
|
|
31
|
-
# The accumulated +use_mcp+ tag configurations.
|
|
32
16
|
attr_reader :mcp_configs #: Array[Hash[Symbol, untyped]] # @dynamic mcp_configs
|
|
33
|
-
|
|
34
|
-
# The configured tool runtime.
|
|
35
|
-
attr_reader :tool_runtime #: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc) # @dynamic tool_runtime
|
|
36
|
-
|
|
37
|
-
# The configured skills.
|
|
38
17
|
attr_accessor :skills_config #: Riffer::Skills::Config? # @dynamic skills_config, skills_config=
|
|
39
|
-
|
|
40
|
-
# Registered guardrail entries keyed by phase.
|
|
41
18
|
attr_reader :guardrails #: Hash[Symbol, Array[Hash[Symbol, untyped]]] # @dynamic guardrails
|
|
42
19
|
|
|
43
|
-
# Builds a new Config. Raises Riffer::ArgumentError if +model+ or
|
|
44
|
-
# +instructions+ is invalid (e.g. an empty string).
|
|
45
20
|
#--
|
|
46
21
|
#: (
|
|
47
22
|
# ?identifier: String?,
|
|
@@ -52,7 +27,7 @@ class Riffer::Agent::Config
|
|
|
52
27
|
# ?max_steps: Numeric?,
|
|
53
28
|
# ?tools_config: (Array[singleton(Riffer::Tool)] | Proc)?,
|
|
54
29
|
# ?mcp_configs: Array[Hash[Symbol, untyped]],
|
|
55
|
-
# ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
30
|
+
# ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?,
|
|
56
31
|
# ?skills_config: Riffer::Skills::Config?,
|
|
57
32
|
# ?guardrails: Hash[Symbol, Array[Hash[Symbol, untyped]]]
|
|
58
33
|
# ) -> void
|
|
@@ -65,7 +40,7 @@ class Riffer::Agent::Config
|
|
|
65
40
|
max_steps: DEFAULT_MAX_STEPS,
|
|
66
41
|
tools_config: nil,
|
|
67
42
|
mcp_configs: [],
|
|
68
|
-
tool_runtime:
|
|
43
|
+
tool_runtime: nil,
|
|
69
44
|
skills_config: nil,
|
|
70
45
|
guardrails: { before: [], after: [] }
|
|
71
46
|
)
|
|
@@ -79,17 +54,15 @@ class Riffer::Agent::Config
|
|
|
79
54
|
self.model = model
|
|
80
55
|
self.instructions = instructions
|
|
81
56
|
self.structured_output = structured_output
|
|
82
|
-
self.tool_runtime = tool_runtime
|
|
57
|
+
self.tool_runtime = tool_runtime if tool_runtime
|
|
83
58
|
end
|
|
84
59
|
|
|
85
|
-
# Sets +identifier+, coercing the value to String.
|
|
86
60
|
#--
|
|
87
61
|
#: (untyped) -> String?
|
|
88
62
|
def identifier=(value)
|
|
89
63
|
@identifier = value&.to_s
|
|
90
64
|
end
|
|
91
65
|
|
|
92
|
-
# Sets +structured_output+. Raises Riffer::ArgumentError on an invalid value.
|
|
93
66
|
#--
|
|
94
67
|
#: (Riffer::Params?) -> Riffer::Params?
|
|
95
68
|
def structured_output=(value)
|
|
@@ -100,7 +73,14 @@ class Riffer::Agent::Config
|
|
|
100
73
|
@structured_output = value
|
|
101
74
|
end
|
|
102
75
|
|
|
103
|
-
|
|
76
|
+
#--
|
|
77
|
+
#: () -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
78
|
+
def tool_runtime
|
|
79
|
+
# Resolved here rather than at construction so a copy can tell an inherited
|
|
80
|
+
# runtime from a defaulted one.
|
|
81
|
+
@tool_runtime || Riffer.config.tool_runtime
|
|
82
|
+
end
|
|
83
|
+
|
|
104
84
|
#--
|
|
105
85
|
#: ((singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
|
|
106
86
|
def tool_runtime=(value)
|
|
@@ -114,8 +94,6 @@ class Riffer::Agent::Config
|
|
|
114
94
|
@tool_runtime = value
|
|
115
95
|
end
|
|
116
96
|
|
|
117
|
-
# Sets +model+. Raises Riffer::ArgumentError on an invalid value (e.g. an
|
|
118
|
-
# empty string).
|
|
119
97
|
#--
|
|
120
98
|
#: ((String | Proc)?) -> (String | Proc)?
|
|
121
99
|
def model=(value)
|
|
@@ -123,8 +101,6 @@ class Riffer::Agent::Config
|
|
|
123
101
|
@model = value
|
|
124
102
|
end
|
|
125
103
|
|
|
126
|
-
# Sets +instructions+. Raises Riffer::ArgumentError on an invalid value (e.g.
|
|
127
|
-
# an empty string).
|
|
128
104
|
#--
|
|
129
105
|
#: ((String | Proc)?) -> (String | Proc)?
|
|
130
106
|
def instructions=(value)
|
|
@@ -132,8 +108,6 @@ class Riffer::Agent::Config
|
|
|
132
108
|
@instructions = value
|
|
133
109
|
end
|
|
134
110
|
|
|
135
|
-
# Appends an MCP tag entry to +mcp_configs+.
|
|
136
|
-
#
|
|
137
111
|
#--
|
|
138
112
|
#: (String | Symbol, ?progressive: bool) -> Array[Hash[Symbol, untyped]]
|
|
139
113
|
def add_mcp(tag, progressive: true)
|
|
@@ -142,9 +116,6 @@ class Riffer::Agent::Config
|
|
|
142
116
|
@mcp_configs << { tags: [tag.to_sym], progressive: progressive }
|
|
143
117
|
end
|
|
144
118
|
|
|
145
|
-
# Appends a guardrail entry to +guardrails+ for the given phase; +:around+
|
|
146
|
-
# appends to both +:before+ and +:after+. Raises Riffer::ArgumentError unless
|
|
147
|
-
# +phase+ is :before, :after, or :around.
|
|
148
119
|
#--
|
|
149
120
|
#: (Symbol, klass: singleton(Riffer::Guardrail), ?options: Hash[Symbol, untyped]) -> void
|
|
150
121
|
def add_guardrail(phase, klass:, options: {})
|
|
@@ -167,8 +138,6 @@ class Riffer::Agent::Config
|
|
|
167
138
|
end
|
|
168
139
|
end
|
|
169
140
|
|
|
170
|
-
# Returns the guardrail entries for the given phase, or +[]+ if none.
|
|
171
|
-
#
|
|
172
141
|
#--
|
|
173
142
|
#: (Symbol) -> Array[Hash[Symbol, untyped]]
|
|
174
143
|
def guardrails_for(phase)
|
|
@@ -177,6 +146,20 @@ class Riffer::Agent::Config
|
|
|
177
146
|
|
|
178
147
|
private
|
|
179
148
|
|
|
149
|
+
#--
|
|
150
|
+
#: (Riffer::Agent::Config) -> void
|
|
151
|
+
def initialize_copy(source)
|
|
152
|
+
super
|
|
153
|
+
# A shallow copy would share collections, letting a declaration on either
|
|
154
|
+
# config reach the other; entries nest (mcp +:tags+, guardrail +:options+).
|
|
155
|
+
@model_options = Riffer::Helpers::DeepDup.call(source.model_options)
|
|
156
|
+
@mcp_configs = Riffer::Helpers::DeepDup.call(source.mcp_configs)
|
|
157
|
+
@guardrails = Riffer::Helpers::DeepDup.call(source.guardrails)
|
|
158
|
+
@tools_config = Riffer::Helpers::DeepDup.call(source.tools_config)
|
|
159
|
+
@skills_config = source.skills_config&.dup
|
|
160
|
+
@structured_output = source.structured_output&.dup
|
|
161
|
+
end
|
|
162
|
+
|
|
180
163
|
#--
|
|
181
164
|
#: (untyped, String) -> void
|
|
182
165
|
def validate_string_or_proc!(value, name)
|