samagotchi 0.154.1 → 0.156.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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/bin/chi +1 -1
  4. data/docs/architecture.md +12 -7
  5. data/docs/bundles.md +21 -0
  6. data/docs/cli.md +1 -1
  7. data/docs/guardrails.md +2 -2
  8. data/docs/hooks.md +1 -1
  9. data/docs/internals/anthropic.md +5 -5
  10. data/docs/internals/attached-context.md +9 -6
  11. data/docs/internals/background-tasks.md +1 -1
  12. data/docs/internals/block-renderers.md +8 -1
  13. data/docs/internals/engine.md +642 -831
  14. data/docs/internals/inline-widgets.md +136 -0
  15. data/docs/internals/llm-context-forget.md +8 -3
  16. data/docs/internals/objectives.md +8 -6
  17. data/docs/internals/participants.md +5 -5
  18. data/docs/internals/prompt-caching.md +13 -6
  19. data/docs/internals/remote-sessions.md +3 -1
  20. data/docs/internals/web-frontend.md +1 -1
  21. data/docs/internals/working-set.md +2 -1
  22. data/docs/plugins.md +2 -0
  23. data/docs/releasing.md +1 -1
  24. data/docs/sessions.md +4 -4
  25. data/docs/system-one.md +3 -2
  26. data/docs/testing.md +1 -1
  27. data/lib/samagotchi/block_renderers.rb +5 -0
  28. data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
  29. data/lib/samagotchi/broadcast_command.rb +3 -3
  30. data/lib/samagotchi/bundle_command/check.rb +1 -1
  31. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  32. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  33. data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
  34. data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
  35. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  36. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  37. data/lib/samagotchi/child_move.rb +7 -51
  38. data/lib/samagotchi/children_status.rb +1 -1
  39. data/lib/samagotchi/cli/parent_wait.rb +1 -1
  40. data/lib/samagotchi/config.rb +17 -112
  41. data/lib/samagotchi/context_command.rb +1 -2
  42. data/lib/samagotchi/context_window.rb +3 -3
  43. data/lib/samagotchi/desktop/macos.rb +1 -1
  44. data/lib/samagotchi/engine/context_judge.rb +21 -119
  45. data/lib/samagotchi/engine/guardrails.rb +4 -2
  46. data/lib/samagotchi/engine/messages.rb +9 -6
  47. data/lib/samagotchi/engine/models.rb +5 -2
  48. data/lib/samagotchi/engine/objectives.rb +30 -80
  49. data/lib/samagotchi/engine/participants.rb +15 -10
  50. data/lib/samagotchi/engine/plugins.rb +3 -1
  51. data/lib/samagotchi/engine/questions.rb +1 -1
  52. data/lib/samagotchi/engine/side_requests.rb +26 -13
  53. data/lib/samagotchi/engine/toolkit.rb +35 -191
  54. data/lib/samagotchi/engine/turn_end.rb +1 -1
  55. data/lib/samagotchi/engine/turn_helpers.rb +16 -39
  56. data/lib/samagotchi/engine/verify.rb +7 -143
  57. data/lib/samagotchi/engine.rb +166 -98
  58. data/lib/samagotchi/engine_peers.rb +1 -1
  59. data/lib/samagotchi/event_fanout.rb +18 -12
  60. data/lib/samagotchi/guardrail_wiring.rb +83 -82
  61. data/lib/samagotchi/guardrails/gate.rb +41 -40
  62. data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
  63. data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
  64. data/lib/samagotchi/guardrails/targets.rb +28 -11
  65. data/lib/samagotchi/guardrails_command.rb +1 -1
  66. data/lib/samagotchi/host_config.rb +72 -1
  67. data/lib/samagotchi/host_model.rb +2 -6
  68. data/lib/samagotchi/host_registry.rb +1 -1
  69. data/lib/samagotchi/inline_widgets.rb +325 -0
  70. data/lib/samagotchi/kernel_loop.rb +215 -313
  71. data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
  72. data/lib/samagotchi/llm/backend.rb +3 -1
  73. data/lib/samagotchi/llm/chat_loop.rb +87 -49
  74. data/lib/samagotchi/llm/native_backend.rb +3 -2
  75. data/lib/samagotchi/llm_context_apply.rb +54 -1
  76. data/lib/samagotchi/llm_context_forget.rb +5 -3
  77. data/lib/samagotchi/llm_context_notice.rb +1 -1
  78. data/lib/samagotchi/llm_context_strategy.rb +7 -9
  79. data/lib/samagotchi/llm_context_view.rb +9 -0
  80. data/lib/samagotchi/model_binding.rb +3 -3
  81. data/lib/samagotchi/model_price.rb +4 -10
  82. data/lib/samagotchi/model_profile.rb +6 -6
  83. data/lib/samagotchi/model_settings.rb +37 -2
  84. data/lib/samagotchi/note_command.rb +1 -2
  85. data/lib/samagotchi/objectives_audit.rb +4 -3
  86. data/lib/samagotchi/participants.rb +2 -2
  87. data/lib/samagotchi/plugin/context.rb +1 -1
  88. data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
  89. data/lib/samagotchi/plugin/sessions.rb +2 -5
  90. data/lib/samagotchi/plugin/tool_spec.rb +5 -3
  91. data/lib/samagotchi/project_env.rb +1 -1
  92. data/lib/samagotchi/question_desk.rb +1 -1
  93. data/lib/samagotchi/reasoning_history.rb +3 -3
  94. data/lib/samagotchi/reminder_carry.rb +1 -1
  95. data/lib/samagotchi/reprefill_meter.rb +45 -0
  96. data/lib/samagotchi/review_command.rb +1 -1
  97. data/lib/samagotchi/revise_anchor.rb +1 -1
  98. data/lib/samagotchi/sampling_settings.rb +3 -3
  99. data/lib/samagotchi/self_report.rb +7 -7
  100. data/lib/samagotchi/session.rb +50 -7
  101. data/lib/samagotchi/session_access/local.rb +7 -5
  102. data/lib/samagotchi/session_access/remote.rb +17 -3
  103. data/lib/samagotchi/session_archive_command.rb +2 -2
  104. data/lib/samagotchi/session_chain.rb +7 -6
  105. data/lib/samagotchi/session_continue.rb +6 -5
  106. data/lib/samagotchi/session_delete_command.rb +1 -1
  107. data/lib/samagotchi/session_manager/delivery.rb +254 -0
  108. data/lib/samagotchi/session_manager/emptiness.rb +138 -0
  109. data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
  110. data/lib/samagotchi/session_manager/listing.rb +189 -0
  111. data/lib/samagotchi/session_manager/removal.rb +190 -0
  112. data/lib/samagotchi/session_manager/spawn.rb +242 -0
  113. data/lib/samagotchi/session_manager/worker_command.rb +157 -0
  114. data/lib/samagotchi/session_manager.rb +26 -1540
  115. data/lib/samagotchi/session_metrics.rb +1 -1
  116. data/lib/samagotchi/sessions_command.rb +1 -1
  117. data/lib/samagotchi/share_command.rb +1 -1
  118. data/lib/samagotchi/starting_marker.rb +65 -0
  119. data/lib/samagotchi/steer_cut_after.rb +3 -3
  120. data/lib/samagotchi/system_one/recorder.rb +49 -0
  121. data/lib/samagotchi/system_prompt.rb +37 -30
  122. data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
  123. data/lib/samagotchi/terminal_ui.rb +2 -6
  124. data/lib/samagotchi/testing/boundary.rb +7 -0
  125. data/lib/samagotchi/testing/bundle.rb +7 -5
  126. data/lib/samagotchi/testing/install.rb +3 -2
  127. data/lib/samagotchi/testing/sessions.rb +1 -1
  128. data/lib/samagotchi/thinking.rb +3 -3
  129. data/lib/samagotchi/token_usage.rb +1 -1
  130. data/lib/samagotchi/tool_activity.rb +15 -7
  131. data/lib/samagotchi/tool_declarations.rb +6 -6
  132. data/lib/samagotchi/tool_output_cut.rb +17 -0
  133. data/lib/samagotchi/tool_response.rb +4 -3
  134. data/lib/samagotchi/tool_runner.rb +96 -102
  135. data/lib/samagotchi/toolkit_binding.rb +40 -14
  136. data/lib/samagotchi/toolkit_command.rb +1 -1
  137. data/lib/samagotchi/tools/deferred_tools.rb +17 -21
  138. data/lib/samagotchi/tools/delegate.rb +10 -5
  139. data/lib/samagotchi/tools/delegate_result.rb +4 -4
  140. data/lib/samagotchi/tools/index_hint.rb +14 -0
  141. data/lib/samagotchi/tools/registry.rb +64 -44
  142. data/lib/samagotchi/tools/send_note.rb +4 -6
  143. data/lib/samagotchi/tools/tool_index.rb +150 -102
  144. data/lib/samagotchi/tools/tool_surface.rb +187 -0
  145. data/lib/samagotchi/turn_context.rb +68 -0
  146. data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
  147. data/lib/samagotchi/turn_stages/objectives.rb +150 -0
  148. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
  149. data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
  150. data/lib/samagotchi/turn_stages/warmup.rb +82 -0
  151. data/lib/samagotchi/turn_state.rb +46 -4
  152. data/lib/samagotchi/version.rb +1 -1
  153. data/lib/samagotchi/vision_support.rb +3 -3
  154. data/lib/samagotchi/web/app.rb +15 -8
  155. data/lib/samagotchi/web/markdown_renderer.rb +39 -14
  156. data/lib/samagotchi/web/message_display.rb +1 -1
  157. data/lib/samagotchi/web/participant_access.rb +2 -2
  158. data/lib/samagotchi/web/public/app.js +5 -1
  159. data/lib/samagotchi/web/public/block_host.js +74 -199
  160. data/lib/samagotchi/web/public/block_renderers.css +2 -0
  161. data/lib/samagotchi/web/public/block_source.js +7 -4
  162. data/lib/samagotchi/web/public/block_watch.js +256 -0
  163. data/lib/samagotchi/web/public/composer_fold.js +3 -0
  164. data/lib/samagotchi/web/public/index.html +28 -9
  165. data/lib/samagotchi/web/public/inline_widgets.js +146 -0
  166. data/lib/samagotchi/web/public/owner_verify.js +1 -1
  167. data/lib/samagotchi/web/public/participant.js +1 -1
  168. data/lib/samagotchi/web/public/revise_stack.js +1 -1
  169. data/lib/samagotchi/web/public/stage_watch.js +6 -2
  170. data/lib/samagotchi/web/public/text_roots.js +1 -1
  171. data/lib/samagotchi/web/public/verify_target.js +1 -1
  172. data/lib/samagotchi/web/public/widget_picks.js +130 -0
  173. data/lib/samagotchi/web/public/widget_walk.js +230 -0
  174. data/lib/samagotchi/web/public/widgets.css +70 -0
  175. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  176. data/lib/samagotchi/web/session_control.rb +3 -3
  177. data/lib/samagotchi/web/session_create.rb +3 -3
  178. data/lib/samagotchi/web/session_hub.rb +1 -1
  179. data/lib/samagotchi/web/session_show.rb +3 -3
  180. data/lib/samagotchi/web/session_summary.rb +3 -4
  181. data/lib/samagotchi/worker.rb +55 -27
  182. data/lib/samagotchi/worker_boot.rb +184 -0
  183. data/lib/samagotchi/worker_signals.rb +1 -1
  184. data/lib/samagotchi/worker_task_wakes.rb +2 -2
  185. metadata +29 -1
