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
|
@@ -25,7 +25,7 @@ module Samagotchi
|
|
|
25
25
|
result = text.to_s
|
|
26
26
|
while (open_pos = result.index(open))
|
|
27
27
|
close_pos = result.index(close, open_pos + open.length)
|
|
28
|
-
result = result[0...open_pos] + (close_pos ? result[close_pos + close.length..] : "")
|
|
28
|
+
result = result[0...open_pos] + (close_pos ? result[(close_pos + close.length)..] : "")
|
|
29
29
|
end
|
|
30
30
|
result.strip
|
|
31
31
|
end
|
|
@@ -37,7 +37,7 @@ module Samagotchi
|
|
|
37
37
|
class Gemma
|
|
38
38
|
# Digits after the first character: plugin tools may have them
|
|
39
39
|
# (Plugin::Api::TOOL_NAME).
|
|
40
|
-
TOOL_CALL_BODY_RE
|
|
40
|
+
TOOL_CALL_BODY_RE = /\Acall:([a-z_][a-z0-9_]{0,49})\{/
|
|
41
41
|
THOUGHT_CHANNEL_OPEN = ModelProfile::GEMMA_THOUGHT_CHANNEL_OPEN
|
|
42
42
|
THOUGHT_CHANNEL_CLOSE = ModelProfile::GEMMA_THOUGHT_CHANNEL_CLOSE
|
|
43
43
|
CONTROL_TOKEN_START = "<|"
|
|
@@ -102,7 +102,7 @@ module Samagotchi
|
|
|
102
102
|
while (open_pos = result.index(THOUGHT_CHANNEL_OPEN))
|
|
103
103
|
close_pos = result.index(THOUGHT_CHANNEL_CLOSE, open_pos)
|
|
104
104
|
result = if close_pos
|
|
105
|
-
result[0...open_pos] + result[close_pos + THOUGHT_CHANNEL_CLOSE.length..]
|
|
105
|
+
result[0...open_pos] + result[(close_pos + THOUGHT_CHANNEL_CLOSE.length)..]
|
|
106
106
|
else
|
|
107
107
|
result[0...open_pos]
|
|
108
108
|
end
|
|
@@ -128,11 +128,10 @@ module Samagotchi
|
|
|
128
128
|
pos = start
|
|
129
129
|
while (p = text.index(CONTROL_TOKEN_START, pos))
|
|
130
130
|
# Skip over a <|"|> token entirely
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
end
|
|
131
|
+
return p unless text[p, @string_delim.length] == @string_delim
|
|
132
|
+
|
|
133
|
+
pos = p + @string_delim.length
|
|
134
|
+
|
|
136
135
|
end
|
|
137
136
|
nil
|
|
138
137
|
end
|
|
@@ -213,7 +212,7 @@ module Samagotchi
|
|
|
213
212
|
e = params_raw.index(@string_delim, s + @string_delim.length)
|
|
214
213
|
break unless e
|
|
215
214
|
|
|
216
|
-
arr << params_raw[s + @string_delim.length...e]
|
|
215
|
+
arr << params_raw[(s + @string_delim.length)...e]
|
|
217
216
|
pos = e + @string_delim.length
|
|
218
217
|
end
|
|
219
218
|
unless arr.empty?
|
|
@@ -334,13 +333,13 @@ module Samagotchi
|
|
|
334
333
|
|
|
335
334
|
def strip_thought(text)
|
|
336
335
|
# Remove all complete <think>...</think> blocks with the newlines after them
|
|
337
|
-
result = text.gsub(
|
|
336
|
+
result = text.gsub(%r{<think>.*?</think>\n*}m, "")
|
|
338
337
|
# Remove any stray opening tags
|
|
339
|
-
result = result.gsub(/<think>.*?(?=\n|$)/m,
|
|
338
|
+
result = result.gsub(/<think>.*?(?=\n|$)/m, "")
|
|
340
339
|
# Remove any orphaned closing tags (and the whitespace around them)
|
|
341
340
|
# The answer's own blank lines stay: they are its markdown paragraphs
|
|
342
341
|
# and lists (a chat host's answer is saved as stripped here).
|
|
343
|
-
result.gsub(
|
|
342
|
+
result.gsub(%r{^\s*</think>\s*}m, "")
|
|
344
343
|
end
|
|
345
344
|
|
|
346
345
|
def strip_tool_calls(text)
|
|
@@ -365,11 +364,11 @@ module Samagotchi
|
|
|
365
364
|
def qwen_params(body)
|
|
366
365
|
params = {}
|
|
367
366
|
|
|
368
|
-
body.scan(
|
|
367
|
+
body.scan(%r{<parameter=(\w+)>(.*?)</parameter>}m) do |key, value|
|
|
369
368
|
params[key.to_s.downcase] = trim_tag_newline(value)
|
|
370
369
|
end
|
|
371
370
|
|
|
372
|
-
body.scan(
|
|
371
|
+
body.scan(%r{<arg_key>(.*?)</arg_key>\s*<arg_value>(.*?)</arg_value>}m) do |key, value|
|
|
373
372
|
params[key.to_s.downcase.strip] = trim_tag_newline(value)
|
|
374
373
|
end
|
|
375
374
|
|
|
@@ -71,7 +71,7 @@ module Samagotchi
|
|
|
71
71
|
description: "Content to write to the file"
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
|
-
required: [
|
|
74
|
+
required: %w[path content]
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
77
|
{
|
|
@@ -101,7 +101,7 @@ module Samagotchi
|
|
|
101
101
|
description: "Optional end line (1-based, inclusive) for range mode. Must be provided with start_line."
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
|
-
required: [
|
|
104
|
+
required: %w[path new_text]
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
107
|
{
|
|
@@ -148,7 +148,7 @@ module Samagotchi
|
|
|
148
148
|
description: "Set true to save this entry as a model-specific overlay for the current model only (<name>.<model>.md); it is auto-appended when the entry is read under that model and never listed in the index."
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
|
-
required: [
|
|
151
|
+
required: %w[name content scope]
|
|
152
152
|
}
|
|
153
153
|
},
|
|
154
154
|
{
|
|
@@ -268,7 +268,7 @@ module Samagotchi
|
|
|
268
268
|
description: "How often to remind (1-1440 minutes, i.e. up to 1 day)"
|
|
269
269
|
}
|
|
270
270
|
},
|
|
271
|
-
required: [
|
|
271
|
+
required: %w[name description interval_minutes]
|
|
272
272
|
}
|
|
273
273
|
},
|
|
274
274
|
{
|
|
@@ -321,7 +321,7 @@ module Samagotchi
|
|
|
321
321
|
description: "The note: what the other session should know (up to 16 KiB)"
|
|
322
322
|
}
|
|
323
323
|
},
|
|
324
|
-
required: [
|
|
324
|
+
required: %w[session text]
|
|
325
325
|
}
|
|
326
326
|
},
|
|
327
327
|
{
|
|
@@ -399,30 +399,15 @@ module Samagotchi
|
|
|
399
399
|
description: "Allow freeform/Other text alongside selection. Default false."
|
|
400
400
|
}
|
|
401
401
|
},
|
|
402
|
-
required: [
|
|
402
|
+
required: %w[question options]
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
405
|
].freeze
|
|
406
406
|
|
|
407
|
-
# Where Gemma's declaration text says something the schema doesn't:
|
|
408
|
-
# description text
|
|
407
|
+
# Where Gemma's declaration text says something the schema doesn't:
|
|
408
|
+
# extra description text.
|
|
409
409
|
GEMMA_PARAM_OVERRIDES = {
|
|
410
410
|
"edit" => { new_text: { description: "Replacement text (required)" } },
|
|
411
|
-
"task_wait" => {
|
|
412
|
-
timeout: { required: false },
|
|
413
|
-
tail_lines: { required: false },
|
|
414
|
-
done_pattern: { required: false }
|
|
415
|
-
},
|
|
416
|
-
"delegate" => {
|
|
417
|
-
model: { required: false },
|
|
418
|
-
session: { required: false },
|
|
419
|
-
wait: { required: false },
|
|
420
|
-
timeout: { required: false }
|
|
421
|
-
},
|
|
422
|
-
"delegate_result" => {
|
|
423
|
-
session: { required: false },
|
|
424
|
-
timeout: { required: false }
|
|
425
|
-
},
|
|
426
411
|
"ask_user_question" => {
|
|
427
412
|
options: { description: "2-8 answer options as strings (labels). Single/multi selection via multi_select flag." }
|
|
428
413
|
}
|
|
@@ -440,26 +425,42 @@ module Samagotchi
|
|
|
440
425
|
|
|
441
426
|
module_function
|
|
442
427
|
|
|
443
|
-
# Gemma 4 <|tool>declaration:NAME{…}<tool|> blocks for every tool,
|
|
428
|
+
# Gemma 4 <|tool>declaration:NAME{…}<tool|> blocks for every tool, back
|
|
429
|
+
# to back, as Gemma 4's chat template declares tools.
|
|
444
430
|
# @param schemas [Array<Hash>] a Tools::Registry's schemas
|
|
445
431
|
def gemma_declarations(schemas = TOOL_SCHEMAS)
|
|
446
|
-
schemas.map { |schema| gemma_declaration(schema) }.join
|
|
432
|
+
schemas.map { |schema| gemma_declaration(schema) }.join
|
|
447
433
|
end
|
|
448
434
|
|
|
435
|
+
# One declaration in the template's shape: properties sorted by name,
|
|
436
|
+
# types upper-cased, the required names as a list.
|
|
449
437
|
def gemma_declaration(schema)
|
|
450
438
|
q = GEMMA_QUOTE
|
|
451
|
-
|
|
439
|
+
parameters = schema[:parameters] || {}
|
|
452
440
|
overrides = GEMMA_PARAM_OVERRIDES.fetch(schema[:name], {})
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
description = override.fetch(:description, param[:description])
|
|
456
|
-
line = " #{name}:{type:#{q}#{param[:type]}#{q}, description:#{q}#{description}#{q}"
|
|
457
|
-
req = override.fetch(:required, required.include?(name.to_s) ? true : nil)
|
|
458
|
-
line += ", required:#{req}" unless req.nil?
|
|
459
|
-
"#{line}}"
|
|
441
|
+
properties = (parameters[:properties] || {}).to_h do |name, param|
|
|
442
|
+
[name.to_s, param.merge(overrides.fetch(name.to_sym, {}))]
|
|
460
443
|
end
|
|
461
|
-
|
|
462
|
-
|
|
444
|
+
required = Array(parameters[:required]).map(&:to_s)
|
|
445
|
+
fields = []
|
|
446
|
+
fields << "properties:{#{gemma_properties(properties)}}" unless properties.empty?
|
|
447
|
+
fields << "required:[#{required.map { |name| "#{q}#{name}#{q}" }.join(",")}]" unless required.empty?
|
|
448
|
+
fields << "type:#{q}#{(parameters[:type] || "object").to_s.upcase}#{q}"
|
|
449
|
+
"<|tool>declaration:#{schema[:name]}{description:#{q}#{schema[:description]}#{q},parameters:{#{fields.join(",")}}}<tool|>"
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def gemma_properties(properties)
|
|
453
|
+
q = GEMMA_QUOTE
|
|
454
|
+
properties.sort.map do |name, param|
|
|
455
|
+
fields = []
|
|
456
|
+
fields << "description:#{q}#{param[:description]}#{q}" unless param[:description].to_s.empty?
|
|
457
|
+
items = param[:items]
|
|
458
|
+
if param[:type].to_s.casecmp?("array") && items.is_a?(Hash) && !items.empty?
|
|
459
|
+
fields << "items:{#{items.sort_by { |key, _| key.to_s }.map { |key, value| "#{key}:#{q}#{value.to_s.upcase}#{q}" if key.to_s == "type" }.compact.join(",")}}"
|
|
460
|
+
end
|
|
461
|
+
fields << "type:#{q}#{param[:type].to_s.upcase}#{q}"
|
|
462
|
+
"#{name}:{#{fields.join(",")}}"
|
|
463
|
+
end.join(",")
|
|
463
464
|
end
|
|
464
465
|
|
|
465
466
|
# The schemas for the chat path's tools: array: +schemas+ with
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "tool_activity"
|
|
4
|
+
require_relative "tool_view"
|
|
4
5
|
require_relative "guardrails"
|
|
5
6
|
require_relative "vision_context"
|
|
6
7
|
require_relative "log"
|
|
@@ -55,6 +56,9 @@ module Samagotchi
|
|
|
55
56
|
# The worker runs in its session's working directory.
|
|
56
57
|
title = ToolActivity.tool_title(call[:name], call, cwd: Dir.pwd)
|
|
57
58
|
started[:title] = title if title
|
|
59
|
+
# The full command for a richer UI (params stays cut at 80).
|
|
60
|
+
view = ToolView.for(call[:name], call)&.to_h
|
|
61
|
+
started[:view] = view if view
|
|
58
62
|
emit(on_stream_event, started)
|
|
59
63
|
|
|
60
64
|
# The ask comes after tool_call_started: the UI shows the tool line,
|
|
@@ -95,6 +99,9 @@ module Samagotchi
|
|
|
95
99
|
completed[:images] = images if images&.any?
|
|
96
100
|
completed[:diff] = diff if diff
|
|
97
101
|
completed[:waited_ms] = waited_ms if waited_ms
|
|
102
|
+
# Also here: a UI that missed the start (a replay gap) builds its row
|
|
103
|
+
# from this event.
|
|
104
|
+
completed[:view] = view if view
|
|
98
105
|
emit(on_stream_event, completed)
|
|
99
106
|
# After the completed event: a hook's card (check-in, skills) prints
|
|
100
107
|
# under the call's tool row, not before it.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "tools/execute"
|
|
4
|
+
require_relative "tools/task_create"
|
|
5
|
+
|
|
6
|
+
module Samagotchi
|
|
7
|
+
# What a richer UI shows of a tool call beyond its one-line params: the
|
|
8
|
+
# full command of an execute / task_create (whitespace kept), and the
|
|
9
|
+
# call's own cwd: argument. Built by the server on every path (the live
|
|
10
|
+
# tool_call_started, the bridge's snapshot and replay, a reload from the
|
|
11
|
+
# saved call), so the UIs never parse the call themselves. params and its
|
|
12
|
+
# 80-char cut stay as they are (the guardrails, "ran as:", the TUI).
|
|
13
|
+
#
|
|
14
|
+
# +truncated+: the command was longer than COMMAND_LIMIT and is cut to it;
|
|
15
|
+
# +chars+ is then its full length.
|
|
16
|
+
ToolView = Data.define(:command, :cwd, :truncated, :chars) do
|
|
17
|
+
def to_h = super.compact.reject { |_key, value| value == false }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class ToolView
|
|
21
|
+
# The longest command a view carries (the spike's longest was 3,799).
|
|
22
|
+
COMMAND_LIMIT = 8_000
|
|
23
|
+
|
|
24
|
+
TOOLS = [Tools::Execute::NAME, Tools::TaskCreate::NAME].freeze
|
|
25
|
+
|
|
26
|
+
# @return [ToolView, nil] nil for a tool without a view and for an
|
|
27
|
+
# empty command
|
|
28
|
+
def self.for(tool_name, call)
|
|
29
|
+
return nil unless TOOLS.include?(tool_name) && call.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
command = call[:content].to_s
|
|
32
|
+
return nil if command.strip.empty?
|
|
33
|
+
|
|
34
|
+
cwd = call[:cwd].to_s.strip
|
|
35
|
+
truncated = command.length > COMMAND_LIMIT
|
|
36
|
+
new(command: truncated ? command[0, COMMAND_LIMIT] : command, cwd: cwd.empty? ? nil : cwd,
|
|
37
|
+
truncated: truncated, chars: truncated ? command.length : nil)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -56,7 +56,7 @@ module Samagotchi
|
|
|
56
56
|
|
|
57
57
|
# Booleans the way models send them (Gemma passes "true"/"false").
|
|
58
58
|
def self.to_bool(value)
|
|
59
|
-
return value if
|
|
59
|
+
return value if [true, false].include?(value)
|
|
60
60
|
|
|
61
61
|
%w[1 true yes on].include?(value.to_s.strip.downcase)
|
|
62
62
|
end
|
|
@@ -90,6 +90,7 @@ module Samagotchi
|
|
|
90
90
|
begin
|
|
91
91
|
parsed = JSON.parse(s)
|
|
92
92
|
return parsed.dup if parsed.is_a?(Array)
|
|
93
|
+
|
|
93
94
|
# If parsed is a String like "a, b", fall through to split
|
|
94
95
|
if parsed.is_a?(String)
|
|
95
96
|
s = parsed
|
|
@@ -124,8 +125,6 @@ module Samagotchi
|
|
|
124
125
|
parts = t.split(/[,;\n]+/).map(&:strip)
|
|
125
126
|
# Strip surrounding quotes from each part
|
|
126
127
|
parts.map { |p| p.gsub(/\A["'\s]+|["'\s]+\z/, "").strip }
|
|
127
|
-
else
|
|
128
|
-
return nil
|
|
129
128
|
end
|
|
130
129
|
end
|
|
131
130
|
|
|
@@ -143,7 +142,7 @@ module Samagotchi
|
|
|
143
142
|
s = s.strip
|
|
144
143
|
return nil if s.empty?
|
|
145
144
|
return nil if s.match?(/\A[\[\],\s]+\z/)
|
|
146
|
-
return nil if
|
|
145
|
+
return nil if ["]", "["].include?(s)
|
|
147
146
|
|
|
148
147
|
s
|
|
149
148
|
end
|
|
@@ -87,7 +87,7 @@ module Samagotchi
|
|
|
87
87
|
# one of its keys must be a property; the rest must all be.
|
|
88
88
|
unknown = ([path_key].compact + Array(o[:verbatim]) + Array(o[:also]) + o.fetch(:aliases, {}).keys) - properties
|
|
89
89
|
unknown << content_keys.join("/") unless content_keys.empty? || content_keys.intersect?(properties)
|
|
90
|
-
raise ArgumentError, "#{name}: overrides name no schema property: #{unknown.join(
|
|
90
|
+
raise ArgumentError, "#{name}: overrides name no schema property: #{unknown.join(", ")}" unless unknown.empty?
|
|
91
91
|
raise ArgumentError, "#{name}: a property would replace the call's name" if fields.include?("name")
|
|
92
92
|
|
|
93
93
|
Row.new(name: name, content_keys: content_keys, path_key: path_key, fields: fields,
|
|
@@ -155,7 +155,7 @@ module Samagotchi
|
|
|
155
155
|
# true unless told otherwise; text-based parsers hand strings over.
|
|
156
156
|
def self.parse_wait(value)
|
|
157
157
|
return true if value.nil? || value.to_s.strip.empty?
|
|
158
|
-
return value if
|
|
158
|
+
return value if [true, false].include?(value)
|
|
159
159
|
|
|
160
160
|
!%w[false 0 no off].include?(value.to_s.strip.downcase)
|
|
161
161
|
end
|
|
@@ -137,7 +137,7 @@ module Samagotchi
|
|
|
137
137
|
|
|
138
138
|
# execute: git push origin main
|
|
139
139
|
def what_text(facts)
|
|
140
|
-
facts = facts.is_a?(Hash)
|
|
140
|
+
facts = {} unless facts.is_a?(Hash)
|
|
141
141
|
tool = facts[:label] || facts[:tool] || "tool"
|
|
142
142
|
what = facts[:command] || Array(facts[:paths]).join(", ")
|
|
143
143
|
what = facts[:args].to_s if what.to_s.empty?
|
|
@@ -27,7 +27,7 @@ module Samagotchi
|
|
|
27
27
|
updated, message = result
|
|
28
28
|
File.write(path, updated)
|
|
29
29
|
message
|
|
30
|
-
rescue => e
|
|
30
|
+
rescue StandardError => e
|
|
31
31
|
"Error: #{e.message}"
|
|
32
32
|
end
|
|
33
33
|
|
|
@@ -49,16 +49,16 @@ module Samagotchi
|
|
|
49
49
|
old_text = old_text.to_s
|
|
50
50
|
new_text = new_text.to_s
|
|
51
51
|
|
|
52
|
-
source = read.(path)
|
|
52
|
+
source = read.call(path)
|
|
53
53
|
count = count_occurrences(source, old_text)
|
|
54
54
|
|
|
55
55
|
return "Error: old text not found in #{path}" if count == 0
|
|
56
56
|
return "Error: old text matches #{count} times in #{path}; make it unique" if count > 1
|
|
57
57
|
|
|
58
58
|
idx = source.index(old_text)
|
|
59
|
-
updated = source[0, idx] + new_text + source[idx + old_text.length..]
|
|
59
|
+
updated = source[0, idx] + new_text + source[(idx + old_text.length)..]
|
|
60
60
|
[updated, "Edited #{path}: replaced #{old_text.bytesize} bytes with #{new_text.bytesize} bytes"]
|
|
61
|
-
rescue => e
|
|
61
|
+
rescue StandardError => e
|
|
62
62
|
"Error: #{e.message}"
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -77,7 +77,7 @@ module Samagotchi
|
|
|
77
77
|
# guessed start would silently overwrite the top of the file.
|
|
78
78
|
return START_LINE_REQUIRED if start_num.nil?
|
|
79
79
|
|
|
80
|
-
source = read.(path)
|
|
80
|
+
source = read.call(path)
|
|
81
81
|
lines = source.lines
|
|
82
82
|
total_lines = lines.length
|
|
83
83
|
|
|
@@ -94,12 +94,11 @@ module Samagotchi
|
|
|
94
94
|
end_value = nil
|
|
95
95
|
if end_provided
|
|
96
96
|
if end_num > total_lines
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
end
|
|
97
|
+
return "Error: range out of bounds for #{path}: file has #{total_lines} lines" unless OutputGuardrails.env_bool("SAMAGOTCHI_EDIT_ALLOW_OOR_END", default: true)
|
|
98
|
+
|
|
99
|
+
clamp_note = " (end_line #{end_num} exceeds #{total_lines} lines; clamped to line #{total_lines})"
|
|
100
|
+
end_value = total_lines
|
|
101
|
+
|
|
103
102
|
else
|
|
104
103
|
end_value = end_num
|
|
105
104
|
end
|
|
@@ -107,6 +106,7 @@ module Samagotchi
|
|
|
107
106
|
unless OutputGuardrails.env_bool("SAMAGOTCHI_EDIT_END_OPTIONAL", default: true)
|
|
108
107
|
return "Error: start_line and end_line must both be provided for range edits"
|
|
109
108
|
end
|
|
109
|
+
|
|
110
110
|
end_value = total_lines
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -118,13 +118,13 @@ module Samagotchi
|
|
|
118
118
|
suffix = lines[end_value..]&.join.to_s
|
|
119
119
|
# Ensure new_text ends with a newline when a suffix follows so that the
|
|
120
120
|
# first suffix line isn't concatenated onto the last replacement line.
|
|
121
|
-
normalized =
|
|
121
|
+
normalized = !new_text.empty? && !suffix.empty? && !new_text.end_with?("\n") ? new_text + "\n" : new_text
|
|
122
122
|
updated = prefix + normalized + suffix
|
|
123
123
|
|
|
124
124
|
replaced_lines = (end_value - start_num) + 1
|
|
125
125
|
new_line_count = new_text.lines.length
|
|
126
126
|
[updated, "Edited #{path}: replaced lines #{start_num}-#{end_value} (#{replaced_lines} lines) with #{new_line_count} lines#{clamp_note}"]
|
|
127
|
-
rescue => e
|
|
127
|
+
rescue StandardError => e
|
|
128
128
|
"Error: #{e.message}"
|
|
129
129
|
end
|
|
130
130
|
private_class_method :apply_range_mode
|
|
@@ -136,6 +136,7 @@ module Samagotchi
|
|
|
136
136
|
while (i = haystack.index(needle, pos))
|
|
137
137
|
count += 1
|
|
138
138
|
break if count > 1 # early exit — we only care whether it's 0, 1, or >1
|
|
139
|
+
|
|
139
140
|
pos = i + needle.length
|
|
140
141
|
end
|
|
141
142
|
count
|
|
@@ -67,7 +67,7 @@ module Samagotchi
|
|
|
67
67
|
stopped_result("Error: command timed out after #{timeout_sec}s\n#{TIMEOUT_HINT}", e)
|
|
68
68
|
rescue CommandCancelled => e
|
|
69
69
|
stopped_result("Error: command stopped by the user after #{e.elapsed.round}s (killed; rerun it if still needed)", e)
|
|
70
|
-
rescue => e
|
|
70
|
+
rescue StandardError => e
|
|
71
71
|
"Error: #{e.message}"
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -186,7 +186,7 @@ module Samagotchi
|
|
|
186
186
|
@thread = Thread.new do
|
|
187
187
|
Thread.current.report_on_exception = false
|
|
188
188
|
loop { @buffer << io.readpartial(READ_CHUNK_BYTES) }
|
|
189
|
-
rescue EOFError
|
|
189
|
+
rescue IOError # EOFError is one
|
|
190
190
|
nil
|
|
191
191
|
end
|
|
192
192
|
end
|
|
@@ -221,9 +221,7 @@ module Samagotchi
|
|
|
221
221
|
signal_process(pid, "TERM")
|
|
222
222
|
|
|
223
223
|
deadline = monotonic_time + STOP_GRACE_SEC
|
|
224
|
-
while process_alive?(pid) && monotonic_time < deadline
|
|
225
|
-
sleep(STOP_POLL_INTERVAL_SEC)
|
|
226
|
-
end
|
|
224
|
+
sleep(STOP_POLL_INTERVAL_SEC) while process_alive?(pid) && monotonic_time < deadline
|
|
227
225
|
|
|
228
226
|
signal_process(pid, "KILL") if process_alive?(pid)
|
|
229
227
|
end
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require "fileutils"
|
|
@@ -46,6 +45,7 @@ module Samagotchi
|
|
|
46
45
|
if names.empty?
|
|
47
46
|
return "Error: no memory names provided"
|
|
48
47
|
end
|
|
48
|
+
|
|
49
49
|
bad = names.find { |name| invalid_name?(name) }
|
|
50
50
|
return invalid_name_error(bad) if bad
|
|
51
51
|
|
|
@@ -58,34 +58,34 @@ module Samagotchi
|
|
|
58
58
|
found = false
|
|
59
59
|
resolved_scopes.each do |resolved_scope|
|
|
60
60
|
path = memory_path(name, resolved_scope)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
end
|
|
71
|
-
results << body
|
|
72
|
-
found = true
|
|
61
|
+
next unless File.exist?(path)
|
|
62
|
+
|
|
63
|
+
body = File.read(path, encoding: "UTF-8")
|
|
64
|
+
# Append model-specific overlay if key is provided and overlay exists.
|
|
65
|
+
[model_key, fallback_model_key].compact.each do |key|
|
|
66
|
+
overlay_path = ModelOverlay.overlay_path_for(name, key, resolved_scope)
|
|
67
|
+
next unless overlay_path && File.exist?(overlay_path)
|
|
68
|
+
|
|
69
|
+
body += SEPARATOR + "Model-specific guidance (#{key}):\n" + File.read(overlay_path, encoding: "UTF-8")
|
|
73
70
|
break
|
|
74
71
|
end
|
|
72
|
+
results << body
|
|
73
|
+
found = true
|
|
74
|
+
break
|
|
75
75
|
end
|
|
76
76
|
missing << name unless found
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
if results.empty? && !missing.empty?
|
|
80
|
-
"Error: memory not found: #{missing.join(
|
|
80
|
+
"Error: memory not found: #{missing.join(", ")}"
|
|
81
81
|
elsif missing.empty?
|
|
82
82
|
results.join(SEPARATOR)
|
|
83
83
|
else
|
|
84
|
-
results.join(SEPARATOR) + SEPARATOR + "Error: memory not found: #{missing.join(
|
|
84
|
+
results.join(SEPARATOR) + SEPARATOR + "Error: memory not found: #{missing.join(", ")}"
|
|
85
85
|
end
|
|
86
86
|
rescue Errno::ENOENT => e
|
|
87
87
|
"Error: #{e.message}"
|
|
88
|
-
rescue => e
|
|
88
|
+
rescue StandardError => e
|
|
89
89
|
"Error: #{e.message}"
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -108,7 +108,7 @@ module Samagotchi
|
|
|
108
108
|
return nil if value.empty?
|
|
109
109
|
return value if VALID_SCOPES.include?(value)
|
|
110
110
|
|
|
111
|
-
raise ArgumentError, "invalid scope '#{scope}', expected one of: #{VALID_SCOPES.join(
|
|
111
|
+
raise ArgumentError, "invalid scope '#{scope}', expected one of: #{VALID_SCOPES.join(", ")}"
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def self.scoped_index(scope)
|
|
@@ -157,6 +157,7 @@ module Samagotchi
|
|
|
157
157
|
return MemoryRead.invalid_name_error(entry_name) if MemoryRead.invalid_name?(entry_name)
|
|
158
158
|
return "Error: scope is required" if scope.to_s.strip.empty?
|
|
159
159
|
return "Error: content is required" if body.empty?
|
|
160
|
+
|
|
160
161
|
resolved_scope = MemoryRead.normalize_scope(scope)
|
|
161
162
|
|
|
162
163
|
if current_model_only
|
|
@@ -195,14 +196,12 @@ module Samagotchi
|
|
|
195
196
|
|
|
196
197
|
# The verbatim "write to index.md" behavior (path: "index") must not
|
|
197
198
|
# trigger upsert logic.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
message += " Index line refreshed automatically."
|
|
201
|
-
end
|
|
199
|
+
if !(entry_name == MEMORY_INDEX) && manage_index(resolved_scope, entry_name, bytes, description)
|
|
200
|
+
message += " Index line refreshed automatically."
|
|
202
201
|
end
|
|
203
202
|
|
|
204
203
|
message
|
|
205
|
-
rescue => e
|
|
204
|
+
rescue StandardError => e
|
|
206
205
|
"Error: #{e.message}"
|
|
207
206
|
end
|
|
208
207
|
|
|
@@ -65,7 +65,7 @@ module Samagotchi
|
|
|
65
65
|
build_truncated_preview(path: path, file_size: size, preview_bytes: preview_bytes)
|
|
66
66
|
rescue Errno::ENOENT
|
|
67
67
|
"Error: file not found: #{path}"
|
|
68
|
-
rescue => e
|
|
68
|
+
rescue StandardError => e
|
|
69
69
|
"Error: #{e.message}"
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -124,6 +124,7 @@ module Samagotchi
|
|
|
124
124
|
|
|
125
125
|
start_num = parse_positive_line_number(start_line, "start_line")
|
|
126
126
|
return start_num if start_num.is_a?(String)
|
|
127
|
+
|
|
127
128
|
# end_line alone reads from the top of the file.
|
|
128
129
|
start_num ||= 1
|
|
129
130
|
|
|
@@ -145,12 +146,11 @@ module Samagotchi
|
|
|
145
146
|
# the historical hard error instead.
|
|
146
147
|
clamp_note = ""
|
|
147
148
|
if end_provided && end_num > total_lines
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
end
|
|
149
|
+
return "Error: range out of bounds for #{path}: file has #{total_lines} lines" unless OutputGuardrails.env_bool("SAMAGOTCHI_READ_ALLOW_OOR_END", default: true)
|
|
150
|
+
|
|
151
|
+
clamp_note = "\n[read: end_line #{end_num} exceeds #{total_lines} lines; returning lines #{start_num}-#{total_lines}]"
|
|
152
|
+
end_num = total_lines
|
|
153
|
+
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
# A start past EOF is genuinely unusable -> keep a hard error. A slice
|