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
|
@@ -28,7 +28,7 @@ module Samagotchi
|
|
|
28
28
|
lower = path.downcase
|
|
29
29
|
if lower.end_with?(".zip")
|
|
30
30
|
normalize_zip(path)
|
|
31
|
-
elsif lower.end_with?(".tar.gz"
|
|
31
|
+
elsif lower.end_with?(".tar.gz", ".tgz", ".tar")
|
|
32
32
|
normalize_tar(path)
|
|
33
33
|
else
|
|
34
34
|
ext = File.extname(path).downcase
|
|
@@ -57,6 +57,7 @@ module Samagotchi
|
|
|
57
57
|
def self.git_url?(str)
|
|
58
58
|
s = str.to_s.strip
|
|
59
59
|
return false if s.empty?
|
|
60
|
+
|
|
60
61
|
s.match?(%r{\A(?:https?://|git@|ssh://|git://|file://).+})
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -78,6 +79,7 @@ module Samagotchi
|
|
|
78
79
|
args += [url, clone_dir]
|
|
79
80
|
result = system(*args)
|
|
80
81
|
raise UnknownSourceError, "git clone failed for #{git_url}" unless result && File.directory?(clone_dir)
|
|
82
|
+
|
|
81
83
|
# Capture HEAD commit before stripping .git (audit trail)
|
|
82
84
|
commit = nil
|
|
83
85
|
begin
|
|
@@ -103,12 +105,11 @@ module Samagotchi
|
|
|
103
105
|
end
|
|
104
106
|
end
|
|
105
107
|
|
|
106
|
-
private
|
|
107
|
-
|
|
108
108
|
def self.normalize_zip(zip_path)
|
|
109
109
|
Dir.mktmpdir("samagotchi-zip-") do |extract_dir|
|
|
110
110
|
result = system("unzip", "-o", zip_path, "-d", extract_dir)
|
|
111
111
|
raise UnknownSourceError, "unzip failed for #{zip_path}" unless result
|
|
112
|
+
|
|
112
113
|
clean_copy_of(extract_dir)
|
|
113
114
|
end
|
|
114
115
|
end
|
|
@@ -119,6 +120,7 @@ module Samagotchi
|
|
|
119
120
|
cmd = is_gz ? ["tar", "-xzf"] : ["tar", "-xf"]
|
|
120
121
|
result = system(*cmd, tar_path, "-C", extract_dir)
|
|
121
122
|
raise UnknownSourceError, "tar extraction failed for #{tar_path}" unless result
|
|
123
|
+
|
|
122
124
|
clean_copy_of(extract_dir)
|
|
123
125
|
end
|
|
124
126
|
end
|
|
@@ -150,6 +152,8 @@ module Samagotchi
|
|
|
150
152
|
end
|
|
151
153
|
[dst, true]
|
|
152
154
|
end
|
|
155
|
+
|
|
156
|
+
private_class_method :normalize_zip, :normalize_tar, :clean_copy_of
|
|
153
157
|
end
|
|
154
158
|
end
|
|
155
159
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "digest"
|
|
3
4
|
require_relative "provenance"
|
|
4
5
|
require_relative "manifest"
|
|
@@ -20,7 +21,7 @@ module Samagotchi
|
|
|
20
21
|
return nil unless data
|
|
21
22
|
|
|
22
23
|
raw_scope = scope_override || data[:scope]&.to_s
|
|
23
|
-
scope =
|
|
24
|
+
scope = raw_scope.nil? || raw_scope.strip.empty? ? "system" : raw_scope
|
|
24
25
|
target_dir = MemoryPaths.scope_dir(scope)
|
|
25
26
|
files = data[:files] || {}
|
|
26
27
|
details = {}
|
|
@@ -108,6 +109,7 @@ module Samagotchi
|
|
|
108
109
|
def self.index_has_entry?(scope, file_key)
|
|
109
110
|
path = IndexUpdater.index_path_for(scope)
|
|
110
111
|
return false unless path && File.exist?(path)
|
|
112
|
+
|
|
111
113
|
content = File.read(path)
|
|
112
114
|
[file_key.delete_suffix(".md"), file_key].uniq.any? { |name| content.match?(IndexUpdater.managed_pattern(name)) }
|
|
113
115
|
end
|
|
@@ -110,12 +110,13 @@ module Samagotchi
|
|
|
110
110
|
|
|
111
111
|
def skip?
|
|
112
112
|
val = ENV[SKIP_ENV].to_s.strip.downcase
|
|
113
|
-
|
|
113
|
+
%w[1 true].include?(val)
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
# Unparseable versions fall back to the old "any difference upgrades" behaviour.
|
|
117
117
|
def installed_newer?(installed, shipped)
|
|
118
|
-
installed
|
|
118
|
+
installed = installed.to_s
|
|
119
|
+
shipped = shipped.to_s
|
|
119
120
|
return false unless Gem::Version.correct?(installed) && Gem::Version.correct?(shipped)
|
|
120
121
|
|
|
121
122
|
Gem::Version.new(installed) > Gem::Version.new(shipped)
|
|
@@ -44,7 +44,7 @@ module Samagotchi
|
|
|
44
44
|
def self.empty!(bundles_dir: MemoryPaths.bundles_dir, older_than_days: nil, now: Time.now, dry_run: false)
|
|
45
45
|
entries = self.entries(bundles_dir: bundles_dir, now: now)
|
|
46
46
|
if older_than_days
|
|
47
|
-
cutoff = now - older_than_days.to_i * 86_400
|
|
47
|
+
cutoff = now - (older_than_days.to_i * 86_400)
|
|
48
48
|
entries = entries.select { |e| e.time < cutoff }
|
|
49
49
|
end
|
|
50
50
|
return entries if dry_run
|
|
@@ -98,7 +98,7 @@ module Samagotchi
|
|
|
98
98
|
File.directory?(path) && !File.symlink?(path)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
def self.entry_for(trash_dir, name,
|
|
101
|
+
def self.entry_for(trash_dir, name, _now)
|
|
102
102
|
path = File.join(trash_dir, name)
|
|
103
103
|
time = parse_timestamp(name) || File.mtime(path)
|
|
104
104
|
files, bytes = count_files(path)
|
|
@@ -126,11 +126,14 @@ module Samagotchi
|
|
|
126
126
|
bytes = 0
|
|
127
127
|
Dir.glob(File.join(dir, "**", "*"), File::FNM_DOTMATCH).each do |f|
|
|
128
128
|
next if File.directory?(f)
|
|
129
|
+
|
|
129
130
|
files += 1
|
|
130
131
|
bytes += File.size(f) rescue 0
|
|
131
132
|
end
|
|
132
133
|
[files, bytes]
|
|
133
134
|
end
|
|
135
|
+
|
|
136
|
+
private_class_method :real_dir?, :entry_for, :parse_timestamp, :count_files
|
|
134
137
|
end
|
|
135
138
|
end
|
|
136
139
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "fileutils"
|
|
3
4
|
require "digest"
|
|
4
5
|
require_relative "../memory_paths"
|
|
@@ -38,7 +39,7 @@ module Samagotchi
|
|
|
38
39
|
# Determine scope from flag or provenance
|
|
39
40
|
cli_scope = @scope.to_s.strip.empty? ? nil : @scope
|
|
40
41
|
raw = cli_scope || data[:scope]&.to_s
|
|
41
|
-
target_scope =
|
|
42
|
+
target_scope = raw.nil? || raw.strip.empty? ? "system" : raw
|
|
42
43
|
target_dir = MemoryPaths.scope_dir(target_scope) or raise UninstallError, "invalid scope: #{target_scope}"
|
|
43
44
|
|
|
44
45
|
files = data[:files] || {}
|
|
@@ -55,19 +56,20 @@ module Samagotchi
|
|
|
55
56
|
end.reject { |_, others| others.empty? }
|
|
56
57
|
|
|
57
58
|
blocked = []
|
|
58
|
-
files.each do |file_key,
|
|
59
|
+
files.each do |file_key, _meta|
|
|
59
60
|
file_key_str = file_key.to_s
|
|
60
61
|
target_path = File.join(target_dir, file_key_str)
|
|
61
62
|
base_path = provenance.base_path(file_key_str)
|
|
62
63
|
next unless File.exist?(target_path)
|
|
63
64
|
next if shared.key?(file_key_str)
|
|
65
|
+
|
|
64
66
|
if !@force && File.exist?(base_path) && Merger.current_modified?(base_path, target_path)
|
|
65
67
|
blocked << file_key_str
|
|
66
68
|
@warnings << "Skipped #{file_key_str}: local edits detected (use --force to remove)"
|
|
67
69
|
end
|
|
68
70
|
end
|
|
69
71
|
|
|
70
|
-
raise UninstallError, "Uninstall blocked: #{blocked.join(
|
|
72
|
+
raise UninstallError, "Uninstall blocked: #{blocked.join(", ")} has local edits (use --force)" if blocked.any?
|
|
71
73
|
|
|
72
74
|
files.each do |file_key, _meta|
|
|
73
75
|
file_key_str = file_key.to_s
|
|
@@ -108,10 +110,11 @@ module Samagotchi
|
|
|
108
110
|
end
|
|
109
111
|
end
|
|
110
112
|
# Remove hooks dir if empty, else rm_rf will clean
|
|
111
|
-
FileUtils.rm_rf(hooks_dir)
|
|
113
|
+
FileUtils.rm_rf(hooks_dir)
|
|
112
114
|
else
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
+
# A stale hooks dir goes even when provenance has no hooks map
|
|
116
|
+
# (rm_rf ignores a missing dir).
|
|
117
|
+
FileUtils.rm_rf(provenance.hooks_dir)
|
|
115
118
|
end
|
|
116
119
|
|
|
117
120
|
plugin_path = provenance.plugin_path(data)
|
|
@@ -32,10 +32,7 @@ module Samagotchi
|
|
|
32
32
|
|
|
33
33
|
key = model_name.to_s
|
|
34
34
|
.downcase
|
|
35
|
-
.gsub(/[^a-z0-9]+/, "-")
|
|
36
|
-
.gsub(/-+/, "-")
|
|
37
|
-
.sub(/\A-/, "")
|
|
38
|
-
.sub(/-\z/, "")
|
|
35
|
+
.gsub(/[^a-z0-9]+/, "-").squeeze("-").delete_prefix("-").delete_suffix("-")
|
|
39
36
|
key.empty? ? nil : key
|
|
40
37
|
end
|
|
41
38
|
|
|
@@ -43,12 +40,13 @@ module Samagotchi
|
|
|
43
40
|
# Respects MemoryBundle overrides (delegates to Tools::MemoryRead.memories_dir).
|
|
44
41
|
def self.overlay_path_for(name, key, scope)
|
|
45
42
|
return nil unless key && !key.to_s.strip.empty?
|
|
43
|
+
|
|
46
44
|
File.join(Tools::MemoryRead.memories_dir(scope), "#{name}.#{key.to_s.strip}.md")
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
# Regex matching overlay file names: /<something>.<lowercase-dashed-key>.md\z
|
|
50
48
|
# Useful for tooling, tests, and future index suppression.
|
|
51
|
-
OVERLAY_SUFFIX_PATTERN = /\.([a-z0-9
|
|
49
|
+
OVERLAY_SUFFIX_PATTERN = /\.([a-z0-9-]+)\.md\z/
|
|
52
50
|
|
|
53
51
|
# The base file name (`tips.md`) an overlay-looking name
|
|
54
52
|
# (`tips.qwen3.md`) would belong to, or nil when it doesn't match the
|
|
@@ -17,10 +17,13 @@ module Samagotchi
|
|
|
17
17
|
MODEL_FROM_CLI_ENV = "SAMAGOTCHI_DEFAULT_MODEL_FROM_CLI"
|
|
18
18
|
# No model anywhere (--model, default.model in config.yml, the env): a
|
|
19
19
|
# first run before any config. An ArgumentError, as before.
|
|
20
|
-
MissingModel
|
|
20
|
+
class MissingModel < ArgumentError
|
|
21
|
+
end
|
|
22
|
+
|
|
21
23
|
# A host-qualified model whose host isn't configured (a MissingModel,
|
|
22
24
|
# so every surface that reports a missing model reports it the same way).
|
|
23
|
-
UnknownHost
|
|
25
|
+
class UnknownHost < MissingModel
|
|
26
|
+
end
|
|
24
27
|
|
|
25
28
|
# Gemma 4 thinks in a channel: `<|channel>thought` … `<channel|>`. The
|
|
26
29
|
# one source for the parser, the stream splitter, the literal guard and
|
|
@@ -74,7 +77,7 @@ module Samagotchi
|
|
|
74
77
|
new(
|
|
75
78
|
name: "gemma4",
|
|
76
79
|
turn_start: "<|turn>",
|
|
77
|
-
turn_end: "<
|
|
80
|
+
turn_end: "<turn|>",
|
|
78
81
|
tool_call_open: "<|tool_call>",
|
|
79
82
|
tool_call_close: "<tool_call|>",
|
|
80
83
|
tool_response_open: "<|tool_response>",
|
|
@@ -88,7 +91,7 @@ module Samagotchi
|
|
|
88
91
|
user_prefix: "",
|
|
89
92
|
assistant_prefix: "",
|
|
90
93
|
model_prefix: "",
|
|
91
|
-
stop_sequences: ["<
|
|
94
|
+
stop_sequences: ["<turn|>", "<|tool_response>"],
|
|
92
95
|
tool_decl_format: :gemma4
|
|
93
96
|
)
|
|
94
97
|
end
|
|
@@ -165,14 +168,15 @@ module Samagotchi
|
|
|
165
168
|
if !parsed.host_name && (disabled = disabled_host_prefix(ref, hosts, env))
|
|
166
169
|
raise UnknownHost, "host '#{disabled}' is disabled (enabled: false in config.yml)"
|
|
167
170
|
end
|
|
171
|
+
|
|
168
172
|
warn_host_slash(ref, hosts) unless parsed.host_name
|
|
169
173
|
host = Samagotchi::ConfigFile.unknown_host_prefix(ref, hosts: hosts)
|
|
170
174
|
return model_name unless host
|
|
171
175
|
|
|
172
176
|
names = hosts.keys.map { |k| k.to_s.downcase }.sort
|
|
173
177
|
near = Samagotchi::Config.near_names(host, names).first(3)
|
|
174
|
-
hint = near.empty? ? "" : " (did you mean #{near.map { |n| "'#{n}'" }.join(
|
|
175
|
-
raise UnknownHost, "unknown host '#{host}' in model '#{ref}'#{hint}; the configured hosts are #{names.join(
|
|
178
|
+
hint = near.empty? ? "" : " (did you mean #{near.map { |n| "'#{n}'" }.join(" or ")}?)"
|
|
179
|
+
raise UnknownHost, "unknown host '#{host}' in model '#{ref}'#{hint}; the configured hosts are #{names.join(", ")}"
|
|
176
180
|
end
|
|
177
181
|
|
|
178
182
|
# The prefix of "box:x" when box is a host config.yml has with
|
|
@@ -275,7 +279,7 @@ module Samagotchi
|
|
|
275
279
|
FINGERPRINTS.each do |name, markers|
|
|
276
280
|
return [name, markers.join(" + ")] if markers.all? { |marker| template.include?(marker) }
|
|
277
281
|
end
|
|
278
|
-
return [
|
|
282
|
+
return %w[qwen36 ChatML] if template.include?("<|im_start|>")
|
|
279
283
|
|
|
280
284
|
nil
|
|
281
285
|
end
|
|
@@ -341,7 +345,7 @@ module Samagotchi
|
|
|
341
345
|
|
|
342
346
|
def self.configured(value, where)
|
|
343
347
|
profile = named(value)
|
|
344
|
-
Log.warn(:config, "unknown_profile", echo: "Warning: unknown profile #{value.to_s.inspect} in #{where} (allowed: #{NAMES.join(
|
|
348
|
+
Log.warn(:config, "unknown_profile", echo: "Warning: unknown profile #{value.to_s.inspect} in #{where} (allowed: #{NAMES.join(", ")}) — ignored", profile: value.to_s, where: where) unless profile
|
|
345
349
|
profile
|
|
346
350
|
end
|
|
347
351
|
private_class_method :configured
|
|
@@ -92,6 +92,7 @@ module Samagotchi
|
|
|
92
92
|
# passes down may never close: waiting on either would hang a script.
|
|
93
93
|
def read_stdin
|
|
94
94
|
return nil if @stdin.respond_to?(:tty?) && @stdin.tty?
|
|
95
|
+
|
|
95
96
|
if @stdin.respond_to?(:stat)
|
|
96
97
|
stat = @stdin.stat
|
|
97
98
|
return nil unless stat.pipe? || stat.file?
|
|
@@ -135,7 +136,7 @@ module Samagotchi
|
|
|
135
136
|
if owner
|
|
136
137
|
@stdout.puts("#{short} queued: its worker adds it within a few seconds")
|
|
137
138
|
else
|
|
138
|
-
queued = SessionInbox.find_new_note_files(File.dirname(
|
|
139
|
+
queued = SessionInbox.find_new_note_files(File.dirname(path, 2)).size
|
|
139
140
|
@stdout.puts("#{short} waits for the session's next start (#{queued} #{queued == 1 ? "note" : "notes"} queued)")
|
|
140
141
|
end
|
|
141
142
|
true
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Samagotchi
|
|
3
4
|
# OutputFormatter strips model wire-format tokens from raw engine output at
|
|
4
5
|
# render time, so downstream renderers (web UI, terminal UI) never show the
|
|
@@ -23,7 +24,6 @@ module Samagotchi
|
|
|
23
24
|
# wrapper forms; bare <name> content (no pipe) is deliberately preserved.
|
|
24
25
|
CONTROL_NAMES = %w[
|
|
25
26
|
turn
|
|
26
|
-
end_of_turn
|
|
27
27
|
tool_call
|
|
28
28
|
tool_response
|
|
29
29
|
tool
|
|
@@ -40,31 +40,32 @@ module Samagotchi
|
|
|
40
40
|
# <word> in the surrounding content is NOT stripped:
|
|
41
41
|
# <|name> / <name|> / <|name|>
|
|
42
42
|
INDIVIDUAL_RE = Regexp.new(
|
|
43
|
-
'<\\|(?:' + CONTROL_NAMES.join(
|
|
44
|
-
|
|
43
|
+
'<\\|(?:' + CONTROL_NAMES.join("|") + ')\\|?>' \
|
|
44
|
+
"|<(?:" + CONTROL_NAMES.join("|") + ')\\|>'
|
|
45
45
|
)
|
|
46
46
|
INDIVIDUAL_RE.freeze
|
|
47
47
|
|
|
48
48
|
# Angle-bracketed literal tokens stripped wholesale (including the thought
|
|
49
49
|
# channel's trailing word). Applied before INDIVIDUAL_RE so multi-part tokens
|
|
50
50
|
# are removed as a unit. Pipes are escaped; angle brackets are literal.
|
|
51
|
-
LITERALS_RE = /<\|"
|
|
51
|
+
LITERALS_RE = /<\|"\|>/
|
|
52
52
|
|
|
53
53
|
# Gemma 4's thought channel, as a whole block: `<|channel>thought` to its
|
|
54
54
|
# `<channel|>` close, or to the end of the text when the close is missing
|
|
55
55
|
# (ToolCallParser::Gemma strips the same pair). Removing only the marker
|
|
56
56
|
# would leave the model's private reasoning in the answer; the channel is
|
|
57
57
|
# the profile's thinking lane, not output.
|
|
58
|
-
GEMMA_THOUGHT_RE = /<\|\w*channel>thought.*?(?:<channel\|>|\z)/m
|
|
58
|
+
GEMMA_THOUGHT_RE = /<\|\w*channel>thought.*?(?:<channel\|>|\z)/m
|
|
59
59
|
|
|
60
60
|
# Qwen prompt-literal placeholders, e.g. </think>.
|
|
61
|
-
PROMPT_LITERALS = /\[\[SAMAGOTCHI_LITERAL_[A-Z_]+\]\]
|
|
61
|
+
PROMPT_LITERALS = /\[\[SAMAGOTCHI_LITERAL_[A-Z_]+\]\]/
|
|
62
62
|
|
|
63
63
|
# Qwen3.6 thinking blocks: <think>...</think> (including empty)
|
|
64
|
-
QWEN_THINK_RE =
|
|
64
|
+
QWEN_THINK_RE = %r{<think>.*?</think>}m
|
|
65
65
|
# Qwen tool_call XML blocks that survive when a tool call is rendered as text
|
|
66
66
|
# e.g. <tool_call>\n<function=list_reminders>\n</function>\n</tool_call>
|
|
67
|
-
QWEN_TOOL_CALL_RE =
|
|
67
|
+
QWEN_TOOL_CALL_RE = %r{<tool_call>.*?</tool_call>}m
|
|
68
|
+
|
|
68
69
|
module_function
|
|
69
70
|
|
|
70
71
|
# Remove both control-token and prompt-literal token families from +text+.
|
|
@@ -79,7 +80,7 @@ module Samagotchi
|
|
|
79
80
|
.gsub(/[ \t]+/, " ")
|
|
80
81
|
.gsub(/ *\n */, "\n")
|
|
81
82
|
.strip
|
|
82
|
-
cleaned.empty? ?
|
|
83
|
+
cleaned.empty? ? "" : cleaned
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
# The same tokens as #strip, but the text keeps its layout (indentation,
|
|
@@ -91,15 +92,15 @@ module Samagotchi
|
|
|
91
92
|
|
|
92
93
|
def remove_tokens(text)
|
|
93
94
|
text.to_s
|
|
94
|
-
.gsub(QWEN_THINK_RE,
|
|
95
|
-
.gsub(GEMMA_THOUGHT_RE,
|
|
96
|
-
.gsub(QWEN_TOOL_CALL_RE,
|
|
97
|
-
.gsub(LITERALS_RE,
|
|
98
|
-
.gsub(INDIVIDUAL_RE,
|
|
99
|
-
.gsub(PROMPT_LITERALS,
|
|
95
|
+
.gsub(QWEN_THINK_RE, "")
|
|
96
|
+
.gsub(GEMMA_THOUGHT_RE, "")
|
|
97
|
+
.gsub(QWEN_TOOL_CALL_RE, "")
|
|
98
|
+
.gsub(LITERALS_RE, "")
|
|
99
|
+
.gsub(INDIVIDUAL_RE, "")
|
|
100
|
+
.gsub(PROMPT_LITERALS, "")
|
|
100
101
|
# orphaned closing tags that may remain after block removal
|
|
101
|
-
.gsub(
|
|
102
|
-
.gsub(
|
|
102
|
+
.gsub(%r{</?think>}, "")
|
|
103
|
+
.gsub(%r{</?tool_call>}, "")
|
|
103
104
|
end
|
|
104
105
|
end
|
|
105
106
|
end
|
|
@@ -40,7 +40,8 @@ module Samagotchi
|
|
|
40
40
|
# @return [OwnerLock, nil] nil when another owner holds it
|
|
41
41
|
def self.acquire(session_dir, kind:, wait: DEFAULT_WAIT)
|
|
42
42
|
FileUtils.mkdir_p(session_dir)
|
|
43
|
-
|
|
43
|
+
# Held open on purpose: the flock lives as long as the OwnerLock (#release closes it).
|
|
44
|
+
file = File.open(path(session_dir), File::RDWR | File::CREAT, 0o644) # rubocop:disable Style/FileOpen
|
|
44
45
|
deadline = monotonic_now + wait.to_f
|
|
45
46
|
until file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
46
47
|
if monotonic_now >= deadline
|
|
@@ -51,6 +51,7 @@ module Samagotchi
|
|
|
51
51
|
# it runs as other commands do)
|
|
52
52
|
def command(name, description, anytime: false, &block)
|
|
53
53
|
raise ArgumentError, "command #{name.inspect} needs a block" unless block
|
|
54
|
+
|
|
54
55
|
name = name.to_s
|
|
55
56
|
raise ArgumentError, "command name #{name.inspect} must look like /name (a-z, 0-9, _ and -)" unless name.match?(COMMAND_NAME)
|
|
56
57
|
if (taken = @registries.commands.entries.find { |entry| entry.name == name })
|
|
@@ -78,9 +79,9 @@ module Samagotchi
|
|
|
78
79
|
# @param targets [#call, nil] args → what guardrail rules match: a Hash
|
|
79
80
|
# with paths: (absolute or relative to the cwd), command: (a shell
|
|
80
81
|
# command) and cwd:, each optional
|
|
81
|
-
def tool(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &
|
|
82
|
+
def tool(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &)
|
|
82
83
|
spec = Api.tool_spec(name, description, params: params, schema: schema, label: label, preview: preview,
|
|
83
|
-
targets: targets, &
|
|
84
|
+
targets: targets, &)
|
|
84
85
|
if (taken = @registries.tools[spec[:name]])
|
|
85
86
|
raise ArgumentError, "tool #{spec[:name]} is already registered (#{taken.source})"
|
|
86
87
|
end
|
|
@@ -124,9 +125,9 @@ module Samagotchi
|
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
# As Api#tool.
|
|
127
|
-
def tool(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &
|
|
128
|
+
def tool(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &)
|
|
128
129
|
spec = Api.tool_spec(name, description, params: params, schema: schema, label: label, preview: preview,
|
|
129
|
-
targets: targets, &
|
|
130
|
+
targets: targets, &)
|
|
130
131
|
raise ArgumentError, "tool #{spec[:name]} is registered twice" if @specs.any? { |s| s[:name] == spec[:name] }
|
|
131
132
|
|
|
132
133
|
@specs << spec
|
|
@@ -159,6 +160,7 @@ module Samagotchi
|
|
|
159
160
|
# @return [Service]
|
|
160
161
|
def service(name, eager: false, &block)
|
|
161
162
|
raise ArgumentError, "service #{name.inspect} needs a block" unless block
|
|
163
|
+
|
|
162
164
|
name = "#{@bundle}:#{name}"
|
|
163
165
|
raise ArgumentError, "service #{name} is registered twice" if @services.any? { |svc| svc.name == name }
|
|
164
166
|
raise ArgumentError, "this chi has no services (plugins: false)" unless @registries.services
|
|
@@ -249,6 +251,7 @@ module Samagotchi
|
|
|
249
251
|
# @return [Hash] {name:, schema:, label:, preview:, targets:, block:}
|
|
250
252
|
def self.tool_spec(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, &block)
|
|
251
253
|
raise ArgumentError, "tool #{name.inspect} needs a block" unless block
|
|
254
|
+
|
|
252
255
|
name = name.to_s
|
|
253
256
|
raise ArgumentError, "tool name #{name.inspect} must be a-z, 0-9 and _ (at most 48)" unless name.match?(TOOL_NAME)
|
|
254
257
|
raise ArgumentError, "tool #{name}: preview must respond to #call" if preview && !preview.respond_to?(:call)
|
|
@@ -323,7 +326,7 @@ module Samagotchi
|
|
|
323
326
|
# (a call built without one, in a spec say: the call without its
|
|
324
327
|
# name), string keys, typed by +parameters+, frozen.
|
|
325
328
|
def self.args_of(call, parameters = nil)
|
|
326
|
-
given = call[:args].is_a?(Hash) ? call[:args] : call.reject { |key, _|
|
|
329
|
+
given = call[:args].is_a?(Hash) ? call[:args] : call.reject { |key, _| %i[name args].include?(key) }
|
|
327
330
|
Tools::Args.coerce(given, parameters).freeze
|
|
328
331
|
end
|
|
329
332
|
|
|
@@ -107,9 +107,7 @@ module Samagotchi
|
|
|
107
107
|
# $XDG_STATE_HOME/samagotchi/plugins/<bundle>/, created on first use.
|
|
108
108
|
# @return [String]
|
|
109
109
|
def data_dir
|
|
110
|
-
@data_dir ||=
|
|
111
|
-
File.join(Paths.state_dir(env: @env), "plugins", @bundle).tap { |dir| FileUtils.mkdir_p(dir) }
|
|
112
|
-
end
|
|
110
|
+
@data_dir ||= File.join(Paths.state_dir(env: @env), "plugins", @bundle).tap { |dir| FileUtils.mkdir_p(dir) }
|
|
113
111
|
end
|
|
114
112
|
|
|
115
113
|
# The conversation so far, a frozen copy, without the system prompt.
|
|
@@ -73,7 +73,7 @@ module Samagotchi
|
|
|
73
73
|
true
|
|
74
74
|
rescue Exception => e # rubocop:disable Lint/RescueException -- a plugin's SyntaxError or exit must not stop chi
|
|
75
75
|
api&.abort!
|
|
76
|
-
raise if e.is_a?(
|
|
76
|
+
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
77
77
|
|
|
78
78
|
failed(bundle_name, basename, "#{e.class}: #{e.message}", failures)
|
|
79
79
|
end
|
|
@@ -62,7 +62,7 @@ module Samagotchi
|
|
|
62
62
|
def running? = @state == :running
|
|
63
63
|
|
|
64
64
|
# @return [Symbol] :idle, :running or :stopped
|
|
65
|
-
|
|
65
|
+
attr_reader :state
|
|
66
66
|
|
|
67
67
|
# Run the stop callbacks (newest first) once; a raise is logged, the
|
|
68
68
|
# rest still run. It never starts again after this.
|
|
@@ -38,6 +38,7 @@ module Samagotchi
|
|
|
38
38
|
def fork(messages:, title: nil, prompt: nil)
|
|
39
39
|
parent_id = @host.session_id.call or raise Error, "this session has no id yet"
|
|
40
40
|
raise Error, "a scratch session starts no other sessions: they would outlive it" if @host.scratch&.call
|
|
41
|
+
|
|
41
42
|
state_dir = self.state_dir
|
|
42
43
|
prompt = prompt.to_s.strip.empty? ? nil : prompt.to_s
|
|
43
44
|
check_children(parent_id, state_dir) if prompt
|
data/lib/samagotchi/prompt.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "model_profile"
|
|
4
4
|
require_relative "prompt_literal_guard"
|
|
5
5
|
require_relative "vision_context"
|
|
6
|
+
require_relative "tool_response"
|
|
7
|
+
require_relative "steer"
|
|
6
8
|
|
|
7
9
|
module Samagotchi
|
|
8
10
|
# Formats a message list into a prompt string ready for the /completion endpoint.
|
|
@@ -10,10 +12,13 @@ module Samagotchi
|
|
|
10
12
|
# Supports multiple model profiles (Gemma 4, Qwen 3.6, etc.) with different
|
|
11
13
|
# token formats, role prefixing strategies, and tool response handling.
|
|
12
14
|
#
|
|
13
|
-
# Gemma 4:
|
|
14
|
-
# - Turn markers: <|turn>ROLE\n...<
|
|
15
|
-
# - Tool
|
|
16
|
-
#
|
|
15
|
+
# Gemma 4 (as its chat template renders a conversation):
|
|
16
|
+
# - Turn markers: <|turn>ROLE\n...<turn|>\n, the text trimmed
|
|
17
|
+
# - Tool calls and their responses stay inside the model's turn:
|
|
18
|
+
# ...<tool_call|><|tool_response>response:NAME{value:<|"|>...<|"|>}<tool_response|>
|
|
19
|
+
# and the model goes on in the same turn
|
|
20
|
+
# - Generation cue: <|turn>model\n, or nothing after a tool response
|
|
21
|
+
# - An answer's thought is dropped once a later user turn starts
|
|
17
22
|
#
|
|
18
23
|
# Qwen 3.6:
|
|
19
24
|
# - Role framing: <|im_start|>ROLE\n...<|im_end|>
|
|
@@ -51,11 +56,9 @@ module Samagotchi
|
|
|
51
56
|
else
|
|
52
57
|
format_with_turn_markers(messages, profile, suffixes)
|
|
53
58
|
end
|
|
54
|
-
[text + prefill
|
|
59
|
+
[text + prefill_for(messages, profile, prefill), images]
|
|
55
60
|
end
|
|
56
61
|
|
|
57
|
-
private
|
|
58
|
-
|
|
59
62
|
def self.image_lines(items, profile, images)
|
|
60
63
|
return "" if items.empty?
|
|
61
64
|
|
|
@@ -71,19 +74,70 @@ module Samagotchi
|
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
def self.format_with_turn_markers(messages, profile, suffixes)
|
|
74
|
-
|
|
77
|
+
last_user = messages.rindex { |m| m[:role] == "user" } || -1
|
|
75
78
|
parts = messages.each_with_index.map do |m, index|
|
|
76
79
|
content = prompt_content_for(m, profile) + suffixes[index]
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
previous = index.positive? ? messages[index - 1][:role] : nil
|
|
81
|
+
following = messages[index + 1]&.dig(:role)
|
|
82
|
+
case m[:role]
|
|
83
|
+
when "tool_response"
|
|
84
|
+
# Inside the model's turn; closed when another role follows.
|
|
85
|
+
tool_response_blocks(content, profile) +
|
|
86
|
+
(following && following != "tool_response" && following != "model" ? "#{profile.turn_end}\n" : "")
|
|
87
|
+
when "model"
|
|
88
|
+
content = strip_gemma_thought(content) if index < last_user && !content.include?(profile.tool_call_open)
|
|
89
|
+
opener = previous == "tool_response" ? "" : "#{profile.turn_start}model\n"
|
|
90
|
+
closer = following == "tool_response" ? "" : "#{profile.turn_end}\n"
|
|
91
|
+
"#{opener}#{content.strip}#{closer}"
|
|
79
92
|
else
|
|
80
|
-
"#{profile.turn_start}#{m[:role]}\n#{content}#{profile.turn_end}\n"
|
|
93
|
+
"#{profile.turn_start}#{m[:role]}\n#{content.strip}#{profile.turn_end}\n"
|
|
81
94
|
end
|
|
82
95
|
end
|
|
83
|
-
parts << "#{profile.turn_start}model\n"
|
|
96
|
+
parts << "#{profile.turn_start}model\n" unless ends_in_tool_response?(messages)
|
|
84
97
|
parts.join
|
|
85
98
|
end
|
|
86
99
|
|
|
100
|
+
def self.ends_in_tool_response?(messages)
|
|
101
|
+
messages.last&.dig(:role) == "tool_response"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The text after the generation cue: +prefill+, except where Gemma's
|
|
105
|
+
# turn goes on after a tool response (no cue, so nothing to fill).
|
|
106
|
+
# The prompt and the model message the kernel keeps both use it.
|
|
107
|
+
def self.prefill_for(messages, profile, prefill)
|
|
108
|
+
return "" if !profile.uses_role_prefixes? && ends_in_tool_response?(messages)
|
|
109
|
+
|
|
110
|
+
prefill.to_s
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A native tool_response entry (ToolResponse.joined: "[name]" outputs
|
|
114
|
+
# joined by its SEPARATOR) as Gemma's template writes the results: one
|
|
115
|
+
# response:NAME{value:<|"|>…<|"|>} block per call. A part that doesn't
|
|
116
|
+
# open with a "[name]" is the previous output's own text.
|
|
117
|
+
def self.tool_response_blocks(content, profile)
|
|
118
|
+
runs = content.split(ToolResponse::SEPARATOR).each_with_object([]) do |part, acc|
|
|
119
|
+
if (match = part.match(/\A\[([^\]\s]+)\](?:\n| |\z)/))
|
|
120
|
+
acc << [match[1], match.post_match]
|
|
121
|
+
elsif acc.empty?
|
|
122
|
+
acc << ["unknown", part]
|
|
123
|
+
else
|
|
124
|
+
acc.last[1] = "#{acc.last[1]}#{ToolResponse::SEPARATOR}#{part}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
runs = [["unknown", ""]] if runs.empty?
|
|
128
|
+
q = profile.string_delim
|
|
129
|
+
runs.map do |name, body|
|
|
130
|
+
"#{profile.tool_response_open}response:#{name}{value:#{q}#{body}#{q}}#{profile.tool_response_close}"
|
|
131
|
+
end.join
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
GEMMA_THOUGHT_BLOCK = /#{Regexp.escape(ModelProfile::GEMMA_THOUGHT_CHANNEL_OPEN.delete_suffix("thought"))}.*?#{Regexp.escape(ModelProfile::GEMMA_THOUGHT_CHANNEL_CLOSE)}/m
|
|
135
|
+
private_constant :GEMMA_THOUGHT_BLOCK
|
|
136
|
+
|
|
137
|
+
def self.strip_gemma_thought(content)
|
|
138
|
+
content.gsub(GEMMA_THOUGHT_BLOCK, "")
|
|
139
|
+
end
|
|
140
|
+
|
|
87
141
|
def self.format_with_prefixes(messages, profile, suffixes)
|
|
88
142
|
# Qwen 3.6 style: <|im_start|>ROLE\n...<|im_end|>
|
|
89
143
|
parts = []
|
|
@@ -115,7 +169,12 @@ module Samagotchi
|
|
|
115
169
|
# peer session), and a turn note carries an error's text: escaped like
|
|
116
170
|
# user content so it can't close its turn.
|
|
117
171
|
role = ESCAPED_KINDS.include?(message[:kind].to_s) ? "user" : message[:role]
|
|
118
|
-
|
|
172
|
+
# A steer or merged input is led by a header naming its sender (added
|
|
173
|
+
# after the escape: the header has no literals to guard).
|
|
174
|
+
Steer.wire_text(message, PromptLiteralGuard.escape(message[:content], profile: profile, role: role))
|
|
119
175
|
end
|
|
176
|
+
|
|
177
|
+
private_class_method :image_lines, :format_with_turn_markers, :ends_in_tool_response?, :tool_response_blocks,
|
|
178
|
+
:strip_gemma_thought, :format_with_prefixes, :prompt_content_for
|
|
120
179
|
end
|
|
121
180
|
end
|
|
@@ -8,7 +8,7 @@ module Samagotchi
|
|
|
8
8
|
module PromptLiteralGuard
|
|
9
9
|
GEMMA_TOKEN_PAIRS = [
|
|
10
10
|
["[[SAMAGOTCHI_LITERAL_TURN_START]]", "<|turn>"],
|
|
11
|
-
["[[SAMAGOTCHI_LITERAL_TURN_END]]", "<
|
|
11
|
+
["[[SAMAGOTCHI_LITERAL_TURN_END]]", "<turn|>"],
|
|
12
12
|
["[[SAMAGOTCHI_LITERAL_TOOL_CALL_OPEN]]", "<|tool_call>"],
|
|
13
13
|
["[[SAMAGOTCHI_LITERAL_TOOL_CALL_CLOSE]]", "<tool_call|>"],
|
|
14
14
|
["[[SAMAGOTCHI_LITERAL_TOOL_RESPONSE_OPEN]]", "<|tool_response>"],
|