@@ -142,7 +142,7 @@ module Samagotchi
142
142
  :cached_sum,
143
143
  :cache_write_sum,
144
144
  # What the server prefilled again of the previous request's prompt
145
- # (KernelLoop#reprefilled_tokens; an LLM context edit's cache break).
145
+ # (ReprefillMeter#tokens; an LLM context edit's cache break).
146
146
  :reprefill_sum,
147
147
  :reasoning_sum,
148
148
  :cost_sum,
@@ -302,7 +302,7 @@ module Samagotchi
302
302
  waiting = s.waiting_question(live: !!live)
303
303
  status = if waiting&.dig(:relayed_to) then "waiting (in parent #{waiting[:relayed_to]})"
304
304
  elsif waiting then "waiting"
305
- else s.status
305
+ else s.displayed_status(owner: Samagotchi::SessionManager.session_owner(s.id, state_dir: state_dir))
306
306
  end
307
307
  "#{s.id} #{status.ljust(8)} #{ctx.call(s).ljust(8)} #{s.updated_at} #{list_text.call(s)}#{flag}#{child}"
308
308
  end
@@ -101,7 +101,7 @@ module Samagotchi
101
101
  error_line("#{command_name}: #{e.message}")
102
102
  CLI::Exit::FAILED
103
103
  rescue ArgumentError => e
