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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# What the running generation is doing, as the Engine's stream handler
|
|
5
|
+
# sees it: whether a steer may cut it (Engine#cut_for_steer). A
|
|
6
|
+
# generation is cuttable while it has only been thinking: it streamed
|
|
7
|
+
# thinking, still streams it (a thinking delta in the last FRESH_SECONDS),
|
|
8
|
+
# and has streamed no visible text and no tool call. Its age counts from
|
|
9
|
+
# its first thinking delta, so the wait for a first token (a busy or
|
|
10
|
+
# remote server) never counts as thinking. Fed from the stream
|
|
11
|
+
# thread, read from a steer's thread: its own Mutex, never held while
|
|
12
|
+
# calling out.
|
|
13
|
+
class GenerationPhase
|
|
14
|
+
# A thinking delta this recent means the model is still thinking.
|
|
15
|
+
FRESH_SECONDS = 2.0
|
|
16
|
+
|
|
17
|
+
# @param clock [#call] monotonic seconds
|
|
18
|
+
def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
|
|
19
|
+
@clock = clock
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
@running = false
|
|
22
|
+
clear_seen
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A generation began.
|
|
26
|
+
def started!
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
@running = true
|
|
29
|
+
clear_seen
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# One streamed chunk's lanes. Blank text (a stray newline before Gemma's
|
|
34
|
+
# thought channel) is no text.
|
|
35
|
+
def chunk!(thinking:, text:, tool_call: false)
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
unless thinking.to_s.empty?
|
|
38
|
+
@thinking_at = @clock.call
|
|
39
|
+
@thinking_since ||= @thinking_at
|
|
40
|
+
end
|
|
41
|
+
@text = true unless text.to_s.strip.empty?
|
|
42
|
+
@tool_call = true if tool_call
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The request is sent again (a network retry): it streams from the
|
|
47
|
+
# start, so what it had seen (its thinking clock too) is forgotten.
|
|
48
|
+
def retrying!
|
|
49
|
+
@mutex.synchronize { clear_seen }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The generation ended (completed, cut, cancelled, or the turn ended).
|
|
53
|
+
def finished!
|
|
54
|
+
@mutex.synchronize { @running = false }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether a steer may cut the running generation: thinking only, still
|
|
58
|
+
# thinking, and thinking for at least +min_age+ seconds.
|
|
59
|
+
def cuttable?(min_age)
|
|
60
|
+
@mutex.synchronize do
|
|
61
|
+
next false unless @running && @thinking_since && !@text && !@tool_call
|
|
62
|
+
|
|
63
|
+
now = @clock.call
|
|
64
|
+
now - @thinking_at <= FRESH_SECONDS && now - @thinking_since >= min_age
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Seconds since the running generation's first thinking delta; nil
|
|
69
|
+
# with none.
|
|
70
|
+
def age
|
|
71
|
+
@mutex.synchronize { @running && @thinking_since ? @clock.call - @thinking_since : nil }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def clear_seen
|
|
77
|
+
@thinking_since = nil
|
|
78
|
+
@thinking_at = nil
|
|
79
|
+
@text = false
|
|
80
|
+
@tool_call = false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -159,7 +159,7 @@ module Samagotchi
|
|
|
159
159
|
raise Guardrails::Rules::ParseError, "its sha256 differs from the installed one (edited after install? reinstall the bundle)"
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
doc = YAML.
|
|
162
|
+
doc = YAML.safe_load_file(path)
|
|
163
163
|
raise Guardrails::Rules::ParseError, "expected a mapping with rules:" unless doc.is_a?(Hash)
|
|
164
164
|
|
|
165
165
|
loaded.concat(Guardrails::Rules.parse(doc["rules"], source: "bundle #{bundle_name}"))
|
|
@@ -150,13 +150,13 @@ module Samagotchi
|
|
|
150
150
|
return nil unless preview.is_a?(Hash)
|
|
151
151
|
|
|
152
152
|
get = ->(key) { preview[key] || preview[key.to_s] }
|
|
153
|
-
return "would fail: #{get.(:error)}" if get.(:error)
|
|
154
|
-
return "not shown (#{get.(:skipped)})" if get.(:skipped)
|
|
153
|
+
return "would fail: #{get.call(:error)}" if get.call(:error)
|
|
154
|
+
return "not shown (#{get.call(:skipped)})" if get.call(:skipped)
|
|
155
155
|
|
|
156
|
-
added = get.(:added).to_i
|
|
157
|
-
return "new file, #{added} #{added == 1 ? "line" : "lines"}" if get.(:new_file)
|
|
156
|
+
added = get.call(:added).to_i
|
|
157
|
+
return "new file, #{added} #{added == 1 ? "line" : "lines"}" if get.call(:new_file)
|
|
158
158
|
|
|
159
|
-
"+#{added} −#{get.(:removed).to_i}"
|
|
159
|
+
"+#{added} −#{get.call(:removed).to_i}"
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
162
|
end
|
|
@@ -72,8 +72,8 @@ module Samagotchi
|
|
|
72
72
|
list.find do |e|
|
|
73
73
|
case e["scope"]
|
|
74
74
|
when "session" then session_id && e["session_id"] == session_id && e["key"] == key
|
|
75
|
-
when "repo" then here.(e) && e["key"] == key
|
|
76
|
-
when "rule" then verdict.rule && e["rule"] == verdict.rule && e["source"] == verdict.source && here.(e)
|
|
75
|
+
when "repo" then here.call(e) && e["key"] == key
|
|
76
|
+
when "rule" then verdict.rule && e["rule"] == verdict.rule && e["source"] == verdict.source && here.call(e)
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
end
|
|
@@ -120,8 +120,8 @@ module Samagotchi
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# Registry#fire_each rescues a raising hook itself.
|
|
123
|
-
def fire_each(name, event, &
|
|
124
|
-
@hooks_lookup.call&.fire_each(name, event, &
|
|
123
|
+
def fire_each(name, event, &)
|
|
124
|
+
@hooks_lookup.call&.fire_each(name, event, &)
|
|
125
125
|
end
|
|
126
126
|
end
|
|
127
127
|
end
|
|
@@ -167,7 +167,7 @@ module Samagotchi
|
|
|
167
167
|
program = nil
|
|
168
168
|
until rest.empty?
|
|
169
169
|
arg = rest.shift
|
|
170
|
-
if
|
|
170
|
+
if ["-F", "-v"].include?(arg) then rest.shift
|
|
171
171
|
elsif arg.match?(/\A-[Fv]./m) then next
|
|
172
172
|
elsif arg == "--" then program = rest.shift
|
|
173
173
|
elsif arg.start_with?("-") then return false
|
|
@@ -296,14 +296,16 @@ module Samagotchi
|
|
|
296
296
|
delim = @s.getch
|
|
297
297
|
return false if delim.nil? || delim.match?(/[\s\\]/)
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
# Two calls, two fields: each one advances the scanner (pattern, then replacement).
|
|
300
|
+
delimited(delim) && delimited(delim) && @s.skip(/[gpiImM\d]*/) && true # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
|
|
300
301
|
end
|
|
301
302
|
|
|
302
303
|
def transliterate
|
|
303
304
|
delim = @s.getch
|
|
304
305
|
return false if delim.nil? || delim.match?(/[\s\\]/)
|
|
305
306
|
|
|
306
|
-
|
|
307
|
+
# Source set, then target set: each call advances the scanner.
|
|
308
|
+
delimited(delim) && delimited(delim) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
|
|
307
309
|
end
|
|
308
310
|
|
|
309
311
|
# Up to the next unescaped +delim+ (consumed); false at the end.
|
|
@@ -123,7 +123,7 @@ module Samagotchi
|
|
|
123
123
|
def expand(arg, dir)
|
|
124
124
|
return nil if arg.include?(ShellLex::SUBST)
|
|
125
125
|
|
|
126
|
-
arg = arg.sub(
|
|
126
|
+
arg = arg.sub(%r{\A(~|\$HOME|\$\{HOME\})(?=/|\z)}) { @home }
|
|
127
127
|
return nil if arg.include?("$") || arg.start_with?("~")
|
|
128
128
|
return File.expand_path(arg) if arg.start_with?("/")
|
|
129
129
|
|
|
@@ -5,13 +5,23 @@ module Samagotchi
|
|
|
5
5
|
# A small shell lexer for the guardrails' text checks (ShellGitDirs
|
|
6
6
|
# and the rules' shell matchers): words with quotes and
|
|
7
7
|
# backslashes undone, operators apart, comments dropped, and $(…) or
|
|
8
|
-
# backticks (quoted or not) as one opaque SUBST marker inside a word.
|
|
9
|
-
#
|
|
8
|
+
# backticks (quoted or not) as one opaque SUBST marker inside a word. A
|
|
9
|
+
# heredoc's body (<<EOF, <<'EOF', <<-EOF) is one word after its <<TAG
|
|
10
|
+
# word and lexing goes on after the terminator line: HEREDOC when cat or
|
|
11
|
+
# tee read it as data, SUBST when an unquoted tag's body holds $(…) or
|
|
12
|
+
# backticks, else the body text itself (a script, as sh -c '…' gives).
|
|
13
|
+
# An unterminated heredoc is no heredoc: the rest is lexed as commands.
|
|
14
|
+
# It never runs anything, and it does its best on odd input (an
|
|
15
|
+
# unbalanced quote).
|
|
10
16
|
module ShellLex
|
|
11
17
|
# Longest first: "&&" before "&", ";;" before ";".
|
|
12
18
|
OPERATORS = ["&&", "||", ";;", "|&", ";", "|", "&", "\n", "(", ")"].freeze
|
|
13
19
|
# What a $(…) or `…` becomes inside a word.
|
|
14
20
|
SUBST = "\0SUBST"
|
|
21
|
+
# What a heredoc body that cat or tee read as data becomes.
|
|
22
|
+
HEREDOC = "\0HEREDOC"
|
|
23
|
+
# Commands that read a heredoc as data (when not piped on).
|
|
24
|
+
DATA_SINKS = %w[cat tee].freeze
|
|
15
25
|
# A word that is a redirection (>, >>, 2>&1, <).
|
|
16
26
|
REDIRECT = /\A\d*[<>]/
|
|
17
27
|
|
|
@@ -50,6 +60,7 @@ module Samagotchi
|
|
|
50
60
|
@word = +""
|
|
51
61
|
@specials = +""
|
|
52
62
|
@in_word = false
|
|
63
|
+
@heredocs = []
|
|
53
64
|
end
|
|
54
65
|
|
|
55
66
|
def tokens
|
|
@@ -139,15 +150,19 @@ module Samagotchi
|
|
|
139
150
|
@i = @s.index("\n", @i) || @s.size
|
|
140
151
|
end
|
|
141
152
|
|
|
142
|
-
# >, >>, 2>&1, &>, <,
|
|
153
|
+
# >, >>, 2>&1, &>, <, <<<: kept in the word (the walk drops them).
|
|
154
|
+
# << starts a heredoc.
|
|
143
155
|
def redirection
|
|
156
|
+
return add("<<<", 3) if @s[@i, 3] == "<<<"
|
|
157
|
+
return if @s[@i, 2] == "<<" && heredoc
|
|
158
|
+
|
|
144
159
|
special(@s[@i])
|
|
145
|
-
add(@s[@i], 1) while @i < @s.size && @s[@i].match?(/[&\d>-]/) && @word.
|
|
160
|
+
add(@s[@i], 1) while @i < @s.size && @s[@i].match?(/[&\d>-]/) && @word.end_with?(">")
|
|
146
161
|
end
|
|
147
162
|
|
|
148
163
|
def operator_or_char(char)
|
|
149
164
|
op = OPERATORS.find { |o| @s[@i, o.size] == o }
|
|
150
|
-
return special(char) if
|
|
165
|
+
return special(char) if ["{", "}"].include?(char)
|
|
151
166
|
return add(char, 1) unless op
|
|
152
167
|
|
|
153
168
|
if op == "&" && @word.match?(/\d*>\z/)
|
|
@@ -156,8 +171,101 @@ module Samagotchi
|
|
|
156
171
|
flush
|
|
157
172
|
@tokens << [:op, op]
|
|
158
173
|
@i += op.size
|
|
174
|
+
bodies if op == "\n" && !@heredocs.empty?
|
|
159
175
|
end
|
|
160
176
|
end
|
|
177
|
+
|
|
178
|
+
# <<TAG, <<-TAG, <<'TAG', << "TAG": its own word, the body noted to
|
|
179
|
+
# be read after the line ends. False (nothing read) without a tag.
|
|
180
|
+
def heredoc
|
|
181
|
+
dash = @s[@i + 2] == "-"
|
|
182
|
+
j = @i + (dash ? 3 : 2)
|
|
183
|
+
j += 1 while [" ", "\t"].include?(@s[j])
|
|
184
|
+
tag, quoted, stop = heredoc_tag(j)
|
|
185
|
+
return false if tag.empty?
|
|
186
|
+
|
|
187
|
+
flush unless @word.match?(/\A\d*\z/)
|
|
188
|
+
special("<")
|
|
189
|
+
@word << "<" << (dash ? "-" : "") << tag
|
|
190
|
+
@i = stop
|
|
191
|
+
flush
|
|
192
|
+
@heredocs << { tag: tag, dash: dash, quoted: quoted, at: @tokens.size }
|
|
193
|
+
true
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The tag starting at +j+ with quotes and backslashes undone, whether
|
|
197
|
+
# any were there, and where it ends.
|
|
198
|
+
def heredoc_tag(j)
|
|
199
|
+
tag = +""
|
|
200
|
+
quoted = false
|
|
201
|
+
while j < @s.size && !@s[j].match?(/[\s;&|<>()]/)
|
|
202
|
+
if ["'", '"'].include?(@s[j])
|
|
203
|
+
close = @s.index(@s[j], j + 1) or return ["", false, j]
|
|
204
|
+
tag << @s[(j + 1)...close]
|
|
205
|
+
j = close + 1
|
|
206
|
+
quoted = true
|
|
207
|
+
elsif @s[j] == "\\"
|
|
208
|
+
tag << @s[j + 1].to_s
|
|
209
|
+
j += 2
|
|
210
|
+
quoted = true
|
|
211
|
+
else
|
|
212
|
+
tag << @s[j]
|
|
213
|
+
j += 1
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
[tag, quoted, j]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# After a line with heredocs: each body in order, up to its
|
|
220
|
+
# terminator line, as one word in its command. Unterminated: none
|
|
221
|
+
# are, and the rest is lexed as commands.
|
|
222
|
+
def bodies
|
|
223
|
+
heredocs = @heredocs
|
|
224
|
+
@heredocs = []
|
|
225
|
+
pos = @i
|
|
226
|
+
read = []
|
|
227
|
+
heredocs.each do |doc|
|
|
228
|
+
body, pos = body(doc, pos)
|
|
229
|
+
break unless body
|
|
230
|
+
|
|
231
|
+
read << [doc, body]
|
|
232
|
+
end
|
|
233
|
+
return if read.size < heredocs.size
|
|
234
|
+
|
|
235
|
+
read.reverse_each { |doc, body| @tokens.insert(doc[:at], [:word, body_word(doc, body), ""]) }
|
|
236
|
+
@i = pos
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# The body from +pos+ and where lexing goes on, or nil.
|
|
240
|
+
def body(doc, pos)
|
|
241
|
+
start = pos
|
|
242
|
+
while pos < @s.size
|
|
243
|
+
eol = @s.index("\n", pos) || @s.size
|
|
244
|
+
line = @s[pos...eol]
|
|
245
|
+
line = line.sub(/\A\t+/, "") if doc[:dash]
|
|
246
|
+
return [@s[start...pos], [eol + 1, @s.size].min] if line == doc[:tag]
|
|
247
|
+
|
|
248
|
+
pos = eol + 1
|
|
249
|
+
end
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def body_word(doc, body)
|
|
254
|
+
return SUBST if !doc[:quoted] && body.match?(/\$\(|`/)
|
|
255
|
+
return HEREDOC if data_sink?(doc[:at])
|
|
256
|
+
|
|
257
|
+
body
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Whether the command the heredoc at token +at+ is in is cat or tee
|
|
261
|
+
# and not piped into another command.
|
|
262
|
+
def data_sink?(at)
|
|
263
|
+
start = at
|
|
264
|
+
start -= 1 while start.positive? && @tokens[start - 1][0] == :word
|
|
265
|
+
verb = @tokens[start...at].map { |t| t[1] }.find { |w| !w.match?(/\A[A-Za-z_]\w*=/) && !w.match?(REDIRECT) }
|
|
266
|
+
next_op = @tokens[at..].find { |t| t[0] == :op }
|
|
267
|
+
DATA_SINKS.include?(File.basename(verb.to_s)) && !["|", "|&"].include?(next_op&.dig(1))
|
|
268
|
+
end
|
|
161
269
|
end
|
|
162
270
|
end
|
|
163
271
|
end
|
|
@@ -107,14 +107,14 @@ module Samagotchi
|
|
|
107
107
|
def candidate(word)
|
|
108
108
|
word = word.sub(REDIRECT_PREFIX, "")
|
|
109
109
|
word = word.split("=", 2).last if word.match?(/\A(--?[\w-]+|[A-Za-z_]\w*)=/)
|
|
110
|
-
return nil if word.empty? || word == "-" || word.match?(
|
|
110
|
+
return nil if word.empty? || word == "-" || word.match?(%r{\A-[^/]})
|
|
111
111
|
|
|
112
112
|
word
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
# An absolute path for +word+ against +dir+, or nil.
|
|
116
116
|
def resolve(word, dir)
|
|
117
|
-
return nil if word.include?(ShellLex::SUBST) || word.match?(SCRIPT)
|
|
117
|
+
return nil if word.include?(ShellLex::SUBST) || word == ShellLex::HEREDOC || word.match?(SCRIPT)
|
|
118
118
|
|
|
119
119
|
word = expand(word) or return nil
|
|
120
120
|
return nil if word.match?(GLOB)
|
|
@@ -124,7 +124,7 @@ module Samagotchi
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
def expand(word)
|
|
127
|
-
word = word.sub(
|
|
127
|
+
word = word.sub(%r{\A~(?=/|\z)}) { @home }
|
|
128
128
|
word = word.sub(EXPANSION) { value(Regexp.last_match(1) || Regexp.last_match(2)) || "$" }
|
|
129
129
|
return nil if word.include?("$") || word.start_with?("~")
|
|
130
130
|
|
|
@@ -65,13 +65,13 @@ module Samagotchi
|
|
|
65
65
|
next if basename.empty?
|
|
66
66
|
|
|
67
67
|
meta = ->(key) { raw_meta && (raw_meta[key] || raw_meta[key.to_s]) }
|
|
68
|
-
event = meta.(:event).to_s
|
|
68
|
+
event = meta.call(:event).to_s
|
|
69
69
|
# A hook with no declared event cannot be auto-registered.
|
|
70
70
|
next if event.empty?
|
|
71
71
|
|
|
72
72
|
event_sym = event.to_sym
|
|
73
|
-
on_error = (meta.(:on_error) || "skip").to_s
|
|
74
|
-
priority = (meta.(:priority) || 100).to_i
|
|
73
|
+
on_error = (meta.call(:on_error) || "skip").to_s
|
|
74
|
+
priority = (meta.call(:priority) || 100).to_i
|
|
75
75
|
fail_closed = on_error == "fail_closed" && event_sym == :before_tool_call
|
|
76
76
|
what = "hook #{basename} (bundle #{bundle_name})"
|
|
77
77
|
file = hooks_dir && File.join(hooks_dir, basename)
|
|
@@ -79,7 +79,7 @@ module Samagotchi
|
|
|
79
79
|
failures&.add(what, "the file is missing", required: true) if fail_closed
|
|
80
80
|
next
|
|
81
81
|
end
|
|
82
|
-
if (mismatch = too_old || sha_mismatch(file, meta.(:sha256), required: fail_closed))
|
|
82
|
+
if (mismatch = too_old || sha_mismatch(file, meta.call(:sha256), required: fail_closed))
|
|
83
83
|
Log.warn(:hooks, "bundle_hook_mismatch", echo: "[samagotchi:hooks] bundle '#{bundle_name}' hook '#{basename}' not loaded: #{mismatch}", bundle: bundle_name, hook: basename.to_s)
|
|
84
84
|
failures&.add(what, mismatch, required: fail_closed)
|
|
85
85
|
next
|
|
@@ -91,12 +91,14 @@ module Samagotchi
|
|
|
91
91
|
elsif on_error == "log" then :log
|
|
92
92
|
else :skip
|
|
93
93
|
end
|
|
94
|
-
label = "#{
|
|
94
|
+
label = "#{"guardrail " if fail_closed}#{basename} (bundle #{bundle_name})"
|
|
95
95
|
handler = Hooks.wrap(label: label, event: event_sym, policy: policy, log: [:hooks, "bundle_hook_failed"],
|
|
96
96
|
fields: { bundle: bundle_name }) { |event| plugin.call(event) }
|
|
97
97
|
registry.register_bundle(bundle_name, event_sym, hook_name: basename, priority: priority, &handler)
|
|
98
98
|
loaded += 1
|
|
99
|
-
rescue Exception => e
|
|
99
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
|
|
100
|
+
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
101
|
+
|
|
100
102
|
Log.error(:hooks, "bundle_hook_load_failed", echo: "[samagotchi:hooks] bundle '#{bundle_name}' hook '#{basename}' failed to load: #{e.class}: #{e.message}", bundle: bundle_name, hook: basename.to_s, error: e.class.name)
|
|
101
103
|
failures&.add(what, "#{e.class}: #{e.message}", required: fail_closed)
|
|
102
104
|
end
|
|
@@ -127,6 +129,7 @@ module Samagotchi
|
|
|
127
129
|
klass = Hooks.class_for(basename, ns)
|
|
128
130
|
instance = Hooks.build_plugin(klass, settings)
|
|
129
131
|
raise ArgumentError, "plugin #{klass.name.split("::").last} does not respond to #call" unless instance.respond_to?(:call)
|
|
132
|
+
|
|
130
133
|
instance
|
|
131
134
|
end
|
|
132
135
|
|
|
@@ -67,17 +67,15 @@ module Samagotchi
|
|
|
67
67
|
definitions = parse_definitions(hooks_config)
|
|
68
68
|
|
|
69
69
|
definitions.each do |defn|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
failures&.add("hook #{defn[:path]} (config)", "#{e.class}: #{e.message}", required: defn[:required])
|
|
80
|
-
end
|
|
70
|
+
plugin = load_plugin(hooks_dir, defn[:path], settings: defn[:settings])
|
|
71
|
+
# Persistent: config hooks must fire on every turn, not be wiped
|
|
72
|
+
# by Engine#run_turn's per-turn clear_hooks after turn 1.
|
|
73
|
+
registry.register_persistent(defn[:event_type].to_sym, label: "#{defn[:path]} (config)",
|
|
74
|
+
&wrapped(plugin, defn))
|
|
75
|
+
rescue ScriptError, StandardError => e
|
|
76
|
+
# ScriptError: a SyntaxError (or LoadError) from `require`.
|
|
77
|
+
Log.error(:hooks, "hook_load_failed", echo: "[samagotchi:hooks] hook #{defn[:path]} failed to load: #{e.class}: #{e.message}", hook: defn[:path].to_s, error: e.class.name)
|
|
78
|
+
failures&.add("hook #{defn[:path]} (config)", "#{e.class}: #{e.message}", required: defn[:required])
|
|
81
79
|
end
|
|
82
80
|
|
|
83
81
|
registry
|
|
@@ -87,6 +85,7 @@ module Samagotchi
|
|
|
87
85
|
# Expand a path that may start with ~.
|
|
88
86
|
def self.expand_path(path, env = ENV)
|
|
89
87
|
return path unless path.start_with?("~")
|
|
88
|
+
|
|
90
89
|
home = env["HOME"] || Dir.home
|
|
91
90
|
File.join(home, path[1..])
|
|
92
91
|
end
|
|
@@ -97,8 +96,10 @@ module Samagotchi
|
|
|
97
96
|
definitions = []
|
|
98
97
|
hooks_config.each do |event_type, configs|
|
|
99
98
|
next unless event_type.to_s != "hooks_dir" && configs.is_a?(Array)
|
|
99
|
+
|
|
100
100
|
configs.each do |cfg|
|
|
101
101
|
next unless cfg.is_a?(Hash) && cfg["path"]
|
|
102
|
+
|
|
102
103
|
definitions << {
|
|
103
104
|
event_type: event_type.to_s,
|
|
104
105
|
path: cfg["path"],
|
|
@@ -123,9 +124,7 @@ module Samagotchi
|
|
|
123
124
|
|
|
124
125
|
# Check if already loaded (Ruby's require caching handles this)
|
|
125
126
|
# We cache the instance separately to avoid re-instantiating
|
|
126
|
-
|
|
127
|
-
@plugin_cache = {}
|
|
128
|
-
end
|
|
127
|
+
@plugin_cache ||= {}
|
|
129
128
|
|
|
130
129
|
@plugin_cache[[full_path, settings]] ||= begin
|
|
131
130
|
require full_path
|
|
@@ -133,6 +132,7 @@ module Samagotchi
|
|
|
133
132
|
instance = Hooks.build_plugin(klass, settings)
|
|
134
133
|
# Validate that the instance responds to #call
|
|
135
134
|
raise ArgumentError, "Plugin #{klass.name} does not respond to #call" unless instance.respond_to?(:call)
|
|
135
|
+
|
|
136
136
|
instance
|
|
137
137
|
end
|
|
138
138
|
end
|
|
@@ -142,9 +142,11 @@ module Samagotchi
|
|
|
142
142
|
# on_error: log warns and skip is silent.
|
|
143
143
|
def self.wrapped(plugin, defn)
|
|
144
144
|
if defn[:required] && defn[:event_type] == "before_tool_call"
|
|
145
|
-
policy
|
|
145
|
+
policy = :deny
|
|
146
|
+
label = "required hook #{defn[:path]}"
|
|
146
147
|
else
|
|
147
|
-
policy
|
|
148
|
+
policy = (defn[:on_error] == "log" ? :log : :skip)
|
|
149
|
+
label = "#{defn[:path]} (config)"
|
|
148
150
|
end
|
|
149
151
|
Hooks.wrap(label: label, event: defn[:event_type].to_sym, policy: policy) { |event| plugin.call(event) }
|
|
150
152
|
end
|
|
@@ -13,7 +13,11 @@ module Samagotchi
|
|
|
13
13
|
# @param settings [Hash]
|
|
14
14
|
def self.build_plugin(klass, settings)
|
|
15
15
|
takes_settings = klass.instance_method(:initialize).arity != 0
|
|
16
|
-
takes_settings
|
|
16
|
+
if takes_settings
|
|
17
|
+
klass.new(settings.is_a?(Hash) ? settings : {})
|
|
18
|
+
else
|
|
19
|
+
klass.new
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
# A stream hook (:generation_progress) fires ~once a second: a broken
|
|
@@ -62,7 +66,7 @@ module Samagotchi
|
|
|
62
66
|
def self.wrap(label:, event:, policy:, log: [:hooks, "hook_failed"], echo: true, fields: {}, &block)
|
|
63
67
|
log_failure = failure_log_gate(event)
|
|
64
68
|
lambda do |payload|
|
|
65
|
-
|
|
69
|
+
yield(payload)
|
|
66
70
|
rescue Exception => e # rubocop:disable Lint/RescueException -- :deny fails closed on anything
|
|
67
71
|
raise unless policy == :deny || e.is_a?(StandardError) || e.is_a?(ScriptError)
|
|
68
72
|
|
|
@@ -130,6 +134,7 @@ module Samagotchi
|
|
|
130
134
|
def register(name, &block)
|
|
131
135
|
raise ArgumentError, "hook name must be a Symbol" unless name.is_a?(Symbol)
|
|
132
136
|
raise ArgumentError, "hook block is required" unless block_given?
|
|
137
|
+
|
|
133
138
|
@mutex.synchronize { (@hooks[name] ||= []) << block }
|
|
134
139
|
end
|
|
135
140
|
|
|
@@ -144,6 +149,7 @@ module Samagotchi
|
|
|
144
149
|
def register_persistent(name, label: nil, &block)
|
|
145
150
|
raise ArgumentError, "hook name must be a Symbol" unless name.is_a?(Symbol)
|
|
146
151
|
raise ArgumentError, "hook block is required" unless block_given?
|
|
152
|
+
|
|
147
153
|
label = label.to_s.strip
|
|
148
154
|
label = CONFIG_HOOK_LABEL if label.empty?
|
|
149
155
|
@mutex.synchronize { (@persistent_hooks[name] ||= []) << { label: label, proc: block } }
|
|
@@ -160,6 +166,7 @@ module Samagotchi
|
|
|
160
166
|
# @return [void]
|
|
161
167
|
def register_bundle(bundle_name, event_name, hook_name:, priority: 100, &block)
|
|
162
168
|
raise ArgumentError, "hook block is required" unless block_given?
|
|
169
|
+
|
|
163
170
|
@mutex.synchronize do
|
|
164
171
|
(@bundle_hooks[event_name] ||= []) << {
|
|
165
172
|
bundle: bundle_name,
|
|
@@ -180,6 +187,7 @@ module Samagotchi
|
|
|
180
187
|
if event_name
|
|
181
188
|
arr = @bundle_hooks[event_name]
|
|
182
189
|
return 0 unless arr
|
|
190
|
+
|
|
183
191
|
before = arr.size
|
|
184
192
|
@bundle_hooks[event_name] = arr.reject { |h| h[:bundle] == bundle_name }
|
|
185
193
|
removed = before - @bundle_hooks[event_name].size
|
|
@@ -118,9 +118,7 @@ module Samagotchi
|
|
|
118
118
|
@model_index = nil # downcased model_id => [host_name] in hosts: order
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
@entries
|
|
123
|
-
end
|
|
121
|
+
attr_reader :entries
|
|
124
122
|
|
|
125
123
|
# Loopback, private (RFC 1918, IPv6 unique local) and link-local nets.
|
|
126
124
|
LOCAL_NETS = %w[127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 100.64.0.0/10
|
|
@@ -46,7 +46,7 @@ module Samagotchi
|
|
|
46
46
|
module TranscriptFilter
|
|
47
47
|
# A tool call as the model wrote it inline: gemma, qwen, or the qwen
|
|
48
48
|
# prompt-literal form. An unterminated gemma call runs to the end.
|
|
49
|
-
TOOL_CALL_RE =
|
|
49
|
+
TOOL_CALL_RE = %r{<\|tool_call>(?:.*?<tool_call\|>|.*\z)|<tool_call>.*?</tool_call>|\[\[SAMAGOTCHI_LITERAL_TOOL_CALL_OPEN\]\].*?\[\[SAMAGOTCHI_LITERAL_TOOL_CALL_CLOSE\]\]}m
|
|
50
50
|
# Each dispatched call's output starts with "[name]"; the kernel loop
|
|
51
51
|
# joins one step's outputs into a single tool_response with this
|
|
52
52
|
# separator, while the chat loop writes one message per call.
|
|
@@ -63,8 +63,10 @@ module Samagotchi
|
|
|
63
63
|
|
|
64
64
|
case message["role"]
|
|
65
65
|
when "user"
|
|
66
|
-
# A plugin's steer is a prod to the model, not what the user said
|
|
67
|
-
|
|
66
|
+
# A plugin's steer is a prod to the model, not what the user said;
|
|
67
|
+
# a continue steer from the user or the parent agent is an
|
|
68
|
+
# instruction, kept.
|
|
69
|
+
next nil if Steer.steer?(message) && !Steer.person?(message)
|
|
68
70
|
|
|
69
71
|
# An image is a line naming it (refs only, never its bytes), unless
|
|
70
72
|
# the text is only the web's placeholder naming it already.
|
|
@@ -215,7 +217,7 @@ module Samagotchi
|
|
|
215
217
|
system = format(SYSTEM, range) + (previous ? UPDATE + format(UPDATE_LENGTH, range) : "") + tools
|
|
216
218
|
user = +""
|
|
217
219
|
user << "Earlier recap:\n#{previous}\n\n" if previous
|
|
218
|
-
user << "Transcript#{
|
|
220
|
+
user << "Transcript#{" since the earlier recap" if previous} (the system prompt and tool " \
|
|
219
221
|
"internals were removed; only user turns and assistant prose remain):\n---\n#{body}\n---\n"
|
|
220
222
|
user << "#{OFFER_LINE}\n" if offer
|
|
221
223
|
user << "Write the recap now."
|
|
@@ -225,7 +227,7 @@ module Samagotchi
|
|
|
225
227
|
# "2-4 sentences", or "3 sentences" for [3, 3] ("1 sentence" for [1, 1])
|
|
226
228
|
def sentences_text((min, max), adjective = "")
|
|
227
229
|
count = min == max ? min.to_s : "#{min}-#{max}"
|
|
228
|
-
"#{count} #{adjective}#{max == 1 ?
|
|
230
|
+
"#{count} #{adjective}#{max == 1 ? "sentence" : "sentences"}"
|
|
229
231
|
end
|
|
230
232
|
|
|
231
233
|
# The tail of +body+ when it is over MAX_NEW_CHARS, from a paragraph
|
|
@@ -243,7 +245,7 @@ module Samagotchi
|
|
|
243
245
|
def tools_used(names)
|
|
244
246
|
return "" if names.empty?
|
|
245
247
|
|
|
246
|
-
" (#{names.tally.map { |name, n| n > 1 ? "#{name} x#{n}" : name }.join(
|
|
248
|
+
" (#{names.tally.map { |name, n| n > 1 ? "#{name} x#{n}" : name }.join(", ")})"
|
|
247
249
|
end
|
|
248
250
|
end
|
|
249
251
|
|
|
@@ -395,6 +397,17 @@ module Samagotchi
|
|
|
395
397
|
@engine.activity_seq > @last_fire_activity_seq
|
|
396
398
|
end
|
|
397
399
|
|
|
400
|
+
# SHA1 of one message's role and text: detects a rewrite a count alone
|
|
401
|
+
# misses. Model text is taken without its thinking, which is dropped from
|
|
402
|
+
# older model messages when the next turn starts.
|
|
403
|
+
def self.digest(message)
|
|
404
|
+
return nil unless message.is_a?(Hash)
|
|
405
|
+
|
|
406
|
+
text = message["content"].to_s
|
|
407
|
+
text = TranscriptFilter.strip_thought(text) if %w[model assistant].include?(message["role"])
|
|
408
|
+
Digest::SHA1.hexdigest("#{message["role"]}\0#{text}")
|
|
409
|
+
end
|
|
410
|
+
|
|
398
411
|
private
|
|
399
412
|
|
|
400
413
|
# A failing check writes the recap without the offer line.
|
|
@@ -431,9 +444,11 @@ module Samagotchi
|
|
|
431
444
|
# Nothing new said (only notes, tool traffic, or no messages at all):
|
|
432
445
|
# the recap still stands, so no request.
|
|
433
446
|
return :nothing_new if transcript.strip.empty?
|
|
447
|
+
|
|
434
448
|
prompt = RecapPrompt.build(transcript, tool_names: TranscriptFilter.tool_names(fresh), previous: previous&.dig(:text),
|
|
435
449
|
sentences: @sentences, offer: offer_open?)
|
|
436
450
|
return :nothing_new if prompt.nil?
|
|
451
|
+
|
|
437
452
|
asked = target
|
|
438
453
|
@in_flight = { thread: spawn_summarize(client_for(asked), prompt), generation: gen, deadline: @clock.call + @timeout,
|
|
439
454
|
covered: parsed.size, covered_digest: self.class.digest(parsed.last), model: asked[:label] }
|
|
@@ -459,6 +474,7 @@ module Samagotchi
|
|
|
459
474
|
@in_flight = nil
|
|
460
475
|
recap = safe_value(job[:thread])
|
|
461
476
|
return if recap.nil? || recap.to_s.strip.empty?
|
|
477
|
+
|
|
462
478
|
# An IdleClient::Summary names the model that answered; a plain String
|
|
463
479
|
# (a stubbed client) doesn't, and the asked label stands in.
|
|
464
480
|
served = recap.model if recap.respond_to?(:model)
|
|
@@ -503,17 +519,6 @@ module Samagotchi
|
|
|
503
519
|
saved
|
|
504
520
|
end
|
|
505
521
|
|
|
506
|
-
# SHA1 of one message's role and text: detects a rewrite a count alone
|
|
507
|
-
# misses. Model text is taken without its thinking, which is dropped from
|
|
508
|
-
# older model messages when the next turn starts.
|
|
509
|
-
def self.digest(message)
|
|
510
|
-
return nil unless message.is_a?(Hash)
|
|
511
|
-
|
|
512
|
-
text = message["content"].to_s
|
|
513
|
-
text = TranscriptFilter.strip_thought(text) if %w[model assistant].include?(message["role"])
|
|
514
|
-
Digest::SHA1.hexdigest("#{message['role']}\0#{text}")
|
|
515
|
-
end
|
|
516
|
-
|
|
517
522
|
def bump_generation
|
|
518
523
|
@mutex.synchronize { @generation += 1 }
|
|
519
524
|
end
|