riffer 0.47.1 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/rbs-inline.md +5 -1
  3. data/.release-please-manifest.json +1 -1
  4. data/CHANGELOG.md +31 -0
  5. data/Rakefile +1 -1
  6. data/Steepfile +1 -15
  7. data/docs/AGENTS.md +37 -3
  8. data/docs/AGENT_LIFECYCLE.md +1 -0
  9. data/docs/CONFIGURATION.md +1 -1
  10. data/docs/EVALS.md +2 -1
  11. data/docs/MCP.md +0 -4
  12. data/docs/MESSAGES.md +85 -17
  13. data/docs/STREAM_EVENTS.md +31 -4
  14. data/docs/TRACING.md +1 -1
  15. data/docs/providers/AMAZON_BEDROCK.md +1 -1
  16. data/docs/providers/CUSTOM_PROVIDERS.md +58 -4
  17. data/docs/providers/GEMINI.md +1 -1
  18. data/docs/providers/MOCK_PROVIDER.md +17 -0
  19. data/lib/riffer/agent/config.rb +40 -16
  20. data/lib/riffer/agent/outcome.rb +2 -2
  21. data/lib/riffer/agent/response.rb +16 -9
  22. data/lib/riffer/agent/run.rb +24 -9
  23. data/lib/riffer/agent/session.rb +2 -1
  24. data/lib/riffer/agent/structured_output/result.rb +2 -2
  25. data/lib/riffer/agent/structured_output.rb +1 -1
  26. data/lib/riffer/agent.rb +40 -16
  27. data/lib/riffer/config.rb +31 -31
  28. data/lib/riffer/evals/evaluator.rb +29 -2
  29. data/lib/riffer/evals/judge.rb +13 -8
  30. data/lib/riffer/evals/result.rb +6 -6
  31. data/lib/riffer/evals/run_result.rb +1 -1
  32. data/lib/riffer/evals/scenario_result.rb +6 -6
  33. data/lib/riffer/files/resolver.rb +3 -3
  34. data/lib/riffer/guardrails/modification.rb +3 -3
  35. data/lib/riffer/guardrails/result.rb +3 -3
  36. data/lib/riffer/guardrails/runner.rb +4 -4
  37. data/lib/riffer/guardrails/tripwire.rb +4 -4
  38. data/lib/riffer/helpers/deep_dup.rb +42 -0
  39. data/lib/riffer/mcp/manifest.rb +5 -5
  40. data/lib/riffer/mcp/registration.rb +1 -1
  41. data/lib/riffer/mcp/search_tool.rb +1 -1
  42. data/lib/riffer/messages/assistant/reasoning_part.rb +90 -0
  43. data/lib/riffer/messages/assistant/tool_call.rb +63 -0
  44. data/lib/riffer/messages/assistant.rb +55 -7
  45. data/lib/riffer/messages/base.rb +7 -25
  46. data/lib/riffer/messages/system.rb +10 -0
  47. data/lib/riffer/messages/tool.rb +21 -4
  48. data/lib/riffer/messages/{file_part.rb → user/file_part.rb} +18 -17
  49. data/lib/riffer/messages/user.rb +13 -2
  50. data/lib/riffer/params/param.rb +20 -8
  51. data/lib/riffer/params.rb +10 -1
  52. data/lib/riffer/providers/amazon_bedrock.rb +34 -6
  53. data/lib/riffer/providers/anthropic.rb +9 -2
  54. data/lib/riffer/providers/base.rb +28 -10
  55. data/lib/riffer/providers/finish_reason.rb +2 -2
  56. data/lib/riffer/providers/gemini.rb +2 -2
  57. data/lib/riffer/providers/mock.rb +19 -3
  58. data/lib/riffer/providers/open_ai.rb +14 -5
  59. data/lib/riffer/providers/open_router.rb +6 -5
  60. data/lib/riffer/providers/repository.rb +0 -4
  61. data/lib/riffer/providers/token_usage.rb +21 -5
  62. data/lib/riffer/skills/adapter.rb +1 -1
  63. data/lib/riffer/skills/config.rb +12 -0
  64. data/lib/riffer/skills/context.rb +4 -7
  65. data/lib/riffer/skills/filesystem_backend.rb +1 -2
  66. data/lib/riffer/skills/frontmatter.rb +4 -4
  67. data/lib/riffer/skills/xml_adapter.rb +1 -1
  68. data/lib/riffer/stream_events/base.rb +1 -1
  69. data/lib/riffer/stream_events/finish_reason_done.rb +2 -2
  70. data/lib/riffer/stream_events/guardrail_modification.rb +1 -1
  71. data/lib/riffer/stream_events/guardrail_tripwire.rb +1 -1
  72. data/lib/riffer/stream_events/interrupt.rb +2 -2
  73. data/lib/riffer/stream_events/reasoning_delta.rb +1 -1
  74. data/lib/riffer/stream_events/reasoning_done.rb +10 -8
  75. data/lib/riffer/stream_events/skill_activation.rb +1 -1
  76. data/lib/riffer/stream_events/text_delta.rb +1 -1
  77. data/lib/riffer/stream_events/text_done.rb +1 -1
  78. data/lib/riffer/stream_events/token_usage_done.rb +1 -1
  79. data/lib/riffer/stream_events/tool_call_delta.rb +3 -3
  80. data/lib/riffer/stream_events/tool_call_done.rb +4 -4
  81. data/lib/riffer/stream_events/web_search_done.rb +2 -2
  82. data/lib/riffer/stream_events/web_search_status.rb +3 -3
  83. data/lib/riffer/tools/response.rb +4 -4
  84. data/lib/riffer/tools/runtime.rb +3 -4
  85. data/lib/riffer/tracing/capture.rb +2 -4
  86. data/lib/riffer/tracing/stream_recorder.rb +6 -6
  87. data/lib/riffer/version.rb +1 -1
  88. data/lib/riffer.rb +7 -0
  89. data/rbs_collection.lock.yaml +320 -0
  90. data/rbs_collection.yaml +12 -0
  91. data/sig/_private/anthropic.rbs +5 -4
  92. data/sig/_private/aws-sdk-core/event_error.rbs +15 -0
  93. data/sig/generated/riffer/agent/config.rbs +19 -5
  94. data/sig/generated/riffer/agent/response.rbs +6 -1
  95. data/sig/generated/riffer/agent/run.rbs +15 -8
  96. data/sig/generated/riffer/agent.rbs +25 -9
  97. data/sig/generated/riffer/config.rbs +1 -1
  98. data/sig/generated/riffer/evals/evaluator.rbs +20 -1
  99. data/sig/generated/riffer/evals/judge.rbs +5 -2
  100. data/sig/generated/riffer/files/resolver.rbs +6 -6
  101. data/sig/generated/riffer/helpers/deep_dup.rbs +21 -0
  102. data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +58 -0
  103. data/sig/generated/riffer/messages/assistant/tool_call.rbs +42 -0
  104. data/sig/generated/riffer/messages/assistant.rbs +24 -10
  105. data/sig/generated/riffer/messages/system.rbs +6 -0
  106. data/sig/generated/riffer/messages/tool.rbs +6 -0
  107. data/sig/generated/riffer/messages/{file_part.rbs → user/file_part.rbs} +11 -6
  108. data/sig/generated/riffer/messages/user.rbs +9 -3
  109. data/sig/generated/riffer/params/param.rbs +7 -0
  110. data/sig/generated/riffer/params.rbs +6 -0
  111. data/sig/generated/riffer/providers/amazon_bedrock.rbs +14 -5
  112. data/sig/generated/riffer/providers/anthropic.rbs +2 -2
  113. data/sig/generated/riffer/providers/base.rbs +22 -10
  114. data/sig/generated/riffer/providers/gemini.rbs +4 -4
  115. data/sig/generated/riffer/providers/mock.rbs +7 -2
  116. data/sig/generated/riffer/providers/open_ai.rbs +4 -4
  117. data/sig/generated/riffer/providers/open_router.rbs +4 -4
  118. data/sig/generated/riffer/providers/repository.rbs +0 -2
  119. data/sig/generated/riffer/providers/token_usage.rbs +6 -0
  120. data/sig/generated/riffer/skills/config.rbs +9 -0
  121. data/sig/generated/riffer/stream_events/reasoning_done.rbs +8 -6
  122. data/sig/generated/riffer/tools/runtime.rbs +2 -2
  123. data/sig/generated/riffer/tracing/capture.rbs +4 -4
  124. data/sig/generated/riffer.rbs +8 -0
  125. data/sig/manual/riffer/helpers/deep_dup.rbs +5 -0
  126. metadata +13 -5
  127. data/sig/_private/async.rbs +0 -28
  128. data/sig/_private/minitest.rbs +0 -9