104
- error_line("#{command_name}: #{e.is_a?(Session::AmbiguousId) ? e.message : e.message.sub(/\ASession not found: /, "no session ")}")
104
+ error_line("#{command_name}: #{e.message}")
105
105
  CLI::Exit::FAILED
106
106
  end
107
107
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "time"
6
+ require_relative "atomic_file"
7
+ require_relative "session"
8
+
9
+ module Samagotchi
10
+ # A continue's "starting" marker on its new link: written before the
11
+ # continue moves delegates there (ChildMove) and spawns the link's worker,
12
+ # removed by that worker once it holds the owner lock (Worker#start). While
13
+ # it is #fresh?, nothing else spawns a worker for the link (SpawnPart's
14
+ # resume_session and wake_for_report): a ring from a moved child must not
15
+ # start a second one. CROSS-VERSION: the file name and its {pid, at} are
16
+ # read and cleared by other chi versions (spec/worker_contract_spec.rb).
17
+ module StartingMarker
18
+ # <new link dir>/starting: {pid, at}, from the move to the worker's spawn.
19
+ FILE = "starting"
20
+ # A starting marker older than this, or whose process is gone, is left
21
+ # over from a continue that died: it holds nothing back.
22
+ STALE = 60.0
23
+
24
+ module_function
25
+
26
+ # Hold off a worker for the continue's new link (#fresh?) until its
27
+ # own worker holds it: written before the move, removed by that worker
28
+ # once it has the owner lock (Worker#start).
29
+ def mark(session_id, state_dir:)
30
+ dir = Session.session_dir(session_id, state_dir: state_dir)
31
+ FileUtils.mkdir_p(dir)
32
+ AtomicFile.write(File.join(dir, FILE), JSON.generate({ "pid" => Process.pid, "at" => Time.now.iso8601(3) }))
33
+ end
34
+
35
+ def clear(session_id, state_dir:)
36
+ FileUtils.rm_f(File.join(Session.session_dir(session_id, state_dir: state_dir), FILE))
37
+ end
38
+
39
+ # Whether a continue is moving delegates to +session_id+ right now: its
40
+ # marker is there, fresh, and the process that wrote it runs.
41
+ def fresh?(session_id, state_dir:)
42
+ file = File.join(Session.session_dir(session_id, state_dir: state_dir), FILE)
43
+ return false unless File.exist?(file)
44
+
45
+ data = JSON.parse(File.read(file))
46
+ return false if Time.now - Time.iso8601(data["at"].to_s) > STALE
47
+
48
+ process_alive?(data["pid"].to_i)
49
+ rescue JSON::ParserError, ArgumentError, SystemCallError
50
+ false
51
+ end
52
+
53
+ # Signal 0 only asks whether the process exists.
54
+ def process_alive?(pid)
55
+ return false unless pid.positive?
56
+
57
+ Process.kill(0, pid)
58
+ true
59
+ rescue Errno::ESRCH
60
+ false
61
+ rescue Errno::EPERM
62
+ true
63
+ end
64
+ end
65
+ end
@@ -37,18 +37,18 @@ module Samagotchi
37
37
  # "steer.cut_after"; nil for the default).
38
38
  # @param target [HostRegistry::ModelTarget]
39
39
  # @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
40
- # @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
40
+ # @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
41
41
  # @return [Array(Integer, String|nil)]
42
42
  def resolve(target, names:, models: nil)
43
43
  value, origin = Config.get_with_origin("steer.cut_after")
44
44
  return [value.to_i, ENV_KEY] if origin == :env
45
45
 
46
46
  models ||= begin
47
- ConfigFile.model_settings
47
+ ModelSettings.all
48
48
  rescue StandardError
49
49
  {}
50
50
  end
51
- setting = ConfigFile.model_setting(names, :steer_cut_after, models: models)
51
+ setting = ModelSettings.find(names, :steer_cut_after, models: models)
52
52
  return [setting.value, "models: #{setting.key}"] if setting
53
53
 
