samagotchi 0.154.1 → 0.156.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/bin/chi +1 -1
  4. data/docs/architecture.md +12 -7
  5. data/docs/bundles.md +21 -0
  6. data/docs/cli.md +1 -1
  7. data/docs/guardrails.md +2 -2
  8. data/docs/hooks.md +1 -1
  9. data/docs/internals/anthropic.md +5 -5
  10. data/docs/internals/attached-context.md +9 -6
  11. data/docs/internals/background-tasks.md +1 -1
  12. data/docs/internals/block-renderers.md +8 -1
  13. data/docs/internals/engine.md +642 -831
  14. data/docs/internals/inline-widgets.md +136 -0
  15. data/docs/internals/llm-context-forget.md +8 -3
  16. data/docs/internals/objectives.md +8 -6
  17. data/docs/internals/participants.md +5 -5
  18. data/docs/internals/prompt-caching.md +13 -6
  19. data/docs/internals/remote-sessions.md +3 -1
  20. data/docs/internals/web-frontend.md +1 -1
  21. data/docs/internals/working-set.md +2 -1
  22. data/docs/plugins.md +2 -0
  23. data/docs/releasing.md +1 -1
  24. data/docs/sessions.md +4 -4
  25. data/docs/system-one.md +3 -2
  26. data/docs/testing.md +1 -1
  27. data/lib/samagotchi/block_renderers.rb +5 -0
  28. data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
  29. data/lib/samagotchi/broadcast_command.rb +3 -3
  30. data/lib/samagotchi/bundle_command/check.rb +1 -1
  31. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  32. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  33. data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
  34. data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
  35. data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
  36. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  37. data/lib/samagotchi/child_move.rb +7 -51
  38. data/lib/samagotchi/children_status.rb +1 -1
  39. data/lib/samagotchi/cli/parent_wait.rb +1 -1
  40. data/lib/samagotchi/config.rb +17 -112
  41. data/lib/samagotchi/context_command.rb +1 -2
  42. data/lib/samagotchi/context_window.rb +3 -3
  43. data/lib/samagotchi/desktop/macos.rb +1 -1
  44. data/lib/samagotchi/engine/context_judge.rb +21 -119
  45. data/lib/samagotchi/engine/guardrails.rb +4 -2
  46. data/lib/samagotchi/engine/messages.rb +9 -6
  47. data/lib/samagotchi/engine/models.rb +5 -2
  48. data/lib/samagotchi/engine/objectives.rb +30 -80
  49. data/lib/samagotchi/engine/participants.rb +15 -10
  50. data/lib/samagotchi/engine/plugins.rb +3 -1
  51. data/lib/samagotchi/engine/questions.rb +1 -1
  52. data/lib/samagotchi/engine/side_requests.rb +26 -13
  53. data/lib/samagotchi/engine/toolkit.rb +35 -191
  54. data/lib/samagotchi/engine/turn_end.rb +1 -1
  55. data/lib/samagotchi/engine/turn_helpers.rb +16 -39
  56. data/lib/samagotchi/engine/verify.rb +7 -143
  57. data/lib/samagotchi/engine.rb +166 -98
  58. data/lib/samagotchi/engine_peers.rb +1 -1
  59. data/lib/samagotchi/event_fanout.rb +18 -12
  60. data/lib/samagotchi/guardrail_wiring.rb +83 -82
  61. data/lib/samagotchi/guardrails/gate.rb +41 -40
  62. data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
  63. data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
  64. data/lib/samagotchi/guardrails/targets.rb +28 -11
  65. data/lib/samagotchi/guardrails_command.rb +1 -1
  66. data/lib/samagotchi/host_config.rb +72 -1
  67. data/lib/samagotchi/host_model.rb +2 -6
  68. data/lib/samagotchi/host_registry.rb +1 -1
  69. data/lib/samagotchi/inline_widgets.rb +325 -0
  70. data/lib/samagotchi/kernel_loop.rb +215 -313
  71. data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
  72. data/lib/samagotchi/llm/backend.rb +3 -1
  73. data/lib/samagotchi/llm/chat_loop.rb +87 -49
  74. data/lib/samagotchi/llm/native_backend.rb +3 -2
  75. data/lib/samagotchi/llm_context_apply.rb +54 -1
  76. data/lib/samagotchi/llm_context_forget.rb +5 -3
  77. data/lib/samagotchi/llm_context_notice.rb +1 -1
  78. data/lib/samagotchi/llm_context_strategy.rb +7 -9
  79. data/lib/samagotchi/llm_context_view.rb +9 -0
  80. data/lib/samagotchi/model_binding.rb +3 -3
  81. data/lib/samagotchi/model_price.rb +4 -10
  82. data/lib/samagotchi/model_profile.rb +6 -6
  83. data/lib/samagotchi/model_settings.rb +37 -2
  84. data/lib/samagotchi/note_command.rb +1 -2
  85. data/lib/samagotchi/objectives_audit.rb +4 -3
  86. data/lib/samagotchi/participants.rb +2 -2
  87. data/lib/samagotchi/plugin/context.rb +1 -1
  88. data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
  89. data/lib/samagotchi/plugin/sessions.rb +2 -5
  90. data/lib/samagotchi/plugin/tool_spec.rb +5 -3
  91. data/lib/samagotchi/project_env.rb +1 -1
  92. data/lib/samagotchi/question_desk.rb +1 -1
  93. data/lib/samagotchi/reasoning_history.rb +3 -3
  94. data/lib/samagotchi/reminder_carry.rb +1 -1
  95. data/lib/samagotchi/reprefill_meter.rb +45 -0
  96. data/lib/samagotchi/review_command.rb +1 -1
  97. data/lib/samagotchi/revise_anchor.rb +1 -1
  98. data/lib/samagotchi/sampling_settings.rb +3 -3
  99. data/lib/samagotchi/self_report.rb +7 -7
  100. data/lib/samagotchi/session.rb +50 -7
  101. data/lib/samagotchi/session_access/local.rb +7 -5
  102. data/lib/samagotchi/session_access/remote.rb +17 -3
  103. data/lib/samagotchi/session_archive_command.rb +2 -2
  104. data/lib/samagotchi/session_chain.rb +7 -6
  105. data/lib/samagotchi/session_continue.rb +6 -5
  106. data/lib/samagotchi/session_delete_command.rb +1 -1
  107. data/lib/samagotchi/session_manager/delivery.rb +254 -0
  108. data/lib/samagotchi/session_manager/emptiness.rb +138 -0
  109. data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
  110. data/lib/samagotchi/session_manager/listing.rb +189 -0
  111. data/lib/samagotchi/session_manager/removal.rb +190 -0
  112. data/lib/samagotchi/session_manager/spawn.rb +242 -0
  113. data/lib/samagotchi/session_manager/worker_command.rb +157 -0
  114. data/lib/samagotchi/session_manager.rb +26 -1540
  115. data/lib/samagotchi/session_metrics.rb +1 -1
  116. data/lib/samagotchi/sessions_command.rb +1 -1
  117. data/lib/samagotchi/share_command.rb +1 -1
  118. data/lib/samagotchi/starting_marker.rb +65 -0
  119. data/lib/samagotchi/steer_cut_after.rb +3 -3
  120. data/lib/samagotchi/system_one/recorder.rb +49 -0
  121. data/lib/samagotchi/system_prompt.rb +37 -30
  122. data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
  123. data/lib/samagotchi/terminal_ui.rb +2 -6
  124. data/lib/samagotchi/testing/boundary.rb +7 -0
  125. data/lib/samagotchi/testing/bundle.rb +7 -5
  126. data/lib/samagotchi/testing/install.rb +3 -2
  127. data/lib/samagotchi/testing/sessions.rb +1 -1
  128. data/lib/samagotchi/thinking.rb +3 -3
  129. data/lib/samagotchi/token_usage.rb +1 -1
  130. data/lib/samagotchi/tool_activity.rb +15 -7
  131. data/lib/samagotchi/tool_declarations.rb +6 -6
  132. data/lib/samagotchi/tool_output_cut.rb +17 -0
  133. data/lib/samagotchi/tool_response.rb +4 -3
  134. data/lib/samagotchi/tool_runner.rb +96 -102
  135. data/lib/samagotchi/toolkit_binding.rb +40 -14
  136. data/lib/samagotchi/toolkit_command.rb +1 -1
  137. data/lib/samagotchi/tools/deferred_tools.rb +17 -21
  138. data/lib/samagotchi/tools/delegate.rb +10 -5
  139. data/lib/samagotchi/tools/delegate_result.rb +4 -4
  140. data/lib/samagotchi/tools/index_hint.rb +14 -0
  141. data/lib/samagotchi/tools/registry.rb +64 -44
  142. data/lib/samagotchi/tools/send_note.rb +4 -6
  143. data/lib/samagotchi/tools/tool_index.rb +150 -102
  144. data/lib/samagotchi/tools/tool_surface.rb +187 -0
  145. data/lib/samagotchi/turn_context.rb +68 -0
  146. data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
  147. data/lib/samagotchi/turn_stages/objectives.rb +150 -0
  148. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
  149. data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
  150. data/lib/samagotchi/turn_stages/warmup.rb +82 -0
  151. data/lib/samagotchi/turn_state.rb +46 -4
  152. data/lib/samagotchi/version.rb +1 -1
  153. data/lib/samagotchi/vision_support.rb +3 -3
  154. data/lib/samagotchi/web/app.rb +15 -8
  155. data/lib/samagotchi/web/markdown_renderer.rb +39 -14
  156. data/lib/samagotchi/web/message_display.rb +1 -1
  157. data/lib/samagotchi/web/participant_access.rb +2 -2
  158. data/lib/samagotchi/web/public/app.js +5 -1
  159. data/lib/samagotchi/web/public/block_host.js +74 -199
  160. data/lib/samagotchi/web/public/block_renderers.css +2 -0
  161. data/lib/samagotchi/web/public/block_source.js +7 -4
  162. data/lib/samagotchi/web/public/block_watch.js +256 -0
  163. data/lib/samagotchi/web/public/composer_fold.js +3 -0
  164. data/lib/samagotchi/web/public/index.html +28 -9
  165. data/lib/samagotchi/web/public/inline_widgets.js +146 -0
  166. data/lib/samagotchi/web/public/owner_verify.js +1 -1
  167. data/lib/samagotchi/web/public/participant.js +1 -1
  168. data/lib/samagotchi/web/public/revise_stack.js +1 -1
  169. data/lib/samagotchi/web/public/stage_watch.js +6 -2
  170. data/lib/samagotchi/web/public/text_roots.js +1 -1
  171. data/lib/samagotchi/web/public/verify_target.js +1 -1
  172. data/lib/samagotchi/web/public/widget_picks.js +130 -0
  173. data/lib/samagotchi/web/public/widget_walk.js +230 -0
  174. data/lib/samagotchi/web/public/widgets.css +70 -0
  175. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  176. data/lib/samagotchi/web/session_control.rb +3 -3
  177. data/lib/samagotchi/web/session_create.rb +3 -3
  178. data/lib/samagotchi/web/session_hub.rb +1 -1
  179. data/lib/samagotchi/web/session_show.rb +3 -3
  180. data/lib/samagotchi/web/session_summary.rb +3 -4
  181. data/lib/samagotchi/worker.rb +55 -27
  182. data/lib/samagotchi/worker_boot.rb +184 -0
  183. data/lib/samagotchi/worker_signals.rb +1 -1
  184. data/lib/samagotchi/worker_task_wakes.rb +2 -2
  185. metadata +29 -1
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require_relative "args"
5
5
  require_relative "builtin_calls"
