samagotchi 0.156.0 → 0.158.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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -1
  3. data/README.md +1 -1
  4. data/bin/chi +4 -1
  5. data/docs/architecture.md +18 -12
  6. data/docs/bundle-authoring.md +5 -5
  7. data/docs/bundles.md +4 -1
  8. data/docs/cli.md +29 -24
  9. data/docs/configuration.md +43 -9
  10. data/docs/context.md +5 -3
  11. data/docs/guardrails.md +12 -7
  12. data/docs/hooks.md +53 -83
  13. data/docs/internals/bridge.md +84 -7
  14. data/docs/internals/bundles.md +69 -46
  15. data/docs/internals/context-telemetry.md +4 -3
  16. data/docs/internals/engine.md +23 -7
  17. data/docs/internals/participants.md +3 -2
  18. data/docs/internals/remote-sessions.md +5 -2
  19. data/docs/internals/web-frontend.md +3 -2
  20. data/docs/internals/working-set.md +1 -1
  21. data/docs/memory.md +51 -10
  22. data/docs/plugins.md +48 -11
  23. data/docs/releasing.md +6 -5
  24. data/docs/sessions.md +13 -13
  25. data/docs/sub-agent.md +17 -2
  26. data/docs/testing.md +56 -5
  27. data/lib/samagotchi/answer_command.rb +2 -1
  28. data/lib/samagotchi/archive_store.rb +4 -3
  29. data/lib/samagotchi/bootstrap_command.rb +3 -2
  30. data/lib/samagotchi/bridge/card_places.rb +5 -7
  31. data/lib/samagotchi/bridge/card_store.rb +3 -0
  32. data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
  33. data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
  34. data/lib/samagotchi/bridge.rb +31 -6
  35. data/lib/samagotchi/broadcast/recipients.rb +8 -8
  36. data/lib/samagotchi/broadcast/triage_model.rb +1 -1
  37. data/lib/samagotchi/bundle_command/check.rb +41 -10
  38. data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
  39. data/lib/samagotchi/bundle_command/try/run.rb +4 -1
  40. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  41. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  42. data/lib/samagotchi/bundle_command.rb +20 -45
  43. data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
  44. data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
  45. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
  46. data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
  47. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  48. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  49. data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
  50. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  51. data/lib/samagotchi/child_move.rb +6 -6
  52. data/lib/samagotchi/child_reports.rb +2 -2
  53. data/lib/samagotchi/children_status.rb +15 -21
  54. data/lib/samagotchi/cli/parent_wait.rb +8 -22
  55. data/lib/samagotchi/config.rb +3 -0
  56. data/lib/samagotchi/context_command.rb +7 -5
  57. data/lib/samagotchi/context_sources.rb +9 -6
  58. data/lib/samagotchi/context_window.rb +9 -12
  59. data/lib/samagotchi/effective_model.rb +57 -22
  60. data/lib/samagotchi/engine/plugins.rb +1 -1
  61. data/lib/samagotchi/engine/recap.rb +8 -4
  62. data/lib/samagotchi/engine.rb +30 -9
  63. data/lib/samagotchi/events.rb +3 -2
  64. data/lib/samagotchi/extension_load.rb +26 -44
  65. data/lib/samagotchi/guardrails/load_failures.rb +3 -2
  66. data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
  67. data/lib/samagotchi/guardrails/review_paths.rb +1 -5
  68. data/lib/samagotchi/guardrails_command.rb +1 -1
  69. data/lib/samagotchi/hook_routes.rb +2 -2
  70. data/lib/samagotchi/hooks.rb +2 -3
  71. data/lib/samagotchi/host_address.rb +52 -0
  72. data/lib/samagotchi/host_registry.rb +57 -45
  73. data/lib/samagotchi/kernel_loop.rb +7 -3
  74. data/lib/samagotchi/llm/chat_loop.rb +6 -3
  75. data/lib/samagotchi/llm_context_strategy.rb +27 -63
  76. data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
  77. data/lib/samagotchi/memory_bundle/builder.rb +12 -35
  78. data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
  79. data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
  80. data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
  81. data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
  82. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  83. data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
  84. data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
  85. data/lib/samagotchi/memory_bundle/installer.rb +212 -178
  86. data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
  87. data/lib/samagotchi/memory_bundle/merger.rb +69 -65
  88. data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
  89. data/lib/samagotchi/memory_bundle/profile.rb +5 -5
  90. data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
  91. data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
  92. data/lib/samagotchi/memory_bundle/shas.rb +5 -8
  93. data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
  94. data/lib/samagotchi/memory_bundle/status.rb +26 -30
  95. data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
  96. data/lib/samagotchi/memory_bundle/trash.rb +28 -18
  97. data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
  98. data/lib/samagotchi/memory_orphans.rb +4 -4
  99. data/lib/samagotchi/memory_paths.rb +102 -13
  100. data/lib/samagotchi/memory_usage.rb +1 -1
  101. data/lib/samagotchi/model_lookup.rb +75 -0
  102. data/lib/samagotchi/model_profile.rb +1 -0
  103. data/lib/samagotchi/models_command.rb +1 -1
  104. data/lib/samagotchi/note_command.rb +1 -1
  105. data/lib/samagotchi/participant_mode.rb +2 -1
  106. data/lib/samagotchi/plugin/api.rb +42 -10
  107. data/lib/samagotchi/plugin/loader.rb +75 -15
  108. data/lib/samagotchi/plugin/sessions.rb +1 -1
  109. data/lib/samagotchi/project_command.rb +167 -0
  110. data/lib/samagotchi/project_init.rb +69 -0
  111. data/lib/samagotchi/project_scope.rb +6 -4
  112. data/lib/samagotchi/prompt_files.rb +10 -7
  113. data/lib/samagotchi/reasoning_history.rb +14 -20
  114. data/lib/samagotchi/recap_place.rb +26 -0
  115. data/lib/samagotchi/reply_wait/liveness.rb +38 -0
  116. data/lib/samagotchi/reply_wait/poll.rb +44 -15
  117. data/lib/samagotchi/reply_wait/target.rb +113 -0
  118. data/lib/samagotchi/reply_wait.rb +22 -2
  119. data/lib/samagotchi/run_options.rb +1 -0
  120. data/lib/samagotchi/sampling_settings.rb +9 -16
  121. data/lib/samagotchi/self_report.rb +34 -35
  122. data/lib/samagotchi/send_command.rb +17 -23
  123. data/lib/samagotchi/session.rb +349 -441
  124. data/lib/samagotchi/session_access/contract.rb +1 -1
  125. data/lib/samagotchi/session_access/local.rb +5 -6
  126. data/lib/samagotchi/session_access/remote.rb +5 -3
  127. data/lib/samagotchi/session_field.rb +134 -0
  128. data/lib/samagotchi/session_inbox.rb +49 -1
  129. data/lib/samagotchi/session_manager/emptiness.rb +2 -1
  130. data/lib/samagotchi/session_manager/listing.rb +29 -100
  131. data/lib/samagotchi/session_manager.rb +2 -1
  132. data/lib/samagotchi/session_metrics.rb +7 -4
  133. data/lib/samagotchi/session_paths.rb +57 -0
  134. data/lib/samagotchi/session_retention.rb +2 -2
  135. data/lib/samagotchi/session_row.rb +200 -0
  136. data/lib/samagotchi/session_setup.rb +15 -12
  137. data/lib/samagotchi/sessions_command.rb +11 -129
  138. data/lib/samagotchi/steer.rb +10 -0
  139. data/lib/samagotchi/steer_cut_after.rb +7 -14
  140. data/lib/samagotchi/system_prompt.rb +20 -11
  141. data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
  142. data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
  143. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  144. data/lib/samagotchi/terminal_ui.rb +3 -1
  145. data/lib/samagotchi/testing/bundle.rb +5 -12
  146. data/lib/samagotchi/testing/install.rb +5 -4
  147. data/lib/samagotchi/testing/recorder.rb +1 -1
  148. data/lib/samagotchi/testing.rb +1 -1
  149. data/lib/samagotchi/thinking.rb +16 -21
  150. data/lib/samagotchi/tool_declarations.rb +1 -1
  151. data/lib/samagotchi/tool_response.rb +5 -3
  152. data/lib/samagotchi/tool_runner.rb +18 -1
  153. data/lib/samagotchi/toolkit_binding.rb +1 -4
  154. data/lib/samagotchi/toolkit_command.rb +1 -4
  155. data/lib/samagotchi/tools/delegate.rb +60 -51
  156. data/lib/samagotchi/tools/delegate_relay.rb +5 -5
  157. data/lib/samagotchi/tools/delegate_result.rb +1 -1
  158. data/lib/samagotchi/tools/delegate_wait.rb +41 -17
  159. data/lib/samagotchi/tools/list_sessions.rb +7 -7
  160. data/lib/samagotchi/update_command.rb +11 -7
  161. data/lib/samagotchi/verify_fresh.rb +7 -11
  162. data/lib/samagotchi/version.rb +1 -1
  163. data/lib/samagotchi/vision_support.rb +11 -14
  164. data/lib/samagotchi/web/app.rb +66 -21
  165. data/lib/samagotchi/web/message_display.rb +57 -7
  166. data/lib/samagotchi/web/participant_access.rb +1 -1
  167. data/lib/samagotchi/web/project_list.rb +3 -2
  168. data/lib/samagotchi/web/public/activity.js +16 -7
  169. data/lib/samagotchi/web/public/app.js +29 -8
  170. data/lib/samagotchi/web/public/bubbles.js +8 -3
  171. data/lib/samagotchi/web/public/cards_place.js +15 -0
  172. data/lib/samagotchi/web/public/commands.js +5 -1
  173. data/lib/samagotchi/web/public/data.js +4 -2
  174. data/lib/samagotchi/web/public/history_shape.js +220 -0
  175. data/lib/samagotchi/web/public/index.html +6 -1
  176. data/lib/samagotchi/web/public/project_picker.js +86 -4
  177. data/lib/samagotchi/web/public/recap_stamp.js +17 -0
  178. data/lib/samagotchi/web/public/reload_check.js +71 -0
  179. data/lib/samagotchi/web/public/stage_model.js +7 -5
  180. data/lib/samagotchi/web/public/stage_view.js +7 -3
  181. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  182. data/lib/samagotchi/web/public/timing.js +43 -2
  183. data/lib/samagotchi/web/public/turn_block.js +215 -30
  184. data/lib/samagotchi/web/public/turn_drawing.js +43 -0
  185. data/lib/samagotchi/web/public/turn_events.js +10 -1
  186. data/lib/samagotchi/web/public/turn_pieces.js +29 -0
  187. data/lib/samagotchi/web/reload_check_route.rb +95 -0
  188. data/lib/samagotchi/web/server.rb +4 -2
  189. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  190. data/lib/samagotchi/web/session_control.rb +1 -1
  191. data/lib/samagotchi/web/session_hub.rb +9 -17
  192. data/lib/samagotchi/web/session_show.rb +64 -2
  193. data/lib/samagotchi/web/session_summary.rb +36 -24
  194. data/lib/samagotchi/worker.rb +28 -5
  195. data/lib/samagotchi/worker_link.rb +3 -0
  196. metadata +22 -5
  197. data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
  198. data/lib/samagotchi/send_command/wait_target.rb +0 -85
