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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../bridge/turn_accumulator"
4
+ require_relative "../bridge/turn_drawings"
3
5
  require_relative "../context_note"
4
6
  require_relative "../image_store"
5
7
  require_relative "../log"
@@ -31,11 +33,32 @@ module Samagotchi
31
33
  @renderer = renderer
32
34
  end
33
35
 
36
+ # A snapshot's last_turn_drawing (Bridge#snapshot: the last exchange's
37
+ # turn as live drew it) draws that turn's rows; without one (an older
38
+ # worker, a turn before drawings) they are rebuilt from the messages.
34
39
  def render_join(snapshot, saved)
35
- render_join_header(Array(snapshot[:messages]))
40
+ render_join_header(Array(snapshot[:messages]), drawing: snapshot[:last_turn_drawing])
36
41
  render_join_gap(snapshot, saved)
37
42
  end
38
43
 
44
+ # One replayed event of a turn's rows (TurnAccumulator.replay_events),
45
+ # as live drew it: a joined running turn's (AttachedLoop#render_current_turn)
46
+ # and a drawn finished one's (#render_drawn_turn). Its tool rows with
47
+ # their action and duration, merged input, reminders, a plugin's steer,
48
+ # a hook's notice, a retry's, a steer cut's and a ✂ row's line. The
49
+ # step text, the turn's start and questions are the caller's (the
50
+ # pending one is asked after the replay, answered ones showed no line).
51
+ def replay_row(event)
52
+ case event[:type]
53
+ when :tool_call_started then @renderer.call(event)
54
+ when :tool_call_completed then replay_tool_completed(event)
55
+ when :merged_input then @screen.commit("input> #{event[:content]}")
56
+ when :reminder_injected then @screen.commit(reminder_line(event[:reminders]))
57
+ when :pending_input_merged, :hook_notice, :empty_answer_retry, :steer_cut, :llm_context_edited
58
+ @renderer.call(event)
59
+ end
60
+ end
61
+
39
62
  # The snapshot's cards and between-turns notices (Bridge CardStore):
40
63
  # on join the ones since the last turn (and the running turn's), as
41
64
  # the join header shows the last exchange; on a resync only cards not
@@ -60,6 +83,18 @@ module Samagotchi
60
83
 
61
84
  private
62
85
 
86
+ # A finished tool call: the live row (action, duration: none when the
87
+ # part has none, never the replay's own clock). A snapshot from an
88
+ # older worker has no action: its row names the tool only.
89
+ def replay_tool_completed(event)
90
+ return @renderer.call({ duration_ms: nil }.merge(event)) if event.dig(:activity, :action)
91
+
92
+ activity = EventRenderer.described(event) || {}
93
+ @screen.commit(snapshot_tool_line({ tool: event[:tool], params: activity[:params], status: activity[:status],
94
+ description: activity[:description], output_truncated: event[:output_truncated],
95
+ images: event[:images], diff: event[:diff] }))
96
+ end
97
+
63
98
  # Where the session's last activity was: its newest stamped entry,
64
99
  # else its saved recap's time. A gap ago (and no turn running): one
65
100
  # line after the join block says when.
@@ -71,7 +106,7 @@ module Samagotchi
71
106
  @screen.commit(gap_divider(line)) if line
72
107
  end
73
108
 
74
- def render_join_header(messages)
109
+ def render_join_header(messages, drawing: nil)
75
110
  exchange = messages.select { |m| %w[user model assistant].include?(m[:role].to_s) }
76
111
  # The id stays findable: the detach line and chi sessions list show it.
77
112
  Log.session_id = @link.client.session_id
@@ -89,6 +124,11 @@ module Samagotchi
89
124
  unless ImageRef.placeholder_only?(exchange[last_user][:content])
90
125
  Array(exchange[last_user][:images]).each { |ref| @screen.commit(format_image_line(ref)) }
91
126
  end
127
+ if (drawn = drawn_turn(drawing, exchange[last_user]))
128
+ render_drawn_turn(*drawn, exchange[(last_user + 1)..])
129
+ return render_join_notes(messages, from, after_exchange: true)
130
+ end
131
+
92
132
  render_join_steps(messages, from, durations: join_tool_records(exchange[last_user]))
93
133
  record = @link.saved_turn_record(exchange[last_user][:turn_id] || exchange[last_user]["turn_id"])
94
134
  failed = join_failed_record(record)
