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
|
@@ -65,13 +65,13 @@ module Samagotchi
|
|
|
65
65
|
next if basename.empty?
|
|
66
66
|
|
|
67
67
|
meta = ->(key) { raw_meta && (raw_meta[key] || raw_meta[key.to_s]) }
|
|
68
|
-
event = meta.(:event).to_s
|
|
68
|
+
event = meta.call(:event).to_s
|
|
69
69
|
# A hook with no declared event cannot be auto-registered.
|
|
70
70
|
next if event.empty?
|
|
71
71
|
|
|
72
72
|
event_sym = event.to_sym
|
|
73
|
-
on_error = (meta.(:on_error) || "skip").to_s
|
|
74
|
-
priority = (meta.(:priority) || 100).to_i
|
|
73
|
+
on_error = (meta.call(:on_error) || "skip").to_s
|
|
74
|
+
priority = (meta.call(:priority) || 100).to_i
|
|
75
75
|
fail_closed = on_error == "fail_closed" && event_sym == :before_tool_call
|
|
76
76
|
what = "hook #{basename} (bundle #{bundle_name})"
|
|
77
77
|
file = hooks_dir && File.join(hooks_dir, basename)
|
|
@@ -79,7 +79,7 @@ module Samagotchi
|
|
|
79
79
|
failures&.add(what, "the file is missing", required: true) if fail_closed
|
|
80
80
|
next
|
|
81
81
|
end
|
|
82
|
-
if (mismatch = too_old || sha_mismatch(file, meta.(:sha256), required: fail_closed))
|
|
82
|
+
if (mismatch = too_old || sha_mismatch(file, meta.call(:sha256), required: fail_closed))
|
|
83
83
|
Log.warn(:hooks, "bundle_hook_mismatch", echo: "[samagotchi:hooks] bundle '#{bundle_name}' hook '#{basename}' not loaded: #{mismatch}", bundle: bundle_name, hook: basename.to_s)
|
|
84
84
|
failures&.add(what, mismatch, required: fail_closed)
|
|
85
85
|
next
|
|
@@ -91,12 +91,14 @@ module Samagotchi
|
|
|
91
91
|
elsif on_error == "log" then :log
|
|
92
92
|
else :skip
|
|
93
93
|
end
|
|
94
|
-
label = "#{
|
|
94
|
+
label = "#{"guardrail " if fail_closed}#{basename} (bundle #{bundle_name})"
|
|
95
95
|
handler = Hooks.wrap(label: label, event: event_sym, policy: policy, log: [:hooks, "bundle_hook_failed"],
|
|
96
96
|
fields: { bundle: bundle_name }) { |event| plugin.call(event) }
|
|
97
97
|
registry.register_bundle(bundle_name, event_sym, hook_name: basename, priority: priority, &handler)
|
|
98
98
|
loaded += 1
|
|
99
|
-
rescue Exception => e
|
|
99
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
|
|
100
|
+
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
101
|
+
|
|
100
102
|
Log.error(:hooks, "bundle_hook_load_failed", echo: "[samagotchi:hooks] bundle '#{bundle_name}' hook '#{basename}' failed to load: #{e.class}: #{e.message}", bundle: bundle_name, hook: basename.to_s, error: e.class.name)
|
|
101
103
|
failures&.add(what, "#{e.class}: #{e.message}", required: fail_closed)
|
|
102
104
|
end
|
|
@@ -127,6 +129,7 @@ module Samagotchi
|
|
|
127
129
|
klass = Hooks.class_for(basename, ns)
|
|
128
130
|
instance = Hooks.build_plugin(klass, settings)
|
|
129
131
|
raise ArgumentError, "plugin #{klass.name.split("::").last} does not respond to #call" unless instance.respond_to?(:call)
|
|
132
|
+
|
|
130
133
|
instance
|
|
131
134
|
end
|
|
132
135
|
|
|
@@ -67,17 +67,15 @@ module Samagotchi
|
|
|
67
67
|
definitions = parse_definitions(hooks_config)
|
|
68
68
|
|
|
69
69
|
definitions.each do |defn|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
failures&.add("hook #{defn[:path]} (config)", "#{e.class}: #{e.message}", required: defn[:required])
|
|
80
|
-
end
|
|
70
|
+
plugin = load_plugin(hooks_dir, defn[:path], settings: defn[:settings])
|
|
71
|
+
# Persistent: config hooks must fire on every turn, not be wiped
|
|
72
|
+
# by Engine#run_turn's per-turn clear_hooks after turn 1.
|
|
73
|
+
registry.register_persistent(defn[:event_type].to_sym, label: "#{defn[:path]} (config)",
|
|
74
|
+
&wrapped(plugin, defn))
|
|
75
|
+
rescue ScriptError, StandardError => e
|
|
76
|
+
# ScriptError: a SyntaxError (or LoadError) from `require`.
|
|
77
|
+
Log.error(:hooks, "hook_load_failed", echo: "[samagotchi:hooks] hook #{defn[:path]} failed to load: #{e.class}: #{e.message}", hook: defn[:path].to_s, error: e.class.name)
|
|
78
|
+
failures&.add("hook #{defn[:path]} (config)", "#{e.class}: #{e.message}", required: defn[:required])
|
|
81
79
|
end
|
|
82
80
|
|
|
83
81
|
registry
|
|
@@ -87,6 +85,7 @@ module Samagotchi
|
|
|
87
85
|
# Expand a path that may start with ~.
|
|
88
86
|
def self.expand_path(path, env = ENV)
|
|
89
87
|
return path unless path.start_with?("~")
|
|
88
|
+
|
|
90
89
|
home = env["HOME"] || Dir.home
|
|
91
90
|
File.join(home, path[1..])
|
|
92
91
|
end
|
|
@@ -97,8 +96,10 @@ module Samagotchi
|
|
|
97
96
|
definitions = []
|
|
98
97
|
hooks_config.each do |event_type, configs|
|
|
99
98
|
next unless event_type.to_s != "hooks_dir" && configs.is_a?(Array)
|
|
99
|
+
|
|
100
100
|
configs.each do |cfg|
|
|
101
101
|
next unless cfg.is_a?(Hash) && cfg["path"]
|
|
102
|
+
|
|
102
103
|
definitions << {
|
|
103
104
|
event_type: event_type.to_s,
|
|
104
105
|
path: cfg["path"],
|
|
@@ -123,9 +124,7 @@ module Samagotchi
|
|
|
123
124
|
|
|
124
125
|
# Check if already loaded (Ruby's require caching handles this)
|
|
125
126
|
# We cache the instance separately to avoid re-instantiating
|
|
126
|
-
|
|
127
|
-
@plugin_cache = {}
|
|
128
|
-
end
|
|
127
|
+
@plugin_cache ||= {}
|
|
129
128
|
|
|
130
129
|
@plugin_cache[[full_path, settings]] ||= begin
|
|
131
130
|
require full_path
|
|
@@ -133,6 +132,7 @@ module Samagotchi
|
|
|
133
132
|
instance = Hooks.build_plugin(klass, settings)
|
|
134
133
|
# Validate that the instance responds to #call
|
|
135
134
|
raise ArgumentError, "Plugin #{klass.name} does not respond to #call" unless instance.respond_to?(:call)
|
|
135
|
+
|
|
136
136
|
instance
|
|
137
137
|
end
|
|
138
138
|
end
|
|
@@ -142,9 +142,11 @@ module Samagotchi
|
|
|
142
142
|
# on_error: log warns and skip is silent.
|
|
143
143
|
def self.wrapped(plugin, defn)
|
|
144
144
|
if defn[:required] && defn[:event_type] == "before_tool_call"
|
|
145
|
-
policy
|
|
145
|
+
policy = :deny
|
|
146
|
+
label = "required hook #{defn[:path]}"
|
|
146
147
|
else
|
|
147
|
-
policy
|
|
148
|
+
policy = (defn[:on_error] == "log" ? :log : :skip)
|
|
149
|
+
label = "#{defn[:path]} (config)"
|
|
148
150
|
end
|
|
149
151
|
Hooks.wrap(label: label, event: defn[:event_type].to_sym, policy: policy) { |event| plugin.call(event) }
|
|
150
152
|
end
|
|
@@ -13,7 +13,11 @@ module Samagotchi
|
|
|
13
13
|
# @param settings [Hash]
|
|
14
14
|
def self.build_plugin(klass, settings)
|
|
15
15
|
takes_settings = klass.instance_method(:initialize).arity != 0
|
|
16
|
-
takes_settings
|
|
16
|
+
if takes_settings
|
|
17
|
+
klass.new(settings.is_a?(Hash) ? settings : {})
|
|
18
|
+
else
|
|
19
|
+
klass.new
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
# A stream hook (:generation_progress) fires ~once a second: a broken
|
|
@@ -62,7 +66,7 @@ module Samagotchi
|
|
|
62
66
|
def self.wrap(label:, event:, policy:, log: [:hooks, "hook_failed"], echo: true, fields: {}, &block)
|
|
63
67
|
log_failure = failure_log_gate(event)
|
|
64
68
|
lambda do |payload|
|
|
65
|
-
|
|
69
|
+
yield(payload)
|
|
66
70
|
rescue Exception => e # rubocop:disable Lint/RescueException -- :deny fails closed on anything
|
|
67
71
|
raise unless policy == :deny || e.is_a?(StandardError) || e.is_a?(ScriptError)
|
|
68
72
|
|
|
@@ -130,6 +134,7 @@ module Samagotchi
|
|
|
130
134
|
def register(name, &block)
|
|
131
135
|
raise ArgumentError, "hook name must be a Symbol" unless name.is_a?(Symbol)
|
|
132
136
|
raise ArgumentError, "hook block is required" unless block_given?
|
|
137
|
+
|
|
133
138
|
@mutex.synchronize { (@hooks[name] ||= []) << block }
|
|
134
139
|
end
|
|
135
140
|
|
|
@@ -144,6 +149,7 @@ module Samagotchi
|
|
|
144
149
|
def register_persistent(name, label: nil, &block)
|
|
145
150
|
raise ArgumentError, "hook name must be a Symbol" unless name.is_a?(Symbol)
|
|
146
151
|
raise ArgumentError, "hook block is required" unless block_given?
|
|
152
|
+
|
|
147
153
|
label = label.to_s.strip
|
|
148
154
|
label = CONFIG_HOOK_LABEL if label.empty?
|
|
149
155
|
@mutex.synchronize { (@persistent_hooks[name] ||= []) << { label: label, proc: block } }
|
|
@@ -160,6 +166,7 @@ module Samagotchi
|
|
|
160
166
|
# @return [void]
|
|
161
167
|
def register_bundle(bundle_name, event_name, hook_name:, priority: 100, &block)
|
|
162
168
|
raise ArgumentError, "hook block is required" unless block_given?
|
|
169
|
+
|
|
163
170
|
@mutex.synchronize do
|
|
164
171
|
(@bundle_hooks[event_name] ||= []) << {
|
|
165
172
|
bundle: bundle_name,
|
|
@@ -180,6 +187,7 @@ module Samagotchi
|
|
|
180
187
|
if event_name
|
|
181
188
|
arr = @bundle_hooks[event_name]
|
|
182
189
|
return 0 unless arr
|
|
190
|
+
|
|
183
191
|
before = arr.size
|
|
184
192
|
@bundle_hooks[event_name] = arr.reject { |h| h[:bundle] == bundle_name }
|
|
185
193
|
removed = before - @bundle_hooks[event_name].size
|
|
@@ -118,9 +118,7 @@ module Samagotchi
|
|
|
118
118
|
@model_index = nil # downcased model_id => [host_name] in hosts: order
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
@entries
|
|
123
|
-
end
|
|
121
|
+
attr_reader :entries
|
|
124
122
|
|
|
125
123
|
# Loopback, private (RFC 1918, IPv6 unique local) and link-local nets.
|
|
126
124
|
LOCAL_NETS = %w[127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 100.64.0.0/10
|
|
@@ -46,7 +46,7 @@ module Samagotchi
|
|
|
46
46
|
module TranscriptFilter
|
|
47
47
|
# A tool call as the model wrote it inline: gemma, qwen, or the qwen
|
|
48
48
|
# prompt-literal form. An unterminated gemma call runs to the end.
|
|
49
|
-
TOOL_CALL_RE =
|
|
49
|
+
TOOL_CALL_RE = %r{<\|tool_call>(?:.*?<tool_call\|>|.*\z)|<tool_call>.*?</tool_call>|\[\[SAMAGOTCHI_LITERAL_TOOL_CALL_OPEN\]\].*?\[\[SAMAGOTCHI_LITERAL_TOOL_CALL_CLOSE\]\]}m
|
|
50
50
|
# Each dispatched call's output starts with "[name]"; the kernel loop
|
|
51
51
|
# joins one step's outputs into a single tool_response with this
|
|
52
52
|
# separator, while the chat loop writes one message per call.
|
|
@@ -215,7 +215,7 @@ module Samagotchi
|
|
|
215
215
|
system = format(SYSTEM, range) + (previous ? UPDATE + format(UPDATE_LENGTH, range) : "") + tools
|
|
216
216
|
user = +""
|
|
217
217
|
user << "Earlier recap:\n#{previous}\n\n" if previous
|
|
218
|
-
user << "Transcript#{
|
|
218
|
+
user << "Transcript#{" since the earlier recap" if previous} (the system prompt and tool " \
|
|
219
219
|
"internals were removed; only user turns and assistant prose remain):\n---\n#{body}\n---\n"
|
|
220
220
|
user << "#{OFFER_LINE}\n" if offer
|
|
221
221
|
user << "Write the recap now."
|
|
@@ -225,7 +225,7 @@ module Samagotchi
|
|
|
225
225
|
# "2-4 sentences", or "3 sentences" for [3, 3] ("1 sentence" for [1, 1])
|
|
226
226
|
def sentences_text((min, max), adjective = "")
|
|
227
227
|
count = min == max ? min.to_s : "#{min}-#{max}"
|
|
228
|
-
"#{count} #{adjective}#{max == 1 ?
|
|
228
|
+
"#{count} #{adjective}#{max == 1 ? "sentence" : "sentences"}"
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
# The tail of +body+ when it is over MAX_NEW_CHARS, from a paragraph
|
|
@@ -243,7 +243,7 @@ module Samagotchi
|
|
|
243
243
|
def tools_used(names)
|
|
244
244
|
return "" if names.empty?
|
|
245
245
|
|
|
246
|
-
" (#{names.tally.map { |name, n| n > 1 ? "#{name} x#{n}" : name }.join(
|
|
246
|
+
" (#{names.tally.map { |name, n| n > 1 ? "#{name} x#{n}" : name }.join(", ")})"
|
|
247
247
|
end
|
|
248
248
|
end
|
|
249
249
|
|
|
@@ -395,6 +395,17 @@ module Samagotchi
|
|
|
395
395
|
@engine.activity_seq > @last_fire_activity_seq
|
|
396
396
|
end
|
|
397
397
|
|
|
398
|
+
# SHA1 of one message's role and text: detects a rewrite a count alone
|
|
399
|
+
# misses. Model text is taken without its thinking, which is dropped from
|
|
400
|
+
# older model messages when the next turn starts.
|
|
401
|
+
def self.digest(message)
|
|
402
|
+
return nil unless message.is_a?(Hash)
|
|
403
|
+
|
|
404
|
+
text = message["content"].to_s
|
|
405
|
+
text = TranscriptFilter.strip_thought(text) if %w[model assistant].include?(message["role"])
|
|
406
|
+
Digest::SHA1.hexdigest("#{message["role"]}\0#{text}")
|
|
407
|
+
end
|
|
408
|
+
|
|
398
409
|
private
|
|
399
410
|
|
|
400
411
|
# A failing check writes the recap without the offer line.
|
|
@@ -431,9 +442,11 @@ module Samagotchi
|
|
|
431
442
|
# Nothing new said (only notes, tool traffic, or no messages at all):
|
|
432
443
|
# the recap still stands, so no request.
|
|
433
444
|
return :nothing_new if transcript.strip.empty?
|
|
445
|
+
|
|
434
446
|
prompt = RecapPrompt.build(transcript, tool_names: TranscriptFilter.tool_names(fresh), previous: previous&.dig(:text),
|
|
435
447
|
sentences: @sentences, offer: offer_open?)
|
|
436
448
|
return :nothing_new if prompt.nil?
|
|
449
|
+
|
|
437
450
|
asked = target
|
|
438
451
|
@in_flight = { thread: spawn_summarize(client_for(asked), prompt), generation: gen, deadline: @clock.call + @timeout,
|
|
439
452
|
covered: parsed.size, covered_digest: self.class.digest(parsed.last), model: asked[:label] }
|
|
@@ -459,6 +472,7 @@ module Samagotchi
|
|
|
459
472
|
@in_flight = nil
|
|
460
473
|
recap = safe_value(job[:thread])
|
|
461
474
|
return if recap.nil? || recap.to_s.strip.empty?
|
|
475
|
+
|
|
462
476
|
# An IdleClient::Summary names the model that answered; a plain String
|
|
463
477
|
# (a stubbed client) doesn't, and the asked label stands in.
|
|
464
478
|
served = recap.model if recap.respond_to?(:model)
|
|
@@ -503,17 +517,6 @@ module Samagotchi
|
|
|
503
517
|
saved
|
|
504
518
|
end
|
|
505
519
|
|
|
506
|
-
# SHA1 of one message's role and text: detects a rewrite a count alone
|
|
507
|
-
# misses. Model text is taken without its thinking, which is dropped from
|
|
508
|
-
# older model messages when the next turn starts.
|
|
509
|
-
def self.digest(message)
|
|
510
|
-
return nil unless message.is_a?(Hash)
|
|
511
|
-
|
|
512
|
-
text = message["content"].to_s
|
|
513
|
-
text = TranscriptFilter.strip_thought(text) if %w[model assistant].include?(message["role"])
|
|
514
|
-
Digest::SHA1.hexdigest("#{message['role']}\0#{text}")
|
|
515
|
-
end
|
|
516
|
-
|
|
517
520
|
def bump_generation
|
|
518
521
|
@mutex.synchronize { @generation += 1 }
|
|
519
522
|
end
|
|
@@ -46,6 +46,7 @@ module Samagotchi
|
|
|
46
46
|
def should_check?
|
|
47
47
|
return false if @engine.turn_running?
|
|
48
48
|
return false unless last_idle_seconds >= @inactivity
|
|
49
|
+
|
|
49
50
|
true
|
|
50
51
|
end
|
|
51
52
|
|
|
@@ -57,15 +58,17 @@ module Samagotchi
|
|
|
57
58
|
|
|
58
59
|
def check_due_reminders
|
|
59
60
|
return unless @queue
|
|
61
|
+
|
|
60
62
|
due_names = @queue.due.map { |r| r[:name] }
|
|
61
63
|
return if due_names.empty?
|
|
64
|
+
|
|
62
65
|
# Signal the engine to create a synthetic turn via callback.
|
|
63
66
|
# The callback is responsible for triggering a turn (e.g. SessionManager
|
|
64
67
|
# writes a file, TerminalUI queues input). The engine's run_turn or
|
|
65
68
|
# REPL injection point then picks up and delivers the reminders.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
return unless @auto_turn_callback
|
|
70
|
+
|
|
71
|
+
@auto_turn_callback.call(due_names)
|
|
69
72
|
end
|
|
70
73
|
end
|
|
71
74
|
end
|
|
@@ -100,7 +100,8 @@ module Samagotchi
|
|
|
100
100
|
# Downscales and converts images with whatever tool the machine has:
|
|
101
101
|
# macOS sips first, then ImageMagick. `none` when neither is installed.
|
|
102
102
|
class ImageResizer
|
|
103
|
-
Error
|
|
103
|
+
class Error < StandardError
|
|
104
|
+
end
|
|
104
105
|
|
|
105
106
|
attr_reader :tool
|
|
106
107
|
|
|
@@ -191,7 +192,8 @@ module Samagotchi
|
|
|
191
192
|
# Messages hold small refs to those files; the base64 is built only when
|
|
192
193
|
# a request is sent.
|
|
193
194
|
module ImageStore
|
|
194
|
-
Error
|
|
195
|
+
class Error < StandardError
|
|
196
|
+
end
|
|
195
197
|
|
|
196
198
|
DIR = "images"
|
|
197
199
|
REF_RE = %r{\Aimages/[0-9a-f]{16}\.(png|jpe?g|gif|webp)\z}
|
|
@@ -234,6 +236,7 @@ module Samagotchi
|
|
|
234
236
|
|
|
235
237
|
name = (name || (path && File.basename(path)) || "image").to_s
|
|
236
238
|
raise Error, "#{name} is too large (over 50 MB)" if bytes.bytesize > MAX_SOURCE_BYTES
|
|
239
|
+
|
|
237
240
|
info = ImageHeader.read(bytes)
|
|
238
241
|
raise Error, "#{name} is not an image chi can send (png, jpeg, gif, webp)" unless info
|
|
239
242
|
|
|
@@ -134,8 +134,7 @@ module Samagotchi
|
|
|
134
134
|
# @return [Tools::Registry] the tools #dispatch runs. The Engine sets
|
|
135
135
|
# its own on a kernel it is given, as with hooks.
|
|
136
136
|
attr_accessor :tools
|
|
137
|
-
attr_accessor :client
|
|
138
|
-
attr_accessor :model_key
|
|
137
|
+
attr_accessor :client, :model_key
|
|
139
138
|
# @return [String, nil] the key whose overlay memory_read takes when
|
|
140
139
|
# #model_key has none (#sync_model_key!)
|
|
141
140
|
attr_reader :model_key_fallback
|
|
@@ -210,6 +209,18 @@ module Samagotchi
|
|
|
210
209
|
@model_key_fallback = fallback
|
|
211
210
|
end
|
|
212
211
|
|
|
212
|
+
# Resolve the per-output character cap for the emitted tool call events.
|
|
213
|
+
#
|
|
214
|
+
# Precedence: an explicit override wins, then max_tool_output_chars
|
|
215
|
+
# (Config). A non-positive value falls back to
|
|
216
|
+
# DEFAULT_MAX_TOOL_OUTPUT_CHARS (there is intentionally no "unlimited" — live UIs get a
|
|
217
|
+
# bounded `output:` plus a truthful `output_truncated:` flag).
|
|
218
|
+
# Class-level so the chat loop resolves it the same way.
|
|
219
|
+
def self.resolve_output_char_cap(override)
|
|
220
|
+
parsed = (override || Samagotchi::Config.get("max_tool_output_chars")).to_i
|
|
221
|
+
parsed.positive? ? parsed : DEFAULT_MAX_TOOL_OUTPUT_CHARS
|
|
222
|
+
end
|
|
223
|
+
|
|
213
224
|
private
|
|
214
225
|
|
|
215
226
|
# ── One turn (#run) ────────────────────────────────────────────────────────
|
|
@@ -223,7 +234,7 @@ module Samagotchi
|
|
|
223
234
|
:cancel_controller, :cap, :emit, :iteration, :empty_steps, :ended_empty, keyword_init: true)
|
|
224
235
|
# One request: the prompt and its images as sent, the images' token
|
|
225
236
|
# estimate, and the window it was measured against.
|
|
226
|
-
Request = Struct.new(:prompt, :images, :image_tokens, :window, keyword_init: true)
|
|
237
|
+
Request = Struct.new(:prompt, :images, :image_tokens, :window, :prefill, keyword_init: true)
|
|
227
238
|
# One generation: the response (nil when cut), the cut's detail, this
|
|
228
239
|
# generation's own server counts, the model the server named, the
|
|
229
240
|
# thinking it streamed, and why it stopped (the transport's finish
|
|
@@ -254,7 +265,7 @@ module Samagotchi
|
|
|
254
265
|
generation = generate(turn, request)
|
|
255
266
|
return after_cut(turn, generation) if generation.cut
|
|
256
267
|
|
|
257
|
-
turn.conversation << { role: "model", content:
|
|
268
|
+
turn.conversation << { role: "model", content: request.prefill + generation.response.to_s }
|
|
258
269
|
# Profile-specific parse (incl. Qwen unterminated-block recovery); the
|
|
259
270
|
# returned fragment (non-nil only for Qwen) is fed back on the next
|
|
260
271
|
# iteration if the model opened a tool-call block it did not close.
|
|
@@ -279,7 +290,10 @@ module Samagotchi
|
|
|
279
290
|
turn.conversation << line
|
|
280
291
|
prompt, images = format_prompt(turn)
|
|
281
292
|
end
|
|
282
|
-
|
|
293
|
+
# The prefill the prompt ended with (none where Gemma's turn goes on
|
|
294
|
+
# after a tool response): the model message keeps it.
|
|
295
|
+
prefill = Prompt.prefill_for(turn.conversation, @profile, turn.prefill)
|
|
296
|
+
Request.new(prompt: prompt, images: images, image_tokens: image_tokens, window: window, prefill: prefill)
|
|
283
297
|
end
|
|
284
298
|
|
|
285
299
|
def format_prompt(turn)
|
|
@@ -379,7 +393,7 @@ module Samagotchi
|
|
|
379
393
|
# after the cut is a plain cancel. Returns :next or the turn's result.
|
|
380
394
|
def after_cut(turn, generation)
|
|
381
395
|
cut = generation.cut
|
|
382
|
-
raise Client::RequestCancelled
|
|
396
|
+
raise Client::RequestCancelled, turn.cancel_controller.reason if turn.cancel_controller.cancelled?
|
|
383
397
|
|
|
384
398
|
if turn.empty_retry.left?
|
|
385
399
|
return :next if inject_pending_input!(turn)
|
|
@@ -493,7 +507,6 @@ module Samagotchi
|
|
|
493
507
|
nil
|
|
494
508
|
end
|
|
495
509
|
|
|
496
|
-
|
|
497
510
|
# ── Hook dispatch helper ───────────────────────────────────────────────────
|
|
498
511
|
|
|
499
512
|
# Fire a named hook on the registry (if present).
|
|
@@ -505,6 +518,7 @@ module Samagotchi
|
|
|
505
518
|
|
|
506
519
|
def fire_hook(name, event)
|
|
507
520
|
return unless @hooks
|
|
521
|
+
|
|
508
522
|
@hooks.fire(name, event)
|
|
509
523
|
rescue StandardError
|
|
510
524
|
# A failing hook must not break the turn.
|
|
@@ -517,34 +531,22 @@ module Samagotchi
|
|
|
517
531
|
|
|
518
532
|
# ── Output char cap resolution ─────────────────────────────────────────────
|
|
519
533
|
|
|
520
|
-
# Resolve the per-output character cap for the emitted tool call events.
|
|
521
|
-
#
|
|
522
|
-
# Precedence: an explicit override wins, then max_tool_output_chars
|
|
523
|
-
# (Config). A non-positive value falls back to
|
|
524
|
-
# DEFAULT_MAX_TOOL_OUTPUT_CHARS (there is intentionally no "unlimited" — live UIs get a
|
|
525
|
-
# bounded `output:` plus a truthful `output_truncated:` flag).
|
|
526
|
-
# Class-level so the chat loop resolves it the same way.
|
|
527
|
-
def self.resolve_output_char_cap(override)
|
|
528
|
-
parsed = (override || Samagotchi::Config.get("max_tool_output_chars")).to_i
|
|
529
|
-
parsed.positive? ? parsed : DEFAULT_MAX_TOOL_OUTPUT_CHARS
|
|
530
|
-
end
|
|
531
|
-
|
|
532
534
|
def resolve_output_char_cap(override)
|
|
533
535
|
self.class.resolve_output_char_cap(override)
|
|
534
536
|
end
|
|
535
537
|
|
|
536
538
|
# One request, cancelled by +generation_controller+ (the turn's child)
|
|
537
539
|
# when there is one.
|
|
538
|
-
def request_generation(prompt, generation_controller:, cancel_controller:, **
|
|
539
|
-
@client.complete(prompt, **complete_kwargs(cancel_controller: generation_controller || cancel_controller, **
|
|
540
|
+
def request_generation(prompt, generation_controller:, cancel_controller:, **)
|
|
541
|
+
@client.complete(prompt, **complete_kwargs(cancel_controller: generation_controller || cancel_controller, **))
|
|
540
542
|
end
|
|
541
543
|
|
|
542
544
|
# Yields the turn controller's child for one generation (nil without a
|
|
543
545
|
# controller).
|
|
544
|
-
def with_generation(cancel_controller, &
|
|
546
|
+
def with_generation(cancel_controller, &)
|
|
545
547
|
return yield(nil) unless cancel_controller
|
|
546
548
|
|
|
547
|
-
cancel_controller.generation(&
|
|
549
|
+
cancel_controller.generation(&)
|
|
548
550
|
end
|
|
549
551
|
|
|
550
552
|
# The generation was cut (stop_generation) and the turn goes on.
|
|
@@ -561,19 +563,14 @@ module Samagotchi
|
|
|
561
563
|
kwargs[:on_retry] = on_retry if on_retry
|
|
562
564
|
kwargs[:cancel_controller] = cancel_controller if cancel_controller
|
|
563
565
|
kwargs[:stop] = @profile.stop_sequences
|
|
564
|
-
|
|
565
|
-
kwargs[:n_predict] =
|
|
566
|
+
max_tokens = Samagotchi::Config.max_tokens
|
|
567
|
+
kwargs[:n_predict] = max_tokens if max_tokens
|
|
566
568
|
resolved_model_name = completion_model_name(model_name)
|
|
567
569
|
kwargs[:model] = resolved_model_name if resolved_model_name
|
|
568
570
|
kwargs[:sampling] = sampling if sampling && !sampling.empty?
|
|
569
571
|
kwargs
|
|
570
572
|
end
|
|
571
573
|
|
|
572
|
-
def completion_n_predict
|
|
573
|
-
value = Samagotchi::Config.get("default.n_predict").to_i
|
|
574
|
-
value if value.positive?
|
|
575
|
-
end
|
|
576
|
-
|
|
577
574
|
def completion_model_name(override = nil)
|
|
578
575
|
ModelProfile.required_model_name(override)
|
|
579
576
|
end
|
|
@@ -635,6 +632,7 @@ module Samagotchi
|
|
|
635
632
|
@parser_profile = @profile
|
|
636
633
|
@parser
|
|
637
634
|
end
|
|
635
|
+
|
|
638
636
|
private
|
|
639
637
|
|
|
640
638
|
# The generation's thinking: what the stream split into the thinking
|
|
@@ -682,6 +680,7 @@ module Samagotchi
|
|
|
682
680
|
end
|
|
683
681
|
|
|
684
682
|
public
|
|
683
|
+
|
|
685
684
|
# Public entry point for executing an ALREADY-NORMALIZED internal tool call
|
|
686
685
|
# (the {name:, content:, path:, scope:, …} shape).
|
|
687
686
|
#
|
|
@@ -696,6 +695,7 @@ module Samagotchi
|
|
|
696
695
|
end
|
|
697
696
|
|
|
698
697
|
private
|
|
698
|
+
|
|
699
699
|
def dispatch(call)
|
|
700
700
|
entry = @tools[call[:name]]
|
|
701
701
|
unless entry
|
|
@@ -723,7 +723,7 @@ module Samagotchi
|
|
|
723
723
|
dispatched[:image_only] = true if result.respond_to?(:image_only?) && result.image_only?
|
|
724
724
|
end
|
|
725
725
|
dispatched
|
|
726
|
-
rescue => e
|
|
726
|
+
rescue StandardError => e
|
|
727
727
|
dump_log("tool_error", e.message, tool: call[:name], error: e.class.name)
|
|
728
728
|
result = "Error: #{e.message}"
|
|
729
729
|
{
|
|
@@ -155,12 +155,15 @@ module Samagotchi
|
|
|
155
155
|
(@thinking_refused ||= Set.new) << model
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
-
# One generation's options:
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
158
|
+
# One generation's options: default.max_tokens as max_tokens (the
|
|
159
|
+
# adapter's own default, OpenRouter's, otherwise), the thinking fields
|
|
160
|
+
# under the sampling (a sampling key wins, chat_template_kwargs merges
|
|
161
|
+
# per sub-key), the empty-answer retry's temperature on top, then every
|
|
162
|
+
# null dropped at any depth (a sampling null means "don't send it").
|
|
162
163
|
def request_options(retry_generation: false, model: nil)
|
|
164
|
+
max_tokens = Samagotchi::Config.max_tokens
|
|
163
165
|
options = deep_merge(thinking_fields(model), sampling)
|
|
166
|
+
options = { max_tokens: max_tokens }.merge(options) if max_tokens
|
|
164
167
|
options = EmptyAnswerRetry.sampling(options) if retry_generation
|
|
165
168
|
deep_compact(options)
|
|
166
169
|
end
|
|
@@ -449,10 +452,10 @@ module Samagotchi
|
|
|
449
452
|
|
|
450
453
|
# The request runs under the generation's own controller (the turn
|
|
451
454
|
# controller's child), so a plugin's stop_generation cuts it alone.
|
|
452
|
-
def with_generation(&
|
|
455
|
+
def with_generation(&)
|
|
453
456
|
return yield(nil) unless @cancel_controller
|
|
454
457
|
|
|
455
|
-
@cancel_controller.generation(&
|
|
458
|
+
@cancel_controller.generation(&)
|
|
456
459
|
end
|
|
457
460
|
|
|
458
461
|
# The empty response of a cut generation: its spinners close as any
|
|
@@ -125,12 +125,12 @@ module Samagotchi
|
|
|
125
125
|
|
|
126
126
|
# @param hint [String, nil] what to try, added to the summary
|
|
127
127
|
def initialize(message = nil, context_overflow: false, tools_unsupported: false, reasoning_refused: false, hint: nil,
|
|
128
|
-
**
|
|
128
|
+
**)
|
|
129
129
|
@context_overflow = context_overflow
|
|
130
130
|
@tools_unsupported = tools_unsupported
|
|
131
131
|
@reasoning_refused = reasoning_refused
|
|
132
132
|
@hint = hint
|
|
133
|
-
super(message, **
|
|
133
|
+
super(message, **)
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
attr_reader :hint
|
data/lib/samagotchi/llm/http.rb
CHANGED
|
@@ -138,7 +138,7 @@ module Samagotchi
|
|
|
138
138
|
end
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
def stream_attempt(uri, request, current, &
|
|
141
|
+
def stream_attempt(uri, request, current, &)
|
|
142
142
|
# Time to first token is the answering attempt's, not the retries'.
|
|
143
143
|
current[:attempt_started_at] = monotonic_now
|
|
144
144
|
shown = lambda do
|
|
@@ -155,11 +155,11 @@ module Samagotchi
|
|
|
155
155
|
response.read_body do |chunk|
|
|
156
156
|
buffer << chunk
|
|
157
157
|
while (newline_index = buffer.index("\n"))
|
|
158
|
-
|
|
158
|
+
yield(buffer.slice!(0, newline_index + 1).strip, shown)
|
|
159
159
|
end
|
|
160
160
|
end
|
|
161
161
|
# A body that doesn't end in a newline still has a last line.
|
|
162
|
-
|
|
162
|
+
yield(buffer.strip, shown) unless buffer.strip.empty?
|
|
163
163
|
end
|
|
164
164
|
end
|
|
165
165
|
end
|
|
@@ -192,7 +192,7 @@ module Samagotchi
|
|
|
192
192
|
cancellable(cancel_controller, current) do
|
|
193
193
|
attempt.call(current.merge!(attempts: 1))
|
|
194
194
|
rescue StandardError => e
|
|
195
|
-
raise RequestCancelled
|
|
195
|
+
raise RequestCancelled, cancel_controller.reason if !e.is_a?(RequestCancelled) && cancel_controller&.cancelled?
|
|
196
196
|
|
|
197
197
|
raise
|
|
198
198
|
end
|
|
@@ -208,7 +208,7 @@ module Samagotchi
|
|
|
208
208
|
@api_key&.authorize(request)
|
|
209
209
|
end
|
|
210
210
|
|
|
211
|
-
def start(uri, open_timeout: nil, read_timeout: nil, max_retries: nil, &
|
|
211
|
+
def start(uri, open_timeout: nil, read_timeout: nil, max_retries: nil, &)
|
|
212
212
|
options = { open_timeout: open_timeout || @open_timeout, read_timeout: read_timeout || @read_timeout }
|
|
213
213
|
# Net::HTTP retries an idempotent request once on its own (max_retries
|
|
214
214
|
# defaults to 1) when the connection drops before an answer: chi's
|
|
@@ -218,15 +218,15 @@ module Samagotchi
|
|
|
218
218
|
retries = max_retries || 0
|
|
219
219
|
options[:max_retries] = retries
|
|
220
220
|
options[:use_ssl] = true if uri.scheme == "https"
|
|
221
|
-
Net::HTTP.start(uri.host, uri.port, **options, &
|
|
221
|
+
Net::HTTP.start(uri.host, uri.port, **options, &)
|
|
222
222
|
end
|
|
223
223
|
|
|
224
224
|
# Runs the block (one attempt) until it returns, retrying network
|
|
225
225
|
# errors. The block gets a hash to put the attempt's Net::HTTP in, so a
|
|
226
226
|
# cancel can reach its socket.
|
|
227
|
-
def with_retries(cancel_controller, on_retry, on_network_error, current = { mutex: Mutex.new })
|
|
227
|
+
def with_retries(cancel_controller, on_retry, on_network_error, current = { mutex: Mutex.new }, &block)
|
|
228
228
|
cancellable(cancel_controller, current) do
|
|
229
|
-
retrying(cancel_controller, on_retry, on_network_error, current
|
|
229
|
+
retrying(cancel_controller, on_retry, on_network_error, current, &block)
|
|
230
230
|
end
|
|
231
231
|
end
|
|
232
232
|
|
|
@@ -236,7 +236,7 @@ module Samagotchi
|
|
|
236
236
|
def cancellable(cancel_controller, current)
|
|
237
237
|
requesting_thread = Thread.current
|
|
238
238
|
listener_id = cancel_controller&.on_cancel { |reason| abort_request(current, requesting_thread, reason) }
|
|
239
|
-
raise RequestCancelled
|
|
239
|
+
raise RequestCancelled, cancel_controller.reason if cancel_controller&.cancelled?
|
|
240
240
|
|
|
241
241
|
yield
|
|
242
242
|
ensure
|
|
@@ -257,7 +257,7 @@ module Samagotchi
|
|
|
257
257
|
rescue RequestCancelled
|
|
258
258
|
raise
|
|
259
259
|
rescue ProviderError => e
|
|
260
|
-
raise RequestCancelled
|
|
260
|
+
raise RequestCancelled, cancel_controller.reason if cancel_controller&.cancelled?
|
|
261
261
|
|
|
262
262
|
e.attempts = attempts
|
|
263
263
|
raise if !e.retryable? || current[:streamed]
|
|
@@ -267,7 +267,7 @@ module Samagotchi
|
|
|
267
267
|
|
|
268
268
|
retry_after(e, attempts, delay, on_retry, cancel_controller, current)
|
|
269
269
|
rescue StandardError => e
|
|
270
|
-
raise RequestCancelled
|
|
270
|
+
raise RequestCancelled, cancel_controller.reason if cancel_controller&.cancelled?
|
|
271
271
|
raise first_token_timeout if current[:first_token_expired]
|
|
272
272
|
raise unless self.class.network_error?(e)
|
|
273
273
|
|
|
@@ -432,7 +432,7 @@ module Samagotchi
|
|
|
432
432
|
|
|
433
433
|
remaining = seconds
|
|
434
434
|
while remaining.positive?
|
|
435
|
-
raise RequestCancelled
|
|
435
|
+
raise RequestCancelled, cancel_controller.reason if cancel_controller.cancelled?
|
|
436
436
|
|
|
437
437
|
slice = [remaining, WAIT_TICK].min
|
|
438
438
|
@sleeper.call(slice)
|