@@ -2,12 +2,11 @@
2
2
 
3
3
  require_relative "hooks/registry"
4
4
  require_relative "hooks/loader"
5
- require_relative "hooks/bundle_loader"
6
5
  require_relative "hooks/stream_watch"
7
6
 
8
7
  module Samagotchi
9
- # Thin wrapper that exposes the Hooks::Registry, Hooks::Loader and
10
- # Hooks::BundleLoader (bundle-owned hook plugins).
8
+ # Thin wrapper that exposes the Hooks::Registry and Hooks::Loader
9
+ # (config.yml's hooks; a bundle's hook through its plugin, Plugin::Api#on).
11
10
  #
12
11
  # The Engine holds an instance of Hooks::Registry and provides:
13
12
  # - #register_hook(name, &block) — register a turn-scoped hook
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+ require_relative "config"
5
+
6
+ module Samagotchi
7
+ # What a host's address says about it, without asking it: whether it is
8
+ # remote (a provider on the network rather than a local server), and the
9
+ # configured first-token limit. A leaf: HostRegistry::HostEntry and
10
+ # BootstrapCommand read it, it reads only Config.
11
+ module HostAddress
12
+ # Loopback, private (RFC 1918, IPv6 unique local) and link-local nets.
13
+ LOCAL_NETS = %w[127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 100.64.0.0/10
14
+ ::1/128 fc00::/7 fe80::/10].map { |net| IPAddr.new(net) }.freeze
15
+
16
+ # Name suffixes that stay on a home or office network: mDNS, the
17
+ # special-use and customary private names, router defaults (fritz.box)
18
+ # and Tailscale's MagicDNS. Compared on whole labels.
19
+ LOCAL_NAME_SUFFIXES = %w[localhost local lan home home.arpa internal intranet localdomain private corp test
20
+ box ts.net].freeze
21
+
22
+ # Whether an address is remote: https, an http IP address outside the
23
+ # local nets (LOCAL_NETS), or an http name with a dot whose suffix isn't
24
+ # a local one (LOCAL_NAME_SUFFIXES): `gpu.example.com` is remote, while
25
+ # `box`, `mac.local`, `gpu.lan` and `pc.tail1234.ts.net` are local. chi
26
+ # doesn't look names up; hosts.<name>.remote overrides either way.
27
+ def self.remote_address?(scheme, host)
28
+ return true if scheme.to_s == "https"
29
+
30
+ address = IPAddr.new(host.to_s.delete_prefix("[").delete_suffix("]"))
31
+ address = address.native if address.ipv4_mapped?
32
+ LOCAL_NETS.none? { |net| net.family == address.family && net.include?(address) }
33
+ rescue IPAddr::Error
34
+ remote_name?(host)
35
+ end
36
+
37
+ def self.remote_name?(host)
38
+ name = host.to_s.downcase.delete_suffix(".")
39
+ return false unless name.include?(".")
40
+
41
+ LOCAL_NAME_SUFFIXES.none? { |suffix| name == suffix || name.end_with?(".#{suffix}") }
42
+ end
43
+ private_class_method :remote_name?
44
+
45
+ # server.first_token_timeout, or nil when unset or unreadable.
46
+ def self.configured_first_token_timeout
47
+ Config.get("server.first_token_timeout")
48
+ rescue StandardError # degrade-ok: no configured limit; the host's own or the remote default applies
49
+ nil
50
+ end
51
+ end
52
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ipaddr"
4
3
  require_relative "config"
