samagotchi 0.67.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 +21 -1
- data/docs/architecture.md +6 -3
- data/docs/internals/engine.md +58 -9
- data/docs/sessions.md +1 -1
- data/docs/testing.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/effective_model.rb +283 -0
- data/lib/samagotchi/engine.rb +211 -358
- data/lib/samagotchi/level_setting_command.rb +99 -0
- data/lib/samagotchi/llm_context_strategy.rb +5 -10
- data/lib/samagotchi/reasoning_history_command.rb +29 -90
- data/lib/samagotchi/session.rb +22 -8
- 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/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +55 -84
- data/lib/samagotchi/web/public/app.js +74 -505
- data/lib/samagotchi/web/public/bubbles.js +291 -0
- data/lib/samagotchi/web/public/cards_place.js +184 -0
- 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/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 +36 -105
- data/lib/samagotchi/web/public/model_picker.js +26 -22
- data/lib/samagotchi/web/public/ref_scan.js +1 -7
- data/lib/samagotchi/web/public/refs.js +13 -0
- data/lib/samagotchi/web/public/stage_frame.js +179 -0
- data/lib/samagotchi/web/public/stage_model.js +33 -0
- 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/vendor/directive-helpers.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
- data/lib/samagotchi/worker.rb +11 -16
- metadata +14 -1
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -14,6 +14,9 @@ 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"
|
|
@@ -134,15 +137,17 @@ module Samagotchi
|
|
|
134
137
|
# is used (the REPL checks the model it starts on, #switch_model!).
|
|
135
138
|
@default_model_name = ModelProfile.required_model_name(model_name)
|
|
136
139
|
ModelProfile.check_host!(model_name, hosts: @host_registry.entries) unless model_name.to_s.strip.empty?
|
|
137
|
-
|
|
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)
|
|
138
144
|
# An injected client (specs) stands in for every host's client.
|
|
139
145
|
@host_registry.client_override = client if client
|
|
140
|
-
@client =
|
|
146
|
+
@client = effective_model.client
|
|
141
147
|
# A caller's profile pins it (until a model switch); otherwise
|
|
142
148
|
# #profile_resolution decides on first need (see there), so building an
|
|
143
149
|
# Engine makes no network call.
|
|
144
150
|
@given_profile = profile ? ModelProfile.normalize(profile) : nil
|
|
145
|
-
@typed_model_name = model_typed.to_s.strip.empty? ? @default_model_name : model_typed.to_s.strip
|
|
146
151
|
@profile_resolution = nil
|
|
147
152
|
# Ensure the built-in system bundle is installed (lazy, warn-only).
|
|
148
153
|
# This is the single seam for both TUI and non-TUI (web/worker) paths.
|
|
@@ -195,6 +200,9 @@ module Samagotchi
|
|
|
195
200
|
# The session's toolkit: the kits (the plugins' too, chi.toolkit),
|
|
196
201
|
# what its next turn declares, the turn-start apply.
|
|
197
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) })
|
|
198
206
|
# The tools plugins publish without declaring (chi.tool_source), and
|
|
199
207
|
# the search and call tools that reach them (Tools::ToolIndex).
|
|
200
208
|
@tool_index = Tools::ToolIndex.new
|
|
@@ -288,7 +296,7 @@ module Samagotchi
|
|
|
288
296
|
def wire_kernel(kernel, muted_memories)
|
|
289
297
|
# A given kernel (specs) gets the Engine's store, hooks and tools, as
|
|
290
298
|
# the one built here does.
|
|
291
|
-
@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,
|
|
292
300
|
hooks: @hooks, reminder_store: @reminder_store, tools: @tools)
|
|
293
301
|
@kernel.reminder_store = @reminder_store
|
|
294
302
|
@kernel.hooks = @hooks
|
|
@@ -373,8 +381,8 @@ module Samagotchi
|
|
|
373
381
|
end
|
|
374
382
|
|
|
375
383
|
# The backend for the next turn: the loop the effective model's host speaks.
|
|
376
|
-
def backend
|
|
377
|
-
backend_for(
|
|
384
|
+
def backend(model = effective_model)
|
|
385
|
+
backend_for(model)
|
|
378
386
|
end
|
|
379
387
|
|
|
380
388
|
# Record that activity happened (user input or a completed turn): the
|
|
@@ -522,14 +530,17 @@ module Samagotchi
|
|
|
522
530
|
|
|
523
531
|
# @return [SessionMetrics] the per-session analytics collector
|
|
524
532
|
attr_reader :metrics
|
|
525
|
-
|
|
526
|
-
#
|
|
527
|
-
|
|
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
|
|
528
539
|
|
|
529
540
|
# The resolved ref a session stores for the effective model (ModelRef#ref:
|
|
530
541
|
# the alias applied, "host:id" when it names a host).
|
|
531
542
|
def effective_model_ref
|
|
532
|
-
model_ref_for(
|
|
543
|
+
model_ref_for(effective_model_name)
|
|
533
544
|
end
|
|
534
545
|
|
|
535
546
|
# The effective model as the system prompt names it (SystemPrompt::ModelIdentity):
|
|
@@ -537,11 +548,11 @@ module Samagotchi
|
|
|
537
548
|
# only, the model its /props names when that differs. Never a new
|
|
538
549
|
# request: only the /props answer profile resolution already cached.
|
|
539
550
|
def model_identity
|
|
540
|
-
|
|
541
|
-
entry =
|
|
551
|
+
model = effective_model
|
|
552
|
+
entry = model.entry
|
|
542
553
|
host = entry && "host #{entry.name}, #{entry.url || "#{entry.host}:#{entry.port}"}"
|
|
543
|
-
SystemPrompt::ModelIdentity.new(ref:
|
|
544
|
-
served:
|
|
554
|
+
SystemPrompt::ModelIdentity.new(ref: model.ref, host: host, key: @model_key,
|
|
555
|
+
served: model.prompt_served, fallback_key: @fallback_model_key)
|
|
545
556
|
end
|
|
546
557
|
|
|
547
558
|
# The model notes for the effective model (ModelNotes): matched on its
|
|
@@ -586,10 +597,10 @@ module Samagotchi
|
|
|
586
597
|
summary = error.respond_to?(:summary) ? error.summary : error.message
|
|
587
598
|
return summary unless error.is_a?(LLM::ProviderError) && error.host
|
|
588
599
|
|
|
589
|
-
note = @host_registry.alias_note(
|
|
600
|
+
note = @host_registry.alias_note(effective_model_name)
|
|
590
601
|
note && note.end_with?("on host #{error.host}") ? "#{summary} (#{note})" : summary
|
|
591
|
-
rescue StandardError
|
|
592
|
-
summary
|
|
602
|
+
rescue StandardError => e
|
|
603
|
+
EffectiveModel::Unresolved.fallback(:alias_note_failed, effective_model_name, e, summary)
|
|
593
604
|
end
|
|
594
605
|
|
|
595
606
|
# +name+'s resolved ref (ModelRef#ref).
|
|
@@ -603,7 +614,8 @@ module Samagotchi
|
|
|
603
614
|
def store_model!(session)
|
|
604
615
|
ref = effective_model_ref
|
|
605
616
|
session.model_name = ref
|
|
606
|
-
|
|
617
|
+
typed = typed_model_name
|
|
618
|
+
session.model_typed = typed.to_s == ref ? nil : typed
|
|
607
619
|
session
|
|
608
620
|
end
|
|
609
621
|
|
|
@@ -622,11 +634,17 @@ module Samagotchi
|
|
|
622
634
|
# and kept, so the system prompt and the server's KV prefix stay stable;
|
|
623
635
|
# #switch_model! starts over, and a failed server probe is retried before
|
|
624
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).
|
|
625
639
|
# @return [ModelProfile::Resolution]
|
|
626
640
|
def profile_resolution
|
|
627
|
-
|
|
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))
|
|
628
646
|
end
|
|
629
|
-
attr_reader :host_registry, :client
|
|
647
|
+
attr_reader :default_model_name, :host_registry, :client
|
|
630
648
|
|
|
631
649
|
# @return [Commands::Registry] the commands this session runs: the
|
|
632
650
|
# built-ins, and the ones bundle plugins add
|
|
@@ -645,8 +663,9 @@ module Samagotchi
|
|
|
645
663
|
# (before aliases were applied there): that key is read when the
|
|
646
664
|
# target's overlay is missing (KernelLoop#sync_model_key!).
|
|
647
665
|
def sync_model_key!
|
|
648
|
-
|
|
649
|
-
|
|
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)
|
|
650
669
|
@fallback_model_key = typed_key == @model_key ? nil : typed_key
|
|
651
670
|
@kernel.sync_model_key!(@model_key, fallback: @fallback_model_key)
|
|
652
671
|
end
|
|
@@ -655,17 +674,17 @@ module Samagotchi
|
|
|
655
674
|
# The effective model's bare name for the guardrails' models: rules;
|
|
656
675
|
# nil without a model name.
|
|
657
676
|
def guardrail_model_name
|
|
658
|
-
|
|
677
|
+
model = effective_model_name
|
|
678
|
+
name = model && bare_model_name(model)
|
|
659
679
|
name.to_s.strip.empty? ? nil : name
|
|
660
680
|
end
|
|
661
681
|
|
|
662
682
|
# Point the kernel (and a chat backend) at the effective model's host
|
|
663
683
|
# (after /model, --model, resume).
|
|
664
|
-
def sync_kernel_client!
|
|
665
|
-
|
|
666
|
-
@client =
|
|
667
|
-
|
|
668
|
-
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)
|
|
669
688
|
end
|
|
670
689
|
|
|
671
690
|
# Subscribe a persistent observer to engine events.
|
|
@@ -966,11 +985,14 @@ module Samagotchi
|
|
|
966
985
|
# The alias is applied where the model is resolved (HostRegistry#resolve),
|
|
967
986
|
# once; checked here: an unknown host, an alias naming another host.
|
|
968
987
|
resolved = ModelProfile.check_host!(ModelProfile.required_model_name(model_name), hosts: @host_registry.entries)
|
|
969
|
-
|
|
970
|
-
@
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
|
974
996
|
sync_model_key!
|
|
975
997
|
@prompt_builder.reset!
|
|
976
998
|
# The notes the new model's prompt loads at its next build, so /model,
|
|
@@ -1041,22 +1063,24 @@ module Samagotchi
|
|
|
1041
1063
|
# @return [Hash] @metrics.snapshot, filled in
|
|
1042
1064
|
def stats_snapshot
|
|
1043
1065
|
snapshot = @metrics.snapshot
|
|
1044
|
-
|
|
1045
|
-
|
|
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)
|
|
1046
1070
|
snapshot = snapshot.merge(served_model: served, served_model_for: served_for,
|
|
1047
|
-
served_expected_by: served_expected_by(served
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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)
|
|
1051
1075
|
unless snapshot.dig(:context, :window_tokens)
|
|
1052
|
-
window = current_context_window(
|
|
1076
|
+
window = current_context_window(model)
|
|
1053
1077
|
if window
|
|
1054
1078
|
snapshot = snapshot.merge(context: snapshot[:context].merge(window_tokens: window.tokens,
|
|
1055
1079
|
window_source: window.source.to_s))
|
|
1056
1080
|
end
|
|
1057
1081
|
end
|
|
1058
1082
|
# The chat loop uses no prompt profile: drop one a native turn reported.
|
|
1059
|
-
return snapshot.except(:profile, :profile_source) if
|
|
1083
|
+
return snapshot.except(:profile, :profile_source) if model.chat?
|
|
1060
1084
|
|
|
1061
1085
|
unless snapshot[:profile]
|
|
1062
1086
|
resolution = profile_resolution
|
|
@@ -1069,10 +1093,11 @@ module Samagotchi
|
|
|
1069
1093
|
# no turn yet (it woke after an idle exit): nil when either count is
|
|
1070
1094
|
# unknown or context.status is off. Counted as the turns count it:
|
|
1071
1095
|
# against the effective model's llm_context budget when it has one.
|
|
1072
|
-
def saved_context_status(context)
|
|
1096
|
+
def saved_context_status(context, model: :current)
|
|
1073
1097
|
return nil unless context
|
|
1074
1098
|
|
|
1075
|
-
|
|
1099
|
+
llm_context = model == :current ? turn_llm_context : model&.llm_context_explained(llm_context_session_args)&.resolved
|
|
1100
|
+
ContextStatus.new(llm_context: llm_context)
|
|
1076
1101
|
.display_for(used_tokens: context[:used_tokens], window_tokens: context[:window_tokens])
|
|
1077
1102
|
end
|
|
1078
1103
|
|
|
@@ -1080,10 +1105,7 @@ module Samagotchi
|
|
|
1080
1105
|
# (LLMContextStrategy.explain), nil when it can't be resolved.
|
|
1081
1106
|
# @return [LLMContextStrategy::Explained, nil]
|
|
1082
1107
|
def llm_context_explained
|
|
1083
|
-
|
|
1084
|
-
LLMContextStrategy.explain(target, names: model_lookup_names(target), **llm_context_session_args)
|
|
1085
|
-
rescue StandardError
|
|
1086
|
-
nil
|
|
1108
|
+
model_setting(nil) { |model| model.llm_context_explained(llm_context_session_args) }
|
|
1087
1109
|
end
|
|
1088
1110
|
|
|
1089
1111
|
# #llm_context_explained's summary (Explained#summary), nil when it
|
|
@@ -1095,7 +1117,7 @@ module Samagotchi
|
|
|
1095
1117
|
|
|
1096
1118
|
# The session's own llm_context values, nil when it has none.
|
|
1097
1119
|
# @return [LLMContextOverride, nil]
|
|
1098
|
-
def llm_context_override = @session
|
|
1120
|
+
def llm_context_override = SessionSetting::LLM_CONTEXT.own(@session)
|
|
1099
1121
|
|
|
1100
1122
|
# Set the session's own llm_context values (an empty override unsets
|
|
1101
1123
|
# them); the next turn's start resolves them, never a running one (its
|
|
@@ -1104,23 +1126,18 @@ module Samagotchi
|
|
|
1104
1126
|
def llm_context_override=(override)
|
|
1105
1127
|
raise ArgumentError, "no session yet" unless @session
|
|
1106
1128
|
|
|
1107
|
-
@session
|
|
1129
|
+
SessionSetting::LLM_CONTEXT.set(@session, override)
|
|
1108
1130
|
# The status a later client reads counts against the new budget.
|
|
1109
1131
|
@last_context_status = saved_context_status(@metrics.snapshot[:context]) || @last_context_status if @last_context_status
|
|
1110
1132
|
end
|
|
1111
1133
|
|
|
1112
1134
|
# The effective model is on a chat host (api: openai or anthropic), whose loop uses
|
|
1113
1135
|
# no prompt profile.
|
|
1114
|
-
def chat_model? =
|
|
1136
|
+
def chat_model? = effective_model.chat?
|
|
1115
1137
|
|
|
1116
1138
|
# "temperature=0.6 (hosts.work)" for /model: the effective model's
|
|
1117
1139
|
# configured sampling, nil when none.
|
|
1118
|
-
def sampling_summary
|
|
1119
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
1120
|
-
SamplingSettings.summary(target, names: model_lookup_names(target))
|
|
1121
|
-
rescue StandardError
|
|
1122
|
-
nil
|
|
1123
|
-
end
|
|
1140
|
+
def sampling_summary = model_setting(nil, &:sampling_summary)
|
|
1124
1141
|
|
|
1125
1142
|
# "off (models: qwen)", "low (session)" for /model: the effective
|
|
1126
1143
|
# model's thinking level and where it came from, nil when none is set.
|
|
@@ -1134,10 +1151,7 @@ module Samagotchi
|
|
|
1134
1151
|
# turn_sampling; on any error, the global and its name.
|
|
1135
1152
|
# @return [Array(Integer, String|nil)]
|
|
1136
1153
|
def steer_cut_after_explained
|
|
1137
|
-
|
|
1138
|
-
SteerCutAfter.resolve(target, names: model_lookup_names(target))
|
|
1139
|
-
rescue StandardError
|
|
1140
|
-
[SteerCut.cut_after, "steer.cut_after"]
|
|
1154
|
+
model_setting([SteerCut.cut_after, "steer.cut_after"], &:steer_cut_after_explained)
|
|
1141
1155
|
end
|
|
1142
1156
|
|
|
1143
1157
|
# #steer_cut_after_explained's seconds: what SteerCut#cut_for_steer
|
|
@@ -1159,10 +1173,7 @@ module Samagotchi
|
|
|
1159
1173
|
# +own+: the session's own level to resolve with (the current one by
|
|
1160
1174
|
# default; /thinking asks what a change would resolve to).
|
|
1161
1175
|
def thinking_explained(own: thinking_override)
|
|
1162
|
-
|
|
1163
|
-
Thinking.explain(target, names: model_lookup_names(target), session: own)
|
|
1164
|
-
rescue StandardError
|
|
1165
|
-
nil
|
|
1176
|
+
model_setting(nil) { |model| model.thinking_explained(own: own) }
|
|
1166
1177
|
end
|
|
1167
1178
|
|
|
1168
1179
|
# "drop (models: qwen)" for /model: the effective chat model's
|
|
@@ -1170,14 +1181,7 @@ module Samagotchi
|
|
|
1170
1181
|
# default, the model runs on the native loop (which ignores it) or it
|
|
1171
1182
|
# can't be resolved.
|
|
1172
1183
|
def reasoning_history_summary
|
|
1173
|
-
|
|
1174
|
-
return nil unless target.entry&.chat?
|
|
1175
|
-
|
|
1176
|
-
level, source = ReasoningHistory.resolve(target, names: model_lookup_names(target),
|
|
1177
|
-
session: reasoning_history_override)
|
|
1178
|
-
source ? ReasoningHistory.label(level, source) : nil
|
|
1179
|
-
rescue StandardError
|
|
1180
|
-
nil
|
|
1184
|
+
model_setting(nil) { |model| model.reasoning_history_summary(own: reasoning_history_override) }
|
|
1181
1185
|
end
|
|
1182
1186
|
|
|
1183
1187
|
# The effective chat model's reasoning_history level with where it came
|
|
@@ -1189,12 +1193,7 @@ module Samagotchi
|
|
|
1189
1193
|
# +own+: the session's own level to resolve with (the current one by
|
|
1190
1194
|
# default; a command asks what a change would resolve to).
|
|
1191
1195
|
def reasoning_history_explained(native: false, own: reasoning_history_override)
|
|
1192
|
-
|
|
1193
|
-
return nil unless native || target.entry&.chat?
|
|
1194
|
-
|
|
1195
|
-
ReasoningHistory.explain(target, names: model_lookup_names(target), session: own)
|
|
1196
|
-
rescue StandardError
|
|
1197
|
-
nil
|
|
1196
|
+
model_setting(nil) { |model| model.reasoning_history_explained(own: own, native: native) }
|
|
1198
1197
|
end
|
|
1199
1198
|
|
|
1200
1199
|
# What a reasoning_history change from level +from+ to level +to+ costs
|
|
@@ -1203,10 +1202,7 @@ module Samagotchi
|
|
|
1203
1202
|
# marked as refusing it may still get :history (one needless ask).
|
|
1204
1203
|
# @return [Symbol, nil]
|
|
1205
1204
|
def reasoning_history_cache_cost(from, to)
|
|
1206
|
-
|
|
1207
|
-
ReasoningHistory.cache_cost(from, to, target: target, conversation: @session&.messages)
|
|
1208
|
-
rescue StandardError
|
|
1209
|
-
nil
|
|
1205
|
+
model_setting(nil) { |model| model.reasoning_history_cache_cost(from, to, conversation: @session&.messages) }
|
|
1210
1206
|
end
|
|
1211
1207
|
|
|
1212
1208
|
# Whether the session has a model reply (a cache to restart at all).
|
|
@@ -1216,7 +1212,7 @@ module Samagotchi
|
|
|
1216
1212
|
|
|
1217
1213
|
# The session's own reasoning_history level, nil when it has none.
|
|
1218
1214
|
# @return [Symbol, nil]
|
|
1219
|
-
def reasoning_history_override = @session
|
|
1215
|
+
def reasoning_history_override = SessionSetting::REASONING_HISTORY.own(@session)
|
|
1220
1216
|
|
|
1221
1217
|
# Set the session's own reasoning_history level (nil unsets it); the
|
|
1222
1218
|
# next turn's start resolves it. The caller saves the session.
|
|
@@ -1224,19 +1220,18 @@ module Samagotchi
|
|
|
1224
1220
|
def reasoning_history_override=(level)
|
|
1225
1221
|
raise ArgumentError, "no session yet" unless @session
|
|
1226
1222
|
|
|
1227
|
-
@session
|
|
1223
|
+
SessionSetting::REASONING_HISTORY.set(@session, level)
|
|
1228
1224
|
end
|
|
1229
1225
|
|
|
1230
1226
|
# The session's own thinking level, nil when it has none.
|
|
1231
1227
|
# @return [Symbol, nil]
|
|
1232
|
-
def thinking_override = @session
|
|
1228
|
+
def thinking_override = SessionSetting::THINKING.own(@session)
|
|
1233
1229
|
|
|
1234
1230
|
# What a thinking level change costs the effective model's prompt
|
|
1235
1231
|
# cache (Thinking.cache_cost): :full, :provider or :tail.
|
|
1236
1232
|
# @return [Symbol]
|
|
1237
1233
|
def thinking_cache_cost
|
|
1238
|
-
|
|
1239
|
-
Thinking.cache_cost(target, profile: target.entry&.chat? ? nil : profile&.name)
|
|
1234
|
+
effective_model.thinking_cache_cost { profile }
|
|
1240
1235
|
end
|
|
1241
1236
|
|
|
1242
1237
|
# Set the session's own thinking level (nil unsets it); the next turn's
|
|
@@ -1246,7 +1241,7 @@ module Samagotchi
|
|
|
1246
1241
|
def thinking_override=(level)
|
|
1247
1242
|
raise ArgumentError, "no session yet" unless @session
|
|
1248
1243
|
|
|
1249
|
-
@session
|
|
1244
|
+
SessionSetting::THINKING.set(@session, level)
|
|
1250
1245
|
end
|
|
1251
1246
|
|
|
1252
1247
|
# The model the server serves for the current model, and the name asked
|
|
@@ -1255,17 +1250,15 @@ module Samagotchi
|
|
|
1255
1250
|
# else [nil, nil].
|
|
1256
1251
|
# @return [Array(String, String), Array(nil, nil)]
|
|
1257
1252
|
def served_model(probe: true)
|
|
1258
|
-
served_model_for(@metrics.served_report,
|
|
1253
|
+
served_model_for(@metrics.served_report, model: probe ? effective_model : nil)
|
|
1259
1254
|
end
|
|
1260
1255
|
|
|
1261
1256
|
# The host whose hosts.<name>.models.<id>.served names +served+ for the
|
|
1262
1257
|
# current model (a gateway's round-robin targets): not a mismatch to
|
|
1263
1258
|
# warn about. nil when none does.
|
|
1264
1259
|
# @return [String, nil]
|
|
1265
|
-
def served_expected_by(served
|
|
1266
|
-
|
|
1267
|
-
rescue StandardError
|
|
1268
|
-
nil
|
|
1260
|
+
def served_expected_by(served)
|
|
1261
|
+
model_setting(nil) { |model| model.served_expected_by(served) }
|
|
1269
1262
|
end
|
|
1270
1263
|
|
|
1271
1264
|
def served_expected?(served) = !served_expected_by(served).nil?
|
|
@@ -1297,6 +1290,10 @@ module Samagotchi
|
|
|
1297
1290
|
def session_state_snapshot
|
|
1298
1291
|
served_pair = served_model(probe: false)
|
|
1299
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 }
|
|
1300
1297
|
{
|
|
1301
1298
|
status: @session&.status,
|
|
1302
1299
|
message_count: (@session&.messages || []).size,
|
|
@@ -1312,12 +1309,9 @@ module Samagotchi
|
|
|
1312
1309
|
model_name: effective_model_ref,
|
|
1313
1310
|
served_model: served_pair[0],
|
|
1314
1311
|
served_model_for: served_pair[1],
|
|
1315
|
-
served_expected_by: served_expected_by(served_pair[0]),
|
|
1316
|
-
context_status: @last_context_status&.dup || saved_context_status(metrics[:context]),
|
|
1317
|
-
|
|
1318
|
-
thinking: thinking_explained&.summary,
|
|
1319
|
-
reasoning_history: reasoning_history_explained&.summary,
|
|
1320
|
-
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),
|
|
1321
1315
|
recap_enabled: !@recap.nil?,
|
|
1322
1316
|
recap_min_user_turns: @recap&.min_user_turns,
|
|
1323
1317
|
recap_inactivity_seconds: @recap&.inactivity&.to_i
|
|
@@ -1673,15 +1667,17 @@ module Samagotchi
|
|
|
1673
1667
|
# the chat loop's leaves out the raw-prompt tool text, since its tools go
|
|
1674
1668
|
# as schemas. Built once per loop (and again after a model switch) so the
|
|
1675
1669
|
# prompt prefix, and the server's KV cache for it, stay stable.
|
|
1676
|
-
# @param
|
|
1670
|
+
# @param model [EffectiveModel, nil]
|
|
1677
1671
|
# @return [String]
|
|
1678
1672
|
# The native prompt depends on the thinking level too (Gemma's token,
|
|
1679
|
-
# Qwen's turn preamble): a level change builds it again.
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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))
|
|
1685
1681
|
# The memory indexes this prompt holds: /stats, the snapshot, analytics.json.
|
|
1686
1682
|
@metrics.memory_index = @prompt_builder.memory_index
|
|
1687
1683
|
# The model notes it carries: the session records them (/stats, /model, the web).
|
|
@@ -1689,16 +1685,6 @@ module Samagotchi
|
|
|
1689
1685
|
prompt
|
|
1690
1686
|
end
|
|
1691
1687
|
|
|
1692
|
-
# The LLM context layers +target+'s turns run under: the native prompt
|
|
1693
|
-
# declares the tools they add (forget_outputs under forget), so a
|
|
1694
|
-
# model switch may change the tool list. [] under none.
|
|
1695
|
-
def llm_context_layers(target)
|
|
1696
|
-
LLMContextStrategy.resolve(target, names: model_lookup_names(target), **llm_context_session_args).active_layers
|
|
1697
|
-
rescue StandardError
|
|
1698
|
-
[]
|
|
1699
|
-
end
|
|
1700
|
-
private :llm_context_layers
|
|
1701
|
-
|
|
1702
1688
|
# The base prompt (specs, plugins' declarations).
|
|
1703
1689
|
def assist_system_prompt(chat: false, thinking: nil)
|
|
1704
1690
|
@prompt_builder.base(chat: chat, thinking: thinking)
|
|
@@ -2029,14 +2015,16 @@ module Samagotchi
|
|
|
2029
2015
|
# sampling and thinking, the thinking resolved before the hooks run.
|
|
2030
2016
|
# The model name is added in #generate, after the hooks that may switch
|
|
2031
2017
|
# the model.
|
|
2018
|
+
# One resolve for all of them: nothing runs between them that could
|
|
2019
|
+
# change what a resolve gives.
|
|
2032
2020
|
def turn_settings(session)
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
LLM::TurnSettings.new(vision: vision, sampling: sampling, thinking:
|
|
2039
|
-
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,
|
|
2040
2028
|
window_setting: nil, llm_context: nil, price: nil)
|
|
2041
2029
|
end
|
|
2042
2030
|
private :turn_settings
|
|
@@ -2058,12 +2046,12 @@ module Samagotchi
|
|
|
2058
2046
|
def warm_up_next_turn(turn, result)
|
|
2059
2047
|
return if result.canceled? || result.resumable?
|
|
2060
2048
|
|
|
2061
|
-
|
|
2062
|
-
client =
|
|
2063
|
-
return unless warmup_target?(
|
|
2049
|
+
model = effective_model
|
|
2050
|
+
client = model.client
|
|
2051
|
+
return unless warmup_target?(model, client)
|
|
2064
2052
|
return if reminders_due? || @turn_state.carried_steers? || @next_turn_waiting&.call
|
|
2065
2053
|
|
|
2066
|
-
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) })
|
|
2067
2055
|
# Under the next turn's strategy (a /model switch may have changed it);
|
|
2068
2056
|
# the last turn's when it can't be resolved.
|
|
2069
2057
|
prompt, images = @kernel.warmup_prompt(messages, llm_context: turn_llm_context || @kernel.turn_settings&.llm_context)
|
|
@@ -2080,8 +2068,8 @@ module Samagotchi
|
|
|
2080
2068
|
# Never a paid or remote host (cache.warmup auto means local only), nor
|
|
2081
2069
|
# the chat loop (Splash keeps its own cache; llama.cpp's chat path is
|
|
2082
2070
|
# not measured), nor a non-llama.cpp transport.
|
|
2083
|
-
def warmup_target?(
|
|
2084
|
-
return false if
|
|
2071
|
+
def warmup_target?(model, client)
|
|
2072
|
+
return false if model.chat? || model.entry.remote?
|
|
2085
2073
|
return false unless client.respond_to?(:warm_up) && client.respond_to?(:transport) && client.transport.native?
|
|
2086
2074
|
|
|
2087
2075
|
PromptWarmup.enabled?
|
|
@@ -2122,16 +2110,19 @@ module Samagotchi
|
|
|
2122
2110
|
|
|
2123
2111
|
# Ask the effective model's backend.
|
|
2124
2112
|
def generate(turn, max_iterations:, max_tool_output_chars:, pending_input:)
|
|
2125
|
-
|
|
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)
|
|
2126
2117
|
# Route model name as bare (without host prefix) to the transport;
|
|
2127
2118
|
# host selection already done via active client.
|
|
2128
|
-
bare_for_backend = bare_model_name(
|
|
2119
|
+
bare_for_backend = bare_model_name(model.name)
|
|
2129
2120
|
# The turn's settings, set once here. The chat loop dispatches tools
|
|
2130
2121
|
# through the kernel without its #run: tag those dumps with this
|
|
2131
2122
|
# turn's model, not the last native one.
|
|
2132
|
-
explained = llm_context_explained
|
|
2133
|
-
@kernel.turn_settings = turn.settings.with(model_name: bare_for_backend, window_setting:
|
|
2134
|
-
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)
|
|
2135
2126
|
log_llm_context(explained)
|
|
2136
2127
|
|
|
2137
2128
|
turn.limit = @no_interrupt ? IterationLimit.for(no_interrupt: true) : max_iterations || IterationLimit.for
|
|
@@ -2139,7 +2130,7 @@ module Samagotchi
|
|
|
2139
2130
|
# end keeps it, so a Continue resumes this run.
|
|
2140
2131
|
run_start = turn.messages.length
|
|
2141
2132
|
reasoning_since = resumed_run_start(turn, run_start)
|
|
2142
|
-
result = backend.complete(
|
|
2133
|
+
result = backend(model).complete(
|
|
2143
2134
|
messages: turn.messages,
|
|
2144
2135
|
max_iterations: turn.limit,
|
|
2145
2136
|
on_stream_event: build_stream_event_handler(turn.on_event, cancel_controller: turn.controller),
|
|
@@ -2353,164 +2344,30 @@ module Samagotchi
|
|
|
2353
2344
|
|
|
2354
2345
|
# The turn's VisionContext: the session's images folder, and whether the
|
|
2355
2346
|
# effective model can see images, asked only when a request carries one.
|
|
2356
|
-
def turn_vision(session)
|
|
2357
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
2347
|
+
def turn_vision(session, model = effective_model)
|
|
2358
2348
|
VisionContext.new(session_dir: Session.session_dir(session.id, state_dir: session_state_dir),
|
|
2359
|
-
capability:
|
|
2360
|
-
VisionSupport.for(target, profile: profile, adapter: vision_adapter(target), names: model_lookup_names(target))
|
|
2361
|
-
})
|
|
2349
|
+
capability: -> { model.vision_support(profile) })
|
|
2362
2350
|
end
|
|
2363
2351
|
|
|
2364
2352
|
# The effective model's request parameters (hosts: and models:
|
|
2365
2353
|
# sampling), read per turn: /model can change the model between turns.
|
|
2366
|
-
def turn_sampling
|
|
2367
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
2368
|
-
SamplingSettings.for(target, names: model_lookup_names(target))
|
|
2369
|
-
rescue StandardError
|
|
2370
|
-
SamplingSettings::EMPTY
|
|
2371
|
-
end
|
|
2354
|
+
def turn_sampling = model_setting(SamplingSettings::EMPTY, &:sampling)
|
|
2372
2355
|
|
|
2373
2356
|
# The effective model's thinking level (Thinking.resolve), read each turn.
|
|
2374
|
-
def turn_thinking
|
|
2375
|
-
thinking_level(@host_registry.resolve(@effective_model_name))
|
|
2376
|
-
rescue StandardError
|
|
2377
|
-
Thinking::DEFAULT
|
|
2378
|
-
end
|
|
2379
|
-
|
|
2380
|
-
# An effort on a native host, which has no knob for it: said once per
|
|
2381
|
-
# session and host.
|
|
2382
|
-
def announce_thinking_level(level, target)
|
|
2383
|
-
return if target.entry.chat? || Thinking.native(level, profile).honoured
|
|
2384
|
-
|
|
2385
|
-
thinking_notice_once(:unsupported, target, :info,
|
|
2386
|
-
"#{level} isn't supported by native #{profile.name} on #{target.entry.name}; " \
|
|
2387
|
-
"thinking stays as the model has it")
|
|
2388
|
-
rescue StandardError
|
|
2389
|
-
nil
|
|
2390
|
-
end
|
|
2391
|
-
|
|
2392
|
-
# An effort on a llama.cpp chat host whose chat template takes none
|
|
2393
|
-
# (its /props says so): said once per session and host, as for a native
|
|
2394
|
-
# host. Read from the /props answer the turn's window probe left in the
|
|
2395
|
-
# cache, so it asks the server nothing.
|
|
2396
|
-
def announce_effort_ignored
|
|
2397
|
-
level, target = @turn_thinking
|
|
2398
|
-
return unless target&.entry&.chat? && Thinking::EFFORTS.include?(level)
|
|
2399
|
-
|
|
2400
|
-
client = target.client
|
|
2401
|
-
props = client.cached_server_props(model: target.bare_model)
|
|
2402
|
-
return unless Thinking.effort_ignored?(level, props)
|
|
2403
|
-
|
|
2404
|
-
thinking_notice_once(:unsupported, target, :info,
|
|
2405
|
-
"#{level} isn't supported by #{target.bare_model}'s chat template on #{target.entry.name} " \
|
|
2406
|
-
"(/props: supports_reasoning_effort false); thinking stays as the model has it")
|
|
2407
|
-
rescue StandardError
|
|
2408
|
-
nil
|
|
2409
|
-
end
|
|
2410
|
-
|
|
2411
|
-
# Whether this generation's thinking lane said anything: nil while it
|
|
2412
|
-
# streamed none, false while only whitespace (Gemma 4's empty thought
|
|
2413
|
-
# with thinking off), true once a real thought came.
|
|
2414
|
-
def note_generation_thought(thinking)
|
|
2415
|
-
return if thinking.empty?
|
|
2416
|
-
|
|
2417
|
-
@generation_thought ||= thinking.match?(/\S/)
|
|
2418
|
-
end
|
|
2419
|
-
|
|
2420
|
-
# Thinking off, and the model thought anyway: logged each time, said
|
|
2421
|
-
# once per session and host. A thinking lane of whitespace alone is no
|
|
2422
|
-
# thought.
|
|
2423
|
-
def check_thinking_honoured(event)
|
|
2424
|
-
level, target = @turn_thinking
|
|
2425
|
-
chars = event[:thinking_chars].to_i
|
|
2426
|
-
return unless level == :off && chars.positive? && target
|
|
2427
|
-
return if @generation_thought == false
|
|
2428
|
-
|
|
2429
|
-
Log.warn(:model, "thinking_not_honoured", level: level, host: target.entry.name, model: target.bare_model, chars: chars)
|
|
2430
|
-
thinking_notice_once(:not_honoured, target, :warn,
|
|
2431
|
-
"off wasn't honoured by #{target.bare_model} on #{target.entry.name} " \
|
|
2432
|
-
"(#{chars} chars of thinking); a sampling: override on the host or model may turn it off " \
|
|
2433
|
-
"(see Thinking in docs/configuration.md)")
|
|
2434
|
-
rescue StandardError
|
|
2435
|
-
nil
|
|
2436
|
-
end
|
|
2357
|
+
def turn_thinking = model_setting(Thinking::DEFAULT) { |model| thinking_level(model) }
|
|
2437
2358
|
|
|
2438
|
-
#
|
|
2439
|
-
# thinking off) and the chat loop sent the request without them: said
|
|
2440
|
-
# once per session and host, standing in for the not-honoured notice.
|
|
2441
|
-
def thinking_refused(event)
|
|
2442
|
-
_level, target = @turn_thinking
|
|
2443
|
-
return unless target
|
|
2444
|
-
|
|
2445
|
-
Log.warn(:model, "thinking_refused", level: event[:level], host: target.entry.name, model: event[:model],
|
|
2446
|
-
detail: event[:detail])
|
|
2447
|
-
(@thinking_notices ||= Set.new) << [@session&.id, target.entry.name, :not_honoured]
|
|
2448
|
-
thinking_notice_once(:refused, target, :warn,
|
|
2449
|
-
"#{target.entry.name} refused thinking: #{event[:level]} for #{event[:model]} (#{event[:detail]}); " \
|
|
2450
|
-
"sent without it, so thinking stays as the model has it")
|
|
2451
|
-
rescue StandardError
|
|
2452
|
-
nil
|
|
2453
|
-
end
|
|
2454
|
-
|
|
2455
|
-
# The host refused a sampling field (the empty-answer retry's
|
|
2456
|
-
# temperature on a model that takes none) and the chat loop asked again
|
|
2457
|
-
# without them, as it will for the model from now on: a log line.
|
|
2458
|
-
def sampling_refused(event)
|
|
2459
|
-
Log.info(:model, "sampling_refused", model: event[:model], detail: event[:detail])
|
|
2460
|
-
rescue StandardError
|
|
2461
|
-
nil
|
|
2462
|
-
end
|
|
2463
|
-
|
|
2464
|
-
# The host refused the model's reasoning sent back (reasoning_history)
|
|
2465
|
-
# and the chat loop asked the step again without it, as it will from
|
|
2466
|
-
# now on: logged, and said once per session and host.
|
|
2467
|
-
def reasoning_history_refused(event)
|
|
2468
|
-
_level, target = @turn_thinking
|
|
2469
|
-
return unless target
|
|
2470
|
-
|
|
2471
|
-
Log.warn(:model, "reasoning_history_refused", level: event[:level], host: target.entry.name, model: event[:model],
|
|
2472
|
-
detail: event[:detail])
|
|
2473
|
-
return unless (@thinking_notices ||= Set.new).add?([@session&.id, target.entry.name, :reasoning_history_refused])
|
|
2474
|
-
|
|
2475
|
-
hook_notify("#{target.entry.name} refused reasoning sent back (reasoning_history: #{event[:level]}) for " \
|
|
2476
|
-
"#{event[:model]} (#{event[:detail]}); sent without it from now on", :warn, "reasoning history")
|
|
2477
|
-
rescue StandardError
|
|
2478
|
-
nil
|
|
2479
|
-
end
|
|
2480
|
-
|
|
2481
|
-
def thinking_notice_once(kind, target, level, text)
|
|
2482
|
-
key = [@session&.id, target.entry.name, kind]
|
|
2483
|
-
return unless (@thinking_notices ||= Set.new).add?(key)
|
|
2484
|
-
|
|
2485
|
-
hook_notify(text, level, "thinking")
|
|
2486
|
-
end
|
|
2487
|
-
|
|
2488
|
-
# +target+'s ReasoningHistory level, the session's own first; the
|
|
2359
|
+
# +model+'s ReasoningHistory level, the session's own first; the
|
|
2489
2360
|
# default when it can't be resolved.
|
|
2490
|
-
def reasoning_history_level(
|
|
2491
|
-
ReasoningHistory.resolve(target, names: model_lookup_names(target), session: reasoning_history_override).first
|
|
2492
|
-
rescue StandardError
|
|
2493
|
-
ReasoningHistory::DEFAULT
|
|
2494
|
-
end
|
|
2361
|
+
def reasoning_history_level(model) = model.reasoning_history(own: reasoning_history_override)
|
|
2495
2362
|
|
|
2496
|
-
# +
|
|
2497
|
-
def thinking_level(
|
|
2498
|
-
Thinking.resolve(target, names: model_lookup_names(target), session: thinking_override).first
|
|
2499
|
-
rescue StandardError
|
|
2500
|
-
Thinking::DEFAULT
|
|
2501
|
-
end
|
|
2502
|
-
|
|
2503
|
-
def vision_adapter(target)
|
|
2504
|
-
target.entry.chat? ? @host_registry.adapter_for(target.entry) : nil
|
|
2505
|
-
rescue StandardError
|
|
2506
|
-
nil
|
|
2507
|
-
end
|
|
2363
|
+
# +model+'s thinking level (Thinking.resolve), the session's own first.
|
|
2364
|
+
def thinking_level(model) = model.thinking(own: thinking_override)
|
|
2508
2365
|
|
|
2509
2366
|
# A model known not to see images fails a turn with images up front.
|
|
2510
2367
|
def refuse_images!(vision)
|
|
2511
2368
|
return if vision.sendable?
|
|
2512
2369
|
|
|
2513
|
-
host =
|
|
2370
|
+
host = effective_model.host_name
|
|
2514
2371
|
raise LLM::VisionUnsupported.new("#{host}: #{vision.refusal_reason}", host: host)
|
|
2515
2372
|
end
|
|
2516
2373
|
|
|
@@ -2605,14 +2462,14 @@ module Samagotchi
|
|
|
2605
2462
|
session.messages = messages
|
|
2606
2463
|
end
|
|
2607
2464
|
|
|
2608
|
-
# Pick the loop for
|
|
2609
|
-
# host's adapter (one per host, kept for its cached model list).
|
|
2610
|
-
def backend_for(
|
|
2611
|
-
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?
|
|
2612
2469
|
|
|
2613
2470
|
@chat_backend_mutex.synchronize do
|
|
2614
2471
|
@chat_backend ||= LLM::ChatLoop.new(kernel: @kernel, stable_length: ->(text) { @prompt_builder.stable_length(text) })
|
|
2615
|
-
@chat_backend.adapter = @host_registry.adapter_for(
|
|
2472
|
+
@chat_backend.adapter = @host_registry.adapter_for(model.entry)
|
|
2616
2473
|
@chat_backend.session_id = session&.id
|
|
2617
2474
|
@chat_backend
|
|
2618
2475
|
end
|
|
@@ -2793,7 +2650,7 @@ module Samagotchi
|
|
|
2793
2650
|
# and bare model name, as a turn resolves them.
|
|
2794
2651
|
# @return [IdleTarget]
|
|
2795
2652
|
def session_model_recap_target
|
|
2796
|
-
IdleTarget.of_model(@host_registry,
|
|
2653
|
+
IdleTarget.of_model(@host_registry, effective_model_name)
|
|
2797
2654
|
end
|
|
2798
2655
|
|
|
2799
2656
|
# Build the idle reminders job. Always created (reminders are opt-in
|
|
@@ -2840,29 +2697,26 @@ module Samagotchi
|
|
|
2840
2697
|
@steer_cut.observe(event, progress)
|
|
2841
2698
|
case event[:type]
|
|
2842
2699
|
when :generation_started
|
|
2843
|
-
@
|
|
2700
|
+
@thinking_notices.generation_started
|
|
2844
2701
|
watch&.started(event[:iteration])
|
|
2845
2702
|
when :generation_retrying
|
|
2846
2703
|
# A dropped stream's step asked again: it streams from the start.
|
|
2847
2704
|
if event[:restarted]
|
|
2848
|
-
@
|
|
2705
|
+
@thinking_notices.generation_started
|
|
2849
2706
|
watch&.started(event[:iteration], restarted: true)
|
|
2850
2707
|
end
|
|
2851
|
-
when :generation_chunk then
|
|
2708
|
+
when :generation_chunk then @thinking_notices.note_thought(progress[:thinking])
|
|
2852
2709
|
end
|
|
2853
2710
|
# The chat loop asked again without the thinking fields or the
|
|
2854
2711
|
# reasoning sent back: a notice, not an event of its own.
|
|
2855
|
-
next thinking_refused(event) if event[:type] == :thinking_refused
|
|
2856
|
-
next reasoning_history_refused(event) if event[:type] == :reasoning_history_refused
|
|
2857
|
-
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
|
|
2858
2715
|
|
|
2859
2716
|
event = with_generation_report(event) if event[:type] == :generation_completed
|
|
2860
2717
|
emit_event(on_event, event)
|
|
2861
2718
|
watch.feed(**progress) if watch && progress
|
|
2862
|
-
if event[:type] == :generation_completed
|
|
2863
|
-
check_thinking_honoured(event)
|
|
2864
|
-
announce_effort_ignored
|
|
2865
|
-
end
|
|
2719
|
+
@thinking_notices.generation_completed(event) if event[:type] == :generation_completed
|
|
2866
2720
|
end
|
|
2867
2721
|
end
|
|
2868
2722
|
|
|
@@ -2919,38 +2773,13 @@ module Samagotchi
|
|
|
2919
2773
|
emit_event(on_event, { type: :used_memories_updated, used_memory_names: used_memory_names, read_names: read_names })
|
|
2920
2774
|
end
|
|
2921
2775
|
|
|
2922
|
-
# The window as the target's loop would see it (see ChatLoop#context_window:
|
|
2923
|
-
# a remote chat host has no /props, only its model list).
|
|
2924
|
-
def current_context_window(target)
|
|
2925
|
-
client = target.client
|
|
2926
|
-
adapter = nil
|
|
2927
|
-
if target.entry.chat?
|
|
2928
|
-
adapter = @host_registry.adapter_for(target.entry)
|
|
2929
|
-
client = nil if adapter.remote?
|
|
2930
|
-
end
|
|
2931
|
-
ContextWindow.resolve(client: client, model: target.bare_model, adapter: adapter,
|
|
2932
|
-
setting: ContextWindow.setting(target, names: model_lookup_names(target)))
|
|
2933
|
-
rescue StandardError
|
|
2934
|
-
nil
|
|
2935
|
-
end
|
|
2936
|
-
|
|
2937
2776
|
# The effective model's models:/hosts: window_tokens, read per turn
|
|
2938
2777
|
# (after the hooks that may switch the model).
|
|
2939
|
-
def turn_window_setting
|
|
2940
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
2941
|
-
ContextWindow.setting(target, names: model_lookup_names(target))
|
|
2942
|
-
rescue StandardError
|
|
2943
|
-
nil
|
|
2944
|
-
end
|
|
2778
|
+
def turn_window_setting = model_setting(nil, &:window_setting)
|
|
2945
2779
|
|
|
2946
2780
|
# The effective model's price on its host (hosts.<name>.models.<id>.price),
|
|
2947
2781
|
# by the requested id, never the served one; read per turn as the window is.
|
|
2948
|
-
def turn_price
|
|
2949
|
-
target = @host_registry.resolve(@effective_model_name)
|
|
2950
|
-
target.entry.models&.dig(target.bare_model.to_s.strip.downcase)&.price
|
|
2951
|
-
rescue StandardError
|
|
2952
|
-
nil
|
|
2953
|
-
end
|
|
2782
|
+
def turn_price = model_setting(nil, &:price)
|
|
2954
2783
|
|
|
2955
2784
|
# The LLM context strategy the next turn runs under: the session's own
|
|
2956
2785
|
# (Session#llm_context), else the effective model's (models:/hosts:
|
|
@@ -2964,57 +2793,82 @@ module Samagotchi
|
|
|
2964
2793
|
private :llm_context_session_args
|
|
2965
2794
|
|
|
2966
2795
|
# See #served_model. A report for another name (before a /model switch)
|
|
2967
|
-
# doesn't count. Without a
|
|
2968
|
-
# /props: nil until a turn.
|
|
2969
|
-
|
|
2970
|
-
|
|
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)
|
|
2971
2801
|
return [snapshot[:served_model], asked] if snapshot[:served_model] && snapshot[:served_model_for] == asked
|
|
2972
|
-
return [nil, nil] unless
|
|
2802
|
+
return [nil, nil] unless model
|
|
2973
2803
|
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
return [nil, nil] if adapter.remote?
|
|
2978
|
-
end
|
|
2979
|
-
served = ServedModel.from_props(client.server_props(model: target.bare_model))
|
|
2980
|
-
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.
|
|
2981
2807
|
rescue StandardError
|
|
2982
2808
|
[nil, nil]
|
|
2983
2809
|
end
|
|
2984
2810
|
|
|
2985
|
-
#
|
|
2986
|
-
|
|
2987
|
-
def prompt_served_model(target)
|
|
2988
|
-
entry = target.entry
|
|
2989
|
-
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
|
|
2990
2813
|
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
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] }
|
|
2995
2818
|
end
|
|
2996
2819
|
|
|
2997
|
-
#
|
|
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)
|
|
2998
2825
|
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
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)
|
|
3002
2837
|
end
|
|
2838
|
+
yield model
|
|
2839
|
+
end
|
|
3003
2840
|
|
|
3004
|
-
|
|
3005
|
-
|
|
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
|
|
3006
2850
|
end
|
|
3007
2851
|
|
|
3008
|
-
#
|
|
3009
|
-
#
|
|
3010
|
-
|
|
3011
|
-
|
|
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
|
|
3012
2865
|
end
|
|
3013
2866
|
|
|
3014
2867
|
# Everything that holds a profile follows the resolution: the kernel's
|
|
3015
2868
|
# prompt format and parser, and the system prompts built for the old one.
|
|
3016
2869
|
def apply_profile(resolution)
|
|
3017
|
-
|
|
2870
|
+
kept = @profile_resolution&.last
|
|
2871
|
+
@prompt_builder&.reset! if kept && kept.profile.name != resolution.profile.name
|
|
3018
2872
|
@kernel.use_profile!(resolution)
|
|
3019
2873
|
resolution
|
|
3020
2874
|
end
|
|
@@ -3023,11 +2877,10 @@ module Samagotchi
|
|
|
3023
2877
|
# server probe failed (unreachable, or 503 while loading a model). A
|
|
3024
2878
|
# profile that changes here drops the cached system prompts.
|
|
3025
2879
|
def refresh_profile!
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
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))
|
|
3031
2884
|
end
|
|
3032
2885
|
end
|
|
3033
2886
|
end
|