samagotchi 0.155.0 → 0.157.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 (234) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/bin/chi +2 -1
  4. data/docs/architecture.md +22 -13
  5. data/docs/bundles.md +3 -1
  6. data/docs/cli.md +9 -8
  7. data/docs/configuration.md +36 -4
  8. data/docs/guardrails.md +9 -5
  9. data/docs/hooks.md +11 -5
  10. data/docs/internals/anthropic.md +1 -1
  11. data/docs/internals/attached-context.md +3 -2
  12. data/docs/internals/background-tasks.md +1 -1
  13. data/docs/internals/bridge.md +83 -6
  14. data/docs/internals/bundles.md +28 -18
  15. data/docs/internals/context-telemetry.md +4 -3
  16. data/docs/internals/engine.md +641 -937
  17. data/docs/internals/llm-context-forget.md +8 -3
  18. data/docs/internals/objectives.md +2 -2
  19. data/docs/internals/participants.md +3 -2
  20. data/docs/internals/prompt-caching.md +4 -1
  21. data/docs/internals/remote-sessions.md +8 -3
  22. data/docs/internals/web-frontend.md +3 -2
  23. data/docs/internals/working-set.md +2 -2
  24. data/docs/memory.md +13 -2
  25. data/docs/plugins.md +35 -2
  26. data/docs/sessions.md +11 -11
  27. data/docs/sub-agent.md +17 -2
  28. data/docs/system-one.md +3 -2
  29. data/docs/testing.md +53 -2
  30. data/lib/samagotchi/answer_command.rb +2 -1
  31. data/lib/samagotchi/archive_store.rb +4 -3
  32. data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
  33. data/lib/samagotchi/bootstrap_command.rb +3 -2
  34. data/lib/samagotchi/bridge/card_places.rb +10 -4
  35. data/lib/samagotchi/bridge/card_store.rb +3 -0
  36. data/lib/samagotchi/bridge/turn_accumulator.rb +109 -9
  37. data/lib/samagotchi/bridge/turn_drawings.rb +265 -0
  38. data/lib/samagotchi/bridge.rb +31 -6
  39. data/lib/samagotchi/broadcast/recipients.rb +8 -8
  40. data/lib/samagotchi/broadcast/triage_model.rb +1 -1
  41. data/lib/samagotchi/broadcast_command.rb +3 -3
  42. data/lib/samagotchi/bundle_command/check.rb +18 -0
  43. data/lib/samagotchi/bundle_command/try/sandbox.rb +2 -2
  44. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  45. data/lib/samagotchi/bundle_command.rb +5 -2
  46. data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
  47. data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
  48. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
  49. data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
  50. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  51. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  52. data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
  53. data/lib/samagotchi/child_move.rb +13 -57
  54. data/lib/samagotchi/child_reports.rb +2 -2
  55. data/lib/samagotchi/children_status.rb +15 -21
  56. data/lib/samagotchi/cli/parent_wait.rb +9 -23
  57. data/lib/samagotchi/config.rb +20 -112
  58. data/lib/samagotchi/context_command.rb +1 -2
  59. data/lib/samagotchi/context_sources.rb +5 -4
  60. data/lib/samagotchi/context_window.rb +9 -12
  61. data/lib/samagotchi/desktop/macos.rb +1 -1
  62. data/lib/samagotchi/effective_model.rb +57 -22
  63. data/lib/samagotchi/engine/context_judge.rb +19 -11
  64. data/lib/samagotchi/engine/messages.rb +1 -1
  65. data/lib/samagotchi/engine/models.rb +5 -2
  66. data/lib/samagotchi/engine/objectives.rb +29 -11
  67. data/lib/samagotchi/engine/plugins.rb +1 -1
  68. data/lib/samagotchi/engine/recap.rb +8 -4
  69. data/lib/samagotchi/engine/side_requests.rb +21 -11
  70. data/lib/samagotchi/engine/toolkit.rb +10 -9
  71. data/lib/samagotchi/engine/turn_end.rb +1 -1
  72. data/lib/samagotchi/engine/turn_helpers.rb +7 -7
  73. data/lib/samagotchi/engine/verify.rb +1 -1
  74. data/lib/samagotchi/engine.rb +43 -12
  75. data/lib/samagotchi/events.rb +3 -2
  76. data/lib/samagotchi/extension_load.rb +6 -3
  77. data/lib/samagotchi/guardrails/targets.rb +24 -9
  78. data/lib/samagotchi/guardrails_command.rb +1 -1
  79. data/lib/samagotchi/host_address.rb +52 -0
  80. data/lib/samagotchi/host_config.rb +72 -1
  81. data/lib/samagotchi/host_model.rb +2 -6
  82. data/lib/samagotchi/host_registry.rb +58 -46
  83. data/lib/samagotchi/kernel_loop.rb +53 -124
  84. data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
  85. data/lib/samagotchi/llm/chat_loop.rb +40 -21
  86. data/lib/samagotchi/llm/native_backend.rb +1 -1
  87. data/lib/samagotchi/llm_context_apply.rb +54 -1
  88. data/lib/samagotchi/llm_context_notice.rb +1 -1
  89. data/lib/samagotchi/llm_context_strategy.rb +29 -67
  90. data/lib/samagotchi/llm_context_view.rb +9 -0
  91. data/lib/samagotchi/memory_bundle/asset_installer.rb +71 -76
  92. data/lib/samagotchi/memory_bundle/builder.rb +2 -1
  93. data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
  94. data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
  95. data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
  96. data/lib/samagotchi/memory_bundle/installed_copy.rb +2 -1
  97. data/lib/samagotchi/memory_bundle/installed_manifest.rb +8 -1
  98. data/lib/samagotchi/memory_bundle/installer.rb +185 -169
  99. data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
  100. data/lib/samagotchi/memory_bundle/merger.rb +69 -65
  101. data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
  102. data/lib/samagotchi/memory_bundle/profile.rb +5 -5
  103. data/lib/samagotchi/memory_bundle/provenance.rb +15 -1
  104. data/lib/samagotchi/memory_bundle/remerge.rb +25 -12
  105. data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -6
  106. data/lib/samagotchi/memory_bundle/status.rb +14 -20
  107. data/lib/samagotchi/memory_bundle/system_bundle.rb +24 -6
  108. data/lib/samagotchi/memory_bundle/trash.rb +23 -8
  109. data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
  110. data/lib/samagotchi/memory_usage.rb +1 -1
  111. data/lib/samagotchi/model_lookup.rb +75 -0
  112. data/lib/samagotchi/model_price.rb +4 -10
  113. data/lib/samagotchi/model_profile.rb +7 -6
  114. data/lib/samagotchi/model_settings.rb +37 -2
  115. data/lib/samagotchi/models_command.rb +1 -1
  116. data/lib/samagotchi/note_command.rb +2 -3
  117. data/lib/samagotchi/objectives_audit.rb +4 -3
  118. data/lib/samagotchi/participant_mode.rb +2 -1
  119. data/lib/samagotchi/participants.rb +2 -2
  120. data/lib/samagotchi/plugin/api.rb +41 -6
  121. data/lib/samagotchi/plugin/context.rb +1 -1
  122. data/lib/samagotchi/plugin/loader.rb +33 -10
  123. data/lib/samagotchi/plugin/sessions.rb +3 -6
  124. data/lib/samagotchi/plugin/tool_spec.rb +5 -3
  125. data/lib/samagotchi/project_env.rb +1 -1
  126. data/lib/samagotchi/reasoning_history.rb +14 -20
  127. data/lib/samagotchi/recap_place.rb +27 -0
  128. data/lib/samagotchi/reminder_carry.rb +1 -1
  129. data/lib/samagotchi/reply_wait/liveness.rb +38 -0
  130. data/lib/samagotchi/reply_wait/poll.rb +44 -15
  131. data/lib/samagotchi/reply_wait/target.rb +113 -0
  132. data/lib/samagotchi/reply_wait.rb +22 -2
  133. data/lib/samagotchi/reprefill_meter.rb +45 -0
  134. data/lib/samagotchi/review_command.rb +1 -1
  135. data/lib/samagotchi/sampling_settings.rb +9 -16
  136. data/lib/samagotchi/self_report.rb +36 -37
  137. data/lib/samagotchi/send_command.rb +17 -23
  138. data/lib/samagotchi/session.rb +370 -445
  139. data/lib/samagotchi/session_access/local.rb +12 -11
  140. data/lib/samagotchi/session_access/remote.rb +22 -6
  141. data/lib/samagotchi/session_archive_command.rb +2 -2
  142. data/lib/samagotchi/session_chain.rb +7 -6
  143. data/lib/samagotchi/session_continue.rb +6 -5
  144. data/lib/samagotchi/session_delete_command.rb +1 -1
  145. data/lib/samagotchi/session_field.rb +133 -0
  146. data/lib/samagotchi/session_inbox.rb +49 -1
  147. data/lib/samagotchi/session_manager/delivery.rb +254 -0
  148. data/lib/samagotchi/session_manager/emptiness.rb +139 -0
  149. data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
  150. data/lib/samagotchi/session_manager/listing.rb +118 -0
  151. data/lib/samagotchi/session_manager/removal.rb +190 -0
  152. data/lib/samagotchi/session_manager/spawn.rb +242 -0
  153. data/lib/samagotchi/session_manager/worker_command.rb +157 -0
  154. data/lib/samagotchi/session_manager.rb +28 -1541
  155. data/lib/samagotchi/session_metrics.rb +8 -5
  156. data/lib/samagotchi/session_paths.rb +57 -0
  157. data/lib/samagotchi/session_retention.rb +2 -2
  158. data/lib/samagotchi/session_row.rb +200 -0
  159. data/lib/samagotchi/session_setup.rb +15 -12
  160. data/lib/samagotchi/sessions_command.rb +11 -129
  161. data/lib/samagotchi/share_command.rb +1 -1
  162. data/lib/samagotchi/starting_marker.rb +65 -0
  163. data/lib/samagotchi/steer_cut_after.rb +7 -14
  164. data/lib/samagotchi/system_one/recorder.rb +49 -0
  165. data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
  166. data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
  167. data/lib/samagotchi/terminal_ui/join_replay.rb +86 -8
  168. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  169. data/lib/samagotchi/terminal_ui.rb +5 -7
  170. data/lib/samagotchi/testing/install.rb +2 -1
  171. data/lib/samagotchi/testing/sessions.rb +1 -1
  172. data/lib/samagotchi/thinking.rb +16 -21
  173. data/lib/samagotchi/token_usage.rb +1 -1
  174. data/lib/samagotchi/tool_activity.rb +10 -4
  175. data/lib/samagotchi/tool_declarations.rb +2 -2
  176. data/lib/samagotchi/tool_output_cut.rb +17 -0
  177. data/lib/samagotchi/tool_response.rb +5 -3
  178. data/lib/samagotchi/tool_runner.rb +25 -5
  179. data/lib/samagotchi/toolkit_command.rb +1 -1
  180. data/lib/samagotchi/tools/delegate.rb +55 -45
  181. data/lib/samagotchi/tools/delegate_relay.rb +5 -5
  182. data/lib/samagotchi/tools/delegate_result.rb +5 -5
  183. data/lib/samagotchi/tools/delegate_wait.rb +41 -17
  184. data/lib/samagotchi/tools/index_hint.rb +14 -0
  185. data/lib/samagotchi/tools/list_sessions.rb +7 -7
  186. data/lib/samagotchi/tools/registry.rb +28 -11
  187. data/lib/samagotchi/tools/send_note.rb +4 -6
  188. data/lib/samagotchi/tools/tool_index.rb +52 -51
  189. data/lib/samagotchi/tools/tool_surface.rb +22 -8
  190. data/lib/samagotchi/turn_context.rb +2 -1
  191. data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
  192. data/lib/samagotchi/turn_stages/objectives.rb +56 -52
  193. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
  194. data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
  195. data/lib/samagotchi/turn_stages/warmup.rb +14 -15
  196. data/lib/samagotchi/update_command.rb +11 -7
  197. data/lib/samagotchi/verify_fresh.rb +7 -11
  198. data/lib/samagotchi/version.rb +1 -1
  199. data/lib/samagotchi/vision_support.rb +11 -14
  200. data/lib/samagotchi/web/app.rb +45 -28
  201. data/lib/samagotchi/web/message_display.rb +57 -7
  202. data/lib/samagotchi/web/participant_access.rb +3 -3
  203. data/lib/samagotchi/web/public/activity.js +16 -7
  204. data/lib/samagotchi/web/public/app.js +29 -8
  205. data/lib/samagotchi/web/public/bubbles.js +8 -3
  206. data/lib/samagotchi/web/public/cards_place.js +15 -0
  207. data/lib/samagotchi/web/public/commands.js +5 -1
  208. data/lib/samagotchi/web/public/data.js +4 -2
  209. data/lib/samagotchi/web/public/history_shape.js +220 -0
  210. data/lib/samagotchi/web/public/recap_stamp.js +17 -0
  211. data/lib/samagotchi/web/public/reload_check.js +71 -0
  212. data/lib/samagotchi/web/public/stage_model.js +7 -5
  213. data/lib/samagotchi/web/public/stage_view.js +7 -3
  214. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  215. data/lib/samagotchi/web/public/timing.js +43 -2
  216. data/lib/samagotchi/web/public/turn_block.js +215 -30
  217. data/lib/samagotchi/web/public/turn_drawing.js +43 -0
  218. data/lib/samagotchi/web/public/turn_events.js +10 -1
  219. data/lib/samagotchi/web/public/turn_pieces.js +29 -0
  220. data/lib/samagotchi/web/reload_check_route.rb +95 -0
  221. data/lib/samagotchi/web/server.rb +4 -2
  222. data/lib/samagotchi/web/session_access_routes.rb +6 -6
  223. data/lib/samagotchi/web/session_control.rb +4 -4
  224. data/lib/samagotchi/web/session_create.rb +3 -3
  225. data/lib/samagotchi/web/session_hub.rb +9 -17
  226. data/lib/samagotchi/web/session_show.rb +67 -5
  227. data/lib/samagotchi/web/session_summary.rb +39 -28
  228. data/lib/samagotchi/worker.rb +83 -32
  229. data/lib/samagotchi/worker_boot.rb +184 -0
  230. data/lib/samagotchi/worker_link.rb +3 -0
  231. data/lib/samagotchi/worker_signals.rb +1 -1
  232. data/lib/samagotchi/worker_task_wakes.rb +2 -2
  233. metadata +32 -4
  234. data/lib/samagotchi/send_command/wait_target.rb +0 -85
