riffer-rig 0.5.0 → 0.6.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 +22 -0
- data/README.md +43 -6
- data/lib/riffer/rig/cli.rb +46 -41
- data/lib/riffer/rig/credentials/resolution.rb +16 -0
- data/lib/riffer/rig/credentials.rb +151 -60
- data/lib/riffer/rig/env.rb +29 -0
- data/lib/riffer/rig/events/command_output.rb +27 -0
- data/lib/riffer/rig/events/event.rb +30 -0
- data/lib/riffer/rig/events/notify.rb +27 -0
- data/lib/riffer/rig/events/session_end.rb +24 -0
- data/lib/riffer/rig/events/session_start.rb +27 -0
- data/lib/riffer/rig/events/skill_activated.rb +24 -0
- data/lib/riffer/rig/events/turn_end.rb +32 -0
- data/lib/riffer/rig/events.rb +4 -0
- data/lib/riffer/rig/extension.rb +0 -4
- data/lib/riffer/rig/hosts/base.rb +36 -0
- data/lib/riffer/rig/hosts/mirror.rb +60 -0
- data/lib/riffer/rig/hosts/null.rb +36 -0
- data/lib/riffer/rig/provider_setup/field.rb +24 -0
- data/lib/riffer/rig/provider_setup.rb +84 -0
- data/lib/riffer/rig/registrar.rb +14 -2
- data/lib/riffer/rig/repl.rb +7 -8
- data/lib/riffer/rig/runtime/cancel_flag.rb +27 -0
- data/lib/riffer/rig/runtime.rb +104 -41
- data/lib/riffer/rig/settings/document.rb +6 -1
- data/lib/riffer/rig/settings/pricing.rb +0 -1
- data/lib/riffer/rig/settings.rb +47 -50
- data/lib/riffer/rig/token_tally.rb +0 -20
- data/lib/riffer/rig/tools/bash.rb +32 -11
- data/lib/riffer/rig/ui/animator.rb +2 -9
- data/lib/riffer/rig/ui/cursor.rb +0 -2
- data/lib/riffer/rig/ui/renderer.rb +9 -24
- data/lib/riffer/rig/ui/smoother.rb +11 -19
- data/lib/riffer/rig/ui/theme.rb +3 -5
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +2 -7
- data/sig/generated/riffer/rig/cli.rbs +20 -12
- data/sig/generated/riffer/rig/credentials/resolution.rbs +13 -0
- data/sig/generated/riffer/rig/credentials.rbs +70 -41
- data/sig/generated/riffer/rig/env.rbs +20 -0
- data/sig/generated/riffer/rig/events/command_output.rbs +19 -0
- data/sig/generated/riffer/rig/events/event.rbs +20 -0
- data/sig/generated/riffer/rig/events/notify.rbs +19 -0
- data/sig/generated/riffer/rig/events/session_end.rbs +16 -0
- data/sig/generated/riffer/rig/events/session_start.rbs +19 -0
- data/sig/generated/riffer/rig/events/skill_activated.rbs +16 -0
- data/sig/generated/riffer/rig/events/turn_end.rbs +22 -0
- data/sig/generated/riffer/rig/events.rbs +4 -0
- data/sig/generated/riffer/rig/extension.rbs +0 -4
- data/sig/generated/riffer/rig/hosts/base.rbs +26 -0
- data/sig/generated/riffer/rig/hosts/mirror.rbs +41 -0
- data/sig/generated/riffer/rig/hosts/null.rbs +26 -0
- data/sig/generated/riffer/rig/provider_setup/field.rbs +22 -0
- data/sig/generated/riffer/rig/provider_setup.rbs +30 -0
- data/sig/generated/riffer/rig/registrar.rbs +10 -2
- data/sig/generated/riffer/rig/repl.rbs +0 -8
- data/sig/generated/riffer/rig/runtime/cancel_flag.rbs +19 -0
- data/sig/generated/riffer/rig/runtime.rbs +58 -39
- data/sig/generated/riffer/rig/settings/document.rbs +5 -1
- data/sig/generated/riffer/rig/settings/pricing.rbs +0 -1
- data/sig/generated/riffer/rig/settings.rbs +25 -46
- data/sig/generated/riffer/rig/token_tally.rbs +0 -19
- data/sig/generated/riffer/rig/tools/bash.rbs +13 -1
- data/sig/generated/riffer/rig/ui/animator.rbs +0 -8
- data/sig/generated/riffer/rig/ui/cursor.rbs +0 -2
- data/sig/generated/riffer/rig/ui/renderer.rbs +0 -24
- data/sig/generated/riffer/rig/ui/smoother.rbs +5 -18
- data/sig/generated/riffer/rig/ui/theme.rbs +2 -4
- data/sig/generated/riffer/rig.rbs +0 -7
- data/sig/manual/riffer/rig/cli.rbs +2 -4
- data/sig/manual/riffer/rig/credentials.rbs +2 -4
- data/sig/manual/riffer/rig/hosts.rbs +5 -0
- data/sig/manual/riffer/rig/settings.rbs +2 -4
- data/sig/manual/riffer/rig/ui/banner.rbs +2 -4
- metadata +38 -13
- data/lib/riffer/rig/host.rb +0 -52
- data/sig/generated/riffer/rig/host.rbs +0 -42
data/lib/riffer/rig/settings.rb
CHANGED
|
@@ -3,36 +3,6 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
|
|
6
|
-
# Reads and writes the riffer-rig user settings file at
|
|
7
|
-
# <tt>~/.riffer/settings.json</tt>.
|
|
8
|
-
#
|
|
9
|
-
# Example file:
|
|
10
|
-
#
|
|
11
|
-
# {
|
|
12
|
-
# "model": "anthropic/claude-sonnet-4-6",
|
|
13
|
-
# "reasoning": "low",
|
|
14
|
-
# "models": {
|
|
15
|
-
# "anthropic/claude-sonnet-4-6": {
|
|
16
|
-
# "input": 3.0,
|
|
17
|
-
# "output": 15.0,
|
|
18
|
-
# "cache_write": 3.75,
|
|
19
|
-
# "cache_read": 0.3
|
|
20
|
-
# }
|
|
21
|
-
# }
|
|
22
|
-
# }
|
|
23
|
-
#
|
|
24
|
-
# All keys are optional. Missing pricing means cost display is suppressed.
|
|
25
|
-
# <tt>"reasoning"</tt> is translated to the appropriate provider-specific
|
|
26
|
-
# parameter on each API call; accepted values depend on the provider:
|
|
27
|
-
#
|
|
28
|
-
# - Anthropic: <tt>"low"</tt>, <tt>"medium"</tt>, <tt>"high"</tt>,
|
|
29
|
-
# <tt>"xhigh"</tt>, <tt>"max"</tt>
|
|
30
|
-
# - OpenAI / OpenRouter: <tt>"low"</tt>, <tt>"medium"</tt>, <tt>"high"</tt>,
|
|
31
|
-
# <tt>"xhigh"</tt>
|
|
32
|
-
#
|
|
33
|
-
# Omitting the key (or supplying an unrecognised value) leaves the model's
|
|
34
|
-
# default reasoning behaviour unchanged.
|
|
35
|
-
#
|
|
36
6
|
module Riffer::Rig::Settings
|
|
37
7
|
extend self
|
|
38
8
|
|
|
@@ -46,17 +16,12 @@ module Riffer::Rig::Settings
|
|
|
46
16
|
'openrouter' => %w[low medium high xhigh].freeze
|
|
47
17
|
}.freeze #: Hash[String, Array[String]]
|
|
48
18
|
|
|
49
|
-
# Returns the configured model string, or +DEFAULT_MODEL+ if not set.
|
|
50
|
-
#
|
|
51
19
|
# @rbs path: String
|
|
52
20
|
# @rbs return: String
|
|
53
21
|
def model(path: PATH)
|
|
54
22
|
read(path).model || DEFAULT_MODEL
|
|
55
23
|
end
|
|
56
24
|
|
|
57
|
-
# Returns model options for the configured model and reasoning level, ready
|
|
58
|
-
# to pass directly to the Riffer agent's +model_options+.
|
|
59
|
-
#
|
|
60
25
|
# @rbs path: String
|
|
61
26
|
# @rbs return: Hash[Symbol, untyped]
|
|
62
27
|
def model_options(path: PATH)
|
|
@@ -64,18 +29,12 @@ module Riffer::Rig::Settings
|
|
|
64
29
|
base_options(provider).merge(reasoning_options(reasoning_for(path:, provider:), provider))
|
|
65
30
|
end
|
|
66
31
|
|
|
67
|
-
# Returns the provider prefix for +model_string+, e.g. <tt>"anthropic"</tt>
|
|
68
|
-
# for <tt>"anthropic/claude-sonnet-4-6"</tt>. Returns +nil+ if the model
|
|
69
|
-
# string contains no slash.
|
|
70
|
-
#
|
|
71
32
|
# @rbs model_string: String
|
|
72
33
|
# @rbs return: String?
|
|
73
34
|
def provider_for(model_string)
|
|
74
35
|
model_string.split('/', 2).first if model_string.include?('/')
|
|
75
36
|
end
|
|
76
37
|
|
|
77
|
-
# Returns the pricing for +model+, or +nil+ if not configured.
|
|
78
|
-
#
|
|
79
38
|
# @rbs model: String
|
|
80
39
|
# @rbs path: String
|
|
81
40
|
# @rbs return: Pricing?
|
|
@@ -83,8 +42,46 @@ module Riffer::Rig::Settings
|
|
|
83
42
|
read(path).models[model]
|
|
84
43
|
end
|
|
85
44
|
|
|
45
|
+
# @rbs identifier: String
|
|
46
|
+
# @rbs path: String
|
|
47
|
+
# @rbs return: Hash[String, String]
|
|
48
|
+
def provider_fields(identifier, path: PATH)
|
|
49
|
+
read(path).providers[identifier] || {}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @rbs identifier: String
|
|
53
|
+
# @rbs fields: Hash[String, String]
|
|
54
|
+
# @rbs path: String
|
|
55
|
+
# @rbs return: void
|
|
56
|
+
def store_provider(identifier, fields, path: PATH)
|
|
57
|
+
update_providers(path) do |providers|
|
|
58
|
+
providers.merge(identifier => fields) { |_identifier, stored, given| stored.merge(given) }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @rbs identifier: String
|
|
63
|
+
# @rbs path: String
|
|
64
|
+
# @rbs return: void
|
|
65
|
+
def remove_provider(identifier, path: PATH)
|
|
66
|
+
update_providers(path) { |providers| providers.except(identifier) }
|
|
67
|
+
end
|
|
68
|
+
|
|
86
69
|
private
|
|
87
70
|
|
|
71
|
+
# @rbs path: String
|
|
72
|
+
# @rbs &: (Hash[String, Hash[String, String]]) -> Hash[String, Hash[String, String]]
|
|
73
|
+
# @rbs return: void
|
|
74
|
+
def update_providers(path)
|
|
75
|
+
source = read_source(path)
|
|
76
|
+
current = Document.new(source).providers
|
|
77
|
+
providers = yield current
|
|
78
|
+
return if providers == current
|
|
79
|
+
|
|
80
|
+
FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
|
|
81
|
+
document = providers.empty? ? source.except('providers') : source.merge('providers' => providers)
|
|
82
|
+
File.write(path, JSON.pretty_generate(document))
|
|
83
|
+
end
|
|
84
|
+
|
|
88
85
|
# @rbs provider: String?
|
|
89
86
|
# @rbs return: Hash[Symbol, untyped]
|
|
90
87
|
def base_options(provider)
|
|
@@ -102,9 +99,6 @@ module Riffer::Rig::Settings
|
|
|
102
99
|
valid_levels.include?(level) ? level : nil
|
|
103
100
|
end
|
|
104
101
|
|
|
105
|
-
# Maps a reasoning level to the provider-specific model option hash expected
|
|
106
|
-
# by Riffer. Returns an empty hash when +level+ is +nil+.
|
|
107
|
-
#
|
|
108
102
|
# @rbs level: String?
|
|
109
103
|
# @rbs provider: String?
|
|
110
104
|
# @rbs return: Hash[Symbol, untyped]
|
|
@@ -121,16 +115,19 @@ module Riffer::Rig::Settings
|
|
|
121
115
|
end
|
|
122
116
|
end
|
|
123
117
|
|
|
124
|
-
# The parsed settings file, or an empty document when the file is absent or
|
|
125
|
-
# malformed.
|
|
126
|
-
#
|
|
127
118
|
# @rbs path: String
|
|
128
119
|
# @rbs return: Document
|
|
129
120
|
def read(path)
|
|
130
|
-
|
|
121
|
+
Document.new(read_source(path))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @rbs path: String
|
|
125
|
+
# @rbs return: Hash[String, untyped]
|
|
126
|
+
def read_source(path)
|
|
127
|
+
return {} unless File.file?(path)
|
|
131
128
|
|
|
132
|
-
|
|
129
|
+
JSON.parse(File.read(path))
|
|
133
130
|
rescue JSON::ParserError
|
|
134
|
-
|
|
131
|
+
{}
|
|
135
132
|
end
|
|
136
133
|
end
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Accumulates token usage across multiple turns in a session and computes an
|
|
4
|
-
# estimated cost based on a pricing object sourced from
|
|
5
|
-
# +Riffer::Rig::Settings+.
|
|
6
|
-
#
|
|
7
|
-
# Instantiate once per REPL session, call +add+ after each turn, then read
|
|
8
|
-
# +total_tokens+ and +estimated_cost+ for display.
|
|
9
|
-
#
|
|
10
|
-
# pricing = Riffer::Rig::Settings.pricing_for('anthropic/claude-sonnet-4-6')
|
|
11
|
-
# tally = Riffer::Rig::TokenTally.new(pricing: pricing)
|
|
12
|
-
# tally.add(usage)
|
|
13
|
-
# tally.estimated_cost # => 0.000042 (USD), or nil if pricing is nil
|
|
14
|
-
#
|
|
15
3
|
class Riffer::Rig::TokenTally
|
|
16
4
|
# @rbs @input_tokens: Integer
|
|
17
5
|
# @rbs @output_tokens: Integer
|
|
@@ -32,8 +20,6 @@ class Riffer::Rig::TokenTally
|
|
|
32
20
|
@cache_read_tokens = 0
|
|
33
21
|
end
|
|
34
22
|
|
|
35
|
-
# Accumulates token counts from a +Riffer::Providers::TokenUsage+ object.
|
|
36
|
-
#
|
|
37
23
|
# @rbs usage: Riffer::Providers::TokenUsage
|
|
38
24
|
# @rbs return: void
|
|
39
25
|
def add(usage)
|
|
@@ -43,22 +29,16 @@ class Riffer::Rig::TokenTally
|
|
|
43
29
|
@cache_read_tokens += usage.cache_read_tokens || 0
|
|
44
30
|
end
|
|
45
31
|
|
|
46
|
-
# Returns the total token count across all categories.
|
|
47
|
-
#
|
|
48
32
|
# @rbs return: Integer
|
|
49
33
|
def total_tokens
|
|
50
34
|
@input_tokens + @output_tokens + @cache_write_tokens + @cache_read_tokens
|
|
51
35
|
end
|
|
52
36
|
|
|
53
|
-
# Returns +true+ if any tokens have been counted.
|
|
54
|
-
#
|
|
55
37
|
# @rbs return: bool
|
|
56
38
|
def any?
|
|
57
39
|
total_tokens.positive?
|
|
58
40
|
end
|
|
59
41
|
|
|
60
|
-
# Returns the estimated cost in USD, or +nil+ if no pricing was provided.
|
|
61
|
-
#
|
|
62
42
|
# @rbs return: Float?
|
|
63
43
|
def estimated_cost
|
|
64
44
|
pricing = @pricing
|
|
@@ -13,6 +13,8 @@ class Riffer::Rig::Tools::Bash < Riffer::Tool
|
|
|
13
13
|
|
|
14
14
|
MAX_OUTPUT_BYTES = 30_000 #: Integer
|
|
15
15
|
|
|
16
|
+
POLL_SECONDS = 0.1 #: Float
|
|
17
|
+
|
|
16
18
|
params do
|
|
17
19
|
required :command, String, description: 'The shell command to run'
|
|
18
20
|
optional :timeout_ms,
|
|
@@ -26,7 +28,8 @@ class Riffer::Rig::Tools::Bash < Riffer::Tool
|
|
|
26
28
|
# @rbs timeout_ms: Integer
|
|
27
29
|
# @rbs return: Riffer::Tools::Response
|
|
28
30
|
def call(context:, command:, timeout_ms: DEFAULT_TIMEOUT_MS)
|
|
29
|
-
|
|
31
|
+
cancel_flag = context&.[](:cancel_flag) #: Riffer::Rig::Runtime::CancelFlag?
|
|
32
|
+
output, status = run(command, timeout_ms / 1000.0, cancel_flag)
|
|
30
33
|
output = truncate(output.rstrip)
|
|
31
34
|
|
|
32
35
|
return error("Command exited with status #{status}\n#{output}", type: :command_failed) unless status.zero?
|
|
@@ -38,23 +41,41 @@ class Riffer::Rig::Tools::Bash < Riffer::Tool
|
|
|
38
41
|
|
|
39
42
|
# @rbs command: String
|
|
40
43
|
# @rbs timeout_seconds: Float
|
|
44
|
+
# @rbs cancel_flag: Riffer::Rig::Runtime::CancelFlag?
|
|
41
45
|
# @rbs return: [String, Integer]
|
|
42
|
-
def run(command, timeout_seconds)
|
|
46
|
+
def run(command, timeout_seconds, cancel_flag)
|
|
43
47
|
stdin, stdout_and_stderr, wait_thread = Open3.popen2e(command, chdir: Dir.pwd, pgroup: true)
|
|
44
48
|
stdin.close
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
ending = await(wait_thread, monotonic_now + timeout_seconds, cancel_flag)
|
|
51
|
+
kill_group(wait_thread.pid) unless ending == :exited
|
|
52
|
+
output = stdout_and_stderr.read
|
|
53
|
+
stdout_and_stderr.close
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return ["#{output}\n[timed out after #{timeout_seconds.round}s]", 124]
|
|
55
|
+
case ending
|
|
56
|
+
when :timed_out then ["#{output}\n[timed out after #{timeout_seconds.round}s]", 124]
|
|
57
|
+
when :cancelled then ["#{output}\n[cancelled]", 130]
|
|
58
|
+
else [output, wait_thread.value.exitstatus || 1]
|
|
53
59
|
end
|
|
60
|
+
end
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
# @rbs wait_thread: Process::Waiter
|
|
63
|
+
# @rbs deadline: Float
|
|
64
|
+
# @rbs cancel_flag: Riffer::Rig::Runtime::CancelFlag?
|
|
65
|
+
# @rbs return: Symbol
|
|
66
|
+
def await(wait_thread, deadline, cancel_flag)
|
|
67
|
+
# Upstream candidate: a cancel token on riffer's run loop would reach a
|
|
68
|
+
# running tool directly; until then the flag is polled from the context.
|
|
69
|
+
until wait_thread.join(POLL_SECONDS)
|
|
70
|
+
return :cancelled if cancel_flag&.set?
|
|
71
|
+
return :timed_out if monotonic_now >= deadline
|
|
72
|
+
end
|
|
73
|
+
:exited
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @rbs return: Float
|
|
77
|
+
def monotonic_now
|
|
78
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
58
79
|
end
|
|
59
80
|
|
|
60
81
|
# @rbs pid: Integer
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Animated output is gated behind an interactive, colour-enabled TTY, so piped
|
|
4
|
-
# or tested runs stay silent and escape-free.
|
|
5
3
|
class Riffer::Rig::UI::Animator
|
|
6
4
|
REVEAL_FRAME_SECONDS = 0.05 #: Float
|
|
7
5
|
|
|
@@ -41,8 +39,6 @@ class Riffer::Rig::UI::Animator
|
|
|
41
39
|
@phrase = nil
|
|
42
40
|
end
|
|
43
41
|
|
|
44
|
-
# When not on a TTY, prints the final frame once instead of animating.
|
|
45
|
-
#
|
|
46
42
|
# @rbs frames: Array[Array[String]]
|
|
47
43
|
# @rbs return: void
|
|
48
44
|
def reveal(frames)
|
|
@@ -104,7 +100,7 @@ class Riffer::Rig::UI::Animator
|
|
|
104
100
|
# @rbs return: void
|
|
105
101
|
def animate
|
|
106
102
|
tick = 0
|
|
107
|
-
roll_at = 0.0
|
|
103
|
+
roll_at = 0.0
|
|
108
104
|
until @stop
|
|
109
105
|
roll_at = roll_phrase(roll_at)
|
|
110
106
|
@io.print("\r #{equalizer(tick, label)}\e[K")
|
|
@@ -114,8 +110,6 @@ class Riffer::Rig::UI::Animator
|
|
|
114
110
|
end
|
|
115
111
|
end
|
|
116
112
|
|
|
117
|
-
# Re-rolls both phrase and duration whenever the reasoning tick expires.
|
|
118
|
-
#
|
|
119
113
|
# @rbs roll_at: Float
|
|
120
114
|
# @rbs return: Float
|
|
121
115
|
def roll_phrase(roll_at)
|
|
@@ -126,11 +120,10 @@ class Riffer::Rig::UI::Animator
|
|
|
126
120
|
now + roll_rand
|
|
127
121
|
end
|
|
128
122
|
|
|
129
|
-
# Range rand returns nil for an empty range; this one is a non-empty constant.
|
|
130
|
-
#
|
|
131
123
|
# @rbs return: Integer
|
|
132
124
|
def roll_rand
|
|
133
125
|
x = rand(REASONING_TICK_RANGE)
|
|
126
|
+
# Range rand returns nil for an empty range; this one is a non-empty constant.
|
|
134
127
|
x || 0
|
|
135
128
|
end
|
|
136
129
|
|
data/lib/riffer/rig/ui/cursor.rb
CHANGED
|
@@ -39,28 +39,24 @@ class Riffer::Rig::UI::Renderer
|
|
|
39
39
|
when Riffer::StreamEvents::Interrupt
|
|
40
40
|
render_block(0) { @theme.dim("[interrupted: #{event.reason}]") }
|
|
41
41
|
when Riffer::StreamEvents::TokenUsageDone
|
|
42
|
+
# Usage arrives per model call but tool results arrive via the session
|
|
43
|
+
# callback after each call's stream ends, so rendering inline would print
|
|
44
|
+
# the stats above the results they belong with.
|
|
42
45
|
current = @deferred_usage
|
|
43
46
|
@deferred_usage = current ? current + event.token_usage : event.token_usage
|
|
44
47
|
end
|
|
45
48
|
end
|
|
46
49
|
|
|
47
|
-
# The prompt holds the line open for typed input, so unlike render_block it
|
|
48
|
-
# ends without a newline. It sits at the renderer so a tool group left open by
|
|
49
|
-
# the previous turn closes here, and the next turn's blocks open cleanly.
|
|
50
|
-
#
|
|
51
50
|
# @rbs return: void
|
|
52
51
|
def prompt
|
|
52
|
+
# Closes any tool group the previous turn left open, so the next turn's
|
|
53
|
+
# blocks open cleanly.
|
|
53
54
|
@prose_gap_pending = true
|
|
54
55
|
@io.puts
|
|
55
56
|
@io.print("#{@theme.pink('›')} ")
|
|
56
57
|
@io.flush
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
# Usage arrives per model call but tool results arrive via the session
|
|
60
|
-
# callback after each call's stream ends, so rendering inline would print the
|
|
61
|
-
# stats above the results they belong with. Held until the turn's output is
|
|
62
|
-
# done, then printed as one line below it.
|
|
63
|
-
#
|
|
64
60
|
# @rbs return: void
|
|
65
61
|
def flush_usage
|
|
66
62
|
usage = @deferred_usage
|
|
@@ -86,11 +82,6 @@ class Riffer::Rig::UI::Renderer
|
|
|
86
82
|
|
|
87
83
|
private
|
|
88
84
|
|
|
89
|
-
# Rule 2 for one-off blocks (skill line, interrupt, stats): one blank line
|
|
90
|
-
# above, no indent. The smoother drains first so any pending partial prose
|
|
91
|
-
# block ends cleanly before the gap is written, and any open tool group closes
|
|
92
|
-
# so the next group opens with its own gap.
|
|
93
|
-
#
|
|
94
85
|
# @rbs indent: Integer
|
|
95
86
|
# @rbs &block: () -> String
|
|
96
87
|
# @rbs return: void
|
|
@@ -102,13 +93,11 @@ class Riffer::Rig::UI::Renderer
|
|
|
102
93
|
@io.flush
|
|
103
94
|
end
|
|
104
95
|
|
|
105
|
-
# The blank line above a prose block is written at the first delta after a
|
|
106
|
-
# non-prose block. Streaming makes "which block is first?" a stateful
|
|
107
|
-
# question, so the answer is tracked rather than embedded at each render site.
|
|
108
|
-
#
|
|
109
96
|
# @rbs content: String
|
|
110
97
|
# @rbs return: void
|
|
111
98
|
def render_prose(content)
|
|
99
|
+
# Streaming makes "which delta opens a prose block?" a stateful question,
|
|
100
|
+
# so the gap is tracked in a flag rather than written at each render site.
|
|
112
101
|
if @prose_gap_pending
|
|
113
102
|
@prose_gap_pending = false
|
|
114
103
|
@io.print("\n")
|
|
@@ -125,12 +114,10 @@ class Riffer::Rig::UI::Renderer
|
|
|
125
114
|
@io.flush
|
|
126
115
|
end
|
|
127
116
|
|
|
128
|
-
# Tool-activity lines (⚙ calls, ↳ results) share one blank line above the
|
|
129
|
-
# group instead of one between each line — the gap goes between groups, not
|
|
130
|
-
# inside a pair. The group stays open so prose after it pays the closing gap.
|
|
131
|
-
#
|
|
132
117
|
# @rbs return: void
|
|
133
118
|
def open_tool_activity
|
|
119
|
+
# Tool-activity lines (⚙ calls, ↳ results) share one blank line above the
|
|
120
|
+
# group; the group stays open so the prose after it pays the closing gap.
|
|
134
121
|
return if @prose_gap_pending
|
|
135
122
|
|
|
136
123
|
drain_smoother
|
|
@@ -157,8 +144,6 @@ class Riffer::Rig::UI::Renderer
|
|
|
157
144
|
@smoother || PassThroughSmoother.new(@io)
|
|
158
145
|
end
|
|
159
146
|
|
|
160
|
-
# Stand-in when no smoother is injected, so a bare Riffer::Rig::UI::Renderer still prints
|
|
161
|
-
# synchronously.
|
|
162
147
|
class PassThroughSmoother
|
|
163
148
|
# @rbs @io: IO
|
|
164
149
|
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Paces streamed text onto a 60fps cadence so API bursts read as continuous
|
|
4
|
-
# typing. On a non-TTY output it is transparent: writes pass through
|
|
5
|
-
# synchronously and no thread ever runs.
|
|
6
3
|
class Riffer::Rig::UI::Smoother
|
|
7
4
|
TICK_SECONDS = 1.0 / 60.0 #: Float
|
|
8
5
|
|
|
9
|
-
#
|
|
10
|
-
# decay
|
|
11
|
-
# never visibly stops between chunks.
|
|
12
|
-
# releases so the effective rate can settle at the provider's throughput;
|
|
13
|
-
# the floor is fractional (10 chars/s) so it can't over-drain a backlog
|
|
14
|
-
# that's trickling in slower than 60 chars/s.
|
|
15
|
-
#
|
|
6
|
+
# Releasing a fraction of the backlog, not a fixed rate, makes the reveal an
|
|
7
|
+
# exponential decay: its tail keeps text flowing for a second or more, so
|
|
8
|
+
# output never visibly stops between chunks.
|
|
16
9
|
BACKLOG_FRACTION_PER_TICK = Rational(1, 60) #: Rational
|
|
17
10
|
|
|
11
|
+
# Fractional (10 chars/s) so the floor can't over-drain a backlog that's
|
|
12
|
+
# trickling in slower than 60 chars/s.
|
|
18
13
|
MIN_CHARS_PER_TICK = Rational(1, 6) #: Rational
|
|
19
14
|
|
|
20
15
|
# @rbs @io: IO
|
|
@@ -72,13 +67,13 @@ class Riffer::Rig::UI::Smoother
|
|
|
72
67
|
self
|
|
73
68
|
end
|
|
74
69
|
|
|
75
|
-
# The write sits inside the mutex so a concurrent tick can't reorder a drain.
|
|
76
|
-
#
|
|
77
70
|
# @rbs return: void
|
|
78
71
|
def tick
|
|
79
72
|
@mutex.synchronize do
|
|
80
73
|
return if @backlog.empty?
|
|
81
74
|
|
|
75
|
+
# The carry accrues sub-character releases so the effective rate can
|
|
76
|
+
# settle at the provider's throughput.
|
|
82
77
|
@carry += [@backlog.length * BACKLOG_FRACTION_PER_TICK, MIN_CHARS_PER_TICK].max
|
|
83
78
|
count = @carry.floor
|
|
84
79
|
return if count.zero?
|
|
@@ -86,16 +81,12 @@ class Riffer::Rig::UI::Smoother
|
|
|
86
81
|
@carry -= count
|
|
87
82
|
chunk = @backlog.slice!(0, count) || ''
|
|
88
83
|
@newline_pending = !chunk.end_with?("\n")
|
|
84
|
+
# Written inside the mutex so a concurrent drain can't reorder output.
|
|
89
85
|
@io.print(chunk)
|
|
90
86
|
@io.flush
|
|
91
87
|
end
|
|
92
88
|
end
|
|
93
89
|
|
|
94
|
-
# Called before non-prose output so printed order matches stream order. The
|
|
95
|
-
# backlog is flushed, then any partial prose block the model left without a
|
|
96
|
-
# trailing newline gets one, so block spacing never depends on the model's
|
|
97
|
-
# last character.
|
|
98
|
-
#
|
|
99
90
|
# @rbs return: void
|
|
100
91
|
def drain
|
|
101
92
|
@mutex.synchronize do
|
|
@@ -106,16 +97,17 @@ class Riffer::Rig::UI::Smoother
|
|
|
106
97
|
end
|
|
107
98
|
return unless @newline_pending
|
|
108
99
|
|
|
100
|
+
# Block spacing must not depend on whether the model ended its prose
|
|
101
|
+
# with a newline.
|
|
109
102
|
@newline_pending = false
|
|
110
103
|
@io.print("\n")
|
|
111
104
|
@io.flush
|
|
112
105
|
end
|
|
113
106
|
end
|
|
114
107
|
|
|
115
|
-
# Idempotent: the REPL's ensure path runs it even after a happy-path finish.
|
|
116
|
-
#
|
|
117
108
|
# @rbs return: void
|
|
118
109
|
def finish
|
|
110
|
+
# Idempotent: the REPL's ensure path runs it even after a happy-path finish.
|
|
119
111
|
thread = @thread
|
|
120
112
|
if thread
|
|
121
113
|
@stop = true
|
data/lib/riffer/rig/ui/theme.rb
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# +enabled+ gates every escape so the same code path emits plain, escape-free
|
|
4
|
-
# text when piped or tested.
|
|
5
3
|
class Riffer::Rig::UI::Theme
|
|
6
4
|
# @rbs @enabled: bool
|
|
7
5
|
|
|
8
|
-
# @rbs return: bool
|
|
9
6
|
# @dynamic enabled
|
|
10
7
|
attr_reader :enabled #: bool
|
|
11
8
|
|
|
@@ -16,9 +13,10 @@ class Riffer::Rig::UI::Theme
|
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
# @rbs io: IO
|
|
16
|
+
# @rbs env: Riffer::Rig::Env
|
|
19
17
|
# @rbs return: Riffer::Rig::UI::Theme
|
|
20
|
-
def self.for(io)
|
|
21
|
-
new(enabled: io.tty? && !
|
|
18
|
+
def self.for(io, env: Riffer::Rig::Env.new)
|
|
19
|
+
new(enabled: io.tty? && !env.no_color)
|
|
22
20
|
end
|
|
23
21
|
|
|
24
22
|
# @rbs text: String
|
data/lib/riffer/rig/version.rb
CHANGED
data/lib/riffer/rig.rb
CHANGED
|
@@ -13,13 +13,6 @@ module Riffer::Rig
|
|
|
13
13
|
# @rbs self.@extensions: Hash[String, Riffer::Rig::Extension]
|
|
14
14
|
@extensions = {} #: Hash[String, Riffer::Rig::Extension]
|
|
15
15
|
|
|
16
|
-
# Records an extension block in the process registry and returns the
|
|
17
|
-
# extension object. Re-recording a name replaces the block.
|
|
18
|
-
#
|
|
19
|
-
# Riffer::Rig.extension('git') do |rig|
|
|
20
|
-
# rig.tool GitLog
|
|
21
|
-
# end
|
|
22
|
-
#
|
|
23
16
|
# @rbs name: String
|
|
24
17
|
# @rbs requires: String?
|
|
25
18
|
# @rbs &block: (::Riffer::Rig::Registrar) -> void
|
|
@@ -31,6 +24,8 @@ module Riffer::Rig
|
|
|
31
24
|
"extension #{name} requires riffer-rig #{ext.requires}, found #{Riffer::Rig::VERSION}"
|
|
32
25
|
end
|
|
33
26
|
|
|
27
|
+
# Keyed by name so reloading a file replaces its block rather than
|
|
28
|
+
# duplicating it.
|
|
34
29
|
@extensions[name] = ext
|
|
35
30
|
end
|
|
36
31
|
|
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
# Generated from lib/riffer/rig/cli.rb with RBS::Inline
|
|
2
2
|
|
|
3
3
|
module Riffer::Rig::CLI
|
|
4
|
-
PROVIDER_URLS: Hash[String, String]
|
|
5
|
-
|
|
6
4
|
# @rbs output: IO
|
|
7
5
|
# @rbs input: IO
|
|
6
|
+
# @rbs model: String
|
|
7
|
+
# @rbs env: Riffer::Rig::Env
|
|
8
8
|
# @rbs return: Integer
|
|
9
|
-
def start: (?output: IO, ?input: IO) -> Integer
|
|
9
|
+
def start: (?output: IO, ?input: IO, ?model: String, ?env: Riffer::Rig::Env) -> Integer
|
|
10
10
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
|
-
# @rbs provider: String
|
|
14
|
-
# @rbs
|
|
15
|
-
# @rbs
|
|
16
|
-
|
|
13
|
+
# @rbs provider: String
|
|
14
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
15
|
+
# @rbs output: IO
|
|
16
|
+
# @rbs input: IO
|
|
17
|
+
# @rbs env: Riffer::Rig::Env
|
|
18
|
+
# @rbs return: Hash[Symbol, String]?
|
|
19
|
+
def credentials_for: (String provider, Riffer::Rig::UI::Theme theme, output: IO, input: IO, env: Riffer::Rig::Env) -> Hash[Symbol, String]?
|
|
17
20
|
|
|
18
|
-
# @rbs provider: String
|
|
21
|
+
# @rbs provider: String
|
|
22
|
+
# @rbs resolution: Riffer::Rig::Credentials::Resolution
|
|
19
23
|
# @rbs theme: Riffer::Rig::UI::Theme
|
|
20
24
|
# @rbs output: IO
|
|
21
25
|
# @rbs input: IO
|
|
22
|
-
# @rbs return: String?
|
|
23
|
-
def onboard: (String
|
|
26
|
+
# @rbs return: Hash[Symbol, String]?
|
|
27
|
+
def onboard: (String provider, Riffer::Rig::Credentials::Resolution resolution, Riffer::Rig::UI::Theme theme, output: IO, input: IO) -> Hash[Symbol, String]?
|
|
24
28
|
|
|
25
|
-
# @rbs provider: String
|
|
29
|
+
# @rbs provider: String
|
|
30
|
+
# @rbs field: Riffer::Rig::ProviderSetup::Field
|
|
31
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
32
|
+
# @rbs output: IO
|
|
33
|
+
# @rbs input: IO
|
|
26
34
|
# @rbs return: String
|
|
27
|
-
def
|
|
35
|
+
def prompt: (String provider, Riffer::Rig::ProviderSetup::Field field, Riffer::Rig::UI::Theme theme, output: IO, input: IO) -> String
|
|
28
36
|
|
|
29
37
|
# @rbs input: IO
|
|
30
38
|
# @rbs return: String?
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Generated from lib/riffer/rig/credentials/resolution.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Credentials::Resolution
|
|
4
|
+
# @dynamic values, missing
|
|
5
|
+
attr_reader values: Hash[Symbol, String]
|
|
6
|
+
|
|
7
|
+
attr_reader missing: Array[Symbol]
|
|
8
|
+
|
|
9
|
+
# @rbs values: Hash[Symbol, String]
|
|
10
|
+
# @rbs missing: Array[Symbol]
|
|
11
|
+
# @rbs return: void
|
|
12
|
+
def initialize: (values: Hash[Symbol, String], missing: Array[Symbol]) -> void
|
|
13
|
+
end
|