samagotchi 0.2.0 → 0.4.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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +198 -1
  3. data/README.md +56 -4
  4. data/bin/chi +118 -50
  5. data/docs/cli.md +184 -9
  6. data/docs/configuration.md +333 -47
  7. data/docs/desktop.md +45 -4
  8. data/docs/guardrails.md +11 -0
  9. data/docs/hooks.md +208 -5
  10. data/docs/plugins.md +68 -2
  11. data/docs/releasing.md +23 -13
  12. data/docs/sessions.md +45 -17
  13. data/lib/samagotchi/answer_display.rb +95 -0
  14. data/lib/samagotchi/archive_store.rb +90 -0
  15. data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
  16. data/lib/samagotchi/bootstrap/probe.rb +262 -0
  17. data/lib/samagotchi/bootstrap_command.rb +347 -0
  18. data/lib/samagotchi/bridge/pending_card.rb +89 -0
  19. data/lib/samagotchi/bridge/turn_accumulator.rb +15 -3
  20. data/lib/samagotchi/bridge.rb +13 -1
  21. data/lib/samagotchi/bridge_client.rb +6 -2
  22. data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
  23. data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
  24. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +531 -0
  25. data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
  26. data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
  27. data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
  28. data/lib/samagotchi/bundles/system/delegated.md +6 -7
  29. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  30. data/lib/samagotchi/bundles/system/self_map.md +8 -2
  31. data/lib/samagotchi/client.rb +81 -19
  32. data/lib/samagotchi/commands/registry.rb +8 -0
  33. data/lib/samagotchi/config.rb +252 -48
  34. data/lib/samagotchi/desktop/macos/App.swift +12 -8
  35. data/lib/samagotchi/desktop/macos/ChiRunner.swift +17 -9
  36. data/lib/samagotchi/desktop/macos/Images.swift +113 -0
  37. data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
  38. data/lib/samagotchi/desktop/macos/Panel.swift +180 -25
  39. data/lib/samagotchi/desktop/macos.rb +59 -8
  40. data/lib/samagotchi/desktop_command.rb +6 -3
  41. data/lib/samagotchi/edit_preview.rb +82 -0
  42. data/lib/samagotchi/empty_answer_retry.rb +43 -0
  43. data/lib/samagotchi/engine.rb +434 -140
  44. data/lib/samagotchi/gem_update.rb +89 -0
  45. data/lib/samagotchi/guardrails/approval.rb +35 -4
  46. data/lib/samagotchi/guardrails/load_failures.rb +9 -3
  47. data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
  48. data/lib/samagotchi/guardrails.rb +1 -0
  49. data/lib/samagotchi/hooks/registry.rb +24 -5
  50. data/lib/samagotchi/host_registry.rb +9 -12
  51. data/lib/samagotchi/idle_client.rb +24 -15
  52. data/lib/samagotchi/idle_recap.rb +5 -1
  53. data/lib/samagotchi/idle_reminders.rb +2 -2
  54. data/lib/samagotchi/image_store.rb +10 -6
  55. data/lib/samagotchi/kernel_loop.rb +73 -94
  56. data/lib/samagotchi/live_versions.rb +59 -0
  57. data/lib/samagotchi/llm/api_key.rb +41 -0
  58. data/lib/samagotchi/llm/chat_loop.rb +132 -29
  59. data/lib/samagotchi/llm/errors.rb +41 -9
  60. data/lib/samagotchi/llm/http.rb +57 -17
  61. data/lib/samagotchi/llm/openai_chat.rb +17 -30
  62. data/lib/samagotchi/log_subscriber.rb +18 -3
  63. data/lib/samagotchi/memory_bundle/installer.rb +65 -63
  64. data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
  65. data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
  66. data/lib/samagotchi/memory_bundle/status.rb +4 -1
  67. data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
  68. data/lib/samagotchi/model_profile.rb +24 -1
  69. data/lib/samagotchi/plugin/context.rb +22 -1
  70. data/lib/samagotchi/plugin/sessions.rb +3 -1
  71. data/lib/samagotchi/prompt.rb +4 -2
  72. data/lib/samagotchi/reminder_store.rb +1 -9
  73. data/lib/samagotchi/reply_wait.rb +126 -0
  74. data/lib/samagotchi/sampling_settings.rb +58 -0
  75. data/lib/samagotchi/self_report.rb +18 -3
  76. data/lib/samagotchi/send_command.rb +252 -11
  77. data/lib/samagotchi/session.rb +52 -11
  78. data/lib/samagotchi/session_archive_command.rb +107 -0
  79. data/lib/samagotchi/session_commands.rb +46 -7
  80. data/lib/samagotchi/session_manager.rb +115 -25
  81. data/lib/samagotchi/session_metrics.rb +222 -106
  82. data/lib/samagotchi/steer.rb +72 -0
  83. data/lib/samagotchi/terminal_ui/attached_loop.rb +57 -28
  84. data/lib/samagotchi/terminal_ui/event_renderer.rb +21 -11
  85. data/lib/samagotchi/terminal_ui/formatting.rb +40 -8
  86. data/lib/samagotchi/terminal_ui/input_support.rb +7 -19
  87. data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
  88. data/lib/samagotchi/terminal_ui.rb +134 -247
  89. data/lib/samagotchi/text_diff.rb +181 -0
  90. data/lib/samagotchi/thinking.rb +115 -0
  91. data/lib/samagotchi/tool_activity.rb +3 -1
  92. data/lib/samagotchi/tool_runner.rb +34 -1
  93. data/lib/samagotchi/tools/ask_user_question.rb +41 -33
  94. data/lib/samagotchi/tools/builtins.rb +15 -4
  95. data/lib/samagotchi/tools/delegate_wait.rb +26 -69
  96. data/lib/samagotchi/tools/edit.rb +23 -9
  97. data/lib/samagotchi/tools/execute.rb +52 -14
  98. data/lib/samagotchi/tools/task_runtime.rb +19 -0
  99. data/lib/samagotchi/tools/task_wait.rb +27 -3
  100. data/lib/samagotchi/tools/write.rb +4 -0
  101. data/lib/samagotchi/turn_flow.rb +12 -2
  102. data/lib/samagotchi/turn_note.rb +60 -6
  103. data/lib/samagotchi/update_command.rb +308 -0
  104. data/lib/samagotchi/update_hint.rb +59 -0
  105. data/lib/samagotchi/version.rb +1 -1
  106. data/lib/samagotchi/vision_support.rb +7 -9
  107. data/lib/samagotchi/web/app.rb +91 -7
  108. data/lib/samagotchi/web/message_parts.rb +8 -3
  109. data/lib/samagotchi/web/public/activity.js +13 -1
  110. data/lib/samagotchi/web/public/annotate_presets.js +26 -0
  111. data/lib/samagotchi/web/public/annotations.js +13 -0
  112. data/lib/samagotchi/web/public/app.js +472 -111
  113. data/lib/samagotchi/web/public/card.js +5 -3
  114. data/lib/samagotchi/web/public/chat_view.js +13 -1
  115. data/lib/samagotchi/web/public/copy.js +20 -4
  116. data/lib/samagotchi/web/public/ctx.js +15 -0
  117. data/lib/samagotchi/web/public/data.js +23 -6
  118. data/lib/samagotchi/web/public/diff_view.js +58 -0
  119. data/lib/samagotchi/web/public/format.js +9 -0
  120. data/lib/samagotchi/web/public/index.html +60 -3
  121. data/lib/samagotchi/web/public/notify.js +175 -0
  122. data/lib/samagotchi/web/public/question_card.js +5 -2
  123. data/lib/samagotchi/web/public/sessions_list.js +7 -0
  124. data/lib/samagotchi/web/public/timing.js +39 -14
  125. data/lib/samagotchi/web/public/turn_events.js +75 -5
  126. data/lib/samagotchi/web/public/turn_view.js +49 -8
  127. data/lib/samagotchi/web/server.rb +8 -4
  128. data/lib/samagotchi/web/session_hub.rb +2 -1
  129. data/lib/samagotchi/web/session_summary.rb +24 -1
  130. data/lib/samagotchi/worker.rb +16 -4
  131. metadata +31 -1