4
+ require_relative "host_address"
5
5
  require_relative "log"
6
6
  require_relative "model_ref"
7
7
  require_relative "client"
@@ -88,11 +88,11 @@ module Samagotchi
88
88
  def dialect_profile = LLM::Dialect.for(dialect)
89
89
 
90
90
  # A provider on the network rather than a local server: hosts.<name>.remote
91
- # when set, else by its address (HostRegistry.remote_address?). An API
91
+ # when set, else by its address (HostAddress.remote_address?). An API
92
92
  # key doesn't decide: a llama.cpp on the LAN can have one. A remote
93
93
  # host's model list is cached longer, it gets a first-token limit, and
94
94
  # it isn't asked for llama.cpp's /props.
95
- def remote? = remote.nil? ? HostRegistry.remote_address?(scheme, host) : remote
95
+ def remote? = remote.nil? ? HostAddress.remote_address?(scheme, host) : remote
96
96
 
97
97
  def models_ttl = remote? ? REMOTE_CACHE_TTL_SECONDS : CACHE_TTL_SECONDS
98
98
 
@@ -102,7 +102,7 @@ module Samagotchi
102
102
  # and read_timeout catches a dead one). 0 turns it off.
103
103
  def first_token_limit
104
104
  seconds = first_token_timeout