@@ -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
@@ -613,39 +613,19 @@ module Samagotchi
613
613
  lane = :writing
614
614
  when :generation_chunk
615
615
  tail, lane = event[:thinking].to_s.empty? ? [event[:text], :writing] : [event[:thinking], :thinking]
616
- when :tool_call_started
617
- running_tool = event[:tool]
618
- @renderer.call(event)
619
- when :tool_call_completed
620
- running_tool = nil
621
- replay_tool_completed(event)
622
- when :merged_input then @screen.commit("input> #{event[:content]}")
623
- when :reminder_injected then @screen.commit(reminder_line(event[:reminders]))
624
- # The turn's start (its images), a plugin's steer, and its rows (a
625
- # hook's notice, a retry's line), as they were drawn live.
626
- # Questions are left out: the pending one is asked after the
627
- # replay, answered ones showed no line.
616
+ # The turn's start (its images), as it was drawn live.
628
617
  when :turn_started then @renderer.call(without_named_images(event, turn[:prompt]))
629
- when :pending_input_merged, :hook_notice, :empty_answer_retry, :steer_cut, :llm_context_edited
630
- @renderer.call(event)
618
+ # Its rows, as a drawn turn's (JoinReplay#replay_row).
619
+ else
620
+ running_tool = event[:tool] if event[:type] == :tool_call_started
621
+ running_tool = nil if event[:type] == :tool_call_completed
622
+ @join_replay.replay_row(event)
631
623
  end
