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.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/architecture.md +18 -11
  3. data/.agents/code-style.md +1 -1
  4. data/.agents/rbs-inline.md +2 -2
  5. data/.agents/testing.md +9 -5
  6. data/.release-please-manifest.json +1 -1
  7. data/AGENTS.md +17 -10
  8. data/CHANGELOG.md +31 -0
  9. data/README.md +17 -18
  10. data/Steepfile +7 -1
  11. data/docs/03_AGENTS.md +34 -3
  12. data/docs/04_AGENT_LIFECYCLE.md +134 -86
  13. data/docs/05_AGENT_LOOP.md +2 -2
  14. data/docs/06_TOOLS.md +9 -4
  15. data/docs/07_TOOL_ADVANCED.md +23 -19
  16. data/docs/08_MESSAGES.md +28 -31
  17. data/docs/09_STREAM_EVENTS.md +1 -1
  18. data/docs/10_CONFIGURATION.md +25 -15
  19. data/docs/providers/01_PROVIDERS.md +6 -0
  20. data/docs/providers/06_MOCK_PROVIDER.md +2 -1
  21. data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
  22. data/docs/providers/08_GEMINI.md +2 -2
  23. data/docs/providers/09_OPENROUTER.md +242 -0
  24. data/lib/riffer/agent/config.rb +173 -0
  25. data/lib/riffer/agent/context.rb +125 -0
  26. data/lib/riffer/agent/response.rb +11 -2
  27. data/lib/riffer/agent/run.rb +308 -0
  28. data/lib/riffer/agent/session/repair.rb +112 -0
  29. data/lib/riffer/agent/session.rb +268 -0
  30. data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
  31. data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
  32. data/lib/riffer/agent.rb +246 -684
  33. data/lib/riffer/config.rb +56 -7
  34. data/lib/riffer/evals/evaluator.rb +13 -3
  35. data/lib/riffer/evals/judge.rb +2 -2
  36. data/lib/riffer/evals/run_result.rb +2 -1
  37. data/lib/riffer/evals/scenario_result.rb +2 -1
  38. data/lib/riffer/guardrails/runner.rb +3 -2
  39. data/lib/riffer/helpers/call_or_value.rb +16 -0
  40. data/lib/riffer/helpers.rb +0 -1
  41. data/lib/riffer/mcp/authenticated_tool.rb +4 -0
  42. data/lib/riffer/mcp/client.rb +1 -1
  43. data/lib/riffer/mcp/registration.rb +2 -3
  44. data/lib/riffer/mcp/registry.rb +3 -1
  45. data/lib/riffer/mcp/tool_factory.rb +5 -0
  46. data/lib/riffer/messages/assistant.rb +9 -3
  47. data/lib/riffer/messages/base.rb +22 -0
  48. data/lib/riffer/messages/converter.rb +6 -6
  49. data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
  50. data/lib/riffer/messages/tool.rb +1 -1
  51. data/lib/riffer/messages/user.rb +4 -4
  52. data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
  53. data/lib/riffer/{param.rb → params/param.rb} +6 -6
  54. data/lib/riffer/params.rb +27 -21
  55. data/lib/riffer/providers/amazon_bedrock.rb +19 -20
  56. data/lib/riffer/providers/anthropic.rb +27 -28
  57. data/lib/riffer/providers/base.rb +10 -9
  58. data/lib/riffer/providers/gemini.rb +15 -12
  59. data/lib/riffer/providers/mock.rb +41 -13
  60. data/lib/riffer/providers/open_ai.rb +24 -22
  61. data/lib/riffer/providers/open_router.rb +318 -0
  62. data/lib/riffer/providers/repository.rb +1 -0
  63. data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
  64. data/lib/riffer/providers.rb +1 -0
  65. data/lib/riffer/runner/fibers.rb +4 -3
  66. data/lib/riffer/runner/sequential.rb +1 -1
  67. data/lib/riffer/runner/threaded.rb +1 -1
  68. data/lib/riffer/runner.rb +1 -1
  69. data/lib/riffer/skills/activate_tool.rb +4 -3
  70. data/lib/riffer/skills/config.rb +1 -1
  71. data/lib/riffer/skills/context.rb +3 -3
  72. data/lib/riffer/skills/filesystem_backend.rb +7 -5
  73. data/lib/riffer/skills/markdown_adapter.rb +1 -1
  74. data/lib/riffer/skills/xml_adapter.rb +1 -1
  75. data/lib/riffer/stream_events/interrupt.rb +10 -3
  76. data/lib/riffer/stream_events/token_usage_done.rb +2 -2
  77. data/lib/riffer/stream_events/web_search_status.rb +1 -1
  78. data/lib/riffer/tool.rb +3 -3
  79. data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
  80. data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
  81. data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
  82. data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +21 -15
  83. data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
  84. data/lib/riffer/version.rb +1 -1
  85. data/lib/riffer.rb +2 -1
  86. data/sig/generated/riffer/agent/config.rbs +119 -0
  87. data/sig/generated/riffer/agent/context.rbs +91 -0
  88. data/sig/generated/riffer/agent/response.rbs +10 -2
  89. data/sig/generated/riffer/agent/run.rbs +144 -0
  90. data/sig/generated/riffer/agent/session/repair.rbs +51 -0
  91. data/sig/generated/riffer/agent/session.rbs +145 -0
  92. data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
  93. data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
  94. data/sig/generated/riffer/agent.rbs +154 -225
  95. data/sig/generated/riffer/config.rbs +50 -5
  96. data/sig/generated/riffer/evals/judge.rbs +2 -2
  97. data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
  98. data/sig/generated/riffer/helpers.rbs +0 -1
  99. data/sig/generated/riffer/messages/assistant.rbs +7 -3
  100. data/sig/generated/riffer/messages/base.rbs +18 -0
  101. data/sig/generated/riffer/messages/converter.rbs +4 -4
  102. data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
  103. data/sig/generated/riffer/messages/user.rbs +4 -4
  104. data/sig/generated/riffer/params/boolean.rbs +10 -0
  105. data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
  106. data/sig/generated/riffer/params.rbs +15 -15
  107. data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
  108. data/sig/generated/riffer/providers/anthropic.rbs +4 -4
  109. data/sig/generated/riffer/providers/base.rbs +10 -10
  110. data/sig/generated/riffer/providers/gemini.rbs +4 -4
  111. data/sig/generated/riffer/providers/mock.rbs +25 -5
  112. data/sig/generated/riffer/providers/open_ai.rbs +4 -4
  113. data/sig/generated/riffer/providers/open_router.rbs +85 -0
  114. data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
  115. data/sig/generated/riffer/providers.rbs +1 -0
  116. data/sig/generated/riffer/runner/fibers.rbs +2 -2
  117. data/sig/generated/riffer/runner/sequential.rbs +2 -2
  118. data/sig/generated/riffer/runner/threaded.rbs +2 -2
  119. data/sig/generated/riffer/runner.rbs +2 -2
  120. data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
  121. data/sig/generated/riffer/skills/config.rbs +1 -1
  122. data/sig/generated/riffer/skills/context.rbs +2 -2
  123. data/sig/generated/riffer/stream_events/interrupt.rbs +7 -2
  124. data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
  125. data/sig/generated/riffer/tool.rbs +5 -5
  126. data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
  127. data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
  128. data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
  129. data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +19 -13
  130. data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
  131. data/sig/stubs/agent_ivars.rbs +7 -0
  132. data/sig/stubs/async.rbs +24 -0
  133. data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
  134. data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
  135. data/sig/stubs/extend_self.rbs +11 -0
  136. data/sig/stubs/lib_ivars.rbs +101 -0
  137. data/sig/stubs/mcp_sdk.rbs +22 -0
  138. data/sig/stubs/provider_ivars.rbs +36 -0
  139. data/sig/stubs/provider_sdk_methods.rbs +50 -0
  140. data/sig/stubs/zeitwerk.rbs +12 -0
  141. metadata +54 -33
  142. data/lib/riffer/core.rb +0 -28
  143. data/lib/riffer/helpers/validations.rb +0 -18
  144. data/sig/generated/riffer/boolean.rbs +0 -10
  145. data/sig/generated/riffer/core.rbs +0 -19
  146. data/sig/generated/riffer/helpers/validations.rbs +0 -12
