samagotchi 0.11.0 → 0.12.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 +29 -1
- data/bin/chi +5 -2
- data/docs/architecture.md +4 -4
- data/docs/cli.md +25 -1
- data/docs/desktop.md +24 -2
- data/lib/samagotchi/bridge.rb +4 -4
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/App.swift +5 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +19 -0
- data/lib/samagotchi/desktop/macos/Models.swift +238 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +301 -42
- data/lib/samagotchi/engine.rb +445 -482
- data/lib/samagotchi/host_registry.rb +26 -2
- data/lib/samagotchi/idle_reminders.rb +13 -36
- data/lib/samagotchi/kernel_loop.rb +82 -120
- data/lib/samagotchi/llm/chat_loop.rb +17 -38
- data/lib/samagotchi/llm/model_result.rb +14 -20
- data/lib/samagotchi/llm/native_backend.rb +3 -21
- data/lib/samagotchi/llm/openai_chat.rb +1 -1
- data/lib/samagotchi/llm/turn_settings.rb +19 -0
- data/lib/samagotchi/llm/usage.rb +2 -57
- data/lib/samagotchi/memory_bundle/installer.rb +1 -1
- data/lib/samagotchi/model_catalog.rb +101 -0
- data/lib/samagotchi/model_profile.rb +1 -1
- data/lib/samagotchi/models_command.rb +112 -0
- data/lib/samagotchi/reminder_queue.rb +73 -0
- data/lib/samagotchi/reminder_store.rb +3 -3
- data/lib/samagotchi/self_report.rb +8 -1
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session_commands.rb +5 -5
- data/lib/samagotchi/session_manager.rb +2 -6
- data/lib/samagotchi/terminal_ui.rb +8 -28
- data/lib/samagotchi/thinking.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +4 -6
- data/lib/samagotchi/tools/builtins.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/turn_flow.rb +4 -4
- data/lib/samagotchi/turn_state.rb +126 -0
- data/lib/samagotchi/used_memories.rb +88 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -4
- data/lib/samagotchi/web/app.rb +14 -33
- data/lib/samagotchi/web/session_hub.rb +0 -4
- data/lib/samagotchi/web/session_summary.rb +6 -6
- data/lib/samagotchi/worker.rb +1 -1
- metadata +8 -1
|
@@ -12,7 +12,6 @@ require_relative "context_note"
|
|
|
12
12
|
require_relative "cancellation_controller"
|
|
13
13
|
require_relative "llm/errors"
|
|
14
14
|
require_relative "host_registry"
|
|
15
|
-
require_relative "kernel_loop"
|
|
16
15
|
require_relative "session"
|
|
17
16
|
require_relative "owner_lock"
|
|
18
17
|
require_relative "engine"
|
|
@@ -114,9 +113,6 @@ module Samagotchi
|
|
|
114
113
|
# The model it starts on names a configured host (a bad default.model
|
|
115
114
|
# doesn't matter when --model or the session picks another).
|
|
116
115
|
ModelProfile.check_host!(@effective_model_name, hosts: @host_registry.entries)
|
|
117
|
-
# Only a caller's profile goes in; otherwise the Engine resolves one
|
|
118
|
-
# for the effective model and hands it to the kernel.
|
|
119
|
-
@kernel = KernelLoop.new(client: @client, profile: profile, reminder_store: Samagotchi::ReminderStore.new)
|
|
120
116
|
@no_default_input = no_default_input
|
|
121
117
|
@non_interactive = non_interactive
|
|
122
118
|
# --memory and --mute, with a resumed session's own lists first: a
|
|
@@ -157,7 +153,6 @@ module Samagotchi
|
|
|
157
153
|
model_name: nil,
|
|
158
154
|
memories: @requested_memories,
|
|
159
155
|
muted_memories: @muted_memory_names,
|
|
160
|
-
kernel: @kernel,
|
|
161
156
|
recap: scratch ? nil : recap_config,
|
|
162
157
|
scratch: scratch,
|
|
163
158
|
reminders: {
|
|
@@ -245,7 +240,9 @@ module Samagotchi
|
|
|
245
240
|
cancel_controller: nil,
|
|
246
241
|
images: ImageInput.extract(@prompt)
|
|
247
242
|
)
|
|
248
|
-
|
|
243
|
+
# No answer (the empty-answer retries used up): no text, or the chat
|
|
244
|
+
# loop's placeholder (LLM::ModelResult#empty_answer?).
|
|
245
|
+
if result.empty_answer?
|
|
249
246
|
# Nothing to print: say so on stderr and fail, so a script that
|
|
250
247
|
# pipes the answer doesn't take the silence for one.
|
|
251
248
|
session.save
|
|
@@ -281,15 +278,6 @@ module Samagotchi
|
|
|
281
278
|
|
|
282
279
|
EMPTY_ANSWER_ERROR = "chi: the model gave an empty answer"
|
|
283
280
|
|
|
284
|
-
# A -p --non-interactive turn that ended with no answer (the empty-answer
|
|
285
|
-
# retries used up): no text, or the chat loop's placeholder.
|
|
286
|
-
def empty_answer?(result)
|
|
287
|
-
return false if result.respond_to?(:canceled?) && result.canceled?
|
|
288
|
-
return false if result.respond_to?(:resumable?) && result.resumable?
|
|
289
|
-
|
|
290
|
-
result.output.to_s.strip.empty? || (result.respond_to?(:empty_answer?) && result.empty_answer?)
|
|
291
|
-
end
|
|
292
|
-
|
|
293
281
|
# Delete the scratch session, the last thing the REPL does. Quiet after
|
|
294
282
|
# a one-shot or on the way out of an error or a signal.
|
|
295
283
|
def delete_scratch(session, quiet:)
|
|
@@ -669,7 +657,7 @@ module Samagotchi
|
|
|
669
657
|
# After a prompt or continue turn: TurnFlow keeps the checkpoint and the
|
|
670
658
|
# continue offer; the REPL saves and tells the user.
|
|
671
659
|
def finish_turn(session, result, continue:)
|
|
672
|
-
if result.
|
|
660
|
+
if result.canceled?
|
|
673
661
|
outcome = @turn_flow.after_turn(result, continue: continue)
|
|
674
662
|
# A cancelled continue is back where it started, the offer still open.
|
|
675
663
|
return if outcome == :continue_cancelled
|
|
@@ -680,7 +668,7 @@ module Samagotchi
|
|
|
680
668
|
# user's next message continues from it. !rollback restores the
|
|
681
669
|
# pre-turn checkpoint for an explicit full discard (the renderer's
|
|
682
670
|
# hint under :turn_canceled says so).
|
|
683
|
-
save_session(session) if result.
|
|
671
|
+
save_session(session) if result.conversation.is_a?(Array)
|
|
684
672
|
return
|
|
685
673
|
end
|
|
686
674
|
|
|
@@ -758,9 +746,9 @@ module Samagotchi
|
|
|
758
746
|
ensure
|
|
759
747
|
@surface.clear_slot(:hints)
|
|
760
748
|
end
|
|
761
|
-
canceled = result
|
|
749
|
+
canceled = result&.canceled?
|
|
762
750
|
if result && !canceled
|
|
763
|
-
session.messages = result.conversation if result.
|
|
751
|
+
session.messages = result.conversation if result.conversation.is_a?(Array)
|
|
764
752
|
@engine.store_model!(session)
|
|
765
753
|
session.save
|
|
766
754
|
end
|
|
@@ -996,15 +984,7 @@ module Samagotchi
|
|
|
996
984
|
end
|
|
997
985
|
|
|
998
986
|
def cancelled_result_from(messages, reason:)
|
|
999
|
-
|
|
1000
|
-
output: "",
|
|
1001
|
-
conversation: clone_messages(messages),
|
|
1002
|
-
exhausted: false,
|
|
1003
|
-
pending_tool_calls: false,
|
|
1004
|
-
tool_activity: [],
|
|
1005
|
-
canceled: true,
|
|
1006
|
-
cancellation_reason: reason
|
|
1007
|
-
)
|
|
987
|
+
LLM::ModelResult.new(text: "", conversation: clone_messages(messages), canceled: true, cancellation_reason: reason)
|
|
1008
988
|
end
|
|
1009
989
|
|
|
1010
990
|
# The status row under the prompt, from the Engine's state (what a
|
data/lib/samagotchi/thinking.rb
CHANGED
|
@@ -65,7 +65,7 @@ module Samagotchi
|
|
|
65
65
|
key, value = ConfigFile.model_setting(names, :thinking, models: models)
|
|
66
66
|
return [value, "models: #{key}"] if key
|
|
67
67
|
|
|
68
|
-
host = target.entry.
|
|
68
|
+
host = target.entry.thinking
|
|
69
69
|
return [host, "hosts.#{target.entry.name}"] if host
|
|
70
70
|
return [global, "thinking.level"] if global
|
|
71
71
|
|
|
@@ -146,7 +146,7 @@ module Samagotchi
|
|
|
146
146
|
# says the image is attached, that line replaces the text.
|
|
147
147
|
# @return [Array(Hash, Array<Hash>)] the result and its refs
|
|
148
148
|
def attach_images(call, result)
|
|
149
|
-
vision = @kernel.
|
|
149
|
+
vision = @kernel.turn_settings.vision
|
|
150
150
|
reason = if vision.nil? then "images can't be attached here"
|
|
151
151
|
elsif !vision.sendable? then ImagePlan::CANT_SEE
|
|
152
152
|
end
|
|
@@ -192,7 +192,7 @@ module Samagotchi
|
|
|
192
192
|
end
|
|
193
193
|
|
|
194
194
|
# The kernel's tools, for the activity line of a tool that isn't built in.
|
|
195
|
-
def tools = @kernel.
|
|
195
|
+
def tools = @kernel.tools
|
|
196
196
|
|
|
197
197
|
# A tool the registry has from a plugin (not core, not unknown).
|
|
198
198
|
def plugin_tool?(name)
|
|
@@ -206,8 +206,7 @@ module Samagotchi
|
|
|
206
206
|
# The Engine sets the kernel's gate (its context, later the approval
|
|
207
207
|
# flow); a bare kernel (specs) gets one that only runs the hooks.
|
|
208
208
|
def gate
|
|
209
|
-
|
|
210
|
-
given || (@gate ||= Guardrails::Gate.new(-> { @kernel.hooks if @kernel.respond_to?(:hooks) }, tools_lookup: -> { tools }))
|
|
209
|
+
@kernel.guardrail_gate || (@gate ||= Guardrails::Gate.new(-> { @kernel.hooks }, tools_lookup: -> { tools }))
|
|
211
210
|
end
|
|
212
211
|
|
|
213
212
|
# A gate that fails denies the call (fail closed).
|
|
@@ -254,8 +253,7 @@ module Samagotchi
|
|
|
254
253
|
|
|
255
254
|
# A failing hook must not break the turn.
|
|
256
255
|
def fire(name, event)
|
|
257
|
-
|
|
258
|
-
hooks&.fire(name, event)
|
|
256
|
+
@kernel.hooks&.fire(name, event)
|
|
259
257
|
rescue StandardError
|
|
260
258
|
nil
|
|
261
259
|
end
|
|
@@ -103,7 +103,7 @@ module Samagotchi
|
|
|
103
103
|
# (as DelegateWait does); a bare kernel has no peers: never cancelled.
|
|
104
104
|
def cancelled_proc(kctx)
|
|
105
105
|
peers = kctx.peers
|
|
106
|
-
-> { peers
|
|
106
|
+
-> { !!peers&.cancelled? }
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# @return [Registry] a new registry with the built-ins, in
|
|
@@ -43,7 +43,7 @@ module Samagotchi
|
|
|
43
43
|
def call(child_id, peers:, timeout: TIMEOUT_DEFAULT, poll_interval: POLL_INTERVAL)
|
|
44
44
|
sd = peers.state_dir || Session.default_state_dir
|
|
45
45
|
key = [peers.session_id, child_id]
|
|
46
|
-
cancelled = -> { peers.
|
|
46
|
+
cancelled = -> { peers.cancelled? }
|
|
47
47
|
wait = ReplyWait.call(child_id, state_dir: sd, cursor: seen[key], timeout: timeout.to_i,
|
|
48
48
|
poll_interval: poll_interval, cancelled: cancelled, baseline: baselines[key])
|
|
49
49
|
# The turn sent to is handed over: a later wait looks for a later one.
|
data/lib/samagotchi/turn_flow.rb
CHANGED
|
@@ -72,7 +72,7 @@ module Samagotchi
|
|
|
72
72
|
# turn: its partial progress stays, !rollback can undo it) or
|
|
73
73
|
# :continue_cancelled (back to before the continue; the offer stays)
|
|
74
74
|
def after_turn(result, continue: false, no_interrupt: false)
|
|
75
|
-
if result.
|
|
75
|
+
if result.canceled?
|
|
76
76
|
if continue
|
|
77
77
|
restore(@continue_checkpoint)
|
|
78
78
|
return :continue_cancelled
|
|
@@ -85,7 +85,7 @@ module Samagotchi
|
|
|
85
85
|
return :cancelled
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
if result.
|
|
88
|
+
if result.resumable?
|
|
89
89
|
@offer = { context: interrupted_turn_context(result), no_interrupt: no_interrupt }
|
|
90
90
|
:continue_offered
|
|
91
91
|
else
|
|
@@ -172,7 +172,7 @@ module Samagotchi
|
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
def conversation_of(result)
|
|
175
|
-
result.conversation if result.
|
|
175
|
+
result.conversation if result.conversation.is_a?(Array)
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
def interrupted_turn_context(result)
|
|
@@ -198,7 +198,7 @@ module Samagotchi
|
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
def tool_trace(result)
|
|
201
|
-
activities =
|
|
201
|
+
activities = Array(result.tool_activity)
|
|
202
202
|
activities.last(SUMMARY_TOOLS_LIMIT).map do |activity|
|
|
203
203
|
tool = activity[:tool].to_s.strip
|
|
204
204
|
status = activity[:status].to_s.strip
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Samagotchi
|
|
6
|
+
# A session's turn as other threads see it: whether one runs, its cancel
|
|
7
|
+
# controller and event sink, the plugin steers waiting for its next
|
|
8
|
+
# boundary, and the activity clock the idle jobs read.
|
|
9
|
+
#
|
|
10
|
+
# The turn thread writes it (Engine#begin_turn, #release_turn); the idle
|
|
11
|
+
# scheduler, the bridge's request threads, plugin threads and the UIs
|
|
12
|
+
# read it. The lock is a leaf: nothing is called out while it is held,
|
|
13
|
+
# so any lock may be held around a call into it, and it is never held
|
|
14
|
+
# around anything else.
|
|
15
|
+
class TurnState
|
|
16
|
+
# @param clock [#call] monotonic seconds
|
|
17
|
+
def initialize(clock:)
|
|
18
|
+
@clock = clock
|
|
19
|
+
@lock = Monitor.new
|
|
20
|
+
@last_activity_at = clock.call
|
|
21
|
+
@activity_seq = 0
|
|
22
|
+
@running = false
|
|
23
|
+
@controller = nil
|
|
24
|
+
@sink = nil
|
|
25
|
+
@steers = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ── Activity clock ──────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
# Activity happened (user input, a finished turn): the clock moves to
|
|
31
|
+
# +now+ and the sequence advances, so the idle jobs re-arm.
|
|
32
|
+
# @param now [Float, nil] monotonic seconds; the clock's now by default
|
|
33
|
+
def record_activity(now = nil)
|
|
34
|
+
now = now ? now.to_f : @clock.call
|
|
35
|
+
@lock.synchronize do
|
|
36
|
+
@last_activity_at = now
|
|
37
|
+
@activity_seq += 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Start the idle window now without counting it as activity (the
|
|
42
|
+
# sequence stays): the Engine does once its plugins have loaded, so a
|
|
43
|
+
# slow plugin start doesn't shorten the first window.
|
|
44
|
+
def restart_clock!
|
|
45
|
+
now = @clock.call
|
|
46
|
+
@lock.synchronize { @last_activity_at = now }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Float] monotonic seconds of the last activity
|
|
50
|
+
def last_activity_at
|
|
51
|
+
@lock.synchronize { @last_activity_at }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Integer] how many times activity was recorded
|
|
55
|
+
def activity_seq
|
|
56
|
+
@lock.synchronize { @activity_seq }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ── The turn ────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
# @return [Boolean] whether a turn runs
|
|
62
|
+
def running?
|
|
63
|
+
@lock.synchronize { @running }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A turn begins: running, with its controller and sink, in one step.
|
|
67
|
+
def begin!(controller:, sink:)
|
|
68
|
+
@lock.synchronize do
|
|
69
|
+
@running = true
|
|
70
|
+
@controller = controller
|
|
71
|
+
@sink = sink
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The turn is over: not running, no controller, sink or steers.
|
|
76
|
+
# @return [Array<Hash>] the steers it never took ({text:, source:})
|
|
77
|
+
def finish!
|
|
78
|
+
@lock.synchronize do
|
|
79
|
+
@running = false
|
|
80
|
+
@controller = nil
|
|
81
|
+
@sink = nil
|
|
82
|
+
@steers.tap { @steers = [] }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [CancellationController, nil] the running turn's controller
|
|
87
|
+
def controller
|
|
88
|
+
@lock.synchronize { @controller }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Cancel the running turn, if any (outside the lock).
|
|
92
|
+
# @return [Boolean] whether a cancellation was triggered
|
|
93
|
+
def cancel!(reason)
|
|
94
|
+
ctrl = controller
|
|
95
|
+
return false unless ctrl
|
|
96
|
+
|
|
97
|
+
ctrl.cancel!(reason)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Array(Boolean, #call)] whether a turn runs, and its sink
|
|
101
|
+
def in_turn_sink
|
|
102
|
+
@lock.synchronize { [@running, @sink] }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ── Steers ──────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
# Queue +text+ for the running turn's next boundary.
|
|
108
|
+
# @return [Boolean] whether it was queued (false: blank, or no turn)
|
|
109
|
+
def steer(text, source:)
|
|
110
|
+
text = text.to_s.strip
|
|
111
|
+
return false if text.empty?
|
|
112
|
+
|
|
113
|
+
@lock.synchronize do
|
|
114
|
+
return false unless @running
|
|
115
|
+
|
|
116
|
+
@steers << { text: text, source: source.to_s }
|
|
117
|
+
end
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @return [Array<Hash>] the steers queued so far ({text:, source:}), taken
|
|
122
|
+
def take_steers
|
|
123
|
+
@lock.synchronize { @steers.tap { @steers = [] } }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
require_relative "muted_memories"
|
|
6
|
+
require_relative "tools/memory"
|
|
7
|
+
require_relative "tools/read"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
# The memories a session used: the ones its turns read (memory_read, or a
|
|
11
|
+
# read of a memories/*.md file) or preloaded. Deduped, in first-use
|
|
12
|
+
# order. Written by the turn thread (and the main thread setting the
|
|
13
|
+
# session), read by any (the bridge's snapshot, the UIs).
|
|
14
|
+
class UsedMemories
|
|
15
|
+
# The memory names a tool call reads, or nil when it reads none.
|
|
16
|
+
# @param call [Hash] {name:, content:}
|
|
17
|
+
# @return [Array<String>, String, nil]
|
|
18
|
+
def self.names_from_call(call)
|
|
19
|
+
return nil unless call.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
content = call[:content].to_s.strip
|
|
22
|
+
return nil if content.empty?
|
|
23
|
+
|
|
24
|
+
case call[:name].to_s
|
|
25
|
+
when Tools::MemoryRead::NAME
|
|
26
|
+
Tools::MemoryRead.parse_names(content).filter_map { |name| normalize(name) }
|
|
27
|
+
when Tools::Read::NAME
|
|
28
|
+
path = content.tr("\\", "/")
|
|
29
|
+
path.match?(%r{memories/.+\.md\z}) ? normalize(path) : nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A memory's name as the list keeps it: the basename without .md.
|
|
34
|
+
def self.normalize(raw)
|
|
35
|
+
value = raw.to_s.strip
|
|
36
|
+
return nil if value.empty?
|
|
37
|
+
|
|
38
|
+
base = File.basename(value, ".md").strip
|
|
39
|
+
base.empty? ? nil : base
|
|
40
|
+
end
|
|
41
|
+
private_class_method :normalize
|
|
42
|
+
|
|
43
|
+
# @param names [Array<String>] the names already used (a resumed session's)
|
|
44
|
+
def initialize(names = [])
|
|
45
|
+
@lock = Monitor.new
|
|
46
|
+
@names = []
|
|
47
|
+
add(names)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Array<String>] a copy of the list
|
|
51
|
+
def names
|
|
52
|
+
@lock.synchronize { @names.dup }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Add names, skipping blanks and ones already there.
|
|
56
|
+
def add(names)
|
|
57
|
+
@lock.synchronize do
|
|
58
|
+
Array(names).each do |name|
|
|
59
|
+
value = name.to_s.strip
|
|
60
|
+
@names << value unless value.empty? || @names.include?(value)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Add the names a session already used.
|
|
67
|
+
def absorb(session)
|
|
68
|
+
return self unless session
|
|
69
|
+
|
|
70
|
+
add(Array(session.used_memory_names))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A memory read as it starts (:tool_call_started): its names join the
|
|
74
|
+
# list. A refused read of a muted memory is not a use of it.
|
|
75
|
+
# @param muted [Array<String>] the session's muted names (normalized)
|
|
76
|
+
# @return [Array<String>, nil] the names this call read, nil for any other event
|
|
77
|
+
def capture(event, muted:)
|
|
78
|
+
return nil unless event.is_a?(Hash) && event[:type] == :tool_call_started
|
|
79
|
+
|
|
80
|
+
call = event[:call].is_a?(Hash) ? event[:call] : {}
|
|
81
|
+
names = Array(self.class.names_from_call(call)).reject { |name| MutedMemories.muted?(name, muted) }
|
|
82
|
+
return nil if names.empty?
|
|
83
|
+
|
|
84
|
+
add(names)
|
|
85
|
+
names
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -60,10 +60,10 @@ module Samagotchi
|
|
|
60
60
|
|
|
61
61
|
def self.native(target, entry, profile, trust_config:)
|
|
62
62
|
client = target.client
|
|
63
|
-
transport = client.
|
|
63
|
+
transport = client.transport.name
|
|
64
64
|
return no("#{transport} hosts take no images (only llama.cpp does)") unless transport == :llama_cpp
|
|
65
65
|
|
|
66
|
-
props = client.
|
|
66
|
+
props = client.server_props(model: target.bare_model)
|
|
67
67
|
return no("can't reach /props for the media marker") unless props&.answered?
|
|
68
68
|
|
|
69
69
|
body = props.body.is_a?(Hash) ? props.body : {}
|
|
@@ -83,14 +83,14 @@ module Samagotchi
|
|
|
83
83
|
|
|
84
84
|
def self.chat(target, entry, adapter)
|
|
85
85
|
unless entry.remote?
|
|
86
|
-
props = entry.client.
|
|
86
|
+
props = entry.client.server_props(model: target.bare_model)
|
|
87
87
|
if props&.answered? && props.body.is_a?(Hash) && props.body.key?("modalities")
|
|
88
88
|
vision = props.body.dig("modalities", "vision") == true
|
|
89
89
|
return vision ? Answer.new(value: true, reason: nil) : no("the server has no vision model loaded (start llama.cpp with --mmproj)")
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
listed = adapter
|
|
93
|
+
listed = adapter&.image_input(model: target.bare_model)
|
|
94
94
|
return Answer.new(value: true, reason: nil) if listed == true
|
|
95
95
|
return no("host #{entry.name} lists #{target.bare_model} as text-only") if listed == false
|
|
96
96
|
|
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "../session_manager"
|
|
|
16
16
|
require_relative "../session_commands"
|
|
17
17
|
require_relative "../steer"
|
|
18
18
|
require_relative "../host_registry"
|
|
19
|
+
require_relative "../model_catalog"
|
|
19
20
|
require_relative "../model_profile"
|
|
20
21
|
require_relative "../project_scope"
|
|
21
22
|
require_relative "../prompt_history"
|
|
@@ -335,12 +336,10 @@ module Samagotchi
|
|
|
335
336
|
|
|
336
337
|
root = dir && ProjectScope.root_for(dir)
|
|
337
338
|
# Lazy retention sweep (once per 24h)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
nil
|
|
343
|
-
end
|
|
339
|
+
begin
|
|
340
|
+
@manager.retention_sweep_if_due(state_dir: @state_dir)
|
|
341
|
+
rescue StandardError
|
|
342
|
+
nil
|
|
344
343
|
end
|
|
345
344
|
limit = sanitize_limit(req.params["limit"])
|
|
346
345
|
offset = sanitize_offset(req.params["offset"])
|
|
@@ -417,19 +416,11 @@ module Samagotchi
|
|
|
417
416
|
warnings << "the hosts are still listing their models"
|
|
418
417
|
else
|
|
419
418
|
default_host = registry.default_entry&.name
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
next
|
|
426
|
-
end
|
|
427
|
-
Array(data[:models]).each do |info|
|
|
428
|
-
id = info.id.to_s
|
|
429
|
-
next if id.strip.empty? || id.end_with?(":batch")
|
|
430
|
-
|
|
431
|
-
models << { name: host == default_host ? id : "#{host}:#{id}", host: host, id: id }
|
|
432
|
-
end
|
|
419
|
+
listing = ModelCatalog.listing(results, registry: registry)
|
|
420
|
+
warnings.concat(listing.warnings)
|
|
421
|
+
# the web's own spelling, kept as it was: bare on the default host
|
|
422
|
+
listing.rows.each do |row|
|
|
423
|
+
models << { name: row.host == default_host ? row.id : "#{row.host}:#{row.id}", host: row.host, id: row.id }
|
|
433
424
|
end
|
|
434
425
|
end
|
|
435
426
|
rescue StandardError => e
|
|
@@ -552,7 +543,7 @@ module Samagotchi
|
|
|
552
543
|
# saved can't be answered (its turn is gone).
|
|
553
544
|
pending = if turn_snapshot
|
|
554
545
|
current_turn && current_turn["pending_question"]
|
|
555
|
-
elsif owner
|
|
546
|
+
elsif owner
|
|
556
547
|
session.pending_question
|
|
557
548
|
end
|
|
558
549
|
# A /model in the worker changes it before the file catches up.
|
|
@@ -581,7 +572,7 @@ module Samagotchi
|
|
|
581
572
|
session: session_json,
|
|
582
573
|
history: read_history(id),
|
|
583
574
|
messages: messages_for_display(raw_messages, parts: req.params["parts"] == "1",
|
|
584
|
-
cwd: session.
|
|
575
|
+
cwd: session.working_directory),
|
|
585
576
|
failed_turn: failed_turn_for(session, raw_messages),
|
|
586
577
|
current_turn: current_turn,
|
|
587
578
|
queued: turn_snapshot ? Array(turn_snapshot["queued"]) : [],
|
|
@@ -677,7 +668,7 @@ module Samagotchi
|
|
|
677
668
|
line = body["line"].to_s.strip
|
|
678
669
|
return error_response(400, "missing_fields", "line is required") if line.empty?
|
|
679
670
|
|
|
680
|
-
@manager.resume_session(id, state_dir: @state_dir)
|
|
671
|
+
@manager.resume_session(id, state_dir: @state_dir)
|
|
681
672
|
# card: a card's action; its command events say so (no echo).
|
|
682
673
|
request = ->(client) { client.post_command(line: line, client_id: body["client_id"], card: body["card"] == true) }
|
|
683
674
|
relay(id, live_bridge_client(id), request, what: "the command was not run", cant: "run commands") do |reply|
|
|
@@ -852,8 +843,6 @@ module Samagotchi
|
|
|
852
843
|
|
|
853
844
|
# The process holding the session (an OwnerLock::Owner), or nil. A worker always runs a Bridge; a TUI (plain `chi`) doesn't share.
|
|
854
845
|
def session_owner(id)
|
|
855
|
-
return nil unless @manager.respond_to?(:session_owner)
|
|
856
|
-
|
|
857
846
|
@manager.session_owner(id, state_dir: @state_dir)
|
|
858
847
|
rescue StandardError
|
|
859
848
|
nil
|
|
@@ -1125,16 +1114,8 @@ module Samagotchi
|
|
|
1125
1114
|
end
|
|
1126
1115
|
end
|
|
1127
1116
|
|
|
1128
|
-
# status is turn state (idle/running): SessionSummary.displayed_status
|
|
1129
|
-
# unless the manager can't tell who owns a session (then the file's
|
|
1130
|
-
# word stands).
|
|
1117
|
+
# status is turn state (idle/running): SessionSummary.displayed_status.
|
|
1131
1118
|
def displayed_status(session, snapshot = nil, owner: session_owner(session.id))
|
|
1132
|
-
unless @manager.respond_to?(:session_owner)
|
|
1133
|
-
return snapshot["status"] if snapshot.is_a?(Hash) && snapshot["status"]
|
|
1134
|
-
|
|
1135
|
-
return session.status
|
|
1136
|
-
end
|
|
1137
|
-
|
|
1138
1119
|
SessionSummary.displayed_status(session, snapshot, owner: owner)
|
|
1139
1120
|
end
|
|
1140
1121
|
|
|
@@ -197,8 +197,6 @@ module Samagotchi
|
|
|
197
197
|
end
|
|
198
198
|
|
|
199
199
|
def sweep
|
|
200
|
-
return unless @manager.respond_to?(:retention_sweep_if_due)
|
|
201
|
-
|
|
202
200
|
@manager.retention_sweep_if_due(state_dir: @state_dir)
|
|
203
201
|
rescue StandardError
|
|
204
202
|
nil
|
|
@@ -288,8 +286,6 @@ module Samagotchi
|
|
|
288
286
|
end
|
|
289
287
|
|
|
290
288
|
def owner_of(id)
|
|
291
|
-
return nil unless @manager.respond_to?(:session_owner)
|
|
292
|
-
|
|
293
289
|
@manager.session_owner(id, state_dir: @state_dir)
|
|
294
290
|
rescue StandardError
|
|
295
291
|
nil
|
|
@@ -28,7 +28,7 @@ module Samagotchi
|
|
|
28
28
|
# looks its project up)
|
|
29
29
|
# @return [Hash] symbol keys
|
|
30
30
|
def build(session, owner:, session_dir:, status: displayed_status(session, owner: owner), root_cache: nil)
|
|
31
|
-
used =
|
|
31
|
+
used = Array(session.used_memory_names)
|
|
32
32
|
up = bridge_up?(session_dir, owner)
|
|
33
33
|
{
|
|
34
34
|
id: session.id,
|
|
@@ -43,10 +43,10 @@ module Samagotchi
|
|
|
43
43
|
test_run: !!session.test_run,
|
|
44
44
|
used_memory_names: used,
|
|
45
45
|
# The session's --memory and --mute lists (info-bar tooltip).
|
|
46
|
-
preloaded_memory_names:
|
|
47
|
-
muted_memory_names:
|
|
46
|
+
preloaded_memory_names: Array(session.preloaded_memory_names),
|
|
47
|
+
muted_memory_names: Array(session.muted_memory_names),
|
|
48
48
|
# The session that delegated this one (the `delegate` tool), else nil.
|
|
49
|
-
parent_id: session.
|
|
49
|
+
parent_id: session.parent_id,
|
|
50
50
|
first_preview: first_preview_for(session),
|
|
51
51
|
owner: owner&.kind,
|
|
52
52
|
# The saved recap's first sentence, for the session card.
|
|
@@ -64,14 +64,14 @@ module Samagotchi
|
|
|
64
64
|
# ended. A question saved by a worker that died is not open.
|
|
65
65
|
pending_question: owner ? pending_question_for(session) : nil,
|
|
66
66
|
pending_card: up ? Bridge::PendingCard.read(session_dir) : nil,
|
|
67
|
-
last_turn: session.
|
|
67
|
+
last_turn: session.last_turn
|
|
68
68
|
}
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
# The open question as {id:, kind:} ("approval" for a guardrail's,
|
|
72
72
|
# else "question"); nil when there is none. The text stays out.
|
|
73
73
|
def pending_question_for(session)
|
|
74
|
-
pending = session.
|
|
74
|
+
pending = session.pending_question
|
|
75
75
|
return nil unless pending.is_a?(Hash) && pending[:id]
|
|
76
76
|
|
|
77
77
|
{ id: pending[:id], kind: pending[:kind].to_s == "approval" ? "approval" : "question" }
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -475,7 +475,7 @@ module Samagotchi
|
|
|
475
475
|
refuse_queued_commands
|
|
476
476
|
end
|
|
477
477
|
yield result, error
|
|
478
|
-
response = result
|
|
478
|
+
response = result&.output
|
|
479
479
|
SessionInbox.write_output(@session_dir, response) unless response.nil? || response.strip.empty?
|
|
480
480
|
save_session
|
|
481
481
|
end
|