@@ -7,38 +7,37 @@
7
7
  class Riffer::Agent::Config
8
8
  DEFAULT_MAX_STEPS = 16 #: Integer
9
9
 
10
+ # @rbs @tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?
11
+
10
12
  # The configured agent identifier.
11
- attr_reader :identifier #: String?
13
+ attr_reader :identifier #: String? # @dynamic identifier
12
14
 
13
15
  # The configured model.
14
- attr_reader :model #: (String | Proc)?
16
+ attr_reader :model #: (String | Proc)? # @dynamic model
15
17
 
16
18
  # The configured instructions.
17
- attr_reader :instructions #: (String | Proc)?
19
+ attr_reader :instructions #: (String | Proc)? # @dynamic instructions
18
20
 
19
21
  # Options passed to generate_text/stream_text.
20
- attr_accessor :model_options #: Hash[Symbol, untyped]
22
+ attr_accessor :model_options #: Hash[Symbol, untyped] # @dynamic model_options, model_options=
21
23
 
22
24
  # The configured structured-output schema.
23
- attr_reader :structured_output #: Riffer::Params?
25
+ attr_reader :structured_output #: Riffer::Params? # @dynamic structured_output
24
26
 
25
27
  # The maximum number of LLM call steps in the tool-use loop.
26
- attr_accessor :max_steps #: Numeric?
28
+ attr_accessor :max_steps #: Numeric? # @dynamic max_steps, max_steps=
27
29
 
28
30
  # The configured tools.
29
- attr_accessor :tools_config #: (Array[singleton(Riffer::Tool)] | Proc)?
31
+ attr_accessor :tools_config #: (Array[singleton(Riffer::Tool)] | Proc)? # @dynamic tools_config, tools_config=
30
32
 
31
33
  # The accumulated +use_mcp+ tag configurations.
32
- attr_reader :mcp_configs #: Array[Hash[Symbol, untyped]]
33
-
34
- # The configured tool runtime.
35
- attr_reader :tool_runtime #: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
34
+ attr_reader :mcp_configs #: Array[Hash[Symbol, untyped]] # @dynamic mcp_configs
36
35
 
37
36
  # The configured skills.
38
- attr_accessor :skills_config #: Riffer::Skills::Config?
37
+ attr_accessor :skills_config #: Riffer::Skills::Config? # @dynamic skills_config, skills_config=
39
38
 