@@ -31,8 +31,9 @@ class Riffer::Skills::FilesystemBackend < Riffer::Skills::Backend
31
31
  #--
32
32
  #: () -> Array[Riffer::Skills::Frontmatter]
33
33
  def list_skills
34
- @skills_cache = {}
35
- frontmatters = []
34
+ cache = {} #: Hash[String, String]
35
+ @skills_cache = cache
36
+ frontmatters = [] #: Array[Riffer::Skills::Frontmatter]
36
37
 
37
38
  @paths.each do |path|
38
39
  next unless File.directory?(path)
@@ -45,10 +46,10 @@ class Riffer::Skills::FilesystemBackend < Riffer::Skills::Backend
45
46
  frontmatter = Riffer::Skills::Frontmatter.parse_frontmatter(File.read(skill_file))
46
47
 
47
48
  validate_dirname_matches_name!(dirname, frontmatter.name)
48
- next if @skills_cache.key?(frontmatter.name)
49
+ next if cache.key?(frontmatter.name)
49
50
 
50
51
  frontmatters << frontmatter
51
- @skills_cache[frontmatter.name] = dir
52
+ cache[frontmatter.name] = dir
52
53
  end
53
54
  end
54
55
 
@@ -65,7 +66,8 @@ class Riffer::Skills::FilesystemBackend < Riffer::Skills::Backend
65
66
  #: (String) -> String