54
54
  host = target.entry.steer_cut_after
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../log"
4
+ require_relative "recording"
5
+ require_relative "runs"
6
+
7
+ module Samagotchi
8
+ module SystemOne
9
+ # What a turn stage needs to save its System 1 asks as runs: the
10
+ # Engine's Runs and the session's model as the Engine names it now.
11
+ # Engine#system_one_recorder builds one fresh at each stage call (never
12
+ # kept: an outgoing ask after a before_turn hook's model switch names
13
+ # the new model), on the caller's thread; the stage keeps it only for
14
+ # that call. NONE is what a stage gets when the Engine couldn't build
15
+ # one: no runs, every client as it was.
16
+ #
17
+ # +runs+ [Runs, nil] where the runs go (Runs#on? false: system_one.runs
18
+ # off); nil: NONE
19
+ # +session_model+ [String, nil] Engine#effective_model_name at the call
20
+ Recorder = Data.define(:runs, :session_model) do
21
+ # +client+ (nil for none) saving its asks as +judgment+'s runs. Anything
22
+ # going wrong here leaves the client as it was: a run is never worth a
23
+ # judgment. A scratch session's asks aren't saved; a participant's
24
+ # turn is never judged, so it never comes here.
25
+ # @param mode [String, nil] the use's mode, for its runs (Recording.wrap)
26
+ def wrap(client, judgment, session, mode: nil)
27
+ return client if client.nil? || runs.nil? || session&.scratch
28
+
29
+ context = Recording::Context.of(session, session_model: session && session_model)
30
+ Recording.wrap(client, judgment: judgment, runs: runs, context: context, recorded_mode: mode)
31
+ rescue StandardError => e
32
+ Recorder.failed(e)
33
+ client
34
+ end
35
+
36
+ # Whether runs are saved (an ask whose only use is its run is made
37
+ # only then: ObjectivesAudit).
38
+ def saving? = !runs.nil? && runs.on?
39
+
40
+ # The one warn for a recording that can't be set up.
41
+ def self.failed(error)
42
+ Samagotchi::Log.warn(:system_one, "system_one_recording_failed", error: error.class.name, msg: error.message.to_s[0, 200])
43
+ end
44
+ end
45
+
46
+ # No recording: the plain client, no runs.
47
+ Recorder::NONE = Recorder.new(runs: nil, session_model: nil)
48
+ end
49
+ end
@@ -8,6 +8,7 @@ require_relative "memory_paths"
8
8
  require_relative "paths"
9
9
  require_relative "tool_declarations"
10
10
  require_relative "tools/memory"
11
+ require_relative "tools/tool_surface"
11
12
  require_relative "muted_memories"
12
13
  require_relative "model_notes"
13
14
  require_relative "bundle_needs"
@@ -46,7 +47,8 @@ module Samagotchi
46
47
  attr_reader :requested_memories
47
48
 
48
49
  # @param profile [#call] → ModelProfile
49
- # @param tools [#call] → Tools::Registry
50
+ # @param tools [#call] → Tools::Registry, nil (no tools): each build
51
+ # reads it as a Tools::ToolSurface under the build's layers
50
52
  # @param session [#call] → Session, nil
51
53
  # @param thinking [#call] → Symbol, the effective model's level (Thinking)
52
54
  # @param model [#call] → ModelIdentity, nil (the section is left out)
@@ -91,8 +93,10 @@ module Samagotchi
91
93
 
92
94
  # A prompt #build made, with what it read while it did: the memory
93
95
  # index sizes (#memory_index), the model notes, the files and the
94
- # plugins' sections it holds, and its stable part's length (chat).
95
- Built = Data.define(:prompt, :memory_index, :notes, :files, :sections, :stable_length)
96
+ # plugins' sections it holds, its stable part's length (chat) and the
97
+ # Tools::ToolSurface it declares (the tools, the not-loaded line, the
98
+ # kit's prompt text).
99
+ Built = Data.define(:prompt, :memory_index, :notes, :files, :sections, :stable_length, :surface)
96
100
 
97
101
  # The full prompt: #base wrapped with the index and the rest. Built once
98
102
  # per loop and level (until #reset!) so the prompt prefix, and the
@@ -110,7 +114,7 @@ module Samagotchi
110
114
  # @param chat [Boolean] for the chat loop
111
115
  # @param thinking [Symbol, nil] the level (Thinking); nil: the effective model's
112
116
  # @param layers [Array<Symbol>] the turn's LLM context layers: the
113
- # native prompt declares the tools they add (Tools::Registry#entries)
117
+ # native prompt declares the tools they add (Tools::ToolSurface#declared)
114
118
  def build(chat: false, thinking: nil, layers: [])
115
119
  key = [chat, thinking, layers]
116
120
  cache = @cache
@@ -172,7 +176,8 @@ module Samagotchi
172
176
  # The base prompt (specs, plugins' declarations), with Gemma's tool
173
177
  # declarations at its end as #build has them.
174
178
  def base(chat: false, layers: [])
175
- with_tail_declarations(assist_system_prompt(chat: chat, layers: layers), chat: chat, layers: layers)
179
+ surface = tool_surface(layers)
180
+ with_tail_declarations(assist_system_prompt(chat: chat, surface: surface), chat: chat, surface: surface)
176
181
  end
177
182
 
178
183
  # Names activated via preloaded --memory entries during prompt
@@ -192,12 +197,20 @@ module Samagotchi
192
197
  @section_files = []
193
198
  @section_plugins = []
194
199
  @measured_stable_length = nil
195
- prompt = system_prompt_with_index(assist_system_prompt(chat: chat, layers: layers),
196
- chat: chat, thinking: thinking, layers: layers)
200
+ surface = tool_surface(layers)
201
+ prompt = system_prompt_with_index(assist_system_prompt(chat: chat, surface: surface),
202
+ chat: chat, thinking: thinking, surface: surface)
197
203
  Built.new(prompt: prompt, memory_index: @measured_index, notes: @section_notes || [],
198
- files: @section_files.freeze, sections: @section_plugins.freeze, stable_length: @measured_stable_length)
204
+ files: @section_files.freeze, sections: @section_plugins.freeze, stable_length: @measured_stable_length,
205
+ surface: surface)
199
206
  end
200
207
 
208
+ # The session's tools as a build under +layers+ declares them, read
209
+ # from the Registry now (one per build: the declarations, the
210
+ # not-loaded line and the kit's text agree).
211
+ # @return [Tools::ToolSurface]
212
+ def tool_surface(layers) = Tools::ToolSurface.for(@tools_lookup.call, layers: layers)
213
+
201
214
  def profile = @profile_lookup.call