@@ -13,11 +13,15 @@
13
13
  - Sessions: each is one file, `<sessions dir>/<id>.json`. A `<id>/` dir beside it exists only
14
14
  for background/web workers (input/, notes/, output/, pid). List them with `chi sessions list`
15
15
  (`--live` for the ones a worker runs now); delete one with `chi sessions delete ID` (never by hand).
16
+ An archived session (`chi sessions archive ID`, a marker file `<id>/archived`) is hidden from
17
+ every list and kept for good; `chi sessions list --archived` shows it.
16
18
  - `[CONTEXT NOTE from …]` messages are context notes (`chi note`, or another session's
17
19
  `send_note`): background, not requests. `list_sessions` + `send_note` tell another session
18
20
  something without starting a turn there; see `docs/sessions.md` "Context notes".
19
21
  - `chi send -m TEXT <id>` is the other half: the text goes in as the user's message and a
20
22
  turn runs (stdin piped too = quoted context above it); see `docs/sessions.md` "Sending a message".
23
+ `chi send --new --wait -m TEXT` starts a session the user can watch in the web and prints its
24
+ answer (exit 3: it waits for the user's answer); see "Starting a session".
21
25
  - `delegate` hands a task to a child session that runs in parallel and returns only its final
22
26
  reply (`delegate_result` waits for it; `session:` sends a follow-up to a child). A child is a
23
27
  normal session: it shows in `chi sessions list` with `↳ <parent>`, and the user can steer it
@@ -26,8 +30,10 @@
26
30
  commands (`anytime:` ones run mid-turn), model tools (they can return images I see), hooks,
27
31
  cards, side answers (`ask_model`),
28
32
  child sessions and services (e.g. MCP server processes). Shipped: `btw` (`/btw` side question),
29
- `mcp` (MCP server tools; a screenshot comes as a picture), `guardrails` (rules), `known-names` (typo guard), `loop-guard`
30
- (denies a repeated tool call with the same result, stops the turn after a few). `chi bundle list`
33
+ `mcp` (MCP server tools; a screenshot comes as a picture), `guardrails` (rules), `known-names` (typo guard), `source-links`
34
+ (turns source refs like JIRA-123 in an answer into links in the web, and a one-line note), `loop-guard`
35
+ (denies a repeated tool call with the same result, stops the turn after a few), `check-in` (after N tool
36
+ calls with no answer, a card asks the user to nudge me, let me go on or stop; `/checkin`). `chi bundle list`
31
37
  shows installed + available; `chi bundle install <name>`. Settings: config.yml `bundles: <name>:`
32
38
  (`config_modification_protocol`), read at session start: after an install or a settings change,
33
39
  tell the user to restart the session. API and bundle docs: `docs/plugins.md`.
@@ -8,6 +8,7 @@ require_relative "cancellation_controller"
8
8
  require_relative "llm/http"
9
9
  require_relative "vision_context"
10
10
  require_relative "vision_support"
11
+ require_relative "sampling_settings"
11
12
 
12
13
  module Samagotchi
13
14
  # Thin HTTP client for llama.cpp's native /completion endpoint, or an
@@ -27,6 +28,10 @@ module Samagotchi
27
28
  # budget and no retry (see #server_props).
28
29
  CONTEXT_WINDOW_PROBE_OPEN_TIMEOUT = 1
29
30
  CONTEXT_WINDOW_PROBE_READ_TIMEOUT = 2
31
+ # Seconds a probe that got no answer (refused, timed out) is remembered:
32
+ # a down or hung server then costs one probe per window, not one per
33
+ # generation, and a new turn doesn't ask again at once.
34
+ PROPS_FAILURE_TTL = 30
30
35
 
31
36
  SERVER_TRANSPORT_ENV = "SAMAGOTCHI_SERVER_TRANSPORT"
32
37
  DEFAULT_TRANSPORT = :llama_cpp
@@ -109,7 +114,7 @@ module Samagotchi
109
114
  end
110
115
 
111
116
  # One /props probe's outcome. `answered?` is false when the probe failed:
112
- # a network error, a timeout or any non-200 (llama.cpp answers 503 while
117
+ # a network error, a timeout, a turn's cancel or any non-200 (llama.cpp answers 503 while
113
118
  # it loads a model). `body` is the parsed JSON of a 200, or nil when it
114
119
  # isn't JSON.
115
120
  ServerProps = Data.define(:body, :status) do
@@ -121,21 +126,38 @@ module Samagotchi
121
126
  # Moved to its own file; the old name keeps working.
122
127
  CancellationController = Samagotchi::CancellationController
123
128
 
129
+ PROBE_CANCEL_KEY = :samagotchi_probe_cancel
130
+
131
+ # The cancel a /props probe made on this thread listens to: a turn sets
132
+ # its own (Engine#run_turn), so a Stop cuts the probes it makes before
133
+ # its first request. Probes on other threads (chi self, /model, a
134
+ # status snapshot) have none and run to their timeout.
135
+ def self.probe_cancel = Thread.current[PROBE_CANCEL_KEY]
136
+
137
+ # Sets this thread's probe cancel; returns the one it replaces.
138
+ def self.swap_probe_cancel(controller)
139
+ previous = Thread.current[PROBE_CANCEL_KEY]
140
+ Thread.current[PROBE_CANCEL_KEY] = controller
141
+ previous
142
+ end
143
+
124
144
  # @param sleeper [#call, nil] waits between retries (specs pass a no-op)
125
145
  # @param scheme [String, nil] "https" for a TLS server (default http)
126
146
  # @param first_token_timeout [Numeric, nil] seconds a completion may take
127
147
  # to stream its first text (LLM::HTTP); nil: no limit
128
148
  # @param name [String, nil] the host's config name, for error lines
129
149
  # (default: the transport's label)
150
+ # @param api_key_env [String, nil] the variable holding the host's API
151
+ # key (llama.cpp's --api-key), sent as a bearer token on every request;
152
+ # nil sends no Authorization header
153
+ # @param env [Hash] where the key variable is read
130
154
  def initialize(host: nil, port: nil, open_timeout: nil, read_timeout: nil, transport: nil, sleeper: nil, scheme: nil,
131
- first_token_timeout: nil, name: nil)
155
+ first_token_timeout: nil, name: nil, api_key_env: nil, env: ENV)
132
156
  # Unified config precedence: CLI > ENV > file > default (via Samagotchi::Config)