105
- seconds = HostRegistry.configured_first_token_timeout if seconds.nil?
105
+ seconds = HostAddress.configured_first_token_timeout if seconds.nil?
106
106
  seconds = remote? ? REMOTE_FIRST_TOKEN_TIMEOUT : nil if seconds.nil?
107
107
  seconds&.positive? ? seconds : nil
108
108
  end
@@ -115,13 +115,61 @@ module Samagotchi
115
115
  def openai_base_url = entry.openai_base_url
116
116
  end
117
117
 
118
+ # Raised when a client override is set on the shared registry (.current):
119
+ # every command and chi web's routes would then send through it.
120
+ class Shared < StandardError
121
+ def initialize(msg = "HostRegistry.current is shared: set a client override on a registry of your own") = super
122
+ end
123
+
118
124
  # A client that every target uses instead of its host's own (specs inject
119
125
  # a stub this way; the Engine/TUI `client:` keyword sets it).
120
- attr_accessor :client_override
126
+ attr_reader :client_override
127
+
128
+ # @raise [Shared] on the shared registry (.current)
129
+ def client_override=(client)
130
+ raise Shared if @shared
131
+
132
+ @client_override = client
133
+ end
134
+
135
+ @current_mutex = Mutex.new
136
+
137
+ # The registry this process's commands and chi web share: one per
138
+ # process, built again when config.yml's hosts: no longer equal the ones
139
+ # it was built from (HostConfig.all, compared as read: the YAML read is
140
+ # mtime-cached), so a host added while chi web runs is listed on the next
141
+ # read, and its model lists, index and adapters are kept while hosts:
142
+ # stay. It doesn't rebuild on server.first_token_timeout or the
143
+ # server.host/port fallback entry: those are read at construction. It
144
+ # refuses a client override (Shared). An Engine and the TerminalUI take
145
+ # it once, at their start: a running worker keeps the hosts it started
146
+ # with.
147
+ # For any env but the process ENV (chi self's spec envs): a fresh
148
+ # registry on that env, never kept.
149
+ # @return [HostRegistry]
150
+ def self.current(env: ENV)
151
+ return new(env: env) unless env.equal?(ENV)
152
+
153
+ @current_mutex.synchronize do
154
+ hosts = HostConfig.all
155
+ if @current.nil? || hosts != @current_hosts
156
+ @current = new(hosts_config: hosts, shared: true)
157
+ @current_hosts = hosts
158
+ end
159
+ @current
160
+ end
161
+ end
162
+
163
+ # Forget the shared registry (specs: one per example).
164
+ def self.reset_current!
165
+ @current_mutex.synchronize { @current = @current_hosts = nil }
166
+ end
121
167
 