6
+ require_relative "index_hint"
6
7
  require_relative "registry"
7
8
  require_relative "../log"
8
9
  require_relative "../token_usage"
@@ -11,6 +12,10 @@ require_relative "../plugin/tool_spec"
11
12
 
12
13
  module Samagotchi
13
14
  module Tools
15
+ # Read only when a surface is built (#declare, #refresh!): the bundle
16
+ # test kit loads this file for ToolIndex.new and never builds one.
17
+ autoload :ToolSurface, File.expand_path("tool_surface", __dir__)
18
+
14
19
  # The tools a session knows but doesn't declare one by one, and the two
15
20
  # tools that reach them (docs/plugins.md, "Publishing tools to the
16
21
  # index"): find_tools, a keyword search that answers each match's
@@ -18,11 +23,20 @@ module Samagotchi
18
23
  # A source (the mcp bundle, chi.tool_source) publishes its groups (an
19
24
  # MCP server each) and runs the calls; the index's own group, chi, is
20
25
  # the tools the session's toolkit leaves out but doesn't remove, chi's
21
- # built-ins and its bundles' (chi.tool) alike (Registry#reachable_names),
26
+ # built-ins and its bundles' (chi.tool) alike (ToolSurface#index_only),
22
27
  # and a call_tool of one runs as that tool (#unwrap, before the
