samagotchi 0.2.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 +7 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE +21 -0
- data/README.md +126 -0
- data/bin/chi +1140 -0
- data/docs/architecture.md +299 -0
- data/docs/cli.md +490 -0
- data/docs/configuration.md +494 -0
- data/docs/desktop.md +97 -0
- data/docs/guardrails.md +218 -0
- data/docs/hooks.md +309 -0
- data/docs/internals/background-tasks.md +26 -0
- data/docs/internals/context-telemetry.md +36 -0
- data/docs/internals/gemma4-contract.md +23 -0
- data/docs/internals/tool-guardrails.md +45 -0
- data/docs/memory.md +85 -0
- data/docs/plugins.md +819 -0
- data/docs/releasing.md +135 -0
- data/docs/sessions.md +155 -0
- data/lib/samagotchi/bridge/bounded_queue.rb +70 -0
- data/lib/samagotchi/bridge/card_store.rb +126 -0
- data/lib/samagotchi/bridge/event_id.rb +25 -0
- data/lib/samagotchi/bridge/ring_buffer.rb +63 -0
- data/lib/samagotchi/bridge/sse_writer.rb +248 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +189 -0
- data/lib/samagotchi/bridge.rb +993 -0
- data/lib/samagotchi/bridge_client/event_stream.rb +158 -0
- data/lib/samagotchi/bridge_client/sse_parser.rb +51 -0
- data/lib/samagotchi/bridge_client.rb +330 -0
- data/lib/samagotchi/bundle_needs.rb +97 -0
- data/lib/samagotchi/bundles/btw/manifest.yml +10 -0
- data/lib/samagotchi/bundles/btw/plugin.rb +100 -0
- data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +82 -0
- data/lib/samagotchi/bundles/guardrails/guardrails.md +14 -0
- data/lib/samagotchi/bundles/guardrails/manifest.yml +8 -0
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +210 -0
- data/lib/samagotchi/bundles/known-names/known_names.md +3 -0
- data/lib/samagotchi/bundles/known-names/manifest.yml +14 -0
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +10 -0
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +158 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +11 -0
- data/lib/samagotchi/bundles/mcp/plugin.rb +631 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +149 -0
- data/lib/samagotchi/bundles/system/delegated.md +10 -0
- data/lib/samagotchi/bundles/system/identity.md +7 -0
- data/lib/samagotchi/bundles/system/manifest.yml +11 -0
- data/lib/samagotchi/bundles/system/memory_guide.md +107 -0
- data/lib/samagotchi/bundles/system/self_map.md +55 -0
- data/lib/samagotchi/cancellation_controller.rb +78 -0
- data/lib/samagotchi/client.rb +429 -0
- data/lib/samagotchi/commands/registry.rb +112 -0
- data/lib/samagotchi/config.rb +910 -0
- data/lib/samagotchi/context_note.rb +77 -0
- data/lib/samagotchi/context_quote.rb +21 -0
- data/lib/samagotchi/context_usage.rb +66 -0
- data/lib/samagotchi/context_window.rb +76 -0
- data/lib/samagotchi/debug_log.rb +110 -0
- data/lib/samagotchi/desktop/macos/App.swift +102 -0
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +201 -0
- data/lib/samagotchi/desktop/macos/Hotkey.swift +42 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +42 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +383 -0
- data/lib/samagotchi/desktop/macos.rb +255 -0
- data/lib/samagotchi/desktop.rb +21 -0
- data/lib/samagotchi/desktop_command.rb +143 -0
- data/lib/samagotchi/engine.rb +2807 -0
- data/lib/samagotchi/guardrails/approval.rb +125 -0
- data/lib/samagotchi/guardrails/approvals.rb +177 -0
- data/lib/samagotchi/guardrails/context.rb +71 -0
- data/lib/samagotchi/guardrails/gate.rb +125 -0
- data/lib/samagotchi/guardrails/load_failures.rb +46 -0
- data/lib/samagotchi/guardrails/protected_paths.rb +77 -0
- data/lib/samagotchi/guardrails/rules.rb +199 -0
- data/lib/samagotchi/guardrails/targets.rb +119 -0
- data/lib/samagotchi/guardrails/verdict.rb +134 -0
- data/lib/samagotchi/guardrails.rb +18 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +158 -0
- data/lib/samagotchi/hooks/loader.rb +162 -0
- data/lib/samagotchi/hooks/registry.rb +261 -0
- data/lib/samagotchi/hooks.rb +30 -0
- data/lib/samagotchi/host_registry.rb +315 -0
- data/lib/samagotchi/idle_client.rb +147 -0
- data/lib/samagotchi/idle_recap.rb +549 -0
- data/lib/samagotchi/idle_reminders.rb +101 -0
- data/lib/samagotchi/idle_scheduler.rb +76 -0
- data/lib/samagotchi/image_store.rb +393 -0
- data/lib/samagotchi/installed_gem.rb +38 -0
- data/lib/samagotchi/kernel_loop.rb +1017 -0
- data/lib/samagotchi/launch_mode.rb +34 -0
- data/lib/samagotchi/llm/backend.rb +28 -0
- data/lib/samagotchi/llm/chat_loop.rb +450 -0
- data/lib/samagotchi/llm/errors.rb +329 -0
- data/lib/samagotchi/llm/http.rb +412 -0
- data/lib/samagotchi/llm/model_result.rb +72 -0
- data/lib/samagotchi/llm/native_backend.rb +50 -0
- data/lib/samagotchi/llm/native_tool_normalizer.rb +277 -0
- data/lib/samagotchi/llm/openai_chat.rb +403 -0
- data/lib/samagotchi/llm/usage.rb +79 -0
- data/lib/samagotchi/log.rb +200 -0
- data/lib/samagotchi/log_line.rb +127 -0
- data/lib/samagotchi/log_path.rb +31 -0
- data/lib/samagotchi/log_subscriber.rb +163 -0
- data/lib/samagotchi/memory_bundle/builder.rb +364 -0
- data/lib/samagotchi/memory_bundle/index_updater.rb +123 -0
- data/lib/samagotchi/memory_bundle/installer.rb +528 -0
- data/lib/samagotchi/memory_bundle/listing.rb +72 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +225 -0
- data/lib/samagotchi/memory_bundle/merger.rb +52 -0
- data/lib/samagotchi/memory_bundle/placeholder.rb +37 -0
- data/lib/samagotchi/memory_bundle/provenance.rb +257 -0
- data/lib/samagotchi/memory_bundle/source.rb +153 -0
- data/lib/samagotchi/memory_bundle/status.rb +107 -0
- data/lib/samagotchi/memory_bundle/system_bundle.rb +161 -0
- data/lib/samagotchi/memory_bundle/uninstaller.rb +128 -0
- data/lib/samagotchi/memory_bundle.rb +17 -0
- data/lib/samagotchi/memory_paths.rb +101 -0
- data/lib/samagotchi/model_overlay.rb +53 -0
- data/lib/samagotchi/model_profile.rb +309 -0
- data/lib/samagotchi/muted_memories.rb +66 -0
- data/lib/samagotchi/note_command.rb +163 -0
- data/lib/samagotchi/output_formatter.rb +100 -0
- data/lib/samagotchi/owner_lock.rb +110 -0
- data/lib/samagotchi/pending_input_queue.rb +48 -0
- data/lib/samagotchi/plugin/api.rb +362 -0
- data/lib/samagotchi/plugin/context.rb +193 -0
- data/lib/samagotchi/plugin/loader.rb +126 -0
- data/lib/samagotchi/plugin/service.rb +117 -0
- data/lib/samagotchi/plugin/sessions.rb +150 -0
- data/lib/samagotchi/plugin/side_question.rb +60 -0
- data/lib/samagotchi/plugin/tool_result.rb +24 -0
- data/lib/samagotchi/project_scope.rb +25 -0
- data/lib/samagotchi/prompt.rb +119 -0
- data/lib/samagotchi/prompt_literal_guard.rb +70 -0
- data/lib/samagotchi/recap_store.rb +92 -0
- data/lib/samagotchi/reminder_store.rb +165 -0
- data/lib/samagotchi/self_report.rb +195 -0
- data/lib/samagotchi/send_command.rb +170 -0
- data/lib/samagotchi/served_model.rb +32 -0
- data/lib/samagotchi/session.rb +508 -0
- data/lib/samagotchi/session_commands.rb +527 -0
- data/lib/samagotchi/session_delete_command.rb +105 -0
- data/lib/samagotchi/session_manager.rb +1049 -0
- data/lib/samagotchi/session_metrics.rb +466 -0
- data/lib/samagotchi/session_observer.rb +117 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +118 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1037 -0
- data/lib/samagotchi/terminal_ui/attached_view.rb +264 -0
- data/lib/samagotchi/terminal_ui/event_renderer.rb +192 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +291 -0
- data/lib/samagotchi/terminal_ui/image_input.rb +36 -0
- data/lib/samagotchi/terminal_ui/input_support.rb +324 -0
- data/lib/samagotchi/terminal_ui/legacy_surface.rb +111 -0
- data/lib/samagotchi/terminal_ui/line_reader.rb +113 -0
- data/lib/samagotchi/terminal_ui/live_region.rb +36 -0
- data/lib/samagotchi/terminal_ui/plain_surface.rb +51 -0
- data/lib/samagotchi/terminal_ui/question_prompt.rb +153 -0
- data/lib/samagotchi/terminal_ui/question_slot.rb +131 -0
- data/lib/samagotchi/terminal_ui/reline_seam.rb +216 -0
- data/lib/samagotchi/terminal_ui/repl_input.rb +138 -0
- data/lib/samagotchi/terminal_ui/screen.rb +316 -0
- data/lib/samagotchi/terminal_ui/surface.rb +47 -0
- data/lib/samagotchi/terminal_ui/thinking_line.rb +101 -0
- data/lib/samagotchi/terminal_ui.rb +1992 -0
- data/lib/samagotchi/thinking_ticker.rb +110 -0
- data/lib/samagotchi/thought_stream_splitter.rb +149 -0
- data/lib/samagotchi/token_usage.rb +88 -0
- data/lib/samagotchi/tool_activity.rb +216 -0
- data/lib/samagotchi/tool_call_parser.rb +637 -0
- data/lib/samagotchi/tool_declarations.rb +561 -0
- data/lib/samagotchi/tool_runner.rb +211 -0
- data/lib/samagotchi/tools/args.rb +259 -0
- data/lib/samagotchi/tools/ask_user_question.rb +152 -0
- data/lib/samagotchi/tools/builtins.rb +122 -0
- data/lib/samagotchi/tools/cancel_reminder.rb +21 -0
- data/lib/samagotchi/tools/delegate.rb +167 -0
- data/lib/samagotchi/tools/delegate_result.rb +53 -0
- data/lib/samagotchi/tools/delegate_wait.rb +153 -0
- data/lib/samagotchi/tools/edit.rb +155 -0
- data/lib/samagotchi/tools/execute.rb +214 -0
- data/lib/samagotchi/tools/list_reminders.rb +20 -0
- data/lib/samagotchi/tools/list_sessions.rb +74 -0
- data/lib/samagotchi/tools/memory.rb +256 -0
- data/lib/samagotchi/tools/output_guardrails.rb +93 -0
- data/lib/samagotchi/tools/peers.rb +18 -0
- data/lib/samagotchi/tools/read.rb +182 -0
- data/lib/samagotchi/tools/register_reminder.rb +53 -0
- data/lib/samagotchi/tools/registry.rb +60 -0
- data/lib/samagotchi/tools/send_note.rb +49 -0
- data/lib/samagotchi/tools/task_create.rb +29 -0
- data/lib/samagotchi/tools/task_get.rb +39 -0
- data/lib/samagotchi/tools/task_list.rb +43 -0
- data/lib/samagotchi/tools/task_runtime.rb +311 -0
- data/lib/samagotchi/tools/task_stop.rb +29 -0
- data/lib/samagotchi/tools/task_wait.rb +104 -0
- data/lib/samagotchi/tools/tool_path.rb +18 -0
- data/lib/samagotchi/tools/web_fetch.rb +163 -0
- data/lib/samagotchi/tools/write.rb +26 -0
- data/lib/samagotchi/turn_flow.rb +242 -0
- data/lib/samagotchi/turn_note.rb +76 -0
- data/lib/samagotchi/turn_tally.rb +101 -0
- data/lib/samagotchi/version.rb +7 -0
- data/lib/samagotchi/vision_context.rb +132 -0
- data/lib/samagotchi/vision_support.rb +109 -0
- data/lib/samagotchi/web/app.rb +1349 -0
- data/lib/samagotchi/web/markdown_renderer.rb +107 -0
- data/lib/samagotchi/web/message_parts.rb +169 -0
- data/lib/samagotchi/web/public/activity.js +100 -0
- data/lib/samagotchi/web/public/annotations.js +67 -0
- data/lib/samagotchi/web/public/app.js +2382 -0
- data/lib/samagotchi/web/public/card.js +74 -0
- data/lib/samagotchi/web/public/chat_view.js +360 -0
- data/lib/samagotchi/web/public/chunk_router.js +25 -0
- data/lib/samagotchi/web/public/command_complete.js +39 -0
- data/lib/samagotchi/web/public/composer_size.js +19 -0
- data/lib/samagotchi/web/public/copy.js +142 -0
- data/lib/samagotchi/web/public/ctx.js +35 -0
- data/lib/samagotchi/web/public/data.js +256 -0
- data/lib/samagotchi/web/public/format.js +232 -0
- data/lib/samagotchi/web/public/hold.js +78 -0
- data/lib/samagotchi/web/public/images.js +77 -0
- data/lib/samagotchi/web/public/index.html +568 -0
- data/lib/samagotchi/web/public/init_row.js +60 -0
- data/lib/samagotchi/web/public/model_pick.js +23 -0
- data/lib/samagotchi/web/public/question_card.js +100 -0
- data/lib/samagotchi/web/public/route.js +17 -0
- data/lib/samagotchi/web/public/scope.js +36 -0
- data/lib/samagotchi/web/public/scroll.js +24 -0
- data/lib/samagotchi/web/public/sentences.js +88 -0
- data/lib/samagotchi/web/public/sessions_list.js +60 -0
- data/lib/samagotchi/web/public/strip.js +25 -0
- data/lib/samagotchi/web/public/tally.js +37 -0
- data/lib/samagotchi/web/public/thinking_ticker.js +79 -0
- data/lib/samagotchi/web/public/timing.js +185 -0
- data/lib/samagotchi/web/public/turn_events.js +209 -0
- data/lib/samagotchi/web/public/turn_model.js +204 -0
- data/lib/samagotchi/web/public/turn_view.js +587 -0
- data/lib/samagotchi/web/server.rb +183 -0
- data/lib/samagotchi/web/session_hub.rb +329 -0
- data/lib/samagotchi/web/session_summary.rb +85 -0
- data/lib/samagotchi/worker.rb +635 -0
- data/lib/samagotchi/worker_idle_exit.rb +87 -0
- data/lib/samagotchi.rb +12 -0
- metadata +374 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "model_profile"
|
|
4
|
+
require_relative "tools/execute"
|
|
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"
|
|
23
|
+
require_relative "tools/args"
|
|
24
|
+
|
|
25
|
+
module Samagotchi
|
|
26
|
+
# Per-profile strategy for parsing raw model output into internal tool
|
|
27
|
+
# calls ({name:, content:, path:, scope:, ...}) and stripping per-profile
|
|
28
|
+
# thought blocks. KernelLoop delegates here so it no longer carries dual
|
|
29
|
+
# Gemma/Qwen parse code paths; the ModelProfile remains the source of truth
|
|
30
|
+
# for call/thought delimiter tokens.
|
|
31
|
+
class ToolCallParser
|
|
32
|
+
# @param profile [ModelProfile]
|
|
33
|
+
# @return [Gemma, Qwen]
|
|
34
|
+
def self.for_profile(profile)
|
|
35
|
+
profile.name == "qwen36" ? Qwen.new(profile) : Gemma.new(profile)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# +text+ without its tool-call blocks (+open+ … +close+); a block the model
|
|
39
|
+
# never closed runs to the end. Surrounding whitespace is trimmed.
|
|
40
|
+
def self.strip_calls(text, open, close)
|
|
41
|
+
result = text.to_s
|
|
42
|
+
while (open_pos = result.index(open))
|
|
43
|
+
close_pos = result.index(close, open_pos + open.length)
|
|
44
|
+
result = result[0...open_pos] + (close_pos ? result[close_pos + close.length..] : "")
|
|
45
|
+
end
|
|
46
|
+
result.strip
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ── Gemma 4 ────────────────────────────────────────────────────────────
|
|
50
|
+
# Tool calls: <|tool_call>call:NAME{params}<tool_call|>
|
|
51
|
+
# Thoughts: <|think|>CONTENT (ends at next real <| token) and
|
|
52
|
+
# <|channel>thought......<channel|>
|
|
53
|
+
class Gemma
|
|
54
|
+
# Digits after the first character: plugin tools may have them
|
|
55
|
+
# (Plugin::Api::TOOL_NAME).
|
|
56
|
+
TOOL_CALL_BODY_RE = /\Acall:([a-z_][a-z0-9_]{0,49})\{/
|
|
57
|
+
THOUGHT_CHANNEL_OPEN = "<|channel>thought"
|
|
58
|
+
THOUGHT_CHANNEL_CLOSE = "<channel|>"
|
|
59
|
+
CONTROL_TOKEN_START = "<|"
|
|
60
|
+
|
|
61
|
+
def initialize(profile)
|
|
62
|
+
@tool_call_open = profile.tool_call_open
|
|
63
|
+
@tool_call_close = profile.tool_call_close
|
|
64
|
+
@thought_open = profile.thought_open
|
|
65
|
+
@string_delim = profile.string_delim
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Parse canonical Gemma 4 tool calls:
|
|
69
|
+
# <|tool_call>call:NAME{params}<tool_call|>
|
|
70
|
+
# Uses String#index for outer delimiters (no backtracking risk).
|
|
71
|
+
def parse(text)
|
|
72
|
+
results = []
|
|
73
|
+
|
|
74
|
+
pos = 0
|
|
75
|
+
while (open_pos = text.index(@tool_call_open, pos))
|
|
76
|
+
body_start = open_pos + @tool_call_open.length
|
|
77
|
+
close_pos = text.index(@tool_call_close, body_start)
|
|
78
|
+
break unless close_pos
|
|
79
|
+
|
|
80
|
+
body = text[body_start...close_pos]
|
|
81
|
+
if (m = TOOL_CALL_BODY_RE.match(body))
|
|
82
|
+
name = m[1]
|
|
83
|
+
params_raw = body[m.end(0)..]
|
|
84
|
+
params_raw = params_raw[0..-2] if params_raw.end_with?("}")
|
|
85
|
+
results << native_call(name, params_raw.strip)
|
|
86
|
+
end
|
|
87
|
+
pos = close_pos + @tool_call_close.length
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
results
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Remove Gemma 4 thought blocks from text.
|
|
94
|
+
# <|think|>CONTENT — ends at the next real <| control token
|
|
95
|
+
# (skipping any <|"|> Gemma string-delimiter tokens) or EOS.
|
|
96
|
+
# <|channel>thought......</channel|> — channel format blocks
|
|
97
|
+
def strip_thought(text)
|
|
98
|
+
result = text
|
|
99
|
+
|
|
100
|
+
# Canonical format: strip from <|think|> to the next real <| (exclusive) or EOS
|
|
101
|
+
while (open_pos = result.index(@thought_open))
|
|
102
|
+
body_start = open_pos + @thought_open.length
|
|
103
|
+
close_pos = next_real_control_token(result, body_start)
|
|
104
|
+
result = if close_pos
|
|
105
|
+
result[0...open_pos] + result[close_pos..]
|
|
106
|
+
else
|
|
107
|
+
result[0...open_pos]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Emitted thought-channel format: strip from <|channel>thought to the
|
|
112
|
+
# corresponding closing tag, or EOS if the close is missing.
|
|
113
|
+
while (open_pos = result.index(THOUGHT_CHANNEL_OPEN))
|
|
114
|
+
close_pos = result.index(THOUGHT_CHANNEL_CLOSE, open_pos)
|
|
115
|
+
result = if close_pos
|
|
116
|
+
result[0...open_pos] + result[close_pos + THOUGHT_CHANNEL_CLOSE.length..]
|
|
117
|
+
else
|
|
118
|
+
result[0...open_pos]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
result
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def strip_tool_calls(text)
|
|
126
|
+
ToolCallParser.strip_calls(text, @tool_call_open, @tool_call_close)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Gemma has no unterminated-tool-call recovery flow.
|
|
130
|
+
def parse_with_recovery(response, _partial_fragment = nil)
|
|
131
|
+
[parse(response.to_s), nil]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# Scan forward from +start+ for the next <| sequence that is NOT the Gemma
|
|
137
|
+
# string delimiter <|"|>. Returns the position of that <| or nil if none.
|
|
138
|
+
def next_real_control_token(text, start)
|
|
139
|
+
pos = start
|
|
140
|
+
while (p = text.index(CONTROL_TOKEN_START, pos))
|
|
141
|
+
# Skip over a <|"|> token entirely
|
|
142
|
+
if text[p, @string_delim.length] == @string_delim
|
|
143
|
+
pos = p + @string_delim.length
|
|
144
|
+
else
|
|
145
|
+
return p
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Strip all occurrences of the Gemma string-delimiter token from +str+.
|
|
152
|
+
# Used to clean up bare values that use <|"|> as quoting.
|
|
153
|
+
def strip_gemma_delimiters(str)
|
|
154
|
+
str.gsub(@string_delim, "")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Map native {key: "value"} params to the internal call hash.
|
|
158
|
+
# Uses well-known named params for each tool; falls back to params_raw if
|
|
159
|
+
# no recognised param is present.
|
|
160
|
+
#
|
|
161
|
+
# The model sometimes omits quotes and/or the space after the colon, e.g.
|
|
162
|
+
# {command:ruby -e 'puts 1'} instead of {command: "ruby -e 'puts 1'"}
|
|
163
|
+
# In that case extract_native_params finds nothing and params_raw still
|
|
164
|
+
# contains the "key:" prefix. strip_param_prefix removes it so the actual
|
|
165
|
+
# command/path value is passed to the tool rather than the raw fragment.
|
|
166
|
+
#
|
|
167
|
+
# Gemma 4 may also use its <|"|> string delimiter token instead of plain
|
|
168
|
+
# quotes. strip_gemma_delimiters is applied to all fallback values so that
|
|
169
|
+
# <|"|>value<|"|> is cleaned to just "value" before being dispatched.
|
|
170
|
+
def native_call(name, params_raw)
|
|
171
|
+
params = extract_native_params(params_raw)
|
|
172
|
+
|
|
173
|
+
case name
|
|
174
|
+
when Tools::Execute::NAME
|
|
175
|
+
content = params["command"] ||
|
|
176
|
+
strip_param_prefix(params_raw, "command") ||
|
|
177
|
+
params_raw
|
|
178
|
+
{ name: name, content: strip_gemma_delimiters(content), path: nil, scope: nil }
|
|
179
|
+
when Tools::Read::NAME
|
|
180
|
+
content = params["path"] ||
|
|
181
|
+
strip_param_prefix(params_raw, "path") ||
|
|
182
|
+
params_raw
|
|
183
|
+
{
|
|
184
|
+
name: name,
|
|
185
|
+
content: strip_gemma_delimiters(content),
|
|
186
|
+
path: nil,
|
|
187
|
+
scope: nil,
|
|
188
|
+
start_line: params["start_line"],
|
|
189
|
+
end_line: params["end_line"]
|
|
190
|
+
}
|
|
191
|
+
when Tools::Write::NAME
|
|
192
|
+
{ name: name, content: params["content"] || "", path: params["path"], scope: nil }
|
|
193
|
+
when Tools::MemoryRead::NAME
|
|
194
|
+
content = params["name"] ||
|
|
195
|
+
strip_param_prefix(params_raw, "name") ||
|
|
196
|
+
params_raw
|
|
197
|
+
{ name: name, content: strip_gemma_delimiters(content), path: nil, scope: params["scope"] }
|
|
198
|
+
when Tools::MemoryWrite::NAME
|
|
199
|
+
# The declaration uses "name" (required) and "scope" (required).
|
|
200
|
+
entry_name = params["name"] || ""
|
|
201
|
+
{ name: name, content: params["content"] || "", path: entry_name, scope: params["scope"], description: params["description"] ? strip_gemma_delimiters(params["description"]) : nil,
|
|
202
|
+
current_model_only: params["current_model_only"] }
|
|
203
|
+
when Tools::Edit::NAME
|
|
204
|
+
old_text = params["old_text"] || params["old"] || ""
|
|
205
|
+
new_text = params["new_text"] || params["new"] || ""
|
|
206
|
+
content = "<old>#{old_text}</old><new>#{new_text}</new>"
|
|
207
|
+
{
|
|
208
|
+
name: name,
|
|
209
|
+
content: content,
|
|
210
|
+
path: params["path"],
|
|
211
|
+
scope: nil,
|
|
212
|
+
start_line: params["start_line"],
|
|
213
|
+
end_line: params["end_line"]
|
|
214
|
+
}
|
|
215
|
+
when Tools::TaskCreate::NAME
|
|
216
|
+
command = params["command"] ||
|
|
217
|
+
strip_param_prefix(params_raw, "command") ||
|
|
218
|
+
params_raw
|
|
219
|
+
{
|
|
220
|
+
name: name,
|
|
221
|
+
content: strip_gemma_delimiters(command),
|
|
222
|
+
path: nil,
|
|
223
|
+
scope: nil,
|
|
224
|
+
cwd: params["cwd"],
|
|
225
|
+
env: params["env"]
|
|
226
|
+
}
|
|
227
|
+
when Tools::TaskGet::NAME, Tools::TaskStop::NAME
|
|
228
|
+
task_id = params["id"] ||
|
|
229
|
+
params["task_id"] ||
|
|
230
|
+
strip_param_prefix(params_raw, "id") ||
|
|
231
|
+
strip_param_prefix(params_raw, "task_id") ||
|
|
232
|
+
params_raw
|
|
233
|
+
{
|
|
234
|
+
name: name,
|
|
235
|
+
content: strip_gemma_delimiters(task_id),
|
|
236
|
+
path: nil,
|
|
237
|
+
scope: nil
|
|
238
|
+
}
|
|
239
|
+
when Tools::TaskWait::NAME
|
|
240
|
+
task_id = params["id"] ||
|
|
241
|
+
params["task_id"] ||
|
|
242
|
+
strip_param_prefix(params_raw, "id") ||
|
|
243
|
+
strip_param_prefix(params_raw, "task_id") ||
|
|
244
|
+
params_raw
|
|
245
|
+
{
|
|
246
|
+
name: name,
|
|
247
|
+
content: strip_gemma_delimiters(task_id),
|
|
248
|
+
path: nil,
|
|
249
|
+
scope: nil,
|
|
250
|
+
timeout: params["timeout"],
|
|
251
|
+
tail_lines: params["tail_lines"],
|
|
252
|
+
done_pattern: params["done_pattern"]
|
|
253
|
+
}
|
|
254
|
+
when Tools::TaskList::NAME
|
|
255
|
+
{ name: name, content: "", path: nil, scope: nil }
|
|
256
|
+
when Tools::WebFetch::NAME
|
|
257
|
+
content = params["url"] ||
|
|
258
|
+
strip_param_prefix(params_raw, "url") ||
|
|
259
|
+
params_raw
|
|
260
|
+
{ name: name, content: strip_gemma_delimiters(content), path: nil, scope: nil }
|
|
261
|
+
when Tools::RegisterReminder::NAME
|
|
262
|
+
content = params["name"] || strip_param_prefix(params_raw, "name") || params_raw
|
|
263
|
+
{
|
|
264
|
+
name: name,
|
|
265
|
+
content: strip_gemma_delimiters(content),
|
|
266
|
+
path: nil,
|
|
267
|
+
scope: nil,
|
|
268
|
+
description: params["description"] || "",
|
|
269
|
+
interval_minutes: params["interval_minutes"] || "1"
|
|
270
|
+
}
|
|
271
|
+
when Tools::CancelReminder::NAME
|
|
272
|
+
content = params["name"] || strip_param_prefix(params_raw, "name") || params_raw
|
|
273
|
+
{ name: name, content: strip_gemma_delimiters(content), path: nil, scope: nil }
|
|
274
|
+
when Tools::ListReminders::NAME
|
|
275
|
+
{ name: name, content: "", path: nil, scope: nil }
|
|
276
|
+
when Tools::ListSessions::NAME
|
|
277
|
+
cwd = params["cwd"] || strip_param_prefix(params_raw, "cwd")
|
|
278
|
+
{ name: name, content: "", path: nil, scope: nil, cwd: cwd && strip_gemma_delimiters(cwd) }
|
|
279
|
+
when Tools::SendNote::NAME
|
|
280
|
+
{ name: name, content: strip_gemma_delimiters(params["text"].to_s), path: nil, scope: nil,
|
|
281
|
+
session: strip_gemma_delimiters(params["session"].to_s) }
|
|
282
|
+
when Tools::Delegate::NAME
|
|
283
|
+
task = params["task"] || strip_param_prefix(params_raw, "task") || params_raw
|
|
284
|
+
{ name: name, content: strip_gemma_delimiters(task.to_s), path: nil, scope: nil,
|
|
285
|
+
model: params["model"] && strip_gemma_delimiters(params["model"]),
|
|
286
|
+
session: params["session"] && strip_gemma_delimiters(params["session"]),
|
|
287
|
+
wait: params["wait"], timeout: params["timeout"] }
|
|
288
|
+
when Tools::DelegateResult::NAME
|
|
289
|
+
{ name: name, content: "", path: nil, scope: nil,
|
|
290
|
+
session: params["session"] && strip_gemma_delimiters(params["session"]), timeout: params["timeout"] }
|
|
291
|
+
when Tools::AskUserQuestion::NAME
|
|
292
|
+
{ name: name, content: params["question"] ? strip_gemma_delimiters(params["question"]) : strip_gemma_delimiters(params_raw),
|
|
293
|
+
path: nil, scope: nil,
|
|
294
|
+
question: params["question"] ? strip_gemma_delimiters(params["question"]) : strip_gemma_delimiters(params_raw),
|
|
295
|
+
options: parse_ask_options(params, params_raw),
|
|
296
|
+
header: params["header"] ? strip_gemma_delimiters(params["header"]) : nil,
|
|
297
|
+
multi_select: params["multi_select"],
|
|
298
|
+
allow_freeform: params["allow_freeform"] }
|
|
299
|
+
else
|
|
300
|
+
# A registry (plugin) tool, or an unknown one: its arguments as a
|
|
301
|
+
# Hash on args: (typed by the schema at dispatch), and the raw text
|
|
302
|
+
# as content for the unknown-tool error.
|
|
303
|
+
args = Tools::Args.parse_gemma(params_raw, @string_delim) || params
|
|
304
|
+
{ name: name, content: strip_gemma_delimiters(params_raw), path: nil, scope: nil, args: args }
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def parse_ask_options(params, params_raw)
|
|
309
|
+
raw = params["options"]
|
|
310
|
+
# Use tolerant normalizer for String and Array; handles dumb-model noise like ["\"Cats\""] or "]"
|
|
311
|
+
if raw.is_a?(String) || raw.is_a?(Array)
|
|
312
|
+
norm = Samagotchi::Tools::AskUserQuestion.normalize_options_lenient(raw)
|
|
313
|
+
return norm unless norm.empty?
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Fallback: extract array-like syntax from raw string e.g. options:["a","b"]
|
|
317
|
+
if params_raw.include?("options")
|
|
318
|
+
# Try to find a JSON array substring first
|
|
319
|
+
if (m = params_raw.match(/options:\s*(\[[^\]]*\])/))
|
|
320
|
+
norm = Samagotchi::Tools::AskUserQuestion.normalize_options_lenient(m[1])
|
|
321
|
+
return norm unless norm.empty?
|
|
322
|
+
end
|
|
323
|
+
if (m = params_raw.match(/options:\s*\[([^\]]*)\]/))
|
|
324
|
+
inner = m[1]
|
|
325
|
+
norm = Samagotchi::Tools::AskUserQuestion.normalize_options_lenient(inner)
|
|
326
|
+
return norm unless norm.empty?
|
|
327
|
+
end
|
|
328
|
+
# also try Gemma delimiter array
|
|
329
|
+
arr = []
|
|
330
|
+
pos = 0
|
|
331
|
+
while (s = params_raw.index(@string_delim, pos))
|
|
332
|
+
e = params_raw.index(@string_delim, s + @string_delim.length)
|
|
333
|
+
break unless e
|
|
334
|
+
|
|
335
|
+
arr << params_raw[s + @string_delim.length...e]
|
|
336
|
+
pos = e + @string_delim.length
|
|
337
|
+
end
|
|
338
|
+
unless arr.empty?
|
|
339
|
+
norm = Samagotchi::Tools::AskUserQuestion.normalize_options_lenient(arr)
|
|
340
|
+
return norm unless norm.empty?
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
nil
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Strip a known "key:" or "key: " prefix from a raw params string.
|
|
347
|
+
# Returns the remainder of the string, or nil if the prefix is absent.
|
|
348
|
+
# Handles both {command:value} (no space) and {command: value} (space).
|
|
349
|
+
def strip_param_prefix(params_raw, key)
|
|
350
|
+
return nil unless params_raw.start_with?("#{key}:")
|
|
351
|
+
|
|
352
|
+
params_raw.sub(/\A#{Regexp.escape(key)}:\s*/, "")
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Extract key: "value" / key: 'value' / key:<|"|>value<|"|> pairs from a
|
|
356
|
+
# native params string.
|
|
357
|
+
# Atomic groups (?>…) prevent ReDoS on adversarial inputs.
|
|
358
|
+
# Both quote styles and the Gemma 4 delimiter are always scanned;
|
|
359
|
+
# double-quoted values take precedence.
|
|
360
|
+
def extract_native_params(params_raw)
|
|
361
|
+
params = {}
|
|
362
|
+
params_raw.scan(/(\w+):\s*"((?>[^"\\]|\\.)*)"/) do |k, v|
|
|
363
|
+
params[k] = unescape_native_value(v)
|
|
364
|
+
end
|
|
365
|
+
params_raw.scan(/(\w+):\s*'((?>[^'\\]|\\.)*)'/) do |k, v|
|
|
366
|
+
params[k] ||= unescape_native_value(v)
|
|
367
|
+
end
|
|
368
|
+
params_raw.scan(/(\w+):\s*(-?\d+)/) do |k, v|
|
|
369
|
+
params[k] ||= v
|
|
370
|
+
end
|
|
371
|
+
# Unquoted boolean values: key:true or key:false
|
|
372
|
+
params_raw.scan(/(\w+):\s*(true|false)\b/) do |k, v|
|
|
373
|
+
params[k] ||= v
|
|
374
|
+
end
|
|
375
|
+
# Gemma 4 string delimiter: key:<|"|>value<|"|>
|
|
376
|
+
# Use plain String#index to avoid any regex backtracking risk on the
|
|
377
|
+
# value content (mirrors the approach used for control-token scanning).
|
|
378
|
+
search_pos = 0
|
|
379
|
+
while (delim_pos = params_raw.index(@string_delim, search_pos))
|
|
380
|
+
val_start = delim_pos + @string_delim.length
|
|
381
|
+
close_pos = params_raw.index(@string_delim, val_start)
|
|
382
|
+
break unless close_pos
|
|
383
|
+
|
|
384
|
+
# Extract the key name by scanning backwards: strip trailing whitespace,
|
|
385
|
+
# expect a colon, then extract trailing word characters — all without
|
|
386
|
+
# a backtracking regex so there is no polynomial-ReDoS risk.
|
|
387
|
+
prefix = params_raw[0...delim_pos].rstrip
|
|
388
|
+
unless prefix.end_with?(":")
|
|
389
|
+
search_pos = close_pos + @string_delim.length
|
|
390
|
+
next
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
key_part = prefix[0...-1].rstrip
|
|
394
|
+
k_end = key_part.length
|
|
395
|
+
k_start = k_end
|
|
396
|
+
# Check each character directly (no regex) — no backtracking risk.
|
|
397
|
+
while k_start > 0
|
|
398
|
+
c = key_part[k_start - 1]
|
|
399
|
+
break unless (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") ||
|
|
400
|
+
(c >= "0" && c <= "9") || c == "_"
|
|
401
|
+
|
|
402
|
+
k_start -= 1
|
|
403
|
+
end
|
|
404
|
+
if k_start < k_end
|
|
405
|
+
k = key_part[k_start...k_end]
|
|
406
|
+
params[k] ||= params_raw[val_start...close_pos]
|
|
407
|
+
end
|
|
408
|
+
search_pos = close_pos + @string_delim.length
|
|
409
|
+
end
|
|
410
|
+
params
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def unescape_native_value(s)
|
|
414
|
+
s.gsub('\\"', '"').gsub("\\'", "'").gsub("\\n", "\n").gsub("\\\\", "\\")
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# ── Qwen 3.6 ────────────────────────────────────────────────────────────
|
|
419
|
+
# Per-profile strategy for Qwen 3.6 (function/parameter XML blocks and literal
|
|
420
|
+
# think tokens). Method bodies are ported verbatim from KernelLoop.
|
|
421
|
+
class Qwen
|
|
422
|
+
def initialize(profile)
|
|
423
|
+
@profile = profile
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def parse(text)
|
|
427
|
+
results = []
|
|
428
|
+
|
|
429
|
+
pos = 0
|
|
430
|
+
tool_open = "<tool_call>"
|
|
431
|
+
tool_close = "</tool_call>"
|
|
432
|
+
|
|
433
|
+
while (open_pos = text.index(tool_open, pos))
|
|
434
|
+
body_start = open_pos + tool_open.length
|
|
435
|
+
close_pos = text.index(tool_close, body_start)
|
|
436
|
+
break unless close_pos
|
|
437
|
+
|
|
438
|
+
body = text[body_start...close_pos]
|
|
439
|
+
if (name = qwen_function_name(body))
|
|
440
|
+
params = qwen_params(body)
|
|
441
|
+
results << qwen_call_to_internal(name, params)
|
|
442
|
+
end
|
|
443
|
+
pos = close_pos + tool_close.length
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
results
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def strip_thought(text)
|
|
450
|
+
# Remove all complete <think>...</think> blocks with the newlines after them
|
|
451
|
+
result = text.gsub(/<think>.*?<\/think>\n*/m, '')
|
|
452
|
+
# Remove any stray opening tags
|
|
453
|
+
result = result.gsub(/<think>.*?(?=\n|$)/m, '')
|
|
454
|
+
# Remove any orphaned closing tags (and the whitespace around them)
|
|
455
|
+
# The answer's own blank lines stay: they are its markdown paragraphs
|
|
456
|
+
# and lists (a chat host's answer is saved as stripped here).
|
|
457
|
+
result.gsub(/^\s*<\/think>\s*/m, '')
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def strip_tool_calls(text)
|
|
461
|
+
ToolCallParser.strip_calls(text, "<tool_call>", "</tool_call>")
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def parse_with_recovery(response, partial_fragment)
|
|
465
|
+
# If the previous turn opened a tool-call block the model never closed,
|
|
466
|
+
# prepend that fragment to this response before parsing. Returns
|
|
467
|
+
# [calls, fragment_or_nil]; a non-nil fragment signals an incomplete call.
|
|
468
|
+
input = partial_fragment ? (partial_fragment + response.to_s) : response.to_s
|
|
469
|
+
[parse(input), qwen_unterminated_tool_call_fragment(input)]
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
private
|
|
473
|
+
|
|
474
|
+
def qwen_function_name(body)
|
|
475
|
+
match = body.match(/<function=([a-z0-9_]+)(?:>|(?=<)|$)/i)
|
|
476
|
+
match && match[1].to_s.downcase
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def qwen_params(body)
|
|
480
|
+
params = {}
|
|
481
|
+
|
|
482
|
+
body.scan(/<parameter=(\w+)>(.*?)<\/parameter>/m) do |key, value|
|
|
483
|
+
params[key.to_s.downcase] = trim_tag_newline(value)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
body.scan(/<arg_key>(.*?)<\/arg_key>\s*<arg_value>(.*?)<\/arg_value>/m) do |key, value|
|
|
487
|
+
params[key.to_s.downcase.strip] = trim_tag_newline(value)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
params
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def trim_tag_newline(value)
|
|
494
|
+
value.sub(/\A\r?\n/, "").sub(/\r?\n\z/, "")
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def qwen_unterminated_tool_call_fragment(text)
|
|
498
|
+
open_tag = "<tool_call>"
|
|
499
|
+
close_tag = "</tool_call>"
|
|
500
|
+
pos = 0
|
|
501
|
+
|
|
502
|
+
while (open_pos = text.index(open_tag, pos))
|
|
503
|
+
body_start = open_pos + open_tag.length
|
|
504
|
+
close_pos = text.index(close_tag, body_start)
|
|
505
|
+
return text[open_pos..] unless close_pos
|
|
506
|
+
|
|
507
|
+
pos = close_pos + close_tag.length
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
nil
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def qwen_call_to_internal(name, params)
|
|
514
|
+
case name
|
|
515
|
+
when Tools::Execute::NAME
|
|
516
|
+
{ name: name, content: qwen_param_value(params, "command"), path: nil, scope: nil }
|
|
517
|
+
when Tools::Read::NAME
|
|
518
|
+
{
|
|
519
|
+
name: name,
|
|
520
|
+
content: qwen_param_value(params, "path"),
|
|
521
|
+
path: nil,
|
|
522
|
+
scope: nil,
|
|
523
|
+
start_line: qwen_param_value(params, "start_line"),
|
|
524
|
+
end_line: qwen_param_value(params, "end_line")
|
|
525
|
+
}
|
|
526
|
+
when Tools::Write::NAME
|
|
527
|
+
content = qwen_param_value(params, "content", "text", strip: false)
|
|
528
|
+
{ name: name, content: content, path: qwen_param_value(params, "path"), scope: nil }
|
|
529
|
+
when Tools::MemoryRead::NAME
|
|
530
|
+
memory_name = qwen_param_value(params, "name")
|
|
531
|
+
{ name: name, content: memory_name, path: nil, scope: qwen_param_value(params, "scope") }
|
|
532
|
+
when Tools::MemoryWrite::NAME
|
|
533
|
+
entry_name = qwen_param_value(params, "name")
|
|
534
|
+
content = qwen_param_value(params, "content", "text", "body", "value", strip: false)
|
|
535
|
+
{ name: name, content: content, path: entry_name, scope: qwen_param_value(params, "scope"), description: qwen_param_value(params, "description"),
|
|
536
|
+
current_model_only: qwen_param_value(params, "current_model_only") }
|
|
537
|
+
when Tools::Edit::NAME
|
|
538
|
+
old_text = qwen_param_value(params, "old_text", "old", strip: false)
|
|
539
|
+
new_text = qwen_param_value(params, "new_text", "new", strip: false)
|
|
540
|
+
content = "<old>#{old_text}</old><new>#{new_text}</new>"
|
|
541
|
+
{
|
|
542
|
+
name: name,
|
|
543
|
+
content: content,
|
|
544
|
+
path: qwen_param_value(params, "path"),
|
|
545
|
+
scope: nil,
|
|
546
|
+
start_line: qwen_param_value(params, "start_line"),
|
|
547
|
+
end_line: qwen_param_value(params, "end_line")
|
|
548
|
+
}
|
|
549
|
+
when Tools::TaskCreate::NAME
|
|
550
|
+
{
|
|
551
|
+
name: name,
|
|
552
|
+
content: qwen_param_value(params, "command"),
|
|
553
|
+
path: nil,
|
|
554
|
+
scope: nil,
|
|
555
|
+
cwd: qwen_param_value(params, "cwd"),
|
|
556
|
+
env: qwen_param_value(params, "env", strip: false)
|
|
557
|
+
}
|
|
558
|
+
when Tools::TaskGet::NAME, Tools::TaskStop::NAME
|
|
559
|
+
{
|
|
560
|
+
name: name,
|
|
561
|
+
content: qwen_param_value(params, "id", "task_id"),
|
|
562
|
+
path: nil,
|
|
563
|
+
scope: nil
|
|
564
|
+
}
|
|
565
|
+
when Tools::TaskWait::NAME
|
|
566
|
+
{
|
|
567
|
+
name: name,
|
|
568
|
+
content: qwen_param_value(params, "id", "task_id"),
|
|
569
|
+
path: nil,
|
|
570
|
+
scope: nil,
|
|
571
|
+
timeout: qwen_param_value(params, "timeout"),
|
|
572
|
+
tail_lines: qwen_param_value(params, "tail_lines"),
|
|
573
|
+
done_pattern: qwen_param_value(params, "done_pattern")
|
|
574
|
+
}
|
|
575
|
+
when Tools::TaskList::NAME
|
|
576
|
+
{ name: name, content: "", path: nil, scope: nil }
|
|
577
|
+
when Tools::WebFetch::NAME
|
|
578
|
+
{ name: name, content: qwen_param_value(params, "url"), path: nil, scope: nil }
|
|
579
|
+
when Tools::RegisterReminder::NAME
|
|
580
|
+
{
|
|
581
|
+
name: name,
|
|
582
|
+
content: qwen_param_value(params, "name"),
|
|
583
|
+
path: nil,
|
|
584
|
+
scope: nil,
|
|
585
|
+
description: qwen_param_value(params, "description"),
|
|
586
|
+
interval_minutes: qwen_param_value(params, "interval_minutes")
|
|
587
|
+
}
|
|
588
|
+
when Tools::CancelReminder::NAME
|
|
589
|
+
{ name: name, content: qwen_param_value(params, "name"), path: nil, scope: nil }
|
|
590
|
+
when Tools::ListReminders::NAME
|
|
591
|
+
{ name: name, content: "", path: nil, scope: nil }
|
|
592
|
+
when Tools::ListSessions::NAME
|
|
593
|
+
{ name: name, content: "", path: nil, scope: nil, cwd: qwen_param_value(params, "cwd") }
|
|
594
|
+
when Tools::SendNote::NAME
|
|
595
|
+
{ name: name, content: qwen_param_value(params, "text"), path: nil, scope: nil,
|
|
596
|
+
session: qwen_param_value(params, "session") }
|
|
597
|
+
when Tools::Delegate::NAME
|
|
598
|
+
{ name: name, content: qwen_param_value(params, "task"), path: nil, scope: nil,
|
|
599
|
+
model: qwen_param_value(params, "model"), session: qwen_param_value(params, "session"),
|
|
600
|
+
wait: qwen_param_value(params, "wait"), timeout: qwen_param_value(params, "timeout") }
|
|
601
|
+
when Tools::DelegateResult::NAME
|
|
602
|
+
{ name: name, content: "", path: nil, scope: nil,
|
|
603
|
+
session: qwen_param_value(params, "session"), timeout: qwen_param_value(params, "timeout") }
|
|
604
|
+
when Tools::AskUserQuestion::NAME
|
|
605
|
+
opts_raw = qwen_param_value(params, "options", strip: false)
|
|
606
|
+
opts = qwen_ask_options(opts_raw)
|
|
607
|
+
{
|
|
608
|
+
name: name,
|
|
609
|
+
content: qwen_param_value(params, "question"),
|
|
610
|
+
path: nil, scope: nil,
|
|
611
|
+
question: qwen_param_value(params, "question"),
|
|
612
|
+
options: opts,
|
|
613
|
+
header: qwen_param_value(params, "header"),
|
|
614
|
+
multi_select: qwen_param_value(params, "multi_select"),
|
|
615
|
+
allow_freeform: qwen_param_value(params, "allow_freeform")
|
|
616
|
+
}
|
|
617
|
+
else
|
|
618
|
+
# A registry (plugin) tool, or an unknown one: the parameters as
|
|
619
|
+
# given (text; typed by the schema at dispatch).
|
|
620
|
+
{ name: name, content: params.to_s, path: nil, scope: nil, args: params }
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def qwen_param_value(params, *keys, strip: true)
|
|
625
|
+
value = keys.lazy.map { |key| params[key] }.find { |candidate| !candidate.nil? }
|
|
626
|
+
return "" if value.nil?
|
|
627
|
+
|
|
628
|
+
strip ? value.to_s.strip : value.to_s
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def qwen_ask_options(raw)
|
|
632
|
+
norm = Samagotchi::Tools::AskUserQuestion.normalize_options_lenient(raw)
|
|
633
|
+
norm.empty? ? nil : norm
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
end
|