122
168
  # @param clock [#call, nil] monotonic seconds (specs)
123
- def initialize(hosts_config: nil, env: ENV, client_override: nil, clock: nil)
169
+ # @param shared [Boolean] .current's own: refuses a client override
170
+ def initialize(hosts_config: nil, env: ENV, client_override: nil, clock: nil, shared: false)
124
171
  @client_override = client_override
172
+ @shared = shared
125
173
  @env = env
126
174
  @clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
127
175
  @adapters = {}
@@ -155,45 +203,9 @@ module Samagotchi
155
203
 
156
204
  attr_reader :entries
157
205
 
158
- # Loopback, private (RFC 1918, IPv6 unique local) and link-local nets.
159
- LOCAL_NETS = %w[127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 100.64.0.0/10
160
- ::1/128 fc00::/7 fe80::/10].map { |net| IPAddr.new(net) }.freeze
161
-
162
- # Name suffixes that stay on a home or office network: mDNS, the
163
- # special-use and customary private names, router defaults (fritz.box)
164
- # and Tailscale's MagicDNS. Compared on whole labels.
165
- LOCAL_NAME_SUFFIXES = %w[localhost local lan home home.arpa internal intranet localdomain private corp test
166
- box ts.net].freeze
167
-
168
- # Whether an address is remote: https, an http IP address outside the
169
- # local nets (LOCAL_NETS), or an http name with a dot whose suffix isn't
170
- # a local one (LOCAL_NAME_SUFFIXES): `gpu.example.com` is remote, while
171
- # `box`, `mac.local`, `gpu.lan` and `pc.tail1234.ts.net` are local. chi
172
- # doesn't look names up; hosts.<name>.remote overrides either way.
173
- def self.remote_address?(scheme, host)
174
- return true if scheme.to_s == "https"
175
-
176
- address = IPAddr.new(host.to_s.delete_prefix("[").delete_suffix("]"))
177
- address = address.native if address.ipv4_mapped?
178
- LOCAL_NETS.none? { |net| net.family == address.family && net.include?(address) }
179
- rescue IPAddr::Error
180
- remote_name?(host)
181
- end
182
-
183
- def self.remote_name?(host)
184
- name = host.to_s.downcase.delete_suffix(".")
185
- return false unless name.include?(".")
186
-
187
- LOCAL_NAME_SUFFIXES.none? { |suffix| name == suffix || name.end_with?(".#{suffix}") }
188
- end
189
- private_class_method :remote_name?
190
-
191
- # server.first_token_timeout, or nil when unset or unreadable.
192
- def self.configured_first_token_timeout
193
- Config.get("server.first_token_timeout")
194
- rescue StandardError
195
- nil
196
- end
206
+ # The env its hosts, aliases and saved lists are read with (ENV, or chi
207
+ # self's): what an EffectiveModel's ModelLookup reads every layer with.
208
+ attr_reader :env
197
209
 
