samagotchi 0.50.0 → 0.52.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 +89 -1
- data/README.md +1 -1
- data/bin/chi +11 -3
- data/docs/architecture.md +1 -1
- data/docs/cli.md +40 -11
- data/docs/configuration.md +63 -21
- data/docs/hooks.md +3 -2
- data/docs/internals/engine.md +4 -2
- data/docs/memory.md +11 -0
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +10 -5
- data/docs/testing.md +6 -1
- data/lib/samagotchi/bootstrap/config_writer.rb +6 -37
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +29 -17
- data/lib/samagotchi/bridge_client.rb +3 -1
- data/lib/samagotchi/broadcast/recipients.rb +1 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/commands/registry.rb +2 -1
- data/lib/samagotchi/config.rb +50 -182
- data/lib/samagotchi/config_text_edit.rb +8 -32
- data/lib/samagotchi/context_sources.rb +0 -7
- data/lib/samagotchi/context_window.rb +2 -2
- data/lib/samagotchi/continue_offer.rb +5 -4
- data/lib/samagotchi/cut_policy.rb +0 -1
- data/lib/samagotchi/delivery.rb +56 -0
- data/lib/samagotchi/engine.rb +48 -17
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/host_config.rb +183 -0
- data/lib/samagotchi/host_registry.rb +21 -13
- data/lib/samagotchi/kernel_loop.rb +28 -2
- data/lib/samagotchi/last_turn.rb +48 -0
- data/lib/samagotchi/live_versions.rb +0 -6
- data/lib/samagotchi/llm/native_tool_normalizer.rb +2 -7
- data/lib/samagotchi/llm_context_override.rb +3 -22
- data/lib/samagotchi/llm_context_strategy.rb +50 -16
- data/lib/samagotchi/memory_bundle/placeholder.rb +0 -5
- data/lib/samagotchi/model_profile.rb +4 -4
- data/lib/samagotchi/model_settings.rb +46 -0
- data/lib/samagotchi/plugin/context.rb +3 -3
- data/lib/samagotchi/plugin/sessions.rb +2 -2
- data/lib/samagotchi/queued_command.rb +35 -0
- data/lib/samagotchi/reminder_store.rb +0 -27
- data/lib/samagotchi/reply_wait.rb +11 -13
- data/lib/samagotchi/sampling_settings.rb +4 -4
- data/lib/samagotchi/self_report.rb +8 -5
- data/lib/samagotchi/send_command.rb +92 -21
- data/lib/samagotchi/session.rb +45 -24
- data/lib/samagotchi/session_commands.rb +41 -3
- data/lib/samagotchi/session_inbox.rb +36 -7
- data/lib/samagotchi/session_manager.rb +21 -10
- data/lib/samagotchi/session_metrics.rb +25 -6
- data/lib/samagotchi/session_setup.rb +30 -0
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/steer_cut.rb +21 -10
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +10 -4
- data/lib/samagotchi/terminal_ui/attached_loop.rb +48 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +69 -4
- data/lib/samagotchi/terminal_ui.rb +56 -8
- data/lib/samagotchi/thinking.rb +67 -7
- data/lib/samagotchi/thinking_command.rb +72 -0
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_runner.rb +2 -0
- data/lib/samagotchi/tools/builtin_calls.rb +33 -0
- data/lib/samagotchi/tools/builtins.rb +7 -2
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -3
- data/lib/samagotchi/web/app.rb +59 -7
- data/lib/samagotchi/web/message_parts.rb +3 -0
- data/lib/samagotchi/web/public/activity.js +6 -0
- data/lib/samagotchi/web/public/app.js +199 -48
- data/lib/samagotchi/web/public/data.js +8 -4
- data/lib/samagotchi/web/public/format.js +8 -4
- data/lib/samagotchi/web/public/index.html +25 -20
- data/lib/samagotchi/web/public/kept_commands.js +52 -0
- data/lib/samagotchi/web/public/model_pick.js +3 -1
- data/lib/samagotchi/web/public/session_title.js +26 -0
- data/lib/samagotchi/web/public/strip.js +4 -6
- data/lib/samagotchi/web/public/thinking_chip.js +169 -0
- data/lib/samagotchi/web/public/timing.js +15 -0
- data/lib/samagotchi/web/public/turn_events.js +26 -2
- data/lib/samagotchi/web/public/turn_html.js +3 -1
- data/lib/samagotchi/web/public/turn_view.js +328 -310
- data/lib/samagotchi/web/public/vendor/lit-html.js +8 -0
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +44 -43
- data/lib/samagotchi/worker_inbound.rb +10 -7
- data/lib/samagotchi/worker_wakes.rb +0 -2
- data/lib/samagotchi/yaml_lines.rb +72 -0
- data/lib/samagotchi.rb +1 -0
- metadata +13 -1
|
@@ -6,6 +6,7 @@ require "fileutils"
|
|
|
6
6
|
require "ipaddr"
|
|
7
7
|
require_relative "../atomic_file"
|
|
8
8
|
require_relative "../config"
|
|
9
|
+
require_relative "../yaml_lines"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
module Bootstrap
|
|
@@ -15,6 +16,8 @@ module Samagotchi
|
|
|
15
16
|
# comments), after a backup, and is checked by parsing it again: every
|
|
16
17
|
# other key must be unchanged, or the backup goes back.
|
|
17
18
|
class ConfigWriter
|
|
19
|
+
include YAMLLines
|
|
20
|
+
|
|
18
21
|
class Error < StandardError
|
|
19
22
|
end
|
|
20
23
|
|
|
@@ -33,7 +36,6 @@ module Samagotchi
|
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
HOSTS_LINE_RE = /\Ahosts:[ \t]*(#.*)?\z/
|
|
36
|
-
DEFAULT_INDENT = 2
|
|
37
39
|
SERVER_TRANSPORTS = ConfigFile::VALID_TRANSPORTS_FOR_CONFIG
|
|
38
40
|
|
|
39
41
|
# The host part of a name: localhost/127.x → local, another IP → lan,
|
|
@@ -157,9 +159,7 @@ module Samagotchi
|
|
|
157
159
|
original = File.binread(@path).force_encoding(Encoding::UTF_8)
|
|
158
160
|
return snippet(name, fields) unless data.is_a?(Hash) && !anchors?(original)
|
|
159
161
|
|
|
160
|
-
eol = original
|
|
161
|
-
lines = original.split(/(?<=\n)/)
|
|
162
|
-
lines[-1] = "#{lines[-1]}#{eol}" if lines.any? && !lines[-1].end_with?("\n")
|
|
162
|
+
lines, eol = lines_of(original)
|
|
163
163
|
hosts_at = lines.index { |line| line.chomp.match?(HOSTS_LINE_RE) }
|
|
164
164
|
return snippet(name, fields) if hosts_at.nil? && data.key?("hosts")
|
|
165
165
|
|
|
@@ -207,28 +207,14 @@ module Samagotchi
|
|
|
207
207
|
# before trailing blank lines and comments shallower than an entry.
|
|
208
208
|
# @return [index, entry indent, field indent step]
|
|
209
209
|
def block_end(lines, hosts_at)
|
|
210
|
-
stop = hosts_at
|
|
211
|
-
stop += 1 while stop < lines.length && inside_block?(lines[stop])
|
|
210
|
+
stop = block_stop(lines, hosts_at)
|
|
212
211
|
children = lines[(hosts_at + 1)...stop].reject { |line| blank_or_comment?(line) }
|
|
213
212
|
child = children.first ? indent(children.first) : DEFAULT_INDENT
|
|
214
213
|
field = children.find { |line| indent(line) > child }
|
|
215
214
|
step = field ? indent(field) - child : child
|
|
216
|
-
|
|
217
|
-
at -= 1 while at > hosts_at + 1 && (lines[at - 1].strip.empty? || (comment?(lines[at - 1]) && indent(lines[at - 1]) < child))
|
|
218
|
-
[at, child, step]
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def inside_block?(line)
|
|
222
|
-
return true if line.strip.empty?
|
|
223
|
-
return false if line.start_with?("---", "...")
|
|
224
|
-
|
|
225
|
-
line.start_with?(" ", "\t") || comment?(line)
|
|
215
|
+
[insert_at(lines, hosts_at, stop, child), child, step]
|
|
226
216
|
end
|
|
227
217
|
|
|
228
|
-
def blank_or_comment?(line) = line.strip.empty? || comment?(line)
|
|
229
|
-
def comment?(line) = line.lstrip.start_with?("#")
|
|
230
|
-
def indent(line) = line[/\A */].length
|
|
231
|
-
|
|
232
218
|
def entry_lines(name, fields, child, step)
|
|
233
219
|
["#{" " * child}#{name}:\n", *fields.map { |key, value| "#{" " * (child + step)}#{key}: #{scalar(value)}\n" }]
|
|
234
220
|
end
|
|
@@ -316,23 +302,6 @@ module Samagotchi
|
|
|
316
302
|
false
|
|
317
303
|
end
|
|
318
304
|
|
|
319
|
-
def parse(text) = YAML.safe_load(text, permitted_classes: [], aliases: false)
|
|
320
|
-
|
|
321
|
-
# Anchors and aliases: chi's reader refuses aliases, and an insert
|
|
322
|
-
# can't know what an anchor shares.
|
|
323
|
-
def anchors?(text)
|
|
324
|
-
stack = [Psych.parse_stream(text)]
|
|
325
|
-
until stack.empty?
|
|
326
|
-
node = stack.pop
|
|
327
|
-
return true if node.is_a?(Psych::Nodes::Alias) || (node.respond_to?(:anchor) && node.anchor)
|
|
328
|
-
|
|
329
|
-
stack.concat(Array(node.children)) if node.respond_to?(:children)
|
|
330
|
-
end
|
|
331
|
-
false
|
|
332
|
-
rescue Psych::Exception
|
|
333
|
-
true
|
|
334
|
-
end
|
|
335
|
-
|
|
336
305
|
def backup_path(real)
|
|
337
306
|
stamp = @now.call.utc.strftime("%Y%m%dT%H%M%SZ")
|
|
338
307
|
candidate = "#{real}.bak-#{stamp}"
|
|
@@ -40,11 +40,6 @@ module Samagotchi
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
# Whether any buffered event could satisfy a reconnect from +from_seq+.
|
|
44
|
-
def has_any_after?(from_seq:)
|
|
45
|
-
@mutex.synchronize { @events.any? { |e| e[:seq] > from_seq } }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
43
|
# @return [Integer, nil] the highest seq buffered so far.
|
|
49
44
|
def last_seq
|
|
50
45
|
@mutex.synchronize { @events.last&.[](:seq) }
|
|
@@ -158,7 +158,8 @@ module Samagotchi
|
|
|
158
158
|
def self.tool_replay_events(part)
|
|
159
159
|
call = { iteration: part[:iteration], call_index: part[:call_index], tool: part[:tool] }
|
|
160
160
|
call[:label] = part[:label] if part[:label]
|
|
161
|
-
|
|
161
|
+
# Its title and, for an alias's call, the name the model used.
|
|
162
|
+
title = part.slice(:title, :called_as).compact
|
|
162
163
|
view = part[:view] ? { view: part[:view] } : {}
|
|
163
164
|
events = [{ type: :tool_call_started, **call, params: part[:params], **title, **view }]
|
|
164
165
|
if part[:status] == "running"
|
|
@@ -274,6 +275,7 @@ module Samagotchi
|
|
|
274
275
|
tool: event[:tool], params: event[:params], status: "running" }
|
|
275
276
|
part[:label] = event[:label] if event[:label]
|
|
276
277
|
part[:title] = event[:title] if event[:title]
|
|
278
|
+
part[:called_as] = event[:called_as] if event[:called_as]
|
|
277
279
|
part[:view] = event[:view] if event[:view]
|
|
278
280
|
parts << part
|
|
279
281
|
@tool_started_at[[event[:iteration], event[:call_index]]] = @clock.call
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "fileutils"
|
|
|
7
7
|
require "securerandom"
|
|
8
8
|
require "time"
|
|
9
9
|
require_relative "atomic_file"
|
|
10
|
+
require_relative "delivery"
|
|
10
11
|
require_relative "session_inbox"
|
|
11
12
|
|
|
12
13
|
require_relative "bridge/bounded_queue"
|
|
@@ -20,6 +21,7 @@ require_relative "session"
|
|
|
20
21
|
require_relative "engine"
|
|
21
22
|
require_relative "guardrails/parent_approvals"
|
|
22
23
|
require_relative "session_commands"
|
|
24
|
+
require_relative "queued_command"
|
|
23
25
|
require_relative "steer"
|
|
24
26
|
require_relative "image_store"
|
|
25
27
|
require_relative "log"
|
|
@@ -285,12 +287,11 @@ module Samagotchi
|
|
|
285
287
|
# basename): it runs once that file is taken (Worker#on_command)
|
|
286
288
|
# @return [String] its command_id
|
|
287
289
|
def queue_command(line, client_id: nil, card: false, after_file: nil)
|
|
288
|
-
command = { command_id: SecureRandom.uuid, client_id: client_id, line: line.to_s.strip }
|
|
289
290
|
# A card's action (the step-limit question's answer): the UIs leave
|
|
290
291
|
# its line out.
|
|
291
|
-
command
|
|
292
|
+
command = QueuedCommand.mint(line.to_s.strip, client_id: client_id, card: card)
|
|
292
293
|
@engine.synchronize_events { queue_command_locked(command, after_file: after_file) }
|
|
293
|
-
command
|
|
294
|
+
command.command_id
|
|
294
295
|
end
|
|
295
296
|
|
|
296
297
|
private
|
|
@@ -721,18 +722,17 @@ module Samagotchi
|
|
|
721
722
|
# :command_queued, unless +deadline+ has passed. Returns [headers,
|
|
722
723
|
# status, body]: 202 with its command_id, or 408.
|
|
723
724
|
def queue_command_request(session_id, line, client_id:, deadline: nil, card: false)
|
|
724
|
-
command = { command_id: SecureRandom.uuid, client_id: client_id, line: line }
|
|
725
725
|
# A card's action: the UIs leave its line out (the card is the echo).
|
|
726
|
-
command
|
|
726
|
+
command = QueuedCommand.mint(line, client_id: client_id, card: card)
|
|
727
727
|
queued = @engine.synchronize_events do
|
|
728
|
-
next false if expired?("command_expired", deadline, sid: session_id, client_id: command
|
|
728
|
+
next false if expired?("command_expired", deadline, sid: session_id, client_id: command.client_id)
|
|
729
729
|
|
|
730
730
|
queue_command_locked(command)
|
|
731
731
|
true
|
|
732
732
|
end
|
|
733
733
|
return deadline_passed("command") unless queued
|
|
734
734
|
|
|
735
|
-
[{}, 202, { status: "accepted", command_id: command
|
|
735
|
+
[{}, 202, { status: "accepted", command_id: command.command_id, session_id: @session_id }]
|
|
736
736
|
end
|
|
737
737
|
|
|
738
738
|
# With the event log held. +on_command+ answers what it did with the
|
|
@@ -747,7 +747,7 @@ module Samagotchi
|
|
|
747
747
|
when :queue then { waits: "turn_end" }
|
|
748
748
|
else {}
|
|
749
749
|
end
|
|
750
|
-
@engine.announce(type: :command_queued, **command, **marks)
|
|
750
|
+
@engine.announce(type: :command_queued, **command.event_fields, **marks)
|
|
751
751
|
end
|
|
752
752
|
|
|
753
753
|
# A client asks the worker to exit now (`/exit` in the attached TUI), or
|
|
@@ -826,6 +826,13 @@ module Samagotchi
|
|
|
826
826
|
deadline = fetched(parsed, "deadline")
|
|
827
827
|
return BAD_DEADLINE unless deadline_valid?(deadline)
|
|
828
828
|
|
|
829
|
+
raw_delivery = fetched(parsed, "delivery")
|
|
830
|
+
if (refused = Delivery.refusal(raw_delivery))
|
|
831
|
+
return [{ "Allow" => "POST" }, 400, refused]
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
delivery = Delivery.parse(raw_delivery)
|
|
835
|
+
|
|
829
836
|
# A session command sent as a message (chi send -m "/model x", chi -p,
|
|
830
837
|
# a UI whose command list lags) runs as the command, as typed in a
|
|
831
838
|
# TUI; an unknown /word stays a prompt. Not with images: those are
|
|
@@ -844,7 +851,7 @@ module Samagotchi
|
|
|
844
851
|
next :expired if expired?("turn_expired", deadline, sid: sid, client_id: client_id)
|
|
845
852
|
|
|
846
853
|
enqueue_turn(prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
847
|
-
no_interrupt: no_interrupt, images: images).tap do |ok|
|
|
854
|
+
no_interrupt: no_interrupt, images: images, delivery: delivery).tap do |ok|
|
|
848
855
|
next unless ok
|
|
849
856
|
|
|
850
857
|
enqueued_event = { type: :turn_enqueued, enqueued_id: enqueued_id, client_id: client_id, prompt: prompt.to_s }
|
|
@@ -856,12 +863,17 @@ module Samagotchi
|
|
|
856
863
|
return deadline_passed("turn") if enqueued == :expired
|
|
857
864
|
return [{}, 500, { error: "enqueue_failed", detail: "could not write turn input" }] unless enqueued
|
|
858
865
|
|
|
859
|
-
# Queued (outside the event lock): a
|
|
860
|
-
#
|
|
861
|
-
# chi doesn't know sent it. An image input never
|
|
862
|
-
# it never cuts.
|
|
863
|
-
|
|
864
|
-
|
|
866
|
+
# Queued (outside the event lock): a message sent as +cut+ cuts a
|
|
867
|
+
# running generation that has only been thinking for long, unless a
|
|
868
|
+
# plugin or a client chi doesn't know sent it. An image input never
|
|
869
|
+
# merges mid-turn, so it never cuts. Nothing else cuts: a message
|
|
870
|
+
# that says nothing (an older client, the web composer) goes in at
|
|
871
|
+
# the running turn's next step.
|
|
872
|
+
cut = nil
|
|
873
|
+
cut = @engine.cut_for_steer(Steer.source_for_client(client_id), epoch: epoch) if images.empty? && Delivery.cut?(delivery)
|
|
874
|
+
ack = { status: "accepted", enqueued_id: enqueued_id, session_id: @session_id, delivery: delivery }
|
|
875
|
+
ack[:cut] = cut.to_s if cut
|
|
876
|
+
[{}, 202, ack]
|
|
865
877
|
end
|
|
866
878
|
|
|
867
879
|
# Read-only snapshot surface. AC #4: too-old reconnects re-derive state
|
|
@@ -933,9 +945,9 @@ module Samagotchi
|
|
|
933
945
|
|
|
934
946
|
# Write a turn into this session's input dir, reusing the file IPC the
|
|
935
947
|
# worker polls. Never calls run_turn across the boundary.
|
|
936
|
-
def enqueue_turn(prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, images: [])
|
|
948
|
+
def enqueue_turn(prompt:, client_id: nil, enqueued_id: nil, no_interrupt: false, images: [], delivery: nil)
|
|
937
949
|
SessionInbox.write_input(session_dir, prompt: prompt, client_id: client_id, enqueued_id: enqueued_id,
|
|
938
|
-
no_interrupt: no_interrupt, images: images)
|
|
950
|
+
no_interrupt: no_interrupt, images: images, delivery: delivery)
|
|
939
951
|
end
|
|
940
952
|
|
|
941
953
|
# ── HTTP plumbing ────────────────────────────────────────────────────────
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "socket"
|
|
5
5
|
|
|
6
6
|
require_relative "worker_sidecar"
|
|
7
|
+
require_relative "delivery"
|
|
7
8
|
require_relative "bridge_client/sse_parser"
|
|
8
9
|
require_relative "bridge_client/event_stream"
|
|
9
10
|
|
|
@@ -139,10 +140,11 @@ module Samagotchi
|
|
|
139
140
|
# @param no_interrupt [Boolean] run the turn with the raised iteration limit
|
|
140
141
|
# @param images [Array<Hash>] refs ({file:, name:}) to images already in
|
|
141
142
|
# the session's images/
|
|
142
|
-
def post_turn(prompt:, client_id: nil, no_interrupt: false, images: nil)
|
|
143
|
+
def post_turn(prompt:, client_id: nil, no_interrupt: false, images: nil, delivery: nil)
|
|
143
144
|
body = { session_id: @session_id, prompt: prompt, client_id: client_id }
|
|
144
145
|
body[:no_interrupt] = true if no_interrupt
|
|
145
146
|
body[:images] = images if images && !images.empty?
|
|
147
|
+
body[:delivery] = delivery unless delivery.nil? || Delivery.next_step?(delivery)
|
|
146
148
|
body[:deadline] = deadline
|
|
147
149
|
post("turn", body, read_body: true)
|
|
148
150
|
end
|
|
@@ -46,8 +46,7 @@ module Samagotchi
|
|
|
46
46
|
return false unless (time(row[:updated_at]) || Time.at(0)) >= cutoff
|
|
47
47
|
|
|
48
48
|
session = Session.summary_from_file(Session.session_file(row[:id], state_dir: state_dir))
|
|
49
|
-
|
|
50
|
-
ended = last_turn.is_a?(Hash) ? time(last_turn["ended_at"] || last_turn[:ended_at]) : nil
|
|
49
|
+
ended = time(session&.last_turn&.ended_at)
|
|
51
50
|
(ended || time(row[:updated_at])) >= cutoff
|
|
52
51
|
end
|
|
53
52
|
private_class_method :active_since?
|
|
@@ -58,7 +58,7 @@ Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_D
|
|
|
58
58
|
|
|
59
59
|
- `model_aliases:` map of alias → model id (`ModelRef.parse`, through `HostRegistry#resolve`). Keys lowercased on write (`ConfigFile.write_model_alias!`). Values may be bare `model` or qualified `host:model` (hybrid; only `:` names a host, `org/model` is an id). Aliases apply once: never point an alias at another alias.
|
|
60
60
|
- `hosts:` map of `name → {host, port | url, transport, api, api_key_env, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`ConfigFile.hosts_config`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `transport` overrides `server.transport`; `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; `models:` (a map of model ids, or a plain list) declares ids the host serves whatever its `/v1/models` lists: no "doesn't list" warning, and a bare id routes there as if listed (`HostModel`); an entry may hold `price: {input, output, cache_read?, cache_write?}` in USD per 1M tokens (`ModelPrice`; an unset cache rate is the input rate), which estimates a cost the provider doesn't report or reports as 0 (`cost_estimate`, shown as `~$`), and `served: [ids] | any`, the models a gateway may answer that id with (no served-model warning for them); model settings stay in the root `models:`; workers inherit via `SAMAGOTCHI_HOSTS_JSON` (`hosts_json_for_env`, `session_manager.rb`).
|
|
61
|
-
- `models:` map of model id or alias → `{profile, vision, sampling, thinking}` (`ConfigFile.model_settings`). `thinking:` (here, on a host, or `thinking.level` for every model) is `off|low|medium|high|default` (`Thinking`; unquoted `off` works, `on` is not a level; `default` = send nothing). Precedence:
|
|
61
|
+
- `models:` map of model id or alias → `{profile, vision, sampling, thinking}` (`ConfigFile.model_settings`). `thinking:` (here, on a host, or `thinking.level` for every model) is `off|low|medium|high|default` (`Thinking`; unquoted `off` works, `on` is not a level; `default` = send nothing). Precedence: the session's own level (`/thinking LEVEL`, saved in the session; `chi --thinking` on a session run, `chi send --new --thinking`) > `chi web --thinking`/`SAMAGOTCHI_THINKING_LEVEL` > `models:` > `hosts.<name>.thinking` > `thinking.level` in config.yml > `default`. A `sampling:` key wins over the fields a level sends (`null` drops one); `docs/configuration.md` "Thinking". `sampling:` (here or on a host) is a map of request fields passed to the provider as written (`temperature`, `top_p`, `presence_penalty`, `repeat_penalty`, …; a model's fields win over its host's per field; `null` = don't send; chi's own fields like `max_tokens`/`stream` are refused with a warning; `docs/configuration.md` "Sampling"). Keys match case-insensitively. `profile` (here or on a host) is `qwen36|gemma4`: the raw prompt format for native hosts. Precedence: `--profile`/`SAMAGOTCHI_MODEL_PROFILE` > `models:` > `hosts.<name>.profile` > the llama.cpp server's chat template > the name (`qwen`/`gemma`) > `qwen36` (`ModelProfile.resolve`). Set one when a model's name hides its family (e.g. a Qwen fine-tune under another name on mlx, which has no template to read).
|
|
62
62
|
- `hooks:` map of `hooks_dir` + per-event lists `{path, on_error}` (`Hooks::Loader.load`). `hooks_dir` may start with `~`.
|
|
63
63
|
- `guardrails:` tool-call rules (`docs/guardrails.md`; read in `Engine#guardrail_rules`, parsed by `Guardrails::Rules.parse`): `enabled` (bool, default true; `false` drops rules and hooks' asks, a deny still applies; config.yml only, like every `guardrails` setting: no env variable), `parent_approvals` (`off`, the default: a parent agent's `chi answer` can only deny an approval; `once`: it may give "Allow once" too; a convention for an honest parent, not a security boundary, and the user's choice: set it only when the user asks), `rules:` (list), `disable:` (a rule id or a list of them, `id` or `bundle:id`, switching off a bundle's or config rule without editing it), `small_models` (`auto`, the default: small = 32B or less read from the name; a glob or list of globs; `[]` for none). A rule takes only `id`, `tool`, `command`, `path`, `git`, `models`, `verdict`, `reason`, `scopes`: `id` required; at least one of `tool` (a name, `shell` = execute + task_create, a `File.fnmatch` glob like `"mcp_*"`, or a list), `command` (a Ruby regex on the shell command), `path` (a glob, or `outside_repo`: outside the session's repo, tmp folders and memory files excepted), `git` (only `outside_repo`: a shell call runs commit/add/reset/… in another checkout); `models` (optional: `small`, a model-name glob, or a list; without it the rule is for every model); `verdict` `ask|deny`; `scopes` (for `ask`) a subset of `once, session, repo, rule`. **Any parse error (an unknown key, a bad regex, no verdict) makes chi deny every tool call** until fixed, so validate with `YAML.safe_load` and keep the list shape. Rules are read again on the next tool call after config.yml or a bundle's rules change: no restart needed. Installed bundles' rule files (`chi bundle install guardrails`) add to them; `/guardrails` lists what loaded.
|
|
64
64
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.52.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
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
8
|
self_map.md: sha256:dfc19266259a9af1e0d769b7e0f67996b740b0c9921fdf3dadad973f6e5c144a
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
9
|
+
config_modification_protocol.md: sha256:a7a6352c03979db69d98955bee8f36eb468a6cf68904d418b7f3ddb788c83e7b
|
|
10
10
|
memory_guide.md: sha256:15cd3b24eda853cc458371c6c75809a2d4b5352311d075aaa4384926b5221c27
|
|
11
11
|
delegated.md: sha256:aedaa1e756a244904c5560fff0424e06fb468bbdcb0358bccefe321d67cd2614
|
|
@@ -95,7 +95,7 @@ module Samagotchi
|
|
|
95
95
|
return "running" if Tools::Delegate.running?(row)
|
|
96
96
|
return "stopped" if session.status == Session::STATUS_STOPPED
|
|
97
97
|
|
|
98
|
-
outcome = session.last_turn
|
|
98
|
+
outcome = session.last_turn&.outcome
|
|
99
99
|
return "failed" if session.status == Session::STATUS_ERROR || outcome == "failed"
|
|
100
100
|
return "done" if outcome == "completed" && reply
|
|
101
101
|
|
|
@@ -21,7 +21,8 @@ module Samagotchi
|
|
|
21
21
|
# turn runs (MID_TURN): a symbol, or a lambda line (stripped) → one
|
|
22
22
|
# @!attribute local [Boolean] the UI runs it; completion and help only
|
|
23
23
|
# @!attribute uis [Array<Symbol>, nil] a local entry's UIs (:repl,
|
|
24
|
-
# :attached) for completion; nil means every UI
|
|
24
|
+
# :attached, :web) for completion and /help; nil means every UI
|
|
25
|
+
# (for a local entry, every terminal)
|
|
25
26
|
# @!attribute match [#call] line (stripped) → whether it is this command
|
|
26
27
|
# @!attribute handler [Proc, nil] runs it; SessionCommands instance_execs
|
|
27
28
|
# a built-in's with the line, and calls a bundle's with the text
|