202
215
 
203
216
  def turn_thinking = @thinking_lookup.call
@@ -208,8 +221,8 @@ module Samagotchi
208
221
 
209
222
  # ── Tool declarations ──────────────────────────────────────────────────────
210
223
 
211
- def tool_declarations(layers)
212
- schemas = ToolDeclarations.native_schemas(@tools_lookup.call, layers: layers)
224
+ def tool_declarations(surface)
225
+ schemas = ToolDeclarations.native_schemas(surface)
213
226
  case profile.name
214
227
  when "qwen36"
215
228
  ToolDeclarations.qwen_declarations(schemas)
@@ -232,7 +245,7 @@ module Samagotchi
232
245
 
233
246
  # @param chat [Boolean] for the chat loop: no tool declarations or call
234
247
  # syntax (its tools go as schemas with each request)
235
- def assist_system_prompt(chat: false, layers: [])
248
+ def assist_system_prompt(surface:, chat: false)
236
249
  return chat_system_prompt if chat
237
250
 
238
251
  hint = tool_call_hint
@@ -241,7 +254,7 @@ module Samagotchi
241
254
  tools = if gemma_tail_declarations?
242
255
  "Your tools are declared at the end of this system prompt."
243
256
  else
244
- "You have access to the following tools:\n\n#{tool_declarations(layers)}\n"
257
+ "You have access to the following tools:\n\n#{tool_declarations(surface)}\n"
245
258
  end
246
259
 
247
260
  <<~SYS
@@ -332,7 +345,7 @@ module Samagotchi
332
345
  # memory write changes them) after all of them; the model, the location
333
346
  # and the session close the prompt (Gemma's tool declarations still
334
347
  # follow them).
335
- def system_prompt_with_index(base, chat: false, thinking: nil, layers: [])
348
+ def system_prompt_with_index(base, chat: false, thinking: nil, surface: tool_surface([]))
336
349
  thinking_token = chat ? "" : Thinking.native(thinking || turn_thinking, profile).system_token
337
350
  indexes = { project: read_memory_index("project"), system: read_memory_index("system") }
338
351
  @measured_index = measure_indexes(indexes)
@@ -340,15 +353,15 @@ module Samagotchi
340
353
  "Project memories:\n#{indexes[:project]}",
341
354
  "System memories:\n#{indexes[:system]}"
342
355
  ].join("\n\n")
343
- stable = [thinking_token + base, not_loaded_line(layers), rg_guidance, system_identity_section, model_notes_section, explicit_memory_section,
344
- project_specific_description, appended_section, kit_prompt_section, plugin_prompt_section, memory_sections]
356
+ stable = [thinking_token + base, not_loaded_line(surface), rg_guidance, system_identity_section, model_notes_section, explicit_memory_section,
357
+ project_specific_description, appended_section, kit_prompt_section(surface), plugin_prompt_section, memory_sections]
345
358
  stable_text = stable.compact.join("\n")
346
359
  volatile = [participants_section, current_model, project_location, current_session].compact
347
- return with_tail_declarations(stable_text, chat: chat, layers: layers) if volatile.empty?
360
+ return with_tail_declarations(stable_text, chat: chat, surface: surface) if volatile.empty?
348
361
 
349
362
  prompt = "#{stable_text}\n#{volatile.join("\n")}"
350
363
  @measured_stable_length = stable_text.length + 1 if chat
351
- with_tail_declarations(prompt, chat: chat, layers: layers)
364
+ with_tail_declarations(prompt, chat: chat, surface: surface)
352
365
  end
353
366
 
354
367
  # The tools the session's toolkit leaves out, named once right after the
@@ -357,15 +370,12 @@ module Samagotchi
357
370
  # the prompt is what it was before toolkits. The ones call_tool reaches
358
371
  # and the ones the toolkit removes (only the user brings those back)
359
372
  # get a sentence each; with call_tool not declared, one list.
360
- def not_loaded_line(layers)
361
- registry = @tools_lookup.call
362
- return nil unless registry.respond_to?(:left_out_names)
363
-
364
- names = registry.left_out_names(layers: layers)
373
+ def not_loaded_line(surface)
374
+ names = surface.left_out.keys
365
375
  return nil if names.empty?
366
376
 
367
- toolkit = "(toolkit #{registry.toolkit&.name})"
368
- reachable = registry.reachable_names(layers: layers)
377
+ toolkit = "(toolkit #{surface.toolkit&.name})"
378
+ reachable = surface.index_only.map(&:name)
369
379
  if reachable.empty?
370
380
  return "Not loaded in this session #{toolkit}: #{names.join(", ")}. Only the user can load one (/toolkit add <name>)."
371
381
  end
@@ -387,10 +397,7 @@ module Samagotchi
387
397
  # sessions on that kit share it, and a memory write doesn't move it.
388
398
  # nil on a kit without one and on full: the prompt is what it was
389
399
  # before kit prompts.
390
- def kit_prompt_section
391
- registry = @tools_lookup.call
392
- registry.respond_to?(:toolkit) ? registry.toolkit&.prompt : nil
393
- end
400
+ def kit_prompt_section(surface) = surface.toolkit&.prompt
394
401
 
395
402
  # The plugins' prompt sections (chi.prompt_section), right after the
396
403
  # kit prompt and before the memory indexes: a plugin's text depends on
@@ -406,8 +413,8 @@ module Samagotchi
406
413
  end
407
414
  end
408
415
 
409
- def with_tail_declarations(prompt, chat:, layers:)
410
- chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations(layers)
416
+ def with_tail_declarations(prompt, chat:, surface:)
417
+ chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations(surface)
411
418
  end
412
419
 
413
420
  def gemma_tail_declarations? = profile.name == "gemma4"
@@ -14,7 +14,7 @@ module Samagotchi
14
14
  LATE = "worker not answering in time"
