samagotchi 0.40.0 → 0.42.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 +82 -1
- data/README.md +1 -1
- data/bin/chi +19 -2
- data/docs/cli.md +32 -7
- data/docs/configuration.md +34 -3
- data/docs/guardrails.md +15 -3
- data/docs/internals/prompt-caching.md +2 -1
- data/docs/memory.md +108 -1
- data/docs/sessions.md +5 -5
- data/docs/testing.md +16 -0
- data/lib/samagotchi/bridge/card_store.rb +6 -7
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +23 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +13 -0
- data/lib/samagotchi/bundles/system/self_map.md +4 -2
- data/lib/samagotchi/child_reports.rb +8 -4
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +5 -2
- data/lib/samagotchi/engine.rb +121 -16
- data/lib/samagotchi/events.rb +8 -1
- data/lib/samagotchi/guardrails/model_size.rb +3 -4
- data/lib/samagotchi/guardrails/rules.rb +27 -17
- data/lib/samagotchi/guardrails/targets.rb +40 -1
- data/lib/samagotchi/guardrails/verdict.rb +6 -3
- data/lib/samagotchi/llm/errors.rb +33 -1
- data/lib/samagotchi/llm_context_command.rb +151 -0
- data/lib/samagotchi/llm_context_override.rb +180 -0
- data/lib/samagotchi/llm_context_strategy.rb +63 -13
- data/lib/samagotchi/memory_bundle/index_size.rb +103 -0
- data/lib/samagotchi/memory_bundle/index_sync.rb +3 -2
- data/lib/samagotchi/model_match.rb +54 -0
- data/lib/samagotchi/model_notes.rb +205 -0
- data/lib/samagotchi/parent_report.rb +12 -2
- data/lib/samagotchi/plugin/context.rb +3 -2
- data/lib/samagotchi/plugin/sessions.rb +3 -1
- data/lib/samagotchi/prompt_note.rb +53 -0
- data/lib/samagotchi/reply_wait.rb +3 -17
- data/lib/samagotchi/self_report.rb +47 -0
- data/lib/samagotchi/send_command.rb +22 -2
- data/lib/samagotchi/session.rb +44 -4
- data/lib/samagotchi/session_commands.rb +45 -6
- data/lib/samagotchi/session_manager.rb +5 -2
- data/lib/samagotchi/session_metrics.rb +47 -7
- data/lib/samagotchi/system_prompt.rb +79 -11
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +13 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +32 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +40 -0
- data/lib/samagotchi/terminal_ui.rb +16 -6
- data/lib/samagotchi/tool_runner.rb +26 -1
- data/lib/samagotchi/tools/memory.rb +70 -3
- data/lib/samagotchi/turn_flow.rb +44 -8
- data/lib/samagotchi/turn_note.rb +40 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +35 -6
- data/lib/samagotchi/web/public/app.js +47 -7
- data/lib/samagotchi/web/public/ctx.js +28 -5
- data/lib/samagotchi/web/public/format.js +29 -2
- data/lib/samagotchi/web/public/index.html +12 -0
- data/lib/samagotchi/web/public/live_turn.js +1 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +194 -0
- data/lib/samagotchi/web/public/session_state.js +6 -0
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_view.js +15 -2
- data/lib/samagotchi/web/session_summary.rb +4 -0
- data/lib/samagotchi/worker.rb +36 -18
- data/lib/samagotchi/worker_wakes.rb +2 -1
- metadata +8 -1
|
@@ -10,6 +10,8 @@ require_relative "session"
|
|
|
10
10
|
require_relative "log_path"
|
|
11
11
|
require_relative "model_profile"
|
|
12
12
|
require_relative "model_overlay"
|
|
13
|
+
require_relative "model_notes"
|
|
14
|
+
require_relative "prompt_note"
|
|
13
15
|
require_relative "thinking"
|
|
14
16
|
require_relative "served_model"
|
|
15
17
|
require_relative "host_registry"
|
|
@@ -20,6 +22,7 @@ require_relative "hooks/loader"
|
|
|
20
22
|
require_relative "memory_bundle/provenance"
|
|
21
23
|
require_relative "memory_bundle/manifest"
|
|
22
24
|
require_relative "memory_bundle/system_bundle"
|
|
25
|
+
require_relative "memory_bundle/index_size"
|
|
23
26
|
require_relative "desktop"
|
|
24
27
|
require_relative "live_versions"
|
|
25
28
|
require_relative "web/lan"
|
|
@@ -53,10 +56,12 @@ module Samagotchi
|
|
|
53
56
|
["hooks dir", hooks_dir(env)],
|
|
54
57
|
["memories", Tools::MemoryRead.memories_dir("system", env: env)],
|
|
55
58
|
["project memories", Tools::MemoryRead.memories_dir("project", env: env)],
|
|
59
|
+
["memory index", memory_index_summary(env)],
|
|
56
60
|
["sessions", Session.default_state_dir(env: env)],
|
|
57
61
|
["log", log_path(env)],
|
|
58
62
|
["model", model ? model_label(model, default, env) : "(not configured)"],
|
|
59
63
|
["model key", model ? model_key_for(model, env) : "-"],
|
|
64
|
+
["model notes", model ? model_notes_for(model, env) : "-"],
|
|
60
65
|
["host", model ? host_for(model, env) : "-"],
|
|
61
66
|
["api key", model ? api_key_for(model, env) : "-"],
|
|
62
67
|
["loop", model ? loop_for(model, env) : "-"],
|
|
@@ -100,6 +105,19 @@ module Samagotchi
|
|
|
100
105
|
LogPath.resolve(env: env) || "(disabled: log.disable)"
|
|
101
106
|
end
|
|
102
107
|
|
|
108
|
+
# "system ~2.0k tokens (54 lines), project ~1.4k tokens (35 lines)":
|
|
109
|
+
# the two indexes every prompt carries (before a session's mutes), a
|
|
110
|
+
# scope over memory.index_warn_tokens marked so. Two file reads. The
|
|
111
|
+
# indexes come from +env+'s memories dirs; the limit and
|
|
112
|
+
# context.chars_per_token from Config, which reads the process's ENV
|
|
113
|
+
# and config.yml as the rest of chi self's settings (web.port) do.
|
|
114
|
+
def memory_index_summary(env)
|
|
115
|
+
limit = MemoryBundle::IndexSize.warn_limit
|
|
116
|
+
%w[system project].map { |scope| MemoryBundle::IndexSize.of_scope(scope, env: env).summary(limit) }.join(", ")
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
"(unreadable: #{e.message})"
|
|
119
|
+
end
|
|
120
|
+
|
|
103
121
|
def with_presence(path)
|
|
104
122
|
File.exist?(path) ? path : "#{path} (missing)"
|
|
105
123
|
end
|
|
@@ -172,6 +190,35 @@ module Samagotchi
|
|
|
172
190
|
ModelOverlay.key_for(model) || "-"
|
|
173
191
|
end
|
|
174
192
|
|
|
193
|
+
# "model_notes_deepseek (system, 612 chars)": the model notes a prompt
|
|
194
|
+
# of +model+ loads (ModelNotes: matched on its bare id and key, the
|
|
195
|
+
# session's --mute list left out when chi self runs in one), "none"
|
|
196
|
+
# without any. Read from the memories dirs now: a session's prompt
|
|
197
|
+
# takes them at its next build (/stats names the ones it carries).
|
|
198
|
+
def model_notes_for(model, env)
|
|
199
|
+
bare = HostRegistry.new(env: env).bare_name(model)
|
|
200
|
+
notes = ModelNotes.for(name: bare, key: ModelOverlay.key_for(bare), muted: session_muted(env), env: env,
|
|
201
|
+
warn: false)
|
|
202
|
+
text = PromptNote.text(notes.map { |note| PromptNote.from_note(note) })
|
|
203
|
+
text.empty? ? "none" : text
|
|
204
|
+
rescue StandardError => e
|
|
205
|
+
"(unreadable: #{e.message})"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The --mute list of the session chi self runs in (its execute exports
|
|
209
|
+
# SAMAGOTCHI_PARENT_SESSION); [] outside one.
|
|
210
|
+
def session_muted(env)
|
|
211
|
+
id = env[PARENT_SESSION_ENV].to_s
|
|
212
|
+
return [] if id.empty? || !Session.valid_id?(id)
|
|
213
|
+
|
|
214
|
+
state_dir = Session.default_state_dir(env: env)
|
|
215
|
+
return [] unless Session.exist?(id, state_dir: state_dir)
|
|
216
|
+
|
|
217
|
+
MutedMemories.normalize_list(Session.load(id, state_dir: state_dir).muted_memory_names)
|
|
218
|
+
rescue StandardError
|
|
219
|
+
[]
|
|
220
|
+
end
|
|
221
|
+
|
|
175
222
|
def model_name
|
|
176
223
|
ModelProfile.required_model_name
|
|
177
224
|
rescue ArgumentError
|
|
@@ -33,7 +33,7 @@ module Samagotchi
|
|
|
33
33
|
|
|
34
34
|
USAGE = <<~TEXT
|
|
35
35
|
Usage: chi send [-m TEXT] [--image PATH]... (ID|PREFIX)...
|
|
36
|
-
chi send --new [--dir DIR] [--model M] [-m TEXT] [--image PATH]...
|
|
36
|
+
chi send --new [--dir DIR] [--model M] [--llm-context LAYERS] [-m TEXT] [--image PATH]...
|
|
37
37
|
chi send --wait [--timeout S] [--format json] [-m TEXT] [--image PATH]... (--new | ID)
|
|
38
38
|
chi send --wait [--timeout S] [--format json] ID
|
|
39
39
|
Sends a message to each session, as if typed in it: a turn starts,
|
|
@@ -53,6 +53,9 @@ module Samagotchi
|
|
|
53
53
|
--dir DIR (--new) its folder, the project it belongs to; default
|
|
54
54
|
the current one
|
|
55
55
|
--model M (--new) its model; default the configured one
|
|
56
|
+
--llm-context LAYERS, --llm-context-apply RULE, --llm-context-budget N
|
|
57
|
+
(--new) its own LLM context strategy (none, stale,forget),
|
|
58
|
+
apply rule and budget (64k, off), as chi takes them
|
|
56
59
|
--wait wait for the answer and print it (one session); the
|
|
57
60
|
other lines go to stderr. Exit 3: it waits for an
|
|
58
61
|
answer from you: the question, its options and the
|
|
@@ -81,6 +84,9 @@ module Samagotchi
|
|
|
81
84
|
f.switch "--new"
|
|
82
85
|
f.value "--dir"
|
|
83
86
|
f.value "--model"
|
|
87
|
+
f.value "--llm-context", key: :llm_strategy
|
|
88
|
+
f.value "--llm-context-apply", key: :llm_apply
|
|
89
|
+
f.value "--llm-context-budget", key: :llm_budget
|
|
84
90
|
f.value "--image", key: :images, repeat: true
|
|
85
91
|
f.switch "--wait"
|
|
86
92
|
f.value "--timeout"
|
|
@@ -200,14 +206,27 @@ module Samagotchi
|
|
|
200
206
|
return new_options(options) if options[:new]
|
|
201
207
|
|
|
202
208
|
%i[dir model].each { |key| return usage_error("--#{key} needs --new") if options[key] }
|
|
209
|
+
LLM_CONTEXT_FLAGS.each { |key, flag| return usage_error("#{flag} needs --new") if options[key] }
|
|
203
210
|
return usage_error("give session ids") if options[:ids].empty?
|
|
204
211
|
|
|
205
212
|
options
|
|
206
213
|
end
|
|
207
214
|
|
|
215
|
+
# The --llm-context* option keys, and their fields.
|
|
216
|
+
LLM_CONTEXT_FLAGS = { llm_strategy: "--llm-context", llm_apply: "--llm-context-apply",
|
|
217
|
+
llm_budget: "--llm-context-budget" }.freeze
|
|
218
|
+
LLM_CONTEXT_FIELDS = { llm_strategy: :strategy, llm_apply: :apply, llm_budget: :budget_tokens }.freeze
|
|
219
|
+
|
|
208
220
|
def new_options(options)
|
|
209
221
|
return usage_error("--new takes no session ids: it starts one session") unless options[:ids].empty?
|
|
210
222
|
|
|
223
|
+
words = LLM_CONTEXT_FIELDS.filter_map { |key, field| [field, options[key]] if options[key] }.to_h
|
|
224
|
+
begin
|
|
225
|
+
options[:llm_context] = LLMContextOverride.update(nil, words) unless words.empty?
|
|
226
|
+
rescue ArgumentError => e
|
|
227
|
+
return usage_error(e.message)
|
|
228
|
+
end
|
|
229
|
+
|
|
211
230
|
if options[:dir]
|
|
212
231
|
options[:dir] = File.expand_path(options[:dir])
|
|
213
232
|
return usage_error("no folder #{options[:dir]}") unless File.directory?(options[:dir])
|
|
@@ -246,7 +265,8 @@ module Samagotchi
|
|
|
246
265
|
begin
|
|
247
266
|
start = through_worker ? { prompt: nil, title: prompt } : { prompt: prompt }
|
|
248
267
|
session = SessionManager.spawn_session(**start, working_directory: options[:dir],
|
|
249
|
-
model_name: options[:model],
|
|
268
|
+
model_name: options[:model], llm_context: options[:llm_context],
|
|
269
|
+
state_dir: @state_dir)
|
|
250
270
|
rescue StandardError => e
|
|
251
271
|
error_line("chi send: could not start a session: #{e.message}")
|
|
252
272
|
return 1
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -6,6 +6,8 @@ require "securerandom"
|
|
|
6
6
|
require "time"
|
|
7
7
|
require_relative "atomic_file"
|
|
8
8
|
require_relative "context_note"
|
|
9
|
+
require_relative "llm_context_override"
|
|
10
|
+
require_relative "prompt_note"
|
|
9
11
|
|
|
10
12
|
require_relative "paths"
|
|
11
13
|
require_relative "project_scope"
|
|
@@ -43,6 +45,17 @@ module Samagotchi
|
|
|
43
45
|
# from it (--mute): the worker rebuilds the same prompt on a respawn.
|
|
44
46
|
# Names as given; the engine normalizes them.
|
|
45
47
|
attr_accessor :preloaded_memory_names, :muted_memory_names
|
|
48
|
+
# The model notes the session's system prompt carried at its last build
|
|
49
|
+
# (Array<PromptNote>: name, scope, chars, digest; ModelNotes), set by the
|
|
50
|
+
# Engine at each build (a start, a resume's or a woken worker's first
|
|
51
|
+
# turn, /model) and saved, so what a session ran with can be read later.
|
|
52
|
+
attr_reader :prompt_notes
|
|
53
|
+
|
|
54
|
+
# @param notes [Array<PromptNote, Hash>] a file's hashes are read
|
|
55
|
+
# (PromptNote.list)
|
|
56
|
+
def prompt_notes=(notes)
|
|
57
|
+
@prompt_notes = PromptNote.list(notes)
|
|
58
|
+
end
|
|
46
59
|
# The name model_name was typed as when that differs from the resolved
|
|
47
60
|
# ref stored in model_name (an alias: `small` for `gemma-small`), else
|
|
48
61
|
# nil. The models: lookup takes it (HostRegistry#lookup_names).
|
|
@@ -70,6 +83,26 @@ module Samagotchi
|
|
|
70
83
|
# delegate children's reports (ChildReports); context: one an attached
|
|
71
84
|
# context source's change started (Worker).
|
|
72
85
|
attr_accessor :last_turn
|
|
86
|
+
# The session's own llm_context values (LLMContextOverride: strategy,
|
|
87
|
+
# apply rule, budget), before the model's; nil when it has none.
|
|
88
|
+
# Saved, so a --resume and a respawned worker keep them; a plugin's
|
|
89
|
+
# fork copies them, a delegate child starts without.
|
|
90
|
+
attr_reader :llm_context
|
|
91
|
+
|
|
92
|
+
# @param value [LLMContextOverride, Hash, nil] an empty one is nil; a
|
|
93
|
+
# file's Hash keeps the fields it can't read (LLMContextOverride.unread),
|
|
94
|
+
# saved back as they were until the session sets its own
|
|
95
|
+
def llm_context=(value)
|
|
96
|
+
@llm_context_unread = LLMContextOverride.unread(value)
|
|
97
|
+
@llm_context = LLMContextOverride.from_file(value)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The session file's "llm_context": the override's fields over the ones
|
|
101
|
+
# it couldn't read; nil when there are none.
|
|
102
|
+
def llm_context_file
|
|
103
|
+
saved = @llm_context_unread.merge(@llm_context&.to_file || {})
|
|
104
|
+
saved.empty? ? nil : saved
|
|
105
|
+
end
|
|
73
106
|
|
|
74
107
|
# The hook that stopped the last turn (stop_turn, or a cut with no retry
|
|
75
108
|
# left), e.g. "loop-guard"; nil when it ended otherwise. Both strings
|
|
@@ -97,7 +130,7 @@ module Samagotchi
|
|
|
97
130
|
first_preview: "", test_run: false, pending_question: nil,
|
|
98
131
|
used_memory_names: [], project_root: nil,
|
|
99
132
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
100
|
-
last_turn: nil, model_typed: nil, delegate: false)
|
|
133
|
+
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [])
|
|
101
134
|
@id = id
|
|
102
135
|
@metadata_version = metadata_version
|
|
103
136
|
@mode = mode
|
|
@@ -115,11 +148,13 @@ module Samagotchi
|
|
|
115
148
|
@used_memory_names = self.class.name_list(used_memory_names)
|
|
116
149
|
@preloaded_memory_names = self.class.name_list(preloaded_memory_names)
|
|
117
150
|
@muted_memory_names = self.class.name_list(muted_memory_names)
|
|
151
|
+
self.prompt_notes = prompt_notes
|
|
118
152
|
@project_root = project_root
|
|
119
153
|
@parent_id = parent_id&.to_s
|
|
120
154
|
@scratch = !!scratch
|
|
121
155
|
@last_turn = last_turn
|
|
122
156
|
@delegate = !!delegate
|
|
157
|
+
self.llm_context = llm_context
|
|
123
158
|
@archived = false
|
|
124
159
|
end
|
|
125
160
|
|
|
@@ -170,7 +205,7 @@ module Samagotchi
|
|
|
170
205
|
# seed); [] by default
|
|
171
206
|
def self.new_session(mode:, model_name:, working_directory:, test_run: nil,
|
|
172
207
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, messages: [],
|
|
173
|
-
scratch: false, model_typed: nil, delegate: false)
|
|
208
|
+
scratch: false, model_typed: nil, delegate: false, llm_context: nil)
|
|
174
209
|
now = Time.now.iso8601(3)
|
|
175
210
|
resolved_test = if test_run.nil?
|
|
176
211
|
test_session_env?
|
|
@@ -194,7 +229,8 @@ module Samagotchi
|
|
|
194
229
|
muted_memory_names: muted_memory_names,
|
|
195
230
|
parent_id: parent_id,
|
|
196
231
|
scratch: scratch,
|
|
197
|
-
delegate: delegate
|
|
232
|
+
delegate: delegate,
|
|
233
|
+
llm_context: llm_context
|
|
198
234
|
)
|
|
199
235
|
end
|
|
200
236
|
|
|
@@ -246,10 +282,12 @@ module Samagotchi
|
|
|
246
282
|
"project_root" => nil,
|
|
247
283
|
"preloaded_memory_names" => [],
|
|
248
284
|
"muted_memory_names" => [],
|
|
285
|
+
"prompt_notes" => [],
|
|
249
286
|
"parent_id" => nil,
|
|
250
287
|
"scratch" => false,
|
|
251
288
|
"last_turn" => nil,
|
|
252
|
-
"delegate" => false
|
|
289
|
+
"delegate" => false,
|
|
290
|
+
"llm_context" => nil
|
|
253
291
|
}.freeze
|
|
254
292
|
|
|
255
293
|
# A session from a parsed session file. +messages+ false leaves the
|
|
@@ -368,6 +406,8 @@ module Samagotchi
|
|
|
368
406
|
when "pending_question" then @pending_question && scrub_utf8(stringify_message_keys(@pending_question))
|
|
369
407
|
when "test_run" then !!@test_run
|
|
370
408
|
when "delegate" then @delegate
|
|
409
|
+
when "llm_context" then llm_context_file
|
|
410
|
+
when "prompt_notes" then @prompt_notes.map(&:to_file)
|
|
371
411
|
when "used_memory_names", "preloaded_memory_names", "muted_memory_names"
|
|
372
412
|
Array(instance_variable_get(:"@#{key}"))
|
|
373
413
|
else instance_variable_get(:"@#{key}")
|
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
require_relative "config"
|
|
4
4
|
require_relative "model_profile"
|
|
5
5
|
require_relative "served_model"
|
|
6
|
+
require_relative "prompt_note"
|
|
6
7
|
require_relative "turn_flow"
|
|
7
8
|
require_relative "tools/execute"
|
|
8
9
|
require_relative "tools/builtins"
|
|
9
10
|
require_relative "tools/context_read"
|
|
10
11
|
require_relative "tools/peers"
|
|
11
12
|
require_relative "commands/registry"
|
|
13
|
+
require_relative "llm_context_command"
|
|
12
14
|
|
|
13
15
|
module Samagotchi
|
|
14
16
|
# The session commands a REPL and a session worker both run: /model,
|
|
15
|
-
# /models, /guardrails, /context, !rollback, !cmd and the answer to a continue offer. They act on
|
|
17
|
+
# /models, /llm-context, /guardrails, /context, !rollback, !cmd and the answer to a continue offer. They act on
|
|
16
18
|
# the Engine and its TurnFlow; the host prints the result's output and
|
|
17
19
|
# runs a continue turn when asked to (#run never runs a turn).
|
|
18
20
|
#
|
|
@@ -26,6 +28,7 @@ module Samagotchi
|
|
|
26
28
|
GUARDRAILS_COMMAND = "/guardrails"
|
|
27
29
|
CONTEXT_COMMAND = "/context"
|
|
28
30
|
HELP_COMMAND = "/help"
|
|
31
|
+
LLM_CONTEXT_COMMAND = LLMContextCommand::NAME
|
|
29
32
|
# A remote catalog has hundreds of ids (OpenRouter ~380): plain /models
|
|
30
33
|
# shows this many per host; /models <text> lists every match.
|
|
31
34
|
MODELS_PER_HOST = 20
|
|
@@ -73,6 +76,9 @@ module Samagotchi
|
|
|
73
76
|
end
|
|
74
77
|
registry.register(MODEL_COMMAND, "show or switch the model",
|
|
75
78
|
match: ->(text) { text.match?(%r{\A/model(?:\s+.*)?\z}) }) { |text| model(text) }
|
|
79
|
+
# Not anytime: it saves the session; it takes effect at the next turn's start.
|
|
80
|
+
registry.register(LLM_CONTEXT_COMMAND, "show or set this session's LLM context strategy, apply rule and budget",
|
|
81
|
+
id: :llm_context, match: ->(text) { text.match?(%r{\A/llm-context(?:\s+.*)?\z}) }) { |text| llm_context(text) }
|
|
76
82
|
registry.register(HELP_COMMAND, "list the commands, the bundles' too", anytime: true) { |_text| reply(help_listing) }
|
|
77
83
|
registry.register("/stats", "show the session's stats", local: true)
|
|
78
84
|
registry.register("/recap", "show the session's recap", local: true)
|
|
@@ -105,6 +111,14 @@ module Samagotchi
|
|
|
105
111
|
@builtin_registry ||= register_builtins(Commands::Registry.new).freeze
|
|
106
112
|
end
|
|
107
113
|
|
|
114
|
+
# The name of this chi's own session command +line+ is (one a worker
|
|
115
|
+
# runs), or nil. A worker that answers such a line unknown_command runs
|
|
116
|
+
# an older chi that doesn't have it yet (/llm-context against 0.39.0):
|
|
117
|
+
# the UIs say so (BridgeClient.stale_worker_message) instead of "not a
|
|
118
|
+
# session command".
|
|
119
|
+
# @return [String, nil]
|
|
120
|
+
def self.builtin_name(line) = builtin_registry.lookup(line)&.name
|
|
121
|
+
|
|
108
122
|
# @return [String] the model /model clear goes back to
|
|
109
123
|
attr_reader :default_model
|
|
110
124
|
|
|
@@ -330,6 +344,21 @@ module Samagotchi
|
|
|
330
344
|
@save.call(session)
|
|
331
345
|
end
|
|
332
346
|
|
|
347
|
+
# /llm-context (LLMContextCommand); :llm_context in changed when it set
|
|
348
|
+
# the session's values.
|
|
349
|
+
def llm_context(text)
|
|
350
|
+
output, changed = LLMContextCommand.new(engine: @engine, save: ->(session) { save_llm_context(session) })
|
|
351
|
+
.run(text.delete_prefix(LLM_CONTEXT_COMMAND))
|
|
352
|
+
reply(output, changed: changed ? [:llm_context] : [])
|
|
353
|
+
rescue ArgumentError => e
|
|
354
|
+
reply(e.message, status: :error)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def save_llm_context(session)
|
|
358
|
+
@engine.store_model!(session)
|
|
359
|
+
@save.call(session)
|
|
360
|
+
end
|
|
361
|
+
|
|
333
362
|
def model(text)
|
|
334
363
|
output, switched = model_command(text)
|
|
335
364
|
reply(output, changed: switched ? [:model] : [])
|
|
@@ -342,9 +371,9 @@ module Samagotchi
|
|
|
342
371
|
def model_command(input)
|
|
343
372
|
suffix = input.delete_prefix(MODEL_COMMAND).strip
|
|
344
373
|
if suffix.empty?
|
|
345
|
-
return ["runtime model: #{model_name}#{model_note}#{served_note}#{sampling_note}#{thinking_note}", false] if model_name == @engine.model_ref_for(@default_model)
|
|
374
|
+
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)
|
|
346
375
|
|
|
347
|
-
return ["runtime model: #{model_name}#{model_note("default: #{@default_model}")}#{served_note}#{sampling_note}#{thinking_note}", false]
|
|
376
|
+
return ["runtime model: #{model_name}#{model_note("default: #{@default_model}")}#{served_note}#{sampling_note}#{thinking_note}#{notes_note}", false]
|
|
348
377
|
end
|
|
349
378
|
|
|
350
379
|
# Parse flags: --default and --alias <name> / --alias=<name> (tolerant order)
|
|
@@ -414,14 +443,14 @@ module Samagotchi
|
|
|
414
443
|
return ["--default cannot be combined with clear/default/none/off", false] if persist_default
|
|
415
444
|
|
|
416
445
|
switch_model(@default_model)
|
|
417
|
-
return ["runtime model reset to #{model_name}#{model_note}", true]
|
|
446
|
+
return ["runtime model reset to #{model_name}#{model_note}#{notes_note}", true]
|
|
418
447
|
end
|
|
419
448
|
|
|
420
449
|
switch_model(arg, persist_default: persist_default)
|
|
421
450
|
if persist_default
|
|
422
|
-
["runtime model set to #{model_name}#{model_note} and default updated", true]
|
|
451
|
+
["runtime model set to #{model_name}#{model_note} and default updated#{notes_note}", true]
|
|
423
452
|
else
|
|
424
|
-
["runtime model set to #{model_name}#{model_note}", true]
|
|
453
|
+
["runtime model set to #{model_name}#{model_note}#{notes_note}", true]
|
|
425
454
|
end
|
|
426
455
|
end
|
|
427
456
|
end
|
|
@@ -462,6 +491,16 @@ module Samagotchi
|
|
|
462
491
|
""
|
|
463
492
|
end
|
|
464
493
|
|
|
494
|
+
# "; notes: model_notes_deepseek (system, 612 chars)": the model notes
|
|
495
|
+
# the session's prompt carries (after a switch, the ones the new
|
|
496
|
+
# model's prompt loads); "" without any.
|
|
497
|
+
def notes_note
|
|
498
|
+
text = PromptNote.text(@engine.prompt_notes)
|
|
499
|
+
text.empty? ? "" : "; notes: #{text}"
|
|
500
|
+
rescue StandardError
|
|
501
|
+
""
|
|
502
|
+
end
|
|
503
|
+
|
|
465
504
|
# "; thinking: off (models: qwen)" when the model has a level set.
|
|
466
505
|
def thinking_note
|
|
467
506
|
summary = @engine.thinking_summary
|
|
@@ -129,11 +129,13 @@ module Samagotchi
|
|
|
129
129
|
# @param images_from [String, nil] the session dir the seed's images are in
|
|
130
130
|
# @param title [String, nil] what the lists show before the first turn
|
|
131
131
|
# (the prompt's preview by default, else the seed's first user message)
|
|
132
|
+
# @param llm_context [LLMContextOverride, nil] the session's own
|
|
133
|
+
# llm_context values (chi --llm-context; a fork copies its parent's)
|
|
132
134
|
# @return [Session] with #seed_images_dropped: refs whose file was gone,
|
|
133
135
|
# and #model_warning: an id its host's saved list doesn't have
|
|
134
136
|
def self.spawn_session(prompt:, mode: "assist", working_directory: nil, model_name: nil, state_dir: nil,
|
|
135
137
|
memories: [], muted_memories: [], parent_id: nil, messages: [], images_from: nil,
|
|
136
|
-
title: nil, delegate: false)
|
|
138
|
+
title: nil, delegate: false, llm_context: nil)
|
|
137
139
|
sd = state_dir || Session.default_state_dir
|
|
138
140
|
# The resolved ref is stored (a resumed session keeps its model when an
|
|
139
141
|
# alias is retargeted), with the name as typed beside it. A wrong host
|
|
@@ -153,7 +155,8 @@ module Samagotchi
|
|
|
153
155
|
muted_memory_names: muted_memories,
|
|
154
156
|
parent_id: parent_id,
|
|
155
157
|
messages: messages,
|
|
156
|
-
delegate: delegate
|
|
158
|
+
delegate: delegate,
|
|
159
|
+
llm_context: llm_context
|
|
157
160
|
)
|
|
158
161
|
session.model_warning = model_warning
|
|
159
162
|
# With no prompt there is no first turn to run (an attaching UI sends
|
|
@@ -37,6 +37,8 @@ module Samagotchi
|
|
|
37
37
|
# The most turns (with their tool calls) a live snapshot carries: the
|
|
38
38
|
# unsaved ones of a collector whose saves keep failing stay bounded.
|
|
39
39
|
LIVE_RECORDS_CAP = 20
|
|
40
|
+
# The most characters of a failed turn's error message its record keeps.
|
|
41
|
+
FAILURE_MESSAGE_CHARS = 2000
|
|
40
42
|
|
|
41
43
|
# A generation shorter than this gets no decode speed of its own: one
|
|
42
44
|
# tiny generation would skew the session's average.
|
|
@@ -53,8 +55,14 @@ module Samagotchi
|
|
|
53
55
|
# had none) and the session's tokens block as the snapshot has it.
|
|
54
56
|
GenerationReport = Data.define(:speed, :tokens)
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
# memory_index: the memory indexes the session's prompt held
|
|
59
|
+
# ({ system: { tokens:, lines: }, project: … }, string keys as saved),
|
|
60
|
+
# nil when an older file has none.
|
|
61
|
+
SavedSummary = Data.define(:ctx_pct, :prompt_sum, :completion_sum, :cached_sum, :reasoning_sum, :cost_sum,
|
|
62
|
+
:memory_index) do
|
|
63
|
+
def initialize(memory_index: nil, **) = super
|
|
64
|
+
|
|
65
|
+
def tokens = to_h.except(:ctx_pct, :memory_index)
|
|
58
66
|
end
|
|
59
67
|
|
|
60
68
|
# Per-turn transient state, reset on each turn_started.
|
|
@@ -150,7 +158,8 @@ module Samagotchi
|
|
|
150
158
|
count = ->(key) { tokens[key].is_a?(Numeric) ? tokens[key] : 0 }
|
|
151
159
|
SavedSummary.new(ctx_pct: saved_pct(data["context"]), prompt_sum: count.call("prompt_sum"),
|
|
152
160
|
completion_sum: count.call("completion_sum"), cached_sum: count.call("cached_sum"),
|
|
153
|
-
reasoning_sum: count.call("reasoning_sum"), cost_sum: count.call("cost_sum")
|
|
161
|
+
reasoning_sum: count.call("reasoning_sum"), cost_sum: count.call("cost_sum"),
|
|
162
|
+
memory_index: data["memory_index"].is_a?(Hash) ? data["memory_index"] : nil)
|
|
154
163
|
rescue JSON::ParserError, SystemCallError, TypeError
|
|
155
164
|
nil
|
|
156
165
|
end
|
|
@@ -196,6 +205,7 @@ module Samagotchi
|
|
|
196
205
|
@wall_clock = wall_clock || -> { Time.now }
|
|
197
206
|
@session_id = nil
|
|
198
207
|
@state_dir = nil
|
|
208
|
+
@memory_index = nil
|
|
199
209
|
@started_at = nil
|
|
200
210
|
@last_activity_at = nil
|
|
201
211
|
@turn = nil
|
|
@@ -217,6 +227,14 @@ module Samagotchi
|
|
|
217
227
|
end
|
|
218
228
|
end
|
|
219
229
|
|
|
230
|
+
# The memory indexes the session's prompt holds (SystemPrompt#memory_index:
|
|
231
|
+
# { system: { tokens:, lines: }, project: … }), set by the Engine when
|
|
232
|
+
# it builds the prompt it sends; the snapshot and analytics.json carry it.
|
|
233
|
+
# @param block [Hash, nil]
|
|
234
|
+
def memory_index=(block)
|
|
235
|
+
@mutex.synchronize { @memory_index = block if block }
|
|
236
|
+
end
|
|
237
|
+
|
|
220
238
|
# The sessions dir analytics.json is read from and saved to (nil: the
|
|
221
239
|
# XDG default). Set it before #session_id=.
|
|
222
240
|
attr_writer :state_dir
|
|
@@ -292,7 +310,7 @@ module Samagotchi
|
|
|
292
310
|
when :turn_canceled
|
|
293
311
|
end_turn(status: "canceled", reason: event[:cancellation_reason], by: event[:cancelled_by])
|
|
294
312
|
when :turn_failed
|
|
295
|
-
end_turn(status: "failed")
|
|
313
|
+
end_turn(status: "failed", failure: failure_fields(event))
|
|
296
314
|
end
|
|
297
315
|
|
|
298
316
|
@mutex.synchronize { @last_activity_at = now.iso8601(3) }
|
|
@@ -367,6 +385,7 @@ module Samagotchi
|
|
|
367
385
|
cancellations: totals[:cancellations],
|
|
368
386
|
tokens: tokens_block(totals, turn),
|
|
369
387
|
context: context_block(@turn_records),
|
|
388
|
+
memory_index: @memory_index,
|
|
370
389
|
profile: @profile,
|
|
371
390
|
profile_source: @profile_source,
|
|
372
391
|
served_model: @served_model,
|
|
@@ -480,9 +499,10 @@ module Samagotchi
|
|
|
480
499
|
end
|
|
481
500
|
|
|
482
501
|
# A canceled turn's record keeps why (+reason+: "user", "ctrl_c", ...)
|
|
483
|
-
# and who stopped it (+by+: a hook's bundle),
|
|
484
|
-
#
|
|
485
|
-
|
|
502
|
+
# and who stopped it (+by+: a hook's bundle), a failed one what its line
|
|
503
|
+
# says (+failure+, #failure_fields), so a reloaded web history can show
|
|
504
|
+
# the cancel or failure line the live one did.
|
|
505
|
+
def end_turn(status: "completed", reason: nil, by: nil, failure: nil)
|
|
486
506
|
@mutex.synchronize do
|
|
487
507
|
if @turn
|
|
488
508
|
# A turn that failed mid-stream never saw its generation end:
|
|
@@ -505,6 +525,7 @@ module Samagotchi
|
|
|
505
525
|
}
|
|
506
526
|
record[:cancellation_reason] = reason.to_s unless reason.nil? || reason.to_s.empty?
|
|
507
527
|
record[:cancelled_by] = by.to_s unless by.nil? || by.to_s.empty?
|
|
528
|
+
record[:failure] = failure if failure
|
|
508
529
|
record.merge!(turn_token_fields(@turn))
|
|
509
530
|
@turn_records << record
|
|
510
531
|
count_turn(record)
|
|
@@ -513,6 +534,19 @@ module Samagotchi
|
|
|
513
534
|
end
|
|
514
535
|
end
|
|
515
536
|
|
|
537
|
+
# What turn_failed's line is made of (format.js failedTurnText): a
|
|
538
|
+
# provider error's summary, else the message (capped: a server's error
|
|
539
|
+
# page can be long) and class, and the steps that stayed (kept_steps).
|
|
540
|
+
def failure_fields(event)
|
|
541
|
+
summary = event[:summary].to_s
|
|
542
|
+
return { summary: summary, kept_steps: event[:kept_steps] }.compact unless summary.empty?
|
|
543
|
+
|
|
544
|
+
message = event[:message].to_s
|
|
545
|
+
message = "#{message[0, FAILURE_MESSAGE_CHARS - 1]}…" if message.length > FAILURE_MESSAGE_CHARS
|
|
546
|
+
{ message: message.empty? ? nil : message, error_class: event[:error_class]&.to_s,
|
|
547
|
+
kept_steps: event[:kept_steps] }.compact
|
|
548
|
+
end
|
|
549
|
+
|
|
516
550
|
# The model the server says answered, and the name asked for then (a
|
|
517
551
|
# /model switch makes it stale until the next generation reports).
|
|
518
552
|
def record_served_model(event)
|
|
@@ -875,6 +909,8 @@ module Samagotchi
|
|
|
875
909
|
@served_model = prior["served_model"]
|
|
876
910
|
@served_model_for = prior["served_model_for"]
|
|
877
911
|
end
|
|
912
|
+
# The indexes the prompt last held, until this process builds its own.
|
|
913
|
+
@memory_index ||= symbolized(prior["memory_index"]) if prior["memory_index"].is_a?(Hash)
|
|
878
914
|
# The window last seen, until this process's first generation reports.
|
|
879
915
|
window = prior["context"].is_a?(Hash) ? prior["context"] : {}
|
|
880
916
|
if window["window_tokens"] && @context_window_tokens.nil?
|
|
@@ -885,6 +921,10 @@ module Samagotchi
|
|
|
885
921
|
nil
|
|
886
922
|
end
|
|
887
923
|
|
|
924
|
+
def symbolized(hash)
|
|
925
|
+
hash.to_h { |key, value| [key.to_sym, value.is_a?(Hash) ? symbolized(value) : value] }
|
|
926
|
+
end
|
|
927
|
+
|
|
888
928
|
def loaded_records(records)
|
|
889
929
|
Array(records).filter_map { |record| record.transform_keys(&:to_sym) if record.is_a?(Hash) && record["id"] }
|
|
890
930
|
end
|