23
28
  # guardrail gate). The index searches the
24
29
  # groups, looks a call's tool up and checks it.
25
30
  #
31
+ # What it reads of the session's tools is a ToolSurface, always given:
32
+ # the turn's (TurnContext#surface) on the turn's path (the two tools'
33
+ # handlers, through their ToolContext; call_tool's row and targets,
34
+ # from the row and the gate; #unwrap, #wrap, #deferrable and
35
+ # #unreached_tool, from the runner and the deferred list); one built
36
+ # from the Registry #declare registered in at load and at #refresh!;
37
+ # ToolkitBinding#published's for ctx.load_tools (#deferred_name). None
38
+ # where one is needed raises MissingSurface.
39
+ #
26
40
  # The two tools are registered once, after the plugins load, and
27
41
  # declared per toolkit (Registry#declared?): when a source registered
28
42
  # then, or the toolkit leaves a tool reachable. The search tool's
@@ -58,8 +72,6 @@ module Samagotchi
58
72
  group: { type: "string", description: "search only this group" }
59
73
  }.freeze
60
74
  CALL_DESCRIPTION = "Call a tool #{FIND_NAME} found.".freeze
61
- # A keyword answer's first line.
62
- CALL_HINT = "Call one with #{CALL_NAME}(tool: \"<group>/<tool>\", args: {…}).".freeze
63
75
  CALL_PARAMS = {
64
76
  tool: { type: "string", required: true, description: "the tool as <group>/<tool>" },
65
77
  args: { type: "object", description: "the arguments, per the inputSchema #{FIND_NAME} gave" }
@@ -70,10 +82,11 @@ module Samagotchi
70
82
  class ToolkitGroupRefusal < StandardError
71
83
  RULE = "toolkit-group"
72
84
 
73
- # The refusal of +tool+ under +registry+'s toolkit, naming a kit that
74
- # reaches its group when the registry can tell (Registry#reaching_kit).
75
- def self.for(tool, registry)
76
- new(tool, registry.toolkit, kit: registry.respond_to?(:reaching_kit) ? registry.reaching_kit&.call(tool.group) : nil)
85
+ # The refusal of +tool+ under +surface+'s toolkit (a ToolSurface),
86
+ # naming a kit that reaches its group when it can tell
87
+ # (ToolSurface#reaching_kit).
88
+ def self.for(tool, surface)
89
+ new(tool, surface.toolkit, kit: surface.reaching_kit&.call(tool.group))
77
90
  end
78
91
 
79
92
  def initialize(tool, toolkit, kit: nil)
@@ -248,12 +261,12 @@ module Samagotchi
248
261
  # @return [Array<Source>] the sources added, in order
249
262
  def sources = @sources.dup
250
263
 
251
- # Every group, now: the index's own (#registry_groups), then the
252
- # sources'. A source group with a reserved name, or one an earlier
253
- # group has, is left out of search, lookup and the description (said
254
- # once per name: a source publishes after load too).
255
- def all_groups
256
- own = registry_groups
264
+ # Every group, now: the index's own (#registry_groups, from
265
+ # +surface+), then the sources'. A source group with a reserved name,
266
+ # or one an earlier group has, is left out of search, lookup and the
267
+ # description (said once per name: a source publishes after load too).
268
+ def all_groups(surface)
269
+ own = registry_groups(surface)
257
270
  taken = own.map(&:name) | RESERVED_GROUPS
258
271
  kept = @sources.flat_map(&:groups).select do |group|
259
272
  why = if RESERVED_GROUPS.include?(group.name) then "reserved"
@@ -266,9 +279,11 @@ module Samagotchi
266
279
  own + kept
267
280
  end
268
281
 
269
- def reached_groups
270
- groups = all_groups
271
- toolkit = @registry&.toolkit
282
+ # The groups +surface+'s toolkit reaches (#all_groups' own, and the
283
+ # source groups #declare found that it reaches).
284
+ def reached_groups(surface)
285
+ groups = all_groups(surface)
286
+ toolkit = surface.toolkit
272
287
  return groups if toolkit.nil? || toolkit.groups == :all
273
288
 
274
289
  declared = @declared_group_names || groups.reject { |group| own?(group) }.map(&:name)
@@ -285,10 +300,11 @@ module Samagotchi
285
300
  # @return [Array<String>] the names registered
286
301
  def declare(registry, failures: nil)
287
302
  @registry = registry
288
- @described = description_lines(all_groups.reject { |group| own?(group) }).freeze
303
+ at_load = ToolSurface.for(registry)
304
+ @described = description_lines(all_groups(at_load).reject { |group| own?(group) }).freeze
289
305
  @declared_group_names = @described.keys.freeze
290
306
  loaded = registry.registered
291
- specs = [find_spec, call_spec]
307
+ specs = [find_spec(at_load), call_spec]
292
308
  declared = specs.filter_map do |spec|
293
309
  if (taken = registry[spec.name])
294
310
  failures&.add("tool #{spec.name}", "already registered (#{taken.source}); the tool index's is left out",
@@ -296,14 +312,17 @@ module Samagotchi
296
312
  next
297
313
  end
298
314
 
299
- unwrap = (->(call, layers) { unwrap(call, layers) } if spec.name == CALL_NAME)
300
- registry.register(spec.name, source: SOURCE, unwrap: unwrap, **spec.entry_fields(nil))
315
+ unwrap = (->(call, surface) { unwrap(call, surface) } if spec.name == CALL_NAME)
316
+ registry.register(spec.name, source: SOURCE, unwrap: unwrap,
317
+ **spec.entry_fields(nil).merge(own_fields(spec)))
301
318
  spec
302
319
  end
303
320
  registry.index = Registry::Index.new(source: SOURCE, call_name: CALL_NAME, groups: @declared_group_names,
304
- deferrable: -> { deferrable }, wrap: ->(call) { wrap(call) },
305
- unreached: ->(name) { unreached_tool(name) }, loaded: loaded)
306
- declared!(declared)
321
+ deferrable: ->(surface) { deferrable(surface) },
322
+ wrap: ->(call, surface) { wrap(call, surface) },
323
+ unreached: ->(name, surface) { unreached_tool(name, surface) },
324
+ loaded: loaded)
325
+ declared!(declared, ToolSurface.for(registry))
307
326
  declared.map(&:name)
308
327
  end
309
328
 
@@ -314,8 +333,9 @@ module Samagotchi
314
333
  entry = @registry && @registry[FIND_NAME]
315
334
  return unless entry&.source == SOURCE
316
335
 
317
- entry.schema = entry.schema.merge(description: find_description)
318
- declared!([find_spec, call_spec].select { |spec| @registry[spec.name]&.source == SOURCE })
336
+ surface = ToolSurface.for(@registry)
337
+ entry.schema = entry.schema.merge(description: find_description(surface))
338
+ declared!([find_spec(surface), call_spec].select { |spec| @registry[spec.name]&.source == SOURCE }, surface)
319
339
  end
320
340
 
321
341
  # A checked tool declaration's (Plugin::ToolSpec) definition as
@@ -340,10 +360,10 @@ module Samagotchi
340
360
  # server's: its name, and its summary if it has one). Never every
341
361
  # tool: knowing the names, a model searches less and guesses
342
362
  # arguments.
343
- def find_description
363
+ def find_description(surface)
344
364
  "Search tools this session doesn't declare, by keywords. It answers up to #{FIND_RESULTS} tools, each with " \
345
365
  "its name (<group>/<tool>) and inputSchema; call one with #{CALL_NAME}. An empty query lists every tool's " \
346
- "name.\nGroups:\n#{(description_lines(registry_groups).values + described_groups.values).join("\n")}"
366
+ "name.\nGroups:\n#{(description_lines(registry_groups(surface)).values + described_groups(surface).values).join("\n")}"
347
367
  end
348
368
 
349
369
  # ── Search ──────────────────────────────────────────────────────────
@@ -351,10 +371,10 @@ module Samagotchi
351
371
  # The search tool: the best matches with their schemas, or with an
352
372
  # empty query every group's tool names. +server+ is the parameter's
353
373
  # name before it was +group+: a model that still gives it is told
354
- # every group was searched.
374
+ # every group was searched. +surface+: the turn's (the handler's).
355
375
  # @return [FoundText]
356
- def find(query, only = nil, server: nil)
357
- answer = find_in(query, only)
376
+ def find(query, only = nil, surface:, server: nil)
377
+ answer = find_in(query, only, surface)
358
378
  return answer unless (only.nil? || only.strip.empty?) && !server.to_s.strip.empty?
359
379
 
360
380
  FoundText.new("(searched every group)\n#{answer}", answer.loaded)
@@ -366,11 +386,12 @@ module Samagotchi
366
386
  # run by its source. It fails closed: a tool that isn't the one
367
387
  # guardrails were given (#claimed) is refused, and so is one of the
368
388
  # index's own (a chi tool runs as itself, #unwrap, or not at all).
369
- def call(name, args)
370
- tool = lookup(name)
389
+ # +surface+: the turn's (the handler's).
390
+ def call(name, args, surface:)
391
+ tool = lookup(name, surface)
371
392
  return tool if tool.is_a?(String)
372
393
 
373
- unless claimed(name.strip)&.first == tool.acts_as
394
+ unless claimed(name.strip, surface)&.first == tool.acts_as
374
395
  # A bare acts_as of a source with no prefix: guardrails can't
375
396
  # name it before its tool is listed (#claimed), so only
376
397
  # <group>/<tool> is taken.
@@ -399,16 +420,16 @@ module Samagotchi
399
420
  # any registered built-in or plugin tool: one the toolkit removes or declares
400
421
  # is then refused by its name before the gate (ToolRunner#left_out?),
401
422
  # with no hook or card. Nil, so the call runs as call_tool, when the
402
- # turn under +layers+ doesn't offer call_tool, for an MCP tool, a miss
423
+ # turn's +surface+ doesn't declare call_tool, for an MCP tool, a miss
403
424
  # or arguments that aren't an object.
404
425
  # @return [Hash, nil]
405
- def unwrap(call, layers = [])
406
- return nil unless @registry&.offered(CALL_NAME, layers: layers)
426
+ def unwrap(call, surface)
427
+ return nil unless surface.declared_entry(CALL_NAME)
407
428
 
408
429
  args = call[:args].is_a?(Hash) ? call[:args] : {}
409
430
  name = args["tool"].to_s.strip
410
- tool = lookup_quietly(name)
411
- entry = tool&.entry || builtin_named(name)
431
+ tool = lookup_quietly(name, surface)
432
+ entry = tool&.entry || builtin_named(name, surface)
412
433
  return nil unless entry
413
434
 
414
435
  inner = inner_args(args["args"], tool)
@@ -421,13 +442,13 @@ module Samagotchi
421
442
  # registered tool's. Sorted by acts_as, so the same index gives the
422
443
  # same list in any session and after a respawn.
423
444
  # @return [Array<Tool>]
424
- def deferrable
425
- all = all_groups.reject { |group| own?(group) }.flat_map(&:tools)
426
- reached = reached_groups.reject { |group| own?(group) }.flat_map(&:tools)
445
+ def deferrable(surface)
446
+ all = all_groups(surface).reject { |group| own?(group) }.flat_map(&:tools)
447
+ reached = reached_groups(surface).reject { |group| own?(group) }.flat_map(&:tools)
427
448
  counts = all.map(&:acts_as).tally
428
449
  reached.select do |tool|
429
450
  tool.refusal.nil? && counts[tool.acts_as] == 1 && tool.acts_as.match?(DEFERRABLE_NAME) &&
430
- !@registry&.key?(tool.acts_as)
451
+ !surface.registered?(tool.acts_as)
431
452
  end.sort_by(&:acts_as)
432
453
  end
433
454
 
@@ -437,29 +458,29 @@ module Samagotchi
437
458
  # <group>/<tool>). Nil for any other: declared, removed, unknown, or a
438
459
  # source tool that can't be deferred. Never waits for a start.
439
460
  # @return [String, nil]
440
- def deferred_name(name)
461
+ def deferred_name(name, surface)
441
462
  name = name.to_s.strip
442
463
  bare = name.delete_prefix("#{CHI_GROUP}/")
443
- return bare if @registry&.indexed(bare)
464
+ return bare if surface.indexed_entry(bare)
444
465
 
445
- tool = lookup_quietly(name) or return nil
466
+ tool = lookup_quietly(name, surface) or return nil
446
467
  return nil if tool.entry || tool.refusal
447
468
 
448
- deferrable.any? { |t| t.acts_as == tool.acts_as } ? tool.acts_as : nil
469
+ deferrable(surface).any? { |t| t.acts_as == tool.acts_as } ? tool.acts_as : nil
449
470
  end
450
471
 
451
472
  # Finds a source tool regardless of the active toolkit, without
452
473
  # waiting for a starting group. Used only to refuse a call or explain
453
474
  # a deferred stub for a group the session cannot reach.
454
- def unreached_tool(name)
475
+ def unreached_tool(name, surface)
455
476
  name = name.to_s.strip
456
477
  return nil if name.empty?
457
478
 
458
- groups = all_groups.reject { |group| own?(group) }
479
+ groups = all_groups(surface).reject { |group| own?(group) }
459
480
  groups.select! { |group| @declared_group_names.include?(group.name) } if @declared_group_names
460
481
  tool = tool_in(name, groups)
461
482
  return nil unless tool
462
- return nil if reached_groups.any? { |group| group.name == tool.group }
483
+ return nil if reached_groups(surface).any? { |group| group.name == tool.group }
463
484
 
464
485
  tool
465
486
  end
@@ -473,18 +494,19 @@ module Samagotchi
473
494
  # loads tools): the call tool's row, rules and approvals. Nil for any
474
495
  # other call.
475
496
  # @return [Hash, nil]
476
- def wrap(call)
497
+ def wrap(call, surface)
477
498
  name = call[:name].to_s
478
- return nil if name.empty? || @registry&.key?(name)
499
+ return nil if name.empty? || surface.registered?(name)
479
500
 
480
- tool = deferrable.find { |t| t.acts_as == name } or return nil
501
+ tool = deferrable(surface).find { |t| t.acts_as == name } or return nil
481
502
  args = call[:args].is_a?(Hash) ? call[:args] : {}
482
503
  BuiltinCalls.build(CALL_NAME, { "tool" => tool.full_name, "args" => args })
483
504
  end
484
505
 
485
- # The call tool's activity row: <group>/<tool> and its arguments.
486
- def call_preview(args)
487
- tool = lookup_quietly(args["tool"].to_s)
506
+ # The call tool's activity row: <group>/<tool> and its arguments, as
507
+ # +surface+ (the turn's) reaches it.
508
+ def call_preview(args, surface)
509
+ tool = lookup_quietly(args["tool"].to_s, surface)
488
510
  name = tool ? tool.full_name : args["tool"].to_s.strip
489
511
  inner = tool && inner_args(args["args"], tool)
490
512
  inner ? [name, preview(inner)].reject(&:empty?).join(" ") : name
@@ -495,11 +517,11 @@ module Samagotchi
495
517
  # indexed yet (its server still starting) is named from what the
496
518
  # model gave (#claimed), so a rule on it fires before the call waits
497
519
  # for the server. Nothing for a name that can't be a tool (the call
498
- # refuses it).
499
- def call_targets(args)
520
+ # refuses it). +surface+: the turn's (the gate's).
521
+ def call_targets(args, surface)
500
522
  name = args["tool"].to_s.strip
501
- tool = lookup_quietly(name)
502
- acts_as, label = tool ? [tool.acts_as, "#{tool.group}: #{tool.name}"] : claimed(name)
523
+ tool = lookup_quietly(name, surface)
524
+ acts_as, label = tool ? [tool.acts_as, "#{tool.group}: #{tool.name}"] : claimed(name, surface)
503
525
  return {} unless acts_as
504
526
 
505
527
  { acts_as: acts_as, args: inner_args(args["args"], tool) || {}, label: label }.compact
@@ -507,20 +529,41 @@ module Samagotchi
507
529
 
508
530
  private
509
531
 
510
- def find_spec
511
- Plugin::ToolSpec.build(FIND_NAME, find_description, params: FIND_PARAMS,
532
+ # The fields +spec+'s tool registers with beside its entry_fields: a
533
+ # handler whose block is given the call's typed args and the turn's
534
+ # surface (its ToolContext's), and call_tool's preview and targets,
535
+ # given the turn's surface by the row and the gate
536
+ # (Registry::Entry#preview_of / #targets_of). Without a surface each
537
+ # raises MissingSurface: none reads the Registry as it is now.
538
+ def own_fields(spec)
539
+ parameters = spec.schema[:parameters]
540
+ args = ->(call) { Plugin::ToolSpec.args_of(call, parameters) }
541
+ fields = { handler: lambda { |call, kctx|
542
+ Plugin::ToolSpec.result_text(spec.block.call(args.call(call), kctx&.surface || raise(MissingSurface)))
543
+ } }
544
+ return fields unless spec.name == CALL_NAME
545
+
546
+ fields.merge(preview: ->(call, surface = nil) { call_preview(args.call(call), surface || raise(MissingSurface)) },
547
+ targets: ->(call, surface = nil) { call_targets(args.call(call), surface || raise(MissingSurface)) })
548
+ end
549
+
550
+ # The search tool's declaration, its description read from +surface+
551
+ # (the Registry's now: #declare, #refresh!); its block searches the
552
+ # turn's (own_fields' handler gives it).
553
+ def find_spec(surface)
554
+ Plugin::ToolSpec.build(FIND_NAME, find_description(surface), params: FIND_PARAMS,
512
555
  preview: lambda { |args|
513
556
  [args["query"].to_s.strip, args["group"] && "in #{args["group"]}"].compact.join(" ")
514
- }) do |args, _ctx|
515
- find(args["query"].to_s, args["group"], server: args["server"])
557
+ }) do |args, turn|
558
+ find(args["query"].to_s, args["group"], server: args["server"], surface: turn)
516
559
  end