632
624
  end
633
625
  @view.resume(tail: tail, lane: lane, tool: running_tool, parts: turn[:parts])
634
626
  ask(turn[:pending_question]) if turn[:pending_question]
635
627
  end
636
628
 
637
- # A finished tool call of the joined turn: the live row (action,
638
- # duration). A snapshot from an older worker has no action: its row
639
- # names the tool only.
640
- def replay_tool_completed(event)
641
- return @renderer.call({ duration_ms: nil }.merge(event)) if event.dig(:activity, :action)
642
-
643
- activity = EventRenderer.described(event) || {}
644
- @screen.commit(snapshot_tool_line({ tool: event[:tool], params: activity[:params], status: activity[:status],
645
- description: activity[:description], output_truncated: event[:output_truncated],
646
- images: event[:images], diff: event[:diff] }))
647
- end
648
-
649
629
  def start_turn(event)
650
630
  @running = true
651
631
  @joined_mid_turn = false
@@ -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.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../bridge/turn_accumulator"
3
4
  require_relative "../context_note"
4
5
  require_relative "../image_store"
5
6
  require_relative "../log"
@@ -31,11 +32,32 @@ module Samagotchi
31
32
  @renderer = renderer
32
33
  end
33
34
 
35
+ # A snapshot's last_turn_drawing (Bridge#snapshot: the last exchange's
36
+ # turn as live drew it) draws that turn's rows; without one (an older
37
+ # worker, a turn before drawings) they are rebuilt from the messages.
34
38
  def render_join(snapshot, saved)
