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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +11 -0
- data/README.md +1 -0
- data/docs/01_OVERVIEW.md +1 -0
- data/docs/03_AGENTS.md +1 -1
- data/docs/15_SERIALIZATION.md +103 -0
- data/lib/riffer/agent/config.rb +2 -2
- data/lib/riffer/agent/run.rb +2 -1
- data/lib/riffer/agent/serializer.rb +215 -0
- data/lib/riffer/agent.rb +82 -18
- data/lib/riffer/params/param.rb +84 -4
- data/lib/riffer/params.rb +34 -3
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/agent/config.rbs +3 -3
- data/sig/generated/riffer/agent/serializer.rbs +132 -0
- data/sig/generated/riffer/agent.rbs +65 -11
- data/sig/generated/riffer/params/param.rbs +46 -5
- data/sig/generated/riffer/params.rbs +25 -6
- data/sig/manual/riffer/agent/serializer.rbs +5 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7557c63ca04fa006d66a61a53bef6b1f7069ebab67439f00104fbb877d1aceda
|
|
4
|
+
data.tar.gz: 39bbe45b7e111ae343e32c54b9769db17dd7d8d9b98cd5cb7d42338aa5e4179c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 61694198a05d63d831dca9b4b37e38b6cacbd7b67214fa47c2027a0a7e1636ee07e5f85641f9fe00ee597d66dfdb0a6fccd0e494b6ec670a84a2020907cfdd0b
|
|
7
|
+
data.tar.gz: fbff4457d39da36a1c77e9cb7ea86ccb5e96f0887ba982f6a8a960557ee4beceda3a604011fcfd20951f9baedd59eadbf188832cb6bcc8fcf71e256d93e8cdc3
|
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 `
|
|
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
|
data/lib/riffer/agent/config.rb
CHANGED
|
@@ -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
|
|
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,
|
data/lib/riffer/agent/run.rb
CHANGED
|
@@ -77,7 +77,8 @@ module Riffer::Agent::Run
|
|
|
77
77
|
|
|
78
78
|
break unless processed_response.has_tool_calls?
|
|
79
79
|
|
|
80
|
-
|
|
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
|
-
#
|
|
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
|
-
#: (
|
|
110
|
-
def self.max_steps(value
|
|
111
|
-
|
|
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
|
-
|
|
312
|
-
@provider =
|
|
346
|
+
@provider_name, @model_name = resolve_provider_and_model
|
|
347
|
+
@provider = build_provider
|
|
313
348
|
|
|
314
|
-
@context.skills = resolve_skills
|
|
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+)
|
|
452
|
+
# form against +@context+) and parses it.
|
|
399
453
|
#
|
|
400
|
-
# Returns +[
|
|
401
|
-
#
|
|
454
|
+
# Returns +[provider_name, model_name]+. Raises Riffer::ArgumentError on an
|
|
455
|
+
# invalid model string.
|
|
402
456
|
#
|
|
403
457
|
#--
|
|
404
|
-
#: () -> [
|
|
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
|
-
|
|
416
|
-
|
|
469
|
+
[provider_name, model_name]
|
|
470
|
+
end
|
|
417
471
|
|
|
418
|
-
|
|
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
|
-
#: (
|
|
426
|
-
def resolve_skills
|
|
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 ||
|
|
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(
|
data/lib/riffer/params/param.rb
CHANGED
|
@@ -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[
|
|
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 #:
|
|
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 #:
|
|
41
|
+
attr_reader :item_type #: Module?
|
|
30
42
|
attr_reader :nested_params #: Riffer::Params?
|
|
31
43
|
|
|
32
44
|
#--
|
|
33
|
-
|
|
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,
|
|
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,
|
|
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
|
-
#: (
|
|
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"
|
data/lib/riffer/version.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
#
|
|
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
|
-
# : (
|
|
88
|
-
def self.max_steps: (
|
|
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+)
|
|
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
|
-
#
|
|
317
|
-
# an invalid model string or an unregistered provider.
|
|
371
|
+
# Raises Riffer::ArgumentError on an unregistered provider.
|
|
318
372
|
#
|
|
319
373
|
# --
|
|
320
|
-
# : () ->
|
|
321
|
-
def
|
|
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
|
-
# : (
|
|
328
|
-
def resolve_skills: (
|
|
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[
|
|
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:
|
|
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:
|
|
30
|
+
attr_reader item_type: Module?
|
|
26
31
|
|
|
27
32
|
attr_reader nested_params: Riffer::Params?
|
|
28
33
|
|
|
29
34
|
# --
|
|
30
|
-
#
|
|
31
|
-
|
|
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,
|
|
23
|
-
def required: (Symbol,
|
|
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,
|
|
29
|
-
def optional: (Symbol,
|
|
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
|
-
# : (
|
|
53
|
-
def build_nested: (
|
|
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
|
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.
|
|
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
|