15
15
  # An exit the worker agreed to in an empty session. Said before it has
16
16
  # left: it checks again once it lets go, and a note or prompt arriving
17
- # first keeps the session (SessionManager.discard_left_session).
17
+ # first keeps the session (WorkerBoot.discard_left_session).
18
18
  EMPTY_WORDS = "the session is empty: it will be discarded if nothing arrives before its worker stops."
19
19
  EMPTY_EXIT = "Detached; #{EMPTY_WORDS}"
20
20
  # Why the worker stayed up after /exit, by the reason it gave.
@@ -44,8 +44,6 @@ module Samagotchi
44
44
  # Raised when another process (a worker or another chi) owns the session
45
45
  # this run was asked to use.
46
46
  class SessionBusy < StandardError; end
47
- # --resume with an id that has no saved session.
48
- class SessionNotFound < StandardError; end
49
47
 
50
48
  EMPTY_ANSWER_ERROR = "chi: the model gave an empty answer"
51
49
 
@@ -100,9 +98,7 @@ module Samagotchi
100
98
  # between the load and the lock that this run would later save over.
101
99
  # An unknown id is refused before the claim, which would leave an
102
100
  # empty session dir behind.
103
- if session_id && !Session.exist?(session_id)
104
- raise SessionNotFound, "Session not found: #{session_id}"
105
- end
101
+ raise Session::NotFound, session_id if session_id && !Session.exist?(session_id)
106
102
 
107
103
  claim_session!(session_id) if session_id
108
104
  @resume_session = session_id ? Session.load(session_id) : nil
@@ -434,7 +430,7 @@ module Samagotchi
434
430
 
435
431
  # A fresh session reaches the disk before its first turn: a tool in that
436
432
  # turn reads it from there (delegate loads its parent to start a child,
437
- # "Session not found" otherwise).
433
+ # "no session …" otherwise).
438
434
  def save_before_first_turn(session)
439
435
  session.save unless Session.exist?(session.id)
440
436
  end
@@ -94,6 +94,13 @@ module Samagotchi
94
94
  "Samagotchi::AnswerRefs.new" => "spec: after_turn's refs: helper, as chi's fire site builds it",
95
95
  "Samagotchi::Log" => "spec: stubs Log to check what ctx.log recorded: a kit log recorder could replace it"
96
96
  },
97
+ "inline-widgets" => {
98
+ 'require "samagotchi/inline_widgets"' => "spec: for InlineWidgets",
99
+ "Samagotchi::InlineWidgets.parse" => "spec: each example the sections teach parses strictly, as chi web " \
100
+ "parses a block (the drawing is core's, so is the parse)",
101
+ "Samagotchi::Plugin::PromptSections::MAX" => "spec: the prompt-section limit a section over is dropped at: " \
102
+ "could be a stable constant"
103
+ },
97
104
  "loop-guard" => {
98
105
  'require "samagotchi/llm/native_tool_normalizer"' => "spec: for NativeToolNormalizer",
99
106
  "Samagotchi::LLM::NativeToolNormalizer.normalize" => "spec: replays recorded calls in the shape chi's parser gives",
@@ -11,6 +11,7 @@ require_relative "../tools/tool_index"
11
11
  require_relative "../peer_sources"
12
12
  require_relative "../toolkit"
13
13
  require_relative "../guardrails/gate"
14
+ require_relative "../turn_context"
14
15
  require_relative "../memory_bundle/manifest"
15
16
  require_relative "../memory_bundle/source"
16
17
  require_relative "../session_setup"
@@ -239,8 +240,9 @@ module Samagotchi
239
240
  dispatched = nil
240
241
  capture = Hooks::Registry.new
241
242
  capture.register(:before_tool_call) { |event| dispatched = event.dup }
242
- gate = Guardrails::Gate.new(-> { capture }, context_lookup: -> { context }, tools_lookup: -> { tools })
243
- gate.evaluate(call, iteration: iteration, params: params, verdict: verdict || Verdict.new(call: call))
243
+ gate = Guardrails::Gate.new(hooks: capture, tools: tools, context_for: ->(_turn) { context })
244
+ gate.evaluate(call, context: TurnContext.none, iteration: iteration, params: params,
245
+ verdict: verdict || Verdict.new(call: call))
244
246
  raise ArgumentError, "guardrails did not dispatch a before_tool_call event for #{call[:name].inspect}" unless dispatched
245
247
 
246
248
  dispatched.slice(:type, :iteration, :call, :params, :blocked, :block_reason, :guardrail, :context, :targets)
@@ -256,11 +258,11 @@ module Samagotchi
256
258
  def tool_call(call, params: nil, cwd: nil, context: nil, verdict: nil, iteration: 1)
257
259
  context ||= Guardrails::Context.new(cwd: cwd || @recorder[:cwd], session_id: @recorder[:session_id],
258
260
  interface: @recorder[:frontend])
259
- gate = Guardrails::Gate.new(-> { hooks }, context_lookup: -> { context }, tools_lookup: -> { tools },
260
- checks_lookup: -> { [] })
261
+ gate = Guardrails::Gate.new(hooks: hooks, tools: tools, context_for: ->(_turn) { context },
262
+ checks_for: ->(_turn) { [] })
261
263
  verdict ||= Verdict.new(call: call)
262
264
  params ||= "#{call[:name]} #{call[:content] || call[:path]}".strip
263
- failing { gate.evaluate(call, iteration: iteration, params: params, verdict: verdict) }
265
+ failing { gate.evaluate(call, context: TurnContext.none, iteration: iteration, params: params, verdict: verdict) }
264
266
  end
265
267
 
266
268
  # Run a command line ("/name args") as chi does: the first matching
@@ -31,7 +31,7 @@ module Samagotchi
31
31
  # What a bundle registered (#registered): commands, tools (names),
32
32
  # hooks per event, inits (labels), toolkits, tool sources and peer
33
33
  # sources (names).
34
- Registered = Data.define(:commands, :tools, :hooks, :inits, :toolkits, :tool_sources, :peer_sources)
34
+ Registered = Data.define(:commands, :tools, :hooks, :inits, :toolkits, :tool_sources, :peer_sources, :prompt_sections)
35
35
  # A command: its mid_turn policy and its selection-bar labels (chi.annotation).
36
36
  Command = Data.define(:name, :mid_turn, :labels)
37
37
  # A hook on an event: the bundle, the hook (its file, or the plugin's) and its priority.
@@ -98,7 +98,8 @@ module Samagotchi
98
98
  inits: inits.map { |init| init[:label] },
99
99
  toolkits: Array(registries.toolkits).map(&:name),
100
100
  tool_sources: registries.tool_index.sources.map(&:label),
101
- peer_sources: registries.peer_sources.names
101
+ peer_sources: registries.peer_sources.names,
102
+ prompt_sections: registries.prompt_sections.names(name)
102
103
  )