198
210
  def default_entry
199
211
  @entries["default"] || @entries.values.first
@@ -792,20 +792,24 @@ module Samagotchi
792
792
  turn.qwen_attempts = 0
793
793
  turn.qwen_partial = nil
794
794
  runs = []
795
+ # The batch's ids before it runs (each run's events carry its own):
796
+ # nothing appends to the conversation during a batch, so they are the
797
+ # ids the entry would get after it (spec/tool_ids_pin_spec.rb).
798
+ ids = ToolIds.next_ids(turn.conversation, calls.size)
795
799
  begin
796
800
  ToolResponse.run_batch(tool_runner, calls, context: @context, iteration: turn.iteration, emit: turn.emit,
797
- on_stream_event: turn.on_stream_event, cap: turn.cap) { |run, _| runs << run }
801
+ on_stream_event: turn.on_stream_event, cap: turn.cap, ids: ids) { |run, _| runs << run }
798
802
  rescue Interrupt
799
803
  # A Ctrl-C mid-batch: the entry still answers every call (the
800
804
  # finished runs, then run_batch's interrupted ones), for #run to
801
805
  # hand on with the conversation.
802
- turn.conversation << ToolResponse.joined(runs, ids: ToolIds.next_ids(turn.conversation, runs.size))
806
+ turn.conversation << ToolResponse.joined(runs, ids: ids.first(runs.size))
803
807
  raise
804
808
  end
805
809
  turn.tool_activity.concat(ToolResponse.activities(runs))
806
810
  # One entry for the batch: the capped outputs joined (ToolResponse.joined),
807
811
  # an id per run (ToolIds).
808
- turn.conversation << ToolResponse.joined(runs, ids: ToolIds.next_ids(turn.conversation, runs.size))
812
+ turn.conversation << ToolResponse.joined(runs, ids: ids.first(runs.size))
809
813
  turn.pending_tool_calls = true
810
814
  :next
811
815
  end
@@ -1132,10 +1132,13 @@ module Samagotchi
1132
1132
  def dispatch(tool_calls, iteration, cap)
1133
1133
  log_direct_deferred(tool_calls)
1134
1134
  calls = tool_calls.map { |tool_call| NativeToolNormalizer.normalize(tool_call) }
1135
+ # The batch's ids before it runs (each run's events carry its own):
1136
+ # the same as one next id per result as it lands
1137
+ # (spec/tool_ids_pin_spec.rb).
1138
+ ids = ToolIds.next_ids(@conversation, calls.size)
1135
1139
  runs = ToolResponse.run_batch(@loop.tool_runner, calls, context: @turn_context, iteration: iteration, emit: method(:emit),
1136
- on_stream_event: @on_stream_event, cap: cap) do |run, index|
1137
- @conversation << ToolResponse.single(run, tool_call_id: tool_calls[index].id,
1138
- ids: ToolIds.next_ids(@conversation, 1))
1140
+ on_stream_event: @on_stream_event, cap: cap, ids: ids) do |run, index|
1141
+ @conversation << ToolResponse.single(run, tool_call_id: tool_calls[index].id, ids: [ids[index]])
1139
1142
  end
1140
1143
  @tool_activity.concat(ToolResponse.activities(runs))
1141
1144
  end