517
560
  end
518
561
 
562
+ # The call tool's declaration; its preview and targets are own_fields'.
519
563
  def call_spec
520
- Plugin::ToolSpec.build(CALL_NAME, CALL_DESCRIPTION, params: CALL_PARAMS, label: @sources.first&.label,
521
- preview: ->(args) { call_preview(args) },
522
- targets: ->(args) { call_targets(args) }) do |args, _ctx|
523
- call(args["tool"].to_s, args["args"])
564
+ Plugin::ToolSpec.build(CALL_NAME, CALL_DESCRIPTION, params: CALL_PARAMS,
565
+ label: @sources.first&.label) do |args, turn|
566
+ call(args["tool"].to_s, args["args"], surface: turn)
524
567
  end
525
568
  end
526
569
 
@@ -532,8 +575,8 @@ module Samagotchi
532
575
  @sources.find { |source| source.groups.any? { |g| g.name == group } }
533
576
  end
534
577
 
535
- def find_in(query, only)
536
- all = reached_groups
578
+ def find_in(query, only, surface)
579
+ all = reached_groups(surface)
537
580
  searched = all
538
581
  if only && !only.strip.empty?
539
582
  searched = all.select { |group| group.name == only.strip }
@@ -547,62 +590,63 @@ module Samagotchi
547
590
  blocks = matches.map { |tool| match_text(tool, all) }