133
157
  cfg_host = nil; cfg_port = nil; cfg_transport_raw = nil
134
158
  begin
135
159
  cfg_host = Samagotchi::Config.get("server.host")
136
160
  cfg_port = Samagotchi::Config.get("server.port")
137
- cfg_open_timeout = Samagotchi::Config.get("server.open_timeout")
138
- cfg_read_timeout = Samagotchi::Config.get("server.read_timeout")
139
161
  cfg_transport_raw = Samagotchi::Config.get("server.transport")
140
162
  rescue StandardError
141
163
  nil
@@ -143,17 +165,19 @@ module Samagotchi
143
165
  @host = host || cfg_host
144
166
  @port = (port || cfg_port).to_i
145
167
  @scheme = scheme || "http"
146
- @open_timeout = (open_timeout || cfg_open_timeout).to_i
147
- @read_timeout = (read_timeout || cfg_read_timeout).to_i
168
+ @open_timeout = Samagotchi::Config.positive_seconds("server.open_timeout", open_timeout)
169
+ @read_timeout = Samagotchi::Config.positive_seconds("server.read_timeout", read_timeout)
148
170
  transport_fallback = cfg_transport_raw || ENV.fetch(SERVER_TRANSPORT_ENV, DEFAULT_TRANSPORT.to_s)