@@ -109,16 +149,59 @@ module Samagotchi
109
149
  @screen.commit(@view.turn_canceled_line(record["cancellation_reason"], record["duration_ms"], by: record["cancelled_by"]))
110
150
  @screen.commit(@view.turn_end_hint(ROLLBACK_HINT)) unless record["continue"]
111
151
  else
112
- # The saved answer is raw: the latest keeps its thinking, and one may
113
- # be only a tool call.
114
- answer = exchange[(last_user + 1)..].reverse_each
115
- .map { |m| OutputFormatter.strip_markup(m[:content]) }
116
- .find { |text| !text.empty? }
117
- @screen.commit(last_lines(answer)) if answer
152
+ render_join_answer(exchange[(last_user + 1)..])
118
153
  end
119
154
  render_join_notes(messages, from, after_exchange: true)
120
155
  end
121
156
 
157
+ # The last answer of the messages after the prompt (+after+). The saved
158
+ # answer is raw: the latest keeps its thinking, and one may be only a
159
+ # tool call.
160
+ def render_join_answer(after)
161
+ answer = after.reverse_each.map { |m| OutputFormatter.strip_markup(m[:content]) }.find { |text| !text.empty? }
162
+ @screen.commit(last_lines(answer)) if answer
163
+ end
164
+
165
+ # The snapshot's drawing as [turn, end event] when it is +prompt+'s
166
+ # turn (a Continue's or a reminder's drawing isn't the prompt's: the
167
+ # rows are rebuilt), else nil; nil too for one whose parts aren't a
168
+ # list of rows.
169
+ def drawn_turn(drawing, prompt)
170
+ return nil unless drawing.is_a?(Hash) && drawing[:drawing].is_a?(Hash) && drawing[:end].is_a?(Hash)
171
+
172
+ turn = Bridge::TurnAccumulator.from_wire(drawing[:drawing])
173
+ id = prompt[:turn_id] || prompt["turn_id"]
174
+ return nil unless id && turn[:turn_id].to_s == id.to_s
175
+ # An older worker sends a line its parse didn't check (the unfold
176
+ # would raise after the prompt line is drawn).
177
+ return nil unless Bridge::TurnDrawings.rows?(turn[:parts])
178
+
179
+ [turn, EventRenderer.symbolize(Bridge::TurnAccumulator.from_wire(drawing[:end]))]
180
+ end
181
+
182
+ # The last turn from its drawing, through the renderer as live drew it
183
+ # (the running turn's join path, AttachedLoop#render_current_turn): its
184
+ # rows with their status and duration, a retry's line, a hook's notice,
185
+ # steers, merged input and reminders; no step text (the live ticker's)
186
+ # and no cards (the snapshot's cards are drawn after). Then its end: a
187
+ # cancel's or a failure's line from the end event, else the empty
188
+ # answer's notice or the saved answer (+after+, the messages after the
189
+ # prompt).
190
+ def render_drawn_turn(turn, ending, after)
191
+ Bridge::TurnAccumulator.replay_events(turn).each do |event|
192
+ # The prompt line named its images already.
193
+ event[:type] == :turn_started ? @renderer.call(event.except(:images)) : replay_row(event)
194
+ end
195
+ case ending[:type]
196
+ when :turn_canceled, :turn_failed then @renderer.call(ending)
197
+ else
198
+ empty = ending.dig(:turn_summary, :empty_answer)
199
+ empty ? @screen.commit(format_empty_answer_line(empty[:retries], empty[:refusal])) : render_join_answer(after)
200
+ end
201
+ # A call the turn never answered left the tool slot running.
202
+ @view.finish_thinking_spinner
203
+ end
204
+
122
205
  # What the last exchange did, as the live turn drew it: each saved
123
206
  # call's tool row (action, params, status from its result, and its
124
207
  # duration from +durations+, the turn's saved tool records) and a
@@ -112,13 +112,13 @@ module Samagotchi
112
112
  # no turn id (an older session) or a session with no analytics.
113
113
  def saved_tool_records(turn_id)
114
114
  SessionMetrics.saved_tool_records(session_dir, turn_id)
115
- rescue Session::InvalidId
115
+ rescue SessionPaths::InvalidId
116
116
  []
117
117
  end
118
118
 
119
119
  def saved_turn_record(turn_id)
120
120
  SessionMetrics.saved_turn_record(session_dir, turn_id)
121
- rescue Session::InvalidId
121
+ rescue SessionPaths::InvalidId
122
122
  nil
123
123
  end
124
124
 
