samagotchi 0.38.0 → 0.40.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 +64 -1
- data/docs/cli.md +2 -1
- data/docs/configuration.md +58 -2
- data/docs/hooks.md +1 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/context-telemetry.md +16 -0
- data/docs/internals/llm-context-bench.md +133 -0
- data/docs/internals/llm-context-forget.md +45 -0
- data/docs/internals/prompt-caching.md +3 -2
- data/docs/plugins.md +4 -3
- data/docs/testing.md +8 -0
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +1 -1
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -10
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +4 -0
- data/lib/samagotchi/config.rb +43 -4
- data/lib/samagotchi/context_note.rb +7 -3
- data/lib/samagotchi/context_status.rb +112 -11
- data/lib/samagotchi/engine.rb +36 -9
- data/lib/samagotchi/guardrails/targets.rb +15 -3
- data/lib/samagotchi/host_registry.rb +8 -3
- data/lib/samagotchi/kernel_loop.rb +142 -14
- data/lib/samagotchi/llm/chat_loop.rb +57 -18
- data/lib/samagotchi/llm/turn_settings.rb +5 -4
- data/lib/samagotchi/llm_context_apply.rb +165 -0
- data/lib/samagotchi/llm_context_edit.rb +102 -0
- data/lib/samagotchi/llm_context_forget.rb +258 -0
- data/lib/samagotchi/llm_context_stale.rb +271 -0
- data/lib/samagotchi/llm_context_strategy.rb +177 -0
- data/lib/samagotchi/llm_context_view.rb +201 -0
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/prompt.rb +4 -11
- data/lib/samagotchi/session_manager.rb +9 -1
- data/lib/samagotchi/session_metrics.rb +18 -1
- data/lib/samagotchi/system_prompt.rb +20 -15
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -2
- data/lib/samagotchi/token_usage.rb +3 -0
- data/lib/samagotchi/tool_activity.rb +18 -0
- data/lib/samagotchi/tool_declarations.rb +60 -4
- data/lib/samagotchi/tool_ids.rb +84 -0
- data/lib/samagotchi/tool_response.rb +67 -6
- data/lib/samagotchi/tools/builtin_calls.rb +3 -2
- data/lib/samagotchi/tools/builtins.rb +15 -5
- data/lib/samagotchi/tools/context_read.rb +27 -10
- data/lib/samagotchi/tools/forget_outputs.rb +62 -0
- data/lib/samagotchi/tools/registry.rb +25 -8
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +1 -1
- data/lib/samagotchi/worker.rb +4 -0
- metadata +11 -1
data/lib/samagotchi/config.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "config_text_edit"
|
|
|
9
9
|
require_relative "log"
|
|
10
10
|
require_relative "model_ref"
|
|
11
11
|
require_relative "paths"
|
|
12
|
+
require_relative "llm_context_strategy"
|
|
12
13
|
|
|
13
14
|
module Samagotchi
|
|
14
15
|
# Parses the thinking: levels of host and model entries (it needs Config).
|
|
@@ -154,6 +155,24 @@ module Samagotchi
|
|
|
154
155
|
# 10 minutes, within session.max_wakes); false: it waits as a note for the next turn.
|
|
155
156
|
Entry.new(key: "context.wake", yaml_path: %w[context wake], type: :bool, default: true, expose: %i[env config cli]),
|
|
156
157
|
|
|
158
|
+
# What the model is sent of the conversation (LLMContextStrategy, LLMContextView): none, or a list of layers
|
|
159
|
+
# (stale, forget; "|"-separated or a YAML list); forget (the model's forget_outputs) is experimental.
|
|
160
|
+
# models.<key>.llm_context_strategy and hosts.<name>.llm_context_strategy come first.
|
|
161
|
+
Entry.new(key: "llm_context.strategy", yaml_path: %w[llm_context strategy], type: :string, default: "none", expose: %i[env config]),
|
|
162
|
+
# When a layer's edits reach the prompt (LLMContextApply): payoff, next_request or turn_end;
|
|
163
|
+
# models.<key>.llm_context_apply and hosts.<name>.llm_context_apply come first.
|
|
164
|
+
Entry.new(key: "llm_context.apply", yaml_path: %w[llm_context apply], type: :string, default: "payoff", expose: %i[env config]),
|
|
165
|
+
# Whether an edit or write of a file makes its earlier reads stale too (opt-in, experimental).
|
|
166
|
+
Entry.new(key: "llm_context.stale_edits", yaml_path: %w[llm_context stale_edits], type: :bool, default: false, expose: %i[env config]),
|
|
167
|
+
# The steps (tool batches) back whose edited files' reads stale never stubs.
|
|
168
|
+
Entry.new(key: "llm_context.protect_steps", yaml_path: %w[llm_context protect_steps], type: :integer, default: 3, expose: %i[env config]),
|
|
169
|
+
# A soft context budget in tokens: the context status bands (and the forget layer's offers) count against it
|
|
170
|
+
# instead of the window; unset or 0: off. models.<key>.llm_context_budget_tokens and
|
|
171
|
+
# hosts.<name>.llm_context_budget_tokens come first.
|
|
172
|
+
Entry.new(key: "llm_context.budget_tokens", yaml_path: %w[llm_context budget_tokens], type: :integer, default: nil, expose: %i[env config]),
|
|
173
|
+
# The policy sentence forget_outputs' description carries (the forget layer); blank: none.
|
|
174
|
+
Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default: LLMContextStrategy::DEFAULT_POLICY, expose: %i[env config]),
|
|
175
|
+
|
|
157
176
|
Entry.new(key: "thinking.turn_preamble", yaml_path: %w[thinking turn_preamble], type: :bool, default: true, expose: %i[env config cli]),
|
|
158
177
|
# How much models think: off|low|medium|high|default, parsed by Thinking
|
|
159
178
|
# (a :string, since YAML reads an unquoted off as false). The CLI flag
|
|
@@ -241,8 +260,9 @@ module Samagotchi
|
|
|
241
260
|
# (ConfigFile.hosts_config, ConfigFile.model_settings).
|
|
242
261
|
MAP_ENTRY_KEYS = {
|
|
243
262
|
"hosts" => %w[host port url transport api api_key_env profile first_token_timeout vision sampling thinking enabled
|
|
244
|
-
remote window_tokens].freeze,
|
|
245
|
-
"models" => %w[profile vision sampling thinking window_tokens
|
|
263
|
+
remote window_tokens llm_context_strategy llm_context_apply llm_context_budget_tokens].freeze,
|
|
264
|
+
"models" => %w[profile vision sampling thinking window_tokens llm_context_strategy llm_context_apply
|
|
265
|
+
llm_context_budget_tokens].freeze
|
|
246
266
|
}.freeze
|
|
247
267
|
# Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
|
|
248
268
|
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze }.freeze
|
|
@@ -826,6 +846,12 @@ module Samagotchi
|
|
|
826
846
|
"hosts entry '#{name}'")
|
|
827
847
|
sampling = ConfigFile.sampling_map(raw_cfg.key?("sampling") ? raw_cfg["sampling"] : raw_cfg[:sampling], "hosts entry '#{name}'")
|
|
828
848
|
thinking = Thinking.level(raw_cfg.key?("thinking") ? raw_cfg["thinking"] : raw_cfg[:thinking], "hosts entry '#{name}'")
|
|
849
|
+
llm_context = LLMContextStrategy.parse(raw_cfg.key?(LLMContextStrategy::KEY) ? raw_cfg[LLMContextStrategy::KEY] : raw_cfg[:llm_context_strategy],
|
|
850
|
+
"hosts entry '#{name}'")
|
|
851
|
+
llm_context_apply = LLMContextStrategy.parse_apply(raw_cfg.key?(LLMContextStrategy::APPLY_KEY) ? raw_cfg[LLMContextStrategy::APPLY_KEY] : raw_cfg[:llm_context_apply],
|
|
852
|
+
"hosts entry '#{name}'")
|
|
853
|
+
llm_context_budget = LLMContextStrategy.parse_budget(raw_cfg.key?(LLMContextStrategy::BUDGET_KEY) ? raw_cfg[LLMContextStrategy::BUDGET_KEY] : raw_cfg[:llm_context_budget_tokens],
|
|
854
|
+
"hosts entry '#{name}'")
|
|
829
855
|
unless first_token_timeout.nil? || (first_token_timeout.is_a?(Numeric) && !first_token_timeout.negative?)
|
|
830
856
|
warn_once "Warning: hosts entry '#{name}': first_token_timeout must be seconds (0 = off); using the default"
|
|
831
857
|
first_token_timeout = nil
|
|
@@ -890,7 +916,8 @@ module Samagotchi
|
|
|
890
916
|
url: url.empty? ? nil : url, api_key_env: api_key_env.empty? ? nil : api_key_env,
|
|
891
917
|
profile: profile.empty? ? nil : profile, first_token_timeout: first_token_timeout,
|
|
892
918
|
vision: vision, sampling: sampling, thinking: thinking, remote: remote,
|
|
893
|
-
window_tokens: window
|
|
919
|
+
window_tokens: window, llm_context_strategy: llm_context,
|
|
920
|
+
llm_context_apply: llm_context_apply, llm_context_budget_tokens: llm_context_budget }
|
|
894
921
|
end
|
|
895
922
|
end
|
|
896
923
|
|
|
@@ -1037,7 +1064,10 @@ module Samagotchi
|
|
|
1037
1064
|
location.merge("transport" => v[:transport]&.to_s, "api" => v[:api]&.to_s, "api_key_env" => v[:api_key_env],
|
|
1038
1065
|
"profile" => v[:profile], "first_token_timeout" => v[:first_token_timeout],
|
|
1039
1066
|
"vision" => v[:vision], "sampling" => v[:sampling], "thinking" => v[:thinking]&.to_s,
|
|
1040
|
-
"remote" => v[:remote], "window_tokens" => v[:window_tokens]
|
|
1067
|
+
"remote" => v[:remote], "window_tokens" => v[:window_tokens],
|
|
1068
|
+
"llm_context_strategy" => v[:llm_context_strategy]&.map(&:to_s),
|
|
1069
|
+
"llm_context_apply" => v[:llm_context_apply]&.to_s,
|
|
1070
|
+
"llm_context_budget_tokens" => v[:llm_context_budget_tokens]).compact
|
|
1041
1071
|
end
|
|
1042
1072
|
# Disabled hosts travel as just that, so a worker refuses "box:x"
|
|
1043
1073
|
# the way its parent does instead of sending it to the default host.
|
|
@@ -1129,6 +1159,15 @@ module Samagotchi
|
|
|
1129
1159
|
result[key][:thinking] = thinking if thinking
|
|
1130
1160
|
window = window_tokens(v.key?("window_tokens") ? v["window_tokens"] : v[:window_tokens], "models: #{key}")
|
|
1131
1161
|
result[key][:window_tokens] = window if window
|
|
1162
|
+
layers = LLMContextStrategy.parse(v.key?(LLMContextStrategy::KEY) ? v[LLMContextStrategy::KEY] : v[:llm_context_strategy],
|
|
1163
|
+
"models: #{key}")
|
|
1164
|
+
result[key][:llm_context_strategy] = layers if layers
|
|
1165
|
+
apply = LLMContextStrategy.parse_apply(v.key?(LLMContextStrategy::APPLY_KEY) ? v[LLMContextStrategy::APPLY_KEY] : v[:llm_context_apply],
|
|
1166
|
+
"models: #{key}")
|
|
1167
|
+
result[key][:llm_context_apply] = apply if apply
|
|
1168
|
+
budget = LLMContextStrategy.parse_budget(v.key?(LLMContextStrategy::BUDGET_KEY) ? v[LLMContextStrategy::BUDGET_KEY] : v[:llm_context_budget_tokens],
|
|
1169
|
+
"models: #{key}")
|
|
1170
|
+
result[key][:llm_context_budget_tokens] = budget if budget
|
|
1132
1171
|
end
|
|
1133
1172
|
rescue StandardError
|
|
1134
1173
|
{}
|
|
@@ -64,9 +64,13 @@ module Samagotchi
|
|
|
64
64
|
# (a note that arrived before the first turn, a turn note left by a
|
|
65
65
|
# failed first turn) stays and the prompt goes before it.
|
|
66
66
|
def with_system_head(messages, system_message)
|
|
67
|
-
first
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
[system_message] + (system_head?(messages.first) ? messages.drop(1) : messages)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A system prompt at a conversation's head: a system message of no kind
|
|
71
|
+
# (#with_system_head replaces it).
|
|
72
|
+
def system_head?(message)
|
|
73
|
+
message.is_a?(Hash) && fetch(message, :role).to_s == "system" && fetch(message, :kind).to_s.empty?
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
def clock(created_at)
|
|
@@ -19,6 +19,26 @@ module Samagotchi
|
|
|
19
19
|
#
|
|
20
20
|
# The context.* settings are read once, when the tracker is built (one per
|
|
21
21
|
# run, so a changed setting counts from the next turn).
|
|
22
|
+
#
|
|
23
|
+
# The turn's LLM context strategy (LLMContextStrategy::Resolved) may set
|
|
24
|
+
# a soft budget (llm_context.budget_tokens): the buckets then count
|
|
25
|
+
# against it instead of the window (the smaller of the two).
|
|
26
|
+
#
|
|
27
|
+
# Under the forget layer the model's lines offer forget_outputs in
|
|
28
|
+
# tiers, never "forget now" (the spike: told to free context, models
|
|
29
|
+
# forget nearly everything), each with a "~N/M tokens" readout:
|
|
30
|
+
# - the first bucket with guidance, below the top two: the readout alone
|
|
31
|
+
# (CLM: how-to at low pressure triggers wholesale deletion);
|
|
32
|
+
# - the one under the top: finish the unit of work in flight, then tidy
|
|
33
|
+
# once;
|
|
34
|
+
# - the top, or over the budget: compact settled outputs now, keep what
|
|
35
|
+
# will still be edited against, don't wipe.
|
|
36
|
+
# When: at a turn's first request (the turn before it answered, its
|
|
37
|
+
# outputs are settled), on a rise as ever, and again when the last line
|
|
38
|
+
# was the readout alone (a mid-turn rise) or the context is over the
|
|
39
|
+
# budget (every turn then). Mid-turn only the top tier offers; a lower
|
|
40
|
+
# rise gets the readout alone (the layout check: pushed mid-task, models
|
|
41
|
+
# forget at the base rate).
|
|
22
42
|
class ContextStatus
|
|
23
43
|
STATUS_PREFIX = "CONTEXT_STATUS"
|
|
24
44
|
# The model's own line about its context (a tail system message, kind
|
|
@@ -62,19 +82,35 @@ module Samagotchi
|
|
|
62
82
|
# The bucket of the last status line +conversation+ holds: the model's
|
|
63
83
|
# own line, or a legacy session's injected telemetry.
|
|
64
84
|
def self.last_bucket(conversation)
|
|
85
|
+
content = last_line(conversation)
|
|
86
|
+
match = content&.match(/\bbucket=([a-z0-9_]+)/)
|
|
87
|
+
match && match[1]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The text of the last status line +conversation+ holds, nil for none.
|
|
91
|
+
def self.last_line(conversation)
|
|
65
92
|
message = conversation.reverse.find do |entry|
|
|
66
93
|
content = entry[:content].to_s
|
|
67
94
|
entry[:role] == "system" && content.start_with?(STATUS_PREFIX, LINE_PREFIX)
|
|
68
95
|
end
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
match = message[:content].match(/\bbucket=([a-z0-9_]+)/)
|
|
72
|
-
match && match[1]
|
|
96
|
+
message && message[:content].to_s
|
|
73
97
|
end
|
|
74
98
|
|
|
99
|
+
# What a forget-layer line names when it offers the tool.
|
|
100
|
+
OFFER_TOOL = "forget_outputs"
|
|
101
|
+
# The forget layer's tiers' words (#forget_line).
|
|
102
|
+
TIDY = "Finish the unit of work in flight, then tidy once with #{OFFER_TOOL}: settled outputs, with a note " \
|
|
103
|
+
"that carries what they established."
|
|
104
|
+
COMPACT = "Compact settled outputs now with #{OFFER_TOOL}: keep what you'll still edit against; don't wipe."
|
|
105
|
+
|
|
75
106
|
# @param conversation [Array<Hash>] the turn's conversation so far (its
|
|
76
107
|
# last status line's bucket is where rises count from)
|
|
77
|
-
|
|
108
|
+
# @param llm_context [LLMContextStrategy::Resolved, nil] the turn's
|
|
109
|
+
# strategy (its budget)
|
|
110
|
+
def initialize(conversation: [], llm_context: nil)
|
|
111
|
+
@budget = llm_context&.budget_tokens
|
|
112
|
+
@forget = llm_context&.forget? || false
|
|
113
|
+
@offered = self.class.last_line(conversation).to_s.include?(OFFER_TOOL)
|
|
78
114
|
@enabled = self.class.enabled?
|
|
79
115
|
chars_per_token = Config.get("context.chars_per_token").to_f
|
|
80
116
|
@chars_per_token = chars_per_token.positive? ? chars_per_token : DEFAULT_CHARS_PER_TOKEN
|
|
@@ -85,6 +121,7 @@ module Samagotchi
|
|
|
85
121
|
@last_bucket = self.class.last_bucket(conversation)
|
|
86
122
|
@server_usage = nil
|
|
87
123
|
@counted = nil
|
|
124
|
+
@edited = false
|
|
88
125
|
@display = nil
|
|
89
126
|
@guidance = nil
|
|
90
127
|
end
|
|
@@ -95,6 +132,12 @@ module Samagotchi
|
|
|
95
132
|
|
|
96
133
|
def enabled? = @enabled
|
|
97
134
|
|
|
135
|
+
# The status line's value is in the top configured bucket (the apply
|
|
136
|
+
# rule's payoff applies staged edits there at once).
|
|
137
|
+
def top_bucket?
|
|
138
|
+
!@display.nil? && bucket_rank(@display[:bucket]) == @thresholds.size
|
|
139
|
+
end
|
|
140
|
+
|
|
98
141
|
# Before a request: estimate the usage of a prompt of +prompt_chars+
|
|
99
142
|
# against +window+ (this request's ContextWindow::Resolved) and update
|
|
100
143
|
# the status line's value. A rise into a bucket that asks the model for
|
|
@@ -114,12 +157,25 @@ module Samagotchi
|
|
|
114
157
|
emit = emit?(bucket: bucket, iteration_index: iteration_index)
|
|
115
158
|
previous_bucket = @last_bucket
|
|
116
159
|
@last_bucket = bucket
|
|
160
|
+
line = if @forget
|
|
161
|
+
forget_line(usage, bucket, previous_bucket, iteration_index)
|
|
162
|
+
elsif guidance_due?(previous_bucket, bucket)
|
|
163
|
+
guidance_message(usage: usage, bucket: bucket)
|
|
164
|
+
end
|
|
165
|
+
@guidance = line if line
|
|
117
166
|
return nil unless emit
|
|
118
167
|
|
|
119
|
-
@guidance = guidance_message(usage: usage, bucket: bucket) if guidance_due?(previous_bucket, bucket)
|
|
120
168
|
{ status: status_message(usage: usage, bucket: bucket), usage: usage, bucket: bucket, source: usage[:source] }
|
|
121
169
|
end
|
|
122
170
|
|
|
171
|
+
# An LLM context edit was applied (LLMContextApply): the prompt is
|
|
172
|
+
# shorter than the one the server's count was for. Until the next
|
|
173
|
+
# count the estimate takes what the prompt lost off that count too,
|
|
174
|
+
# instead of holding it (a shorter prompt counts as trimmed otherwise).
|
|
175
|
+
def edited!
|
|
176
|
+
@edited = true
|
|
177
|
+
end
|
|
178
|
+
|
|
123
179
|
# @return [Hash, nil] the model's line from the last #observe (once)
|
|
124
180
|
def take_guidance
|
|
125
181
|
line = @guidance
|
|
@@ -145,6 +201,7 @@ module Samagotchi
|
|
|
145
201
|
return unless usage
|
|
146
202
|
|
|
147
203
|
@counted = { chars: prompt_chars, image_tokens: image_tokens }
|
|
204
|
+
@edited = false
|
|
148
205
|
value = display_for(used_tokens: usage[:total_tokens], window_tokens: usage[:context_window_tokens] || window&.tokens)
|
|
149
206
|
@display = value if value
|
|
150
207
|
end
|
|
@@ -155,7 +212,7 @@ module Samagotchi
|
|
|
155
212
|
return nil unless @enabled
|
|
156
213
|
return nil unless ContextWindow.positive_integer?(used_tokens) && ContextWindow.positive_integer?(window_tokens)
|
|
157
214
|
|
|
158
|
-
pct = (used_tokens.to_f / window_tokens) * 100.0
|
|
215
|
+
pct = (used_tokens.to_f / counted_against(window_tokens)) * 100.0
|
|
159
216
|
{ est_pct: pct, bucket: bucket_for(pct) }
|
|
160
217
|
end
|
|
161
218
|
|
|
@@ -171,11 +228,11 @@ module Samagotchi
|
|
|
171
228
|
window_source = :server if server_usage && server_usage[:context_window_tokens]
|
|
172
229
|
|
|
173
230
|
if server_usage && server_usage[:prompt_tokens]
|
|
174
|
-
window_tokens = server_usage[:context_window_tokens] || window.tokens
|
|
175
|
-
used_tokens = server_usage[:prompt_tokens] + appended_tokens(prompt_chars, image_tokens, counted)
|
|
231
|
+
window_tokens = counted_against(server_usage[:context_window_tokens] || window.tokens)
|
|
232
|
+
used_tokens = [server_usage[:prompt_tokens] + appended_tokens(prompt_chars, image_tokens, counted), 0].max
|
|
176
233
|
source = "server"
|
|
177
234
|
else
|
|
178
|
-
window_tokens = window.tokens
|
|
235
|
+
window_tokens = counted_against(window.tokens)
|
|
179
236
|
used_tokens = (prompt_chars / @chars_per_token).ceil + image_tokens
|
|
180
237
|
source = "estimate"
|
|
181
238
|
end
|
|
@@ -192,11 +249,19 @@ module Samagotchi
|
|
|
192
249
|
|
|
193
250
|
private
|
|
194
251
|
|
|
195
|
-
#
|
|
252
|
+
# What the buckets count against: the window, or the budget when one is
|
|
253
|
+
# set and smaller.
|
|
254
|
+
def counted_against(window_tokens)
|
|
255
|
+
@budget ? [@budget, window_tokens].min : window_tokens
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# The estimate for what the prompt added since the +counted+ one;
|
|
259
|
+
# after an edit (#edited!) what it lost too, as a negative.
|
|
196
260
|
def appended_tokens(prompt_chars, image_tokens, counted)
|
|
197
261
|
return 0 unless counted
|
|
198
262
|
|
|
199
263
|
chars = prompt_chars - counted[:chars]
|
|
264
|
+
return -(-chars / @chars_per_token).ceil if @edited && chars.negative?
|
|
200
265
|
return 0 unless chars.positive?
|
|
201
266
|
|
|
202
267
|
(chars / @chars_per_token).ceil + [image_tokens - counted[:image_tokens].to_i, 0].max
|
|
@@ -230,6 +295,42 @@ module Samagotchi
|
|
|
230
295
|
bucket_changed || cadence_due
|
|
231
296
|
end
|
|
232
297
|
|
|
298
|
+
# The forget layer's line for this request, or nil (the tiers and when
|
|
299
|
+
# they come: the class comment).
|
|
300
|
+
def forget_line(usage, bucket, previous, iteration_index)
|
|
301
|
+
over = over_budget?(usage)
|
|
302
|
+
tier = over ? :compact : tier_for(bucket)
|
|
303
|
+
return nil unless tier
|
|
304
|
+
|
|
305
|
+
rise = guidance_due?(previous, bucket)
|
|
306
|
+
if iteration_index.zero?
|
|
307
|
+
return nil unless rise || over || (tier != :info && !@offered)
|
|
308
|
+
else
|
|
309
|
+
return nil unless rise
|
|
310
|
+
|
|
311
|
+
tier = :info unless tier == :compact
|
|
312
|
+
end
|
|
313
|
+
@offered = tier != :info
|
|
314
|
+
readout = "~#{k(usage[:estimated_used_tokens])}/#{k(usage[:window_tokens])} tokens in use"
|
|
315
|
+
readout += ", over the budget" if over
|
|
316
|
+
words = { info: nil, tidy: TIDY, compact: COMPACT }.fetch(tier)
|
|
317
|
+
{ role: "system", kind: LINE_KIND, content: "#{LINE_PREFIX}#{readout} (bucket=#{bucket}).#{" #{words}" if words}]" }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# :info, :tidy or :compact for a bucket with guidance (the top one
|
|
321
|
+
# compacts, the one under it tidies, lower ones inform); nil below.
|
|
322
|
+
def tier_for(bucket)
|
|
323
|
+
rank = bucket_rank(bucket)
|
|
324
|
+
return nil if rank < GUIDANCE_FROM_RANK
|
|
325
|
+
return :compact if rank == @thresholds.size
|
|
326
|
+
|
|
327
|
+
rank == @thresholds.size - 1 ? :tidy : :info
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def over_budget?(usage) = !@budget.nil? && usage[:estimated_used_tokens] >= @budget
|
|
331
|
+
|
|
332
|
+
def k(tokens) = tokens >= 1000 ? "#{(tokens / 1000.0).round}k" : tokens.to_s
|
|
333
|
+
|
|
233
334
|
def guidance_message(usage:, bucket:)
|
|
234
335
|
how = usage[:source].to_s == "server" ? "as the server reports" : "estimated"
|
|
235
336
|
{ role: "system", kind: LINE_KIND,
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -176,7 +176,8 @@ module Samagotchi
|
|
|
176
176
|
# The tools this session offers (the prompts' declarations and the
|
|
177
177
|
# kernel's dispatch): the built-ins, per Engine. execute.description
|
|
178
178
|
# off: execute offers no description parameter.
|
|
179
|
-
@tools = Tools::Builtins.registry(command_description: Config.get("execute.description") != false
|
|
179
|
+
@tools = Tools::Builtins.registry(command_description: Config.get("execute.description") != false,
|
|
180
|
+
policy: Config.get(LLMContextStrategy::POLICY_SETTING))
|
|
180
181
|
if @scratch
|
|
181
182
|
# A scratch session's children would outlive it, and so would a
|
|
182
183
|
# note it left in a peer; its memories would too (write and edit
|
|
@@ -930,11 +931,13 @@ module Samagotchi
|
|
|
930
931
|
|
|
931
932
|
# The status line's ctx from the saved context, for a worker that has run
|
|
932
933
|
# no turn yet (it woke after an idle exit): nil when either count is
|
|
933
|
-
# unknown or context.status is off.
|
|
934
|
+
# unknown or context.status is off. Counted as the turns count it:
|
|
935
|
+
# against the effective model's llm_context budget when it has one.
|
|
934
936
|
def saved_context_status(context)
|
|
935
937
|
return nil unless context
|
|
936
938
|
|
|
937
|
-
ContextStatus.new
|
|
939
|
+
ContextStatus.new(llm_context: turn_llm_context)
|
|
940
|
+
.display_for(used_tokens: context[:used_tokens], window_tokens: context[:window_tokens])
|
|
938
941
|
end
|
|
939
942
|
|
|
940
943
|
# The effective model is on a chat host (api: openai), whose loop uses
|
|
@@ -1410,9 +1413,19 @@ module Samagotchi
|
|
|
1410
1413
|
target ||= @host_registry.resolve(@effective_model_name)
|
|
1411
1414
|
chat = target.entry.chat?
|
|
1412
1415
|
level = chat ? nil : thinking_level(target)
|
|
1413
|
-
@prompt_builder.build(chat: chat, thinking: level)
|
|
1416
|
+
@prompt_builder.build(chat: chat, thinking: level, layers: llm_context_layers(target))
|
|
1414
1417
|
end
|
|
1415
1418
|
|
|
1419
|
+
# The LLM context layers +target+'s turns run under: the native prompt
|
|
1420
|
+
# declares the tools they add (forget_outputs under forget), so a
|
|
1421
|
+
# model switch may change the tool list. [] under none.
|
|
1422
|
+
def llm_context_layers(target)
|
|
1423
|
+
LLMContextStrategy.resolve(target, names: model_lookup_names(target)).active_layers
|
|
1424
|
+
rescue StandardError
|
|
1425
|
+
[]
|
|
1426
|
+
end
|
|
1427
|
+
private :llm_context_layers
|
|
1428
|
+
|
|
1416
1429
|
# The base prompt (specs, plugins' declarations).
|
|
1417
1430
|
def assist_system_prompt(chat: false, thinking: nil)
|
|
1418
1431
|
@prompt_builder.base(chat: chat, thinking: thinking)
|
|
@@ -1727,7 +1740,7 @@ module Samagotchi
|
|
|
1727
1740
|
@turn_thinking = [thinking_level(thinking_target), thinking_target]
|
|
1728
1741
|
announce_thinking_level(*@turn_thinking)
|
|
1729
1742
|
LLM::TurnSettings.new(vision: vision, sampling: sampling, thinking: @turn_thinking.first, model_name: nil,
|
|
1730
|
-
window_setting: nil)
|
|
1743
|
+
window_setting: nil, llm_context: nil)
|
|
1731
1744
|
end
|
|
1732
1745
|
private :turn_settings
|
|
1733
1746
|
|
|
@@ -1754,11 +1767,14 @@ module Samagotchi
|
|
|
1754
1767
|
return if reminders_due? || @turn_state.carried_steers? || @next_turn_waiting&.call
|
|
1755
1768
|
|
|
1756
1769
|
messages = ContextNote.with_system_head(turn.session.messages.dup, { role: "system", content: system_prompt(target) })
|
|
1757
|
-
|
|
1770
|
+
# Under the next turn's strategy (a /model switch may have changed it);
|
|
1771
|
+
# the last turn's when it can't be resolved.
|
|
1772
|
+
prompt, images = @kernel.warmup_prompt(messages, llm_context: turn_llm_context || @kernel.turn_settings&.llm_context)
|
|
1758
1773
|
return unless prompt
|
|
1759
1774
|
|
|
1760
|
-
@prompt_warmup.start(client: client, prompt: prompt, model: @kernel.turn_settings.model_name,
|
|
1761
|
-
|
|
1775
|
+
started = @prompt_warmup.start(client: client, prompt: prompt, model: @kernel.turn_settings.model_name,
|
|
1776
|
+
slot: @kernel.last_slot, images: images)
|
|
1777
|
+
@kernel.warmed_up! if started
|
|
1762
1778
|
rescue StandardError => e
|
|
1763
1779
|
Log.warn(:model, "warmup_skipped", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
1764
1780
|
end
|
|
@@ -1816,7 +1832,8 @@ module Samagotchi
|
|
|
1816
1832
|
# The turn's settings, set once here. The chat loop dispatches tools
|
|
1817
1833
|
# through the kernel without its #run: tag those dumps with this
|
|
1818
1834
|
# turn's model, not the last native one.
|
|
1819
|
-
@kernel.turn_settings = turn.settings.with(model_name: bare_for_backend, window_setting: turn_window_setting
|
|
1835
|
+
@kernel.turn_settings = turn.settings.with(model_name: bare_for_backend, window_setting: turn_window_setting,
|
|
1836
|
+
llm_context: turn_llm_context)
|
|
1820
1837
|
|
|
1821
1838
|
turn.limit = @no_interrupt ? IterationLimit.for(no_interrupt: true) : max_iterations || IterationLimit.for
|
|
1822
1839
|
backend.complete(
|
|
@@ -2512,6 +2529,16 @@ module Samagotchi
|
|
|
2512
2529
|
nil
|
|
2513
2530
|
end
|
|
2514
2531
|
|
|
2532
|
+
# The effective model's LLM context strategy (models:/hosts:
|
|
2533
|
+
# llm_context_strategy, else llm_context.strategy), read per turn as
|
|
2534
|
+
# the window is.
|
|
2535
|
+
def turn_llm_context
|
|
2536
|
+
target = @host_registry.resolve(@effective_model_name)
|
|
2537
|
+
LLMContextStrategy.resolve(target, names: model_lookup_names(target))
|
|
2538
|
+
rescue StandardError
|
|
2539
|
+
nil
|
|
2540
|
+
end
|
|
2541
|
+
|
|
2515
2542
|
# See #served_model. A report for another name (before a /model switch)
|
|
2516
2543
|
# doesn't count. Without a target, no probe. A remote chat host has no
|
|
2517
2544
|
# /props: nil until a turn.
|
|
@@ -23,7 +23,7 @@ module Samagotchi
|
|
|
23
23
|
PATH_TOOLS = %w[write edit read memory_write].freeze
|
|
24
24
|
|
|
25
25
|
# chi's own tools, by name, whether this session has them or not.
|
|
26
|
-
CORE_TOOLS = ToolDeclarations::
|
|
26
|
+
CORE_TOOLS = ToolDeclarations::BUILTIN_SCHEMAS.map { |schema| schema[:name] }.freeze
|
|
27
27
|
|
|
28
28
|
attr_reader :tool, :command, :paths, :cwd, :repo_root
|
|
29
29
|
|
|
@@ -149,11 +149,23 @@ module Samagotchi
|
|
|
149
149
|
end
|
|
150
150
|
return name unless whose
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
if first_drop?(registry, entry.name, name)
|
|
153
|
+
Log.warn(:plugins, "plugin_acts_as_dropped", tool: entry.name, acts_as: name,
|
|
154
|
+
msg: "#{entry.name} targets: acts_as #{name} dropped: it is #{whose}")
|
|
155
|
+
end
|
|
154
156
|
nil
|
|
155
157
|
end
|
|
156
158
|
|
|
159
|
+
# The drops logged, per registry (a session's): each tool's acts_as
|
|
160
|
+
# once, not at every call (targets run several times a call).
|
|
161
|
+
DROPS_LOGGED = ObjectSpace::WeakKeyMap.new
|
|
162
|
+
DROPS_LOCK = Mutex.new
|
|
163
|
+
|
|
164
|
+
def self.first_drop?(registry, tool, acts_as)
|
|
165
|
+
DROPS_LOCK.synchronize { !(DROPS_LOGGED[registry] ||= Set.new).add?([tool, acts_as]).nil? }
|
|
166
|
+
end
|
|
167
|
+
private_class_method :first_drop?
|
|
168
|
+
|
|
157
169
|
# The longest label: the question takes from targets:.
|
|
158
170
|
LABEL_CHARS = 80
|
|
159
171
|
|
|
@@ -36,10 +36,13 @@ module Samagotchi
|
|
|
36
36
|
# sampling: the configured request parameters (SamplingSettings), nil when unset;
|
|
37
37
|
# thinking: the configured level (Thinking), nil when unset;
|
|
38
38
|
# remote: the configured true/false (#remote?), nil when unset;
|
|
39
|
-
# window_tokens: the configured context window (ContextWindow.setting), nil when unset
|
|
39
|
+
# window_tokens: the configured context window (ContextWindow.setting), nil when unset;
|
|
40
|
+
# llm_context_strategy: the configured layers (LLMContextStrategy), nil when unset;
|
|
41
|
+
# llm_context_apply: the configured apply rule (LLMContextStrategy), nil when unset;
|
|
42
|
+
# llm_context_budget_tokens: the configured context budget (LLMContextStrategy), nil when unset.
|
|
40
43
|
HostEntry = Struct.new(:name, :host, :port, :transport, :client, :api, :scheme, :url, :api_key_env, :profile,
|
|
41
44
|
:first_token_timeout, :vision, :sampling, :thinking, :remote, :window_tokens,
|
|
42
|
-
keyword_init: true) do
|
|
45
|
+
:llm_context_strategy, :llm_context_apply, :llm_context_budget_tokens, keyword_init: true) do
|
|
43
46
|
# Talks the OpenAI chat API (the chat loop); nil and raw apis use the
|
|
44
47
|
# raw-prompt loop.
|
|
45
48
|
def chat? = api == :openai
|
|
@@ -99,7 +102,9 @@ module Samagotchi
|
|
|
99
102
|
api: cfg[:api]&.to_sym, scheme: cfg[:scheme], url: cfg[:url], api_key_env: cfg[:api_key_env],
|
|
100
103
|
profile: cfg[:profile], first_token_timeout: cfg[:first_token_timeout],
|
|
101
104
|
vision: cfg[:vision], sampling: cfg[:sampling], thinking: cfg[:thinking],
|
|
102
|
-
remote: cfg[:remote], window_tokens: cfg[:window_tokens]
|
|
105
|
+
remote: cfg[:remote], window_tokens: cfg[:window_tokens],
|
|
106
|
+
llm_context_strategy: cfg[:llm_context_strategy], llm_context_apply: cfg[:llm_context_apply],
|
|
107
|
+
llm_context_budget_tokens: cfg[:llm_context_budget_tokens])
|
|
103
108
|
entry.client = Client.new(host: cfg[:host], port: cfg[:port], transport: transport, scheme: cfg[:scheme],
|
|
104
109
|
first_token_timeout: entry.first_token_limit, name: entry.name,
|
|
105
110
|
api_key_env: entry.api_key_env, env: env)
|