samagotchi 0.22.0 → 0.24.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 +72 -1
- data/bin/chi +9 -4
- data/docs/cli.md +6 -1
- data/docs/configuration.md +13 -4
- data/docs/hooks.md +20 -6
- data/docs/plugins.md +1 -1
- data/docs/releasing.md +1 -1
- data/docs/sub-agent.md +4 -3
- 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/card_store.rb +4 -3
- data/lib/samagotchi/bridge/sse_writer.rb +0 -6
- data/lib/samagotchi/bridge/turn_accumulator.rb +6 -3
- data/lib/samagotchi/bridge/turn_notice.rb +1 -0
- data/lib/samagotchi/bridge.rb +19 -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 +4 -1
- 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 +42 -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 +97 -18
- data/lib/samagotchi/gem_update.rb +2 -1
- data/lib/samagotchi/generation_phase.rb +83 -0
- 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 +113 -5
- data/lib/samagotchi/guardrails/shell_paths.rb +3 -3
- 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 +22 -17
- data/lib/samagotchi/idle_reminders.rb +6 -3
- data/lib/samagotchi/image_store.rb +5 -2
- data/lib/samagotchi/kernel_loop.rb +50 -40
- data/lib/samagotchi/llm/chat_loop.rb +29 -16
- data/lib/samagotchi/llm/errors.rb +2 -2
- data/lib/samagotchi/llm/http.rb +12 -12
- data/lib/samagotchi/llm/openai_chat.rb +42 -28
- data/lib/samagotchi/llm/prompt_cache.rb +1 -1
- data/lib/samagotchi/log.rb +2 -2
- data/lib/samagotchi/log_subscriber.rb +5 -3
- 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 +72 -13
- 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/steer.rb +103 -8
- 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 +3 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +4 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +19 -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/one_shot_sink.rb +1 -0
- 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 +14 -5
- data/lib/samagotchi/text_diff.rb +12 -12
- data/lib/samagotchi/thinking.rb +6 -2
- data/lib/samagotchi/thought_stream_splitter.rb +15 -6
- 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/tool_runner.rb +7 -0
- data/lib/samagotchi/tool_view.rb +40 -0
- 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/waiting_steer.rb +64 -0
- data/lib/samagotchi/web/app.rb +13 -6
- data/lib/samagotchi/web/markdown_renderer.rb +2 -1
- data/lib/samagotchi/web/message_parts.rb +5 -0
- data/lib/samagotchi/web/public/activity.js +5 -0
- data/lib/samagotchi/web/public/app.js +2 -1
- data/lib/samagotchi/web/public/command_view.js +28 -0
- data/lib/samagotchi/web/public/copy.js +7 -0
- data/lib/samagotchi/web/public/index.html +8 -0
- data/lib/samagotchi/web/public/stage_model.js +11 -4
- data/lib/samagotchi/web/public/stage_view.js +5 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -1
- data/lib/samagotchi/web/public/turn_view.js +15 -6
- data/lib/samagotchi/web/qr.rb +7 -1
- data/lib/samagotchi/web/server.rb +6 -2
- data/lib/samagotchi/worker.rb +14 -3
- metadata +24 -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.
|
|
@@ -45,6 +45,15 @@ module Samagotchi
|
|
|
45
45
|
paint("↻ #{what}, asking again (#{event[:attempt]}/#{event[:of]})", 90)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# "↪ cut in for your message": a message for the running turn cut a
|
|
49
|
+
# generation that was only thinking (Engine#cut_for_steer); an empty
|
|
50
|
+
# or unknown source is the user's.
|
|
51
|
+
STEER_CUT_FOR = { "chi_send" => "a message sent with chi send", "parent_agent" => "the parent agent's message" }.freeze
|
|
52
|
+
|
|
53
|
+
def format_steer_cut_line(event)
|
|
54
|
+
paint("↪ cut in for #{STEER_CUT_FOR.fetch(event[:source].to_s, "your message")}", 90)
|
|
55
|
+
end
|
|
56
|
+
|
|
48
57
|
# "no answer: the model returned nothing (after 1 retry)", dim like the
|
|
49
58
|
# retry row: a turn that ended with no answer (TurnNote.empty_answer_line).
|
|
50
59
|
def format_empty_answer_line(retries)
|
|
@@ -75,7 +84,7 @@ module Samagotchi
|
|
|
75
84
|
return "" unless diff.is_a?(Hash)
|
|
76
85
|
|
|
77
86
|
get = ->(key) { diff[key] || diff[key.to_s] }
|
|
78
|
-
" #{paint("+#{get.(:added).to_i}", 32)} #{paint("\u2212#{get.(:removed).to_i}", 31)}"
|
|
87
|
+
" #{paint("+#{get.call(:added).to_i}", 32)} #{paint("\u2212#{get.call(:removed).to_i}", 31)}"
|
|
79
88
|
end
|
|
80
89
|
|
|
81
90
|
# Prompt labels by the sender's client_id prefix (turn_events.js
|
|
@@ -113,7 +122,7 @@ module Samagotchi
|
|
|
113
122
|
params = part[:params].to_s.strip
|
|
114
123
|
params_suffix = params.empty? ? "" : " #{paint(params, 90)}"
|
|
115
124
|
status = part[:status].to_s
|
|
116
|
-
"#{paint(
|
|
125
|
+
"#{paint("tool>", 36)} #{part[:tool]}#{params_suffix}: #{paint(status, status_color(status))}" \
|
|
117
126
|
"#{format_tool_image_suffix(part[:images])}#{format_tool_diff_suffix(part[:diff])}"
|
|
118
127
|
end
|
|
119
128
|
|
|
@@ -233,8 +242,8 @@ module Samagotchi
|
|
|
233
242
|
value = tps.to_f
|
|
234
243
|
return "" unless value.positive?
|
|
235
244
|
|
|
236
|
-
number = value >= 1000 ? "#{format(
|
|
237
|
-
"#{
|
|
245
|
+
number = value >= 1000 ? "#{format("%.1f", (value / 100).round / 10.0)}k" : value.round.to_s
|
|
246
|
+
"#{"~" if source.to_s == "estimate"}#{number} tok/s"
|
|
238
247
|
end
|
|
239
248
|
|
|
240
249
|
# "$0.42"; a cost under a cent keeps four decimals ("$0.0012"); "" for
|
|
@@ -297,7 +306,7 @@ module Samagotchi
|
|
|
297
306
|
def status_rows(segments, width)
|
|
298
307
|
return [] if segments.empty? || width <= 0
|
|
299
308
|
|
|
300
|
-
row = cap_preview_text("status> #{segments.join(
|
|
309
|
+
row = cap_preview_text("status> #{segments.join(" | ")}", width)
|
|
301
310
|
[color_output? ? paint(row, 90) : row]
|
|
302
311
|
end
|
|
303
312
|
|
|
@@ -336,7 +345,7 @@ module Samagotchi
|
|
|
336
345
|
|
|
337
346
|
visible = names.first(limit)
|
|
338
347
|
suffix = names.length > visible.length ? ", +#{names.length - visible.length}" : ""
|
|
339
|
-
"#{label}: #{visible.join(
|
|
348
|
+
"#{label}: #{visible.join(", ")}#{suffix}"
|
|
340
349
|
end
|
|
341
350
|
|
|
342
351
|
def cap_preview_text(text, width)
|
|
@@ -411,8 +420,8 @@ module Samagotchi
|
|
|
411
420
|
return nil unless last
|
|
412
421
|
|
|
413
422
|
source = tokens[:tps_source].to_s
|
|
414
|
-
text =
|
|
415
|
-
text << ", #{speed_text(tokens[:last_prefill_tps],
|
|
423
|
+
text = "#{speed_text(last, source)} out"
|
|
424
|
+
text << ", #{speed_text(tokens[:last_prefill_tps], "server")} prompt" if tokens[:last_prefill_tps]
|
|
416
425
|
text << " (last, #{source})"
|
|
417
426
|
text << ", avg #{speed_text(tokens[:avg_decode_tps], source)}" if tokens[:avg_decode_tps]
|
|
418
427
|
text
|
|
@@ -428,7 +437,7 @@ module Samagotchi
|
|
|
428
437
|
return "#{seconds.round}s" if seconds < 60
|
|
429
438
|
|
|
430
439
|
total_seconds = seconds.round
|
|
431
|
-
"#{total_seconds / 60}m #{format(
|
|
440
|
+
"#{total_seconds / 60}m #{format("%02d", total_seconds % 60)}s"
|
|
432
441
|
end
|
|
433
442
|
|
|
434
443
|
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
|
|
@@ -29,6 +29,7 @@ module Samagotchi
|
|
|
29
29
|
def call(event)
|
|
30
30
|
case event[:type]
|
|
31
31
|
when :empty_answer_retry then line(format_empty_retry_line(event))
|
|
32
|
+
when :steer_cut then line(format_steer_cut_line(event))
|
|
32
33
|
when :generation_retrying then line(format_generation_retry_line(event))
|
|
33
34
|
when :turn_completed, :turn_canceled then @turn_over = true
|
|
34
35
|
when :hook_notice
|
|
@@ -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)
|
|
@@ -1002,7 +1007,12 @@ module Samagotchi
|
|
|
1002
1007
|
# Steering merges text only: a line with images runs as the next turn.
|
|
1003
1008
|
return image_line_waits if ImageInput.extract(line).any?
|
|
1004
1009
|
|
|
1010
|
+
epoch = @engine.input_epoch
|
|
1005
1011
|
@pending_input_queue.push(line.strip)
|
|
1012
|
+
# The user's line cuts a generation that has streamed only thinking
|
|
1013
|
+
# for long (steer.cut_after), now or once it has: the model starts the
|
|
1014
|
+
# step again with it.
|
|
1015
|
+
@engine.cut_for_steer(nil, epoch: epoch)
|
|
1006
1016
|
true
|
|
1007
1017
|
end
|
|
1008
1018
|
|
|
@@ -1092,7 +1102,6 @@ module Samagotchi
|
|
|
1092
1102
|
ConfigFile.recap_config
|
|
1093
1103
|
end
|
|
1094
1104
|
|
|
1095
|
-
|
|
1096
1105
|
# Reset the Engine's shared inactivity clock when a prompt opens (a
|
|
1097
1106
|
# Reline.pre_input_hook) and on each key typed (RelineSeam.key_handler). This is the
|
|
1098
1107
|
# 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
|
|
@@ -46,12 +46,16 @@ module Samagotchi
|
|
|
46
46
|
@active = nil
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# Feed one raw chunk. Returns the newly-routed deltas for this chunk
|
|
50
|
-
#
|
|
49
|
+
# Feed one raw chunk. Returns the newly-routed deltas for this chunk
|
|
50
|
+
# only, and +tool+: whether the chunk opened, streamed into or closed a
|
|
51
|
+
# tool_call block (its bytes are dropped, but a steer must not cut a
|
|
52
|
+
# generation that writes a tool call: Engine#cut_for_steer).
|
|
53
|
+
# @return [Hash{ text: String, thinking: String, tool: Boolean }]
|
|
51
54
|
def feed(chunk)
|
|
52
55
|
@carry << chunk.to_s
|
|
53
56
|
text = +""
|
|
54
57
|
thinking = +""
|
|
58
|
+
tool = false
|
|
55
59
|
loop do
|
|
56
60
|
if @state == :normal
|
|
57
61
|
hit = earliest_open
|
|
@@ -64,9 +68,11 @@ module Samagotchi
|
|
|
64
68
|
@carry = @carry[hit[:pos]..]
|
|
65
69
|
@active = hit[:block]
|
|
66
70
|
@state = :in_block
|
|
71
|
+
tool ||= @active[:lane] == :drop
|
|
67
72
|
# Drop the open marker itself; its body follows.
|
|
68
73
|
@carry = @carry[@active[:open].length..].to_s
|
|
69
74
|
else
|
|
75
|
+
tool ||= @active[:lane] == :drop && !@carry.empty?
|
|
70
76
|
close_pos = @carry.index(@active[:close])
|
|
71
77
|
if close_pos.nil?
|
|
72
78
|
flush_in_block(thinking)
|
|
@@ -74,12 +80,12 @@ module Samagotchi
|
|
|
74
80
|
end
|
|
75
81
|
body = @carry[0...close_pos]
|
|
76
82
|
route_body(body, thinking)
|
|
77
|
-
@carry = @carry[close_pos + @active[:close].length..].to_s
|
|
83
|
+
@carry = @carry[(close_pos + @active[:close].length)..].to_s
|
|
78
84
|
@active = nil
|
|
79
85
|
@state = :normal
|
|
80
86
|
end
|
|
81
87
|
end
|
|
82
|
-
{ text: text, thinking: thinking }
|
|
88
|
+
{ text: text, thinking: thinking, tool: tool }
|
|
83
89
|
end
|
|
84
90
|
|
|
85
91
|
# Flush any residual state at end of stream (e.g. an unterminated thinking
|
|
@@ -108,6 +114,7 @@ module Samagotchi
|
|
|
108
114
|
@blocks.each do |block|
|
|
109
115
|
pos = @carry.index(block[:open])
|
|
110
116
|
next if pos.nil?
|
|
117
|
+
|
|
111
118
|
best = { pos: pos, block: block } if best.nil? || pos < best[:pos]
|
|
112
119
|
end
|
|
113
120
|
best
|
|
@@ -118,7 +125,7 @@ module Samagotchi
|
|
|
118
125
|
def flush_normal(text)
|
|
119
126
|
partial = @blocks.map { |block| longest_suffix_prefix_of(@carry, block[:open]) }.max.to_i
|
|
120
127
|
text << @carry[0...(@carry.length - partial)]
|
|
121
|
-
@carry = @carry[@carry.length - partial..].to_s
|
|
128
|
+
@carry = @carry[(@carry.length - partial)..].to_s
|
|
122
129
|
end
|
|
123
130
|
|
|
124
131
|
# In-block state: emit the body seen so far to the active lane, retaining a
|
|
@@ -127,12 +134,13 @@ module Samagotchi
|
|
|
127
134
|
close = @active[:close]
|
|
128
135
|
partial = longest_suffix_prefix_of(@carry, close)
|
|
129
136
|
route_body(@carry[0...(@carry.length - partial)], thinking)
|
|
130
|
-
@carry = @carry[@carry.length - partial..].to_s
|
|
137
|
+
@carry = @carry[(@carry.length - partial)..].to_s
|
|
131
138
|
end
|
|
132
139
|
|
|
133
140
|
# Route a body fragment to the active block's lane (:thinking or :drop).
|
|
134
141
|
def route_body(body, thinking)
|
|
135
142
|
return if body.nil? || body.empty?
|
|
143
|
+
|
|
136
144
|
thinking << body if @active && @active[:lane] == :thinking
|
|
137
145
|
end
|
|
138
146
|
|
|
@@ -141,6 +149,7 @@ module Samagotchi
|
|
|
141
149
|
def longest_suffix_prefix_of(str, marker)
|
|
142
150
|
mlen = marker.length
|
|
143
151
|
return 0 if mlen.zero? || str.empty?
|
|
152
|
+
|
|
144
153
|
[str.length, mlen].min.downto(1) do |n|
|
|
145
154
|
return n if str[-n..] == marker[0, n]
|
|
146
155
|
end
|