samagotchi 0.155.0 → 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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -1
  3. data/bin/chi +1 -1
  4. data/docs/architecture.md +10 -6
  5. data/docs/internals/anthropic.md +1 -1
  6. data/docs/internals/attached-context.md +3 -2
  7. data/docs/internals/background-tasks.md +1 -1
  8. data/docs/internals/engine.md +625 -935
  9. data/docs/internals/llm-context-forget.md +8 -3
  10. data/docs/internals/objectives.md +2 -2
  11. data/docs/internals/prompt-caching.md +4 -1
  12. data/docs/internals/remote-sessions.md +3 -1
  13. data/docs/internals/working-set.md +1 -1
  14. data/docs/sessions.md +2 -2
  15. data/docs/system-one.md +3 -2
  16. data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
  17. data/lib/samagotchi/broadcast_command.rb +3 -3
  18. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  19. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  20. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  21. data/lib/samagotchi/child_move.rb +7 -51
  22. data/lib/samagotchi/children_status.rb +1 -1
  23. data/lib/samagotchi/cli/parent_wait.rb +1 -1
  24. data/lib/samagotchi/config.rb +17 -112
  25. data/lib/samagotchi/context_command.rb +1 -2
  26. data/lib/samagotchi/context_window.rb +3 -3
  27. data/lib/samagotchi/desktop/macos.rb +1 -1
  28. data/lib/samagotchi/engine/context_judge.rb +19 -11
  29. data/lib/samagotchi/engine/messages.rb +1 -1
  30. data/lib/samagotchi/engine/models.rb +5 -2
  31. data/lib/samagotchi/engine/objectives.rb +29 -11
  32. data/lib/samagotchi/engine/side_requests.rb +21 -11
  33. data/lib/samagotchi/engine/toolkit.rb +10 -9
  34. data/lib/samagotchi/engine/turn_end.rb +1 -1
  35. data/lib/samagotchi/engine/turn_helpers.rb +7 -7
  36. data/lib/samagotchi/engine/verify.rb +1 -1
  37. data/lib/samagotchi/engine.rb +14 -9
  38. data/lib/samagotchi/guardrails/targets.rb +24 -9
  39. data/lib/samagotchi/host_config.rb +72 -1
  40. data/lib/samagotchi/host_model.rb +2 -6
  41. data/lib/samagotchi/host_registry.rb +1 -1
  42. data/lib/samagotchi/kernel_loop.rb +46 -121
  43. data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
  44. data/lib/samagotchi/llm/chat_loop.rb +34 -18
  45. data/lib/samagotchi/llm/native_backend.rb +1 -1
  46. data/lib/samagotchi/llm_context_apply.rb +54 -1
  47. data/lib/samagotchi/llm_context_notice.rb +1 -1
  48. data/lib/samagotchi/llm_context_strategy.rb +7 -9
  49. data/lib/samagotchi/llm_context_view.rb +9 -0
  50. data/lib/samagotchi/model_price.rb +4 -10
  51. data/lib/samagotchi/model_profile.rb +6 -6
  52. data/lib/samagotchi/model_settings.rb +37 -2
  53. data/lib/samagotchi/note_command.rb +1 -2
  54. data/lib/samagotchi/objectives_audit.rb +4 -3
  55. data/lib/samagotchi/participants.rb +2 -2
  56. data/lib/samagotchi/plugin/context.rb +1 -1
  57. data/lib/samagotchi/plugin/sessions.rb +2 -5
  58. data/lib/samagotchi/plugin/tool_spec.rb +5 -3
  59. data/lib/samagotchi/project_env.rb +1 -1
  60. data/lib/samagotchi/reasoning_history.rb +3 -3
  61. data/lib/samagotchi/reminder_carry.rb +1 -1
  62. data/lib/samagotchi/reprefill_meter.rb +45 -0
  63. data/lib/samagotchi/review_command.rb +1 -1
  64. data/lib/samagotchi/sampling_settings.rb +3 -3
  65. data/lib/samagotchi/self_report.rb +7 -7
  66. data/lib/samagotchi/session.rb +50 -7
  67. data/lib/samagotchi/session_access/local.rb +7 -5
  68. data/lib/samagotchi/session_access/remote.rb +17 -3
  69. data/lib/samagotchi/session_archive_command.rb +2 -2
  70. data/lib/samagotchi/session_chain.rb +7 -6
  71. data/lib/samagotchi/session_continue.rb +6 -5
  72. data/lib/samagotchi/session_delete_command.rb +1 -1
  73. data/lib/samagotchi/session_manager/delivery.rb +254 -0
  74. data/lib/samagotchi/session_manager/emptiness.rb +138 -0
  75. data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
  76. data/lib/samagotchi/session_manager/listing.rb +189 -0
  77. data/lib/samagotchi/session_manager/removal.rb +190 -0
  78. data/lib/samagotchi/session_manager/spawn.rb +242 -0
  79. data/lib/samagotchi/session_manager/worker_command.rb +157 -0
  80. data/lib/samagotchi/session_manager.rb +26 -1540
  81. data/lib/samagotchi/session_metrics.rb +1 -1
  82. data/lib/samagotchi/sessions_command.rb +1 -1
  83. data/lib/samagotchi/share_command.rb +1 -1
  84. data/lib/samagotchi/starting_marker.rb +65 -0
  85. data/lib/samagotchi/steer_cut_after.rb +3 -3
  86. data/lib/samagotchi/system_one/recorder.rb +49 -0
  87. data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
  88. data/lib/samagotchi/terminal_ui.rb +2 -6
  89. data/lib/samagotchi/testing/sessions.rb +1 -1
  90. data/lib/samagotchi/thinking.rb +3 -3
  91. data/lib/samagotchi/token_usage.rb +1 -1
  92. data/lib/samagotchi/tool_activity.rb +10 -4
  93. data/lib/samagotchi/tool_declarations.rb +2 -2
  94. data/lib/samagotchi/tool_output_cut.rb +17 -0
  95. data/lib/samagotchi/tool_runner.rb +7 -4
  96. data/lib/samagotchi/toolkit_command.rb +1 -1
  97. data/lib/samagotchi/tools/delegate.rb +10 -5
  98. data/lib/samagotchi/tools/delegate_result.rb +4 -4
  99. data/lib/samagotchi/tools/index_hint.rb +14 -0
  100. data/lib/samagotchi/tools/registry.rb +28 -11
  101. data/lib/samagotchi/tools/send_note.rb +4 -6
  102. data/lib/samagotchi/tools/tool_index.rb +52 -51
  103. data/lib/samagotchi/tools/tool_surface.rb +22 -8
  104. data/lib/samagotchi/turn_context.rb +2 -1
  105. data/lib/samagotchi/turn_stages/context_judge.rb +38 -36
  106. data/lib/samagotchi/turn_stages/objectives.rb +56 -52
  107. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +13 -17
  108. data/lib/samagotchi/turn_stages/verify_anchors.rb +9 -10
  109. data/lib/samagotchi/turn_stages/warmup.rb +14 -15
  110. data/lib/samagotchi/version.rb +1 -1
  111. data/lib/samagotchi/vision_support.rb +3 -3
  112. data/lib/samagotchi/web/app.rb +15 -8
  113. data/lib/samagotchi/web/participant_access.rb +2 -2
  114. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  115. data/lib/samagotchi/web/session_control.rb +3 -3
  116. data/lib/samagotchi/web/session_create.rb +3 -3
  117. data/lib/samagotchi/web/session_hub.rb +1 -1
  118. data/lib/samagotchi/web/session_show.rb +3 -3
  119. data/lib/samagotchi/web/session_summary.rb +3 -4
  120. data/lib/samagotchi/worker.rb +55 -27
  121. data/lib/samagotchi/worker_boot.rb +184 -0
  122. data/lib/samagotchi/worker_signals.rb +1 -1
  123. data/lib/samagotchi/worker_task_wakes.rb +2 -2
  124. metadata +13 -1