35
- render_join_header(Array(snapshot[:messages]))
39
+ render_join_header(Array(snapshot[:messages]), drawing: snapshot[:last_turn_drawing])
36
40
  render_join_gap(snapshot, saved)
37
41
  end
38
42
 
43
+ # One replayed event of a turn's rows (TurnAccumulator.replay_events),
44
+ # as live drew it: a joined running turn's (AttachedLoop#render_current_turn)
45
+ # and a drawn finished one's (#render_drawn_turn). Its tool rows with
46
+ # their action and duration, merged input, reminders, a plugin's steer,
47
+ # a hook's notice, a retry's, a steer cut's and a ✂ row's line. The
48
+ # step text, the turn's start and questions are the caller's (the
49
+ # pending one is asked after the replay, answered ones showed no line).
50
+ def replay_row(event)
51
+ case event[:type]
52
+ when :tool_call_started then @renderer.call(event)
53
+ when :tool_call_completed then replay_tool_completed(event)
54
+ when :merged_input then @screen.commit("input> #{event[:content]}")
55
+ when :reminder_injected then @screen.commit(reminder_line(event[:reminders]))
56
+ when :pending_input_merged, :hook_notice, :empty_answer_retry, :steer_cut, :llm_context_edited
57
+ @renderer.call(event)
58
+ end
59
+ end
60
+
39
61
  # The snapshot's cards and between-turns notices (Bridge CardStore):
