samagotchi 0.100.1 → 0.102.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 +50 -1
- data/bin/chi +14 -10
- data/docs/architecture.md +10 -2
- data/docs/cli.md +100 -5
- data/docs/configuration.md +26 -4
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +1 -0
- data/docs/internals/llm-context-forget.md +14 -0
- data/docs/internals/prompt-caching.md +8 -1
- data/docs/internals/working-set.md +205 -0
- data/docs/plugins.md +6 -3
- data/docs/sessions.md +28 -11
- data/docs/testing.md +8 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +16 -0
- data/lib/samagotchi/bridge.rb +9 -3
- data/lib/samagotchi/bundle_command/check.rb +5 -1
- data/lib/samagotchi/bundle_command/try/command.rb +6 -6
- data/lib/samagotchi/bundle_command/try/sandbox.rb +3 -3
- data/lib/samagotchi/bundle_command.rb +4 -4
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/commands/registry.rb +12 -1
- data/lib/samagotchi/config.rb +92 -53
- data/lib/samagotchi/engine.rb +153 -4
- data/lib/samagotchi/host_keys.rb +35 -18
- data/lib/samagotchi/idle_scheduler.rb +16 -1
- data/lib/samagotchi/llm_context_edit.rb +32 -10
- data/lib/samagotchi/llm_context_forget.rb +12 -3
- data/lib/samagotchi/llm_context_view.rb +19 -7
- data/lib/samagotchi/memory_bundle/installer.rb +2 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +13 -2
- data/lib/samagotchi/message_parts.rb +8 -3
- data/lib/samagotchi/plugin/context.rb +2 -2
- data/lib/samagotchi/prompt.rb +1 -1
- data/lib/samagotchi/recap_command.rb +68 -0
- data/lib/samagotchi/recap_setup.rb +27 -14
- data/lib/samagotchi/send_command.rb +14 -11
- data/lib/samagotchi/session.rb +19 -12
- data/lib/samagotchi/session_commands.rb +44 -2
- data/lib/samagotchi/session_config.rb +411 -0
- data/lib/samagotchi/session_setup.rb +9 -9
- data/lib/samagotchi/sessions_command.rb +6 -2
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +10 -3
- data/lib/samagotchi/terminal_ui.rb +8 -6
- data/lib/samagotchi/testing/bundle.rb +3 -2
- data/lib/samagotchi/testing/fake_executable.rb +6 -16
- data/lib/samagotchi/testing/fake_executable_launcher +6 -0
- data/lib/samagotchi/tool_response.rb +10 -6
- data/lib/samagotchi/tool_runner.rb +25 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +76 -11
- data/lib/samagotchi/web/public/app.js +35 -3
- data/lib/samagotchi/web/public/chip_popover.js +56 -17
- data/lib/samagotchi/web/public/commands.js +8 -2
- data/lib/samagotchi/web/public/data.js +7 -0
- data/lib/samagotchi/web/public/index.html +145 -1
- data/lib/samagotchi/web/public/info_bar.js +16 -4
- data/lib/samagotchi/web/public/info_bar_model.js +7 -3
- data/lib/samagotchi/web/public/refs.js +10 -1
- data/lib/samagotchi/web/public/stage_model.js +6 -5
- data/lib/samagotchi/web/public/stage_view.js +34 -2
- data/lib/samagotchi/web/public/stream_handlers.js +10 -1
- data/lib/samagotchi/web/public/turn_html.js +5 -0
- data/lib/samagotchi/web/public/turn_view.js +16 -4
- data/lib/samagotchi/web/public/working_set.js +612 -0
- data/lib/samagotchi/web/public/working_set_rail.js +278 -0
- data/lib/samagotchi/web/public/working_set_view.js +541 -0
- data/lib/samagotchi/web/server.rb +6 -4
- data/lib/samagotchi/worker.rb +9 -6
- data/lib/samagotchi/working_set.rb +238 -0
- data/lib/samagotchi/working_set_command.rb +119 -0
- data/lib/samagotchi/working_set_steer.rb +250 -0
- metadata +11 -2
- data/lib/samagotchi/bundle_session_settings.rb +0 -251
|
@@ -14,17 +14,21 @@ module Samagotchi
|
|
|
14
14
|
# are replaced, never changed in place (.store), or an edit would reach
|
|
15
15
|
# a checkpoint it was rolled back from.
|
|
16
16
|
#
|
|
17
|
-
# kind: :stale (chi saw a later read or edit supersede it)
|
|
18
|
-
# (the model forgot it, its note keeping the finding)
|
|
19
|
-
#
|
|
17
|
+
# kind: :stale (chi saw a later read or edit supersede it), :forget
|
|
18
|
+
# (the model forgot it, its note keeping the finding) or :drop (the user
|
|
19
|
+
# dropped it from the working set: WorkingSetSteer; sent whatever the
|
|
20
|
+
# strategy, LLMContextView#sends?); note: what the
|
|
21
|
+
# stub says; by: who made it ("chi", the model, "user"); staged_at/applied_at:
|
|
20
22
|
# ISO 8601 times, applied_at nil while it waits to reach the prompt;
|
|
21
23
|
# keep: the output's lines a forget keeps ([[first, last], …], as the
|
|
22
24
|
# model named them; saved only when there are some), sent under the
|
|
23
25
|
# stub; keep_offset: what those numbers are past the output's own lines
|
|
24
26
|
# (a ranged read's start_line - 1: a read's keep names file lines; saved
|
|
25
|
-
# only when not 0)
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
# only when not 0); was: a drop's, the edit the output held before it
|
|
28
|
+
# (its saved Hash: an applied forget sent whole under none, a staged
|
|
29
|
+
# one, a stale stub), which an undrop puts back (one edit per id).
|
|
30
|
+
LLMContextEdit = Data.define(:id, :kind, :note, :by, :staged_at, :applied_at, :keep, :keep_offset, :was) do
|
|
31
|
+
def initialize(keep: [], keep_offset: 0, was: nil, **fields) = super
|
|
28
32
|
|
|
29
33
|
def applied? = !applied_at.nil?
|
|
30
34
|
|
|
@@ -33,19 +37,27 @@ module Samagotchi
|
|
|
33
37
|
# What the model reads instead of the output, after the output's
|
|
34
38
|
# "[name]" lead.
|
|
35
39
|
def stub
|
|
36
|
-
kind
|
|
40
|
+
case kind
|
|
41
|
+
when :forget then "(forgotten) #{note}"
|
|
42
|
+
when :drop then LLMContextEdit::DROP_STUB
|
|
43
|
+
else note.to_s
|
|
44
|
+
end
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
# The saved form, without the id (it is the key).
|
|
40
48
|
def to_h
|
|
41
49
|
saved = { "kind" => kind.to_s, "note" => note, "by" => by, "staged_at" => staged_at, "applied_at" => applied_at }
|
|
42
50
|
saved = saved.merge("keep" => keep) if keep?
|
|
43
|
-
|
|
51
|
+
saved = saved.merge("keep_offset" => keep_offset) unless keep_offset.zero?
|
|
52
|
+
was ? saved.merge("was" => was) : saved
|
|
44
53
|
end
|
|
45
54
|
end
|
|
46
55
|
|
|
47
56
|
class LLMContextEdit
|
|
48
|
-
KINDS = %i[stale forget].freeze
|
|
57
|
+
KINDS = %i[stale forget drop].freeze
|
|
58
|
+
# A user's drop is sent whatever the strategy (no layer names it).
|
|
59
|
+
DROP = :drop
|
|
60
|
+
DROP_STUB = "(dropped by the user) — read it again if the task needs it"
|
|
49
61
|
|
|
50
62
|
# +hash+ as saved (string or symbol keys), or nil for one of no known
|
|
51
63
|
# kind.
|
|
@@ -58,7 +70,8 @@ module Samagotchi
|
|
|
58
70
|
|
|
59
71
|
new(id: id.to_s, kind: kind, note: field.call(:note).to_s, by: field.call(:by), staged_at: field.call(:staged_at),
|
|
60
72
|
applied_at: field.call(:applied_at), keep: ranges(field.call(:keep)),
|
|
61
|
-
keep_offset: field.call(:keep_offset).is_a?(Integer) ? [field.call(:keep_offset), 0].max : 0
|
|
73
|
+
keep_offset: field.call(:keep_offset).is_a?(Integer) ? [field.call(:keep_offset), 0].max : 0,
|
|
74
|
+
was: field.call(:was).is_a?(Hash) ? field.call(:was) : nil)
|
|
62
75
|
end
|
|
63
76
|
|
|
64
77
|
# Saved keep ranges as [[first, last], …]: pairs of positive Integers,
|
|
@@ -87,6 +100,15 @@ module Samagotchi
|
|
|
87
100
|
entry
|
|
88
101
|
end
|
|
89
102
|
|
|
103
|
+
# Whether +entry+ holds a saved drop (applied or not): the cheap check
|
|
104
|
+
# LLMContextView#messages makes under none.
|
|
105
|
+
def self.drop_on?(entry)
|
|
106
|
+
saved = entry[:edits]
|
|
107
|
+
saved.is_a?(Hash) && saved.each_value.any? do |hash|
|
|
108
|
+
hash.is_a?(Hash) && (hash["kind"] || hash[:kind]).to_s == "drop"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
90
112
|
# The edits saved on +entry+, by id.
|
|
91
113
|
# @return [Hash{String => LLMContextEdit}]
|
|
92
114
|
def self.on(entry)
|
|
@@ -9,6 +9,7 @@ require_relative "llm_context_stale"
|
|
|
9
9
|
require_relative "llm_context_apply"
|
|
10
10
|
require_relative "llm_context_view"
|
|
11
11
|
require_relative "tools/forget_outputs"
|
|
12
|
+
require_relative "working_set_steer"
|
|
12
13
|
|
|
13
14
|
module Samagotchi
|
|
14
15
|
# The forget layer (LLMContextStrategy :forget, experimental): the
|
|
@@ -33,7 +34,9 @@ module Samagotchi
|
|
|
33
34
|
# read (CLM's lesson);
|
|
34
35
|
# - a keep that holds all of the output, an output too small for a stub
|
|
35
36
|
# to free anything (SMALL), an output already stubbed or forgotten, an
|
|
36
|
-
# id nothing in context has
|
|
37
|
+
# id nothing in context has;
|
|
38
|
+
# - an output the user dropped (WorkingSetSteer): neither forgotten nor
|
|
39
|
+
# restored by the model, in WorkingSetSteer::Refused's words.
|
|
37
40
|
# A read is never restored: reading the file again is its restore, and
|
|
38
41
|
# the file may have changed.
|
|
39
42
|
module LLMContextForget
|
|
@@ -123,7 +126,10 @@ module Samagotchi
|
|
|
123
126
|
# the model's latest outputs.
|
|
124
127
|
def refusal(conversation, output, keep, last, protect_steps, kept_reads)
|
|
125
128
|
return "no output with that id in context (ids look like t42)" unless output
|
|
126
|
-
|
|
129
|
+
|
|
130
|
+
held = LLMContextEdit.on(conversation[output.index])[output.ref.id]
|
|
131
|
+
return WorkingSetSteer::Refused.text(:dropped) if held&.kind == LLMContextEdit::DROP
|
|
132
|
+
return "already forgotten or stubbed" if held
|
|
127
133
|
if protect_steps.positive? && output.step >= last - protect_steps
|
|
128
134
|
return "from your last #{protect_steps} step#{"s" unless protect_steps == 1}; you may still need it, forget it later"
|
|
129
135
|
end
|
|
@@ -172,7 +178,10 @@ module Samagotchi
|
|
|
172
178
|
ids.each do |id|
|
|
173
179
|
output = outputs[id]
|
|
174
180
|
edit = output && LLMContextEdit.on(conversation[output.index])[id]
|
|
175
|
-
if edit
|
|
181
|
+
if edit&.kind == LLMContextEdit::DROP
|
|
182
|
+
# The user's: only the user takes it back (/working-set undrop).
|
|
183
|
+
refused[id] = WorkingSetSteer::Refused.text(:dropped)
|
|
184
|
+
elsif edit.nil? || edit.kind != :forget
|
|
176
185
|
refused[id] = "not forgotten"
|
|
177
186
|
elsif output.read? && edit.applied?
|
|
178
187
|
refused[id] = "a read: read the file again instead (it may have changed)"
|
|
@@ -5,6 +5,7 @@ require_relative "tool_response"
|
|
|
5
5
|
require_relative "tool_ids"
|
|
6
6
|
require_relative "llm_context_edit"
|
|
7
7
|
require_relative "llm_context_stale"
|
|
8
|
+
require_relative "llm_context_strategy"
|
|
8
9
|
|
|
9
10
|
module Samagotchi
|
|
10
11
|
# What the model is sent of the stored conversation: the one step
|
|
@@ -15,11 +16,14 @@ module Samagotchi
|
|
|
15
16
|
#
|
|
16
17
|
# Under +none+ (the default) nothing changes: #messages returns the
|
|
17
18
|
# conversation it was given, the same Array of the same entries, so the
|
|
18
|
-
# prompt is byte for byte what it was before the view existed.
|
|
19
|
+
# prompt is byte for byte what it was before the view existed. The one
|
|
20
|
+
# exception is the user's layer: an applied :drop (WorkingSetSteer, the
|
|
21
|
+
# working set's drop) is sent as its stub under every strategy, none
|
|
22
|
+
# too; a conversation without one is still returned as it is.
|
|
19
23
|
#
|
|
20
|
-
# Under a strategy of layers (:stale, :forget), each
|
|
21
|
-
#
|
|
22
|
-
# layers sends that run's output as its stub. The entry stays where it
|
|
24
|
+
# Under a strategy of layers (:stale, :forget), each edit saved on a
|
|
25
|
+
# tool_response entry (LLMContextEdit) that #sends? (applied, and its
|
|
26
|
+
# kind one of the layers) sends that run's output as its stub. The entry stays where it
|
|
23
27
|
# is, with its role and tool_call_id, so a call keeps its response and
|
|
24
28
|
# the image plan (which indexes by entry) lines up. A stub keeps the
|
|
25
29
|
# output's "[name]" lead (Gemma's prompt writes response:NAME from it),
|
|
@@ -44,7 +48,10 @@ module Samagotchi
|
|
|
44
48
|
# (re-reading the file is a read's restore).
|
|
45
49
|
class LLMContextView
|
|
46
50
|
NONE = :none
|
|
47
|
-
LAYERS
|
|
51
|
+
# The strategy's layers (LLMContextStrategy::LAYERS), not the edit
|
|
52
|
+
# kinds (LLMContextEdit::KINDS): a kind no strategy names never
|
|
53
|
+
# becomes a layer, whatever a caller passes as the strategy.
|
|
54
|
+
LAYERS = LLMContextStrategy::LAYERS
|
|
48
55
|
|
|
49
56
|
# @return [Array<Symbol>] the layers; empty under none
|
|
50
57
|
attr_reader :layers
|
|
@@ -78,7 +85,7 @@ module Samagotchi
|
|
|
78
85
|
# @param conversation [Array<Hash>] the stored entries, in order
|
|
79
86
|
# @return [Array<Hash>] the entries to format
|
|
80
87
|
def messages(conversation)
|
|
81
|
-
return conversation if none?
|
|
88
|
+
return conversation if none? && conversation.none? { |entry| LLMContextEdit.drop_on?(entry) }
|
|
82
89
|
|
|
83
90
|
group = Group.new
|
|
84
91
|
group.paired = forget? ? LLMContextStale.paired_runs(conversation).to_set { |run| run.ref.id } : Set.new
|
|
@@ -87,6 +94,11 @@ module Samagotchi
|
|
|
87
94
|
|
|
88
95
|
def forget? = layers.include?(:forget)
|
|
89
96
|
|
|
97
|
+
# Whether +edit+ (an LLMContextEdit) is sent: the one rule for which
|
|
98
|
+
# edits the view applies, applied and of one of the layers, or the
|
|
99
|
+
# user's drop (always on: no strategy names it).
|
|
100
|
+
def sends?(edit) = edit.applied? && (edit.kind == LLMContextEdit::DROP || layers.include?(edit.kind))
|
|
101
|
+
|
|
90
102
|
# The forget whose stub carries the note the next forgotten output of
|
|
91
103
|
# the same call points to: its first id and its note, author and stamp.
|
|
92
104
|
# +paired+: the ids of the runs that pair with their calls.
|
|
@@ -100,7 +112,7 @@ module Samagotchi
|
|
|
100
112
|
def edited(conversation, entry, index, group)
|
|
101
113
|
return entry unless entry[:role].to_s == "tool_response"
|
|
102
114
|
|
|
103
|
-
edits = LLMContextEdit.on(entry).select { |_id, edit|
|
|
115
|
+
edits = LLMContextEdit.on(entry).select { |_id, edit| sends?(edit) }
|
|
104
116
|
return entry if edits.empty? && !forget?
|
|
105
117
|
|
|
106
118
|
refs = ToolIds.refs_at(conversation, index)
|
|
@@ -259,6 +259,8 @@ module Samagotchi
|
|
|
259
259
|
@source_owned = prepared.owned
|
|
260
260
|
@source_commit = prepared.commit
|
|
261
261
|
@manifest = Manifest.read(dir: @bundle_dir)
|
|
262
|
+
clash = @manifest.session_settings_clash
|
|
263
|
+
raise InstallError, clash if clash
|
|
262
264
|
rescue SourceNormalizer::UnknownSourceError => e
|
|
263
265
|
raise InstallError, "Source normalization failed: #{e.message}"
|
|
264
266
|
rescue Manifest::ValidationError => e
|
|
@@ -93,6 +93,17 @@ module Samagotchi
|
|
|
93
93
|
@session_setting_types = self.class.parse_session_setting_types(raw["session_settings"])
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Why the install refuses this bundle's session_settings: a key whose
|
|
97
|
+
# NAME.KEY is chi's own session setting (SessionConfig.clash, the
|
|
98
|
+
# words chi bundle check reports too), nil when none clashes.
|
|
99
|
+
# @return [String, nil]
|
|
100
|
+
def session_settings_clash
|
|
101
|
+
return nil if @session_settings.empty?
|
|
102
|
+
|
|
103
|
+
require_relative "../session_config"
|
|
104
|
+
SessionConfig.clash(@name, @session_settings)
|
|
105
|
+
end
|
|
106
|
+
|
|
96
107
|
# A profile: it installs the bundles in includes instead of files.
|
|
97
108
|
def meta?
|
|
98
109
|
!@includes.empty?
|
|
@@ -171,11 +182,11 @@ module Samagotchi
|
|
|
171
182
|
File.write(File.join(dir, "manifest.yml"), YAML.dump(manifest))
|
|
172
183
|
end
|
|
173
184
|
|
|
174
|
-
# A setting key a session may set (
|
|
185
|
+
# A setting key a session may set (SessionConfig::KEY).
|
|
175
186
|
SESSION_SETTING = /\A[A-Za-z0-9_][A-Za-z0-9_-]*\z/
|
|
176
187
|
|
|
177
188
|
# The types a session setting may be given (session_settings: {key:
|
|
178
|
-
# type}), checked by
|
|
189
|
+
# type}), checked by SessionConfig.check!: number (or off),
|
|
179
190
|
# boolean, string; any (or a list's key) takes every scalar.
|
|
180
191
|
SESSION_SETTING_TYPES = %w[number boolean string any].freeze
|
|
181
192
|
|
|
@@ -187,6 +187,9 @@ module Samagotchi
|
|
|
187
187
|
mark[:staged] = true unless edit.applied?
|
|
188
188
|
if edit.kind == :stale
|
|
189
189
|
mark[:note] = edit.note[/superseded by .*\z/] || edit.note
|
|
190
|
+
elsif edit.kind == LLMContextEdit::DROP
|
|
191
|
+
# The user's: no note, no grouping (a forget's key is its note).
|
|
192
|
+
mark[:by] = edit.by
|
|
190
193
|
else
|
|
191
194
|
key = [edit.note, edit.by, edit.staged_at]
|
|
192
195
|
if first.key?(key)
|
|
@@ -201,12 +204,14 @@ module Samagotchi
|
|
|
201
204
|
mark
|
|
202
205
|
end
|
|
203
206
|
|
|
204
|
-
# +output+'s ✂ mark (+marks+ by +id+); an applied stale stub
|
|
205
|
-
# what it frees: its output, ~chars/4.
|
|
207
|
+
# +output+'s ✂ mark (+marks+ by +id+); an applied stale stub or a
|
|
208
|
+
# user's drop says what it frees: its output, ~chars/4.
|
|
209
|
+
WHOLE_STUBS = %w[stale drop].freeze
|
|
210
|
+
|
|
206
211
|
def mark_of(id, marks, output)
|
|
207
212
|
mark = marks[id]
|
|
208
213
|
return nil unless mark
|
|
209
|
-
return mark unless mark[:kind]
|
|
214
|
+
return mark unless WHOLE_STUBS.include?(mark[:kind]) && !mark[:staged] && output
|
|
210
215
|
|
|
211
216
|
mark.merge(tokens: (output.length / TokenUsage::CHARS_PER_TOKEN).ceil)
|
|
212
217
|
end
|
|
@@ -104,7 +104,7 @@ module Samagotchi
|
|
|
104
104
|
|
|
105
105
|
# config.yml `bundles: <bundle>:` with the session's own values for
|
|
106
106
|
# the bundle laid over it (chi --set BUNDLE.KEY=VALUE, through the
|
|
107
|
-
# Host's setup: SessionSetup#
|
|
107
|
+
# Host's setup: SessionSetup#settings's bundles), string keys, frozen.
|
|
108
108
|
# Plugin init and chi.ctx run before the session is bound: with no
|
|
109
109
|
# session id yet it is the config alone, and it is built again once
|
|
110
110
|
# per session id (the session's values never change after it starts).
|
|
@@ -473,7 +473,7 @@ module Samagotchi
|
|
|
473
473
|
# The session's own values for this bundle ({} without any; nil when
|
|
474
474
|
# the Host couldn't tell them, so #settings can retry).
|
|
475
475
|
def session_values(id)
|
|
476
|
-
values = @host.setup&.call&.
|
|
476
|
+
values = @host.setup&.call&.settings&.bundles
|
|
477
477
|
values.is_a?(Hash) ? values.fetch(@bundle, {}) : {}
|
|
478
478
|
rescue StandardError => e
|
|
479
479
|
unless (@session_settings_unread ||= {}).key?(id)
|
data/lib/samagotchi/prompt.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Samagotchi
|
|
|
27
27
|
# - Generation cue: <|im_start|>assistant\n
|
|
28
28
|
module Prompt
|
|
29
29
|
# Tail system messages whose text isn't the harness's own.
|
|
30
|
-
ESCAPED_KINDS = %w[note turn_note context].freeze
|
|
30
|
+
ESCAPED_KINDS = %w[note turn_note context working_set].freeze
|
|
31
31
|
|
|
32
32
|
# @param messages [Array<Hash>] each element has :role and :content.
|
|
33
33
|
# Recognised roles: "system", "user", "model", "tool_response".
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# /recap on|off|default, the setting form of /recap (the bare /recap,
|
|
7
|
+
# which shows the recap, is the UIs' own): this session's own
|
|
8
|
+
# recap.enabled (Engine#recap_override=), the same value
|
|
9
|
+
# `chi --set recap.enabled=…` saves, applied at once (the idle recap job
|
|
10
|
+
# is rebuilt or dropped). `default` removes it, and config.yml (or env)
|
|
11
|
+
# decides again. The session's value beats config.yml's: `/recap on`
|
|
12
|
+
# turns it on under `recap: false` too. It doesn't save: its host does
|
|
13
|
+
# on the reply's changed: [:recap] (Worker#run_command).
|
|
14
|
+
class RecapCommand
|
|
15
|
+
NAME = "/recap"
|
|
16
|
+
CHOICES = { "on" => true, "off" => false, "default" => nil }.freeze
|
|
17
|
+
USAGE = "usage: #{NAME} [on|off|default]".freeze
|
|
18
|
+
|
|
19
|
+
# @param engine [Engine] #session, #recap_override=, #recap
|
|
20
|
+
def initialize(engine:)
|
|
21
|
+
@engine = engine
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param args [String] what follows /recap
|
|
25
|
+
# @return [Array(String, Boolean, Symbol)] the reply, whether this
|
|
26
|
+
# session's recap setting changed, and :ok or :error
|
|
27
|
+
def run(args)
|
|
28
|
+
word = args.to_s.strip.downcase
|
|
29
|
+
return [USAGE, false, :error] unless CHOICES.key?(word)
|
|
30
|
+
return ["no session yet", false, :error] unless @engine.session
|
|
31
|
+
|
|
32
|
+
value = CHOICES.fetch(word)
|
|
33
|
+
changed = @engine.recap_override != value
|
|
34
|
+
@engine.recap_override = value if changed
|
|
35
|
+
[status_line(value), changed, :ok]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# "recap: off (this session; config.yml: on)", "recap: on (config.yml)".
|
|
39
|
+
# @param own [Boolean, nil] the session's own value, nil for none
|
|
40
|
+
def status_line(own)
|
|
41
|
+
from_config = self.class.config_status
|
|
42
|
+
line = if own.nil?
|
|
43
|
+
"recap: #{word(from_config[:on])} (#{from_config[:source]})"
|
|
44
|
+
else
|
|
45
|
+
"recap: #{word(own)} (this session; #{from_config[:source]}: #{word(from_config[:on])})"
|
|
46
|
+
end
|
|
47
|
+
# On by its own word, yet no job: a pinned recap host or model that
|
|
48
|
+
# doesn't resolve (warned in the log), or an embedder's recap: false.
|
|
49
|
+
on = own.nil? ? from_config[:on] : own
|
|
50
|
+
on && @engine.recap.nil? ? "#{line}; no recap is written: its host or model doesn't resolve (see the log)" : line
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Whether the recap is on without the session's own value: off by a
|
|
54
|
+
# scalar `recap: false`, `recap: {enabled: false}` or
|
|
55
|
+
# SAMAGOTCHI_RECAP_ENABLED=false (ConfigFile.recap_config sees all
|
|
56
|
+
# three), else on (the default).
|
|
57
|
+
# @return [Hash] {on: Boolean, source: "config.yml" or "env"}
|
|
58
|
+
def self.config_status
|
|
59
|
+
on = ConfigFile.recap_config != false
|
|
60
|
+
_value, origin = Config.get_with_origin("recap.enabled")
|
|
61
|
+
{ on: on, source: origin == :env ? "env" : "config.yml" }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def word(enabled) = enabled ? "on" : "off"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -7,8 +7,10 @@ require_relative "idle_target"
|
|
|
7
7
|
require_relative "recap_store"
|
|
8
8
|
|
|
9
9
|
module Samagotchi
|
|
10
|
-
# Builds (or disables) an Engine's idle recap job from the `recap:` kwarg
|
|
11
|
-
# and the Config registry.
|
|
10
|
+
# Builds (or disables) an Engine's idle recap job from the `recap:` kwarg,
|
|
11
|
+
# the session's own settings and the Config registry. The Engine builds
|
|
12
|
+
# one at start, and again when it binds a session whose own recap values
|
|
13
|
+
# differ (Engine#session=).
|
|
12
14
|
module RecapSetup
|
|
13
15
|
# Build (or disable) the idle recap job. On by default: with no recap
|
|
14
16
|
# host or model configured it asks the session's current model on its
|
|
@@ -18,9 +20,10 @@ module Samagotchi
|
|
|
18
20
|
# --model goes; an incomplete one warns and leaves recap off.
|
|
19
21
|
#
|
|
20
22
|
# Single precedence path: explicit `recap:` kwarg > Config registry
|
|
21
|
-
# (CLI > ENV > file > default). An explicit disable
|
|
22
|
-
# the kwarg or in the config file, `recap: {enabled:
|
|
23
|
-
# SAMAGOTCHI_RECAP_ENABLED=false)
|
|
23
|
+
# (session > CLI > ENV > file > default). An explicit disable
|
|
24
|
+
# (`recap: false` as the kwarg or in the config file, `recap: {enabled:
|
|
25
|
+
# false}`, or SAMAGOTCHI_RECAP_ENABLED=false) wins, but over a session's
|
|
26
|
+
# own recap.enabled (chi --set recap.enabled=true) only as the kwarg.
|
|
24
27
|
#
|
|
25
28
|
# @param recap [Hash, false, nil] the Engine's `recap:` kwarg
|
|
26
29
|
# @param engine [Engine] the recap job's engine
|
|
@@ -28,13 +31,22 @@ module Samagotchi
|
|
|
28
31
|
# @param session_target [#call] → the session's current model as a recap target
|
|
29
32
|
# @param session_id [#call] → String, nil
|
|
30
33
|
# @param state_dir [#call] → String
|
|
34
|
+
# @param session_config [#call] → the session's own core settings
|
|
35
|
+
# (SessionConfig::Settings#core), read once here
|
|
31
36
|
# @return [IdleRecap, nil]
|
|
32
|
-
def self.build(recap, engine:, host_registry:, session_target:, session_id:, state_dir:)
|
|
37
|
+
def self.build(recap, engine:, host_registry:, session_target:, session_id:, state_dir:, session_config: -> { {} })
|
|
33
38
|
return nil if recap == false
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
|
|
40
|
+
session = session_config.call || {}
|
|
41
|
+
# The session's own recap.enabled replaces both checks below.
|
|
42
|
+
if session.key?("recap.enabled")
|
|
43
|
+
return nil unless session["recap.enabled"]
|
|
44
|
+
else
|
|
45
|
+
# The REPL and a worker pass nothing: the config is read here alone
|
|
46
|
+
# (a scalar `recap: false` is only seen this way).
|
|
47
|
+
return nil if recap.nil? && ConfigFile.recap_config == false
|
|
48
|
+
return nil if Samagotchi::Config.get("recap.enabled") == false
|
|
49
|
+
end
|
|
38
50
|
|
|
39
51
|
# An explicit kwarg (a spec, an embedder) over the registry.
|
|
40
52
|
kwarg_config = recap.is_a?(Hash) ? recap : {}
|
|
@@ -64,7 +76,7 @@ module Samagotchi
|
|
|
64
76
|
inactivity: recap_number_setting(kwarg_config, :inactivity, "recap.inactivity", IdleRecap::DEFAULT_INACTIVITY_SECONDS, :float),
|
|
65
77
|
min_user_turns: recap_number_setting(kwarg_config, :min_user_turns, "recap.min_user_turns", IdleRecap::DEFAULT_MIN_USER_TURNS, :int),
|
|
66
78
|
timeout: recap_number_setting(kwarg_config, :timeout, "recap.timeout", IdleRecap::DEFAULT_TIMEOUT_SECONDS, :float),
|
|
67
|
-
sentences: recap_sentences(string_config(kwarg_config, :sentences) || registry_string("recap.sentences")),
|
|
79
|
+
sentences: recap_sentences(string_config(kwarg_config, :sentences) || registry_string("recap.sentences", session: session)),
|
|
68
80
|
store: RecapStore.new(session_id_lookup: -> { session_id.call }, state_dir_lookup: -> { state_dir.call }),
|
|
69
81
|
session_id: -> { session_id.call },
|
|
70
82
|
on_usage: ->(usage) { engine.note_side_usage(usage) }
|
|
@@ -108,9 +120,10 @@ module Samagotchi
|
|
|
108
120
|
end
|
|
109
121
|
private_class_method :recap_sentences
|
|
110
122
|
|
|
111
|
-
# Read a scalar recap setting via the Config registry (
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
# Read a scalar recap setting via the Config registry (session > CLI >
|
|
124
|
+
# ENV > file > default).
|
|
125
|
+
def self.registry_string(key, session: nil)
|
|
126
|
+
value = Samagotchi::Config.get(key, session: session)
|
|
114
127
|
value = value.to_s.strip
|
|
115
128
|
value.empty? ? nil : value
|
|
116
129
|
rescue StandardError
|
|
@@ -8,7 +8,7 @@ require_relative "session_commands"
|
|
|
8
8
|
require_relative "parent_answers_command"
|
|
9
9
|
require_relative "session_inbox"
|
|
10
10
|
require_relative "session_manager"
|
|
11
|
-
require_relative "
|
|
11
|
+
require_relative "session_config"
|
|
12
12
|
require_relative "session_chain"
|
|
13
13
|
require_relative "context_quote"
|
|
14
14
|
require_relative "reply_wait"
|
|
@@ -46,7 +46,7 @@ module Samagotchi
|
|
|
46
46
|
|
|
47
47
|
USAGE = <<~TEXT
|
|
48
48
|
Usage: chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...
|
|
49
|
-
chi send --new [--dir DIR] [--model M] [--llm-context LAYERS] [--thinking LEVEL] [--toolkit NAME] [--set
|
|
49
|
+
chi send --new [--dir DIR] [--model M] [--llm-context LAYERS] [--thinking LEVEL] [--toolkit NAME] [--set KEY=VALUE]... [--parent-answers LEVEL] [-m TEXT] [--image PATH]...
|
|
50
50
|
chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT] [--image PATH]...
|
|
51
51
|
chi send --wait [--timeout S] [--format json] [-m TEXT] [--image PATH]... (--new | ID)
|
|
52
52
|
chi send --queue --wait [--timeout S] [--format json] [-m TEXT] ID
|
|
@@ -92,12 +92,15 @@ module Samagotchi
|
|
|
92
92
|
(full, lean, child, a config or bundle one; default
|
|
93
93
|
config's toolkit.default), and tools declared on top
|
|
94
94
|
of it or left out (repeatable, a comma list too)
|
|
95
|
-
--set
|
|
96
|
-
(--new) a
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
95
|
+
--set KEY=VALUE
|
|
96
|
+
(--new) a setting for this session, as chi --set:
|
|
97
|
+
a core key a session may set (recap.enabled,
|
|
98
|
+
recap.sentences), typed by it, or BUNDLE.KEY over
|
|
99
|
+
config.yml's bundles: (a key its bundle's manifest
|
|
100
|
+
lists in session_settings:; numbers (3, .5) and
|
|
101
|
+
booleans (true, off, no) are typed, the rest is
|
|
102
|
+
text); repeatable: --set recap.enabled=false,
|
|
103
|
+
--set cost-cap.cap=3
|
|
101
104
|
--parent-answers LEVEL
|
|
102
105
|
(--new) what a parent agent may answer on its
|
|
103
106
|
questions: hard, standard or soft, only stricter than
|
|
@@ -372,8 +375,8 @@ module Samagotchi
|
|
|
372
375
|
toolkit = Toolkit.from_flags(name: options[:toolkit], add: options[:tools], remove: options[:no_tools])
|
|
373
376
|
options[:toolkit_value] = toolkit if toolkit
|
|
374
377
|
begin
|
|
375
|
-
options[:
|
|
376
|
-
rescue
|
|
378
|
+
options[:settings] = SessionConfig.from_flags(options[:sets]) unless Array(options[:sets]).empty?
|
|
379
|
+
rescue SessionConfig::Error => e
|
|
377
380
|
return usage_error(e.message)
|
|
378
381
|
end
|
|
379
382
|
|
|
@@ -426,7 +429,7 @@ module Samagotchi
|
|
|
426
429
|
setup: SessionSetup.new(llm_context: options[:llm_context],
|
|
427
430
|
thinking: options[:thinking],
|
|
428
431
|
toolkit: options[:toolkit_value],
|
|
429
|
-
|
|
432
|
+
settings: options[:settings],
|
|
430
433
|
parent_answers: options[:parent_answers]),
|
|
431
434
|
state_dir: @state_dir)
|
|
432
435
|
end
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -213,16 +213,19 @@ module Samagotchi
|
|
|
213
213
|
@toolkit = value.is_a?(Toolkit) ? value : Toolkit.from_file(value)
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
-
# The session's own
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
|
|
216
|
+
# The session's own settings (chi --set), a SessionConfig::Settings:
|
|
217
|
+
# empty for none and for a session from before them. Its core part
|
|
218
|
+
# ({key => value}: recap.enabled) is the file's config_settings, its
|
|
219
|
+
# bundles part ({bundle => {key => value}}) the file's bundle_settings.
|
|
220
|
+
# Given when it was created; a respawn, a continue and a fork keep them
|
|
221
|
+
# (SessionSetup.of), a delegate child doesn't. Plugin::Context#settings
|
|
222
|
+
# lays the bundles part over config.yml's; the core part goes first in
|
|
223
|
+
# Config.get(key, session:) (RecapSetup).
|
|
224
|
+
attr_reader :settings
|
|
222
225
|
|
|
223
|
-
# @param value [
|
|
224
|
-
def
|
|
225
|
-
@
|
|
226
|
+
# @param value [SessionConfig::Settings, nil]
|
|
227
|
+
def settings=(value)
|
|
228
|
+
@settings = SessionConfig::Settings.of(value)
|
|
226
229
|
end
|
|
227
230
|
|
|
228
231
|
# The session's own guardrails.parent_answers (hard|standard|soft), nil
|
|
@@ -280,7 +283,7 @@ module Samagotchi
|
|
|
280
283
|
preloaded_memory_names: [], muted_memory_names: [], parent_id: nil, scratch: false,
|
|
281
284
|
last_turn: nil, model_typed: nil, delegate: false, llm_context: nil, prompt_notes: [],
|
|
282
285
|
continues: nil, thinking: nil, reasoning_history: nil, appended_prompt_files: [],
|
|
283
|
-
toolkit: nil,
|
|
286
|
+
toolkit: nil, settings: nil, interrupted_turn: nil, parent_answers: nil, title: nil)
|
|
284
287
|
@id = id
|
|
285
288
|
@metadata_version = metadata_version
|
|
286
289
|
@mode = mode
|
|
@@ -311,7 +314,7 @@ module Samagotchi
|
|
|
311
314
|
self.reasoning_history = reasoning_history
|
|
312
315
|
self.appended_prompt_files = appended_prompt_files
|
|
313
316
|
self.toolkit = toolkit
|
|
314
|
-
self.
|
|
317
|
+
self.settings = settings
|
|
315
318
|
self.parent_answers = parent_answers
|
|
316
319
|
self.interrupted_turn = interrupted_turn
|
|
317
320
|
@turn_checkpoint = nil
|
|
@@ -410,7 +413,7 @@ module Samagotchi
|
|
|
410
413
|
reasoning_history: setup.reasoning_history,
|
|
411
414
|
appended_prompt_files: setup.appended_prompt_files,
|
|
412
415
|
toolkit: setup.toolkit,
|
|
413
|
-
|
|
416
|
+
settings: setup.settings,
|
|
414
417
|
parent_answers: setup.parent_answers,
|
|
415
418
|
continues: continues
|
|
416
419
|
)
|
|
@@ -479,6 +482,7 @@ module Samagotchi
|
|
|
479
482
|
"appended_prompt_files" => [],
|
|
480
483
|
"toolkit" => nil,
|
|
481
484
|
"bundle_settings" => {},
|
|
485
|
+
"config_settings" => {},
|
|
482
486
|
"interrupted_turn" => nil,
|
|
483
487
|
"parent_answers" => nil
|
|
484
488
|
}.freeze
|
|
@@ -490,6 +494,7 @@ module Samagotchi
|
|
|
490
494
|
attrs = FIELDS.to_h do |key, default|
|
|
491
495
|
[key.to_sym, default.equal?(REQUIRED) ? data.fetch(key) : data.fetch(key, default)]
|
|
492
496
|
end
|
|
497
|
+
attrs[:settings] = SessionConfig::Settings.new(core: attrs.delete(:config_settings), bundles: attrs.delete(:bundle_settings))
|
|
493
498
|
attrs[:messages] = messages ? Array(attrs[:messages]).map { |msg| symbolize_message_keys(msg) } : []
|
|
494
499
|
new(**attrs)
|
|
495
500
|
end
|
|
@@ -618,6 +623,8 @@ module Samagotchi
|
|
|
618
623
|
when "thinking" then thinking_file
|
|
619
624
|
when "reasoning_history" then reasoning_history_file
|
|
620
625
|
when "toolkit" then @toolkit&.to_file
|
|
626
|
+
when "bundle_settings" then @settings.bundles
|
|
627
|
+
when "config_settings" then @settings.core
|
|
621
628
|
when "prompt_notes" then @prompt_notes.map(&:to_file)
|
|
622
629
|
when "last_turn" then @last_turn&.to_file
|
|
623
630
|
when "used_memory_names", "preloaded_memory_names", "muted_memory_names", "appended_prompt_files"
|