66
67
  def read_skill(name)
67
68
  list_skills unless @skills_cache
68
- dir = @skills_cache[name]
69
+ cache = @skills_cache #: Hash[String, String]
70
+ dir = cache[name]
69
71
  raise Riffer::ArgumentError, "Skill not found: '#{name}'" unless dir
70
72
 
71
73
  _, body = Riffer::Skills::Frontmatter.parse(File.read(File.join(dir, SKILL_FILENAME)))
@@ -15,7 +15,7 @@ class Riffer::Skills::MarkdownAdapter < Riffer::Skills::Adapter
15
15
  #--
16
16
  #: (Array[Riffer::Skills::Frontmatter]) -> String
17
17
  def render_catalog(skills)
18
- lines = []
18
+ lines = [] #: Array[String]
19
19
  lines << "## Available Skills"
20
20
  lines << ""
21
21
  lines << "When a user's request matches a skill description below, call the `#{skill_activate_tool.name}` tool with the skill name. After activation, follow the skill's instructions."
@@ -16,7 +16,7 @@ class Riffer::Skills::XmlAdapter < Riffer::Skills::Adapter
16
16
  #--
17
17
  #: (Array[Riffer::Skills::Frontmatter]) -> String
18
18
  def render_catalog(skills)
19
- lines = []
19
+ lines = [] #: Array[String]
20
20
  lines << "When a user's request matches a skill description below, call the `#{skill_activate_tool.name}` tool with the skill name. After activation, follow the skill's instructions."
21
21
  lines << ""
22
22
  lines << "<available_skills>"
@@ -8,11 +8,17 @@ class Riffer::StreamEvents::Interrupt < Riffer::StreamEvents::Base
8
8
  # The reason provided with the interrupt, if any.
9
9
  attr_reader :reason #: (String | Symbol)?
10
10
 
11
+ # Call ids of tool_use blocks that riffer filled with placeholder
12
+ # results when the interrupt fired. Populated only when
13
+ # +Riffer.config.experimental_history_healing+ is on.
14
+ attr_reader :healed_tool_call_ids #: Array[String]
15
+
11
16
  #--
12
- #: (?reason: (String | Symbol)?) -> void
13
- def initialize(reason: nil)
17
+ #: (?reason: (String | Symbol)?, ?healed_tool_call_ids: Array[String]) -> void
18
+ def initialize(reason: nil, healed_tool_call_ids: [])
14
19
  super(role: :system)
15
20
  @reason = reason
21
+ @healed_tool_call_ids = healed_tool_call_ids
16
22
  end
17
23
 
18
24
  # Converts the event to a hash.
@@ -20,8 +26,9 @@ class Riffer::StreamEvents::Interrupt < Riffer::StreamEvents::Base
20
26
  #--
21
27
  #: () -> Hash[Symbol, untyped]
22
28
  def to_h
23
- h = {role: @role, interrupt: true}
29
+ h = {role: @role, interrupt: true} #: Hash[Symbol, untyped]
24
30
  h[:reason] = @reason if @reason
31
+ h[:healed_tool_call_ids] = @healed_tool_call_ids unless @healed_tool_call_ids.empty?
25
32
  h
26
33
  end
27
34
  end
@@ -11,10 +11,10 @@
11
11
  #
12
12
  class Riffer::StreamEvents::TokenUsageDone < Riffer::StreamEvents::Base
13
13
  # The token usage data for this response.
14
- attr_reader :token_usage #: Riffer::TokenUsage
14
+ attr_reader :token_usage #: Riffer::Providers::TokenUsage
15
15
 
16
16
  #--
17
- #: (token_usage: Riffer::TokenUsage, ?role: Symbol) -> void
17
+ #: (token_usage: Riffer::Providers::TokenUsage, ?role: Symbol) -> void
18
18
  def initialize(token_usage:, role: :assistant)
19
19
  super(role: role)
20
20
  @token_usage = token_usage
@@ -26,7 +26,7 @@ class Riffer::StreamEvents::WebSearchStatus < Riffer::StreamEvents::Base
26
26
  #--
27
27
  #: () -> Hash[Symbol, untyped]
28
28
  def to_h
29
- h = {role: @role, status: @status}
29
+ h = {role: @role, status: @status} #: Hash[Symbol, untyped]
30
30
  h[:url] = @url if @url
31
31
  h[:query] = @query if @query
32
32
  h
data/lib/riffer/tool.rb CHANGED
@@ -24,7 +24,7 @@ require "timeout"
24
24
  # end
25
25
  #
26
26
  class Riffer::Tool
27
- extend Riffer::Toolable
27
+ extend Riffer::Tools::Toolable
28
28
 
29
29
  kind :tool
30
30
 
@@ -33,7 +33,7 @@ class Riffer::Tool
33
33
  # Raises NotImplementedError if not implemented by subclass.
