samagotchi 0.67.0 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -1
- data/docs/architecture.md +6 -3
- data/docs/internals/engine.md +58 -9
- data/docs/sessions.md +1 -1
- data/docs/testing.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/effective_model.rb +283 -0
- data/lib/samagotchi/engine.rb +211 -358
- data/lib/samagotchi/level_setting_command.rb +99 -0
- data/lib/samagotchi/llm_context_strategy.rb +5 -10
- data/lib/samagotchi/reasoning_history_command.rb +29 -90
- data/lib/samagotchi/session.rb +22 -8
- data/lib/samagotchi/session_setting.rb +86 -0
- data/lib/samagotchi/thinking_command.rb +24 -76
- data/lib/samagotchi/thinking_notices.rb +169 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +55 -84
- data/lib/samagotchi/web/public/app.js +74 -505
- data/lib/samagotchi/web/public/bubbles.js +291 -0
- data/lib/samagotchi/web/public/cards_place.js +184 -0
- data/lib/samagotchi/web/public/chip_popover.js +111 -0
- data/lib/samagotchi/web/public/command_list.js +12 -10
- data/lib/samagotchi/web/public/index.html +15 -7
- data/lib/samagotchi/web/public/info_bar.js +3 -3
- data/lib/samagotchi/web/public/info_bar_model.js +110 -0
- data/lib/samagotchi/web/public/init_row.js +21 -10
- data/lib/samagotchi/web/public/level_chip.js +29 -80
- data/lib/samagotchi/web/public/lists_view.js +64 -9
- data/lib/samagotchi/web/public/llm_context_chip.js +36 -105
- data/lib/samagotchi/web/public/model_picker.js +26 -22
- data/lib/samagotchi/web/public/ref_scan.js +1 -7
- data/lib/samagotchi/web/public/refs.js +13 -0
- data/lib/samagotchi/web/public/stage_frame.js +179 -0
- data/lib/samagotchi/web/public/stage_model.js +33 -0
- data/lib/samagotchi/web/public/stage_slots.js +130 -0
- data/lib/samagotchi/web/public/stage_view.js +56 -335
- data/lib/samagotchi/web/public/stage_watch.js +65 -0
- data/lib/samagotchi/web/public/vendor/directive-helpers.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
- data/lib/samagotchi/worker.rb +11 -16
- metadata +14 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cache_confirm"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# A command for a session's own level of a setting (/thinking,
|
|
7
|
+
# /reasoning-history): with no argument, the level the next turn runs at
|
|
8
|
+
# and where it came from; with a level, sets the session's own (the
|
|
9
|
+
# setting's unset word unsets it). A change takes effect at the next
|
|
10
|
+
# turn's start (the REPL and the worker run it between turns), and the
|
|
11
|
+
# reply says what it costs the prompt cache. A costly change asks first
|
|
12
|
+
# (CacheConfirm); --yes (or -y) applies it.
|
|
13
|
+
#
|
|
14
|
+
# A subclass gives its Spec (#spec): what differs between the settings.
|
|
15
|
+
class LevelSettingCommand
|
|
16
|
+
# name: the command; label: the reply's ("thinking: low (session)");
|
|
17
|
+
# usage: the usage line an unknown word gets;
|
|
18
|
+
# parse: word → [known?, the session's own level (nil unsets)];
|
|
19
|
+
# own: engine → the session's own level now;
|
|
20
|
+
# explained: (engine, own) → SettingExplained, nil when it can't be resolved;
|
|
21
|
+
# set: (engine, own) → sets the session's own;
|
|
22
|
+
# cache_cost: (engine, from_level, to_level) → a cost_notes key, nil for none;
|
|
23
|
+
# cost_notes: {cost => the reply's line};
|
|
24
|
+
# follows: what the session follows with no own level;
|
|
25
|
+
# extra_line: engine → one more listing line, nil for none.
|
|
26
|
+
Spec = Data.define(:name, :label, :usage, :parse, :own, :explained, :set, :cache_cost, :cost_notes, :follows,
|
|
27
|
+
:extra_line)
|
|
28
|
+
|
|
29
|
+
# @param engine [Engine] the session's (#session, #history? and the spec's readers)
|
|
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 the command (--yes anywhere)
|
|
37
|
+
# @param confirm [Boolean] ask before a costly change (CacheConfirm);
|
|
38
|
+
# false applies it at once (chi send, a delegate, a start flag)
|
|
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 the usage)
|
|
43
|
+
def run(args, confirm: true)
|
|
44
|
+
word, yes = CacheConfirm.split(args)
|
|
45
|
+
return [listing(explained), false, nil] if word.empty?
|
|
46
|
+
|
|
47
|
+
known, own = spec.parse.call(word)
|
|
48
|
+
raise ArgumentError, "unknown #{spec.label} level #{word}\n#{spec.usage}" unless known
|
|
49
|
+
|
|
50
|
+
session = @engine.session or raise ArgumentError, "no session yet"
|
|
51
|
+
before = explained
|
|
52
|
+
ask = confirm && !yes ? ask(before, own, word) : nil
|
|
53
|
+
return ask if ask
|
|
54
|
+
|
|
55
|
+
spec.set.call(@engine, own)
|
|
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 "#{spec.label}: can't be resolved for this model" unless explained
|
|
64
|
+
|
|
65
|
+
lines = ["#{spec.label}: #{explained.label}"]
|
|
66
|
+
lines << " this session's own: none (follows #{spec.follows})" unless explained.own
|
|
67
|
+
extra = spec.extra_line&.call(@engine)
|
|
68
|
+
lines << extra if extra
|
|
69
|
+
lines.join("\n")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def spec = self.class::SPEC
|
|
75
|
+
|
|
76
|
+
def explained(own: spec.own.call(@engine)) = spec.explained.call(@engine, own)
|
|
77
|
+
|
|
78
|
+
# The reply that asks before a costly change, nil when it needn't.
|
|
79
|
+
def ask(before, own, word)
|
|
80
|
+
would = explained(own: own)
|
|
81
|
+
return nil unless before && would
|
|
82
|
+
|
|
83
|
+
cost = spec.cache_cost.call(@engine, before.level, would.level)
|
|
84
|
+
return nil unless CacheConfirm.needed?(cost: cost, history: @engine.history?, changed: would.level != before.level)
|
|
85
|
+
|
|
86
|
+
step = CacheConfirm.confirm(spec.name, word)
|
|
87
|
+
[[listing(before), "Switching to #{would.level} restarts the prompt cache: #{spec.cost_notes[cost]}", step.text].join("\n"),
|
|
88
|
+
false, step]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def notes(before, after)
|
|
92
|
+
return [] unless before && after
|
|
93
|
+
return ["(no change to what the next turn runs under)"] if before.level == after.level
|
|
94
|
+
|
|
95
|
+
cost = spec.cache_cost.call(@engine, before.level, after.level)
|
|
96
|
+
["From the next turn's start (a running turn keeps its own).", spec.cost_notes[cost]].compact
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -211,17 +211,12 @@ module Samagotchi
|
|
|
211
211
|
def for_session(session, registry: nil)
|
|
212
212
|
return nil if session.nil?
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
HostRegistry.new
|
|
219
|
-
end
|
|
220
|
-
name = session.model_name.to_s.strip.empty? ? ModelProfile.required_model_name(nil) : session.model_name
|
|
221
|
-
target = registry.resolve(name)
|
|
222
|
-
names = registry.lookup_names(session.model_typed || name, resolved: name, target: target)
|
|
214
|
+
# Here, not at the top: config.rb requires this file before ConfigFile
|
|
215
|
+
# exists, and effective_model.rb requires config.rb (via host_registry.rb).
|
|
216
|
+
require_relative "effective_model"
|
|
217
|
+
registry ||= HostRegistry.new
|
|
223
218
|
own = session.llm_context
|
|
224
|
-
|
|
219
|
+
EffectiveModel.of_session(session, registry: registry).llm_context_explained(own&.resolve_args || {})&.summary(own)
|
|
225
220
|
rescue StandardError
|
|
226
221
|
nil
|
|
227
222
|
end
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "level_setting_command"
|
|
4
4
|
require_relative "reasoning_history"
|
|
5
5
|
|
|
6
6
|
module Samagotchi
|
|
7
7
|
# /reasoning-history, the session's own reasoning_history level
|
|
8
|
-
# (Session#reasoning_history):
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# the
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
# --yes (or -y) applies it.
|
|
17
|
-
class ReasoningHistoryCommand
|
|
8
|
+
# (Session#reasoning_history): `/reasoning-history drop|turn|all` sets it
|
|
9
|
+
# (an explicit turn too), `/reasoning-history default` unsets it (the
|
|
10
|
+
# session follows SAMAGOTCHI_REASONING_HISTORY, the model, its host, then
|
|
11
|
+
# turn). Resolved on any host: a native one ignores the level (its loop
|
|
12
|
+
# keeps the running turn's reasoning), but the session keeps it for a
|
|
13
|
+
# chat model. A change that rewrites the earlier replies asks first. See
|
|
14
|
+
# LevelSettingCommand.
|
|
15
|
+
class ReasoningHistoryCommand < LevelSettingCommand
|
|
18
16
|
NAME = "/reasoning-history"
|
|
19
17
|
USAGE = "usage: #{NAME} [#{(ReasoningHistory::LEVELS + [ReasoningHistory::UNSET]).join("|")}]".freeze
|
|
20
18
|
NATIVE_NOTE = " (ignored on this host: the native loop keeps the running turn's reasoning)"
|
|
@@ -24,84 +22,25 @@ module Samagotchi
|
|
|
24
22
|
"from the first earlier thought."
|
|
25
23
|
}.freeze
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
25
|
+
SPEC = Spec.new(
|
|
26
|
+
name: NAME, label: "reasoning history", usage: USAGE, cost_notes: COST_NOTES,
|
|
27
|
+
parse: lambda { |word|
|
|
28
|
+
level = ReasoningHistory.session_level(word)
|
|
29
|
+
[!level.nil? || word == ReasoningHistory::UNSET, level]
|
|
30
|
+
},
|
|
31
|
+
own: lambda(&:reasoning_history_override),
|
|
32
|
+
explained: ->(engine, own) { engine.reasoning_history_explained(native: true, own: own) },
|
|
33
|
+
set: ->(engine, own) { engine.reasoning_history_override = own },
|
|
34
|
+
cache_cost: ->(engine, from, to) { engine.reasoning_history_cache_cost(from, to) },
|
|
35
|
+
follows: "#{ReasoningHistory::ENV_NAME}, the model, its host, then turn",
|
|
36
|
+
extra_line: lambda { |engine|
|
|
37
|
+
chat = begin
|
|
38
|
+
engine.chat_model?
|
|
39
|
+
rescue StandardError
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
chat ? nil : NATIVE_NOTE
|
|
43
|
+
}
|
|
44
|
+
)
|
|
106
45
|
end
|
|
107
46
|
end
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -259,6 +259,8 @@ module Samagotchi
|
|
|
259
259
|
self.interrupted_turn = interrupted_turn
|
|
260
260
|
@turn_checkpoint = nil
|
|
261
261
|
@archived = false
|
|
262
|
+
# #save's lock: one snapshot-and-write at a time (see #save).
|
|
263
|
+
@save_lock = Mutex.new
|
|
262
264
|
end
|
|
263
265
|
|
|
264
266
|
# A `delegate` child: its turns report to its parent (ChildReports). A
|
|
@@ -513,18 +515,30 @@ module Samagotchi
|
|
|
513
515
|
end
|
|
514
516
|
|
|
515
517
|
# Persist the session atomically. Updates +updated_at+ in place.
|
|
518
|
+
#
|
|
519
|
+
# One save at a time per Session: a worker saves its session from the
|
|
520
|
+
# loop/turn thread (TurnCheckpoint, Engine's turn end, WorkerInbound),
|
|
521
|
+
# from Bridge threads (QuestionDesk, commands run mid-turn) and from a
|
|
522
|
+
# signal's thread. AtomicFile keeps each write whole, but two
|
|
523
|
+
# unserialized saves could rename an older snapshot over a newer one (a
|
|
524
|
+
# lost update); under the lock the last rename is the latest snapshot.
|
|
525
|
+
# Never call it from a trap handler (Mutex#lock raises ThreadError
|
|
526
|
+
# there; WorkerSignals saves on a thread of its own) or from inside a
|
|
527
|
+
# save (the lock isn't re-entrant).
|
|
516
528
|
def save(state_dir: self.class.default_state_dir)
|
|
517
|
-
@
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
529
|
+
@save_lock.synchronize do
|
|
530
|
+
@updated_at = Time.now.iso8601(3)
|
|
531
|
+
# Persist with current metadata version so new flag is written
|
|
532
|
+
@metadata_version = METADATA_VERSION
|
|
533
|
+
FileUtils.mkdir_p(state_dir)
|
|
521
534
|
|
|
522
|
-
|
|
535
|
+
path = self.class.session_file(@id, state_dir: state_dir)
|
|
523
536
|
|
|
524
|
-
|
|
525
|
-
|
|
537
|
+
# Auto-compute first_preview if not yet cached and messages contain a user entry.
|
|
538
|
+
compute_first_preview!
|
|
526
539
|
|
|
527
|
-
|
|
540
|
+
AtomicFile.write(path, JSON.pretty_generate(to_h) + "\n")
|
|
541
|
+
end
|
|
528
542
|
self
|
|
529
543
|
end
|
|
530
544
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "effective_model"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# One per-session setting (the session's own value, before its model's):
|
|
7
|
+
# what the UIs show as a chip and the commands set. +key+ is the Session
|
|
8
|
+
# attribute and the chip's key everywhere (the session state, /stats, a
|
|
9
|
+
# :command_ran, the web's session JSON); +changed_by+ the commands'
|
|
10
|
+
# `changed` entries that move the chip; +snapshot_fallback+ how the web
|
|
11
|
+
# treats a worker snapshot's value: :or (a nil value falls back to the
|
|
12
|
+
# session file's) or :key (a key present stands, nil included: a worker
|
|
13
|
+
# says nil on a native host, which ignores reasoning_history; an older
|
|
14
|
+
# worker has no key). +chip+ the chip from an EffectiveModel and the
|
|
15
|
+
# session's own value; nil for the toolkit, which resolves against the
|
|
16
|
+
# worker's kits (Engine#toolkit_summary), not the model. +live+ the
|
|
17
|
+
# chip as an Engine's public readers give it (a :command_ran's).
|
|
18
|
+
SessionSetting = Data.define(:key, :changed_by, :snapshot_fallback, :chip, :live) do
|
|
19
|
+
# The session's own value, nil without a session or one.
|
|
20
|
+
def own(session) = session&.public_send(key)
|
|
21
|
+
|
|
22
|
+
# Sets the session's own value (nil unsets it); the caller saves.
|
|
23
|
+
def set(session, value) = session.public_send(:"#{key}=", value)
|
|
24
|
+
|
|
25
|
+
# Whether a command that changed +changed+ moves this chip.
|
|
26
|
+
def changed_by?(changed) = changed.intersect?(changed_by)
|
|
27
|
+
|
|
28
|
+
# Whether the chip comes from the model (not the toolkit).
|
|
29
|
+
def model? = !chip.nil?
|
|
30
|
+
|
|
31
|
+
# The chip for +session+ on +model+ (EffectiveModel, nil when it
|
|
32
|
+
# doesn't resolve: nil then).
|
|
33
|
+
def chip_for(model, session) = model && chip.call(model, own(session))
|
|
34
|
+
|
|
35
|
+
# The chip as +engine+ (a worker's) gives it now.
|
|
36
|
+
def summary(engine) = live.call(engine)
|
|
37
|
+
|
|
38
|
+
# The chip for a saved session no worker runs (Web::App, the session
|
|
39
|
+
# lists): resolved from its file, config and the registry (no network);
|
|
40
|
+
# nil when it can't be.
|
|
41
|
+
def saved_summary(session, registry:)
|
|
42
|
+
return saved_toolkit(session.toolkit) unless model?
|
|
43
|
+
|
|
44
|
+
chip_for(EffectiveModel.of_session(session, registry: registry), session)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The web's chip: a worker's snapshot (string keys) per
|
|
50
|
+
# +snapshot_fallback+, else the block's (the saved one).
|
|
51
|
+
def snapshot_value(snapshot)
|
|
52
|
+
name = key.to_s
|
|
53
|
+
return snapshot[name] if snapshot_fallback == :key && snapshot&.key?(name)
|
|
54
|
+
return snapshot[name] if snapshot_fallback == :or && snapshot && snapshot[name]
|
|
55
|
+
|
|
56
|
+
yield
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# A toolkit as its file names it ({name:, label:}), nil on full:
|
|
62
|
+
# without a worker nothing resolves it (a bundle's kits load there).
|
|
63
|
+
def saved_toolkit(toolkit)
|
|
64
|
+
toolkit && !toolkit.full? ? { name: toolkit.name, label: toolkit.label } : nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class SessionSetting
|
|
69
|
+
LLM_CONTEXT = new(key: :llm_context, changed_by: %i[llm_context model], snapshot_fallback: :or,
|
|
70
|
+
chip: ->(model, own) { model.llm_context_explained(own&.resolve_args || {})&.summary(own) },
|
|
71
|
+
live: lambda(&:llm_context_summary))
|
|
72
|
+
THINKING = new(key: :thinking, changed_by: %i[thinking model], snapshot_fallback: :or,
|
|
73
|
+
chip: ->(model, own) { model.thinking_explained(own: own)&.summary },
|
|
74
|
+
live: ->(engine) { engine.thinking_explained&.summary })
|
|
75
|
+
# nil on a native host (its loop ignores the level).
|
|
76
|
+
REASONING_HISTORY = new(key: :reasoning_history, changed_by: %i[reasoning_history model], snapshot_fallback: :key,
|
|
77
|
+
chip: ->(model, own) { model.reasoning_history_explained(own: own)&.summary },
|
|
78
|
+
live: ->(engine) { engine.reasoning_history_explained&.summary })
|
|
79
|
+
TOOLKIT = new(key: :toolkit, changed_by: %i[toolkit], snapshot_fallback: :key, chip: nil, live: lambda(&:toolkit_summary))
|
|
80
|
+
|
|
81
|
+
# In the order the session state, a :command_ran and the web name them.
|
|
82
|
+
ALL = [LLM_CONTEXT, THINKING, REASONING_HISTORY, TOOLKIT].freeze
|
|
83
|
+
# /stats's (no toolkit: its JSON never had one).
|
|
84
|
+
STATS = [LLM_CONTEXT, THINKING, REASONING_HISTORY].freeze
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "level_setting_command"
|
|
4
4
|
require_relative "thinking"
|
|
5
5
|
|
|
6
6
|
module Samagotchi
|
|
7
|
-
# /thinking, the session's own thinking level (Session#thinking):
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
# the reply says what it costs the prompt cache (Thinking.cache_cost).
|
|
14
|
-
# A costly change asks first (CacheConfirm); --yes (or -y) applies it.
|
|
15
|
-
class ThinkingCommand
|
|
7
|
+
# /thinking, the session's own thinking level (Session#thinking):
|
|
8
|
+
# `/thinking off|low|medium|high` sets it, `/thinking default` unsets it
|
|
9
|
+
# (the session follows the process default, the model, its host, then
|
|
10
|
+
# thinking.level). What a change costs the prompt cache:
|
|
11
|
+
# Thinking.cache_cost. See LevelSettingCommand.
|
|
12
|
+
class ThinkingCommand < LevelSettingCommand
|
|
16
13
|
NAME = "/thinking"
|
|
17
14
|
SETTABLE = (Thinking::LEVELS - [Thinking::DEFAULT]).freeze
|
|
18
15
|
USAGE = "usage: #{NAME} [#{Thinking::LEVELS.join("|")}]".freeze
|
|
@@ -24,71 +21,22 @@ module Samagotchi
|
|
|
24
21
|
tail: "Only the prompt's tail changes: the cache keeps the rest."
|
|
25
22
|
}.freeze
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return [listing(@engine.thinking_explained), false, nil] if word.empty?
|
|
45
|
-
|
|
46
|
-
level = word.to_sym
|
|
47
|
-
raise ArgumentError, "unknown thinking level #{word}\n#{USAGE}" unless Thinking::LEVELS.include?(level)
|
|
48
|
-
|
|
49
|
-
session = @engine.session or raise ArgumentError, "no session yet"
|
|
50
|
-
own = SETTABLE.include?(level) ? level : nil
|
|
51
|
-
before = @engine.thinking_explained
|
|
52
|
-
ask = confirm && !yes ? ask(before, own, word) : nil
|
|
53
|
-
return ask if ask
|
|
54
|
-
|
|
55
|
-
@engine.thinking_override = own
|
|
56
|
-
@save.call(session)
|
|
57
|
-
after = @engine.thinking_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 "thinking: can't be resolved for this model" unless explained
|
|
64
|
-
|
|
65
|
-
lines = ["thinking: #{explained.label}"]
|
|
66
|
-
unless explained.own
|
|
67
|
-
lines << " this session's own: none (follows chi web --thinking / SAMAGOTCHI_THINKING_LEVEL, the model, " \
|
|
68
|
-
"its host, then thinking.level)"
|
|
69
|
-
end
|
|
70
|
-
lines.join("\n")
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
# The reply that asks before a costly change, nil when it needn't.
|
|
76
|
-
def ask(before, own, word)
|
|
77
|
-
would = @engine.thinking_explained(own: own)
|
|
78
|
-
return nil unless before && would
|
|
79
|
-
|
|
80
|
-
cost = @engine.thinking_cache_cost
|
|
81
|
-
return nil unless CacheConfirm.needed?(cost: cost, history: @engine.history?, changed: would.level != before.level)
|
|
82
|
-
|
|
83
|
-
step = CacheConfirm.confirm(NAME, word)
|
|
84
|
-
[[listing(before), "Switching to #{would.level} restarts the prompt cache: #{COST_NOTES[cost]}", step.text].join("\n"), false, step]
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def notes(before, after)
|
|
88
|
-
return [] unless before && after
|
|
89
|
-
return ["(no change to what the next turn runs under)"] if before.level == after.level
|
|
90
|
-
|
|
91
|
-
["From the next turn's start (a running turn keeps its own).", COST_NOTES[@engine.thinking_cache_cost]].compact
|
|
92
|
-
end
|
|
24
|
+
SPEC = Spec.new(
|
|
25
|
+
name: NAME, label: "thinking", usage: USAGE, cost_notes: COST_NOTES,
|
|
26
|
+
parse: lambda { |word|
|
|
27
|
+
level = word.to_sym
|
|
28
|
+
if Thinking::LEVELS.include?(level)
|
|
29
|
+
[true, SETTABLE.include?(level) ? level : nil]
|
|
30
|
+
else
|
|
31
|
+
[false, nil]
|
|
32
|
+
end
|
|
33
|
+
},
|
|
34
|
+
own: lambda(&:thinking_override),
|
|
35
|
+
explained: ->(engine, own) { engine.thinking_explained(own: own) },
|
|
36
|
+
set: ->(engine, own) { engine.thinking_override = own },
|
|
37
|
+
cache_cost: ->(engine, _from, _to) { engine.thinking_cache_cost },
|
|
38
|
+
follows: "chi web --thinking / SAMAGOTCHI_THINKING_LEVEL, the model, its host, then thinking.level",
|
|
39
|
+
extra_line: nil
|
|
40
|
+
)
|
|
93
41
|
end
|
|
94
42
|
end
|