149
171
  @transport = build_transport(resolve_transport(transport || transport_fallback))
150
172
  @props_cache = {}
173
+ @props_failures = {}
151
174
  @props_mutex = Mutex.new
152
175
  @first_token_timeout = first_token_timeout
153
176
  @host_name = name
154
177
  @label = name.to_s.empty? ? @transport.label : name.to_s
155
178
  @http = LLM::HTTP.new(label: @label, open_timeout: @open_timeout, read_timeout: @read_timeout,
156
- sleeper: sleeper, first_token_timeout: first_token_timeout)
179
+ sleeper: sleeper, first_token_timeout: first_token_timeout,
180
+ api_key: LLM::ApiKey.for(api_key_env, host: @label, env: env))
157
181
  end
158
182
 
159
183
  # Seconds a completion may take to stream its first text, or nil.
@@ -182,11 +206,14 @@ module Samagotchi
182
206
  # @param on_retry [Proc, nil] optional callback before retry sleep
183
207
  # @param images [Array<String>] base64 images, one per
184
208
  # ImagePlan::NATIVE_PLACEHOLDER in the prompt (llama.cpp only)
209
+ # @param sampling [Hash] request fields to add (SamplingSettings):
210
+ # temperature, penalties, …; they can't replace the fields above
185
211
  # @return [String] the generated text