40
62
  # on join the ones since the last turn (and the running turn's), as
41
63
  # the join header shows the last exchange; on a resync only cards not
@@ -60,6 +82,18 @@ module Samagotchi
60
82
 
61
83
  private
62
84
 
85
+ # A finished tool call: the live row (action, duration: none when the
86
+ # part has none, never the replay's own clock). A snapshot from an
87
+ # older worker has no action: its row names the tool only.
88
+ def replay_tool_completed(event)
89
+ return @renderer.call({ duration_ms: nil }.merge(event)) if event.dig(:activity, :action)
90
+
91
+ activity = EventRenderer.described(event) || {}
92
+ @screen.commit(snapshot_tool_line({ tool: event[:tool], params: activity[:params], status: activity[:status],
93
+ description: activity[:description], output_truncated: event[:output_truncated],
94
+ images: event[:images], diff: event[:diff] }))
95
+ end
96
+
63
97
  # Where the session's last activity was: its newest stamped entry,
64
98
  # else its saved recap's time. A gap ago (and no turn running): one
65
99
  # line after the join block says when.
@@ -71,7 +105,7 @@ module Samagotchi
71
105
  @screen.commit(gap_divider(line)) if line
72
106
  end
73
107
 
74
- def render_join_header(messages)
108
+ def render_join_header(messages, drawing: nil)
75
109
  exchange = messages.select { |m| %w[user model assistant].include?(m[:role].to_s) }
