riffer-rig 0.4.0 → 0.5.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/lib/riffer/rig/cli.rb +6 -7
  4. data/lib/riffer/rig/coding_agent.rb +1 -6
  5. data/lib/riffer/rig/credentials.rb +7 -7
  6. data/lib/riffer/rig/extension.rb +31 -0
  7. data/lib/riffer/rig/host.rb +52 -0
  8. data/lib/riffer/rig/registrar.rb +23 -0
  9. data/lib/riffer/rig/repl.rb +39 -27
  10. data/lib/riffer/rig/runtime.rb +162 -0
  11. data/lib/riffer/rig/settings/document.rb +23 -0
  12. data/lib/riffer/rig/settings/pricing.rb +11 -0
  13. data/lib/riffer/rig/settings.rb +9 -17
  14. data/lib/riffer/rig/token_tally.rb +1 -1
  15. data/lib/riffer/rig/tools/bash.rb +1 -1
  16. data/lib/riffer/rig/tools/edit.rb +1 -1
  17. data/lib/riffer/rig/tools/read.rb +2 -2
  18. data/lib/riffer/rig/ui/animator.rb +6 -6
  19. data/lib/riffer/rig/ui/cursor.rb +4 -4
  20. data/lib/riffer/rig/ui/renderer.rb +9 -7
  21. data/lib/riffer/rig/ui/smoother.rb +4 -4
  22. data/lib/riffer/rig/ui/theme.rb +2 -2
  23. data/lib/riffer/rig/version.rb +1 -1
  24. data/lib/riffer/rig.rb +31 -0
  25. data/sig/generated/riffer/rig/cli.rbs +8 -8
  26. data/sig/generated/riffer/rig/credentials.rbs +4 -3
  27. data/sig/generated/riffer/rig/extension.rbs +28 -0
  28. data/sig/generated/riffer/rig/host.rbs +42 -0
  29. data/sig/generated/riffer/rig/registrar.rbs +17 -0
  30. data/sig/generated/riffer/rig/repl.rbs +21 -12
  31. data/sig/generated/riffer/rig/runtime.rbs +101 -0
  32. data/sig/generated/riffer/rig/settings/document.rbs +20 -0
  33. data/sig/generated/riffer/rig/settings/pricing.rbs +4 -0
  34. data/sig/generated/riffer/rig/settings.rbs +5 -5
  35. data/sig/generated/riffer/rig/token_tally.rbs +2 -2
  36. data/sig/generated/riffer/rig/tools/bash.rbs +2 -2
  37. data/sig/generated/riffer/rig/tools/edit.rbs +2 -2
  38. data/sig/generated/riffer/rig/tools/read.rbs +3 -3
  39. data/sig/generated/riffer/rig/ui/animator.rbs +8 -8
  40. data/sig/generated/riffer/rig/ui/cursor.rbs +4 -4
  41. data/sig/generated/riffer/rig/ui/renderer.rbs +13 -11
  42. data/sig/generated/riffer/rig/ui/smoother.rbs +4 -4
  43. data/sig/generated/riffer/rig/ui/theme.rbs +2 -2
  44. data/sig/generated/riffer/rig.rbs +19 -0
  45. data/sig/manual/riffer/rig.rbs +11 -0
  46. metadata +22 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e833f2a4dfe0b87598e2be9d585f3cb772562ab39a2dd528684a38dca87f63ab
4
- data.tar.gz: f469b328103a49e52773c0073e02d8c2fbd06d8cf6779daa50c09b49ab2eba8e
3
+ metadata.gz: 4a1580a7e4f2ed5e6d1f18714ef469b7d4623bec94552510a836a5f5e5664504
4
+ data.tar.gz: 7e871b6b1f8c9de6e3788979103f7afe65beea7a9c5fa7813e17b29e4e8eb528
5
5
  SHA512:
