samagotchi 0.9.0 → 0.10.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 +56 -1
- data/docs/architecture.md +24 -9
- data/docs/cli.md +4 -3
- data/docs/guardrails.md +3 -2
- data/docs/hooks.md +44 -7
- data/docs/internals/gemma4-contract.md +1 -0
- data/docs/plugins.md +34 -12
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bootstrap_command.rb +29 -29
- data/lib/samagotchi/bridge/card_store.rb +4 -4
- data/lib/samagotchi/bridge/pending_card.rb +2 -2
- data/lib/samagotchi/bridge/sse_writer.rb +22 -28
- data/lib/samagotchi/bridge/turn_accumulator.rb +92 -5
- data/lib/samagotchi/bridge.rb +59 -112
- data/lib/samagotchi/bridge_client.rb +11 -28
- data/lib/samagotchi/bundle_command.rb +158 -212
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +3 -4
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +7 -3
- 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 +10 -10
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -4
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +4 -6
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +0 -1
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cli/command.rb +51 -0
- data/lib/samagotchi/cli/flags.rb +116 -0
- data/lib/samagotchi/config.rb +70 -55
- data/lib/samagotchi/config_text_edit.rb +107 -0
- data/lib/samagotchi/desktop_command.rb +24 -18
- data/lib/samagotchi/edit_preview.rb +2 -2
- data/lib/samagotchi/engine.rb +20 -75
- data/lib/samagotchi/events.rb +23 -0
- data/lib/samagotchi/guardrail_wiring.rb +2 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +13 -30
- data/lib/samagotchi/hooks/loader.rb +23 -33
- data/lib/samagotchi/hooks/registry.rb +41 -0
- data/lib/samagotchi/hooks.rb +0 -3
- data/lib/samagotchi/host_registry.rb +15 -0
- data/lib/samagotchi/idle_reminders.rb +0 -7
- data/lib/samagotchi/image_store.rb +22 -0
- data/lib/samagotchi/kernel_loop.rb +12 -16
- data/lib/samagotchi/live_versions.rb +2 -13
- data/lib/samagotchi/llm/native_tool_normalizer.rb +10 -227
- data/lib/samagotchi/memory_bundle/builder.rb +1 -14
- data/lib/samagotchi/memory_bundle/index_updater.rb +2 -28
- data/lib/samagotchi/memory_bundle/installer.rb +1 -27
- data/lib/samagotchi/memory_bundle/provenance.rb +2 -6
- data/lib/samagotchi/memory_bundle/status.rb +1 -14
- data/lib/samagotchi/memory_bundle/system_bundle.rb +2 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +1 -24
- data/lib/samagotchi/memory_paths.rb +10 -0
- data/lib/samagotchi/note_command.rb +23 -37
- data/lib/samagotchi/owner_lock.rb +14 -3
- data/lib/samagotchi/plugin/api.rb +11 -17
- data/lib/samagotchi/plugin/context.rb +70 -15
- data/lib/samagotchi/plugin/loader.rb +2 -3
- data/lib/samagotchi/sampling_settings.rb +3 -5
- data/lib/samagotchi/self_report.rb +6 -9
- data/lib/samagotchi/send_command.rb +33 -52
- data/lib/samagotchi/session.rb +88 -92
- data/lib/samagotchi/session_archive_command.rb +8 -14
- data/lib/samagotchi/session_commands.rb +3 -21
- data/lib/samagotchi/session_delete_command.rb +15 -29
- data/lib/samagotchi/session_manager.rb +27 -13
- data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -80
- data/lib/samagotchi/terminal_ui/attached_view.rb +1 -10
- data/lib/samagotchi/terminal_ui/between_turns.rb +133 -0
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +51 -1
- data/lib/samagotchi/terminal_ui/status_row.rb +32 -0
- data/lib/samagotchi/terminal_ui/thinking_line.rb +3 -3
- data/lib/samagotchi/terminal_ui.rb +156 -234
- data/lib/samagotchi/thinking.rb +2 -3
- data/lib/samagotchi/thought_stream_splitter.rb +10 -9
- data/lib/samagotchi/tool_call_parser.rb +56 -292
- data/lib/samagotchi/tools/builtin_calls.rb +146 -0
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/edit.rb +21 -29
- data/lib/samagotchi/tools/memory.rb +1 -11
- data/lib/samagotchi/tools/send_note.rb +1 -1
- data/lib/samagotchi/update_command.rb +32 -22
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -5
- data/lib/samagotchi/web/app.rb +89 -151
- data/lib/samagotchi/web/public/activity.js +1 -11
- data/lib/samagotchi/web/public/annotate_bar.js +120 -0
- data/lib/samagotchi/web/public/app.js +158 -785
- data/lib/samagotchi/web/public/chunk_router.js +7 -6
- data/lib/samagotchi/web/public/command_list.js +99 -0
- data/lib/samagotchi/web/public/data.js +22 -66
- data/lib/samagotchi/web/public/format.js +0 -23
- data/lib/samagotchi/web/public/index.html +0 -2
- data/lib/samagotchi/web/public/model_pick.js +0 -7
- data/lib/samagotchi/web/public/notifications.js +169 -0
- data/lib/samagotchi/web/public/question_cards.js +352 -0
- data/lib/samagotchi/web/public/session_state.js +34 -0
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/turn_events.js +11 -2
- data/lib/samagotchi/web/public/turn_model.js +0 -14
- data/lib/samagotchi/web/session_hub.rb +6 -4
- data/lib/samagotchi/web/session_summary.rb +5 -3
- data/lib/samagotchi/worker.rb +59 -53
- data/lib/samagotchi/worker_sidecar.rb +27 -1
- metadata +12 -1
|
@@ -141,8 +141,8 @@ module Samagotchi
|
|
|
141
141
|
# Run the conversation loop and return the final model response plus
|
|
142
142
|
# resumable conversation state when execution stops at max_iterations.
|
|
143
143
|
#
|
|
144
|
-
# @param messages [Array<Hash
|
|
145
|
-
#
|
|
144
|
+
# @param messages [Array<Hash>] conversation so far ({role:, content:});
|
|
145
|
+
# a stopped run resumes from its Result#conversation
|
|
146
146
|
# @param max_iterations [Integer] safety cap on tool-call rounds
|
|
147
147
|
# @param on_stream_event [Proc, nil] optional callback for generation events
|
|
148
148
|
# @param cancel_controller [CancellationController, nil] optional cancellation source
|
|
@@ -170,7 +170,6 @@ module Samagotchi
|
|
|
170
170
|
empty_retries = 0
|
|
171
171
|
empty_retry_limit = EmptyAnswerRetry.limit
|
|
172
172
|
@retry_generation = false
|
|
173
|
-
stream_splitter = ThoughtStreamSplitter.for_profile(@profile)
|
|
174
173
|
# Qwen with thinking off: an empty thought after the cue, so the model
|
|
175
174
|
# answers at once. Kept in the turn's model messages, so each tool-loop
|
|
176
175
|
# prompt starts with what the server already has cached.
|
|
@@ -192,6 +191,9 @@ module Samagotchi
|
|
|
192
191
|
conversation << line
|
|
193
192
|
prompt, images = Prompt.format_with_images(conversation, profile: @profile, vision: @vision, prefill: prefill)
|
|
194
193
|
end
|
|
194
|
+
# Each generation splits its own stream: one that ended inside a
|
|
195
|
+
# thought or a tool call doesn't leave the next one "inside" it.
|
|
196
|
+
stream_splitter = ThoughtStreamSplitter.for_profile(@profile)
|
|
195
197
|
emit_stream_event(
|
|
196
198
|
on_stream_event,
|
|
197
199
|
type: :generation_started,
|
|
@@ -235,6 +237,7 @@ module Samagotchi
|
|
|
235
237
|
type: :generation_chunk,
|
|
236
238
|
iteration: iteration_index + 1,
|
|
237
239
|
content: chunk[:content],
|
|
240
|
+
text: split[:text],
|
|
238
241
|
thinking: split[:thinking],
|
|
239
242
|
payload: chunk[:payload]
|
|
240
243
|
)
|
|
@@ -263,9 +266,8 @@ module Samagotchi
|
|
|
263
266
|
""
|
|
264
267
|
end
|
|
265
268
|
if cut
|
|
266
|
-
# The cut stream
|
|
267
|
-
#
|
|
268
|
-
stream_splitter = ThoughtStreamSplitter.for_profile(@profile)
|
|
269
|
+
# The cut stream's visible text goes with it: the buffer as it was
|
|
270
|
+
# before (the next generation gets a fresh splitter).
|
|
269
271
|
partial_assistant_buffer.slice!(buffer_mark..)
|
|
270
272
|
emit_stream_event(on_stream_event, type: :generation_completed, iteration: iteration_index + 1,
|
|
271
273
|
content_length: 0, thinking_chars: streamed_thinking,
|
|
@@ -660,8 +662,8 @@ module Samagotchi
|
|
|
660
662
|
private
|
|
661
663
|
|
|
662
664
|
# The generation's thinking: what the stream split into the thinking
|
|
663
|
-
# lane
|
|
664
|
-
#
|
|
665
|
+
# lane, else what the profile's thought blocks hold (an empty thought, a
|
|
666
|
+
# bare Gemma <|think|> cue, a response that wasn't streamed).
|
|
665
667
|
def thinking_chars(response, streamed)
|
|
666
668
|
return streamed if streamed.positive?
|
|
667
669
|
|
|
@@ -687,14 +689,8 @@ module Samagotchi
|
|
|
687
689
|
end
|
|
688
690
|
end
|
|
689
691
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
duplicate_conversation(messages.conversation)
|
|
693
|
-
else
|
|
694
|
-
# Standard multi-turn compliance: never pass prior raw thought blocks.
|
|
695
|
-
sanitize_history(messages)
|
|
696
|
-
end
|
|
697
|
-
end
|
|
692
|
+
# Standard multi-turn compliance: never pass prior raw thought blocks.
|
|
693
|
+
def prepare_conversation(messages) = sanitize_history(messages)
|
|
698
694
|
|
|
699
695
|
def duplicate_conversation(messages)
|
|
700
696
|
messages.map(&:dup)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "socket"
|
|
4
3
|
require_relative "session"
|
|
5
4
|
require_relative "version"
|
|
6
5
|
require_relative "worker_sidecar"
|
|
@@ -12,7 +11,6 @@ module Samagotchi
|
|
|
12
11
|
# older one names none) and a `chi web` on its port (/api/info). Read-only:
|
|
13
12
|
# a dead worker's sidecar is left for the next client to clean up.
|
|
14
13
|
module LiveVersions
|
|
15
|
-
PROBE_TIMEOUT = 0.2
|
|
16
14
|
WEB_TIMEOUT = 0.5
|
|
17
15
|
|
|
18
16
|
# version is nil for a sidecar written before sidecars carried one.
|
|
@@ -24,8 +22,8 @@ module Samagotchi
|
|
|
24
22
|
def workers(state_dir: Session.default_state_dir)
|
|
25
23
|
Dir[File.join(state_dir, "*", WorkerSidecar::FILE)].sort.filter_map do |path|
|
|
26
24
|
session_dir = File.dirname(path)
|
|
27
|
-
sidecar = WorkerSidecar.
|
|
28
|
-
next unless sidecar
|
|
25
|
+
sidecar = WorkerSidecar.live(session_dir, unlink: false)
|
|
26
|
+
next unless sidecar
|
|
29
27
|
|
|
30
28
|
Worker.new(session_id: File.basename(session_dir), version: sidecar.version)
|
|
31
29
|
end
|
|
@@ -48,14 +46,5 @@ module Samagotchi
|
|
|
48
46
|
info = Web::InfoProbe.call(host, port, timeout: timeout)
|
|
49
47
|
info.is_a?(Hash) ? info : nil
|
|
50
48
|
end
|
|
51
|
-
|
|
52
|
-
def listening?(port, host: "127.0.0.1")
|
|
53
|
-
return false unless port.positive?
|
|
54
|
-
|
|
55
|
-
Socket.tcp(host, port, connect_timeout: PROBE_TIMEOUT).close
|
|
56
|
-
true
|
|
57
|
-
rescue StandardError
|
|
58
|
-
false
|
|
59
|
-
end
|
|
60
49
|
end
|
|
61
50
|
end
|
|
@@ -1,217 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require_relative "../tools/
|
|
5
|
-
require_relative "../tools/read"
|
|
6
|
-
require_relative "../tools/write"
|
|
7
|
-
require_relative "../tools/memory"
|
|
8
|
-
require_relative "../tools/edit"
|
|
9
|
-
require_relative "../tools/task_create"
|
|
10
|
-
require_relative "../tools/task_get"
|
|
11
|
-
require_relative "../tools/task_list"
|
|
12
|
-
require_relative "../tools/task_stop"
|
|
13
|
-
require_relative "../tools/task_wait"
|
|
14
|
-
require_relative "../tools/web_fetch"
|
|
15
|
-
require_relative "../tools/register_reminder"
|
|
16
|
-
require_relative "../tools/cancel_reminder"
|
|
17
|
-
require_relative "../tools/list_reminders"
|
|
18
|
-
require_relative "../tools/list_sessions"
|
|
19
|
-
require_relative "../tools/send_note"
|
|
20
|
-
require_relative "../tools/delegate"
|
|
21
|
-
require_relative "../tools/delegate_result"
|
|
22
|
-
require_relative "../tools/ask_user_question"
|
|
4
|
+
require_relative "../tools/builtin_calls"
|
|
23
5
|
|
|
24
6
|
module Samagotchi
|
|
25
7
|
module LLM
|
|
26
8
|
# Bridges a native tool call (an LLM::ToolCall from the chat adapter; any
|
|
27
|
-
# object with #name and #arguments) into samagotchi's internal tool-call
|
|
28
|
-
#
|
|
9
|
+
# object with #name and #arguments) into samagotchi's internal tool-call
|
|
10
|
+
# shape: its arguments read as a Hash, then Tools::BuiltinCalls builds
|
|
11
|
+
# the call, as it does for the Gemma and Qwen parsers.
|
|
29
12
|
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# Hash), while the internal `content` is a per-tool serialized text blob
|
|
34
|
-
# (e.g. `Edit` wants "<old>…</old><new>…</new>"; `Read` wants a `path`;
|
|
35
|
-
# `Execute` wants the command). Rather than re-architect every tool to accept
|
|
36
|
-
# structured args (huge blast radius), we rebuild the exact blob per tool so
|
|
37
|
-
# `dispatch` and every tool stay unchanged.
|
|
38
|
-
#
|
|
39
|
-
# Unknown tool names pass through untouched so `dispatch` renders its standard
|
|
40
|
-
# "unknown tool … available: …" error — the loop feeds that back like any tool
|
|
41
|
-
# result.
|
|
42
|
-
#
|
|
43
|
-
# This is the Phase-3 crux from Spike #4: the native→internal mapping is
|
|
44
|
-
# independent of how tools were driven in the request body.
|
|
13
|
+
# Unknown tool names pass through with their arguments on args:, so
|
|
14
|
+
# `dispatch` renders its standard "unknown tool … available: …" error —
|
|
15
|
+
# the loop feeds that back like any tool result.
|
|
45
16
|
class NativeToolNormalizer
|
|
46
|
-
# Map each known tool NAME to a proc(args => internal-call-hash).
|
|
47
|
-
# Each proc receives the tool call's arguments (`.arguments` is a Hash of string→value)
|
|
48
|
-
# and returns the internal dispatch shape with every expected key present
|
|
49
|
-
# (missing values are nil).
|
|
50
|
-
#
|
|
51
|
-
# Args come back as strings from the wire (e.g. "start_line" => "1"); tools
|
|
52
|
-
# that read them do `.to_s.strip`, so we leave them as-is here and let the
|
|
53
|
-
# dispatch site coerce.
|
|
54
|
-
MAP = {
|
|
55
|
-
Tools::Execute::NAME => lambda { |args|
|
|
56
|
-
{
|
|
57
|
-
name: Tools::Execute::NAME,
|
|
58
|
-
content: args["command"].to_s,
|
|
59
|
-
path: nil, scope: nil,
|
|
60
|
-
start_line: nil, end_line: nil,
|
|
61
|
-
cwd: args["cwd"], env: args["env"]
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
Tools::Read::NAME => lambda { |args|
|
|
65
|
-
{
|
|
66
|
-
name: Tools::Read::NAME,
|
|
67
|
-
content: args["path"].to_s,
|
|
68
|
-
path: nil, scope: nil,
|
|
69
|
-
start_line: args["start_line"], end_line: args["end_line"],
|
|
70
|
-
cwd: nil, env: nil, description: nil,
|
|
71
|
-
args: args.is_a?(Hash) ? args : {}
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
Tools::Write::NAME => lambda { |args|
|
|
75
|
-
{
|
|
76
|
-
name: Tools::Write::NAME,
|
|
77
|
-
content: args["content"] || args["text"],
|
|
78
|
-
path: args["path"], scope: nil,
|
|
79
|
-
start_line: nil, end_line: nil,
|
|
80
|
-
cwd: nil, env: nil, description: nil
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
Tools::MemoryRead::NAME => lambda { |args|
|
|
84
|
-
{
|
|
85
|
-
name: Tools::MemoryRead::NAME,
|
|
86
|
-
content: args["name"].to_s,
|
|
87
|
-
path: nil, scope: args["scope"],
|
|
88
|
-
start_line: nil, end_line: nil,
|
|
89
|
-
cwd: nil, env: nil, description: nil
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
|
-
Tools::MemoryWrite::NAME => lambda { |args|
|
|
93
|
-
{
|
|
94
|
-
name: Tools::MemoryWrite::NAME,
|
|
95
|
-
content: args["content"] || args["text"] || args["body"],
|
|
96
|
-
path: args["name"], scope: args["scope"],
|
|
97
|
-
description: args["description"],
|
|
98
|
-
current_model_only: args["current_model_only"],
|
|
99
|
-
start_line: nil, end_line: nil,
|
|
100
|
-
cwd: nil, env: nil
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
Tools::Edit::NAME => lambda { |args|
|
|
104
|
-
old_text = args["old_text"] || args["old"]
|
|
105
|
-
new_text = args["new_text"] || args["new"]
|
|
106
|
-
{
|
|
107
|
-
name: Tools::Edit::NAME,
|
|
108
|
-
content: "<old>#{old_text}</old><new>#{new_text}</new>",
|
|
109
|
-
path: args["path"],
|
|
110
|
-
start_line: args["start_line"], end_line: args["end_line"],
|
|
111
|
-
scope: nil, cwd: nil, env: nil, description: nil
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
Tools::TaskCreate::NAME => lambda { |args|
|
|
115
|
-
{
|
|
116
|
-
name: Tools::TaskCreate::NAME,
|
|
117
|
-
content: args["command"].to_s,
|
|
118
|
-
path: nil, scope: nil,
|
|
119
|
-
start_line: nil, end_line: nil,
|
|
120
|
-
cwd: args["cwd"], env: args["env"], description: nil
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
Tools::TaskGet::NAME => lambda { |args|
|
|
124
|
-
{
|
|
125
|
-
name: Tools::TaskGet::NAME,
|
|
126
|
-
content: (args["id"] || args["task_id"]).to_s,
|
|
127
|
-
path: nil, scope: nil,
|
|
128
|
-
start_line: nil, end_line: nil,
|
|
129
|
-
cwd: nil, env: nil, description: nil
|
|
130
|
-
}
|
|
131
|
-
},
|
|
132
|
-
Tools::TaskStop::NAME => lambda { |args|
|
|
133
|
-
{
|
|
134
|
-
name: Tools::TaskStop::NAME,
|
|
135
|
-
content: (args["id"] || args["task_id"]).to_s,
|
|
136
|
-
path: nil, scope: nil,
|
|
137
|
-
start_line: nil, end_line: nil,
|
|
138
|
-
cwd: nil, env: nil, description: nil
|
|
139
|
-
}
|
|
140
|
-
},
|
|
141
|
-
Tools::TaskWait::NAME => lambda { |args|
|
|
142
|
-
{
|
|
143
|
-
name: Tools::TaskWait::NAME,
|
|
144
|
-
content: (args["id"] || args["task_id"]).to_s,
|
|
145
|
-
path: nil, scope: nil,
|
|
146
|
-
start_line: nil, end_line: nil,
|
|
147
|
-
cwd: nil, env: nil,
|
|
148
|
-
timeout: args["timeout"], tail_lines: args["tail_lines"], done_pattern: args["done_pattern"],
|
|
149
|
-
description: nil
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
Tools::TaskList::NAME => lambda { |_args|
|
|
153
|
-
{
|
|
154
|
-
name: Tools::TaskList::NAME,
|
|
155
|
-
content: "", path: nil, scope: nil,
|
|
156
|
-
start_line: nil, end_line: nil,
|
|
157
|
-
cwd: nil, env: nil, description: nil
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
Tools::WebFetch::NAME => lambda { |args|
|
|
161
|
-
{
|
|
162
|
-
name: Tools::WebFetch::NAME,
|
|
163
|
-
content: args["url"].to_s,
|
|
164
|
-
path: nil, scope: nil,
|
|
165
|
-
start_line: nil, end_line: nil,
|
|
166
|
-
cwd: nil, env: nil, description: nil
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
Tools::RegisterReminder::NAME => lambda { |args|
|
|
170
|
-
{
|
|
171
|
-
name: Tools::RegisterReminder::NAME,
|
|
172
|
-
content: args["name"].to_s,
|
|
173
|
-
path: nil, scope: nil,
|
|
174
|
-
description: args["description"],
|
|
175
|
-
interval_minutes: args["interval_minutes"]
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
Tools::CancelReminder::NAME => lambda { |args|
|
|
179
|
-
{ name: Tools::CancelReminder::NAME, content: args["name"].to_s, path: nil, scope: nil }
|
|
180
|
-
},
|
|
181
|
-
Tools::ListReminders::NAME => lambda { |_args|
|
|
182
|
-
{ name: Tools::ListReminders::NAME, content: "", path: nil, scope: nil }
|
|
183
|
-
},
|
|
184
|
-
Tools::ListSessions::NAME => lambda { |args|
|
|
185
|
-
{ name: Tools::ListSessions::NAME, content: "", path: nil, scope: nil, cwd: args["cwd"] }
|
|
186
|
-
},
|
|
187
|
-
Tools::SendNote::NAME => lambda { |args|
|
|
188
|
-
{ name: Tools::SendNote::NAME, content: args["text"].to_s, path: nil, scope: nil, session: args["session"].to_s }
|
|
189
|
-
},
|
|
190
|
-
Tools::Delegate::NAME => lambda { |args|
|
|
191
|
-
{ name: Tools::Delegate::NAME, content: args["task"].to_s, path: nil, scope: nil,
|
|
192
|
-
model: args["model"], session: args["session"], wait: args["wait"], timeout: args["timeout"] }
|
|
193
|
-
},
|
|
194
|
-
Tools::DelegateResult::NAME => lambda { |args|
|
|
195
|
-
{ name: Tools::DelegateResult::NAME, content: "", path: nil, scope: nil,
|
|
196
|
-
session: args["session"], timeout: args["timeout"] }
|
|
197
|
-
},
|
|
198
|
-
Tools::AskUserQuestion::NAME => lambda { |args|
|
|
199
|
-
raw_opts = args["options"]
|
|
200
|
-
norm_opts = Samagotchi::Tools::AskUserQuestion.normalize_options_lenient(raw_opts)
|
|
201
|
-
norm_opts = raw_opts if norm_opts.empty? && raw_opts.is_a?(Array)
|
|
202
|
-
{
|
|
203
|
-
name: Tools::AskUserQuestion::NAME,
|
|
204
|
-
content: args["question"].to_s,
|
|
205
|
-
path: nil, scope: nil,
|
|
206
|
-
question: args["question"].to_s,
|
|
207
|
-
options: norm_opts.empty? ? raw_opts : norm_opts,
|
|
208
|
-
header: args["header"],
|
|
209
|
-
multi_select: args["multi_select"],
|
|
210
|
-
allow_freeform: args["allow_freeform"]
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}.freeze
|
|
214
|
-
|
|
215
17
|
class << self
|
|
216
18
|
# Map a single native tool call (LLM::ToolCall) to the internal call hash.
|
|
217
19
|
#
|
|
@@ -221,12 +23,7 @@ module Samagotchi
|
|
|
221
23
|
def normalize(call)
|
|
222
24
|
return nil if call.nil?
|
|
223
25
|
|
|
224
|
-
|
|
225
|
-
args = args_for(call)
|
|
226
|
-
proc = MAP[name]
|
|
227
|
-
return passthrough(name, args) unless proc
|
|
228
|
-
|
|
229
|
-
proc.call(args).merge(name: name)
|
|
26
|
+
Tools::BuiltinCalls.build(call.name.to_s, args_for(call))
|
|
230
27
|
end
|
|
231
28
|
|
|
232
29
|
# Map an Array of tool calls to internal call hashes (nils dropped).
|
|
@@ -252,25 +49,11 @@ module Samagotchi
|
|
|
252
49
|
end
|
|
253
50
|
|
|
254
51
|
def safe_json_parse(str)
|
|
255
|
-
JSON.parse(str)
|
|
52
|
+
parsed = JSON.parse(str)
|
|
53
|
+
parsed.is_a?(Hash) ? parsed : { "__raw__" => str }
|
|
256
54
|
rescue JSON::ParserError
|
|
257
55
|
{ "__raw__" => str }
|
|
258
56
|
end
|
|
259
|
-
|
|
260
|
-
# A registry (plugin) tool, or an unknown one: the arguments as args:
|
|
261
|
-
# (typed by the schema at dispatch), plus enough for `dispatch` to
|
|
262
|
-
# render the standard "unknown tool" error. `content` is the joined
|
|
263
|
-
# argument values (harmless; the unknown-tool branch ignores it).
|
|
264
|
-
def passthrough(name, args)
|
|
265
|
-
{
|
|
266
|
-
name: name,
|
|
267
|
-
content: args.is_a?(Hash) ? args.values.join(" ") : name,
|
|
268
|
-
path: nil, scope: nil,
|
|
269
|
-
start_line: nil, end_line: nil,
|
|
270
|
-
cwd: nil, env: nil, description: nil,
|
|
271
|
-
args: args.is_a?(Hash) ? args : {}
|
|
272
|
-
}
|
|
273
|
-
end
|
|
274
57
|
end
|
|
275
58
|
end
|
|
276
59
|
end
|
|
@@ -14,8 +14,6 @@ module Samagotchi
|
|
|
14
14
|
# Inverse of Installer: reads from the resolved scope directory,
|
|
15
15
|
# computes SHA256 checksums, writes manifest.yml via Manifest.write,
|
|
16
16
|
# and optionally zips/tars the staging directory.
|
|
17
|
-
#
|
|
18
|
-
# Reuses Installer.system_dir / project_dir_base overrides for test isolation.
|
|
19
17
|
class Builder
|
|
20
18
|
class BuildError < StandardError; end
|
|
21
19
|
|
|
@@ -291,18 +289,7 @@ module Samagotchi
|
|
|
291
289
|
end
|
|
292
290
|
|
|
293
291
|
def resolve_target_dir(scope)
|
|
294
|
-
|
|
295
|
-
when "system"
|
|
296
|
-
Samagotchi::MemoryBundle::Installer.system_dir
|
|
297
|
-
when "project"
|
|
298
|
-
if Samagotchi::MemoryBundle::Installer.project_dir_base_override
|
|
299
|
-
Samagotchi::MemoryBundle::Installer.project_dir_base
|
|
300
|
-
else
|
|
301
|
-
File.join(Samagotchi::MemoryBundle::Installer.project_dir_base, MemoryPaths.project_key)
|
|
302
|
-
end
|
|
303
|
-
else
|
|
304
|
-
raise BuildError, "invalid scope: #{scope}"
|
|
305
|
-
end
|
|
292
|
+
MemoryPaths.scope_dir(scope) or raise BuildError, "invalid scope: #{scope}"
|
|
306
293
|
end
|
|
307
294
|
|
|
308
295
|
def resolve_out_path(out_arg, default_name)
|
|
@@ -7,22 +7,9 @@ module Samagotchi
|
|
|
7
7
|
module MemoryBundle
|
|
8
8
|
# Updates the scoped index.md for a single entry, locked and atomically.
|
|
9
9
|
# Used by Installer (P3) to keep index.md in sync.
|
|
10
|
-
# Delegates to MemoryRead.memories_dir for test-isolated paths.
|
|
11
10
|
module IndexUpdater
|
|
12
11
|
MEMORY_INDEX = "index"
|
|
13
12
|
LOCK_FILE = ".index.lock"
|
|
14
|
-
def self.system_dir_override
|
|
15
|
-
@system_dir_override
|
|
16
|
-
end
|
|
17
|
-
def self.system_dir_override=(val)
|
|
18
|
-
@system_dir_override = val
|
|
19
|
-
end
|
|
20
|
-
def self.project_dir_base_override
|
|
21
|
-
@project_dir_base_override
|
|
22
|
-
end
|
|
23
|
-
def self.project_dir_base_override=(val)
|
|
24
|
-
@project_dir_base_override = val
|
|
25
|
-
end
|
|
26
13
|
def self.update_index(scope, entry_name, byte_count, description = nil)
|
|
27
14
|
index_path = index_path_for(scope)
|
|
28
15
|
return true unless index_path
|
|
@@ -93,21 +80,8 @@ module Samagotchi
|
|
|
93
80
|
Date.today.iso8601
|
|
94
81
|
end
|
|
95
82
|
def self.index_path_for(scope)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
base_dir = case normalized
|
|
99
|
-
when "system"
|
|
100
|
-
system_dir_override || MemoryPaths.system_dir
|
|
101
|
-
when "project"
|
|
102
|
-
if project_dir_base_override
|
|
103
|
-
File.join(project_dir_base_override, MemoryPaths.project_key)
|
|
104
|
-
else
|
|
105
|
-
MemoryPaths.project_dir
|
|
106
|
-
end
|
|
107
|
-
else
|
|
108
|
-
return nil
|
|
109
|
-
end
|
|
110
|
-
File.join(base_dir, "#{MEMORY_INDEX}.md")
|
|
83
|
+
base_dir = MemoryPaths.scope_dir(scope.to_s.strip)
|
|
84
|
+
base_dir && File.join(base_dir, "#{MEMORY_INDEX}.md")
|
|
111
85
|
end
|
|
112
86
|
end
|
|
113
87
|
end
|
|
@@ -29,18 +29,6 @@ module Samagotchi
|
|
|
29
29
|
|
|
30
30
|
attr_reader :results, :warnings, :placeholder_warnings, :conflicts
|
|
31
31
|
|
|
32
|
-
class << self
|
|
33
|
-
attr_accessor :system_dir_override, :project_dir_base_override
|
|
34
|
-
|
|
35
|
-
def system_dir
|
|
36
|
-
system_dir_override || MemoryPaths.system_dir
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def project_dir_base
|
|
40
|
-
project_dir_base_override || MemoryPaths.projects_dir
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
32
|
def initialize(source:, name:, scope: nil, force: false, strict: true, upgrade: false, dry_run: false)
|
|
45
33
|
@source = source
|
|
46
34
|
@name = name
|
|
@@ -53,9 +41,6 @@ module Samagotchi
|
|
|
53
41
|
@warnings = []
|
|
54
42
|
@placeholder_warnings = []
|
|
55
43
|
@conflicts = {}
|
|
56
|
-
# Propagate scope overrides so IndexUpdater resolves the correct directories.
|
|
57
|
-
IndexUpdater.system_dir_override = self.class.system_dir_override
|
|
58
|
-
IndexUpdater.project_dir_base_override = self.class.project_dir_base_override
|
|
59
44
|
end
|
|
60
45
|
|
|
61
46
|
def run
|
|
@@ -531,18 +516,7 @@ module Samagotchi
|
|
|
531
516
|
end
|
|
532
517
|
|
|
533
518
|
def resolve_target_dir(scope)
|
|
534
|
-
|
|
535
|
-
when "system", ""
|
|
536
|
-
self.class.system_dir
|
|
537
|
-
when "project"
|
|
538
|
-
if self.class.project_dir_base_override
|
|
539
|
-
self.class.project_dir_base
|
|
540
|
-
else
|
|
541
|
-
File.join(self.class.project_dir_base, MemoryPaths.project_key)
|
|
542
|
-
end
|
|
543
|
-
else
|
|
544
|
-
raise InstallError, "invalid scope: #{scope}"
|
|
545
|
-
end
|
|
519
|
+
MemoryPaths.scope_dir(scope) or raise InstallError, "invalid scope: #{scope}"
|
|
546
520
|
end
|
|
547
521
|
end
|
|
548
522
|
end
|
|
@@ -14,12 +14,8 @@ module Samagotchi
|
|
|
14
14
|
# manifest.json — metadata about the install (merged on reinstall)
|
|
15
15
|
# bases/ — snapshots of every file as installed (for 3-way merge later)
|
|
16
16
|
class Provenance
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def bundles_dir
|
|
21
|
-
bundles_dir_override || MemoryPaths.bundles_dir
|
|
22
|
-
end
|
|
17
|
+
def self.bundles_dir
|
|
18
|
+
MemoryPaths.bundles_dir
|
|
23
19
|
end
|
|
24
20
|
|
|
25
21
|
attr_reader :name, :bundle_dir
|
|
@@ -80,20 +80,7 @@ module Samagotchi
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def self.resolve_target_dir(scope)
|
|
83
|
-
|
|
84
|
-
when "system"
|
|
85
|
-
Samagotchi::MemoryBundle::Installer.system_dir
|
|
86
|
-
when "project"
|
|
87
|
-
base = Samagotchi::MemoryBundle::Installer.project_dir_base
|
|
88
|
-
# If override set via Installer, use it; else compute hash
|
|
89
|
-
if Samagotchi::MemoryBundle::Installer.project_dir_base_override
|
|
90
|
-
base
|
|
91
|
-
else
|
|
92
|
-
File.join(base, MemoryPaths.project_key)
|
|
93
|
-
end
|
|
94
|
-
else
|
|
95
|
-
Samagotchi::MemoryBundle::Installer.system_dir
|
|
96
|
-
end
|
|
83
|
+
MemoryPaths.scope_dir(scope) || MemoryPaths.system_dir
|
|
97
84
|
end
|
|
98
85
|
|
|
99
86
|
# Index lines name the entry without ".md" (Installer#update_target_index,
|
|
@@ -160,7 +160,7 @@ module Samagotchi
|
|
|
160
160
|
# back via the Installer (which classifies it as :install), without a
|
|
161
161
|
# full reinstall.
|
|
162
162
|
def verify_files_present(gem_manifest, version, dry_run)
|
|
163
|
-
target_dir =
|
|
163
|
+
target_dir = MemoryPaths.system_dir
|
|
164
164
|
missing = gem_manifest.files.keys.any? { |k| !File.exist?(File.join(target_dir, k)) }
|
|
165
165
|
return result(:up_to_date, from: version, to: version) unless missing
|
|
166
166
|
return result(:restored, from: version, to: version) if dry_run
|
|
@@ -174,7 +174,7 @@ module Samagotchi
|
|
|
174
174
|
def conflicting(gem_manifest)
|
|
175
175
|
provenance = Provenance.new(name: BUNDLE_NAME)
|
|
176
176
|
gem_manifest.files.keys.map(&:to_s).sort.select do |key|
|
|
177
|
-
Merger.classify(base_path: provenance.base_path(key), current_path: File.join(
|
|
177
|
+
Merger.classify(base_path: provenance.base_path(key), current_path: File.join(MemoryPaths.system_dir, key),
|
|
178
178
|
incoming_path: File.join(GEM_BUNDLE_DIR, key)) == :conflict
|
|
179
179
|
end
|
|
180
180
|
end
|
|
@@ -13,24 +13,12 @@ module Samagotchi
|
|
|
13
13
|
|
|
14
14
|
attr_reader :warnings, :removed_files
|
|
15
15
|
|
|
16
|
-
class << self
|
|
17
|
-
attr_accessor :system_dir_override, :project_dir_base_override
|
|
18
|
-
def system_dir
|
|
19
|
-
system_dir_override || MemoryPaths.system_dir
|
|
20
|
-
end
|
|
21
|
-
def project_dir_base
|
|
22
|
-
project_dir_base_override || MemoryPaths.projects_dir
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
16
|
def initialize(name:, scope: nil, force: false)
|
|
27
17
|
@name = name
|
|
28
18
|
@scope = scope&.to_s&.strip&.downcase
|
|
29
19
|
@force = force
|
|
30
20
|
@warnings = []
|
|
31
21
|
@removed_files = []
|
|
32
|
-
IndexUpdater.system_dir_override = self.class.system_dir_override
|
|
33
|
-
IndexUpdater.project_dir_base_override = self.class.project_dir_base_override
|
|
34
22
|
end
|
|
35
23
|
|
|
36
24
|
def run
|
|
@@ -110,18 +98,7 @@ module Samagotchi
|
|
|
110
98
|
private
|
|
111
99
|
|
|
112
100
|
def resolve_target_dir(scope)
|
|
113
|
-
|
|
114
|
-
when "system", ""
|
|
115
|
-
self.class.system_dir
|
|
116
|
-
when "project"
|
|
117
|
-
if self.class.project_dir_base_override
|
|
118
|
-
self.class.project_dir_base
|
|
119
|
-
else
|
|
120
|
-
File.join(self.class.project_dir_base, MemoryPaths.project_key)
|
|
121
|
-
end
|
|
122
|
-
else
|
|
123
|
-
raise UninstallError, "invalid scope: #{scope}"
|
|
124
|
-
end
|
|
101
|
+
MemoryPaths.scope_dir(scope) or raise UninstallError, "invalid scope: #{scope}"
|
|
125
102
|
end
|
|
126
103
|
end
|
|
127
104
|
end
|
|
@@ -94,6 +94,16 @@ module Samagotchi
|
|
|
94
94
|
File.join(projects_dir(env: env), project_key(cwd))
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# The folder of a memory scope: "system" (or blank) or "project"; nil for
|
|
98
|
+
# anything else. Every scope→folder lookup (bundle install/uninstall/build/
|
|
99
|
+
# status, index.md updates, the memory tools) resolves here.
|
|
100
|
+
def scope_dir(scope, env: ENV, cwd: Dir.pwd)
|
|
101
|
+
case scope.to_s
|
|
102
|
+
when "system", "" then system_dir(env: env)
|
|
103
|
+
when "project" then project_dir(env: env, cwd: cwd)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
97
107
|
def bundles_dir(env: ENV)
|
|
98
108
|
File.join(system_dir(env: env), ".bundles")
|
|
99
109
|
end
|