34
34
  #
35
35
  #--
36
- #: (context: Hash[Symbol, untyped]?, **untyped) -> Riffer::Tools::Response
36
+ #: (context: Riffer::Agent::Context?, **untyped) -> Riffer::Tools::Response
37
37
  def call(context:, **kwargs)
38
38
  raise NotImplementedError, "#{self.class} must implement #call"
39
39
  end
@@ -69,7 +69,7 @@ class Riffer::Tool
69
69
  # Raises Riffer::Error if the tool does not return a Response object.
70
70
  #
71
71
  #--
72
- #: (context: Hash[Symbol, untyped]?, **untyped) -> Riffer::Tools::Response
72
+ #: (context: Riffer::Agent::Context?, **untyped) -> Riffer::Tools::Response
73
73
  def call_with_validation(context:, **kwargs)
74
74
  params_builder = self.class.params
75
75
  validated_args = params_builder ? params_builder.validate(kwargs) : kwargs
@@ -4,10 +4,10 @@
4
4
  # Executes tool calls concurrently using fibers via the +async+ gem.
5
5
  #
6
6
  # class MyAgent < Riffer::Agent
7
- # tool_runtime Riffer::ToolRuntime::Fibers
7
+ # tool_runtime Riffer::Tools::Runtime::Fibers
8
8
  # end
9
9
  #
10
- class Riffer::ToolRuntime::Fibers < Riffer::ToolRuntime
10
+ class Riffer::Tools::Runtime::Fibers < Riffer::Tools::Runtime
11
11
  # [max_concurrency] maximum number of tool calls to execute simultaneously.
12
12
  # When +nil+, all tool calls run as fibers without limit.
13
13
  #
@@ -5,7 +5,7 @@
5
5
  #
6
6
  # This is the default tool runtime used when no runtime is configured.
7
7
  #
8
- class Riffer::ToolRuntime::Inline < Riffer::ToolRuntime
8
+ class Riffer::Tools::Runtime::Inline < Riffer::Tools::Runtime
9
9
  #--
10
10
  #: () -> void
11
11
  def initialize
@@ -4,10 +4,10 @@
4
4
  # Executes tool calls concurrently using threads.
5
5
  #
6
6
  # class MyAgent < Riffer::Agent
7
- # tool_runtime Riffer::ToolRuntime::Threaded
7
+ # tool_runtime Riffer::Tools::Runtime::Threaded
8
8
  # end
9
9
  #
10
- class Riffer::ToolRuntime::Threaded < Riffer::ToolRuntime
10
+ class Riffer::Tools::Runtime::Threaded < Riffer::Tools::Runtime
11
11
  DEFAULT_MAX_CONCURRENCY = 5 #: Integer
12
12
 
13
13
  # [max_concurrency] maximum number of tool calls to execute simultaneously.
@@ -3,7 +3,7 @@
3
3
 
4
4
  require "json"
5
5
 
6
- # Riffer::ToolRuntime handles tool call execution for an agent.
6
+ # Riffer::Tools::Runtime handles tool call execution for an agent.
7
7
  #
8
8
  # Composes with a Riffer::Runner for concurrency control and provides
9
9
  # +execute+ as the sole public entry point.
@@ -11,19 +11,19 @@ require "json"
11
11
  # Subclass and override +dispatch_tool_call+ to customize how individual
12
12
  # tool calls are dispatched (e.g., HTTP, gRPC).
13
13
  #
14
- # runtime = Riffer::ToolRuntime::Inline.new
14
+ # runtime = Riffer::Tools::Runtime::Inline.new
15
15
  # results = runtime.execute(tool_calls, tools: tools, context: context)
16
16
  #
17
- class Riffer::ToolRuntime
17
+ class Riffer::Tools::Runtime
18
18
  # [runner] the concurrency runner to use for batch execution.
19
19
  #
20
- # Subclasses must provide a runner; instantiating ToolRuntime directly
20
+ # Subclasses must provide a runner; instantiating Riffer::Tools::Runtime directly
21
21
  # raises +NotImplementedError+.
22
22
  #
23
23
  #--
24
24
  #: (runner: Riffer::Runner) -> void
25
25
  def initialize(runner:)
26
- raise NotImplementedError, "#{self.class} is abstract — use a subclass like Riffer::ToolRuntime::Inline" if instance_of?(Riffer::ToolRuntime)
26
+ raise NotImplementedError, "#{self.class} is abstract — use a subclass like Riffer::Tools::Runtime::Inline" if instance_of?(Riffer::Tools::Runtime)
27
27
  @runner = runner
28
28
  end
29
29
 
@@ -32,13 +32,17 @@ class Riffer::ToolRuntime
32
32
  # [tool_calls] the tool calls to execute.
33
33
  # [tools] the resolved tool classes.
34
34
  # [context] the context hash.
