samagotchi 0.41.0 → 0.43.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 +56 -1
- data/docs/cli.md +8 -6
- data/docs/configuration.md +8 -0
- data/docs/context.md +46 -0
- data/docs/memory.md +18 -0
- data/docs/plugins.md +27 -5
- data/docs/sessions.md +5 -5
- data/docs/testing.md +16 -0
- data/lib/samagotchi/bundles/github-pr/manifest.yml +4 -4
- data/lib/samagotchi/bundles/github-pr/plugin.rb +177 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +8 -4
- data/lib/samagotchi/context_command.rb +5 -2
- data/lib/samagotchi/context_sources.rb +40 -0
- data/lib/samagotchi/engine.rb +31 -1
- data/lib/samagotchi/model_notes.rb +7 -4
- data/lib/samagotchi/plugin/attached_context.rb +75 -11
- data/lib/samagotchi/prompt_note.rb +53 -0
- data/lib/samagotchi/self_report.rb +32 -0
- data/lib/samagotchi/session.rb +16 -1
- data/lib/samagotchi/session_commands.rb +24 -5
- data/lib/samagotchi/session_metrics.rb +21 -4
- data/lib/samagotchi/system_prompt.rb +14 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +12 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
- data/lib/samagotchi/tool_declarations.rb +2 -1
- data/lib/samagotchi/turn_flow.rb +8 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +55 -8
- data/lib/samagotchi/web/public/app.js +73 -11
- data/lib/samagotchi/web/public/ctx.js +27 -2
- data/lib/samagotchi/web/public/data.js +6 -3
- data/lib/samagotchi/web/public/format.js +24 -0
- data/lib/samagotchi/web/public/index.html +8 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +151 -21
- data/lib/samagotchi/web/public/model_pick.js +3 -1
- data/lib/samagotchi/web/public/model_picker.js +2 -0
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/timing.js +3 -1
- data/lib/samagotchi/web/public/turn_view.js +15 -2
- data/lib/samagotchi/web/session_summary.rb +2 -0
- data/lib/samagotchi/worker.rb +16 -4
- data/lib/samagotchi/worker_wakes.rb +2 -1
- metadata +2 -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"
|
|
@@ -59,6 +61,7 @@ module Samagotchi
|
|
|
59
61
|
["log", log_path(env)],
|
|
60
62
|
["model", model ? model_label(model, default, env) : "(not configured)"],
|
|
61
63
|
["model key", model ? model_key_for(model, env) : "-"],
|
|
64
|
+
["model notes", model ? model_notes_for(model, env) : "-"],
|
|
62
65
|
["host", model ? host_for(model, env) : "-"],
|
|
63
66
|
["api key", model ? api_key_for(model, env) : "-"],
|
|
64
67
|
["loop", model ? loop_for(model, env) : "-"],
|
|
@@ -187,6 +190,35 @@ module Samagotchi
|
|
|
187
190
|
ModelOverlay.key_for(model) || "-"
|
|
188
191
|
end
|
|
189
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
|
+
|
|
190
222
|
def model_name
|
|
191
223
|
ModelProfile.required_model_name
|
|
192
224
|
rescue ArgumentError
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "time"
|
|
|
7
7
|
require_relative "atomic_file"
|
|
8
8
|
require_relative "context_note"
|
|
9
9
|
require_relative "llm_context_override"
|
|
10
|
+
require_relative "prompt_note"
|
|
10
11
|
|
|
11
12
|
require_relative "paths"
|
|
12
13
|
require_relative "project_scope"
|
|
@@ -44,6 +45,17 @@ module Samagotchi
|
|
|
44
45
|
# from it (--mute): the worker rebuilds the same prompt on a respawn.
|
|
45
46
|
# Names as given; the engine normalizes them.
|
|
46
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
|
|
47
59
|
# The name model_name was typed as when that differs from the resolved
|
|
48
60
|
# ref stored in model_name (an alias: `small` for `gemma-small`), else
|
|
49
61
|
# nil. The models: lookup takes it (HostRegistry#lookup_names).
|
|
@@ -118,7 +130,7 @@ module Samagotchi
|
|
|
118
130
|
first_preview: "", test_run: false, pending_question: nil,
|
|
119
131
|
used_memory_names: [], project_root: nil,
|
|
120
132
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
121
|
-
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil)
|
|
133
|
+
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [])
|
|
122
134
|
@id = id
|
|
123
135
|
@metadata_version = metadata_version
|
|
124
136
|
@mode = mode
|
|
@@ -136,6 +148,7 @@ module Samagotchi
|
|
|
136
148
|
@used_memory_names = self.class.name_list(used_memory_names)
|
|
137
149
|
@preloaded_memory_names = self.class.name_list(preloaded_memory_names)
|
|
138
150
|
@muted_memory_names = self.class.name_list(muted_memory_names)
|
|
151
|
+
self.prompt_notes = prompt_notes
|
|
139
152
|
@project_root = project_root
|
|
140
153
|
@parent_id = parent_id&.to_s
|
|
141
154
|
@scratch = !!scratch
|
|
@@ -269,6 +282,7 @@ module Samagotchi
|
|
|
269
282
|
"project_root" => nil,
|
|
270
283
|
"preloaded_memory_names" => [],
|
|
271
284
|
"muted_memory_names" => [],
|
|
285
|
+
"prompt_notes" => [],
|
|
272
286
|
"parent_id" => nil,
|
|
273
287
|
"scratch" => false,
|
|
274
288
|
"last_turn" => nil,
|
|
@@ -393,6 +407,7 @@ module Samagotchi
|
|
|
393
407
|
when "test_run" then !!@test_run
|
|
394
408
|
when "delegate" then @delegate
|
|
395
409
|
when "llm_context" then llm_context_file
|
|
410
|
+
when "prompt_notes" then @prompt_notes.map(&:to_file)
|
|
396
411
|
when "used_memory_names", "preloaded_memory_names", "muted_memory_names"
|
|
397
412
|
Array(instance_variable_get(:"@#{key}"))
|
|
398
413
|
else instance_variable_get(:"@#{key}")
|
|
@@ -3,6 +3,7 @@
|
|
|
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"
|
|
@@ -110,6 +111,14 @@ module Samagotchi
|
|
|
110
111
|
@builtin_registry ||= register_builtins(Commands::Registry.new).freeze
|
|
111
112
|
end
|
|
112
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
|
+
|
|
113
122
|
# @return [String] the model /model clear goes back to
|
|
114
123
|
attr_reader :default_model
|
|
115
124
|
|
|
@@ -362,9 +371,9 @@ module Samagotchi
|
|
|
362
371
|
def model_command(input)
|
|
363
372
|
suffix = input.delete_prefix(MODEL_COMMAND).strip
|
|
364
373
|
if suffix.empty?
|
|
365
|
-
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)
|
|
366
375
|
|
|
367
|
-
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]
|
|
368
377
|
end
|
|
369
378
|
|
|
370
379
|
# Parse flags: --default and --alias <name> / --alias=<name> (tolerant order)
|
|
@@ -434,14 +443,14 @@ module Samagotchi
|
|
|
434
443
|
return ["--default cannot be combined with clear/default/none/off", false] if persist_default
|
|
435
444
|
|
|
436
445
|
switch_model(@default_model)
|
|
437
|
-
return ["runtime model reset to #{model_name}#{model_note}", true]
|
|
446
|
+
return ["runtime model reset to #{model_name}#{model_note}#{notes_note}", true]
|
|
438
447
|
end
|
|
439
448
|
|
|
440
449
|
switch_model(arg, persist_default: persist_default)
|
|
441
450
|
if persist_default
|
|
442
|
-
["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]
|
|
443
452
|
else
|
|
444
|
-
["runtime model set to #{model_name}#{model_note}", true]
|
|
453
|
+
["runtime model set to #{model_name}#{model_note}#{notes_note}", true]
|
|
445
454
|
end
|
|
446
455
|
end
|
|
447
456
|
end
|
|
@@ -482,6 +491,16 @@ module Samagotchi
|
|
|
482
491
|
""
|
|
483
492
|
end
|
|
484
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
|
+
|
|
485
504
|
# "; thinking: off (models: qwen)" when the model has a level set.
|
|
486
505
|
def thinking_note
|
|
487
506
|
summary = @engine.thinking_summary
|
|
@@ -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.
|
|
@@ -308,7 +310,7 @@ module Samagotchi
|
|
|
308
310
|
when :turn_canceled
|
|
309
311
|
end_turn(status: "canceled", reason: event[:cancellation_reason], by: event[:cancelled_by])
|
|
310
312
|
when :turn_failed
|
|
311
|
-
end_turn(status: "failed")
|
|
313
|
+
end_turn(status: "failed", failure: failure_fields(event))
|
|
312
314
|
end
|
|
313
315
|
|
|
314
316
|
@mutex.synchronize { @last_activity_at = now.iso8601(3) }
|
|
@@ -497,9 +499,10 @@ module Samagotchi
|
|
|
497
499
|
end
|
|
498
500
|
|
|
499
501
|
# A canceled turn's record keeps why (+reason+: "user", "ctrl_c", ...)
|
|
500
|
-
# and who stopped it (+by+: a hook's bundle),
|
|
501
|
-
#
|
|
502
|
-
|
|
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)
|
|
503
506
|
@mutex.synchronize do
|
|
504
507
|
if @turn
|
|
505
508
|
# A turn that failed mid-stream never saw its generation end:
|
|
@@ -522,6 +525,7 @@ module Samagotchi
|
|
|
522
525
|
}
|
|
523
526
|
record[:cancellation_reason] = reason.to_s unless reason.nil? || reason.to_s.empty?
|
|
524
527
|
record[:cancelled_by] = by.to_s unless by.nil? || by.to_s.empty?
|
|
528
|
+
record[:failure] = failure if failure
|
|
525
529
|
record.merge!(turn_token_fields(@turn))
|
|
526
530
|
@turn_records << record
|
|
527
531
|
count_turn(record)
|
|
@@ -530,6 +534,19 @@ module Samagotchi
|
|
|
530
534
|
end
|
|
531
535
|
end
|
|
532
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
|
+
|
|
533
550
|
# The model the server says answered, and the name asked for then (a
|
|
534
551
|
# /model switch makes it stale until the next generation reports).
|
|
535
552
|
def record_served_model(event)
|
|
@@ -80,13 +80,17 @@ module Samagotchi
|
|
|
80
80
|
key = [chat, thinking, layers]
|
|
81
81
|
@built ||= {}
|
|
82
82
|
@index_sizes ||= {}
|
|
83
|
+
@notes_loaded ||= {}
|
|
83
84
|
unless @built.key?(key)
|
|
85
|
+
@section_notes = nil
|
|
84
86
|
@built[key] = system_prompt_with_index(
|
|
85
87
|
assist_system_prompt(chat: chat, thinking: thinking, layers: layers), chat: chat, thinking: thinking, layers: layers
|
|
86
88
|
)
|
|
87
89
|
@index_sizes[key] = @measured_index
|
|
90
|
+
@notes_loaded[key] = @section_notes || []
|
|
88
91
|
end
|
|
89
92
|
@memory_index = @index_sizes[key]
|
|
93
|
+
@prompt_notes = @notes_loaded[key]
|
|
90
94
|
@built[key]
|
|
91
95
|
end
|
|
92
96
|
|
|
@@ -98,11 +102,19 @@ module Samagotchi
|
|
|
98
102
|
# @return [Hash, nil]
|
|
99
103
|
attr_reader :memory_index
|
|
100
104
|
|
|
105
|
+
# The model notes the prompt #build returned last carries
|
|
106
|
+
# (Array<ModelNotes::Note>, [] with none), nil before a build and after
|
|
107
|
+
# #reset!. Read when the prompt is built, as the section is.
|
|
108
|
+
# @return [Array<ModelNotes::Note>, nil]
|
|
109
|
+
attr_reader :prompt_notes
|
|
110
|
+
|
|
101
111
|
# Drops the built prompts: the next #build reads the profile, tools,
|
|
102
112
|
# indexes and memories again (a model or profile switch, changed tools).
|
|
103
113
|
def reset!
|
|
104
114
|
@built = nil
|
|
105
115
|
@index_sizes = nil
|
|
116
|
+
@notes_loaded = nil
|
|
117
|
+
@prompt_notes = nil
|
|
106
118
|
@stable_lengths = nil
|
|
107
119
|
end
|
|
108
120
|
|
|
@@ -323,6 +335,7 @@ module Samagotchi
|
|
|
323
335
|
# identity: a heading per scope, each note under its name, the models:
|
|
324
336
|
# line left out. nil without any, so the prompt stays as it was.
|
|
325
337
|
def model_notes_section
|
|
338
|
+
@section_notes = []
|
|
326
339
|
notes = Array(@model_notes_lookup.call)
|
|
327
340
|
return nil if notes.empty?
|
|
328
341
|
|
|
@@ -330,7 +343,7 @@ module Samagotchi
|
|
|
330
343
|
notes.group_by(&:scope).map do |scope, list|
|
|
331
344
|
heading = ref.to_s.empty? ? "Model notes (scope=#{scope}):" : "Model notes (for #{ref}, scope=#{scope}):"
|
|
332
345
|
[heading, *list.map { |note| "## #{note.name}\n#{note.body}" }].join("\n\n")
|
|
333
|
-
end.join("\n\n")
|
|
346
|
+
end.join("\n\n").tap { @section_notes = notes }
|
|
334
347
|
rescue StandardError
|
|
335
348
|
nil
|
|
336
349
|
end
|
|
@@ -522,6 +522,9 @@ module Samagotchi
|
|
|
522
522
|
return
|
|
523
523
|
end
|
|
524
524
|
return @screen.commit(stale_worker("run commands")) if reply.status == 404
|
|
525
|
+
|
|
526
|
+
name = older_worker_command(reply, line)
|
|
527
|
+
return @screen.commit(stale_worker("run #{name}")) if name
|
|
525
528
|
return @screen.commit("could not run the command (#{LATE})") if too_late?(reply)
|
|
526
529
|
|
|
527
530
|
detail = reply.json&.fetch("detail", nil) || reply.json&.fetch("error", nil)
|
|
@@ -828,6 +831,14 @@ module Samagotchi
|
|
|
828
831
|
# so it shows above what it got.
|
|
829
832
|
def echo_answer(text) = @screen.commit("#{paint(QUESTION_PROMPT, 33)}#{text}")
|
|
830
833
|
|
|
834
|
+
# The name of this chi's own command +line+ when the worker answered it
|
|
835
|
+
# unknown_command (it runs an older chi without it), else nil.
|
|
836
|
+
def older_worker_command(reply, line)
|
|
837
|
+
return nil unless reply.status == 400 && reply.json&.fetch("error", nil) == "unknown_command"
|
|
838
|
+
|
|
839
|
+
SessionCommands.builtin_name(line)
|
|
840
|
+
end
|
|
841
|
+
|
|
831
842
|
def stale_worker(cant)
|
|
832
843
|
BridgeClient.stale_worker_message(@client.session_id, cant: cant)
|
|
833
844
|
end
|
|
@@ -1002,6 +1013,7 @@ module Samagotchi
|
|
|
1002
1013
|
|
|
1003
1014
|
why =
|
|
1004
1015
|
if reply.status == 404 then stale_worker("run commands")
|
|
1016
|
+
elsif (name = older_worker_command(reply, line)) then stale_worker("run #{name}")
|
|
1005
1017
|
elsif too_late?(reply) then LATE
|
|
1006
1018
|
else "the worker answered #{reply.status}"
|
|
1007
1019
|
end
|
|
@@ -6,6 +6,7 @@ require_relative "../image_store"
|
|
|
6
6
|
require_relative "../turn_note"
|
|
7
7
|
require_relative "../steer"
|
|
8
8
|
require_relative "../memory_bundle/index_size"
|
|
9
|
+
require_relative "../prompt_note"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
class TerminalUI
|
|
@@ -485,6 +486,9 @@ module Samagotchi
|
|
|
485
486
|
lines << "prompt profile: #{snapshot[:profile]} (#{snapshot[:profile_source]})" if snapshot[:profile]
|
|
486
487
|
memory_index = memory_index_text(snapshot[:memory_index])
|
|
487
488
|
lines << "memory index: #{memory_index}" unless memory_index.empty?
|
|
489
|
+
# The model notes the session's prompt carried (Engine#prompt_notes).
|
|
490
|
+
notes = PromptNote.text(snapshot[:prompt_notes])
|
|
491
|
+
lines << "model notes: #{notes}" unless notes.empty?
|
|
488
492
|
llm_context = llm_context_stats_text(snapshot[:llm_context])
|
|
489
493
|
lines << "llm context: #{llm_context}" if llm_context
|
|
490
494
|
if snapshot[:served_model]
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "tools/args"
|
|
5
5
|
require_relative "llm_context_strategy"
|
|
6
|
+
require_relative "tools/memory"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# Tool-declaration constants, protocol constants, and guidance text.
|
|
@@ -146,7 +147,7 @@ module Samagotchi
|
|
|
146
147
|
},
|
|
147
148
|
description: {
|
|
148
149
|
type: "string",
|
|
149
|
-
description: "Optional one-line description (at most
|
|
150
|
+
description: "Optional one-line description (at most #{Tools::MemoryWrite::DESCRIPTION_LIMIT} characters) appended to the managed index line; without content, it changes only that line"
|
|
150
151
|
},
|
|
151
152
|
current_model_only: {
|
|
152
153
|
type: "boolean",
|
data/lib/samagotchi/turn_flow.rb
CHANGED
|
@@ -46,11 +46,18 @@ module Samagotchi
|
|
|
46
46
|
# (context summarizes the interrupted turn for a "no, <reason>")
|
|
47
47
|
attr_reader :offer
|
|
48
48
|
|
|
49
|
+
# How many times the conversation went back to a checkpoint (a failed
|
|
50
|
+
# or canceled turn's restore, !rollback): a worker compares it across a
|
|
51
|
+
# turn to tell whether the turn's messages (the delegate reports it
|
|
52
|
+
# merged) left the conversation.
|
|
53
|
+
attr_reader :restores
|
|
54
|
+
|
|
49
55
|
def initialize(engine:)
|
|
50
56
|
@engine = engine
|
|
51
57
|
@checkpoint = nil
|
|
52
58
|
@continue_checkpoint = nil
|
|
53
59
|
@offer = nil
|
|
60
|
+
@restores = 0
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
def awaiting_continue? = !@offer.nil?
|
|
@@ -198,6 +205,7 @@ module Samagotchi
|
|
|
198
205
|
restored = Array(checkpoint) + arrived
|
|
199
206
|
restored = TurnNote.replace_trailing(restored, note) if note
|
|
200
207
|
@engine.rollback_to(restored)
|
|
208
|
+
@restores += 1
|
|
201
209
|
end
|
|
202
210
|
|
|
203
211
|
def conversation_of(result)
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -20,6 +20,7 @@ require_relative "../session_metrics"
|
|
|
20
20
|
require_relative "../session_commands"
|
|
21
21
|
require_relative "../steer"
|
|
22
22
|
require_relative "../host_registry"
|
|
23
|
+
require_relative "../llm_context_override"
|
|
23
24
|
require_relative "../model_catalog"
|
|
24
25
|
require_relative "../model_profile"
|
|
25
26
|
require_relative "../sampling_settings"
|
|
@@ -458,7 +459,8 @@ module Samagotchi
|
|
|
458
459
|
end
|
|
459
460
|
|
|
460
461
|
# Each model's configured sampling as /model words it ("temperature=0.6
|
|
461
|
-
# (hosts.work)"), on the models that have some: the picker's tooltip
|
|
462
|
+
# (hosts.work)"), on the models that have some: the picker's tooltip;
|
|
463
|
+
# and each model's LLM context (LLMContextStrategy::Explained#summary).
|
|
462
464
|
# A config that can't be read leaves the list as it is.
|
|
463
465
|
def add_sampling(models, registry)
|
|
464
466
|
return if registry.nil? || models.empty?
|
|
@@ -466,14 +468,23 @@ module Samagotchi
|
|
|
466
468
|
settings = ConfigFile.model_settings
|
|
467
469
|
models.each do |model|
|
|
468
470
|
target = registry.resolve(model[:name])
|
|
469
|
-
|
|
470
|
-
|
|
471
|
+
names = registry.lookup_names(model[:name], target: target)
|
|
472
|
+
summary = SamplingSettings.summary(target, names: names, models: settings)
|
|
471
473
|
model[:sampling] = summary if summary
|
|
474
|
+
add_llm_context(model, target, names, settings)
|
|
472
475
|
end
|
|
473
476
|
rescue StandardError
|
|
474
477
|
nil
|
|
475
478
|
end
|
|
476
479
|
|
|
480
|
+
# The LLM context +model+ starts under (the start page's llm ctx chip);
|
|
481
|
+
# one that can't be read leaves this model without, not the others.
|
|
482
|
+
def add_llm_context(model, target, names, settings)
|
|
483
|
+
model[:llm_context] = LLMContextStrategy.explain(target, names: names, models: settings).summary(nil)
|
|
484
|
+
rescue StandardError
|
|
485
|
+
nil
|
|
486
|
+
end
|
|
487
|
+
|
|
477
488
|
# A session's LLM context strategy, apply rule and budget, each with
|
|
478
489
|
# where it came from (LLMContextStrategy::Explained#summary), as its
|
|
479
490
|
# worker would resolve them; nil when they can't be.
|
|
@@ -563,6 +574,12 @@ module Samagotchi
|
|
|
563
574
|
dir, error = scope_dir(body["dir"])
|
|
564
575
|
return error if error
|
|
565
576
|
|
|
577
|
+
# llm_context: the session's own LLM context values before its first
|
|
578
|
+
# turn (the start page's llm ctx chip; chi --llm-context), read
|
|
579
|
+
# before anything spawns so a bad one spawns nothing.
|
|
580
|
+
llm_context, error = request_llm_context(body["llm_context"])
|
|
581
|
+
return error if error
|
|
582
|
+
|
|
566
583
|
folder = dir ? { working_directory: dir } : {}
|
|
567
584
|
# model: the model the session starts on (the start page's picker,
|
|
568
585
|
# spelled as GET /api/models lists it); blank means the default.
|
|
@@ -573,6 +590,7 @@ module Samagotchi
|
|
|
573
590
|
# until that turn is saved.
|
|
574
591
|
preview = idle ? body["preview"].to_s.strip : ""
|
|
575
592
|
folder[:title] = preview unless preview.empty?
|
|
593
|
+
folder[:llm_context] = llm_context if llm_context
|
|
576
594
|
begin
|
|
577
595
|
session = @manager.spawn_session(prompt: idle ? nil : prompt.to_s, state_dir: @state_dir, **folder)
|
|
578
596
|
rescue ModelProfile::MissingModel => e
|
|
@@ -588,6 +606,25 @@ module Samagotchi
|
|
|
588
606
|
json_response(201, session_to_json(session).merge(bridge_port: port))
|
|
589
607
|
end
|
|
590
608
|
|
|
609
|
+
# The create request's "llm_context" ({strategy:, apply:, budget:},
|
|
610
|
+
# each a word as /llm-context takes it, "default" for unset) as an
|
|
611
|
+
# override: [override, nil] (nil when absent or all default), or
|
|
612
|
+
# [nil, a 400 response] for anything else.
|
|
613
|
+
def request_llm_context(raw)
|
|
614
|
+
return [nil, nil] if raw.nil?
|
|
615
|
+
|
|
616
|
+
fields = LLMContextOverride::COMMAND_WORDS.invert
|
|
617
|
+
unless raw.is_a?(Hash) && raw.all? { |key, word| fields.key?(key) && word.is_a?(String) }
|
|
618
|
+
return [nil, error_response(400, "invalid_llm_context",
|
|
619
|
+
"llm_context is an object of #{fields.keys.join(", ")} words")]
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
override = LLMContextOverride.update(nil, raw.to_h { |key, word| [fields.fetch(key), word] })
|
|
623
|
+
[override.empty? ? nil : override, nil]
|
|
624
|
+
rescue ArgumentError => e
|
|
625
|
+
[nil, error_response(400, "invalid_llm_context", e.message)]
|
|
626
|
+
end
|
|
627
|
+
|
|
591
628
|
def handle_show(req, id)
|
|
592
629
|
# The page's light re-reads: never the whole conversation.
|
|
593
630
|
return handle_cards_read(id) if req.params["cards"] == "1"
|
|
@@ -626,6 +663,8 @@ module Samagotchi
|
|
|
626
663
|
# What the worker's server said it served for that model (after a turn).
|
|
627
664
|
if snapshot
|
|
628
665
|
session_json = session_json.merge(served_model: snapshot["served_model"], served_model_for: snapshot["served_model_for"])
|
|
666
|
+
# The model notes its prompt carries now (a /model there changes them).
|
|
667
|
+
session_json = session_json.merge(prompt_notes: snapshot["prompt_notes"]) if snapshot["prompt_notes"].is_a?(Array)
|
|
629
668
|
end
|
|
630
669
|
# The LLM context strategy the next turn runs under (the info bar's
|
|
631
670
|
# chip): the worker's, else worked out here from the session file.
|
|
@@ -680,8 +719,9 @@ module Samagotchi
|
|
|
680
719
|
# history again (nor a render of every earlier answer). Live: the
|
|
681
720
|
# worker's GET tail alone (no session file read); else the file. The
|
|
682
721
|
# session part is what the page reads of it (id, status, used memory
|
|
683
|
-
# names
|
|
684
|
-
#
|
|
722
|
+
# names, the model notes its prompt carried: the first turn's build or
|
|
723
|
+
# a /model records them). ?turn_id=: that turn's answer (a queued
|
|
724
|
+
# turn's page re-reads after the next one started). ?recent=1 (the page since this
|
|
685
725
|
# release): the timing's records are the newest turn's (#recent_timing),
|
|
686
726
|
# which the page merges by id; without it, the whole timing as before.
|
|
687
727
|
def handle_tail_read(req, id)
|
|
@@ -692,12 +732,14 @@ module Samagotchi
|
|
|
692
732
|
tail = live_tail(id, turn_id: turn_id)
|
|
693
733
|
state = tail && tail["session_state_snapshot"]
|
|
694
734
|
if state.is_a?(Hash)
|
|
695
|
-
session_json = { id: id, status: state["status"], used_memory_names: Array(state["used_memory_names"])
|
|
735
|
+
session_json = { id: id, status: state["status"], used_memory_names: Array(state["used_memory_names"]),
|
|
736
|
+
prompt_notes: Array(state["prompt_notes"]) }
|
|
696
737
|
messages = last_assistant_for_display([tail["answer"]].compact)
|
|
697
738
|
else
|
|
698
739
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
699
740
|
state = nil
|
|
700
|
-
session_json = { id: id, status: displayed_status(session), used_memory_names: Array(session.used_memory_names)
|
|
741
|
+
session_json = { id: id, status: displayed_status(session), used_memory_names: Array(session.used_memory_names),
|
|
742
|
+
prompt_notes: session.prompt_notes.map(&:to_h) }
|
|
701
743
|
messages = last_assistant_for_display(session.messages, turn_id: turn_id)
|
|
702
744
|
end
|
|
703
745
|
live_metrics = state && state["metrics"]
|
|
@@ -839,7 +881,12 @@ module Samagotchi
|
|
|
839
881
|
when 202
|
|
840
882
|
record_history(body, line) if PromptHistory.shell_line?(line)
|
|
841
883
|
json_response(202, reply.json || { status: "accepted" })
|
|
842
|
-
when 400
|
|
884
|
+
when 400
|
|
885
|
+
# One of this chi's own commands the worker doesn't know: it runs an older chi.
|
|
886
|
+
name = reply.json&.dig("error") == "unknown_command" && SessionCommands.builtin_name(line)
|
|
887
|
+
next error_response(501, "not_supported", BridgeClient.stale_worker_message(id, cant: "run #{name}")) if name
|
|
888
|
+
|
|
889
|
+
error_response(400, reply.json&.dig("error") || "unknown_command", reply_detail(reply, "not a session command"))
|
|
843
890
|
end
|
|
844
891
|
end
|
|
845
892
|
rescue SessionManager::OwnedByTUI => e
|