@@ -228,13 +228,16 @@ module Samagotchi
228
228
  private
229
229
 
230
230
  # Pick the loop for +model+ (EffectiveModel); the chat loop is pointed
231
- # at its host's adapter (one per host, kept for its cached model list).
231
+ # at its host's adapter (one per host, kept for its cached model list)
232
+ # and client (the window probe, the re-prefill's host).
232
233
  def backend_for(model)
233
234
  return @native_backend unless model.chat?
234
235
 
235
236
  @chat_backend_mutex.synchronize do
236
- @chat_backend ||= LLM::ChatLoop.new(kernel: @kernel, stable_length: ->(text) { @prompt_builder.stable_length(text) })
237
+ @chat_backend ||= LLM::ChatLoop.new(kernel: @kernel, reprefill: @reprefill,
238
+ stable_length: ->(text) { @prompt_builder.stable_length(text) })
237
239
  @chat_backend.adapter = @host_registry.adapter_for(model.entry)
240
+ @chat_backend.client = model.client
238
241
  @chat_backend.session_id = session&.id
239
242
  @chat_backend
240
243
  end
@@ -9,8 +9,22 @@ module Samagotchi
9
9
  # @host_registry, @session.
10
10
  module ObjectivesPart
11
11
  # The session's objectives as the page draws them, when that differs
