rubino-agent 0.5.2.1 → 0.5.2.2
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/.dockerignore +15 -0
- data/CHANGELOG.md +56 -0
- data/Dockerfile +56 -0
- data/agent.md +112 -0
- data/docs/design/bg-shell-pty-port.md +88 -0
- data/docs/design/bg-shell-review-refinements.md +65 -0
- data/docs/design/bg-shell-ux.md +130 -0
- data/docs/tools.md +3 -12
- data/lib/rubino/agent/iteration_budget.rb +13 -0
- data/lib/rubino/agent/loop.rb +43 -5
- data/lib/rubino/agent/prompts/build.txt +3 -5
- data/lib/rubino/agent/prompts/memory_guidance.txt +5 -0
- data/lib/rubino/agent/prompts/tool_use_enforcement.txt +4 -0
- data/lib/rubino/agent/prompts/tool_use_enforcement_google.txt +9 -0
- data/lib/rubino/agent/prompts/tool_use_enforcement_openai.txt +48 -0
- data/lib/rubino/agent/runner.rb +55 -12
- data/lib/rubino/agent/tool_executor.rb +1 -1
- data/lib/rubino/cli/chat/idle_card_host.rb +6 -1
- data/lib/rubino/cli/chat_command.rb +119 -17
- data/lib/rubino/cli/commands.rb +5 -0
- data/lib/rubino/commands/handlers/agents.rb +27 -18
- data/lib/rubino/commands/handlers/status.rb +6 -3
- data/lib/rubino/config/configuration.rb +25 -8
- data/lib/rubino/config/defaults.rb +15 -13
- data/lib/rubino/context/prompt_assembler.rb +89 -1
- data/lib/rubino/context/summary_builder.rb +0 -22
- data/lib/rubino/interaction/events.rb +2 -2
- data/lib/rubino/interaction/lifecycle.rb +54 -20
- data/lib/rubino/llm/ruby_llm_adapter.rb +178 -20
- data/lib/rubino/security/redactor.rb +1 -1
- data/lib/rubino/session/message.rb +12 -0
- data/lib/rubino/tools/background_tasks.rb +107 -12
- data/lib/rubino/tools/base.rb +1 -1
- data/lib/rubino/tools/read_attachment_tool.rb +52 -54
- data/lib/rubino/tools/registry.rb +21 -72
- data/lib/rubino/tools/shell_entry_adapter.rb +97 -0
- data/lib/rubino/tools/shell_input_tool.rb +1 -1
- data/lib/rubino/tools/shell_kill_tool.rb +4 -4
- data/lib/rubino/tools/shell_registry.rb +178 -38
- data/lib/rubino/tools/shell_tool.rb +45 -5
- data/lib/rubino/tools/task_result_tool.rb +4 -1
- data/lib/rubino/tools/task_tool.rb +74 -11
- data/lib/rubino/tools/vision_tool.rb +1 -1
- data/lib/rubino/ui/agent_menu.rb +8 -2
- data/lib/rubino/ui/api.rb +11 -0
- data/lib/rubino/ui/bottom_composer.rb +24 -11
- data/lib/rubino/ui/cli.rb +254 -15
- data/lib/rubino/ui/markdown_renderer.rb +4 -1
- data/lib/rubino/ui/stdout_proxy.rb +25 -10
- data/lib/rubino/ui/streaming_markdown.rb +67 -12
- data/lib/rubino/ui/subagent_cards.rb +8 -7
- data/lib/rubino/ui/tool_args_stream.rb +143 -0
- data/lib/rubino/update_check.rb +10 -2
- data/lib/rubino/version.rb +1 -1
- metadata +14 -6
- data/AGENTS.md +0 -97
- data/docs/agents.md +0 -216
- data/lib/rubino/jobs/handlers/summarize_session_job.rb +0 -21
- data/lib/rubino/tools/summarize_file_tool.rb +0 -194
|
@@ -56,6 +56,13 @@ module Rubino
|
|
|
56
56
|
# produced the "1. Mercury / 1. Venus / 1. Earth" off-by-one (B4).
|
|
57
57
|
LIST_ITEM_RE = /\A\s*(?:[-*+]|\d+[.)])\s/
|
|
58
58
|
|
|
59
|
+
# An ATX heading: 1-6 `#` (up to 3 leading spaces) followed by a space or
|
|
60
|
+
# end-of-line. CommonMark makes this a SINGLE-LINE block that can interrupt
|
|
61
|
+
# a paragraph, so the splitter commits it on its own — that's what lets a
|
|
62
|
+
# heading render WITH its H1/H2 breathing-room spacing live, instead of
|
|
63
|
+
# staying glued inside a multi-line block stuck in the capped live tail.
|
|
64
|
+
ATX_HEADING_RE = /\A {0,3}\#{1,6}(?:\s|\z)/
|
|
65
|
+
|
|
59
66
|
# A GFM pipe-table separator row, e.g. "|---|:--:|---|" or "---|---". The
|
|
60
67
|
# EXACT regex MarkdownRenderer uses to detect a table (markdown_renderer.rb)
|
|
61
68
|
# — DRY: a separator row is the single unambiguous "this is a table" signal,
|
|
@@ -124,8 +131,7 @@ module Rubino
|
|
|
124
131
|
while (idx = @pending.index("\n"))
|
|
125
132
|
line = @pending[0...idx]
|
|
126
133
|
@pending = @pending[(idx + 1)..] || +""
|
|
127
|
-
|
|
128
|
-
completed << block if block
|
|
134
|
+
completed.concat(consume_line(line))
|
|
129
135
|
end
|
|
130
136
|
|
|
131
137
|
completed
|
|
@@ -140,6 +146,34 @@ module Rubino
|
|
|
140
146
|
parts.join("\n")
|
|
141
147
|
end
|
|
142
148
|
|
|
149
|
+
# How many trailing lines of the in-flight block the LIVE markdown preview
|
|
150
|
+
# ever needs as render input. We only SHOW the last few rendered rows
|
|
151
|
+
# (CLI::LIVE_TAIL_ROWS), so feeding the renderer more than a screenful of
|
|
152
|
+
# source is wasted work: it re-parses the WHOLE growing block on EVERY
|
|
153
|
+
# delta and throws all but the last rows away — O(N²), which freezes the
|
|
154
|
+
# stream once a long ``` fence (a streamed file/code dump) accumulates.
|
|
155
|
+
# Generous enough that ordinary prose/lists/tables still render in full.
|
|
156
|
+
LIVE_SOURCE_MAX_LINES = 60
|
|
157
|
+
|
|
158
|
+
# Bounded source text for the LIVE markdown preview (#show_live_markdown):
|
|
159
|
+
# at most the last +max_lines+ lines of the in-flight block. When the block
|
|
160
|
+
# is inside a code fence the OPENER line (```lang) is always prepended so
|
|
161
|
+
# the windowed tail still parses AS a fenced block (same language, same box
|
|
162
|
+
# framing) — which makes the rendered last-LIVE_TAIL_ROWS rows identical to
|
|
163
|
+
# rendering the whole block, just without the quadratic cost. Outside a
|
|
164
|
+
# fence the cap only bites a pathologically long loose list (the one other
|
|
165
|
+
# unbounded block type); its transient numbering may differ for a frame,
|
|
166
|
+
# which beats freezing. Small blocks return the full #tail unchanged.
|
|
167
|
+
def live_source(max_lines = LIVE_SOURCE_MAX_LINES)
|
|
168
|
+
lines = @block.dup
|
|
169
|
+
lines << @pending unless @pending.empty?
|
|
170
|
+
return lines.join("\n") if lines.length <= max_lines
|
|
171
|
+
|
|
172
|
+
window = lines.last(max_lines)
|
|
173
|
+
window.unshift(@block.find { |l| l.match?(FENCE_OPEN_RE) }) if @in_fence
|
|
174
|
+
window.compact.join("\n")
|
|
175
|
+
end
|
|
176
|
+
|
|
143
177
|
# The in-progress tail to show live (raw): the LAST +rows+ lines of the
|
|
144
178
|
# in-flight block — its most recent already-newlined lines plus the
|
|
145
179
|
# un-newlined remainder. Newline-joined; the live region renders one row
|
|
@@ -190,16 +224,18 @@ module Rubino
|
|
|
190
224
|
private
|
|
191
225
|
|
|
192
226
|
# Feed one complete (newline-stripped) line through the state machine.
|
|
193
|
-
# Returns the
|
|
227
|
+
# Returns the block texts this line COMPLETED — usually 0 or 1, but a
|
|
228
|
+
# heading can close the open block AND emit itself, so the contract is an
|
|
229
|
+
# Array (feed concats them; the public #feed already returns an array).
|
|
194
230
|
def consume_line(line)
|
|
195
231
|
if @in_fence
|
|
196
232
|
@block << line
|
|
197
|
-
return
|
|
233
|
+
return [] unless fence_line_closes?(line)
|
|
198
234
|
|
|
199
235
|
@in_fence = false
|
|
200
236
|
@fence_len = 0
|
|
201
237
|
@fence_depth = nil
|
|
202
|
-
return take_block
|
|
238
|
+
return [take_block]
|
|
203
239
|
end
|
|
204
240
|
|
|
205
241
|
if (m = line.match(FENCE_OPEN_RE)) # opening fence starts a code block
|
|
@@ -212,7 +248,7 @@ module Rubino
|
|
|
212
248
|
@fence_depth = MARKDOWN_FENCE_LANGS.include?(m[2].to_s.strip.downcase) ? 1 : nil
|
|
213
249
|
flush_blanks
|
|
214
250
|
@block << line
|
|
215
|
-
return
|
|
251
|
+
return []
|
|
216
252
|
end
|
|
217
253
|
|
|
218
254
|
# A GFM pipe table is its own block type (like a fence): handled in
|
|
@@ -220,8 +256,15 @@ module Rubino
|
|
|
220
256
|
# header-ish row), holds its rows, and closes it on a blank/non-row line.
|
|
221
257
|
# Returns a 2-tuple [handled?, completed_block_or_nil].
|
|
222
258
|
handled, table_block = consume_table_line(line)
|
|
223
|
-
return table_block if handled
|
|
259
|
+
return Array(table_block) if handled
|
|
224
260
|
|
|
261
|
+
consume_prose_line(line)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# A non-fence, non-table line: blank separators, ATX headings, list items,
|
|
265
|
+
# and plain prose. Returns the block texts this line completed (see
|
|
266
|
+
# #consume_line for the Array contract).
|
|
267
|
+
def consume_prose_line(line)
|
|
225
268
|
if line.strip.empty?
|
|
226
269
|
# A blank line inside a list is BUFFERED, not a separator: it only ends
|
|
227
270
|
# the block if the list doesn't continue (handled when the next
|
|
@@ -229,11 +272,23 @@ module Rubino
|
|
|
229
272
|
# blank line ends the current prose block (separator consumed).
|
|
230
273
|
if @in_list
|
|
231
274
|
@blanks += 1
|
|
232
|
-
return
|
|
275
|
+
return []
|
|
233
276
|
end
|
|
234
|
-
return
|
|
277
|
+
return [] if @block.empty?
|
|
278
|
+
|
|
279
|
+
return [take_block]
|
|
280
|
+
end
|
|
235
281
|
|
|
236
|
-
|
|
282
|
+
# An ATX heading is a single-line block that can interrupt a paragraph
|
|
283
|
+
# (ATX_HEADING_RE). Close any open block first (dropping a list's trailing
|
|
284
|
+
# separator blank, like the list-exit path below), then emit the heading
|
|
285
|
+
# on its OWN so it commits — and renders with its breathing-room spacing —
|
|
286
|
+
# the instant it arrives, instead of staying glued in the capped live tail
|
|
287
|
+
# with the following lines until the whole block ends.
|
|
288
|
+
if line.match?(ATX_HEADING_RE)
|
|
289
|
+
@blanks = 0 if @in_list
|
|
290
|
+
finished = @block.empty? ? [] : [take_block]
|
|
291
|
+
return finished + [line]
|
|
237
292
|
end
|
|
238
293
|
|
|
239
294
|
is_item = line.match?(LIST_ITEM_RE)
|
|
@@ -245,13 +300,13 @@ module Rubino
|
|
|
245
300
|
@blanks = 0 # drop the trailing blank(s) that separated list from this line
|
|
246
301
|
finished = take_block
|
|
247
302
|
@block << line
|
|
248
|
-
return finished
|
|
303
|
+
return [finished]
|
|
249
304
|
end
|
|
250
305
|
|
|
251
306
|
flush_blanks
|
|
252
307
|
@in_list = true if is_item
|
|
253
308
|
@block << line
|
|
254
|
-
|
|
309
|
+
[]
|
|
255
310
|
end
|
|
256
311
|
|
|
257
312
|
# Does this line (already appended to the open fence's block) CLOSE that
|
|
@@ -71,13 +71,14 @@ module Rubino
|
|
|
71
71
|
else
|
|
72
72
|
glyph = @pastel.cyan(COLLAPSED)
|
|
73
73
|
state = entry.status == :stopping ? "stopping" : "running"
|
|
74
|
-
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
# shown
|
|
78
|
-
# lives in the agent's
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
# Compact card: id · label · state · [N tools ·] elapsed. The tool count
|
|
75
|
+
# shows only when the entry HAS one (subagents); a background shell runs
|
|
76
|
+
# no tools, so its tool_count is nil and the segment is omitted instead of
|
|
77
|
+
# a meaningless "0 tools". last_activity is NOT shown — too noisy on the
|
|
78
|
+
# always-visible card; the live detail lives in the agent's view (Enter).
|
|
79
|
+
count = entry.tool_count
|
|
80
|
+
metric = count ? "#{count.to_i} tool#{"s" if count.to_i != 1} · " : ""
|
|
81
|
+
body = "#{entry.id} · #{safe(card_label(entry))} · #{state} · #{metric}#{elapsed(entry)}"
|
|
81
82
|
" #{glyph} #{body}"
|
|
82
83
|
end
|
|
83
84
|
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubino
|
|
4
|
+
module UI
|
|
5
|
+
# Decodes a tool call's JSON arguments AS THEY STREAM into readable text, so
|
|
6
|
+
# the timeline can show what a `write`/`edit`/`shell` is composing while the
|
|
7
|
+
# model is still emitting it (#608). It surfaces the JSON string VALUES only
|
|
8
|
+
# (the `content` a write is producing, the `command` a shell will run) and
|
|
9
|
+
# drops the keys/structure, JSON-unescaping as it goes.
|
|
10
|
+
#
|
|
11
|
+
# #feed returns the COMPLETE decoded lines available so far (each ending in
|
|
12
|
+
# "\n"), holding the partial last line back until its newline lands so the
|
|
13
|
+
# caller never renders a half-line that the next fragment continues. #flush
|
|
14
|
+
# returns whatever partial remains at the end of the call.
|
|
15
|
+
#
|
|
16
|
+
# Single pass, O(n): fragments are concatenated only across an incomplete
|
|
17
|
+
# trailing escape (a "\\" or "\\uXXX" split mid-fragment), carried to the next
|
|
18
|
+
# feed so an escape is never decoded half-formed.
|
|
19
|
+
class ToolArgsStream
|
|
20
|
+
# JSON single-char escapes → their literal char (\uXXXX is handled inline).
|
|
21
|
+
UNESCAPE = { "n" => "\n", "t" => "\t", "r" => "\r", "b" => "\b",
|
|
22
|
+
"f" => "\f", '"' => '"', "\\" => "\\", "/" => "/" }.freeze
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@in_string = false
|
|
26
|
+
@is_value = false # the current string is a VALUE (emit) vs a KEY (skip)
|
|
27
|
+
@containers = [] # stack of :obj / :arr to read `,` correctly
|
|
28
|
+
@expect = :value # what the next opening string is (top-level value)
|
|
29
|
+
@line = +"" # decoded VALUE text not yet returned (current line)
|
|
30
|
+
@carry = +"" # raw chars held back across an incomplete escape
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Feed a raw argument fragment; returns the complete decoded lines unlocked
|
|
34
|
+
# by it (possibly empty), with the partial last line held back.
|
|
35
|
+
def feed(fragment)
|
|
36
|
+
return "" if fragment.nil? || fragment.empty?
|
|
37
|
+
|
|
38
|
+
scan(@carry + fragment)
|
|
39
|
+
take_complete_lines
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Final partial line (the value tail with no trailing newline), or "".
|
|
43
|
+
def flush
|
|
44
|
+
out = @line.dup
|
|
45
|
+
@line.clear
|
|
46
|
+
out
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Walks chars, decoding string VALUES into @line. Stops early and stashes
|
|
52
|
+
# an incomplete trailing escape into @carry so it is completed next feed.
|
|
53
|
+
def scan(buf)
|
|
54
|
+
@carry = +""
|
|
55
|
+
i = 0
|
|
56
|
+
n = buf.length
|
|
57
|
+
while i < n
|
|
58
|
+
ch = buf[i]
|
|
59
|
+
if @in_string
|
|
60
|
+
if ch == "\\"
|
|
61
|
+
consumed = decode_escape(buf, i)
|
|
62
|
+
return if consumed.nil? # incomplete escape → carried, wait for more
|
|
63
|
+
|
|
64
|
+
i += consumed
|
|
65
|
+
next
|
|
66
|
+
elsif ch == '"'
|
|
67
|
+
close_string
|
|
68
|
+
elsif @is_value
|
|
69
|
+
@line << ch
|
|
70
|
+
end
|
|
71
|
+
else
|
|
72
|
+
structural(ch)
|
|
73
|
+
end
|
|
74
|
+
i += 1
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Handles a backslash escape at buf[pos]. Returns the number of chars
|
|
79
|
+
# consumed (>=2), or nil when the escape is split across the fragment
|
|
80
|
+
# boundary (the raw tail is stashed in @carry to retry on the next feed).
|
|
81
|
+
def decode_escape(buf, pos)
|
|
82
|
+
nxt = buf[pos + 1]
|
|
83
|
+
return carry(buf[pos..]) if nxt.nil?
|
|
84
|
+
|
|
85
|
+
if nxt == "u"
|
|
86
|
+
hex = buf[pos + 2, 4]
|
|
87
|
+
return carry(buf[pos..]) if hex.nil? || hex.length < 4
|
|
88
|
+
|
|
89
|
+
@line << [hex.to_i(16)].pack("U") if @is_value
|
|
90
|
+
6
|
|
91
|
+
else
|
|
92
|
+
@line << UNESCAPE.fetch(nxt, nxt) if @is_value
|
|
93
|
+
2
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Stash the unfinished tail and signal "incomplete" to #scan.
|
|
98
|
+
def carry(tail)
|
|
99
|
+
@carry = tail.dup
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def structural(chr)
|
|
104
|
+
case chr
|
|
105
|
+
when '"'
|
|
106
|
+
@in_string = true
|
|
107
|
+
@is_value = (@expect == :value)
|
|
108
|
+
when "{"
|
|
109
|
+
@containers.push(:obj)
|
|
110
|
+
@expect = :key
|
|
111
|
+
when "["
|
|
112
|
+
@containers.push(:arr)
|
|
113
|
+
@expect = :value
|
|
114
|
+
when "}", "]"
|
|
115
|
+
@containers.pop
|
|
116
|
+
when ":"
|
|
117
|
+
@expect = :value
|
|
118
|
+
when ","
|
|
119
|
+
@expect = @containers.last == :arr ? :value : :key
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A string just closed. After a VALUE, drop a newline so the next value
|
|
124
|
+
# (e.g. a write's `content` after its `path`) starts on its own line.
|
|
125
|
+
def close_string
|
|
126
|
+
@in_string = false
|
|
127
|
+
@line << "\n" if @is_value
|
|
128
|
+
@expect = @containers.last == :arr ? :value : :key
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Splits @line at its last newline: everything up to and including it is
|
|
132
|
+
# returned; the remainder stays buffered as the partial current line.
|
|
133
|
+
def take_complete_lines
|
|
134
|
+
idx = @line.rindex("\n")
|
|
135
|
+
return "" if idx.nil?
|
|
136
|
+
|
|
137
|
+
out = @line[0..idx]
|
|
138
|
+
@line = @line[(idx + 1)..] || +""
|
|
139
|
+
out
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
data/lib/rubino/update_check.rb
CHANGED
|
@@ -191,9 +191,17 @@ module Rubino
|
|
|
191
191
|
installed_gem_version(GEM_NAME) ? :gem : :source
|
|
192
192
|
end
|
|
193
193
|
|
|
194
|
+
# The HIGHEST installed version of +name+ on disk, or nil when none is
|
|
195
|
+
# installed. Uses find_all_by_name (NOT find_by_name) because the running
|
|
196
|
+
# `rubino` process has its own gem version ACTIVATED — find_by_name returns
|
|
197
|
+
# that activated spec, so right after `gem update` pulls a newer gem it still
|
|
198
|
+
# reported the old version and `rubino update` wrongly said "already up to
|
|
199
|
+
# date". find_all_by_name isn't activation-filtered; taking the max sees the
|
|
200
|
+
# freshly-installed version (callers that need post-install freshness run
|
|
201
|
+
# Gem.refresh first — see Commands#update).
|
|
194
202
|
def installed_gem_version(name)
|
|
195
|
-
Gem::Specification.
|
|
196
|
-
rescue
|
|
203
|
+
Gem::Specification.find_all_by_name(name).map(&:version).max&.to_s
|
|
204
|
+
rescue StandardError
|
|
197
205
|
nil
|
|
198
206
|
end
|
|
199
207
|
|
data/lib/rubino/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubino-agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.2.
|
|
4
|
+
version: 0.5.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jhon Rojas
|
|
@@ -516,21 +516,25 @@ extensions:
|
|
|
516
516
|
- ext/landlock/extconf.rb
|
|
517
517
|
extra_rdoc_files: []
|
|
518
518
|
files:
|
|
519
|
+
- ".dockerignore"
|
|
519
520
|
- ".rspec"
|
|
520
521
|
- ".rubocop.yml"
|
|
521
522
|
- ".rubocop_todo.yml"
|
|
522
523
|
- ".ruby-version"
|
|
523
|
-
- AGENTS.md
|
|
524
524
|
- CHANGELOG.md
|
|
525
525
|
- CONTRIBUTING.md
|
|
526
|
+
- Dockerfile
|
|
526
527
|
- LICENSE
|
|
527
528
|
- README.md
|
|
528
529
|
- Rakefile
|
|
529
|
-
-
|
|
530
|
+
- agent.md
|
|
530
531
|
- docs/api/v1.md
|
|
531
532
|
- docs/architecture.md
|
|
532
533
|
- docs/commands.md
|
|
533
534
|
- docs/configuration.md
|
|
535
|
+
- docs/design/bg-shell-pty-port.md
|
|
536
|
+
- docs/design/bg-shell-review-refinements.md
|
|
537
|
+
- docs/design/bg-shell-ux.md
|
|
534
538
|
- docs/getting-started.md
|
|
535
539
|
- docs/jobs.md
|
|
536
540
|
- docs/mcp.md
|
|
@@ -562,7 +566,11 @@ files:
|
|
|
562
566
|
- lib/rubino/agent/prompts/compaction.txt
|
|
563
567
|
- lib/rubino/agent/prompts/explore.txt
|
|
564
568
|
- lib/rubino/agent/prompts/general.txt
|
|
569
|
+
- lib/rubino/agent/prompts/memory_guidance.txt
|
|
565
570
|
- lib/rubino/agent/prompts/plan.txt
|
|
571
|
+
- lib/rubino/agent/prompts/tool_use_enforcement.txt
|
|
572
|
+
- lib/rubino/agent/prompts/tool_use_enforcement_google.txt
|
|
573
|
+
- lib/rubino/agent/prompts/tool_use_enforcement_openai.txt
|
|
566
574
|
- lib/rubino/agent/response_validator.rb
|
|
567
575
|
- lib/rubino/agent/runner.rb
|
|
568
576
|
- lib/rubino/agent/tool_executor.rb
|
|
@@ -724,7 +732,6 @@ files:
|
|
|
724
732
|
- lib/rubino/jobs/handlers/compact_session_job.rb
|
|
725
733
|
- lib/rubino/jobs/handlers/distill_skill_job.rb
|
|
726
734
|
- lib/rubino/jobs/handlers/extract_memory_job.rb
|
|
727
|
-
- lib/rubino/jobs/handlers/summarize_session_job.rb
|
|
728
735
|
- lib/rubino/jobs/queue.rb
|
|
729
736
|
- lib/rubino/jobs/registry.rb
|
|
730
737
|
- lib/rubino/jobs/runner.rb
|
|
@@ -860,6 +867,7 @@ files:
|
|
|
860
867
|
- lib/rubino/tools/retrieve_output_tool.rb
|
|
861
868
|
- lib/rubino/tools/ruby_tool.rb
|
|
862
869
|
- lib/rubino/tools/session_search_tool.rb
|
|
870
|
+
- lib/rubino/tools/shell_entry_adapter.rb
|
|
863
871
|
- lib/rubino/tools/shell_input_tool.rb
|
|
864
872
|
- lib/rubino/tools/shell_kill_tool.rb
|
|
865
873
|
- lib/rubino/tools/shell_output_tool.rb
|
|
@@ -868,7 +876,6 @@ files:
|
|
|
868
876
|
- lib/rubino/tools/shell_tool.rb
|
|
869
877
|
- lib/rubino/tools/steer_tool.rb
|
|
870
878
|
- lib/rubino/tools/subagent_probe.rb
|
|
871
|
-
- lib/rubino/tools/summarize_file_tool.rb
|
|
872
879
|
- lib/rubino/tools/task_result_tool.rb
|
|
873
880
|
- lib/rubino/tools/task_stop_tool.rb
|
|
874
881
|
- lib/rubino/tools/task_tool.rb
|
|
@@ -907,6 +914,7 @@ files:
|
|
|
907
914
|
- lib/rubino/ui/stdout_proxy.rb
|
|
908
915
|
- lib/rubino/ui/streaming_markdown.rb
|
|
909
916
|
- lib/rubino/ui/subagent_cards.rb
|
|
917
|
+
- lib/rubino/ui/tool_args_stream.rb
|
|
910
918
|
- lib/rubino/ui/tool_label.rb
|
|
911
919
|
- lib/rubino/update_check.rb
|
|
912
920
|
- lib/rubino/util/atomic_file.rb
|
|
@@ -955,7 +963,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
955
963
|
- !ruby/object:Gem::Version
|
|
956
964
|
version: '0'
|
|
957
965
|
requirements: []
|
|
958
|
-
rubygems_version: 4.0.
|
|
966
|
+
rubygems_version: 4.0.6
|
|
959
967
|
specification_version: 4
|
|
960
968
|
summary: A lightweight Ruby coding and automation agent with persistent memory, sessions,
|
|
961
969
|
and context compaction
|
data/AGENTS.md
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# AGENTS.md — rubino
|
|
2
|
-
|
|
3
|
-
Onboarding for anyone (human or AI) working on this codebase.
|
|
4
|
-
|
|
5
|
-
## Vision
|
|
6
|
-
|
|
7
|
-
`rubino` is a **micro agent**: a small, self-contained Ruby gem that runs an LLM-driven coding/automation agent *on the machine where the work happens* — the user's PC or a VM. You drop it onto that machine and it works there, reachable over a clean HTTP API and a CLI. It executes conversation turns with tool calls, streams events, manages skills, and persists sessions/memory in a single SQLite file. It can be embedded as a library, run as a CLI, or run as a small service behind a REST gateway.
|
|
8
|
-
|
|
9
|
-
It is a lightweight agent, **not a heavy framework**. We pick the few primitives that have real value and skip everything else. Scope discipline is part of the product.
|
|
10
|
-
|
|
11
|
-
## Non-goals (explicit)
|
|
12
|
-
|
|
13
|
-
These decisions are permanent. Reopen only with a written justification.
|
|
14
|
-
|
|
15
|
-
- **No dual HTTP APIs.** One server, one versioned prefix (`/v1`). When we break, bump to `/v2`. Never aliases.
|
|
16
|
-
- **No multi-tenant inside one process.** One instance = one workspace = one identity. Multi-tenant deployments run N instances.
|
|
17
|
-
- **No dashboard / theme system / plugin hub.** The dashboard is whatever client consumes our API. We don't ship a UI server.
|
|
18
|
-
- **No skill catalog bundled.** Skills are user-supplied directories. We don't ship a catalog.
|
|
19
|
-
- **No compat layers.** Renamed something? Rename everywhere. Reshaped a response? Bump version.
|
|
20
|
-
- **No auto-update endpoints.** Deploy = gem update + restart. Period.
|
|
21
|
-
- **No backwards-compat shims, no _deprecated suffixes, no "legacy" anything.**
|
|
22
|
-
|
|
23
|
-
## Architectural rules (load-bearing)
|
|
24
|
-
|
|
25
|
-
These are load-bearing. When a PR violates them, it is the PR that gets redone.
|
|
26
|
-
|
|
27
|
-
1. **One interface per concept.** One `LLM::Adapter`, one `Tools::Base`, one `Memory::Store`. Variants are subclasses, not parallels.
|
|
28
|
-
2. **No PORO commands in `lib/`.** Domain logic on models/repositories; orchestration as class methods. No `app/services/Foo::DoThingCommand`.
|
|
29
|
-
3. **No optional-magic params.** Signatures explicit. If long, refactor responsibilities, don't add `**opts`.
|
|
30
|
-
4. **Schema-validated input at boundaries** (HTTP, CLI, MCP). Internals trust types.
|
|
31
|
-
5. **Typed errors only.** `raise NotFoundError, "session"` not `raise "session not found"`.
|
|
32
|
-
6. **Test contracts, not implementation.** Internals change without breaking tests, not vice versa.
|
|
33
|
-
7. **Structured logging from day one.** JSON-line. No `puts`/`pp` outside the UI layer. Logger DI-injected.
|
|
34
|
-
8. **No global singletons.** Config flows from the main loop into everything via constructor.
|
|
35
|
-
9. **Handlers stay thin.** Endpoint >30 LOC → extract `Operation` class.
|
|
36
|
-
10. **Less code, less bugs.** A refactor is good if it deletes net lines. Distrust preparatory abstractions (PORO/Result/custom errors before you need them).
|
|
37
|
-
|
|
38
|
-
## Tech stack
|
|
39
|
-
|
|
40
|
-
| Layer | Choice | Why |
|
|
41
|
-
|---|---|---|
|
|
42
|
-
| LLM | `ruby_llm ~> 1.0` (thin wrap) | One mature gem covering OpenAI/Anthropic/Gemini/Bedrock. We never call providers directly. |
|
|
43
|
-
| MCP | `ruby_llm-mcp ~> 0.8` | Pairs natively with `ruby_llm`. |
|
|
44
|
-
| HTTP server | `rack` + `puma` | Standard. WEBrick is gone. |
|
|
45
|
-
| DB | `sequel` + `sqlite3` | One file, zero ops. Postgres later if multi-writer matters. |
|
|
46
|
-
| Schema | `dry-schema` | Boundary validation. |
|
|
47
|
-
| Scheduler | `rufus-scheduler` | Cron syntax, in-process. |
|
|
48
|
-
| Config | `dry-configurable` | Already in use. |
|
|
49
|
-
| CLI | `thor` | Already in use. |
|
|
50
|
-
| Autoload | `zeitwerk` | Already in use. |
|
|
51
|
-
| CLI UI | `tty-*` + `reline` | The interactive `rubino chat` prompt (history, completion, multi-line editing). |
|
|
52
|
-
|
|
53
|
-
`ruby_llm` is the foundation. `LLM::RubyLLMAdapter` is a thin wrapper: it configures `RubyLLM`, delegates `chat`/`stream`/`model_info`, and exposes nothing the underlying gem can't already do. **If you find yourself adding business logic to the adapter, push it into the run loop instead.**
|
|
54
|
-
|
|
55
|
-
## Layout
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
lib/rubino/
|
|
59
|
-
agent/ # run loop, lifecycle, multi-agent routing
|
|
60
|
-
api/ # HTTP gateway (Rack app, middleware, operations)
|
|
61
|
-
cli/ # thor commands
|
|
62
|
-
config/ # dry-configurable
|
|
63
|
-
context/ # compaction, prompt assembly
|
|
64
|
-
interaction/ # conversation lifecycle primitives
|
|
65
|
-
jobs/ # scheduler + worker (cron)
|
|
66
|
-
llm/ # ruby_llm adapter + model registry
|
|
67
|
-
mcp/ # MCP client + tool wrapper
|
|
68
|
-
memory/ # Memory::Store + extractors + retrievers
|
|
69
|
-
oauth/ # OAuth providers + connection storage
|
|
70
|
-
security/ # approval policy, sandboxing
|
|
71
|
-
session/ # repository + persistence
|
|
72
|
-
skills/ # SKILL.md loader
|
|
73
|
-
tools/ # built-in tools
|
|
74
|
-
ui/ # cli/null/api stub UIs
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Surfaces this project exposes
|
|
78
|
-
|
|
79
|
-
- **HTTP API** (`/v1/*`) — the canonical interface. See `docs/api/v1.md`.
|
|
80
|
-
- **CLI** — `rubino {setup,chat,prompt,server,config,memory,sessions,jobs,skills,tools,tls_cert,doctor,version,update}`.
|
|
81
|
-
- **Library** — `require "rubino"; Rubino.run(...)`.
|
|
82
|
-
|
|
83
|
-
The interactive CLI ships as part of `rubino chat`. Multi-agent routing, MCP, and plugin hooks are designed in but not fully wired yet.
|
|
84
|
-
|
|
85
|
-
## Working on this codebase
|
|
86
|
-
|
|
87
|
-
- Read `docs/architecture.md` for the bigger picture.
|
|
88
|
-
- Read `docs/api/v1.md` for HTTP contract.
|
|
89
|
-
- Read `docs/oauth-providers.md` for OAuth design.
|
|
90
|
-
- Run `bundle exec rspec` before pushing.
|
|
91
|
-
- Don't commit if rubocop fails.
|
|
92
|
-
|
|
93
|
-
## Git/commit conventions
|
|
94
|
-
|
|
95
|
-
- Conventional commits welcome but not required.
|
|
96
|
-
- No AI attribution lines in commit messages.
|
|
97
|
-
- Small commits over big ones. A commit that touches 30 files needs a justification.
|