40
39
  # Registered guardrail entries keyed by phase.
41
- attr_reader :guardrails #: Hash[Symbol, Array[Hash[Symbol, untyped]]]
40
+ attr_reader :guardrails #: Hash[Symbol, Array[Hash[Symbol, untyped]]] # @dynamic guardrails
42
41
 
43
42
  # Builds a new Config. Raises Riffer::ArgumentError if +model+ or
44
43
  # +instructions+ is invalid (e.g. an empty string).
@@ -52,7 +51,7 @@ class Riffer::Agent::Config
52
51
  # ?max_steps: Numeric?,
53
52
  # ?tools_config: (Array[singleton(Riffer::Tool)] | Proc)?,
54
53
  # ?mcp_configs: Array[Hash[Symbol, untyped]],
55
- # ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc),
54
+ # ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?,
56
55
  # ?skills_config: Riffer::Skills::Config?,
57
56
  # ?guardrails: Hash[Symbol, Array[Hash[Symbol, untyped]]]
58
57
  # ) -> void
@@ -65,7 +64,7 @@ class Riffer::Agent::Config
65
64
  max_steps: DEFAULT_MAX_STEPS,
66
65
  tools_config: nil,
67
66
  mcp_configs: [],
68
- tool_runtime: Riffer.config.tool_runtime,
67
+ tool_runtime: nil,
69
68
  skills_config: nil,
70
69
  guardrails: { before: [], after: [] }
71
70
  )
@@ -79,7 +78,7 @@ class Riffer::Agent::Config
79
78
  self.model = model
80
79
  self.instructions = instructions
81
80
  self.structured_output = structured_output
82
- self.tool_runtime = tool_runtime
81
+ self.tool_runtime = tool_runtime if tool_runtime
83
82
  end
84
83
 
85
84
  # Sets +identifier+, coercing the value to String.
@@ -100,6 +99,15 @@ class Riffer::Agent::Config
100
99
  @structured_output = value
101
100
  end
102
101
 
102
+ # Returns the declared tool runtime, or +Riffer.config.tool_runtime+ when none
103
+ # was declared. Resolving the global here rather than at construction is what
104
+ # lets a copy tell an inherited runtime from a defaulted one.
105
+ #--
106
+ #: () -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
107
+ def tool_runtime
108
+ @tool_runtime || Riffer.config.tool_runtime
109
+ end
110
+
103
111
  # Sets +tool_runtime+. Raises Riffer::ArgumentError on an invalid value.
104
112
  #--
105
113
  #: ((singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
@@ -177,6 +185,22 @@ class Riffer::Agent::Config
177
185
 
178
186
  private
179
187
 
188
+ # +dup+ would leave the copy sharing every collection with this one, so a
189
+ # declaration on either would reach the other. The nesting runs deeper than one
190
+ # level: an mcp entry holds its own +:tags+ array, a guardrail entry its own
191
+ # +:options+ hash, and +model_options+ is arbitrary.
192
+ #--
193
+ #: (Riffer::Agent::Config) -> void
194
+ def initialize_copy(source)
195
+ super
196
+ @model_options = Riffer::Helpers::DeepDup.call(source.model_options)
197
+ @mcp_configs = Riffer::Helpers::DeepDup.call(source.mcp_configs)
198
+ @guardrails = Riffer::Helpers::DeepDup.call(source.guardrails)
199
+ @tools_config = Riffer::Helpers::DeepDup.call(source.tools_config)
200
+ @skills_config = source.skills_config&.dup
201
+ @structured_output = source.structured_output&.dup
202
+ end
203
+
180
204
  #--
181
205
  #: (untyped, String) -> void
182
206
  def validate_string_or_proc!(value, name)
@@ -25,10 +25,10 @@ class Riffer::Agent::Outcome
25
25
  PROVIDER_STOP_REASONS).freeze #: Array[Symbol]
26
26
 
27
27
  # Why the run ended.
28
- attr_reader :reason #: Symbol
28
+ attr_reader :reason #: Symbol # @dynamic reason
29
29
 
30
30
  # Human-readable specifics for +reason+, when there are any.
31
- attr_reader :detail #: String?
31
+ attr_reader :detail #: String? # @dynamic detail
32
32
 
33
33
  # Raises Riffer::ArgumentError when +reason+ is outside VALUES.
34
34
  #--
@@ -13,33 +13,37 @@
13
13
  # end
14
14
  class Riffer::Agent::Response
15
15
  # The response content.
16
- attr_reader :content #: String
16
+ attr_reader :content #: String # @dynamic content
17
17
 
18
18
  # How the run ended.
19
- attr_reader :outcome #: Riffer::Agent::Outcome
19
+ attr_reader :outcome #: Riffer::Agent::Outcome # @dynamic outcome
20
20
 
21
21
  # The tripwire if execution was blocked.
22
- attr_reader :tripwire #: Riffer::Guardrails::Tripwire?
22
+ attr_reader :tripwire #: Riffer::Guardrails::Tripwire? # @dynamic tripwire
23
23
 
24
24
  # The modifications made by guardrails during processing.
25
- attr_reader :modifications #: Array[Riffer::Guardrails::Modification]
25
+ attr_reader :modifications #: Array[Riffer::Guardrails::Modification] # @dynamic modifications
26
+
27
+ # The reasoning parts on the final assistant message, if the provider
28
+ # produced any.
29
+ attr_reader :reasoning #: Array[Riffer::Messages::Assistant::ReasoningPart] # @dynamic reasoning
26
30
 