@@ -90,7 +90,9 @@ module Samagotchi
90
90
  # As typed: the Engine applies an alias where it resolves the model.
91
91
  flag_model_name = model_name.to_s.strip.empty? ? nil : ModelProfile.required_model_name(model_name)
92
92
  @effective_model_name = flag_model_name || @default_model_name
93
- @host_registry = host_registry || Samagotchi::HostRegistry.new
93
+ # Taken once (as the Engine's); a client (specs) goes on a registry of
94
+ # its own, never the shared one.
95
+ @host_registry = host_registry || (client ? Samagotchi::HostRegistry.new : Samagotchi::HostRegistry.current)
94
96
  # An injected client (specs) stands in for every host's client.
95
97
  @host_registry.client_override = client if client
96
98
  @client = @host_registry.resolve(@effective_model_name).client
@@ -25,7 +25,7 @@ require_relative "recording_api"
25
25
 
26
26
  module Samagotchi
27
27
  module Testing
28
- # A hook (a plugin's chi.on block or a hook file) raised while the kit
28
+ # A hook (a plugin's chi.on block) raised while the kit
29
29
  # fired it: chi would log it and go on, a test fails with it.
30
30
  class HookFailed < StandardError
31
31
  # @return [Hooks::Failure]
@@ -53,8 +53,7 @@ module Samagotchi
53
53
 
54
54
  # One bundle loaded for a test through chi's own seams: its plugin
55
55
  # through Plugin::Loader.register_file (Plugin::Api, a real
56
- # Plugin::Context) and its hook files through
57
- # Hooks::BundleLoader.load_hook_file, into real registries (commands
56
+ # Plugin::Context), into real registries (commands
58
57
  # and tools with chi's built-ins in them, so a name clash fails as in
59
58
  # chi). Only the edges are recorded: the Host and hook Runtime
60
59
  # (Recorder), ctx.sessions (Testing::Sessions), chi.init tasks and
@@ -130,8 +129,6 @@ module Samagotchi
130
129
  # @return [Module, nil] the module the plugin was evaluated in
131
130
  # (bundle.namespace::ThinkingWatch)
132
131
  attr_reader :namespace
133
- # @return [Module] the module the hook files were evaluated in
134
- attr_reader :hooks_namespace
135
132
  # @return [Array<Init>]
136
133
  attr_reader :inits
137
134
  # @return [Array<StagedTools>]
@@ -152,6 +149,9 @@ module Samagotchi
152
149
  @session_moves = 1
153
150
  own = session_settings.nil? || session_settings.empty? ? {} : { setup: setup_with(session_settings, host[:setup]) }
154
151
  @own_sandbox = sandbox.nil? && Sandbox.current.nil?
152
+ # With no active sandbox, this one saves ENV at construction and
153
+ # restores that copy at shutdown; don't lazily build it inside a block
154
+ # that sets ENV for one example (your own helper, or ClimateControl).
155
155
  @sandbox = sandbox || Sandbox.current || Sandbox.enter
156
156
  build(host, own)
157
157
  rescue Exception # rubocop:disable Lint/RescueException -- leave the sandbox it entered, whatever failed
@@ -184,13 +184,6 @@ module Samagotchi
184
184
  @plugin = built
185
185
  end
186
186
  end
187
- @hooks_namespace = Module.new
188
- @manifest.hooks.each do |basename, hook|
189
- next if hook.events.empty?
190
-
191
- Hooks::BundleLoader.load_hook_file(@name, basename, File.join(@dir, "hooks", basename), hook, hooks,
192
- settings: @settings, namespace: @hooks_namespace)
193
- end
194
187
  self
195
188
  rescue Exception # rubocop:disable Lint/RescueException -- leave the sandbox it entered, whatever failed
196
189
  shutdown
@@ -15,7 +15,7 @@ module Samagotchi
15
15
  # A bundle as chi installs and loads it, unlike Bundle (its source as it
16
16
  # is): MemoryBundle::Installer copies it into the sandbox's state (sha256
17
17
  # and requires_chi checked), then chi's own loaders load what is
18
- # installed (ExtensionLoad: the bundle hooks, then the plugins). What
18
+ # installed (ExtensionLoad: config hooks, then the plugins). What
19
19
  # failed names the bundle's source files, not the installed copies.
20
20
  #
21
21
  # result = Samagotchi::Testing::Install.run("path/to/bundle", settings: { "every" => 3 })
@@ -23,7 +23,8 @@ module Samagotchi
23
23
  module Install