6
- metadata.gz: 696e8b92a7c1b7a3b71e1f628894e89aed725fbe072dba712d94df277fc5730b06d74def094445b02842d0849409b7900038aedf5d65f29f5416d1450a5b5121
7
- data.tar.gz: 3dc42ac6e625d292462e00119bd7b3c7a8d1f23ad3c4f66763fff96cbfc3328f962dca67bdc4613e884ba81a6e3f61d9af86f1ba1d8bbd16e0f3caa307ca453c
6
+ metadata.gz: db021260a12a48c02bb6ff92278819416bcc267bd78015f01f80bd3a9ed64c585e37d17573a3ba47ba28d99bd0ef586665a465782d36a6a85231c08ad4a4fd51
7
+ data.tar.gz: 5ea0ed0a06d1a318267bd3d63b693f974ff8ca814e26be0eb5837cdac09527bea33a75357f7a1a11b419854ed72810a5d571c8508010e8bd62f327eb922f04eb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0](https://github.com/bottrall/riffer-rig/compare/v0.4.0...v0.5.0) (2026-09-19)
4
+
5
+
6
+ ### Features
7
+
8
+ * add ask, max_steps and riffer response ([#178](https://github.com/bottrall/riffer-rig/issues/178)) ([948d753](https://github.com/bottrall/riffer-rig/commit/948d75354a7d0a5a9d35055b7d890dea9e51f1a1))
9
+ * add Runtime with a per-instance agent, base prompt and tool seam ([#160](https://github.com/bottrall/riffer-rig/issues/160)) ([97019ca](https://github.com/bottrall/riffer-rig/commit/97019ca6f80c95bcf22e443652f1d90d453d02de))
10
+ * type IO streams and JSON boundaries, dropping explicit untyped ([#174](https://github.com/bottrall/riffer-rig/issues/174)) ([22407be](https://github.com/bottrall/riffer-rig/commit/22407befc471d1fbbe14188456ee1ce248ddc4c7))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * drain smoother backlog before every animator restart ([#177](https://github.com/bottrall/riffer-rig/issues/177)) ([3126155](https://github.com/bottrall/riffer-rig/commit/3126155fdeba3de2ab2b9905b7e7de55593de834))
16
+ * drop invalid ? optional markers from rbs-inline annotations ([#171](https://github.com/bottrall/riffer-rig/issues/171)) ([60030bf](https://github.com/bottrall/riffer-rig/commit/60030bf7ca930e9d35cbd8c2e3a8ae8ae1692830))
17
+
3
18
  ## [0.4.0](https://github.com/bottrall/riffer-rig/compare/v0.3.0...v0.4.0) (2026-09-13)
4
19
 
5
20
 
@@ -12,8 +12,8 @@ module Riffer::Rig::CLI
12
12
  'openrouter' => 'https://openrouter.ai/keys'
13
13
  }.freeze #: Hash[String, String]
14
14
 
15
- # @rbs output: untyped
16
- # @rbs input: untyped
15
+ # @rbs output: IO
16
+ # @rbs input: IO
17
17
  # @rbs return: Integer
18
18
  def start(output: $stdout, input: $stdin)
19
19
  theme = Riffer::Rig::UI::Theme.for(output)
@@ -21,8 +21,7 @@ module Riffer::Rig::CLI
21
21
  model = Riffer::Rig::Settings.model
22
22
  provider = Riffer::Rig::Settings.provider_for(model)
23
23
 
24
- api_key = (provider && Riffer::Rig::Credentials.api_key_for(provider)) ||
25
- onboard(provider, theme, output:, input:)
24
+ api_key = (provider && Riffer::Rig::Credentials.api_key_for(provider)) || onboard(provider, theme, output:, input:)
26
25
  return 1 if api_key.nil?
27
26
 
28
27
  configure_provider(provider, api_key)
@@ -55,8 +54,8 @@ module Riffer::Rig::CLI
55
54
 
56
55
  # @rbs provider: String?
57
56
  # @rbs theme: Riffer::Rig::UI::Theme
58
- # @rbs output: untyped
59
- # @rbs input: untyped
57
+ # @rbs output: IO
58
+ # @rbs input: IO
60
59
  # @rbs return: String?
61
60
  def onboard(provider, theme, output:, input:)
62
61
  url = (provider && PROVIDER_URLS[provider]) || 'your provider'
@@ -85,7 +84,7 @@ module Riffer::Rig::CLI
85
84
  (provider && Riffer::Rig::Credentials::PROVIDER_ENV_VARS[provider]) || 'the appropriate API key env var'
86
85
  end
87
86
 
88
- # @rbs input: untyped
87
+ # @rbs input: IO
89
88
  # @rbs return: String?
90
89
  def read_secret(input)
91
90
  return input.noecho(&:gets) if input.respond_to?(:noecho) && input.tty?
@@ -12,12 +12,7 @@ class Riffer::Rig::CodingAgent < Riffer::Agent
12
12
  model Riffer::Rig::Settings.model
13
13
  model_options Riffer::Rig::Settings.model_options
14
14
 
15
- uses_tools [
16
- Riffer::Rig::Tools::Read,
17
- Riffer::Rig::Tools::Write,
18
- Riffer::Rig::Tools::Edit,
19
- Riffer::Rig::Tools::Bash
20
- ]
15
+ uses_tools [Riffer::Rig::Tools::Read, Riffer::Rig::Tools::Write, Riffer::Rig::Tools::Edit, Riffer::Rig::Tools::Bash]
21
16
 
22
17
  skills do
23
18
  backend(->(_ctx) { Riffer::Skills::FilesystemBackend.new(GLOBAL_SKILLS_DIR, PROJECT_SKILLS_DIR.call) })
@@ -54,7 +54,7 @@ module Riffer::Rig::Credentials
54
54
  # @rbs return: String
55
55
  def save_api_key(provider, key, path: PATH)
56
56
  FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
57
- data = read(path).tap { |h| h[provider] = key }
57
+ data = read(path).merge(provider => key)
58
58
  File.open(path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
59
59
  file.write(JSON.pretty_generate(data))
60
60
  end
@@ -68,21 +68,21 @@ module Riffer::Rig::Credentials
68
68
  # @rbs return: String?
69
69
  def key_from_file(provider, path)
70
70
  key = read(path)[provider]
71
- return nil unless key.is_a?(String)
72
- return key unless key.strip.empty?
71
+ return nil if key.nil? || key.strip.empty?
73
72
 
74
- nil
73
+ key
75
74
  end
76
75
 
77
76
  # The parsed auth file, or an empty hash when the file is absent or
78
- # malformed.
77
+ # malformed. Non-string values can't be keys, so they're dropped here rather
78
+ # than re-checked by every reader.
79
79
  #
80
80
  # @rbs path: String
81
- # @rbs return: Hash[String, untyped]
81
+ # @rbs return: Hash[String, String]
82
82
  def read(path)
83
83
  return {} unless File.file?(path)
84
84
 
85
- JSON.parse(File.read(path))
85
+ JSON.parse(File.read(path)).select { |_provider, key| key.is_a?(String) }
86
86
  rescue JSON::ParserError
87
87
  {}
88
88
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # An extension recorded by <tt>Riffer::Rig.extension</tt>: a name, an optional
4
+ # Gem::Requirement on riffer-rig, and the block that runs against a Runtime's
5
+ # registrar. The process registry holds these objects; Runtime.new runs their
6
+ # blocks.
7
+ class Riffer::Rig::Extension
8
+ # @rbs @name: String
9
+ # @rbs @requires: Gem::Requirement?
10
+ # @rbs @block: ^(::Riffer::Rig::Registrar) -> void
11
+
12
+ # @dynamic name, requires
13
+ attr_reader :name #: String
14
+ attr_reader :requires #: Gem::Requirement?
15
+
16
+ # @rbs name: String
17
+ # @rbs requires: String?
18
+ # @rbs &block: (::Riffer::Rig::Registrar) -> void
19
+ # @rbs return: void
20
+ def initialize(name, requires: nil, &block)
21
+ @name = name
22
+ @requires = requires && Gem::Requirement.new(requires)
23
+ @block = block
24
+ end
25
+
26
+ # @rbs registrar: Riffer::Rig::Registrar
27
+ # @rbs return: void
28
+ def run(registrar)
29
+ @block.call(registrar)
30
+ end
31
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A host receives questions and out-of-band messages from a Runtime. It is
4
+ # duck-typed: any object with #ask, #confirm, #notify, #progress and
5
+ # #capabilities is a host.
6
+ #
7
+ # class HeadlessHost
8
+ # def capabilities = Set[:notify, :progress].freeze
9
+ # def ask(*) = nil
10
+ # def confirm(*) = false
11
+ # def notify(message, level:) = warn("[#{level}] #{message}")
12
+ # def progress(label) = yield
13
+ # end
14
+ #
15
+ # Callers check #capabilities before asking, so a host that declines a
16
+ # capability is never asked and the caller takes the declined path.
17
+ # The do-nothing host: the default +host:+ for a Runtime. Every capability is
18
+ # declined, so callers take the declined path without any UI attempt.
19
+ class Riffer::Rig::Host
20
+ # @rbs return: Set[Symbol]
21
+ def capabilities
22
+ Set.new.freeze
23
+ end
24
+
25
+ # @rbs _question: String?
26
+ # @rbs options: Array[String]?
27
+ # @rbs secret: bool
28
+ # @rbs return: String?
29
+ def ask(_question = nil, options: nil, secret: false)
30
+ nil
31
+ end
32
+
33
+ # @rbs _question: String?
34
+ # @rbs return: bool
35
+ def confirm(_question = nil)
36
+ false
37
+ end
38
+
39
+ # @rbs _message: String?
40
+ # @rbs level: Symbol
41
+ # @rbs return: void
42
+ def notify(_message = nil, level: :info)
43
+ nil
44
+ end
45
+
46
+ # @rbs _label: String?
47
+ # @rbs &block: ^() -> void
48
+ # @rbs return: void
49
+ def progress(_label = nil, &block)
50
+ block&.call
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The per-Runtime registrar extension blocks run against. Seams are added one
4
+ # at a time; today only #tool exists.
5
+ class Riffer::Rig::Registrar
6
+ # @rbs @tools: Array[singleton(Riffer::Tool)]
7
+
8
+ # @rbs return: void
9
+ def initialize
10
+ @tools = []
11
+ end
12
+
13
+ # @rbs klass: singleton(Riffer::Tool)
14
+ # @rbs return: void
15
+ def tool(klass)
16
+ @tools << klass
17
+ end
18
+
19
+ # @rbs return: Array[singleton(Riffer::Tool)]
20
+ def tools
21
+ @tools.dup
22
+ end
23
+ end
@@ -11,28 +11,28 @@ class Riffer::Rig::REPL
11
11
  # @rbs @smoother: Riffer::Rig::UI::Smoother
12
12
  # @rbs @cursor: Riffer::Rig::UI::Cursor
13
13
  # @rbs @theme: Riffer::Rig::UI::Theme
14
- # @rbs @input: untyped
15
- # @rbs @output: untyped
16
-
17
- # @rbs ?agent: Riffer::Agent
18
- # @rbs ?renderer: Riffer::Rig::UI::Renderer
19
- # @rbs input: untyped
20
- # @rbs output: untyped
21
- # @rbs ?theme: Riffer::Rig::UI::Theme
22
- # @rbs ?animator: Riffer::Rig::UI::Animator
23
- # @rbs ?smoother: Riffer::Rig::UI::Smoother
24
- # @rbs ?cursor: Riffer::Rig::UI::Cursor
14
+ # @rbs @input: IO
15
+ # @rbs @output: IO
16
+
17
+ # @rbs agent: Riffer::Agent
18
+ # @rbs renderer: Riffer::Rig::UI::Renderer
19
+ # @rbs input: IO
20
+ # @rbs output: IO
21
+ # @rbs theme: Riffer::Rig::UI::Theme
22
+ # @rbs animator: Riffer::Rig::UI::Animator
23
+ # @rbs smoother: Riffer::Rig::UI::Smoother
24
+ # @rbs cursor: Riffer::Rig::UI::Cursor
25
25
  # @rbs return: void
26
- def initialize(agent:, renderer:, input: $stdin, output: $stdout, theme: Riffer::Rig::UI::Theme.for(output),
27
- animator: Riffer::Rig::UI::Animator.new(
28
- io: output,
29
- theme:
30
- ), smoother: Riffer::Rig::UI::Smoother.new(
31
- io: output,
32
- theme:
33
- ), cursor: Riffer::Rig::UI::Cursor.new(
34
- io: output, theme:
35
- ))
26
+ def initialize(
27
+ agent:,
28
+ renderer:,
29
+ input: $stdin,
30
+ output: $stdout,
31
+ theme: Riffer::Rig::UI::Theme.for(output),
32
+ animator: Riffer::Rig::UI::Animator.new(io: output, theme:),
33
+ smoother: Riffer::Rig::UI::Smoother.new(io: output, theme:),
34
+ cursor: Riffer::Rig::UI::Cursor.new(io: output, theme:)
35
+ )
36
36
  @agent = agent
37
37
  @renderer = renderer
38
38
  @animator = animator
@@ -74,14 +74,14 @@ class Riffer::Rig::REPL
74
74
  # @rbs return: void
75
75
  def run_turn(prompt)
76
76
  @cursor.hide
77
- @animator.start
77
+ start_animator
78
78
  @smoother.start
79
79
  @agent.stream(prompt).each do |event|
80
80
  case event
81
81
  when Riffer::StreamEvents::ReasoningDelta
82
- @animator.start(:reasoning)
82
+ start_animator(:reasoning)
83
83
  when Riffer::StreamEvents::ReasoningDone
84
- @animator.start
84
+ start_animator
85
85
  when Riffer::StreamEvents::ToolCallDelta, Riffer::StreamEvents::FinishReasonDone
86
86
  next
87
87
  when Riffer::StreamEvents::SkillActivation
@@ -89,13 +89,13 @@ class Riffer::Rig::REPL
89
89
  # spinner comes straight back on to cover the silent stretch.
90
90
  @animator.stop
91
91
  @renderer.render(event)
92
- @animator.start
92
+ start_animator
93
93
  next
94
94
  when Riffer::StreamEvents::TokenUsageDone
95
95
  # Usage renders nothing inline — it flushes below the turn's output.
96
96
  # Restarting here covers the silent tool-execution stretch that follows.
97
97
  @renderer.render(event)
98
- @animator.start
98
+ start_animator
99
99
  next
100
100
  else
101
101
  @animator.stop
@@ -158,6 +158,7 @@ class Riffer::Rig::REPL
158
158
  # their line with a newline but leave the renderer's tool-group state alone —
159
159
  # they can only follow prose, never mid-group.
160
160
  #
161
+ # @rbs &block: () -> String
161
162
  # @rbs return: void
162
163
  def print_block(&)
163
164
  @output.puts
@@ -172,6 +173,17 @@ class Riffer::Rig::REPL
172
173
 
173
174
  @animator.stop
174
175
  @renderer.render_tool_result(message)
175
- @animator.start
176
+ start_animator
177
+ end
178
+
179
+ # Every animator start sits behind a smoother drain: the smoother's backlog
180
+ # may still be trickling out from streamed prose, and spinner frames drawn
181
+ # mid-drain would carve `\r…\e[K` through a half-printed sentence.
182
+ #
183
+ # @rbs mode: Symbol
184
+ # @rbs return: void
185
+ def start_animator(mode = :neutral)
186
+ @smoother.drain
187
+ @animator.start(mode)
176
188
  end
177
189
  end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ # One Runtime builds its own Riffer::Agent from a per-instance
6
+ # Riffer::Agent::Config and runs a prompt: streamed as events, or run to
7
+ # completion and returned as riffer's Agent::Response.
8
+ #
9
+ # runtime = Riffer::Rig::Runtime.new('mock/x')
10
+ # runtime.prompt('hello') { |event| ... } # yields riffer StreamEvents
11
+ # runtime.prompt('hello').each { |event| } # an Enumerator without a block
12
+ # response = runtime.ask('hello') # a Riffer::Agent::Response
13
+ #
14
+ # Two Runtimes in one process share nothing but the process-wide extension
15
+ # registry and riffer's provider repository. One prompt runs at a time; a
16
+ # second while one is running raises Riffer::Rig::Runtime::BusyError. The
17
+ # Runtime never renders, never prints, never reads the filesystem.
18
+ class Riffer::Rig::Runtime
19
+ # Raised when a second prompt, ask or registrar build runs while one is
20
+ # already running on this Runtime.
21
+ class BusyError < StandardError; end
22
+
23
+ BASE_PROMPT_TEMPLATE = <<~TEXT
24
+ You are %<name>s, a general-purpose agent. You work by using the tools you have
25
+ been given; each tool describes what it does and when to use it.
26
+
27
+ - Verify with your tools before answering. When a tool can settle a question,
28
+ look rather than guess.
29
+ - Do what was asked, and all of what was asked. Do not widen the scope, tidy
30
+ nearby things, or add extras that were not requested.
31
+ - Be concise. Lead with the outcome; do not restate the question.
32
+ TEXT
33
+
34
+ DEFAULT_NAME = 'riffer'
35
+
36
+ # The legacy CodingAgent's default: an unlimited agent loop. riffer's own
37
+ # default (16) is too small for a general-purpose harness.
38
+ DEFAULT_MAX_STEPS = nil #: Integer?
39
+
40
+ # @rbs!
41
+ # interface _Host
42
+ # def ask: (?String, ?options: Array[String]?, ?secret: bool) -> String?
43
+ # def confirm: (?String) -> bool
44
+ # def notify: (?String, ?level: Symbol) -> void
45
+ # def progress: (?String) { () -> void } -> void
46
+ # def capabilities: () -> Set[Symbol]
47
+ # end
48
+
49
+ # @rbs @agent: Riffer::Agent
50
+ # @rbs @cwd: String
51
+ # @rbs @host: _Host
52
+ # @rbs @settings: Hash[Symbol, untyped]
53
+ # @rbs @busy: bool
54
+ # @rbs @registrar: Riffer::Rig::Registrar
55
+
56
+ # @dynamic agent, cwd, host, settings
57
+ attr_reader :agent #: Riffer::Agent
58
+ attr_reader :cwd #: String
59
+ attr_reader :host #: _Host
60
+ attr_reader :settings #: Hash[Symbol, untyped]
61
+
62
+ # @rbs model: String
63
+ # @rbs extensions: Array[Riffer::Rig::Extension]
64
+ # @rbs tools: Array[String]?
65
+ # @rbs settings: Hash[Symbol, untyped]
66
+ # @rbs host: _Host
67
+ # @rbs cwd: String?
68
+ # @rbs name: String
69
+ # @rbs instructions: String?
70
+ # @rbs credentials: Hash[String, String]
71
+ # @rbs pricing: Hash[String, Riffer::Rig::Settings::Pricing]
72
+ # @rbs max_steps: Integer?
73
+ # @rbs snapshot: Hash[Symbol, untyped]?
74
+ # @rbs return: void
75
+ def initialize(
76
+ model,
77
+ extensions: [],
78
+ tools: nil,
79
+ settings: {},
80
+ host: Riffer::Rig::Host.new,
81
+ cwd: nil,
82
+ name: DEFAULT_NAME,
83
+ instructions: nil,
84
+ credentials: {},
85
+ pricing: {},
86
+ max_steps: DEFAULT_MAX_STEPS,
87
+ snapshot: nil
88
+ )
89
+ @host = host
90
+ @cwd = cwd || Dir.pwd
91
+ @settings = settings
92
+
93
+ @busy = false
94
+ @registrar = build_registrar(extensions)
95
+ tool_classes = select_tools(@registrar.tools, tools)
96
+ base_prompt = instructions || format(BASE_PROMPT_TEMPLATE, name: name)
97
+
98
+ @agent = Riffer::Agent.new(
99
+ config: Riffer::Agent::Config.new(
100
+ model: model,
101
+ instructions: system_prompt(base_prompt),
102
+ tools_config: tool_classes,
103
+ max_steps: max_steps
104
+ )
105
+ )
106
+ end
107
+
108
+ # @rbs text: String
109
+ # @rbs &block: ?(Riffer::StreamEvents::Base) -> void
110
+ # @rbs return: (nil | Enumerator[Riffer::StreamEvents::Base, void])
111
+ def prompt(text, &block)
112
+ raise BusyError, 'a prompt is already running on this Runtime' if @busy
113
+
114
+ @busy = true
115
+ if block
116
+ @agent.stream(text).each(&block)
117
+ nil
118
+ else
119
+ @agent.stream(text)
120
+ end
121
+ ensure
122
+ @busy = false
123
+ end
124
+
125
+ # @rbs text: String
126
+ # @rbs return: Riffer::Agent::Response
127
+ def ask(text)
128
+ raise BusyError, 'a prompt is already running on this Runtime' if @busy
129
+
130
+ @busy = true
131
+ @agent.stream(text).each { |event| event }
132
+ ensure
133
+ @busy = false
134
+ end
135
+
136
+ private
137
+
138
+ # @rbs extensions: Array[Riffer::Rig::Extension]
139
+ # @rbs return: Riffer::Rig::Registrar
140
+ def build_registrar(extensions)
141
+ raise BusyError, 'a prompt is already running on this Runtime' if @busy
142
+
143
+ extensions.each_with_object(Riffer::Rig::Registrar.new) do |extension, registrar|
144
+ extension.run(registrar)
145
+ end
146
+ end
147
+
148
+ # @rbs registered: Array[singleton(Riffer::Tool)]
149
+ # @rbs allowlist: Array[String]?
150
+ # @rbs return: Array[singleton(Riffer::Tool)]
151
+ def select_tools(registered, allowlist)
152
+ return registered if allowlist.nil?
153
+
154
+ registered.select { |klass| allowlist.include?(klass.name) }
155
+ end
156
+
157
+ # @rbs base_prompt: String
158
+ # @rbs return: String
159
+ def system_prompt(base_prompt)
160
+ "#{base_prompt}\n\nCurrent date: #{Date.today}\nCurrent working directory: #{@cwd}"
161
+ end
162
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Riffer::Rig::Settings::Document
4
+ # @rbs @model: String?
5
+ # @rbs @reasoning: String?
6
+ # @rbs @models: Hash[String, Riffer::Rig::Settings::Pricing]
7
+
8
+ # @dynamic model, reasoning, models
9
+ attr_reader :model #: String?
10
+ attr_reader :reasoning #: String?
11
+ attr_reader :models #: Hash[String, Riffer::Rig::Settings::Pricing]
12
+
13
+ # @rbs source: untyped
14
+ # @rbs return: void
15
+ def initialize(source)
16
+ @model = source['model'].is_a?(String) ? source['model'] : nil
17
+ @reasoning = source['reasoning'].is_a?(String) ? source['reasoning'] : nil
18
+ entries = source['models'].is_a?(Hash) ? source['models'] : {} #: Hash[String, untyped]
19
+ @models = entries.filter_map do |name, entry|
20
+ [name, Riffer::Rig::Settings::Pricing.from(entry)] if name.is_a?(String) && entry.is_a?(Hash)
21
+ end.to_h
22
+ end
23
+ end
@@ -5,6 +5,17 @@ class Riffer::Rig::Settings::Pricing
5
5
  # @dynamic input, output, cache_write, cache_read
6
6
  attr_reader :input, :output, :cache_write, :cache_read #: Float
7
7
 
8
+ # @rbs entry: Hash[String, untyped]
9
+ # @rbs return: ::Riffer::Rig::Settings::Pricing
10
+ def self.from(entry)
11
+ new(
12
+ input: entry.fetch('input', 0).to_f,
13
+ output: entry.fetch('output', 0).to_f,
14
+ cache_write: entry.fetch('cache_write', 0).to_f,
15
+ cache_read: entry.fetch('cache_read', 0).to_f
16
+ )
17
+ end
18
+
8
19
  # @rbs input: Float
9
20
  # @rbs output: Float
10
21
  # @rbs cache_write: Float
@@ -51,7 +51,7 @@ module Riffer::Rig::Settings
51
51
  # @rbs path: String
52
52
  # @rbs return: String
53
53
  def model(path: PATH)
54
- read(path).fetch('model', DEFAULT_MODEL)
54
+ read(path).model || DEFAULT_MODEL
55
55
  end
56
56
 
57
57
  # Returns model options for the configured model and reasoning level, ready
@@ -80,15 +80,7 @@ module Riffer::Rig::Settings
80
80
  # @rbs path: String
81
81
  # @rbs return: Pricing?
82
82
  def pricing_for(model, path: PATH)
83
- entry = read(path).dig('models', model)
84
- return nil unless entry.is_a?(Hash)
85
-
86
- Pricing.new(
87
- input: entry.fetch('input', 0).to_f,
88
- output: entry.fetch('output', 0).to_f,
89
- cache_write: entry.fetch('cache_write', 0).to_f,
90
- cache_read: entry.fetch('cache_read', 0).to_f
91
- )
83
+ read(path).models[model]
92
84
  end
93
85
 
94
86
  private
@@ -102,10 +94,10 @@ module Riffer::Rig::Settings
102
94
  end
103
95
 
104
96
  # @rbs path: String
105
- # @rbs ?provider: String?
97
+ # @rbs provider: String?
106
98
  # @rbs return: String?
107
99
  def reasoning_for(path: PATH, provider: nil)
108
- level = read(path)['reasoning']
100
+ level = read(path).reasoning
109
101
  valid_levels = (provider && REASONING_LEVELS_BY_PROVIDER[provider]) || []
110
102
  valid_levels.include?(level) ? level : nil
111
103
  end
@@ -129,16 +121,16 @@ module Riffer::Rig::Settings
129
121
  end
130
122
  end
131
123
 
132
- # The parsed settings file, or an empty hash when the file is absent or
124
+ # The parsed settings file, or an empty document when the file is absent or
133
125
  # malformed.
134
126
  #
135
127
  # @rbs path: String
136
- # @rbs return: Hash[String, untyped]
128
+ # @rbs return: Document
137
129
  def read(path)
138
- return {} unless File.file?(path)
130
+ return Document.new({}) unless File.file?(path)
139
131
 
140
- JSON.parse(File.read(path))
132
+ Document.new(JSON.parse(File.read(path)))
141
133
  rescue JSON::ParserError
142
- {}
134
+ Document.new({})
143
135
  end
144
136
  end
@@ -22,7 +22,7 @@ class Riffer::Rig::TokenTally
22
22
  # @dynamic input_tokens, output_tokens, cache_write_tokens, cache_read_tokens
23
23
  attr_reader :input_tokens, :output_tokens, :cache_write_tokens, :cache_read_tokens
24
24
 
25
- # @rbs ?pricing: Riffer::Rig::Settings::Pricing?
25
+ # @rbs pricing: Riffer::Rig::Settings::Pricing?
26
26
  # @rbs return: void
27
27
  def initialize(pricing: nil)
28
28
  @pricing = pricing
@@ -23,7 +23,7 @@ class Riffer::Rig::Tools::Bash < Riffer::Tool
23
23
 
24
24
  # @rbs context: Riffer::Agent::Context?
25
25
  # @rbs command: String
26
- # @rbs ?timeout_ms: Integer
26
+ # @rbs timeout_ms: Integer
27
27
  # @rbs return: Riffer::Tools::Response
28
28
  def call(context:, command:, timeout_ms: DEFAULT_TIMEOUT_MS)
29
29
  output, status = run(command, timeout_ms / 1000.0)
@@ -19,7 +19,7 @@ class Riffer::Rig::Tools::Edit < Riffer::Tool
19
19
  # @rbs path: String
20
20
  # @rbs old_string: String
21
21
  # @rbs new_string: String
22
- # @rbs ?replace_all: bool
22
+ # @rbs replace_all: bool
23
23
  # @rbs return: Riffer::Tools::Response
24
24
  def call(context:, path:, old_string:, new_string:, replace_all: false)
25
25
  resolved = File.expand_path(path, Dir.pwd)
@@ -15,8 +15,8 @@ class Riffer::Rig::Tools::Read < Riffer::Tool
15
15
 
16
16
  # @rbs context: Riffer::Agent::Context?
17
17
  # @rbs path: String
18
- # @rbs ?offset: Integer?
19
- # @rbs ?limit: Integer
18
+ # @rbs offset: Integer?
19
+ # @rbs limit: Integer
20
20
  # @rbs return: Riffer::Tools::Response
21
21
  def call(context:, path:, offset: nil, limit: DEFAULT_LIMIT)
22
22
  resolved = File.expand_path(path, Dir.pwd)