12
- # from the last announced (TurnStages::Objectives#announce).
13
- def announce_objectives(session = @session) = @objectives_stage.announce(session)
12
+ # from the last announced (TurnStages::Objectives#announce), from any
13
+ # thread: end_turn's step, a command, Check this's thread, a note,
14
+ # a rollback. Under the event lock, which guards the last view: the
15
+ # sessions dir and the running turn's conversation read there, the
16
+ # event announced in the same step. It only informs: a failure is
17
+ # logged, never raised.
18
+ def announce_objectives(session = @session)
19
+ return unless session
20
+
21
+ synchronize_events do
22
+ event = @objectives_stage.announce(session, state_dir: session_state_dir, running: live_conversation)
23
+ announce(event) if event
24
+ end
25
+ rescue StandardError => e
26
+ Log.warn(:turn, "objectives_announce_failed", error: e.class.name, msg: e.message)
27
+ end
14
28
 
15
29
  # Check this (ObjectivesCheck)'s one side request. Its usage is in the
16
30
  # session's side line. Raises as ctx.ask_model's request does.
@@ -32,16 +46,20 @@ module Samagotchi
32
46
  # The objectives' stage (TurnStages::Objectives).
33
47
  attr_reader :objectives_stage
34
48
 
35
- # The stage over this Engine's side asks and its event lock (built in
36
- # #initialize, once the host registry is).
49
+ # However +turn+ ended (run_turn's ensure, after the turn flag is
50
+ # released): a completed turn's proposal cards shown
51
+ # (TurnStages::Objectives#finally). A card that fails is logged.
52
+ def show_objective_proposals(turn)
53
+ @objectives_stage.finally(turn).each { |card| show_card(**card) }
54
+ rescue StandardError => e
55
+ Log.warn(:turn, "objectives_proposal_failed", error: e.class.name, msg: e.message)
56
+ end
57
+
58
+ # The stage over this Engine's side asks (built in #initialize, once
59
+ # the host registry is). Each call takes its values: the recorder,
60
+ # the sessions dir, the running conversation.
37
61
  def build_objectives_stage
38
- TurnStages::Objectives.new(
39
- side_judgments: @side_judgments, host_registry: @host_registry, state_dir: -> { session_state_dir },
40
- recording: ->(client, judgment, session, mode:) { system_one_recording(client, judgment, session, mode: mode) },
41
- runs: -> { system_one_runs }, synchronize: ->(&block) { synchronize_events(&block) },
42
- announce: ->(event) { announce(event) }, running: -> { live_conversation },
43
- show_card: ->(**card) { show_card(**card) }
44
- )
62
+ TurnStages::Objectives.new(side_judgments: @side_judgments, host_registry: @host_registry)
45
63
  end
46
64
  end
47
65
  end
@@ -6,6 +6,7 @@ module Samagotchi
6
6
  # IdleClient, their usage in the session's side line, and System 1's
7
7
  # recording of judged asks. Writes @side_client/@side_client_key (under
8
8
  # @side_client_mutex); reads @metrics, @session, @host_registry.
9
+ # The recorder (SystemOne::Recorder) is the turn stages' recording seam.
9
10
  module SideRequestsPart
10
11
  # Count one side request (a recap, a plugin's ctx.ask_model: IdleClient's
11
12
  # on_usage) in the session's side line, apart from its turns' totals.
@@ -52,24 +53,33 @@ module Samagotchi
52
53
 
53
54
  private
54
55
 
55
- # +client+ (nil for none) saving its asks as +judgment+'s runs. Anything
56
- # going wrong here leaves the client as it was: a run is never worth a
57
- # judgment.
58
- #
59
- # A scratch session's asks aren't saved; a participant's turn is never
60
- # judged, so it never comes here.
56
+ # +client+ (nil for none) saving its asks as +judgment+'s runs
57
+ # (SystemOne::Recorder#wrap, over a recorder built now): the
58
+ # guardrails' System 1 client. No client or a scratch session: the
59
+ # client as it is, with nothing read (no recorder built).
61
60
  # @param mode [String, nil] the use's mode, for its runs (Recording.wrap)
62
61
  def system_one_recording(client, judgment, session, mode: nil)
63
62
  return client if client.nil? || session&.scratch
64
63
 
65
- context = SystemOne::Recording::Context.of(session, session_model: session && effective_model_name)
66
- SystemOne::Recording.wrap(client, judgment: judgment, runs: system_one_runs, context: context, recorded_mode: mode)
67
- rescue StandardError => e
68
- Log.warn(:system_one, "system_one_recording_failed", error: e.class.name, msg: e.message.to_s[0, 200])
69
- client
64
+ system_one_recorder.wrap(client, judgment, session, mode: mode)
70
65
  end