76
110
  # The id stays findable: the detach line and chi sessions list show it.
77
111
  Log.session_id = @link.client.session_id
@@ -89,6 +123,11 @@ module Samagotchi
89
123
  unless ImageRef.placeholder_only?(exchange[last_user][:content])
90
124
  Array(exchange[last_user][:images]).each { |ref| @screen.commit(format_image_line(ref)) }
91
125
  end
126
+ if (drawn = drawn_turn(drawing, exchange[last_user]))
127
+ render_drawn_turn(*drawn, exchange[(last_user + 1)..])
128
+ return render_join_notes(messages, from, after_exchange: true)
129
+ end
130
+
92
131
  render_join_steps(messages, from, durations: join_tool_records(exchange[last_user]))
93
132
  record = @link.saved_turn_record(exchange[last_user][:turn_id] || exchange[last_user]["turn_id"])
94
133
  failed = join_failed_record(record)
@@ -109,16 +148,55 @@ module Samagotchi
109
148
  @screen.commit(@view.turn_canceled_line(record["cancellation_reason"], record["duration_ms"], by: record["cancelled_by"]))
110
149
  @screen.commit(@view.turn_end_hint(ROLLBACK_HINT)) unless record["continue"]
111
150
  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
151
+ render_join_answer(exchange[(last_user + 1)..])
118
152
  end
119
153
  render_join_notes(messages, from, after_exchange: true)
120
154
  end
121
155
 