27
31
  # The parsed structured output, if structured output was configured.
28
- attr_reader :structured_output #: Hash[Symbol, untyped]?
32
+ attr_reader :structured_output #: Hash[Symbol, untyped]? # @dynamic structured_output
29
33
 
30
34
  # The aggregate token usage across this run's LLM calls, if any was reported.
31
- attr_reader :token_usage #: Riffer::Providers::TokenUsage?
35
+ attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
32
36
 
33
37
  # The number of LLM calls made during this run (0 when a before-guardrail
34
38
  # blocks before any call). Distinct from the session's cumulative step count.
35
- attr_reader :steps #: Integer
39
+ attr_reader :steps #: Integer # @dynamic steps
36
40
 
37
41
  # The full message history from the agent conversation.
38
- attr_reader :messages #: Array[Riffer::Messages::Base]
42
+ attr_reader :messages #: Array[Riffer::Messages::Base] # @dynamic messages
39
43
 
40
44
  # Call ids of tool_use blocks riffer filled with placeholder results this
41
45
  # turn (when an interrupt left them unanswered and history healing is on).
42
- attr_reader :healed_tool_call_ids #: Array[String]
46
+ attr_reader :healed_tool_call_ids #: Array[String] # @dynamic healed_tool_call_ids
43
47
 
44
48
  #--
45
49
  #: (
@@ -47,6 +51,7 @@ class Riffer::Agent::Response
47
51
  # outcome: Riffer::Agent::Outcome,
48
52
  # ?tripwire: Riffer::Guardrails::Tripwire?,
49
53
  # ?modifications: Array[Riffer::Guardrails::Modification],
54
+ # ?reasoning: Array[Riffer::Messages::Assistant::ReasoningPart],
50
55
  # ?structured_output: Hash[Symbol, untyped]?,
51
56
  # ?messages: Array[Riffer::Messages::Base],
52
57
  # ?healed_tool_call_ids: Array[String],
@@ -58,6 +63,7 @@ class Riffer::Agent::Response
58
63
  outcome:,
59
64
  tripwire: nil,
60
65
  modifications: [],
66
+ reasoning: [],
61
67
  structured_output: nil,
62
68
  messages: [],
63
69
  healed_tool_call_ids: [],
@@ -68,6 +74,7 @@ class Riffer::Agent::Response
68
74
  @outcome = outcome
69
75
  @tripwire = tripwire
70
76
  @modifications = modifications
77
+ @reasoning = reasoning
71
78
  @structured_output = structured_output
72
79
  @messages = messages
73
80
  @healed_tool_call_ids = healed_tool_call_ids
@@ -10,7 +10,7 @@ module Riffer::Agent::Run
10
10
  # for prompt/files semantics.
11
11
  #
12
12
  #--
13
- #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
13
+ #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
14
14
  def generate(agent:, prompt: nil, files: nil, tags: {})
15
15
  append_user_message(agent, prompt, files: files)
16
16
  run_loop(agent, tags: tags)
@@ -20,7 +20,7 @@ module Riffer::Agent::Run
20
20
  # for prompt/files semantics.
21
21
  #
22
22
  #--
23
- #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
23
+ #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
24
24
  def stream(agent:, prompt: nil, files: nil, tags: {})
25
25
  append_user_message(agent, prompt, files: files)
26
26
  # The enumerator body runs in its own fiber, where the fiber-local OTEL
@@ -36,16 +36,17 @@ module Riffer::Agent::Run
36
36
  private
37
37
 
38
38
  # Both +generate+ and +stream+ funnel here, so this is the single place raw
39
- # +tags+ are normalized. The clean <tt>String => String</tt> map is then
39
+ # +tags+ are normalized and merged over the default tags (a caller tag wins
40
+ # on a shared key). The clean <tt>String => String</tt> map is then
40
41
  # threaded to every span builder in the run as +riffer.tag.*+ and to each
41
42
  # provider call (via +merged_model_options+) for native request-metadata
42
43
  # mapping.
43
44
  #--
44
45
  #: (Riffer::Agent, ?tags: Hash[(String | Symbol), untyped]?, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
45
46
  def run_loop(agent, tags: {}, stream_yielder: nil)
46
- tags = normalize_tags(tags)
47
+ tags = default_tags(agent).merge(normalize_tags(tags))
47
48
  Riffer::Tracing.in_span(
48
- "invoke_agent #{agent.class.identifier}",
49
+ "invoke_agent #{agent.identifier}",
49
50
  attributes: run_span_attributes(agent, tags),
50
51
  kind: :internal,
51
52
  ) do |span|
@@ -153,6 +154,7 @@ module Riffer::Agent::Run
153
154
  def accumulate_streamed_response(agent, stream_yielder, tags = {})
154
155
  accumulated_content = +""
155
156
  accumulated_tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
157
+ accumulated_reasoning = [] #: Array[Riffer::Messages::Assistant::ReasoningPart]
156
158
  accumulated_token_usage = nil #: Riffer::Providers::TokenUsage?
157
159
  accumulated_finish_reason = nil #: Symbol?
158
160
  accumulated_finish_reason_raw = nil #: String?
@@ -175,6 +177,8 @@ module Riffer::Agent::Run
175
177
  name: event.name,
176
178
  arguments: event.arguments,
177
179
  )
180
+ when Riffer::StreamEvents::ReasoningDone
181
+ accumulated_reasoning << event.part
178
182
  when Riffer::StreamEvents::TokenUsageDone
179
183
  accumulated_token_usage = event.token_usage
180
184
  when Riffer::StreamEvents::FinishReasonDone