71
66
  private :system_one_recording
72
67
 
68
+ # What a turn stage saves its System 1 asks with (SystemOne::Recorder):
69
+ # this Engine's runs and the model it names now, read at this call and
70
+ # never kept, so each stage call sees the model as it is then (an
71
+ # outgoing ask after a before_turn hook's switch, the new one). Never
72
+ # raises: on a failure, SystemOne::Recorder::NONE (no runs, the plain
73
+ # client), said once per failure as before.
74
+ # @return [SystemOne::Recorder]
75
+ def system_one_recorder
76
+ SystemOne::Recorder.new(runs: system_one_runs, session_model: effective_model_name)
77
+ rescue StandardError => e
78
+ SystemOne::Recorder.failed(e)
79
+ SystemOne::Recorder::NONE
80
+ end
81
+ private :system_one_recorder
82
+
73
83
  # Where this Engine's runs are saved; read each time, as the settings are.
74
84
  def system_one_runs = SystemOne::Runs.for_sessions_dir(session_state_dir)
75
85
  private :system_one_runs
@@ -22,10 +22,14 @@ module Samagotchi
22
22
  # turn's staged tool sets and its toolkit apply.
23
23
  def publish_tools! = @toolkit_binding.publish!
24
24
 
25
+ # That published Registry (ToolkitBinding#published): what a reader
26
+ # off the turn thread (/toolkit) reads of the tools.
27
+ def published_tools = @toolkit_binding.published
28
+
25
29
  # The tools this session registered (Tools::Registry), declared or not.
26
30
  attr_reader :tools
27
31
 
28
- public :tools, :toolkit_kits, :resolved_toolkit, :toolkit_explained, :toolkit_summary
32
+ public :tools, :published_tools, :toolkit_kits, :resolved_toolkit, :toolkit_explained, :toolkit_summary
29
33
 
30
34
  # A plugin's ctx.load_tools: stage the reachable tools +names+ names
31
35
  # (Tools::ToolIndex#deferred_name, on the published Registry) that the
@@ -35,7 +39,7 @@ module Samagotchi
35
39
  # @return [Array<String>] the wire names staged
36
40
  def load_tools(names, bundle)
37
41
  added = ToolLoads.added(@session&.messages)
38
- published = @toolkit_binding.published
42
+ published = published_tools
39
43
  surface = Tools::ToolSurface.for(published)
40
44
  wanted = names.filter_map do |name|
41
45
  wire = @tool_index.deferred_name(name, surface)
@@ -97,14 +101,11 @@ module Samagotchi
97
101
  public :warm_toolkit_suggestion!
98
102
 
99
103
  # The stage over this Engine's side asks, binding and tools (built in
100
- # #build_tools_and_plugins, before the plugins load).
104
+ # #build_tools_and_plugins, before the plugins load). Its turn's start
105
+ # takes the recorder (#system_one_recorder) per call.
101
106
  def build_toolkit_suggestion
102
- TurnStages::ToolkitSuggestion.new(
103
- side_judgments: @side_judgments, host_registry: @host_registry, toolkit_binding: @toolkit_binding,
104
- tools: @tools, state_dir: -> { session_state_dir },
105
- recording: ->(client, judgment, session, mode:) { system_one_recording(client, judgment, session, mode: mode) },
106
- runs: -> { system_one_runs }
107
- )
107
+ TurnStages::ToolkitSuggestion.new(side_judgments: @side_judgments, host_registry: @host_registry,
108
+ toolkit_binding: @toolkit_binding, tools: @tools)
108
109
  end
109
110
  private :build_toolkit_suggestion
110
111
  end
@@ -117,7 +117,7 @@ module Samagotchi
117
117
  # a display version of the answer (AnswerDisplay) and event[:refs]
118
118
  # for refs on it (AnswerRefs).
119
119
  session = turn.session
120
- @objectives_stage.outgoing(turn, result) unless canceled || result.exhausted?
120
+ @objectives_stage.outgoing(turn, result, recorder: system_one_recorder) unless canceled || result.exhausted?
121
121
  display = AnswerDisplay.new(session.messages)
122
122
  refs = AnswerRefs.new(session.messages, cwd: session.working_directory)
