riffer 0.29.0 → 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/.agents/rbs-inline.md +51 -0
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +18 -0
- data/README.md +1 -0
- data/Steepfile +2 -1
- 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/context.rb +2 -0
- data/lib/riffer/agent/response.rb +2 -0
- data/lib/riffer/agent/run.rb +2 -1
- data/lib/riffer/agent/serializer.rb +215 -0
- data/lib/riffer/agent/session.rb +2 -0
- data/lib/riffer/agent.rb +84 -18
- data/lib/riffer/evals/evaluator.rb +5 -0
- data/lib/riffer/evals/judge.rb +5 -0
- data/lib/riffer/mcp/client.rb +2 -0
- data/lib/riffer/mcp/registration.rb +4 -0
- data/lib/riffer/mcp/registry.rb +3 -0
- data/lib/riffer/messages/file_part.rb +2 -0
- data/lib/riffer/params/param.rb +84 -4
- data/lib/riffer/params.rb +34 -3
- data/lib/riffer/providers/amazon_bedrock.rb +28 -21
- data/lib/riffer/providers/anthropic.rb +13 -9
- data/lib/riffer/providers/base.rb +2 -0
- data/lib/riffer/providers/gemini.rb +4 -0
- data/lib/riffer/providers/mock.rb +4 -0
- data/lib/riffer/providers/open_ai.rb +10 -7
- data/lib/riffer/providers/open_router.rb +25 -18
- data/lib/riffer/runner/fibers.rb +2 -0
- data/lib/riffer/runner/threaded.rb +2 -0
- data/lib/riffer/skills/config.rb +5 -0
- data/lib/riffer/skills/context.rb +3 -0
- data/lib/riffer/skills/filesystem_backend.rb +3 -0
- data/lib/riffer/tools/response.rb +2 -0
- data/lib/riffer/tools/runtime.rb +2 -0
- data/lib/riffer/tools/toolable.rb +7 -0
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -0
- data/sig/_private/anthropic.rbs +16 -0
- data/sig/_private/openai.rbs +29 -0
- data/sig/_private/riffer/providers/amazon_bedrock.rbs +4 -0
- data/sig/_private/riffer/providers/anthropic.rbs +4 -0
- data/sig/_private/riffer/providers/open_ai.rbs +4 -0
- data/sig/_private/riffer/providers/open_router.rbs +4 -0
- data/sig/generated/riffer/agent/config.rbs +3 -3
- data/sig/generated/riffer/agent/context.rbs +2 -0
- data/sig/generated/riffer/agent/response.rbs +2 -0
- data/sig/generated/riffer/agent/serializer.rbs +132 -0
- data/sig/generated/riffer/agent/session.rbs +2 -0
- data/sig/generated/riffer/agent.rbs +67 -11
- data/sig/generated/riffer/evals/evaluator.rbs +8 -0
- data/sig/generated/riffer/evals/judge.rbs +8 -0
- data/sig/generated/riffer/mcp/client.rbs +2 -0
- data/sig/generated/riffer/mcp/registration.rbs +6 -0
- data/sig/generated/riffer/mcp/registry.rbs +4 -0
- data/sig/generated/riffer/messages/file_part.rbs +2 -0
- data/sig/generated/riffer/params/param.rbs +46 -5
- data/sig/generated/riffer/params.rbs +25 -6
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +20 -20
- data/sig/generated/riffer/providers/anthropic.rbs +10 -10
- data/sig/generated/riffer/providers/base.rbs +2 -0
- data/sig/generated/riffer/providers/gemini.rbs +6 -0
- data/sig/generated/riffer/providers/mock.rbs +6 -0
- data/sig/generated/riffer/providers/open_ai.rbs +8 -8
- data/sig/generated/riffer/providers/open_router.rbs +16 -16
- data/sig/generated/riffer/runner/fibers.rbs +2 -0
- data/sig/generated/riffer/runner/threaded.rbs +2 -0
- data/sig/generated/riffer/skills/config.rbs +8 -0
- data/sig/generated/riffer/skills/context.rbs +4 -0
- data/sig/generated/riffer/skills/filesystem_backend.rbs +4 -0
- data/sig/generated/riffer/tools/response.rbs +2 -0
- data/sig/generated/riffer/tools/runtime.rbs +2 -0
- data/sig/generated/riffer/tools/toolable.rbs +12 -0
- data/sig/generated/riffer.rbs +2 -0
- data/sig/manifest.yaml +3 -0
- data/sig/manual/riffer/agent/run.rbs +5 -0
- data/sig/manual/riffer/agent/serializer.rbs +5 -0
- data/sig/manual/riffer/helpers/call_or_value.rbs +5 -0
- data/sig/manual/riffer/tools/toolable.rbs +6 -0
- metadata +20 -11
- data/sig/stubs/agent_ivars.rbs +0 -7
- data/sig/stubs/extend_self.rbs +0 -11
- data/sig/stubs/lib_ivars.rbs +0 -101
- data/sig/stubs/provider_ivars.rbs +0 -36
- data/sig/stubs/provider_sdk_methods.rbs +0 -50
- /data/sig/{stubs → _private}/async.rbs +0 -0
- /data/sig/{stubs → _private}/aws-sdk-core/seahorse_request_context.rbs +0 -0
- /data/sig/{stubs → _private}/aws-sdk-core/static_token_provider.rbs +0 -0
- /data/sig/{stubs/mcp_sdk.rbs → _private/mcp.rbs} +0 -0
- /data/sig/{stubs → _private}/zeitwerk.rbs +0 -0
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/.agents/rbs-inline.md
CHANGED
|
@@ -98,6 +98,23 @@ VERSION = "1.0.0" #: String
|
|
|
98
98
|
DEFAULTS = {}.freeze #: Hash[Symbol, untyped]
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
### Instance variables
|
|
102
|
+
|
|
103
|
+
The `#:` shorthand on an assignment is a Steep _assertion_ — it types the expression but does
|
|
104
|
+
**not declare the ivar**. To declare an ivar's type, use a `# @rbs` comment inside the class
|
|
105
|
+
body. `# @rbs` is used **only** for ivar declarations in this codebase; everything else uses
|
|
106
|
+
`#:`.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
class Riffer::Agent::Session
|
|
110
|
+
# @rbs @callbacks: Array[^(Riffer::Messages::Base) -> void] # instance ivar
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
module Riffer
|
|
114
|
+
# @rbs self.@config: Riffer::Config? # class/module-level ivar
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
101
118
|
## Common Type Patterns
|
|
102
119
|
|
|
103
120
|
| Pattern | Meaning |
|
|
@@ -112,6 +129,40 @@ DEFAULTS = {}.freeze #: Hash[Symbol, untyped]
|
|
|
112
129
|
| `untyped` | Any type |
|
|
113
130
|
| `void` | No meaningful return |
|
|
114
131
|
|
|
132
|
+
## Optional-dependency types (consumer-safe signatures)
|
|
133
|
+
|
|
134
|
+
`sig/generated/` ships with the gem and is loaded by downstream projects (`rbs collection` / `rbs -r riffer`). rbs-inline copies a method's `#:` signature **verbatim** into the shipped sig, so **never name an optional-dependency type in a `#:` signature** — `OpenAI::*`, `Anthropic::*`, `Aws::*`, `MCP::*`, `Async::*`, `Zeitwerk::*`, etc. A consumer who installs riffer without that gem would hit `Cannot find type`, because those providers are pluggable and the gems ship no usable RBS of their own.
|
|
135
|
+
|
|
136
|
+
**Workaround — assert the type inside the method body instead.** An inline assertion (`local = arg #: OpenAI::Models::…`) is a Steep-only hint that rbs-inline does **not** emit into the signature. Leave the SDK param/return `untyped` in the `#:` line, keep every riffer/stdlib param and return typed, and recover the SDK type with a body assertion:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
#: (untyped) -> String
|
|
140
|
+
def extract_content(response)
|
|
141
|
+
message = response #: Anthropic::Models::Message
|
|
142
|
+
message.content&.first&.text || "" # fully type-checked against the SDK type
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
#: (untyped, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
|
|
146
|
+
def handle_stream_chunk(chunk, state:, yielder:)
|
|
147
|
+
typed = chunk #: OpenAI::Models::Chat::ChatCompletionChunk
|
|
148
|
+
# ...
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# When the return value IS the SDK object, type the return `untyped` (no body assertion needed):
|
|
152
|
+
#: (Hash[Symbol, untyped]) -> untyped
|
|
153
|
+
def execute_generate(params)
|
|
154
|
+
@client.messages.create(**params)
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`test/shipped_signatures_test.rb` enforces this — it fails if any optional-dependency type appears in `sig/generated/` or `sig/manual/`.
|
|
159
|
+
|
|
160
|
+
### Where stubs and stdlib deps live
|
|
161
|
+
|
|
162
|
+
- `sig/_private/` — signatures that must **not** ship. RBS **skips** `_`-prefixed directories in library mode, so consumers never load them; riffer's own `steep check` does (via the `Steepfile`). Two kinds, by predictable path: external-gem signatures are named by gem at the top level (`async.rbs`, `mcp.rbs`, `zeitwerk.rbs`, `openai.rbs`, `anthropic.rbs`, `aws-sdk-core/*` — full stubs for RBS-less gems plus arity patches for the provider SDKs); riffer's own hidden stubs mirror `lib/` under `riffer/` (e.g. `riffer/providers/anthropic.rbs` declares the SDK-typed `@client` ivar).
|
|
163
|
+
- `sig/manual/` — hand-written riffer-only signatures that are **safe to ship**, for the few things rbs-inline can't generate _at all_ (mirroring `lib/`). In practice that's `extend self` modules (`riffer/agent/run.rbs`, `riffer/helpers/call_or_value.rbs`) and modeling an include applied dynamically (`riffer/tools/toolable.rbs`). SDK-free ivars are **not** hand-written here — declare them inline with `# @rbs` (see "Instance variables"). SDK-typed ivars can't ship, so they go in `_private/riffer/providers/` (`@client`).
|
|
164
|
+
- `sig/manifest.yaml` — declares the **stdlib** RBS the shipped sigs reference (`uri`, `net-http`) so `rbs -r riffer` resolves them.
|
|
165
|
+
|
|
115
166
|
## Workflow
|
|
116
167
|
|
|
117
168
|
After changing type annotations:
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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
|
+
|
|
19
|
+
## [0.29.1](https://github.com/janeapp/riffer/compare/riffer/v0.29.0...riffer/v0.29.1) (2026-06-01)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **rbs:** ship consumer-safe RBS signatures ([#286](https://github.com/janeapp/riffer/issues/286)) ([ac8ce6c](https://github.com/janeapp/riffer/commit/ac8ce6c40ab665456cee6cd9275649024492f4a0))
|
|
25
|
+
|
|
8
26
|
## [0.29.0](https://github.com/janeapp/riffer/compare/riffer/v0.28.0...riffer/v0.29.0) (2026-05-29)
|
|
9
27
|
|
|
10
28
|
|
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/Steepfile
CHANGED
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/context.rb
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
# context.token_usage # => nil
|
|
20
20
|
#
|
|
21
21
|
class Riffer::Agent::Context
|
|
22
|
+
# @rbs @data: Hash[Symbol, untyped]
|
|
23
|
+
|
|
22
24
|
# Keys reserved for framework use. Passing any of these to the
|
|
23
25
|
# constructor raises +Riffer::ArgumentError+.
|
|
24
26
|
RESERVED_KEYS = [:skills, :token_usage].freeze #: Array[Symbol]
|
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/session.rb
CHANGED