samagotchi 0.50.0 → 0.52.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 +89 -1
- data/README.md +1 -1
- data/bin/chi +11 -3
- data/docs/architecture.md +1 -1
- data/docs/cli.md +40 -11
- data/docs/configuration.md +63 -21
- data/docs/hooks.md +3 -2
- data/docs/internals/engine.md +4 -2
- data/docs/memory.md +11 -0
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +10 -5
- data/docs/testing.md +6 -1
- data/lib/samagotchi/bootstrap/config_writer.rb +6 -37
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +29 -17
- data/lib/samagotchi/bridge_client.rb +3 -1
- data/lib/samagotchi/broadcast/recipients.rb +1 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/commands/registry.rb +2 -1
- data/lib/samagotchi/config.rb +50 -182
- data/lib/samagotchi/config_text_edit.rb +8 -32
- data/lib/samagotchi/context_sources.rb +0 -7
- data/lib/samagotchi/context_window.rb +2 -2
- data/lib/samagotchi/continue_offer.rb +5 -4
- data/lib/samagotchi/cut_policy.rb +0 -1
- data/lib/samagotchi/delivery.rb +56 -0
- data/lib/samagotchi/engine.rb +48 -17
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/host_config.rb +183 -0
- data/lib/samagotchi/host_registry.rb +21 -13
- data/lib/samagotchi/kernel_loop.rb +28 -2
- data/lib/samagotchi/last_turn.rb +48 -0
- data/lib/samagotchi/live_versions.rb +0 -6
- data/lib/samagotchi/llm/native_tool_normalizer.rb +2 -7
- data/lib/samagotchi/llm_context_override.rb +3 -22
- data/lib/samagotchi/llm_context_strategy.rb +50 -16
- data/lib/samagotchi/memory_bundle/placeholder.rb +0 -5
- data/lib/samagotchi/model_profile.rb +4 -4
- data/lib/samagotchi/model_settings.rb +46 -0
- data/lib/samagotchi/plugin/context.rb +3 -3
- data/lib/samagotchi/plugin/sessions.rb +2 -2
- data/lib/samagotchi/queued_command.rb +35 -0
- data/lib/samagotchi/reminder_store.rb +0 -27
- data/lib/samagotchi/reply_wait.rb +11 -13
- data/lib/samagotchi/sampling_settings.rb +4 -4
- data/lib/samagotchi/self_report.rb +8 -5
- data/lib/samagotchi/send_command.rb +92 -21
- data/lib/samagotchi/session.rb +45 -24
- data/lib/samagotchi/session_commands.rb +41 -3
- data/lib/samagotchi/session_inbox.rb +36 -7
- data/lib/samagotchi/session_manager.rb +21 -10
- data/lib/samagotchi/session_metrics.rb +25 -6
- data/lib/samagotchi/session_setup.rb +30 -0
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/steer_cut.rb +21 -10
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +10 -4
- data/lib/samagotchi/terminal_ui/attached_loop.rb +48 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +69 -4
- data/lib/samagotchi/terminal_ui.rb +56 -8
- data/lib/samagotchi/thinking.rb +67 -7
- data/lib/samagotchi/thinking_command.rb +72 -0
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_runner.rb +2 -0
- data/lib/samagotchi/tools/builtin_calls.rb +33 -0
- data/lib/samagotchi/tools/builtins.rb +7 -2
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -3
- data/lib/samagotchi/web/app.rb +59 -7
- data/lib/samagotchi/web/message_parts.rb +3 -0
- data/lib/samagotchi/web/public/activity.js +6 -0
- data/lib/samagotchi/web/public/app.js +199 -48
- data/lib/samagotchi/web/public/data.js +8 -4
- data/lib/samagotchi/web/public/format.js +8 -4
- data/lib/samagotchi/web/public/index.html +25 -20
- data/lib/samagotchi/web/public/kept_commands.js +52 -0
- data/lib/samagotchi/web/public/model_pick.js +3 -1
- data/lib/samagotchi/web/public/session_title.js +26 -0
- data/lib/samagotchi/web/public/strip.js +4 -6
- data/lib/samagotchi/web/public/thinking_chip.js +169 -0
- data/lib/samagotchi/web/public/timing.js +15 -0
- data/lib/samagotchi/web/public/turn_events.js +26 -2
- data/lib/samagotchi/web/public/turn_html.js +3 -1
- data/lib/samagotchi/web/public/turn_view.js +328 -310
- data/lib/samagotchi/web/public/vendor/lit-html.js +8 -0
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +44 -43
- data/lib/samagotchi/worker_inbound.rb +10 -7
- data/lib/samagotchi/worker_wakes.rb +0 -2
- data/lib/samagotchi/yaml_lines.rb +72 -0
- data/lib/samagotchi.rb +1 -0
- metadata +13 -1
data/lib/samagotchi/config.rb
CHANGED
|
@@ -15,6 +15,11 @@ require_relative "host_model"
|
|
|
15
15
|
module Samagotchi
|
|
16
16
|
# Parses the thinking: levels of host and model entries (it needs Config).
|
|
17
17
|
autoload :Thinking, File.expand_path("thinking", __dir__)
|
|
18
|
+
# One hosts: entry; it needs ConfigFile's readers.
|
|
19
|
+
autoload :HostConfig, File.expand_path("host_config", __dir__)
|
|
20
|
+
# One models: entry, and one field found in them; they need ConfigFile's readers.
|
|
21
|
+
autoload :ModelSettings, File.expand_path("model_settings", __dir__)
|
|
22
|
+
autoload :ModelSetting, File.expand_path("model_settings", __dir__)
|
|
18
23
|
|
|
19
24
|
# Unified configuration registry implementing the implicit convention:
|
|
20
25
|
# ENV SAMAGOTCHI_ATTR (UPPER + prefix + _ = nesting)
|
|
@@ -170,10 +175,10 @@ module Samagotchi
|
|
|
170
175
|
Entry.new(key: "llm_context.stale_edits", yaml_path: %w[llm_context stale_edits], type: :bool, default: false, expose: %i[env config]),
|
|
171
176
|
# The steps (tool batches) back whose edited files' reads stale never stubs.
|
|
172
177
|
Entry.new(key: "llm_context.protect_steps", yaml_path: %w[llm_context protect_steps], type: :integer, default: 3, expose: %i[env config]),
|
|
173
|
-
# A soft context budget in tokens: the context status bands (and the forget layer's
|
|
174
|
-
# instead of the window; unset or
|
|
175
|
-
# hosts.<name>.llm_context_budget_tokens come first.
|
|
176
|
-
Entry.new(key: "llm_context.budget_tokens", yaml_path: %w[llm_context budget_tokens], type: :
|
|
178
|
+
# A soft context budget in tokens (64000, 64k; 4k to 10M): the context status bands (and the forget layer's
|
|
179
|
+
# offers) count against it instead of the window; unset, 0 or off: off. models.<key>.llm_context_budget_tokens
|
|
180
|
+
# and hosts.<name>.llm_context_budget_tokens come first. Kept as written: LLMContextStrategy.parse_budget reads it.
|
|
181
|
+
Entry.new(key: "llm_context.budget_tokens", yaml_path: %w[llm_context budget_tokens], type: :string, default: nil, expose: %i[env config]),
|
|
177
182
|
# The policy sentence forget_outputs' description carries (the forget layer); blank: none.
|
|
178
183
|
Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default: LLMContextStrategy::DEFAULT_POLICY, expose: %i[env config]),
|
|
179
184
|
|
|
@@ -841,6 +846,10 @@ module Samagotchi
|
|
|
841
846
|
HOST_NAME_RE = /\A[a-z0-9][a-z0-9._-]*\z/i
|
|
842
847
|
ENV_NAME_RE = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
843
848
|
|
|
849
|
+
# config.yml's hosts: (or a worker's SAMAGOTCHI_HOSTS_JSON) by lowercased
|
|
850
|
+
# name; an entry that is disabled or invalid (warned) is left out. No
|
|
851
|
+
# entry left: "default", from server.host/port/transport.
|
|
852
|
+
# @return [Hash{String => HostConfig}]
|
|
844
853
|
def hosts_config(env: ENV, path: global_path(env: env))
|
|
845
854
|
data = read_yaml(env: env, path: path)
|
|
846
855
|
raw_hosts = hosts_source(data, env)
|
|
@@ -848,143 +857,38 @@ module Samagotchi
|
|
|
848
857
|
normalized = {}
|
|
849
858
|
if raw_hosts.is_a?(Hash)
|
|
850
859
|
raw_hosts.each do |raw_name, raw_cfg|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
unless name.match?(HOST_NAME_RE)
|
|
855
|
-
warn_once "Warning: ignoring hosts entry '#{name}': must match /[a-z0-9][a-z0-9._-]*/i"
|
|
856
|
-
next
|
|
857
|
-
end
|
|
858
|
-
lowered = name.downcase
|
|
859
|
-
unless raw_cfg.is_a?(Hash)
|
|
860
|
-
warn_once "Warning: ignoring hosts entry '#{name}': expected mapping"
|
|
861
|
-
next
|
|
862
|
-
end
|
|
863
|
-
host = raw_cfg["host"] || raw_cfg[:host]
|
|
864
|
-
port = raw_cfg["port"] || raw_cfg[:port]
|
|
865
|
-
transport = raw_cfg["transport"] || raw_cfg[:transport]
|
|
866
|
-
api = raw_cfg["api"] || raw_cfg[:api]
|
|
867
|
-
next if host_disabled?(raw_cfg)
|
|
868
|
-
|
|
869
|
-
url = (raw_cfg["url"] || raw_cfg[:url]).to_s.strip
|
|
870
|
-
api_key_env = (raw_cfg["api_key_env"] || raw_cfg[:api_key_env]).to_s.strip
|
|
871
|
-
# Kept as written; ModelProfile.resolve warns about an unknown one.
|
|
872
|
-
profile = (raw_cfg["profile"] || raw_cfg[:profile]).to_s.strip.downcase
|
|
873
|
-
first_token_timeout = raw_cfg.key?("first_token_timeout") ? raw_cfg["first_token_timeout"] : raw_cfg[:first_token_timeout]
|
|
874
|
-
vision = ConfigFile.vision_flag(raw_cfg.key?("vision") ? raw_cfg["vision"] : raw_cfg[:vision], "hosts entry '#{name}'")
|
|
875
|
-
remote = ConfigFile.bool_flag(raw_cfg.key?("remote") ? raw_cfg["remote"] : raw_cfg[:remote], "hosts entry '#{name}'",
|
|
876
|
-
"remote")
|
|
877
|
-
window = ConfigFile.window_tokens(raw_cfg.key?("window_tokens") ? raw_cfg["window_tokens"] : raw_cfg[:window_tokens],
|
|
878
|
-
"hosts entry '#{name}'")
|
|
879
|
-
sampling = ConfigFile.sampling_map(raw_cfg.key?("sampling") ? raw_cfg["sampling"] : raw_cfg[:sampling], "hosts entry '#{name}'")
|
|
880
|
-
thinking = Thinking.level(raw_cfg.key?("thinking") ? raw_cfg["thinking"] : raw_cfg[:thinking], "hosts entry '#{name}'")
|
|
881
|
-
llm_context = LLMContextStrategy.parse(raw_cfg.key?(LLMContextStrategy::KEY) ? raw_cfg[LLMContextStrategy::KEY] : raw_cfg[:llm_context_strategy],
|
|
882
|
-
"hosts entry '#{name}'")
|
|
883
|
-
llm_context_apply = LLMContextStrategy.parse_apply(raw_cfg.key?(LLMContextStrategy::APPLY_KEY) ? raw_cfg[LLMContextStrategy::APPLY_KEY] : raw_cfg[:llm_context_apply],
|
|
884
|
-
"hosts entry '#{name}'")
|
|
885
|
-
models = HostModel.parse_map(raw_cfg.key?("models") ? raw_cfg["models"] : raw_cfg[:models], name)
|
|
886
|
-
llm_context_budget = LLMContextStrategy.parse_budget(raw_cfg.key?(LLMContextStrategy::BUDGET_KEY) ? raw_cfg[LLMContextStrategy::BUDGET_KEY] : raw_cfg[:llm_context_budget_tokens],
|
|
887
|
-
"hosts entry '#{name}'")
|
|
888
|
-
unless first_token_timeout.nil? || (first_token_timeout.is_a?(Numeric) && !first_token_timeout.negative?)
|
|
889
|
-
warn_once "Warning: hosts entry '#{name}': first_token_timeout must be seconds (0 = off); using the default"
|
|
890
|
-
first_token_timeout = nil
|
|
891
|
-
end
|
|
892
|
-
unless api_key_env.empty? || api_key_env.match?(ENV_NAME_RE)
|
|
893
|
-
warn_once "Warning: ignoring hosts entry '#{name}': api_key_env must be an environment variable name"
|
|
894
|
-
next
|
|
895
|
-
end
|
|
896
|
-
scheme = "http"
|
|
897
|
-
unless url.empty?
|
|
898
|
-
unless host.to_s.strip.empty? && port.to_s.strip.empty?
|
|
899
|
-
warn_once "Warning: ignoring hosts entry '#{name}': give url or host/port, not both"
|
|
900
|
-
next
|
|
901
|
-
end
|
|
902
|
-
uri = begin
|
|
903
|
-
URI.parse(url)
|
|
904
|
-
rescue URI::InvalidURIError
|
|
905
|
-
nil
|
|
906
|
-
end
|
|
907
|
-
unless uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
|
|
908
|
-
warn_once "Warning: ignoring hosts entry '#{name}': url must be an http(s) URL"
|
|
909
|
-
next
|
|
910
|
-
end
|
|
911
|
-
host = uri.host
|
|
912
|
-
port = uri.port
|
|
913
|
-
scheme = uri.scheme
|
|
914
|
-
url = url.chomp("/")
|
|
915
|
-
end
|
|
916
|
-
host = host.to_s.strip
|
|
917
|
-
if host.empty?
|
|
918
|
-
warn_once "Warning: ignoring hosts entry '#{name}': host is required"
|
|
919
|
-
next
|
|
920
|
-
end
|
|
921
|
-
port_val = port.to_s.strip.empty? ? 8080 : port.to_i
|
|
922
|
-
if port_val <= 0 || port_val > 65_535
|
|
923
|
-
warn_once "Warning: ignoring hosts entry '#{name}': invalid port"
|
|
924
|
-
next
|
|
925
|
-
end
|
|
926
|
-
transport_val = transport.to_s.strip.downcase
|
|
927
|
-
if transport_val.empty?
|
|
928
|
-
transport_val = nil
|
|
929
|
-
elsif !VALID_TRANSPORTS_FOR_CONFIG.include?(transport_val)
|
|
930
|
-
warn_once "Warning: ignoring hosts entry '#{name}': unknown transport '#{transport_val}'"
|
|
931
|
-
next
|
|
932
|
-
end
|
|
933
|
-
api_val = api.to_s.strip.downcase
|
|
934
|
-
if api_val.empty?
|
|
935
|
-
api_val = nil
|
|
936
|
-
elsif !VALID_APIS_FOR_CONFIG.include?(api_val)
|
|
937
|
-
warn_once "Warning: ignoring hosts entry '#{name}': unknown api '#{api_val}'"
|
|
938
|
-
next
|
|
939
|
-
elsif VALID_TRANSPORTS_FOR_CONFIG.include?(api_val)
|
|
940
|
-
# A raw-prompt api is the transport; a different transport contradicts it.
|
|
941
|
-
if transport_val && transport_val != api_val
|
|
942
|
-
warn_once "Warning: ignoring hosts entry '#{name}': api '#{api_val}' conflicts with transport '#{transport_val}'"
|
|
943
|
-
next
|
|
944
|
-
end
|
|
945
|
-
transport_val = api_val
|
|
946
|
-
end
|
|
947
|
-
normalized[lowered] = { name: lowered, host: host, port: port_val, transport: transport_val ? transport_val.to_sym : nil,
|
|
948
|
-
api: api_val&.to_sym, original_name: name, scheme: scheme,
|
|
949
|
-
url: url.empty? ? nil : url, api_key_env: api_key_env.empty? ? nil : api_key_env,
|
|
950
|
-
profile: profile.empty? ? nil : profile, first_token_timeout: first_token_timeout,
|
|
951
|
-
vision: vision, sampling: sampling, thinking: thinking, remote: remote,
|
|
952
|
-
window_tokens: window, llm_context_strategy: llm_context,
|
|
953
|
-
llm_context_apply: llm_context_apply, llm_context_budget_tokens: llm_context_budget,
|
|
954
|
-
models: models }
|
|
860
|
+
host = HostConfig.parse(raw_name, raw_cfg)
|
|
861
|
+
normalized[host.name] = host if host
|
|
955
862
|
end
|
|
956
863
|
warn_duplicate_host_models(normalized)
|
|
957
864
|
end
|
|
958
|
-
|
|
959
|
-
# If no hosts defined, synthesize "default" from server.host/port/transport.
|
|
960
|
-
if normalized.empty?
|
|
961
|
-
opts = { file_data: data, env: env, cli_overrides: Samagotchi::Config.cli_overrides }
|
|
962
|
-
default_host = Samagotchi::Config.resolve("server.host", **opts).to_s.strip
|
|
963
|
-
default_host = "localhost" if default_host.empty?
|
|
964
|
-
default_port = Samagotchi::Config.resolve("server.port", **opts).to_i
|
|
965
|
-
default_port = 8080 if default_port <= 0 || default_port > 65_535
|
|
966
|
-
transport_sym = Samagotchi::Config.resolve_with_origin("server.transport", **opts).then do |value, origin|
|
|
967
|
-
origin == :default ? nil : value.to_sym
|
|
968
|
-
end
|
|
969
|
-
normalized["default"] = { name: "default", host: default_host, port: default_port, transport: transport_sym, original_name: "default" }
|
|
970
|
-
end
|
|
865
|
+
normalized["default"] = default_host_config(data, env) if normalized.empty?
|
|
971
866
|
normalized
|
|
972
|
-
rescue StandardError
|
|
867
|
+
rescue StandardError => e
|
|
868
|
+
Log.error(:config, "hosts_config_failed", error: e.class.name, message: e.message)
|
|
973
869
|
{}
|
|
974
870
|
end
|
|
975
871
|
|
|
976
|
-
#
|
|
977
|
-
def
|
|
978
|
-
|
|
872
|
+
# The host chi uses when hosts: names none: server.host/port/transport.
|
|
873
|
+
def default_host_config(data, env)
|
|
874
|
+
opts = { file_data: data, env: env, cli_overrides: Samagotchi::Config.cli_overrides }
|
|
875
|
+
host = Samagotchi::Config.resolve("server.host", **opts).to_s.strip
|
|
876
|
+
host = "localhost" if host.empty?
|
|
877
|
+
port = Samagotchi::Config.resolve("server.port", **opts).to_i
|
|
878
|
+
port = 8080 if port <= 0 || port > 65_535
|
|
879
|
+
transport = Samagotchi::Config.resolve_with_origin("server.transport", **opts).then do |value, origin|
|
|
880
|
+
origin == :default ? nil : value.to_sym
|
|
881
|
+
end
|
|
882
|
+
HostConfig.new(name: "default", host: host, port: port, transport: transport)
|
|
979
883
|
end
|
|
980
|
-
private_class_method :
|
|
884
|
+
private_class_method :default_host_config
|
|
981
885
|
|
|
982
886
|
# Two hosts declaring one id under hosts.<name>.models: a bare id goes
|
|
983
887
|
# where HostRegistry#host_for_model sends it (the default host when it
|
|
984
888
|
# declares the id, else the first in hosts: order); say so once.
|
|
985
889
|
def warn_duplicate_host_models(hosts)
|
|
986
890
|
declared = Hash.new { |h, k| h[k] = [] }
|
|
987
|
-
hosts.each { |name, cfg| cfg
|
|
891
|
+
hosts.each { |name, cfg| cfg.models.each { |down, model| declared[down] << [name, model.id] } }
|
|
988
892
|
declared.each_value do |owners|
|
|
989
893
|
next if owners.size < 2
|
|
990
894
|
|
|
@@ -1005,7 +909,7 @@ module Samagotchi
|
|
|
1005
909
|
raw = hosts_source(read_yaml(env: env, path: path), env)
|
|
1006
910
|
return [] unless raw.is_a?(Hash)
|
|
1007
911
|
|
|
1008
|
-
raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) &&
|
|
912
|
+
raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && HostConfig.disabled?(cfg) }
|
|
1009
913
|
rescue StandardError
|
|
1010
914
|
[]
|
|
1011
915
|
end
|
|
@@ -1026,17 +930,6 @@ module Samagotchi
|
|
|
1026
930
|
end
|
|
1027
931
|
private_class_method :hosts_source
|
|
1028
932
|
|
|
1029
|
-
# enabled: false (or "false", any case) in a hosts entry.
|
|
1030
|
-
def host_disabled?(cfg)
|
|
1031
|
-
enabled = if cfg.key?("enabled")
|
|
1032
|
-
cfg["enabled"]
|
|
1033
|
-
else
|
|
1034
|
-
(cfg.key?(:enabled) ? cfg[:enabled] : true)
|
|
1035
|
-
end
|
|
1036
|
-
enabled == false || enabled.to_s.strip.downcase == "false"
|
|
1037
|
-
end
|
|
1038
|
-
private_class_method :host_disabled?
|
|
1039
|
-
|
|
1040
933
|
# Resolve the `recap:` section through the Config registry so scalar
|
|
1041
934
|
# recap settings share the single precedence path (CLI > ENV > file >
|
|
1042
935
|
# default). Returns false when explicitly disabled (file `recap: false`
|
|
@@ -1115,25 +1008,14 @@ module Samagotchi
|
|
|
1115
1008
|
# include when hosts file exists with hosts: section or when workers need propagation
|
|
1116
1009
|
return nil if hosts.nil? || hosts.empty?
|
|
1117
1010
|
|
|
1118
|
-
#
|
|
1119
|
-
simple = hosts.transform_values
|
|
1120
|
-
# A url entry travels as its url (host/port come from it); the API
|
|
1121
|
-
# key stays in the environment, which workers inherit.
|
|
1122
|
-
location = v[:url] ? { "url" => v[:url] } : { "host" => v[:host], "port" => v[:port] }
|
|
1123
|
-
location.merge("transport" => v[:transport]&.to_s, "api" => v[:api]&.to_s, "api_key_env" => v[:api_key_env],
|
|
1124
|
-
"profile" => v[:profile], "first_token_timeout" => v[:first_token_timeout],
|
|
1125
|
-
"vision" => v[:vision], "sampling" => v[:sampling], "thinking" => v[:thinking]&.to_s,
|
|
1126
|
-
"remote" => v[:remote], "window_tokens" => v[:window_tokens],
|
|
1127
|
-
"llm_context_strategy" => v[:llm_context_strategy]&.map(&:to_s),
|
|
1128
|
-
"llm_context_apply" => v[:llm_context_apply]&.to_s,
|
|
1129
|
-
"llm_context_budget_tokens" => v[:llm_context_budget_tokens],
|
|
1130
|
-
"models" => host_models_config(v[:models])).compact
|
|
1131
|
-
end
|
|
1011
|
+
# Every field travels (HostConfig#to_env_h); the worker parses it back.
|
|
1012
|
+
simple = hosts.transform_values(&:to_env_h)
|
|
1132
1013
|
# Disabled hosts travel as just that, so a worker refuses "box:x"
|
|
1133
1014
|
# the way its parent does instead of sending it to the default host.
|
|
1134
1015
|
disabled_host_names(env: env, path: path).grep(HOST_NAME_RE).each { |name| simple[name] ||= { "enabled" => false } }
|
|
1135
1016
|
JSON.generate(simple)
|
|
1136
|
-
rescue StandardError
|
|
1017
|
+
rescue StandardError => e
|
|
1018
|
+
Log.error(:config, "hosts_json_failed", error: e.class.name, message: e.message)
|
|
1137
1019
|
nil
|
|
1138
1020
|
end
|
|
1139
1021
|
|
|
@@ -1197,9 +1079,9 @@ module Samagotchi
|
|
|
1197
1079
|
end
|
|
1198
1080
|
|
|
1199
1081
|
# The top-level `models:` map: per-model settings keyed by model id or
|
|
1200
|
-
# alias (downcased), e.g. `models: {my-alias: {profile: qwen36}}
|
|
1201
|
-
#
|
|
1202
|
-
#
|
|
1082
|
+
# alias (downcased), e.g. `models: {my-alias: {profile: qwen36}}`, each a
|
|
1083
|
+
# ModelSettings; an entry that is not a map is skipped.
|
|
1084
|
+
# @return [Hash{String => ModelSettings}]
|
|
1203
1085
|
def model_settings(env: ENV, path: global_path(env: env))
|
|
1204
1086
|
data = read_yaml(env: env, path: path)
|
|
1205
1087
|
raw = data[MODELS_KEY] if data.is_a?(Hash)
|
|
@@ -1207,40 +1089,26 @@ module Samagotchi
|
|
|
1207
1089
|
|
|
1208
1090
|
raw.each_with_object({}) do |(k, v), result|
|
|
1209
1091
|
key = k.to_s.strip.downcase
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
profile = (v["profile"] || v[:profile]).to_s.strip.downcase
|
|
1213
|
-
vision = vision_flag(v.key?("vision") ? v["vision"] : v[:vision], "models: #{key}")
|
|
1214
|
-
result[key] = { profile: profile.empty? ? nil : profile }
|
|
1215
|
-
result[key][:vision] = vision unless vision.nil?
|
|
1216
|
-
sampling = sampling_map(v.key?("sampling") ? v["sampling"] : v[:sampling], "models: #{key}")
|
|
1217
|
-
result[key][:sampling] = sampling if sampling
|
|
1218
|
-
thinking = Thinking.level(v.key?("thinking") ? v["thinking"] : v[:thinking], "models: #{key}")
|
|
1219
|
-
result[key][:thinking] = thinking if thinking
|
|
1220
|
-
window = window_tokens(v.key?("window_tokens") ? v["window_tokens"] : v[:window_tokens], "models: #{key}")
|
|
1221
|
-
result[key][:window_tokens] = window if window
|
|
1222
|
-
layers = LLMContextStrategy.parse(v.key?(LLMContextStrategy::KEY) ? v[LLMContextStrategy::KEY] : v[:llm_context_strategy],
|
|
1223
|
-
"models: #{key}")
|
|
1224
|
-
result[key][:llm_context_strategy] = layers if layers
|
|
1225
|
-
apply = LLMContextStrategy.parse_apply(v.key?(LLMContextStrategy::APPLY_KEY) ? v[LLMContextStrategy::APPLY_KEY] : v[:llm_context_apply],
|
|
1226
|
-
"models: #{key}")
|
|
1227
|
-
result[key][:llm_context_apply] = apply if apply
|
|
1228
|
-
budget = LLMContextStrategy.parse_budget(v.key?(LLMContextStrategy::BUDGET_KEY) ? v[LLMContextStrategy::BUDGET_KEY] : v[:llm_context_budget_tokens],
|
|
1229
|
-
"models: #{key}")
|
|
1230
|
-
result[key][:llm_context_budget_tokens] = budget if budget
|
|
1092
|
+
settings = ModelSettings.parse(key, v)
|
|
1093
|
+
result[key] = settings if settings
|
|
1231
1094
|
end
|
|
1232
1095
|
rescue StandardError
|
|
1233
1096
|
{}
|
|
1234
1097
|
end
|
|
1235
1098
|
|
|
1236
1099
|
# The first +names+ (model as typed, alias-resolved, bare, …) whose
|
|
1237
|
-
# models: entry sets +field
|
|
1238
|
-
#
|
|
1100
|
+
# models: entry sets +field+ (a ModelSettings member; another name
|
|
1101
|
+
# raises ArgumentError): a ModelSetting (key, value), or nil.
|
|
1102
|
+
# VisionSupport and SamplingSettings look a model up the same way.
|
|
1103
|
+
# @param models [Hash{String => ModelSettings}, nil] model_settings by default
|
|
1104
|
+
# @return [ModelSetting, nil]
|
|
1239
1105
|
def model_setting(names, field, models: nil)
|
|
1106
|
+
raise ArgumentError, "unknown models: field #{field.inspect}" unless ModelSettings.members.include?(field)
|
|
1107
|
+
|
|
1240
1108
|
models ||= model_settings
|
|
1241
1109
|
Array(names).map { |name| name.to_s.strip.downcase }.reject(&:empty?).uniq.each do |key|
|
|
1242
|
-
value = models
|
|
1243
|
-
return
|
|
1110
|
+
value = models[key]&.public_send(field)
|
|
1111
|
+
return ModelSetting.new(key: key, value: value) unless value.nil?
|
|
1244
1112
|
end
|
|
1245
1113
|
nil
|
|
1246
1114
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "yaml"
|
|
5
5
|
require "psych"
|
|
6
|
+
require_relative "yaml_lines"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# Sets one section.key in config.yml text, line by line, so the user's
|
|
@@ -15,7 +16,9 @@ module Samagotchi
|
|
|
15
16
|
# section, anchors, a duplicate section), the whole file is dumped from
|
|
16
17
|
# +expected+ instead.
|
|
17
18
|
module ConfigTextEdit
|
|
18
|
-
|
|
19
|
+
extend YAMLLines
|
|
20
|
+
|
|
21
|
+
DEFAULT_INDENT = YAMLLines::DEFAULT_INDENT
|
|
19
22
|
|
|
20
23
|
module_function
|
|
21
24
|
|
|
@@ -42,9 +45,7 @@ module Samagotchi
|
|
|
42
45
|
section_data = original[section]
|
|
43
46
|
has_key = section_data.is_a?(Hash) && section_data.keys.any? { |k| k.to_s.downcase == key.downcase }
|
|
44
47
|
|
|
45
|
-
eol = text
|
|
46
|
-
lines = text.split(/(?<=\n)/)
|
|
47
|
-
lines[-1] = "#{lines[-1]}#{eol}" if lines.any? && !lines[-1].end_with?("\n")
|
|
48
|
+
lines, eol = lines_of(text)
|
|
48
49
|
header = /\A#{Regexp.escape(section)}:[ \t]*(#.*)?\z/
|
|
49
50
|
starts = lines.each_index.select { |i| lines[i].chomp.match?(header) }
|
|
50
51
|
return nil if starts.length > 1
|
|
@@ -57,9 +58,8 @@ module Samagotchi
|
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
at = starts.first
|
|
60
|
-
stop = at
|
|
61
|
-
|
|
62
|
-
children = lines[(at + 1)...stop].reject { |line| line.strip.empty? || comment?(line) }
|
|
61
|
+
stop = block_stop(lines, at)
|
|
62
|
+
children = lines[(at + 1)...stop].reject { |line| blank_or_comment?(line) }
|
|
63
63
|
child = children.first ? indent(children.first) : DEFAULT_INDENT
|
|
64
64
|
new_line = "#{" " * child}#{key}: #{scalar(value)}#{eol}"
|
|
65
65
|
found = ((at + 1)...stop).find { |i| indent(lines[i]) == child && key_of(lines[i])&.downcase == key.downcase }
|
|
@@ -68,8 +68,7 @@ module Samagotchi
|
|
|
68
68
|
else
|
|
69
69
|
return nil if has_key
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
lines.insert(stop, new_line)
|
|
71
|
+
lines.insert(insert_at(lines, at, stop, child), new_line)
|
|
73
72
|
end
|
|
74
73
|
lines.join
|
|
75
74
|
end
|
|
@@ -78,30 +77,7 @@ module Samagotchi
|
|
|
78
77
|
line[/\A\s*(["']?)([^"':#\s][^"':#]*?)\1:(?:\s|\z)/, 2]
|
|
79
78
|
end
|
|
80
79
|
|
|
81
|
-
def inside_block?(line)
|
|
82
|
-
return true if line.strip.empty?
|
|
83
|
-
return false if line.start_with?("---", "...")
|
|
84
|
-
|
|
85
|
-
line.start_with?(" ", "\t") || comment?(line)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def comment?(line) = line.lstrip.start_with?("#")
|
|
89
|
-
def indent(line) = line[/\A */].length
|
|
90
|
-
|
|
91
80
|
# JSON's quoting is valid YAML: model ids hold "/" and ":".
|
|
92
81
|
def scalar(value) = JSON.generate(value.to_s)
|
|
93
|
-
|
|
94
|
-
def parse(text) = YAML.safe_load(text, permitted_classes: [], aliases: false)
|
|
95
|
-
|
|
96
|
-
def anchors?(text)
|
|
97
|
-
stack = [Psych.parse_stream(text)]
|
|
98
|
-
until stack.empty?
|
|
99
|
-
node = stack.pop
|
|
100
|
-
return true if node.is_a?(Psych::Nodes::Alias) || (node.respond_to?(:anchor) && node.anchor)
|
|
101
|
-
|
|
102
|
-
stack.concat(Array(node.children)) if node.respond_to?(:children)
|
|
103
|
-
end
|
|
104
|
-
false
|
|
105
|
-
end
|
|
106
82
|
end
|
|
107
83
|
end
|
|
@@ -220,13 +220,6 @@ module Samagotchi
|
|
|
220
220
|
data ? Snapshot.from_h(data) : Snapshot.empty
|
|
221
221
|
end
|
|
222
222
|
|
|
223
|
-
# @return [Time, nil] the snapshot file's mtime: the worker's cheap "anything new?"
|
|
224
|
-
def snapshot_mtime(name)
|
|
225
|
-
File.mtime(snapshot_path(name))
|
|
226
|
-
rescue SystemCallError
|
|
227
|
-
nil
|
|
228
|
-
end
|
|
229
|
-
|
|
230
223
|
def write_snapshot(name, snapshot)
|
|
231
224
|
FileUtils.mkdir_p(dir)
|
|
232
225
|
AtomicFile.write(snapshot_path(name), JSON.generate(snapshot.to_h))
|
|
@@ -50,11 +50,11 @@ module Samagotchi
|
|
|
50
50
|
# and vision look a model up), else hosts.<name>.window_tokens.
|
|
51
51
|
# @param target [HostRegistry::ModelTarget]
|
|
52
52
|
# @param names [Array<String>] HostRegistry#lookup_names
|
|
53
|
-
# @param models [Hash, nil] ConfigFile.model_settings (specs)
|
|
53
|
+
# @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
|
|
54
54
|
# @return [Resolved, nil]
|
|
55
55
|
def setting(target, names:, models: nil)
|
|
56
56
|
models ||= ConfigFile.model_settings
|
|
57
|
-
|
|
57
|
+
tokens = ConfigFile.model_setting(names, :window_tokens, models: models)&.value
|
|
58
58
|
return Resolved.new(tokens: tokens, source: :model_setting) if positive_integer?(tokens)
|
|
59
59
|
|
|
60
60
|
tokens = target&.entry&.window_tokens
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
|
+
require_relative "last_turn"
|
|
4
5
|
require_relative "log"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
@@ -85,11 +86,11 @@ module Samagotchi
|
|
|
85
86
|
# Stop runs no turn: last_turn says so (not_continued), so a wait on
|
|
86
87
|
# it ends (chi answer --option Stop).
|
|
87
88
|
def announce_resolved(result, command)
|
|
88
|
-
@engine.announce(type: :continue_resolved, decision: result.decision.to_s, client_id: command
|
|
89
|
+
@engine.announce(type: :continue_resolved, decision: result.decision.to_s, client_id: command.client_id)
|
|
89
90
|
return if result.resume
|
|
90
91
|
|
|
91
92
|
session = @engine.session
|
|
92
|
-
session.last_turn =
|
|
93
|
+
session.last_turn = LastTurn.new(outcome: NOT_CONTINUED, ended_at: Time.now.iso8601(3)) if session
|
|
93
94
|
end
|
|
94
95
|
|
|
95
96
|
# After a command's command_ran and save: a /continue that answered the
|
|
@@ -128,7 +129,7 @@ module Samagotchi
|
|
|
128
129
|
Log.warn(:worker, "turn_not_begun", continue: true, error: e.class.name, msg: e.message)
|
|
129
130
|
return announce_offer(offer)
|
|
130
131
|
end
|
|
131
|
-
@run_turn.call(nil, continue: true, origin: { client_id: command
|
|
132
|
+
@run_turn.call(nil, continue: true, origin: { client_id: command.client_id }.compact,
|
|
132
133
|
max_iterations: @max_iterations.call(offer[:no_interrupt])) do |result, error|
|
|
133
134
|
if error
|
|
134
135
|
announce_offer(offer)
|
|
@@ -183,7 +184,7 @@ module Samagotchi
|
|
|
183
184
|
# The limit the offer's turn ran out at (Engine's last_turn marker).
|
|
184
185
|
def limit_of_last_turn
|
|
185
186
|
last = @engine.session&.last_turn
|
|
186
|
-
last
|
|
187
|
+
last&.exhausted ? last.limit : nil
|
|
187
188
|
end
|
|
188
189
|
|
|
189
190
|
# The question's answer as the /continue line a typed answer would be:
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# What a message sent to a running turn does when it arrives: wait for the
|
|
5
|
+
# turn's next step boundary and go in there (the default), cut the running
|
|
6
|
+
# generation short to go in now, or wait for the turn to end and run as a
|
|
7
|
+
# turn of its own (see docs/configuration.md). The field travels as the
|
|
8
|
+
# wire value name; a sender that names nothing gets the default.
|
|
9
|
+
module Delivery
|
|
10
|
+
NEXT_STEP = "next_step"
|
|
11
|
+
CUT = "cut"
|
|
12
|
+
QUEUE = "queue"
|
|
13
|
+
VALUES = [NEXT_STEP, CUT, QUEUE].freeze
|
|
14
|
+
|
|
15
|
+
# A body's delivery field as one of VALUES. Missing, empty or unknown:
|
|
16
|
+
# NEXT_STEP (an older client, a hand-written request).
|
|
17
|
+
# @param raw [Object] the wire value
|
|
18
|
+
# @return [String] one of VALUES
|
|
19
|
+
def self.parse(raw)
|
|
20
|
+
raw.is_a?(String) && VALUES.include?(raw) ? raw : NEXT_STEP
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Why a request body's delivery field can't be taken, as {error:,
|
|
24
|
+
# detail:} for a 400, or nil when it can. The Bridge and the web's
|
|
25
|
+
# POST /turn both ask this, so they refuse the same values. A missing
|
|
26
|
+
# or unknown string is not refused: it is the default (#parse). QUEUE
|
|
27
|
+
# is refused until a worker runs it (no worker reads it yet, so it
|
|
28
|
+
# would merge at the next step while the ack said queue).
|
|
29
|
+
# @param raw [Object] the wire value
|
|
30
|
+
# @return [Hash, nil]
|
|
31
|
+
def self.refusal(raw)
|
|
32
|
+
return nil if raw.nil?
|
|
33
|
+
return { error: "bad_delivery", detail: "delivery must be a string" } unless raw.is_a?(String)
|
|
34
|
+
return QUEUE_UNAVAILABLE if raw == QUEUE
|
|
35
|
+
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
QUEUE_UNAVAILABLE = {
|
|
40
|
+
error: "delivery_unavailable",
|
|
41
|
+
detail: "delivery queue isn't available yet; without it the message goes in at the running turn's next step " \
|
|
42
|
+
"(or runs as a turn when none runs)"
|
|
43
|
+
}.freeze
|
|
44
|
+
private_constant :QUEUE_UNAVAILABLE
|
|
45
|
+
|
|
46
|
+
# Whether it is the one every sender defaulting to a step boundary uses.
|
|
47
|
+
def self.next_step?(value) = parse(value) == NEXT_STEP
|
|
48
|
+
|
|
49
|
+
# Whether the running turn should be cut for it (only when the message
|
|
50
|
+
# asks: nothing else cuts).
|
|
51
|
+
def self.cut?(value) = parse(value) == CUT
|
|
52
|
+
|
|
53
|
+
# Whether it waits for the turn to end and runs as a turn of its own.
|
|
54
|
+
def self.queue?(value) = parse(value) == QUEUE
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -954,7 +954,7 @@ module Samagotchi
|
|
|
954
954
|
served, served_for = served_model_for(snapshot, target: target)
|
|
955
955
|
snapshot = snapshot.merge(served_model: served, served_model_for: served_for,
|
|
956
956
|
served_expected_by: served_expected_by(served, target: target), llm_context: llm_context_summary,
|
|
957
|
-
prompt_notes: prompt_notes.map(&:to_h), price_host: target.entry.name)
|
|
957
|
+
thinking: thinking_explained&.summary, prompt_notes: prompt_notes.map(&:to_h), price_host: target.entry.name)
|
|
958
958
|
unless snapshot.dig(:context, :window_tokens)
|
|
959
959
|
window = current_context_window(target)
|
|
960
960
|
if window
|
|
@@ -1029,16 +1029,46 @@ module Samagotchi
|
|
|
1029
1029
|
nil
|
|
1030
1030
|
end
|
|
1031
1031
|
|
|
1032
|
-
# "off (models: qwen)" for /model: the effective
|
|
1033
|
-
# and where it came from, nil when none is set.
|
|
1032
|
+
# "off (models: qwen)", "low (session)" for /model: the effective
|
|
1033
|
+
# model's thinking level and where it came from, nil when none is set.
|
|
1034
1034
|
def thinking_summary
|
|
1035
|
+
explained = thinking_explained
|
|
1036
|
+
explained&.source ? explained.label : nil
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
# The effective model's thinking level with where it came from and the
|
|
1040
|
+
# session's own (Thinking::Explained), as the next turn resolves it;
|
|
1041
|
+
# nil when it can't be resolved.
|
|
1042
|
+
# @return [Thinking::Explained, nil]
|
|
1043
|
+
def thinking_explained
|
|
1035
1044
|
target = @host_registry.resolve(@effective_model_name)
|
|
1036
|
-
|
|
1037
|
-
source ? "#{level} (#{source})" : nil
|
|
1045
|
+
Thinking.explain(target, names: model_lookup_names(target), session: thinking_override)
|
|
1038
1046
|
rescue StandardError
|
|
1039
1047
|
nil
|
|
1040
1048
|
end
|
|
1041
1049
|
|
|
1050
|
+
# The session's own thinking level, nil when it has none.
|
|
1051
|
+
# @return [Symbol, nil]
|
|
1052
|
+
def thinking_override = @session&.thinking
|
|
1053
|
+
|
|
1054
|
+
# What a thinking level change costs the effective model's prompt
|
|
1055
|
+
# cache (Thinking.cache_cost): :full, :provider or :tail.
|
|
1056
|
+
# @return [Symbol]
|
|
1057
|
+
def thinking_cache_cost
|
|
1058
|
+
target = @host_registry.resolve(@effective_model_name)
|
|
1059
|
+
Thinking.cache_cost(target, profile: target.entry&.chat? ? nil : profile&.name)
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
# Set the session's own thinking level (nil unsets it); the next turn's
|
|
1063
|
+
# start resolves it, never a running one (its TurnSettings are fixed).
|
|
1064
|
+
# The caller saves the session.
|
|
1065
|
+
# @raise [ArgumentError] with no session yet
|
|
1066
|
+
def thinking_override=(level)
|
|
1067
|
+
raise ArgumentError, "no session yet" unless @session
|
|
1068
|
+
|
|
1069
|
+
@session.thinking = level
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1042
1072
|
# The model the server serves for the current model, and the name asked
|
|
1043
1073
|
# for: what the last generation of that name reported, else llama.cpp's
|
|
1044
1074
|
# model_alias (/props, one short cached probe; not with probe: false),
|
|
@@ -1105,6 +1135,7 @@ module Samagotchi
|
|
|
1105
1135
|
served_expected_by: served_expected_by(served_pair[0]),
|
|
1106
1136
|
context_status: @last_context_status&.dup || saved_context_status(metrics[:context]),
|
|
1107
1137
|
llm_context: llm_context_summary,
|
|
1138
|
+
thinking: thinking_explained&.summary,
|
|
1108
1139
|
recap_enabled: !@recap.nil?,
|
|
1109
1140
|
recap_min_user_turns: @recap&.min_user_turns,
|
|
1110
1141
|
recap_inactivity_seconds: @recap&.inactivity&.to_i
|
|
@@ -1242,22 +1273,22 @@ module Samagotchi
|
|
|
1242
1273
|
elsif client_id == ClientId::REMINDER then "reminder"
|
|
1243
1274
|
else "client"
|
|
1244
1275
|
end
|
|
1245
|
-
session.last_turn =
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
session.last_turn.merge!(stop_facts(event))
|
|
1276
|
+
session.last_turn = LastTurn.new(outcome: outcome, ended_at: Time.now.iso8601(3), seconds: seconds.round(1),
|
|
1277
|
+
origin: source, exhausted: limit ? true : nil, limit: limit,
|
|
1278
|
+
**stop_facts(event))
|
|
1249
1279
|
end
|
|
1250
1280
|
|
|
1251
|
-
# Why a turn stopped, from its end event, for a
|
|
1252
|
-
# (ReplyWait): a provider error's kind and whether
|
|
1253
|
-
# cancel's reason and the hook that stopped it.
|
|
1281
|
+
# Why a turn stopped, from its end event (LastTurn::STOP_FACTS), for a
|
|
1282
|
+
# parent agent's wait (ReplyWait): a provider error's kind and whether
|
|
1283
|
+
# a retry may help, a cancel's reason and the hook that stopped it.
|
|
1284
|
+
# Only what is known.
|
|
1254
1285
|
def stop_facts(event)
|
|
1255
1286
|
case event&.dig(:type)
|
|
1256
1287
|
# kept_steps: the turn's work stayed (a parent must not send the task again).
|
|
1257
1288
|
when :turn_failed
|
|
1258
|
-
{
|
|
1289
|
+
{ error_kind: event[:error_kind]&.to_s, retryable: event[:retryable], kept_steps: event[:kept_steps] }
|
|
1259
1290
|
when :turn_canceled
|
|
1260
|
-
{
|
|
1291
|
+
{ cancel_reason: event[:cancellation_reason]&.to_s, stopped_by: event[:cancelled_by]&.to_s }
|
|
1261
1292
|
else {}
|
|
1262
1293
|
end
|
|
1263
1294
|
end
|
|
@@ -2237,9 +2268,9 @@ module Samagotchi
|
|
|
2237
2268
|
hook_notify(text, level, "thinking")
|
|
2238
2269
|
end
|
|
2239
2270
|
|
|
2240
|
-
# +target+'s thinking level (Thinking.resolve).
|
|
2271
|
+
# +target+'s thinking level (Thinking.resolve), the session's own first.
|
|
2241
2272
|
def thinking_level(target)
|
|
2242
|
-
Thinking.resolve(target, names: model_lookup_names(target)).first
|
|
2273
|
+
Thinking.resolve(target, names: model_lookup_names(target), session: thinking_override).first
|
|
2243
2274
|
rescue StandardError
|
|
2244
2275
|
Thinking::DEFAULT
|
|
2245
2276
|
end
|
|
@@ -2470,7 +2501,7 @@ module Samagotchi
|
|
|
2470
2501
|
model_key: -> { @model_key },
|
|
2471
2502
|
state_dir: -> { session_state_dir },
|
|
2472
2503
|
scratch: -> { @scratch },
|
|
2473
|
-
|
|
2504
|
+
setup: -> { SessionSetup.of(@session) },
|
|
2474
2505
|
frontend: -> { interface }
|
|
2475
2506
|
)
|
|
2476
2507
|
end
|