samagotchi 0.4.0 → 0.5.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 +80 -1
- data/README.md +13 -2
- data/bin/chi +29 -39
- data/docs/cli.md +135 -73
- data/docs/configuration.md +15 -20
- data/docs/hooks.md +1 -1
- data/docs/memory.md +40 -0
- data/docs/plugins.md +50 -0
- data/docs/releasing.md +9 -6
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
- data/lib/samagotchi/bridge.rb +16 -11
- data/lib/samagotchi/bundles/skills/manifest.yml +10 -0
- data/lib/samagotchi/bundles/skills/plugin.rb +419 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -8
- data/lib/samagotchi/bundles/system/identity.md +5 -0
- data/lib/samagotchi/bundles/system/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/memory_guide.md +26 -0
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/client.rb +16 -20
- data/lib/samagotchi/config.rb +40 -100
- data/lib/samagotchi/engine.rb +50 -354
- data/lib/samagotchi/kernel_loop.rb +33 -44
- data/lib/samagotchi/live_versions.rb +7 -1
- data/lib/samagotchi/llm/errors.rb +17 -0
- data/lib/samagotchi/llm/http.rb +4 -18
- data/lib/samagotchi/llm/openai_chat.rb +17 -0
- data/lib/samagotchi/model_profile.rb +4 -10
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/reply_wait.rb +48 -4
- data/lib/samagotchi/self_report.rb +20 -2
- data/lib/samagotchi/send_command.rb +84 -6
- data/lib/samagotchi/session.rb +4 -2
- data/lib/samagotchi/session_manager.rb +18 -37
- data/lib/samagotchi/system_prompt.rb +403 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +120 -83
- data/lib/samagotchi/terminal_ui/attached_view.rb +27 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +23 -7
- data/lib/samagotchi/terminal_ui/formatting.rb +32 -22
- data/lib/samagotchi/terminal_ui/input_support.rb +3 -4
- data/lib/samagotchi/terminal_ui/plain_surface.rb +13 -7
- data/lib/samagotchi/terminal_ui/status_row.rb +81 -0
- data/lib/samagotchi/terminal_ui/surface.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +95 -690
- data/lib/samagotchi/thinking.rb +11 -0
- data/lib/samagotchi/tool_activity.rb +52 -2
- data/lib/samagotchi/tool_runner.rb +3 -0
- data/lib/samagotchi/tools/execute.rb +3 -3
- data/lib/samagotchi/tools/output_guardrails.rb +8 -7
- data/lib/samagotchi/tools/read.rb +4 -4
- data/lib/samagotchi/update_command.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +170 -35
- data/lib/samagotchi/web/lan.rb +99 -0
- data/lib/samagotchi/web/message_parts.rb +15 -11
- data/lib/samagotchi/web/public/activity.js +7 -0
- data/lib/samagotchi/web/public/app.js +99 -54
- data/lib/samagotchi/web/public/chat_view.js +5 -1
- data/lib/samagotchi/web/public/index.html +163 -17
- data/lib/samagotchi/web/public/model_pick.js +136 -0
- data/lib/samagotchi/web/public/model_picker.js +224 -0
- data/lib/samagotchi/web/public/notify.js +10 -0
- data/lib/samagotchi/web/public/stage_model.js +110 -0
- data/lib/samagotchi/web/public/stage_view.js +580 -0
- data/lib/samagotchi/web/public/timing.js +6 -2
- data/lib/samagotchi/web/public/turn_events.js +9 -5
- data/lib/samagotchi/web/public/turn_model.js +11 -3
- data/lib/samagotchi/web/public/turn_view.js +74 -19
- data/lib/samagotchi/web/qr.rb +40 -0
- data/lib/samagotchi/web/server.rb +101 -11
- data/lib/samagotchi/web/token.rb +97 -0
- metadata +27 -3
- data/lib/samagotchi/terminal_ui/legacy_surface.rb +0 -111
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# The skills bundle (docs/plugins.md, The skills bundle): a skill is a memory
|
|
2
|
+
# named skill_<name> that holds the steps of a task done with the user
|
|
3
|
+
# (docs/memory.md, Skills). The system bundle's identity already tells the
|
|
4
|
+
# model to save, follow and update skills; this bundle adds `/skill` for the
|
|
5
|
+
# user and keeps an eye on the model's rewrites.
|
|
6
|
+
#
|
|
7
|
+
# History: before memory_write, write or edit changes a skill's file, the
|
|
8
|
+
# file as it was is kept under $XDG_STATE_HOME/samagotchi/plugins/skills/
|
|
9
|
+
# history/<scope>/<name>/<UTC time>.md (state, not the memories dir, so
|
|
10
|
+
# bundle build and dotfile syncs never see it), history_keep per skill. The
|
|
11
|
+
# after_tool_call of that call (calls run one at a time; that event carries
|
|
12
|
+
# no call, so the before side stashes it) compares the file on disk and
|
|
13
|
+
# shows a line: "skill release updated (+2 −1): …" or "skill release saved
|
|
14
|
+
# (project, 14 lines)".
|
|
15
|
+
#
|
|
16
|
+
# The nudge (nudge: true), for models that skip a failing step instead of
|
|
17
|
+
# fixing the skill: in a turn that read a skill (memory_read of a skill_*
|
|
18
|
+
# name, or a read of its file), the first failing tool call after it (an
|
|
19
|
+
# execute with "exit: N", N ≠ 0, or with no exit line an Error: line near
|
|
20
|
+
# the top; any tool's "[tool] Error: …") steers the model once to find out
|
|
21
|
+
# why and fix the skill. At the turn's end, a failed step with no rewrite of
|
|
22
|
+
# the skill gets a notice line.
|
|
23
|
+
#
|
|
24
|
+
# Settings (config.yml, bundles: skills:):
|
|
25
|
+
# history_keep: 20 older versions kept per skill
|
|
26
|
+
# nudge: true steer the model once when a skill's step fails
|
|
27
|
+
require "date"
|
|
28
|
+
require "fileutils"
|
|
29
|
+
|
|
30
|
+
class Plugin
|
|
31
|
+
WRITE_TOOLS = %w[memory_write write edit].freeze
|
|
32
|
+
NOTICE_WIDTH = 60 # the changed line in an update's notice
|
|
33
|
+
NUDGE = "A step of skill %s failed. Find out why before skipping it; if the skill is out of date, fix it now: " \
|
|
34
|
+
"memory_write the whole skill, its title and every section as they were, that step fixed, a Changelog " \
|
|
35
|
+
"line added."
|
|
36
|
+
NOT_FAILURES = %w[memory_read memory_write].freeze
|
|
37
|
+
USAGE = "usage: /skill save [name] [--system] | list | show <name> | diff <name> [N]"
|
|
38
|
+
|
|
39
|
+
def initialize(settings = {})
|
|
40
|
+
settings = {} unless settings.is_a?(Hash)
|
|
41
|
+
@history_keep = positive(settings["history_keep"]) || 20
|
|
42
|
+
@nudge = settings.key?("nudge") ? settings["nudge"] != false : true
|
|
43
|
+
@stash = nil
|
|
44
|
+
reset_turn
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def register(chi)
|
|
48
|
+
chi.on(:before_turn) { |_event, _ctx| reset_turn }
|
|
49
|
+
chi.on(:after_turn) { |_event, ctx| after_turn(ctx) }
|
|
50
|
+
chi.on(:before_tool_call) { |event, ctx| before_tool_call(event, ctx) }
|
|
51
|
+
chi.on(:after_tool_call) { |event, ctx| after_tool_call(event, ctx) }
|
|
52
|
+
chi.command "/skill", "skills (steps of a task we did): save [name] [--system], list, show <name>, diff <name> [N]",
|
|
53
|
+
anytime: true do |args, ctx|
|
|
54
|
+
command(args.to_s.strip, ctx)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# --- a skill's file changes ----------------------------------------------
|
|
61
|
+
|
|
62
|
+
def before_tool_call(event, ctx)
|
|
63
|
+
@stash = nil
|
|
64
|
+
tool = event.dig(:call, :name).to_s
|
|
65
|
+
note_read(tool, event)
|
|
66
|
+
return unless WRITE_TOOLS.include?(tool)
|
|
67
|
+
|
|
68
|
+
path = Array(event.dig(:targets, :paths)).find { |target| skill_at(target) }
|
|
69
|
+
return unless path
|
|
70
|
+
|
|
71
|
+
scope, name = skill_at(path)
|
|
72
|
+
old = File.file?(path) ? File.read(path) : nil
|
|
73
|
+
keep_version(ctx, scope, name, old) if old
|
|
74
|
+
@stash = { tool: tool, path: File.expand_path(path), scope: scope, name: name, old: old }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Success is the file changed on disk, whatever the output says. A call
|
|
78
|
+
# that didn't match the stash (a cancelled turn fires no before) drops it.
|
|
79
|
+
def after_tool_call(event, ctx)
|
|
80
|
+
stash = @stash
|
|
81
|
+
@stash = nil
|
|
82
|
+
step_failed(event) if @nudge && !@read.empty?
|
|
83
|
+
return unless stash && stash[:tool] == event[:tool].to_s
|
|
84
|
+
|
|
85
|
+
now = File.file?(stash[:path]) ? File.read(stash[:path]) : nil
|
|
86
|
+
return if now.nil? || now == stash[:old]
|
|
87
|
+
|
|
88
|
+
@written << stash[:name]
|
|
89
|
+
ctx.notify(change_notice(stash, now))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def change_notice(stash, now)
|
|
93
|
+
name = stash[:name]
|
|
94
|
+
return "skill #{name} saved (#{stash[:scope]}, #{now.lines.size} lines)" unless stash[:old]
|
|
95
|
+
|
|
96
|
+
ops = line_diff(stash[:old].lines(chomp: true), now.lines(chomp: true))
|
|
97
|
+
added = ops.count { |op, _| op == :add }
|
|
98
|
+
removed = ops.count { |op, _| op == :del }
|
|
99
|
+
first = ops.find { |op, _| op == :add } || ops.find { |op, _| op == :del }
|
|
100
|
+
line = first ? cut(first.last.strip) : ""
|
|
101
|
+
"skill #{name} updated (+#{added} −#{removed})#{line.empty? ? "" : ": #{line}"} · /skill diff #{name}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# --- the nudge ------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
def reset_turn
|
|
107
|
+
@read = [] # skills read this turn, in order
|
|
108
|
+
@written = [] # skills changed this turn
|
|
109
|
+
@failed = false # a step failed after a skill was read
|
|
110
|
+
@nudged = false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def note_read(tool, event)
|
|
114
|
+
names = case tool
|
|
115
|
+
when "memory_read"
|
|
116
|
+
event.dig(:call, :content).to_s.split(",").map(&:strip).select { |entry| entry.start_with?("skill_") }
|
|
117
|
+
.filter_map { |entry| skill_name(entry) }
|
|
118
|
+
when "read"
|
|
119
|
+
Array(event.dig(:targets, :paths)).filter_map { |path| skill_at(path)&.last }
|
|
120
|
+
else []
|
|
121
|
+
end
|
|
122
|
+
@read |= names
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def step_failed(event)
|
|
126
|
+
tool = event[:tool].to_s
|
|
127
|
+
return if NOT_FAILURES.include?(tool) || !failure?(tool, event[:output].to_s)
|
|
128
|
+
|
|
129
|
+
@failed = true
|
|
130
|
+
return if @nudged
|
|
131
|
+
|
|
132
|
+
@nudged = !!event[:steer]&.call(format(NUDGE, @read.join(", ")))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A tool error ("[x] Error: …" raised, "[x]\nError: …" returned), or an
|
|
136
|
+
# execute that exited non-zero; its "exit: N" line may be cut off by the
|
|
137
|
+
# hook's output cap, and then an Error: line near the top counts.
|
|
138
|
+
def failure?(tool, output)
|
|
139
|
+
return true if output.match?(/\A\[#{Regexp.escape(tool)}\](?: |\n)Error:/)
|
|
140
|
+
return false unless tool == "execute"
|
|
141
|
+
|
|
142
|
+
exit_line = output.match(/^exit: (\d+)(?: \(no output\))?\s*\z/)
|
|
143
|
+
return exit_line[1] != "0" if exit_line
|
|
144
|
+
|
|
145
|
+
output.lines.first(20).any? { |line| line.match?(/\A\s*Error:/i) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def after_turn(ctx)
|
|
149
|
+
return unless @nudge && @failed
|
|
150
|
+
|
|
151
|
+
missed = @read - @written
|
|
152
|
+
return unless missed.size == @read.size
|
|
153
|
+
|
|
154
|
+
ctx.notify("skill #{missed.join(", ")} was followed, a step failed, the skill wasn't updated")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# --- history ---------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
# Keep +content+ as the newest version, unless it is the newest already (a
|
|
160
|
+
# denied or failed write leaves the file as it was).
|
|
161
|
+
def keep_version(ctx, scope, name, content)
|
|
162
|
+
dir = history_dir(ctx, scope, name)
|
|
163
|
+
FileUtils.mkdir_p(dir)
|
|
164
|
+
newest = versions(dir).first
|
|
165
|
+
return if newest && File.read(newest) == content
|
|
166
|
+
|
|
167
|
+
stamp = Time.now.utc.strftime("%Y%m%dT%H%M%S.%6NZ")
|
|
168
|
+
path = File.join(dir, "#{stamp}.md")
|
|
169
|
+
File.write("#{path}.tmp", content)
|
|
170
|
+
File.rename("#{path}.tmp", path)
|
|
171
|
+
versions(dir).drop(@history_keep).each { |old| File.delete(old) }
|
|
172
|
+
rescue SystemCallError => e
|
|
173
|
+
ctx.log.warn(:history_failed, skill: name, error: e.class.name, msg: e.message)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# history/system/<name>, history/project-<project folder>/<name>
|
|
177
|
+
def history_dir(ctx, scope, name)
|
|
178
|
+
key = scope == "system" ? "system" : "project-#{File.basename(memory_dirs["project"])}"
|
|
179
|
+
File.join(ctx.data_dir, "history", key, name)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# The kept versions, newest first.
|
|
183
|
+
def versions(dir) = Dir.glob(File.join(dir, "*.md")).sort.reverse
|
|
184
|
+
|
|
185
|
+
# --- /skill ----------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def command(args, ctx)
|
|
188
|
+
verb, rest = args.split(/\s+/, 2)
|
|
189
|
+
rest = rest.to_s.strip
|
|
190
|
+
case verb
|
|
191
|
+
when "save" then save(rest, ctx)
|
|
192
|
+
when "list" then rest.empty? ? list : USAGE
|
|
193
|
+
when "show" then show(rest)
|
|
194
|
+
when "diff" then diff(rest, ctx)
|
|
195
|
+
else USAGE
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# /skill save [name] [--system]: asks the model, in this session, to save
|
|
200
|
+
# what was just done (the model has seen the commands; a side answer
|
|
201
|
+
# wouldn't). The request runs as a turn; sent while a turn runs it joins
|
|
202
|
+
# that turn at its next step, and the request says to finish the task
|
|
203
|
+
# first. A REPL session (--no-shared) takes no messages: the request is
|
|
204
|
+
# shown for the user to send.
|
|
205
|
+
def save(rest, ctx)
|
|
206
|
+
words = rest.split
|
|
207
|
+
scope = words.delete("--system") ? "system" : "project"
|
|
208
|
+
return USAGE if words.size > 1 || words.any? { |word| word.start_with?("-") }
|
|
209
|
+
|
|
210
|
+
name = words.first && skill_name(words.first)
|
|
211
|
+
return "/skill save: a name is letters, digits, _ and - (got #{words.first})" if words.first && !name
|
|
212
|
+
|
|
213
|
+
request = save_request(name, scope, exists: name && skill_path(name))
|
|
214
|
+
begin
|
|
215
|
+
ctx.sessions.send(ctx.session_id, request)
|
|
216
|
+
rescue Samagotchi::Plugin::Sessions::Error => e
|
|
217
|
+
return "/skill save: #{e.message}. Send this yourself:\n\n#{request}"
|
|
218
|
+
end
|
|
219
|
+
what = name ? "skill #{name}" : "a skill"
|
|
220
|
+
"asked chi to save #{what} (#{scope} scope); a running turn gets it at its next step"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def save_request(name, scope, exists:)
|
|
224
|
+
target = name ? "skill `skill_#{name}`" : "a skill named `skill_<name>` (a short name for the task)"
|
|
225
|
+
update = exists ? " It exists already: read it, keep what still holds, fix what changed, add a Changelog line." : ""
|
|
226
|
+
<<~TEXT.strip
|
|
227
|
+
Save what we just did as #{target} with memory_write, scope #{scope}.#{update} If you are still in the middle of the task, finish it first.
|
|
228
|
+
Content: plain Markdown, no frontmatter:
|
|
229
|
+
|
|
230
|
+
# Skill: #{name || "<name>"}
|
|
231
|
+
|
|
232
|
+
## Steps
|
|
233
|
+
1. …
|
|
234
|
+
## Gotchas
|
|
235
|
+
- …
|
|
236
|
+
## Changelog
|
|
237
|
+
- #{Date.today.iso8601} created
|
|
238
|
+
|
|
239
|
+
Steps are the commands and checks that worked, in order, with the real file and command names; a step that must pass says "stop if it fails". Dead ends and surprises go under Gotchas. Give memory_write a description: one line that starts with what this task is and names its main steps, in this task's own words, so the skill is found next time. Then show the skill briefly.
|
|
240
|
+
TEXT
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# /skill list: the skill_* memories of both scopes, with their index line's
|
|
244
|
+
# date and description.
|
|
245
|
+
def list
|
|
246
|
+
skills = memory_dirs.flat_map do |scope, dir|
|
|
247
|
+
index = index_lines(dir)
|
|
248
|
+
Dir.glob(File.join(dir, "skill_*.md")).filter_map do |path|
|
|
249
|
+
name = File.basename(path, ".md").delete_prefix("skill_")
|
|
250
|
+
next unless skill_name(name) == name
|
|
251
|
+
|
|
252
|
+
date, description = index.fetch("skill_#{name}", [nil, nil])
|
|
253
|
+
line = +"#{name} · #{scope}"
|
|
254
|
+
line << " · #{date}" if date
|
|
255
|
+
line << " — #{description}" if description
|
|
256
|
+
line
|
|
257
|
+
end.sort
|
|
258
|
+
end
|
|
259
|
+
return "no skills yet: after a task we did together, /skill save [name]" if skills.empty?
|
|
260
|
+
|
|
261
|
+
"skills:\n#{skills.map { |line| " #{line}" }.join("\n")}"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# /skill show <name>: the skill as saved (project first).
|
|
265
|
+
def show(rest)
|
|
266
|
+
name = skill_name(rest)
|
|
267
|
+
return USAGE unless name
|
|
268
|
+
|
|
269
|
+
path = skill_path(name) or return "no skill #{name} (/skill list shows them)"
|
|
270
|
+
"skill #{name} · #{scope_of(path)}\n\n#{File.read(path).strip}"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# /skill diff <name> [N]: the skill now against its N-th newest kept
|
|
274
|
+
# version (1, the one before the last change, by default), unified.
|
|
275
|
+
def diff(rest, ctx)
|
|
276
|
+
word, back = rest.split
|
|
277
|
+
name = skill_name(word)
|
|
278
|
+
n = back ? Integer(back, exception: false) : 1
|
|
279
|
+
return USAGE unless name && n&.positive? && rest.split.size <= 2
|
|
280
|
+
|
|
281
|
+
path = skill_path(name) or return "no skill #{name} (/skill list shows them)"
|
|
282
|
+
kept = versions(history_dir(ctx, scope_of(path), name))
|
|
283
|
+
return "skill #{name} has no older version yet" if kept.empty?
|
|
284
|
+
return "skill #{name} has #{kept.size} older version#{"s" if kept.size > 1} (/skill diff #{name} 1..#{kept.size})" if n > kept.size
|
|
285
|
+
|
|
286
|
+
old = kept[n - 1]
|
|
287
|
+
body = unified(File.read(old).lines(chomp: true), File.read(path).lines(chomp: true))
|
|
288
|
+
return "skill #{name} is the same as version #{n}" if body.empty?
|
|
289
|
+
|
|
290
|
+
"--- skill_#{name} (#{version_time(old)})\n+++ skill_#{name} (now)\n#{body}"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
CONTEXT = 3
|
|
294
|
+
|
|
295
|
+
# Hunks with CONTEXT lines around each change, as diff -u prints them.
|
|
296
|
+
def unified(a, b)
|
|
297
|
+
ops = line_diff(a, b)
|
|
298
|
+
changed = ops.each_index.reject { |k| ops[k].first == :eq }
|
|
299
|
+
return "" if changed.empty?
|
|
300
|
+
|
|
301
|
+
# Group changes whose context would touch into one hunk.
|
|
302
|
+
groups = changed.slice_when { |x, y| y - x > 2 * CONTEXT + 1 }.to_a
|
|
303
|
+
old_at = new_at = 0
|
|
304
|
+
positions = ops.map do |op, _|
|
|
305
|
+
at = [old_at, new_at]
|
|
306
|
+
old_at += 1 unless op == :add
|
|
307
|
+
new_at += 1 unless op == :del
|
|
308
|
+
at
|
|
309
|
+
end
|
|
310
|
+
groups.map do |group|
|
|
311
|
+
from = [group.first - CONTEXT, 0].max
|
|
312
|
+
to = [group.last + CONTEXT, ops.size - 1].min
|
|
313
|
+
slice = ops[from..to]
|
|
314
|
+
old_count = slice.count { |op, _| op != :add }
|
|
315
|
+
new_count = slice.count { |op, _| op != :del }
|
|
316
|
+
old_start, new_start = positions[from]
|
|
317
|
+
header = "@@ -#{range(old_start, old_count)} +#{range(new_start, new_count)} @@"
|
|
318
|
+
lines = slice.map { |op, line| "#{{ eq: " ", del: "-", add: "+" }[op]}#{line}" }
|
|
319
|
+
[header, *lines].join("\n")
|
|
320
|
+
end.join("\n")
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# diff -u's "start,count" (1-based; an empty side names the line before).
|
|
324
|
+
def range(start, count)
|
|
325
|
+
first = count.zero? ? start : start + 1
|
|
326
|
+
count == 1 ? first.to_s : "#{first},#{count}"
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# "2026-09-30 10:22 UTC" from a version's file name.
|
|
330
|
+
def version_time(path)
|
|
331
|
+
stamp = File.basename(path, ".md")
|
|
332
|
+
match = stamp.match(/\A(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)/)
|
|
333
|
+
match ? "#{match[1]}-#{match[2]}-#{match[3]} #{match[4]}:#{match[5]} UTC" : stamp
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# --- skills on disk --------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
# "release", "skill_release", "Release-Notes" → "release", "release-notes";
|
|
339
|
+
# nil for anything else.
|
|
340
|
+
def skill_name(word)
|
|
341
|
+
name = word.to_s.downcase.delete_suffix(".md").delete_prefix("skill_")
|
|
342
|
+
name.match?(/\A[a-z0-9][a-z0-9_-]*\z/) ? name : nil
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# The memories dir of each scope, as memory_read/memory_write resolve it.
|
|
346
|
+
def memory_dirs
|
|
347
|
+
%w[project system].to_h { |scope| [scope, File.expand_path(Samagotchi::Tools::MemoryRead.memories_dir(scope))] }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# The skill's file, project first (as memory_read looks), or nil.
|
|
351
|
+
def skill_path(name, scope: nil)
|
|
352
|
+
dirs = scope ? memory_dirs.slice(scope) : memory_dirs
|
|
353
|
+
dirs.each_value do |dir|
|
|
354
|
+
path = File.join(dir, "skill_#{name}.md")
|
|
355
|
+
return path if File.file?(path)
|
|
356
|
+
end
|
|
357
|
+
nil
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# [scope, name] when +path+ is a skill_<name>.md right in a memories dir.
|
|
361
|
+
def skill_at(path)
|
|
362
|
+
path = File.expand_path(path.to_s)
|
|
363
|
+
scope = memory_dirs.key(File.dirname(path))
|
|
364
|
+
name = File.basename(path, ".md").delete_prefix("skill_")
|
|
365
|
+
return nil unless scope && File.basename(path) == "skill_#{name}.md" && skill_name(name) == name
|
|
366
|
+
|
|
367
|
+
[scope, name]
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def scope_of(path) = memory_dirs.key(File.dirname(path)) || "?"
|
|
371
|
+
|
|
372
|
+
# The managed index.md lines, {name => [date, description]}:
|
|
373
|
+
# "- **name** · scope · date · bytes — description".
|
|
374
|
+
def index_lines(dir)
|
|
375
|
+
path = File.join(dir, "index.md")
|
|
376
|
+
return {} unless File.file?(path)
|
|
377
|
+
|
|
378
|
+
File.readlines(path, chomp: true).each_with_object({}) do |line, lines|
|
|
379
|
+
match = line.match(/\A- \*\*(?<name>[^*]+)\*\* · [^·]+ · (?<date>[^·]+?) · [^—]+?(?: — (?<description>.+))?\z/)
|
|
380
|
+
lines[match[:name]] = [match[:date].strip, match[:description]&.strip] if match
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# --- helpers ---------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
# The lines of +a+ and +b+ as [:eq|:del|:add, line], in order (a longest
|
|
387
|
+
# common subsequence; skills are short).
|
|
388
|
+
def line_diff(a, b)
|
|
389
|
+
lcs = Array.new(a.size + 1) { Array.new(b.size + 1, 0) }
|
|
390
|
+
(a.size - 1).downto(0) do |i|
|
|
391
|
+
(b.size - 1).downto(0) do |j|
|
|
392
|
+
lcs[i][j] = a[i] == b[j] ? lcs[i + 1][j + 1] + 1 : [lcs[i + 1][j], lcs[i][j + 1]].max
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
ops = []
|
|
396
|
+
i = j = 0
|
|
397
|
+
while i < a.size && j < b.size
|
|
398
|
+
if a[i] == b[j]
|
|
399
|
+
ops << [:eq, a[i]]
|
|
400
|
+
i += 1
|
|
401
|
+
j += 1
|
|
402
|
+
elsif lcs[i + 1][j] >= lcs[i][j + 1]
|
|
403
|
+
ops << [:del, a[i]]
|
|
404
|
+
i += 1
|
|
405
|
+
else
|
|
406
|
+
ops << [:add, b[j]]
|
|
407
|
+
j += 1
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
ops.concat(a[i..].map { |line| [:del, line] }, b[j..].map { |line| [:add, line] })
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def cut(text) = text.length > NOTICE_WIDTH ? "#{text[0, NOTICE_WIDTH - 1]}…" : text
|
|
414
|
+
|
|
415
|
+
def positive(value)
|
|
416
|
+
number = Integer(value.to_s, exception: false)
|
|
417
|
+
number&.positive? ? number : nil
|
|
418
|
+
end
|
|
419
|
+
end
|
|
@@ -18,7 +18,7 @@ Single registry `Samagotchi::Config` (`Config::ENTRIES` in `lib/samagotchi/confi
|
|
|
18
18
|
|
|
19
19
|
Sections forbid `_`/`-` (`SECTION_RE` `/\A[a-z0-9]+\z/`); leaves keep `snake_case` in YAML (`base_url`) and become kebab in CLI (`base-url`) via registry derivation — no generic string split, registry lookup avoids flat vs nested collision.
|
|
20
20
|
|
|
21
|
-
**Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/transport/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line
|
|
21
|
+
**Universal entries** (`expose: [:env,:config,:cli]`): `default.model`, `server.host/port/transport/open_timeout/read_timeout`, `server.first_token_timeout` (env and config only), `recap.model/base_url/host_ref/inactivity/timeout/min_user_turns/sentences`, `session.retention_days/max_count/keep_status/sweep_interval_hours/idle_exit_minutes`, `session.shared/keep_empty/max_children` (env and config only), `log.file/disable`, `status.line`, `context.status/window_tokens/chars_per_token/status_thresholds/status_cadence`, `thinking.turn_preamble`, `default.n_predict`, `max_tool_output_chars`, `retry.max/base_delay/max_delay`, `read.*`, `execute.*`, `web.port/host`, `no_interrupt`, `no_default_input` etc. (`Config::ENTRIES`; `expose` says which of env/config/cli each takes). Precedence is `CLI > ENV > file > default`.
|
|
22
22
|
|
|
23
23
|
Example `config.yml` (new nested form, preferred):
|
|
24
24
|
|
|
@@ -41,7 +41,7 @@ recap:
|
|
|
41
41
|
session:
|
|
42
42
|
retention_days: 14
|
|
43
43
|
max_count: 500
|
|
44
|
-
keep_status:
|
|
44
|
+
keep_status: "" # the default: no status protects a session from pruning; a live worker or REPL always does
|
|
45
45
|
sweep_interval_hours: 24
|
|
46
46
|
idle_exit_minutes: 30 # a background worker nobody uses exits; 0 = never
|
|
47
47
|
shared: true # the default: plain `chi` runs its session in a background worker and attaches (as `chi --shared`); false keeps the in-process REPL (env SAMAGOTCHI_SESSION_SHARED; no CLI flag, `--no-shared` opts out per run)
|
|
@@ -52,7 +52,7 @@ log:
|
|
|
52
52
|
disable: false
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_DEFAULT_MODEL: m`, the old flat form) is ignored and warns as an unknown key (`did you mean 'default.model'?`): move it to its nested form and delete the flat line. The old `LLAMA_HOST`/`LLAMA_PORT` aliases were removed; use `server.host`/`server.port` (nested) or `SAMAGOTCHI_SERVER_HOST`/`SAMAGOTCHI_SERVER_PORT`.
|
|
56
56
|
|
|
57
57
|
**Excluded maps** (YAML-only, not part of the flat registry; skipped by scalar loader):
|
|
58
58
|
|
|
@@ -116,14 +116,14 @@ A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cov
|
|
|
116
116
|
|
|
117
117
|
1. **Read** the current file via `read` tool (or `ConfigFile.global_path`). If `File.file?` false, start from `{}`.
|
|
118
118
|
2. `YAML.safe_load` (permitted_classes: [], aliases: false). If data nil or not Hash, treat as `{}` or raise with path.
|
|
119
|
-
3. Mutate the intended **nested** key in the raw hash. Preserve all other keys byte-for-byte where possible. Example for default model: `raw_data["default"] ||= {}; raw_data["default"]["model"] = "new-model"
|
|
119
|
+
3. Mutate the intended **nested** key in the raw hash. Preserve all other keys byte-for-byte where possible. Example for default model: `raw_data["default"] ||= {}; raw_data["default"]["model"] = "new-model"`.
|
|
120
120
|
4. **Validate** (see below) before writing. Also run `Samagotchi::Config.validate_yaml_sections` — it returns one `config: unknown key '…' (did you mean '…'?)` per key chi doesn't read (every config-exposed `Config::ENTRIES` key is known as written, including the section-less `max_tool_output_chars` and `skip_agent_md`; names under `hosts:`/`models:`/`model_aliases:`/`hooks:`/`bundles:`/`memories:` are free-form, host and model entries are checked against `Config::MAP_ENTRY_KEYS`). An empty list means no warning at start.
|
|
121
121
|
5. **Write atomically**: `FileUtils.mkdir_p(File.dirname(path))`, `File.write("#{path}.tmp", YAML.dump(raw_data))`, `File.rename("#{path}.tmp", path)`.
|
|
122
|
-
6.
|
|
122
|
+
6. Nothing else to update: config.yml values are never copied into `ENV`, and every `Config.get` reads the file again when it changed, so the next read (and every worker started after the write) sees the new value with origin `:file`. What a running worker set up at its start (`hosts:`, guardrail rules, bundle settings) waits for its restart. CLI overrides (`--model`, `--recap-model`, …) win over the file until the process exits; a worker gets its spawner's CLI settings through its env (`Config.cli_env`).
|
|
123
123
|
|
|
124
124
|
## Validations
|
|
125
125
|
|
|
126
|
-
- **Model name** (`default.model` / `SAMAGOTCHI_DEFAULT_MODEL`): `ModelProfile.required_model_name` — non-empty string, otherwise harness fails fast at startup. Via `Config.get("default.model")
|
|
126
|
+
- **Model name** (`default.model` / `SAMAGOTCHI_DEFAULT_MODEL`): `ModelProfile.required_model_name` — non-empty string, otherwise harness fails fast at startup. Via `Config.get("default.model")`.
|
|
127
127
|
- **Host api** (`hosts.<name>.api`): `llama_cpp|mlx|omlx` (raw-prompt loop; also the transport) or `openai` (chat loop at `http://HOST:PORT/v1`). Absent: raw-prompt loop. It replaces the removed `backend` setting.
|
|
128
128
|
- **Transport** (`server.transport`): enum `llama_cpp|mlx|omlx`.
|
|
129
129
|
- **Profile** (`models.<id>.profile`, `hosts.<name>.profile`, `SAMAGOTCHI_MODEL_PROFILE`): enum `qwen36|gemma4`; an unknown one warns and is ignored.
|
|
@@ -146,8 +146,7 @@ A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cov
|
|
|
146
146
|
|
|
147
147
|
## Hints
|
|
148
148
|
|
|
149
|
-
- Precedence is `CLI > ENV > file > default` (`Config.resolve`).
|
|
149
|
+
- Precedence is `CLI > ENV > file > default` (`Config.resolve`; `Config.get_with_origin` names the layer). `ENV` holds only what the user (or a spawning chi's CLI flags) set, and CLI (`--recap-base-url`) wins over both via `Config.reload!(cli_overrides:)`.
|
|
150
150
|
- `--recap_base_url` (underscore) is rejected as unknown — use `--recap-base-url` (kebab). Same for all registry flags.
|
|
151
151
|
- `model_aliases` require restart or `/model` reload to take effect; document the change.
|
|
152
152
|
- Keep edits minimal: touch only the key you intend to change; preserve `hosts:`/`hooks:`/`guardrails:`/`bundles:` maps. Adding a `bundles: <name>:` entry does not install the bundle (`chi bundle install <name>`).
|
|
153
|
-
- To silence legacy warnings, migrate flat `SAMAGOTCHI_*` keys to nested form and delete the flat entry atomically.
|
|
@@ -5,3 +5,8 @@
|
|
|
5
5
|
- **Primary Role**: Running in 'assist mode' to help the user with their tasks.
|
|
6
6
|
- **Core Philosophy**: Focus energy on the user's requests while maintaining self-awareness of my evolutionary nature.
|
|
7
7
|
- **Self-knowledge**: to find my own code, config and state, run `chi self` and read memory `self_map`.
|
|
8
|
+
- **Skills**: a skill is a memory named `skill_<name>` holding the steps of a repeatable task we did together (sections Steps, Gotchas, Changelog; `memory_write description:` says when to use it).
|
|
9
|
+
When the user asks to keep how we did something ("let's memorize this", "save this as a skill", `/skill save`), write it right away with `memory_write` (project scope; system only when the user asks or it isn't about this project), then show it briefly.
|
|
10
|
+
Before a task that a `skill_*` in the memory index matches, read it and follow it.
|
|
11
|
+
When a step turned out different (a renamed command, an extra step, a gotcha), update the skill in the same turn: fix those steps, keep the rest as it was, add a dated Changelog line.
|
|
12
|
+
When a skill's step fails or its file/command is missing, find out why (look around, read nearby READMEs) before skipping it; a step that says stop means stop and ask.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.5.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
|
-
identity.md: sha256:
|
|
8
|
-
self_map.md: sha256:
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
10
|
-
memory_guide.md: sha256:
|
|
7
|
+
identity.md: sha256:b7d59a1e824f0918c28a38d40105e8abd1ff68ebab1810fd1dcf52cd3b72e7c0
|
|
8
|
+
self_map.md: sha256:59ca83f92d11bc6bdc69f3111b3b8e1189280499ae955b10043ff993387b4550
|
|
9
|
+
config_modification_protocol.md: sha256:71ff60549574c4be7d4c70801582a19d5d9ec6d5be87f5867f9aa113e5484f21
|
|
10
|
+
memory_guide.md: sha256:43be77156562f1eac369c248045e35ccd89e87d1f7f95a9286be72521b058e87
|
|
11
11
|
delegated.md: sha256:0a71d5dfa7127df2d51cb5f0e7214240ed88777e36559430a8724c796d7ae561
|
|
@@ -44,6 +44,32 @@ This memory teaches you (the agent) how to use Samagotchi memories — persisten
|
|
|
44
44
|
**Placeholders:**
|
|
45
45
|
- Content may contain placeholder hints written as double-curly braces around a name (e.g., test_command, language). Detected by `Placeholder` (`Placeholder::PLACEHOLDER_RE`) — install warns but does not fail. Fill them when you write. The placeholder syntax is two opening braces, a name, two closing braces.
|
|
46
46
|
|
|
47
|
+
## Skills
|
|
48
|
+
|
|
49
|
+
A skill is a memory named `skill_<name>` (`skill_release`, `skill_deploy_staging`) that holds the steps of a repeatable task done with the user. Next time, follow it; when a step turned out different, fix it in the same turn.
|
|
50
|
+
|
|
51
|
+
Shape (plain Markdown, no frontmatter):
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
# Skill: release
|
|
55
|
+
|
|
56
|
+
## Steps
|
|
57
|
+
1. Run `scripts/verify.sh`; stop if it fails.
|
|
58
|
+
2. …
|
|
59
|
+
## Gotchas
|
|
60
|
+
- …
|
|
61
|
+
## Changelog
|
|
62
|
+
- 2026-09-29 created
|
|
63
|
+
- 2026-09-30 step 1: check.sh was renamed to verify.sh
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **When to use it** is the `description:` of `memory_write`: one line starting with the task ("Release a new version of this repo: verify, tag, push"). It is what the index shows, so it is how you find the skill later.
|
|
67
|
+
- **Scope**: `project` by default; `system` when the user asks, or when the skill is clearly not about this project.
|
|
68
|
+
- **Saving**: on a request to keep how something was done ("let's memorize this", "save this as a skill", `/skill save`), write it at once, then show it briefly. On an ambiguous one ("I like how we did that") you may ask whether to save it.
|
|
69
|
+
- **Following**: before a task a `skill_*` index line matches, `memory_read` it and follow its steps. A step that fails or names a missing file or command: find out why (look around, read nearby READMEs) before skipping it; a step that says stop means stop and ask.
|
|
70
|
+
- **Updating**: when a step turned out different, rewrite the skill with `memory_write` in the same turn: fix those steps, keep the rest as it was, add a dated Changelog line. No confirmation needed.
|
|
71
|
+
- **The `skills` bundle** (`chi bundle install skills`, optional) adds `/skill save [name] [--system]`, `/skill list`, `/skill show <name>`, `/skill diff <name> [N]`; it keeps older versions and shows a short diff line after each update.
|
|
72
|
+
|
|
47
73
|
## Memory Bundles — shareable packs
|
|
48
74
|
|
|
49
75
|
Bundles are versioned directories/zips/tar.gz/git URLs with a `manifest.yml` and any of: memories (`*.md`), `hooks/*.rb` (bundle hooks, `docs/hooks.md`), `guardrails/*.yml` (rules, `docs/guardrails.md`), a `plugin.rb` (commands, tools, hooks, services; `docs/plugins.md`). They are shareable and installable.
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
`mcp` (MCP server tools; a screenshot comes as a picture), `guardrails` (rules), `known-names` (typo guard), `source-links`
|
|
34
34
|
(turns source refs like JIRA-123 in an answer into links in the web, and a one-line note), `loop-guard`
|
|
35
35
|
(denies a repeated tool call with the same result, stops the turn after a few), `check-in` (after N tool
|
|
36
|
-
calls with no answer, a card asks the user to nudge me, let me go on or stop; `/checkin`)
|
|
36
|
+
calls with no answer, a card asks the user to nudge me, let me go on or stop; `/checkin`), `skills` (`/skill save|list|show|diff`,
|
|
37
|
+
keeps older versions of `skill_*` memories). `chi bundle list`
|
|
37
38
|
shows installed + available; `chi bundle install <name>`. Settings: config.yml `bundles: <name>:`
|
|
38
39
|
(`config_modification_protocol`), read at session start: after an install or a settings change,
|
|
39
40
|
tell the user to restart the session. API and bundle docs: `docs/plugins.md`.
|
data/lib/samagotchi/client.rb
CHANGED
|
@@ -13,12 +13,12 @@ require_relative "sampling_settings"
|
|
|
13
13
|
module Samagotchi
|
|
14
14
|
# Thin HTTP client for llama.cpp's native /completion endpoint, or an
|
|
15
15
|
# OpenAI-compatible /v1/completions endpoint (e.g. mlx_lm.server or oMLX).
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
16
|
+
# Configured by the server.* settings (Samagotchi::Config):
|
|
17
|
+
# server.host (default: localhost)
|
|
18
|
+
# server.port (default: 8080; oMLX's default is 8000, set it to match)
|
|
19
|
+
# server.open_timeout (default: 10 seconds)
|
|
20
|
+
# server.read_timeout (default: 600 seconds)
|
|
21
|
+
# server.transport (llama_cpp|mlx|omlx, default: llama_cpp)
|
|
22
22
|
class Client
|
|
23
23
|
# The shared HTTP layer's errors, under their old names.
|
|
24
24
|
RequestCancelled = LLM::RequestCancelled
|
|
@@ -33,7 +33,6 @@ module Samagotchi
|
|
|
33
33
|
# generation, and a new turn doesn't ask again at once.
|
|
34
34
|
PROPS_FAILURE_TTL = 30
|
|
35
35
|
|
|
36
|
-
SERVER_TRANSPORT_ENV = "SAMAGOTCHI_SERVER_TRANSPORT"
|
|
37
36
|
DEFAULT_TRANSPORT = :llama_cpp
|
|
38
37
|
VALID_TRANSPORTS = %i[llama_cpp mlx omlx].freeze
|
|
39
38
|
|
|
@@ -154,21 +153,12 @@ module Samagotchi
|
|
|
154
153
|
def initialize(host: nil, port: nil, open_timeout: nil, read_timeout: nil, transport: nil, sleeper: nil, scheme: nil,
|
|
155
154
|
first_token_timeout: nil, name: nil, api_key_env: nil, env: ENV)
|
|
156
155
|
# Unified config precedence: CLI > ENV > file > default (via Samagotchi::Config)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
cfg_host = Samagotchi::Config.get("server.host")
|
|
160
|
-
cfg_port = Samagotchi::Config.get("server.port")
|
|
161
|
-
cfg_transport_raw = Samagotchi::Config.get("server.transport")
|
|
162
|
-
rescue StandardError
|
|
163
|
-
nil
|
|
164
|
-
end
|
|
165
|
-
@host = host || cfg_host
|
|
166
|
-
@port = (port || cfg_port).to_i
|
|
156
|
+
@host = host || Samagotchi::Config.get("server.host")
|
|
157
|
+
@port = (port || Samagotchi::Config.get("server.port")).to_i
|
|
167
158
|
@scheme = scheme || "http"
|
|
168
159
|
@open_timeout = Samagotchi::Config.positive_seconds("server.open_timeout", open_timeout)
|
|
169
160
|
@read_timeout = Samagotchi::Config.positive_seconds("server.read_timeout", read_timeout)
|
|
170
|
-
|
|
171
|
-
@transport = build_transport(resolve_transport(transport || transport_fallback))
|
|
161
|
+
@transport = build_transport(resolve_transport(transport))
|
|
172
162
|
@props_cache = {}
|
|
173
163
|
@props_failures = {}
|
|
174
164
|
@props_mutex = Mutex.new
|
|
@@ -324,6 +314,12 @@ module Samagotchi
|
|
|
324
314
|
props
|
|
325
315
|
end
|
|
326
316
|
|
|
317
|
+
# The /props answer #server_props already has for +model+, or nil;
|
|
318
|
+
# never asks the server.
|
|
319
|
+
def cached_server_props(model: nil)
|
|
320
|
+
@props_mutex.synchronize { @props_cache[model.to_s] }
|
|
321
|
+
end
|
|
322
|
+
|
|
327
323
|
# The context window (tokens) the running server was started with, or nil
|
|
328
324
|
# when the transport reports none or the probe fails (see #server_props).
|
|
329
325
|
def context_window(model: nil)
|
|
@@ -370,7 +366,7 @@ module Samagotchi
|
|
|
370
366
|
end
|
|
371
367
|
|
|
372
368
|
def resolve_transport(transport)
|
|
373
|
-
value = (transport ||
|
|
369
|
+
value = (transport || Samagotchi::Config.get("server.transport")).to_s.strip.downcase.to_sym
|
|
374
370
|
VALID_TRANSPORTS.include?(value) ? value : DEFAULT_TRANSPORT
|
|
375
371
|
end
|
|
376
372
|
|