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.
Files changed (243) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE +21 -0
  4. data/README.md +126 -0
  5. data/bin/chi +1140 -0
  6. data/docs/architecture.md +299 -0
  7. data/docs/cli.md +490 -0
  8. data/docs/configuration.md +494 -0
  9. data/docs/desktop.md +97 -0
  10. data/docs/guardrails.md +218 -0
  11. data/docs/hooks.md +309 -0
  12. data/docs/internals/background-tasks.md +26 -0
  13. data/docs/internals/context-telemetry.md +36 -0
  14. data/docs/internals/gemma4-contract.md +23 -0
  15. data/docs/internals/tool-guardrails.md +45 -0
  16. data/docs/memory.md +85 -0
  17. data/docs/plugins.md +819 -0
  18. data/docs/releasing.md +135 -0
  19. data/docs/sessions.md +155 -0
  20. data/lib/samagotchi/bridge/bounded_queue.rb +70 -0
  21. data/lib/samagotchi/bridge/card_store.rb +126 -0
  22. data/lib/samagotchi/bridge/event_id.rb +25 -0
  23. data/lib/samagotchi/bridge/ring_buffer.rb +63 -0
  24. data/lib/samagotchi/bridge/sse_writer.rb +248 -0
  25. data/lib/samagotchi/bridge/turn_accumulator.rb +189 -0
  26. data/lib/samagotchi/bridge.rb +993 -0
  27. data/lib/samagotchi/bridge_client/event_stream.rb +158 -0
  28. data/lib/samagotchi/bridge_client/sse_parser.rb +51 -0
  29. data/lib/samagotchi/bridge_client.rb +330 -0
  30. data/lib/samagotchi/bundle_needs.rb +97 -0
  31. data/lib/samagotchi/bundles/btw/manifest.yml +10 -0
  32. data/lib/samagotchi/bundles/btw/plugin.rb +100 -0
  33. data/lib/samagotchi/bundles/guardrails/guardrails/rules.yml +82 -0
  34. data/lib/samagotchi/bundles/guardrails/guardrails.md +14 -0
  35. data/lib/samagotchi/bundles/guardrails/manifest.yml +8 -0
  36. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +210 -0
  37. data/lib/samagotchi/bundles/known-names/known_names.md +3 -0
  38. data/lib/samagotchi/bundles/known-names/manifest.yml +14 -0
  39. data/lib/samagotchi/bundles/loop-guard/manifest.yml +10 -0
  40. data/lib/samagotchi/bundles/loop-guard/plugin.rb +158 -0
  41. data/lib/samagotchi/bundles/mcp/manifest.yml +11 -0
  42. data/lib/samagotchi/bundles/mcp/plugin.rb +631 -0
  43. data/lib/samagotchi/bundles/system/config_modification_protocol.md +149 -0
  44. data/lib/samagotchi/bundles/system/delegated.md +10 -0
  45. data/lib/samagotchi/bundles/system/identity.md +7 -0
  46. data/lib/samagotchi/bundles/system/manifest.yml +11 -0
  47. data/lib/samagotchi/bundles/system/memory_guide.md +107 -0
  48. data/lib/samagotchi/bundles/system/self_map.md +55 -0
  49. data/lib/samagotchi/cancellation_controller.rb +78 -0
  50. data/lib/samagotchi/client.rb +429 -0
  51. data/lib/samagotchi/commands/registry.rb +112 -0
  52. data/lib/samagotchi/config.rb +910 -0
  53. data/lib/samagotchi/context_note.rb +77 -0
  54. data/lib/samagotchi/context_quote.rb +21 -0
  55. data/lib/samagotchi/context_usage.rb +66 -0
  56. data/lib/samagotchi/context_window.rb +76 -0
  57. data/lib/samagotchi/debug_log.rb +110 -0
  58. data/lib/samagotchi/desktop/macos/App.swift +102 -0
  59. data/lib/samagotchi/desktop/macos/ChiRunner.swift +201 -0
  60. data/lib/samagotchi/desktop/macos/Hotkey.swift +42 -0
  61. data/lib/samagotchi/desktop/macos/Info.plist.erb +42 -0
  62. data/lib/samagotchi/desktop/macos/Panel.swift +383 -0
  63. data/lib/samagotchi/desktop/macos.rb +255 -0
  64. data/lib/samagotchi/desktop.rb +21 -0
  65. data/lib/samagotchi/desktop_command.rb +143 -0
  66. data/lib/samagotchi/engine.rb +2807 -0
  67. data/lib/samagotchi/guardrails/approval.rb +125 -0
  68. data/lib/samagotchi/guardrails/approvals.rb +177 -0
  69. data/lib/samagotchi/guardrails/context.rb +71 -0
  70. data/lib/samagotchi/guardrails/gate.rb +125 -0
  71. data/lib/samagotchi/guardrails/load_failures.rb +46 -0
  72. data/lib/samagotchi/guardrails/protected_paths.rb +77 -0
  73. data/lib/samagotchi/guardrails/rules.rb +199 -0
  74. data/lib/samagotchi/guardrails/targets.rb +119 -0
  75. data/lib/samagotchi/guardrails/verdict.rb +134 -0
  76. data/lib/samagotchi/guardrails.rb +18 -0
  77. data/lib/samagotchi/hooks/bundle_loader.rb +158 -0
  78. data/lib/samagotchi/hooks/loader.rb +162 -0
  79. data/lib/samagotchi/hooks/registry.rb +261 -0
  80. data/lib/samagotchi/hooks.rb +30 -0
  81. data/lib/samagotchi/host_registry.rb +315 -0
  82. data/lib/samagotchi/idle_client.rb +147 -0
  83. data/lib/samagotchi/idle_recap.rb +549 -0
  84. data/lib/samagotchi/idle_reminders.rb +101 -0
  85. data/lib/samagotchi/idle_scheduler.rb +76 -0
  86. data/lib/samagotchi/image_store.rb +393 -0
  87. data/lib/samagotchi/installed_gem.rb +38 -0
  88. data/lib/samagotchi/kernel_loop.rb +1017 -0
  89. data/lib/samagotchi/launch_mode.rb +34 -0
  90. data/lib/samagotchi/llm/backend.rb +28 -0
  91. data/lib/samagotchi/llm/chat_loop.rb +450 -0
  92. data/lib/samagotchi/llm/errors.rb +329 -0
  93. data/lib/samagotchi/llm/http.rb +412 -0
  94. data/lib/samagotchi/llm/model_result.rb +72 -0
  95. data/lib/samagotchi/llm/native_backend.rb +50 -0
  96. data/lib/samagotchi/llm/native_tool_normalizer.rb +277 -0
  97. data/lib/samagotchi/llm/openai_chat.rb +403 -0
  98. data/lib/samagotchi/llm/usage.rb +79 -0
  99. data/lib/samagotchi/log.rb +200 -0
  100. data/lib/samagotchi/log_line.rb +127 -0
  101. data/lib/samagotchi/log_path.rb +31 -0
  102. data/lib/samagotchi/log_subscriber.rb +163 -0
  103. data/lib/samagotchi/memory_bundle/builder.rb +364 -0
  104. data/lib/samagotchi/memory_bundle/index_updater.rb +123 -0
  105. data/lib/samagotchi/memory_bundle/installer.rb +528 -0
  106. data/lib/samagotchi/memory_bundle/listing.rb +72 -0
  107. data/lib/samagotchi/memory_bundle/manifest.rb +225 -0
  108. data/lib/samagotchi/memory_bundle/merger.rb +52 -0
  109. data/lib/samagotchi/memory_bundle/placeholder.rb +37 -0
  110. data/lib/samagotchi/memory_bundle/provenance.rb +257 -0
  111. data/lib/samagotchi/memory_bundle/source.rb +153 -0
  112. data/lib/samagotchi/memory_bundle/status.rb +107 -0
  113. data/lib/samagotchi/memory_bundle/system_bundle.rb +161 -0
  114. data/lib/samagotchi/memory_bundle/uninstaller.rb +128 -0
  115. data/lib/samagotchi/memory_bundle.rb +17 -0
  116. data/lib/samagotchi/memory_paths.rb +101 -0
  117. data/lib/samagotchi/model_overlay.rb +53 -0
  118. data/lib/samagotchi/model_profile.rb +309 -0
  119. data/lib/samagotchi/muted_memories.rb +66 -0
  120. data/lib/samagotchi/note_command.rb +163 -0
  121. data/lib/samagotchi/output_formatter.rb +100 -0
  122. data/lib/samagotchi/owner_lock.rb +110 -0
  123. data/lib/samagotchi/pending_input_queue.rb +48 -0
  124. data/lib/samagotchi/plugin/api.rb +362 -0
  125. data/lib/samagotchi/plugin/context.rb +193 -0
  126. data/lib/samagotchi/plugin/loader.rb +126 -0
  127. data/lib/samagotchi/plugin/service.rb +117 -0
  128. data/lib/samagotchi/plugin/sessions.rb +150 -0
  129. data/lib/samagotchi/plugin/side_question.rb +60 -0
  130. data/lib/samagotchi/plugin/tool_result.rb +24 -0
  131. data/lib/samagotchi/project_scope.rb +25 -0
  132. data/lib/samagotchi/prompt.rb +119 -0
  133. data/lib/samagotchi/prompt_literal_guard.rb +70 -0
  134. data/lib/samagotchi/recap_store.rb +92 -0
  135. data/lib/samagotchi/reminder_store.rb +165 -0
  136. data/lib/samagotchi/self_report.rb +195 -0
  137. data/lib/samagotchi/send_command.rb +170 -0
  138. data/lib/samagotchi/served_model.rb +32 -0
  139. data/lib/samagotchi/session.rb +508 -0
  140. data/lib/samagotchi/session_commands.rb +527 -0
  141. data/lib/samagotchi/session_delete_command.rb +105 -0
  142. data/lib/samagotchi/session_manager.rb +1049 -0
  143. data/lib/samagotchi/session_metrics.rb +466 -0
  144. data/lib/samagotchi/session_observer.rb +117 -0
  145. data/lib/samagotchi/terminal_ui/attach_launcher.rb +118 -0
  146. data/lib/samagotchi/terminal_ui/attached_loop.rb +1037 -0
  147. data/lib/samagotchi/terminal_ui/attached_view.rb +264 -0
  148. data/lib/samagotchi/terminal_ui/event_renderer.rb +192 -0
  149. data/lib/samagotchi/terminal_ui/formatting.rb +291 -0
  150. data/lib/samagotchi/terminal_ui/image_input.rb +36 -0
  151. data/lib/samagotchi/terminal_ui/input_support.rb +324 -0
  152. data/lib/samagotchi/terminal_ui/legacy_surface.rb +111 -0
  153. data/lib/samagotchi/terminal_ui/line_reader.rb +113 -0
  154. data/lib/samagotchi/terminal_ui/live_region.rb +36 -0
  155. data/lib/samagotchi/terminal_ui/plain_surface.rb +51 -0
  156. data/lib/samagotchi/terminal_ui/question_prompt.rb +153 -0
  157. data/lib/samagotchi/terminal_ui/question_slot.rb +131 -0
  158. data/lib/samagotchi/terminal_ui/reline_seam.rb +216 -0
  159. data/lib/samagotchi/terminal_ui/repl_input.rb +138 -0
  160. data/lib/samagotchi/terminal_ui/screen.rb +316 -0
  161. data/lib/samagotchi/terminal_ui/surface.rb +47 -0
  162. data/lib/samagotchi/terminal_ui/thinking_line.rb +101 -0
  163. data/lib/samagotchi/terminal_ui.rb +1992 -0
  164. data/lib/samagotchi/thinking_ticker.rb +110 -0
  165. data/lib/samagotchi/thought_stream_splitter.rb +149 -0
  166. data/lib/samagotchi/token_usage.rb +88 -0
  167. data/lib/samagotchi/tool_activity.rb +216 -0
  168. data/lib/samagotchi/tool_call_parser.rb +637 -0
  169. data/lib/samagotchi/tool_declarations.rb +561 -0
  170. data/lib/samagotchi/tool_runner.rb +211 -0
  171. data/lib/samagotchi/tools/args.rb +259 -0
  172. data/lib/samagotchi/tools/ask_user_question.rb +152 -0
  173. data/lib/samagotchi/tools/builtins.rb +122 -0
  174. data/lib/samagotchi/tools/cancel_reminder.rb +21 -0
  175. data/lib/samagotchi/tools/delegate.rb +167 -0
  176. data/lib/samagotchi/tools/delegate_result.rb +53 -0
  177. data/lib/samagotchi/tools/delegate_wait.rb +153 -0
  178. data/lib/samagotchi/tools/edit.rb +155 -0
  179. data/lib/samagotchi/tools/execute.rb +214 -0
  180. data/lib/samagotchi/tools/list_reminders.rb +20 -0
  181. data/lib/samagotchi/tools/list_sessions.rb +74 -0
  182. data/lib/samagotchi/tools/memory.rb +256 -0
  183. data/lib/samagotchi/tools/output_guardrails.rb +93 -0
  184. data/lib/samagotchi/tools/peers.rb +18 -0
  185. data/lib/samagotchi/tools/read.rb +182 -0
  186. data/lib/samagotchi/tools/register_reminder.rb +53 -0
  187. data/lib/samagotchi/tools/registry.rb +60 -0
  188. data/lib/samagotchi/tools/send_note.rb +49 -0
  189. data/lib/samagotchi/tools/task_create.rb +29 -0
  190. data/lib/samagotchi/tools/task_get.rb +39 -0
  191. data/lib/samagotchi/tools/task_list.rb +43 -0
  192. data/lib/samagotchi/tools/task_runtime.rb +311 -0
  193. data/lib/samagotchi/tools/task_stop.rb +29 -0
  194. data/lib/samagotchi/tools/task_wait.rb +104 -0
  195. data/lib/samagotchi/tools/tool_path.rb +18 -0
  196. data/lib/samagotchi/tools/web_fetch.rb +163 -0
  197. data/lib/samagotchi/tools/write.rb +26 -0
  198. data/lib/samagotchi/turn_flow.rb +242 -0
  199. data/lib/samagotchi/turn_note.rb +76 -0
  200. data/lib/samagotchi/turn_tally.rb +101 -0
  201. data/lib/samagotchi/version.rb +7 -0
  202. data/lib/samagotchi/vision_context.rb +132 -0
  203. data/lib/samagotchi/vision_support.rb +109 -0
  204. data/lib/samagotchi/web/app.rb +1349 -0
  205. data/lib/samagotchi/web/markdown_renderer.rb +107 -0
  206. data/lib/samagotchi/web/message_parts.rb +169 -0
  207. data/lib/samagotchi/web/public/activity.js +100 -0
  208. data/lib/samagotchi/web/public/annotations.js +67 -0
  209. data/lib/samagotchi/web/public/app.js +2382 -0
  210. data/lib/samagotchi/web/public/card.js +74 -0
  211. data/lib/samagotchi/web/public/chat_view.js +360 -0
  212. data/lib/samagotchi/web/public/chunk_router.js +25 -0
  213. data/lib/samagotchi/web/public/command_complete.js +39 -0
  214. data/lib/samagotchi/web/public/composer_size.js +19 -0
  215. data/lib/samagotchi/web/public/copy.js +142 -0
  216. data/lib/samagotchi/web/public/ctx.js +35 -0
  217. data/lib/samagotchi/web/public/data.js +256 -0
  218. data/lib/samagotchi/web/public/format.js +232 -0
  219. data/lib/samagotchi/web/public/hold.js +78 -0
  220. data/lib/samagotchi/web/public/images.js +77 -0
  221. data/lib/samagotchi/web/public/index.html +568 -0
  222. data/lib/samagotchi/web/public/init_row.js +60 -0
  223. data/lib/samagotchi/web/public/model_pick.js +23 -0
  224. data/lib/samagotchi/web/public/question_card.js +100 -0
  225. data/lib/samagotchi/web/public/route.js +17 -0
  226. data/lib/samagotchi/web/public/scope.js +36 -0
  227. data/lib/samagotchi/web/public/scroll.js +24 -0
  228. data/lib/samagotchi/web/public/sentences.js +88 -0
  229. data/lib/samagotchi/web/public/sessions_list.js +60 -0
  230. data/lib/samagotchi/web/public/strip.js +25 -0
  231. data/lib/samagotchi/web/public/tally.js +37 -0
  232. data/lib/samagotchi/web/public/thinking_ticker.js +79 -0
  233. data/lib/samagotchi/web/public/timing.js +185 -0
  234. data/lib/samagotchi/web/public/turn_events.js +209 -0
  235. data/lib/samagotchi/web/public/turn_model.js +204 -0
  236. data/lib/samagotchi/web/public/turn_view.js +587 -0
  237. data/lib/samagotchi/web/server.rb +183 -0
  238. data/lib/samagotchi/web/session_hub.rb +329 -0
  239. data/lib/samagotchi/web/session_summary.rb +85 -0
  240. data/lib/samagotchi/worker.rb +635 -0
  241. data/lib/samagotchi/worker_idle_exit.rb +87 -0
  242. data/lib/samagotchi.rb +12 -0
  243. metadata +374 -0
