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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/riffer/rig/cli.rb +6 -7
- data/lib/riffer/rig/coding_agent.rb +1 -6
- data/lib/riffer/rig/credentials.rb +7 -7
- data/lib/riffer/rig/extension.rb +31 -0
- data/lib/riffer/rig/host.rb +52 -0
- data/lib/riffer/rig/registrar.rb +23 -0
- data/lib/riffer/rig/repl.rb +39 -27
- data/lib/riffer/rig/runtime.rb +162 -0
- data/lib/riffer/rig/settings/document.rb +23 -0
- data/lib/riffer/rig/settings/pricing.rb +11 -0
- data/lib/riffer/rig/settings.rb +9 -17
- data/lib/riffer/rig/token_tally.rb +1 -1
- data/lib/riffer/rig/tools/bash.rb +1 -1
- data/lib/riffer/rig/tools/edit.rb +1 -1
- data/lib/riffer/rig/tools/read.rb +2 -2
- data/lib/riffer/rig/ui/animator.rb +6 -6
- data/lib/riffer/rig/ui/cursor.rb +4 -4
- data/lib/riffer/rig/ui/renderer.rb +9 -7
- data/lib/riffer/rig/ui/smoother.rb +4 -4
- data/lib/riffer/rig/ui/theme.rb +2 -2
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +31 -0
- data/sig/generated/riffer/rig/cli.rbs +8 -8
- data/sig/generated/riffer/rig/credentials.rbs +4 -3
- data/sig/generated/riffer/rig/extension.rbs +28 -0
- data/sig/generated/riffer/rig/host.rbs +42 -0
- data/sig/generated/riffer/rig/registrar.rbs +17 -0
- data/sig/generated/riffer/rig/repl.rbs +21 -12
- data/sig/generated/riffer/rig/runtime.rbs +101 -0
- data/sig/generated/riffer/rig/settings/document.rbs +20 -0
- data/sig/generated/riffer/rig/settings/pricing.rbs +4 -0
- data/sig/generated/riffer/rig/settings.rbs +5 -5
- data/sig/generated/riffer/rig/token_tally.rbs +2 -2
- data/sig/generated/riffer/rig/tools/bash.rbs +2 -2
- data/sig/generated/riffer/rig/tools/edit.rbs +2 -2
- data/sig/generated/riffer/rig/tools/read.rbs +3 -3
- data/sig/generated/riffer/rig/ui/animator.rbs +8 -8
- data/sig/generated/riffer/rig/ui/cursor.rbs +4 -4
- data/sig/generated/riffer/rig/ui/renderer.rbs +13 -11
- data/sig/generated/riffer/rig/ui/smoother.rbs +4 -4
- data/sig/generated/riffer/rig/ui/theme.rbs +2 -2
- data/sig/generated/riffer/rig.rbs +19 -0
- data/sig/manual/riffer/rig.rbs +11 -0
- metadata +22 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a1580a7e4f2ed5e6d1f18714ef469b7d4623bec94552510a836a5f5e5664504
|
|
4
|
+
data.tar.gz: 7e871b6b1f8c9de6e3788979103f7afe65beea7a9c5fa7813e17b29e4e8eb528
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
data/lib/riffer/rig/cli.rb
CHANGED
|
@@ -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:
|
|
16
|
-
# @rbs input:
|
|
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:
|
|
59
|
-
# @rbs input:
|
|
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:
|
|
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).
|
|
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
|
|
72
|
-
return key unless key.strip.empty?
|
|
71
|
+
return nil if key.nil? || key.strip.empty?
|
|
73
72
|
|
|
74
|
-
|
|
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,
|
|
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
|
data/lib/riffer/rig/repl.rb
CHANGED
|
@@ -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:
|
|
15
|
-
# @rbs @output:
|
|
16
|
-
|
|
17
|
-
# @rbs
|
|
18
|
-
# @rbs
|
|
19
|
-
# @rbs input:
|
|
20
|
-
# @rbs output:
|
|
21
|
-
# @rbs
|
|
22
|
-
# @rbs
|
|
23
|
-
# @rbs
|
|
24
|
-
# @rbs
|
|
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(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
start_animator(:reasoning)
|
|
83
83
|
when Riffer::StreamEvents::ReasoningDone
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/riffer/rig/settings.rb
CHANGED
|
@@ -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).
|
|
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
|
-
|
|
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
|
|
97
|
+
# @rbs provider: String?
|
|
106
98
|
# @rbs return: String?
|
|
107
99
|
def reasoning_for(path: PATH, provider: nil)
|
|
108
|
-
level = read(path)
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
19
|
-
# @rbs
|
|
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)
|