123
123
  after_turn = { type: :after_turn, status: canceled ? "canceled" : "completed",
@@ -7,7 +7,7 @@ module Samagotchi
7
7
  # with, the used memories it publishes, and the turn-end warm-up of the
8
8
  # next one (TurnStages::Warmup, built here). Writes @next_turn_waiting;
9
9
  # reads @model_settings, @kernel, @prompt_warmup, @thinking_notices,
10
- # @turn_state, @turn_warmup.
10
+ # @turn_state, @reminder_queue, @turn_warmup.
11
11
  module TurnHelpersPart
12
12
  # The turn's settings for the kernel (LLM::TurnSettings): vision,
13
13
  # sampling and thinking, the thinking resolved before the hooks run.
@@ -40,13 +40,13 @@ module Samagotchi
40
40
  private :turn_warmup
41
41
 
42
42
  # The stage over the kernel and the Engine's PromptWarmup (built in
43
- # #initialize, once the kernel is).
43
+ # #initialize, once the kernel and the reminder queue are). Its one
44
+ # callable is the system prompt, built late (side effects); the
45
+ # host's probe goes with each call (#next_turn_waiting, set after).
44
46
  def build_turn_warmup
45
- TurnStages::Warmup.new(
46
- prompt_warmup: @prompt_warmup, kernel: @kernel, turn_state: @turn_state, model_settings: @model_settings,
47
- model: -> { effective_model }, system_prompt: ->(model) { system_prompt(model) },
48
- reminders_due: -> { reminders_due? }, next_turn_waiting: -> { next_turn_waiting&.call }
49
- )
47
+ TurnStages::Warmup.new(prompt_warmup: @prompt_warmup, kernel: @kernel, turn_state: @turn_state,
48
+ model_settings: @model_settings, reminder_queue: @reminder_queue,
49
+ system_prompt: ->(model) { system_prompt(model) })
50
50
  end
51
51
  private :build_turn_warmup
52
52
 
@@ -11,7 +11,7 @@ module Samagotchi
11
11
  # The anchors' stage (TurnStages::VerifyAnchors).
12
12
  attr_reader :verify_anchors
13
13
 
14
- def build_verify_anchors = TurnStages::VerifyAnchors.new(state_dir: -> { session_state_dir })
14
+ def build_verify_anchors = TurnStages::VerifyAnchors.new
15
15
  end
16
16
  end
17
17
  end
@@ -27,6 +27,7 @@ require_relative "thinking_notices"
27
27
  require_relative "cancellation_controller"
28
28
  require_relative "context_window"
29
29
  require_relative "kernel_loop"
30
+ require_relative "reprefill_meter"
30
31
  require_relative "tools/builtins"
31
32
  require_relative "tools/task_runtime"
32
33
  require_relative "tools/tool_index"
@@ -55,6 +56,7 @@ require_relative "context_judge"
55
56
  require_relative "objectives_audit"
56
57
  require_relative "owner_prompt"
57
58
  require_relative "side_judgments"
59
+ require_relative "system_one/recorder"
58
60
  require_relative "toolkit_suggest"
59
61
  require_relative "turn_stages/context_judge"
60
62
  require_relative "turn_stages/objectives"
@@ -451,6 +453,9 @@ module Samagotchi
451
453
  self.guardrail_state_dir = Session.default_state_dir
452
454
  # The turn-end warm-up; the kernel asks it for a slot pin per request.
453
455
  @prompt_warmup = PromptWarmup.new
456
+ # One re-prefill baseline for both loops (the native kernel's and the
457
+ # chat loop's requests), reset by the warm-up (KernelLoop#warmed_up!).
458
+ @reprefill = ReprefillMeter.new
454
459
  services = KernelLoop::Services.new(
455
460
  hooks: @hooks, tools: @tools, reminder_store: @reminder_store, muted_memory_names: @muted_memory_names,
456
461
  # ask_user_question blocks on the Engine's question flow (TUI/Web answer it).
@@ -466,8 +471,8 @@ module Samagotchi
466
471
  )
467
472
  # A given kernel (specs) gets the same services as the one built here.
468
473
  @kernel = kernel || KernelLoop.new(client: @client, profile: @model_binding.given_profile,
469
- model_name: effective_model_name, services: services)
470
- @kernel.attach(services) if kernel
474
+ model_name: effective_model_name, services: services, reprefill: @reprefill)
475
+ @kernel.attach(services, reprefill: @reprefill) if kernel
471
476
  sync_kernel_client!
472
477
  sync_model_key!
473
478
  # What a hook can do beyond reading its event (event[:notify],
@@ -727,7 +732,7 @@ module Samagotchi
727
732
  event[:verdict] = verdict if verdict
728
733
  event[:revises] = revises.to_h if revises
729
734
  emit_working_set(turn.on_event, turn.session, turn.session.messages)
730
- @objectives_stage.announce(turn.session)
735
+ announce_objectives(turn.session)
731
736
  turn.session.status = Session::STATUS_IDLE
