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
|
@@ -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)
|
|
@@ -341,8 +355,12 @@ module Samagotchi
|
|
|
341
355
|
generation.streamed_thinking += split[:thinking].to_s.length
|
|
342
356
|
return unless turn.on_stream_event
|
|
343
357
|
|
|
344
|
-
|
|
345
|
-
|
|
358
|
+
event = { type: :generation_chunk, iteration: turn.iteration, content: chunk[:content], text: split[:text],
|
|
359
|
+
thinking: split[:thinking], payload: chunk[:payload] }
|
|
360
|
+
# A chunk of a tool call (its bytes are dropped from both lanes): a
|
|
361
|
+
# steer doesn't cut this generation (Engine#cut_for_steer).
|
|
362
|
+
event[:tool_call] = true if split[:tool]
|
|
363
|
+
emit(turn, **event)
|
|
346
364
|
end
|
|
347
365
|
|
|
348
366
|
# The cut stream's visible text goes with it: the buffer as it was
|
|
@@ -372,18 +390,24 @@ module Samagotchi
|
|
|
372
390
|
messages: AnswerDisplay.strip_all(turn.conversation).map(&:dup).freeze })
|
|
373
391
|
end
|
|
374
392
|
|
|
375
|
-
# A generation a plugin cut is an empty answer made early
|
|
376
|
-
#
|
|
377
|
-
#
|
|
378
|
-
#
|
|
379
|
-
#
|
|
393
|
+
# A generation a plugin cut is an empty answer made early. Queued input
|
|
394
|
+
# (a user's line, a plugin's steer) goes in first, with or without a
|
|
395
|
+
# retry left, and spends no attempt: the model answers it. A steer's cut
|
|
396
|
+
# (Engine#cut_for_steer) with nothing queued (its message was taken at
|
|
397
|
+
# a boundary already) is asked again as is. Else it is asked again with
|
|
398
|
+
# its own nudge while the retry budget lasts, else the turn ends as
|
|
399
|
+
# cancelled (hook), with nothing salvaged and without the spent nudge.
|
|
400
|
+
# A Stop that came right after the cut is a plain cancel. Returns :next
|
|
401
|
+
# or the turn's result.
|
|
380
402
|
def after_cut(turn, generation)
|
|
381
403
|
cut = generation.cut
|
|
382
|
-
raise Client::RequestCancelled
|
|
404
|
+
raise Client::RequestCancelled, turn.cancel_controller.reason if turn.cancel_controller.cancelled?
|
|
383
405
|
|
|
384
|
-
|
|
385
|
-
|
|
406
|
+
# The row under the cut step, above the message it was cut for.
|
|
407
|
+
emit(turn, type: :steer_cut, iteration: turn.iteration, source: cut[:source].to_s) if cut[:steer]
|
|
408
|
+
return :next if inject_pending_input!(turn) || cut[:steer]
|
|
386
409
|
|
|
410
|
+
if turn.empty_retry.left?
|
|
387
411
|
turn.empty_retry.nudge!(turn.conversation, TurnNote.cut_retry(cut[:by], cut[:reason]),
|
|
388
412
|
emit: turn.emit, iteration: turn.iteration, finish_reason: "stopped",
|
|
389
413
|
thinking_chars: generation.streamed_thinking, stopped_by: cut[:by])
|
|
@@ -493,7 +517,6 @@ module Samagotchi
|
|
|
493
517
|
nil
|
|
494
518
|
end
|
|
495
519
|
|
|
496
|
-
|
|
497
520
|
# ── Hook dispatch helper ───────────────────────────────────────────────────
|
|
498
521
|
|
|
499
522
|
# Fire a named hook on the registry (if present).
|
|
@@ -505,6 +528,7 @@ module Samagotchi
|
|
|
505
528
|
|
|
506
529
|
def fire_hook(name, event)
|
|
507
530
|
return unless @hooks
|
|
531
|
+
|
|
508
532
|
@hooks.fire(name, event)
|
|
509
533
|
rescue StandardError
|
|
510
534
|
# A failing hook must not break the turn.
|
|
@@ -517,34 +541,22 @@ module Samagotchi
|
|
|
517
541
|
|
|
518
542
|
# ── Output char cap resolution ─────────────────────────────────────────────
|
|
519
543
|
|
|
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
544
|
def resolve_output_char_cap(override)
|
|
533
545
|
self.class.resolve_output_char_cap(override)
|
|
534
546
|
end
|
|
535
547
|
|
|
536
548
|
# One request, cancelled by +generation_controller+ (the turn's child)
|
|
537
549
|
# 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, **
|
|
550
|
+
def request_generation(prompt, generation_controller:, cancel_controller:, **)
|
|
551
|
+
@client.complete(prompt, **complete_kwargs(cancel_controller: generation_controller || cancel_controller, **))
|
|
540
552
|
end
|
|
541
553
|
|
|
542
554
|
# Yields the turn controller's child for one generation (nil without a
|
|
543
555
|
# controller).
|
|
544
|
-
def with_generation(cancel_controller, &
|
|
556
|
+
def with_generation(cancel_controller, &)
|
|
545
557
|
return yield(nil) unless cancel_controller
|
|
546
558
|
|
|
547
|
-
cancel_controller.generation(&
|
|
559
|
+
cancel_controller.generation(&)
|
|
548
560
|
end
|
|
549
561
|
|
|
550
562
|
# The generation was cut (stop_generation) and the turn goes on.
|
|
@@ -561,19 +573,14 @@ module Samagotchi
|
|
|
561
573
|
kwargs[:on_retry] = on_retry if on_retry
|
|
562
574
|
kwargs[:cancel_controller] = cancel_controller if cancel_controller
|
|
563
575
|
kwargs[:stop] = @profile.stop_sequences
|
|
564
|
-
|
|
565
|
-
kwargs[:n_predict] =
|
|
576
|
+
max_tokens = Samagotchi::Config.max_tokens
|
|
577
|
+
kwargs[:n_predict] = max_tokens if max_tokens
|
|
566
578
|
resolved_model_name = completion_model_name(model_name)
|
|
567
579
|
kwargs[:model] = resolved_model_name if resolved_model_name
|
|
568
580
|
kwargs[:sampling] = sampling if sampling && !sampling.empty?
|
|
569
581
|
kwargs
|
|
570
582
|
end
|
|
571
583
|
|
|
572
|
-
def completion_n_predict
|
|
573
|
-
value = Samagotchi::Config.get("default.n_predict").to_i
|
|
574
|
-
value if value.positive?
|
|
575
|
-
end
|
|
576
|
-
|
|
577
584
|
def completion_model_name(override = nil)
|
|
578
585
|
ModelProfile.required_model_name(override)
|
|
579
586
|
end
|
|
@@ -635,6 +642,7 @@ module Samagotchi
|
|
|
635
642
|
@parser_profile = @profile
|
|
636
643
|
@parser
|
|
637
644
|
end
|
|
645
|
+
|
|
638
646
|
private
|
|
639
647
|
|
|
640
648
|
# The generation's thinking: what the stream split into the thinking
|
|
@@ -682,6 +690,7 @@ module Samagotchi
|
|
|
682
690
|
end
|
|
683
691
|
|
|
684
692
|
public
|
|
693
|
+
|
|
685
694
|
# Public entry point for executing an ALREADY-NORMALIZED internal tool call
|
|
686
695
|
# (the {name:, content:, path:, scope:, …} shape).
|
|
687
696
|
#
|
|
@@ -696,6 +705,7 @@ module Samagotchi
|
|
|
696
705
|
end
|
|
697
706
|
|
|
698
707
|
private
|
|
708
|
+
|
|
699
709
|
def dispatch(call)
|
|
700
710
|
entry = @tools[call[:name]]
|
|
701
711
|
unless entry
|
|
@@ -723,7 +733,7 @@ module Samagotchi
|
|
|
723
733
|
dispatched[:image_only] = true if result.respond_to?(:image_only?) && result.image_only?
|
|
724
734
|
end
|
|
725
735
|
dispatched
|
|
726
|
-
rescue => e
|
|
736
|
+
rescue StandardError => e
|
|
727
737
|
dump_log("tool_error", e.message, tool: call[:name], error: e.class.name)
|
|
728
738
|
result = "Error: #{e.message}"
|
|
729
739
|
{
|
|
@@ -107,7 +107,9 @@ module Samagotchi
|
|
|
107
107
|
when "system"
|
|
108
108
|
wire << { role: "system", content: entry[:content].to_s }
|
|
109
109
|
when "user"
|
|
110
|
-
|
|
110
|
+
# A steer or merged input (always a String) goes with its header.
|
|
111
|
+
content = Steer.header(entry) ? Steer.wire_text(entry, entry[:content].to_s) : entry[:content]
|
|
112
|
+
wire << { role: "user", content: parts(content, items) }
|
|
111
113
|
when "model"
|
|
112
114
|
wire << assistant_message(entry, paired)
|
|
113
115
|
when "tool_response"
|
|
@@ -155,12 +157,15 @@ module Samagotchi
|
|
|
155
157
|
(@thinking_refused ||= Set.new) << model
|
|
156
158
|
end
|
|
157
159
|
|
|
158
|
-
# One generation's options:
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
160
|
+
# One generation's options: default.max_tokens as max_tokens (the
|
|
161
|
+
# adapter's own default, OpenRouter's, otherwise), the thinking fields
|
|
162
|
+
# under the sampling (a sampling key wins, chat_template_kwargs merges
|
|
163
|
+
# per sub-key), the empty-answer retry's temperature on top, then every
|
|
164
|
+
# null dropped at any depth (a sampling null means "don't send it").
|
|
162
165
|
def request_options(retry_generation: false, model: nil)
|
|
166
|
+
max_tokens = Samagotchi::Config.max_tokens
|
|
163
167
|
options = deep_merge(thinking_fields(model), sampling)
|
|
168
|
+
options = { max_tokens: max_tokens }.merge(options) if max_tokens
|
|
164
169
|
options = EmptyAnswerRetry.sampling(options) if retry_generation
|
|
165
170
|
deep_compact(options)
|
|
166
171
|
end
|
|
@@ -378,17 +383,22 @@ module Samagotchi
|
|
|
378
383
|
end
|
|
379
384
|
|
|
380
385
|
# A generation a plugin cut (stop_generation) is an empty answer made
|
|
381
|
-
# early
|
|
382
|
-
#
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
#
|
|
386
|
+
# early. Queued input (a user's line, a plugin's steer) goes in
|
|
387
|
+
# first, with or without a retry left, and spends no attempt. A
|
|
388
|
+
# steer's cut (Engine#cut_for_steer) with nothing queued is asked
|
|
389
|
+
# again as is. Else it is asked again with its own nudge while the
|
|
390
|
+
# retry budget lasts, else the turn ends as cancelled (hook), with
|
|
391
|
+
# nothing salvaged and without a spent nudge. A Stop that came right
|
|
392
|
+
# after the cut is a plain cancel. Returns :retry, or the turn's
|
|
393
|
+
# result.
|
|
386
394
|
def after_cut(iteration, response)
|
|
387
395
|
return canceled(iteration, @cancel_controller.reason) if @cancel_controller.cancelled?
|
|
388
396
|
|
|
389
|
-
|
|
390
|
-
|
|
397
|
+
# The row under the cut step, above the message it was cut for.
|
|
398
|
+
emit(type: :steer_cut, iteration: iteration, source: response.cut[:source].to_s) if response.cut[:steer]
|
|
399
|
+
return :retry if inject_pending_input(iteration) || response.cut[:steer]
|
|
391
400
|
|
|
401
|
+
if @empty_retry.left?
|
|
392
402
|
@empty_retry.nudge!(@conversation, TurnNote.cut_retry(response.cut[:by], response.cut[:reason]),
|
|
393
403
|
emit: method(:emit), iteration: iteration, finish_reason: response.finish_reason,
|
|
394
404
|
thinking_chars: response.reasoning.to_s.length, stopped_by: response.cut[:by])
|
|
@@ -449,10 +459,10 @@ module Samagotchi
|
|
|
449
459
|
|
|
450
460
|
# The request runs under the generation's own controller (the turn
|
|
451
461
|
# controller's child), so a plugin's stop_generation cuts it alone.
|
|
452
|
-
def with_generation(&
|
|
462
|
+
def with_generation(&)
|
|
453
463
|
return yield(nil) unless @cancel_controller
|
|
454
464
|
|
|
455
|
-
@cancel_controller.generation(&
|
|
465
|
+
@cancel_controller.generation(&)
|
|
456
466
|
end
|
|
457
467
|
|
|
458
468
|
# The empty response of a cut generation: its spinners close as any
|
|
@@ -475,8 +485,11 @@ module Samagotchi
|
|
|
475
485
|
on_delta: lambda { |content:, reasoning:, payload:|
|
|
476
486
|
streamed << content
|
|
477
487
|
thought << reasoning
|
|
478
|
-
|
|
479
|
-
|
|
488
|
+
event = { type: :generation_chunk, iteration: iteration, content: reasoning + content, text: content,
|
|
489
|
+
thinking: reasoning, payload: payload }
|
|
490
|
+
# A tool call streaming: a steer doesn't cut it (Engine#cut_for_steer).
|
|
491
|
+
event[:tool_call] = true if OpenAIChat.tool_call_delta?(payload)
|
|
492
|
+
emit(event)
|
|
480
493
|
},
|
|
481
494
|
on_retry: ->(**retry_event) { emit({ type: :generation_retrying, iteration: iteration }.merge(retry_event)) }
|
|
482
495
|
)
|
|
@@ -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)
|
|
@@ -53,13 +53,31 @@ module Samagotchi
|
|
|
53
53
|
# How much of a 200's non-stream body is read as a possible JSON error.
|
|
54
54
|
PLAIN_ERROR_LIMIT = 64 * 1024
|
|
55
55
|
DEFAULT_MODELS_TTL = 60
|
|
56
|
+
# OpenRouter holds a paid request's estimated cost against the balance
|
|
57
|
+
# while it runs: the input plus the output max_tokens allows (a fixed
|
|
58
|
+
# per-request cap when it is unset), and refuses a request with a 402
|
|
59
|
+
# once the running ones fill the budget. A request that names a sane
|
|
60
|
+
# limit holds less. 32k leaves room for any one step's answer.
|
|
61
|
+
OPENROUTER_MAX_TOKENS = 32_768
|
|
56
62
|
|
|
57
|
-
attr_reader :base_url, :host_name, :api_key_env, :models_ttl, :first_token_timeout
|
|
63
|
+
attr_reader :base_url, :host_name, :api_key_env, :models_ttl, :first_token_timeout, :default_max_tokens
|
|
58
64
|
|
|
59
65
|
# @param entry [HostRegistry::HostEntry]
|
|
60
|
-
def self.for(entry, **
|
|
66
|
+
def self.for(entry, **)
|
|
61
67
|
new(base_url: entry.openai_base_url, host_name: entry.name, api_key_env: entry.api_key_env,
|
|
62
|
-
remote: entry.remote?,
|
|
68
|
+
remote: entry.remote?, default_max_tokens: (OPENROUTER_MAX_TOKENS if openrouter?(entry.openai_base_url)),
|
|
69
|
+
**)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# true when a streamed +payload+ carries a tool-call delta.
|
|
73
|
+
def self.tool_call_delta?(payload) = Assembly.tool_call_delta?(payload)
|
|
74
|
+
|
|
75
|
+
# true when +url+ is OpenRouter's API (openrouter.ai or a subdomain).
|
|
76
|
+
def self.openrouter?(url)
|
|
77
|
+
host = URI(url.to_s).host.to_s.downcase
|
|
78
|
+
host == "openrouter.ai" || host.end_with?(".openrouter.ai")
|
|
79
|
+
rescue URI::Error
|
|
80
|
+
false
|
|
63
81
|
end
|
|
64
82
|
|
|
65
83
|
# Tool arguments as a Hash; "" is {}, invalid JSON stays a String.
|
|
@@ -87,10 +105,13 @@ module Samagotchi
|
|
|
87
105
|
# @param remote [Boolean] a provider on the network, not a local server
|
|
88
106
|
# @param first_token_timeout [Numeric, nil] seconds a streamed answer
|
|
89
107
|
# may take to show something (LLM::HTTP); nil: no limit
|
|
108
|
+
# @param default_max_tokens [Integer, nil] max_tokens for a request
|
|
109
|
+
# whose options name none (OPENROUTER_MAX_TOKENS); nil sends none
|
|
90
110
|
def initialize(base_url:, host_name:, api_key_env: nil, stream: true, retries: true, timeout: nil,
|
|
91
111
|
env: ENV, sleeper: nil, retry_policy: nil, models_ttl: DEFAULT_MODELS_TTL, remote: false,
|
|
92
|
-
first_token_timeout: nil, purpose: "chat")
|
|
112
|
+
first_token_timeout: nil, purpose: "chat", default_max_tokens: nil)
|
|
93
113
|
@remote = remote
|
|
114
|
+
@default_max_tokens = default_max_tokens
|
|
94
115
|
# What its requests are for, in the log (LLM::HTTP::LOGGED_PURPOSES).
|
|
95
116
|
@purpose = purpose
|
|
96
117
|
@first_token_timeout = first_token_timeout
|
|
@@ -163,7 +184,7 @@ module Samagotchi
|
|
|
163
184
|
models = []
|
|
164
185
|
after = nil
|
|
165
186
|
MAX_MODEL_PAGES.times do
|
|
166
|
-
uri = URI("#{@base_url}/models#{
|
|
187
|
+
uri = URI("#{@base_url}/models#{"?after=#{URI.encode_www_form_component(after)}" if after}")
|
|
167
188
|
body = parse_json(@http.fetch(uri, get_request(uri), log_fields: { purpose: "models" }).body, "model list")
|
|
168
189
|
models.concat(model_entries(body).map { |raw| model_info(raw) })
|
|
169
190
|
break unless body.is_a?(Hash) && body["has_more"] && body["last_id"]
|
|
@@ -193,6 +214,18 @@ module Samagotchi
|
|
|
193
214
|
nil
|
|
194
215
|
end
|
|
195
216
|
|
|
217
|
+
def self.served_model(payload)
|
|
218
|
+
model = payload.is_a?(Hash) ? payload["model"] : nil
|
|
219
|
+
model.is_a?(String) && !model.strip.empty? ? model : nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# OpenRouter names the provider that served a request (Baseten,
|
|
223
|
+
# Fireworks, …) in the body's `provider`; other servers send none.
|
|
224
|
+
def self.served_provider(payload)
|
|
225
|
+
provider = payload.is_a?(Hash) ? payload["provider"] : nil
|
|
226
|
+
provider.is_a?(String) && !provider.strip.empty? ? provider : nil
|
|
227
|
+
end
|
|
228
|
+
|
|
196
229
|
private
|
|
197
230
|
|
|
198
231
|
def timeouts(timeout)
|
|
@@ -211,11 +244,12 @@ module Samagotchi
|
|
|
211
244
|
stream: @stream
|
|
212
245
|
}
|
|
213
246
|
body[:stream_options] = { include_usage: true } if @stream
|
|
247
|
+
body[:max_tokens] = @default_max_tokens if @default_max_tokens
|
|
214
248
|
unless Array(tools).empty?
|
|
215
249
|
body[:tools] = tools
|
|
216
250
|
body[:tool_choice] = "auto"
|
|
217
251
|
end
|
|
218
|
-
# A configured value replaces chi's own (temperature); nil drops it.
|
|
252
|
+
# A configured value replaces chi's own (temperature, max_tokens); nil drops it.
|
|
219
253
|
body.merge(options || {}).compact
|
|
220
254
|
end
|
|
221
255
|
|
|
@@ -259,34 +293,15 @@ module Samagotchi
|
|
|
259
293
|
function = call["function"] || {}
|
|
260
294
|
ToolCall.new(id: call["id"], name: function["name"].to_s, arguments: self.class.parse_arguments(function["arguments"]))
|
|
261
295
|
end
|
|
262
|
-
ChatResponse.new(text:
|
|
296
|
+
ChatResponse.new(text: message["content"].to_s,
|
|
263
297
|
reasoning: (message["reasoning_content"] || message["reasoning"]).to_s,
|
|
264
298
|
tool_calls: calls, usage: Usage.from_payload(body) || Usage.none,
|
|
265
299
|
finish_reason: body.dig("choices", 0, "finish_reason"), model: served_model(body),
|
|
266
300
|
provider: self.class.served_provider(body))
|
|
267
301
|
end
|
|
268
302
|
|
|
269
|
-
# Workaround for Splash 1.0.2/1.1.0, which keeps the "\n\n" the model
|
|
270
|
-
# writes after </think> at the start of the answer
|
|
271
|
-
# (https://github.com/incoai/splash/issues/254). Remove once Splash
|
|
272
|
-
# strips it; llama.cpp already does. Streamed, it applies to the chunks
|
|
273
|
-
# until the first text.
|
|
274
|
-
def self.drop_leading_newlines(text) = text.sub(/\A[\r\n]+/, "")
|
|
275
|
-
|
|
276
|
-
def self.served_model(payload)
|
|
277
|
-
model = payload.is_a?(Hash) ? payload["model"] : nil
|
|
278
|
-
model.is_a?(String) && !model.strip.empty? ? model : nil
|
|
279
|
-
end
|
|
280
|
-
|
|
281
303
|
def served_model(payload) = self.class.served_model(payload)
|
|
282
304
|
|
|
283
|
-
# OpenRouter names the provider that served a request (Baseten,
|
|
284
|
-
# Fireworks, …) in the body's `provider`; other servers send none.
|
|
285
|
-
def self.served_provider(payload)
|
|
286
|
-
provider = payload.is_a?(Hash) ? payload["provider"] : nil
|
|
287
|
-
provider.is_a?(String) && !provider.strip.empty? ? provider : nil
|
|
288
|
-
end
|
|
289
|
-
|
|
290
305
|
def served_provider(payload) = self.class.served_provider(payload)
|
|
291
306
|
|
|
292
307
|
# The parsed payload of a `data:` line; nil for blank lines, comments
|
|
@@ -357,7 +372,7 @@ module Samagotchi
|
|
|
357
372
|
listed = raw["supported_parameters"] || raw["capabilities"]
|
|
358
373
|
return nil unless listed.is_a?(Array)
|
|
359
374
|
|
|
360
|
-
listed.include?("tools")
|
|
375
|
+
listed.include?("tools") || nil
|
|
361
376
|
end
|
|
362
377
|
|
|
363
378
|
def cached_models
|
|
@@ -405,7 +420,6 @@ module Samagotchi
|
|
|
405
420
|
@finish_reason = choice["finish_reason"] if choice["finish_reason"]
|
|
406
421
|
delta = choice["delta"].is_a?(Hash) ? choice["delta"] : {}
|
|
407
422
|
content = delta["content"].to_s
|
|
408
|
-
content = OpenAIChat.drop_leading_newlines(content) if @text.empty?
|
|
409
423
|
reasoning = (delta["reasoning_content"] || delta["reasoning"]).to_s
|
|
410
424
|
@text << content
|
|
411
425
|
@reasoning << reasoning
|
data/lib/samagotchi/log.rb
CHANGED
|
@@ -143,7 +143,7 @@ module Samagotchi
|
|
|
143
143
|
def safe_value(key, value)
|
|
144
144
|
return "[redacted]" if key.match?(SECRET_KEY)
|
|
145
145
|
|
|
146
|
-
value = value.
|
|
146
|
+
value = value.round(3) if value.is_a?(Float)
|
|
147
147
|
value.is_a?(String) && value.match?(URL) ? safe_url(value) : value
|
|
148
148
|
end
|
|
149
149
|
|
|
@@ -185,7 +185,7 @@ module Samagotchi
|
|
|
185
185
|
def build_state
|
|
186
186
|
options = @options
|
|
187
187
|
path = options.fetch(:path, :auto)
|
|
188
|
-
path =
|
|
188
|
+
path = LogPath.resolve rescue nil if path == :auto
|
|
189
189
|
level = options[:level] || (Config.get("log.level") rescue nil)
|
|
190
190
|
level = LEVELS.key?(level.to_s.downcase.to_sym) ? level.to_s.downcase.to_sym : DEFAULT_LEVEL
|
|
191
191
|
{ writer: DebugLog.new(path: path), level: LEVELS.fetch(level), mirror: options[:mirror] ? true : false }
|