24
24
  # warnings: the Installer's; plugin_failures / hook_failures:
25
25
  # [Guardrails::LoadFailures::Failure] of this bundle, each location
26
- # in the source dir; registered: what it registered (#registered).
26
+ # in the source dir (hook_failures: the guardrails': a required
27
+ # plugin's load failure, a config hook's); registered: what it registered (#registered).
27
28
  Result = Data.define(:name, :warnings, :plugin_failures, :hook_failures, :registered) do
28
29
  def failures = plugin_failures + hook_failures
29
30
  end
@@ -34,7 +35,7 @@ module Samagotchi
34
35
  Registered = Data.define(:commands, :tools, :hooks, :inits, :toolkits, :tool_sources, :peer_sources, :prompt_sections)
35
36
  # A command: its mid_turn policy and its selection-bar labels (chi.annotation).
36
37
  Command = Data.define(:name, :mid_turn, :labels)
37
- # A hook on an event: the bundle, the hook (its file, or the plugin's) and its priority.
38
+ # A hook on an event: the bundle, the hook (the plugin's label) and its priority.
38
39
  Hook = Data.define(:bundle, :hook, :priority)
39
40
 
40
41
  module_function
@@ -118,7 +119,7 @@ module Samagotchi
118
119
  # installed copy to the source dir.
119
120
  def own(failures, name, installed, dir)
120
121
  failures.list.select { |f| f.what.end_with?("(bundle #{name})") }.map do |failure|