35
+ # [assistant_message] the assistant message that produced these tool
36
+ # calls, when known. Forwarded to +around_tool_call+ and
37
+ # +dispatch_tool_call+ so subclasses can access it (e.g. for
38
+ # instrumentation that needs the accompanying assistant text).
35
39
  #
36
40
  #--
37
- #: (Array[Riffer::Messages::Assistant::ToolCall], tools: Array[singleton(Riffer::Tool)], context: Hash[Symbol, untyped]?) -> Array[[Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]]
38
- def execute(tool_calls, tools:, context:)
41
+ #: (Array[Riffer::Messages::Assistant::ToolCall], tools: Array[singleton(Riffer::Tool)], context: Riffer::Agent::Context?, ?assistant_message: Riffer::Messages::Assistant?) -> Array[[Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]]
42
+ def execute(tool_calls, tools:, context:, assistant_message: nil)
39
43
  @runner.map(tool_calls, context: context) do |tool_call|
40
- result = around_tool_call(tool_call, context: context) do
41
- dispatch_tool_call(tool_call, tools: tools, context: context)
44
+ result = around_tool_call(tool_call, context: context, assistant_message: assistant_message) do
45
+ dispatch_tool_call(tool_call, tools: tools, context: context, assistant_message: assistant_message)
42
46
  end
43
47
  [tool_call, result]
44
48
  end
@@ -49,10 +53,10 @@ class Riffer::ToolRuntime
49
53
  #
50
54
  # The default implementation simply yields.
51
55
  #
52
- # class InstrumentedRuntime < Riffer::ToolRuntime::Inline
56
+ # class InstrumentedRuntime < Riffer::Tools::Runtime::Inline
53
57
  # private
54
58
  #
55
- # def around_tool_call(tool_call, context:)
59
+ # def around_tool_call(tool_call, context:, assistant_message: nil)
56
60
  # start = Time.now
57
61
  # result = yield
58
62
  # Rails.logger.info("Tool #{tool_call.name} took #{Time.now - start}s")
@@ -61,8 +65,8 @@ class Riffer::ToolRuntime
61
65
  # end
62
66
  #
63
67
  #--
64
- #: (Riffer::Messages::Assistant::ToolCall, context: Hash[Symbol, untyped]?) { () -> Riffer::Tools::Response } -> Riffer::Tools::Response
65
- def around_tool_call(tool_call, context:)
68
+ #: (Riffer::Messages::Assistant::ToolCall, context: Riffer::Agent::Context?, ?assistant_message: Riffer::Messages::Assistant?) { () -> Riffer::Tools::Response } -> Riffer::Tools::Response
69
+ def around_tool_call(tool_call, context:, assistant_message: nil)
66
70
  yield
67
71
  end
68
72
 
@@ -74,10 +78,12 @@ class Riffer::ToolRuntime
74
78
  # [tool_call] the tool call to execute.
75
79
  # [tools] the resolved tool classes.
76
80
  # [context] the context hash.
81
+ # [assistant_message] the assistant message that produced this tool
82
+ # call, when known.
77
83
  #
78
84
  #--
79
- #: (Riffer::Messages::Assistant::ToolCall, tools: Array[singleton(Riffer::Tool)], context: Hash[Symbol, untyped]?) -> Riffer::Tools::Response
80
- def dispatch_tool_call(tool_call, tools:, context:)
85
+ #: (Riffer::Messages::Assistant::ToolCall, tools: Array[singleton(Riffer::Tool)], context: Riffer::Agent::Context?, ?assistant_message: Riffer::Messages::Assistant?) -> Riffer::Tools::Response
86
+ def dispatch_tool_call(tool_call, tools:, context:, assistant_message: nil)
81
87
  tool_class = tools.find { |tc| tc.name == tool_call.name }
82
88
 
83
89
  if tool_class.nil?
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Riffer::Toolable provides the shared class-level DSL for anything that can
4
+ # Riffer::Tools::Toolable provides the shared class-level DSL for anything that can
5
5
  # present as a tool to an LLM — tools today, and subagents/workflows in the
6
6
  # future.
7
7
  #
@@ -13,7 +13,7 @@
13
13
  # are NOT part of Toolable — those belong on Riffer::Tool.
14
14
  #
15
15
  # class MyTool
16
- # extend Riffer::Toolable
16
+ # extend Riffer::Tools::Toolable
17
17
  #
18
18
  # description "Does something useful"
19
19
  #
@@ -22,7 +22,7 @@
22
22
  # end
23
23
  # end
24
24
  #
25
- module Riffer::Toolable
25
+ module Riffer::Tools::Toolable
26
26
  DEFAULT_TIMEOUT = 10 #: Integer
27
27
 
28
28
  # Tracks all classes that extend Toolable.
@@ -31,8 +31,8 @@ module Riffer::Toolable
31
31
  #: (Module) -> void
32
32
  def self.extended(base)
33
33
  base.extend Riffer::Helpers::ClassNameConverter