186
212
  def complete(prompt, stop: ["<end_of_turn>", "<|tool_response>"], n_predict: nil, model: nil, on_chunk: nil, cancel_controller: nil, on_retry: nil,
187
- images: [])
213
+ images: [], sampling: {})
188
214
  images = Array(images)
189
- return stream_completion(scrub_utf8(prompt), stop, n_predict, model, on_chunk, cancel_controller, on_retry) if images.empty?
215
+ request = { stop: stop, n_predict: n_predict, model: model, sampling: sampling }
216
+ return stream_completion(scrub_utf8(prompt), request, on_chunk, cancel_controller, on_retry) if images.empty?
190
217
 
191
218
  # The media marker is random per server process: a restart between the
192
219
  # /props read and the request makes the prompt fail to tokenize, so
@@ -196,7 +223,7 @@ module Samagotchi
196
223
  attempts += 1
197
224
  payload_prompt = { prompt_string: scrub_utf8(prompt.gsub(ImagePlan::NATIVE_PLACEHOLDER, media_marker!(model))),
198
225
  multimodal_data: images }
199
- stream_completion(payload_prompt, stop, n_predict, model, on_chunk, cancel_controller, on_retry)
226
+ stream_completion(payload_prompt, request, on_chunk, cancel_controller, on_retry)
200
227
  rescue LLM::BadRequest => e
201
228
  raise unless attempts == 1 && e.message.include?("Failed to tokenize prompt")
202
229
 
@@ -205,11 +232,14 @@ module Samagotchi
205
232
  end
206
233
  end
207
234
 
208
- private def stream_completion(prompt, stop, n_predict, model, on_chunk, cancel_controller, on_retry)
235
+ OPENAI_API_HINT = "does this host speak the OpenAI API? set `api: openai` on it"
236
+
237
+ private def stream_completion(prompt, fields, on_chunk, cancel_controller, on_retry)
209
238
  uri = completion_uri
210
239
  request = Net::HTTP::Post.new(uri)
211
240
  request["Content-Type"] = "application/json"
212
- request.body = completion_payload(prompt, stop: stop, n_predict: n_predict, model: model).to_json
241
+ request.body = completion_payload(prompt, **fields).to_json
242
+ model = fields[:model]
213
243
 
214
244
  result = +""
215
245
  reset_on_retry = lambda do |event|
@@ -219,7 +249,8 @@ module Samagotchi
219
249
  end
220
250
  @http.stream_lines(uri, request, cancel_controller: cancel_controller, on_retry: reset_on_retry,
221
251
  on_network_error: ->(_error) { invalidate_context_window! },
222
- log_fields: { model: model, purpose: "chat" }) do |line, shown|
252
+ log_fields: { model: model, purpose: "chat",
253
+ sampling: SamplingSettings.log_text(sendable_sampling(fields[:sampling])) }) do |line, shown|
223
254
  parsed_chunk = parse_stream_line(line)
224
255
  next unless parsed_chunk
225
256
 
@@ -229,6 +260,11 @@ module Samagotchi
229
260
  on_chunk&.call(content: content, payload: payload)
230
261
  end
231
262
  result
263
+ rescue LLM::BadRequest => e
264
+ raise unless e.status == 404 && @transport.name == :llama_cpp
265
+
266
+ # No native /completion here: likely an OpenAI-compatible server.
267
+ raise LLM::BadRequest.new(e.message, host: e.host, status: e.status, attempts: e.attempts, hint: OPENAI_API_HINT)
232
268
  rescue RequestCancelled, LLM::ProviderError
233
269
  raise
234
270
  rescue StandardError => e
@@ -260,7 +296,9 @@ module Samagotchi
260
296
  # a turn. The probe names the model (`?model=`): a llama.cpp router
261
297
  # answers a stub without it, and a single-model server ignores it.
262
298
  # Whatever the server answers (a non-200 too) is cached per model; a
263
- # network failure is not, so the next call asks again.
299
+ # network failure for PROPS_FAILURE_TTL seconds, then the next call asks
300
+ # again. A probe cut by this thread's Client.probe_cancel answers
301
+ # :cancelled, uncached, and the turn's next request ends it.
264
302
  def server_props(model: nil)
265
303
  path = @transport.props_path
266
304
  return nil unless path
@@ -268,10 +306,21 @@ module Samagotchi
268
306
  key = model.to_s
269
307
  @props_mutex.synchronize do
