riffer-rig 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +19 -13
  4. data/lib/riffer/rig/cli/flags.rb +8 -1
  5. data/lib/riffer/rig/commands/reasoning.rb +46 -0
  6. data/lib/riffer/rig/env.rb +3 -1
  7. data/lib/riffer/rig/headless.rb +2 -0
  8. data/lib/riffer/rig/loader/options.rb +5 -1
  9. data/lib/riffer/rig/loader.rb +57 -3
  10. data/lib/riffer/rig/registrar.rb +3 -1
  11. data/lib/riffer/rig/runtime/snapshot.rb +7 -3
  12. data/lib/riffer/rig/runtime.rb +42 -10
  13. data/lib/riffer/rig/settings/document.rb +5 -5
  14. data/lib/riffer/rig/settings/file.rb +7 -0
  15. data/lib/riffer/rig/settings/model_options.rb +13 -33
  16. data/lib/riffer/rig/settings.rb +4 -1
  17. data/lib/riffer/rig/stores/entries.rb +3 -0
  18. data/lib/riffer/rig/stores/jsonl.rb +1 -0
  19. data/lib/riffer/rig/stores/reasoning_entry.rb +31 -0
  20. data/lib/riffer/rig/stores/recorder.rb +11 -4
  21. data/lib/riffer/rig/terminal/console.rb +13 -1
  22. data/lib/riffer/rig/terminal/count.rb +14 -0
  23. data/lib/riffer/rig/terminal/frame.rb +176 -51
  24. data/lib/riffer/rig/terminal/frame_prompter.rb +3 -1
  25. data/lib/riffer/rig/terminal/idle_ticker.rb +28 -0
  26. data/lib/riffer/rig/terminal/key_decoder.rb +65 -1
  27. data/lib/riffer/rig/terminal/key_pipeline.rb +8 -0
  28. data/lib/riffer/rig/terminal/line_editor.rb +10 -2
  29. data/lib/riffer/rig/terminal/loading.rb +71 -0
  30. data/lib/riffer/rig/terminal/repl.rb +19 -11
  31. data/lib/riffer/rig/terminal/spinner.rb +13 -0
  32. data/lib/riffer/rig/terminal/status_bar.rb +126 -0
  33. data/lib/riffer/rig/terminal/ticker.rb +21 -101
  34. data/lib/riffer/rig/terminal/ticker_display.rb +5 -9
  35. data/lib/riffer/rig/terminal/wrapper.rb +19 -7
  36. data/lib/riffer/rig/terminal.rb +59 -20
  37. data/lib/riffer/rig/tools/bash.rb +9 -2
  38. data/lib/riffer/rig/version.rb +1 -1
  39. data/sig/generated/riffer/rig/cli/flags.rbs +5 -2
  40. data/sig/generated/riffer/rig/commands/reasoning.rbs +16 -0
  41. data/sig/generated/riffer/rig/env.rbs +3 -1
  42. data/sig/generated/riffer/rig/loader/options.rbs +5 -2
  43. data/sig/generated/riffer/rig/loader.rbs +30 -0
  44. data/sig/generated/riffer/rig/runtime/snapshot.rbs +5 -2
  45. data/sig/generated/riffer/rig/runtime.rbs +22 -6
  46. data/sig/generated/riffer/rig/settings/document.rbs +2 -2
  47. data/sig/generated/riffer/rig/settings/file.rbs +5 -0
  48. data/sig/generated/riffer/rig/settings/model_options.rbs +8 -14
  49. data/sig/generated/riffer/rig/stores/reasoning_entry.rbs +23 -0
  50. data/sig/generated/riffer/rig/stores/recorder.rbs +8 -4
  51. data/sig/generated/riffer/rig/terminal/console.rbs +7 -1
  52. data/sig/generated/riffer/rig/terminal/count.rbs +7 -0
  53. data/sig/generated/riffer/rig/terminal/frame.rbs +75 -12
  54. data/sig/generated/riffer/rig/terminal/idle_ticker.rbs +21 -0
  55. data/sig/generated/riffer/rig/terminal/key_decoder.rbs +29 -0
  56. data/sig/generated/riffer/rig/terminal/key_pipeline.rbs +6 -0
  57. data/sig/generated/riffer/rig/terminal/line_editor.rbs +3 -0
  58. data/sig/generated/riffer/rig/terminal/loading.rbs +42 -0
  59. data/sig/generated/riffer/rig/terminal/repl.rbs +5 -3
  60. data/sig/generated/riffer/rig/terminal/spinner.rbs +9 -0
  61. data/sig/generated/riffer/rig/terminal/status_bar.rbs +88 -0
  62. data/sig/generated/riffer/rig/terminal/ticker.rbs +14 -60
  63. data/sig/generated/riffer/rig/terminal/ticker_display.rbs +3 -6
  64. data/sig/generated/riffer/rig/terminal/wrapper.rbs +8 -1
  65. data/sig/generated/riffer/rig/terminal.rbs +23 -0
  66. data/sig/manual/riffer/rig/{terminal/equalizer.rbs → commands/reasoning.rbs} +2 -2
  67. data/sig/manual/riffer/rig/stores.rbs +2 -2
  68. data/sig/manual/riffer/rig/terminal/count.rbs +6 -0
  69. data/sig/manual/riffer/rig/terminal/spinner.rbs +6 -0
  70. data/sig/manual/riffer/rig/terminal/status_bar.rbs +6 -0
  71. metadata +21 -8
  72. data/lib/riffer/rig/settings/pricing.rb +0 -40
  73. data/lib/riffer/rig/terminal/equalizer.rb +0 -20
  74. data/sig/generated/riffer/rig/settings/pricing.rbs +0 -31
  75. data/sig/generated/riffer/rig/terminal/equalizer.rbs +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fe0ea5d0af46d44401a20ba9880877d1f631d4131de70452256a5def7d206aa