103
104
  end
104
105
 
@@ -60,7 +60,7 @@ module Samagotchi
60
60
  end
61
61
 
62
62
  def read(id)
63
- answer(:read, [id], {}) { raise Plugin::Sessions::Error, "no session #{id}" }
63
+ answer(:read, [id], {}) { raise Plugin::Sessions::Error, Session::NotFound.new(id).message }
64
64
  end
65
65
 
66
66
  def children(all: false)
@@ -84,7 +84,7 @@ module Samagotchi
84
84
  # the default).
85
85
  # @param target [HostRegistry::ModelTarget]
86
86
  # @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
87
- # @param models [Hash{String => ModelSettings}, nil] ConfigFile.model_settings (specs)
87
+ # @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
88
88
  # @param session [Symbol, nil] the session's own level (Session#thinking)
89
89
  # @return [Array(Symbol, String|nil)]
90
90
  def resolve(target, names:, models: nil, session: nil)
@@ -94,11 +94,11 @@ module Samagotchi
94
94
  return [global, origin == :cli ? "--thinking" : "SAMAGOTCHI_THINKING_LEVEL"] if global && %i[cli env].include?(origin)
95
95
 
96
96
  models ||= begin
97
- ConfigFile.model_settings
97
+ ModelSettings.all
98
98
  rescue StandardError
99
99
  {}
100
100
  end
101
- setting = ConfigFile.model_setting(names, :thinking, models: models)
101
+ setting = ModelSettings.find(names, :thinking, models: models)
102
102
  return [setting.value, "models: #{setting.key}"] if setting
103
103
 
104
104
  host = target.entry.thinking
@@ -23,7 +23,7 @@ module Samagotchi
23
23
  class TokenUsage < Data.define(:prompt_tokens, :completion_tokens, :cached_tokens, :cache_write_tokens,
24
24
  :reasoning_tokens, :cost, :predicted_per_second, :prompt_per_second, :predicted_ms, :source)
25
25
  CHARS_PER_TOKEN = 4.0
26
- # Re-prefilled tokens under this are none (KernelLoop#reprefilled_tokens,
26
+ # Re-prefilled tokens under this are none (ReprefillMeter#tokens,
27
27
  # SessionMetrics): servers count the cache in blocks (OpenAI's 128).
28
28
  REPREFILL_MIN_TOKENS = 128
29
29
 
@@ -5,6 +5,7 @@ require_relative "log"
5
5
  require_relative "command_steps"
6
6
  require_relative "tool_view"
7
7
  require_relative "tools/execute"
8
+ require_relative "tools/registry"
8
9
  require_relative "tools/read"
9
10
  require_relative "tools/write"
10
11
  require_relative "tools/memory"
@@ -58,11 +59,11 @@ module Samagotchi
58
59
  # @param registry [Tools::Registry, nil] the session's tools
59
60
  # @param cwd [String, nil] what a file tool's title is relative to (the
60
61
  # worker runs in its session's working directory)
61
- def tool_activity_event(tool_name, call, result, registry: nil, cwd: Dir.pwd)
62
+ def tool_activity_event(tool_name, call, result, registry: nil, cwd: Dir.pwd, surface: nil)
62
63
  event = {
63
64
  action: tool_activity_action(tool_name, registry: registry, call: call),
64
65
  tool: tool_name,
65
- params: tool_activity_params(tool_name, call, registry: registry),
66
+ params: tool_activity_params(tool_name, call, registry: registry, surface: surface),
66
67
  status: tool_activity_status(result, tool_name, command: call.is_a?(Hash) ? call[:content] : nil)
67
68
  }
68
69
  # A grep that found nothing: ok, and the web row says "no match".
@@ -359,7 +360,10 @@ module Samagotchi
359
360
  name == "git" && args.first == "diff" && args.intersect?(%w[--quiet --exit-code])
360
361
  end
361
362
 
362
- def tool_activity_params(tool_name, call, registry: nil)
363
+ # +surface+: the turn's Tools::ToolSurface, which a tool's entry is
364
+ # looked up in, for a preview that reads it (call_tool's); nil outside a
365
+ # turn (+registry+'s entry then).
366
+ def tool_activity_params(tool_name, call, registry: nil, surface: nil)
363
367
  case tool_name
364
368
  when Tools::Execute::NAME
365
369
  "command=#{preview_tool_param(call[:content])}"
@@ -453,7 +457,8 @@ module Samagotchi
453
457
  parts << "options=#{preview_tool_param(Array(opts).join(","))}" if opts && !Array(opts).empty?
454
458
  parts.join(" ")
455
459
  else
456
- registry_params(registry_entry(registry, tool_name), call)
460
+ registry_params(surface ? surface.registered[tool_name.to_s] : registry_entry(registry, tool_name), call,
461
+ surface)
457
462
  end
458
463
  end
459
464
 