34
- @extenders ||= []
35
- @extenders << base
34
+ extenders = (@extenders ||= []) #: Array[Module]
35
+ extenders << base
36
36
  end
37
37
 
38
38
  # Returns all classes that have extended Toolable.
@@ -82,11 +82,12 @@ module Riffer::Toolable
82
82
  # Defines parameters using the Params DSL.
83
83
  #
84
84
  #--
85
- #: () ?{ () -> void } -> Riffer::Params?
85
+ #: () ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
86
86
  def params(&block)
87
87
  return @params_builder if block.nil?
88
- @params_builder = Riffer::Params.new
89
- @params_builder.instance_eval(&block)
88
+ builder = Riffer::Params.new
89
+ builder.instance_eval(&block)
90
+ @params_builder = builder
90
91
  end
91
92
 
92
93
  # Returns the JSON Schema for the tool's parameters.
@@ -135,6 +136,8 @@ module Riffer::Toolable
135
136
  private
136
137
 
137
138
  def empty_schema # :nodoc:
138
- {type: "object", properties: {}, required: [], additionalProperties: false}
139
+ properties = {} #: Hash[Symbol, untyped]
140
+ required = [] #: Array[untyped]
141
+ {type: "object", properties: properties, required: required, additionalProperties: false}
139
142
  end
140
143
  end
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.27.2" #: String
5
+ VERSION = "0.29.0" #: String
6
6
  end
data/lib/riffer.rb CHANGED
@@ -11,7 +11,8 @@ require "zeitwerk"
11
11
  loader = Zeitwerk::Loader.for_gem
12
12
  loader.inflector.inflect(
13
13
  "open_ai" => "OpenAI",
14
- "azure_open_ai" => "AzureOpenAI"
14
+ "azure_open_ai" => "AzureOpenAI",
15
+ "open_router" => "OpenRouter"
15
16
  )
16
17
  loader.setup
17
18
 
