samagotchi 0.22.0 → 0.24.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 +72 -1
- data/bin/chi +9 -4
- data/docs/cli.md +6 -1
- data/docs/configuration.md +13 -4
- data/docs/hooks.md +20 -6
- data/docs/plugins.md +1 -1
- data/docs/releasing.md +1 -1
- data/docs/sub-agent.md +4 -3
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +4 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +2 -2
- data/lib/samagotchi/bridge/bounded_queue.rb +0 -2
- data/lib/samagotchi/bridge/card_store.rb +4 -3
- data/lib/samagotchi/bridge/sse_writer.rb +0 -6
- data/lib/samagotchi/bridge/turn_accumulator.rb +6 -3
- data/lib/samagotchi/bridge/turn_notice.rb +1 -0
- data/lib/samagotchi/bridge.rb +19 -9
- data/lib/samagotchi/bridge_client/event_stream.rb +2 -2
- data/lib/samagotchi/bridge_client.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +39 -29
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +2 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +4 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +6 -5
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +2 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +5 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -7
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cancellation_controller.rb +1 -1
- data/lib/samagotchi/cli/command.rb +1 -1
- data/lib/samagotchi/client.rb +3 -5
- data/lib/samagotchi/commands/registry.rb +2 -2
- data/lib/samagotchi/config.rb +42 -14
- data/lib/samagotchi/context_note.rb +1 -1
- data/lib/samagotchi/context_status.rb +6 -6
- data/lib/samagotchi/continue_offer.rb +15 -8
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/desktop/macos.rb +5 -4
- data/lib/samagotchi/empty_answer_retry.rb +1 -1
- data/lib/samagotchi/engine.rb +97 -18
- data/lib/samagotchi/gem_update.rb +2 -1
- data/lib/samagotchi/generation_phase.rb +83 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +5 -5
- data/lib/samagotchi/guardrails/approvals.rb +2 -2
- data/lib/samagotchi/guardrails/gate.rb +2 -2
- data/lib/samagotchi/guardrails/read_only_shell.rb +5 -3
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +113 -5
- data/lib/samagotchi/guardrails/shell_paths.rb +3 -3
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -6
- data/lib/samagotchi/hooks/loader.rb +18 -16
- data/lib/samagotchi/hooks/registry.rb +10 -2
- data/lib/samagotchi/host_registry.rb +1 -3
- data/lib/samagotchi/idle_recap.rb +22 -17
- data/lib/samagotchi/idle_reminders.rb +6 -3
- data/lib/samagotchi/image_store.rb +5 -2
- data/lib/samagotchi/kernel_loop.rb +50 -40
- data/lib/samagotchi/llm/chat_loop.rb +29 -16
- data/lib/samagotchi/llm/errors.rb +2 -2
- data/lib/samagotchi/llm/http.rb +12 -12
- data/lib/samagotchi/llm/openai_chat.rb +42 -28
- data/lib/samagotchi/llm/prompt_cache.rb +1 -1
- data/lib/samagotchi/log.rb +2 -2
- data/lib/samagotchi/log_subscriber.rb +5 -3
- data/lib/samagotchi/memory_bundle/builder.rb +9 -3
- data/lib/samagotchi/memory_bundle/index_updater.rb +13 -1
- data/lib/samagotchi/memory_bundle/installer.rb +67 -57
- data/lib/samagotchi/memory_bundle/listing.rb +5 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +6 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +16 -13
- data/lib/samagotchi/memory_bundle/source.rb +7 -3
- data/lib/samagotchi/memory_bundle/status.rb +3 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +3 -2
- data/lib/samagotchi/memory_bundle/trash.rb +5 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +9 -6
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/model_overlay.rb +3 -5
- data/lib/samagotchi/model_profile.rb +12 -8
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/output_formatter.rb +18 -17
- data/lib/samagotchi/owner_lock.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +8 -5
- data/lib/samagotchi/plugin/context.rb +1 -3
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/plugin/service.rb +1 -1
- data/lib/samagotchi/plugin/sessions.rb +1 -0
- data/lib/samagotchi/plugin_tasks.rb +1 -1
- data/lib/samagotchi/prompt.rb +72 -13
- data/lib/samagotchi/prompt_literal_guard.rb +1 -1
- data/lib/samagotchi/question_desk.rb +7 -7
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/relay_desk.rb +1 -1
- data/lib/samagotchi/reminder_store.rb +2 -1
- data/lib/samagotchi/send_command.rb +4 -2
- data/lib/samagotchi/session.rb +2 -2
- data/lib/samagotchi/session_archive_command.rb +5 -1
- data/lib/samagotchi/session_commands.rb +1 -3
- data/lib/samagotchi/session_manager.rb +2 -4
- data/lib/samagotchi/session_metrics.rb +10 -10
- data/lib/samagotchi/session_observer.rb +3 -2
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/sessions_command.rb +14 -5
- data/lib/samagotchi/steer.rb +103 -8
- data/lib/samagotchi/system_prompt.rb +19 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +3 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +4 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +19 -10
- data/lib/samagotchi/terminal_ui/input_support.rb +2 -2
- data/lib/samagotchi/terminal_ui/line_reader.rb +3 -3
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +1 -0
- data/lib/samagotchi/terminal_ui/question_prompt.rb +23 -23
- data/lib/samagotchi/terminal_ui/question_slot.rb +10 -10
- data/lib/samagotchi/terminal_ui/reline_seam.rb +1 -1
- data/lib/samagotchi/terminal_ui/repl_input.rb +3 -2
- data/lib/samagotchi/terminal_ui.rb +14 -5
- data/lib/samagotchi/text_diff.rb +12 -12
- data/lib/samagotchi/thinking.rb +6 -2
- data/lib/samagotchi/thought_stream_splitter.rb +15 -6
- data/lib/samagotchi/tool_activity.rb +1 -0
- data/lib/samagotchi/tool_call_parser.rb +13 -14
- data/lib/samagotchi/tool_declarations.rb +36 -35
- data/lib/samagotchi/tool_runner.rb +7 -0
- data/lib/samagotchi/tool_view.rb +40 -0
- data/lib/samagotchi/tools/ask_user_question.rb +3 -4
- data/lib/samagotchi/tools/builtin_calls.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +0 -1
- data/lib/samagotchi/tools/edit.rb +14 -13
- data/lib/samagotchi/tools/execute.rb +3 -5
- data/lib/samagotchi/tools/list_reminders.rb +1 -1
- data/lib/samagotchi/tools/memory.rb +21 -22
- data/lib/samagotchi/tools/read.rb +7 -7
- data/lib/samagotchi/tools/task_create.rb +1 -1
- data/lib/samagotchi/tools/task_get.rb +1 -1
- data/lib/samagotchi/tools/task_list.rb +1 -1
- data/lib/samagotchi/tools/task_runtime.rb +8 -9
- data/lib/samagotchi/tools/task_stop.rb +1 -1
- data/lib/samagotchi/tools/task_wait.rb +3 -4
- data/lib/samagotchi/tools/web_fetch.rb +2 -4
- data/lib/samagotchi/tools/write.rb +1 -1
- data/lib/samagotchi/turn_flow.rb +8 -8
- data/lib/samagotchi/turn_note.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +2 -2
- data/lib/samagotchi/waiting_steer.rb +64 -0
- data/lib/samagotchi/web/app.rb +13 -6
- data/lib/samagotchi/web/markdown_renderer.rb +2 -1
- data/lib/samagotchi/web/message_parts.rb +5 -0
- data/lib/samagotchi/web/public/activity.js +5 -0
- data/lib/samagotchi/web/public/app.js +2 -1
- data/lib/samagotchi/web/public/command_view.js +28 -0
- data/lib/samagotchi/web/public/copy.js +7 -0
- data/lib/samagotchi/web/public/index.html +8 -0
- data/lib/samagotchi/web/public/stage_model.js +11 -4
- data/lib/samagotchi/web/public/stage_view.js +5 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -1
- data/lib/samagotchi/web/public/turn_view.js +15 -6
- data/lib/samagotchi/web/qr.rb +7 -1
- data/lib/samagotchi/web/server.rb +6 -2
- data/lib/samagotchi/worker.rb +14 -3
- metadata +24 -20
|
@@ -80,7 +80,7 @@ module Samagotchi
|
|
|
80
80
|
# A line that is one word: a slash and a name, no spaces, no second
|
|
81
81
|
# slash. `/modle` is one; `/foo bar` and `/usr/bin/env` are not (they
|
|
82
82
|
# are prompts).
|
|
83
|
-
UNKNOWN_COMMAND_WORD =
|
|
83
|
+
UNKNOWN_COMMAND_WORD = %r{\A/[A-Za-z][A-Za-z0-9_-]*\z}
|
|
84
84
|
|
|
85
85
|
# Whether +line+ is a command word no command answers: a typo like
|
|
86
86
|
# `/modle`, which a UI must not send to the model as a prompt.
|
|
@@ -100,7 +100,7 @@ module Samagotchi
|
|
|
100
100
|
|
|
101
101
|
text = line.to_s.strip
|
|
102
102
|
close = DidYouMean::SpellChecker.new(dictionary: @entries.map(&:name)).correct(text).first
|
|
103
|
-
"Unknown command #{text}. #{
|
|
103
|
+
"Unknown command #{text}. #{"Did you mean #{close}? " if close}/help lists the commands."
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
# @return [Entry, nil] the first entry the UI runs itself (local) that
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -4,7 +4,6 @@ require "yaml"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require "uri"
|
|
7
|
-
require "set"
|
|
8
7
|
require_relative "atomic_file"
|
|
9
8
|
require_relative "config_text_edit"
|
|
10
9
|
require_relative "log"
|
|
@@ -56,8 +55,8 @@ module Samagotchi
|
|
|
56
55
|
end
|
|
57
56
|
|
|
58
57
|
# Section names: lower alnum only, no _ or -.
|
|
59
|
-
SECTION_RE = /\A[a-z0-9]+\z
|
|
60
|
-
LEAF_RE = /\A[a-z0-9_]+\z
|
|
58
|
+
SECTION_RE = /\A[a-z0-9]+\z/
|
|
59
|
+
LEAF_RE = /\A[a-z0-9_]+\z/
|
|
61
60
|
|
|
62
61
|
ENTRIES = [
|
|
63
62
|
# universal – env+config+cli
|
|
@@ -126,7 +125,7 @@ module Samagotchi
|
|
|
126
125
|
Entry.new(key: "context.status", yaml_path: %w[context status], type: :bool, default: true, expose: %i[env config cli]),
|
|
127
126
|
Entry.new(key: "context.window_tokens", yaml_path: %w[context window_tokens], type: :integer, default: nil, expose: %i[env config cli]),
|
|
128
127
|
Entry.new(key: "context.chars_per_token", yaml_path: %w[context chars_per_token], type: :float, default: 4.0, expose: %i[env config cli]),
|
|
129
|
-
Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds],type: :string, default: "20,40,60,80", expose: %i[env config cli]),
|
|
128
|
+
Entry.new(key: "context.status_thresholds", yaml_path: %w[context status_thresholds], type: :string, default: "20,40,60,80", expose: %i[env config cli]),
|
|
130
129
|
Entry.new(key: "context.status_cadence", yaml_path: %w[context status_cadence], type: :integer, default: 0, expose: %i[env config cli]),
|
|
131
130
|
|
|
132
131
|
Entry.new(key: "thinking.turn_preamble", yaml_path: %w[thinking turn_preamble], type: :bool, default: true, expose: %i[env config cli]),
|
|
@@ -136,7 +135,7 @@ module Samagotchi
|
|
|
136
135
|
# the flag and the env, above config.yml's value.
|
|
137
136
|
Entry.new(key: "thinking.level", yaml_path: %w[thinking level], type: :string, default: "default", expose: %i[env config]),
|
|
138
137
|
|
|
139
|
-
Entry.new(key: "default.
|
|
138
|
+
Entry.new(key: "default.max_tokens", yaml_path: %w[default max_tokens], type: :integer, default: nil, expose: %i[env config cli]),
|
|
140
139
|
Entry.new(key: "max_tool_output_chars", yaml_path: %w[max_tool_output_chars], type: :integer, default: 10_000, expose: %i[env config cli]),
|
|
141
140
|
|
|
142
141
|
Entry.new(key: "retry.max", yaml_path: %w[retry max], type: :integer, default: 5, expose: %i[env config cli]),
|
|
@@ -145,6 +144,10 @@ module Samagotchi
|
|
|
145
144
|
# Asks again in the same turn after an empty answer (EmptyAnswerRetry);
|
|
146
145
|
# 0 = off, capped at 3. No CLI flag: workers get no CLI args.
|
|
147
146
|
Entry.new(key: "retry.empty_answer", yaml_path: %w[retry empty_answer], type: :integer, default: 1, expose: %i[env config]),
|
|
147
|
+
# Seconds a generation must have streamed only thinking before a message
|
|
148
|
+
# for the running turn (the user's, chi send's, a parent agent's) cuts
|
|
149
|
+
# it (Engine#cut_for_steer); 0 = never. No CLI flag: workers get no CLI args.
|
|
150
|
+
Entry.new(key: "steer.cut_after", yaml_path: %w[steer cut_after], type: :integer, default: 20, expose: %i[env config]),
|
|
148
151
|
# A turn's step limit: model ↔ tool rounds before it stops and offers to continue (IterationLimit); integer ≥ 1.
|
|
149
152
|
# --no-interrupt turns get max(1000, this). No CLI flag: --no-interrupt is the flag.
|
|
150
153
|
Entry.new(key: "turn.max_iterations", yaml_path: %w[turn max_iterations], type: :integer, default: 100, expose: %i[env config]),
|
|
@@ -186,7 +189,7 @@ module Samagotchi
|
|
|
186
189
|
# env+config only
|
|
187
190
|
Entry.new(key: "default.input", yaml_path: %w[default input], type: :string, default: nil, expose: %i[env config]),
|
|
188
191
|
Entry.new(key: "history.file", yaml_path: %w[history file], type: :string, default: nil, expose: %i[env config]),
|
|
189
|
-
Entry.new(key: "skip_agent_md", yaml_path: %w[skip_agent_md], type: :bool, default: false, expose: %i[env config])
|
|
192
|
+
Entry.new(key: "skip_agent_md", yaml_path: %w[skip_agent_md], type: :bool, default: false, expose: %i[env config])
|
|
190
193
|
].freeze
|
|
191
194
|
|
|
192
195
|
# Top-level maps read by their own code, whose entry names and contents
|
|
@@ -228,11 +231,13 @@ module Samagotchi
|
|
|
228
231
|
# Coercion helpers
|
|
229
232
|
def coerce(entry, raw)
|
|
230
233
|
return nil if raw.nil?
|
|
234
|
+
|
|
231
235
|
# For string, preserve as-is (including trailing spaces like "Please ")
|
|
232
236
|
if entry.type == :string
|
|
233
237
|
# A YAML list for a "|"-separated setting (web.annotate_presets).
|
|
234
238
|
str = raw.is_a?(Array) ? raw.join("|") : raw.to_s
|
|
235
239
|
return nil if str.empty? && entry.type != :string
|
|
240
|
+
|
|
236
241
|
return str
|
|
237
242
|
end
|
|
238
243
|
str = raw.to_s.strip
|
|
@@ -267,7 +272,7 @@ module Samagotchi
|
|
|
267
272
|
lowered = str.downcase
|
|
268
273
|
allowed = entry.enum_values.map(&:downcase)
|
|
269
274
|
unless allowed.include?(lowered)
|
|
270
|
-
Log.warn(:config, "invalid_value", echo: "Warning: invalid value for #{entry.key}: #{raw.inspect} (allowed: #{entry.enum_values.join(
|
|
275
|
+
Log.warn(:config, "invalid_value", echo: "Warning: invalid value for #{entry.key}: #{raw.inspect} (allowed: #{entry.enum_values.join(", ")}) — using default", key: entry.key)
|
|
271
276
|
return entry.default
|
|
272
277
|
end
|
|
273
278
|
# return canonical casing from enum_values
|
|
@@ -296,6 +301,7 @@ module Samagotchi
|
|
|
296
301
|
raw = cli_overrides[entry.key]
|
|
297
302
|
# cli_overrides may already be coerced; detect by type
|
|
298
303
|
return [raw, :cli] if already_coerced?(entry, raw)
|
|
304
|
+
|
|
299
305
|
return [coerce(entry, raw), :cli]
|
|
300
306
|
end
|
|
301
307
|
if cli_overrides.key?(entry.cli_flag)
|
|
@@ -323,7 +329,7 @@ module Samagotchi
|
|
|
323
329
|
|
|
324
330
|
def already_coerced?(entry, val)
|
|
325
331
|
case entry.type
|
|
326
|
-
when :bool then
|
|
332
|
+
when :bool then [true, false].include?(val)
|
|
327
333
|
when :integer then val.is_a?(Integer)
|
|
328
334
|
when :float then val.is_a?(Float) || val.is_a?(Integer)
|
|
329
335
|
when :enum then entry.enum_values.include?(val)
|
|
@@ -343,6 +349,7 @@ module Samagotchi
|
|
|
343
349
|
cur = data
|
|
344
350
|
yaml_path.each_with_index do |seg, idx|
|
|
345
351
|
return nil unless cur.is_a?(Hash)
|
|
352
|
+
|
|
346
353
|
last = idx == yaml_path.size - 1
|
|
347
354
|
if last
|
|
348
355
|
candidates = [seg, seg.to_sym]
|
|
@@ -361,6 +368,7 @@ module Samagotchi
|
|
|
361
368
|
# also try kebab alias for section (should not exist per spec, but be lenient)
|
|
362
369
|
nxt ||= cur[seg.tr("_", "-")] || cur[seg.tr("_", "-").to_sym]
|
|
363
370
|
return nil if nxt.nil?
|
|
371
|
+
|
|
364
372
|
cur = nxt
|
|
365
373
|
end
|
|
366
374
|
end
|
|
@@ -404,6 +412,13 @@ module Samagotchi
|
|
|
404
412
|
get_with_origin(key).first
|
|
405
413
|
end
|
|
406
414
|
|
|
415
|
+
# default.max_tokens: the most tokens one generation may produce, or
|
|
416
|
+
# nil when unset or not positive (the server's own limit).
|
|
417
|
+
def max_tokens
|
|
418
|
+
value = get("default.max_tokens").to_i
|
|
419
|
+
value if value.positive?
|
|
420
|
+
end
|
|
421
|
+
|
|
407
422
|
# A timeout setting (server.open_timeout, server.read_timeout) in
|
|
408
423
|
# seconds, +given+ (a caller's own value) first; one that isn't
|
|
409
424
|
# positive (0, a word) is the key's default, on every kind of host.
|
|
@@ -419,6 +434,7 @@ module Samagotchi
|
|
|
419
434
|
def get_with_origin(key)
|
|
420
435
|
entry = find_by_key(key)
|
|
421
436
|
raise ArgumentError, "unknown config key: #{key}" unless entry
|
|
437
|
+
|
|
422
438
|
# Live resolve so ENV changes (as in specs) are reflected without explicit reload
|
|
423
439
|
# Use current ENV and file (via ConfigFile's cached reader), plus any CLI
|
|
424
440
|
# overrides captured via reload!
|
|
@@ -546,7 +562,7 @@ module Samagotchi
|
|
|
546
562
|
candidates = candidates.uniq
|
|
547
563
|
near = near_names(probe, candidates)
|
|
548
564
|
close = (near + candidates.select { |c| c.split(".").last == probe.split(".").last }).uniq.first(3)
|
|
549
|
-
hint = close.empty? ? "" : " (did you mean #{close.map { |c| "'#{prefix}#{c}'" }.join(
|
|
565
|
+
hint = close.empty? ? "" : " (did you mean #{close.map { |c| "'#{prefix}#{c}'" }.join(" or ")}?)"
|
|
550
566
|
"config: unknown key '#{key}'#{hint}"
|
|
551
567
|
end
|
|
552
568
|
|
|
@@ -582,6 +598,7 @@ module Samagotchi
|
|
|
582
598
|
# parsed Hash or nil (missing file / parse error / non-mapping top level).
|
|
583
599
|
def read_yaml(env: ENV, path: global_path(env: env))
|
|
584
600
|
return nil if path.nil?
|
|
601
|
+
|
|
585
602
|
cache = @yaml_cache ||= {}
|
|
586
603
|
unless File.file?(path)
|
|
587
604
|
cache.delete(path)
|
|
@@ -593,7 +610,7 @@ module Samagotchi
|
|
|
593
610
|
return hit[1] if hit && hit[0] == key
|
|
594
611
|
|
|
595
612
|
data = begin
|
|
596
|
-
YAML.
|
|
613
|
+
YAML.safe_load_file(path, permitted_classes: [], aliases: false)
|
|
597
614
|
rescue StandardError
|
|
598
615
|
nil
|
|
599
616
|
end
|
|
@@ -631,7 +648,7 @@ module Samagotchi
|
|
|
631
648
|
# else warns once and counts as unset.
|
|
632
649
|
def bool_flag(value, where, key)
|
|
633
650
|
return nil if value.nil?
|
|
634
|
-
return value if
|
|
651
|
+
return value if [true, false].include?(value)
|
|
635
652
|
|
|
636
653
|
text = value.to_s.strip.downcase
|
|
637
654
|
return true if text == "true"
|
|
@@ -653,6 +670,7 @@ module Samagotchi
|
|
|
653
670
|
# nil. Reserved keys warn once and are dropped. nil when nothing is left.
|
|
654
671
|
def sampling_map(value, where)
|
|
655
672
|
return nil if value.nil?
|
|
673
|
+
|
|
656
674
|
unless value.is_a?(Hash)
|
|
657
675
|
warn_once "Warning: #{where}: sampling must be a map of request parameters; ignored"
|
|
658
676
|
return nil
|
|
@@ -661,6 +679,7 @@ module Samagotchi
|
|
|
661
679
|
result = value.each_with_object({}) do |(raw_key, raw_value), acc|
|
|
662
680
|
key = raw_key.to_s.strip
|
|
663
681
|
next if key.empty?
|
|
682
|
+
|
|
664
683
|
if SAMPLING_RESERVED_KEYS.include?(key)
|
|
665
684
|
warn_once "Warning: #{where}: sampling.#{key} is set by chi; ignored"
|
|
666
685
|
next
|
|
@@ -687,6 +706,7 @@ module Samagotchi
|
|
|
687
706
|
def clear_yaml_cache!(path = nil)
|
|
688
707
|
cache = @yaml_cache
|
|
689
708
|
return if cache.nil?
|
|
709
|
+
|
|
690
710
|
if path
|
|
691
711
|
cache.delete(path)
|
|
692
712
|
else
|
|
@@ -734,6 +754,7 @@ module Samagotchi
|
|
|
734
754
|
raw_hosts.each do |raw_name, raw_cfg|
|
|
735
755
|
name = raw_name.to_s.strip
|
|
736
756
|
next if name.empty?
|
|
757
|
+
|
|
737
758
|
unless name.match?(HOST_NAME_RE)
|
|
738
759
|
warn_once "Warning: ignoring hosts entry '#{name}': must match /[a-z0-9][a-z0-9._-]*/i"
|
|
739
760
|
next
|
|
@@ -795,7 +816,7 @@ module Samagotchi
|
|
|
795
816
|
next
|
|
796
817
|
end
|
|
797
818
|
port_val = port.to_s.strip.empty? ? 8080 : port.to_i
|
|
798
|
-
if port_val <= 0 || port_val >
|
|
819
|
+
if port_val <= 0 || port_val > 65_535
|
|
799
820
|
warn_once "Warning: ignoring hosts entry '#{name}': invalid port"
|
|
800
821
|
next
|
|
801
822
|
end
|
|
@@ -835,7 +856,7 @@ module Samagotchi
|
|
|
835
856
|
default_host = Samagotchi::Config.resolve("server.host", **opts).to_s.strip
|
|
836
857
|
default_host = "localhost" if default_host.empty?
|
|
837
858
|
default_port = Samagotchi::Config.resolve("server.port", **opts).to_i
|
|
838
|
-
default_port = 8080 if default_port <= 0 || default_port >
|
|
859
|
+
default_port = 8080 if default_port <= 0 || default_port > 65_535
|
|
839
860
|
transport_sym = Samagotchi::Config.resolve_with_origin("server.transport", **opts).then do |value, origin|
|
|
840
861
|
origin == :default ? nil : value.to_sym
|
|
841
862
|
end
|
|
@@ -877,7 +898,11 @@ module Samagotchi
|
|
|
877
898
|
|
|
878
899
|
# enabled: false (or "false", any case) in a hosts entry.
|
|
879
900
|
def host_disabled?(cfg)
|
|
880
|
-
enabled = cfg.key?("enabled")
|
|
901
|
+
enabled = if cfg.key?("enabled")
|
|
902
|
+
cfg["enabled"]
|
|
903
|
+
else
|
|
904
|
+
(cfg.key?(:enabled) ? cfg[:enabled] : true)
|
|
905
|
+
end
|
|
881
906
|
enabled == false || enabled.to_s.strip.downcase == "false"
|
|
882
907
|
end
|
|
883
908
|
private_class_method :host_disabled?
|
|
@@ -959,6 +984,7 @@ module Samagotchi
|
|
|
959
984
|
# Only serialize if non-default or explicitly configured hosts:
|
|
960
985
|
# include when hosts file exists with hosts: section or when workers need propagation
|
|
961
986
|
return nil if hosts.nil? || hosts.empty?
|
|
987
|
+
|
|
962
988
|
# Serialize to JSON with string keys
|
|
963
989
|
simple = hosts.transform_values do |v|
|
|
964
990
|
# A url entry travels as its url (host/port come from it); the API
|
|
@@ -990,6 +1016,7 @@ module Samagotchi
|
|
|
990
1016
|
def bundle_settings(env: ENV, path: global_path(env: env))
|
|
991
1017
|
raw = section("bundles", env: env, path: path)
|
|
992
1018
|
return {} if raw.nil?
|
|
1019
|
+
|
|
993
1020
|
unless raw.is_a?(Hash)
|
|
994
1021
|
Log.warn(:hooks, "bundles_section_invalid", echo: "[samagotchi:hooks] config.yml bundles: must be a mapping of bundle name to settings; ignored")
|
|
995
1022
|
return {}
|
|
@@ -1025,6 +1052,7 @@ module Samagotchi
|
|
|
1025
1052
|
raw.each_with_object({}) do |(k, v), result|
|
|
1026
1053
|
key = k.to_s.strip
|
|
1027
1054
|
next if key.empty?
|
|
1055
|
+
|
|
1028
1056
|
val = v.to_s.strip
|
|
1029
1057
|
next if val.empty?
|
|
1030
1058
|
|
|
@@ -42,7 +42,7 @@ module Samagotchi
|
|
|
42
42
|
|
|
43
43
|
# The note's own text, without its frame.
|
|
44
44
|
def text_of(message)
|
|
45
|
-
fetch(message, :content).to_s.sub(/\A\[CONTEXT NOTE[^\n]*\n/, "").
|
|
45
|
+
fetch(message, :content).to_s.sub(/\A\[CONTEXT NOTE[^\n]*\n/, "").delete_suffix("\n[END NOTE]")
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def fetch(message, key)
|
|
@@ -64,7 +64,7 @@ module Samagotchi
|
|
|
64
64
|
def self.last_bucket(conversation)
|
|
65
65
|
message = conversation.reverse.find do |entry|
|
|
66
66
|
content = entry[:content].to_s
|
|
67
|
-
entry[:role] == "system" &&
|
|
67
|
+
entry[:role] == "system" && content.start_with?(STATUS_PREFIX, LINE_PREFIX)
|
|
68
68
|
end
|
|
69
69
|
return nil unless message
|
|
70
70
|
|
|
@@ -221,11 +221,11 @@ module Samagotchi
|
|
|
221
221
|
end
|
|
222
222
|
|
|
223
223
|
def emit?(bucket:, iteration_index:)
|
|
224
|
-
bucket_changed = if @last_bucket.nil?
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
224
|
+
bucket_changed = bucket != if @last_bucket.nil?
|
|
225
|
+
"under#{@thresholds.first}"
|
|
226
|
+
else
|
|
227
|
+
@last_bucket
|
|
228
|
+
end
|
|
229
229
|
cadence_due = @cadence.positive? && ((iteration_index + 1) % @cadence).zero?
|
|
230
230
|
bucket_changed || cadence_due
|
|
231
231
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
|
+
require_relative "log"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
6
7
|
# A session worker's continue offer: what it announces when a turn runs out
|
|
@@ -55,9 +56,9 @@ module Samagotchi
|
|
|
55
56
|
# (or kept, after a cancelled continue turn) is announced and asked.
|
|
56
57
|
def after_turn(result, continue: false, no_interrupt: false)
|
|
57
58
|
outcome = @turn_flow.after_turn(result, continue: continue, no_interrupt: no_interrupt)
|
|
58
|
-
return unless
|
|
59
|
+
return unless %i[continue_offered continue_cancelled].include?(outcome)
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
announce_offer
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
# A prompt (or a reminder turn) taken while a continue is offered
|
|
@@ -111,20 +112,26 @@ module Samagotchi
|
|
|
111
112
|
# The continue offer was answered yes: resume the conversation without a
|
|
112
113
|
# user message, with the iteration limit the offer's turn had. A failure
|
|
113
114
|
# keeps the offer, as the REPL does ("continue prompt preserved"), and
|
|
114
|
-
# asks again
|
|
115
|
-
#
|
|
115
|
+
# asks again, one before the turn began too (logged, no :turn_failed).
|
|
116
|
+
# The answer's text is carried into the turn as it begins; a turn that
|
|
117
|
+
# never began leaves it to no other.
|
|
116
118
|
def run_continue_turn(command)
|
|
117
119
|
offer = @turn_flow.offer
|
|
118
120
|
close("answered")
|
|
119
121
|
steer = @continue_steer
|
|
120
122
|
@continue_steer = nil
|
|
121
123
|
begin
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
begin
|
|
125
|
+
@engine.steer_next_turn(steer[:text], source: steer[:source]) if steer
|
|
126
|
+
@turn_flow.before_continue_turn
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
Log.warn(:worker, "turn_not_begun", continue: true, error: e.class.name, msg: e.message)
|
|
129
|
+
return announce_offer(offer)
|
|
130
|
+
end
|
|
124
131
|
@run_turn.call(nil, continue: true, origin: { client_id: command[:client_id] }.compact,
|
|
125
132
|
max_iterations: @max_iterations.call(offer[:no_interrupt])) do |result, error|
|
|
126
133
|
if error
|
|
127
|
-
|
|
134
|
+
announce_offer(offer)
|
|
128
135
|
else
|
|
129
136
|
after_turn(result, continue: true, no_interrupt: offer[:no_interrupt])
|
|
130
137
|
end
|
|
@@ -145,7 +152,7 @@ module Samagotchi
|
|
|
145
152
|
|
|
146
153
|
private
|
|
147
154
|
|
|
148
|
-
def
|
|
155
|
+
def announce_offer(offer = @turn_flow.offer)
|
|
149
156
|
@engine.announce(type: :continue_offered, context: offer[:context], no_interrupt: offer[:no_interrupt])
|
|
150
157
|
ask
|
|
151
158
|
end
|
data/lib/samagotchi/debug_log.rb
CHANGED
|
@@ -86,7 +86,7 @@ module Samagotchi
|
|
|
86
86
|
|
|
87
87
|
def rotate
|
|
88
88
|
File.open("#{@path}.lock", File::RDWR | File::CREAT, 0o600) do |lock|
|
|
89
|
-
|
|
89
|
+
next unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
90
90
|
|
|
91
91
|
stat = File.stat(@path)
|
|
92
92
|
File.rename(@path, "#{@path}.1") if stat.ino == @ino && stat.size >= @max_bytes
|
|
@@ -22,7 +22,8 @@ module Samagotchi
|
|
|
22
22
|
# command goes through the runner, and the dirs are injectable, so specs
|
|
23
23
|
# (and smokes with register: false) never touch the real system.
|
|
24
24
|
class MacOS
|
|
25
|
-
Error
|
|
25
|
+
class Error < StandardError
|
|
26
|
+
end
|
|
26
27
|
|
|
27
28
|
APP_NAME = "Chi Helper"
|
|
28
29
|
BUNDLE_ID = "dev.samagotchi.chi-helper"
|
|
@@ -166,9 +167,9 @@ module Samagotchi
|
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
# Keeps the login item as it was.
|
|
169
|
-
def upgrade(&
|
|
170
|
+
def upgrade(&)
|
|
170
171
|
was_login = installed? && login == "enabled"
|
|
171
|
-
install(force: true, login: was_login, &
|
|
172
|
+
install(force: true, login: was_login, &)
|
|
172
173
|
end
|
|
173
174
|
|
|
174
175
|
# SMAppService status as the app reports it ("enabled",
|
|
@@ -212,7 +213,7 @@ module Samagotchi
|
|
|
212
213
|
env = launch["env"].is_a?(Hash) ? launch["env"] : {}
|
|
213
214
|
dump, = @runner.run([PBS, "-dump"])
|
|
214
215
|
_, running = @runner.run(["pgrep", "-x", EXECUTABLE])
|
|
215
|
-
hotkey =
|
|
216
|
+
hotkey = JSON.parse(File.read(hotkey_path)) rescue nil
|
|
216
217
|
result.merge(
|
|
217
218
|
app_version: app_version,
|
|
218
219
|
login: login,
|
|
@@ -30,7 +30,7 @@ module Samagotchi
|
|
|
30
30
|
# The request options of a retry generation: the turn's sampling, with
|
|
31
31
|
# TEMPERATURE when it sets none (a configured nil keeps "don't send").
|
|
32
32
|
def self.sampling(base)
|
|
33
|
-
base
|
|
33
|
+
base ||= {}
|
|
34
34
|
base.key?(:temperature) ? base : base.merge(temperature: TEMPERATURE)
|
|
35
35
|
end
|
|
36
36
|
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -47,6 +47,8 @@ require_relative "tools/memory"
|
|
|
47
47
|
require_relative "muted_memories"
|
|
48
48
|
require_relative "used_memories"
|
|
49
49
|
require_relative "turn_state"
|
|
50
|
+
require_relative "generation_phase"
|
|
51
|
+
require_relative "waiting_steer"
|
|
50
52
|
require_relative "bundle_needs"
|
|
51
53
|
require_relative "model_overlay"
|
|
52
54
|
require_relative "served_model"
|
|
@@ -103,6 +105,10 @@ module Samagotchi
|
|
|
103
105
|
# steers) and the idle layer's activity clock. Built first: a plugin
|
|
104
106
|
# may steer or ask whether a turn runs while it loads.
|
|
105
107
|
@turn_state = TurnState.new(clock: -> { monotonic_now })
|
|
108
|
+
# What the running generation streams: whether a steer may cut it.
|
|
109
|
+
@generation_phase = GenerationPhase.new(clock: -> { monotonic_now })
|
|
110
|
+
# A message that may cut it but came too early: it cuts later.
|
|
111
|
+
@waiting_steer = WaitingSteer.new
|
|
106
112
|
@scratch = scratch
|
|
107
113
|
@no_interrupt = no_interrupt
|
|
108
114
|
@chat_backend = nil
|
|
@@ -325,6 +331,69 @@ module Samagotchi
|
|
|
325
331
|
@turn_state.steer(text, source: source)
|
|
326
332
|
end
|
|
327
333
|
|
|
334
|
+
# A message for the running turn from +source+ (Steer.source_for_client:
|
|
335
|
+
# nil for the user, "chi_send", "parent_agent") cuts the streaming
|
|
336
|
+
# generation when it has streamed only thinking for steer.cut_after
|
|
337
|
+
# seconds (GenerationPhase): the loops' cut path then starts the step
|
|
338
|
+
# again with the message, no nudge, no retry spent. Too early, the
|
|
339
|
+
# message waits (WaitingSteer) and cuts once the thinking passes it
|
|
340
|
+
# (#recheck_waiting_steer, on each thinking chunk), unless a boundary
|
|
341
|
+
# hands it to the model first. A plugin's message never cuts.
|
|
342
|
+
# Call it after the message is queued, with the #input_epoch read
|
|
343
|
+
# before queueing it. Outside any lock; a cut that lands just after the
|
|
344
|
+
# generation ended is harmless (the loop re-asks).
|
|
345
|
+
# @return [Boolean] whether a generation was cut now
|
|
346
|
+
def cut_for_steer(source, epoch: nil)
|
|
347
|
+
return false unless Steer.cuts?(source)
|
|
348
|
+
|
|
349
|
+
after = Config.get("steer.cut_after").to_i
|
|
350
|
+
return false unless after.positive?
|
|
351
|
+
|
|
352
|
+
ctrl = active_cancel_controller
|
|
353
|
+
return false unless ctrl
|
|
354
|
+
return steer_cut!(ctrl, source.to_s) if @generation_phase.cuttable?(after)
|
|
355
|
+
|
|
356
|
+
Log.info(:turn, "steer_cut_waits", source: source.to_s) if @waiting_steer.wait!(source, epoch: epoch)
|
|
357
|
+
false
|
|
358
|
+
rescue StandardError
|
|
359
|
+
false
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# The drains that took input so far (WaitingSteer#epoch): read it before
|
|
363
|
+
# queueing a message for #cut_for_steer.
|
|
364
|
+
def input_epoch
|
|
365
|
+
@waiting_steer.epoch
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# A thinking chunk streamed: a waiting message cuts once the thinking
|
|
369
|
+
# passes steer.cut_after.
|
|
370
|
+
def recheck_waiting_steer
|
|
371
|
+
return unless @waiting_steer.waiting?
|
|
372
|
+
|
|
373
|
+
after = Config.get("steer.cut_after").to_i
|
|
374
|
+
return unless after.positive? && @generation_phase.cuttable?(after)
|
|
375
|
+
|
|
376
|
+
source = @waiting_steer.take
|
|
377
|
+
ctrl = active_cancel_controller
|
|
378
|
+
steer_cut!(ctrl, source, waited: true) if source && ctrl
|
|
379
|
+
rescue StandardError
|
|
380
|
+
nil
|
|
381
|
+
end
|
|
382
|
+
private :recheck_waiting_steer
|
|
383
|
+
|
|
384
|
+
def steer_cut!(ctrl, source, waited: false)
|
|
385
|
+
age = @generation_phase.age
|
|
386
|
+
cut = ctrl.cancel_generation!(:steer, { by: "steer", steer: true, source: source, reason: "a new message" })
|
|
387
|
+
if cut
|
|
388
|
+
@waiting_steer.clear!
|
|
389
|
+
fields = { source: source, age: age&.round(1) }
|
|
390
|
+
fields[:waited] = true if waited
|
|
391
|
+
Log.info(:turn, "steer_cut", **fields)
|
|
392
|
+
end
|
|
393
|
+
cut
|
|
394
|
+
end
|
|
395
|
+
private :steer_cut!
|
|
396
|
+
|
|
328
397
|
# Like #steer, for the turn that begins next (a continue turn answered
|
|
329
398
|
# with a text): it joins that turn at its first boundary. Callable from
|
|
330
399
|
# any thread; a turn that never begins keeps it for the next one, unless
|
|
@@ -344,10 +413,13 @@ module Samagotchi
|
|
|
344
413
|
# in a --non-interactive run) and the plugin steers. At the after-answer
|
|
345
414
|
# boundary the steers are dropped: the model answered, and a nudge would
|
|
346
415
|
# only restart the turn. The steer part fails on its own, never taking
|
|
347
|
-
# the user's lines with it.
|
|
416
|
+
# the user's lines with it. Items are a String (a user line), a
|
|
417
|
+
# Steer::Line (a worker's input line with its sender) or a steer Hash.
|
|
348
418
|
def turn_drain(pending_input)
|
|
349
419
|
lambda do |at_answer: false|
|
|
350
420
|
lines = pending_input ? Array(pending_input.call) : []
|
|
421
|
+
# The input went to the model: a message that waited to cut is in.
|
|
422
|
+
@waiting_steer.delivered! unless lines.empty?
|
|
351
423
|
lines + take_steers(at_answer)
|
|
352
424
|
end
|
|
353
425
|
end
|
|
@@ -557,8 +629,8 @@ module Samagotchi
|
|
|
557
629
|
|
|
558
630
|
# Run the block with the event log held: no event is numbered or
|
|
559
631
|
# delivered meanwhile, and events the block emits keep their order.
|
|
560
|
-
def synchronize_events(&
|
|
561
|
-
@session_observer.synchronize(&
|
|
632
|
+
def synchronize_events(&)
|
|
633
|
+
@session_observer.synchronize(&)
|
|
562
634
|
end
|
|
563
635
|
|
|
564
636
|
# Add a context note to the session's conversation, between turns: a
|
|
@@ -638,8 +710,8 @@ module Samagotchi
|
|
|
638
710
|
# Start an anytime command on its own thread (D8), one #shutdown waits
|
|
639
711
|
# for, so its command_ran is announced before the process leaves.
|
|
640
712
|
# @return [Thread]
|
|
641
|
-
def spawn_anytime(&
|
|
642
|
-
thread = Thread.new(&
|
|
713
|
+
def spawn_anytime(&)
|
|
714
|
+
thread = Thread.new(&)
|
|
643
715
|
@lifecycle_mutex.synchronize do
|
|
644
716
|
@anytime_threads.select!(&:alive?)
|
|
645
717
|
@anytime_threads << thread
|
|
@@ -656,9 +728,9 @@ module Samagotchi
|
|
|
656
728
|
|
|
657
729
|
# Add a plugin's init task (Plugin::Api#init at commit); it starts with
|
|
658
730
|
# #start_init_tasks!.
|
|
659
|
-
def add_init_task(bundle:, label:, plugin_label:, provides_tools:, quiet:, timeout:, failed: nil, &
|
|
731
|
+
def add_init_task(bundle:, label:, plugin_label:, provides_tools:, quiet:, timeout:, failed: nil, &)
|
|
660
732
|
@plugin_tasks.add(bundle: bundle, label: label, plugin_label: plugin_label, provides_tools: provides_tools,
|
|
661
|
-
quiet: quiet, timeout: timeout, failed: failed, &
|
|
733
|
+
quiet: quiet, timeout: timeout, failed: failed, &)
|
|
662
734
|
end
|
|
663
735
|
|
|
664
736
|
# Start the init tasks not started yet, each on its own thread
|
|
@@ -836,8 +908,8 @@ module Samagotchi
|
|
|
836
908
|
# @param name [Symbol] one of the hook names (see Hooks module)
|
|
837
909
|
# @param block [Proc] receives an event hash (may mutate in place)
|
|
838
910
|
# @return [void]
|
|
839
|
-
def register_hook(name, &
|
|
840
|
-
@hooks.register(name, &
|
|
911
|
+
def register_hook(name, &)
|
|
912
|
+
@hooks.register(name, &)
|
|
841
913
|
end
|
|
842
914
|
|
|
843
915
|
# Unregister a previously registered hook.
|
|
@@ -1369,9 +1441,7 @@ module Samagotchi
|
|
|
1369
1441
|
end
|
|
1370
1442
|
|
|
1371
1443
|
# @return [Session] current session (Engine owns create/resume)
|
|
1372
|
-
|
|
1373
|
-
@session
|
|
1374
|
-
end
|
|
1444
|
+
attr_reader :session
|
|
1375
1445
|
|
|
1376
1446
|
# The kernel's Tools::Peers, following the current session. cancelled?
|
|
1377
1447
|
# is the running turn's cancel, for a tool that waits (delegate_result):
|
|
@@ -1424,9 +1494,7 @@ module Samagotchi
|
|
|
1424
1494
|
end
|
|
1425
1495
|
|
|
1426
1496
|
# @return [IdleRecap, nil] the idle recap detector, or nil when disabled
|
|
1427
|
-
|
|
1428
|
-
@recap
|
|
1429
|
-
end
|
|
1497
|
+
attr_reader :recap
|
|
1430
1498
|
|
|
1431
1499
|
# Write the recap now, before the session is left (IdleRecap#write_now).
|
|
1432
1500
|
# @return [String, nil] the recap written, nil when none was (recap off,
|
|
@@ -1887,6 +1955,8 @@ module Samagotchi
|
|
|
1887
1955
|
# shared inactivity clock so the idle recap detector (shared with the REPL)
|
|
1888
1956
|
# treats the just-finished turn as activity and re-arms its window.
|
|
1889
1957
|
left = @turn_state.finish!
|
|
1958
|
+
@generation_phase.finished!
|
|
1959
|
+
@waiting_steer.clear!
|
|
1890
1960
|
Client.swap_probe_cancel(probe_cancel_before)
|
|
1891
1961
|
# The next turn's window and served model are asked again: the cache
|
|
1892
1962
|
# (this host's, process-wide) is dropped here, at the turn's end,
|
|
@@ -2128,6 +2198,7 @@ module Samagotchi
|
|
|
2128
2198
|
config_path = Samagotchi::ConfigFile.global_path
|
|
2129
2199
|
data = Samagotchi::ConfigFile.read_yaml(path: config_path)
|
|
2130
2200
|
return Hooks::Loader.load(data, failures: @guardrail_failures) if data.is_a?(Hash)
|
|
2201
|
+
|
|
2131
2202
|
Hooks::Registry.new
|
|
2132
2203
|
end
|
|
2133
2204
|
|
|
@@ -2150,11 +2221,15 @@ module Samagotchi
|
|
|
2150
2221
|
Hooks::BundleLoader.load(bundle_name: bundle_name, hooks_dir: hooks_dir, metadata: data[:hooks], registry: @hooks,
|
|
2151
2222
|
failures: @guardrail_failures, settings: settings[bundle_name.to_s] || {},
|
|
2152
2223
|
requires_chi: data[:requires_chi])
|
|
2153
|
-
rescue Exception => e
|
|
2224
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
|
|
2225
|
+
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
2226
|
+
|
|
2154
2227
|
Log.error(:hooks, "bundle_load_failed", echo: "[samagotchi:hooks] bundle '#{bundle_name}' failed to load hooks: #{e.class}: #{e.message}", bundle: bundle_name, error: e.class.name)
|
|
2155
2228
|
end
|
|
2156
2229
|
end
|
|
2157
|
-
rescue Exception => e
|
|
2230
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
|
|
2231
|
+
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
2232
|
+
|
|
2158
2233
|
Log.error(:hooks, "bundles_load_failed", echo: "[samagotchi:hooks] failed to load bundle hooks: #{e.class}: #{e.message}", error: e.class.name)
|
|
2159
2234
|
end
|
|
2160
2235
|
|
|
@@ -2278,7 +2353,6 @@ module Samagotchi
|
|
|
2278
2353
|
end
|
|
2279
2354
|
private :bundle_settings
|
|
2280
2355
|
|
|
2281
|
-
|
|
2282
2356
|
# The session's current model as a recap target: its host's OpenAI API
|
|
2283
2357
|
# (native llama.cpp hosts serve /v1/chat/completions too), key variable
|
|
2284
2358
|
# and bare model name, as a turn resolves them.
|
|
@@ -2331,11 +2405,16 @@ module Samagotchi
|
|
|
2331
2405
|
progress = nil
|
|
2332
2406
|
case event[:type]
|
|
2333
2407
|
when :generation_started
|
|
2408
|
+
@generation_phase.started!
|
|
2334
2409
|
watch&.started(event[:iteration])
|
|
2335
2410
|
when :generation_chunk
|
|
2336
2411
|
# A chunk without the lanes (no loop of ours sends one) counts as text.
|
|
2337
2412
|
text = event.key?(:text) ? event[:text] : event[:content]
|
|
2338
2413
|
progress = { thinking: event[:thinking].to_s, text: text.to_s }
|
|
2414
|
+
@generation_phase.chunk!(**progress, tool_call: event[:tool_call])
|
|
2415
|
+
recheck_waiting_steer unless progress[:thinking].empty?
|
|
2416
|
+
when :generation_retrying then @generation_phase.retrying!
|
|
2417
|
+
when :generation_completed, :generation_cancelled then @generation_phase.finished!
|
|
2339
2418
|
end
|
|
2340
2419
|
# The chat loop asked again without the thinking fields: a notice,
|
|
2341
2420
|
# not an event of its own.
|
|
@@ -15,7 +15,8 @@ module Samagotchi
|
|
|
15
15
|
# SAMAGOTCHI_UPDATE_GEM_FILE=<path to a .gem> (hidden, for smokes) makes that
|
|
16
16
|
# file the newest version and installs it instead.
|
|
17
17
|
class GemUpdate
|
|
18
|
-
Error
|
|
18
|
+
class Error < StandardError
|
|
19
|
+
end
|
|
19
20
|
|
|
20
21
|
LATEST_URL = URI("https://rubygems.org/api/v1/versions/samagotchi/latest.json")
|
|
21
22
|
# Not SAMAGOTCHI_UPDATE_GEM: that is the env form of config update.gem.
|