156
+ # The last answer of the messages after the prompt (+after+). The saved
157
+ # answer is raw: the latest keeps its thinking, and one may be only a
158
+ # tool call.
159
+ def render_join_answer(after)
160
+ answer = after.reverse_each.map { |m| OutputFormatter.strip_markup(m[:content]) }.find { |text| !text.empty? }
161
+ @screen.commit(last_lines(answer)) if answer
162
+ end
163
+
164
+ # The snapshot's drawing as [turn, end event] when it is +prompt+'s
165
+ # turn (a Continue's or a reminder's drawing isn't the prompt's: the
166
+ # rows are rebuilt), else nil.
167
+ def drawn_turn(drawing, prompt)
168
+ return nil unless drawing.is_a?(Hash) && drawing[:drawing].is_a?(Hash) && drawing[:end].is_a?(Hash)
169
+
170
+ turn = Bridge::TurnAccumulator.from_wire(drawing[:drawing])
171
+ id = prompt[:turn_id] || prompt["turn_id"]
172
+ return nil unless id && turn[:turn_id].to_s == id.to_s
173
+
174
+ [turn, EventRenderer.symbolize(Bridge::TurnAccumulator.from_wire(drawing[:end]))]
175
+ end
176
+
177
+ # The last turn from its drawing, through the renderer as live drew it
178
+ # (the running turn's join path, AttachedLoop#render_current_turn): its
179
+ # rows with their status and duration, a retry's line, a hook's notice,
180
+ # steers, merged input and reminders; no step text (the live ticker's)
181
+ # and no cards (the snapshot's cards are drawn after). Then its end: a
182
+ # cancel's or a failure's line from the end event, else the empty
183
+ # answer's notice or the saved answer (+after+, the messages after the
184
+ # prompt).
185
+ def render_drawn_turn(turn, ending, after)
186
+ Bridge::TurnAccumulator.replay_events(turn).each do |event|
187
+ # The prompt line named its images already.
188
+ event[:type] == :turn_started ? @renderer.call(event.except(:images)) : replay_row(event)
189
+ end
190
+ case ending[:type]
191
+ when :turn_canceled, :turn_failed then @renderer.call(ending)
192
+ else
193
+ empty = ending.dig(:turn_summary, :empty_answer)
194
+ empty ? @screen.commit(format_empty_answer_line(empty[:retries], empty[:refusal])) : render_join_answer(after)
195
+ end
196
+ # A call the turn never answered left the tool slot running.
197
+ @view.finish_thinking_spinner
198
+ end
199
+
122
200
  # What the last exchange did, as the live turn drew it: each saved
123
201
  # call's tool row (action, params, status from its result, and its
124
202
  # 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
 
@@ -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
 
@@ -92,7 +90,9 @@ module Samagotchi
92
90
  # As typed: the Engine applies an alias where it resolves the model.
93
91
  flag_model_name = model_name.to_s.strip.empty? ? nil : ModelProfile.required_model_name(model_name)
94
92
  @effective_model_name = flag_model_name || @default_model_name
95
- @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)
96
96
  # An injected client (specs) stands in for every host's client.
97
97
  @host_registry.client_override = client if client
98
98
  @client = @host_registry.resolve(@effective_model_name).client
@@ -100,9 +100,7 @@ module Samagotchi
100
100
  # between the load and the lock that this run would later save over.
101
101
  # An unknown id is refused before the claim, which would leave an
102
102
  # empty session dir behind.
103
- if session_id && !Session.exist?(session_id)
104
- raise SessionNotFound, "Session not found: #{session_id}"
105
- end
103
+ raise Session::NotFound, session_id if session_id && !Session.exist?(session_id)
106
104
 
107
105
  claim_session!(session_id) if session_id
108
106
  @resume_session = session_id ? Session.load(session_id) : nil
@@ -434,7 +432,7 @@ module Samagotchi
434
432
 
435
433
  # A fresh session reaches the disk before its first turn: a tool in that
436
434
  # turn reads it from there (delegate loads its parent to start a child,
437
- # "Session not found" otherwise).
435
+ # "no session …" otherwise).
438
436
  def save_before_first_turn(session)
439
437
  session.save unless Session.exist?(session.id)
440
438
  end
