riffer-rig 0.3.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 +27 -0
- data/lib/riffer/rig/cli.rb +16 -8
- data/lib/riffer/rig/coding_agent.rb +31 -34
- 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 +63 -59
- 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 +5 -2
- data/lib/riffer/rig/tools/edit.rb +11 -3
- 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 +115 -36
- data/lib/riffer/rig/ui/smoother.rb +29 -13
- 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 +24 -23
- 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 +65 -15
- data/sig/generated/riffer/rig/ui/smoother.rbs +10 -5
- 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 +24 -7
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,32 @@
|
|
|
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
|
+
|
|
18
|
+
## [0.4.0](https://github.com/bottrall/riffer-rig/compare/v0.3.0...v0.4.0) (2026-09-13)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **ui:** one blank line between blocks, prompt-owned gaps ([#170](https://github.com/bottrall/riffer-rig/issues/170)) ([636e4d8](https://github.com/bottrall/riffer-rig/commit/636e4d82a069f158775043037a43e2aa494f56dc))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* **lint:** adjust rubocop rules and reformat to match ([#168](https://github.com/bottrall/riffer-rig/issues/168)) ([56b81aa](https://github.com/bottrall/riffer-rig/commit/56b81aa904609f246340c8b8135ee7e57791abdd))
|
|
29
|
+
|
|
3
30
|
## [0.3.0](https://github.com/bottrall/riffer-rig/compare/v0.2.1...v0.3.0) (2026-09-10)
|
|
4
31
|
|
|
5
32
|
|
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?
|
|
@@ -101,7 +100,16 @@ module Riffer::Rig::CLI
|
|
|
101
100
|
loaded = [Riffer::Rig::CodingAgent::GLOBAL_AGENTS_FILE, File.join(Dir.pwd, 'AGENTS.md')].select { |path| File.file?(path) }
|
|
102
101
|
context = loaded.empty? ? 'none' : loaded.join(', ')
|
|
103
102
|
|
|
104
|
-
animator.reveal(
|
|
103
|
+
animator.reveal(
|
|
104
|
+
[Riffer::Rig::UI::Banner.lines(
|
|
105
|
+
theme,
|
|
106
|
+
model: model,
|
|
107
|
+
cwd: Dir.pwd,
|
|
108
|
+
context: context,
|
|
109
|
+
skills: count_skills,
|
|
110
|
+
version: Riffer::Rig::VERSION
|
|
111
|
+
)]
|
|
112
|
+
)
|
|
105
113
|
end
|
|
106
114
|
|
|
107
115
|
# @rbs return: String
|
|
@@ -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) })
|
|
@@ -25,32 +20,34 @@ class Riffer::Rig::CodingAgent < Riffer::Agent
|
|
|
25
20
|
|
|
26
21
|
max_steps nil
|
|
27
22
|
|
|
28
|
-
instructions(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
23
|
+
instructions(
|
|
24
|
+
lambda do
|
|
25
|
+
context_files = [GLOBAL_AGENTS_FILE, File.join(Dir.pwd, 'AGENTS.md')]
|
|
26
|
+
.select { |path| File.file?(path) }
|
|
27
|
+
.map { |path| "<project_instructions path=\"#{path}\">\n#{File.read(path)}\n</project_instructions>" }
|
|
28
|
+
|
|
29
|
+
project_context =
|
|
30
|
+
if context_files.empty?
|
|
31
|
+
''
|
|
32
|
+
else
|
|
33
|
+
"\n\n<project_context>\n#{context_files.join("\n\n")}\n</project_context>"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
<<~PROMPT.chomp + project_context + "\n\nCurrent date: #{Date.today}\nCurrent working directory: #{Dir.pwd}"
|
|
37
|
+
You are an expert coding assistant running inside riffer-rig, a terminal coding agent. You help the user by reading files, running shell commands, editing code, and writing new files.
|
|
38
|
+
|
|
39
|
+
Available tools:
|
|
40
|
+
- read: read a file's contents
|
|
41
|
+
- write: write content to a file
|
|
42
|
+
- edit: replace an exact string in a file
|
|
43
|
+
- bash: run a shell command in the working directory
|
|
44
|
+
|
|
45
|
+
Guidelines:
|
|
46
|
+
- Be concise and direct in your responses.
|
|
47
|
+
- Show file paths clearly when working with files.
|
|
48
|
+
- Use bash for file exploration (ls, rg, find) rather than guessing.
|
|
49
|
+
- Prefer editing existing files over creating new ones.
|
|
50
|
+
PROMPT
|
|
51
|
+
end
|
|
52
|
+
)
|
|
56
53
|
end
|
|
@@ -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,20 +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
|
-
|
|
18
|
-
# @rbs
|
|
19
|
-
# @rbs
|
|
20
|
-
# @rbs
|
|
21
|
-
# @rbs
|
|
22
|
-
# @rbs
|
|
23
|
-
# @rbs
|
|
24
|
-
# @rbs
|
|
25
|
-
# @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
|
|
26
25
|
# @rbs return: void
|
|
27
|
-
def initialize(
|
|
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
|
+
)
|
|
28
36
|
@agent = agent
|
|
29
37
|
@renderer = renderer
|
|
30
38
|
@animator = animator
|
|
@@ -33,14 +41,13 @@ class Riffer::Rig::REPL
|
|
|
33
41
|
@theme = theme
|
|
34
42
|
@input = input
|
|
35
43
|
@output = output
|
|
36
|
-
@pending_tool_calls = []
|
|
37
44
|
@agent.session.on_message { |message| render_tool_result(message) }
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
# @rbs return: Symbol
|
|
41
48
|
def run
|
|
42
49
|
loop do
|
|
43
|
-
@
|
|
50
|
+
@renderer.prompt
|
|
44
51
|
line = @input.gets
|
|
45
52
|
break if line.nil?
|
|
46
53
|
|
|
@@ -57,7 +64,7 @@ class Riffer::Rig::REPL
|
|
|
57
64
|
end
|
|
58
65
|
end
|
|
59
66
|
|
|
60
|
-
|
|
67
|
+
print_block { @theme.grey('see you on the next riff.') }
|
|
61
68
|
:done
|
|
62
69
|
end
|
|
63
70
|
|
|
@@ -67,49 +74,41 @@ class Riffer::Rig::REPL
|
|
|
67
74
|
# @rbs return: void
|
|
68
75
|
def run_turn(prompt)
|
|
69
76
|
@cursor.hide
|
|
70
|
-
|
|
77
|
+
start_animator
|
|
71
78
|
@smoother.start
|
|
72
79
|
@agent.stream(prompt).each do |event|
|
|
73
80
|
case event
|
|
74
81
|
when Riffer::StreamEvents::ReasoningDelta
|
|
75
|
-
|
|
82
|
+
start_animator(:reasoning)
|
|
76
83
|
when Riffer::StreamEvents::ReasoningDone
|
|
77
|
-
|
|
84
|
+
start_animator
|
|
78
85
|
when Riffer::StreamEvents::ToolCallDelta, Riffer::StreamEvents::FinishReasonDone
|
|
79
|
-
# Round bookkeeping: nothing renders, so the indicator just carries on
|
|
80
|
-
# (or stays parked while the smoother finishes typing earlier text).
|
|
81
86
|
next
|
|
82
|
-
when Riffer::StreamEvents::
|
|
83
|
-
# Tool
|
|
84
|
-
#
|
|
85
|
-
@pending_tool_calls << event
|
|
86
|
-
next
|
|
87
|
-
when Riffer::StreamEvents::SkillActivation, Riffer::StreamEvents::TokenUsageDone
|
|
88
|
-
# The spinner shares its line with what's about to print, and tool
|
|
89
|
-
# execution plus the next model invocation emit no events — stop it for
|
|
90
|
-
# the render, then bring it straight back to cover the silent stretch.
|
|
91
|
-
# Stats print before any pending tool call lines (see below).
|
|
87
|
+
when Riffer::StreamEvents::SkillActivation
|
|
88
|
+
# Tool execution and the next model invocation emit no events, so the
|
|
89
|
+
# spinner comes straight back on to cover the silent stretch.
|
|
92
90
|
@animator.stop
|
|
93
91
|
@renderer.render(event)
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
start_animator
|
|
93
|
+
next
|
|
94
|
+
when Riffer::StreamEvents::TokenUsageDone
|
|
95
|
+
# Usage renders nothing inline — it flushes below the turn's output.
|
|
96
|
+
# Restarting here covers the silent tool-execution stretch that follows.
|
|
97
|
+
@renderer.render(event)
|
|
98
|
+
start_animator
|
|
96
99
|
next
|
|
97
100
|
else
|
|
98
101
|
@animator.stop
|
|
99
102
|
end
|
|
100
|
-
flush_pending_tool_calls
|
|
101
103
|
@renderer.render(event)
|
|
102
104
|
end
|
|
103
105
|
@animator.stop
|
|
104
|
-
flush_pending_tool_calls
|
|
105
106
|
@smoother.finish
|
|
106
|
-
@
|
|
107
|
+
@renderer.flush_usage
|
|
107
108
|
rescue StandardError => e
|
|
108
|
-
@animator.stop
|
|
109
|
-
flush_pending_tool_calls
|
|
110
|
-
@output.puts("\nError: #{e.message}")
|
|
111
|
-
ensure
|
|
112
109
|
@smoother.finish
|
|
110
|
+
print_block { @theme.red("Error: #{e.message}") }
|
|
111
|
+
ensure
|
|
113
112
|
@animator.stop
|
|
114
113
|
@cursor.show
|
|
115
114
|
end
|
|
@@ -130,7 +129,7 @@ class Riffer::Rig::REPL
|
|
|
130
129
|
skills = @agent.context.skills
|
|
131
130
|
|
|
132
131
|
unless skills
|
|
133
|
-
@
|
|
132
|
+
print_block { @theme.grey('No skills configured.') }
|
|
134
133
|
return
|
|
135
134
|
end
|
|
136
135
|
|
|
@@ -138,13 +137,13 @@ class Riffer::Rig::REPL
|
|
|
138
137
|
# a non-mutating Context#read. `activate` marks the skill model-activated as
|
|
139
138
|
# a side effect, which drops it from the model's catalog after manual use.
|
|
140
139
|
body = skills.activate(name)
|
|
141
|
-
@
|
|
140
|
+
print_block { @theme.magenta("✦ skill: #{name}") }
|
|
142
141
|
skill_block(name, body)
|
|
143
142
|
rescue Riffer::ArgumentError
|
|
144
|
-
@
|
|
143
|
+
print_block { @theme.red("Unknown skill: #{name}") }
|
|
145
144
|
nil
|
|
146
145
|
rescue StandardError => e
|
|
147
|
-
@
|
|
146
|
+
print_block { @theme.red("Error activating skill: #{e.message}") }
|
|
148
147
|
nil
|
|
149
148
|
end
|
|
150
149
|
|
|
@@ -155,24 +154,18 @@ class Riffer::Rig::REPL
|
|
|
155
154
|
"<skill name=\"#{name}\">\n#{body}\n</skill>"
|
|
156
155
|
end
|
|
157
156
|
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
# or the next event forces them out. Callers must have the spinner stopped —
|
|
162
|
-
# the flush renders between their existing stop/start pair.
|
|
157
|
+
# Chrome lines that never pass through the renderer (errors, exit line) end
|
|
158
|
+
# their line with a newline but leave the renderer's tool-group state alone —
|
|
159
|
+
# they can only follow prose, never mid-group.
|
|
163
160
|
#
|
|
161
|
+
# @rbs &block: () -> String
|
|
164
162
|
# @rbs return: void
|
|
165
|
-
def
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
@
|
|
169
|
-
@pending_tool_calls = []
|
|
163
|
+
def print_block(&)
|
|
164
|
+
@output.puts
|
|
165
|
+
@output.puts(yield)
|
|
166
|
+
@output.flush
|
|
170
167
|
end
|
|
171
168
|
|
|
172
|
-
# Tool results can land mid-animation (tool execution emits no stream events,
|
|
173
|
-
# so the indicator is up); stop it around the line so its next frame doesn't
|
|
174
|
-
# erase what we printed.
|
|
175
|
-
#
|
|
176
169
|
# @rbs message: Riffer::Messages::Base
|
|
177
170
|
# @rbs return: void
|
|
178
171
|
def render_tool_result(message)
|
|
@@ -180,6 +173,17 @@ class Riffer::Rig::REPL
|
|
|
180
173
|
|
|
181
174
|
@animator.stop
|
|
182
175
|
@renderer.render_tool_result(message)
|
|
183
|
-
|
|
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)
|
|
184
188
|
end
|
|
185
189
|
end
|