@@ -0,0 +1,119 @@
1
+ # Generated from lib/riffer/agent/config.rb with RBS::Inline
2
+
3
+ # Typed configuration object holding every class-level DSL setting on a
4
+ # Riffer::Agent subclass.
5
+ #
6
+ # Each subclass of Riffer::Agent owns one Config, accessible via the class
7
+ # method <tt>config</tt>. The class-level DSL (+model+, +instructions+, +uses_tools+,
8
+ # etc.) reads and mutates this Config in place. Append-style DSL methods
9
+ # (+use_mcp+, +guardrail+) are handled by the +add_mcp+ and +add_guardrail+
10
+ # helpers below.
11
+ #
12
+ # Config stores Procs unresolved. Per-instance resolution happens elsewhere
13
+ # (instructions, model, tools, tool runtime, skills).
14
+ class Riffer::Agent::Config
15
+ DEFAULT_MAX_STEPS: Integer
16
+
17
+ attr_reader identifier: String?
18
+
19
+ attr_reader model: (String | Proc)?
20
+
21
+ attr_reader instructions: (String | Proc)?
22
+
23
+ attr_accessor provider_options: Hash[Symbol, untyped]
24
+
25
+ attr_accessor model_options: Hash[Symbol, untyped]
26
+
27
+ attr_reader structured_output: Riffer::Params?
28
+
29
+ attr_accessor max_steps: Numeric
30
+
31
+ attr_accessor tools_config: (Array[singleton(Riffer::Tool)] | Proc)?
32
+
33
+ attr_reader mcp_configs: Array[Hash[Symbol, untyped]]
34
+
35
+ attr_reader tool_runtime: singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc
36
+
37
+ attr_accessor skills_config: Riffer::Skills::Config?
38
+
39
+ attr_reader guardrails: Hash[Symbol, Array[Hash[Symbol, untyped]]]
40
+
41
+ # Builds a new Config. All fields are optional; unset fields take the
42
+ # documented defaults.
43
+ #
44
+ # Raises Riffer::ArgumentError if +model+ or +instructions+ is provided
45
+ # as a non-String, non-Proc value (or as an empty String).
46
+ #
47
+ # --
48
+ # : (?identifier: String?, ?model: (String | Proc)?, ?instructions: (String | Proc)?, ?provider_options: Hash[Symbol, untyped], ?model_options: Hash[Symbol, untyped], ?structured_output: Riffer::Params?, ?max_steps: Numeric, ?tools_config: (Array[singleton(Riffer::Tool)] | Proc)?, ?mcp_configs: Array[Hash[Symbol, untyped]], ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc), ?skills_config: Riffer::Skills::Config?, ?guardrails: Hash[Symbol, Array[Hash[Symbol, untyped]]]) -> void
49
+ def initialize: (?identifier: String?, ?model: (String | Proc)?, ?instructions: (String | Proc)?, ?provider_options: Hash[Symbol, untyped], ?model_options: Hash[Symbol, untyped], ?structured_output: Riffer::Params?, ?max_steps: Numeric, ?tools_config: (Array[singleton(Riffer::Tool)] | Proc)?, ?mcp_configs: Array[Hash[Symbol, untyped]], ?tool_runtime: singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc, ?skills_config: Riffer::Skills::Config?, ?guardrails: Hash[Symbol, Array[Hash[Symbol, untyped]]]) -> void
50
+
51
+ # Sets +identifier+. Accepts +nil+ or any value, coerced to String.
52
+ #
53
+ # --
54
+ # : (untyped) -> String?
55
+ def identifier=: (untyped) -> String?
56
+
57
+ # Sets +structured_output+. Accepts a Riffer::Params instance or +nil+.
58
+ #
59
+ # Raises Riffer::ArgumentError on any other type.
60
+ #
61
+ # --
62
+ # : (Riffer::Params?) -> Riffer::Params?
63
+ def structured_output=: (Riffer::Params?) -> Riffer::Params?
64
+
65
+ # Sets +tool_runtime+. Accepts a Riffer::Tools::Runtime subclass, a
66
+ # Riffer::Tools::Runtime instance, or a Proc.
67
+ #
68
+ # Raises Riffer::ArgumentError on any other type.
69
+ #
70
+ # --
71
+ # : ((singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
72
+ def tool_runtime=: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
73
+
74
+ # Sets +model+. Accepts a String ("provider/model"), a Proc, or +nil+.
75
+ #
76
+ # Raises Riffer::ArgumentError on non-String, non-Proc, or empty-String values.
77
+ #
78
+ # --
79
+ # : ((String | Proc)?) -> (String | Proc)?
80
+ def model=: ((String | Proc)?) -> (String | Proc)?
81
+
82
+ # Sets +instructions+. Accepts a String, a Proc, or +nil+.
83
+ #
84
+ # Raises Riffer::ArgumentError on non-String, non-Proc, or empty-String values.
85
+ #
86
+ # --
87
+ # : ((String | Proc)?) -> (String | Proc)?
88
+ def instructions=: ((String | Proc)?) -> (String | Proc)?
89
+
90
+ # Appends an MCP tag entry to +mcp_configs+.
91
+ #
92
+ # --
93
+ # : (String | Symbol) -> Array[Hash[Symbol, untyped]]
94
+ def add_mcp: (String | Symbol) -> Array[Hash[Symbol, untyped]]
95
+
96
+ # Appends a guardrail entry to +guardrails+ for the given phase.
97
+ #
98
+ # [phase] +:before+, +:after+, or +:around+. +:around+ appends to both
99
+ # +:before+ and +:after+.
100
+ # [klass] the Riffer::Guardrail subclass to register.
101
+ # [options] options forwarded to the guardrail at runtime.
102
+ #
103
+ # Raises Riffer::ArgumentError on an invalid phase or non-Guardrail class.
104
+ #
105
+ # --
106
+ # : (Symbol, klass: singleton(Riffer::Guardrail), ?options: Hash[Symbol, untyped]) -> void
107
+ def add_guardrail: (Symbol, klass: singleton(Riffer::Guardrail), ?options: Hash[Symbol, untyped]) -> void
108
+
109
+ # Returns the guardrail entries for the given phase, or +[]+ if none.
110
+ #
111
+ # --
112
+ # : (Symbol) -> Array[Hash[Symbol, untyped]]
113
+ def guardrails_for: (Symbol) -> Array[Hash[Symbol, untyped]]
114
+
115
+ private
116
+
117
+ # : (untyped, String) -> void
118
+ def validate_string_or_proc!: (untyped, String) -> void
119
+ end
@@ -0,0 +1,91 @@
1
+ # Generated from lib/riffer/agent/context.rb with RBS::Inline
2
+
3
+ # Typed value object wrapping the runtime context Hash held by a
4
+ # Riffer::Agent. Exposes first-class accessors for the framework-managed
5
+ # entries — +skills+ and +token_usage+ — and preserves +#[]+ / +#dig+
6
+ # reads so tools (which receive +context:+ as a keyword) keep working
7
+ # with both built-in and caller-provided keys.
8
+ #
9
+ # Reserved keys (+:skills+, +:token_usage+) cannot be set by the caller
10
+ # at construction; they are owned by Riffer and written through the typed
11
+ # setters. Type invariants are enforced on write — +skills+ must be a
12
+ # +Riffer::Skills::Context+ (or nil); +token_usage+ must be a
13
+ # +Riffer::Providers::TokenUsage+ (or nil).
14
+ #
15
+ # context = Riffer::Agent::Context.new(user_id: 42)
16
+ # context[:user_id] # => 42
17
+ # context.skills # => nil
18
+ # context.token_usage # => nil
19
+ class Riffer::Agent::Context
20
+ # Keys reserved for framework use. Passing any of these to the
21
+ # constructor raises +Riffer::ArgumentError+.
22
+ RESERVED_KEYS: Array[Symbol]
23
+
24
+ # Builds a new context.
25
+ #
26
+ # [data] caller-provided Hash passed as <tt>Agent.new(context:)</tt>.
27
+ # Duped before storage so caller mutations do not affect the
28
+ # agent. Must not contain any +RESERVED_KEYS+.
29
+ #
30
+ # Raises Riffer::ArgumentError when +data+ contains a reserved key.
31
+ #
32
+ # --
33
+ # : (?Hash[Symbol, untyped]) -> void
34
+ def initialize: (?Hash[Symbol, untyped]) -> void
35
+
36
+ # The agent's resolved +Riffer::Skills::Context+, or +nil+ when skills
37
+ # are not configured.
38
+ #
39
+ # --
40
+ # : () -> Riffer::Skills::Context?
41
+ def skills: () -> Riffer::Skills::Context?
42
+
43
+ # Sets the resolved skills context. Called once by +Riffer::Agent+
44
+ # during construction.
45
+ #
46
+ # Raises Riffer::ArgumentError if +value+ is neither +nil+ nor a
47
+ # +Riffer::Skills::Context+.
48
+ #
49
+ # --
50
+ # : (Riffer::Skills::Context?) -> Riffer::Skills::Context?
51
+ def skills=: (Riffer::Skills::Context?) -> Riffer::Skills::Context?
52
+
53
+ # The cumulative +Riffer::Providers::TokenUsage+ across every Run on this agent,
54
+ # or +nil+ before the first response is recorded.
55
+ #
56
+ # --
57
+ # : () -> Riffer::Providers::TokenUsage?
58
+ def token_usage: () -> Riffer::Providers::TokenUsage?
59
+
60
+ # Sets the cumulative token usage. Called by +Riffer::Agent::Run+ after
61
+ # each LLM response.
62
+ #
63
+ # Raises Riffer::ArgumentError if +value+ is neither +nil+ nor a
64
+ # +Riffer::Providers::TokenUsage+.
65
+ #
66
+ # --
67
+ # : (Riffer::Providers::TokenUsage?) -> Riffer::Providers::TokenUsage?
68
+ def token_usage=: (Riffer::Providers::TokenUsage?) -> Riffer::Providers::TokenUsage?
69
+
70
+ # Hash-style read. Preserved so downstream tool runtimes pulling
71
+ # caller-provided keys via <tt>context[:agent]</tt> or
72
+ # <tt>context[:tenant]</tt> keep working unchanged.
73
+ #
74
+ # --
75
+ # : (Symbol) -> untyped
76
+ def []: (Symbol) -> untyped
77
+
78
+ # Hash-style dig. Preserved for tools using
79
+ # <tt>context&.dig(:user_id)</tt>.
80
+ #
81
+ # --
82
+ # : (*Symbol) -> untyped
83
+ def dig: (*Symbol) -> untyped
84
+
85
+ # Returns a copy of the underlying Hash. Mutating the result does not
86
+ # affect this context.
87
+ #
88
+ # --
89
+ # : () -> Hash[Symbol, untyped]
90
+ def to_h: () -> Hash[Symbol, untyped]
91
+ end
@@ -30,6 +30,12 @@ class Riffer::Agent::Response
30
30
  # The full message history from the agent conversation.