270
308
  return @props_cache[key] if @props_cache.key?(key)
309
+
310
+ failed, failed_at = @props_failures[key]
311
+ return failed if failed && monotonic_now - failed_at < PROPS_FAILURE_TTL
271
312
  end
272
313
 
273
314
  props = probe_props(path, key)
274
- @props_mutex.synchronize { @props_cache[key] = props } unless props.status == :network_error
315
+ @props_mutex.synchronize do
316
+ case props.status
317
+ when :cancelled then nil
318
+ when :network_error then @props_failures[key] = [props, monotonic_now]
319
+ else
320
+ @props_cache[key] = props
321
+ @props_failures.delete(key)
322
+ end
323
+ end
275
324
  props
276
325
  end
277
326
 
@@ -286,23 +335,30 @@ module Samagotchi
286
335
 
287
336
  # Forget cached /props answers: the server may have restarted with
288
337
  # another -c, or a model switch may have loaded one with a different
289
- # window.
338
+ # window. A recent failure stays until its PROPS_FAILURE_TTL ends: it
339
+ # holds no stale window, and asking a hung server again at every turn's
340
+ # start is what it saves.
290
341
  def invalidate_context_window!
291
342
  @props_mutex.synchronize { @props_cache.clear }
292
343
  end
293
344
 
294
345
  private
295
346
 
347
+ def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
348
+
296
349
  def probe_props(path, model)
297
350
  query = model.empty? ? "" : "?#{URI.encode_www_form(model: model)}"
298
351
  uri = URI("#{@scheme}://#{@host}:#{@port}#{path}#{query}")
299
352
  response = @http.fetch(uri, Net::HTTP::Get.new(uri), retries: false, check_status: false,
300
353
  log_fields: { model: model.empty? ? nil : model, purpose: "probe" },
301
354
  open_timeout: CONTEXT_WINDOW_PROBE_OPEN_TIMEOUT,
302
- read_timeout: CONTEXT_WINDOW_PROBE_READ_TIMEOUT)
355
+ read_timeout: CONTEXT_WINDOW_PROBE_READ_TIMEOUT,
356
+ cancel_controller: Client.probe_cancel)
303
357
  return ServerProps.new(body: nil, status: :http_error) unless response.code.to_s == "200"
304
358
 
305
359
  ServerProps.new(body: parse_props(response.body), status: :ok)
360
+ rescue RequestCancelled
361
+ ServerProps.new(body: nil, status: :cancelled)
306
362
  rescue StandardError
307
363
  ServerProps.new(body: nil, status: :network_error)
308
364
  end
@@ -337,12 +393,18 @@ module Samagotchi
337
393
  URI("#{@scheme}://#{@host}:#{@port}#{@transport.completion_path}")
338
394
  end
339
395
 
340
- def completion_payload(prompt, stop:, n_predict:, model:)
396
+ def completion_payload(prompt, stop:, n_predict:, model:, sampling: {})
341
397
  payload = { prompt: prompt, stop: stop, stream: true }
342
398
  payload[@transport.token_limit_key] = n_predict if n_predict && n_predict.to_i.positive?
343
399
  model_name = @transport.model_for_payload(model)
344
400
  payload[:model] = model_name if model_name
345
- payload
401
+ sendable_sampling(sampling).merge(payload)
402
+ end
403
+
404
+ # The sampling fields that go out: no nil (nothing to drop here: the
405
+ # server's defaults apply) and none of the reserved request fields.
406
+ def sendable_sampling(sampling)
407
+ (sampling || {}).reject { |key, value| value.nil? || ConfigFile::SAMPLING_RESERVED_KEYS.include?(key.to_s) }
346
408
  end
347
409
 
348
410
  # Conversation content (system prompt + tool responses + model output) can
@@ -74,6 +74,14 @@ module Samagotchi
74
74
 
75
75
  def command?(line) = !lookup(line).nil?
76
76
 
77
+ # @return [Entry, nil] the first entry the UI runs itself (local) that
78
+ # matches +line+, whatever its uis (a UI answers the others' too:
79
+ # the REPL's /detach note)
80
+ def lookup_local(line)
81
+ text = line.to_s.strip
82
+ @entries.find { |entry| entry.local && entry.match?(text) }
83
+ end
84
+
77
85
  # @return [Array<Entry>] every entry, in registration order
78
86
  def entries = @entries.dup
79
87