@@ -471,14 +476,17 @@ module Samagotchi
471
476
  # key="value" (the parsers' args:, which a registry-less reader such as
472
477
  # the web's reload also has); nil for a tool the registry doesn't know
473
478
  # that has no args:, and for a built-in without its own words above
474
- # (the reminder tools).
475
- def registry_params(entry, call)
479
+ # (the reminder tools). A missing surface (Tools::MissingSurface) is
480
+ # raised, never taken as a failing preview.
481
+ def registry_params(entry, call, surface = nil)
476
482
  return nil if entry&.core?
477
483
  return nil if entry.nil? && !call[:args].is_a?(Hash)
478
484
 
479
485
  if entry&.preview
480
486
  begin
481
- return entry.preview.call(call)
487
+ return entry.preview_of(call, surface)
488
+ rescue Tools::MissingSurface
489
+ raise
482
490
  rescue StandardError => e
483
491
  Log.warn(:plugins, "plugin_preview_failed", tool: entry.name, error: e.class.name,
484
492
  msg: "#{entry.name} preview failed: #{e.message}")
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require_relative "tools/args"
5
- require_relative "llm_context_strategy"
5
+ require_relative "config"
6
6
  require_relative "tools/memory"
7
7
 
8
8
  module Samagotchi
@@ -482,7 +482,7 @@ module Samagotchi
482
482
  # (Tools::Registry's layer gate), so it is not in TOOL_SCHEMAS, which
483
483
  # every turn declares. Its description carries llm_context.policy
484
484
  # (#forget_outputs_schema); this one the default's.
485
- FORGET_OUTPUTS_POLICY = LLMContextStrategy::DEFAULT_POLICY
485
+ FORGET_OUTPUTS_POLICY = Config::DEFAULT_LLM_CONTEXT_POLICY
486
486
  FORGET_OUTPUTS_DESCRIPTION = "Forget tool outputs you no longer need, to free context. Every tool output starts " \
487
487
  "with an id like [#t42]: list ids to forget, and each output is replaced in place by " \
488
488
  "a stub with your note (the call stays). %<policy>s\n" \
@@ -635,10 +635,10 @@ module Samagotchi
635
635
 
636
636
  # The schemas the native (Gemma, Qwen) prompts declare: the built-ins
637
637
  # as they are, and each plugin tool's flattened (.flat_schema).
638
- # @param registry [Tools::Registry]
639
- # @param layers [Array<Symbol>] the turn's LLM context layers (Registry#entries)
640
- def native_schemas(registry, layers: [])
641
- registry.entries(layers: layers).map { |entry| entry.core? ? entry.schema : flat_schema(entry.schema) }
638
+ # @param surface [Tools::ToolSurface] the tools a turn (or a build
639
+ # under its layers) declares (ToolSurface#declared)
640
+ def native_schemas(surface)
641
+ surface.declared.map { |entry| entry.core? ? entry.schema : flat_schema(entry.schema) }
642
642
  end
643
643
 
644
644
  # A plugin tool's schema in the shape the built-ins have, which is all
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "config"
3
4
  require_relative "tool_output_files"
4
5
 
5
6
  module Samagotchi
@@ -28,6 +29,22 @@ module Samagotchi
28
29
  # task's log, the child's reply in its output/ folder), which the tool
29
30
  # gave with its text (Tools::RefText) and the gap line names.
30
31
  module ToolOutputCut
32
+ # The per-output cap with nothing set (max_tool_output_chars).
33
+ DEFAULT_MAX_CHARS = 10_000
34
+
35
+ # The per-output cap both loops cut tool outputs to (and the emitted
36
+ # tool call events carry): an explicit +override+ wins, then
37
+ # max_tool_output_chars (Config). A non-positive value falls back to
38
+ # DEFAULT_MAX_CHARS (there is intentionally no "unlimited": live UIs get
39
+ # a bounded `output:` plus a truthful `output_truncated:` flag). The one
40
+ # exception is memory_read, which ToolRunner leaves out of this cap
41
+ # (memory.read_max_chars, 0: no fixed cap; a memory is read whole).
42
+ # @return [Integer]
43
+ def self.max_chars(override)
44
+ parsed = (override || Config.get("max_tool_output_chars")).to_i
45
+ parsed.positive? ? parsed : DEFAULT_MAX_CHARS
46
+ end
47
+
31
48
  POLICIES = { "execute" => :head_tail_file, "task_wait" => :head_tail_ref, "delegate" => :head_tail_ref,
32
49
  "delegate_result" => :head_tail_ref }.freeze
33
50
 
@@ -34,14 +34,15 @@ module Samagotchi
34
34
  # own go to +on_stream_event+. An Interrupt (Ctrl-C) mid-batch yields an
35
35
  # #interrupted run for every call left (the one it cut short too), so
36
36
  # the loop's conversation answers each call, then goes on up: the turn
37
- # keeps its progress (Engine#ctrl_c_messages).
37
+ # keeps its progress (Engine#ctrl_c_messages). +context+: the calls'
38
+ # turn (TurnContext), each run's (ToolRunner#run).
38
39
  # @return [Array<ToolRun>] the runs
39
- def run_batch(runner, calls, iteration:, emit:, on_stream_event:, cap:)
40
+ def run_batch(runner, calls, context:, iteration:, emit:, on_stream_event:, cap:)
40
41
  # Before anything can be interrupted: the rescue counts it.
41
42
  runs = []
42
43
  emit.call({ type: :tool_dispatch_started, iteration: iteration, call_count: calls.length })
43
44
  calls.each_with_index do |call, index|
44
- runs << runner.run(call, iteration: iteration, call_index: index + 1, call_count: calls.length,
45
+ runs << runner.run(call, context: context, iteration: iteration, call_index: index + 1, call_count: calls.length,
45
46
  on_stream_event: on_stream_event, max_tool_output_chars: cap)
46
47
  yield runs.last, index if block_given?
47
48
  end