samagotchi 0.22.0 → 0.23.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 +33 -1
- data/bin/chi +9 -4
- data/docs/configuration.md +4 -4
- data/docs/releasing.md +1 -1
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +4 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +2 -2
- data/lib/samagotchi/bridge/bounded_queue.rb +0 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -6
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -2
- data/lib/samagotchi/bridge.rb +12 -9
- data/lib/samagotchi/bridge_client/event_stream.rb +2 -2
- data/lib/samagotchi/bridge_client.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +39 -29
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +2 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +2 -0
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +6 -5
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +2 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +5 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -7
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cancellation_controller.rb +1 -1
- data/lib/samagotchi/cli/command.rb +1 -1
- data/lib/samagotchi/client.rb +3 -5
- data/lib/samagotchi/commands/registry.rb +2 -2
- data/lib/samagotchi/config.rb +38 -14
- data/lib/samagotchi/context_note.rb +1 -1
- data/lib/samagotchi/context_status.rb +6 -6
- data/lib/samagotchi/continue_offer.rb +15 -8
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/desktop/macos.rb +5 -4
- data/lib/samagotchi/empty_answer_retry.rb +1 -1
- data/lib/samagotchi/engine.rb +17 -17
- data/lib/samagotchi/gem_update.rb +2 -1
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +5 -5
- data/lib/samagotchi/guardrails/approvals.rb +2 -2
- data/lib/samagotchi/guardrails/gate.rb +2 -2
- data/lib/samagotchi/guardrails/read_only_shell.rb +5 -3
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +2 -2
- data/lib/samagotchi/guardrails/shell_paths.rb +2 -2
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -6
- data/lib/samagotchi/hooks/loader.rb +18 -16
- data/lib/samagotchi/hooks/registry.rb +10 -2
- data/lib/samagotchi/host_registry.rb +1 -3
- data/lib/samagotchi/idle_recap.rb +18 -15
- data/lib/samagotchi/idle_reminders.rb +6 -3
- data/lib/samagotchi/image_store.rb +5 -2
- data/lib/samagotchi/kernel_loop.rb +31 -31
- data/lib/samagotchi/llm/chat_loop.rb +9 -6
- data/lib/samagotchi/llm/errors.rb +2 -2
- data/lib/samagotchi/llm/http.rb +12 -12
- data/lib/samagotchi/llm/openai_chat.rb +39 -28
- data/lib/samagotchi/llm/prompt_cache.rb +1 -1
- data/lib/samagotchi/log.rb +2 -2
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/memory_bundle/builder.rb +9 -3
- data/lib/samagotchi/memory_bundle/index_updater.rb +13 -1
- data/lib/samagotchi/memory_bundle/installer.rb +67 -57
- data/lib/samagotchi/memory_bundle/listing.rb +5 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +6 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +16 -13
- data/lib/samagotchi/memory_bundle/source.rb +7 -3
- data/lib/samagotchi/memory_bundle/status.rb +3 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +3 -2
- data/lib/samagotchi/memory_bundle/trash.rb +5 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +9 -6
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/model_overlay.rb +3 -5
- data/lib/samagotchi/model_profile.rb +12 -8
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/output_formatter.rb +18 -17
- data/lib/samagotchi/owner_lock.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +8 -5
- data/lib/samagotchi/plugin/context.rb +1 -3
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/plugin/service.rb +1 -1
- data/lib/samagotchi/plugin/sessions.rb +1 -0
- data/lib/samagotchi/plugin_tasks.rb +1 -1
- data/lib/samagotchi/prompt.rb +68 -12
- data/lib/samagotchi/prompt_literal_guard.rb +1 -1
- data/lib/samagotchi/question_desk.rb +7 -7
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/relay_desk.rb +1 -1
- data/lib/samagotchi/reminder_store.rb +2 -1
- data/lib/samagotchi/send_command.rb +4 -2
- data/lib/samagotchi/session.rb +2 -2
- data/lib/samagotchi/session_archive_command.rb +5 -1
- data/lib/samagotchi/session_commands.rb +1 -3
- data/lib/samagotchi/session_manager.rb +2 -4
- data/lib/samagotchi/session_metrics.rb +10 -10
- data/lib/samagotchi/session_observer.rb +3 -2
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/sessions_command.rb +14 -5
- data/lib/samagotchi/system_prompt.rb +19 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +1 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +10 -10
- data/lib/samagotchi/terminal_ui/input_support.rb +2 -2
- data/lib/samagotchi/terminal_ui/line_reader.rb +3 -3
- data/lib/samagotchi/terminal_ui/question_prompt.rb +23 -23
- data/lib/samagotchi/terminal_ui/question_slot.rb +10 -10
- data/lib/samagotchi/terminal_ui/reline_seam.rb +1 -1
- data/lib/samagotchi/terminal_ui/repl_input.rb +3 -2
- data/lib/samagotchi/terminal_ui.rb +9 -5
- data/lib/samagotchi/text_diff.rb +12 -12
- data/lib/samagotchi/thinking.rb +6 -2
- data/lib/samagotchi/thought_stream_splitter.rb +6 -3
- data/lib/samagotchi/tool_activity.rb +1 -0
- data/lib/samagotchi/tool_call_parser.rb +13 -14
- data/lib/samagotchi/tool_declarations.rb +36 -35
- data/lib/samagotchi/tools/ask_user_question.rb +3 -4
- data/lib/samagotchi/tools/builtin_calls.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +0 -1
- data/lib/samagotchi/tools/edit.rb +14 -13
- data/lib/samagotchi/tools/execute.rb +3 -5
- data/lib/samagotchi/tools/list_reminders.rb +1 -1
- data/lib/samagotchi/tools/memory.rb +21 -22
- data/lib/samagotchi/tools/read.rb +7 -7
- data/lib/samagotchi/tools/task_create.rb +1 -1
- data/lib/samagotchi/tools/task_get.rb +1 -1
- data/lib/samagotchi/tools/task_list.rb +1 -1
- data/lib/samagotchi/tools/task_runtime.rb +8 -9
- data/lib/samagotchi/tools/task_stop.rb +1 -1
- data/lib/samagotchi/tools/task_wait.rb +3 -4
- data/lib/samagotchi/tools/web_fetch.rb +2 -4
- data/lib/samagotchi/tools/write.rb +1 -1
- data/lib/samagotchi/turn_flow.rb +8 -8
- data/lib/samagotchi/turn_note.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +2 -2
- data/lib/samagotchi/web/app.rb +13 -6
- data/lib/samagotchi/web/markdown_renderer.rb +2 -1
- data/lib/samagotchi/web/qr.rb +7 -1
- data/lib/samagotchi/web/server.rb +6 -2
- data/lib/samagotchi/worker.rb +10 -2
- metadata +20 -20
|
@@ -15,7 +15,7 @@ module Samagotchi
|
|
|
15
15
|
params_suffix = params.empty? ? "" : " #{paint(params, 90)}"
|
|
16
16
|
status = activity[:status].to_s
|
|
17
17
|
elapsed_suffix = duration_ms.nil? ? "" : " (#{format_elapsed_duration(duration_ms)})"
|
|
18
|
-
"#{paint(
|
|
18
|
+
"#{paint("tool>", 36)} #{activity[:action]} (#{activity[:tool]}#{params_suffix}): #{paint(status, status_color(status))}#{elapsed_suffix}"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Green ok, yellow stopped (a wait the user's Stop ended), red the rest.
|
|
@@ -75,7 +75,7 @@ module Samagotchi
|
|
|
75
75
|
return "" unless diff.is_a?(Hash)
|
|
76
76
|
|
|
77
77
|
get = ->(key) { diff[key] || diff[key.to_s] }
|
|
78
|
-
" #{paint("+#{get.(:added).to_i}", 32)} #{paint("\u2212#{get.(:removed).to_i}", 31)}"
|
|
78
|
+
" #{paint("+#{get.call(:added).to_i}", 32)} #{paint("\u2212#{get.call(:removed).to_i}", 31)}"
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# Prompt labels by the sender's client_id prefix (turn_events.js
|
|
@@ -113,7 +113,7 @@ module Samagotchi
|
|
|
113
113
|
params = part[:params].to_s.strip
|
|
114
114
|
params_suffix = params.empty? ? "" : " #{paint(params, 90)}"
|
|
115
115
|
status = part[:status].to_s
|
|
116
|
-
"#{paint(
|
|
116
|
+
"#{paint("tool>", 36)} #{part[:tool]}#{params_suffix}: #{paint(status, status_color(status))}" \
|
|
117
117
|
"#{format_tool_image_suffix(part[:images])}#{format_tool_diff_suffix(part[:diff])}"
|
|
118
118
|
end
|
|
119
119
|
|
|
@@ -233,8 +233,8 @@ module Samagotchi
|
|
|
233
233
|
value = tps.to_f
|
|
234
234
|
return "" unless value.positive?
|
|
235
235
|
|
|
236
|
-
number = value >= 1000 ? "#{format(
|
|
237
|
-
"#{
|
|
236
|
+
number = value >= 1000 ? "#{format("%.1f", (value / 100).round / 10.0)}k" : value.round.to_s
|
|
237
|
+
"#{"~" if source.to_s == "estimate"}#{number} tok/s"
|
|
238
238
|
end
|
|
239
239
|
|
|
240
240
|
# "$0.42"; a cost under a cent keeps four decimals ("$0.0012"); "" for
|
|
@@ -297,7 +297,7 @@ module Samagotchi
|
|
|
297
297
|
def status_rows(segments, width)
|
|
298
298
|
return [] if segments.empty? || width <= 0
|
|
299
299
|
|
|
300
|
-
row = cap_preview_text("status> #{segments.join(
|
|
300
|
+
row = cap_preview_text("status> #{segments.join(" | ")}", width)
|
|
301
301
|
[color_output? ? paint(row, 90) : row]
|
|
302
302
|
end
|
|
303
303
|
|
|
@@ -336,7 +336,7 @@ module Samagotchi
|
|
|
336
336
|
|
|
337
337
|
visible = names.first(limit)
|
|
338
338
|
suffix = names.length > visible.length ? ", +#{names.length - visible.length}" : ""
|
|
339
|
-
"#{label}: #{visible.join(
|
|
339
|
+
"#{label}: #{visible.join(", ")}#{suffix}"
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
def cap_preview_text(text, width)
|
|
@@ -411,8 +411,8 @@ module Samagotchi
|
|
|
411
411
|
return nil unless last
|
|
412
412
|
|
|
413
413
|
source = tokens[:tps_source].to_s
|
|
414
|
-
text =
|
|
415
|
-
text << ", #{speed_text(tokens[:last_prefill_tps],
|
|
414
|
+
text = "#{speed_text(last, source)} out"
|
|
415
|
+
text << ", #{speed_text(tokens[:last_prefill_tps], "server")} prompt" if tokens[:last_prefill_tps]
|
|
416
416
|
text << " (last, #{source})"
|
|
417
417
|
text << ", avg #{speed_text(tokens[:avg_decode_tps], source)}" if tokens[:avg_decode_tps]
|
|
418
418
|
text
|
|
@@ -428,7 +428,7 @@ module Samagotchi
|
|
|
428
428
|
return "#{seconds.round}s" if seconds < 60
|
|
429
429
|
|
|
430
430
|
total_seconds = seconds.round
|
|
431
|
-
"#{total_seconds / 60}m #{format(
|
|
431
|
+
"#{total_seconds / 60}m #{format("%02d", total_seconds % 60)}s"
|
|
432
432
|
end
|
|
433
433
|
|
|
434
434
|
def paint(text, code)
|
|
@@ -39,7 +39,7 @@ module Samagotchi
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def alive? = @thread&.alive? || false
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
# Drop the open read and start it again with the current prompt.
|
|
44
44
|
# @param keep_text [Boolean] with what is typed there now
|
|
45
45
|
# @param prefill [String, nil] with this text instead
|
|
@@ -48,7 +48,7 @@ module Samagotchi
|
|
|
48
48
|
@prefill = text unless text.to_s.empty?
|
|
49
49
|
@thread&.raise(Reprompt)
|
|
50
50
|
end
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
# Type +text+ into the next read (from the sink, between two reads).
|
|
53
53
|
def prefill_next(text)
|
|
54
54
|
@prefill = text
|
|
@@ -57,7 +57,7 @@ module Samagotchi
|
|
|
57
57
|
# @return [String, nil] what the open read holds
|
|
58
58
|
def typed_text
|
|
59
59
|
return nil unless $stdin.tty?
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
Reline.line_buffer.to_s
|
|
62
62
|
rescue StandardError
|
|
63
63
|
nil
|
|
@@ -33,29 +33,29 @@ module Samagotchi
|
|
|
33
33
|
# @param pending [Hash] Engine#pending_question (symbol or string keys)
|
|
34
34
|
def initialize(pending)
|
|
35
35
|
field = ->(key) { pending[key] || pending[key.to_s] }
|
|
36
|
-
approval = field.(:approval)
|
|
36
|
+
approval = field.call(:approval)
|
|
37
37
|
@preview = approval.is_a?(Hash) ? (approval[:preview] || approval["preview"]) : nil
|
|
38
|
-
@id = field.(:id).to_s
|
|
39
|
-
@question = field.(:question).to_s
|
|
38
|
+
@id = field.call(:id).to_s
|
|
39
|
+
@question = field.call(:question).to_s
|
|
40
40
|
# A delegate's approval relayed here: the delegate's own text is the
|
|
41
41
|
# question, who asks a dim line (#note).
|
|
42
|
-
relay = field.(:relay)
|
|
42
|
+
relay = field.call(:relay)
|
|
43
43
|
if relay.is_a?(Hash)
|
|
44
44
|
get = ->(key) { relay[key] || relay[key.to_s] }
|
|
45
|
-
chain = Array(get.(:chain)).map(&:to_s)
|
|
46
|
-
chain = [get.(:child_id).to_s[0, 8]] if chain.empty?
|
|
45
|
+
chain = Array(get.call(:chain)).map(&:to_s)
|
|
46
|
+
chain = [get.call(:child_id).to_s[0, 8]] if chain.empty?
|
|
47
47
|
@delegate = chain.reverse.join(" → ")
|
|
48
|
-
@delegate_task = get.(:task).to_s
|
|
49
|
-
@question = get.(:asked).to_s unless get.(:asked).to_s.strip.empty?
|
|
48
|
+
@delegate_task = get.call(:task).to_s
|
|
49
|
+
@question = get.call(:asked).to_s unless get.call(:asked).to_s.strip.empty?
|
|
50
50
|
end
|
|
51
|
-
self.relayed_to = field.(:relayed_to)
|
|
52
|
-
@options = Array(field.(:options)).map { |v| v.to_s.strip }.reject(&:empty?)
|
|
53
|
-
header = field.(:header).to_s.strip
|
|
51
|
+
self.relayed_to = field.call(:relayed_to)
|
|
52
|
+
@options = Array(field.call(:options)).map { |v| v.to_s.strip }.reject(&:empty?)
|
|
53
|
+
header = field.call(:header).to_s.strip
|
|
54
54
|
@header = header.empty? ? nil : header
|
|
55
|
-
@multi = !!field.(:multi_select)
|
|
56
|
-
@free = !!field.(:allow_freeform)
|
|
57
|
-
@approval = field.(:kind).to_s == "approval"
|
|
58
|
-
@continue = field.(:kind).to_s == "continue"
|
|
55
|
+
@multi = !!field.call(:multi_select)
|
|
56
|
+
@free = !!field.call(:allow_freeform)
|
|
57
|
+
@approval = field.call(:kind).to_s == "approval"
|
|
58
|
+
@continue = field.call(:kind).to_s == "continue"
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def multi? = @multi
|
|
@@ -121,19 +121,19 @@ module Samagotchi
|
|
|
121
121
|
return [] unless preview.is_a?(Hash)
|
|
122
122
|
|
|
123
123
|
get = ->(key) { preview[key] || preview[key.to_s] }
|
|
124
|
-
return [paint.("this edit would fail: #{get.(:error)}", 33)] if get.(:error)
|
|
125
|
-
return [paint.("diff not shown: #{get.(:skipped)}", 2)] if get.(:skipped)
|
|
124
|
+
return [paint.call("this edit would fail: #{get.call(:error)}", 33)] if get.call(:error)
|
|
125
|
+
return [paint.call("diff not shown: #{get.call(:skipped)}", 2)] if get.call(:skipped)
|
|
126
126
|
|
|
127
|
-
lines = get.(:text).to_s.split("\n")
|
|
127
|
+
lines = get.call(:text).to_s.split("\n")
|
|
128
128
|
return [] if lines.empty?
|
|
129
129
|
|
|
130
130
|
# TextDiff's own cut note ("… N more lines") counts toward the rest.
|
|
131
131
|
cut = lines.last.to_s[/\A… (\d+) more lines\z/, 1]
|
|
132
132
|
lines.pop if cut
|
|
133
133
|
hidden = [lines.size - PREVIEW_LINES, 0].max + cut.to_i
|
|
134
|
-
shown = lines.first(PREVIEW_LINES).map { |line| paint.(line, diff_code(line)) }
|
|
135
|
-
shown.unshift(paint.("new file", 2)) if get.(:new_file)
|
|
136
|
-
shown << paint.("… #{hidden} more lines (full diff on the web)", 2) if hidden.positive?
|
|
134
|
+
shown = lines.first(PREVIEW_LINES).map { |line| paint.call(line, diff_code(line)) }
|
|
135
|
+
shown.unshift(paint.call("new file", 2)) if get.call(:new_file)
|
|
136
|
+
shown << paint.call("… #{hidden} more lines (full diff on the web)", 2) if hidden.positive?
|
|
137
137
|
shown
|
|
138
138
|
end
|
|
139
139
|
|
|
@@ -142,7 +142,7 @@ module Samagotchi
|
|
|
142
142
|
# @param outcome [String] the answer (#answer_text) or what closed it
|
|
143
143
|
def summary(outcome, paint: ->(text, _code) { text })
|
|
144
144
|
who = @delegate ? "#{@delegate}: " : ""
|
|
145
|
-
"#{paint.("#{mark}#{who}#{question.lines.first.to_s.chomp}", approval? ? 33 : 94)} → #{outcome}"
|
|
145
|
+
"#{paint.call("#{mark}#{who}#{question.lines.first.to_s.chomp}", approval? ? 33 : 94)} → #{outcome}"
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
# @param answer [Answer] an accepted one
|
|
@@ -166,7 +166,7 @@ module Samagotchi
|
|
|
166
166
|
raw = raw.to_s.strip
|
|
167
167
|
# "1,3; my text": the first ';' separates the selection from freeform text.
|
|
168
168
|
sel_part, free_part = raw.include?(";") ? raw.split(";", 2).map(&:strip) : [raw, nil]
|
|
169
|
-
free_part = nil if free_part
|
|
169
|
+
free_part = nil if free_part && free_part.empty?
|
|
170
170
|
# Models forget to flag freeform: accept it anyway, with a note.
|
|
171
171
|
note = free_part && !free? ? "(note: freeform not flagged but accepting '#{free_part}')" : nil
|
|
172
172
|
|
|
@@ -31,7 +31,7 @@ module Samagotchi
|
|
|
31
31
|
|
|
32
32
|
# The line a continue offer leaves in the scrollback.
|
|
33
33
|
def self.continue_summary(outcome, paint: ->(text, _code) { text })
|
|
34
|
-
"#{paint.("? #{CONTINUE_QUESTION}", 33)} → #{outcome}"
|
|
34
|
+
"#{paint.call("? #{CONTINUE_QUESTION}", 33)} → #{outcome}"
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# @param question [String] the first row's text, wrapped when there is room
|
|
@@ -77,14 +77,14 @@ module Samagotchi
|
|
|
77
77
|
|
|
78
78
|
def layout(width, hint:, details:, header:, folded:)
|
|
79
79
|
rows = []
|
|
80
|
-
rows << @paint.(@header, 1) if header && @header
|
|
81
|
-
rows << @paint.(@note, 2) if header && @note
|
|
80
|
+
rows << @paint.call(@header, 1) if header && @header
|
|
81
|
+
rows << @paint.call(@note, 2) if header && @note
|
|
82
82
|
question_rows = wrap(@question, width, first: @mark, indent: " " * @mark.size)
|
|
83
83
|
question_rows = question_rows.first(1) unless details
|
|
84
|
-
rows.concat(question_rows.map { |row| @paint.(row, @question_code) })
|
|
85
|
-
rows.concat(@details.map { |row| @paint.(row, @question_code) }) if details
|
|
86
|
-
rows.concat(folded ? folded_options(width) : @options.map { |o| @paint.(" #{o.text}", 92) })
|
|
87
|
-
rows << @paint.(" [#{@hint}]", 90) if hint && @hint
|
|
84
|
+
rows.concat(question_rows.map { |row| @paint.call(row, @question_code) })
|
|
85
|
+
rows.concat(@details.map { |row| @paint.call(row, @question_code) }) if details
|
|
86
|
+
rows.concat(folded ? folded_options(width) : @options.map { |o| @paint.call(" #{o.text}", 92) })
|
|
87
|
+
rows << @paint.call(" [#{@hint}]", 90) if hint && @hint
|
|
88
88
|
rows
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -97,13 +97,13 @@ module Samagotchi
|
|
|
97
97
|
candidate = line.empty? ? " #{item}" : "#{line} · #{item}"
|
|
98
98
|
if !line.empty? && display_width(candidate) > width
|
|
99
99
|
rows << line
|
|
100
|
-
line =
|
|
100
|
+
line = " #{item}"
|
|
101
101
|
else
|
|
102
102
|
line = +candidate
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
rows << line unless line.empty?
|
|
106
|
-
rows.map { |row| @paint.(row, 92) }
|
|
106
|
+
rows.map { |row| @paint.call(row, 92) }
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Word wrap at +width+ columns, the first row after +first+, the others
|
|
@@ -122,7 +122,7 @@ module Samagotchi
|
|
|
122
122
|
while display_width(line.rstrip) > width
|
|
123
123
|
head, = Reline::Unicode.take_mbchar_range(line, 0, width, padding: false)
|
|
124
124
|
rows << head
|
|
125
|
-
line =
|
|
125
|
+
line = "#{indent}#{line[head.size..]}"
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
rows << line.rstrip unless line.strip.empty? && !rows.empty?
|
|
@@ -216,7 +216,7 @@ module Samagotchi
|
|
|
216
216
|
# The prompt and the text as render_finished writes them: one line per
|
|
217
217
|
# input line, with a trailing space when a line fills the width exactly.
|
|
218
218
|
def seam_final_lines
|
|
219
|
-
@buffer_of_lines.size
|
|
219
|
+
Array.new(@buffer_of_lines.size) do |i|
|
|
220
220
|
line = Reline::Unicode.strip_non_printing_start_end(prompt_list[i]) + modified_lines[i]
|
|
221
221
|
split_line_by_width(line, screen_width).last.empty? ? "#{line} " : line
|
|
222
222
|
end
|
|
@@ -43,6 +43,7 @@ module Samagotchi
|
|
|
43
43
|
def <<(item)
|
|
44
44
|
@lock.synchronize do
|
|
45
45
|
next @answers << item if @answers
|
|
46
|
+
|
|
46
47
|
taken = @turn && item.first == :line && @turn.call(item.last)
|
|
47
48
|
# Not now: the line goes back into the prompt.
|
|
48
49
|
next @reader&.prefill_next(item.last) if taken == :back
|
|
@@ -130,8 +131,8 @@ module Samagotchi
|
|
|
130
131
|
end
|
|
131
132
|
end
|
|
132
133
|
|
|
133
|
-
def with_surface_lock(&
|
|
134
|
-
@surface.respond_to?(:synchronize) ? @surface.synchronize(&
|
|
134
|
+
def with_surface_lock(&)
|
|
135
|
+
@surface.respond_to?(:synchronize) ? @surface.synchronize(&) : yield
|
|
135
136
|
end
|
|
136
137
|
end
|
|
137
138
|
end
|
|
@@ -90,7 +90,7 @@ module Samagotchi
|
|
|
90
90
|
# As typed: the Engine applies an alias where it resolves the model.
|
|
91
91
|
flag_model_name = model_name.to_s.strip.empty? ? nil : ModelProfile.required_model_name(model_name)
|
|
92
92
|
@effective_model_name = flag_model_name || @default_model_name
|
|
93
|
-
@host_registry
|
|
93
|
+
@host_registry = host_registry || Samagotchi::HostRegistry.new
|
|
94
94
|
# An injected client (specs) stands in for every host's client.
|
|
95
95
|
@host_registry.client_override = client if client
|
|
96
96
|
@client = @host_registry.resolve(@effective_model_name).client
|
|
@@ -101,6 +101,7 @@ module Samagotchi
|
|
|
101
101
|
if session_id && !Session.exist?(session_id)
|
|
102
102
|
raise SessionNotFound, "Session not found: #{session_id}"
|
|
103
103
|
end
|
|
104
|
+
|
|
104
105
|
claim_session!(session_id) if session_id
|
|
105
106
|
@resume_session = session_id ? Session.load(session_id) : nil
|
|
106
107
|
if @resume_session
|
|
@@ -146,7 +147,7 @@ module Samagotchi
|
|
|
146
147
|
@between_turns = BetweenTurns.new(surface: @surface, view: @view, renderer: @renderer,
|
|
147
148
|
turn_running: -> { @engine.turn_running? }, quiet: @non_interactive)
|
|
148
149
|
@render_event = ->(event) { handle_stream_event(event) }
|
|
149
|
-
@engine
|
|
150
|
+
@engine = Engine.new(
|
|
150
151
|
client: client,
|
|
151
152
|
host_registry: @host_registry,
|
|
152
153
|
profile: profile,
|
|
@@ -504,6 +505,7 @@ module Samagotchi
|
|
|
504
505
|
end
|
|
505
506
|
end
|
|
506
507
|
break if input.nil?
|
|
508
|
+
|
|
507
509
|
local = local_command(input)
|
|
508
510
|
if local == :exit
|
|
509
511
|
@delete_on_exit ||= SessionCommands.delete_on_exit?(input)
|
|
@@ -705,6 +707,7 @@ module Samagotchi
|
|
|
705
707
|
return
|
|
706
708
|
end
|
|
707
709
|
return if show_local_command(input)
|
|
710
|
+
|
|
708
711
|
# A typo of a command (/modle) is not a prompt: say so, send nothing.
|
|
709
712
|
hint = command_registry.unknown_command_hint(input)
|
|
710
713
|
return @surface.commit(hint) if hint
|
|
@@ -849,6 +852,7 @@ module Samagotchi
|
|
|
849
852
|
# #cancel_turn_from_prompt.
|
|
850
853
|
def poll_input_with_reminder_check(awaiting_continue:)
|
|
851
854
|
return :due unless @engine.due_reminder_names.empty?
|
|
855
|
+
|
|
852
856
|
# Specs and pipes: a plain blocking read.
|
|
853
857
|
unless @repl_input
|
|
854
858
|
@between_turns.flush_cards
|
|
@@ -972,10 +976,10 @@ module Samagotchi
|
|
|
972
976
|
# While a turn runs, a line submitted at the open prompt steers it: it
|
|
973
977
|
# merges at the next iteration boundary (Kernel), and one that comes after
|
|
974
978
|
# the last runs as the next turn. Reminder turns too.
|
|
975
|
-
def with_steering(&
|
|
979
|
+
def with_steering(&)
|
|
976
980
|
return yield unless @repl_input
|
|
977
981
|
|
|
978
|
-
@repl_input.during_turn(method(:steer_line), leftovers: -> { @pending_input_queue.drain }, &
|
|
982
|
+
@repl_input.during_turn(method(:steer_line), leftovers: -> { @pending_input_queue.drain }, &)
|
|
979
983
|
end
|
|
980
984
|
|
|
981
985
|
# On the reader thread, from ReplInput: takes a line for the running turn.
|
|
@@ -994,6 +998,7 @@ module Samagotchi
|
|
|
994
998
|
# A command, never steering text (/archive waits, back in the prompt).
|
|
995
999
|
return command_during_turn(line) if local || command_registry.command?(line)
|
|
996
1000
|
return true if line.strip.empty?
|
|
1001
|
+
|
|
997
1002
|
# A typo of a command (/modle) is not steering text: say so, take it.
|
|
998
1003
|
if (hint = command_registry.unknown_command_hint(line))
|
|
999
1004
|
@surface.commit(hint)
|
|
@@ -1092,7 +1097,6 @@ module Samagotchi
|
|
|
1092
1097
|
ConfigFile.recap_config
|
|
1093
1098
|
end
|
|
1094
1099
|
|
|
1095
|
-
|
|
1096
1100
|
# Reset the Engine's shared inactivity clock when a prompt opens (a
|
|
1097
1101
|
# Reline.pre_input_hook) and on each key typed (RelineSeam.key_handler). This is the
|
|
1098
1102
|
# single seam the Engine-owned idle recap detector reads, so the idle clock
|
data/lib/samagotchi/text_diff.rb
CHANGED
|
@@ -46,8 +46,8 @@ module Samagotchi
|
|
|
46
46
|
a_mid = a[pre...(a.size - suf)]
|
|
47
47
|
b_mid = b[pre...(b.size - suf)]
|
|
48
48
|
middle = myers(a_mid, b_mid) ||
|
|
49
|
-
a_mid.each_index.map { |i| [:del, i, nil] } + b_mid.each_index.map { |j| [:add, nil, j] }
|
|
50
|
-
middle.each { |t, i, j| ops << [t, i && i + pre, j && j + pre] }
|
|
49
|
+
(a_mid.each_index.map { |i| [:del, i, nil] } + b_mid.each_index.map { |j| [:add, nil, j] })
|
|
50
|
+
middle.each { |t, i, j| ops << [t, i && (i + pre), j && (j + pre)] }
|
|
51
51
|
(0...suf).each { |s| ops << [:eq, a.size - suf + s, b.size - suf + s] }
|
|
52
52
|
ops
|
|
53
53
|
end
|
|
@@ -64,13 +64,13 @@ module Samagotchi
|
|
|
64
64
|
n = a.size
|
|
65
65
|
m = b.size
|
|
66
66
|
off = n + m + 1
|
|
67
|
-
v = Array.new(2 * off + 1, 0)
|
|
67
|
+
v = Array.new((2 * off) + 1, 0)
|
|
68
68
|
trace = []
|
|
69
69
|
(0..(n + m)).each do |d|
|
|
70
70
|
return nil if d > max_d
|
|
71
71
|
|
|
72
72
|
# Round d reads k-1 and k+1 for k in -d..d: keep -(d+1)..d+1.
|
|
73
|
-
trace << v[off - d - 1, 2 * d + 3]
|
|
73
|
+
trace << v[off - d - 1, (2 * d) + 3]
|
|
74
74
|
k = -d
|
|
75
75
|
while k <= d
|
|
76
76
|
x = if k == -d || (k != d && v[off + k - 1] < v[off + k + 1])
|
|
@@ -98,8 +98,8 @@ module Samagotchi
|
|
|
98
98
|
vd = trace[d]
|
|
99
99
|
at = ->(k) { vd[k + d + 1] }
|
|
100
100
|
k = x - y
|
|
101
|
-
prev_k = k == -d || (k != d && at.(k - 1) < at.(k + 1)) ? k + 1 : k - 1
|
|
102
|
-
prev_x = at.(prev_k)
|
|
101
|
+
prev_k = k == -d || (k != d && at.call(k - 1) < at.call(k + 1)) ? k + 1 : k - 1
|
|
102
|
+
prev_x = at.call(prev_k)
|
|
103
103
|
prev_y = prev_x - prev_k
|
|
104
104
|
while x > prev_x && y > prev_y
|
|
105
105
|
x -= 1
|
|
@@ -108,11 +108,11 @@ module Samagotchi
|
|
|
108
108
|
end
|
|
109
109
|
break if d.zero?
|
|
110
110
|
|
|
111
|
-
if x == prev_x
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
ops << if x == prev_x
|
|
112
|
+
[:add, nil, prev_y]
|
|
113
|
+
else
|
|
114
|
+
[:del, prev_x, nil]
|
|
115
|
+
end
|
|
116
116
|
x = prev_x
|
|
117
117
|
y = prev_y
|
|
118
118
|
end
|
|
@@ -144,7 +144,7 @@ module Samagotchi
|
|
|
144
144
|
line = t == :add ? b[j] : a[i]
|
|
145
145
|
mark = { eq: " ", del: "-", add: "+" }[t]
|
|
146
146
|
# Only the "\n": chomp("\n") would take a CRLF's "\r" too.
|
|
147
|
-
lines << mark + (line.end_with?("\n") ? line[0...-1] : line)
|
|
147
|
+
lines << (mark + (line.end_with?("\n") ? line[0...-1] : line))
|
|
148
148
|
lines << NO_NEWLINE unless line.end_with?("\n")
|
|
149
149
|
end
|
|
150
150
|
end
|
data/lib/samagotchi/thinking.rb
CHANGED
|
@@ -23,6 +23,10 @@ module Samagotchi
|
|
|
23
23
|
# An empty thought after the Qwen assistant cue: the model answers at once
|
|
24
24
|
# (what the server's own template does with enable_thinking false).
|
|
25
25
|
QWEN_EMPTY_THOUGHT = "<think>\n\n</think>\n\n"
|
|
26
|
+
# Gemma 4's empty thought channel after the model cue, as its chat
|
|
27
|
+
# template writes it with enable_thinking false (only where a model turn
|
|
28
|
+
# starts: Prompt.prefill_for).
|
|
29
|
+
GEMMA_EMPTY_THOUGHT = "<|channel>thought\n<channel|>"
|
|
26
30
|
|
|
27
31
|
# What a native prompt gets for a level: the text before the system
|
|
28
32
|
# prompt, the text after the assistant cue, and whether the level means
|
|
@@ -43,7 +47,7 @@ module Samagotchi
|
|
|
43
47
|
return text.to_sym if LEVELS.include?(text.to_sym)
|
|
44
48
|
|
|
45
49
|
hint = value == true || %w[true on].include?(text) ? "; `on` isn't one, `default` leaves it to the model" : ""
|
|
46
|
-
ConfigFile.warn_once "Warning: #{where}: thinking must be one of #{LEVELS.join(
|
|
50
|
+
ConfigFile.warn_once "Warning: #{where}: thinking must be one of #{LEVELS.join(", ")}#{hint}; ignored"
|
|
47
51
|
nil
|
|
48
52
|
end
|
|
49
53
|
|
|
@@ -116,7 +120,7 @@ module Samagotchi
|
|
|
116
120
|
off = level == :off
|
|
117
121
|
honoured = !EFFORTS.include?(level)
|
|
118
122
|
case profile&.name
|
|
119
|
-
when "gemma4" then Native.new(system_token: off ? "" : GEMMA_THINK_TOKEN, prefill: "", honoured: honoured)
|
|
123
|
+
when "gemma4" then Native.new(system_token: off ? "" : GEMMA_THINK_TOKEN, prefill: off ? GEMMA_EMPTY_THOUGHT : "", honoured: honoured)
|
|
120
124
|
when "qwen36" then Native.new(system_token: "", prefill: off ? QWEN_EMPTY_THOUGHT : "", honoured: honoured)
|
|
121
125
|
else Native.new(system_token: "", prefill: "", honoured: level == DEFAULT)
|
|
122
126
|
end
|
|
@@ -74,7 +74,7 @@ module Samagotchi
|
|
|
74
74
|
end
|
|
75
75
|
body = @carry[0...close_pos]
|
|
76
76
|
route_body(body, thinking)
|
|
77
|
-
@carry = @carry[close_pos + @active[:close].length..].to_s
|
|
77
|
+
@carry = @carry[(close_pos + @active[:close].length)..].to_s
|
|
78
78
|
@active = nil
|
|
79
79
|
@state = :normal
|
|
80
80
|
end
|
|
@@ -108,6 +108,7 @@ module Samagotchi
|
|
|
108
108
|
@blocks.each do |block|
|
|
109
109
|
pos = @carry.index(block[:open])
|
|
110
110
|
next if pos.nil?
|
|
111
|
+
|
|
111
112
|
best = { pos: pos, block: block } if best.nil? || pos < best[:pos]
|
|
112
113
|
end
|
|
113
114
|
best
|
|
@@ -118,7 +119,7 @@ module Samagotchi
|
|
|
118
119
|
def flush_normal(text)
|
|
119
120
|
partial = @blocks.map { |block| longest_suffix_prefix_of(@carry, block[:open]) }.max.to_i
|
|
120
121
|
text << @carry[0...(@carry.length - partial)]
|
|
121
|
-
@carry = @carry[@carry.length - partial..].to_s
|
|
122
|
+
@carry = @carry[(@carry.length - partial)..].to_s
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
# In-block state: emit the body seen so far to the active lane, retaining a
|
|
@@ -127,12 +128,13 @@ module Samagotchi
|
|
|
127
128
|
close = @active[:close]
|
|
128
129
|
partial = longest_suffix_prefix_of(@carry, close)
|
|
129
130
|
route_body(@carry[0...(@carry.length - partial)], thinking)
|
|
130
|
-
@carry = @carry[@carry.length - partial..].to_s
|
|
131
|
+
@carry = @carry[(@carry.length - partial)..].to_s
|
|
131
132
|
end
|
|
132
133
|
|
|
133
134
|
# Route a body fragment to the active block's lane (:thinking or :drop).
|
|
134
135
|
def route_body(body, thinking)
|
|
135
136
|
return if body.nil? || body.empty?
|
|
137
|
+
|
|
136
138
|
thinking << body if @active && @active[:lane] == :thinking
|
|
137
139
|
end
|
|
138
140
|
|
|
@@ -141,6 +143,7 @@ module Samagotchi
|
|
|
141
143
|
def longest_suffix_prefix_of(str, marker)
|
|
142
144
|
mlen = marker.length
|
|
143
145
|
return 0 if mlen.zero? || str.empty?
|
|
146
|
+
|
|
144
147
|
[str.length, mlen].min.downto(1) do |n|
|
|
145
148
|
return n if str[-n..] == marker[0, n]
|
|
146
149
|
end
|
|
@@ -25,7 +25,7 @@ module Samagotchi
|
|
|
25
25
|
result = text.to_s
|
|
26
26
|
while (open_pos = result.index(open))
|
|
27
27
|
close_pos = result.index(close, open_pos + open.length)
|
|
28
|
-
result = result[0...open_pos] + (close_pos ? result[close_pos + close.length..] : "")
|
|
28
|
+
result = result[0...open_pos] + (close_pos ? result[(close_pos + close.length)..] : "")
|
|
29
29
|
end
|
|
30
30
|
result.strip
|
|
31
31
|
end
|
|
@@ -37,7 +37,7 @@ module Samagotchi
|
|
|
37
37
|
class Gemma
|
|
38
38
|
# Digits after the first character: plugin tools may have them
|
|
39
39
|
# (Plugin::Api::TOOL_NAME).
|
|
40
|
-
TOOL_CALL_BODY_RE
|
|
40
|
+
TOOL_CALL_BODY_RE = /\Acall:([a-z_][a-z0-9_]{0,49})\{/
|
|
41
41
|
THOUGHT_CHANNEL_OPEN = ModelProfile::GEMMA_THOUGHT_CHANNEL_OPEN
|
|
42
42
|
THOUGHT_CHANNEL_CLOSE = ModelProfile::GEMMA_THOUGHT_CHANNEL_CLOSE
|
|
43
43
|
CONTROL_TOKEN_START = "<|"
|
|
@@ -102,7 +102,7 @@ module Samagotchi
|
|
|
102
102
|
while (open_pos = result.index(THOUGHT_CHANNEL_OPEN))
|
|
103
103
|
close_pos = result.index(THOUGHT_CHANNEL_CLOSE, open_pos)
|
|
104
104
|
result = if close_pos
|
|
105
|
-
result[0...open_pos] + result[close_pos + THOUGHT_CHANNEL_CLOSE.length..]
|
|
105
|
+
result[0...open_pos] + result[(close_pos + THOUGHT_CHANNEL_CLOSE.length)..]
|
|
106
106
|
else
|
|
107
107
|
result[0...open_pos]
|
|
108
108
|
end
|
|
@@ -128,11 +128,10 @@ module Samagotchi
|
|
|
128
128
|
pos = start
|
|
129
129
|
while (p = text.index(CONTROL_TOKEN_START, pos))
|
|
130
130
|
# Skip over a <|"|> token entirely
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
end
|
|
131
|
+
return p unless text[p, @string_delim.length] == @string_delim
|
|
132
|
+
|
|
133
|
+
pos = p + @string_delim.length
|
|
134
|
+
|
|
136
135
|
end
|
|
137
136
|
nil
|
|
138
137
|
end
|
|
@@ -213,7 +212,7 @@ module Samagotchi
|
|
|
213
212
|
e = params_raw.index(@string_delim, s + @string_delim.length)
|
|
214
213
|
break unless e
|
|
215
214
|
|
|
216
|
-
arr << params_raw[s + @string_delim.length...e]
|
|
215
|
+
arr << params_raw[(s + @string_delim.length)...e]
|
|
217
216
|
pos = e + @string_delim.length
|
|
218
217
|
end
|
|
219
218
|
unless arr.empty?
|
|
@@ -334,13 +333,13 @@ module Samagotchi
|
|
|
334
333
|
|
|
335
334
|
def strip_thought(text)
|
|
336
335
|
# Remove all complete <think>...</think> blocks with the newlines after them
|
|
337
|
-
result = text.gsub(
|
|
336
|
+
result = text.gsub(%r{<think>.*?</think>\n*}m, "")
|
|
338
337
|
# Remove any stray opening tags
|
|
339
|
-
result = result.gsub(/<think>.*?(?=\n|$)/m,
|
|
338
|
+
result = result.gsub(/<think>.*?(?=\n|$)/m, "")
|
|
340
339
|
# Remove any orphaned closing tags (and the whitespace around them)
|
|
341
340
|
# The answer's own blank lines stay: they are its markdown paragraphs
|
|
342
341
|
# and lists (a chat host's answer is saved as stripped here).
|
|
343
|
-
result.gsub(
|
|
342
|
+
result.gsub(%r{^\s*</think>\s*}m, "")
|
|
344
343
|
end
|
|
345
344
|
|
|
346
345
|
def strip_tool_calls(text)
|
|
@@ -365,11 +364,11 @@ module Samagotchi
|
|
|
365
364
|
def qwen_params(body)
|
|
366
365
|
params = {}
|
|
367
366
|
|
|
368
|
-
body.scan(
|
|
367
|
+
body.scan(%r{<parameter=(\w+)>(.*?)</parameter>}m) do |key, value|
|
|
369
368
|
params[key.to_s.downcase] = trim_tag_newline(value)
|
|
370
369
|
end
|
|
371
370
|
|
|
372
|
-
body.scan(
|
|
371
|
+
body.scan(%r{<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>}m) do |key, value|
|
|
373
372
|
params[key.to_s.downcase.strip] = trim_tag_newline(value)
|
|
374
373
|
end
|
|
375
374
|
|