@@ -148,10 +148,10 @@ module Samagotchi
148
148
  end
149
149
 
150
150
  # Where one value came from: :session, :model_setting, :host_setting
151
- # or :config, and the place as a warning names it ("the session",
152
- # "models: deepseek", "hosts entry 'box'", "llm_context.apply").
151
+ # or :config, and the place, named as the other settings name theirs
152
+ # ("session", "models: deepseek", "hosts.box", "llm_context.apply").
153
153
  Origin = Data.define(:source, :where)
154
- SESSION_ORIGIN = Origin.new(source: :session, where: "the session")
154
+ SESSION_ORIGIN = Origin.new(source: :session, where: "session")
155
155
 
156
156
  # What a turn's strategy, apply rule and budget are, each with its
157
157
  # Origin (/llm-context, /stats, the web's info bar).
@@ -169,26 +169,22 @@ module Samagotchi
169
169
  end
170
170
  end
171
171
 
172
- # The strategy for +target+'s turn.
173
- # @param target [HostRegistry::ModelTarget, nil]
174
- # @param names [Array<String>] HostRegistry#lookup_names
175
- # @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
172
+ # The strategy for +lookup+'s model's turn.
173
+ # @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
176
174
  # @param session [Array<Symbol>, nil] the session's own layers ([]: none)
177
175
  # @param session_apply [Symbol, nil] the session's own apply rule
178
176
  # @param session_budget [Integer, nil] the session's own budget (0: off)
179
177
  # @return [Resolved]
180
- def resolve(target, names:, models: nil, session: nil, session_apply: nil, session_budget: nil)
181
- explain(target, names: names, models: models, session: session, session_apply: session_apply,
182
- session_budget: session_budget).resolved
178
+ def resolve(lookup, session: nil, session_apply: nil, session_budget: nil)
179
+ explain(lookup, session: session, session_apply: session_apply, session_budget: session_budget).resolved
183
180
  end
184
181
 
185
182
  # #resolve, with where each value came from.
186
183
  # @return [Explained]
187
- def explain(target, names:, models: nil, session: nil, session_apply: nil, session_budget: nil)
188
- models ||= ModelSettings.all
189
- layers, source, where = layers_for(target, names, models, session)
190
- apply, apply_origin = apply_for(target, names, models, session_apply)
191
- budget, budget_origin = budget_for(target, names, models, session_budget)
184
+ def explain(lookup, session: nil, session_apply: nil, session_budget: nil)
185
+ layers, source, where = layers_for(lookup, session)
186
+ apply, apply_origin = apply_for(lookup, session_apply)
187
+ budget, budget_origin = budget_for(lookup, session_budget)
192
188
  resolved = resolved(layers, source, where).with(apply: apply, protect_steps: protect_steps,
193
189
  stale_edits: Config.get(STALE_EDITS_SETTING) == true,
194
190
  budget_tokens: budget)
@@ -196,79 +192,47 @@ module Samagotchi
196
192
  budget_tokens: budget_origin)
197
193
  end
198
194
 
199
- # A session's LLM context strategy, apply rule and budget, each with
200
- # where it came from (Explained#summary), as its worker would resolve
201
- # them; nil when they can't be (an unreadable config, a model that
202
- # resolves nowhere). Resolves from config and the registry only: no
203
- # network, no model call. What a session page, a session list's ctx %
204
- # and a spawn-time check all read.
205
- # @param session [Session, nil]
206
- # @param registry [HostRegistry, nil] the hosts to resolve against;
207
- # HostRegistry.new by default (what the caller usually has)
208
- # @return [Hash, nil] Explained#summary, symbol keys
209
- def for_session(session, registry: nil)
210
- return nil if session.nil?
211
-
212
- # Here, not at the top: config.rb requires this file before ConfigFile
213
- # exists, and effective_model.rb requires config.rb (via host_registry.rb).
214
- require_relative "effective_model"
215
- registry ||= HostRegistry.new
216
- own = session.llm_context
217
- EffectiveModel.of_session(session, registry: registry).llm_context_explained(own&.resolve_args || {})&.summary(own)
218
- rescue StandardError
219
- nil
220
- end
221
-
222
- # The budget a session's turns run under (for_session's :budget_tokens),
223
- # nil when it is off or can't be resolved. What a session list's ctx %
224
- # counts against, so a saved session's fill reads as the live meter.
225
- # @param session [Session, nil]
226
- # @param registry [HostRegistry, nil] see .for_session
227
- # @return [Integer, nil]
228
- def session_budget(session, registry: nil)
229
- for_session(session, registry: registry)&.fetch(:budget_tokens, nil)
230
- end
231
-
232
195
  # [the budget, its Origin]: the session's own first (0 there is off,
