riffer-rig 0.5.0 → 0.7.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 +58 -0
- data/README.md +77 -7
- data/exe/riffer +1 -1
- data/lib/riffer/rig/bundled/agents_md.rb +7 -0
- data/lib/riffer/rig/bundled/bash.rb +5 -0
- data/lib/riffer/rig/bundled/edit.rb +5 -0
- data/lib/riffer/rig/bundled/mcp.rb +9 -0
- data/lib/riffer/rig/bundled/read.rb +5 -0
- data/lib/riffer/rig/bundled/skills.rb +10 -0
- data/lib/riffer/rig/bundled/write.rb +5 -0
- data/lib/riffer/rig/bundled.rb +7 -0
- data/lib/riffer/rig/cli/flags.rb +77 -0
- data/lib/riffer/rig/cli.rb +70 -102
- data/lib/riffer/rig/command/context.rb +70 -0
- data/lib/riffer/rig/command.rb +29 -0
- data/lib/riffer/rig/commands/model.rb +54 -0
- data/lib/riffer/rig/commands/skill.rb +27 -0
- data/lib/riffer/rig/commands.rb +4 -0
- data/lib/riffer/rig/credentials/resolution.rb +16 -0
- data/lib/riffer/rig/credentials.rb +151 -60
- data/lib/riffer/rig/directories.rb +20 -0
- data/lib/riffer/rig/env/invalid.rb +13 -0
- data/lib/riffer/rig/env.rb +40 -0
- data/lib/riffer/rig/events/after_response.rb +25 -0
- data/lib/riffer/rig/events/after_tool_call.rb +31 -0
- data/lib/riffer/rig/events/before_prompt.rb +25 -0
- data/lib/riffer/rig/events/before_request.rb +25 -0
- data/lib/riffer/rig/events/before_tool_call.rb +28 -0
- data/lib/riffer/rig/events/command_output.rb +28 -0
- data/lib/riffer/rig/events/notify.rb +28 -0
- data/lib/riffer/rig/events/session_end.rb +25 -0
- data/lib/riffer/rig/events/session_start.rb +28 -0
- data/lib/riffer/rig/events/skill_activated.rb +25 -0
- data/lib/riffer/rig/events/turn_end.rb +33 -0
- data/lib/riffer/rig/events.rb +4 -0
- data/lib/riffer/rig/extension/failure.rb +16 -0
- data/lib/riffer/rig/extension.rb +16 -5
- data/lib/riffer/rig/hosts/mirror.rb +59 -0
- data/lib/riffer/rig/hosts/null.rb +36 -0
- data/lib/riffer/rig/loader.rb +163 -0
- data/lib/riffer/rig/mcp/declaration.rb +23 -0
- data/lib/riffer/rig/mcp/server.rb +16 -0
- data/lib/riffer/rig/mcp.rb +12 -0
- data/lib/riffer/rig/prompts/agents_md.rb +32 -0
- data/lib/riffer/rig/prompts.rb +4 -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 +134 -7
- data/lib/riffer/rig/runtime/blocked.rb +13 -0
- data/lib/riffer/rig/runtime/cancel_flag.rb +27 -0
- data/lib/riffer/rig/runtime/hooks.rb +78 -0
- data/lib/riffer/rig/runtime/request_guardrail.rb +24 -0
- data/lib/riffer/rig/runtime/tool_runtime.rb +44 -0
- data/lib/riffer/rig/runtime.rb +582 -70
- data/lib/riffer/rig/settings/document.rb +28 -12
- data/lib/riffer/rig/settings/pricing.rb +16 -6
- data/lib/riffer/rig/settings.rb +108 -81
- data/lib/riffer/rig/skills/sources.rb +28 -0
- data/lib/riffer/rig/skills.rb +12 -0
- data/lib/riffer/rig/support/equatable.rb +21 -0
- data/lib/riffer/rig/{ui → terminal}/animator.rb +6 -13
- data/lib/riffer/rig/{ui → terminal}/banner.rb +17 -21
- data/lib/riffer/rig/{ui → terminal}/cursor.rb +4 -6
- data/lib/riffer/rig/terminal/host.rb +85 -0
- data/lib/riffer/rig/{ui → terminal}/palette.rb +1 -1
- data/lib/riffer/rig/{ui → terminal}/renderer.rb +121 -87
- data/lib/riffer/rig/{ui → terminal}/smoother.rb +15 -23
- data/lib/riffer/rig/{ui → terminal}/theme.rb +12 -14
- data/lib/riffer/rig/terminal.rb +207 -0
- data/lib/riffer/rig/tools/bash.rb +36 -13
- data/lib/riffer/rig/tools/edit.rb +3 -2
- data/lib/riffer/rig/tools/read.rb +3 -2
- data/lib/riffer/rig/tools/write.rb +3 -2
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +10 -15
- data/sig/generated/riffer/rig/bundled/agents_md.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/bash.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/edit.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/mcp.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/read.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/skills.rbs +5 -0
- data/sig/generated/riffer/rig/bundled/write.rbs +5 -0
- data/sig/generated/riffer/rig/bundled.rbs +5 -0
- data/sig/generated/riffer/rig/cli/flags.rbs +40 -0
- data/sig/generated/riffer/rig/cli.rbs +30 -27
- data/sig/generated/riffer/rig/command/context.rbs +50 -0
- data/sig/generated/riffer/rig/command.rbs +23 -0
- data/sig/generated/riffer/rig/commands/model.rbs +27 -0
- data/sig/generated/riffer/rig/commands/skill.rbs +12 -0
- data/sig/generated/riffer/rig/commands.rbs +4 -0
- data/sig/generated/riffer/rig/credentials/resolution.rbs +13 -0
- data/sig/generated/riffer/rig/credentials.rbs +70 -41
- data/sig/generated/riffer/rig/directories.rbs +11 -0
- data/sig/generated/riffer/rig/env/invalid.rbs +10 -0
- data/sig/generated/riffer/rig/env.rbs +26 -0
- data/sig/generated/riffer/rig/events/after_response.rbs +18 -0
- data/sig/generated/riffer/rig/events/after_tool_call.rbs +24 -0
- data/sig/generated/riffer/rig/events/before_prompt.rbs +18 -0
- data/sig/generated/riffer/rig/events/before_request.rbs +18 -0
- data/sig/generated/riffer/rig/events/before_tool_call.rbs +21 -0
- data/sig/generated/riffer/rig/events/command_output.rbs +21 -0
- data/sig/generated/riffer/rig/events/notify.rbs +21 -0
- data/sig/generated/riffer/rig/events/session_end.rbs +18 -0
- data/sig/generated/riffer/rig/events/session_start.rbs +21 -0
- data/sig/generated/riffer/rig/events/skill_activated.rbs +18 -0
- data/sig/generated/riffer/rig/events/turn_end.rbs +24 -0
- data/sig/generated/riffer/rig/events.rbs +4 -0
- data/sig/generated/riffer/rig/extension/failure.rbs +13 -0
- data/sig/generated/riffer/rig/extension.rbs +14 -5
- 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/loader.rbs +77 -0
- data/sig/generated/riffer/rig/mcp/declaration.rbs +18 -0
- data/sig/generated/riffer/rig/mcp/server.rbs +13 -0
- data/sig/generated/riffer/rig/mcp.rbs +8 -0
- data/sig/generated/riffer/rig/prompts/agents_md.rbs +19 -0
- data/sig/generated/riffer/rig/prompts.rbs +4 -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 +88 -6
- data/sig/generated/riffer/rig/runtime/blocked.rbs +10 -0
- data/sig/generated/riffer/rig/runtime/cancel_flag.rbs +19 -0
- data/sig/generated/riffer/rig/runtime/hooks.rbs +49 -0
- data/sig/generated/riffer/rig/runtime/request_guardrail.rbs +16 -0
- data/sig/generated/riffer/rig/runtime/tool_runtime.rbs +23 -0
- data/sig/generated/riffer/rig/runtime.rbs +262 -40
- data/sig/generated/riffer/rig/settings/document.rbs +13 -9
- data/sig/generated/riffer/rig/settings/pricing.rbs +7 -3
- data/sig/generated/riffer/rig/settings.rbs +56 -64
- data/sig/generated/riffer/rig/skills/sources.rbs +18 -0
- data/sig/generated/riffer/rig/skills.rbs +7 -0
- data/sig/generated/riffer/rig/support/equatable.rbs +15 -0
- data/sig/generated/riffer/rig/{ui → terminal}/animator.rbs +5 -13
- data/sig/generated/riffer/rig/terminal/banner.rbs +39 -0
- data/sig/generated/riffer/rig/terminal/cursor.rbs +31 -0
- data/sig/generated/riffer/rig/terminal/host.rbs +50 -0
- data/sig/generated/riffer/rig/{ui → terminal}/palette.rbs +2 -2
- data/sig/generated/riffer/rig/terminal/renderer.rbs +126 -0
- data/sig/generated/riffer/rig/terminal/smoother.rbs +59 -0
- data/sig/generated/riffer/rig/{ui → terminal}/theme.rbs +5 -7
- data/sig/generated/riffer/rig/terminal.rbs +85 -0
- data/sig/generated/riffer/rig/tools/bash.rbs +14 -1
- data/sig/generated/riffer/rig.rbs +4 -7
- data/sig/manual/riffer/rig/cli.rbs +2 -4
- data/sig/manual/riffer/rig/commands/model.rbs +6 -0
- data/sig/manual/riffer/rig/commands/skill.rbs +6 -0
- data/sig/manual/riffer/rig/credentials.rbs +2 -4
- data/sig/manual/riffer/rig/directories.rbs +6 -0
- data/sig/manual/riffer/rig/events.rbs +6 -0
- data/sig/manual/riffer/rig/hosts.rbs +12 -0
- data/sig/manual/riffer/rig/mcp.rbs +11 -0
- data/sig/manual/riffer/rig/prompts/agents_md.rbs +6 -0
- data/sig/manual/riffer/rig/settings.rbs +2 -4
- data/sig/manual/riffer/rig/skills.rbs +6 -0
- data/sig/manual/riffer/rig/support.rbs +8 -0
- data/sig/manual/riffer/rig/terminal/banner.rbs +6 -0
- metadata +173 -35
- data/lib/riffer/rig/coding_agent.rb +0 -53
- data/lib/riffer/rig/host.rb +0 -52
- data/lib/riffer/rig/repl.rb +0 -189
- data/lib/riffer/rig/token_tally.rb +0 -74
- data/sig/generated/riffer/rig/coding_agent.rbs +0 -9
- data/sig/generated/riffer/rig/host.rbs +0 -42
- data/sig/generated/riffer/rig/repl.rbs +0 -77
- data/sig/generated/riffer/rig/token_tally.rbs +0 -61
- data/sig/generated/riffer/rig/ui/banner.rbs +0 -42
- data/sig/generated/riffer/rig/ui/cursor.rbs +0 -33
- data/sig/generated/riffer/rig/ui/renderer.rbs +0 -118
- data/sig/generated/riffer/rig/ui/smoother.rbs +0 -72
- data/sig/manual/riffer/rig/ui/banner.rbs +0 -8
- data/sig/manual/riffer/rig/ui.rbs +0 -5
|
@@ -2,31 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
|
-
class Riffer::Rig::
|
|
5
|
+
class Riffer::Rig::Terminal::Renderer
|
|
6
6
|
RESULT_PREVIEW_LIMIT = 200 #: Integer
|
|
7
7
|
|
|
8
8
|
# @rbs @io: IO
|
|
9
|
-
# @rbs @theme: Riffer::Rig::
|
|
10
|
-
# @rbs @
|
|
11
|
-
# @rbs @
|
|
12
|
-
# @rbs @deferred_usage: Riffer::Providers::TokenUsage?
|
|
9
|
+
# @rbs @theme: Riffer::Rig::Terminal::Theme
|
|
10
|
+
# @rbs @smoother: Riffer::Rig::Terminal::Smoother | PassThroughSmoother
|
|
11
|
+
# @rbs @cursor: Riffer::Rig::Terminal::Cursor
|
|
13
12
|
# @rbs @prose_gap_pending: bool
|
|
14
13
|
|
|
15
14
|
# @rbs io: IO
|
|
16
|
-
# @rbs theme: Riffer::Rig::
|
|
17
|
-
# @rbs
|
|
18
|
-
# @rbs
|
|
15
|
+
# @rbs theme: Riffer::Rig::Terminal::Theme
|
|
16
|
+
# @rbs smoother: Riffer::Rig::Terminal::Smoother | PassThroughSmoother
|
|
17
|
+
# @rbs cursor: Riffer::Rig::Terminal::Cursor
|
|
19
18
|
# @rbs return: void
|
|
20
|
-
def initialize(
|
|
19
|
+
def initialize(
|
|
20
|
+
io: $stdout,
|
|
21
|
+
theme: Riffer::Rig::Terminal::Theme.for(io),
|
|
22
|
+
smoother: PassThroughSmoother.new(io),
|
|
23
|
+
cursor: Riffer::Rig::Terminal::Cursor.new(io: io, theme: theme)
|
|
24
|
+
)
|
|
21
25
|
@io = io
|
|
22
26
|
@theme = theme
|
|
23
|
-
@tally = tally
|
|
24
27
|
@smoother = smoother
|
|
25
|
-
@
|
|
28
|
+
@cursor = cursor
|
|
26
29
|
@prose_gap_pending = false
|
|
27
30
|
end
|
|
28
31
|
|
|
29
|
-
# @rbs event: Riffer::StreamEvents::
|
|
32
|
+
# @rbs event: Riffer::StreamEvents::TextDelta | Riffer::StreamEvents::ToolCallDone | Riffer::StreamEvents::Interrupt
|
|
30
33
|
# @rbs return: void
|
|
31
34
|
def render(event)
|
|
32
35
|
case event
|
|
@@ -34,42 +37,91 @@ class Riffer::Rig::UI::Renderer
|
|
|
34
37
|
render_prose(event.content)
|
|
35
38
|
when Riffer::StreamEvents::ToolCallDone
|
|
36
39
|
render_tool_activity(2) { @theme.cyan("⚙ #{event.name}(#{format_arguments(event.arguments)})") }
|
|
37
|
-
when Riffer::StreamEvents::SkillActivation
|
|
38
|
-
render_block(0) { @theme.magenta("✦ skill: #{event.name}") }
|
|
39
40
|
when Riffer::StreamEvents::Interrupt
|
|
40
41
|
render_block(0) { @theme.dim("[interrupted: #{event.reason}]") }
|
|
41
|
-
when Riffer::StreamEvents::TokenUsageDone
|
|
42
|
-
current = @deferred_usage
|
|
43
|
-
@deferred_usage = current ? current + event.token_usage : event.token_usage
|
|
44
42
|
end
|
|
45
43
|
end
|
|
46
44
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
# @rbs name: String
|
|
46
|
+
# @rbs return: void
|
|
47
|
+
def skill(name)
|
|
48
|
+
render_block(0) { @theme.magenta("✦ skill: #{name}") }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @rbs text: String
|
|
52
|
+
# @rbs return: void
|
|
53
|
+
def notice(text)
|
|
54
|
+
render_block(0) { @theme.grey(text) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @rbs text: String
|
|
58
|
+
# @rbs return: void
|
|
59
|
+
def error(text)
|
|
60
|
+
render_block(0) { @theme.red(text) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @rbs message: String
|
|
64
|
+
# @rbs level: Symbol
|
|
65
|
+
# @rbs return: void
|
|
66
|
+
def notify(message, level)
|
|
67
|
+
level == :error ? error(message) : notice(message)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @rbs usage: Riffer::Providers::TokenUsage?
|
|
71
|
+
# @rbs session: Riffer::Providers::TokenUsage?
|
|
72
|
+
# @rbs return: void
|
|
73
|
+
def usage(usage, session)
|
|
74
|
+
return unless usage && session
|
|
75
|
+
|
|
76
|
+
render_block(0) { @theme.dim(usage_line(usage, session)) }
|
|
77
|
+
end
|
|
78
|
+
|
|
51
79
|
# @rbs return: void
|
|
52
80
|
def prompt
|
|
81
|
+
# Closes any tool group the previous turn left open, so the next turn's
|
|
82
|
+
# blocks open cleanly.
|
|
53
83
|
@prose_gap_pending = true
|
|
54
84
|
@io.puts
|
|
55
85
|
@io.print("#{@theme.pink('›')} ")
|
|
56
86
|
@io.flush
|
|
57
87
|
end
|
|
58
88
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# done, then printed as one line below it.
|
|
63
|
-
#
|
|
89
|
+
# @rbs question: String?
|
|
90
|
+
# @rbs options: Array[String]?
|
|
91
|
+
# @rbs secret: bool
|
|
64
92
|
# @rbs return: void
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
@
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
def question(question, options, secret)
|
|
94
|
+
drain
|
|
95
|
+
@io.puts
|
|
96
|
+
@io.puts(question) if question
|
|
97
|
+
options&.each_with_index { |option, index| @io.puts(" #{@theme.magenta("#{index + 1}.")} #{option}") }
|
|
98
|
+
@io.print("#{@theme.pink('›')} #{@theme.grey('(hidden) ') if secret}")
|
|
99
|
+
@io.flush
|
|
100
|
+
end
|
|
70
101
|
|
|
71
|
-
|
|
72
|
-
|
|
102
|
+
# @rbs return: void
|
|
103
|
+
def newline
|
|
104
|
+
@io.puts
|
|
105
|
+
@io.flush
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# @rbs return: void
|
|
109
|
+
def begin_turn
|
|
110
|
+
# A hidden cursor can't flicker against the animator's erase-and-redraw
|
|
111
|
+
# churn.
|
|
112
|
+
@cursor.hide
|
|
113
|
+
@smoother.start
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @rbs return: void
|
|
117
|
+
def end_turn
|
|
118
|
+
@smoother.finish
|
|
119
|
+
@cursor.show
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @rbs return: void
|
|
123
|
+
def drain
|
|
124
|
+
@smoother.drain
|
|
73
125
|
end
|
|
74
126
|
|
|
75
127
|
# @rbs message: Riffer::Messages::Base
|
|
@@ -84,36 +136,54 @@ class Riffer::Rig::UI::Renderer
|
|
|
84
136
|
@io.flush
|
|
85
137
|
end
|
|
86
138
|
|
|
139
|
+
class PassThroughSmoother
|
|
140
|
+
# @rbs @io: IO
|
|
141
|
+
|
|
142
|
+
# @rbs io: IO
|
|
143
|
+
# @rbs return: void
|
|
144
|
+
def initialize(io) = @io = io
|
|
145
|
+
|
|
146
|
+
# @rbs content: String
|
|
147
|
+
# @rbs return: self
|
|
148
|
+
def <<(content)
|
|
149
|
+
@io.print(content)
|
|
150
|
+
@io.flush
|
|
151
|
+
self
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @rbs return: nil
|
|
155
|
+
def start = nil
|
|
156
|
+
|
|
157
|
+
# @rbs return: nil
|
|
158
|
+
def drain = nil
|
|
159
|
+
|
|
160
|
+
# @rbs return: nil
|
|
161
|
+
def finish = nil
|
|
162
|
+
end
|
|
163
|
+
|
|
87
164
|
private
|
|
88
165
|
|
|
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
166
|
# @rbs indent: Integer
|
|
95
167
|
# @rbs &block: () -> String
|
|
96
168
|
# @rbs return: void
|
|
97
169
|
def render_block(indent, &)
|
|
98
|
-
|
|
170
|
+
drain
|
|
99
171
|
@io.puts
|
|
100
172
|
@prose_gap_pending = true
|
|
101
173
|
@io.puts((' ' * indent) + yield)
|
|
102
174
|
@io.flush
|
|
103
175
|
end
|
|
104
176
|
|
|
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
177
|
# @rbs content: String
|
|
110
178
|
# @rbs return: void
|
|
111
179
|
def render_prose(content)
|
|
180
|
+
# Streaming makes "which delta opens a prose block?" a stateful question,
|
|
181
|
+
# so the gap is tracked in a flag rather than written at each render site.
|
|
112
182
|
if @prose_gap_pending
|
|
113
183
|
@prose_gap_pending = false
|
|
114
184
|
@io.print("\n")
|
|
115
185
|
end
|
|
116
|
-
smoother << content
|
|
186
|
+
@smoother << content
|
|
117
187
|
end
|
|
118
188
|
|
|
119
189
|
# @rbs indent: Integer
|
|
@@ -125,67 +195,31 @@ class Riffer::Rig::UI::Renderer
|
|
|
125
195
|
@io.flush
|
|
126
196
|
end
|
|
127
197
|
|
|
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
198
|
# @rbs return: void
|
|
133
199
|
def open_tool_activity
|
|
200
|
+
# Tool-activity lines (⚙ calls, ↳ results) share one blank line above the
|
|
201
|
+
# group; the group stays open so the prose after it pays the closing gap.
|
|
134
202
|
return if @prose_gap_pending
|
|
135
203
|
|
|
136
|
-
|
|
204
|
+
drain
|
|
137
205
|
@io.puts
|
|
138
206
|
@prose_gap_pending = true
|
|
139
207
|
end
|
|
140
208
|
|
|
141
209
|
# @rbs usage: Riffer::Providers::TokenUsage
|
|
142
|
-
# @rbs
|
|
210
|
+
# @rbs session: Riffer::Providers::TokenUsage
|
|
143
211
|
# @rbs return: String
|
|
144
|
-
def usage_line(usage,
|
|
212
|
+
def usage_line(usage, session)
|
|
145
213
|
parts = ["↑#{usage.input_tokens}", "↓#{usage.output_tokens}"]
|
|
146
214
|
parts << "cache_write:#{usage.cache_write_tokens}" if usage.cache_write_tokens&.positive?
|
|
147
215
|
parts << "cache_read:#{usage.cache_read_tokens}" if usage.cache_read_tokens&.positive?
|
|
148
|
-
parts << "session #{
|
|
149
|
-
cost =
|
|
216
|
+
parts << "session #{session.total_tokens} tok"
|
|
217
|
+
cost = session.cost
|
|
150
218
|
parts << format('~$%.4f', cost) if cost
|
|
151
219
|
|
|
152
220
|
parts.join(' · ')
|
|
153
221
|
end
|
|
154
222
|
|
|
155
|
-
# @rbs return: Riffer::Rig::UI::Smoother | PassThroughSmoother
|
|
156
|
-
def smoother
|
|
157
|
-
@smoother || PassThroughSmoother.new(@io)
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# Stand-in when no smoother is injected, so a bare Riffer::Rig::UI::Renderer still prints
|
|
161
|
-
# synchronously.
|
|
162
|
-
class PassThroughSmoother
|
|
163
|
-
# @rbs @io: IO
|
|
164
|
-
|
|
165
|
-
# @rbs io: IO
|
|
166
|
-
# @rbs return: void
|
|
167
|
-
def initialize(io) = @io = io
|
|
168
|
-
|
|
169
|
-
# @rbs content: String
|
|
170
|
-
# @rbs return: self
|
|
171
|
-
def <<(content)
|
|
172
|
-
@io.print(content)
|
|
173
|
-
@io.flush
|
|
174
|
-
self
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
# @rbs return: nil
|
|
178
|
-
def drain = nil
|
|
179
|
-
|
|
180
|
-
# @rbs return: nil
|
|
181
|
-
def finish = nil
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# @rbs return: void
|
|
185
|
-
def drain_smoother
|
|
186
|
-
smoother.drain
|
|
187
|
-
end
|
|
188
|
-
|
|
189
223
|
# @rbs arguments: String
|
|
190
224
|
# @rbs return: String
|
|
191
225
|
def format_arguments(arguments)
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
# typing. On a non-TTY output it is transparent: writes pass through
|
|
5
|
-
# synchronously and no thread ever runs.
|
|
6
|
-
class Riffer::Rig::UI::Smoother
|
|
3
|
+
class Riffer::Rig::Terminal::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
|
|
21
|
-
# @rbs @theme: Riffer::Rig::
|
|
16
|
+
# @rbs @theme: Riffer::Rig::Terminal::Theme
|
|
22
17
|
# @rbs @clock: singleton(Kernel)
|
|
23
18
|
# @rbs @backlog: String
|
|
24
19
|
# @rbs @carry: Rational
|
|
@@ -28,10 +23,10 @@ class Riffer::Rig::UI::Smoother
|
|
|
28
23
|
# @rbs @newline_pending: bool
|
|
29
24
|
|
|
30
25
|
# @rbs io: IO
|
|
31
|
-
# @rbs theme: Riffer::Rig::
|
|
26
|
+
# @rbs theme: Riffer::Rig::Terminal::Theme
|
|
32
27
|
# @rbs clock: singleton(Kernel)
|
|
33
28
|
# @rbs return: void
|
|
34
|
-
def initialize(io: $stdout, theme: Riffer::Rig::
|
|
29
|
+
def initialize(io: $stdout, theme: Riffer::Rig::Terminal::Theme.for(io), clock: Kernel)
|
|
35
30
|
@io = io
|
|
36
31
|
@theme = theme
|
|
37
32
|
@clock = clock
|
|
@@ -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
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
# text when piped or tested.
|
|
5
|
-
class Riffer::Rig::UI::Theme
|
|
3
|
+
class Riffer::Rig::Terminal::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
|
|
19
|
-
# @rbs
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
# @rbs no_color: bool
|
|
17
|
+
# @rbs return: Riffer::Rig::Terminal::Theme
|
|
18
|
+
def self.for(io, no_color: false)
|
|
19
|
+
new(enabled: io.tty? && !no_color)
|
|
22
20
|
end
|
|
23
21
|
|
|
24
22
|
# @rbs text: String
|
|
@@ -32,31 +30,31 @@ class Riffer::Rig::UI::Theme
|
|
|
32
30
|
|
|
33
31
|
# @rbs text: String
|
|
34
32
|
# @rbs return: String
|
|
35
|
-
def pink(text) = paint(text, Riffer::Rig::
|
|
33
|
+
def pink(text) = paint(text, Riffer::Rig::Terminal::Palette::PINK)
|
|
36
34
|
|
|
37
35
|
# @rbs text: String
|
|
38
36
|
# @rbs return: String
|
|
39
|
-
def magenta(text) = paint(text, Riffer::Rig::
|
|
37
|
+
def magenta(text) = paint(text, Riffer::Rig::Terminal::Palette::MAGENTA)
|
|
40
38
|
|
|
41
39
|
# @rbs text: String
|
|
42
40
|
# @rbs return: String
|
|
43
|
-
def purple(text) = paint(text, Riffer::Rig::
|
|
41
|
+
def purple(text) = paint(text, Riffer::Rig::Terminal::Palette::PURPLE)
|
|
44
42
|
|
|
45
43
|
# @rbs text: String
|
|
46
44
|
# @rbs return: String
|
|
47
|
-
def blue(text) = paint(text, Riffer::Rig::
|
|
45
|
+
def blue(text) = paint(text, Riffer::Rig::Terminal::Palette::BLUE)
|
|
48
46
|
|
|
49
47
|
# @rbs text: String
|
|
50
48
|
# @rbs return: String
|
|
51
|
-
def cyan(text) = paint(text, Riffer::Rig::
|
|
49
|
+
def cyan(text) = paint(text, Riffer::Rig::Terminal::Palette::CYAN)
|
|
52
50
|
|
|
53
51
|
# @rbs text: String
|
|
54
52
|
# @rbs return: String
|
|
55
|
-
def grey(text) = paint(text, Riffer::Rig::
|
|
53
|
+
def grey(text) = paint(text, Riffer::Rig::Terminal::Palette::GREY)
|
|
56
54
|
|
|
57
55
|
# @rbs text: String
|
|
58
56
|
# @rbs return: String
|
|
59
|
-
def red(text) = paint(text, Riffer::Rig::
|
|
57
|
+
def red(text) = paint(text, Riffer::Rig::Terminal::Palette::RED)
|
|
60
58
|
|
|
61
59
|
# @rbs text: String
|
|
62
60
|
# @rbs return: String
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Terminal
|
|
4
|
+
EXIT_COMMANDS = %w[/exit /quit].freeze #: Array[String]
|
|
5
|
+
|
|
6
|
+
SLASH_COMMAND = %r{\A/(\S+)\s*(.*)\z}m #: Regexp
|
|
7
|
+
|
|
8
|
+
SKILL_COMMAND_PREFIX = 'skill:'
|
|
9
|
+
|
|
10
|
+
EXIT_HINT = 'Press Ctrl-C again to exit.'
|
|
11
|
+
|
|
12
|
+
FAREWELL = 'see you on the next riff.'
|
|
13
|
+
|
|
14
|
+
# @rbs @input: IO
|
|
15
|
+
# @rbs @host: Riffer::Rig::Hosts::_Host
|
|
16
|
+
# @rbs @renderer: Riffer::Rig::Terminal::Renderer
|
|
17
|
+
# @rbs @animator: Riffer::Rig::Terminal::Animator
|
|
18
|
+
# @rbs @theme: Riffer::Rig::Terminal::Theme
|
|
19
|
+
# @rbs @version: String
|
|
20
|
+
|
|
21
|
+
# @rbs input: IO
|
|
22
|
+
# @rbs output: IO
|
|
23
|
+
# @rbs version: String
|
|
24
|
+
# @rbs no_color: bool
|
|
25
|
+
# @rbs return: Riffer::Rig::Terminal
|
|
26
|
+
def self.for(input:, output:, version:, no_color: false)
|
|
27
|
+
theme = Theme.for(output, no_color: no_color)
|
|
28
|
+
smoother = Smoother.new(io: output, theme: theme)
|
|
29
|
+
renderer = Renderer.new(io: output, theme: theme, smoother: smoother, cursor: Cursor.new(io: output, theme: theme))
|
|
30
|
+
animator = Animator.new(io: output, theme: theme)
|
|
31
|
+
host = Riffer::Rig::Terminal::Host.new(input: input, renderer: renderer, animator: animator)
|
|
32
|
+
new(input: input, host: host, renderer: renderer, animator: animator, theme: theme, version: version)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @rbs input: IO
|
|
36
|
+
# @rbs host: Riffer::Rig::Hosts::_Host
|
|
37
|
+
# @rbs renderer: Riffer::Rig::Terminal::Renderer
|
|
38
|
+
# @rbs animator: Riffer::Rig::Terminal::Animator
|
|
39
|
+
# @rbs theme: Riffer::Rig::Terminal::Theme
|
|
40
|
+
# @rbs version: String
|
|
41
|
+
# @rbs return: void
|
|
42
|
+
def initialize(input:, host:, renderer:, animator:, theme:, version:)
|
|
43
|
+
@input = input
|
|
44
|
+
@host = host
|
|
45
|
+
@renderer = renderer
|
|
46
|
+
@animator = animator
|
|
47
|
+
@theme = theme
|
|
48
|
+
@version = version
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @rbs &build: (Riffer::Rig::Hosts::_Host) -> Riffer::Rig::Runtime
|
|
52
|
+
# @rbs return: Integer
|
|
53
|
+
def run(&build)
|
|
54
|
+
runtime = load(build)
|
|
55
|
+
return 1 unless runtime
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
runtime.on_message { |message| render_tool_result(message) }
|
|
59
|
+
reveal_banner(runtime)
|
|
60
|
+
repl(runtime)
|
|
61
|
+
@renderer.notice(FAREWELL)
|
|
62
|
+
0
|
|
63
|
+
ensure
|
|
64
|
+
runtime.close
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# @rbs build: ^(Riffer::Rig::Hosts::_Host) -> Riffer::Rig::Runtime
|
|
71
|
+
# @rbs return: Riffer::Rig::Runtime?
|
|
72
|
+
def load(build)
|
|
73
|
+
build.call(@host)
|
|
74
|
+
rescue Riffer::Rig::Loader::ConfigurationError => e
|
|
75
|
+
@renderer.error(e.message)
|
|
76
|
+
nil
|
|
77
|
+
rescue Interrupt
|
|
78
|
+
@renderer.newline
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
83
|
+
# @rbs return: void
|
|
84
|
+
def reveal_banner(runtime)
|
|
85
|
+
skills = runtime.commands.count { |command| command.name.start_with?(SKILL_COMMAND_PREFIX) }
|
|
86
|
+
lines = Banner.lines(
|
|
87
|
+
@theme,
|
|
88
|
+
model: runtime.model,
|
|
89
|
+
cwd: runtime.cwd,
|
|
90
|
+
skills: skills.zero? ? 'none' : skills.to_s,
|
|
91
|
+
version: @version
|
|
92
|
+
)
|
|
93
|
+
@animator.reveal([lines])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
97
|
+
# @rbs return: void
|
|
98
|
+
def repl(runtime)
|
|
99
|
+
armed = false
|
|
100
|
+
loop do
|
|
101
|
+
case (line = read_line)
|
|
102
|
+
when nil then break
|
|
103
|
+
when :interrupt
|
|
104
|
+
break if armed
|
|
105
|
+
|
|
106
|
+
armed = true
|
|
107
|
+
@renderer.notice(EXIT_HINT)
|
|
108
|
+
else
|
|
109
|
+
armed = false
|
|
110
|
+
text = line.strip
|
|
111
|
+
break if EXIT_COMMANDS.include?(text)
|
|
112
|
+
|
|
113
|
+
dispatch(runtime, text) unless text.empty?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @rbs return: String | :interrupt | nil
|
|
119
|
+
def read_line
|
|
120
|
+
@renderer.prompt
|
|
121
|
+
@input.gets
|
|
122
|
+
rescue Interrupt
|
|
123
|
+
:interrupt
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
127
|
+
# @rbs text: String
|
|
128
|
+
# @rbs return: void
|
|
129
|
+
def dispatch(runtime, text)
|
|
130
|
+
command = SLASH_COMMAND.match(text)
|
|
131
|
+
turn(runtime) do |emit|
|
|
132
|
+
if command
|
|
133
|
+
runtime.run_command(command[1].to_s, command[2].to_s, &emit)
|
|
134
|
+
else
|
|
135
|
+
runtime.prompt(text, &emit)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
141
|
+
# @rbs &block: (^(::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void) -> void
|
|
142
|
+
# @rbs return: void
|
|
143
|
+
def turn(runtime)
|
|
144
|
+
# Runtime#cancel takes a Mutex, which Ruby refuses inside a trap handler,
|
|
145
|
+
# so the handler hands the cancel to a thread.
|
|
146
|
+
previous = Signal.trap('INT') { Thread.new { runtime.cancel } }
|
|
147
|
+
begin
|
|
148
|
+
@renderer.begin_turn
|
|
149
|
+
start_animator
|
|
150
|
+
yield(->(event) { render(runtime, event) })
|
|
151
|
+
rescue StandardError => e
|
|
152
|
+
@animator.stop
|
|
153
|
+
@renderer.error("Error: #{e.message}")
|
|
154
|
+
ensure
|
|
155
|
+
Signal.trap('INT', previous)
|
|
156
|
+
@animator.stop
|
|
157
|
+
@renderer.end_turn
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @rbs runtime: Riffer::Rig::Runtime
|
|
162
|
+
# @rbs event: ::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event
|
|
163
|
+
# @rbs return: void
|
|
164
|
+
def render(runtime, event)
|
|
165
|
+
case event
|
|
166
|
+
when Riffer::StreamEvents::ReasoningDelta
|
|
167
|
+
start_animator(:reasoning)
|
|
168
|
+
when Riffer::StreamEvents::ReasoningDone, Riffer::StreamEvents::TokenUsageDone
|
|
169
|
+
# Tool execution and the next model call emit no events, so the spinner
|
|
170
|
+
# comes straight back on to cover the silent stretch.
|
|
171
|
+
start_animator
|
|
172
|
+
when Riffer::StreamEvents::TextDelta, Riffer::StreamEvents::ToolCallDone, Riffer::StreamEvents::Interrupt
|
|
173
|
+
@animator.stop
|
|
174
|
+
@renderer.render(event)
|
|
175
|
+
when Riffer::StreamEvents::SkillActivation, Riffer::Rig::Events::SkillActivated
|
|
176
|
+
@animator.stop
|
|
177
|
+
@renderer.skill(event.name)
|
|
178
|
+
start_animator
|
|
179
|
+
when Riffer::Rig::Events::CommandOutput
|
|
180
|
+
@animator.stop
|
|
181
|
+
@renderer.notice(event.text)
|
|
182
|
+
when Riffer::Rig::Events::TurnEnd
|
|
183
|
+
@animator.stop
|
|
184
|
+
@renderer.usage(event.usage, runtime.tally)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @rbs message: Riffer::Messages::Base
|
|
189
|
+
# @rbs return: void
|
|
190
|
+
def render_tool_result(message)
|
|
191
|
+
return unless message.is_a?(Riffer::Messages::Tool)
|
|
192
|
+
|
|
193
|
+
@animator.stop
|
|
194
|
+
@renderer.render_tool_result(message)
|
|
195
|
+
start_animator
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @rbs mode: Symbol
|
|
199
|
+
# @rbs return: void
|
|
200
|
+
def start_animator(mode = :neutral)
|
|
201
|
+
# The smoother's backlog may still be trickling out from streamed prose,
|
|
202
|
+
# and spinner frames drawn mid-drain would carve `\r…\e[K` through a
|
|
203
|
+
# half-printed sentence.
|
|
204
|
+
@renderer.drain
|
|
205
|
+
@animator.start(mode)
|
|
206
|
+
end
|
|
207
|
+
end
|