@@ -186,6 +190,7 @@ module Riffer::Agent::Run
186
190
  Riffer::Messages::Assistant.new(
187
191
  accumulated_content,
188
192
  tool_calls: accumulated_tool_calls,
193
+ reasoning: accumulated_reasoning,
189
194
  token_usage: accumulated_token_usage,
190
195
  finish_reason: accumulated_finish_reason,
191
196
  finish_reason_raw: accumulated_finish_reason_raw,
@@ -228,6 +233,7 @@ module Riffer::Agent::Run
228
233
  message&.content || "",
229
234
  outcome: final_outcome(message, result, interrupted: interrupted, interrupt_reason: interrupt_reason),
230
235
  modifications: all_modifications,
236
+ reasoning: message&.reasoning || [],
231
237
  structured_output: result&.object,
232
238
  **extra,
233
239
  )
@@ -381,6 +387,13 @@ module Riffer::Agent::Run
381
387
  opts
382
388
  end
383
389
 
390
+ # The tags riffer adds to every run, identifying the agent it's on behalf of.
391
+ #--
392
+ #: (Riffer::Agent) -> Hash[String, String]
393
+ def default_tags(agent)
394
+ { "kind" => "agent", "agent" => agent.identifier }
395
+ end
396
+
384
397
  #--
385
398
  #: (
386
399
  # Riffer::Agent,
@@ -388,6 +401,7 @@ module Riffer::Agent::Run
388
401
  # outcome: Riffer::Agent::Outcome,
389
402
  # ?tripwire: Riffer::Guardrails::Tripwire?,
390
403
  # ?modifications: Array[Riffer::Guardrails::Modification],
404
+ # ?reasoning: Array[Riffer::Messages::Assistant::ReasoningPart],
391
405
  # ?structured_output: Hash[Symbol, untyped]?,
392
406
  # ?healed_tool_call_ids: Array[String],
393
407
  # ?token_usage: Riffer::Providers::TokenUsage?,
@@ -399,6 +413,7 @@ module Riffer::Agent::Run
399
413
  outcome:,
400
414
  tripwire: nil,
401
415
  modifications: [],
416
+ reasoning: [],
402
417
  structured_output: nil,
403
418
  healed_tool_call_ids: [],
404
419
  token_usage: nil,
@@ -410,6 +425,7 @@ module Riffer::Agent::Run
410
425
  outcome: outcome,
411
426
  tripwire: tripwire,
412
427
  modifications: modifications,
428
+ reasoning: reasoning,
413
429
  structured_output: structured_output,
414
430
  messages: messages.frozen? ? messages : messages.dup.freeze,
415
431
  healed_tool_call_ids: healed_tool_call_ids,
@@ -421,12 +437,12 @@ module Riffer::Agent::Run
421
437
  # Raises when +files+ are supplied without a +prompt+ — the provider needs
422
438
  # text to anchor the attachments.
423
439
  #--
424
- #: (Riffer::Agent, String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> void
440
+ #: (Riffer::Agent, String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?) -> void
425
441
  def append_user_message(agent, prompt, files: nil)
426
442
  raise Riffer::ArgumentError, "files: requires a prompt" if files && !files.empty? && prompt.nil?
427
443
  return unless prompt
428
444
 
429
- file_parts = (files || []).map { |f| Riffer::Messages::FilePart.from_hash(f) }
445
+ file_parts = (files || []).map { |f| Riffer::Messages::User::FilePart.from_hash(f) }
430
446
  agent.session.add(Riffer::Messages::User.new(prompt, files: file_parts), silent: true)
431
447
  end
432
448
 
@@ -451,7 +467,7 @@ module Riffer::Agent::Run
451
467
  def run_span_attributes(agent, tags = {})
452
468
  {
453
469
  "gen_ai.operation.name" => "invoke_agent",
454
- "gen_ai.agent.name" => agent.class.identifier,
470
+ "gen_ai.agent.name" => agent.identifier,
455
471
  "gen_ai.provider.name" => agent.provider.class.semconv_provider_name,
456
472
  "gen_ai.request.model" => agent.model_name,
457
473
  }.merge(tag_attributes(tags))
@@ -461,7 +477,6 @@ module Riffer::Agent::Run
461
477
  #: (Hash[(String | Symbol), untyped]?) -> Hash[String, String]
462
478
  def normalize_tags(tags)
463
479
  return {} if tags.nil?
464
- raise Riffer::ArgumentError, "tags: must be a Hash, got #{tags.class}" unless tags.is_a?(Hash)
465
480
 
466
481
  result = {} #: Hash[String, String]
467
482
  tags.each do |key, value|
@@ -18,7 +18,7 @@ class Riffer::Agent::Session
18
18
  # @rbs @callbacks: Array[^(Riffer::Messages::Base) -> void]
19
19
 
20
20
  # The message history.
21
- attr_reader :messages #: Array[Riffer::Messages::Base]
21
+ attr_reader :messages #: Array[Riffer::Messages::Base] # @dynamic messages
22
22
 
23
23
  #--
24
24
  #: (?messages: Array[Riffer::Messages::Base]) -> void
@@ -201,6 +201,7 @@ class Riffer::Agent::Session
201
201
  attrs.fetch(:content, old.content),
202
202
  id: attrs.fetch(:id, old.id),
203
203
  tool_calls: attrs.fetch(:tool_calls, old.tool_calls),
204
+ reasoning: attrs.fetch(:reasoning, old.reasoning),
204
205
  token_usage: attrs.fetch(:token_usage, old.token_usage),