233
196
  # whatever the model says).
234
- def budget_for(target, names, models, session_budget)
197
+ def budget_for(lookup, session_budget)
235
198
  return [session_budget.positive? ? session_budget : nil, SESSION_ORIGIN] unless session_budget.nil?
236
199
 
237
- if (setting = ModelSettings.find(names, :llm_context_budget_tokens, models: models))
200
+ if (setting = lookup.model(:llm_context_budget_tokens))
238
201
  return [setting.value, Origin.new(source: :model_setting, where: "models: #{setting.key}")]
239
202
  end
240
203
 
241
- budget = target&.entry&.llm_context_budget_tokens
242
- return [budget, Origin.new(source: :host_setting, where: "hosts entry '#{target.entry.name}'")] if budget
204
+ budget = lookup.host(:llm_context_budget_tokens)
205
+ return [budget, Origin.new(source: :host_setting, where: "hosts.#{lookup.host_name}")] if budget
243
206
 
244
- [parse_budget(Config.get(BUDGET_SETTING), BUDGET_SETTING), Origin.new(source: :config, where: BUDGET_SETTING)]
207
+ [parse_budget(lookup.global(BUDGET_SETTING).first, BUDGET_SETTING), Origin.new(source: :config, where: BUDGET_SETTING)]
245
208
  end
246
209
 
247
- def layers_for(target, names, models, session)
210
+ def layers_for(lookup, session)
248
211
  return [session, :session, SESSION_ORIGIN.where] if session
249
212
 
250
- if (setting = ModelSettings.find(names, :llm_context_strategy, models: models))
213
+ if (setting = lookup.model(:llm_context_strategy))
251
214
  return [setting.value, :model_setting, "models: #{setting.key}"]
252
215
  end
253
216
 
254
- layers = target&.entry&.llm_context_strategy
255
- return [layers, :host_setting, "hosts entry '#{target.entry.name}'"] if layers
217
+ layers = lookup.host(:llm_context_strategy)
218
+ return [layers, :host_setting, "hosts.#{lookup.host_name}"] if layers
256
219
 
257
- [parse(Config.get(SETTING), SETTING) || [], :config, SETTING]
220
+ [parse(lookup.global(SETTING).first, SETTING) || [], :config, SETTING]
258
221
  end
259
222
 
260
223
  # [the apply rule, its Origin].
261
- def apply_for(target, names, models, session_apply)
224
+ def apply_for(lookup, session_apply)
262
225
  return [session_apply, SESSION_ORIGIN] if session_apply
263
226
 
264
- if (setting = ModelSettings.find(names, :llm_context_apply, models: models))
227
+ if (setting = lookup.model(:llm_context_apply))
265
228
  return [setting.value, Origin.new(source: :model_setting, where: "models: #{setting.key}")]
266
229
  end
267
230
 
268
- apply = target&.entry&.llm_context_apply
269
- return [apply, Origin.new(source: :host_setting, where: "hosts entry '#{target.entry.name}'")] if apply
231
+ apply = lookup.host(:llm_context_apply)
232
+ return [apply, Origin.new(source: :host_setting, where: "hosts.#{lookup.host_name}")] if apply
270
233
 
271
- [parse_apply(Config.get(APPLY_SETTING), APPLY_SETTING) || DEFAULT_APPLY, Origin.new(source: :config, where: APPLY_SETTING)]
234
+ [parse_apply(lookup.global(APPLY_SETTING).first, APPLY_SETTING) || DEFAULT_APPLY,
235
+ Origin.new(source: :config, where: APPLY_SETTING)]
272
236
  end
273
237
 
274
238
  # llm_context.protect_steps: 0 or more (a negative one is the default).