samagotchi 0.3.0 → 0.5.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 +162 -1
- data/README.md +29 -2
- data/bin/chi +60 -69
- data/docs/cli.md +211 -77
- data/docs/configuration.md +118 -21
- data/docs/desktop.md +39 -4
- data/docs/guardrails.md +11 -0
- data/docs/hooks.md +89 -7
- data/docs/memory.md +40 -0
- data/docs/plugins.md +50 -0
- data/docs/releasing.md +15 -12
- data/docs/sessions.md +20 -18
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -2
- data/lib/samagotchi/bridge/sse_writer.rb +0 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
- data/lib/samagotchi/bridge.rb +20 -12
- data/lib/samagotchi/bundles/skills/manifest.yml +10 -0
- data/lib/samagotchi/bundles/skills/plugin.rb +419 -0
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +178 -5
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +9 -10
- data/lib/samagotchi/bundles/system/delegated.md +6 -7
- data/lib/samagotchi/bundles/system/identity.md +5 -0
- data/lib/samagotchi/bundles/system/manifest.yml +6 -6
- data/lib/samagotchi/bundles/system/memory_guide.md +26 -0
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/client.rb +25 -26
- data/lib/samagotchi/commands/registry.rb +8 -0
- data/lib/samagotchi/config.rb +97 -113
- data/lib/samagotchi/desktop/macos/App.swift +12 -8
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +4 -2
- data/lib/samagotchi/desktop/macos/Images.swift +113 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +112 -9
- data/lib/samagotchi/desktop/macos.rb +59 -8
- data/lib/samagotchi/desktop_command.rb +6 -3
- data/lib/samagotchi/edit_preview.rb +82 -0
- data/lib/samagotchi/engine.rb +236 -443
- data/lib/samagotchi/gem_update.rb +89 -0
- data/lib/samagotchi/guardrails/approval.rb +26 -4
- data/lib/samagotchi/guardrails/load_failures.rb +9 -3
- data/lib/samagotchi/host_registry.rb +8 -12
- data/lib/samagotchi/idle_client.rb +24 -15
- data/lib/samagotchi/idle_reminders.rb +2 -2
- data/lib/samagotchi/image_store.rb +10 -6
- data/lib/samagotchi/kernel_loop.rb +59 -123
- data/lib/samagotchi/live_versions.rb +65 -0
- data/lib/samagotchi/llm/api_key.rb +41 -0
- data/lib/samagotchi/llm/chat_loop.rb +77 -13
- data/lib/samagotchi/llm/errors.rb +38 -7
- data/lib/samagotchi/llm/http.rb +19 -22
- data/lib/samagotchi/llm/openai_chat.rb +22 -26
- data/lib/samagotchi/memory_bundle/installer.rb +65 -63
- data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
- data/lib/samagotchi/memory_bundle/status.rb +4 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
- data/lib/samagotchi/model_profile.rb +27 -10
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/prompt.rb +4 -2
- data/lib/samagotchi/reminder_store.rb +1 -9
- data/lib/samagotchi/reply_wait.rb +48 -4
- data/lib/samagotchi/self_report.rb +37 -5
- data/lib/samagotchi/send_command.rb +190 -17
- data/lib/samagotchi/session.rb +4 -2
- data/lib/samagotchi/session_commands.rb +38 -8
- data/lib/samagotchi/session_manager.rb +19 -53
- data/lib/samagotchi/system_prompt.rb +403 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +141 -108
- data/lib/samagotchi/terminal_ui/attached_view.rb +27 -12
- data/lib/samagotchi/terminal_ui/event_renderer.rb +29 -8
- data/lib/samagotchi/terminal_ui/formatting.rb +41 -22
- data/lib/samagotchi/terminal_ui/input_support.rb +7 -23
- data/lib/samagotchi/terminal_ui/plain_surface.rb +13 -7
- data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
- data/lib/samagotchi/terminal_ui/status_row.rb +81 -0
- data/lib/samagotchi/terminal_ui/surface.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +142 -923
- data/lib/samagotchi/text_diff.rb +181 -0
- data/lib/samagotchi/thinking.rb +126 -0
- data/lib/samagotchi/tool_activity.rb +52 -2
- data/lib/samagotchi/tool_runner.rb +37 -1
- data/lib/samagotchi/tools/ask_user_question.rb +41 -33
- data/lib/samagotchi/tools/edit.rb +23 -9
- data/lib/samagotchi/tools/execute.rb +3 -3
- data/lib/samagotchi/tools/output_guardrails.rb +8 -7
- data/lib/samagotchi/tools/read.rb +4 -4
- data/lib/samagotchi/tools/write.rb +4 -0
- data/lib/samagotchi/turn_flow.rb +12 -2
- data/lib/samagotchi/update_command.rb +309 -0
- data/lib/samagotchi/update_hint.rb +59 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +6 -4
- data/lib/samagotchi/web/app.rb +173 -38
- data/lib/samagotchi/web/lan.rb +99 -0
- data/lib/samagotchi/web/message_parts.rb +19 -10
- data/lib/samagotchi/web/public/activity.js +10 -0
- data/lib/samagotchi/web/public/app.js +135 -78
- data/lib/samagotchi/web/public/chat_view.js +8 -1
- data/lib/samagotchi/web/public/data.js +2 -0
- data/lib/samagotchi/web/public/diff_view.js +58 -0
- data/lib/samagotchi/web/public/index.html +185 -18
- data/lib/samagotchi/web/public/model_pick.js +136 -0
- data/lib/samagotchi/web/public/model_picker.js +224 -0
- data/lib/samagotchi/web/public/notify.js +10 -0
- data/lib/samagotchi/web/public/question_card.js +3 -1
- data/lib/samagotchi/web/public/stage_model.js +110 -0
- data/lib/samagotchi/web/public/stage_view.js +580 -0
- data/lib/samagotchi/web/public/timing.js +6 -2
- data/lib/samagotchi/web/public/turn_events.js +38 -10
- data/lib/samagotchi/web/public/turn_model.js +11 -3
- data/lib/samagotchi/web/public/turn_view.js +76 -20
- data/lib/samagotchi/web/qr.rb +40 -0
- data/lib/samagotchi/web/server.rb +101 -11
- data/lib/samagotchi/web/token.rb +97 -0
- data/lib/samagotchi/worker.rb +5 -4
- metadata +38 -3
- data/lib/samagotchi/terminal_ui/legacy_surface.rb +0 -111
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "rubygems/package"
|
|
8
|
+
require_relative "version"
|
|
9
|
+
|
|
10
|
+
module Samagotchi
|
|
11
|
+
# The gem part of `chi update`: the newest samagotchi on rubygems, and a
|
|
12
|
+
# `gem install` of it through this Ruby's own gem command (the real
|
|
13
|
+
# interpreter's bindir, not a shim: the same under mise, rbenv and asdf).
|
|
14
|
+
#
|
|
15
|
+
# SAMAGOTCHI_UPDATE_GEM_FILE=<path to a .gem> (hidden, for smokes) makes that
|
|
16
|
+
# file the newest version and installs it instead.
|
|
17
|
+
class GemUpdate
|
|
18
|
+
Error = Class.new(StandardError)
|
|
19
|
+
|
|
20
|
+
LATEST_URL = URI("https://rubygems.org/api/v1/versions/samagotchi/latest.json")
|
|
21
|
+
# Not SAMAGOTCHI_UPDATE_GEM: that is the env form of config update.gem.
|
|
22
|
+
LOCAL_ENV = "SAMAGOTCHI_UPDATE_GEM_FILE"
|
|
23
|
+
TIMEOUT = 5
|
|
24
|
+
|
|
25
|
+
# source is what `gem install` takes: the gem's name, or a .gem file.
|
|
26
|
+
Latest = Struct.new(:version, :source, keyword_init: true)
|
|
27
|
+
|
|
28
|
+
# Runs a command without a shell; [output, success].
|
|
29
|
+
class Runner
|
|
30
|
+
def run(argv)
|
|
31
|
+
output, status = Open3.capture2e(*argv)
|
|
32
|
+
[output, status.success?]
|
|
33
|
+
rescue SystemCallError => e
|
|
34
|
+
[e.message, false]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param fetcher [#call, nil] → the latest.json body (specs)
|
|
39
|
+
def initialize(env: ENV, fetcher: nil, runner: Runner.new, gem_bin: File.join(RbConfig::CONFIG["bindir"], "gem"))
|
|
40
|
+
@env = env
|
|
41
|
+
@fetcher = fetcher || method(:fetch)
|
|
42
|
+
@runner = runner
|
|
43
|
+
@gem_bin = gem_bin
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Latest]
|
|
47
|
+
# @raise [Error] when it can't be found out (offline, a bad answer)
|
|
48
|
+
def latest
|
|
49
|
+
local = @env[LOCAL_ENV].to_s
|
|
50
|
+
return Latest.new(version: Gem::Package.new(local).spec.version.to_s, source: local) unless local.empty?
|
|
51
|
+
|
|
52
|
+
version = JSON.parse(@fetcher.call.to_s)["version"].to_s
|
|
53
|
+
raise Error, "rubygems.org answered without a version" if version.empty? || !Gem::Version.correct?(version)
|
|
54
|
+
|
|
55
|
+
Latest.new(version: version, source: "samagotchi")
|
|
56
|
+
rescue Error
|
|
57
|
+
raise
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
raise Error, "#{e.class}: #{e.message}".lines.first.strip
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def newer?(latest, current = VERSION)
|
|
63
|
+
Gem::Version.new(latest.version) > Gem::Version.new(current)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def install_argv(latest)
|
|
67
|
+
argv = [@gem_bin, "install", latest.source, "--no-document"]
|
|
68
|
+
latest.source == "samagotchi" ? argv + ["-v", latest.version] : argv
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @return [Array(String, Boolean)] gem's output, success
|
|
72
|
+
def install(latest)
|
|
73
|
+
@runner.run(install_argv(latest))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def fetch
|
|
79
|
+
Net::HTTP.start(LATEST_URL.host, LATEST_URL.port, use_ssl: true, open_timeout: TIMEOUT, read_timeout: TIMEOUT) do |http|
|
|
80
|
+
request = Net::HTTP::Get.new(LATEST_URL)
|
|
81
|
+
request["User-Agent"] = USER_AGENT
|
|
82
|
+
response = http.request(request)
|
|
83
|
+
raise Error, "rubygems.org answered #{response.code}" unless response.code == "200"
|
|
84
|
+
|
|
85
|
+
response.body
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -20,12 +20,14 @@ module Samagotchi
|
|
|
20
20
|
# @param label [String, nil] a plugin tool's label ("chrome:
|
|
21
21
|
# screenshot"): the question names the tool by it, as its row does;
|
|
22
22
|
# approval[:tool] stays the raw name
|
|
23
|
+
# @param preview [Hash, nil] EditPreview.for(verdict.call) for edit and
|
|
24
|
+
# write: the card shows its diff, the question text one change line
|
|
23
25
|
# @return [Hash] open_question fields
|
|
24
|
-
def payload(verdict, label: nil)
|
|
26
|
+
def payload(verdict, label: nil, preview: nil)
|
|
25
27
|
scopes = offered_scopes(verdict)
|
|
26
28
|
targets = verdict.targets
|
|
27
29
|
{
|
|
28
|
-
question: question_text(verdict, label: label),
|
|
30
|
+
question: question_text(verdict, label: label, preview: preview),
|
|
29
31
|
options: scopes.map { |scope| label(scope, verdict) } + [DENY],
|
|
30
32
|
header: HEADER,
|
|
31
33
|
multi_select: false,
|
|
@@ -44,7 +46,8 @@ module Samagotchi
|
|
|
44
46
|
rule: verdict.rule,
|
|
45
47
|
source: verdict.source,
|
|
46
48
|
reason: verdict.reason,
|
|
47
|
-
scopes: scopes
|
|
49
|
+
scopes: scopes,
|
|
50
|
+
preview: preview
|
|
48
51
|
}.compact
|
|
49
52
|
}
|
|
50
53
|
end
|
|
@@ -112,7 +115,8 @@ module Samagotchi
|
|
|
112
115
|
# execute: git push origin main
|
|
113
116
|
# in /path/to/repo (repo samagotchi, branch main)
|
|
114
117
|
# why: git push publishes commits (rule git-push, bundle guardrails)
|
|
115
|
-
|
|
118
|
+
# change: +3 −1 (edit/write, from the preview)
|
|
119
|
+
def question_text(verdict, label: nil, preview: nil)
|
|
116
120
|
targets = verdict.targets
|
|
117
121
|
tool = label || targets&.tool || verdict.call[:name].to_s
|
|
118
122
|
what = targets&.command || (targets && targets.paths.join(", "))
|
|
@@ -127,8 +131,26 @@ module Samagotchi
|
|
|
127
131
|
who = verdict.rule ? ["rule #{verdict.rule}", verdict.source].compact.join(", ") : (verdict.source || "hook")
|
|
128
132
|
reason = verdict.reason.to_s.strip
|
|
129
133
|
lines << " why: #{reason.empty? ? "(no reason given)" : reason} (#{who})"
|
|
134
|
+
change = change_text(preview)
|
|
135
|
+
lines << " change: #{change}" if change
|
|
130
136
|
lines.join("\n")
|
|
131
137
|
end
|
|
138
|
+
|
|
139
|
+
# One line for an EditPreview: "+3 −1", "new file, 12 lines",
|
|
140
|
+
# "would fail: …" or "not shown (binary file)". The diff itself stays
|
|
141
|
+
# out of the question text (size); the card and the TUI show it.
|
|
142
|
+
def change_text(preview)
|
|
143
|
+
return nil unless preview.is_a?(Hash)
|
|
144
|
+
|
|
145
|
+
get = ->(key) { preview[key] || preview[key.to_s] }
|
|
146
|
+
return "would fail: #{get.(:error)}" if get.(:error)
|
|
147
|
+
return "not shown (#{get.(:skipped)})" if get.(:skipped)
|
|
148
|
+
|
|
149
|
+
added = get.(:added).to_i
|
|
150
|
+
return "new file, #{added} #{added == 1 ? "line" : "lines"}" if get.(:new_file)
|
|
151
|
+
|
|
152
|
+
"+#{added} −#{get.(:removed).to_i}"
|
|
153
|
+
end
|
|
132
154
|
end
|
|
133
155
|
end
|
|
134
156
|
end
|
|
@@ -7,7 +7,7 @@ module Samagotchi
|
|
|
7
7
|
# guardrail that silently vanished is the failure this guards against);
|
|
8
8
|
# the rest are only announced.
|
|
9
9
|
class LoadFailures
|
|
10
|
-
Failure = Struct.new(:what, :reason, :required, keyword_init: true)
|
|
10
|
+
Failure = Struct.new(:what, :reason, :required, :group, keyword_init: true)
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
13
13
|
@list = []
|
|
@@ -15,8 +15,14 @@ module Samagotchi
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# @param what [String] e.g. "hook guard.rb (config)"
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
# @param group [Symbol, nil] what loads it again (#drop): :rules
|
|
19
|
+
def add(what, reason, required:, group: nil)
|
|
20
|
+
@mutex.synchronize { @list << Failure.new(what: what, reason: reason, required: required, group: group) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Forget a group's failures before it loads again.
|
|
24
|
+
def drop(group)
|
|
25
|
+
@mutex.synchronize { @list.reject! { |failure| failure.group == group } }
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
def list = @mutex.synchronize { @list.dup }
|
|
@@ -29,9 +29,10 @@ module Samagotchi
|
|
|
29
29
|
# profile: the configured prompt profile name, if any;
|
|
30
30
|
# first_token_timeout: the configured first-token limit (see #first_token_limit);
|
|
31
31
|
# vision: the configured true/false (VisionSupport), nil when unset;
|
|
32
|
-
# sampling: the configured request parameters (SamplingSettings), nil when unset
|
|
32
|
+
# sampling: the configured request parameters (SamplingSettings), nil when unset;
|
|
33
|
+
# thinking: the configured level (Thinking), nil when unset.
|
|
33
34
|
HostEntry = Struct.new(:name, :host, :port, :transport, :client, :api, :scheme, :url, :api_key_env, :profile,
|
|
34
|
-
:first_token_timeout, :vision, :sampling, keyword_init: true) do
|
|
35
|
+
:first_token_timeout, :vision, :sampling, :thinking, keyword_init: true) do
|
|
35
36
|
# Talks the OpenAI chat API (the chat loop); nil and raw apis use the
|
|
36
37
|
# raw-prompt loop.
|
|
37
38
|
def chat? = api == :openai
|
|
@@ -87,9 +88,10 @@ module Samagotchi
|
|
|
87
88
|
entry = HostEntry.new(name: key.to_s.downcase, host: cfg[:host], port: cfg[:port].to_i, transport: transport,
|
|
88
89
|
api: cfg[:api]&.to_sym, scheme: cfg[:scheme], url: cfg[:url], api_key_env: cfg[:api_key_env],
|
|
89
90
|
profile: cfg[:profile], first_token_timeout: cfg[:first_token_timeout],
|
|
90
|
-
vision: cfg[:vision], sampling: cfg[:sampling])
|
|
91
|
+
vision: cfg[:vision], sampling: cfg[:sampling], thinking: cfg[:thinking])
|
|
91
92
|
entry.client = Client.new(host: cfg[:host], port: cfg[:port], transport: transport, scheme: cfg[:scheme],
|
|
92
|
-
first_token_timeout: entry.first_token_limit, name: entry.name
|
|
93
|
+
first_token_timeout: entry.first_token_limit, name: entry.name,
|
|
94
|
+
api_key_env: entry.api_key_env, env: env)
|
|
93
95
|
@entries[entry.name] = entry
|
|
94
96
|
end
|
|
95
97
|
# Fallback single entry (should already be synthesized by hosts_config, but guard)
|
|
@@ -117,10 +119,6 @@ module Samagotchi
|
|
|
117
119
|
nil
|
|
118
120
|
end
|
|
119
121
|
|
|
120
|
-
def entry_names
|
|
121
|
-
@entries.keys
|
|
122
|
-
end
|
|
123
|
-
|
|
124
122
|
def default_entry
|
|
125
123
|
@entries["default"] || @entries.values.first
|
|
126
124
|
end
|
|
@@ -149,10 +147,8 @@ module Samagotchi
|
|
|
149
147
|
rescue StandardError
|
|
150
148
|
nil
|
|
151
149
|
end
|
|
152
|
-
|
|
153
|
-
return [
|
|
154
|
-
# Unknown prefix — treat as bare model on default host
|
|
155
|
-
return [default_entry, raw_model.to_s.strip]
|
|
150
|
+
# parse_qualified_model names a host only when it is one of ours
|
|
151
|
+
return [find_entry(host_ref), bare]
|
|
156
152
|
end
|
|
157
153
|
# Unqualified: also try alias resolution for discovery (small -> gemma-small or small -> small-box:gemma-small)
|
|
158
154
|
begin
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "llm/openai_chat"
|
|
5
|
+
require_relative "thinking"
|
|
6
|
+
require_relative "log"
|
|
5
7
|
|
|
6
8
|
module Samagotchi
|
|
7
9
|
# Standalone summarizer for the idle session-recap feature.
|
|
@@ -33,17 +35,6 @@ module Samagotchi
|
|
|
33
35
|
# Up to 10 sentences (recap.sentences) need ~350 tokens with thinking off.
|
|
34
36
|
MAX_TOKENS = 512
|
|
35
37
|
|
|
36
|
-
# Request fields that turn thinking off. A reasoning model otherwise
|
|
37
|
-
# spends the budget thinking and the recap stops mid-sentence.
|
|
38
|
-
# - chat_template_kwargs.enable_thinking: the chat template's switch
|
|
39
|
-
# (llama.cpp with Qwen/Gemma templates); templates without it ignore it.
|
|
40
|
-
# - reasoning_effort "none": the OpenAI-style knob, for servers that
|
|
41
|
-
# ignore the template switch (Splash thought until max_tokens).
|
|
42
|
-
THINKING_OFF = {
|
|
43
|
-
chat_template_kwargs: { enable_thinking: false },
|
|
44
|
-
reasoning_effort: "none"
|
|
45
|
-
}.freeze
|
|
46
|
-
|
|
47
38
|
# @param base_url [String] the OpenAI API base, e.g. http://host:8081/v1
|
|
48
39
|
# @param api_key_env [String, nil] the variable holding the host's key
|
|
49
40
|
# @param timeout [Numeric] HTTP request timeout. Kept to the recap's own
|
|
@@ -119,10 +110,17 @@ module Samagotchi
|
|
|
119
110
|
# +max_tokens+, the text keeps its finished sentences (+whole_sentences+)
|
|
120
111
|
# or all of it, with "…".
|
|
121
112
|
def generate(messages, max_tokens: MAX_TOKENS, cancel_controller: nil, whole_sentences: true)
|
|
122
|
-
response =
|
|
123
|
-
messages
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
response = begin
|
|
114
|
+
request(messages, max_tokens, cancel_controller)
|
|
115
|
+
rescue LLM::BadRequest => e
|
|
116
|
+
raise unless e.reasoning_refused? && !@thinking_refused
|
|
117
|
+
|
|
118
|
+
# The host won't turn thinking off (gpt-oss): ask again without the
|
|
119
|
+
# fields, and leave them out from now on.
|
|
120
|
+
@thinking_refused = true
|
|
121
|
+
Log.info(:recap, "thinking_refused", model: @model, detail: e.detail)
|
|
122
|
+
request(messages, max_tokens, cancel_controller)
|
|
123
|
+
end
|
|
126
124
|
content = response.text
|
|
127
125
|
reasoning = response.reasoning
|
|
128
126
|
raise SummarizeError, "server returned no parseable assistant content" if content.empty? && reasoning.empty?
|
|
@@ -143,5 +141,16 @@ module Samagotchi
|
|
|
143
141
|
raise SummarizeError, "server returned no parseable assistant content (#{e.message})"
|
|
144
142
|
end
|
|
145
143
|
|
|
144
|
+
def request(messages, max_tokens, cancel_controller)
|
|
145
|
+
@chat.chat(messages: messages, model: @model, tools: [], cancel_controller: cancel_controller,
|
|
146
|
+
options: { max_tokens: max_tokens, **thinking_fields })
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A recap or side answer is short and tool-less: thinking off, whatever
|
|
150
|
+
# the model's level (a reasoning model otherwise spends the budget
|
|
151
|
+
# thinking and the recap stops mid-sentence). None once the host refused them.
|
|
152
|
+
def thinking_fields
|
|
153
|
+
@thinking_refused ? {} : Thinking.chat_fields(:off)
|
|
154
|
+
end
|
|
146
155
|
end
|
|
147
156
|
end
|
|
@@ -9,7 +9,7 @@ module Samagotchi
|
|
|
9
9
|
# (one background thread for the whole idle layer).
|
|
10
10
|
#
|
|
11
11
|
# Two delivery paths cooperate:
|
|
12
|
-
# 1. Pull-based: Engine#
|
|
12
|
+
# 1. Pull-based: Engine#collect_due_reminders
|
|
13
13
|
# reads ReminderStore#due_reminders at turn start → injects
|
|
14
14
|
# [SYSTEM: REMINDERS DUE] → marks all as fired atomically.
|
|
15
15
|
# 2. Synthetic turns: when this job's tick finds due reminders while
|
|
@@ -43,7 +43,7 @@ module Samagotchi
|
|
|
43
43
|
@mutex.synchronize { @due_reminder_name }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
# Get all due reminders (from ReminderStore). Called by Engine#
|
|
46
|
+
# Get all due reminders (from ReminderStore). Called by Engine#collect_due_reminders.
|
|
47
47
|
# Thread-safe. Returns all due reminders, not just one.
|
|
48
48
|
# @return [Array<Hash>] [{name:, description:, interval_minutes:}, ...]
|
|
49
49
|
def due_reminders
|
|
@@ -262,12 +262,7 @@ module Samagotchi
|
|
|
262
262
|
next message unless images.is_a?(Array) && !images.empty?
|
|
263
263
|
|
|
264
264
|
kept, gone = from ? images.partition { |ref| valid_ref?(from, ref) } : [[], images]
|
|
265
|
-
kept.each
|
|
266
|
-
file = (ref[:file] || ref["file"]).to_s
|
|
267
|
-
target = File.join(to.to_s, file)
|
|
268
|
-
FileUtils.mkdir_p(File.dirname(target))
|
|
269
|
-
FileUtils.cp(File.join(from.to_s, file), target) unless File.exist?(target)
|
|
270
|
-
end
|
|
265
|
+
kept.each { |ref| copy_file(ref, from: from, to: to) }
|
|
271
266
|
dropped += gone.size
|
|
272
267
|
message = message.reject { |key, _| key.to_s == "images" }
|
|
273
268
|
message[:images] = kept unless kept.empty?
|
|
@@ -280,6 +275,15 @@ module Samagotchi
|
|
|
280
275
|
[copied, dropped]
|
|
281
276
|
end
|
|
282
277
|
|
|
278
|
+
# One stored image's file from session dir +from+ into +to+ (kept when
|
|
279
|
+
# it is there already: the name is the content's hash).
|
|
280
|
+
def self.copy_file(ref, from:, to:)
|
|
281
|
+
file = (ref[:file] || ref["file"]).to_s
|
|
282
|
+
target = File.join(to.to_s, file)
|
|
283
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
284
|
+
FileUtils.cp(File.join(from.to_s, file), target) unless File.exist?(target)
|
|
285
|
+
end
|
|
286
|
+
|
|
283
287
|
# A ref for an image already stored in this session (a web upload),
|
|
284
288
|
# rebuilt from the file itself: only its name comes from the caller.
|
|
285
289
|
def self.ref_for(session_dir, file:, name: nil, source: "user")
|