riffer 0.29.1 → 0.30.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91f54f388c63e670d155ae2b8d84bdafe18edd2f29423d92b52c9bfb686332d3
4
- data.tar.gz: 7b1bc3bb32951cadbadfe4ba388ca4dc805849a2724cbfae944daf1a59ea241c
3
+ metadata.gz: 7557c63ca04fa006d66a61a53bef6b1f7069ebab67439f00104fbb877d1aceda
4
+ data.tar.gz: 39bbe45b7e111ae343e32c54b9769db17dd7d8d9b98cd5cb7d42338aa5e4179c
5
5
  SHA512:
6
- metadata.gz: 5e573dd0b3ad056266a7c8a88b24754a510ceac1737f730cedf98e73dc18be17e2f9dc93ebe47d4c89e2d483c1c40c40a7fcbad586ba1ceac949b11af2011a8c
7
- data.tar.gz: c1aa387e0298c3999da9da89a640a86048207dd54abe6a336781a9dd265acec49939729fcca7378872efa595131357601b5639b2437508225470fe473a9a5989
6
+ metadata.gz: 61694198a05d63d831dca9b4b37e38b6cacbd7b67214fa47c2027a0a7e1636ee07e5f85641f9fe00ee597d66dfdb0a6fccd0e494b6ec670a84a2020907cfdd0b
7
+ data.tar.gz: fbff4457d39da36a1c77e9cb7ea86ccb5e96f0887ba982f6a8a960557ee4beceda3a604011fcfd20951f9baedd59eadbf188832cb6bcc8fcf71e256d93e8cdc3
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.29.1"
2
+ ".": "0.30.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.30.0](https://github.com/janeapp/riffer/compare/riffer/v0.29.1...riffer/v0.30.0) (2026-06-03)
9
+
10
+
11
+ ### ⚠ BREAKING CHANGES
12
+
13
+ * unlimited max_steps is now represented as `nil` at the agent level (set via `max_steps nil`), not Float::INFINITY — Riffer::Agent::Config#max_steps may be nil and is no longer normalized. Riffer::Params parameter types are typed Module (widened from Class) to honestly include Riffer::Params::Boolean, which is a Module.
14
+
15
+ ### Features
16
+
17
+ * Riffer::Agent::Serializer for transferable agent definitions ([#293](https://github.com/janeapp/riffer/issues/293)) ([99134b0](https://github.com/janeapp/riffer/commit/99134b0bf52bccfd727e52349aba8432389775be))
18
+
8
19
  ## [0.29.1](https://github.com/janeapp/riffer/compare/riffer/v0.29.0...riffer/v0.29.1) (2026-06-01)
9
20
 
10
21
 
data/README.md CHANGED
@@ -61,6 +61,7 @@ For comprehensive documentation, see the [docs](docs/) directory:
61
61
  - [Guardrails](docs/12_GUARDRAILS.md) - Input/output validation
62
62
  - [Skills](docs/13_SKILLS.md) - Packaged agent capabilities
63
63
  - [MCP](docs/14_MCP.md) - Integrating third-party MCP servers
64
+ - [Serialization](docs/15_SERIALIZATION.md) - Persisting and transferring agent definitions
64
65
  - [Providers](docs/providers/01_PROVIDERS.md) - LLM provider adapters
65
66
 
66
67
  ### API Reference
data/docs/01_OVERVIEW.md CHANGED
@@ -134,3 +134,4 @@ Response
134
134
  - [Evals](11_EVALS.md) - Evaluating agent quality
135
135
  - [Guardrails](12_GUARDRAILS.md) - Input/output validation
136
136
  - [Skills](13_SKILLS.md) - Packaged agent capabilities
137
+ - [Serialization](15_SERIALIZATION.md) - Persisting and transferring agent definitions
data/docs/03_AGENTS.md CHANGED
@@ -152,7 +152,7 @@ end
152
152
 
153
153
  ### max_steps
154
154
 
155
- Sets the maximum number of LLM call steps in the tool-use loop. When the limit is reached, the loop interrupts with reason `:max_steps`. Defaults to `16`. Set to `Float::INFINITY` for unlimited steps:
155
+ Sets the maximum number of LLM call steps in the tool-use loop. When the limit is reached, the loop interrupts with reason `:max_steps`. Defaults to `16`. Set to `nil` (`max_steps nil`) for unlimited steps:
156
156
 
157
157
  ```ruby
158
158
  class MyAgent < Riffer::Agent
@@ -0,0 +1,103 @@
1
+ # Serialization
2
+
3
+ `Riffer::Agent::Serializer` turns a **resolved agent** into a self-contained, provider-neutral data dict (`to_h`) and reconstructs a **runnable agent** from that dict (`from_h`). Use it to persist agent definitions outside of code, or to transfer them across a process/service boundary.
4
+
5
+ You normally reach it through the delegators on `Riffer::Agent`:
6
+
7
+ ```ruby
8
+ dict = agent.to_h # snapshot
9
+ rebuilt = Riffer::Agent.from_h(dict) # reconstruct
10
+ ```
11
+
12
+ The dict is plain data — symbol-keyed, JSON-safe. For the wire, use the JSON helpers, which handle generating and parsing for you:
13
+
14
+ ```ruby
15
+ json = agent.to_json # or Riffer::Agent::Serializer.to_json(agent:)
16
+ rebuilt = Riffer::Agent.from_json(json)
17
+ ```
18
+
19
+ The hash forms (`to_h` / `from_h`) are public too, if you want to embed the dict in a larger payload. `from_h` expects symbol keys, so parse with `JSON.parse(str, symbolize_names: true)` — or just use `from_json`, which does that for you.
20
+
21
+ ### Runtime context
22
+
23
+ `from_h` / `from_json` accept an optional `context:` — the rebuilt agent's **runtime** context, exactly the value you'd pass to `Agent.new(context:)`. It is **not** used to re-resolve the serialized definition (that's already resolved); it's threaded into tool dispatch and read by tools/runtimes at call time. Pass it when a tool or a remote runtime needs per-call data — e.g. `context: { tenant: "acme" }` for multi-tenant dispatch, or Maestro passing `context: { agent: self }` so its runtime can call back. Omit it (defaults to empty) when nothing downstream reads context.
24
+
25
+ ## What the dict carries
26
+
27
+ ```ruby
28
+ {
29
+ schema_version: 1, # wire format version
30
+ riffer_version: "0.29.1", # diagnostic only
31
+ identifier: "support_agent",
32
+ model: "openai/gpt-4o", # resolved "provider/model" string
33
+ instructions: "You are…", # resolved system prompt
34
+ model_options: { temperature: 0.2 },
35
+ provider_options: { … }, # see the secrets warning below
36
+ max_steps: 8, # integer; -1 = unlimited (see below)
37
+ structured_output: { type: "object", … }, # JSON Schema, or null
38
+ tools: [ { name:, description:, parameters_schema:, timeout: }, … ]
39
+ }
40
+ ```
41
+
42
+ ### Resolved snapshot
43
+
44
+ `to_h` reads the agent's **resolved** state, not its raw configuration. By the time you call it, `Agent.new` has already evaluated any `Proc`-based `model`, `instructions`, or `uses_tools` against the agent's own context — so the dict carries plain strings and data, never Procs. The receiver's `context:` drives runtime behavior (tool dispatch); it does **not** re-evaluate baked-in fields.
45
+
46
+ ### Structured output
47
+
48
+ Structured output crosses as **provider-neutral JSON Schema** (`Riffer::Params#to_json_schema`), never a provider-rendered schema. `from_h` rebuilds a validating `Riffer::Params` via `Riffer::Params.from_json_schema`, so the rebuilt agent both constrains the model and can `parse_and_validate` responses — rendering provider-correct bytes at call time, the same way an in-code agent does.
49
+
50
+ ## Reconstructing tools
51
+
52
+ Tools cross as `{name, description, parameters_schema, timeout}` descriptors — never code. How a descriptor becomes a runnable tool is controlled by the `tool_resolver:` you pass to `from_h`. The two common shapes:
53
+
54
+ ### In-process (registry lookup)
55
+
56
+ When the rebuilt agent runs in the **same** codebase that defined the tools (e.g. persisting an agent definition and rehydrating it later), resolve each descriptor back to its real class:
57
+
58
+ ```ruby
59
+ rebuilt = Riffer::Agent.from_h(dict,
60
+ tool_resolver: ->(descriptor) { MyToolRegistry.fetch(descriptor[:name]) })
61
+ ```
62
+
63
+ The real classes carry their `#call` bodies, so the agent runs on the default `Inline` runtime — no further wiring.
64
+
65
+ ### Distributed (body-less shells)
66
+
67
+ When the receiver holds **only the Riffer gem**, the default `tool_resolver` synthesizes body-less **tool shells**. A shell advertises the tool's schema to the LLM but has no `#call` — invoking it in-process raises. Pair the default resolver with a remote `Riffer::Tools::Runtime` that forwards each call back to the origin:
68
+
69
+ ```ruby
70
+ rebuilt = Riffer::Agent.from_h(dict,
71
+ tool_runtime: MyRemoteToolRuntime.new(client: rpc_client))
72
+ ```
73
+
74
+ Implement the remote runtime by subclassing `Riffer::Tools::Runtime` and overriding `#dispatch_tool_call` to forward the call over your transport, mapping any failure to `Tools::Response.error`. See [Advanced Tools](07_TOOL_ADVANCED.md) for the runtime API.
75
+
76
+ You own what a resolved tool does: a resolver may return real in-process classes, shells, or classes that themselves make network calls. Riffer does not require a runtime — it only ships shells by default.
77
+
78
+ ## `max_steps`
79
+
80
+ Unlimited steps are `nil` at the agent level — set it with `max_steps nil`. On the wire, the serializer encodes that as **`-1`** (and decodes `-1` back to `nil`), so the dict stays portable across transports where JSON `null` is awkward — proto3, for one, can't distinguish `null` from an absent field. The `-1` is purely a wire detail: the DSL and your code only ever see `nil`, and the encode/decode handles the translation at the boundary.
81
+
82
+ - **DSL** — integer = bounded, `nil` = unlimited, omitted = `Config`'s default (16).
83
+ - **Wire** — integer = bounded, `-1` = unlimited, omitted = default (16).
84
+
85
+ A finite integer round-trips as-is; a dict missing the key falls back to the default rather than running unbounded.
86
+
87
+ ## Versioning
88
+
89
+ `schema_version` is an integer (`Riffer::Agent::Serializer::SCHEMA_VERSION`). `from_h` refuses any version it doesn't recognize with `Riffer::Agent::Serializer::VersionError` (a `Riffer::ArgumentError`). A future incompatible change bumps the integer and adds a backwards-compatible decoder, giving distributed consumers a window to upgrade before the old format is dropped.
90
+
91
+ ## Secrets
92
+
93
+ `provider_options` and `model_options` **ride on the wire as plain data** — they are part of the dict and _will_ transfer. Prefer configuring API keys via environment/global provider configuration rather than `provider_options`. **Never serialize an agent whose options carry sensitive values** — and if a serialized definition ever does, handle it as a secret (encrypt it, keep it out of logs).
94
+
95
+ ## What does **not** transfer
96
+
97
+ - **Guardrails and skills** are **not supported yet** — neither is serialized, so a rebuilt agent enforces no guardrails and has no skills catalog. (As a stopgap, a skills-enabled agent's `skill_activate` tool still crosses as an ordinary tool descriptor.) Both are expected to be revisited.
98
+
99
+ ## Next Steps
100
+
101
+ - [Tools](06_TOOLS.md) - Creating tools
102
+ - [Advanced Tools](07_TOOL_ADVANCED.md) - Tool runtime and dispatch
103
+ - [Configuration](10_CONFIGURATION.md) - Global configuration
@@ -21,7 +21,7 @@ class Riffer::Agent::Config
21
21
  attr_accessor :provider_options #: Hash[Symbol, untyped]
22
22
  attr_accessor :model_options #: Hash[Symbol, untyped]
23
23
  attr_reader :structured_output #: Riffer::Params?
24
- attr_accessor :max_steps #: Numeric
24
+ attr_accessor :max_steps #: Numeric?
25
25
  attr_accessor :tools_config #: (Array[singleton(Riffer::Tool)] | Proc)?
26
26
  attr_reader :mcp_configs #: Array[Hash[Symbol, untyped]]
27
27
  attr_reader :tool_runtime #: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
@@ -35,7 +35,7 @@ class Riffer::Agent::Config
35
35
  # as a non-String, non-Proc value (or as an empty String).
36
36
  #
37
37
  #--
38
- #: (?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
38
+ #: (?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
39
39
  def initialize(
40
40
  identifier: nil,
41
41
  model: nil,
@@ -77,7 +77,8 @@ module Riffer::Agent::Run
77
77
 
78
78
  break unless processed_response.has_tool_calls?
79
79
 
80
- throw :riffer_interrupt, Riffer::Agent::INTERRUPT_MAX_STEPS if step >= agent.config.max_steps
80
+ max_steps = agent.config.max_steps
81
+ throw :riffer_interrupt, Riffer::Agent::INTERRUPT_MAX_STEPS if max_steps && step >= max_steps
81
82
 
82
83
  execute_tool_calls(agent, processed_response)
83
84
  end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "json"
5
+
6
+ # Riffer::Agent::Serializer turns a resolved agent into a self-contained,
7
+ # provider-neutral data dict and back into a runnable agent. A pure module
8
+ # (sibling to Riffer::Agent::Run), reached most often through the
9
+ # +Riffer::Agent#to_h+ / +Riffer::Agent.from_h+ delegators.
10
+ #
11
+ # The dict carries only data — no Procs, no class references, no tool
12
+ # runtime. The same dict serves two rehydration targets:
13
+ #
14
+ # - <b>In-process</b> (a monolith persisting agent definitions): pass a
15
+ # +tool_resolver+ that looks tool descriptors up in a local registry and
16
+ # returns the real, body-bearing classes. They run on the default runtime.
17
+ # - <b>Distributed</b> (a receiver holding only the Riffer gem): the default
18
+ # resolver synthesizes body-less tool shells; inject a remote
19
+ # +Riffer::Tools::Runtime+ to forward each call back to the origin.
20
+ #
21
+ # dict = Riffer::Agent::Serializer.to_h(agent: agent)
22
+ # rebuilt = Riffer::Agent::Serializer.from_h(dict, context: {tenant: "acme"})
23
+ #
24
+ # == What does not transfer
25
+ #
26
+ # Guardrails and the skills subsystem (backend/adapter/catalog) are not
27
+ # serialized; a rebuilt agent enforces no guardrails and renders no skills
28
+ # catalog (the +skill_activate+ tool, if present, crosses as an ordinary
29
+ # tool). Secrets must not be placed in +provider_options+/+model_options+:
30
+ # both ride on the wire as plain data.
31
+ module Riffer::Agent::Serializer
32
+ extend self
33
+
34
+ # The wire format version. Bumped only on an incompatible change to the
35
+ # dict shape; +from_h+ refuses any other version. See +from_h+ for the
36
+ # dispatch seam that carries back-compat decoders.
37
+ SCHEMA_VERSION = 1 #: Integer
38
+
39
+ # Raised by +from_h+ when the dict's +schema_version+ is unsupported.
40
+ class VersionError < Riffer::ArgumentError; end
41
+
42
+ # The default +tool_resolver+: synthesizes a body-less tool shell from a
43
+ # descriptor. Its +#call+ raises — route shells through a remote runtime.
44
+ DEFAULT_TOOL_RESOLVER = ->(descriptor) { build_tool_shell(descriptor) } #: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool)
45
+
46
+ # Snapshots a resolved agent into a self-contained wire dict.
47
+ #
48
+ # Reads the agent's resolved instance state — Proc-based settings have
49
+ # already been evaluated against the agent's own context, so the dict
50
+ # carries plain strings/data, never Procs. Tools are emitted as
51
+ # +{name, description, parameters_schema, timeout}+ descriptors (the
52
+ # resolved +agent.tools+, including MCP tools and +skill_activate+).
53
+ #
54
+ # [agent] a resolved Riffer::Agent instance.
55
+ #
56
+ #--
57
+ #: (agent: Riffer::Agent) -> Hash[Symbol, untyped]
58
+ def to_h(agent:)
59
+ config = agent.config
60
+ {
61
+ schema_version: SCHEMA_VERSION,
62
+ riffer_version: Riffer::VERSION,
63
+ identifier: config.identifier,
64
+ model: "#{agent.provider_name}/#{agent.model_name}",
65
+ instructions: agent.instruction_message&.content,
66
+ model_options: config.model_options,
67
+ provider_options: config.provider_options,
68
+ max_steps: encode_max_steps(config.max_steps),
69
+ structured_output: config.structured_output&.to_json_schema(strict: false),
70
+ tools: agent.tools.map { |tool_class| tool_descriptor(tool_class) }
71
+ }
72
+ end
73
+
74
+ # Reconstructs a runnable agent from a wire dict.
75
+ #
76
+ # [hash] a Symbol-keyed wire dict (parse JSON with +symbolize_names: true+).
77
+ # [context] the rebuilt agent's runtime context — the same value you'd pass
78
+ # to +Agent.new(context:)+. It is *not* used to re-resolve serialized
79
+ # config (the dict is already resolved); it is threaded into tool dispatch
80
+ # and read by tools/runtimes at call time (e.g. a remote runtime keying off
81
+ # <tt>context[:tenant]</tt>). Defaults to an empty context.
82
+ # [tool_resolver] maps a tool descriptor to a Riffer::Tool class. Defaults
83
+ # to DEFAULT_TOOL_RESOLVER (body-less shells). Pass a registry lookup to
84
+ # rebuild real, in-process tools.
85
+ # [tool_runtime] an optional Riffer::Tools::Runtime to inject (e.g. a
86
+ # remote runtime for shells). When omitted, the agent uses the configured
87
+ # default (+Riffer.config.tool_runtime+).
88
+ #
89
+ # Raises Riffer::Agent::Serializer::VersionError on an unsupported
90
+ # +schema_version+, and Riffer::ArgumentError on a malformed dict.
91
+ #
92
+ #--
93
+ #: (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
94
+ def from_h(hash, context: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
95
+ # Version -> decoder dispatch. Adding a +when 2+ arm (a backwards-compatible
96
+ # decoder) is how a future breaking change keeps older dicts readable.
97
+ case hash[:schema_version]
98
+ when SCHEMA_VERSION
99
+ decode_v1(hash, context: context, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
100
+ else
101
+ raise VersionError, "Unsupported schema_version: #{hash[:schema_version].inspect} (this Riffer supports #{SCHEMA_VERSION})"
102
+ end
103
+ end
104
+
105
+ # Snapshots a resolved agent to a JSON string. Convenience over
106
+ # <tt>JSON.generate(to_h(agent:))</tt>.
107
+ #
108
+ #--
109
+ #: (agent: Riffer::Agent) -> String
110
+ def to_json(agent:)
111
+ JSON.generate(to_h(agent: agent))
112
+ end
113
+
114
+ # Reconstructs a runnable agent from a JSON string produced by +to_json+.
115
+ # Handles the JSON parse (with symbol keys) so callers don't have to. See
116
+ # +from_h+ for the arguments.
117
+ #
118
+ #--
119
+ #: (String, ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
120
+ def from_json(json, context: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
121
+ from_h(JSON.parse(json, symbolize_names: true), context: context, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
122
+ end
123
+
124
+ private
125
+
126
+ #--
127
+ #: (Hash[Symbol, untyped], context: Hash[Symbol, untyped]?, tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
128
+ def decode_v1(hash, context:, tool_resolver:, tool_runtime:)
129
+ tools = Array(hash[:tools]).map { |descriptor| tool_resolver.call(descriptor) }
130
+
131
+ config_args = {
132
+ identifier: hash[:identifier],
133
+ model: hash[:model],
134
+ instructions: hash[:instructions],
135
+ provider_options: hash[:provider_options] || {},
136
+ model_options: hash[:model_options] || {},
137
+ structured_output: decode_structured_output(hash[:structured_output]),
138
+ max_steps: decode_max_steps(hash),
139
+ tools_config: tools
140
+ } #: Hash[Symbol, untyped]
141
+ # tool_runtime= rejects nil, so only inject when supplied; otherwise the
142
+ # Config default (Riffer.config.tool_runtime) applies.
143
+ config_args[:tool_runtime] = tool_runtime if tool_runtime
144
+
145
+ Riffer::Agent.new(config: Riffer::Agent::Config.new(**config_args), context: context)
146
+ end
147
+
148
+ #--
149
+ #: (Hash[Symbol, untyped]?) -> Riffer::Params?
150
+ def decode_structured_output(schema)
151
+ return nil if schema.nil?
152
+ Riffer::Params.from_json_schema(schema)
153
+ end
154
+
155
+ # The DSL represents unlimited steps as +nil+, but the wire encodes it as
156
+ # +-1+ so the dict stays portable across transports where JSON +null+ is
157
+ # awkward (e.g. proto3, which can't tell null from an absent field). The
158
+ # magic value lives only on the wire — +encode_max_steps+/+decode_max_steps+
159
+ # translate at the boundary so neither the DSL nor consumers see it.
160
+ #--
161
+ #: (Numeric?) -> Numeric
162
+ def encode_max_steps(value)
163
+ value.nil? ? -1 : value
164
+ end
165
+
166
+ # Reverses +encode_max_steps+: +-1+ (or a literal +null+) means unlimited.
167
+ # An absent key falls back to the default — a partial dict must not silently
168
+ # become an unbounded loop.
169
+ #--
170
+ #: (Hash[Symbol, untyped]) -> Numeric?
171
+ def decode_max_steps(hash)
172
+ return Riffer::Agent::Config::DEFAULT_MAX_STEPS unless hash.key?(:max_steps)
173
+ (hash[:max_steps] == -1) ? nil : hash[:max_steps]
174
+ end
175
+
176
+ #--
177
+ #: (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
178
+ def tool_descriptor(tool_class)
179
+ tool_class.to_tool_schema(strict: false).merge(timeout: tool_class.timeout)
180
+ end
181
+
182
+ # Builds an anonymous, body-less Riffer::Tool subclass that advertises the
183
+ # descriptor's schema to the LLM. Its +#call+ raises — a shell only has
184
+ # identity, not behavior; route its calls through a remote runtime.
185
+ #
186
+ # Returns +untyped+: steep can't see that +Class.new(Riffer::Tool)+ is a
187
+ # +singleton(Riffer::Tool)+ (cf. Riffer::Mcp::ToolFactory#build_tool_class).
188
+ #--
189
+ #: (Hash[Symbol, untyped]) -> untyped
190
+ def build_tool_shell(descriptor)
191
+ tool_name = descriptor[:name]
192
+ tool_description = descriptor[:description]
193
+ schema = descriptor[:parameters_schema]
194
+ tool_timeout = descriptor[:timeout]
195
+
196
+ # An anonymous Riffer::Tool subclass is the idiom for synthesizing a tool
197
+ # from data — the tool DSL is class-level, so there is no value-level
198
+ # builder to type against. Same approach as Riffer::Mcp::ToolFactory;
199
+ # steep can't type the dynamic class body, hence the ignore block.
200
+ Class.new(Riffer::Tool) do
201
+ # steep:ignore:start
202
+ identifier tool_name
203
+ description tool_description
204
+ timeout tool_timeout if tool_timeout
205
+ define_singleton_method(:parameters_schema) { |strict: false| schema }
206
+
207
+ define_method(:call) do |context:, **kwargs|
208
+ raise Riffer::Error,
209
+ "#{self.class.name || "wire tool shell"} '#{self.class.identifier}' has no body; " \
210
+ "route its calls through a remote Riffer::Tools::Runtime (see Riffer::Agent::Serializer)"
211
+ end
212
+ # steep:ignore:end
213
+ end
214
+ end
215
+ end
data/lib/riffer/agent.rb CHANGED
@@ -102,13 +102,19 @@ class Riffer::Agent
102
102
 
103
103
  # Gets or sets the maximum number of LLM call steps in the tool-use loop.
104
104
  #
105
- # Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to
106
- # +Float::INFINITY+ for unlimited steps.
105
+ # Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to +nil+
106
+ # for unlimited steps. The splat distinguishes a getter call (no argument)
107
+ # from setting the limit to +nil+.
108
+ #
109
+ # max_steps # reads the current limit
110
+ # max_steps 8 # cap the loop at 8 steps
111
+ # max_steps nil # unlimited
107
112
  #
108
113
  #--
109
- #: (?Numeric?) -> Numeric
110
- def self.max_steps(value = nil)
111
- value.nil? ? config.max_steps : (config.max_steps = value)
114
+ #: (*Numeric?) -> Numeric?
115
+ def self.max_steps(*value)
116
+ return config.max_steps if value.empty?
117
+ config.max_steps = value.first
112
118
  end
113
119
 
114
120
  # Gets or sets the tools used by this agent.
@@ -206,6 +212,30 @@ class Riffer::Agent
206
212
  new(context: context).stream(prompt, files: files)
207
213
  end
208
214
 
215
+ # Reconstructs a runnable agent from a wire dict produced by +#to_h+.
216
+ #
217
+ # Delegates to Riffer::Agent::Serializer.from_h. See it for the
218
+ # +tool_resolver+ / +tool_runtime+ injection points and what does not
219
+ # transfer.
220
+ #
221
+ #--
222
+ #: (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
223
+ def self.from_h(hash, context: nil, tool_resolver: Riffer::Agent::Serializer::DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
224
+ Riffer::Agent::Serializer.from_h(hash, context: context, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
225
+ end
226
+
227
+ # Reconstructs a runnable agent from a JSON string produced by +#to_json+.
228
+ #
229
+ # Delegates to Riffer::Agent::Serializer.from_json, which parses the JSON
230
+ # (with symbol keys) for you. See Riffer::Agent::Serializer.from_h for the
231
+ # +tool_resolver+ / +tool_runtime+ injection points.
232
+ #
233
+ #--
234
+ #: (String, ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
235
+ def self.from_json(json, context: nil, tool_resolver: Riffer::Agent::Serializer::DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
236
+ Riffer::Agent::Serializer.from_json(json, context: context, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
237
+ end
238
+
209
239
  # Registers a guardrail for input, output, or both phases.
210
240
  #
211
241
  # [phase] :before, :after, or :around.
@@ -261,6 +291,11 @@ class Riffer::Agent
261
291
  # reserved and cannot be passed by the caller.
262
292
  attr_reader :context #: Riffer::Agent::Context
263
293
 
294
+ # The resolved provider name (the part before "provider/"), e.g. +"openai"+.
295
+ # Resolved eagerly at +Agent.new+ alongside +model_name+; together they
296
+ # form the provider-neutral model identifier the agent serializes.
297
+ attr_reader :provider_name #: String
298
+
264
299
  # The resolved model name (the part after "provider/"), used as the model
265
300
  # argument on every LLM call. Resolved eagerly at +Agent.new+.
266
301
  attr_reader :model_name #: String
@@ -308,10 +343,10 @@ class Riffer::Agent
308
343
  @config = config || self.class.config
309
344
  @context = Riffer::Agent::Context.new(context || {})
310
345
 
311
- provider_class, @model_name = resolve_provider_and_model
312
- @provider = provider_class.new(**@config.provider_options)
346
+ @provider_name, @model_name = resolve_provider_and_model
347
+ @provider = build_provider
313
348
 
314
- @context.skills = resolve_skills(provider_class)
349
+ @context.skills = resolve_skills
315
350
 
316
351
  @structured_output = resolve_structured_output
317
352
  @tools = resolve_tools
@@ -376,6 +411,25 @@ class Riffer::Agent
376
411
  throw :riffer_interrupt, reason
377
412
  end
378
413
 
414
+ # Snapshots this resolved agent into a self-contained, provider-neutral
415
+ # wire dict. Delegates to Riffer::Agent::Serializer.to_h.
416
+ #
417
+ #--
418
+ #: () -> Hash[Symbol, untyped]
419
+ def to_h
420
+ Riffer::Agent::Serializer.to_h(agent: self)
421
+ end
422
+
423
+ # Snapshots this resolved agent into a wire JSON string. Delegates to
424
+ # Riffer::Agent::Serializer.to_json. The +*+ absorbs the JSON generator
425
+ # state argument so <tt>JSON.generate(agent)</tt> works too.
426
+ #
427
+ #--
428
+ #: (*untyped) -> String
429
+ def to_json(*)
430
+ Riffer::Agent::Serializer.to_json(agent: self)
431
+ end
432
+
379
433
  private
380
434
 
381
435
  #--
@@ -395,13 +449,13 @@ class Riffer::Agent
395
449
  end
396
450
 
397
451
  # Resolves +Config#model+ to a "provider/model" string (calling the Proc
398
- # form against +@context+), parses it, and looks up the provider class.
452
+ # form against +@context+) and parses it.
399
453
  #
400
- # Returns +[provider_class, model_name]+. Raises Riffer::ArgumentError on
401
- # an invalid model string or an unregistered provider.
454
+ # Returns +[provider_name, model_name]+. Raises Riffer::ArgumentError on an
455
+ # invalid model string.
402
456
  #
403
457
  #--
404
- #: () -> [singleton(Riffer::Providers::Base), String]
458
+ #: () -> [String, String]
405
459
  def resolve_provider_and_model
406
460
  model_string = Riffer::Helpers::CallOrValue.resolve(@config.model, context: @context)
407
461
  raise Riffer::ArgumentError, "Invalid model string: #{model_string}" unless model_string.is_a?(String)
@@ -412,18 +466,28 @@ class Riffer::Agent
412
466
  raise Riffer::ArgumentError, "Invalid model string: #{model_string}"
413
467
  end
414
468
 
415
- provider_class = Riffer::Providers::Repository.find(provider_name)
416
- raise Riffer::ArgumentError, "Provider not found: #{provider_name}" unless provider_class
469
+ [provider_name, model_name]
470
+ end
417
471
 
418
- [provider_class, model_name]
472
+ # Builds the provider client from the resolved +@provider_name+ and the
473
+ # configured +provider_options+.
474
+ #
475
+ # Raises Riffer::ArgumentError on an unregistered provider.
476
+ #
477
+ #--
478
+ #: () -> Riffer::Providers::Base
479
+ def build_provider
480
+ provider_class = Riffer::Providers::Repository.find(@provider_name)
481
+ raise Riffer::ArgumentError, "Provider not found: #{@provider_name}" unless provider_class
482
+ provider_class.new(**@config.provider_options)
419
483
  end
420
484
 
421
485
  # Resolves the skills backend, lists skills, and selects an adapter.
422
486
  # Returns nil if skills are unconfigured or the backend is empty.
423
487
  #
424
488
  #--
425
- #: (singleton(Riffer::Providers::Base)) -> Riffer::Skills::Context?
426
- def resolve_skills(provider_class)
489
+ #: () -> Riffer::Skills::Context?
490
+ def resolve_skills
427
491
  skills_config = @config.skills_config
428
492
  return nil unless skills_config
429
493
 
@@ -434,7 +498,7 @@ class Riffer::Agent
434
498
  return nil if backend.list_skills.empty?
435
499
 
436
500
  skills = backend.list_skills.to_h { |s| [s.name, s] }
437
- adapter_class = skills_config.adapter || provider_class.skills_adapter(@model_name)
501
+ adapter_class = skills_config.adapter || @provider.class.skills_adapter(@model_name)
438
502
  skill_activate_tool_class = skills_config.activate_tool || Riffer.config.skills.default_activate_tool
439
503
 
440
504
  skills_context = Riffer::Skills::Context.new(
@@ -18,19 +18,90 @@ class Riffer::Params::Param
18
18
  }.freeze #: Hash[Module, String]
19
19
 
20
20
  # Primitive types allowed for the <tt>of:</tt> keyword on Array params
21
- PRIMITIVE_TYPES = (TYPE_MAPPINGS.keys - [Array, Hash]).freeze #: Array[Class]
21
+ PRIMITIVE_TYPES = (TYPE_MAPPINGS.keys - [Array, Hash]).freeze #: Array[Module]
22
+
23
+ # Maps JSON Schema type strings back to Ruby types. The inverse of
24
+ # TYPE_MAPPINGS, collapsing the three boolean spellings onto
25
+ # Riffer::Params::Boolean. Used by +from_json_schema+.
26
+ JSON_TYPE_MAPPINGS = {
27
+ "string" => String,
28
+ "integer" => Integer,
29
+ "number" => Float,
30
+ "boolean" => Riffer::Params::Boolean,
31
+ "array" => Array,
32
+ "object" => Hash
33
+ }.freeze #: Hash[String, Module]
22
34
 
23
35
  attr_reader :name #: Symbol
24
- attr_reader :type #: Class
36
+ attr_reader :type #: Module
25
37
  attr_reader :required #: bool
26
38
  attr_reader :description #: String?
27
39
  attr_reader :enum #: Array[untyped]?
28
40
  attr_reader :default #: untyped
29
- attr_reader :item_type #: Class?
41
+ attr_reader :item_type #: Module?
30
42
  attr_reader :nested_params #: Riffer::Params?
31
43
 
32
44
  #--
33
- #: (name: Symbol, type: Class, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Class?, ?nested_params: Riffer::Params?) -> void
45
+ # Reconstructs a Param from a single JSON Schema property.
46
+ #
47
+ # [name] the parameter name (Symbol).
48
+ # [schema] the property's JSON Schema (Symbol-keyed).
49
+ # [required] whether the property appeared in the parent's +required+ list.
50
+ #
51
+ # Raises Riffer::ArgumentError on a type outside the Params-expressible subset.
52
+ #
53
+ #--
54
+ #: (Symbol, Hash[Symbol, untyped], required: bool) -> Riffer::Params::Param
55
+ def self.from_json_schema(name, schema, required:)
56
+ ruby_type = json_type_to_ruby(schema[:type])
57
+ item_type, nested = resolve_nesting(ruby_type, schema)
58
+
59
+ new(
60
+ name: name,
61
+ type: ruby_type,
62
+ required: required,
63
+ description: schema[:description],
64
+ enum: schema[:enum],
65
+ default: schema[:default],
66
+ item_type: item_type,
67
+ nested_params: nested
68
+ )
69
+ end
70
+
71
+ # Resolves the +[item_type, nested_params]+ pair for a reconstructed Param:
72
+ # a nested Params for object / array-of-object schemas, an +item_type+ for
73
+ # typed primitive arrays, and +nil+ for everything else.
74
+ #
75
+ #--
76
+ #: (Module, Hash[Symbol, untyped]) -> [Module?, Riffer::Params?]
77
+ def self.resolve_nesting(ruby_type, schema)
78
+ return [nil, Riffer::Params.from_json_schema(schema)] if ruby_type == Hash && schema[:properties]
79
+ return [nil, nil] unless ruby_type == Array
80
+
81
+ items = schema[:items]
82
+ return [nil, nil] unless items.is_a?(Hash)
83
+ return [nil, Riffer::Params.from_json_schema(items)] if items[:properties]
84
+
85
+ [json_type_to_ruby(items[:type]), nil]
86
+ end
87
+ private_class_method :resolve_nesting
88
+
89
+ # Resolves a JSON Schema +type+ (a String, or a <tt>[type, "null"]</tt>
90
+ # union) back to its Ruby type. Returns a Module because
91
+ # Riffer::Params::Boolean is a Module, not a Class — the same widening the
92
+ # +type+ attribute uses. Raises Riffer::ArgumentError on a type outside the
93
+ # Params-expressible subset (the block runs only for an unmapped type).
94
+ #
95
+ #--
96
+ #: (untyped) -> Module
97
+ def self.json_type_to_ruby(type)
98
+ key = type.is_a?(Array) ? type.find { |t| t != "null" } : type
99
+ JSON_TYPE_MAPPINGS.fetch(key) { raise Riffer::ArgumentError, "Unsupported JSON Schema type: #{type.inspect}" }
100
+ end
101
+ private_class_method :json_type_to_ruby
102
+
103
+ #--
104
+ #: (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Module?, ?nested_params: Riffer::Params?) -> void
34
105
  def initialize(name:, type:, required:, description: nil, enum: nil, default: nil, item_type: nil, nested_params: nil)
35
106
  @name = name.to_sym
36
107
  @type = type
@@ -74,6 +145,11 @@ class Riffer::Params::Param
74
145
  # constraint from the null type, since providers like Anthropic reject
75
146
  # <tt>{"type": ["string", "null"], "enum": [...]}</tt>.
76
147
  #
148
+ # In non-strict mode a +default+ is emitted when set (a standard JSON
149
+ # Schema keyword), making the schema a lossless source for
150
+ # +Riffer::Params.from_json_schema+. Strict mode omits it, since strict
151
+ # providers reject the keyword.
152
+ #
77
153
  #--
78
154
  #: (?strict: bool) -> Hash[Symbol, untyped]
79
155
  def to_json_schema(strict: false)
@@ -91,6 +167,10 @@ class Riffer::Params::Param
91
167
  schema = {type: type} #: Hash[Symbol, untyped]
92
168
  schema[:description] = description if description
93
169
  schema[:enum] = enum if enum
170
+ # Strict providers reject the +default+ keyword; emit it only in
171
+ # non-strict mode, where it makes the schema a lossless round-trip
172
+ # source for +from_json_schema+.
173
+ schema[:default] = default unless strict || default.nil?
94
174
 
95
175
  if self.type == Array && nested_params
96
176
  schema[:items] = nested_params.to_json_schema(strict: strict)
data/lib/riffer/params.rb CHANGED
@@ -20,10 +20,41 @@ class Riffer::Params
20
20
  @parameters = []
21
21
  end
22
22
 
23
+ # Reconstructs a Params from a JSON Schema object (the inverse of
24
+ # +to_json_schema(strict: false)+).
25
+ #
26
+ # Accepts the Symbol-keyed object schema produced by +to_json_schema+
27
+ # (property-name keys may be String or Symbol — both are normalized).
28
+ # Reconstructs types, +required+, +description+, +enum+, +default+,
29
+ # typed-array +item_type+, and nested object/array Params recursively.
30
+ #
31
+ # Round-trips losslessly with +to_json_schema(strict: false)+ over the
32
+ # Params-expressible subset of JSON Schema. Raises Riffer::ArgumentError
33
+ # on a schema using features outside that subset.
34
+ #
35
+ # schema = params.to_json_schema(strict: false)
36
+ # Riffer::Params.from_json_schema(schema) # => equivalent Riffer::Params
37
+ #
38
+ #--
39
+ #: (Hash[Symbol, untyped]) -> Riffer::Params
40
+ def self.from_json_schema(schema)
41
+ params = new
42
+ properties = schema[:properties] || {}
43
+ required = (schema[:required] || []).map { |key| key.to_s }
44
+
45
+ properties.each do |name, property_schema|
46
+ params.parameters << Riffer::Params::Param.from_json_schema(
47
+ name.to_sym, property_schema, required: required.include?(name.to_s)
48
+ )
49
+ end
50
+
51
+ params
52
+ end
53
+
23
54
  # Defines a required parameter.
24
55
  #
25
56
  #--
26
- #: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
57
+ #: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?of: Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
27
58
  def required(name, type, description: nil, enum: nil, of: nil, &block)
28
59
  nested = build_nested(type, of, &block)
29
60
  @parameters << Riffer::Params::Param.new(
@@ -40,7 +71,7 @@ class Riffer::Params
40
71
  # Defines an optional parameter.
41
72
  #
42
73
  #--
43
- #: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
74
+ #: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
44
75
  def optional(name, type, description: nil, enum: nil, default: nil, of: nil, &block)
45
76
  nested = build_nested(type, of, &block)
46
77
  @parameters << Riffer::Params::Param.new(
@@ -126,7 +157,7 @@ class Riffer::Params
126
157
  private
127
158
 
128
159
  #--
129
- #: (Class, Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
160
+ #: (Module, Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
130
161
  def build_nested(type, of, &block)
131
162
  if of && block
132
163
  raise Riffer::ArgumentError, "cannot use both of: and a block"
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.29.1" #: String
5
+ VERSION = "0.30.0" #: String
6
6
  end
@@ -26,7 +26,7 @@ class Riffer::Agent::Config
26
26
 
27
27
  attr_reader structured_output: Riffer::Params?
28
28
 
29
- attr_accessor max_steps: Numeric
29
+ attr_accessor max_steps: Numeric?
30
30
 
31
31
  attr_accessor tools_config: (Array[singleton(Riffer::Tool)] | Proc)?
32
32
 
@@ -45,8 +45,8 @@ class Riffer::Agent::Config
45
45
  # as a non-String, non-Proc value (or as an empty String).
46
46
  #
47
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
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
50
 
51
51
  # Sets +identifier+. Accepts +nil+ or any value, coerced to String.
52
52
  #
@@ -0,0 +1,132 @@
1
+ # Generated from lib/riffer/agent/serializer.rb with RBS::Inline
2
+
3
+ # Riffer::Agent::Serializer turns a resolved agent into a self-contained,
4
+ # provider-neutral data dict and back into a runnable agent. A pure module
5
+ # (sibling to Riffer::Agent::Run), reached most often through the
6
+ # +Riffer::Agent#to_h+ / +Riffer::Agent.from_h+ delegators.
7
+ #
8
+ # The dict carries only data — no Procs, no class references, no tool
9
+ # runtime. The same dict serves two rehydration targets:
10
+ #
11
+ # - <b>In-process</b> (a monolith persisting agent definitions): pass a
12
+ # +tool_resolver+ that looks tool descriptors up in a local registry and
13
+ # returns the real, body-bearing classes. They run on the default runtime.
14
+ # - <b>Distributed</b> (a receiver holding only the Riffer gem): the default
15
+ # resolver synthesizes body-less tool shells; inject a remote
16
+ # +Riffer::Tools::Runtime+ to forward each call back to the origin.
17
+ #
18
+ # dict = Riffer::Agent::Serializer.to_h(agent: agent)
19
+ # rebuilt = Riffer::Agent::Serializer.from_h(dict, context: {tenant: "acme"})
20
+ #
21
+ # == What does not transfer
22
+ #
23
+ # Guardrails and the skills subsystem (backend/adapter/catalog) are not
24
+ # serialized; a rebuilt agent enforces no guardrails and renders no skills
25
+ # catalog (the +skill_activate+ tool, if present, crosses as an ordinary
26
+ # tool). Secrets must not be placed in +provider_options+/+model_options+:
27
+ # both ride on the wire as plain data.
28
+ module Riffer::Agent::Serializer
29
+ # The wire format version. Bumped only on an incompatible change to the
30
+ # dict shape; +from_h+ refuses any other version. See +from_h+ for the
31
+ # dispatch seam that carries back-compat decoders.
32
+ SCHEMA_VERSION: Integer
33
+
34
+ # Raised by +from_h+ when the dict's +schema_version+ is unsupported.
35
+ class VersionError < Riffer::ArgumentError
36
+ end
37
+
38
+ # The default +tool_resolver+: synthesizes a body-less tool shell from a
39
+ # descriptor. Its +#call+ raises — route shells through a remote runtime.
40
+ DEFAULT_TOOL_RESOLVER: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool)
41
+
42
+ # Snapshots a resolved agent into a self-contained wire dict.
43
+ #
44
+ # Reads the agent's resolved instance state — Proc-based settings have
45
+ # already been evaluated against the agent's own context, so the dict
46
+ # carries plain strings/data, never Procs. Tools are emitted as
47
+ # +{name, description, parameters_schema, timeout}+ descriptors (the
48
+ # resolved +agent.tools+, including MCP tools and +skill_activate+).
49
+ #
50
+ # [agent] a resolved Riffer::Agent instance.
51
+ #
52
+ # --
53
+ # : (agent: Riffer::Agent) -> Hash[Symbol, untyped]
54
+ def to_h: (agent: Riffer::Agent) -> Hash[Symbol, untyped]
55
+
56
+ # Reconstructs a runnable agent from a wire dict.
57
+ #
58
+ # [hash] a Symbol-keyed wire dict (parse JSON with +symbolize_names: true+).
59
+ # [context] the rebuilt agent's runtime context — the same value you'd pass
60
+ # to +Agent.new(context:)+. It is *not* used to re-resolve serialized
61
+ # config (the dict is already resolved); it is threaded into tool dispatch
62
+ # and read by tools/runtimes at call time (e.g. a remote runtime keying off
63
+ # <tt>context[:tenant]</tt>). Defaults to an empty context.
64
+ # [tool_resolver] maps a tool descriptor to a Riffer::Tool class. Defaults
65
+ # to DEFAULT_TOOL_RESOLVER (body-less shells). Pass a registry lookup to
66
+ # rebuild real, in-process tools.
67
+ # [tool_runtime] an optional Riffer::Tools::Runtime to inject (e.g. a
68
+ # remote runtime for shells). When omitted, the agent uses the configured
69
+ # default (+Riffer.config.tool_runtime+).
70
+ #
71
+ # Raises Riffer::Agent::Serializer::VersionError on an unsupported
72
+ # +schema_version+, and Riffer::ArgumentError on a malformed dict.
73
+ #
74
+ # --
75
+ # : (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
76
+ def from_h: (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
77
+
78
+ # Snapshots a resolved agent to a JSON string. Convenience over
79
+ # <tt>JSON.generate(to_h(agent:))</tt>.
80
+ #
81
+ # --
82
+ # : (agent: Riffer::Agent) -> String
83
+ def to_json: (agent: Riffer::Agent) -> String
84
+
85
+ # Reconstructs a runnable agent from a JSON string produced by +to_json+.
86
+ # Handles the JSON parse (with symbol keys) so callers don't have to. See
87
+ # +from_h+ for the arguments.
88
+ #
89
+ # --
90
+ # : (String, ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
91
+ def from_json: (String, ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
92
+
93
+ private
94
+
95
+ # --
96
+ # : (Hash[Symbol, untyped], context: Hash[Symbol, untyped]?, tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
97
+ def decode_v1: (Hash[Symbol, untyped], context: Hash[Symbol, untyped]?, tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
98
+
99
+ # --
100
+ # : (Hash[Symbol, untyped]?) -> Riffer::Params?
101
+ def decode_structured_output: (Hash[Symbol, untyped]?) -> Riffer::Params?
102
+
103
+ # The DSL represents unlimited steps as +nil+, but the wire encodes it as
104
+ # +-1+ so the dict stays portable across transports where JSON +null+ is
105
+ # awkward (e.g. proto3, which can't tell null from an absent field). The
106
+ # magic value lives only on the wire — +encode_max_steps+/+decode_max_steps+
107
+ # translate at the boundary so neither the DSL nor consumers see it.
108
+ # --
109
+ # : (Numeric?) -> Numeric
110
+ def encode_max_steps: (Numeric?) -> Numeric
111
+
112
+ # Reverses +encode_max_steps+: +-1+ (or a literal +null+) means unlimited.
113
+ # An absent key falls back to the default — a partial dict must not silently
114
+ # become an unbounded loop.
115
+ # --
116
+ # : (Hash[Symbol, untyped]) -> Numeric?
117
+ def decode_max_steps: (Hash[Symbol, untyped]) -> Numeric?
118
+
119
+ # --
120
+ # : (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
121
+ def tool_descriptor: (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
122
+
123
+ # Builds an anonymous, body-less Riffer::Tool subclass that advertises the
124
+ # descriptor's schema to the LLM. Its +#call+ raises — a shell only has
125
+ # identity, not behavior; route its calls through a remote runtime.
126
+ #
127
+ # Returns +untyped+: steep can't see that +Class.new(Riffer::Tool)+ is a
128
+ # +singleton(Riffer::Tool)+ (cf. Riffer::Mcp::ToolFactory#build_tool_class).
129
+ # --
130
+ # : (Hash[Symbol, untyped]) -> untyped
131
+ def build_tool_shell: (Hash[Symbol, untyped]) -> untyped
132
+ end
@@ -80,12 +80,17 @@ class Riffer::Agent
80
80
 
81
81
  # Gets or sets the maximum number of LLM call steps in the tool-use loop.
82
82
  #
83
- # Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to
84
- # +Float::INFINITY+ for unlimited steps.
83
+ # Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to +nil+
84
+ # for unlimited steps. The splat distinguishes a getter call (no argument)
85
+ # from setting the limit to +nil+.
86
+ #
87
+ # max_steps # reads the current limit
88
+ # max_steps 8 # cap the loop at 8 steps
89
+ # max_steps nil # unlimited
85
90
  #
86
91
  # --
87
- # : (?Numeric?) -> Numeric
88
- def self.max_steps: (?Numeric?) -> Numeric
92
+ # : (*Numeric?) -> Numeric?
93
+ def self.max_steps: (*Numeric?) -> Numeric?
89
94
 
90
95
  # Gets or sets the tools used by this agent.
91
96
  #
@@ -159,6 +164,26 @@ class Riffer::Agent
159
164
  # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
160
165
  def self.stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
161
166
 
167
+ # Reconstructs a runnable agent from a wire dict produced by +#to_h+.
168
+ #
169
+ # Delegates to Riffer::Agent::Serializer.from_h. See it for the
170
+ # +tool_resolver+ / +tool_runtime+ injection points and what does not
171
+ # transfer.
172
+ #
173
+ # --
174
+ # : (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
175
+ def self.from_h: (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
176
+
177
+ # Reconstructs a runnable agent from a JSON string produced by +#to_json+.
178
+ #
179
+ # Delegates to Riffer::Agent::Serializer.from_json, which parses the JSON
180
+ # (with symbol keys) for you. See Riffer::Agent::Serializer.from_h for the
181
+ # +tool_resolver+ / +tool_runtime+ injection points.
182
+ #
183
+ # --
184
+ # : (String, ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
185
+ def self.from_json: (String, ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
186
+
162
187
  # Registers a guardrail for input, output, or both phases.
163
188
  #
164
189
  # [phase] :before, :after, or :around.
@@ -210,6 +235,11 @@ class Riffer::Agent
210
235
  # reserved and cannot be passed by the caller.
211
236
  attr_reader context: Riffer::Agent::Context
212
237
 
238
+ # The resolved provider name (the part before "provider/"), e.g. +"openai"+.
239
+ # Resolved eagerly at +Agent.new+ alongside +model_name+; together they
240
+ # form the provider-neutral model identifier the agent serializes.
241
+ attr_reader provider_name: String
242
+
213
243
  # The resolved model name (the part after "provider/"), used as the model
214
244
  # argument on every LLM call. Resolved eagerly at +Agent.new+.
215
245
  attr_reader model_name: String
@@ -300,6 +330,21 @@ class Riffer::Agent
300
330
  # : (?(String | Symbol)?) -> void
301
331
  def interrupt!: (?(String | Symbol)?) -> void
302
332
 
333
+ # Snapshots this resolved agent into a self-contained, provider-neutral
334
+ # wire dict. Delegates to Riffer::Agent::Serializer.to_h.
335
+ #
336
+ # --
337
+ # : () -> Hash[Symbol, untyped]
338
+ def to_h: () -> Hash[Symbol, untyped]
339
+
340
+ # Snapshots this resolved agent into a wire JSON string. Delegates to
341
+ # Riffer::Agent::Serializer.to_json. The +*+ absorbs the JSON generator
342
+ # state argument so <tt>JSON.generate(agent)</tt> works too.
343
+ #
344
+ # --
345
+ # : (*untyped) -> String
346
+ def to_json: (*untyped) -> String
347
+
303
348
  private
304
349
 
305
350
  # --
@@ -311,21 +356,30 @@ class Riffer::Agent
311
356
  def build_skills_message: () -> Riffer::Messages::System?
312
357
 
313
358
  # Resolves +Config#model+ to a "provider/model" string (calling the Proc
314
- # form against +@context+), parses it, and looks up the provider class.
359
+ # form against +@context+) and parses it.
360
+ #
361
+ # Returns +[provider_name, model_name]+. Raises Riffer::ArgumentError on an
362
+ # invalid model string.
363
+ #
364
+ # --
365
+ # : () -> [String, String]
366
+ def resolve_provider_and_model: () -> [ String, String ]
367
+
368
+ # Builds the provider client from the resolved +@provider_name+ and the
369
+ # configured +provider_options+.
315
370
  #
316
- # Returns +[provider_class, model_name]+. Raises Riffer::ArgumentError on
317
- # an invalid model string or an unregistered provider.
371
+ # Raises Riffer::ArgumentError on an unregistered provider.
318
372
  #
319
373
  # --
320
- # : () -> [singleton(Riffer::Providers::Base), String]
321
- def resolve_provider_and_model: () -> [ singleton(Riffer::Providers::Base), String ]
374
+ # : () -> Riffer::Providers::Base
375
+ def build_provider: () -> Riffer::Providers::Base
322
376
 
323
377
  # Resolves the skills backend, lists skills, and selects an adapter.
324
378
  # Returns nil if skills are unconfigured or the backend is empty.
325
379
  #
326
380
  # --
327
- # : (singleton(Riffer::Providers::Base)) -> Riffer::Skills::Context?
328
- def resolve_skills: (singleton(Riffer::Providers::Base)) -> Riffer::Skills::Context?
381
+ # : () -> Riffer::Skills::Context?
382
+ def resolve_skills: () -> Riffer::Skills::Context?
329
383
 
330
384
  # --
331
385
  # : () -> Riffer::Agent::StructuredOutput?
@@ -8,11 +8,16 @@ class Riffer::Params::Param
8
8
  TYPE_MAPPINGS: Hash[Module, String]
9
9
 
10
10
  # Primitive types allowed for the <tt>of:</tt> keyword on Array params
11
- PRIMITIVE_TYPES: Array[Class]
11
+ PRIMITIVE_TYPES: Array[Module]
12
+
13
+ # Maps JSON Schema type strings back to Ruby types. The inverse of
14
+ # TYPE_MAPPINGS, collapsing the three boolean spellings onto
15
+ # Riffer::Params::Boolean. Used by +from_json_schema+.
16
+ JSON_TYPE_MAPPINGS: Hash[String, Module]
12
17
 
13
18
  attr_reader name: Symbol
14
19
 
15
- attr_reader type: Class
20
+ attr_reader type: Module
16
21
 
17
22
  attr_reader required: bool
18
23
 
@@ -22,13 +27,44 @@ class Riffer::Params::Param
22
27
 
23
28
  attr_reader default: untyped
24
29
 
25
- attr_reader item_type: Class?
30
+ attr_reader item_type: Module?
26
31
 
27
32
  attr_reader nested_params: Riffer::Params?
28
33
 
29
34
  # --
30
- # : (name: Symbol, type: Class, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Class?, ?nested_params: Riffer::Params?) -> void
31
- def initialize: (name: Symbol, type: Class, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Class?, ?nested_params: Riffer::Params?) -> void
35
+ # Reconstructs a Param from a single JSON Schema property.
36
+ #
37
+ # [name] the parameter name (Symbol).
38
+ # [schema] the property's JSON Schema (Symbol-keyed).
39
+ # [required] whether the property appeared in the parent's +required+ list.
40
+ #
41
+ # Raises Riffer::ArgumentError on a type outside the Params-expressible subset.
42
+ #
43
+ # --
44
+ # : (Symbol, Hash[Symbol, untyped], required: bool) -> Riffer::Params::Param
45
+ def self.from_json_schema: (Symbol, Hash[Symbol, untyped], required: bool) -> Riffer::Params::Param
46
+
47
+ # Resolves the +[item_type, nested_params]+ pair for a reconstructed Param:
48
+ # a nested Params for object / array-of-object schemas, an +item_type+ for
49
+ # typed primitive arrays, and +nil+ for everything else.
50
+ #
51
+ # --
52
+ # : (Module, Hash[Symbol, untyped]) -> [Module?, Riffer::Params?]
53
+ def self.resolve_nesting: (Module, Hash[Symbol, untyped]) -> [ Module?, Riffer::Params? ]
54
+
55
+ # Resolves a JSON Schema +type+ (a String, or a <tt>[type, "null"]</tt>
56
+ # union) back to its Ruby type. Returns a Module because
57
+ # Riffer::Params::Boolean is a Module, not a Class — the same widening the
58
+ # +type+ attribute uses. Raises Riffer::ArgumentError on a type outside the
59
+ # Params-expressible subset (the block runs only for an unmapped type).
60
+ #
61
+ # --
62
+ # : (untyped) -> Module
63
+ def self.json_type_to_ruby: (untyped) -> Module
64
+
65
+ # --
66
+ # : (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Module?, ?nested_params: Riffer::Params?) -> void
67
+ def initialize: (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Module?, ?nested_params: Riffer::Params?) -> void
32
68
 
33
69
  # Validates that a value matches the expected type.
34
70
  #
@@ -52,6 +88,11 @@ class Riffer::Params::Param
52
88
  # constraint from the null type, since providers like Anthropic reject
53
89
  # <tt>{"type": ["string", "null"], "enum": [...]}</tt>.
54
90
  #
91
+ # In non-strict mode a +default+ is emitted when set (a standard JSON
92
+ # Schema keyword), making the schema a lossless source for
93
+ # +Riffer::Params.from_json_schema+. Strict mode omits it, since strict
94
+ # providers reject the keyword.
95
+ #
55
96
  # --
56
97
  # : (?strict: bool) -> Hash[Symbol, untyped]
57
98
  def to_json_schema: (?strict: bool) -> Hash[Symbol, untyped]
@@ -16,17 +16,36 @@ class Riffer::Params
16
16
  # : () -> void
17
17
  def initialize: () -> void
18
18
 
19
+ # Reconstructs a Params from a JSON Schema object (the inverse of
20
+ # +to_json_schema(strict: false)+).
21
+ #
22
+ # Accepts the Symbol-keyed object schema produced by +to_json_schema+
23
+ # (property-name keys may be String or Symbol — both are normalized).
24
+ # Reconstructs types, +required+, +description+, +enum+, +default+,
25
+ # typed-array +item_type+, and nested object/array Params recursively.
26
+ #
27
+ # Round-trips losslessly with +to_json_schema(strict: false)+ over the
28
+ # Params-expressible subset of JSON Schema. Raises Riffer::ArgumentError
29
+ # on a schema using features outside that subset.
30
+ #
31
+ # schema = params.to_json_schema(strict: false)
32
+ # Riffer::Params.from_json_schema(schema) # => equivalent Riffer::Params
33
+ #
34
+ # --
35
+ # : (Hash[Symbol, untyped]) -> Riffer::Params
36
+ def self.from_json_schema: (Hash[Symbol, untyped]) -> Riffer::Params
37
+
19
38
  # Defines a required parameter.
20
39
  #
21
40
  # --
22
- # : (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
23
- def required: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
41
+ # : (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?of: Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
42
+ def required: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?of: Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
24
43
 
25
44
  # Defines an optional parameter.
26
45
  #
27
46
  # --
28
- # : (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
29
- def optional: (Symbol, Class, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
47
+ # : (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
48
+ def optional: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?of: Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
30
49
 
31
50
  # Validates arguments against parameter definitions.
32
51
  #
@@ -49,8 +68,8 @@ class Riffer::Params
49
68
  private
50
69
 
51
70
  # --
52
- # : (Class, Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
53
- def build_nested: (Class, Class?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
71
+ # : (Module, Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
72
+ def build_nested: (Module, Module?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
54
73
 
55
74
  # --
56
75
  # : (Riffer::Params::Param, untyped, Array[String]) -> untyped
@@ -0,0 +1,5 @@
1
+ # `Riffer::Agent::Serializer` uses `extend self`; rbs-inline doesn't emit that,
2
+ # so re-extend here to expose its instance methods as singleton methods.
3
+ module Riffer::Agent::Serializer
4
+ extend ::Riffer::Agent::Serializer
5
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riffer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.1
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall
@@ -277,6 +277,7 @@ files:
277
277
  - docs/12_GUARDRAILS.md
278
278
  - docs/13_SKILLS.md
279
279
  - docs/14_MCP.md
280
+ - docs/15_SERIALIZATION.md
280
281
  - docs/providers/01_PROVIDERS.md
281
282
  - docs/providers/02_AMAZON_BEDROCK.md
282
283
  - docs/providers/03_ANTHROPIC.md
@@ -292,6 +293,7 @@ files:
292
293
  - lib/riffer/agent/context.rb
293
294
  - lib/riffer/agent/response.rb
294
295
  - lib/riffer/agent/run.rb
296
+ - lib/riffer/agent/serializer.rb
295
297
  - lib/riffer/agent/session.rb
296
298
  - lib/riffer/agent/session/repair.rb
297
299
  - lib/riffer/agent/structured_output.rb
@@ -398,6 +400,7 @@ files:
398
400
  - sig/generated/riffer/agent/context.rbs
399
401
  - sig/generated/riffer/agent/response.rbs
400
402
  - sig/generated/riffer/agent/run.rbs
403
+ - sig/generated/riffer/agent/serializer.rbs
401
404
  - sig/generated/riffer/agent/session.rbs
402
405
  - sig/generated/riffer/agent/session/repair.rbs
403
406
  - sig/generated/riffer/agent/structured_output.rbs
@@ -489,6 +492,7 @@ files:
489
492
  - sig/generated/riffer/version.rbs
490
493
  - sig/manifest.yaml
491
494
  - sig/manual/riffer/agent/run.rbs
495
+ - sig/manual/riffer/agent/serializer.rbs
492
496
  - sig/manual/riffer/helpers/call_or_value.rbs
493
497
  - sig/manual/riffer/tools/toolable.rbs
494
498
  homepage: https://riffer.ai