samagotchi 0.66.0 → 0.68.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 +80 -1
- data/docs/architecture.md +55 -30
- data/docs/cli.md +3 -1
- data/docs/configuration.md +50 -9
- data/docs/context.md +35 -1
- data/docs/desktop.md +90 -22
- data/docs/guardrails.md +8 -5
- data/docs/hooks.md +2 -2
- data/docs/internals/anthropic.md +156 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -0
- data/docs/internals/engine.md +129 -38
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +26 -11
- data/docs/plugins.md +126 -23
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +4 -1
- data/docs/testing.md +18 -4
- data/lib/samagotchi/bootstrap/probe.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +13 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge.rb +6 -3
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +34 -313
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
- data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +15 -7
- data/lib/samagotchi/context_absorber.rb +66 -13
- data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
- data/lib/samagotchi/desktop/macos/App.swift +3 -3
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
- data/lib/samagotchi/desktop/macos/Images.swift +2 -2
- data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
- data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
- data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
- data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
- data/lib/samagotchi/desktop/macos.rb +84 -13
- data/lib/samagotchi/desktop_command.rb +14 -3
- data/lib/samagotchi/effective_model.rb +283 -0
- data/lib/samagotchi/engine.rb +307 -549
- data/lib/samagotchi/engine_peers.rb +3 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +1 -1
- data/lib/samagotchi/guardrails/approvals.rb +1 -1
- data/lib/samagotchi/guardrails/rules.rb +1 -1
- data/lib/samagotchi/guardrails/targets.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +151 -0
- data/lib/samagotchi/hooks/registry.rb +2 -2
- data/lib/samagotchi/host_keys.rb +147 -0
- data/lib/samagotchi/level_setting_command.rb +99 -0
- data/lib/samagotchi/llm/adapter_common.rb +138 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
- data/lib/samagotchi/llm/api_key.rb +23 -7
- data/lib/samagotchi/llm/chat_loop.rb +10 -6
- data/lib/samagotchi/llm/openai_chat.rb +9 -92
- data/lib/samagotchi/llm/refusal_memo.rb +29 -0
- data/lib/samagotchi/llm_context_override.rb +1 -0
- data/lib/samagotchi/llm_context_strategy.rb +5 -10
- data/lib/samagotchi/peer_sources.rb +58 -0
- data/lib/samagotchi/plugin/api.rb +64 -155
- data/lib/samagotchi/plugin/loader.rb +4 -2
- data/lib/samagotchi/plugin/peer.rb +394 -0
- data/lib/samagotchi/plugin/tool_spec.rb +166 -0
- data/lib/samagotchi/plugin_tasks.rb +2 -2
- data/lib/samagotchi/reasoning_history_command.rb +29 -90
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/self_report.rb +5 -3
- data/lib/samagotchi/send_command.rb +21 -11
- data/lib/samagotchi/session.rb +22 -8
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/session_setting.rb +86 -0
- data/lib/samagotchi/thinking_command.rb +24 -76
- data/lib/samagotchi/thinking_notices.rb +169 -0
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +1 -1
- data/lib/samagotchi/tools/task_create.rb +6 -6
- data/lib/samagotchi/tools/task_get.rb +13 -13
- data/lib/samagotchi/tools/task_list.rb +6 -6
- data/lib/samagotchi/tools/task_record.rb +52 -0
- data/lib/samagotchi/tools/task_runtime.rb +55 -91
- data/lib/samagotchi/tools/task_stop.rb +6 -6
- data/lib/samagotchi/tools/task_wait.rb +12 -13
- data/lib/samagotchi/tools/tool_index.rb +454 -0
- data/lib/samagotchi/update_command.rb +9 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +71 -93
- data/lib/samagotchi/web/capabilities.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/annotate_actions.js +1 -1
- data/lib/samagotchi/web/public/app.js +103 -539
- data/lib/samagotchi/web/public/bubbles.js +291 -0
- data/lib/samagotchi/web/public/cards_place.js +184 -0
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/chip_popover.js +111 -0
- data/lib/samagotchi/web/public/command_list.js +12 -10
- data/lib/samagotchi/web/public/commands.js +2 -2
- data/lib/samagotchi/web/public/composer_size.js +2 -2
- data/lib/samagotchi/web/public/copy.js +1 -1
- data/lib/samagotchi/web/public/ctx.js +1 -1
- data/lib/samagotchi/web/public/delivery_line.js +4 -4
- data/lib/samagotchi/web/public/families_view.js +1 -1
- data/lib/samagotchi/web/public/format.js +11 -7
- data/lib/samagotchi/web/public/images.js +25 -2
- data/lib/samagotchi/web/public/index.html +15 -7
- data/lib/samagotchi/web/public/info_bar.js +3 -3
- data/lib/samagotchi/web/public/info_bar_model.js +110 -0
- data/lib/samagotchi/web/public/init_row.js +21 -10
- data/lib/samagotchi/web/public/level_chip.js +29 -80
- data/lib/samagotchi/web/public/lists_view.js +64 -9
- data/lib/samagotchi/web/public/llm_context_chip.js +38 -107
- data/lib/samagotchi/web/public/model_picker.js +26 -22
- data/lib/samagotchi/web/public/notify.js +6 -4
- data/lib/samagotchi/web/public/question_card.js +1 -1
- data/lib/samagotchi/web/public/ref_scan.js +2 -8
- data/lib/samagotchi/web/public/refs.js +13 -0
- data/lib/samagotchi/web/public/session_state.js +14 -0
- data/lib/samagotchi/web/public/session_title.js +2 -3
- data/lib/samagotchi/web/public/sessions_list.js +0 -8
- data/lib/samagotchi/web/public/stage_frame.js +179 -0
- data/lib/samagotchi/web/public/stage_model.js +35 -2
- data/lib/samagotchi/web/public/stage_slots.js +130 -0
- data/lib/samagotchi/web/public/stage_view.js +56 -335
- data/lib/samagotchi/web/public/stage_watch.js +65 -0
- data/lib/samagotchi/web/public/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +2 -2
- data/lib/samagotchi/web/public/turn_model.js +2 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/web/public/vendor/directive-helpers.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
- data/lib/samagotchi/web/server.rb +0 -4
- data/lib/samagotchi/worker.rb +22 -24
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +27 -1
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -14,11 +14,16 @@ require_relative "context_status"
|
|
|
14
14
|
require_relative "steer"
|
|
15
15
|
require_relative "turn_note"
|
|
16
16
|
require_relative "model_profile"
|
|
17
|
+
require_relative "effective_model"
|
|
18
|
+
require_relative "session_setting"
|
|
19
|
+
require_relative "thinking_notices"
|
|
17
20
|
require_relative "cancellation_controller"
|
|
18
21
|
require_relative "context_window"
|
|
19
22
|
require_relative "kernel_loop"
|
|
20
23
|
require_relative "tools/builtins"
|
|
21
24
|
require_relative "tools/task_runtime"
|
|
25
|
+
require_relative "tools/tool_index"
|
|
26
|
+
require_relative "peer_sources"
|
|
22
27
|
require_relative "session_commands"
|
|
23
28
|
require_relative "plugin/loader"
|
|
24
29
|
require_relative "extension_load"
|
|
@@ -31,7 +36,7 @@ require_relative "llm/backend"
|
|
|
31
36
|
require_relative "llm/openai_chat"
|
|
32
37
|
require_relative "llm/turn_settings"
|
|
33
38
|
require_relative "session"
|
|
34
|
-
require_relative "
|
|
39
|
+
require_relative "toolkit_binding"
|
|
35
40
|
require_relative "answer_tail"
|
|
36
41
|
require_relative "archive_store"
|
|
37
42
|
require_relative "session_observer"
|
|
@@ -44,6 +49,7 @@ require_relative "recap_setup"
|
|
|
44
49
|
require_relative "idle_reminders"
|
|
45
50
|
require_relative "idle_scheduler"
|
|
46
51
|
require_relative "hooks"
|
|
52
|
+
require_relative "hook_routes"
|
|
47
53
|
require_relative "guardrails"
|
|
48
54
|
require_relative "reminder_store"
|
|
49
55
|
require_relative "reminder_queue"
|
|
@@ -131,24 +137,30 @@ module Samagotchi
|
|
|
131
137
|
# is used (the REPL checks the model it starts on, #switch_model!).
|
|
132
138
|
@default_model_name = ModelProfile.required_model_name(model_name)
|
|
133
139
|
ModelProfile.check_host!(model_name, hosts: @host_registry.entries) unless model_name.to_s.strip.empty?
|
|
134
|
-
|
|
140
|
+
# Guards the model choice and the profile cached for it (#store_profile).
|
|
141
|
+
@profile_mutex = Mutex.new
|
|
142
|
+
typed = model_typed.to_s.strip
|
|
143
|
+
@model_choice = ModelChoice.new(name: @default_model_name, typed: typed.empty? ? @default_model_name : typed)
|
|
135
144
|
# An injected client (specs) stands in for every host's client.
|
|
136
145
|
@host_registry.client_override = client if client
|
|
137
|
-
@client =
|
|
146
|
+
@client = effective_model.client
|
|
138
147
|
# A caller's profile pins it (until a model switch); otherwise
|
|
139
148
|
# #profile_resolution decides on first need (see there), so building an
|
|
140
149
|
# Engine makes no network call.
|
|
141
150
|
@given_profile = profile ? ModelProfile.normalize(profile) : nil
|
|
142
|
-
@typed_model_name = model_typed.to_s.strip.empty? ? @default_model_name : model_typed.to_s.strip
|
|
143
151
|
@profile_resolution = nil
|
|
144
152
|
# Ensure the built-in system bundle is installed (lazy, warn-only).
|
|
145
153
|
# This is the single seam for both TUI and non-TUI (web/worker) paths.
|
|
146
154
|
begin
|
|
147
155
|
require_relative "memory_bundle/system_bundle"
|
|
148
156
|
MemoryBundle::SystemBundle.ensure!
|
|
149
|
-
rescue StandardError
|
|
150
|
-
|
|
157
|
+
rescue StandardError => e
|
|
158
|
+
Log.warn(:memory, "system_bundle_ensure_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
151
159
|
end
|
|
160
|
+
# What hooks and plugins do beyond reading their event (a notice, a
|
|
161
|
+
# question, a stop, a steer), routed to the UIs. Built before anything
|
|
162
|
+
# that may notify.
|
|
163
|
+
@hook_routes = build_hook_routes
|
|
152
164
|
# Load hooks from config (plugins) and create the registry; what fails
|
|
153
165
|
# to load is announced, and a required guardrail's failure denies
|
|
154
166
|
# every tool call. Rules load now too, so their errors are announced.
|
|
@@ -185,9 +197,18 @@ module Samagotchi
|
|
|
185
197
|
# plugins' tool sets from chi.replace_tools until the turn thread
|
|
186
198
|
# applies them (#apply_staged_tools!). Built before the plugins load.
|
|
187
199
|
@plugin_tasks = build_plugin_tasks
|
|
188
|
-
# The
|
|
189
|
-
#
|
|
190
|
-
@
|
|
200
|
+
# The session's toolkit: the kits (the plugins' too, chi.toolkit),
|
|
201
|
+
# what its next turn declares, the turn-start apply.
|
|
202
|
+
@toolkit_binding = build_toolkit_binding
|
|
203
|
+
# The notices a turn's thinking level leads to (once per session and host).
|
|
204
|
+
@thinking_notices = ThinkingNotices.new(session: -> { @session }, profile: -> { self.profile },
|
|
205
|
+
notify: ->(text, level, source) { hook_notify(text, level, source) })
|
|
206
|
+
# The tools plugins publish without declaring (chi.tool_source), and
|
|
207
|
+
# the search and call tools that reach them (Tools::ToolIndex).
|
|
208
|
+
@tool_index = Tools::ToolIndex.new
|
|
209
|
+
# The context sources the peer bundles declare as a peer agent's
|
|
210
|
+
# answers (chi.peer_source); the worker's ContextAbsorber asks it.
|
|
211
|
+
@peer_sources = PeerSources.new
|
|
191
212
|
@lifecycle_mutex = Mutex.new
|
|
192
213
|
@shut_down = false
|
|
193
214
|
load_plugins if plugins
|
|
@@ -275,7 +296,7 @@ module Samagotchi
|
|
|
275
296
|
def wire_kernel(kernel, muted_memories)
|
|
276
297
|
# A given kernel (specs) gets the Engine's store, hooks and tools, as
|
|
277
298
|
# the one built here does.
|
|
278
|
-
@kernel = kernel || KernelLoop.new(client: @client, profile: @given_profile, model_name:
|
|
299
|
+
@kernel = kernel || KernelLoop.new(client: @client, profile: @given_profile, model_name: effective_model_name,
|
|
279
300
|
hooks: @hooks, reminder_store: @reminder_store, tools: @tools)
|
|
280
301
|
@kernel.reminder_store = @reminder_store
|
|
281
302
|
@kernel.hooks = @hooks
|
|
@@ -360,8 +381,8 @@ module Samagotchi
|
|
|
360
381
|
end
|
|
361
382
|
|
|
362
383
|
# The backend for the next turn: the loop the effective model's host speaks.
|
|
363
|
-
def backend
|
|
364
|
-
backend_for(
|
|
384
|
+
def backend(model = effective_model)
|
|
385
|
+
backend_for(model)
|
|
365
386
|
end
|
|
366
387
|
|
|
367
388
|
# Record that activity happened (user input or a completed turn): the
|
|
@@ -451,7 +472,8 @@ module Samagotchi
|
|
|
451
472
|
|
|
452
473
|
log_dropped_steers(steers, "answered")
|
|
453
474
|
[]
|
|
454
|
-
rescue StandardError
|
|
475
|
+
rescue StandardError => e
|
|
476
|
+
Log.warn(:turn, "take_steers_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
455
477
|
[]
|
|
456
478
|
end
|
|
457
479
|
private :take_steers
|
|
@@ -508,14 +530,17 @@ module Samagotchi
|
|
|
508
530
|
|
|
509
531
|
# @return [SessionMetrics] the per-session analytics collector
|
|
510
532
|
attr_reader :metrics
|
|
511
|
-
|
|
512
|
-
#
|
|
513
|
-
|
|
533
|
+
|
|
534
|
+
# The model as given (maybe an alias; HostRegistry#resolve applies it).
|
|
535
|
+
def effective_model_name = @model_choice.name
|
|
536
|
+
|
|
537
|
+
# The model as typed at start or at the last /model.
|
|
538
|
+
def typed_model_name = @model_choice.typed
|
|
514
539
|
|
|
515
540
|
# The resolved ref a session stores for the effective model (ModelRef#ref:
|
|
516
541
|
# the alias applied, "host:id" when it names a host).
|
|
517
542
|
def effective_model_ref
|
|
518
|
-
model_ref_for(
|
|
543
|
+
model_ref_for(effective_model_name)
|
|
519
544
|
end
|
|
520
545
|
|
|
521
546
|
# The effective model as the system prompt names it (SystemPrompt::ModelIdentity):
|
|
@@ -523,11 +548,11 @@ module Samagotchi
|
|
|
523
548
|
# only, the model its /props names when that differs. Never a new
|
|
524
549
|
# request: only the /props answer profile resolution already cached.
|
|
525
550
|
def model_identity
|
|
526
|
-
|
|
527
|
-
entry =
|
|
551
|
+
model = effective_model
|
|
552
|
+
entry = model.entry
|
|
528
553
|
host = entry && "host #{entry.name}, #{entry.url || "#{entry.host}:#{entry.port}"}"
|
|
529
|
-
SystemPrompt::ModelIdentity.new(ref:
|
|
530
|
-
served:
|
|
554
|
+
SystemPrompt::ModelIdentity.new(ref: model.ref, host: host, key: @model_key,
|
|
555
|
+
served: model.prompt_served, fallback_key: @fallback_model_key)
|
|
531
556
|
end
|
|
532
557
|
|
|
533
558
|
# The model notes for the effective model (ModelNotes): matched on its
|
|
@@ -552,7 +577,8 @@ module Samagotchi
|
|
|
552
577
|
return saved.dup if !@prompt_builder.prompt_notes.nil? || !saved.empty? || @session.messages.to_a.any?
|
|
553
578
|
|
|
554
579
|
model_notes.map { |note| PromptNote.from_note(note) }
|
|
555
|
-
rescue StandardError
|
|
580
|
+
rescue StandardError => e
|
|
581
|
+
Log.warn(:model, "prompt_notes_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
556
582
|
[]
|
|
557
583
|
end
|
|
558
584
|
|
|
@@ -571,10 +597,10 @@ module Samagotchi
|
|
|
571
597
|
summary = error.respond_to?(:summary) ? error.summary : error.message
|
|
572
598
|
return summary unless error.is_a?(LLM::ProviderError) && error.host
|
|
573
599
|
|
|
574
|
-
note = @host_registry.alias_note(
|
|
600
|
+
note = @host_registry.alias_note(effective_model_name)
|
|
575
601
|
note && note.end_with?("on host #{error.host}") ? "#{summary} (#{note})" : summary
|
|
576
|
-
rescue StandardError
|
|
577
|
-
summary
|
|
602
|
+
rescue StandardError => e
|
|
603
|
+
EffectiveModel::Unresolved.fallback(:alias_note_failed, effective_model_name, e, summary)
|
|
578
604
|
end
|
|
579
605
|
|
|
580
606
|
# +name+'s resolved ref (ModelRef#ref).
|
|
@@ -588,7 +614,8 @@ module Samagotchi
|
|
|
588
614
|
def store_model!(session)
|
|
589
615
|
ref = effective_model_ref
|
|
590
616
|
session.model_name = ref
|
|
591
|
-
|
|
617
|
+
typed = typed_model_name
|
|
618
|
+
session.model_typed = typed.to_s == ref ? nil : typed
|
|
592
619
|
session
|
|
593
620
|
end
|
|
594
621
|
|
|
@@ -607,16 +634,25 @@ module Samagotchi
|
|
|
607
634
|
# and kept, so the system prompt and the server's KV prefix stay stable;
|
|
608
635
|
# #switch_model! starts over, and a failed server probe is retried before
|
|
609
636
|
# the next turn (#refresh_profile!). The kernel follows each resolution.
|
|
637
|
+
# Kept with the model choice it was resolved for: one that finishes
|
|
638
|
+
# after a switch is neither kept nor applied (#store_profile).
|
|
610
639
|
# @return [ModelProfile::Resolution]
|
|
611
640
|
def profile_resolution
|
|
612
|
-
|
|
641
|
+
choice = @model_choice
|
|
642
|
+
cached = @profile_resolution
|
|
643
|
+
return cached.last if cached&.first.equal?(choice)
|
|
644
|
+
|
|
645
|
+
store_profile(choice, resolve_profile(choice))
|
|
613
646
|
end
|
|
614
|
-
attr_reader :host_registry, :client
|
|
647
|
+
attr_reader :default_model_name, :host_registry, :client
|
|
615
648
|
|
|
616
649
|
# @return [Commands::Registry] the commands this session runs: the
|
|
617
650
|
# built-ins, and the ones bundle plugins add
|
|
618
651
|
attr_reader :command_registry
|
|
619
652
|
|
|
653
|
+
# @return [PeerSources] the sources the peer bundles declared
|
|
654
|
+
attr_reader :peer_sources
|
|
655
|
+
|
|
620
656
|
# The model id sent for +full_ref+ (its alias applied, the host prefix off).
|
|
621
657
|
def bare_model_name(full_ref)
|
|
622
658
|
@host_registry.bare_name(full_ref)
|
|
@@ -627,8 +663,9 @@ module Samagotchi
|
|
|
627
663
|
# (before aliases were applied there): that key is read when the
|
|
628
664
|
# target's overlay is missing (KernelLoop#sync_model_key!).
|
|
629
665
|
def sync_model_key!
|
|
630
|
-
|
|
631
|
-
|
|
666
|
+
choice = @model_choice
|
|
667
|
+
@model_key = ModelOverlay.key_for(bare_model_name(choice.name))
|
|
668
|
+
typed_key = ModelOverlay.key_for(@host_registry.parse_qualified_model(choice.typed).last)
|
|
632
669
|
@fallback_model_key = typed_key == @model_key ? nil : typed_key
|
|
633
670
|
@kernel.sync_model_key!(@model_key, fallback: @fallback_model_key)
|
|
634
671
|
end
|
|
@@ -637,17 +674,17 @@ module Samagotchi
|
|
|
637
674
|
# The effective model's bare name for the guardrails' models: rules;
|
|
638
675
|
# nil without a model name.
|
|
639
676
|
def guardrail_model_name
|
|
640
|
-
|
|
677
|
+
model = effective_model_name
|
|
678
|
+
name = model && bare_model_name(model)
|
|
641
679
|
name.to_s.strip.empty? ? nil : name
|
|
642
680
|
end
|
|
643
681
|
|
|
644
682
|
# Point the kernel (and a chat backend) at the effective model's host
|
|
645
683
|
# (after /model, --model, resume).
|
|
646
|
-
def sync_kernel_client!
|
|
647
|
-
|
|
648
|
-
@client =
|
|
649
|
-
|
|
650
|
-
backend_for(target)
|
|
684
|
+
def sync_kernel_client!(model = effective_model)
|
|
685
|
+
@client = model.client
|
|
686
|
+
@kernel.client = model.client if @kernel.client != model.client
|
|
687
|
+
backend_for(model)
|
|
651
688
|
end
|
|
652
689
|
|
|
653
690
|
# Subscribe a persistent observer to engine events.
|
|
@@ -948,11 +985,14 @@ module Samagotchi
|
|
|
948
985
|
# The alias is applied where the model is resolved (HostRegistry#resolve),
|
|
949
986
|
# once; checked here: an unknown host, an alias naming another host.
|
|
950
987
|
resolved = ModelProfile.check_host!(ModelProfile.required_model_name(model_name), hosts: @host_registry.entries)
|
|
951
|
-
|
|
952
|
-
@
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
988
|
+
choice = ModelChoice.new(name: resolved, typed: typed.to_s.strip.empty? ? model_name : typed.to_s.strip)
|
|
989
|
+
@profile_mutex.synchronize do
|
|
990
|
+
# One assignment: a reader on another thread sees the old pair or the new one.
|
|
991
|
+
@model_choice = choice
|
|
992
|
+
# A profile given to .new was for the starting model.
|
|
993
|
+
@given_profile = nil
|
|
994
|
+
@profile_resolution = nil
|
|
995
|
+
end
|
|
956
996
|
sync_model_key!
|
|
957
997
|
@prompt_builder.reset!
|
|
958
998
|
# The notes the new model's prompt loads at its next build, so /model,
|
|
@@ -1023,22 +1063,24 @@ module Samagotchi
|
|
|
1023
1063
|
# @return [Hash] @metrics.snapshot, filled in
|
|
1024
1064
|
def stats_snapshot
|
|
1025
1065
|
snapshot = @metrics.snapshot
|
|
1026
|
-
|
|
1027
|
-
|
|
1066
|
+
# One resolve for the whole snapshot (its readers' rescues stay theirs);
|
|
1067
|
+
# a model that doesn't resolve raises here, as it always has.
|
|
1068
|
+
model = effective_model
|
|
1069
|
+
served, served_for = served_model_for(snapshot, model: model)
|
|
1028
1070
|
snapshot = snapshot.merge(served_model: served, served_model_for: served_for,
|
|
1029
|
-
served_expected_by: served_expected_by(served
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1071
|
+
served_expected_by: model.served_expected_by(served),
|
|
1072
|
+
**setting_chips(SessionSetting::STATS, model),
|
|
1073
|
+
prompt_notes: prompt_notes.map(&:to_h), prompt_files: @prompt_builder.prompt_files,
|
|
1074
|
+
price_host: model.host_name)
|
|
1033
1075
|
unless snapshot.dig(:context, :window_tokens)
|
|
1034
|
-
window = current_context_window(
|
|
1076
|
+
window = current_context_window(model)
|
|
1035
1077
|
if window
|
|
1036
1078
|
snapshot = snapshot.merge(context: snapshot[:context].merge(window_tokens: window.tokens,
|
|
1037
1079
|
window_source: window.source.to_s))
|
|
1038
1080
|
end
|
|
1039
1081
|
end
|
|
1040
1082
|
# The chat loop uses no prompt profile: drop one a native turn reported.
|
|
1041
|
-
return snapshot.except(:profile, :profile_source) if
|
|
1083
|
+
return snapshot.except(:profile, :profile_source) if model.chat?
|
|
1042
1084
|
|
|
1043
1085
|
unless snapshot[:profile]
|
|
1044
1086
|
resolution = profile_resolution
|
|
@@ -1051,10 +1093,11 @@ module Samagotchi
|
|
|
1051
1093
|
# no turn yet (it woke after an idle exit): nil when either count is
|
|
1052
1094
|
# unknown or context.status is off. Counted as the turns count it:
|
|
1053
1095
|
# against the effective model's llm_context budget when it has one.
|
|
1054
|
-
def saved_context_status(context)
|
|
1096
|
+
def saved_context_status(context, model: :current)
|
|
1055
1097
|
return nil unless context
|
|
1056
1098
|
|
|
1057
|
-
|
|
1099
|
+
llm_context = model == :current ? turn_llm_context : model&.llm_context_explained(llm_context_session_args)&.resolved
|
|
1100
|
+
ContextStatus.new(llm_context: llm_context)
|
|
1058
1101
|
.display_for(used_tokens: context[:used_tokens], window_tokens: context[:window_tokens])
|
|
1059
1102
|
end
|
|
1060
1103
|
|
|
@@ -1062,10 +1105,7 @@ module Samagotchi
|
|
|
1062
1105
|
# (LLMContextStrategy.explain), nil when it can't be resolved.
|
|
1063
1106
|
# @return [LLMContextStrategy::Explained, nil]
|
|
1064
1107
|
def llm_context_explained
|
|
1065
|
-
|
|
1066
|
-
LLMContextStrategy.explain(target, names: model_lookup_names(target), **llm_context_session_args)
|
|
1067
|
-
rescue StandardError
|
|
1068
|
-
nil
|
|
1108
|
+
model_setting(nil) { |model| model.llm_context_explained(llm_context_session_args) }
|
|
1069
1109
|
end
|
|
1070
1110
|
|
|
1071
1111
|
# #llm_context_explained's summary (Explained#summary), nil when it
|
|
@@ -1077,7 +1117,7 @@ module Samagotchi
|
|
|
1077
1117
|
|
|
1078
1118
|
# The session's own llm_context values, nil when it has none.
|
|
1079
1119
|
# @return [LLMContextOverride, nil]
|
|
1080
|
-
def llm_context_override = @session
|
|
1120
|
+
def llm_context_override = SessionSetting::LLM_CONTEXT.own(@session)
|
|
1081
1121
|
|
|
1082
1122
|
# Set the session's own llm_context values (an empty override unsets
|
|
1083
1123
|
# them); the next turn's start resolves them, never a running one (its
|
|
@@ -1086,23 +1126,18 @@ module Samagotchi
|
|
|
1086
1126
|
def llm_context_override=(override)
|
|
1087
1127
|
raise ArgumentError, "no session yet" unless @session
|
|
1088
1128
|
|
|
1089
|
-
@session
|
|
1129
|
+
SessionSetting::LLM_CONTEXT.set(@session, override)
|
|
1090
1130
|
# The status a later client reads counts against the new budget.
|
|
1091
1131
|
@last_context_status = saved_context_status(@metrics.snapshot[:context]) || @last_context_status if @last_context_status
|
|
1092
1132
|
end
|
|
1093
1133
|
|
|
1094
1134
|
# The effective model is on a chat host (api: openai or anthropic), whose loop uses
|
|
1095
1135
|
# no prompt profile.
|
|
1096
|
-
def chat_model? =
|
|
1136
|
+
def chat_model? = effective_model.chat?
|
|
1097
1137
|
|
|
1098
1138
|
# "temperature=0.6 (hosts.work)" for /model: the effective model's
|
|
1099
1139
|
# configured sampling, nil when none.
|
|
1100
|
-
def sampling_summary
|
|
1101
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
1102
|
-
SamplingSettings.summary(target, names: model_lookup_names(target))
|
|
1103
|
-
rescue StandardError
|
|
1104
|
-
nil
|
|
1105
|
-
end
|
|
1140
|
+
def sampling_summary = model_setting(nil, &:sampling_summary)
|
|
1106
1141
|
|
|
1107
1142
|
# "off (models: qwen)", "low (session)" for /model: the effective
|
|
1108
1143
|
# model's thinking level and where it came from, nil when none is set.
|
|
@@ -1116,10 +1151,7 @@ module Samagotchi
|
|
|
1116
1151
|
# turn_sampling; on any error, the global and its name.
|
|
1117
1152
|
# @return [Array(Integer, String|nil)]
|
|
1118
1153
|
def steer_cut_after_explained
|
|
1119
|
-
|
|
1120
|
-
SteerCutAfter.resolve(target, names: model_lookup_names(target))
|
|
1121
|
-
rescue StandardError
|
|
1122
|
-
[SteerCut.cut_after, "steer.cut_after"]
|
|
1154
|
+
model_setting([SteerCut.cut_after, "steer.cut_after"], &:steer_cut_after_explained)
|
|
1123
1155
|
end
|
|
1124
1156
|
|
|
1125
1157
|
# #steer_cut_after_explained's seconds: what SteerCut#cut_for_steer
|
|
@@ -1141,10 +1173,7 @@ module Samagotchi
|
|
|
1141
1173
|
# +own+: the session's own level to resolve with (the current one by
|
|
1142
1174
|
# default; /thinking asks what a change would resolve to).
|
|
1143
1175
|
def thinking_explained(own: thinking_override)
|
|
1144
|
-
|
|
1145
|
-
Thinking.explain(target, names: model_lookup_names(target), session: own)
|
|
1146
|
-
rescue StandardError
|
|
1147
|
-
nil
|
|
1176
|
+
model_setting(nil) { |model| model.thinking_explained(own: own) }
|
|
1148
1177
|
end
|
|
1149
1178
|
|
|
1150
1179
|
# "drop (models: qwen)" for /model: the effective chat model's
|
|
@@ -1152,14 +1181,7 @@ module Samagotchi
|
|
|
1152
1181
|
# default, the model runs on the native loop (which ignores it) or it
|
|
1153
1182
|
# can't be resolved.
|
|
1154
1183
|
def reasoning_history_summary
|
|
1155
|
-
|
|
1156
|
-
return nil unless target.entry&.chat?
|
|
1157
|
-
|
|
1158
|
-
level, source = ReasoningHistory.resolve(target, names: model_lookup_names(target),
|
|
1159
|
-
session: reasoning_history_override)
|
|
1160
|
-
source ? ReasoningHistory.label(level, source) : nil
|
|
1161
|
-
rescue StandardError
|
|
1162
|
-
nil
|
|
1184
|
+
model_setting(nil) { |model| model.reasoning_history_summary(own: reasoning_history_override) }
|
|
1163
1185
|
end
|
|
1164
1186
|
|
|
1165
1187
|
# The effective chat model's reasoning_history level with where it came
|
|
@@ -1171,12 +1193,7 @@ module Samagotchi
|
|
|
1171
1193
|
# +own+: the session's own level to resolve with (the current one by
|
|
1172
1194
|
# default; a command asks what a change would resolve to).
|
|
1173
1195
|
def reasoning_history_explained(native: false, own: reasoning_history_override)
|
|
1174
|
-
|
|
1175
|
-
return nil unless native || target.entry&.chat?
|
|
1176
|
-
|
|
1177
|
-
ReasoningHistory.explain(target, names: model_lookup_names(target), session: own)
|
|
1178
|
-
rescue StandardError
|
|
1179
|
-
nil
|
|
1196
|
+
model_setting(nil) { |model| model.reasoning_history_explained(own: own, native: native) }
|
|
1180
1197
|
end
|
|
1181
1198
|
|
|
1182
1199
|
# What a reasoning_history change from level +from+ to level +to+ costs
|
|
@@ -1185,10 +1202,7 @@ module Samagotchi
|
|
|
1185
1202
|
# marked as refusing it may still get :history (one needless ask).
|
|
1186
1203
|
# @return [Symbol, nil]
|
|
1187
1204
|
def reasoning_history_cache_cost(from, to)
|
|
1188
|
-
|
|
1189
|
-
ReasoningHistory.cache_cost(from, to, target: target, conversation: @session&.messages)
|
|
1190
|
-
rescue StandardError
|
|
1191
|
-
nil
|
|
1205
|
+
model_setting(nil) { |model| model.reasoning_history_cache_cost(from, to, conversation: @session&.messages) }
|
|
1192
1206
|
end
|
|
1193
1207
|
|
|
1194
1208
|
# Whether the session has a model reply (a cache to restart at all).
|
|
@@ -1198,7 +1212,7 @@ module Samagotchi
|
|
|
1198
1212
|
|
|
1199
1213
|
# The session's own reasoning_history level, nil when it has none.
|
|
1200
1214
|
# @return [Symbol, nil]
|
|
1201
|
-
def reasoning_history_override = @session
|
|
1215
|
+
def reasoning_history_override = SessionSetting::REASONING_HISTORY.own(@session)
|
|
1202
1216
|
|
|
1203
1217
|
# Set the session's own reasoning_history level (nil unsets it); the
|
|
1204
1218
|
# next turn's start resolves it. The caller saves the session.
|
|
@@ -1206,19 +1220,18 @@ module Samagotchi
|
|
|
1206
1220
|
def reasoning_history_override=(level)
|
|
1207
1221
|
raise ArgumentError, "no session yet" unless @session
|
|
1208
1222
|
|
|
1209
|
-
@session
|
|
1223
|
+
SessionSetting::REASONING_HISTORY.set(@session, level)
|
|
1210
1224
|
end
|
|
1211
1225
|
|
|
1212
1226
|
# The session's own thinking level, nil when it has none.
|
|
1213
1227
|
# @return [Symbol, nil]
|
|
1214
|
-
def thinking_override = @session
|
|
1228
|
+
def thinking_override = SessionSetting::THINKING.own(@session)
|
|
1215
1229
|
|
|
1216
1230
|
# What a thinking level change costs the effective model's prompt
|
|
1217
1231
|
# cache (Thinking.cache_cost): :full, :provider or :tail.
|
|
1218
1232
|
# @return [Symbol]
|
|
1219
1233
|
def thinking_cache_cost
|
|
1220
|
-
|
|
1221
|
-
Thinking.cache_cost(target, profile: target.entry&.chat? ? nil : profile&.name)
|
|
1234
|
+
effective_model.thinking_cache_cost { profile }
|
|
1222
1235
|
end
|
|
1223
1236
|
|
|
1224
1237
|
# Set the session's own thinking level (nil unsets it); the next turn's
|
|
@@ -1228,7 +1241,7 @@ module Samagotchi
|
|
|
1228
1241
|
def thinking_override=(level)
|
|
1229
1242
|
raise ArgumentError, "no session yet" unless @session
|
|
1230
1243
|
|
|
1231
|
-
@session
|
|
1244
|
+
SessionSetting::THINKING.set(@session, level)
|
|
1232
1245
|
end
|
|
1233
1246
|
|
|
1234
1247
|
# The model the server serves for the current model, and the name asked
|
|
@@ -1237,17 +1250,15 @@ module Samagotchi
|
|
|
1237
1250
|
# else [nil, nil].
|
|
1238
1251
|
# @return [Array(String, String), Array(nil, nil)]
|
|
1239
1252
|
def served_model(probe: true)
|
|
1240
|
-
served_model_for(@metrics.served_report,
|
|
1253
|
+
served_model_for(@metrics.served_report, model: probe ? effective_model : nil)
|
|
1241
1254
|
end
|
|
1242
1255
|
|
|
1243
1256
|
# The host whose hosts.<name>.models.<id>.served names +served+ for the
|
|
1244
1257
|
# current model (a gateway's round-robin targets): not a mismatch to
|
|
1245
1258
|
# warn about. nil when none does.
|
|
1246
1259
|
# @return [String, nil]
|
|
1247
|
-
def served_expected_by(served
|
|
1248
|
-
|
|
1249
|
-
rescue StandardError
|
|
1250
|
-
nil
|
|
1260
|
+
def served_expected_by(served)
|
|
1261
|
+
model_setting(nil) { |model| model.served_expected_by(served) }
|
|
1251
1262
|
end
|
|
1252
1263
|
|
|
1253
1264
|
def served_expected?(served) = !served_expected_by(served).nil?
|
|
@@ -1279,6 +1290,10 @@ module Samagotchi
|
|
|
1279
1290
|
def session_state_snapshot
|
|
1280
1291
|
served_pair = served_model(probe: false)
|
|
1281
1292
|
metrics = @metrics.snapshot
|
|
1293
|
+
# One resolve for the model's fields; nil when it doesn't resolve (the
|
|
1294
|
+
# Bridge's /state and reset frames rely on this never raising): those
|
|
1295
|
+
# fields are nil then, as their own rescues always gave.
|
|
1296
|
+
model = model_setting(nil) { |resolved| resolved }
|
|
1282
1297
|
{
|
|
1283
1298
|
status: @session&.status,
|
|
1284
1299
|
message_count: (@session&.messages || []).size,
|
|
@@ -1294,12 +1309,9 @@ module Samagotchi
|
|
|
1294
1309
|
model_name: effective_model_ref,
|
|
1295
1310
|
served_model: served_pair[0],
|
|
1296
1311
|
served_model_for: served_pair[1],
|
|
1297
|
-
served_expected_by: served_expected_by(served_pair[0]),
|
|
1298
|
-
context_status: @last_context_status&.dup || saved_context_status(metrics[:context]),
|
|
1299
|
-
|
|
1300
|
-
thinking: thinking_explained&.summary,
|
|
1301
|
-
reasoning_history: reasoning_history_explained&.summary,
|
|
1302
|
-
toolkit: toolkit_summary,
|
|
1312
|
+
served_expected_by: model&.served_expected_by(served_pair[0]),
|
|
1313
|
+
context_status: @last_context_status&.dup || saved_context_status(metrics[:context], model: model),
|
|
1314
|
+
**setting_chips(SessionSetting::ALL, model),
|
|
1303
1315
|
recap_enabled: !@recap.nil?,
|
|
1304
1316
|
recap_min_user_turns: @recap&.min_user_turns,
|
|
1305
1317
|
recap_inactivity_seconds: @recap&.inactivity&.to_i
|
|
@@ -1494,7 +1506,8 @@ module Samagotchi
|
|
|
1494
1506
|
|
|
1495
1507
|
def save_quietly(session)
|
|
1496
1508
|
session.save(state_dir: session_state_dir)
|
|
1497
|
-
rescue StandardError
|
|
1509
|
+
rescue StandardError => e
|
|
1510
|
+
Log.warn(:turn, "turn_end_save_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
1498
1511
|
nil
|
|
1499
1512
|
end
|
|
1500
1513
|
private :save_quietly
|
|
@@ -1532,122 +1545,59 @@ module Samagotchi
|
|
|
1532
1545
|
end
|
|
1533
1546
|
private :store_answer_display
|
|
1534
1547
|
|
|
1535
|
-
# ── The hook runtime (
|
|
1536
|
-
|
|
1537
|
-
#
|
|
1538
|
-
#
|
|
1539
|
-
def hook_runtime
|
|
1540
|
-
Hooks::Runtime.new(
|
|
1541
|
-
notify: ->(text:, level:, hook:, fallback_for: nil) { hook_notify(text, level, hook, fallback_for: fallback_for) },
|
|
1542
|
-
ask_user: ->(question:, options:, header:, allow_freeform:, hook:) { hook_ask_user(question, options, header, allow_freeform, hook) },
|
|
1543
|
-
stop_turn: ->(reason:, hook:) { hook_stop_turn(reason, hook) },
|
|
1544
|
-
steer: ->(text:, hook:) { steer(text, source: hook_source(hook)) },
|
|
1545
|
-
stop_generation: ->(reason:, hook:) { hook_stop_generation(reason, hook) }
|
|
1546
|
-
)
|
|
1547
|
-
end
|
|
1548
|
+
# ── The hook runtime (HookRoutes) ───────────────────────────────────────
|
|
1549
|
+
|
|
1550
|
+
# What a hook (Hooks::Runtime) and a plugin (Plugin::Host) can do beyond
|
|
1551
|
+
# reading its event, routed to the UIs by HookRoutes; these forward.
|
|
1552
|
+
def hook_runtime = @hook_routes.runtime
|
|
1548
1553
|
private :hook_runtime
|
|
1549
1554
|
|
|
1550
|
-
|
|
1551
|
-
# ("<file> (bundle <name>)"), the label of any other.
|
|
1552
|
-
def hook_source(hook)
|
|
1553
|
-
hook.to_s[/\(bundle (.+)\)\z/, 1] || hook.to_s
|
|
1554
|
-
end
|
|
1555
|
+
def hook_source(hook) = @hook_routes.source(hook)
|
|
1555
1556
|
private :hook_source
|
|
1556
1557
|
|
|
1557
1558
|
# What a notice may stand in for (hook_notify's fallback_for:).
|
|
1558
|
-
NOTICE_FALLBACKS =
|
|
1559
|
+
NOTICE_FALLBACKS = HookRoutes::NOTICE_FALLBACKS
|
|
1559
1560
|
|
|
1560
|
-
# One line to the user (:hook_notice).
|
|
1561
|
-
# event: the turn's sink (the REPL) and the observers (bridge, log).
|
|
1562
|
-
# Outside one (a plugin's command at the prompt) it is announced with
|
|
1563
|
-
# between_turns: true, which every UI shows as cards are shown.
|
|
1564
|
-
#
|
|
1565
|
-
# +fallback_for+ names what the line stands in for (NOTICE_FALLBACKS):
|
|
1566
|
-
# :display, the answer's display (AnswerDisplay), for a UI that doesn't
|
|
1567
|
-
# render its links; :editor, a ref the viewer could open in its editor
|
|
1568
|
-
# (Web::Capabilities#editor), for a UI or a viewer that can't. A UI that
|
|
1569
|
-
# does leaves the line out; the others, the log among them, show it. An
|
|
1570
|
-
# unknown value is dropped, so the line shows everywhere.
|
|
1571
|
-
# +extra+: more fields for the UIs (a toolkit change's summary).
|
|
1561
|
+
# One line to the user (:hook_notice), HookRoutes#notify.
|
|
1572
1562
|
def hook_notify(text, level, hook, fallback_for: nil, extra: {})
|
|
1573
|
-
level
|
|
1574
|
-
level = :info unless %i[info warn].include?(level)
|
|
1575
|
-
notice = { type: :hook_notice, hook: hook.to_s, text: text.to_s, level: level,
|
|
1576
|
-
fallback_for: notice_fallback(fallback_for, hook) }.compact.merge(extra)
|
|
1577
|
-
return @extension_load.hold(notice) && nil if @extension_load.loading?
|
|
1578
|
-
|
|
1579
|
-
in_turn, sink = @turn_state.in_turn_sink
|
|
1580
|
-
if anytime_thread?
|
|
1581
|
-
announce_anytime(notice.merge(between_turns: true))
|
|
1582
|
-
elsif current_init_task
|
|
1583
|
-
announce(notice.merge(between_turns: true))
|
|
1584
|
-
elsif in_turn
|
|
1585
|
-
emit_event(sink, notice)
|
|
1586
|
-
else
|
|
1587
|
-
announce_or_hold(notice.merge(between_turns: true))
|
|
1588
|
-
end
|
|
1589
|
-
nil
|
|
1563
|
+
@hook_routes.notify(text, level, hook, fallback_for: fallback_for, extra: extra)
|
|
1590
1564
|
end
|
|
1591
1565
|
private :hook_notify
|
|
1592
1566
|
|
|
1593
|
-
|
|
1594
|
-
# one is logged once at debug.
|
|
1595
|
-
def notice_fallback(value, hook)
|
|
1596
|
-
return if value.nil?
|
|
1597
|
-
|
|
1598
|
-
fallback = value.to_s.to_sym
|
|
1599
|
-
return fallback if NOTICE_FALLBACKS.include?(fallback)
|
|
1600
|
-
|
|
1601
|
-
@unknown_fallbacks ||= Set.new
|
|
1602
|
-
Log.debug(:hooks, "notice_unknown_fallback", hook: hook.to_s, value: value.to_s) if @unknown_fallbacks.add?(fallback)
|
|
1603
|
-
nil
|
|
1604
|
-
end
|
|
1605
|
-
private :notice_fallback
|
|
1606
|
-
|
|
1607
|
-
# A guardrails line to the user after load (:guardrail_warning, as the
|
|
1608
|
-
# load failures show): a running turn's event, else announced.
|
|
1609
|
-
def guardrail_notify(message)
|
|
1610
|
-
event = { type: :guardrail_warning, message: message.to_s }
|
|
1611
|
-
in_turn, sink = @turn_state.in_turn_sink
|
|
1612
|
-
in_turn ? emit_event(sink, event) : announce_or_hold(event)
|
|
1613
|
-
nil
|
|
1614
|
-
end
|
|
1567
|
+
def guardrail_notify(message) = @hook_routes.guardrail_notify(message)
|
|
1615
1568
|
private :guardrail_notify
|
|
1616
1569
|
|
|
1617
|
-
# A question through the question flow (REPL sync handler, attached TUI,
|
|
1618
|
-
# web), single-select, kind "hook". A --non-interactive run has no one
|
|
1619
|
-
# to ask: nil at once. Anything but an answer (a sync handler's text, no
|
|
1620
|
-
# answer, cancelled) is nil too.
|
|
1621
1570
|
# @return [Hash, nil] {selected:, freeform:, selected_indices:}
|
|
1622
1571
|
def hook_ask_user(question, options, header, allow_freeform, hook)
|
|
1623
|
-
@
|
|
1572
|
+
@hook_routes.ask_user(question, options, header, allow_freeform, hook)
|
|
1624
1573
|
end
|
|
1625
1574
|
private :hook_ask_user
|
|
1626
1575
|
|
|
1627
|
-
# Cancel the running turn (reason :hook), after a notice that says why.
|
|
1628
|
-
# The gate denies the rest of a tool batch once the controller is
|
|
1629
|
-
# cancelled; the next request ends the turn as :turn_canceled.
|
|
1630
1576
|
# @return [Boolean] true when a running turn was cancelled now
|
|
1631
|
-
def hook_stop_turn(reason, hook)
|
|
1632
|
-
ctrl = active_cancel_controller
|
|
1633
|
-
return false unless ctrl && !ctrl.cancelled?
|
|
1634
|
-
|
|
1635
|
-
hook_notify("stopped the turn: #{reason}", :warn, hook)
|
|
1636
|
-
ctrl.cancel!(:hook, { by: hook_source(hook), reason: reason.to_s })
|
|
1637
|
-
end
|
|
1577
|
+
def hook_stop_turn(reason, hook) = @hook_routes.stop_turn(reason, hook)
|
|
1638
1578
|
private :hook_stop_turn
|
|
1639
1579
|
|
|
1640
|
-
# Cut the streaming generation (reason :hook); the turn goes on and asks
|
|
1641
|
-
# the model again (the loops' cut path). No notice: the plugin posts its
|
|
1642
|
-
# own. The bundle and +reason+ go to the log and the retry's nudge.
|
|
1643
1580
|
# @return [Boolean] true when a streaming generation was cut now
|
|
1644
|
-
def hook_stop_generation(reason, hook)
|
|
1645
|
-
|
|
1646
|
-
return false unless ctrl
|
|
1581
|
+
def hook_stop_generation(reason, hook) = @hook_routes.stop_generation(reason, hook)
|
|
1582
|
+
private :hook_stop_generation
|
|
1647
1583
|
|
|
1648
|
-
|
|
1584
|
+
# The hook routes (see #initialize for when).
|
|
1585
|
+
def build_hook_routes
|
|
1586
|
+
HookRoutes.new(
|
|
1587
|
+
extension_load: -> { @extension_load },
|
|
1588
|
+
in_turn_sink: -> { @turn_state.in_turn_sink },
|
|
1589
|
+
anytime_thread: -> { anytime_thread? },
|
|
1590
|
+
announce_anytime: ->(event) { announce_anytime(event) },
|
|
1591
|
+
init_task: -> { current_init_task },
|
|
1592
|
+
announce: ->(event) { announce(event) },
|
|
1593
|
+
announce_or_hold: ->(event) { announce_or_hold(event) },
|
|
1594
|
+
emit: ->(sink, event) { emit_event(sink, event) },
|
|
1595
|
+
question_desk: -> { @question_desk },
|
|
1596
|
+
cancel_controller: -> { active_cancel_controller },
|
|
1597
|
+
steer: ->(text, source) { steer(text, source: source) }
|
|
1598
|
+
)
|
|
1649
1599
|
end
|
|
1650
|
-
private :
|
|
1600
|
+
private :build_hook_routes
|
|
1651
1601
|
|
|
1652
1602
|
# ── Ask-user-question (structured qualification) ──────────────────────────
|
|
1653
1603
|
|
|
@@ -1717,15 +1667,17 @@ module Samagotchi
|
|
|
1717
1667
|
# the chat loop's leaves out the raw-prompt tool text, since its tools go
|
|
1718
1668
|
# as schemas. Built once per loop (and again after a model switch) so the
|
|
1719
1669
|
# prompt prefix, and the server's KV cache for it, stay stable.
|
|
1720
|
-
# @param
|
|
1670
|
+
# @param model [EffectiveModel, nil]
|
|
1721
1671
|
# @return [String]
|
|
1722
1672
|
# The native prompt depends on the thinking level too (Gemma's token,
|
|
1723
|
-
# Qwen's turn preamble): a level change builds it again.
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1673
|
+
# Qwen's turn preamble): a level change builds it again. A build that
|
|
1674
|
+
# misses the cache names the model and the level through its own
|
|
1675
|
+
# lambdas (#model_identity, #turn_thinking), which resolve again.
|
|
1676
|
+
def system_prompt(model = nil)
|
|
1677
|
+
model ||= effective_model
|
|
1678
|
+
chat = model.chat?
|
|
1679
|
+
level = chat ? nil : thinking_level(model)
|
|
1680
|
+
prompt = @prompt_builder.build(chat: chat, thinking: level, layers: model.llm_context_layers(llm_context_session_args))
|
|
1729
1681
|
# The memory indexes this prompt holds: /stats, the snapshot, analytics.json.
|
|
1730
1682
|
@metrics.memory_index = @prompt_builder.memory_index
|
|
1731
1683
|
# The model notes it carries: the session records them (/stats, /model, the web).
|
|
@@ -1733,16 +1685,6 @@ module Samagotchi
|
|
|
1733
1685
|
prompt
|
|
1734
1686
|
end
|
|
1735
1687
|
|
|
1736
|
-
# The LLM context layers +target+'s turns run under: the native prompt
|
|
1737
|
-
# declares the tools they add (forget_outputs under forget), so a
|
|
1738
|
-
# model switch may change the tool list. [] under none.
|
|
1739
|
-
def llm_context_layers(target)
|
|
1740
|
-
LLMContextStrategy.resolve(target, names: model_lookup_names(target), **llm_context_session_args).active_layers
|
|
1741
|
-
rescue StandardError
|
|
1742
|
-
[]
|
|
1743
|
-
end
|
|
1744
|
-
private :llm_context_layers
|
|
1745
|
-
|
|
1746
1688
|
# The base prompt (specs, plugins' declarations).
|
|
1747
1689
|
def assist_system_prompt(chat: false, thinking: nil)
|
|
1748
1690
|
@prompt_builder.base(chat: chat, thinking: thinking)
|
|
@@ -2073,14 +2015,16 @@ module Samagotchi
|
|
|
2073
2015
|
# sampling and thinking, the thinking resolved before the hooks run.
|
|
2074
2016
|
# The model name is added in #generate, after the hooks that may switch
|
|
2075
2017
|
# the model.
|
|
2018
|
+
# One resolve for all of them: nothing runs between them that could
|
|
2019
|
+
# change what a resolve gives.
|
|
2076
2020
|
def turn_settings(session)
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
LLM::TurnSettings.new(vision: vision, sampling: sampling, thinking:
|
|
2083
|
-
reasoning_history: reasoning_history_level(
|
|
2021
|
+
model = effective_model
|
|
2022
|
+
vision = turn_vision(session, model)
|
|
2023
|
+
sampling = model.sampling
|
|
2024
|
+
level = thinking_level(model)
|
|
2025
|
+
@thinking_notices.turn_started(level, model.target)
|
|
2026
|
+
LLM::TurnSettings.new(vision: vision, sampling: sampling, thinking: level,
|
|
2027
|
+
reasoning_history: reasoning_history_level(model), model_name: nil,
|
|
2084
2028
|
window_setting: nil, llm_context: nil, price: nil)
|
|
2085
2029
|
end
|
|
2086
2030
|
private :turn_settings
|
|
@@ -2102,12 +2046,12 @@ module Samagotchi
|
|
|
2102
2046
|
def warm_up_next_turn(turn, result)
|
|
2103
2047
|
return if result.canceled? || result.resumable?
|
|
2104
2048
|
|
|
2105
|
-
|
|
2106
|
-
client =
|
|
2107
|
-
return unless warmup_target?(
|
|
2049
|
+
model = effective_model
|
|
2050
|
+
client = model.client
|
|
2051
|
+
return unless warmup_target?(model, client)
|
|
2108
2052
|
return if reminders_due? || @turn_state.carried_steers? || @next_turn_waiting&.call
|
|
2109
2053
|
|
|
2110
|
-
messages = ContextNote.with_system_head(turn.session.messages.dup, { role: "system", content: system_prompt(
|
|
2054
|
+
messages = ContextNote.with_system_head(turn.session.messages.dup, { role: "system", content: system_prompt(model) })
|
|
2111
2055
|
# Under the next turn's strategy (a /model switch may have changed it);
|
|
2112
2056
|
# the last turn's when it can't be resolved.
|
|
2113
2057
|
prompt, images = @kernel.warmup_prompt(messages, llm_context: turn_llm_context || @kernel.turn_settings&.llm_context)
|
|
@@ -2124,8 +2068,8 @@ module Samagotchi
|
|
|
2124
2068
|
# Never a paid or remote host (cache.warmup auto means local only), nor
|
|
2125
2069
|
# the chat loop (Splash keeps its own cache; llama.cpp's chat path is
|
|
2126
2070
|
# not measured), nor a non-llama.cpp transport.
|
|
2127
|
-
def warmup_target?(
|
|
2128
|
-
return false if
|
|
2071
|
+
def warmup_target?(model, client)
|
|
2072
|
+
return false if model.chat? || model.entry.remote?
|
|
2129
2073
|
return false unless client.respond_to?(:warm_up) && client.respond_to?(:transport) && client.transport.native?
|
|
2130
2074
|
|
|
2131
2075
|
PromptWarmup.enabled?
|
|
@@ -2166,16 +2110,19 @@ module Samagotchi
|
|
|
2166
2110
|
|
|
2167
2111
|
# Ask the effective model's backend.
|
|
2168
2112
|
def generate(turn, max_iterations:, max_tool_output_chars:, pending_input:)
|
|
2169
|
-
|
|
2113
|
+
# One resolve for the request: nothing runs between these reads that
|
|
2114
|
+
# could change what a resolve gives (the hooks ran before).
|
|
2115
|
+
model = effective_model
|
|
2116
|
+
sync_kernel_client!(model)
|
|
2170
2117
|
# Route model name as bare (without host prefix) to the transport;
|
|
2171
2118
|
# host selection already done via active client.
|
|
2172
|
-
bare_for_backend = bare_model_name(
|
|
2119
|
+
bare_for_backend = bare_model_name(model.name)
|
|
2173
2120
|
# The turn's settings, set once here. The chat loop dispatches tools
|
|
2174
2121
|
# through the kernel without its #run: tag those dumps with this
|
|
2175
2122
|
# turn's model, not the last native one.
|
|
2176
|
-
explained = llm_context_explained
|
|
2177
|
-
@kernel.turn_settings = turn.settings.with(model_name: bare_for_backend, window_setting:
|
|
2178
|
-
llm_context: explained&.resolved, price:
|
|
2123
|
+
explained = model.llm_context_explained(llm_context_session_args)
|
|
2124
|
+
@kernel.turn_settings = turn.settings.with(model_name: bare_for_backend, window_setting: model.window_setting,
|
|
2125
|
+
llm_context: explained&.resolved, price: model.price)
|
|
2179
2126
|
log_llm_context(explained)
|
|
2180
2127
|
|
|
2181
2128
|
turn.limit = @no_interrupt ? IterationLimit.for(no_interrupt: true) : max_iterations || IterationLimit.for
|
|
@@ -2183,7 +2130,7 @@ module Samagotchi
|
|
|
2183
2130
|
# end keeps it, so a Continue resumes this run.
|
|
2184
2131
|
run_start = turn.messages.length
|
|
2185
2132
|
reasoning_since = resumed_run_start(turn, run_start)
|
|
2186
|
-
result = backend.complete(
|
|
2133
|
+
result = backend(model).complete(
|
|
2187
2134
|
messages: turn.messages,
|
|
2188
2135
|
max_iterations: turn.limit,
|
|
2189
2136
|
on_stream_event: build_stream_event_handler(turn.on_event, cancel_controller: turn.controller),
|
|
@@ -2228,8 +2175,8 @@ module Samagotchi
|
|
|
2228
2175
|
# Persist deduped used memories onto the session for Web + reload.
|
|
2229
2176
|
begin
|
|
2230
2177
|
session.used_memory_names = used_memory_names
|
|
2231
|
-
rescue StandardError
|
|
2232
|
-
|
|
2178
|
+
rescue StandardError => e
|
|
2179
|
+
Log.warn(:memory, "used_memories_store_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2233
2180
|
end
|
|
2234
2181
|
# Notify live observers of the updated memory list (so yellow bar refreshes
|
|
2235
2182
|
# even without a tool_call event if the preload was the only addition).
|
|
@@ -2238,8 +2185,8 @@ module Samagotchi
|
|
|
2238
2185
|
|
|
2239
2186
|
begin
|
|
2240
2187
|
emit_event(on_event, { type: :used_memories_updated, used_memory_names: used_memory_names })
|
|
2241
|
-
rescue StandardError
|
|
2242
|
-
|
|
2188
|
+
rescue StandardError => e
|
|
2189
|
+
Log.warn(:memory, "used_memories_emit_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2243
2190
|
end
|
|
2244
2191
|
end
|
|
2245
2192
|
private :publish_used_memories
|
|
@@ -2397,164 +2344,30 @@ module Samagotchi
|
|
|
2397
2344
|
|
|
2398
2345
|
# The turn's VisionContext: the session's images folder, and whether the
|
|
2399
2346
|
# effective model can see images, asked only when a request carries one.
|
|
2400
|
-
def turn_vision(session)
|
|
2401
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
2347
|
+
def turn_vision(session, model = effective_model)
|
|
2402
2348
|
VisionContext.new(session_dir: Session.session_dir(session.id, state_dir: session_state_dir),
|
|
2403
|
-
capability:
|
|
2404
|
-
VisionSupport.for(target, profile: profile, adapter: vision_adapter(target), names: model_lookup_names(target))
|
|
2405
|
-
})
|
|
2349
|
+
capability: -> { model.vision_support(profile) })
|
|
2406
2350
|
end
|
|
2407
2351
|
|
|
2408
2352
|
# The effective model's request parameters (hosts: and models:
|
|
2409
2353
|
# sampling), read per turn: /model can change the model between turns.
|
|
2410
|
-
def turn_sampling
|
|
2411
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
2412
|
-
SamplingSettings.for(target, names: model_lookup_names(target))
|
|
2413
|
-
rescue StandardError
|
|
2414
|
-
SamplingSettings::EMPTY
|
|
2415
|
-
end
|
|
2354
|
+
def turn_sampling = model_setting(SamplingSettings::EMPTY, &:sampling)
|
|
2416
2355
|
|
|
2417
2356
|
# The effective model's thinking level (Thinking.resolve), read each turn.
|
|
2418
|
-
def turn_thinking
|
|
2419
|
-
thinking_level(@host_registry.resolve(@effective_model_name))
|
|
2420
|
-
rescue StandardError
|
|
2421
|
-
Thinking::DEFAULT
|
|
2422
|
-
end
|
|
2423
|
-
|
|
2424
|
-
# An effort on a native host, which has no knob for it: said once per
|
|
2425
|
-
# session and host.
|
|
2426
|
-
def announce_thinking_level(level, target)
|
|
2427
|
-
return if target.entry.chat? || Thinking.native(level, profile).honoured
|
|
2428
|
-
|
|
2429
|
-
thinking_notice_once(:unsupported, target, :info,
|
|
2430
|
-
"#{level} isn't supported by native #{profile.name} on #{target.entry.name}; " \
|
|
2431
|
-
"thinking stays as the model has it")
|
|
2432
|
-
rescue StandardError
|
|
2433
|
-
nil
|
|
2434
|
-
end
|
|
2435
|
-
|
|
2436
|
-
# An effort on a llama.cpp chat host whose chat template takes none
|
|
2437
|
-
# (its /props says so): said once per session and host, as for a native
|
|
2438
|
-
# host. Read from the /props answer the turn's window probe left in the
|
|
2439
|
-
# cache, so it asks the server nothing.
|
|
2440
|
-
def announce_effort_ignored
|
|
2441
|
-
level, target = @turn_thinking
|
|
2442
|
-
return unless target&.entry&.chat? && Thinking::EFFORTS.include?(level)
|
|
2443
|
-
|
|
2444
|
-
client = target.client
|
|
2445
|
-
props = client.cached_server_props(model: target.bare_model)
|
|
2446
|
-
return unless Thinking.effort_ignored?(level, props)
|
|
2447
|
-
|
|
2448
|
-
thinking_notice_once(:unsupported, target, :info,
|
|
2449
|
-
"#{level} isn't supported by #{target.bare_model}'s chat template on #{target.entry.name} " \
|
|
2450
|
-
"(/props: supports_reasoning_effort false); thinking stays as the model has it")
|
|
2451
|
-
rescue StandardError
|
|
2452
|
-
nil
|
|
2453
|
-
end
|
|
2357
|
+
def turn_thinking = model_setting(Thinking::DEFAULT) { |model| thinking_level(model) }
|
|
2454
2358
|
|
|
2455
|
-
#
|
|
2456
|
-
# streamed none, false while only whitespace (Gemma 4's empty thought
|
|
2457
|
-
# with thinking off), true once a real thought came.
|
|
2458
|
-
def note_generation_thought(thinking)
|
|
2459
|
-
return if thinking.empty?
|
|
2460
|
-
|
|
2461
|
-
@generation_thought ||= thinking.match?(/\S/)
|
|
2462
|
-
end
|
|
2463
|
-
|
|
2464
|
-
# Thinking off, and the model thought anyway: logged each time, said
|
|
2465
|
-
# once per session and host. A thinking lane of whitespace alone is no
|
|
2466
|
-
# thought.
|
|
2467
|
-
def check_thinking_honoured(event)
|
|
2468
|
-
level, target = @turn_thinking
|
|
2469
|
-
chars = event[:thinking_chars].to_i
|
|
2470
|
-
return unless level == :off && chars.positive? && target
|
|
2471
|
-
return if @generation_thought == false
|
|
2472
|
-
|
|
2473
|
-
Log.warn(:model, "thinking_not_honoured", level: level, host: target.entry.name, model: target.bare_model, chars: chars)
|
|
2474
|
-
thinking_notice_once(:not_honoured, target, :warn,
|
|
2475
|
-
"off wasn't honoured by #{target.bare_model} on #{target.entry.name} " \
|
|
2476
|
-
"(#{chars} chars of thinking); a sampling: override on the host or model may turn it off " \
|
|
2477
|
-
"(see Thinking in docs/configuration.md)")
|
|
2478
|
-
rescue StandardError
|
|
2479
|
-
nil
|
|
2480
|
-
end
|
|
2481
|
-
|
|
2482
|
-
# The host refused the level's request fields (gpt-oss can't turn
|
|
2483
|
-
# thinking off) and the chat loop sent the request without them: said
|
|
2484
|
-
# once per session and host, standing in for the not-honoured notice.
|
|
2485
|
-
def thinking_refused(event)
|
|
2486
|
-
_level, target = @turn_thinking
|
|
2487
|
-
return unless target
|
|
2488
|
-
|
|
2489
|
-
Log.warn(:model, "thinking_refused", level: event[:level], host: target.entry.name, model: event[:model],
|
|
2490
|
-
detail: event[:detail])
|
|
2491
|
-
(@thinking_notices ||= Set.new) << [@session&.id, target.entry.name, :not_honoured]
|
|
2492
|
-
thinking_notice_once(:refused, target, :warn,
|
|
2493
|
-
"#{target.entry.name} refused thinking: #{event[:level]} for #{event[:model]} (#{event[:detail]}); " \
|
|
2494
|
-
"sent without it, so thinking stays as the model has it")
|
|
2495
|
-
rescue StandardError
|
|
2496
|
-
nil
|
|
2497
|
-
end
|
|
2498
|
-
|
|
2499
|
-
# The host refused a sampling field (the empty-answer retry's
|
|
2500
|
-
# temperature on a model that takes none) and the chat loop asked again
|
|
2501
|
-
# without them, as it will for the model from now on: a log line.
|
|
2502
|
-
def sampling_refused(event)
|
|
2503
|
-
Log.info(:model, "sampling_refused", model: event[:model], detail: event[:detail])
|
|
2504
|
-
rescue StandardError
|
|
2505
|
-
nil
|
|
2506
|
-
end
|
|
2507
|
-
|
|
2508
|
-
# The host refused the model's reasoning sent back (reasoning_history)
|
|
2509
|
-
# and the chat loop asked the step again without it, as it will from
|
|
2510
|
-
# now on: logged, and said once per session and host.
|
|
2511
|
-
def reasoning_history_refused(event)
|
|
2512
|
-
_level, target = @turn_thinking
|
|
2513
|
-
return unless target
|
|
2514
|
-
|
|
2515
|
-
Log.warn(:model, "reasoning_history_refused", level: event[:level], host: target.entry.name, model: event[:model],
|
|
2516
|
-
detail: event[:detail])
|
|
2517
|
-
return unless (@thinking_notices ||= Set.new).add?([@session&.id, target.entry.name, :reasoning_history_refused])
|
|
2518
|
-
|
|
2519
|
-
hook_notify("#{target.entry.name} refused reasoning sent back (reasoning_history: #{event[:level]}) for " \
|
|
2520
|
-
"#{event[:model]} (#{event[:detail]}); sent without it from now on", :warn, "reasoning history")
|
|
2521
|
-
rescue StandardError
|
|
2522
|
-
nil
|
|
2523
|
-
end
|
|
2524
|
-
|
|
2525
|
-
def thinking_notice_once(kind, target, level, text)
|
|
2526
|
-
key = [@session&.id, target.entry.name, kind]
|
|
2527
|
-
return unless (@thinking_notices ||= Set.new).add?(key)
|
|
2528
|
-
|
|
2529
|
-
hook_notify(text, level, "thinking")
|
|
2530
|
-
end
|
|
2531
|
-
|
|
2532
|
-
# +target+'s ReasoningHistory level, the session's own first; the
|
|
2359
|
+
# +model+'s ReasoningHistory level, the session's own first; the
|
|
2533
2360
|
# default when it can't be resolved.
|
|
2534
|
-
def reasoning_history_level(
|
|
2535
|
-
ReasoningHistory.resolve(target, names: model_lookup_names(target), session: reasoning_history_override).first
|
|
2536
|
-
rescue StandardError
|
|
2537
|
-
ReasoningHistory::DEFAULT
|
|
2538
|
-
end
|
|
2361
|
+
def reasoning_history_level(model) = model.reasoning_history(own: reasoning_history_override)
|
|
2539
2362
|
|
|
2540
|
-
# +
|
|
2541
|
-
def thinking_level(
|
|
2542
|
-
Thinking.resolve(target, names: model_lookup_names(target), session: thinking_override).first
|
|
2543
|
-
rescue StandardError
|
|
2544
|
-
Thinking::DEFAULT
|
|
2545
|
-
end
|
|
2546
|
-
|
|
2547
|
-
def vision_adapter(target)
|
|
2548
|
-
target.entry.chat? ? @host_registry.adapter_for(target.entry) : nil
|
|
2549
|
-
rescue StandardError
|
|
2550
|
-
nil
|
|
2551
|
-
end
|
|
2363
|
+
# +model+'s thinking level (Thinking.resolve), the session's own first.
|
|
2364
|
+
def thinking_level(model) = model.thinking(own: thinking_override)
|
|
2552
2365
|
|
|
2553
2366
|
# A model known not to see images fails a turn with images up front.
|
|
2554
2367
|
def refuse_images!(vision)
|
|
2555
2368
|
return if vision.sendable?
|
|
2556
2369
|
|
|
2557
|
-
host =
|
|
2370
|
+
host = effective_model.host_name
|
|
2558
2371
|
raise LLM::VisionUnsupported.new("#{host}: #{vision.refusal_reason}", host: host)
|
|
2559
2372
|
end
|
|
2560
2373
|
|
|
@@ -2649,14 +2462,14 @@ module Samagotchi
|
|
|
2649
2462
|
session.messages = messages
|
|
2650
2463
|
end
|
|
2651
2464
|
|
|
2652
|
-
# Pick the loop for
|
|
2653
|
-
# host's adapter (one per host, kept for its cached model list).
|
|
2654
|
-
def backend_for(
|
|
2655
|
-
return @native_backend unless
|
|
2465
|
+
# Pick the loop for +model+ (EffectiveModel); the chat loop is pointed
|
|
2466
|
+
# at its host's adapter (one per host, kept for its cached model list).
|
|
2467
|
+
def backend_for(model)
|
|
2468
|
+
return @native_backend unless model.chat?
|
|
2656
2469
|
|
|
2657
2470
|
@chat_backend_mutex.synchronize do
|
|
2658
2471
|
@chat_backend ||= LLM::ChatLoop.new(kernel: @kernel, stable_length: ->(text) { @prompt_builder.stable_length(text) })
|
|
2659
|
-
@chat_backend.adapter = @host_registry.adapter_for(
|
|
2472
|
+
@chat_backend.adapter = @host_registry.adapter_for(model.entry)
|
|
2660
2473
|
@chat_backend.session_id = session&.id
|
|
2661
2474
|
@chat_backend
|
|
2662
2475
|
end
|
|
@@ -2676,12 +2489,16 @@ module Samagotchi
|
|
|
2676
2489
|
add_init_task(bundle: bundle, label: label, plugin_label: plugin_label, provides_tools: provides_tools,
|
|
2677
2490
|
quiet: quiet, timeout: timeout, failed: failed, &block)
|
|
2678
2491
|
},
|
|
2679
|
-
toolkits: @plugin_kits
|
|
2492
|
+
toolkits: @toolkit_binding.plugin_kits,
|
|
2493
|
+
tool_index: @tool_index,
|
|
2494
|
+
peer_sources: @peer_sources
|
|
2680
2495
|
)
|
|
2681
2496
|
# What plugins show while they load (an MCP server that didn't
|
|
2682
2497
|
# start) waits for the first turn, beside the load warnings: no UI
|
|
2683
2498
|
# is there yet, and the Engine isn't built.
|
|
2684
2499
|
@extension_load.load_plugins(registries, failures: @plugin_failures)
|
|
2500
|
+
# Declared once, from what the sources published while they loaded.
|
|
2501
|
+
@tool_index.declare(@tools, failures: @plugin_failures)
|
|
2685
2502
|
end
|
|
2686
2503
|
|
|
2687
2504
|
# Keep a plugin's new tool set (chi.replace_tools, from any thread)
|
|
@@ -2699,100 +2516,42 @@ module Samagotchi
|
|
|
2699
2516
|
end
|
|
2700
2517
|
public :apply_staged_tools!
|
|
2701
2518
|
|
|
2702
|
-
# The
|
|
2703
|
-
#
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
def toolkit_kits(errors = [])
|
|
2709
|
-
@config_kits = Toolkit.config_kits unless defined?(@config_kits)
|
|
2710
|
-
Toolkit.kits(config: @config_kits, plugins: @plugin_kits, errors: errors)
|
|
2711
|
-
end
|
|
2712
|
-
|
|
2713
|
-
# The session's toolkit resolved (Toolkit::Resolved): what its next
|
|
2714
|
-
# turn declares. A plain full toolkit is nil, the Registry's "every tool".
|
|
2715
|
-
def resolved_toolkit(session, errors = [])
|
|
2716
|
-
resolved = Toolkit.of(session&.toolkit).resolve(toolkit_kits(errors))
|
|
2717
|
-
resolved.everything? ? nil : resolved
|
|
2718
|
-
end
|
|
2519
|
+
# The session's toolkit (ToolkitBinding): the kits, what its next turn
|
|
2520
|
+
# declares, ctx.use_toolkit and the turn-start apply.
|
|
2521
|
+
def toolkit_kits(errors = []) = @toolkit_binding.kits(errors)
|
|
2522
|
+
def resolved_toolkit(session, errors = []) = @toolkit_binding.resolved(session, errors)
|
|
2523
|
+
def toolkit_explained(toolkit = nil) = @toolkit_binding.explained(toolkit)
|
|
2524
|
+
def toolkit_summary = @toolkit_binding.summary
|
|
2719
2525
|
|
|
2720
2526
|
# The tools this session registered (Tools::Registry), declared or not.
|
|
2721
2527
|
attr_reader :tools
|
|
2722
2528
|
|
|
2723
|
-
# +toolkit+ (the session's by default) as its next turn would declare
|
|
2724
|
-
# it (/toolkit, the UIs' chip).
|
|
2725
|
-
# @return [Toolkit::Explained]
|
|
2726
|
-
def toolkit_explained(toolkit = nil)
|
|
2727
|
-
toolkit ||= Toolkit.of(@session&.toolkit)
|
|
2728
|
-
resolved = toolkit.resolve(toolkit_kits)
|
|
2729
|
-
filter = resolved.everything? ? nil : resolved
|
|
2730
|
-
Toolkit::Explained.new(toolkit: toolkit, resolved: resolved, declared: @tools.entries(toolkit: filter),
|
|
2731
|
-
left_out: @tools.left_out_names(toolkit: filter), pending: filter != @tools.toolkit)
|
|
2732
|
-
end
|
|
2733
|
-
|
|
2734
|
-
# The session's toolkit for the UIs (the snapshot, command_ran): nil on
|
|
2735
|
-
# full, or with no session.
|
|
2736
|
-
def toolkit_summary
|
|
2737
|
-
@session && toolkit_explained.summary
|
|
2738
|
-
end
|
|
2739
2529
|
public :tools, :toolkit_kits, :resolved_toolkit, :toolkit_explained, :toolkit_summary
|
|
2740
2530
|
|
|
2741
|
-
# A plugin's ctx.use_toolkit
|
|
2742
|
-
# when it has no model reply yet and no turn runs (D3: no confirm from
|
|
2743
|
-
# plugins), and tell the user as /toolkit would; it applies at the next
|
|
2744
|
-
# turn's start.
|
|
2531
|
+
# A plugin's ctx.use_toolkit (ToolkitBinding#use).
|
|
2745
2532
|
# @return [Boolean] whether it was staged
|
|
2746
2533
|
# @raise [ArgumentError] an unknown toolkit
|
|
2747
|
-
def use_toolkit(name)
|
|
2748
|
-
raise ArgumentError, "no toolkit named #{name}" unless toolkit_kits.key?(name)
|
|
2749
|
-
return false if @session.nil? || turn_running? || history?
|
|
2750
|
-
|
|
2751
|
-
toolkit = Toolkit.new(name: name)
|
|
2752
|
-
return true if @session.toolkit == toolkit
|
|
2753
|
-
|
|
2754
|
-
@session.toolkit = toolkit
|
|
2755
|
-
@session.save(state_dir: session_state_dir)
|
|
2756
|
-
hook_notify("toolkit: #{name} from the next turn's start", :info, "toolkit", extra: { toolkit: toolkit_summary })
|
|
2757
|
-
true
|
|
2758
|
-
end
|
|
2534
|
+
def use_toolkit(name) = @toolkit_binding.use(name)
|
|
2759
2535
|
public :use_toolkit
|
|
2760
2536
|
|
|
2761
|
-
#
|
|
2762
|
-
# a turn's start (before its prompt is built), and build the prompts
|
|
2763
|
-
# again when what it declares changed. A name that doesn't resolve (a
|
|
2764
|
-
# typo, an uninstalled bundle) declares everything for now and says so
|
|
2765
|
-
# once; the saved toolkit stays as it is.
|
|
2537
|
+
# At a turn's start (ToolkitBinding#apply!).
|
|
2766
2538
|
# @return [Boolean] whether the declared tools changed
|
|
2767
|
-
def apply_toolkit!(session, on_event = nil)
|
|
2768
|
-
errors = []
|
|
2769
|
-
resolved = Toolkit.of(session&.toolkit).resolve(toolkit_kits(errors))
|
|
2770
|
-
warn_toolkit(session, resolved, errors, on_event)
|
|
2771
|
-
resolved = nil if resolved.everything?
|
|
2772
|
-
return false if resolved == @tools.toolkit
|
|
2773
|
-
|
|
2774
|
-
@tools.toolkit = resolved
|
|
2775
|
-
@prompt_builder&.reset!
|
|
2776
|
-
true
|
|
2777
|
-
end
|
|
2539
|
+
def apply_toolkit!(session, on_event = nil) = @toolkit_binding.apply!(session, on_event)
|
|
2778
2540
|
private :apply_toolkit!
|
|
2779
2541
|
|
|
2780
|
-
# The
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
Log.warn(:config, "toolkit_unresolved", message: line)
|
|
2792
|
-
emit_event(on_event, { type: :guardrail_warning, message: line, label: "toolkit" })
|
|
2793
|
-
end
|
|
2542
|
+
# The session's toolkit (see #initialize for when).
|
|
2543
|
+
def build_toolkit_binding
|
|
2544
|
+
ToolkitBinding.new(
|
|
2545
|
+
tools: -> { @tools },
|
|
2546
|
+
session: -> { @session },
|
|
2547
|
+
state_dir: -> { session_state_dir },
|
|
2548
|
+
can_stage: -> { !turn_running? && !history? },
|
|
2549
|
+
reset_prompt: -> { @prompt_builder&.reset! },
|
|
2550
|
+
emit: ->(sink, event) { emit_event(sink, event) },
|
|
2551
|
+
notify: ->(text, level, source, extra:) { hook_notify(text, level, source, extra: extra) }
|
|
2552
|
+
)
|
|
2794
2553
|
end
|
|
2795
|
-
private :
|
|
2554
|
+
private :build_toolkit_binding
|
|
2796
2555
|
|
|
2797
2556
|
# The tools changed (a plugin's chi.tools_changed!): the system prompts,
|
|
2798
2557
|
# which declare them, are built again on the next turn.
|
|
@@ -2820,7 +2579,7 @@ module Samagotchi
|
|
|
2820
2579
|
steer: ->(text, label) { steer(text, source: hook_source(label)) },
|
|
2821
2580
|
stop_turn: ->(reason, label) { hook_stop_turn(reason, label) },
|
|
2822
2581
|
stop_generation: ->(reason, label) { hook_stop_generation(reason, label) },
|
|
2823
|
-
toolkit: -> { @
|
|
2582
|
+
toolkit: -> { @toolkit_binding.name },
|
|
2824
2583
|
declared_tools: -> { toolkit_explained.declared.map(&:name) },
|
|
2825
2584
|
use_toolkit: ->(name) { use_toolkit(name) },
|
|
2826
2585
|
ask_model: lambda { |request, timeout:, max_tokens:, cancel_controller:|
|
|
@@ -2891,7 +2650,7 @@ module Samagotchi
|
|
|
2891
2650
|
# and bare model name, as a turn resolves them.
|
|
2892
2651
|
# @return [IdleTarget]
|
|
2893
2652
|
def session_model_recap_target
|
|
2894
|
-
IdleTarget.of_model(@host_registry,
|
|
2653
|
+
IdleTarget.of_model(@host_registry, effective_model_name)
|
|
2895
2654
|
end
|
|
2896
2655
|
|
|
2897
2656
|
# Build the idle reminders job. Always created (reminders are opt-in
|
|
@@ -2938,29 +2697,26 @@ module Samagotchi
|
|
|
2938
2697
|
@steer_cut.observe(event, progress)
|
|
2939
2698
|
case event[:type]
|
|
2940
2699
|
when :generation_started
|
|
2941
|
-
@
|
|
2700
|
+
@thinking_notices.generation_started
|
|
2942
2701
|
watch&.started(event[:iteration])
|
|
2943
2702
|
when :generation_retrying
|
|
2944
2703
|
# A dropped stream's step asked again: it streams from the start.
|
|
2945
2704
|
if event[:restarted]
|
|
2946
|
-
@
|
|
2705
|
+
@thinking_notices.generation_started
|
|
2947
2706
|
watch&.started(event[:iteration], restarted: true)
|
|
2948
2707
|
end
|
|
2949
|
-
when :generation_chunk then
|
|
2708
|
+
when :generation_chunk then @thinking_notices.note_thought(progress[:thinking])
|
|
2950
2709
|
end
|
|
2951
2710
|
# The chat loop asked again without the thinking fields or the
|
|
2952
2711
|
# reasoning sent back: a notice, not an event of its own.
|
|
2953
|
-
next thinking_refused(event) if event[:type] == :thinking_refused
|
|
2954
|
-
next reasoning_history_refused(event) if event[:type] == :reasoning_history_refused
|
|
2955
|
-
next sampling_refused(event) if event[:type] == :sampling_refused
|
|
2712
|
+
next @thinking_notices.thinking_refused(event) if event[:type] == :thinking_refused
|
|
2713
|
+
next @thinking_notices.reasoning_history_refused(event) if event[:type] == :reasoning_history_refused
|
|
2714
|
+
next @thinking_notices.sampling_refused(event) if event[:type] == :sampling_refused
|
|
2956
2715
|
|
|
2957
2716
|
event = with_generation_report(event) if event[:type] == :generation_completed
|
|
2958
2717
|
emit_event(on_event, event)
|
|
2959
2718
|
watch.feed(**progress) if watch && progress
|
|
2960
|
-
if event[:type] == :generation_completed
|
|
2961
|
-
check_thinking_honoured(event)
|
|
2962
|
-
announce_effort_ignored
|
|
2963
|
-
end
|
|
2719
|
+
@thinking_notices.generation_completed(event) if event[:type] == :generation_completed
|
|
2964
2720
|
end
|
|
2965
2721
|
end
|
|
2966
2722
|
|
|
@@ -2973,7 +2729,8 @@ module Samagotchi
|
|
|
2973
2729
|
def with_generation_report(event)
|
|
2974
2730
|
report = @metrics.finish_generation
|
|
2975
2731
|
event.merge(speed: report.speed&.to_h, tokens: report.tokens, served_expected_by: served_expected_by(event[:served_model]))
|
|
2976
|
-
rescue StandardError
|
|
2732
|
+
rescue StandardError => e
|
|
2733
|
+
Log.warn(:turn, "generation_report_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2977
2734
|
event
|
|
2978
2735
|
end
|
|
2979
2736
|
private :with_generation_report
|
|
@@ -2990,7 +2747,8 @@ module Samagotchi
|
|
|
2990
2747
|
# Capture used memories synchronously in the turn thread.
|
|
2991
2748
|
read_names = begin
|
|
2992
2749
|
@used_memories.capture(event, muted: @muted_memory_names)
|
|
2993
|
-
rescue StandardError
|
|
2750
|
+
rescue StandardError => e
|
|
2751
|
+
Log.warn(:memory, "used_memories_capture_failed", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
2994
2752
|
nil
|
|
2995
2753
|
end
|
|
2996
2754
|
# Turn-scoped sink: receives the event hash as the stream handler
|
|
@@ -2999,8 +2757,9 @@ module Samagotchi
|
|
|
2999
2757
|
if on_event
|
|
3000
2758
|
begin
|
|
3001
2759
|
on_event.call(event)
|
|
3002
|
-
rescue StandardError
|
|
2760
|
+
rescue StandardError => e
|
|
3003
2761
|
# Sink errors must not break the kernel loop (same as KernelLoop's own handling)
|
|
2762
|
+
Log.warn(:turn, "sink_failed", event_type: event[:type].to_s, error: e.class.name, msg: e.message.to_s[0, 200])
|
|
3004
2763
|
end
|
|
3005
2764
|
end
|
|
3006
2765
|
|
|
@@ -3014,38 +2773,13 @@ module Samagotchi
|
|
|
3014
2773
|
emit_event(on_event, { type: :used_memories_updated, used_memory_names: used_memory_names, read_names: read_names })
|
|
3015
2774
|
end
|
|
3016
2775
|
|
|
3017
|
-
# The window as the target's loop would see it (see ChatLoop#context_window:
|
|
3018
|
-
# a remote chat host has no /props, only its model list).
|
|
3019
|
-
def current_context_window(target)
|
|
3020
|
-
client = target.client
|
|
3021
|
-
adapter = nil
|
|
3022
|
-
if target.entry.chat?
|
|
3023
|
-
adapter = @host_registry.adapter_for(target.entry)
|
|
3024
|
-
client = nil if adapter.remote?
|
|
3025
|
-
end
|
|
3026
|
-
ContextWindow.resolve(client: client, model: target.bare_model, adapter: adapter,
|
|
3027
|
-
setting: ContextWindow.setting(target, names: model_lookup_names(target)))
|
|
3028
|
-
rescue StandardError
|
|
3029
|
-
nil
|
|
3030
|
-
end
|
|
3031
|
-
|
|
3032
2776
|
# The effective model's models:/hosts: window_tokens, read per turn
|
|
3033
2777
|
# (after the hooks that may switch the model).
|
|
3034
|
-
def turn_window_setting
|
|
3035
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
3036
|
-
ContextWindow.setting(target, names: model_lookup_names(target))
|
|
3037
|
-
rescue StandardError
|
|
3038
|
-
nil
|
|
3039
|
-
end
|
|
2778
|
+
def turn_window_setting = model_setting(nil, &:window_setting)
|
|
3040
2779
|
|
|
3041
2780
|
# The effective model's price on its host (hosts.<name>.models.<id>.price),
|
|
3042
2781
|
# by the requested id, never the served one; read per turn as the window is.
|
|
3043
|
-
def turn_price
|
|
3044
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
3045
|
-
target.entry.models&.dig(target.bare_model.to_s.strip.downcase)&.price
|
|
3046
|
-
rescue StandardError
|
|
3047
|
-
nil
|
|
3048
|
-
end
|
|
2782
|
+
def turn_price = model_setting(nil, &:price)
|
|
3049
2783
|
|
|
3050
2784
|
# The LLM context strategy the next turn runs under: the session's own
|
|
3051
2785
|
# (Session#llm_context), else the effective model's (models:/hosts:
|
|
@@ -3059,57 +2793,82 @@ module Samagotchi
|
|
|
3059
2793
|
private :llm_context_session_args
|
|
3060
2794
|
|
|
3061
2795
|
# See #served_model. A report for another name (before a /model switch)
|
|
3062
|
-
# doesn't count. Without a
|
|
3063
|
-
# /props: nil until a turn.
|
|
3064
|
-
|
|
3065
|
-
|
|
2796
|
+
# doesn't count. Without a model (EffectiveModel), no probe. A remote
|
|
2797
|
+
# chat host has no /props: nil until a turn. The name asked is never
|
|
2798
|
+
# resolved (HostRegistry#bare_name), so it holds on a broken resolve.
|
|
2799
|
+
def served_model_for(snapshot, model: nil)
|
|
2800
|
+
asked = bare_model_name(model ? model.name : effective_model_name)
|
|
3066
2801
|
return [snapshot[:served_model], asked] if snapshot[:served_model] && snapshot[:served_model_for] == asked
|
|
3067
|
-
return [nil, nil] unless
|
|
2802
|
+
return [nil, nil] unless model
|
|
3068
2803
|
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
return [nil, nil] if adapter.remote?
|
|
3073
|
-
end
|
|
3074
|
-
served = ServedModel.from_props(client.server_props(model: target.bare_model))
|
|
3075
|
-
served ? [served, asked] : [nil, nil]
|
|
2804
|
+
model.served_from_props(asked)
|
|
2805
|
+
# Only bare_model_name and the snapshot lookup are left to fail here:
|
|
2806
|
+
# the probe's failure is EffectiveModel#served_from_props's.
|
|
3076
2807
|
rescue StandardError
|
|
3077
2808
|
[nil, nil]
|
|
3078
2809
|
end
|
|
3079
2810
|
|
|
3080
|
-
#
|
|
3081
|
-
|
|
3082
|
-
def prompt_served_model(target)
|
|
3083
|
-
entry = target.entry
|
|
3084
|
-
return nil if entry.nil? || entry.chat? || entry.remote? || !target.client.respond_to?(:cached_server_props)
|
|
2811
|
+
# The window as +model+'s loop would see it (EffectiveModel#context_window).
|
|
2812
|
+
def current_context_window(model) = model.context_window
|
|
3085
2813
|
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
2814
|
+
# +settings+' chips (SessionSetting), the model's from +model+ (nil
|
|
2815
|
+
# when it doesn't resolve): /stats, the session state, a :command_ran.
|
|
2816
|
+
def setting_chips(settings, model)
|
|
2817
|
+
settings.to_h { |setting| [setting.key, setting.model? ? setting.chip_for(model, @session) : toolkit_summary] }
|
|
3090
2818
|
end
|
|
3091
2819
|
|
|
3092
|
-
#
|
|
2820
|
+
# The effective model now (EffectiveModel.of): resolved on each call,
|
|
2821
|
+
# never kept (config.yml, a model listing and another process's saved
|
|
2822
|
+
# lists all move a resolve). Raises as HostRegistry#resolve does.
|
|
2823
|
+
# @return [EffectiveModel]
|
|
2824
|
+
def effective_model(choice = @model_choice) = EffectiveModel.of(@host_registry, choice)
|
|
3093
2825
|
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
2826
|
+
# The block's answer for the effective model, or +fallback+ when the
|
|
2827
|
+
# model doesn't resolve: the readers whose rescue always covered the
|
|
2828
|
+
# resolve too. Each setting's own failure is the EffectiveModel's.
|
|
2829
|
+
def model_setting(fallback)
|
|
2830
|
+
# One read: the log line names the model that failed, even if a
|
|
2831
|
+
# /model switched it meanwhile.
|
|
2832
|
+
choice = @model_choice
|
|
2833
|
+
model = begin
|
|
2834
|
+
effective_model(choice)
|
|
2835
|
+
rescue StandardError => e
|
|
2836
|
+
return EffectiveModel::Unresolved.fallback(:effective_model_unresolved, choice.name, e, fallback)
|
|
3097
2837
|
end
|
|
2838
|
+
yield model
|
|
2839
|
+
end
|
|
3098
2840
|
|
|
3099
|
-
|
|
3100
|
-
|
|
2841
|
+
# ── Prompt profile ─────────────────────────────────────────────────────────
|
|
2842
|
+
|
|
2843
|
+
# +choice+'s profile (ModelProfile.resolve; may probe /props), or the
|
|
2844
|
+
# one given to .new.
|
|
2845
|
+
def resolve_profile(choice)
|
|
2846
|
+
given = @given_profile
|
|
2847
|
+
return ModelProfile::Resolution.new(profile: given, source: :given, detail: nil, retry: false) if given
|
|
2848
|
+
|
|
2849
|
+
EffectiveModel.of(@host_registry, choice).profile_resolution
|
|
3101
2850
|
end
|
|
3102
2851
|
|
|
3103
|
-
#
|
|
3104
|
-
#
|
|
3105
|
-
|
|
3106
|
-
|
|
2852
|
+
# Keeps +resolution+ as +choice+'s and applies it, unless the model
|
|
2853
|
+
# switched while it was resolved (a /stats on a Bridge thread probing
|
|
2854
|
+
# as a /model ran): then it is only returned, and the new model's is
|
|
2855
|
+
# resolved on its first need.
|
|
2856
|
+
# @return [ModelProfile::Resolution]
|
|
2857
|
+
def store_profile(choice, resolution)
|
|
2858
|
+
@profile_mutex.synchronize do
|
|
2859
|
+
return resolution unless @model_choice.equal?(choice)
|
|
2860
|
+
|
|
2861
|
+
apply_profile(resolution)
|
|
2862
|
+
@profile_resolution = [choice, resolution]
|
|
2863
|
+
end
|
|
2864
|
+
resolution
|
|
3107
2865
|
end
|
|
3108
2866
|
|
|
3109
2867
|
# Everything that holds a profile follows the resolution: the kernel's
|
|
3110
2868
|
# prompt format and parser, and the system prompts built for the old one.
|
|
3111
2869
|
def apply_profile(resolution)
|
|
3112
|
-
|
|
2870
|
+
kept = @profile_resolution&.last
|
|
2871
|
+
@prompt_builder&.reset! if kept && kept.profile.name != resolution.profile.name
|
|
3113
2872
|
@kernel.use_profile!(resolution)
|
|
3114
2873
|
resolution
|
|
3115
2874
|
end
|
|
@@ -3118,11 +2877,10 @@ module Samagotchi
|
|
|
3118
2877
|
# server probe failed (unreachable, or 503 while loading a model). A
|
|
3119
2878
|
# profile that changes here drops the cached system prompts.
|
|
3120
2879
|
def refresh_profile!
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
end
|
|
2880
|
+
choice, kept = @profile_resolution
|
|
2881
|
+
return profile_resolution unless choice.equal?(@model_choice) && kept.retry?
|
|
2882
|
+
|
|
2883
|
+
store_profile(choice, resolve_profile(choice))
|
|
3126
2884
|
end
|
|
3127
2885
|
end
|
|
3128
2886
|
end
|