732
737
  record_last_turn(turn.session, outcome, seconds, turn.origin, event, limit: exhausted ? turn.limit : nil)
733
738
  emit_event(turn.on_event, turn.tag(event))
@@ -911,7 +916,7 @@ module Samagotchi
911
916
  pending_input: pending_input)
912
917
  publish_used_memories(session, on_event)
913
918
  complete_turn(turn, result)
914
- @turn_warmup.after(turn, result)
919
+ @turn_warmup.after(turn, result, next_turn_waiting: next_turn_waiting)
915
920
  result
916
921
  rescue Interrupt => e
917
922
  # After the end event (in the after_turn/session_end hooks) the
@@ -941,7 +946,7 @@ module Samagotchi
941
946
  @context_judge.show_cards_after_turn
942
947
  # Only a completed turn shows its proposal cards; any other's are
943
948
  # dropped, so the next turn does not show them either.
944
- @objectives_stage.finally(turn)
949
+ show_objective_proposals(turn)
945
950
  end
946
951
  end
947
952
 
@@ -954,7 +959,7 @@ module Samagotchi
954
959
  # A participant's Verify fresh child: every turn is theirs (their
955
960
  # author, boundary and cap), whoever started it.
956
961
  origin = session.verify_fresh.origin_for(origin) if session.respond_to?(:verify_fresh) && session.verify_fresh
957
- verify = @verify_anchors.verified(session, VerifyAnchor.from(verify))
962
+ verify = @verify_anchors.verified(session, VerifyAnchor.from(verify), state_dir: session_state_dir)
958
963
  # Track the active session for recap and status snapshot.
959
964
  @session = session
960
965
  # status is turn state: running now, idle again before the turn's end
@@ -1044,7 +1049,7 @@ module Samagotchi
1044
1049
  # On mode's ask starts here too; the turn joins it before
1045
1050
  # apply_toolkit! below, so its latency hides behind the probes and
1046
1051
  # the init wait.
1047
- @toolkit_suggestion.start(turn)
1052
+ @toolkit_suggestion.start(turn, recorder: system_one_recorder)
1048
1053
 
1049
1054
  # The window and the profile probes run after run_turn's probe-cancel
1050
1055
  # swap, so a Stop cuts them; a failure ends the turn as turn_failed.
@@ -1081,8 +1086,8 @@ module Samagotchi
1081
1086
  @toolkit_suggestion.settle(turn)
1082
1087
  # The candidate judge: its asks run beside the turn; on mode's
1083
1088
  # attaches reach the model at the next between-turn delivery.
1084
- @context_judge.start(turn)
1085
- @objectives_stage.incoming(turn)
1089
+ @context_judge.start(turn, recorder: system_one_recorder)
1090
+ @objectives_stage.incoming(turn, recorder: system_one_recorder)
1086
1091
  apply_toolkit!(session, turn.on_event)
1087
1092
  refresh_participants!(session)
1088
1093
  # A Stop during the waits above: the turn ends here,
@@ -113,8 +113,10 @@ module Samagotchi
113
113
  # @param model_name [String, nil] the bare model name, for a rule's models:
114
114
  # @param registry [Tools::Registry, nil] the session's tools: a plugin
115
115
  # tool's entry says what it acts on (its targets:)
116
- # @param surface [Tools::ToolSurface, nil] the turn's: call_tool's
117
- # targets name what the turn reaches (nil: the Registry now)
116
+ # @param surface [Tools::ToolSurface, nil] the turn's: the entry is
117
+ # looked up in it (.entry_of) and call_tool's targets name what it
118
+ # reaches (nil: the registry's entry; call_tool's then raises
119
+ # Tools::MissingSurface)
118
120
  def self.for(call, context, model_key: nil, model_name: nil, registry: nil, surface: nil)
119
121
  tool = call[:name].to_s
120
122
  base = context.cwd
@@ -143,7 +145,7 @@ module Samagotchi
143
145
  given_cwd = given[:cwd].to_s.strip
144
146
  cwd = File.expand_path(given_cwd, base) unless given_cwd.empty?
145
147
  paths.concat(given[:paths].flat_map { |path| absolutes(path, cwd) })
146
- entry = registry && registry[tool]
148
+ entry = entry_of(tool, registry, surface)
147
149
  args = given[:args] || call[:args] if entry && !entry.core? && call[:args].is_a?(Hash)
148
150
  acts_as = given[:acts_as]
149
151
  label = given[:label]
@@ -164,9 +166,12 @@ module Samagotchi
164
166
  # logged). args: counts when it is a Hash; acts_as: is dropped (and
165
167
  # logged) when it names a core tool or another bundle's tool, so a