548
591
  unsearched = searched.filter_map { |group| "#{group.name} #{group.note}" if group.tools.empty? && group.note }
549
592
  blocks << "Not searched: #{unsearched.join("; ")}." unless unsearched.empty?
550
- FoundText.new("#{CALL_HINT}\n\n#{blocks.join("\n\n")}",
551
- loadable(matches))
593
+ FoundText.new("#{IndexHint::CALL_HINT}\n\n#{blocks.join("\n\n")}",
594
+ loadable(matches, surface))
552
595
  end
553
596
 
554
597
  # The wire names of +matches+ a request can declare deferred: a
555
598
  # chi tool of the index's own groups by its name, a source's tool by
556
599
  # its acts_as when it is #deferrable.
557
- def loadable(matches)
600
+ def loadable(matches, surface)
558
601
  sourced = nil
559
602
  matches.filter_map do |tool|
560
603
  next tool.name if tool.entry
561
604
  next if tool.refusal
562
605
 
563
- sourced ||= deferrable.map(&:acts_as)
606
+ sourced ||= deferrable(surface).map(&:acts_as)
564
607
  tool.acts_as if sourced.include?(tool.acts_as)
565
608
  end
566
609
  end
567
610
 
568
- # What the sources hear of the declared tools (/mcp's last line).
569
- def declared!(specs)
611
+ # What the sources hear of the declared tools (/mcp's last line), the
612
+ # groups +surface+ (the Registry's now) reaches.
613
+ def declared!(specs, surface)
570
614
  names = specs.map(&:name)