@@ -0,0 +1,561 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "tools/args"
5
+
6
+ module Samagotchi
7
+ # Tool-declaration constants, protocol constants, and guidance text.
8
+ #
9
+ # Placed in a dedicated module so that both Engine and any future UI can
10
+ # reference declarations without instantiating an Engine.
11
+ module ToolDeclarations
12
+ # ── Tool schemas (declared once) ──────────────────────────────────────────
13
+ #
14
+ # Every tool's name, description and JSON Schema parameters. The Gemma 4
15
+ # and Qwen 3.6 prompt declarations and the chat path's tools: are all
16
+ # rendered from this table, in this order.
17
+
18
+ TOOL_SCHEMAS = [
19
+ {
20
+ name: "execute",
21
+ description: "Run any shell command in a working directory (defaults to the project root) and see stdout, stderr, and exit code. Large output may be truncated to a head+tail preview with metadata.",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {
25
+ command: {
26
+ type: "string",
27
+ description: "The shell command to run"
28
+ },
29
+ cwd: {
30
+ type: "string",
31
+ description: "Optional working directory to run in (defaults to the project root). Relative paths are resolved against the project root."
32
+ }
33
+ },
34
+ required: ["command"]
35
+ }
36
+ },
37
+ {
38
+ name: "read",
39
+ description: "Read a file from disk. Large files may be truncated to a head+tail preview with metadata. Optionally pass start_line and end_line (1-based, inclusive) to read only a specific line range. An image file (png, jpeg, gif, webp) comes back as the picture itself: read it to see it.",
40
+ parameters: {
41
+ type: "object",
42
+ properties: {
43
+ path: {
44
+ type: "string",
45
+ description: "Path to the file"
46
+ },
47
+ start_line: {
48
+ type: "integer",
49
+ description: "Optional start line (1-based, inclusive). Must be provided with end_line."
50
+ },
51
+ end_line: {
52
+ type: "integer",
53
+ description: "Optional end line (1-based, inclusive). Must be provided with start_line."
54
+ }
55
+ },
56
+ required: ["path"]
57
+ }
58
+ },
59
+ {
60
+ name: "write",
61
+ description: "Write content to a file (parent directories are created automatically)",
62
+ parameters: {
63
+ type: "object",
64
+ properties: {
65
+ path: {
66
+ type: "string",
67
+ description: "Destination file path"
68
+ },
69
+ content: {
70
+ type: "string",
71
+ description: "Content to write to the file"
72
+ }
73
+ },
74
+ required: ["path", "content"]
75
+ }
76
+ },
77
+ {
78
+ name: "edit",
79
+ description: "Edit an existing file. Mode 1 (default): replace an exact old_text block with new_text, where old_text must appear exactly once. Mode 2 (range): when start_line and end_line are provided, replace that whole line range with new_text.",
80
+ parameters: {
81
+ type: "object",
82
+ properties: {
83
+ path: {
84
+ type: "string",
85
+ description: "File path"
86
+ },
87
+ old_text: {
88
+ type: "string",
89
+ description: "Exact text to replace (required in exact-match mode)"
90
+ },
91
+ new_text: {
92
+ type: "string",
93
+ description: "Replacement text"
94
+ },
95
+ start_line: {
96
+ type: "integer",
97
+ description: "Optional start line (1-based, inclusive) for range mode. Must be provided with end_line."
98
+ },
99
+ end_line: {
100
+ type: "integer",
101
+ description: "Optional end line (1-based, inclusive) for range mode. Must be provided with start_line."
102
+ }
103
+ },
104
+ required: ["path", "new_text"]
105
+ }
106
+ },
107
+ {
108
+ name: "memory_read",
109
+ description: "Read a memory entry from scoped memories. Scope is optional: if omitted, read falls back from project to system. Leave name blank to read indexes.",
110
+ parameters: {
111
+ type: "object",
112
+ properties: {
113
+ name: {
114
+ type: "string",
115
+ description: "Memory entry name without .md extension; leave blank for indexes"
116
+ },
117
+ scope: {
118
+ type: "string",
119
+ description: "Optional scope: project or system"
120
+ }
121
+ }
122
+ }
123
+ },
124
+ {
125
+ name: "memory_write",
126
+ description: "Write or update a memory entry in scoped memories. Scope is required: project or system. Use the `name` parameter for the entry name (use `name`, NOT `path` — the file tools use `path`); each scope's index.md is auto-maintained (one managed line per entry); use name \"index\" to write the index file verbatim. If the user asks to save guidance for the current model only, pass current_model_only: true to save a model-specific overlay.",
127
+ parameters: {
128
+ type: "object",
129
+ properties: {
130
+ name: {
131
+ type: "string",
132
+ description: "Memory entry name without .md extension"
133
+ },
134
+ content: {
135
+ type: "string",
136
+ description: "Markdown content to write"
137
+ },
138
+ scope: {
139
+ type: "string",
140
+ description: "Scope to write into: project or system"
141
+ },
142
+ description: {
143
+ type: "string",
144
+ description: "Optional short description appended to the managed index line"
145
+ },
146
+ current_model_only: {
147
+ type: "boolean",
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
+ }
150
+ },
151
+ required: ["name", "content", "scope"]
152
+ }
153
+ },
154
+ {
155
+ name: "task_create",
156
+ description: "Start a background task for a long-running shell command. Returns task id and output path for later inspection.",
157
+ parameters: {
158
+ type: "object",
159
+ properties: {
160
+ command: {
161
+ type: "string",
162
+ description: "Shell command to run in the background"
163
+ },
164
+ cwd: {
165
+ type: "string",
166
+ description: "Optional working directory (defaults to project root)"
167
+ },
168
+ env: {
169
+ type: "string",
170
+ description: "Optional JSON object of environment overrides; use this for PATH or tool-specific variables"
171
+ }
172
+ },
173
+ required: ["command"]
174
+ }
175
+ },
176
+ {
177
+ name: "task_get",
178
+ description: "Get full metadata for a task by id. Use read on output_path to inspect command output.",
179
+ parameters: {
180
+ type: "object",
181
+ properties: {
182
+ id: {
183
+ type: "string",
184
+ description: "Task id returned by task_create"
185
+ }
186
+ },
187
+ required: ["id"]
188
+ }
189
+ },
190
+ {
191
+ name: "task_list",
192
+ description: "List all background tasks in the current workspace with current status.",
193
+ parameters: {
194
+ type: "object",
195
+ properties: {}
196
+ }
197
+ },
198
+ {
199
+ name: "task_stop",
200
+ description: "Stop a running background task by id.",
201
+ parameters: {
202
+ type: "object",
203
+ properties: {
204
+ id: {
205
+ type: "string",
206
+ description: "Task id to stop"
207
+ }
208
+ },
209
+ required: ["id"]
210
+ }
211
+ },
212
+ {
213
+ name: "task_wait",
214
+ description: "Wait for a background task to finish. Polls every 0.5s until completion, a log pattern matches, or timeout. Timed-out waits include a bounded output tail.",
215
+ parameters: {
216
+ type: "object",
217
+ properties: {
218
+ task_id: {
219
+ type: "string",
220
+ description: "Task id returned by task_create"
221
+ },
222
+ timeout: {
223
+ type: "integer",
224
+ description: "Maximum seconds to wait (default: 600)"
225
+ },
226
+ tail_lines: {
227
+ type: "integer",
228
+ description: "Log lines to return when timing out or matching a pattern (default: 10, max: 100)"
229
+ },
230
+ done_pattern: {
231
+ type: "string",
232
+ description: "Optional regular expression that returns early when it matches the recent log output"
233
+ }
234
+ },
235
+ required: ["task_id"]
236
+ }
237
+ },
238
+ {
239
+ name: "web_fetch",
240
+ description: "Fetch the content of a URL (HTML or text) and return cleaned text. Handles HTML by stripping scripts/styles and extracting visible text. Returns error messages for invalid URLs or HTTP errors.",
241
+ parameters: {
242
+ type: "object",
243
+ properties: {
244
+ url: {
245
+ type: "string",
246
+ description: "The URL to fetch"
247
+ }
248
+ },
249
+ required: ["url"]
250
+ }
251
+ },
252
+ {
253
+ name: "register_reminder",
254
+ description: "Register a periodic reminder. The harness injects a [SYSTEM:] message into your next idle turn when the reminder is due.",
255
+ parameters: {
256
+ type: "object",
257
+ properties: {
258
+ name: {
259
+ type: "string",
260
+ description: "Short identifier, e.g. 'api_health'"
261
+ },
262
+ description: {
263
+ type: "string",
264
+ description: "What should happen when this reminder fires"
265
+ },
266
+ interval_minutes: {
267
+ type: "integer",
268
+ description: "How often to remind (1-1440 minutes, i.e. up to 1 day)"
269
+ }
270
+ },
271
+ required: ["name", "description", "interval_minutes"]
272
+ }
273
+ },
274
+ {
275
+ name: "cancel_reminder",
276
+ description: "Cancel a previously registered reminder so it stops firing.",
277
+ parameters: {
278
+ type: "object",
279
+ properties: {
280
+ name: {
281
+ type: "string",
282
+ description: "The reminder identifier to cancel"
283
+ }
284
+ },
285
+ required: ["name"]
286
+ }
287
+ },
288
+ {
289
+ name: "list_reminders",
290
+ description: "List all active registered reminders.",
291
+ parameters: {
292
+ type: "object",
293
+ properties: {}
294
+ }
295
+ },
296
+ {
297
+ name: "list_sessions",
298
+ description: "List the other chi sessions of this project (newest first, up to 20): id, whether a worker runs it, its folder and last prompt; other projects: cwd \"/\". Use it to find the session send_note should go to.",
299
+ parameters: {
300
+ type: "object",
301
+ properties: {
302
+ cwd: {
303
+ type: "string",
304
+ description: "Only sessions in this folder or below it, in any project (optional)"
305
+ }
306
+ }
307
+ }
308
+ },
309
+ {
310
+ name: "send_note",
311
+ description: "Send a context note to another chi session: background information it sees on its next turn, attributed to this session. It does not start a turn there or ask it to do anything.",
312
+ parameters: {
313
+ type: "object",
314
+ properties: {
315
+ session: {
316
+ type: "string",
317
+ description: "The target session's id or a unique prefix of it (from list_sessions)"
318
+ },
319
+ text: {
320
+ type: "string",
321
+ description: "The note: what the other session should know (up to 16 KiB)"
322
+ }
323
+ },
324
+ required: ["session", "text"]
325
+ }
326
+ },
327
+ {
328
+ name: "delegate",
329
+ description: "Hand a task to a child chi session that runs in parallel in this folder and returns only its final reply (its trace stays out of this context). The child is a normal session: it shows in the lists as a child of this one and the user can attach to it. With session, send a follow-up to one of this session's children instead. Children keep running if this turn is canceled; delegate_result waits for them later. A child stays until stopped: once its work is done, stop it with execute `chi sessions stop ID` (a follow-up with session wakes it again).",
330
+ parameters: {
331
+ type: "object",
332
+ properties: {
333
+ task: {
334
+ type: "string",
335
+ description: "The task, as the child's first message: self-contained, with what to report back"
336
+ },
337
+ model: {
338
+ type: "string",
339
+ description: "The child's model (a name or alias; default: this session's)"
340
+ },
341
+ session: {
342
+ type: "string",
343
+ description: "A child's id or prefix: send the task there as a follow-up instead of starting a new child"
344
+ },
345
+ wait: {
346
+ type: "boolean",
347
+ description: "Wait for the reply (default true); false returns at once so several children can run side by side"
348
+ },
349
+ timeout: {
350
+ type: "integer",
351
+ description: "Maximum seconds to wait (default: 600); a timed-out child keeps running"
352
+ }
353
+ },
354
+ required: ["task"]
355
+ }
356
+ },
357
+ {
358
+ name: "delegate_result",
359
+ description: "Wait for a delegated child session's next reply: the child named, or this session's newest running child. Returns early if the child waits for an approval or a question the user must answer.",
360
+ parameters: {
361
+ type: "object",
362
+ properties: {
363
+ session: {
364
+ type: "string",
365
+ description: "The child's id or prefix (default: the newest running child)"
366
+ },
367
+ timeout: {
368
+ type: "integer",
369
+ description: "Maximum seconds to wait (default: 600)"
370
+ }
371
+ }
372
+ }
373
+ },
374
+ {
375
+ name: "ask_user_question",
376
+ description: "Ask the user a structured qualification question. Supports single/multi selection plus optional freeform/Other input. Prefer this over plain numbered lists when you need a clear choice. The harness renders it natively and returns {selected, freeform}.",
377
+ parameters: {
378
+ type: "object",
379
+ properties: {
380
+ question: {
381
+ type: "string",
382
+ description: "The question to ask the user"
383
+ },
384
+ options: {
385
+ type: "array",
386
+ description: "2-8 answer options as strings (labels)",
387
+ items: { type: "string" }
388
+ },
389
+ header: {
390
+ type: "string",
391
+ description: "Optional short header/title"
392
+ },
393
+ multi_select: {
394
+ type: "boolean",
395
+ description: "Allow selecting multiple options (comma-separated in TUI). Default false."
396
+ },
397
+ allow_freeform: {
398
+ type: "boolean",
399
+ description: "Allow freeform/Other text alongside selection. Default false."
400
+ }
401
+ },
402
+ required: ["question", "options"]
403
+ }
404
+ }
405
+ ].freeze
406
+
407
+ # Where Gemma's declaration text says something the schema doesn't: extra
408
+ # description text, and explicit required:false on optional parameters.
409
+ GEMMA_PARAM_OVERRIDES = {
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
+ "ask_user_question" => {
427
+ options: { description: "2-8 answer options as strings (labels). Single/multi selection via multi_select flag." }
428
+ }
429
+ }.freeze
430
+
431
+ # What only the chat path's JSON Schemas carry (F3): enums the prompt
432
+ # text states in words. Adding them to TOOL_SCHEMAS would change the
433
+ # Qwen prompt, which renders the table as JSON.
434
+ CHAT_PARAM_OVERRIDES = {
435
+ "memory_read" => { scope: { enum: %w[project system] } },
436
+ "memory_write" => { scope: { enum: %w[project system] } }
437
+ }.freeze
438
+
439
+ GEMMA_QUOTE = '<|"|>'
440
+
441
+ module_function
442
+
443
+ # Gemma 4 <|tool>declaration:NAME{…}<tool|> blocks for every tool, one per line group.
444
+ # @param schemas [Array<Hash>] a Tools::Registry's schemas
445
+ def gemma_declarations(schemas = TOOL_SCHEMAS)
446
+ schemas.map { |schema| gemma_declaration(schema) }.join("\n")
447
+ end
448
+
449
+ def gemma_declaration(schema)
450
+ q = GEMMA_QUOTE
451
+ required = Array(schema[:parameters][:required])
452
+ overrides = GEMMA_PARAM_OVERRIDES.fetch(schema[:name], {})
453
+ lines = schema[:parameters][:properties].map do |name, param|
454
+ override = overrides.fetch(name, {})
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}}"
460
+ end
461
+ params = lines.empty? ? " parameters:{}" : " parameters:{\n#{lines.join(",\n")}\n }"
462
+ "<|tool>declaration:#{schema[:name]}{\n description:#{q}#{schema[:description]}#{q},\n#{params}\n}<tool|>"
463
+ end
464
+
465
+ # The schemas for the chat path's tools: array: +schemas+ with
466
+ # CHAT_PARAM_OVERRIDES merged in and each tool's parameters closed
467
+ # (additionalProperties: false, unless a plugin's schema says), so a
468
+ # strict provider rejects made-up parameters instead of the tool
469
+ # ignoring them. A plugin's schema goes as it is, nesting and all.
470
+ def chat_schemas(schemas = TOOL_SCHEMAS)
471
+ schemas.map do |schema|
472
+ overrides = CHAT_PARAM_OVERRIDES.fetch(schema[:name], {})
473
+ properties = schema[:parameters][:properties].to_h do |name, param|
474
+ [name, param.merge(overrides.fetch(name, {}))]
475
+ end
476
+ parameters = schema[:parameters].merge(properties: properties)
477
+ parameters = parameters.merge(additionalProperties: false) unless parameters.key?(:additionalProperties)
478
+ schema.merge(parameters: parameters)
479
+ end
480
+ end
481
+
482
+ # The schemas the native (Gemma, Qwen) prompts declare: the built-ins
483
+ # as they are, and each plugin tool's flattened (.flat_schema).
484
+ # @param registry [Tools::Registry]
485
+ def native_schemas(registry)
486
+ registry.entries.map { |entry| entry.core? ? entry.schema : flat_schema(entry.schema) }
487
+ end
488
+
489
+ # A plugin tool's schema in the shape the built-ins have, which is all
490
+ # the native declarations render: each parameter a type and a
491
+ # description. What doesn't fit goes into the description in words: an
492
+ # enum's values, an object's fields, a list's item type. Nested schemas
493
+ # and additionalProperties are dropped; the call's args are still
494
+ # typed by the full schema (Tools::Args).
495
+ def flat_schema(schema)
496
+ parameters = schema[:parameters] || {}
497
+ properties = (parameters[:properties] || {}).to_h { |name, param| [name.to_sym, flat_param(param)] }
498
+ { name: schema[:name], description: schema[:description].to_s,
499
+ parameters: { type: "object", properties: properties, required: Array(parameters[:required]).map(&:to_s) } }
500
+ end
501
+
502
+ def flat_param(param)
503
+ param = {} unless param.is_a?(Hash)
504
+ type = Tools::Args.type_of(param) || "string"
505
+ description = param[:description].to_s.strip
506
+ notes = []
507
+ notes << "One of: #{param[:enum].map { |value| JSON.generate(value) }.join(", ")}." if param[:enum].is_a?(Array)
508
+ case type
509
+ when "object"
510
+ fields = param[:properties].is_a?(Hash) ? param[:properties] : {}
511
+ notes << "A JSON object with #{fields.map { |name, field| "#{name} (#{field_type(field)})" }.join(", ")}." unless fields.empty?
512
+ when "array"
513
+ items = param[:items]
514
+ notes << "A list of #{field_type(items)} values." if items.is_a?(Hash)
515
+ end
516
+ # "Extra fields." then the words: a description without an end mark
517
+ # would run into them.
518
+ description += "." unless description.empty? || notes.empty? || description.match?(/[.!?:;]\z/)
519
+ { type: type, description: [description, *notes].reject(&:empty?).join(" ") }
520
+ end
521
+
522
+ def field_type(field)
523
+ return "any" unless field.is_a?(Hash)
524
+
525
+ type = Tools::Args.type_of(field) || "any"
526
+ field[:enum].is_a?(Array) ? "#{type}: #{field[:enum].map { |value| JSON.generate(value) }.join("|")}" : type
527
+ end
528
+
529
+ # Qwen 3.6 <tools> block: the schemas as pretty-printed JSON.
530
+ def qwen_declarations(schemas = TOOL_SCHEMAS)
531
+ "<tools>\n#{JSON.pretty_generate(schemas)}\n</tools>"
532
+ end
533
+
534
+ TOOL_CALL_HINT = 'To call a tool, emit: <|tool_call>call:NAME{param:<|"|>value<|"|>}<tool_call|>. CRITICAL: check the tool declaration for the exact parameter names and required fields!'
535
+ QWEN_TOOL_CALL_HINT = <<~HINT
536
+ To call a tool, emit XML in this exact shape:
537
+ <tool_call>
538
+ <function=NAME>
539
+ <parameter=KEY>
540
+ VALUE
541
+ </parameter>
542
+ </function>
543
+ </tool_call>
544
+
545
+ Required parameters must be present and use exact names from the tool declaration.
546
+ You may include optional natural-language reasoning before the <tool_call> block, but never after it.
547
+ If you call a function, end your response at </tool_call> with no suffix.
548
+ For write and memory_write, preserve content bytes exactly as provided by the user (no reformatting, no markdown normalization, no heading level changes).
549
+ HINT
550
+ RG_GUIDANCE = "For fast repository/text search, prefer `rg` (ripgrep) over `grep` when exploring files or text."
551
+ SMALL_CONTEXT_PROTOCOL = <<~PROTOCOL
552
+ Small-context retrieval protocol:
553
+ Default to targeted context before full-file reads.
554
+ Retrieval order:
555
+ 1. If the user provides file:line (for example, src/app.rb:130), inspect that location first.
556
+ 2. Use execute with rg/nl/sed to find the smallest relevant snippet.
557
+ 3. Read a full file only when targeted snippet extraction is insufficient.
558
+ Avoid broad reads early in debugging; gather just enough context to decide the next step.
559
+ PROTOCOL
560
+ end
561
+ end