samagotchi 0.55.0 → 0.56.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 +30 -1
- data/docs/architecture.md +6 -2
- data/docs/cli.md +29 -7
- data/docs/configuration.md +84 -8
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/engine.md +14 -0
- data/docs/internals/prompt-caching.md +8 -0
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cache_confirm.rb +51 -0
- data/lib/samagotchi/client_id.rb +12 -0
- data/lib/samagotchi/config.rb +7 -3
- data/lib/samagotchi/engine.rb +102 -8
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/host_config.rb +6 -3
- data/lib/samagotchi/host_registry.rb +4 -3
- data/lib/samagotchi/kernel_loop.rb +99 -16
- data/lib/samagotchi/llm/chat_loop.rb +122 -71
- data/lib/samagotchi/llm/openai_chat.rb +2 -1
- data/lib/samagotchi/llm/stream_drops.rb +63 -0
- data/lib/samagotchi/llm/turn_settings.rb +9 -5
- data/lib/samagotchi/model_settings.rb +5 -2
- data/lib/samagotchi/reasoning_history.rb +132 -0
- data/lib/samagotchi/reasoning_history_command.rb +107 -0
- data/lib/samagotchi/self_report.rb +21 -0
- data/lib/samagotchi/session.rb +23 -2
- data/lib/samagotchi/session_commands.rb +55 -15
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_setup.rb +7 -5
- data/lib/samagotchi/setting_explained.rb +20 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +2 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +11 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
- data/lib/samagotchi/terminal_ui.rb +13 -6
- data/lib/samagotchi/thinking.rb +20 -25
- data/lib/samagotchi/thinking_command.rb +33 -11
- data/lib/samagotchi/tools/builtins.rb +6 -2
- data/lib/samagotchi/tools/peers.rb +4 -2
- data/lib/samagotchi/tools/task_runtime.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +23 -2
- data/lib/samagotchi/web/public/app.js +20 -1
- data/lib/samagotchi/web/public/commands.js +36 -3
- data/lib/samagotchi/web/public/composer.js +5 -1
- data/lib/samagotchi/web/public/index.html +6 -3
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/level_chip.js +174 -0
- data/lib/samagotchi/web/public/model_pick.js +1 -1
- data/lib/samagotchi/web/public/reasoning_history_chip.js +49 -0
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +10 -2
- data/lib/samagotchi/web/public/thinking_chip.js +33 -138
- data/lib/samagotchi/worker.rb +17 -5
- metadata +8 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cache_confirm"
|
|
4
|
+
require_relative "reasoning_history"
|
|
5
|
+
|
|
6
|
+
module Samagotchi
|
|
7
|
+
# /reasoning-history, the session's own reasoning_history level
|
|
8
|
+
# (Session#reasoning_history): with no argument, the level the next turn
|
|
9
|
+
# runs under and where it came from; `/reasoning-history drop|turn|all`
|
|
10
|
+
# sets the session's own (an explicit turn too), `/reasoning-history
|
|
11
|
+
# default` unsets it (the session follows SAMAGOTCHI_REASONING_HISTORY,
|
|
12
|
+
# the model, its host, then turn). A change takes effect at the next
|
|
13
|
+
# turn's start; a native host ignores the level (its loop keeps the
|
|
14
|
+
# running turn's reasoning), but the session keeps it for a chat model.
|
|
15
|
+
# A change that rewrites the earlier replies asks first (CacheConfirm);
|
|
16
|
+
# --yes (or -y) applies it.
|
|
17
|
+
class ReasoningHistoryCommand
|
|
18
|
+
NAME = "/reasoning-history"
|
|
19
|
+
USAGE = "usage: #{NAME} [#{(ReasoningHistory::LEVELS + [ReasoningHistory::UNSET]).join("|")}]".freeze
|
|
20
|
+
NATIVE_NOTE = " (ignored on this host: the native loop keeps the running turn's reasoning)"
|
|
21
|
+
# ReasoningHistory.cache_cost's; nil (no cost) says nothing.
|
|
22
|
+
COST_NOTES = {
|
|
23
|
+
history: "Earlier replies gain or lose their reasoning: the server reads the conversation again " \
|
|
24
|
+
"from the first earlier thought."
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# @param engine [Engine] #reasoning_history_explained,
|
|
28
|
+
# #reasoning_history_override(=), #session, #chat_model?,
|
|
29
|
+
# #reasoning_history_cache_cost, #history?
|
|
30
|
+
# @param save [#call] saves the session after a change
|
|
31
|
+
def initialize(engine:, save:)
|
|
32
|
+
@engine = engine
|
|
33
|
+
@save = save
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param args [String] what follows /reasoning-history (--yes anywhere)
|
|
37
|
+
# @param confirm [Boolean] ask before a costly change (CacheConfirm);
|
|
38
|
+
# false applies it at once
|
|
39
|
+
# @return [Array(String, Boolean, CacheConfirm::Confirm|nil)] the reply,
|
|
40
|
+
# whether the level changed, and the confirm it asks for (nothing
|
|
41
|
+
# changed then)
|
|
42
|
+
# @raise [ArgumentError] a word that isn't a level (with USAGE)
|
|
43
|
+
def run(args, confirm: true)
|
|
44
|
+
word, yes = CacheConfirm.split(args)
|
|
45
|
+
return [listing(explained), false, nil] if word.empty?
|
|
46
|
+
|
|
47
|
+
level = ReasoningHistory.session_level(word)
|
|
48
|
+
raise ArgumentError, "unknown reasoning history level #{word}\n#{USAGE}" unless level || word == ReasoningHistory::UNSET
|
|
49
|
+
|
|
50
|
+
session = @engine.session or raise ArgumentError, "no session yet"
|
|
51
|
+
before = explained
|
|
52
|
+
ask = confirm && !yes ? ask(before, level, word) : nil
|
|
53
|
+
return ask if ask
|
|
54
|
+
|
|
55
|
+
@engine.reasoning_history_override = level
|
|
56
|
+
@save.call(session)
|
|
57
|
+
after = explained
|
|
58
|
+
[[listing(after), *notes(before, after)].join("\n"), true, nil]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The level, where it came from, and what the session sets itself.
|
|
62
|
+
def listing(explained)
|
|
63
|
+
return "reasoning history: can't be resolved for this model" unless explained
|
|
64
|
+
|
|
65
|
+
lines = ["reasoning history: #{explained.label}"]
|
|
66
|
+
unless explained.own
|
|
67
|
+
lines << " this session's own: none (follows #{ReasoningHistory::ENV_NAME}, the model, its host, then turn)"
|
|
68
|
+
end
|
|
69
|
+
lines << NATIVE_NOTE unless chat?
|
|
70
|
+
lines.join("\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Resolved on any host: a native one ignores it, but /reasoning-history
|
|
76
|
+
# still shows what a chat model would get.
|
|
77
|
+
def explained(own: @engine.reasoning_history_override)
|
|
78
|
+
@engine.reasoning_history_explained(native: true, own: own)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The reply that asks before a costly change, nil when it needn't.
|
|
82
|
+
def ask(before, own, word)
|
|
83
|
+
would = explained(own: own)
|
|
84
|
+
return nil unless before && would
|
|
85
|
+
|
|
86
|
+
cost = @engine.reasoning_history_cache_cost(before.level, would.level)
|
|
87
|
+
return nil unless CacheConfirm.needed?(cost: cost, history: @engine.history?, changed: would.level != before.level)
|
|
88
|
+
|
|
89
|
+
step = CacheConfirm.confirm(NAME, word)
|
|
90
|
+
[[listing(before), "Switching to #{would.level} restarts the prompt cache: #{COST_NOTES[cost]}", step.text].join("\n"), false, step]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def chat?
|
|
94
|
+
@engine.chat_model?
|
|
95
|
+
rescue StandardError
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def notes(before, after)
|
|
100
|
+
return [] unless before && after
|
|
101
|
+
return ["(no change to what the next turn runs under)"] if before.level == after.level
|
|
102
|
+
|
|
103
|
+
cost = @engine.reasoning_history_cache_cost(before.level, after.level)
|
|
104
|
+
["From the next turn's start (a running turn keeps its own).", COST_NOTES[cost]].compact
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -13,6 +13,7 @@ require_relative "model_overlay"
|
|
|
13
13
|
require_relative "model_notes"
|
|
14
14
|
require_relative "prompt_note"
|
|
15
15
|
require_relative "thinking"
|
|
16
|
+
require_relative "reasoning_history"
|
|
16
17
|
require_relative "served_model"
|
|
17
18
|
require_relative "host_registry"
|
|
18
19
|
require_relative "llm/http"
|
|
@@ -67,6 +68,7 @@ module Samagotchi
|
|
|
67
68
|
["loop", model ? loop_for(model, env) : "-"],
|
|
68
69
|
["profile", model ? profile_for(model, env) : "-"],
|
|
69
70
|
["thinking", model ? thinking_for(model, env) : "-"],
|
|
71
|
+
["reasoning history", model ? reasoning_history_for(model, env) : "-"],
|
|
70
72
|
["served model", model ? served_model_for(model, env) : "-"],
|
|
71
73
|
["context window", context_window(env, model)],
|
|
72
74
|
["bundles", bundles_summary],
|
|
@@ -154,6 +156,8 @@ module Samagotchi
|
|
|
154
156
|
PARENT_SESSION_ENV = "SAMAGOTCHI_PARENT_SESSION"
|
|
155
157
|
# The session's own thinking level, beside its model (Builtins.parent_env).
|
|
156
158
|
SESSION_THINKING_ENV = "SAMAGOTCHI_SESSION_THINKING"
|
|
159
|
+
# The session's own reasoning_history level (Builtins.parent_env).
|
|
160
|
+
SESSION_REASONING_HISTORY_ENV = "SAMAGOTCHI_SESSION_REASONING_HISTORY"
|
|
157
161
|
|
|
158
162
|
# The model of the session this chi self runs in (its execute exports
|
|
159
163
|
# SAMAGOTCHI_SESSION_MODEL, the resolved ref, live after /model); nil
|
|
@@ -369,6 +373,23 @@ module Samagotchi
|
|
|
369
373
|
"(unknown: #{e.message})"
|
|
370
374
|
end
|
|
371
375
|
|
|
376
|
+
# Which of its reasoning the model gets back and where that came from
|
|
377
|
+
# (ReasoningHistory.resolve); in a session's commands its own level
|
|
378
|
+
# first (SAMAGOTCHI_SESSION_REASONING_HISTORY); a native host keeps the
|
|
379
|
+
# running turn's on its own.
|
|
380
|
+
def reasoning_history_for(model, env)
|
|
381
|
+
registry = HostRegistry.new(env: env)
|
|
382
|
+
target = registry.resolve(model)
|
|
383
|
+
return "native loop: the running turn's, always" unless target.entry&.chat?
|
|
384
|
+
|
|
385
|
+
session = session_model(env) ? ReasoningHistory.session_level(env[SESSION_REASONING_HISTORY_ENV]) : nil
|
|
386
|
+
level, source = ReasoningHistory.resolve(target, names: registry.lookup_names(model, target: target),
|
|
387
|
+
models: ConfigFile.model_settings(env: env), session: session)
|
|
388
|
+
ReasoningHistory.label(level, source)
|
|
389
|
+
rescue StandardError => e
|
|
390
|
+
"(unknown: #{e.message})"
|
|
391
|
+
end
|
|
392
|
+
|
|
372
393
|
def host_for(model, env)
|
|
373
394
|
target = HostRegistry.new(env: env).resolve(model)
|
|
374
395
|
entry = target.entry
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -137,6 +137,23 @@ module Samagotchi
|
|
|
137
137
|
# read; nil when there is neither.
|
|
138
138
|
def thinking_file = @thinking&.to_s || @thinking_unread
|
|
139
139
|
|
|
140
|
+
# The session's own reasoning_history level (:drop, :turn, :all), before
|
|
141
|
+
# SAMAGOTCHI_REASONING_HISTORY and the model's (ReasoningHistory.resolve);
|
|
142
|
+
# nil when it has none. Saved and carried like #thinking.
|
|
143
|
+
attr_reader :reasoning_history
|
|
144
|
+
|
|
145
|
+
# @param value [Symbol, String, nil] a value that isn't a level reads
|
|
146
|
+
# as nil and is saved back as written, until the session sets its own
|
|
147
|
+
def reasoning_history=(value)
|
|
148
|
+
@reasoning_history = ReasoningHistory.session_level(value)
|
|
149
|
+
unread = @reasoning_history.nil? && value.is_a?(String) && !value.strip.casecmp?(ReasoningHistory::UNSET)
|
|
150
|
+
@reasoning_history_unread = unread ? value : nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The session file's "reasoning_history": the level, else a value it
|
|
154
|
+
# couldn't read; nil when there is neither.
|
|
155
|
+
def reasoning_history_file = @reasoning_history&.to_s || @reasoning_history_unread
|
|
156
|
+
|
|
140
157
|
# The hook that stopped the last turn (stop_turn, or a cut with no retry
|
|
141
158
|
# left), e.g. "loop-guard"; nil when it ended otherwise.
|
|
142
159
|
def stopped_by
|
|
@@ -165,7 +182,7 @@ module Samagotchi
|
|
|
165
182
|
used_memory_names: [], project_root: nil,
|
|
166
183
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
167
184
|
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [],
|
|
168
|
-
continues: nil, thinking: nil)
|
|
185
|
+
continues: nil, thinking: nil, reasoning_history: nil)
|
|
169
186
|
@id = id
|
|
170
187
|
@metadata_version = metadata_version
|
|
171
188
|
@mode = mode
|
|
@@ -192,6 +209,7 @@ module Samagotchi
|
|
|
192
209
|
@continues = continues&.to_s
|
|
193
210
|
self.llm_context = llm_context
|
|
194
211
|
self.thinking = thinking
|
|
212
|
+
self.reasoning_history = reasoning_history
|
|
195
213
|
@archived = false
|
|
196
214
|
end
|
|
197
215
|
|
|
@@ -270,6 +288,7 @@ module Samagotchi
|
|
|
270
288
|
delegate: delegate,
|
|
271
289
|
llm_context: setup.llm_context,
|
|
272
290
|
thinking: setup.thinking,
|
|
291
|
+
reasoning_history: setup.reasoning_history,
|
|
273
292
|
continues: continues
|
|
274
293
|
)
|
|
275
294
|
end
|
|
@@ -331,7 +350,8 @@ module Samagotchi
|
|
|
331
350
|
"delegate" => false,
|
|
332
351
|
"llm_context" => nil,
|
|
333
352
|
"continues" => nil,
|
|
334
|
-
"thinking" => nil
|
|
353
|
+
"thinking" => nil,
|
|
354
|
+
"reasoning_history" => nil
|
|
335
355
|
}.freeze
|
|
336
356
|
|
|
337
357
|
# A session from a parsed session file. +messages+ false leaves the
|
|
@@ -453,6 +473,7 @@ module Samagotchi
|
|
|
453
473
|
when "delegate" then @delegate
|
|
454
474
|
when "llm_context" then llm_context_file
|
|
455
475
|
when "thinking" then thinking_file
|
|
476
|
+
when "reasoning_history" then reasoning_history_file
|
|
456
477
|
when "prompt_notes" then @prompt_notes.map(&:to_file)
|
|
457
478
|
when "last_turn" then @last_turn&.to_file
|
|
458
479
|
when "used_memory_names", "preloaded_memory_names", "muted_memory_names"
|
|
@@ -12,6 +12,7 @@ require_relative "tools/peers"
|
|
|
12
12
|
require_relative "commands/registry"
|
|
13
13
|
require_relative "llm_context_command"
|
|
14
14
|
require_relative "thinking_command"
|
|
15
|
+
require_relative "reasoning_history_command"
|
|
15
16
|
|
|
16
17
|
module Samagotchi
|
|
17
18
|
# The session commands a REPL and a session worker both run: /model,
|
|
@@ -31,6 +32,7 @@ module Samagotchi
|
|
|
31
32
|
HELP_COMMAND = "/help"
|
|
32
33
|
LLM_CONTEXT_COMMAND = LLMContextCommand::NAME
|
|
33
34
|
THINKING_COMMAND = ThinkingCommand::NAME
|
|
35
|
+
REASONING_HISTORY_COMMAND = ReasoningHistoryCommand::NAME
|
|
34
36
|
# A remote catalog has hundreds of ids (OpenRouter ~380): plain /models
|
|
35
37
|
# shows this many per host; /models <text> lists every match.
|
|
36
38
|
MODELS_PER_HOST = 20
|
|
@@ -44,6 +46,10 @@ module Samagotchi
|
|
|
44
46
|
# /help's note per listed mid_turn (the web's command list says the same).
|
|
45
47
|
MID_TURN_NOTES = { "anytime" => "mid-turn too", "depends" => "show: mid-turn too" }.freeze
|
|
46
48
|
|
|
49
|
+
# The commands that ask before a change that restarts the prompt cache
|
|
50
|
+
# (CacheConfirm): #run passes them its confirm flag.
|
|
51
|
+
CONFIRMING = %i[thinking reasoning_history].freeze
|
|
52
|
+
|
|
47
53
|
# @!attribute status [Symbol] :ok, or :error when the line was refused
|
|
48
54
|
# @!attribute output [String, nil] what to tell the user
|
|
49
55
|
# @!attribute changed [Array<Symbol>] :messages and/or :model
|
|
@@ -52,7 +58,9 @@ module Samagotchi
|
|
|
52
58
|
# @!attribute shell [Boolean] output is a !cmd's own output
|
|
53
59
|
# @!attribute decision [Symbol, nil] an answer to the continue offer:
|
|
54
60
|
# :resume, :abort, :abort_with_reason or :invalid
|
|
55
|
-
|
|
61
|
+
# @!attribute confirm [CacheConfirm::Confirm, nil] a costly change asks
|
|
62
|
+
# first (nothing changed): the line that applies it
|
|
63
|
+
Result = Struct.new(:status, :output, :changed, :model_name, :resume, :shell, :decision, :confirm, keyword_init: true)
|
|
56
64
|
|
|
57
65
|
# Puts the built-in commands into +registry+, in lookup order (!rollback
|
|
58
66
|
# before !cmd): the ones #run runs, then the UIs' own (local: Tab and
|
|
@@ -92,6 +100,9 @@ module Samagotchi
|
|
|
92
100
|
id: :llm_context, mid_turn: show_mid_turn(LLM_CONTEXT_COMMAND), match: ->(text) { text.match?(%r{\A/llm-context(?:\s+.*)?\z}) }) { |text| llm_context(text) }
|
|
93
101
|
registry.register(THINKING_COMMAND, "show or set this session's thinking level (off|low|medium|high|default)",
|
|
94
102
|
id: :thinking, mid_turn: show_mid_turn(THINKING_COMMAND), match: ->(text) { text.match?(%r{\A/thinking(?:\s+.*)?\z}) }) { |text| thinking(text) }
|
|
103
|
+
registry.register(REASONING_HISTORY_COMMAND, "show or set which of its reasoning this session's model gets back (drop|turn|all|default)",
|
|
104
|
+
id: :reasoning_history, mid_turn: show_mid_turn(REASONING_HISTORY_COMMAND),
|
|
105
|
+
match: ->(text) { text.match?(%r{\A/reasoning-history(?:\s+.*)?\z}) }) { |text| reasoning_history(text) }
|
|
95
106
|
registry.register(HELP_COMMAND, "list the commands, the bundles' too", anytime: true) { |_text| reply(help_listing) }
|
|
96
107
|
registry.register("/stats", "show the session's stats", local: true)
|
|
97
108
|
registry.register("/recap", "show the session's recap", local: true)
|
|
@@ -231,13 +242,20 @@ module Samagotchi
|
|
|
231
242
|
# How help and errors name an entry (the shell's "!" is "!<cmd>").
|
|
232
243
|
def self.display_name(entry) = entry.name == SHELL_BANG_PREFIX ? "#{SHELL_BANG_PREFIX}<cmd>" : entry.name
|
|
233
244
|
|
|
245
|
+
# @param confirm [Boolean] a costly /thinking or /reasoning-history
|
|
246
|
+
# change asks first (CacheConfirm): true for a person at a web tab or
|
|
247
|
+
# a TUI; false applies it at once (chi send, a delegate, a plugin,
|
|
248
|
+
# chi -p)
|
|
234
249
|
# @return [Result, nil] nil when +line+ is not one of these commands
|
|
235
|
-
def run(line)
|
|
250
|
+
def run(line, confirm: true)
|
|
236
251
|
text = line.to_s.strip
|
|
237
252
|
entry = @registry.lookup(text)
|
|
238
253
|
return nil unless entry&.handler
|
|
239
254
|
return run_bundle_command(entry, text) unless entry.source == "core"
|
|
240
255
|
|
|
256
|
+
# The two that may ask take the flag; the rest never ask.
|
|
257
|
+
return send(entry.id, text, confirm: confirm) if CONFIRMING.include?(entry.id)
|
|
258
|
+
|
|
241
259
|
instance_exec(text, &entry.handler)
|
|
242
260
|
end
|
|
243
261
|
|
|
@@ -283,8 +301,9 @@ module Samagotchi
|
|
|
283
301
|
entry.source == "core" ? 0 : 1
|
|
284
302
|
end
|
|
285
303
|
|
|
286
|
-
def reply(output, status: :ok, changed: [])
|
|
287
|
-
Result.new(status: status, output: output, changed: changed, model_name: model_name, resume: false, shell: false
|
|
304
|
+
def reply(output, status: :ok, changed: [], confirm: nil)
|
|
305
|
+
Result.new(status: status, output: output, changed: changed, model_name: model_name, resume: false, shell: false,
|
|
306
|
+
confirm: confirm)
|
|
288
307
|
end
|
|
289
308
|
|
|
290
309
|
# Explicit escape hatch after a Ctrl-C: discard the salvaged partial
|
|
@@ -427,15 +446,27 @@ module Samagotchi
|
|
|
427
446
|
|
|
428
447
|
# /thinking (ThinkingCommand); :thinking in changed when it set the
|
|
429
448
|
# session's level.
|
|
430
|
-
def thinking(text)
|
|
431
|
-
output, changed = ThinkingCommand.new(engine: @engine, save: ->(session) { save_setup(session) })
|
|
432
|
-
|
|
433
|
-
reply(output, changed: changed ? [:thinking] : [])
|
|
449
|
+
def thinking(text, confirm: true)
|
|
450
|
+
output, changed, ask = ThinkingCommand.new(engine: @engine, save: ->(session) { save_setup(session) })
|
|
451
|
+
.run(text.delete_prefix(THINKING_COMMAND), confirm: confirm)
|
|
452
|
+
reply(output, changed: changed ? [:thinking] : [], confirm: ask)
|
|
453
|
+
rescue ArgumentError => e
|
|
454
|
+
reply(e.message, status: :error)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# /reasoning-history (ReasoningHistoryCommand); :reasoning_history in
|
|
458
|
+
# changed when it set the session's level.
|
|
459
|
+
def reasoning_history(text, confirm: true)
|
|
460
|
+
output, changed, ask = ReasoningHistoryCommand.new(engine: @engine, save: ->(session) { save_setup(session) })
|
|
461
|
+
.run(text.delete_prefix(REASONING_HISTORY_COMMAND),
|
|
462
|
+
confirm: confirm)
|
|
463
|
+
reply(output, changed: changed ? [:reasoning_history] : [], confirm: ask)
|
|
434
464
|
rescue ArgumentError => e
|
|
435
465
|
reply(e.message, status: :error)
|
|
436
466
|
end
|
|
437
467
|
|
|
438
|
-
# A session setting changed (/llm-context, /thinking):
|
|
468
|
+
# A session setting changed (/llm-context, /thinking, /reasoning-history):
|
|
469
|
+
# saved with its model.
|
|
439
470
|
def save_setup(session)
|
|
440
471
|
@engine.store_model!(session)
|
|
441
472
|
@save.call(session)
|
|
@@ -453,9 +484,9 @@ module Samagotchi
|
|
|
453
484
|
def model_command(input)
|
|
454
485
|
suffix = input.delete_prefix(MODEL_COMMAND).strip
|
|
455
486
|
if suffix.empty?
|
|
456
|
-
return ["runtime model: #{model_name}#{model_note}#{served_note}#{sampling_note}#{thinking_note}#{notes_note}", false] if model_name == @engine.model_ref_for(@default_model)
|
|
487
|
+
return ["runtime model: #{model_name}#{model_note}#{served_note}#{sampling_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", false] if model_name == @engine.model_ref_for(@default_model)
|
|
457
488
|
|
|
458
|
-
return ["runtime model: #{model_name}#{model_note("default: #{@default_model}")}#{served_note}#{sampling_note}#{thinking_note}#{notes_note}", false]
|
|
489
|
+
return ["runtime model: #{model_name}#{model_note("default: #{@default_model}")}#{served_note}#{sampling_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", false]
|
|
459
490
|
end
|
|
460
491
|
|
|
461
492
|
# Parse flags: --default and --alias <name> / --alias=<name> (tolerant order)
|
|
@@ -516,7 +547,7 @@ module Samagotchi
|
|
|
516
547
|
|
|
517
548
|
key = alias_name.strip.downcase
|
|
518
549
|
warn_prefix = previous ? "warning: overwriting alias '#{key}' (#{previous} -> #{model_name}); " : ""
|
|
519
|
-
base = persist_default ? "runtime model set to #{model_name}#{model_note} and default updated#{thinking_note}" : "runtime model set to #{model_name}#{model_note}#{thinking_note}"
|
|
550
|
+
base = persist_default ? "runtime model set to #{model_name}#{model_note} and default updated#{thinking_note}#{reasoning_history_note}" : "runtime model set to #{model_name}#{model_note}#{thinking_note}#{reasoning_history_note}"
|
|
520
551
|
["#{warn_prefix}#{base}; alias '#{key}' -> '#{model_name}' persisted", true]
|
|
521
552
|
else
|
|
522
553
|
return ["--default requires a model name: usage /model --default <name> or /model <name> [--default]", false] if arg.empty?
|
|
@@ -525,14 +556,14 @@ module Samagotchi
|
|
|
525
556
|
return ["--default cannot be combined with clear/default/none/off", false] if persist_default
|
|
526
557
|
|
|
527
558
|
switch_model(@default_model)
|
|
528
|
-
return ["runtime model reset to #{model_name}#{model_note}#{thinking_note}#{notes_note}", true]
|
|
559
|
+
return ["runtime model reset to #{model_name}#{model_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", true]
|
|
529
560
|
end
|
|
530
561
|
|
|
531
562
|
switch_model(arg, persist_default: persist_default)
|
|
532
563
|
if persist_default
|
|
533
|
-
["runtime model set to #{model_name}#{model_note} and default updated#{thinking_note}#{notes_note}", true]
|
|
564
|
+
["runtime model set to #{model_name}#{model_note} and default updated#{thinking_note}#{reasoning_history_note}#{notes_note}", true]
|
|
534
565
|
else
|
|
535
|
-
["runtime model set to #{model_name}#{model_note}#{thinking_note}#{notes_note}", true]
|
|
566
|
+
["runtime model set to #{model_name}#{model_note}#{thinking_note}#{reasoning_history_note}#{notes_note}", true]
|
|
536
567
|
end
|
|
537
568
|
end
|
|
538
569
|
end
|
|
@@ -592,6 +623,15 @@ module Samagotchi
|
|
|
592
623
|
""
|
|
593
624
|
end
|
|
594
625
|
|
|
626
|
+
# "; reasoning history: drop (models: qwen)" when the chat model has a
|
|
627
|
+
# level set.
|
|
628
|
+
def reasoning_history_note
|
|
629
|
+
summary = @engine.reasoning_history_summary
|
|
630
|
+
summary ? "; reasoning history: #{summary}" : ""
|
|
631
|
+
rescue StandardError
|
|
632
|
+
""
|
|
633
|
+
end
|
|
634
|
+
|
|
595
635
|
# " (default: x, profile=qwen36, name)": +extra+ and the prompt profile,
|
|
596
636
|
# which a chat host's model doesn't have (its loop uses none); "" when
|
|
597
637
|
# there is neither.
|
|
@@ -272,7 +272,8 @@ module Samagotchi
|
|
|
272
272
|
|
|
273
273
|
GUARDRAILS_ENV_PREFIX = "SAMAGOTCHI_GUARDRAILS_"
|
|
274
274
|
# Tools::Builtins.parent_env's (Tools::TaskRuntime::MARKER_ENV_KEYS).
|
|
275
|
-
SESSION_MARKER_ENV = %w[SAMAGOTCHI_PARENT_SESSION SAMAGOTCHI_SESSION_MODEL SAMAGOTCHI_SESSION_THINKING
|
|
275
|
+
SESSION_MARKER_ENV = %w[SAMAGOTCHI_PARENT_SESSION SAMAGOTCHI_SESSION_MODEL SAMAGOTCHI_SESSION_THINKING
|
|
276
|
+
SAMAGOTCHI_SESSION_REASONING_HISTORY].freeze
|
|
276
277
|
|
|
277
278
|
# List all sessions, reading status from persisted session.json files.
|
|
278
279
|
# +project_root+: only that project's sessions (nil: every session).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "llm_context_override"
|
|
4
|
+
require_relative "reasoning_history"
|
|
4
5
|
require_relative "thinking"
|
|
5
6
|
|
|
6
7
|
module Samagotchi
|
|
@@ -8,15 +9,16 @@ module Samagotchi
|
|
|
8
9
|
# value on the paths that spawn a session from a whole setup
|
|
9
10
|
# (SessionManager.spawn_session, a continue, a plugin's fork, the web's
|
|
10
11
|
# create, chi send --new): +llm_context+ is an LLMContextOverride or nil,
|
|
11
|
-
# +thinking+ a level Symbol (Thinking.session_level) or nil
|
|
12
|
+
# +thinking+ a level Symbol (Thinking.session_level) or nil,
|
|
13
|
+
# +reasoning_history+ a level Symbol (ReasoningHistory.session_level) or nil.
|
|
12
14
|
#
|
|
13
15
|
# It doesn't save itself: the session keeps each setting in its own field
|
|
14
16
|
# and file key, and resolves it on its own. A delegate child starts
|
|
15
17
|
# without one; a continue and a fork copy their session's (.of).
|
|
16
|
-
SessionSetup = Data.define(:llm_context, :thinking) do
|
|
17
|
-
def initialize(llm_context: nil, thinking: nil) = super
|
|
18
|
+
SessionSetup = Data.define(:llm_context, :thinking, :reasoning_history) do
|
|
19
|
+
def initialize(llm_context: nil, thinking: nil, reasoning_history: nil) = super
|
|
18
20
|
|
|
19
|
-
def empty? = llm_context.nil? && thinking.nil?
|
|
21
|
+
def empty? = llm_context.nil? && thinking.nil? && reasoning_history.nil?
|
|
20
22
|
|
|
21
23
|
# A session's own setup, for a session that starts from it.
|
|
22
24
|
# @param session [Session, nil]
|
|
@@ -24,7 +26,7 @@ module Samagotchi
|
|
|
24
26
|
def self.of(session)
|
|
25
27
|
return new if session.nil?
|
|
26
28
|
|
|
27
|
-
new(llm_context: session.llm_context, thinking: session.thinking)
|
|
29
|
+
new(llm_context: session.llm_context, thinking: session.thinking, reasoning_history: session.reasoning_history)
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# A session setting's level and where it came from (Thinking.explain,
|
|
5
|
+
# ReasoningHistory.explain): +source+ is "session",
|
|
6
|
+
# "SAMAGOTCHI_THINKING_LEVEL", "models: qwen", "hosts.work" and the like,
|
|
7
|
+
# nil for the default; +own+ is the session's own level, nil without one.
|
|
8
|
+
# What the commands, /model, /stats, the snapshots and the web read.
|
|
9
|
+
# (LLMContextStrategy::Explained is a different shape: three values,
|
|
10
|
+
# each with its own origin.)
|
|
11
|
+
SettingExplained = Data.define(:level, :source, :own) do
|
|
12
|
+
# "low (session)", "default" with no source.
|
|
13
|
+
def label = source ? "#{level} (#{source})" : level.to_s
|
|
14
|
+
|
|
15
|
+
# The plain form the snapshots, the worker's command_ran event and
|
|
16
|
+
# the web read.
|
|
17
|
+
# @return [Hash] symbol keys
|
|
18
|
+
def summary = { level: level.to_s, source: source, own: own&.to_s }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -54,7 +54,8 @@ module Samagotchi
|
|
|
54
54
|
commands = []
|
|
55
55
|
commands << "/model #{model}" if model && (attach || resume)
|
|
56
56
|
commands << LLMContextOverride.command_line(llm_context) if (attach || resume) && !llm_context.empty?
|
|
57
|
-
|
|
57
|
+
# --yes: typing the flag is the choice (no confirm step).
|
|
58
|
+
commands << "#{ThinkingCommand::NAME} #{thinking} --yes" if (attach || resume) && thinking
|
|
58
59
|
first_command = commands.size > 1 ? commands : commands.first
|
|
59
60
|
surface = open_surface
|
|
60
61
|
attached = AttachedLoop.new(client: client, screen: surface, client_id: "tui:#{Process.pid}", first_prompt: prompt,
|
|
@@ -633,6 +633,7 @@ module Samagotchi
|
|
|
633
633
|
else
|
|
634
634
|
@screen.commit("#{paint("model>", 36)} #{output}")
|
|
635
635
|
end
|
|
636
|
+
prefill_confirm(event)
|
|
636
637
|
return unless event[:model_name]
|
|
637
638
|
|
|
638
639
|
# After the output, so the status row changes with the line saying why.
|
|
@@ -640,6 +641,16 @@ module Samagotchi
|
|
|
640
641
|
@status.update(model: event[:model_name], default_model: default_model_name, served: served)
|
|
641
642
|
end
|
|
642
643
|
|
|
644
|
+
# Our change that asks first (CacheConfirm) puts its --yes line in the
|
|
645
|
+
# input: Enter applies it. Another client's question is its own.
|
|
646
|
+
def prefill_confirm(event)
|
|
647
|
+
return unless own?(event[:client_id])
|
|
648
|
+
|
|
649
|
+
confirm = event[:confirm]
|
|
650
|
+
line = confirm.is_a?(Hash) ? confirm[:line] || confirm["line"] : nil
|
|
651
|
+
@reader&.prefill(line.to_s) if line
|
|
652
|
+
end
|
|
653
|
+
|
|
643
654
|
# Our command the worker couldn't run yet goes back into the prompt, for
|
|
644
655
|
# Enter once the turn ends (not the launch's --model: that ends the launch).
|
|
645
656
|
def put_back_busy_command(event)
|
|
@@ -598,8 +598,11 @@ module Samagotchi
|
|
|
598
598
|
lines << "model notes: #{notes}" unless notes.empty?
|
|
599
599
|
llm_context = llm_context_stats_text(snapshot[:llm_context])
|
|
600
600
|
lines << "llm context: #{llm_context}" if llm_context
|
|
601
|
-
thinking =
|
|
601
|
+
thinking = setting_stats_text(snapshot[:thinking])
|
|
602
602
|
lines << "thinking: #{thinking}" if thinking
|
|
603
|
+
# A chat host's only: a native one ignores it (the summary is nil).
|
|
604
|
+
history = setting_stats_text(snapshot[:reasoning_history])
|
|
605
|
+
lines << "reasoning history: #{history}" if history
|
|
603
606
|
if snapshot[:served_model]
|
|
604
607
|
asked = snapshot[:served_model_for]
|
|
605
608
|
differs = !snapshot[:served_expected_by] && ServedModel.differs?(asked, snapshot[:served_model])
|
|
@@ -641,10 +644,10 @@ module Samagotchi
|
|
|
641
644
|
"budget #{budget} (#{get.call(:budget_where)})"
|
|
642
645
|
end
|
|
643
646
|
|
|
644
|
-
# "low (session)", "default" for /stats (
|
|
645
|
-
# symbol or string keys, the attached
|
|
646
|
-
# without one.
|
|
647
|
-
def
|
|
647
|
+
# "low (session)", "default" for /stats (SettingExplained#summary:
|
|
648
|
+
# thinking, reasoning_history; symbol or string keys, the attached
|
|
649
|
+
# TUI's come as JSON); nil without one.
|
|
650
|
+
def setting_stats_text(summary)
|
|
648
651
|
return nil unless summary.is_a?(Hash)
|
|
649
652
|
|
|
650
653
|
level = summary[:level] || summary["level"]
|
|
@@ -371,12 +371,13 @@ module Samagotchi
|
|
|
371
371
|
|
|
372
372
|
# `chi -p "/models" --non-interactive`: the command's output; a refused
|
|
373
373
|
# one fails the run (bin/chi exits 1). The session is saved only when
|
|
374
|
-
# the command changed it.
|
|
374
|
+
# the command changed it. Nobody is there to answer a confirm: a
|
|
375
|
+
# costly /thinking or /reasoning-history applies at once.
|
|
375
376
|
# @return [Symbol, nil] :turn_failed when the command was refused
|
|
376
377
|
def run_one_shot_command(session)
|
|
377
378
|
line = @prompt.strip
|
|
378
379
|
result = begin
|
|
379
|
-
@commands.run(line)
|
|
380
|
+
@commands.run(line, confirm: false)
|
|
380
381
|
rescue StandardError => e
|
|
381
382
|
SessionCommands::Result.new(status: :error, output: "#{line.split.first}: #{e.message}", changed: [])
|
|
382
383
|
end
|
|
@@ -789,11 +790,14 @@ module Samagotchi
|
|
|
789
790
|
|
|
790
791
|
# /model, /models, !rollback, !cmd, /continue (shared with workers);
|
|
791
792
|
# an anytime command's cards print as it shows them (btw's
|
|
792
|
-
# "thinking…" before the answer).
|
|
793
|
+
# "thinking…" before the answer). Piped input has nobody to answer a
|
|
794
|
+
# confirm (the next line is already written): a costly /thinking or
|
|
795
|
+
# /reasoning-history applies at once.
|
|
796
|
+
confirm = !piped_input?
|
|
793
797
|
command = if command_registry.anytime?(input)
|
|
794
|
-
@engine.running_anytime { @commands.run(input) }
|
|
798
|
+
@engine.running_anytime { @commands.run(input, confirm: confirm) }
|
|
795
799
|
else
|
|
796
|
-
@commands.run(input)
|
|
800
|
+
@commands.run(input, confirm: confirm)
|
|
797
801
|
end
|
|
798
802
|
if command
|
|
799
803
|
show_command_result(command)
|
|
@@ -874,7 +878,9 @@ module Samagotchi
|
|
|
874
878
|
session.save unless Session.exist?(session.id)
|
|
875
879
|
end
|
|
876
880
|
|
|
877
|
-
# A !cmd shows its own output; everything else is the model> line.
|
|
881
|
+
# A !cmd shows its own output; everything else is the model> line. A
|
|
882
|
+
# change that asks first (CacheConfirm) puts its --yes line in the next
|
|
883
|
+
# input: Enter applies it.
|
|
878
884
|
def show_command_result(result)
|
|
879
885
|
if result.shell
|
|
880
886
|
@surface.commit(result.output)
|
|
@@ -882,6 +888,7 @@ module Samagotchi
|
|
|
882
888
|
elsif !result.output.nil?
|
|
883
889
|
@surface.commit("\nmodel> #{result.output}")
|
|
884
890
|
end
|
|
891
|
+
queue_input_prefill(result.confirm.line) if result.confirm && !piped_input?
|
|
885
892
|
sync_model_mirrors if result.changed.include?(:model)
|
|
886
893
|
end
|
|
887
894
|
|
data/lib/samagotchi/thinking.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "config"
|
|
4
|
+
require_relative "model_profile"
|
|
5
|
+
require_relative "setting_explained"
|
|
4
6
|
|
|
5
7
|
module Samagotchi
|
|
6
8
|
# How much a model thinks: one level per model, host or run, mapped to each
|
|
@@ -38,21 +40,6 @@ module Samagotchi
|
|
|
38
40
|
# The source a session's own level reads as.
|
|
39
41
|
SESSION_SOURCE = "session"
|
|
40
42
|
|
|
41
|
-
# A level and where it came from (#explain): +source+ is "session",
|
|
42
|
-
# "--thinking", "SAMAGOTCHI_THINKING_LEVEL", "models: qwen",
|
|
43
|
-
# "hosts.work" or "thinking.level", nil for the default; +own+ is the
|
|
44
|
-
# session's own level, nil without one. What /thinking, /model,
|
|
45
|
-
# /stats, the snapshots and the web read.
|
|
46
|
-
Explained = Data.define(:level, :source, :own) do
|
|
47
|
-
# "low (session)", "default" with no source.
|
|
48
|
-
def label = source ? "#{level} (#{source})" : level.to_s
|
|
49
|
-
|
|
50
|
-
# The plain form the snapshots, the worker's command_ran event and
|
|
51
|
-
# the web read.
|
|
52
|
-
# @return [Hash] symbol keys
|
|
53
|
-
def summary = { level: level.to_s, source: source, own: own&.to_s }
|
|
54
|
-
end
|
|
55
|
-
|
|
56
43
|
module_function
|
|
57
44
|
|
|
58
45
|
# A session's own level from its file or a command word: :off, :low,
|
|
@@ -113,11 +100,11 @@ module Samagotchi
|
|
|
113
100
|
[DEFAULT, nil]
|
|
114
101
|
end
|
|
115
102
|
|
|
116
|
-
# #resolve, as
|
|
117
|
-
# @return [
|
|
103
|
+
# #resolve, as a SettingExplained with the session's own level beside it.
|
|
104
|
+
# @return [SettingExplained]
|
|
118
105
|
def explain(target, names:, models: nil, session: nil)
|
|
119
106
|
level, source = resolve(target, names: names, models: models, session: session)
|
|
120
|
-
|
|
107
|
+
SettingExplained.new(level: level, source: source, own: session)
|
|
121
108
|
end
|
|
122
109
|
|
|
123
110
|
# thinking.level as a level and its origin (:cli, :env, :file), or nil.
|
|
@@ -133,17 +120,25 @@ module Samagotchi
|
|
|
133
120
|
end
|
|
134
121
|
|
|
135
122
|
# What a level change costs the prompt cache on +target+ (/thinking's
|
|
136
|
-
# note;
|
|
137
|
-
# :full
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
# :
|
|
123
|
+
# note; CacheConfirm asks before the costly ones):
|
|
124
|
+
# :full Gemma 4's prompt starts with <|think|> (the native
|
|
125
|
+
# profile, or a local chat server whose model name says
|
|
126
|
+
# gemma: its template writes the same token), so the whole
|
|
127
|
+
# prompt is read again;
|
|
128
|
+
# :provider a remote chat host gets other request fields: Claude's
|
|
129
|
+
# message breakpoints and OpenAI's prefix cache are lost;
|
|
130
|
+
# :tail native Qwen, a local chat server's Qwen and the rest:
|
|
131
|
+
# only the tail changes (llama.cpp and Splash keep the rest).
|
|
142
132
|
# @param target [HostRegistry::ModelTarget]
|
|
143
133
|
# @param profile [String, nil] the native prompt profile's name
|
|
144
134
|
# @return [Symbol]
|
|
145
135
|
def cache_cost(target, profile: nil)
|
|
146
|
-
|
|
136
|
+
entry = target.entry
|
|
137
|
+
if entry&.chat?
|
|
138
|
+
return :provider if entry.remote?
|
|
139
|
+
|
|
140
|
+
return ModelProfile.inferred_profile_name(target.bare_model) == "gemma4" ? :full : :tail
|
|
141
|
+
end
|
|
147
142
|
|
|
148
143
|
profile == "gemma4" ? :full : :tail
|
|
149
144
|
end
|