samagotchi 0.70.0 → 0.72.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 +83 -1
- data/docs/architecture.md +16 -10
- data/docs/cli.md +7 -4
- data/docs/configuration.md +24 -14
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/background-tasks.md +2 -2
- data/docs/internals/engine.md +38 -13
- data/docs/internals/llm-context-live.md +1 -1
- data/docs/internals/prompt-caching.md +6 -0
- data/docs/plugins.md +18 -7
- data/docs/sessions.md +4 -2
- data/docs/testing.md +12 -3
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
- data/lib/samagotchi/bridge.rb +5 -2
- data/lib/samagotchi/broadcast/scope_card.rb +2 -2
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -7
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +2 -2
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +2 -2
- data/lib/samagotchi/bundles/coordinator/manifest.yml +8 -6
- data/lib/samagotchi/bundles/coordinator/plugin.rb +10 -0
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/config.rb +7 -25
- data/lib/samagotchi/context_note.rb +2 -6
- data/lib/samagotchi/effective_model.rb +1 -1
- data/lib/samagotchi/engine.rb +91 -213
- data/lib/samagotchi/engine_peers.rb +2 -0
- data/lib/samagotchi/event_routes.rb +83 -0
- data/lib/samagotchi/hook_routes.rb +16 -37
- data/lib/samagotchi/host_config.rb +1 -1
- data/lib/samagotchi/host_keys.rb +0 -2
- data/lib/samagotchi/paths.rb +13 -0
- data/lib/samagotchi/plugin/api.rb +11 -5
- data/lib/samagotchi/plugin/peer.rb +5 -8
- data/lib/samagotchi/prompt_files.rb +2 -4
- data/lib/samagotchi/session.rb +2 -1
- data/lib/samagotchi/session_manager.rb +6 -16
- data/lib/samagotchi/session_model_settings.rb +227 -0
- data/lib/samagotchi/session_setup.rb +2 -1
- data/lib/samagotchi/sessions_command.rb +12 -3
- data/lib/samagotchi/system_prompt.rb +18 -13
- data/lib/samagotchi/terminal_ui/attached_loop.rb +36 -15
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
- data/lib/samagotchi/tool_activity.rb +20 -1
- data/lib/samagotchi/tool_declarations.rb +4 -0
- data/lib/samagotchi/tool_output_cut.rb +11 -2
- data/lib/samagotchi/toolkit.rb +60 -22
- data/lib/samagotchi/toolkit_binding.rb +7 -5
- data/lib/samagotchi/toolkit_command.rb +28 -5
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +32 -6
- data/lib/samagotchi/tools/list_sessions.rb +2 -2
- data/lib/samagotchi/tools/peers.rb +5 -2
- data/lib/samagotchi/tools/register_reminder.rb +1 -0
- data/lib/samagotchi/tools/registry.rb +13 -2
- data/lib/samagotchi/tools/task_wait.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +15 -4
- data/lib/samagotchi/turn_note.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +18 -17
- data/lib/samagotchi/web/message_parts.rb +1 -1
- data/lib/samagotchi/web/public/answer_refs.js +1 -0
- data/lib/samagotchi/web/public/app.js +20 -18
- data/lib/samagotchi/web/public/bubbles.js +1 -1
- data/lib/samagotchi/web/public/cards_place.js +6 -10
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/chip_popover.js +3 -1
- data/lib/samagotchi/web/public/composer.js +13 -6
- data/lib/samagotchi/web/public/context_chips.js +25 -12
- data/lib/samagotchi/web/public/data.js +5 -1
- data/lib/samagotchi/web/public/format.js +5 -0
- data/lib/samagotchi/web/public/index.html +1 -0
- data/lib/samagotchi/web/public/sessions_list.js +2 -3
- data/lib/samagotchi/web/public/stage_frame.js +1 -1
- data/lib/samagotchi/web/public/stage_view.js +8 -2
- data/lib/samagotchi/web/public/stream_handlers.js +6 -3
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/toolkit_chip.js +3 -1
- data/lib/samagotchi/web/public/turn_html.js +55 -3
- data/lib/samagotchi/web/session_hub.rb +8 -0
- metadata +3 -1
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "turn_note"
|
|
|
16
16
|
require_relative "model_profile"
|
|
17
17
|
require_relative "effective_model"
|
|
18
18
|
require_relative "session_setting"
|
|
19
|
+
require_relative "session_model_settings"
|
|
19
20
|
require_relative "thinking_notices"
|
|
20
21
|
require_relative "cancellation_controller"
|
|
21
22
|
require_relative "context_window"
|
|
@@ -49,6 +50,7 @@ require_relative "recap_setup"
|
|
|
49
50
|
require_relative "idle_reminders"
|
|
50
51
|
require_relative "idle_scheduler"
|
|
51
52
|
require_relative "hooks"
|
|
53
|
+
require_relative "event_routes"
|
|
52
54
|
require_relative "hook_routes"
|
|
53
55
|
require_relative "guardrails"
|
|
54
56
|
require_relative "reminder_store"
|
|
@@ -141,6 +143,10 @@ module Samagotchi
|
|
|
141
143
|
@profile_mutex = Mutex.new
|
|
142
144
|
typed = model_typed.to_s.strip
|
|
143
145
|
@model_choice = ModelChoice.new(name: @default_model_name, typed: typed.empty? ? @default_model_name : typed)
|
|
146
|
+
# The session's settings on its effective model (SessionModelSettings),
|
|
147
|
+
# read at use: /model moves the choice, #session= the session.
|
|
148
|
+
@model_settings = SessionModelSettings.new(registry: @host_registry, choice: -> { @model_choice },
|
|
149
|
+
session: -> { @session })
|
|
144
150
|
# An injected client (specs) stands in for every host's client.
|
|
145
151
|
@host_registry.client_override = client if client
|
|
146
152
|
@client = effective_model.client
|
|
@@ -159,7 +165,9 @@ module Samagotchi
|
|
|
159
165
|
end
|
|
160
166
|
# What hooks and plugins do beyond reading their event (a notice, a
|
|
161
167
|
# question, a stop, a steer), routed to the UIs. Built before anything
|
|
162
|
-
# that may notify.
|
|
168
|
+
# that may notify. Where a card or a notice goes (EventRoutes) first:
|
|
169
|
+
# the hook routes use it.
|
|
170
|
+
@event_routes = build_event_routes
|
|
163
171
|
@hook_routes = build_hook_routes
|
|
164
172
|
# Load hooks from config (plugins) and create the registry; what fails
|
|
165
173
|
# to load is announced, and a required guardrail's failure denies
|
|
@@ -772,9 +780,10 @@ module Samagotchi
|
|
|
772
780
|
|
|
773
781
|
# Show a card in every UI (docs/plugins.md, Cards): {type: :card, id:,
|
|
774
782
|
# source:, title:, body:, level:, actions: [{label:, command:}], in_turn:}.
|
|
775
|
-
#
|
|
776
|
-
#
|
|
777
|
-
# (
|
|
783
|
+
# Where it goes (held while the plugins load, an anytime command's, an
|
|
784
|
+
# init task's, a turn event, else announced) is EventRoutes#route's,
|
|
785
|
+
# marked in_turn: (CARD_MARKS). A card with the id of an earlier one
|
|
786
|
+
# replaces it (btw's "thinking…" → the answer).
|
|
778
787
|
# @param source [String] who shows it (a bundle's name)
|
|
779
788
|
# @param actions [Array<Hash>] {label:, command:}; a command is a line
|
|
780
789
|
# the session runs, as typed (D3)
|
|
@@ -782,18 +791,7 @@ module Samagotchi
|
|
|
782
791
|
# @raise [ArgumentError] a card without a title, or a bad action
|
|
783
792
|
def show_card(source:, title:, body: "", actions: [], level: :info, id: nil)
|
|
784
793
|
card = build_card(source: source, title: title, body: body, actions: actions, level: level, id: id)
|
|
785
|
-
|
|
786
|
-
return announce_anytime(card.merge(in_turn: false))[:id] if anytime_thread?
|
|
787
|
-
# A plugin's init task (chi.init): never a running turn's event.
|
|
788
|
-
return announce(card.merge(in_turn: false)) && card[:id] if current_init_task
|
|
789
|
-
|
|
790
|
-
in_turn, sink = @turn_state.in_turn_sink
|
|
791
|
-
card[:in_turn] = in_turn ? true : false
|
|
792
|
-
if in_turn
|
|
793
|
-
emit_event(sink, card)
|
|
794
|
-
else
|
|
795
|
-
announce_or_hold(card)
|
|
796
|
-
end
|
|
794
|
+
@event_routes.route(card, **EventRoutes::CARD_MARKS)
|
|
797
795
|
card[:id]
|
|
798
796
|
end
|
|
799
797
|
|
|
@@ -1065,7 +1063,8 @@ module Samagotchi
|
|
|
1065
1063
|
# profile come from the effective model's server, so this may make one
|
|
1066
1064
|
# short /props GET; the cheap #session_state_snapshot never does.
|
|
1067
1065
|
# price_host: the effective model's host, whose hosts.<name>.models
|
|
1068
|
-
# prices the cost estimate names.
|
|
1066
|
+
# prices the cost estimate names. kit_prompt: the next turn's kit
|
|
1067
|
+
# prompt text ({kit:, tokens:}), only when its toolkit has one.
|
|
1069
1068
|
# @return [Hash] @metrics.snapshot, filled in
|
|
1070
1069
|
def stats_snapshot
|
|
1071
1070
|
snapshot = @metrics.snapshot
|
|
@@ -1078,6 +1077,8 @@ module Samagotchi
|
|
|
1078
1077
|
**setting_chips(SessionSetting::STATS, model),
|
|
1079
1078
|
prompt_notes: prompt_notes.map(&:to_h), prompt_files: @prompt_builder.prompt_files,
|
|
1080
1079
|
price_host: model.host_name)
|
|
1080
|
+
kit_prompt = kit_prompt_stats
|
|
1081
|
+
snapshot = snapshot.merge(kit_prompt: kit_prompt) if kit_prompt
|
|
1081
1082
|
unless snapshot.dig(:context, :window_tokens)
|
|
1082
1083
|
window = current_context_window(model)
|
|
1083
1084
|
if window
|
|
@@ -1095,36 +1096,45 @@ module Samagotchi
|
|
|
1095
1096
|
snapshot
|
|
1096
1097
|
end
|
|
1097
1098
|
|
|
1098
|
-
# The
|
|
1099
|
-
|
|
1100
|
-
#
|
|
1101
|
-
#
|
|
1102
|
-
|
|
1103
|
-
|
|
1099
|
+
# ── The session's model settings (SessionModelSettings) ──────────────────
|
|
1100
|
+
|
|
1101
|
+
# The session's own thinking, reasoning_history and llm_context values
|
|
1102
|
+
# over the effective model's, the summaries /model and /stats show, and
|
|
1103
|
+
# the saved context's status: SessionModelSettings' (resolved on every
|
|
1104
|
+
# read); these forward.
|
|
1105
|
+
def saved_context_status(context, model: :current) = @model_settings.saved_context_status(context, model: model)
|
|
1106
|
+
def llm_context_explained = @model_settings.llm_context_explained
|
|
1107
|
+
def llm_context_summary = @model_settings.llm_context_summary
|
|
1108
|
+
def llm_context_override = @model_settings.llm_context_override
|
|
1109
|
+
def chat_model? = @model_settings.chat_model?
|
|
1110
|
+
def sampling_summary = @model_settings.sampling_summary
|
|
1111
|
+
def thinking_summary = @model_settings.thinking_summary
|
|
1112
|
+
def steer_cut_after_explained = @model_settings.steer_cut_after_explained
|
|
1113
|
+
def steer_cut_after = @model_settings.steer_cut_after
|
|
1114
|
+
def steer_cut_summary = @model_settings.steer_cut_summary
|
|
1115
|
+
def thinking_explained(own: thinking_override) = @model_settings.thinking_explained(own: own)
|
|
1116
|
+
def reasoning_history_summary = @model_settings.reasoning_history_summary
|
|
1117
|
+
def reasoning_history_cache_cost(from, to) = @model_settings.reasoning_history_cache_cost(from, to)
|
|
1118
|
+
def reasoning_history_override = @model_settings.reasoning_history_override
|
|
1119
|
+
def thinking_override = @model_settings.thinking_override
|
|
1120
|
+
def served_expected_by(served) = @model_settings.served_expected_by(served)
|
|
1121
|
+
def served_expected?(served) = @model_settings.served_expected?(served)
|
|
1104
1122
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
.display_for(used_tokens: context[:used_tokens], window_tokens: context[:window_tokens])
|
|
1123
|
+
def reasoning_history_explained(native: false, own: reasoning_history_override)
|
|
1124
|
+
@model_settings.reasoning_history_explained(native: native, own: own)
|
|
1108
1125
|
end
|
|
1109
1126
|
|
|
1110
|
-
#
|
|
1111
|
-
#
|
|
1112
|
-
# @
|
|
1113
|
-
def
|
|
1114
|
-
|
|
1127
|
+
# Set the session's own reasoning_history or thinking level (nil unsets
|
|
1128
|
+
# it): SessionModelSettings'. The caller saves the session.
|
|
1129
|
+
# @raise [ArgumentError] with no session yet
|
|
1130
|
+
def reasoning_history_override=(level)
|
|
1131
|
+
@model_settings.reasoning_history_override = level
|
|
1115
1132
|
end
|
|
1116
1133
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
# @return [Hash, nil]
|
|
1120
|
-
def llm_context_summary
|
|
1121
|
-
llm_context_explained&.summary(llm_context_override)
|
|
1134
|
+
def thinking_override=(level)
|
|
1135
|
+
@model_settings.thinking_override = level
|
|
1122
1136
|
end
|
|
1123
1137
|
|
|
1124
|
-
# The session's own llm_context values, nil when it has none.
|
|
1125
|
-
# @return [LLMContextOverride, nil]
|
|
1126
|
-
def llm_context_override = SessionSetting::LLM_CONTEXT.own(@session)
|
|
1127
|
-
|
|
1128
1138
|
# Set the session's own llm_context values (an empty override unsets
|
|
1129
1139
|
# them); the next turn's start resolves them, never a running one (its
|
|
1130
1140
|
# TurnSettings are fixed). The caller saves the session.
|
|
@@ -1137,102 +1147,11 @@ module Samagotchi
|
|
|
1137
1147
|
@last_context_status = saved_context_status(@metrics.snapshot[:context]) || @last_context_status if @last_context_status
|
|
1138
1148
|
end
|
|
1139
1149
|
|
|
1140
|
-
# The effective model is on a chat host (api: openai or anthropic), whose loop uses
|
|
1141
|
-
# no prompt profile.
|
|
1142
|
-
def chat_model? = effective_model.chat?
|
|
1143
|
-
|
|
1144
|
-
# "temperature=0.6 (hosts.work)" for /model: the effective model's
|
|
1145
|
-
# configured sampling, nil when none.
|
|
1146
|
-
def sampling_summary = model_setting(nil, &:sampling_summary)
|
|
1147
|
-
|
|
1148
|
-
# "off (models: qwen)", "low (session)" for /model: the effective
|
|
1149
|
-
# model's thinking level and where it came from, nil when none is set.
|
|
1150
|
-
def thinking_summary
|
|
1151
|
-
explained = thinking_explained
|
|
1152
|
-
explained&.source ? explained.label : nil
|
|
1153
|
-
end
|
|
1154
|
-
|
|
1155
|
-
# The seconds a cut waits for the effective model to stream only
|
|
1156
|
-
# thinking (SteerCutAfter.resolve), resolved on every call, as
|
|
1157
|
-
# turn_sampling; on any error, the global and its name.
|
|
1158
|
-
# @return [Array(Integer, String|nil)]
|
|
1159
|
-
def steer_cut_after_explained
|
|
1160
|
-
model_setting([SteerCut.cut_after, "steer.cut_after"], &:steer_cut_after_explained)
|
|
1161
|
-
end
|
|
1162
|
-
|
|
1163
|
-
# #steer_cut_after_explained's seconds: what SteerCut#cut_for_steer
|
|
1164
|
-
# reads, per host and model.
|
|
1165
|
-
def steer_cut_after = steer_cut_after_explained.first
|
|
1166
|
-
|
|
1167
|
-
# "after 60 s (models: qwen)", "never (hosts.splash)" for /model: the
|
|
1168
|
-
# effective model's cut wait and where it came from, nil for the
|
|
1169
|
-
# default.
|
|
1170
|
-
def steer_cut_summary
|
|
1171
|
-
seconds, source = steer_cut_after_explained
|
|
1172
|
-
source ? SteerCutAfter.label(seconds, source) : nil
|
|
1173
|
-
end
|
|
1174
|
-
|
|
1175
|
-
# The effective model's thinking level with where it came from and the
|
|
1176
|
-
# session's own (SettingExplained), as the next turn resolves it;
|
|
1177
|
-
# nil when it can't be resolved.
|
|
1178
|
-
# @return [SettingExplained, nil]
|
|
1179
|
-
# +own+: the session's own level to resolve with (the current one by
|
|
1180
|
-
# default; /thinking asks what a change would resolve to).
|
|
1181
|
-
def thinking_explained(own: thinking_override)
|
|
1182
|
-
model_setting(nil) { |model| model.thinking_explained(own: own) }
|
|
1183
|
-
end
|
|
1184
|
-
|
|
1185
|
-
# "drop (models: qwen)" for /model: the effective chat model's
|
|
1186
|
-
# reasoning_history level and where it came from; nil when it is the
|
|
1187
|
-
# default, the model runs on the native loop (which ignores it) or it
|
|
1188
|
-
# can't be resolved.
|
|
1189
|
-
def reasoning_history_summary
|
|
1190
|
-
model_setting(nil) { |model| model.reasoning_history_summary(own: reasoning_history_override) }
|
|
1191
|
-
end
|
|
1192
|
-
|
|
1193
|
-
# The effective chat model's reasoning_history level with where it came
|
|
1194
|
-
# from and the session's own (SettingExplained), as the next turn
|
|
1195
|
-
# resolves it; nil on a native host (the native loop ignores it; native:
|
|
1196
|
-
# true resolves it there too, for /reasoning-history) or when it can't
|
|
1197
|
-
# be resolved.
|
|
1198
|
-
# @return [SettingExplained, nil]
|
|
1199
|
-
# +own+: the session's own level to resolve with (the current one by
|
|
1200
|
-
# default; a command asks what a change would resolve to).
|
|
1201
|
-
def reasoning_history_explained(native: false, own: reasoning_history_override)
|
|
1202
|
-
model_setting(nil) { |model| model.reasoning_history_explained(own: own, native: native) }
|
|
1203
|
-
end
|
|
1204
|
-
|
|
1205
|
-
# What a reasoning_history change from level +from+ to level +to+ costs
|
|
1206
|
-
# the effective model's prompt cache, given the session's conversation
|
|
1207
|
-
# (ReasoningHistory.cache_cost): :history or nil. A host ChatLoop
|
|
1208
|
-
# marked as refusing it may still get :history (one needless ask).
|
|
1209
|
-
# @return [Symbol, nil]
|
|
1210
|
-
def reasoning_history_cache_cost(from, to)
|
|
1211
|
-
model_setting(nil) { |model| model.reasoning_history_cache_cost(from, to, conversation: @session&.messages) }
|
|
1212
|
-
end
|
|
1213
|
-
|
|
1214
1150
|
# Whether the session has a model reply (a cache to restart at all).
|
|
1215
1151
|
def history?
|
|
1216
1152
|
Array(@session&.messages).any? { |entry| entry[:role].to_s == "model" }
|
|
1217
1153
|
end
|
|
1218
1154
|
|
|
1219
|
-
# The session's own reasoning_history level, nil when it has none.
|
|
1220
|
-
# @return [Symbol, nil]
|
|
1221
|
-
def reasoning_history_override = SessionSetting::REASONING_HISTORY.own(@session)
|
|
1222
|
-
|
|
1223
|
-
# Set the session's own reasoning_history level (nil unsets it); the
|
|
1224
|
-
# next turn's start resolves it. The caller saves the session.
|
|
1225
|
-
# @raise [ArgumentError] with no session yet
|
|
1226
|
-
def reasoning_history_override=(level)
|
|
1227
|
-
raise ArgumentError, "no session yet" unless @session
|
|
1228
|
-
|
|
1229
|
-
SessionSetting::REASONING_HISTORY.set(@session, level)
|
|
1230
|
-
end
|
|
1231
|
-
|
|
1232
|
-
# The session's own thinking level, nil when it has none.
|
|
1233
|
-
# @return [Symbol, nil]
|
|
1234
|
-
def thinking_override = SessionSetting::THINKING.own(@session)
|
|
1235
|
-
|
|
1236
1155
|
# What a thinking level change costs the effective model's prompt
|
|
1237
1156
|
# cache (Thinking.cache_cost): :full, :provider or :tail.
|
|
1238
1157
|
# @return [Symbol]
|
|
@@ -1240,16 +1159,6 @@ module Samagotchi
|
|
|
1240
1159
|
effective_model.thinking_cache_cost { profile }
|
|
1241
1160
|
end
|
|
1242
1161
|
|
|
1243
|
-
# Set the session's own thinking level (nil unsets it); the next turn's
|
|
1244
|
-
# start resolves it, never a running one (its TurnSettings are fixed).
|
|
1245
|
-
# The caller saves the session.
|
|
1246
|
-
# @raise [ArgumentError] with no session yet
|
|
1247
|
-
def thinking_override=(level)
|
|
1248
|
-
raise ArgumentError, "no session yet" unless @session
|
|
1249
|
-
|
|
1250
|
-
SessionSetting::THINKING.set(@session, level)
|
|
1251
|
-
end
|
|
1252
|
-
|
|
1253
1162
|
# The model the server serves for the current model, and the name asked
|
|
1254
1163
|
# for: what the last generation of that name reported, else llama.cpp's
|
|
1255
1164
|
# model_alias (/props, one short cached probe; not with probe: false),
|
|
@@ -1259,16 +1168,6 @@ module Samagotchi
|
|
|
1259
1168
|
served_model_for(@metrics.served_report, model: probe ? effective_model : nil)
|
|
1260
1169
|
end
|
|
1261
1170
|
|
|
1262
|
-
# The host whose hosts.<name>.models.<id>.served names +served+ for the
|
|
1263
|
-
# current model (a gateway's round-robin targets): not a mismatch to
|
|
1264
|
-
# warn about. nil when none does.
|
|
1265
|
-
# @return [String, nil]
|
|
1266
|
-
def served_expected_by(served)
|
|
1267
|
-
model_setting(nil) { |model| model.served_expected_by(served) }
|
|
1268
|
-
end
|
|
1269
|
-
|
|
1270
|
-
def served_expected?(served) = !served_expected_by(served).nil?
|
|
1271
|
-
|
|
1272
1171
|
# Read-only snapshot of the engine's view of the current session plus the
|
|
1273
1172
|
# live event sequence. Cheap primitive used by the bridge's reconnect-too-
|
|
1274
1173
|
# old reset marker and the GET /session/:id/state read surface. Orthogonal
|
|
@@ -1299,7 +1198,7 @@ module Samagotchi
|
|
|
1299
1198
|
# One resolve for the model's fields; nil when it doesn't resolve (the
|
|
1300
1199
|
# Bridge's /state and reset frames rely on this never raising): those
|
|
1301
1200
|
# fields are nil then, as their own rescues always gave.
|
|
1302
|
-
model = model_setting(nil) { |resolved| resolved }
|
|
1201
|
+
model = @model_settings.model_setting(nil) { |resolved| resolved }
|
|
1303
1202
|
{
|
|
1304
1203
|
status: @session&.status,
|
|
1305
1204
|
message_count: (@session&.messages || []).size,
|
|
@@ -1587,17 +1486,26 @@ module Samagotchi
|
|
|
1587
1486
|
def hook_stop_generation(reason, hook) = @hook_routes.stop_generation(reason, hook)
|
|
1588
1487
|
private :hook_stop_generation
|
|
1589
1488
|
|
|
1590
|
-
#
|
|
1591
|
-
|
|
1592
|
-
|
|
1489
|
+
# Where cards and notices go (see #initialize for when). Each lookup
|
|
1490
|
+
# calls the Engine method the routes used before EventRoutes, at use.
|
|
1491
|
+
def build_event_routes
|
|
1492
|
+
EventRoutes.new(
|
|
1593
1493
|
extension_load: -> { @extension_load },
|
|
1594
1494
|
in_turn_sink: -> { @turn_state.in_turn_sink },
|
|
1595
1495
|
anytime_thread: -> { anytime_thread? },
|
|
1596
|
-
announce_anytime: ->(event) { announce_anytime(event) },
|
|
1597
1496
|
init_task: -> { current_init_task },
|
|
1598
1497
|
announce: ->(event) { announce(event) },
|
|
1498
|
+
announce_anytime: ->(event) { announce_anytime(event) },
|
|
1599
1499
|
announce_or_hold: ->(event) { announce_or_hold(event) },
|
|
1600
|
-
emit: ->(sink, event) { emit_event(sink, event) }
|
|
1500
|
+
emit: ->(sink, event) { emit_event(sink, event) }
|
|
1501
|
+
)
|
|
1502
|
+
end
|
|
1503
|
+
private :build_event_routes
|
|
1504
|
+
|
|
1505
|
+
# The hook routes (see #initialize for when).
|
|
1506
|
+
def build_hook_routes
|
|
1507
|
+
HookRoutes.new(
|
|
1508
|
+
event_routes: @event_routes,
|
|
1601
1509
|
question_desk: -> { @question_desk },
|
|
1602
1510
|
cancel_controller: -> { active_cancel_controller },
|
|
1603
1511
|
steer: ->(text, source) { steer(text, source: source) }
|
|
@@ -1682,8 +1590,8 @@ module Samagotchi
|
|
|
1682
1590
|
def system_prompt(model = nil)
|
|
1683
1591
|
model ||= effective_model
|
|
1684
1592
|
chat = model.chat?
|
|
1685
|
-
level = chat ? nil : thinking_level(model)
|
|
1686
|
-
prompt = @prompt_builder.build(chat: chat, thinking: level, layers: model.llm_context_layers(llm_context_session_args))
|
|
1593
|
+
level = chat ? nil : @model_settings.thinking_level(model)
|
|
1594
|
+
prompt = @prompt_builder.build(chat: chat, thinking: level, layers: model.llm_context_layers(@model_settings.llm_context_session_args))
|
|
1687
1595
|
# The memory indexes this prompt holds: /stats, the snapshot, analytics.json.
|
|
1688
1596
|
@metrics.memory_index = @prompt_builder.memory_index
|
|
1689
1597
|
# The model notes it carries: the session records them (/stats, /model, the web).
|
|
@@ -2027,10 +1935,10 @@ module Samagotchi
|
|
|
2027
1935
|
model = effective_model
|
|
2028
1936
|
vision = turn_vision(session, model)
|
|
2029
1937
|
sampling = model.sampling
|
|
2030
|
-
level = thinking_level(model)
|
|
1938
|
+
level = @model_settings.thinking_level(model)
|
|
2031
1939
|
@thinking_notices.turn_started(level, model.target)
|
|
2032
1940
|
LLM::TurnSettings.new(vision: vision, sampling: sampling, thinking: level,
|
|
2033
|
-
reasoning_history: reasoning_history_level(model), model_name: nil,
|
|
1941
|
+
reasoning_history: @model_settings.reasoning_history_level(model), model_name: nil,
|
|
2034
1942
|
window_setting: nil, llm_context: nil, price: nil)
|
|
2035
1943
|
end
|
|
2036
1944
|
private :turn_settings
|
|
@@ -2060,7 +1968,7 @@ module Samagotchi
|
|
|
2060
1968
|
messages = ContextNote.with_system_head(turn.session.messages.dup, { role: "system", content: system_prompt(model) })
|
|
2061
1969
|
# Under the next turn's strategy (a /model switch may have changed it);
|
|
2062
1970
|
# the last turn's when it can't be resolved.
|
|
2063
|
-
prompt, images = @kernel.warmup_prompt(messages, llm_context: turn_llm_context || @kernel.turn_settings&.llm_context)
|
|
1971
|
+
prompt, images = @kernel.warmup_prompt(messages, llm_context: @model_settings.turn_llm_context || @kernel.turn_settings&.llm_context)
|
|
2064
1972
|
return unless prompt
|
|
2065
1973
|
|
|
2066
1974
|
started = @prompt_warmup.start(client: client, prompt: prompt, model: @kernel.turn_settings.model_name,
|
|
@@ -2126,7 +2034,7 @@ module Samagotchi
|
|
|
2126
2034
|
# The turn's settings, set once here. The chat loop dispatches tools
|
|
2127
2035
|
# through the kernel without its #run: tag those dumps with this
|
|
2128
2036
|
# turn's model, not the last native one.
|
|
2129
|
-
explained = model.llm_context_explained(llm_context_session_args)
|
|
2037
|
+
explained = model.llm_context_explained(@model_settings.llm_context_session_args)
|
|
2130
2038
|
@kernel.turn_settings = turn.settings.with(model_name: bare_for_backend, window_setting: model.window_setting,
|
|
2131
2039
|
llm_context: explained&.resolved, price: model.price)
|
|
2132
2040
|
log_llm_context(explained)
|
|
@@ -2355,19 +2263,10 @@ module Samagotchi
|
|
|
2355
2263
|
capability: -> { model.vision_support(profile) })
|
|
2356
2264
|
end
|
|
2357
2265
|
|
|
2358
|
-
# The effective model's
|
|
2359
|
-
#
|
|
2360
|
-
def turn_sampling =
|
|
2361
|
-
|
|
2362
|
-
# The effective model's thinking level (Thinking.resolve), read each turn.
|
|
2363
|
-
def turn_thinking = model_setting(Thinking::DEFAULT) { |model| thinking_level(model) }
|
|
2364
|
-
|
|
2365
|
-
# +model+'s ReasoningHistory level, the session's own first; the
|
|
2366
|
-
# default when it can't be resolved.
|
|
2367
|
-
def reasoning_history_level(model) = model.reasoning_history(own: reasoning_history_override)
|
|
2368
|
-
|
|
2369
|
-
# +model+'s thinking level (Thinking.resolve), the session's own first.
|
|
2370
|
-
def thinking_level(model) = model.thinking(own: thinking_override)
|
|
2266
|
+
# The effective model's sampling and thinking level as the next turn
|
|
2267
|
+
# reads them (SessionModelSettings).
|
|
2268
|
+
def turn_sampling = @model_settings.turn_sampling
|
|
2269
|
+
def turn_thinking = @model_settings.turn_thinking
|
|
2371
2270
|
|
|
2372
2271
|
# A model known not to see images fails a turn with images up front.
|
|
2373
2272
|
def refuse_images!(vision)
|
|
@@ -2778,24 +2677,10 @@ module Samagotchi
|
|
|
2778
2677
|
emit_event(on_event, { type: :used_memories_updated, used_memory_names: used_memory_names, read_names: read_names })
|
|
2779
2678
|
end
|
|
2780
2679
|
|
|
2781
|
-
# The effective model's
|
|
2782
|
-
# (
|
|
2783
|
-
def turn_window_setting =
|
|
2784
|
-
|
|
2785
|
-
# The effective model's price on its host (hosts.<name>.models.<id>.price),
|
|
2786
|
-
# by the requested id, never the served one; read per turn as the window is.
|
|
2787
|
-
def turn_price = model_setting(nil, &:price)
|
|
2788
|
-
|
|
2789
|
-
# The LLM context strategy the next turn runs under: the session's own
|
|
2790
|
-
# (Session#llm_context), else the effective model's (models:/hosts:
|
|
2791
|
-
# llm_context_strategy), else llm_context.strategy; read per turn as
|
|
2792
|
-
# the window is.
|
|
2793
|
-
def turn_llm_context
|
|
2794
|
-
llm_context_explained&.resolved
|
|
2795
|
-
end
|
|
2796
|
-
|
|
2797
|
-
def llm_context_session_args = @session&.llm_context&.resolve_args || {}
|
|
2798
|
-
private :llm_context_session_args
|
|
2680
|
+
# The effective model's window_tokens and price, read per turn
|
|
2681
|
+
# (SessionModelSettings).
|
|
2682
|
+
def turn_window_setting = @model_settings.turn_window_setting
|
|
2683
|
+
def turn_price = @model_settings.turn_price
|
|
2799
2684
|
|
|
2800
2685
|
# See #served_model. A report for another name (before a /model switch)
|
|
2801
2686
|
# doesn't count. Without a model (EffectiveModel), no probe. A remote
|
|
@@ -2813,6 +2698,15 @@ module Samagotchi
|
|
|
2813
2698
|
[nil, nil]
|
|
2814
2699
|
end
|
|
2815
2700
|
|
|
2701
|
+
# The next turn's kit prompt text for /stats: its kit and its estimated
|
|
2702
|
+
# tokens (chars / 4), nil when its toolkit has none.
|
|
2703
|
+
def kit_prompt_stats
|
|
2704
|
+
resolved = toolkit_explained.resolved
|
|
2705
|
+
return nil unless resolved.prompt
|
|
2706
|
+
|
|
2707
|
+
{ kit: resolved.name, tokens: (resolved.prompt.length / TokenUsage::CHARS_PER_TOKEN).ceil }
|
|
2708
|
+
end
|
|
2709
|
+
|
|
2816
2710
|
# The window as +model+'s loop would see it (EffectiveModel#context_window).
|
|
2817
2711
|
def current_context_window(model) = model.context_window
|
|
2818
2712
|
|
|
@@ -2822,26 +2716,10 @@ module Samagotchi
|
|
|
2822
2716
|
settings.to_h { |setting| [setting.key, setting.model? ? setting.chip_for(model, @session) : toolkit_summary] }
|
|
2823
2717
|
end
|
|
2824
2718
|
|
|
2825
|
-
# The effective model now
|
|
2826
|
-
#
|
|
2827
|
-
# lists all move a resolve). Raises as HostRegistry#resolve does.
|
|
2719
|
+
# The effective model now, resolved on each call, never kept
|
|
2720
|
+
# (SessionModelSettings#effective_model). Raises as HostRegistry#resolve does.
|
|
2828
2721
|
# @return [EffectiveModel]
|
|
2829
|
-
def effective_model(choice = @model_choice) =
|
|
2830
|
-
|
|
2831
|
-
# The block's answer for the effective model, or +fallback+ when the
|
|
2832
|
-
# model doesn't resolve: the readers whose rescue always covered the
|
|
2833
|
-
# resolve too. Each setting's own failure is the EffectiveModel's.
|
|
2834
|
-
def model_setting(fallback)
|
|
2835
|
-
# One read: the log line names the model that failed, even if a
|
|
2836
|
-
# /model switched it meanwhile.
|
|
2837
|
-
choice = @model_choice
|
|
2838
|
-
model = begin
|
|
2839
|
-
effective_model(choice)
|
|
2840
|
-
rescue StandardError => e
|
|
2841
|
-
return EffectiveModel::Unresolved.fallback(:effective_model_unresolved, choice.name, e, fallback)
|
|
2842
|
-
end
|
|
2843
|
-
yield model
|
|
2844
|
-
end
|
|
2722
|
+
def effective_model(choice = @model_choice) = @model_settings.effective_model(choice)
|
|
2845
2723
|
|
|
2846
2724
|
# ── Prompt profile ─────────────────────────────────────────────────────────
|
|
2847
2725
|
|
|
@@ -20,6 +20,8 @@ module Samagotchi
|
|
|
20
20
|
def model_ref = engine.effective_model_ref
|
|
21
21
|
def thinking = engine.thinking_override
|
|
22
22
|
def reasoning_history = engine.reasoning_history_override
|
|
23
|
+
# The kits a delegate may start its child on (Engine#toolkit_kits).
|
|
24
|
+
def toolkit_names = engine.toolkit_kits.keys
|
|
23
25
|
# The peer's name when +name+ is a source a peer bundle declared
|
|
24
26
|
# (PeerSources), for context_read's header.
|
|
25
27
|
def peer_label(name) = engine.peer_sources&.label_for(name)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# Where a card or a notice goes, by what this thread is doing (Engine#show_card,
|
|
5
|
+
# HookRoutes#notify, HookRoutes#guardrail_notify):
|
|
6
|
+
#
|
|
7
|
+
# 1. the plugins load (ExtensionLoad#loading?): held, announced after the load
|
|
8
|
+
# (Engine#announce_load_events!);
|
|
9
|
+
# 2. an anytime command's thread (Engine#running_anytime): announced at once,
|
|
10
|
+
# marked anytime: true;
|
|
11
|
+
# 3. a plugin's init task (chi.init): announced, never a running turn's event;
|
|
12
|
+
# 4. a running turn: the turn's event (its sink and the observers);
|
|
13
|
+
# 5. else announced, or held for Engine#holding_announcements' caller.
|
|
14
|
+
#
|
|
15
|
+
# #route takes all five; #route_by_turn only 4 and 5 (a guardrails line).
|
|
16
|
+
# Each caller says what the event is marked with on each route (CARD_MARKS,
|
|
17
|
+
# NOTICE_MARKS). What it needs from the Engine comes through lookups, called
|
|
18
|
+
# at use.
|
|
19
|
+
class EventRoutes
|
|
20
|
+
# A card says whether it came in a turn (in_turn:); a held one, true.
|
|
21
|
+
CARD_MARKS = { held: { in_turn: true }.freeze, in_turn: { in_turn: true }.freeze,
|
|
22
|
+
between: { in_turn: false }.freeze }.freeze
|
|
23
|
+
# A notice is marked only outside a turn (between_turns: true).
|
|
24
|
+
NOTICE_MARKS = { held: {}.freeze, in_turn: {}.freeze, between: { between_turns: true }.freeze }.freeze
|
|
25
|
+
|
|
26
|
+
# @param extension_load [#call] → ExtensionLoad (#loading?, #hold)
|
|
27
|
+
# @param in_turn_sink [#call] → [Boolean, sink] (TurnState#in_turn_sink)
|
|
28
|
+
# @param anytime_thread [#call] → whether this thread runs an anytime command
|
|
29
|
+
# @param init_task [#call] → the init task running on this thread, nil
|
|
30
|
+
# @param announce [#call] (event) → announces an event
|
|
31
|
+
# @param announce_anytime [#call] (event) → announces an anytime command's event
|
|
32
|
+
# @param announce_or_hold [#call] (event) → announces it, or holds it for the caller
|
|
33
|
+
# @param emit [#call] (sink, event) → emits to a turn's sink + observers
|
|
34
|
+
def initialize(extension_load:, in_turn_sink:, anytime_thread:, init_task:, announce:, announce_anytime:,
|
|
35
|
+
announce_or_hold:, emit:)
|
|
36
|
+
@extension_load = extension_load
|
|
37
|
+
@in_turn_sink = in_turn_sink
|
|
38
|
+
@anytime_thread = anytime_thread
|
|
39
|
+
@init_task = init_task
|
|
40
|
+
@announce = announce
|
|
41
|
+
@announce_anytime = announce_anytime
|
|
42
|
+
@announce_or_hold = announce_or_hold
|
|
43
|
+
@emit = emit
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Route +event+ by the five routes, first match wins.
|
|
47
|
+
# @param held [Hash] merged into the event held while the plugins load
|
|
48
|
+
# @param in_turn [Hash] merged into a running turn's event
|
|
49
|
+
# @param between [Hash] merged into an event announced outside a turn (routes 2, 3, 5)
|
|
50
|
+
# @return [Symbol] the route taken: :held, :anytime, :init, :in_turn or :between
|
|
51
|
+
def route(event, held:, in_turn:, between:)
|
|
52
|
+
extension_load = @extension_load.call
|
|
53
|
+
if extension_load.loading?
|
|
54
|
+
extension_load.hold(event.merge(held))
|
|
55
|
+
return :held
|
|
56
|
+
end
|
|
57
|
+
if @anytime_thread.call
|
|
58
|
+
@announce_anytime.call(event.merge(between))
|
|
59
|
+
return :anytime
|
|
60
|
+
end
|
|
61
|
+
if @init_task.call
|
|
62
|
+
@announce.call(event.merge(between))
|
|
63
|
+
return :init
|
|
64
|
+
end
|
|
65
|
+
route_by_turn(event, in_turn: in_turn, between: between)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Route +event+ by routes 4 and 5 only: a running turn's event, else
|
|
69
|
+
# announced or held for the caller. Never held while the plugins load,
|
|
70
|
+
# never marked anytime (a guardrails line).
|
|
71
|
+
# @return [Symbol] :in_turn or :between
|
|
72
|
+
def route_by_turn(event, in_turn: {}, between: {})
|
|
73
|
+
running, sink = @in_turn_sink.call
|
|
74
|
+
if running
|
|
75
|
+
@emit.call(sink, event.merge(in_turn))
|
|
76
|
+
:in_turn
|
|
77
|
+
else
|
|
78
|
+
@announce_or_hold.call(event.merge(between))
|
|
79
|
+
:between
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "event_routes"
|
|
3
4
|
require_relative "hooks"
|
|
4
5
|
require_relative "log"
|
|
5
6
|
|
|
@@ -8,33 +9,21 @@ module Samagotchi
|
|
|
8
9
|
# reading its event (Hooks::Runtime, Plugin::Host): a notice (#notify),
|
|
9
10
|
# a question (#ask_user), stopping the turn or cutting its generation,
|
|
10
11
|
# and a steer; also the guardrails' line after load (#guardrail_notify).
|
|
12
|
+
# Where a notice goes (held, anytime, init task, turn, between turns) is
|
|
13
|
+
# EventRoutes'.
|
|
11
14
|
#
|
|
12
|
-
# What it needs from the Engine comes through lookups, called at use
|
|
15
|
+
# What it needs from the Engine comes through lookups, called at use, and
|
|
16
|
+
# the Engine's EventRoutes.
|
|
13
17
|
class HookRoutes
|
|
14
18
|
# What a notice may stand in for (#notify's fallback_for:).
|
|
15
19
|
NOTICE_FALLBACKS = %i[display editor].freeze
|
|
16
20
|
|
|
17
|
-
# @param
|
|
18
|
-
# @param in_turn_sink [#call] → [Boolean, sink] (TurnState#in_turn_sink)
|
|
19
|
-
# @param anytime_thread [#call] → whether this thread runs an anytime command
|
|
20
|
-
# @param announce_anytime [#call] (event) → announces an anytime command's event
|
|
21
|
-
# @param init_task [#call] → the init task running on this thread, nil
|
|
22
|
-
# @param announce [#call] (event) → announces an event
|
|
23
|
-
# @param announce_or_hold [#call] (event) → announces it, or holds it for the caller
|
|
24
|
-
# @param emit [#call] (sink, event) → emits to a turn's sink + observers
|
|
21
|
+
# @param event_routes [EventRoutes] where a notice goes
|
|
25
22
|
# @param question_desk [#call] → QuestionDesk
|
|
26
23
|
# @param cancel_controller [#call] → the running turn's CancellationController, nil
|
|
27
24
|
# @param steer [#call] (text, source) → steers the running turn
|
|
28
|
-
def initialize(
|
|
29
|
-
|
|
30
|
-
@extension_load = extension_load
|
|
31
|
-
@in_turn_sink = in_turn_sink
|
|
32
|
-
@anytime_thread = anytime_thread
|
|
33
|
-
@announce_anytime = announce_anytime
|
|
34
|
-
@init_task = init_task
|
|
35
|
-
@announce = announce
|
|
36
|
-
@announce_or_hold = announce_or_hold
|
|
37
|
-
@emit = emit
|
|
25
|
+
def initialize(event_routes:, question_desk:, cancel_controller:, steer:)
|
|
26
|
+
@event_routes = event_routes
|
|
38
27
|
@question_desk = question_desk
|
|
39
28
|
@cancel_controller = cancel_controller
|
|
40
29
|
@steer = steer
|
|
@@ -63,7 +52,9 @@ module Samagotchi
|
|
|
63
52
|
# One line to the user (:hook_notice). During a turn it is a turn
|
|
64
53
|
# event: the turn's sink (the REPL) and the observers (bridge, log).
|
|
65
54
|
# Outside one (a plugin's command at the prompt) it is announced with
|
|
66
|
-
# between_turns: true, which every UI shows as cards are shown.
|
|
55
|
+
# between_turns: true, which every UI shows as cards are shown. While the
|
|
56
|
+
# plugins load it is held; on an anytime command's thread or an init task
|
|
57
|
+
# it is announced (EventRoutes#route).
|
|
67
58
|
#
|
|
68
59
|
# +fallback_for+ names what the line stands in for (NOTICE_FALLBACKS):
|
|
69
60
|
# :display, the answer's display (AnswerDisplay), for a UI that doesn't
|
|
@@ -77,28 +68,16 @@ module Samagotchi
|
|
|
77
68
|
level = :info unless %i[info warn].include?(level)
|
|
78
69
|
notice = { type: :hook_notice, hook: hook.to_s, text: text.to_s, level: level,
|
|
79
70
|
fallback_for: notice_fallback(fallback_for, hook) }.compact.merge(extra)
|
|
80
|
-
|
|
81
|
-
return extension_load.hold(notice) && nil if extension_load.loading?
|
|
82
|
-
|
|
83
|
-
in_turn, sink = @in_turn_sink.call
|
|
84
|
-
if @anytime_thread.call
|
|
85
|
-
@announce_anytime.call(notice.merge(between_turns: true))
|
|
86
|
-
elsif @init_task.call
|
|
87
|
-
@announce.call(notice.merge(between_turns: true))
|
|
88
|
-
elsif in_turn
|
|
89
|
-
@emit.call(sink, notice)
|
|
90
|
-
else
|
|
91
|
-
@announce_or_hold.call(notice.merge(between_turns: true))
|
|
92
|
-
end
|
|
71
|
+
@event_routes.route(notice, **EventRoutes::NOTICE_MARKS)
|
|
93
72
|
nil
|
|
94
73
|
end
|
|
95
74
|
|
|
96
75
|
# A guardrails line to the user after load (:guardrail_warning, as the
|
|
97
|
-
# load failures show): a running turn's event, else announced
|
|
76
|
+
# load failures show): a running turn's event, else announced or held for
|
|
77
|
+
# the caller (EventRoutes#route_by_turn: never held while the plugins
|
|
78
|
+
# load, never an anytime command's, unmarked).
|
|
98
79
|
def guardrail_notify(message)
|
|
99
|
-
|
|
100
|
-
in_turn, sink = @in_turn_sink.call
|
|
101
|
-
in_turn ? @emit.call(sink, event) : @announce_or_hold.call(event)
|
|
80
|
+
@event_routes.route_by_turn({ type: :guardrail_warning, message: message.to_s })
|
|
102
81
|
nil
|
|
103
82
|
end
|
|
104
83
|
|