166
168
  # call can't pass for one and match (or reuse the approvals of) its
167
- # rules. label: counts when it is one short line (.label_of).
169
+ # rules. label: counts when it is one short line (.label_of). A
170
+ # missing surface (Tools::MissingSurface) is raised, never taken as
171
+ # nothing: call_tool's acts_as would go missing, and with it the
172
+ # mcp_* rules on the call (fail-open).
168
173
  def self.plugin_targets(call, registry, surface = nil)
169
- entry = registry && registry[call[:name].to_s]
174
+ entry = entry_of(call[:name], registry, surface)
170
175
  none = { command: nil, paths: [], cwd: nil, acts_as: nil, args: nil, label: nil }
171
176
  return none unless entry && !entry.core? && entry.targets
172
177
 
@@ -176,8 +181,10 @@ module Samagotchi
176
181
  given = given.transform_keys(&:to_sym)
177
182
  command = given[:command].to_s
178
183
  { command: command.empty? ? nil : command, paths: Array(given[:paths]).map(&:to_s), cwd: given[:cwd],
179
- acts_as: acts_as_of(given[:acts_as], entry, registry), args: given[:args].is_a?(Hash) ? given[:args] : nil,
184
+ acts_as: acts_as_of(given[:acts_as], entry, registry, surface), args: given[:args].is_a?(Hash) ? given[:args] : nil,
180
185
  label: label_of(given[:label], entry) }
186
+ rescue Tools::MissingSurface
187
+ raise
181
188
  rescue StandardError => e
182
189
  Log.warn(:plugins, "plugin_targets_failed", tool: call[:name].to_s, error: e.class.name,
183
190
  msg: "#{call[:name]} targets failed: #{e.message}")
@@ -192,24 +199,32 @@ module Samagotchi
192
199
  # name, ToolIndex#claimed), so the registry is the wrong owner test,
193
200
  # and a bundle tool sharing an MCP tool's name would otherwise take
194
201
  # call_tool calls of that tool out of its mcp_* rules.
195
- def self.acts_as_of(name, entry, registry)
202
+ def self.acts_as_of(name, entry, registry, surface = nil)
196
203
  name = name.to_s.strip
197
204
  return nil if name.empty? || name == entry.name
198
205
 
199
- other = registry[name]
206
+ other = entry_of(name, registry, surface)
200
207
  whose = if CORE_TOOLS.include?(name) || other&.core? then "a core tool"
201
208
  elsif other && other.source != entry.source && entry.source != Tools::ToolIndex::SOURCE
202
209
  "bundle #{other.source}'s tool"
203
210
  end
204
211
  return name unless whose
205
212
 
206
- if first_drop?(registry, entry.name, name)
213
+ if first_drop?(registry || surface, entry.name, name)
207
214
  Log.warn(:plugins, "plugin_acts_as_dropped", tool: entry.name, acts_as: name,
208
215
  msg: "#{entry.name} targets: acts_as #{name} dropped: it is #{whose}")
209
216
  end
210
217
  nil
211
218
  end
212
219
 
220
+ # The tool +name+ names: the turn's entry (+surface+'s registered),
221
+ # else +registry+'s (a gate without a turn's surface); nil for none.
222
+ # @return [Tools::Registry::Entry, nil]
223
+ def self.entry_of(name, registry, surface)
224
+ name = name.to_s
225
+ surface ? surface.registered[name] : registry&.[](name)
226
+ end
227
+
213
228
  # The drops logged, per registry (a session's): each tool's acts_as
214
229
  # once, not at every call (targets run several times a call).
215
230
  DROPS_LOGGED = ObjectSpace::WeakKeyMap.new
@@ -8,7 +8,7 @@ require_relative "llm/dialect"
8
8
  require_relative "reasoning_history"
9
9
 
10
10
  module Samagotchi
11
- # One config.yml hosts: entry, parsed and checked (ConfigFile.hosts_config
11
+ # One config.yml hosts: entry, parsed and checked (HostConfig.all
12
12
  # holds them by lowercased name; HostRegistry builds its HostEntry from
13
13
  # one).
14
14
  #
@@ -28,6 +28,77 @@ module Samagotchi
28
28
  super(**members.to_h { |m| [m, nil] }, models: {}, **fields)
29
29
  end
30
30
 