571
615
  tokens = specs.sum { |spec| ToolIndex.spec_tokens(spec) }
572
- groups = reached_groups.reject { |group| own?(group) }.map(&:name)
616
+ groups = reached_groups(surface).reject { |group| own?(group) }.map(&:name)
573
617
  @sources.each { |source| source.declared!(names, tokens, groups) }
574
618
  end
575
619
 
576
- def described_groups
577
- return @described unless @registry&.toolkit && @registry.toolkit.groups != :all
620
+ def described_groups(surface)
621
+ toolkit = surface.toolkit
622
+ return @described unless toolkit && toolkit.groups != :all
578
623
 
579
- @described.select { |name, _line| @registry.toolkit.reaches_group?(name) }
624
+ @described.select { |name, _line| toolkit.reaches_group?(name) }
580
625
  end
581
626
 
582
627
  def own?(group) = RESERVED_GROUPS.include?(group.name)
583
628
 
584
629
  # The registered chi tool chi/<name> names (a built-in or a plugin's,
585
630
  # never the index's own), reachable or not.
586
- def builtin_named(name)
631
+ def builtin_named(name, surface)
587
632
  return nil unless name.start_with?("#{CHI_GROUP}/")
588
633
 
589
- entry = @registry[name.delete_prefix("#{CHI_GROUP}/")]
634
+ entry = surface.registered[name.delete_prefix("#{CHI_GROUP}/")]
590
635
  entry unless entry.nil? || entry.source == SOURCE