121
- # The plugin is installed into plugin/, the hooks into hooks/.
122
+ # The plugin is installed into plugin/.
122
123
  location = failure.location&.sub(%r{\A#{Regexp.escape(installed)}/(?:plugin/)?}, "#{dir}/")
123
124
  failure.dup.tap { |copy| copy.location = location }
124
125
  end
@@ -9,7 +9,7 @@ require_relative "judgments"
9
9
  module Samagotchi
10
10
  module Testing
11
11
  # A line to the user (ctx.notify, event[:notify]); +hook+ is the
12
- # plugin's or hook file's label ("plugin.rb (bundle x)").
12
+ # plugin's label ("plugin.rb (bundle x)").
13
13
  Notice = Data.define(:text, :level, :hook, :fallback_for)
14
14
  # Text put into the running turn (ctx.steer, event[:steer]).
15
15
  Steer = Data.define(:text, :hook)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # The bundle test kit (docs/plugins.md, "Testing a bundle"): load a bundle's
4
- # plugin and hook files through chi's own loaders, registries, Context and
4
+ # plugin through chi's own loaders, registries, Context and
5
5
  # guardrail Gate, with only the edges recorded (the session's Host, the hook
6
6
  # runtime, ctx.sessions, init tasks), in a sandboxed environment.
7
7
  #
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "config"
4
- require_relative "model_profile"
5
4
  require_relative "setting_explained"
6
5
 
7
6
  module Samagotchi
@@ -79,30 +78,23 @@ module Samagotchi
79
78
  nil
80
79
  end
81
80
 
82
- # The level for +target+ and where it came from ("session",
81
+ # The level for +lookup+'s model and where it came from ("session",
83
82
  # "--thinking", "models: qwen", "hosts.work", "thinking.level"; nil for
84
83
  # the default).
85
- # @param target [HostRegistry::ModelTarget]
86
- # @param names [Array<String>] the model's lookup names (HostRegistry#lookup_names)
87
- # @param models [Hash{String => ModelSettings}, nil] ModelSettings.all (specs)
84
+ # @param lookup [ModelLookup] the model's layers (EffectiveModel#lookup)
88
85
  # @param session [Symbol, nil] the session's own level (Session#thinking)
89
86
  # @return [Array(Symbol, String|nil)]
90
- def resolve(target, names:, models: nil, session: nil)
87
+ def resolve(lookup, session: nil)
91
88
  return [session, SESSION_SOURCE] if session
92
89
 
93
- global, origin = global_level
90
+ global, origin = global_level(lookup)
94
91
  return [global, origin == :cli ? "--thinking" : "SAMAGOTCHI_THINKING_LEVEL"] if global && %i[cli env].include?(origin)
95
92
 
96
- models ||= begin
97
- ModelSettings.all
98
- rescue StandardError
99
- {}
100
- end
101
- setting = ModelSettings.find(names, :thinking, models: models)
93
+ setting = lookup.model(:thinking)
102
94
  return [setting.value, "models: #{setting.key}"] if setting
103
95
 
104
- host = target.entry.thinking
105
- return [host, "hosts.#{target.entry.name}"] if host
96
+ host = lookup.host(:thinking)
97
+ return [host, "hosts.#{lookup.host_name}"] if host
106
98
  return [global, "thinking.level"] if global
107
99
 
108
100
  [DEFAULT, nil]
@@ -110,14 +102,15 @@ module Samagotchi
110
102
 
111
103
  # #resolve, as a SettingExplained with the session's own level beside it.
112
104
  # @return [SettingExplained]
113
- def explain(target, names:, models: nil, session: nil)
114
- level, source = resolve(target, names: names, models: models, session: session)
105
+ def explain(lookup, session: nil)
106
+ level, source = resolve(lookup, session: session)
115
107
  SettingExplained.new(level: level, source: source, own: session)
116
108
  end
117
109
 
118
110
  # thinking.level as a level and its origin (:cli, :env, :file), or nil.
119
- def global_level
120
- raw, origin = Config.get_with_origin("thinking.level")
111
+ # @param lookup [ModelLookup] whose env the key is read with
112
+ def global_level(lookup)
113
+ raw, origin = lookup.global("thinking.level")
121
114
  return nil if origin == :default
122
115
 
123
116
  where = { cli: "--thinking", env: "SAMAGOTCHI_THINKING_LEVEL" }.fetch(origin, "thinking.level")
@@ -139,13 +132,15 @@ module Samagotchi
139
132
  # only the tail changes (llama.cpp and Splash keep the rest).
140
133
  # @param target [HostRegistry::ModelTarget]
141
134
  # @param profile [String, nil] the native prompt profile's name
135
+ # @param inferred [String, nil] the profile a local chat host's model
136
+ # name says (ModelProfile.inferred_profile_name of its bare id)
142
137
  # @return [Symbol]
143
- def cache_cost(target, profile: nil)
138
+ def cache_cost(target, profile: nil, inferred: nil)
144
139
  entry = target.entry
145
140
  if entry&.chat?
146
141
  return :provider if entry.remote?
147
142
 
148
- return ModelProfile.inferred_profile_name(target.bare_model) == "gemma4" ? :full : :tail
143
+ return inferred == "gemma4" ? :full : :tail
149
144
  end
150
145
 
151
146
  profile == "gemma4" ? :full : :tail
@@ -375,7 +375,7 @@ module Samagotchi
375
375
  },
376
376
  cwd: {
377
377
  type: "string",
378
- description: "The new child's working folder: a worktree or subfolder of this session's repository (create a worktree with execute `git worktree add ../<repo>-<name> -b <branch>` first). Default: this session's folder"
378
+ description: "The new child's working folder: a worktree or subfolder of this session's project (a repository, or a `chi project init` folder; create a worktree with execute `git worktree add ../<repo>-<name> -b <branch>` first). Default: this session's folder"
379
379
  },
380
380
  toolkit: {
381
381
  type: "string",
@@ -35,15 +35,17 @@ module Samagotchi
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
37
  # keeps its progress (Engine#ctrl_c_messages). +context+: the calls'
38
- # turn (TurnContext), each run's (ToolRunner#run).
38
+ # turn (TurnContext), each run's (ToolRunner#run). +ids+: the calls'
39
+ # ToolIds, in call order (ToolIds.next_ids before the batch: the ids the
40
+ # entries are saved under), each on its run's tool events; nil: none.
39
41
  # @return [Array<ToolRun>] the runs
40
- def run_batch(runner, calls, context:, iteration:, emit:, on_stream_event:, cap:)
42
+ def run_batch(runner, calls, context:, iteration:, emit:, on_stream_event:, cap:, ids: nil)
41
43
  # Before anything can be interrupted: the rescue counts it.
42
44
  runs = []
43
45
  emit.call({ type: :tool_dispatch_started, iteration: iteration, call_count: calls.length })
44
46
  calls.each_with_index do |call, index|
45
47
  runs << runner.run(call, context: context, iteration: iteration, call_index: index + 1, call_count: calls.length,
46
- on_stream_event: on_stream_event, max_tool_output_chars: cap)
48
+ on_stream_event: on_stream_event, max_tool_output_chars: cap, tool_id: ids&.[](index))
47
49
  yield runs.last, index if block_given?
48
50
  end
49
51
  emit.call({ type: :tool_dispatch_completed, iteration: iteration, call_count: calls.length })
@@ -86,7 +86,10 @@ module Samagotchi
86
86
  # file (EditPreview.change; also on tool_call_completed, never in the
87
87
  # model's output)
88
88
  # @param context [TurnContext] the call's turn
89
- def run(call, context:, iteration:, call_index:, call_count:, on_stream_event:, max_tool_output_chars:)
89
+ # @param tool_id [String, nil] the id the run's output is saved under
90
+ # (ToolIds, handed out before the batch runs), on both tool events
91
+ # so a UI's row and the saved output share it
92
+ def run(call, context:, iteration:, call_index:, call_count:, on_stream_event:, max_tool_output_chars:, tool_id: nil)
90
93
  # A call_tool of one of chi's own tools runs as that tool, from here
91
94
  # on (the gate, the row, the result): rules, approvals and hooks see
92
95
  # the real call, with via: (Tools::ToolIndex#unwrap).
@@ -114,6 +117,7 @@ module Samagotchi
114
117
  started = { type: :tool_call_started, iteration: iteration, call_count: call_count, call_index: call_index,
115
118
  tool: call[:name], call: call.dup, params: params }
116
119
  started[:label] = label if label
120
+ started[:tool_id] = tool_id if tool_id
117
121
  # Its row fields (ToolRowFields: title, called_as, via, view, ref, and a
118
122
  # plugin tool's refs); the worker runs in its session's working
119
123
  # directory.
@@ -121,6 +125,8 @@ module Samagotchi
121
125
  refs = RowRefs.for(tools && !call[:name].nil? ? tools[call[:name]]&.refs : nil, call, cwd: session_cwd(context))
122
126
  fields[:refs] = refs unless refs.empty?
123
127
  started.merge!(fields)
128
+ # The row's clock: tool_call_completed's duration_ms runs from here.
129
+ started_clock = @clock.call
124
130
  emit(on_stream_event, started)
125
131
 
126
132
  # The ask comes after tool_call_started: the UI shows the tool line,
@@ -175,9 +181,20 @@ module Samagotchi
175
181
  completed[:diff] = diff if diff
176
182
  completed[:waited_ms] = waited_ms if waited_ms
177
183
  completed[:output_file] = output_file if output_file
184
+ completed[:tool_id] = tool_id if tool_id
185
+ # A long output's web row (its hover): the cut a reload draws from the
186
+ # saved output (MessageParts#tool_part), so live and reload show the
187
+ # same text. The TUI keeps +output+.
188
+ if capped.length > ToolOutputCut::WEB_PREVIEW_CHARS
189
+ completed[:preview] = ToolOutputCut.preview(capped, cap: ToolOutputCut::WEB_PREVIEW_CHARS, tool: call[:name])
190
+ end
178
191
  # Also here: a UI that missed the start (a replay gap) builds its row
179
192
  # from this event (its title is in the activity).
180
193
  completed.merge!(fields.slice(*ToolRowFields::EVENT_KEYS))
194
+ # The row's duration, the one every UI shows (live, a reload's tool
195
+ # record, the attached terminal): SessionMetrics' definition and
196
+ # rounding order, from the started emit to this one, less the wait.
197
+ completed[:duration_ms] = [((@clock.call - started_clock) * 1000).round - (waited_ms || 0), 0].max
181
198
  emit(on_stream_event, completed)
182
199
  # After the completed event: a hook's card (check-in, skills) prints
183
200
  # under the call's tool row, not before it.
@@ -139,10 +139,7 @@ module Samagotchi
139
139
 
140
140
  toolkit = Toolkit.new(name: name)
141
141
  if session.toolkit == toolkit
142
- if session.toolkit_origin != "chosen"
143
- session.toolkit_origin = "chosen"
144
- session.save(state_dir: @state_dir.call)
145
- end
142
+ session.save(state_dir: @state_dir.call) if session.choose_toolkit_origin!
146
143
  return true
147
144
  end
148
145
 
@@ -47,10 +47,7 @@ module Samagotchi
47
47
  # Nothing the prompt carries changes (full +delegate on full): only
48
48
  # the origin is saved, so a no-op choice still counts as user-selected.
49
49
  if after.prompt_key == before.prompt_key
50
- if session.toolkit_origin != "chosen"
51
- session.toolkit_origin = "chosen"
52
- @save.call(session)
53
- end
50
+ @save.call(session) if session.choose_toolkit_origin!
54
51
  return [listing(before) + "\n(no change to what the next turn declares)", false, nil]
55
52
  end
56
53