205
206
  structured_output: attrs.fetch(:structured_output, old.structured_output),
206
207
  finish_reason: attrs.fetch(:finish_reason, old.finish_reason),
@@ -4,10 +4,10 @@
4
4
  # Wraps the result of structured output parsing and validation.
5
5
  class Riffer::Agent::StructuredOutput::Result
6
6
  # The validated object, or +nil+ on failure.
7
- attr_reader :object #: Hash[Symbol, untyped]?
7
+ attr_reader :object #: Hash[Symbol, untyped]? # @dynamic object
8
8
 
9
9
  # The error message, or +nil+ on success.
10
- attr_reader :error #: String?
10
+ attr_reader :error #: String? # @dynamic error
11
11
 
12
12
  #--
13
13
  #: (?object: Hash[Symbol, untyped]?, ?error: String?) -> void
@@ -7,7 +7,7 @@ require "json"
7
7
  # schema.
8
8
  class Riffer::Agent::StructuredOutput
9
9
  # The schema parameters.
10
- attr_reader :params #: Riffer::Params
10
+ attr_reader :params #: Riffer::Params # @dynamic params
11
11
 
12
12
  #--
13
13
  #: (Riffer::Params) -> void
data/lib/riffer/agent.rb CHANGED
@@ -28,6 +28,19 @@ class Riffer::Agent
28
28
  @config ||= Riffer::Agent::Config.new
29
29
  end
30
30
 
31
+ # +identifier+ is cleared because it is configuration here but identity on the
32
+ # subclass; two classes claiming one raise Riffer::DuplicateIdentifierError at
33
+ # the next registry lookup.
34
+ #--
35
+ #: (Class) -> void
36
+ def self.inherited(subclass)
37
+ super
38
+ copy = config.dup
39
+ copy.identifier = nil
40
+ subclass.instance_variable_set(:@config, copy)
41
+ end
42
+ private_class_method :inherited
43
+
31
44
  # Gets or sets the agent identifier.
32
45
  #
33
46
  #--
@@ -148,14 +161,14 @@ class Riffer::Agent
148
161
 
149
162
  # Generates a response using a new agent instance.
150
163
  #--
151
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
164
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
152
165
  def self.generate(prompt = nil, files: nil, context: nil, tags: {})
153
166
  new(context: context).generate(prompt, files: files, tags: tags)
154
167
  end
155
168
 
156
169
  # Streams a response using a new agent instance.
157
170
  #--
158
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
171
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
159
172
  def self.stream(prompt = nil, files: nil, context: nil, tags: {})
160
173
  new(context: context).stream(prompt, files: files, tags: tags)
161
174
  end
@@ -214,44 +227,44 @@ class Riffer::Agent
214
227
  end
215
228
 
216
229
  # The conversation handle.
217
- attr_reader :session #: Riffer::Agent::Session
230
+ attr_reader :session #: Riffer::Agent::Session # @dynamic session
218
231
 
219
232
  # The per-instance Riffer::Agent::Config.
220
- attr_reader :config #: Riffer::Agent::Config
233
+ attr_reader :config #: Riffer::Agent::Config # @dynamic config
221
234
 
222
235
  # The system message built from the configured +instructions+, or +nil+ when
223
236
  # none are configured.
224
- attr_reader :instruction_message #: Riffer::Messages::System?
237
+ attr_reader :instruction_message #: Riffer::Messages::System? # @dynamic instruction_message
225
238
 
226
239
  # The system message describing the configured skills catalog, or +nil+ when
227
240
  # skills are unconfigured or the catalog is empty.
228
- attr_reader :skills_message #: Riffer::Messages::System?
241
+ attr_reader :skills_message #: Riffer::Messages::System? # @dynamic skills_message
229
242
 
230
243
  # The mutable runtime context shared with every +Riffer::Agent::Run+ this
231
244
  # agent executes and threaded through all Proc-based settings.
232
- attr_reader :context #: Riffer::Agent::Context
245
+ attr_reader :context #: Riffer::Agent::Context # @dynamic context
233
246
 
234
247
  # The resolved provider name (the part before "/" in the model string),
235
248
  # e.g. +"openai"+.
236
- attr_reader :provider_name #: String
249
+ attr_reader :provider_name #: String # @dynamic provider_name
237
250
 
238
251
  # The resolved model name (the part after "/" in the model string), used as
239
252
  # the model argument on every LLM call.
240
- attr_reader :model_name #: String
253
+ attr_reader :model_name #: String # @dynamic model_name
241
254
 
242
255
  # The provider client. Public so tests can pre-queue responses on
243
256
  # +Riffer::Providers::Mock+ before calling +#generate+.
244
- attr_reader :provider #: Riffer::Providers::Base
257
+ attr_reader :provider #: Riffer::Providers::Base # @dynamic provider
245
258
 
246
259
  # The +Riffer::Agent::StructuredOutput+ wrapping the configured schema, or
247
260
  # +nil+ when not configured.
248
- attr_reader :structured_output #: Riffer::Agent::StructuredOutput?
261
+ attr_reader :structured_output #: Riffer::Agent::StructuredOutput? # @dynamic structured_output
249
262
 
250
263
  # The tool classes the LLM sees on every call this agent makes.
251
- attr_reader :tools #: Array[singleton(Riffer::Tool)]
264
+ attr_reader :tools #: Array[singleton(Riffer::Tool)] # @dynamic tools
252
265
 
253
266
  # The tool runtime instance used to execute tool calls.
254
- attr_reader :tool_runtime #: Riffer::Tools::Runtime
267
+ attr_reader :tool_runtime #: Riffer::Tools::Runtime # @dynamic tool_runtime
255
268
 