31
+ # config.yml's hosts: by lowercased name; an entry that is disabled or
32
+ # invalid (warned) is left out. No entry left: "default", from
33
+ # server.host/port.
34
+ # @return [Hash{String => HostConfig}]
35
+ def self.all(env: ENV, path: ConfigFile.global_path(env: env))
36
+ data = ConfigFile.read_yaml(env: env, path: path)
37
+ raw_hosts = data[ConfigFile::HOSTS_KEY] if data.is_a?(Hash)
38
+
39
+ normalized = {}
40
+ if raw_hosts.is_a?(Hash)
41
+ raw_hosts.each do |raw_name, raw_cfg|
42
+ host = parse(raw_name, raw_cfg)
43
+ normalized[host.name] = host if host
44
+ end
45
+ warn_duplicate_models(normalized)
46
+ end
47
+ normalized["default"] = default_host(data, env) if normalized.empty?
48
+ normalized
49
+ rescue StandardError => e
50
+ Log.error(:config, "hosts_config_failed", error: e.class.name, message: e.message)
51
+ {}
52
+ end
53
+
54
+ # The names (lowercased) of the hosts entries with enabled: false, which
55
+ # .all leaves out, from config.yml's hosts:.
56
+ # @return [Array<String>]
57
+ def self.disabled_names(env: ENV, path: ConfigFile.global_path(env: env))
58
+ data = ConfigFile.read_yaml(env: env, path: path)
59
+ raw = data[ConfigFile::HOSTS_KEY] if data.is_a?(Hash)
60
+ return [] unless raw.is_a?(Hash)
61
+
62
+ raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && disabled?(cfg) }
63
+ rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
64
+ []
65
+ end
66
+
67
+ # +raw+ parsed against config.yml's hosts (or +hosts+) and aliases.
68
+ # @return [ModelRef]
69
+ def self.model_ref(raw, env: ENV, path: ConfigFile.global_path(env: env), hosts: nil)
70
+ ModelRef.parse(raw, hosts: hosts || all(env: env, path: path), aliases: ConfigFile.model_aliases(env: env, path: path))
71
+ end
72
+
73
+ # The host chi uses when hosts: names none: server.host/port.
74
+ def self.default_host(data, env)
75
+ opts = { file_data: data, env: env, cli_overrides: Config.cli_overrides }
76
+ host = Config.resolve("server.host", **opts).to_s.strip
77
+ host = "localhost" if host.empty?
78
+ port = Config.resolve("server.port", **opts).to_i
79
+ port = 8080 if port <= 0 || port > 65_535
80
+ new(name: "default", host: host, port: port)
81
+ end
82
+ private_class_method :default_host
83
+
84
+ # Two hosts declaring one id under hosts.<name>.models: a bare id goes
85
+ # where HostRegistry#host_for_model sends it (the default host when it
86
+ # declares the id, else the first in hosts: order); say so once.
87
+ def self.warn_duplicate_models(hosts)
88
+ declared = Hash.new { |h, k| h[k] = [] }
89
+ hosts.each { |name, cfg| cfg.models.each { |down, model| declared[down] << [name, model.id] } }
90
+ declared.each_value do |owners|
91
+ next if owners.size < 2
92
+
93
+ names = owners.map(&:first)
94
+ id = owners.first.last
95
+ goes = names.include?("default") ? "default" : "#{names.first} unless the default host lists it"
96
+ ConfigFile.warn_once "Warning: #{names.map { |n| "hosts.#{n}.models" }.join(" and ")} #{names.size == 2 ? "both" : "all"} " \
97
+ "declare #{id}; a bare #{id} goes to #{goes}"
98
+ end
99
+ end
100
+ private_class_method :warn_duplicate_models
101
+
31
102
  # A hosts: entry as written in config.yml,
32
103
  # or nil when it is disabled (enabled: false) or invalid. An invalid
33
104
  # one warns once, naming the entry, the way each reader of a setting
@@ -67,12 +67,8 @@ module Samagotchi
67
67
  served == "any" || served.any? { |id| id.casecmp?(name.to_s.strip) }
68
68
  end
69
69
 
70
- # config.rb requires this file, so config is required here only when a
71
- # warning needs it (this file loaded alone).
72
- def self.warn_once(message)
73
- require_relative "config"
74
- ConfigFile.warn_once(message)
75
- end
70
+ # Config comes with model_price.
71
+ def self.warn_once(message) = ConfigFile.warn_once(message)
76
72
  private_class_method :warn_once
77
73
 
78
74
  # One host's rows as the listings show them, declared ids first, then
@@ -126,7 +126,7 @@ module Samagotchi
126
126
  @clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
127
127
  @adapters = {}
128
128
  @host_lists = {}
129
- raw = hosts_config || ConfigFile.hosts_config(env: env)
129
+ raw = hosts_config || HostConfig.all(env: env)
130
130
  @entries = {}
131
131
  raw.each do |key, cfg|
132
132
  cfg = HostConfig.coerce(key, cfg)