@@ -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 among them); 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
@@ -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)
@@ -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] ConfigFile.model_settings (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
- ConfigFile.model_settings
98
- rescue StandardError
99
- {}
100
- end
101
- setting = ConfigFile.model_setting(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
@@ -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"
@@ -359,8 +360,9 @@ module Samagotchi
359
360
  name == "git" && args.first == "diff" && args.intersect?(%w[--quiet --exit-code])
360
361
  end
361
362
 
362
- # +surface+: the turn's Tools::ToolSurface, for a preview that reads it
363
- # (call_tool's); nil outside a turn.
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).
364
366
  def tool_activity_params(tool_name, call, registry: nil, surface: nil)
365
367
  case tool_name
366
368
  when Tools::Execute::NAME
@@ -455,7 +457,8 @@ module Samagotchi
455
457
  parts << "options=#{preview_tool_param(Array(opts).join(","))}" if opts && !Array(opts).empty?
456
458
  parts.join(" ")
457
459
  else
458
- registry_params(registry_entry(registry, tool_name), call, surface)
460
+ registry_params(surface ? surface.registered[tool_name.to_s] : registry_entry(registry, tool_name), call,
461
+ surface)
459
462
  end
460
463
  end
461
464
 
@@ -473,7 +476,8 @@ module Samagotchi
473
476
  # key="value" (the parsers' args:, which a registry-less reader such as
474
477
  # the web's reload also has); nil for a tool the registry doesn't know
475
478
  # that has no args:, and for a built-in without its own words above
476
- # (the reminder tools).
479
+ # (the reminder tools). A missing surface (Tools::MissingSurface) is
480
+ # raised, never taken as a failing preview.
477
481
  def registry_params(entry, call, surface = nil)
478
482
  return nil if entry&.core?
479
483
  return nil if entry.nil? && !call[:args].is_a?(Hash)
@@ -481,6 +485,8 @@ module Samagotchi
481
485
  if entry&.preview
482
486
  begin
483
487
  return entry.preview_of(call, surface)
488
+ rescue Tools::MissingSurface
489
+ raise
484
490
  rescue StandardError => e
485
491
  Log.warn(:plugins, "plugin_preview_failed", tool: entry.name, error: e.class.name,
486
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" \
@@ -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
 
@@ -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.
@@ -407,22 +424,25 @@ module Samagotchi
407
424
  # The kernel's tools, for the activity line of a tool that isn't built in.
408
425
  def tools = @kernel.tools
409
426
 
410
- # The turn's Tools::ToolSurface (+context+'s), else one computed now
411
- # from the kernel's tools (a runner outside an Engine turn).
412
- def surface(context) = Tools::ToolSurface.of(context, tools)
427
+ # The turn's Tools::ToolSurface (+context+'s; none raises
428
+ # Tools::MissingSurface).
429
+ def surface(context) = Tools::ToolSurface.of(context)
413
430
 
414
431
  # The call its tool's entry unwraps it to (call_tool on a built-in),
415
432
  # else the call as it is. On a host that loads tools, a direct call of
416
433
  # a source's tool by its acts_as runs as the call_tool call first
417
434
  # (Registry::Index#wrap): call_tool's row, rules and approvals. One
418
435
  # that fails runs as it is: call_tool then refuses a built-in (fail
419
- # closed).
436
+ # closed). A context without a surface raises Tools::MissingSurface
437
+ # here, before the gate: the call never runs.
420
438
  def unwrapped(call, context)
421
439
  return call if tools.nil? || call[:name].nil?
422
440
 
423
441
  surface = surface(context)
424
442
  call = wrapped(surface, call)
425
443
  surface.declared_entry(call[:name])&.unwrap&.call(call, surface) || call
444
+ rescue Tools::MissingSurface
445
+ raise
426
446
  rescue StandardError => e
427
447
  Log.warn(:turn, "tool_unwrap_failed", tool: call[:name].to_s, error: "#{e.class}: #{e.message}")
428
448
  call
@@ -115,7 +115,7 @@ module Samagotchi
115
115
  raise ArgumentError, "#{NAME} #{verb} needs a tool name\n#{USAGE}" if tools.empty?
116
116
 
117
117
  tools.reduce(toolkit) do |kit, tool|
118
- raise ArgumentError, "no tool named #{tool} in this session" unless @engine.tools.key?(tool)
118
+ raise ArgumentError, "no tool named #{tool} in this session" unless @engine.published_tools.key?(tool)
119
119
 
120
120
  verb.casecmp?("add") ? kit.adding(tool) : kit.removing(tool)
121
121
  end