256
269
  # Initializes a new agent.
257
270
  #
@@ -284,6 +297,16 @@ class Riffer::Agent
284
297
  @session.set(Riffer::Agent::Session::Repair.prune_orphans(@session.messages))
285
298
  end
286
299
 
300
+ # The identifier for this agent: the instance config's identifier when set
301
+ # (e.g. an agent restored with +Riffer::Agent.from_h+), otherwise the class
302
+ # identifier.
303
+ #
304
+ #--
305
+ #: () -> String
306
+ def identifier
307
+ config.identifier || self.class.identifier
308
+ end
309
+
287
310
  # Generates a response from the agent.
288
311
  #
289
312
  # With +prompt+, a new user message is appended (silently — +on_message+ does
@@ -296,10 +319,11 @@ class Riffer::Agent
296
319
  # are stamped as +riffer.tag.*+ on every span the call emits. See
297
320
  # +docs/AGENTS.md+ for the per-provider mapping. The reserved key
298
321
  # +user_id+ also maps to the provider's native user identifier where one
299
- # exists.
322
+ # exists. Riffer adds the default tags +kind+ (+"agent"+) and +agent+ (the
323
+ # agent identifier); a caller tag with the same key wins.
300
324
  #
301
325
  #--
302
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
326
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
303
327
  def generate(prompt = nil, files: nil, tags: {})
304
328
  Riffer::Agent::Run.generate(agent: self, prompt: prompt, files: files, tags: tags)
305
329
  end
@@ -311,7 +335,7 @@ class Riffer::Agent
311
335
  # Raises Riffer::ArgumentError if structured output is configured.
312
336
  #
313
337
  #--
314
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
338
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
315
339
  def stream(prompt = nil, files: nil, tags: {})
316
340
  if @structured_output
317
341
  raise Riffer::ArgumentError,
data/lib/riffer/config.rb CHANGED
@@ -16,11 +16,11 @@ class Riffer::Config
16
16
  class Skills
17
17
  # The tool class the LLM calls to activate a skill; defaults to
18
18
  # <tt>Riffer::Skills::ActivateTool</tt>.
19
- attr_reader :default_activate_tool #: singleton(Riffer::Tool)
19
+ attr_reader :default_activate_tool #: singleton(Riffer::Tool) # @dynamic default_activate_tool
20
20
 
21
21
  # Default skills backend for agents that declare a +skills+ block without
22
22
  # one; defaults to +nil+.
23
- attr_reader :default_backend #: (Riffer::Skills::Backend | Proc)?
23
+ attr_reader :default_backend #: (Riffer::Skills::Backend | Proc)? # @dynamic default_backend
24
24
 
25
25
  #--
26
26
  #: () -> void
@@ -61,17 +61,17 @@ class Riffer::Config
61
61
  class Tracing
62
62
  # Whether riffer emits OTEL spans; defaults to +true+, a no-op until a
63
63
  # host wires an OTEL SDK.
64
- attr_reader :enabled #: bool
64
+ attr_reader :enabled #: bool # @dynamic enabled
65
65
 
66
66
  # Whether LLM-call spans capture full message content
67
67
  # (<tt>gen_ai.input.messages</tt>, <tt>gen_ai.output.messages</tt>,
68
68
  # <tt>gen_ai.system_instructions</tt>); defaults to +false+ — message
69
69
  # content routinely carries sensitive data.
70
- attr_reader :capture_messages #: bool
70
+ attr_reader :capture_messages #: bool # @dynamic capture_messages
71
71
 
72
72
  # The backend riffer routes spans through; defaults to +nil+, a no-op.
73
73
  # Riffer auto-detects no backend; assigning one is opt-in.
74
- attr_reader :backend #: untyped
74
+ attr_reader :backend #: untyped # @dynamic backend
75
75
 
76
76
  #--
77
77
  #: () -> void
@@ -115,20 +115,20 @@ class Riffer::Config
115
115
  end
116
116
  end
117
117
 
118
- # File-attachment-download policy for +Riffer::Messages::FilePart+ URL sources
118
+ # File-attachment-download policy for +Riffer::Messages::User::FilePart+ URL sources
119
119
  class Files
120
120
  # Allow file attachments to be downloaded to send to providers.
121
- attr_reader :allow_downloads #: bool
121
+ attr_reader :allow_downloads #: bool # @dynamic allow_downloads
122
122
  # Maximum file size to download before failing.
123
- attr_reader :max_bytes #: Integer
123
+ attr_reader :max_bytes #: Integer # @dynamic max_bytes
124
124
  # Maximum amount of time to spend downloading a file before failing.
125
- attr_reader :timeout #: Integer
125
+ attr_reader :timeout #: Integer # @dynamic timeout
126
126
  # Maximum number of files to include in an individual message.
127
- attr_reader :max_per_message #: Integer?
127
+ attr_reader :max_per_message #: Integer? # @dynamic max_per_message
128
128
  # Execution pattern for downloading files.
129
- attr_reader :runner #: Riffer::Runner
129
+ attr_reader :runner #: Riffer::Runner # @dynamic runner
130
130
  # The object used to fetch a URL source's bytes
131
- attr_reader :downloader #: untyped
131
+ attr_reader :downloader #: untyped # @dynamic downloader
132
132
 
133
133
  #--
134
134
  #: () -> void
@@ -215,16 +215,16 @@ class Riffer::Config
215
215
  # and +cache_write+ fall back to the +input+ rate when unset.
216
216
  class Rates
217
217
  # Input rate per million tokens.