4
- data.tar.gz: '0108f44f3cd248417bee1320ef14921cf75d8018784b103ebf85d676f941cd79'
3
+ metadata.gz: 613c884d96e252c8793e442b39cf843d5c91438fc7d6ae6bc963536cb978de09
4
+ data.tar.gz: f4e2596e6c67e87f19011afa452c207f5f6e8b0394673c58e1ae22a57e2f2269
5
5
  SHA512:
6
- metadata.gz: 761434ca55b8f51971328b39ad0dfa02d4a89e21c41f03b1dfb55778c09cac14f4d3328799e8a6e1b25027f9d30a953d3278f4a0264a0ae558e6f56ef577c779
7
- data.tar.gz: f9e88b15b40d562553f4b0b871a1b992c0da8c6b883159561be132e3b74312b7fce5d31b18391db99366e7bc6a4d0070639efce9abe7b454e572d9d4e7acc659
6
+ metadata.gz: 1d1b670b735b7d06b7b88d38c305e09018db879ca5ffe1a65a89f1424ae25b9d324b234bb3d38e89889305f85cb1f97822d2a76fba083f83e76f256cec6de331
7
+ data.tar.gz: 77c745faeb40661a33801885cee13d7811b05c9e557e5798a5ebb6233aef18e7c74e194f4fc9b4347cac4ff0b8182ca8c42b2e70fda71a051ee13849dbdea8f3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.0](https://github.com/bottrall/riffer-rig/compare/v0.13.0...v0.14.0) (2026-10-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * **headless:** print the session id on stderr ([#556](https://github.com/bottrall/riffer-rig/issues/556)) ([2816938](https://github.com/bottrall/riffer-rig/commit/2816938f66d32ccf557d83a33e40300e68a68032))
9
+ * make reasoning effort configurable like the model ([#553](https://github.com/bottrall/riffer-rig/issues/553)) ([330f190](https://github.com/bottrall/riffer-rig/commit/330f190244353ed5e967b0f29d574c2741d92dba))
10
+ * **terminal:** keep the status bar filled between turns ([#555](https://github.com/bottrall/riffer-rig/issues/555)) ([e0918ec](https://github.com/bottrall/riffer-rig/commit/e0918ecd050519cf06d0593221b3b50e0e1a6778))
11
+ * **terminal:** type a multiline prompt ([#557](https://github.com/bottrall/riffer-rig/issues/557)) ([a55091c](https://github.com/bottrall/riffer-rig/commit/a55091c091e4df4ad6c63a71d7dd16d1de98e835))
12
+ * upgrade riffer to 0.53.0 and adopt the model catalog ([#552](https://github.com/bottrall/riffer-rig/issues/552)) ([70b129b](https://github.com/bottrall/riffer-rig/commit/70b129b27a4d03188ef7831937874d7bb73cea08))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * stop models from polling background bash jobs ([#559](https://github.com/bottrall/riffer-rig/issues/559)) ([57ad0ba](https://github.com/bottrall/riffer-rig/commit/57ad0ba259d0ebd71f4cc4178172364f39a62782))
18
+ * **terminal:** draw the banner and the input row together ([#549](https://github.com/bottrall/riffer-rig/issues/549)) ([e847277](https://github.com/bottrall/riffer-rig/commit/e847277af41ebd824dd115105d7d40e9af236da4))
19
+
3
20
  ## [0.13.0](https://github.com/bottrall/riffer-rig/compare/v0.12.0...v0.13.0) (2026-10-17)
4
21
 
5
22
 
data/README.md CHANGED
@@ -34,13 +34,14 @@ riffer
34
34
 
35
35
  The first run asks which model to use, as `provider/name` (for example `anthropic/claude-sonnet-4-6`), and saves the answer as `model` in `~/.riffer/settings.json`; it then asks for any missing credential ([Authentication](#authentication)). After that, `riffer` opens straight into the prompt. [Getting started](docs/GETTING_STARTED.md) walks through a first session.
36
36
 
37
- The model is the first of these that is set: `--model`, `RIFFER_MODEL`, `model` in `<cwd>/.riffer/settings.json`, then in `~/.riffer/settings.json` ([Configuration](docs/CONFIGURATION.md#model)).
37
+ The model is the first of these that is set: `--model`, `RIFFER_MODEL`, `model` in `<cwd>/.riffer/settings.json`, then in `~/.riffer/settings.json` ([Configuration](docs/CONFIGURATION.md#model)). The reasoning effort resolves the same way: `--reasoning`, `RIFFER_REASONING`, then `reasoning` in the settings ([Configuration](docs/CONFIGURATION.md#reasoning)).
38
38
 
39
39
  ### Flags
40
40
 
41
41
  | Flag | Effect |
42
42
  | ----------------------- | -------------------------------------------------------------------------- |
43
43
  | `--model provider/name` | the model for this session, over `RIFFER_MODEL` and the settings |
44
+ | `--reasoning LEVEL` | the reasoning effort for this session, over `RIFFER_REASONING` and the settings |
44
45
  | `--no-extensions` | skips the [`rig.rb` files](docs/EXTENSIONS.md#the-rigrb-files) and gem autoload; the bundle still loads |
45
46
  | `--no-skills` | leaves out [Agent Skills](docs/SKILLS.md) |
46
47
  | `--no-agents-md` | leaves out the [AGENTS.md](docs/INSTRUCTIONS.md#agentsmd) instructions |
@@ -70,16 +71,20 @@ The exit code says how the turn ended: `0` finished, `1` runtime error, `2` usag
70
71
 
71
72
  ### In the session
72
73
 
73
- Type a prompt and press Enter. While the turn runs, the status line under the input shows its time, steps, tool calls, tokens and cost, and Ctrl-T peeks at what the model is writing; the reply is printed when the turn ends, followed by a stat line (`N steps · mm:ss · ↑in · ↓out · session N tok`, plus the turn's cost and the session's when the model is [priced](docs/CONFIGURATION.md#models)). The input line stays open throughout: a message sent during a turn is queued and runs when the turn ends. [Terminal](docs/TERMINAL.md) has every key.
74
+ Type a prompt and press Enter. While the turn runs, the status line under the input shows its time, steps, tool calls, tokens and cost, and Ctrl-T peeks at what the model is writing; the reply is printed when the turn ends, followed by a stat line (`N steps · mm:ss · ↑in · ↓out · session N tok`, plus the turn's cost and the session's when the model is [priced](docs/CONFIGURATION.md#catalog_files)). The input line stays open throughout: a message sent during a turn is queued and runs when the turn ends. [Terminal](docs/TERMINAL.md) has every key.
74
75
 
75
76
  - Ctrl-C during a turn cancels it. Ctrl-C with no turn running asks for a second one, which exits.
76
77
  - `/exit` or `/quit` ends the session, as does Ctrl-D on an empty line.
77
- - `/help` lists the session's slash commands. `/resume [--all]`, `/new`, `/cost`, `/trace`, `/skill:<name>` and the others are described in [Terminal](docs/TERMINAL.md#commands); `/model` is covered [below](#switching-the-model).
78
+ - `/help` lists the session's slash commands. `/resume [--all]`, `/new`, `/cost`, `/trace`, `/skill:<name>` and the others are described in [Terminal](docs/TERMINAL.md#commands); `/model` is covered [below](#switching-the-model), and `/reasoning` works the same way for the reasoning effort.
78
79
 
79
80
  ### Switching the model
80
81
 
81
82
  `/model provider/name` switches the model for the current session only, keeping the conversation so far, and confirms it in one line: `/model openai/gpt-5` prints `Model switched to openai/gpt-5`. The provider prefix is required: a bare name such as `/model sonnet` is rejected with the list of providers. `/model` alone shows the model in use. Switching runs the same setup for the new provider as startup does: its SDK gem is offered for install (or refused with the exact Gemfile line), and missing credentials are resolved — you are prompted for them when the host can ask, and the values apply to the running session immediately. The switch is refused with one error when the host cannot supply what is missing, and the model is left unchanged. `/model --save` writes the model in effect to the home `settings.json`, so new sessions start with it; `/model provider/name --save` switches and saves in one step, printing `Model switched to openai/gpt-5 and saved to ~/.riffer/settings.json` (a bare `/model --save` prints `Saved openai/gpt-5 to ~/.riffer/settings.json`), and a refused switch saves nothing ([Configuration](docs/CONFIGURATION.md#model)).
82
83
 
84
+ ### Switching the reasoning level
85
+
86
+ `/reasoning level` switches the reasoning effort for the current session only, keeping the conversation so far: `/reasoning high` prints `Reasoning switched to high`, `/reasoning off` turns reasoning off, and `/reasoning` alone shows the level in effect. A level riffer does not accept is rejected naming the levels it accepts. `/reasoning --save` writes the level in effect to the home `settings.json` (`/reasoning high --save` switches and saves in one step), and a home file that no longer parses is reported instead of written. The switch survives a resume, and a `--reasoning` chosen for the run wins over it for that run only ([Configuration](docs/CONFIGURATION.md#reasoning)).
87
+
83
88
  ## Authentication
84
89
 
85
90
  `riffer-rig` talks to the provider named by the model's prefix — `anthropic/claude-sonnet-4-6` means Anthropic. Provide that provider's credentials in either of two ways:
@@ -115,18 +120,19 @@ Inside a session, `/auth` lists every provider with where its secret comes from
115
120
 
116
121
  Settings live in two scopes, both optional: `~/.riffer/settings.json` for every project, and `<cwd>/.riffer/settings.json` for one project, merged key by key with the project winning ([Configuration](docs/CONFIGURATION.md)). Every core key is optional:
117
122
 
118
- | Key | What it sets |
119
- | ------------ | -------------------------------------------------------------------------------- |
120
- | `model` | the model new sessions start with, as `provider/name` |
121
- | `reasoning` | the reasoning effort, translated to the provider's own parameter |
122
- | `models` | prices each model in USD per million tokens |
123
- | `providers` | non-secret provider fields such as an endpoint or region (home file only) |
124
- | `extensions` | `disabled` — bundled extensions to leave out; `autoload` — gem extension autoload |
125
- | `reload` | `"auto"` (default) or `"manual"` — the hot-reload trigger |
126
- | `sessions` | `save: false` stops saving sessions to the store |
127
- | `tools` | the provider-native tool switches (`native`), off by default |
123
+ | Key | What it sets |
124
+ | -------------- | -------------------------------------------------------------------------------- |
125
+ | `model` | the model new sessions start with, as `provider/name` |
126
+ | `reasoning` | the reasoning effort, which riffer translates to the provider's own parameter |
127
+ | `catalog_files`| the model catalog JSON files holding pricing and reasoning maps |
128
+ | `providers` | non-secret provider fields such as an endpoint or region (home file only) |
129
+ | `extensions` | `disabled` — bundled extensions to leave out; `autoload` — gem extension autoload |
130
+ | `reload` | `"auto"` (default) or `"manual"` — the hot-reload trigger |
131
+ | `sessions` | `save: false` stops saving sessions to the store |
132
+ | `tools` | the provider-native tool switches (`native`), off by default |
128
133
 
129
134
  - `RIFFER_MODEL` — the model for this run as `provider/name`, winning over the `model` setting. A bare name such as `sonnet` is rejected with the list of providers.
135
+ - `RIFFER_REASONING` — the reasoning effort for this run, winning over the `reasoning` setting; a level riffer does not accept is rejected naming the levels it accepts.
130
136
  - `AGENTS.md` — `~/.riffer/AGENTS.md` and an `AGENTS.md` in the current working directory or any directory above it are re-read every turn as instructions that take precedence over the default norms ([Instructions](docs/INSTRUCTIONS.md#agentsmd)).
131
137
  - Skills — Agent Skills in `.agents/skills/` from the working directory up to the repository root, and in `~/.agents/skills/`, are offered to the model, and `/skill:<name> [text]` runs one ([Skills](docs/SKILLS.md)).
132
138
  - MCP servers — declared under the `mcp` key in either settings file and registered with the Runtime ([MCP](docs/MCP.md)).
@@ -9,9 +9,10 @@ class Riffer::Rig::CLI::Flags
9
9
  riffer acp serve Runtimes as an ACP agent over stdio
10
10
  TEXT
11
11
 
12
- # @dynamic model, extensions, skills, agents_md, tools, max_steps, help, version
12
+ # @dynamic model, reasoning, extensions, skills, agents_md, tools, max_steps, help, version
13
13
  # @dynamic save, verbose, json, job_idle_timeout, headless, prompt, continue, resume
14
14
  attr_reader :model #: String?
15
+ attr_reader :reasoning #: String?
15
16
  attr_reader :extensions, :skills, :agents_md, :help, :version, :verbose, :json, :headless #: bool
16
17
  attr_reader :tools #: Array[String]?
17
18
  attr_reader :max_steps #: Integer?
@@ -47,6 +48,7 @@ class Riffer::Rig::CLI::Flags
47
48
  def to_options
48
49
  Riffer::Rig::Loader::Options.new(
49
50
  model: model,
51
+ reasoning: reasoning,
50
52
  extensions: extensions,
51
53
  skills: skills,
52
54
  agents_md: agents_md,
@@ -61,6 +63,7 @@ class Riffer::Rig::CLI::Flags
61
63
  parser.banner = BANNER
62
64
  parser.separator('')
63
65
  parser.on('--model PROVIDER/NAME', String, 'Model for this session, over RIFFER_MODEL and settings')
66
+ parser.on('--reasoning LEVEL', String, 'Reasoning level for this session, over RIFFER_REASONING and settings')
64
67
  parser.on('--no-extensions', 'Skip extension files and autoload; the bundle still loads')
65
68
  parser.on('--no-skills', 'Leave out Agent Skills')
66
69
  parser.on('--no-agents-md', 'Leave out AGENTS.md instructions')
@@ -94,6 +97,7 @@ class Riffer::Rig::CLI::Flags
94
97
  def self.assemble(values, prompt)
95
98
  new(
96
99
  model: values[:model],
100
+ reasoning: values[:reasoning],
97
101
  extensions: !values.key?(:'no-extensions'),
98
102
  skills: !values.key?(:'no-skills'),
99
103
  agents_md: !values.key?(:'no-agents-md'),
@@ -114,6 +118,7 @@ class Riffer::Rig::CLI::Flags
114
118
  private_class_method :assemble
115
119
 
116
120
  # @rbs model: String?
121
+ # @rbs reasoning: String?
117
122
  # @rbs extensions: bool
118
123
  # @rbs skills: bool
119
124
  # @rbs agents_md: bool
@@ -132,6 +137,7 @@ class Riffer::Rig::CLI::Flags
132
137
  # @rbs return: void
133
138
  def initialize(
134
139
  model: nil,
140
+ reasoning: nil,
135
141
  extensions: true,
136
142
  skills: true,
137
143
  agents_md: true,
@@ -149,6 +155,7 @@ class Riffer::Rig::CLI::Flags
149
155
  version: false
150
156
  )
151
157
  @model = model
158
+ @reasoning = reasoning
152
159
  @extensions = extensions
153
160
  @skills = skills
154
161
  @agents_md = agents_md
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::Commands::Reasoning
4
+ extend self
5
+
6
+ SAVE = '--save' #: String
7
+ SAVED_TO = '~/.riffer/settings.json' #: String
8
+ DEFAULT = 'default' #: String
9
+
10
+ # @rbs return: Riffer::Rig::Command
11
+ def command
12
+ Riffer::Rig::Command.new(
13
+ 'reasoning',
14
+ description: 'Switch the reasoning level for this session: /reasoning high, /reasoning --save',
15
+ extension: :core
16
+ ) { |ctx| run(ctx) }
17
+ end
18
+
19
+ # @rbs ctx: Riffer::Rig::Command::Context
20
+ # @rbs return: void
21
+ def run(ctx)
22
+ words = ctx.args.split
23
+ save = words.delete(SAVE)
24
+ level = words.join(' ')
25
+
26
+ if level.empty?
27
+ current = ctx.runtime.reasoning
28
+ return ctx.say("Reasoning: #{current || DEFAULT}") unless save
29
+ return ctx.say('No reasoning level is in effect to save') unless current
30
+
31
+ refusal = Riffer::Rig::Settings::File.store_reasoning(current)
32
+ return ctx.host.notify(refusal, level: :error) if refusal
33
+
34
+ return ctx.say("Saved #{current} to #{SAVED_TO}")
35
+ end
36
+
37
+ rejection = Riffer::Rig::Settings::ModelOptions.reasoning_rejection(level)
38
+ return ctx.say(rejection) if rejection
39
+
40
+ ctx.runtime.reasoning = level
41
+ refusal = Riffer::Rig::Settings::File.store_reasoning(level) if save
42
+ ctx.host.notify(refusal, level: :error) if refusal
43
+ saved = save && !refusal
44
+ ctx.say(saved ? "Reasoning switched to #{level} and saved to #{SAVED_TO}" : "Reasoning switched to #{level}")
45
+ end
46
+ end
@@ -3,9 +3,10 @@
3
3
  class Riffer::Rig::Env
4
4
  # @rbs @source: Hash[String, String]
5
5
 
6
- # @dynamic no_color, model
6
+ # @dynamic no_color, model, reasoning
7
7
  attr_reader :no_color #: bool
8
8
  attr_reader :model #: String?
9
+ attr_reader :reasoning #: String?
9
10
 
10
11
  # @rbs source: Hash[String, String]
11
12
  # @rbs return: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
@@ -22,6 +23,7 @@ class Riffer::Rig::Env
22
23
  @source = source.freeze
23
24
  @no_color = @source.key?('NO_COLOR')
24
25
  @model = self['RIFFER_MODEL']
26
+ @reasoning = self['RIFFER_REASONING']
25
27
  freeze
26
28
  end
27
29
 
@@ -203,6 +203,8 @@ class Riffer::Rig::Headless
203
203
  return @output.write(Ndjson.line(event)) if @json
204
204
 
205
205
  case event
206
+ when Riffer::Rig::Events::SessionStart
207
+ @error.puts("session #{event.id}")
206
208
  when Riffer::StreamEvents::TextDelta
207
209
  write_text(event.content)
208
210
  @streamed_text = true
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Riffer::Rig::Loader::Options
4
- # @dynamic model, extensions, skills, agents_md, tools, max_steps
4
+ # @dynamic model, reasoning, extensions, skills, agents_md, tools, max_steps
5
5
  attr_reader :model #: String?
6
+ attr_reader :reasoning #: String?
6
7
  attr_reader :extensions #: bool
7
8
  attr_reader :skills #: bool
8
9
  attr_reader :agents_md #: bool
@@ -10,6 +11,7 @@ class Riffer::Rig::Loader::Options
10
11
  attr_reader :max_steps #: Integer?
11
12
 
12
13
  # @rbs model: String?
14
+ # @rbs reasoning: String?
13
15
  # @rbs extensions: bool
14
16
  # @rbs skills: bool
15
17
  # @rbs agents_md: bool
@@ -18,6 +20,7 @@ class Riffer::Rig::Loader::Options
18
20
  # @rbs return: void
19
21
  def initialize(
20
22
  model: nil,
23
+ reasoning: nil,
21
24
  extensions: true,
22
25
  skills: true,
23
26
  agents_md: true,
@@ -25,6 +28,7 @@ class Riffer::Rig::Loader::Options
25
28
  max_steps: Riffer::Rig::Runtime::DEFAULT_MAX_STEPS
26
29
  )
27
30
  @model = model
31
+ @reasoning = reasoning
28
32
  @extensions = extensions
29
33
  @skills = skills
30
34
  @agents_md = agents_md
@@ -85,11 +85,13 @@ class Riffer::Rig::Loader
85
85
 
86
86
  document = Riffer::Rig::Settings::Document.new(settings)
87
87
  selected = select_model(options.model || @env.model || document.model)
88
+ check_reasoning(options)
88
89
  provider = Riffer::Rig::Settings::ModelOptions.provider_for(selected).to_s
89
90
  ensure_sdk(provider)
90
91
  credentials = install_credentials(provider)
91
92
  raise ConfigurationError, credentials if credentials.is_a?(String)
92
93
 
94
+ @riffer_config.catalog_files = document.catalog_files
93
95
  @extensions_enabled = options.extensions
94
96
  @extra_extensions = extra_extensions
95
97
  @reload_mode = reload
@@ -102,6 +104,9 @@ class Riffer::Rig::Loader
102
104
  @recorder_extensions = recorder ? [Riffer::Rig::Stores::Recorder.extension(recorder)] : []
103
105
  loaded += @recorder_extensions
104
106
 
107
+ snapshot = snapshot_without_forced(snapshot, options)
108
+ reasoning = forced_reasoning(options, snapshot)
109
+
105
110
  built = Riffer::Rig::Runtime.new(
106
111
  selected,
107
112
  extensions: loaded,
@@ -113,11 +118,11 @@ class Riffer::Rig::Loader
113
118
  credentials: { provider.to_sym => credentials },
114
119
  env: @env,
115
120
  auth_path: home_auth_path,
116
- pricing: document.models,
121
+ reasoning: reasoning || settings_reasoning(document),
117
122
  riffer_config: @riffer_config,
118
- model_options: Riffer::Rig::Settings::ModelOptions.model_options(selected, document.reasoning),
123
+ model_options: Riffer::Rig::Settings::ModelOptions.model_options(selected),
119
124
  native_tools: document.native_tools,
120
- snapshot: options.model ? snapshot&.merge(model: nil) : snapshot
125
+ snapshot: snapshot
121
126
  )
122
127
  built.install_command(Riffer::Rig::Commands::Reload.command(self))
123
128
  store = @store
@@ -370,6 +375,55 @@ class Riffer::Rig::Loader
370
375
  File.join(@home, '.riffer', 'auth.json')
371
376
  end
372
377
 
378
+ # A reasoning level from the flag or the environment is refused, the way a
379
+ # bad model string is; one from the settings document or a session's recorded
380
+ # switch falls back to the model's default instead.
381
+ # @rbs options: Riffer::Rig::Loader::Options
382
+ # @rbs return: void
383
+ def check_reasoning(options)
384
+ forced = options.reasoning || @env.reasoning
385
+ return unless forced
386
+
387
+ rejection = Riffer::Rig::Settings::ModelOptions.reasoning_rejection(forced)
388
+ return unless rejection
389
+
390
+ raise ConfigurationError, options.reasoning ? rejection : "RIFFER_REASONING: #{rejection}"
391
+ end
392
+
393
+ # The reasoning in effect for the build, over the settings document: the flag,
394
+ # then the session's recorded switch, then RIFFER_REASONING.
395
+ # @rbs options: Riffer::Rig::Loader::Options
396
+ # @rbs snapshot: Hash[Symbol, untyped]?
397
+ # @rbs return: String?
398
+ def forced_reasoning(options, snapshot)
399
+ options.reasoning || recorded_reasoning(snapshot) || @env.reasoning
400
+ end
401
+
402
+ # @rbs snapshot: Hash[Symbol, untyped]?
403
+ # @rbs return: String?
404
+ def recorded_reasoning(snapshot)
405
+ saved = snapshot && snapshot[:reasoning]
406
+ Riffer::Rig::Settings::ModelOptions.reasoning_level(saved) if saved.is_a?(String)
407
+ end
408
+
409
+ # A settings level riffer does not accept silently keeps the model's default.
410
+ # @rbs document: Riffer::Rig::Settings::Document
411
+ # @rbs return: String?
412
+ def settings_reasoning(document)
413
+ Riffer::Rig::Settings::ModelOptions.reasoning_level(document.reasoning)
414
+ end
415
+
416
+ # A model or reasoning level chosen for the run wins for that run only: it is
417
+ # not recorded, so a later resume restores the session's own switches.
418
+ # @rbs snapshot: Hash[Symbol, untyped]?
419
+ # @rbs options: Riffer::Rig::Loader::Options
420
+ # @rbs return: Hash[Symbol, untyped]?
421
+ def snapshot_without_forced(snapshot, options)
422
+ snapshot = snapshot&.merge(model: nil) if options.model
423
+ snapshot = snapshot&.merge(reasoning: nil) if options.reasoning
424
+ snapshot
425
+ end
426
+
373
427
  # The store records unless it was declined with store: nil or globally with
374
428
  # "sessions": {"save": false}.
375
429
  # @rbs document: Riffer::Rig::Settings::Document
@@ -3,7 +3,9 @@
3
3
  class Riffer::Rig::Registrar
4
4
  class NameCollisionError < StandardError; end
5
5
 
6
- CORE_SETTINGS_KEYS = %w[model reasoning models reload extensions sessions providers tools].freeze #: Array[String]
6
+ CORE_SETTINGS_KEYS = %w[
7
+ model reasoning catalog_files reload extensions sessions providers tools
8
+ ].freeze #: Array[String]
7
9
 
8
10
  EVENTS = %i[
9
11
  session_start session_end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Riffer::Rig::Runtime::Snapshot
4
- # @dynamic id, messages, model, skills
4
+ # @dynamic id, messages, model, reasoning, skills
5
5
  attr_reader :id #: String
6
6
  attr_reader :messages #: Array[Riffer::Messages::Base]
7
7
  attr_reader :model #: String?
8
+ attr_reader :reasoning #: String?
8
9
  attr_reader :skills #: Array[String]
9
10
 
10
11
  # @rbs hash: Hash[Symbol, untyped]
@@ -17,6 +18,7 @@ class Riffer::Rig::Runtime::Snapshot
17
18
  # a Session.from_h could also heal orphaned tool calls on load.
18
19
  messages: hash.fetch(:messages).map { |stored| message(stored) },
19
20
  model: hash.fetch(:model),
21
+ reasoning: hash[:reasoning].is_a?(String) ? hash[:reasoning] : nil,
20
22
  skills: hash.fetch(:skills)
21
23
  )
22
24
  end
@@ -34,19 +36,21 @@ class Riffer::Rig::Runtime::Snapshot
34
36
  # @rbs id: String
35
37
  # @rbs messages: Array[Riffer::Messages::Base]
36
38
  # @rbs model: String?
39
+ # @rbs reasoning: String?
37
40
  # @rbs skills: Array[String]
38
41
  # @rbs return: void
39
- def initialize(id:, messages:, model:, skills:)
42
+ def initialize(id:, messages:, model:, skills:, reasoning: nil)
40
43
  @id = id
41
44
  @messages = messages
42
45
  @model = model
46
+ @reasoning = reasoning
43
47
  @skills = skills
44
48
  freeze
45
49
  end
46
50
 
47
51
  # @rbs return: Hash[Symbol, untyped]
48
52
  def to_h
49
- { id: @id, messages: @messages.map(&:to_h), model: @model, skills: @skills }
53
+ { id: @id, messages: @messages.map(&:to_h), model: @model, reasoning: @reasoning, skills: @skills }
50
54
  end
51
55
 
52
56
  # @rbs return: Riffer::Agent::Session
@@ -61,8 +61,10 @@ class Riffer::Rig::Runtime
61
61
  # @rbs @session_start_pending: bool
62
62
  # @rbs @session_start_reason: Symbol
63
63
  # @rbs @model_override: String?
64
+ # @rbs @reasoning: String?
64
65
  # @rbs @message_observers: Array[^(Riffer::Messages::Base) -> void]
65
66
  # @rbs @model_observers: Array[^(String) -> void]
67
+ # @rbs @reasoning_observers: Array[^(String) -> void]
66
68
  # @rbs @system_prompt: Riffer::Rig::Runtime::SystemPrompt
67
69
  # @rbs @commands: Hash[String, Riffer::Rig::Command]
68
70
  # @rbs @core_commands: Array[Riffer::Rig::Command]
@@ -97,7 +99,7 @@ class Riffer::Rig::Runtime
97
99
  # @rbs credentials: Hash[Symbol, Hash[Symbol, String]]
98
100
  # @rbs env: Riffer::Rig::Env
99
101
  # @rbs auth_path: String
100
- # @rbs pricing: Hash[String, Riffer::Rig::Settings::Pricing]
102
+ # @rbs reasoning: String?
101
103
  # @rbs riffer_config: Riffer::Config
102
104
  # @rbs mcp_registry: Riffer::Rig::Mcp::_Registry
103
105
  # @rbs max_steps: Integer?
@@ -118,7 +120,7 @@ class Riffer::Rig::Runtime
118
120
  credentials: {},
119
121
  env: Riffer::Rig::Env.new,
120
122
  auth_path: Riffer::Rig::Credentials::PATH,
121
- pricing: {},
123
+ reasoning: nil,
122
124
  riffer_config: Riffer.config,
123
125
  mcp_registry: Riffer::Mcp,
124
126
  max_steps: DEFAULT_MAX_STEPS,
@@ -147,7 +149,8 @@ class Riffer::Rig::Runtime
147
149
  @session_start_reason = saved ? :restore : :new
148
150
  @message_observers = []
149
151
  @model_observers = []
150
- Riffer::Rig::Settings::Pricing.register(pricing, riffer_config.pricing)
152
+ @reasoning_observers = []
153
+ @reasoning = reasoning
151
154
  @errors = []
152
155
  @tool_allowlist = tools
153
156
  @native_tools = native_tools
@@ -257,6 +260,21 @@ class Riffer::Rig::Runtime
257
260
  @model_observers.each { |observer| observer.call(model) }
258
261
  end
259
262
 
263
+ # @rbs return: String?
264
+ def reasoning
265
+ @reasoning || Riffer::Rig::Settings::ModelOptions.reasoning_level(Riffer::Rig::Settings::Document.new(@settings).reasoning)
266
+ end
267
+
268
+ # @rbs level: String
269
+ # @rbs return: void
270
+ def reasoning=(level)
271
+ # Riffer's own validation raises before anything is assigned, so a level it
272
+ # refuses leaves both the level and the config unchanged.
273
+ @agent.config.reasoning = level
274
+ @reasoning = level
275
+ @reasoning_observers.each { |observer| observer.call(level) }
276
+ end
277
+
260
278
  # @rbs provider: Symbol
261
279
  # @rbs values: Hash[Symbol, String]
262
280
  # @rbs return: void
@@ -288,7 +306,11 @@ class Riffer::Rig::Runtime
288
306
  # @rbs return: Hash[Symbol, untyped]
289
307
  def to_h
290
308
  Riffer::Rig::Runtime::Snapshot.new(
291
- id: @id, messages: @agent.session.messages, model: @model_override, skills: activated_skills
309
+ id: @id,
310
+ messages: @agent.session.messages,
311
+ model: @model_override,
312
+ reasoning: @reasoning,
313
+ skills: activated_skills
292
314
  ).to_h
293
315
  end
294
316
 
@@ -306,6 +328,13 @@ class Riffer::Rig::Runtime
306
328
  nil
307
329
  end
308
330
 
331
+ # @rbs &block: (String) -> void
332
+ # @rbs return: nil
333
+ def on_reasoning_change(&block)
334
+ @reasoning_observers << block
335
+ nil
336
+ end
337
+
309
338
  # @rbs &: (Riffer::Rig::Runtime::JobCompletion) -> void
310
339
  # @rbs return: nil
311
340
  def on_job_exit(&)
@@ -388,15 +417,15 @@ class Riffer::Rig::Runtime
388
417
  end
389
418
 
390
419
  # The options the settings built for the starting model are provider-shaped
391
- # (cache control, per-provider reasoning levels), so a switch re-derives them.
392
- # Runs after the successor is built, so a failed build leaves the running
393
- # agent's options alone; riffer reads config.model_options per request, so the
394
- # swap still reaches the new agent through the shared config.
420
+ # (anthropic cache control), so a switch re-derives them; the reasoning level
421
+ # is provider-agnostic and travels on the config. Runs after the successor is
422
+ # built, so a failed build leaves the running agent's options alone; riffer
423
+ # reads config.model_options per request, so the swap still reaches the new
424
+ # agent through the shared config.
395
425
  # @rbs model: String
396
426
  # @rbs return: void
397
427
  def rederive_options(model)
398
- reasoning = Riffer::Rig::Settings::Document.new(@settings).reasoning
399
- @model_options = Riffer::Rig::Settings::ModelOptions.model_options(model, reasoning).merge(native_options(model))
428
+ @model_options = Riffer::Rig::Settings::ModelOptions.model_options(model).merge(native_options(model))
400
429
  @agent.config.model_options = @model_options
401
430
  end
402
431
 
@@ -423,6 +452,7 @@ class Riffer::Rig::Runtime
423
452
  model: ->(context) { context[:model] },
424
453
  model_options: @model_options,
425
454
  instructions: @system_prompt.bare,
455
+ reasoning: @reasoning,
426
456
  tools_config: select_tools(registrars.flat_map { |registrar| registrar.tools.to_a }.to_h.values, @tool_allowlist),
427
457
  max_steps: max_steps,
428
458
  tool_runtime: Riffer::Rig::Runtime::ToolRuntime.new(hooks, emit: ->(event) { emit(event) }, clock: @clock),
@@ -462,6 +492,7 @@ class Riffer::Rig::Runtime
462
492
  *@core_commands,
463
493
  Riffer::Rig::Commands::Auth.command,
464
494
  Riffer::Rig::Commands::Model.command,
495
+ Riffer::Rig::Commands::Reasoning.command,
465
496
  Riffer::Rig::Commands::Cost.command,
466
497
  *skills.map { |skill| Riffer::Rig::Commands::Skill.command(skill) },
467
498
  *registrars.flat_map { |registrar| registrar.commands.values }
@@ -795,6 +826,7 @@ class Riffer::Rig::Runtime
795
826
  # @rbs return: Riffer::Agent
796
827
  def restore(snapshot, model, config)
797
828
  @model_override = snapshot.restorable_model(@credentials, @host, model)
829
+ @reasoning = Riffer::Rig::Settings::ModelOptions.reasoning_level(snapshot.reasoning) if snapshot.reasoning
798
830
  agent = build_agent(@model_override || model, config, session: snapshot.session)
799
831
  agent.context.token_usage = snapshot.usage
800
832
  reactivate(agent.context.skills, snapshot.skills)
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Riffer::Rig::Settings::Document
4
- # @dynamic model, reasoning, models, providers, disabled, autoload, native_tools, save, reload
4
+ # @dynamic model, reasoning, catalog_files, providers, disabled, autoload, native_tools, save, reload
5
5
  attr_reader :model #: String?
6
6
  attr_reader :reasoning #: String?
7
- attr_reader :models #: Hash[String, Riffer::Rig::Settings::Pricing]
7
+ attr_reader :catalog_files #: Array[String]
8
8
  attr_reader :providers #: Hash[String, Hash[String, String]]
9
9
  attr_reader :disabled #: Array[String]
10
10
  attr_reader :autoload #: bool
@@ -17,9 +17,9 @@ class Riffer::Rig::Settings::Document
17
17
  def initialize(source)
18
18
  @model = source[:model].is_a?(String) ? source[:model] : nil
19
19
  @reasoning = source[:reasoning].is_a?(String) ? source[:reasoning] : nil
20
- @models = hash_or_empty(source[:models]).filter_map do |name, entry|
21
- [name.to_s, Riffer::Rig::Settings::Pricing.from(entry)] if entry.is_a?(Hash)
22
- end.to_h.freeze
20
+ paths = source[:catalog_files]
21
+ no_files = [] #: Array[String]
22
+ @catalog_files = paths.is_a?(Array) && paths.all?(String) ? paths.dup.freeze : no_files.freeze
23
23
  @providers = hash_or_empty(source[:providers]).filter_map do |identifier, fields|
24
24
  if fields.is_a?(Hash)
25
25
  [identifier.to_s, fields.select do |_name, value|
@@ -24,4 +24,11 @@ module Riffer::Rig::Settings::File
24
24
  def store_model(model, path: PATH)
25
25
  update(path) { |document| document.merge(model: model) }
26
26
  end
27
+
28
+ # @rbs level: String
29
+ # @rbs path: String
30
+ # @rbs return: String?
31
+ def store_reasoning(level, path: PATH)
32
+ update(path) { |document| document.merge(reasoning: level) }
33
+ end
27
34
  end