591
636
  end
592
637
 
593
638
  # The index's own groups: chi, the tools (built-ins and plugin tools)
594
- # the Registry's toolkit leaves out but reaches (none: no group). Each tool's schema is its
595
- # registered entry's (a config's execute description included), and
596
- # it acts as itself.
639
+ # +surface+'s toolkit leaves out but reaches (ToolSurface#index_only;
640
+ # none: no group). Each tool's schema is its registered entry's (a
641
+ # config's execute description included), and it acts as itself.
597
642
  # @return [Array<Group>]
598
- def registry_groups
599
- names = @registry ? @registry.reachable_names : []
600
- return [] if names.empty?
643
+ def registry_groups(surface)
644
+ entries = surface.index_only
645
+ return [] if entries.empty?
601
646
 
602
- tools = names.map do |name|
603
- entry = @registry[name]
647
+ tools = entries.map do |entry|
604
648
  schema = ToolDeclarations.chat_schemas([entry.schema]).first
605
- Tool.new(group: CHI_GROUP, name: name, acts_as: name, description: schema[:description].to_s,
649
+ Tool.new(group: CHI_GROUP, name: entry.name, acts_as: entry.name, description: schema[:description].to_s,
606
650
  schema: JSON.parse(JSON.generate(schema[:parameters])), entry: entry)
607
651
  end
608
652
  [Group.new(name: CHI_GROUP, summary: CHI_SUMMARY, tools: tools)]
@@ -687,9 +731,9 @@ module Samagotchi
687
731
  # miss is the error text: a failed group's error, else the closest
688
732
  # tools.
689
733
  # @return [Tool, String]
690
- def lookup(name, waited: false)
734
+ def lookup(name, surface, waited: false)
691
735
  name = name.strip
692
- all = reached_groups
736
+ all = reached_groups(surface)
693
737
  tool = all.lazy.filter_map do |group|
694
738
  next unless name.start_with?("#{group.name}/")
695
739
 
@@ -703,17 +747,21 @@ module Samagotchi
703
747
  return "Error: #{name} names #{same.map(&:full_name).join(" and ")}; call one as <group>/<tool>" if same.size > 1
704
748
 
705
749
  # A chi tool the session declares (a built-in or a plugin's) is no
706
- # starting server's tool.
707
- declared = @registry&.offered(name)
708
- return "Error: #{name} is declared; call it directly" if declared && declared.source != SOURCE
750
+ # starting server's tool. A layer's tool (forget_outputs) isn't
751
+ # counted, as before the surface (the Registry was asked without
752
+ # the turn's layers).
753
+ declared = surface.declared_entry(name)
754
+ if declared && declared.source != SOURCE && declared.layer.nil?
755
+ return "Error: #{name} is declared; call it directly"
756
+ end
709
757
 
710
758
  # A name with a group's <group>/ waits for that group only; any
711
759
  # other name (an acts_as) for every one starting.
712
- named = reached_groups.select { |group| name.start_with?("#{group.name}/") }
713
- starting = (named.empty? ? reached_groups : named).select { |group| group.state == :starting }
760
+ named = reached_groups(surface).select { |group| name.start_with?("#{group.name}/") }
761
+ starting = (named.empty? ? reached_groups(surface) : named).select { |group| group.state == :starting }
714
762
  unless waited || starting.empty?
715
763
  starting.each { |group| source_of(group.name)&.wait(group.name) }
716
- return lookup(name, waited: true)
764
+ return lookup(name, surface, waited: true)
717
765
  end
718
766
 
719
767
  failed = all.find { |group| group.state == :failed && name.start_with?("#{group.name}/") }
@@ -726,8 +774,8 @@ module Samagotchi
726
774
  end
727
775
 
728
776
  # #lookup's tool, without waiting for a start: nil on a miss.
729
- def lookup_quietly(name)
730
- tool = lookup(name, waited: true)
777
+ def lookup_quietly(name, surface)
778
+ tool = lookup(name, surface, waited: true)
731
779
  tool.is_a?(Tool) ? tool : nil
732
780
  end
733
781
 
@@ -750,8 +798,8 @@ module Samagotchi
750
798
  # bare name with a source's prefix as it is (no label). Nil for
751
799
  # anything else.
752
800
  # @return [Array(String, String), Array(String, nil), nil]
753
- def claimed(name)
754
- all = all_groups
801
+ def claimed(name, surface)
802
+ all = all_groups(surface)
755
803
  if (own = all.select { |g| own?(g) }.find { |g| g.tools.any? { |t| t.acts_as == name } })
756
804
  return [name, "#{own.name}: #{name}"]
757
805
  end