218
- attr_reader :input #: Float
218
+ attr_reader :input #: Float # @dynamic input
219
219
 
220
220
  # Output rate per million tokens.
221
- attr_reader :output #: Float
221
+ attr_reader :output #: Float # @dynamic output
222
222
 
223
223
  # Cache-read rate per million tokens.
224
- attr_reader :cache_read #: Float?
224
+ attr_reader :cache_read #: Float? # @dynamic cache_read
225
225
 
226
226
  # Cache-write rate per million tokens.
227
- attr_reader :cache_write #: Float?
227
+ attr_reader :cache_write #: Float? # @dynamic cache_write
228
228
 
229
229
  #--
230
230
  #: (input: Float, output: Float, ?cache_read: Float?, ?cache_write: Float?) -> void
@@ -329,34 +329,34 @@ class Riffer::Config
329
329
  VALID_MESSAGE_ID_STRATEGIES = %i[none uuid uuidv7].freeze
330
330
 
331
331
  # Amazon Bedrock configuration.
332
- attr_reader :amazon_bedrock #: Riffer::Config::AmazonBedrock
332
+ attr_reader :amazon_bedrock #: Riffer::Config::AmazonBedrock # @dynamic amazon_bedrock
333
333
 
334
334
  # Anthropic configuration.
335
- attr_reader :anthropic #: Riffer::Config::Anthropic
335
+ attr_reader :anthropic #: Riffer::Config::Anthropic # @dynamic anthropic
336
336
 
337
337
  # Azure OpenAI configuration.
338
- attr_reader :azure_openai #: Riffer::Config::AzureOpenAI
338
+ attr_reader :azure_openai #: Riffer::Config::AzureOpenAI # @dynamic azure_openai
339
339
 
340
340
  # Google Gemini configuration.
341
- attr_reader :gemini #: Riffer::Config::Gemini
341
+ attr_reader :gemini #: Riffer::Config::Gemini # @dynamic gemini
342
342
 
343
343
  # OpenAI configuration.
344
- attr_reader :openai #: Riffer::Config::OpenAI
344
+ attr_reader :openai #: Riffer::Config::OpenAI # @dynamic openai
345
345
 
346
346
  # OpenRouter configuration.
347
- attr_reader :openrouter #: Riffer::Config::OpenRouter
347
+ attr_reader :openrouter #: Riffer::Config::OpenRouter # @dynamic openrouter
348
348
 
349
349
  # Evals configuration.
350
- attr_reader :evals #: Riffer::Config::Evals
350
+ attr_reader :evals #: Riffer::Config::Evals # @dynamic evals
351
351
 
352
352
  # MCP configuration. +credentials+ is an optional Proc returning per-run
353
353
  # +tools/call+ headers (or +nil+ to deny); +discovery_runner+ runs tool
354
354
  # discovery.
355
- attr_reader :mcp #: Riffer::Config::Mcp
355
+ attr_reader :mcp #: Riffer::Config::Mcp # @dynamic mcp
356
356
 
357
357
  # Global tool runtime configuration (experimental); defaults to
358
358
  # <tt>Riffer::Tools::Runtime::Inline.new</tt>.
359
- attr_reader :tool_runtime #: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
359
+ attr_reader :tool_runtime #: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc) # @dynamic tool_runtime
360
360
 
361
361
  # Sets the global tool runtime. Raises Riffer::ArgumentError on an invalid
362
362
  # value.
@@ -374,20 +374,20 @@ class Riffer::Config
374
374
  end
375
375
 
376
376
  # Skills-related global configuration.
377
- attr_reader :skills #: Riffer::Config::Skills
377
+ attr_reader :skills #: Riffer::Config::Skills # @dynamic skills
378
378
 
379
379
  # Tracing-related global configuration.
380
- attr_reader :tracing #: Riffer::Config::Tracing
380
+ attr_reader :tracing #: Riffer::Config::Tracing # @dynamic tracing
381
381
 
382
- attr_reader :files #: Riffer::Config::Files
382
+ attr_reader :files #: Riffer::Config::Files # @dynamic files
383
383
 
384
384
  # Consumer-configured per-model token pricing.
385
- attr_reader :pricing #: Riffer::Config::Pricing
385
+ attr_reader :pricing #: Riffer::Config::Pricing # @dynamic pricing
386
386
 
387
387
  # Strategy for auto-generating message ids: +:none+ (default), +:uuid+, or
388
388
  # +:uuidv7+. When not +:none+, messages get an +id+ at construction, and
389
389
  # seeded messages passed to +Riffer::Agent#generate+ must carry their own.
390
- attr_reader :message_id_strategy #: Symbol
390
+ attr_reader :message_id_strategy #: Symbol # @dynamic message_id_strategy
391
391
 
392
392
  # Sets the message id strategy. Raises Riffer::ArgumentError unless the value
393
393
  # is +:none+, +:uuid+, or +:uuidv7+.
@@ -404,7 +404,7 @@ class Riffer::Config
404
404
  # Experimental: when +true+, riffer maintains the +tool_use+ ↔ +tool_result+
405
405
  # invariant itself — stripping orphaned exchanges and filling interrupted
406
406
  # ones. Defaults to +false+; the surface may change without notice.
407
- attr_reader :experimental_history_healing #: bool
407
+ attr_reader :experimental_history_healing #: bool # @dynamic experimental_history_healing
408
408
 
409
409
  # Sets the +experimental_history_healing+ flag, coercing boolean-ish values so
410
410
  # an env-var +"false"+ (truthy in Ruby) doesn't silently enable healing.