31
31
  attr_reader messages: Array[Riffer::Messages::Base]
32
32
 
33
+ # Call ids of tool_use blocks that riffer filled with placeholder
34
+ # results during this turn — populated when an interrupt left them
35
+ # unanswered and +Riffer.config.experimental_history_healing+ is on.
36
+ # Empty otherwise.
37
+ attr_reader healed_tool_call_ids: Array[String]
38
+
33
39
  # Creates a new response.
34
40
  #
35
41
  # [content] the response content.
@@ -39,10 +45,12 @@ class Riffer::Agent::Response
39
45
  # [interrupt_reason] optional reason passed via <tt>throw :riffer_interrupt, reason</tt>.
40
46
  # [structured_output] parsed structured output when structured output is configured.
41
47
  # [messages] the full message history from the agent conversation.
48
+ # [healed_tool_call_ids] call ids filled with placeholder tool results
49
+ # when history healing is enabled.
42
50
  #
43
51
  # --
44
- # : (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?, ?messages: Array[Riffer::Messages::Base]) -> void
45
- def initialize: (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?, ?messages: Array[Riffer::Messages::Base]) -> void
52
+ # : (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?, ?messages: Array[Riffer::Messages::Base], ?healed_tool_call_ids: Array[String]) -> void
53
+ def initialize: (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?, ?messages: Array[Riffer::Messages::Base], ?healed_tool_call_ids: Array[String]) -> void
46
54
 
47
55
  # Returns true if the response was blocked by a guardrail.
48
56
  #