samagotchi 0.22.0 → 0.23.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 +33 -1
- data/bin/chi +9 -4
- data/docs/configuration.md +4 -4
- data/docs/releasing.md +1 -1
- 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/sse_writer.rb +0 -6
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -2
- data/lib/samagotchi/bridge.rb +12 -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 +2 -0
- 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 +38 -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 +17 -17
- data/lib/samagotchi/gem_update.rb +2 -1
- 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 +2 -2
- data/lib/samagotchi/guardrails/shell_paths.rb +2 -2
- 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 +18 -15
- data/lib/samagotchi/idle_reminders.rb +6 -3
- data/lib/samagotchi/image_store.rb +5 -2
- data/lib/samagotchi/kernel_loop.rb +31 -31
- data/lib/samagotchi/llm/chat_loop.rb +9 -6
- data/lib/samagotchi/llm/errors.rb +2 -2
- data/lib/samagotchi/llm/http.rb +12 -12
- data/lib/samagotchi/llm/openai_chat.rb +39 -28
- data/lib/samagotchi/llm/prompt_cache.rb +1 -1
- data/lib/samagotchi/log.rb +2 -2
- data/lib/samagotchi/log_subscriber.rb +2 -1
- 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 +68 -12
- 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/system_prompt.rb +19 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +1 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +10 -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/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 +9 -5
- data/lib/samagotchi/text_diff.rb +12 -12
- data/lib/samagotchi/thinking.rb +6 -2
- data/lib/samagotchi/thought_stream_splitter.rb +6 -3
- 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/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/web/app.rb +13 -6
- data/lib/samagotchi/web/markdown_renderer.rb +2 -1
- data/lib/samagotchi/web/qr.rb +7 -1
- data/lib/samagotchi/web/server.rb +6 -2
- data/lib/samagotchi/worker.rb +10 -2
- metadata +20 -20
|
@@ -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,7 @@
|
|
|
3
3
|
require_relative "model_profile"
|
|
4
4
|
require_relative "prompt_literal_guard"
|
|
5
5
|
require_relative "vision_context"
|
|
6
|
+
require_relative "tool_response"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# Formats a message list into a prompt string ready for the /completion endpoint.
|
|
@@ -10,10 +11,13 @@ module Samagotchi
|
|
|
10
11
|
# Supports multiple model profiles (Gemma 4, Qwen 3.6, etc.) with different
|
|
11
12
|
# token formats, role prefixing strategies, and tool response handling.
|
|
12
13
|
#
|
|
13
|
-
# Gemma 4:
|
|
14
|
-
# - Turn markers: <|turn>ROLE\n...<
|
|
15
|
-
# - Tool
|
|
16
|
-
#
|
|
14
|
+
# Gemma 4 (as its chat template renders a conversation):
|
|
15
|
+
# - Turn markers: <|turn>ROLE\n...<turn|>\n, the text trimmed
|
|
16
|
+
# - Tool calls and their responses stay inside the model's turn:
|
|
17
|
+
# ...<tool_call|><|tool_response>response:NAME{value:<|"|>...<|"|>}<tool_response|>
|
|
18
|
+
# and the model goes on in the same turn
|
|
19
|
+
# - Generation cue: <|turn>model\n, or nothing after a tool response
|
|
20
|
+
# - An answer's thought is dropped once a later user turn starts
|
|
17
21
|
#
|
|
18
22
|
# Qwen 3.6:
|
|
19
23
|
# - Role framing: <|im_start|>ROLE\n...<|im_end|>
|
|
@@ -51,11 +55,9 @@ module Samagotchi
|
|
|
51
55
|
else
|
|
52
56
|
format_with_turn_markers(messages, profile, suffixes)
|
|
53
57
|
end
|
|
54
|
-
[text + prefill
|
|
58
|
+
[text + prefill_for(messages, profile, prefill), images]
|
|
55
59
|
end
|
|
56
60
|
|
|
57
|
-
private
|
|
58
|
-
|
|
59
61
|
def self.image_lines(items, profile, images)
|
|
60
62
|
return "" if items.empty?
|
|
61
63
|
|
|
@@ -71,19 +73,70 @@ module Samagotchi
|
|
|
71
73
|
end
|
|
72
74
|
|
|
73
75
|
def self.format_with_turn_markers(messages, profile, suffixes)
|
|
74
|
-
|
|
76
|
+
last_user = messages.rindex { |m| m[:role] == "user" } || -1
|
|
75
77
|
parts = messages.each_with_index.map do |m, index|
|
|
76
78
|
content = prompt_content_for(m, profile) + suffixes[index]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
previous = index.positive? ? messages[index - 1][:role] : nil
|
|
80
|
+
following = messages[index + 1]&.dig(:role)
|
|
81
|
+
case m[:role]
|
|
82
|
+
when "tool_response"
|
|
83
|
+
# Inside the model's turn; closed when another role follows.
|
|
84
|
+
tool_response_blocks(content, profile) +
|
|
85
|
+
(following && following != "tool_response" && following != "model" ? "#{profile.turn_end}\n" : "")
|
|
86
|
+
when "model"
|
|
87
|
+
content = strip_gemma_thought(content) if index < last_user && !content.include?(profile.tool_call_open)
|
|
88
|
+
opener = previous == "tool_response" ? "" : "#{profile.turn_start}model\n"
|
|
89
|
+
closer = following == "tool_response" ? "" : "#{profile.turn_end}\n"
|
|
90
|
+
"#{opener}#{content.strip}#{closer}"
|
|
79
91
|
else
|
|
80
|
-
"#{profile.turn_start}#{m[:role]}\n#{content}#{profile.turn_end}\n"
|
|
92
|
+
"#{profile.turn_start}#{m[:role]}\n#{content.strip}#{profile.turn_end}\n"
|
|
81
93
|
end
|
|
82
94
|
end
|
|
83
|
-
parts << "#{profile.turn_start}model\n"
|
|
95
|
+
parts << "#{profile.turn_start}model\n" unless ends_in_tool_response?(messages)
|
|
84
96
|
parts.join
|
|
85
97
|
end
|
|
86
98
|
|
|
99
|
+
def self.ends_in_tool_response?(messages)
|
|
100
|
+
messages.last&.dig(:role) == "tool_response"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The text after the generation cue: +prefill+, except where Gemma's
|
|
104
|
+
# turn goes on after a tool response (no cue, so nothing to fill).
|
|
105
|
+
# The prompt and the model message the kernel keeps both use it.
|
|
106
|
+
def self.prefill_for(messages, profile, prefill)
|
|
107
|
+
return "" if !profile.uses_role_prefixes? && ends_in_tool_response?(messages)
|
|
108
|
+
|
|
109
|
+
prefill.to_s
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A native tool_response entry (ToolResponse.joined: "[name]" outputs
|
|
113
|
+
# joined by its SEPARATOR) as Gemma's template writes the results: one
|
|
114
|
+
# response:NAME{value:<|"|>…<|"|>} block per call. A part that doesn't
|
|
115
|
+
# open with a "[name]" is the previous output's own text.
|
|
116
|
+
def self.tool_response_blocks(content, profile)
|
|
117
|
+
runs = content.split(ToolResponse::SEPARATOR).each_with_object([]) do |part, acc|
|
|
118
|
+
if (match = part.match(/\A\[([^\]\s]+)\](?:\n| |\z)/))
|
|
119
|
+
acc << [match[1], match.post_match]
|
|
120
|
+
elsif acc.empty?
|
|
121
|
+
acc << ["unknown", part]
|
|
122
|
+
else
|
|
123
|
+
acc.last[1] = "#{acc.last[1]}#{ToolResponse::SEPARATOR}#{part}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
runs = [["unknown", ""]] if runs.empty?
|
|
127
|
+
q = profile.string_delim
|
|
128
|
+
runs.map do |name, body|
|
|
129
|
+
"#{profile.tool_response_open}response:#{name}{value:#{q}#{body}#{q}}#{profile.tool_response_close}"
|
|
130
|
+
end.join
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
GEMMA_THOUGHT_BLOCK = /#{Regexp.escape(ModelProfile::GEMMA_THOUGHT_CHANNEL_OPEN.delete_suffix("thought"))}.*?#{Regexp.escape(ModelProfile::GEMMA_THOUGHT_CHANNEL_CLOSE)}/m
|
|
134
|
+
private_constant :GEMMA_THOUGHT_BLOCK
|
|
135
|
+
|
|
136
|
+
def self.strip_gemma_thought(content)
|
|
137
|
+
content.gsub(GEMMA_THOUGHT_BLOCK, "")
|
|
138
|
+
end
|
|
139
|
+
|
|
87
140
|
def self.format_with_prefixes(messages, profile, suffixes)
|
|
88
141
|
# Qwen 3.6 style: <|im_start|>ROLE\n...<|im_end|>
|
|
89
142
|
parts = []
|
|
@@ -117,5 +170,8 @@ module Samagotchi
|
|
|
117
170
|
role = ESCAPED_KINDS.include?(message[:kind].to_s) ? "user" : message[:role]
|
|
118
171
|
PromptLiteralGuard.escape(message[:content], profile: profile, role: role)
|
|
119
172
|
end
|
|
173
|
+
|
|
174
|
+
private_class_method :image_lines, :format_with_turn_markers, :ends_in_tool_response?, :tool_response_blocks,
|
|
175
|
+
:strip_gemma_thought, :format_with_prefixes, :prompt_content_for
|
|
120
176
|
end
|
|
121
177
|
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>"],
|
|
@@ -101,8 +101,8 @@ module Samagotchi
|
|
|
101
101
|
# @param payload [Hash] {question:, options:, header:, multi_select:, allow_freeform:}
|
|
102
102
|
# @return [String] normalized answer JSON
|
|
103
103
|
def request(payload)
|
|
104
|
-
result = open_question(
|
|
105
|
-
|
|
104
|
+
result = open_question(payload.slice(:question, :options, :header)
|
|
105
|
+
.merge(multi_select: !!payload[:multi_select], allow_freeform: !!payload[:allow_freeform]))
|
|
106
106
|
# Dismissed (the card's dismiss, Esc): an answer of its own, not a
|
|
107
107
|
# tool failure the model learns to avoid the tool from.
|
|
108
108
|
result = { dismissed: true, id: result[:id], note: DISMISSED_NOTE } if result.is_a?(Hash) && result[:error] == "no answer"
|
|
@@ -288,6 +288,7 @@ module Samagotchi
|
|
|
288
288
|
if parent_agent && (reason = Guardrails::ParentContinue.refusal(pending, sel, allowed: parent_continue))
|
|
289
289
|
raise Refused, reason
|
|
290
290
|
end
|
|
291
|
+
|
|
291
292
|
if @standing
|
|
292
293
|
# No waiter: cleared here, so a second answer finds none.
|
|
293
294
|
standing = @standing
|
|
@@ -332,9 +333,7 @@ module Samagotchi
|
|
|
332
333
|
true
|
|
333
334
|
end
|
|
334
335
|
|
|
335
|
-
|
|
336
|
-
@sync_handler = block
|
|
337
|
-
end
|
|
336
|
+
attr_writer :sync_handler
|
|
338
337
|
|
|
339
338
|
# @return [Boolean] a REPL answers inline (no wait loop)
|
|
340
339
|
def sync_handler? = !@sync_handler.nil?
|
|
@@ -355,7 +354,7 @@ module Samagotchi
|
|
|
355
354
|
next if id && pending[:id].to_s != id.to_s
|
|
356
355
|
next if @answer || pending[:status].to_s != "pending"
|
|
357
356
|
# Its poster closes it; nothing waits on a cancel.
|
|
358
|
-
raise NotDismissable, "answer #{Array(pending[:options]).join(
|
|
357
|
+
raise NotDismissable, "answer #{Array(pending[:options]).join(" or ")}" if @standing && id
|
|
359
358
|
next if @standing
|
|
360
359
|
|
|
361
360
|
pending[:status] = "cancelled"
|
|
@@ -469,9 +468,10 @@ module Samagotchi
|
|
|
469
468
|
def validated_answer(pending, sel, fm, parent_agent:, parent_setting:)
|
|
470
469
|
opts = Array(pending[:options])
|
|
471
470
|
invalid = sel.reject { |v| opts.include?(v) }
|
|
472
|
-
raise ArgumentError, "invalid selection: #{invalid.join(
|
|
471
|
+
raise ArgumentError, "invalid selection: #{invalid.join(", ")} (valid: #{opts.join(", ")})" unless invalid.empty?
|
|
473
472
|
raise ArgumentError, "single-select question: got #{sel.size} selections" if !pending[:multi_select] && sel.size > 1
|
|
474
473
|
raise ArgumentError, "selection required" if pending[:multi_select] == false && sel.empty? && fm.nil?
|
|
474
|
+
|
|
475
475
|
indices = sel.map { |v| opts.index(v) }
|
|
476
476
|
# The one whose answer settles the approval (Approval.settle, by index).
|
|
477
477
|
if parent_setting
|
|
@@ -114,7 +114,7 @@ module Samagotchi
|
|
|
114
114
|
return range if range
|
|
115
115
|
|
|
116
116
|
default = IdleRecap::RecapPrompt::DEFAULT_SENTENCES
|
|
117
|
-
Log.warn(:recap, "sentences_invalid", echo: "Warning: invalid value for recap.sentences: #{value.to_s.inspect} — using #{default.join(
|
|
117
|
+
Log.warn(:recap, "sentences_invalid", echo: "Warning: invalid value for recap.sentences: #{value.to_s.inspect} — using #{default.join("-")}",
|
|
118
118
|
value: value.to_s)
|
|
119
119
|
default
|
|
120
120
|
end
|
|
@@ -35,7 +35,7 @@ module Samagotchi
|
|
|
35
35
|
# The parent's user (or a parent agent) answered the relay card.
|
|
36
36
|
# @param by [String] "user" or "parent_agent"
|
|
37
37
|
# @return [Boolean] false when the relay isn't open (first answer wins)
|
|
38
|
-
def record(id, selected_indices: [], freeform: nil, dismissed: false
|
|
38
|
+
def record(id, by:, selected_indices: [], freeform: nil, dismissed: false)
|
|
39
39
|
@mutex.synchronize do
|
|
40
40
|
relay = @relays[id.to_s]
|
|
41
41
|
next false unless relay && relay[:state] == "open"
|
|
@@ -81,7 +81,7 @@ module Samagotchi
|
|
|
81
81
|
@mutex.synchronize do
|
|
82
82
|
return "No active reminders." if @reminders.empty?
|
|
83
83
|
|
|
84
|
-
lines = @reminders.map do |
|
|
84
|
+
lines = @reminders.map do |_name, r|
|
|
85
85
|
" #{r[:name]} (interval: #{r[:interval_minutes]}m, id: #{r[:id]})"
|
|
86
86
|
end
|
|
87
87
|
"Active reminders:\n" + lines.join("\n")
|
|
@@ -98,6 +98,7 @@ module Samagotchi
|
|
|
98
98
|
@mutex.synchronize do
|
|
99
99
|
@reminders.each_value.filter_map do |r|
|
|
100
100
|
next unless r[:next_fire_at] && now >= r[:next_fire_at]
|
|
101
|
+
|
|
101
102
|
{ name: r[:name], description: r[:description], interval_minutes: r[:interval_minutes] }
|
|
102
103
|
end.freeze
|
|
103
104
|
end
|
|
@@ -197,6 +197,7 @@ module Samagotchi
|
|
|
197
197
|
return usage_error("--wait takes one session") if options[:wait] && options[:ids].uniq.size > 1
|
|
198
198
|
return usage_error("at most #{MAX_IMAGES} images") if options[:images].size > MAX_IMAGES
|
|
199
199
|
return new_options(options) if options[:new]
|
|
200
|
+
|
|
200
201
|
%i[dir model].each { |key| return usage_error("--#{key} needs --new") if options[key] }
|
|
201
202
|
return usage_error("give session ids") if options[:ids].empty?
|
|
202
203
|
|
|
@@ -229,6 +230,7 @@ module Samagotchi
|
|
|
229
230
|
# caller never closes still hangs, as it would for cat.)
|
|
230
231
|
def read_stdin
|
|
231
232
|
return nil if @stdin.respond_to?(:tty?) && @stdin.tty?
|
|
233
|
+
|
|
232
234
|
if @stdin.respond_to?(:stat)
|
|
233
235
|
stat = @stdin.stat
|
|
234
236
|
return nil unless stat.pipe? || stat.file?
|
|
@@ -328,8 +330,8 @@ module Samagotchi
|
|
|
328
330
|
|
|
329
331
|
# The session as it was before the message went in (ReplyWait's
|
|
330
332
|
# baseline).
|
|
331
|
-
def baseline_of(session, **
|
|
332
|
-
ReplyWait.baseline_of(session, **
|
|
333
|
+
def baseline_of(session, **)
|
|
334
|
+
ReplyWait.baseline_of(session, **)
|
|
333
335
|
end
|
|
334
336
|
|
|
335
337
|
# A new idle session's first turn, with the images. Its worker's Bridge
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -35,8 +35,8 @@ module Samagotchi
|
|
|
35
35
|
SORT_ORDERS = %w[asc desc].freeze
|
|
36
36
|
|
|
37
37
|
attr_accessor :id, :metadata_version, :mode, :model_name, :working_directory, :messages,
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
:created_at, :updated_at, :status, :last_prompt, :first_preview, :test_run,
|
|
39
|
+
:pending_question, :used_memory_names
|
|
40
40
|
# Memories the session was started with (--memory) and memories hidden
|
|
41
41
|
# from it (--mute): the worker rebuilds the same prompt on a respawn.
|
|
42
42
|
# Names as given; the engine normalizes them.
|
|
@@ -47,7 +47,11 @@ module Samagotchi
|
|
|
47
47
|
bad = @argv.find { |arg| arg.start_with?("-") }
|
|
48
48
|
return usage_error(bad ? "unknown option #{bad}" : "give session ids") if bad || @argv.empty?
|
|
49
49
|
|
|
50
|
-
@argv.uniq.map { |given| @action == "archive" ? archive(given) : unarchive(given) }.all?
|
|
50
|
+
if @argv.uniq.map { |given| @action == "archive" ? archive(given) : unarchive(given) }.all?
|
|
51
|
+
0
|
|
52
|
+
else
|
|
53
|
+
1
|
|
54
|
+
end
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
private
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "set"
|
|
4
|
-
|
|
5
3
|
require_relative "config"
|
|
6
4
|
require_relative "model_profile"
|
|
7
5
|
require_relative "served_model"
|
|
@@ -65,7 +63,7 @@ module Samagotchi
|
|
|
65
63
|
guardrails(text.delete_prefix(GUARDRAILS_COMMAND).strip)
|
|
66
64
|
end
|
|
67
65
|
registry.register(MODEL_COMMAND, "show or switch the model",
|
|
68
|
-
match: ->(text) { text.match?(
|
|
66
|
+
match: ->(text) { text.match?(%r{\A/model(?:\s+.*)?\z}) }) { |text| model(text) }
|
|
69
67
|
registry.register(HELP_COMMAND, "list the commands, the bundles' too", anytime: true) { |_text| reply(help_listing) }
|
|
70
68
|
registry.register("/stats", "show the session's stats", local: true)
|
|
71
69
|
registry.register("/recap", "show the session's recap", local: true)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require "fileutils"
|
|
@@ -272,7 +271,7 @@ module Samagotchi
|
|
|
272
271
|
root = cwd && folder_path(cwd)
|
|
273
272
|
roots = {}
|
|
274
273
|
summaries = Session.list(state_dir: sd, sort: sort || "updated_at", order: order || "desc", project_root: project_root,
|
|
275
|
-
|
|
274
|
+
include_archived: include_archived).lazy
|
|
276
275
|
.reject { |s| (!include_tests && s.test_run) || s.id == exclude }
|
|
277
276
|
.select { |s| root.nil? || in_folder?(s.working_directory, root) }
|
|
278
277
|
.filter_map do |s|
|
|
@@ -703,7 +702,7 @@ module Samagotchi
|
|
|
703
702
|
Dir.children(dir).all? do |name|
|
|
704
703
|
path = File.join(dir, name)
|
|
705
704
|
if EMPTY_SKELETON_DIRS.include?(name)
|
|
706
|
-
File.directory?(path) && (!EMPTY_DIRS.include?(name) || Dir.
|
|
705
|
+
File.directory?(path) && (!EMPTY_DIRS.include?(name) || Dir.empty?(path))
|
|
707
706
|
elsif name == "analytics.json"
|
|
708
707
|
Array(JSON.parse(File.read(path))["turn_records"]).empty?
|
|
709
708
|
# Cards and notices from before any turn (Bridge::CardStore).
|
|
@@ -1054,4 +1053,3 @@ module Samagotchi
|
|
|
1054
1053
|
end
|
|
1055
1054
|
end
|
|
1056
1055
|
end
|
|
1057
|
-
|
|
@@ -137,9 +137,9 @@ module Samagotchi
|
|
|
137
137
|
|
|
138
138
|
tokens = data["tokens"].is_a?(Hash) ? data["tokens"] : {}
|
|
139
139
|
count = ->(key) { tokens[key].is_a?(Numeric) ? tokens[key] : 0 }
|
|
140
|
-
SavedSummary.new(ctx_pct: saved_pct(data["context"]), prompt_sum: count.("prompt_sum"),
|
|
141
|
-
completion_sum: count.("completion_sum"), cached_sum: count.("cached_sum"),
|
|
142
|
-
reasoning_sum: count.("reasoning_sum"), cost_sum: count.("cost_sum"))
|
|
140
|
+
SavedSummary.new(ctx_pct: saved_pct(data["context"]), prompt_sum: count.call("prompt_sum"),
|
|
141
|
+
completion_sum: count.call("completion_sum"), cached_sum: count.call("cached_sum"),
|
|
142
|
+
reasoning_sum: count.call("reasoning_sum"), cost_sum: count.call("cost_sum"))
|
|
143
143
|
rescue JSON::ParserError, SystemCallError, TypeError
|
|
144
144
|
nil
|
|
145
145
|
end
|
|
@@ -379,15 +379,15 @@ module Samagotchi
|
|
|
379
379
|
# Caller holds the mutex.
|
|
380
380
|
def tokens_block(totals, turn)
|
|
381
381
|
add = ->(key) { totals[key] + (turn&.public_send(key) || 0) }
|
|
382
|
-
decode_ms = add.(:decode_ms_sum)
|
|
383
|
-
decode_tokens = add.(:decode_tokens_sum)
|
|
382
|
+
decode_ms = add.call(:decode_ms_sum)
|
|
383
|
+
decode_tokens = add.call(:decode_tokens_sum)
|
|
384
384
|
speed_turn = turn&.last_decode_tps ? turn : nil
|
|
385
385
|
{
|
|
386
|
-
prompt_sum: add.(:prompt_sum),
|
|
387
|
-
completion_sum: add.(:completion_sum),
|
|
388
|
-
cached_sum: add.(:cached_sum),
|
|
389
|
-
reasoning_sum: add.(:reasoning_sum),
|
|
390
|
-
cost_sum: add.(:cost_sum),
|
|
386
|
+
prompt_sum: add.call(:prompt_sum),
|
|
387
|
+
completion_sum: add.call(:completion_sum),
|
|
388
|
+
cached_sum: add.call(:cached_sum),
|
|
389
|
+
reasoning_sum: add.call(:reasoning_sum),
|
|
390
|
+
cost_sum: add.call(:cost_sum),
|
|
391
391
|
decode_ms_sum: decode_ms.round,
|
|
392
392
|
decode_tokens_sum: decode_tokens,
|
|
393
393
|
avg_decode_tps: decode_ms.positive? ? (decode_tokens * 1000.0 / decode_ms).round(1) : nil,
|
|
@@ -70,6 +70,7 @@ module Samagotchi
|
|
|
70
70
|
# already-unsubscribed never raises)
|
|
71
71
|
def unsubscribe(handle:)
|
|
72
72
|
return false unless handle.is_a?(SubscribedObserver)
|
|
73
|
+
|
|
73
74
|
@mutex.synchronize { !!@observers.delete(handle) }
|
|
74
75
|
end
|
|
75
76
|
|
|
@@ -110,8 +111,8 @@ module Samagotchi
|
|
|
110
111
|
# Run the block while no event is being numbered or delivered, so it sees
|
|
111
112
|
# (and can change) state consistently with the event log. Reentrant: the
|
|
112
113
|
# block may notify. Keep it short; every emitter waits for it.
|
|
113
|
-
def synchronize(&
|
|
114
|
-
@mutex.synchronize(&
|
|
114
|
+
def synchronize(&)
|
|
115
|
+
@mutex.synchronize(&)
|
|
115
116
|
end
|
|
116
117
|
end
|
|
117
118
|
end
|
|
@@ -171,7 +171,7 @@ module Samagotchi
|
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
now = Time.now
|
|
174
|
-
cutoff = days.to_i.positive? ? now - days.to_i * 86_400 : nil
|
|
174
|
+
cutoff = days.to_i.positive? ? now - (days.to_i * 86_400) : nil
|
|
175
175
|
max = max_count.to_i
|
|
176
176
|
|
|
177
177
|
deleted = []
|
|
@@ -249,7 +249,7 @@ module Samagotchi
|
|
|
249
249
|
begin
|
|
250
250
|
FileUtils.rm_f(path)
|
|
251
251
|
sidecar = File.join(state_dir, session.id)
|
|
252
|
-
FileUtils.rm_rf(sidecar)
|
|
252
|
+
FileUtils.rm_rf(sidecar)
|
|
253
253
|
PluginSessionState.remove(session.id)
|
|
254
254
|
deleted << session.id
|
|
255
255
|
rescue StandardError
|
|
@@ -118,6 +118,7 @@ module Samagotchi
|
|
|
118
118
|
return usage_error("unknown format #{format.inspect}: use --format text|json|tsv")
|
|
119
119
|
end
|
|
120
120
|
return usage_error("unknown scope #{scope.inspect}: use --scope=project|all") unless scope.nil? || %w[project all].include?(scope)
|
|
121
|
+
|
|
121
122
|
# The current git project's sessions (Samagotchi::ProjectScope), like
|
|
122
123
|
# chi web's; --scope=all, a folder in no repo or an explicit --cwd: every
|
|
123
124
|
# project's.
|
|
@@ -175,13 +176,17 @@ module Samagotchi
|
|
|
175
176
|
end
|
|
176
177
|
# A delegated session points at its parent.
|
|
177
178
|
child = summary[:parent_short_id] ? " ↳ #{summary[:parent_short_id]}" : ""
|
|
178
|
-
flag = summary[:scratch]
|
|
179
|
+
flag = if summary[:scratch]
|
|
180
|
+
" [scratch]"
|
|
181
|
+
else
|
|
182
|
+
(summary[:test_run] ? " [test]" : "")
|
|
183
|
+
end
|
|
179
184
|
flag += " [archived]" if summary[:archived]
|
|
180
185
|
ctx = Samagotchi::SessionMetrics.context_label(summary[:ctx_pct])
|
|
181
186
|
@stdout.puts "#{summary[:id]} #{state.ljust(8)} #{ctx.ljust(8)} #{summary[:updated_at]} #{desc}#{flag}#{child}"
|
|
182
187
|
end
|
|
183
188
|
if project
|
|
184
|
-
@stdout.puts "#{summaries.empty?
|
|
189
|
+
@stdout.puts "#{"\n" unless summaries.empty?}#{scope_note.call(summaries.size)}"
|
|
185
190
|
else
|
|
186
191
|
@stdout.puts summaries.empty? ? "No sessions." : "\n#{summaries.size} session(s)"
|
|
187
192
|
end
|
|
@@ -204,7 +209,11 @@ module Samagotchi
|
|
|
204
209
|
end
|
|
205
210
|
# A delegated session points at its parent: ↳ <parent's short id>.
|
|
206
211
|
row = lambda do |s|
|
|
207
|
-
flag = s.scratch
|
|
212
|
+
flag = if s.scratch
|
|
213
|
+
" [scratch]"
|
|
214
|
+
else
|
|
215
|
+
(s.test_run ? " [test]" : "")
|
|
216
|
+
end
|
|
208
217
|
flag += " [archived]" if s.archived
|
|
209
218
|
child = s.parent_id ? " ↳ #{s.parent_id[0, 8]}" : ""
|
|
210
219
|
# How full the context was after the last turn: "ctx 12%", blank when unknown.
|
|
@@ -222,7 +231,7 @@ module Samagotchi
|
|
|
222
231
|
end
|
|
223
232
|
if project
|
|
224
233
|
sessions.each { |s| @stdout.puts row.call(s) }
|
|
225
|
-
@stdout.puts "#{sessions.empty?
|
|
234
|
+
@stdout.puts "#{"\n" unless sessions.empty?}#{scope_note.call(sessions.size)}"
|
|
226
235
|
elsif sessions.empty?
|
|
227
236
|
@stdout.puts "No sessions."
|
|
228
237
|
else
|
|
@@ -276,7 +285,7 @@ module Samagotchi
|
|
|
276
285
|
next false
|
|
277
286
|
rescue Samagotchi::SessionManager::RestartRefused => e
|
|
278
287
|
@stdout.flush
|
|
279
|
-
@stderr.puts
|
|
288
|
+
@stderr.puts %i[not_running unsupported].include?(e.reason) ? e.message : "session #{id}: #{e.message}"
|
|
280
289
|
next false
|
|
281
290
|
rescue ArgumentError => e
|
|
282
291
|
@stdout.flush
|
|
@@ -79,9 +79,10 @@ module Samagotchi
|
|
|
79
79
|
@built = nil
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
# The base prompt (specs, plugins' declarations)
|
|
82
|
+
# The base prompt (specs, plugins' declarations), with Gemma's tool
|
|
83
|
+
# declarations at its end as #build has them.
|
|
83
84
|
def base(chat: false, thinking: nil)
|
|
84
|
-
assist_system_prompt(chat: chat, thinking: thinking)
|
|
85
|
+
with_tail_declarations(assist_system_prompt(chat: chat, thinking: thinking), chat: chat)
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
# Names activated via preloaded --memory entries during prompt
|
|
@@ -141,15 +142,18 @@ module Samagotchi
|
|
|
141
142
|
def assist_system_prompt(chat: false, thinking: nil)
|
|
142
143
|
return chat_system_prompt if chat
|
|
143
144
|
|
|
144
|
-
declarations = tool_declarations
|
|
145
145
|
hint = tool_call_hint
|
|
146
146
|
turn_preamble = turn_preamble_instruction(thinking)
|
|
147
|
+
# Gemma 4's template declares the tools at the end of the system turn
|
|
148
|
+
# (#system_prompt_with_index puts them there).
|
|
149
|
+
tools = if gemma_tail_declarations?
|
|
150
|
+
"Your tools are declared at the end of this system prompt."
|
|
151
|
+
else
|
|
152
|
+
"You have access to the following tools:\n\n#{tool_declarations}\n"
|
|
153
|
+
end
|
|
147
154
|
|
|
148
155
|
<<~SYS
|
|
149
|
-
You are Chi (pronounced "chee"), the friendly name for the Samagotchi assistant harness.
|
|
150
|
-
|
|
151
|
-
#{declarations}
|
|
152
|
-
|
|
156
|
+
You are Chi (pronounced "chee"), the friendly name for the Samagotchi assistant harness. #{tools}
|
|
153
157
|
#{hint}
|
|
154
158
|
You may make multiple tool calls. After seeing tool results, continue reasoning or answer the user.
|
|
155
159
|
#{turn_preamble}
|
|
@@ -234,9 +238,16 @@ module Samagotchi
|
|
|
234
238
|
"Project memories:\n#{project_index}",
|
|
235
239
|
"System memories:\n#{system_index}"
|
|
236
240
|
].join("\n\n")
|
|
237
|
-
[thinking_token + base, rg_guidance, project_description, project_location, current_model, current_session, memory_sections, system_identity_section, explicit_memory_section].compact.join("\n")
|
|
241
|
+
prompt = [thinking_token + base, rg_guidance, project_description, project_location, current_model, current_session, memory_sections, system_identity_section, explicit_memory_section].compact.join("\n")
|
|
242
|
+
with_tail_declarations(prompt, chat: chat)
|
|
238
243
|
end
|
|
239
244
|
|
|
245
|
+
def with_tail_declarations(prompt, chat:)
|
|
246
|
+
chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def gemma_tail_declarations? = profile.name == "gemma4"
|
|
250
|
+
|
|
240
251
|
# B-light: auto-preload the built-in identity memory.
|
|
241
252
|
# The file is installed by SystemBundle.ensure! as a normal system memory,
|
|
242
253
|
# but its body is injected here so the agent has it without an extra tool call.
|
|
@@ -46,10 +46,10 @@ module Samagotchi
|
|
|
46
46
|
first_command = model && (attach || resume) ? "/model #{model}" : nil
|
|
47
47
|
surface = open_surface
|
|
48
48
|
attached = AttachedLoop.new(client: client, screen: surface, client_id: "tui:#{Process.pid}", first_prompt: prompt,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
first_command: first_command, no_interrupt: no_interrupt,
|
|
50
|
+
default_input: default_input && !prompt && !attach && !resume,
|
|
51
|
+
wait_at_eof: !$stdin.tty?,
|
|
52
|
+
parent_answers: Guardrails::ParentApprovals.parent_process?)
|
|
53
53
|
ended = begin
|
|
54
54
|
attached.run
|
|
55
55
|
ensure
|
|
@@ -144,7 +144,7 @@ module Samagotchi
|
|
|
144
144
|
return if flags.empty?
|
|
145
145
|
|
|
146
146
|
verb = flags.size == 1 ? "applies" : "apply"
|
|
147
|
-
err.puts "(#{flags.join(
|
|
147
|
+
err.puts "(#{flags.join(" and ")} #{verb} to a new session; #{session_id}'s prompt is already built)"
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
# @return [BridgeClient, nil] the running worker's, or nil when none runs
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "set"
|
|
4
3
|
require_relative "event_renderer"
|
|
5
4
|
require_relative "formatting"
|
|
6
5
|
require_relative "../prompt_history"
|
|
@@ -1174,7 +1173,8 @@ module Samagotchi
|
|
|
1174
1173
|
Bridge::TurnAccumulator.replay_events(turn).each do |event|
|
|
1175
1174
|
case event[:type]
|
|
1176
1175
|
# A new step starts its line over, as live (a join during a hold).
|
|
1177
|
-
when :generation_started then tail
|
|
1176
|
+
when :generation_started then tail = nil
|
|
1177
|
+
lane = :writing
|
|
1178
1178
|
when :generation_chunk
|
|
1179
1179
|
tail, lane = event[:thinking].to_s.empty? ? [event[:text], :writing] : [event[:thinking], :thinking]
|
|
1180
1180
|
when :tool_call_started
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "set"
|
|
4
3
|
require_relative "formatting"
|
|
5
4
|
|
|
6
5
|
module Samagotchi
|
|
@@ -188,7 +187,7 @@ module Samagotchi
|
|
|
188
187
|
Array(event[:steers]).each { |steer| @view.print_line(@view.format_steer_line(source: steer[:source], text: steer[:text])) }
|
|
189
188
|
end
|
|
190
189
|
|
|
191
|
-
private
|
|
190
|
+
private
|
|
192
191
|
|
|
193
192
|
def render_streamed_tool_activity(activity, duration_ms:, images: nil, diff: nil)
|
|
194
193
|
return if activity.nil?
|