samagotchi 0.154.0 → 0.155.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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -1
  3. data/docs/architecture.md +3 -2
  4. data/docs/bundles.md +21 -0
  5. data/docs/cli.md +10 -8
  6. data/docs/guardrails.md +2 -2
  7. data/docs/hooks.md +1 -1
  8. data/docs/internals/anthropic.md +4 -4
  9. data/docs/internals/attached-context.md +8 -6
  10. data/docs/internals/block-renderers.md +8 -1
  11. data/docs/internals/bridge.md +18 -5
  12. data/docs/internals/engine.md +221 -100
  13. data/docs/internals/inline-widgets.md +136 -0
  14. data/docs/internals/objectives.md +8 -6
  15. data/docs/internals/participants.md +5 -5
  16. data/docs/internals/prompt-caching.md +12 -6
  17. data/docs/internals/web-frontend.md +2 -2
  18. data/docs/internals/working-set.md +2 -1
  19. data/docs/plugins.md +2 -0
  20. data/docs/releasing.md +1 -1
  21. data/docs/sessions.md +3 -3
  22. data/docs/testing.md +16 -1
  23. data/lib/samagotchi/answer_text.rb +1 -2
  24. data/lib/samagotchi/block_renderers.rb +5 -0
  25. data/lib/samagotchi/bridge/card_places.rb +56 -7
  26. data/lib/samagotchi/bridge/card_store.rb +24 -9
  27. data/lib/samagotchi/bridge.rb +9 -0
  28. data/lib/samagotchi/bridge_client.rb +9 -5
  29. data/lib/samagotchi/bundle_command/check.rb +1 -1
  30. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  31. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
  33. data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
  34. data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
  35. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  36. data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
  37. data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
  38. data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
  39. data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
  40. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  41. data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
  42. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  43. data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
  44. data/lib/samagotchi/engine/context_judge.rb +19 -125
  45. data/lib/samagotchi/engine/guardrails.rb +4 -2
  46. data/lib/samagotchi/engine/messages.rb +9 -6
  47. data/lib/samagotchi/engine/objectives.rb +22 -90
  48. data/lib/samagotchi/engine/participants.rb +15 -10
  49. data/lib/samagotchi/engine/plugins.rb +3 -1
  50. data/lib/samagotchi/engine/questions.rb +1 -1
  51. data/lib/samagotchi/engine/side_requests.rb +5 -2
  52. data/lib/samagotchi/engine/toolkit.rb +33 -190
  53. data/lib/samagotchi/engine/turn_end.rb +1 -1
  54. data/lib/samagotchi/engine/turn_helpers.rb +16 -39
  55. data/lib/samagotchi/engine/verify.rb +7 -143
  56. data/lib/samagotchi/engine.rb +162 -99
  57. data/lib/samagotchi/engine_peers.rb +1 -1
  58. data/lib/samagotchi/engine_snapshots.rb +9 -8
  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 +6 -4
  65. data/lib/samagotchi/guardrails_command.rb +1 -1
  66. data/lib/samagotchi/idle_recap.rb +3 -1
  67. data/lib/samagotchi/inline_widgets.rb +325 -0
  68. data/lib/samagotchi/kernel_loop.rb +194 -216
  69. data/lib/samagotchi/llm/backend.rb +3 -1
  70. data/lib/samagotchi/llm/chat_loop.rb +66 -42
  71. data/lib/samagotchi/llm/native_backend.rb +3 -2
  72. data/lib/samagotchi/llm_context_forget.rb +5 -3
  73. data/lib/samagotchi/model_binding.rb +3 -3
  74. data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
  75. data/lib/samagotchi/question_desk.rb +1 -1
  76. data/lib/samagotchi/revise_anchor.rb +1 -1
  77. data/lib/samagotchi/session_manager.rb +8 -13
  78. data/lib/samagotchi/session_retention.rb +24 -4
  79. data/lib/samagotchi/steer.rb +12 -1
  80. data/lib/samagotchi/stream_event.rb +22 -0
  81. data/lib/samagotchi/system_prompt.rb +84 -61
  82. data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
  83. data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
  84. data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
  85. data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
  86. data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
  87. data/lib/samagotchi/testing/boundary.rb +7 -0
  88. data/lib/samagotchi/testing/bundle.rb +7 -5
  89. data/lib/samagotchi/testing/install.rb +3 -2
  90. data/lib/samagotchi/tool_activity.rb +8 -6
  91. data/lib/samagotchi/tool_declarations.rb +4 -4
  92. data/lib/samagotchi/tool_response.rb +4 -3
  93. data/lib/samagotchi/tool_runner.rb +93 -101
  94. data/lib/samagotchi/toolkit_binding.rb +40 -14
  95. data/lib/samagotchi/tools/deferred_tools.rb +17 -21
  96. data/lib/samagotchi/tools/registry.rb +43 -40
  97. data/lib/samagotchi/tools/tool_index.rb +135 -88
  98. data/lib/samagotchi/tools/tool_surface.rb +173 -0
  99. data/lib/samagotchi/turn_context.rb +67 -0
  100. data/lib/samagotchi/turn_flow.rb +7 -2
  101. data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
  102. data/lib/samagotchi/turn_stages/objectives.rb +146 -0
  103. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
  104. data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
  105. data/lib/samagotchi/turn_stages/warmup.rb +83 -0
  106. data/lib/samagotchi/turn_state.rb +46 -4
  107. data/lib/samagotchi/version.rb +1 -1
  108. data/lib/samagotchi/web/app.rb +3 -1
  109. data/lib/samagotchi/web/markdown_renderer.rb +39 -14
  110. data/lib/samagotchi/web/message_display.rb +8 -1
  111. data/lib/samagotchi/web/public/app.js +5 -1
  112. data/lib/samagotchi/web/public/block_host.js +74 -199
  113. data/lib/samagotchi/web/public/block_renderers.css +2 -0
  114. data/lib/samagotchi/web/public/block_source.js +7 -4
  115. data/lib/samagotchi/web/public/block_watch.js +256 -0
  116. data/lib/samagotchi/web/public/card_stamps.js +27 -0
  117. data/lib/samagotchi/web/public/cards_place.js +5 -3
  118. data/lib/samagotchi/web/public/composer_fold.js +3 -0
  119. data/lib/samagotchi/web/public/composer_mode.js +1 -1
  120. data/lib/samagotchi/web/public/format.js +3 -2
  121. data/lib/samagotchi/web/public/index.html +28 -9
  122. data/lib/samagotchi/web/public/inline_widgets.js +146 -0
  123. data/lib/samagotchi/web/public/owner_verify.js +1 -1
  124. data/lib/samagotchi/web/public/participant.js +1 -1
  125. data/lib/samagotchi/web/public/revise_stack.js +1 -1
  126. data/lib/samagotchi/web/public/stage_watch.js +6 -2
  127. data/lib/samagotchi/web/public/text_roots.js +1 -1
  128. data/lib/samagotchi/web/public/turn_html.js +8 -4
  129. data/lib/samagotchi/web/public/verify_target.js +1 -1
  130. data/lib/samagotchi/web/public/widget_picks.js +130 -0
  131. data/lib/samagotchi/web/public/widget_walk.js +230 -0
  132. data/lib/samagotchi/web/public/widgets.css +70 -0
  133. data/lib/samagotchi/web/session_access_routes.rb +8 -0
  134. data/lib/samagotchi/web/session_show.rb +4 -0
  135. data/lib/samagotchi/worker.rb +8 -5
  136. data/lib/samagotchi.rb +0 -1
  137. metadata +19 -2
  138. data/lib/samagotchi/pending_input_queue.rb +0 -55
@@ -7,9 +7,10 @@ module Samagotchi
7
7
  # by bundle and name. That order doesn't depend on which plugin
8
8
  # finished its init first or re-registered a changed tool, so the
9
9
  # tool list (part of the prompt's cached prefix) is the same in every
10
- # session with the same tools. The
11
- # native prompts and the chat path's tools: are rendered from #schemas,
12
- # and KernelLoop#dispatch runs a call through its entry's handler.
10
+ # session with the same tools. Its readers (the prompt, a turn's
11
+ # loops, the tool index, /toolkit) read the Tools::ToolSurface built
12
+ # from its lists (#entries, #reachable_names, #left_out_names,
13
+ # #removed_names, #stranded_names) under a toolkit and layers.
13
14
  class Registry
14
15
  # @!attribute name [String]
15
16
  # @!attribute schema [Hash] {name:, description:, parameters:}, as in
@@ -26,14 +27,29 @@ module Samagotchi
26
27
  # @!attribute layer [Symbol, nil] the LLM context layer the tool
27
28
  # belongs to (LLMContextStrategy): offered only in a turn running
28
29
  # under it (forget_outputs, :forget); nil: always
29
- # @!attribute unwrap [#call, nil] (call, layers) → the call it stands
30
- # for, run in its place before the guardrail gate (call_tool on one
31
- # of chi's own tools: Tools::ToolIndex#unwrap), or nil to run it as
32
- # it is; nil on every other tool
30
+ # @!attribute unwrap [#call, nil] (call, surface) → the call it stands
31
+ # for under the turn's Tools::ToolSurface, run in its place before
32
+ # the guardrail gate (call_tool on one of chi's own tools:
33
+ # Tools::ToolIndex#unwrap), or nil to run it as it is; nil on every
34
+ # other tool
33
35
  Entry = Struct.new(:name, :schema, :handler, :label, :preview, :targets, :refs, :source, :layer, :unwrap,
34
36
  keyword_init: true) do
35
37
  def core? = source == "core"
36
38
 
39
+ # Its preview on +call+ (the row's params), nil without one.
40
+ # +surface+, the turn's Tools::ToolSurface (nil: none), goes to a
41
+ # preview that takes a second argument (call_tool's: what the turn
42
+ # reaches); a plugin's takes the call only.
43
+ def preview_of(call, surface = nil) = preview && Entry.per_call(preview, call, surface)
44
+
45
+ # Its targets on +call+ (what guardrail rules match), nil without
46
+ # any; +surface+ as #preview_of.
47
+ def targets_of(call, surface = nil) = targets && Entry.per_call(targets, call, surface)
48
+
49
+ def self.per_call(callable, call, surface)
50
+ callable.arity == 1 ? callable.call(call) : callable.call(call, surface)
51
+ end
52
+
37
53
  # Offered in a turn under +layers+ (the turn's LLM context layers).
38
54
  def offered?(layers) = layer.nil? || Array(layers).include?(layer)
39
55
  end
@@ -43,17 +59,19 @@ module Samagotchi
43
59
  # name, and +groups+ the source groups registered when the plugins
44
60
  # loaded; +loaded+ the entries registered then (nil: whatever is
45
61
  # registered now), which alone decide whether the index's tools are
46
- # needed (Registry#index_needed?). +deferrable+ (→ the sources'
47
- # tools a host that loads tools may declare deferred) and +wrap+
48
- # (call → the call_tool call a direct call of one of them runs as, or
49
- # nil) are nil when nothing reaches them.
62
+ # needed (Registry#index_needed?). +deferrable+ (surface → the
63
+ # sources' tools a host that loads tools may declare deferred),
64
+ # +wrap+ ((call, surface) → the call_tool call a direct call of one
65
+ # of them runs as, or nil) and +unreached+ ((name, surface) → a
66
+ # source tool the toolkit doesn't reach) are nil when nothing reaches
67
+ # them; each is given the turn's Tools::ToolSurface.
50
68
  Index = Data.define(:source, :call_name, :groups, :deferrable, :wrap, :unreached, :loaded) do
51
69
  def initialize(source:, call_name:, groups:, deferrable: nil, wrap: nil, unreached: nil, loaded: nil)
52
70
  super(source: source, call_name: call_name, groups: Array(groups).map(&:to_s).uniq.freeze,
53
71
  deferrable: deferrable, wrap: wrap, unreached: unreached, loaded: loaded&.dup&.freeze)
54
72
  end
55
73
 
56
- def unreached_tool(name) = unreached&.call(name)
74
+ def unreached_tool(name, surface) = unreached&.call(name, surface)
57
75
  end
58
76
 
59
77
  def initialize
@@ -66,7 +84,7 @@ module Samagotchi
66
84
  attr_accessor :index
67
85
 
68
86
  # The session's resolved toolkit (Toolkit::Resolved), which the lists
69
- # below and #offered filter by; nil declares every tool (full). The
87
+ # below filter by; nil declares every tool (full). The
70
88
  # Engine sets it at a turn's start, before the prompt is built, and
71
89
  # only there: a request's declared tools never change mid-turn.
72
90
  attr_accessor :toolkit
@@ -94,22 +112,6 @@ module Samagotchi
94
112
 
95
113
  def key?(name) = @entries.key?(name.to_s)
96
114
 
97
- # The entry +name+ names when a turn under +layers+ offers it (the
98
- # one a call dispatches to).
99
- # @return [Entry, nil]
100
- def offered(name, layers: [])
101
- entry = self[name]
102
- entry if entry&.offered?(layers) && declared?(entry)
103
- end
104
-
105
- # The entry +name+ names when the session's toolkit leaves it out (a
106
- # real tool the model can't call here, by its name), nil otherwise.
107
- # @return [Entry, nil]
108
- def left_out(name, layers: [])
109
- entry = self[name]
110
- entry if entry && left_out?(entry, layers, @toolkit)
111
- end
112
-
113
115
  # The tools +toolkit+ (the session's by default) leaves out of a turn
114
116
  # under +layers+. The tool index's own aren't, unless it removes them:
115
117
  # they are declared when there is something to reach.
@@ -118,14 +120,6 @@ module Samagotchi
118
120
  ordered(@entries.values.select { |entry| left_out?(entry, layers, toolkit) }).map(&:name)
119
121
  end
120
122
 
121
- # The tool +name+ names (a built-in or a plugin's) when the session's
122
- # toolkit leaves it out but the tool index reaches it (#reachable_names).
123
- # @return [Entry, nil]
124
- def indexed(name, layers: [], toolkit: @toolkit)
125
- entry = self[name]
126
- entry if entry && call_offered?(toolkit) && soft?(entry, layers, toolkit)
127
- end
128
-
129
123
  # The tools +toolkit+ leaves out of a turn under +layers+ that the tool
130
124
  # index reaches (call_tool), built-ins and plugin tools alike: not in
131
125
  # its kit's list, but not removed either (remove:, --no-tool, /toolkit
@@ -184,14 +178,23 @@ module Samagotchi
184
178
  ordered(@entries.values.select { |entry| entry.offered?(layers) && declared?(entry, toolkit) })
185
179
  end
186
180
 
187
- # @return [Array<Hash>] the schemas, in declaration order
188
- def schemas(layers: []) = entries(layers: layers).map(&:schema)
189
-
190
181
  def freeze
191
182
  @entries.freeze
192
183
  super
193
184
  end
194
185
 
186
+ # A frozen copy of its entry set, with its toolkit, reaching_kit and
187
+ # index as they are now (ToolkitBinding#publish!, on the turn thread):
188
+ # registering or unregistering here leaves it as it was. Shallow: the
189
+ # Entry Structs are this Registry's own, their fields set in place on
190
+ # the turn thread (Plugin::ToolSpec.apply, ToolIndex#refresh!).
191
+ # @return [Registry]
192
+ def snapshot
193
+ copy = dup
194
+ copy.instance_variable_set(:@entries, @entries.dup)
195
+ copy.freeze
196
+ end
197
+
195
198
  private
196
199
 
197
200
  # Declared by +toolkit+ (nil: every tool, full). The tool index's
@@ -11,6 +11,10 @@ require_relative "../plugin/tool_spec"
11
11
 
12
12
  module Samagotchi
13
13
  module Tools
14
+ # Read only when a reader asks (#live_surface, a handler): the bundle
15
+ # test kit loads this file for ToolIndex.new and never builds one.
16
+ autoload :ToolSurface, File.expand_path("tool_surface", __dir__)
17
+
14
18
  # The tools a session knows but doesn't declare one by one, and the two
15
19
  # tools that reach them (docs/plugins.md, "Publishing tools to the
16
20
  # index"): find_tools, a keyword search that answers each match's
@@ -18,11 +22,18 @@ module Samagotchi
18
22
  # A source (the mcp bundle, chi.tool_source) publishes its groups (an
19
23
  # MCP server each) and runs the calls; the index's own group, chi, is
20
24
  # 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),
25
+ # built-ins and its bundles' (chi.tool) alike (ToolSurface#index_only),
22
26
  # and a call_tool of one runs as that tool (#unwrap, before the
23
27
  # guardrail gate). The index searches the
24
28
  # groups, looks a call's tool up and checks it.
25
29
  #
30
+ # What it reads of the session's tools is a ToolSurface: the turn's
31
+ # (TurnContext#surface) where the caller has one (the two tools'
32
+ # handlers, through their ToolContext; #unwrap, #wrap, #deferrable and
33
+ # #unreached_tool, given it by the runner and the deferred list), else
34
+ # the Registry #declare registered in, read now (#live_surface: at
35
+ # load, a turn's start, a guardrail rule's targets, ctx.load_tools).
36
+ #
26
37
  # The two tools are registered once, after the plugins load, and
27
38
  # declared per toolkit (Registry#declared?): when a source registered
28
39
  # then, or the toolkit leaves a tool reachable. The search tool's
@@ -70,10 +81,11 @@ module Samagotchi
70
81
  class ToolkitGroupRefusal < StandardError
71
82
  RULE = "toolkit-group"
72
83
 
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)
84
+ # The refusal of +tool+ under +surface+'s toolkit (a ToolSurface),
85
+ # naming a kit that reaches its group when it can tell
86
+ # (ToolSurface#reaching_kit).
87
+ def self.for(tool, surface)
88
+ new(tool, surface.toolkit, kit: surface.reaching_kit&.call(tool.group))
77
89
  end
78
90
 
79
91
  def initialize(tool, toolkit, kit: nil)
@@ -248,12 +260,12 @@ module Samagotchi
248
260
  # @return [Array<Source>] the sources added, in order
249
261
  def sources = @sources.dup
250
262
 
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
263
+ # Every group, now: the index's own (#registry_groups, from
264
+ # +surface+), then the sources'. A source group with a reserved name,
265
+ # or one an earlier group has, is left out of search, lookup and the
266
+ # description (said once per name: a source publishes after load too).
267
+ def all_groups(surface = live_surface)
268
+ own = registry_groups(surface)
257
269
  taken = own.map(&:name) | RESERVED_GROUPS
258
270
  kept = @sources.flat_map(&:groups).select do |group|
259
271
  why = if RESERVED_GROUPS.include?(group.name) then "reserved"
@@ -266,9 +278,11 @@ module Samagotchi
266
278
  own + kept
267
279
  end
268
280
 
269
- def reached_groups
270
- groups = all_groups
271
- toolkit = @registry&.toolkit
281
+ # The groups +surface+'s toolkit reaches (#all_groups' own, and the
282
+ # source groups #declare found that it reaches).
283
+ def reached_groups(surface = live_surface)
284
+ groups = all_groups(surface)
285
+ toolkit = surface.toolkit
272
286
  return groups if toolkit.nil? || toolkit.groups == :all
273
287
 
274
288
  declared = @declared_group_names || groups.reject { |group| own?(group) }.map(&:name)
@@ -285,7 +299,7 @@ module Samagotchi
285
299
  # @return [Array<String>] the names registered
286
300
  def declare(registry, failures: nil)
287
301
  @registry = registry
288
- @described = description_lines(all_groups.reject { |group| own?(group) }).freeze
302
+ @described = description_lines(all_groups(live_surface).reject { |group| own?(group) }).freeze
289
303
  @declared_group_names = @described.keys.freeze
290
304
  loaded = registry.registered
291
305
  specs = [find_spec, call_spec]
@@ -296,13 +310,16 @@ module Samagotchi
296
310
  next
297
311
  end
298
312
 
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))
313
+ unwrap = (->(call, surface) { unwrap(call, surface) } if spec.name == CALL_NAME)
314
+ registry.register(spec.name, source: SOURCE, unwrap: unwrap,
315
+ **spec.entry_fields(nil).merge(own_fields(spec)))
301
316
  spec
302
317
  end
303
318
  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)
319
+ deferrable: ->(surface) { deferrable(surface) },
320
+ wrap: ->(call, surface) { wrap(call, surface) },
321
+ unreached: ->(name, surface) { unreached_tool(name, surface) },
322
+ loaded: loaded)
306
323
  declared!(declared)
307
324
  declared.map(&:name)
308
325
  end
@@ -340,10 +357,10 @@ module Samagotchi
340
357
  # server's: its name, and its summary if it has one). Never every
341
358
  # tool: knowing the names, a model searches less and guesses
342
359
  # arguments.
343
- def find_description
360
+ def find_description(surface = live_surface)
344
361
  "Search tools this session doesn't declare, by keywords. It answers up to #{FIND_RESULTS} tools, each with " \
345
362
  "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")}"
363
+ "name.\nGroups:\n#{(description_lines(registry_groups(surface)).values + described_groups(surface).values).join("\n")}"
347
364
  end
348
365
 
349
366
  # ── Search ──────────────────────────────────────────────────────────
@@ -351,10 +368,10 @@ module Samagotchi
351
368
  # The search tool: the best matches with their schemas, or with an
352
369
  # empty query every group's tool names. +server+ is the parameter's
353
370
  # name before it was +group+: a model that still gives it is told
354
- # every group was searched.
371
+ # every group was searched. +surface+: the turn's (the handler's).
355
372
  # @return [FoundText]
356
- def find(query, only = nil, server: nil)
357
- answer = find_in(query, only)
373
+ def find(query, only = nil, server: nil, surface: live_surface)
374
+ answer = find_in(query, only, surface)
358
375
  return answer unless (only.nil? || only.strip.empty?) && !server.to_s.strip.empty?
359
376
 
360
377
  FoundText.new("(searched every group)\n#{answer}", answer.loaded)
@@ -366,11 +383,12 @@ module Samagotchi
366
383
  # run by its source. It fails closed: a tool that isn't the one
367
384
  # guardrails were given (#claimed) is refused, and so is one of the
368
385
  # index's own (a chi tool runs as itself, #unwrap, or not at all).
369
- def call(name, args)
370
- tool = lookup(name)
386
+ # +surface+: the turn's (the handler's).
387
+ def call(name, args, surface: live_surface)
388
+ tool = lookup(name, surface)
371
389
  return tool if tool.is_a?(String)
372
390
 
373
- unless claimed(name.strip)&.first == tool.acts_as
391
+ unless claimed(name.strip, surface)&.first == tool.acts_as
374
392
  # A bare acts_as of a source with no prefix: guardrails can't
375
393
  # name it before its tool is listed (#claimed), so only
376
394
  # <group>/<tool> is taken.
@@ -399,16 +417,16 @@ module Samagotchi
399
417
  # any registered built-in or plugin tool: one the toolkit removes or declares
400
418
  # is then refused by its name before the gate (ToolRunner#left_out?),
401
419
  # 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
420
+ # turn's +surface+ doesn't declare call_tool, for an MCP tool, a miss
403
421
  # or arguments that aren't an object.
404
422
  # @return [Hash, nil]
405
- def unwrap(call, layers = [])
406
- return nil unless @registry&.offered(CALL_NAME, layers: layers)
423
+ def unwrap(call, surface = live_surface)
424
+ return nil unless surface.declared_entry(CALL_NAME)
407
425
 
408
426
  args = call[:args].is_a?(Hash) ? call[:args] : {}
409
427
  name = args["tool"].to_s.strip
410
- tool = lookup_quietly(name)
411
- entry = tool&.entry || builtin_named(name)
428
+ tool = lookup_quietly(name, surface)
429
+ entry = tool&.entry || builtin_named(name, surface)
412
430
  return nil unless entry
413
431
 
414
432
  inner = inner_args(args["args"], tool)
@@ -421,13 +439,13 @@ module Samagotchi
421
439
  # registered tool's. Sorted by acts_as, so the same index gives the
422
440
  # same list in any session and after a respawn.
423
441
  # @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)
442
+ def deferrable(surface = live_surface)
443
+ all = all_groups(surface).reject { |group| own?(group) }.flat_map(&:tools)
444
+ reached = reached_groups(surface).reject { |group| own?(group) }.flat_map(&:tools)
427
445
  counts = all.map(&:acts_as).tally
428
446
  reached.select do |tool|
429
447
  tool.refusal.nil? && counts[tool.acts_as] == 1 && tool.acts_as.match?(DEFERRABLE_NAME) &&
430
- !@registry&.key?(tool.acts_as)
448
+ !surface.registered?(tool.acts_as)
431
449
  end.sort_by(&:acts_as)
432
450
  end
433
451
 
@@ -437,29 +455,29 @@ module Samagotchi
437
455
  # <group>/<tool>). Nil for any other: declared, removed, unknown, or a
438
456
  # source tool that can't be deferred. Never waits for a start.
439
457
  # @return [String, nil]
440
- def deferred_name(name)
458
+ def deferred_name(name, surface = live_surface)
441
459
  name = name.to_s.strip
442
460
  bare = name.delete_prefix("#{CHI_GROUP}/")
443
- return bare if @registry&.indexed(bare)
461
+ return bare if surface.indexed_entry(bare)
444
462
 
445
- tool = lookup_quietly(name) or return nil
463
+ tool = lookup_quietly(name, surface) or return nil
446
464
  return nil if tool.entry || tool.refusal
447
465
 
448
- deferrable.any? { |t| t.acts_as == tool.acts_as } ? tool.acts_as : nil
466
+ deferrable(surface).any? { |t| t.acts_as == tool.acts_as } ? tool.acts_as : nil
449
467
  end
450
468
 
451
469
  # Finds a source tool regardless of the active toolkit, without
452
470
  # waiting for a starting group. Used only to refuse a call or explain
453
471
  # a deferred stub for a group the session cannot reach.
454
- def unreached_tool(name)
472
+ def unreached_tool(name, surface = live_surface)
455
473
  name = name.to_s.strip
456
474
  return nil if name.empty?
457
475
 
458
- groups = all_groups.reject { |group| own?(group) }
476
+ groups = all_groups(surface).reject { |group| own?(group) }
459
477
  groups.select! { |group| @declared_group_names.include?(group.name) } if @declared_group_names
460
478
  tool = tool_in(name, groups)
461
479
  return nil unless tool
462
- return nil if reached_groups.any? { |group| group.name == tool.group }
480
+ return nil if reached_groups(surface).any? { |group| group.name == tool.group }
463
481
 
464
482
  tool
465
483
  end
@@ -473,18 +491,19 @@ module Samagotchi
473
491
  # loads tools): the call tool's row, rules and approvals. Nil for any
474
492
  # other call.
475
493
  # @return [Hash, nil]
476
- def wrap(call)
494
+ def wrap(call, surface = live_surface)
477
495
  name = call[:name].to_s
478
- return nil if name.empty? || @registry&.key?(name)
496
+ return nil if name.empty? || surface.registered?(name)
479
497
 
480
- tool = deferrable.find { |t| t.acts_as == name } or return nil
498
+ tool = deferrable(surface).find { |t| t.acts_as == name } or return nil
481
499
  args = call[:args].is_a?(Hash) ? call[:args] : {}
482
500
  BuiltinCalls.build(CALL_NAME, { "tool" => tool.full_name, "args" => args })
483
501
  end
484
502
 
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)
503
+ # The call tool's activity row: <group>/<tool> and its arguments, as
504
+ # +surface+ (the turn's) reaches it.
505
+ def call_preview(args, surface = live_surface)
506
+ tool = lookup_quietly(args["tool"].to_s, surface)
488
507
  name = tool ? tool.full_name : args["tool"].to_s.strip
489
508
  inner = tool && inner_args(args["args"], tool)
490
509
  inner ? [name, preview(inner)].reject(&:empty?).join(" ") : name
@@ -495,11 +514,11 @@ module Samagotchi
495
514
  # indexed yet (its server still starting) is named from what the
496
515
  # model gave (#claimed), so a rule on it fires before the call waits
497
516
  # for the server. Nothing for a name that can't be a tool (the call
498
- # refuses it).
499
- def call_targets(args)
517
+ # refuses it). +surface+: the turn's (the gate's).
518
+ def call_targets(args, surface = live_surface)
500
519
  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)
520
+ tool = lookup_quietly(name, surface)
521
+ acts_as, label = tool ? [tool.acts_as, "#{tool.group}: #{tool.name}"] : claimed(name, surface)
503
522
  return {} unless acts_as
504
523
 
505
524
  { acts_as: acts_as, args: inner_args(args["args"], tool) || {}, label: label }.compact
@@ -507,20 +526,44 @@ module Samagotchi
507
526
 
508
527
  private
509
528
 
529
+ # The Registry #declare registered in, read now as a ToolSurface (its
530
+ # toolkit, every layer's tools left aside: the index's own group never
531
+ # holds one): what a caller without a turn's surface sees.
532
+ # @return [ToolSurface]
533
+ def live_surface = ToolSurface.for(@registry)
534
+
535
+ # The fields +spec+'s tool registers with beside its entry_fields: a
536
+ # handler whose block is given the call's typed args and the turn's
537
+ # surface (the ToolContext's: a bare kernel's computed on read; none,
538
+ # a spec's nil, reads the Registry now), and call_tool's preview and
539
+ # targets, given the turn's surface by the row and the gate
540
+ # (Registry::Entry#preview_of / #targets_of; nil: the Registry now).
541
+ def own_fields(spec)
542
+ parameters = spec.schema[:parameters]
543
+ args = ->(call) { Plugin::ToolSpec.args_of(call, parameters) }
544
+ fields = { handler: lambda { |call, kctx|
545
+ Plugin::ToolSpec.result_text(spec.block.call(args.call(call), kctx&.surface || live_surface))
546
+ } }
547
+ return fields unless spec.name == CALL_NAME
548
+
549
+ fields.merge(preview: ->(call, surface = nil) { call_preview(args.call(call), surface || live_surface) },
550
+ targets: ->(call, surface = nil) { call_targets(args.call(call), surface || live_surface) })
551
+ end
552
+
510
553
  def find_spec
511
554
  Plugin::ToolSpec.build(FIND_NAME, find_description, 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, surface|
558
+ find(args["query"].to_s, args["group"], server: args["server"], surface: surface)
516
559
  end
517
560
  end
518
561
 
519
562
  def call_spec
520
563
  Plugin::ToolSpec.build(CALL_NAME, CALL_DESCRIPTION, params: CALL_PARAMS, label: @sources.first&.label,
521
564
  preview: ->(args) { call_preview(args) },
522
- targets: ->(args) { call_targets(args) }) do |args, _ctx|
523
- call(args["tool"].to_s, args["args"])
565
+ targets: ->(args) { call_targets(args) }) do |args, surface|
566
+ call(args["tool"].to_s, args["args"], surface: surface)
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 }
@@ -548,19 +591,19 @@ module Samagotchi
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
593
  FoundText.new("#{CALL_HINT}\n\n#{blocks.join("\n\n")}",
551
- loadable(matches))
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
@@ -573,36 +616,36 @@ module Samagotchi
573
616
  @sources.each { |source| source.declared!(names, tokens, groups) }
574
617
  end
575
618
 
576
- def described_groups
577
- return @described unless @registry&.toolkit && @registry.toolkit.groups != :all
619
+ def described_groups(surface)
620
+ toolkit = surface.toolkit
621
+ return @described unless toolkit && toolkit.groups != :all
578
622
 
579
- @described.select { |name, _line| @registry.toolkit.reaches_group?(name) }
623
+ @described.select { |name, _line| toolkit.reaches_group?(name) }
580
624
  end
581
625
 
582
626
  def own?(group) = RESERVED_GROUPS.include?(group.name)
583
627
 
584
628
  # The registered chi tool chi/<name> names (a built-in or a plugin's,
585
629
  # never the index's own), reachable or not.
586
- def builtin_named(name)
630
+ def builtin_named(name, surface)
587
631
  return nil unless name.start_with?("#{CHI_GROUP}/")
588
632
 
589
- entry = @registry[name.delete_prefix("#{CHI_GROUP}/")]
633
+ entry = surface.registered[name.delete_prefix("#{CHI_GROUP}/")]
590
634
  entry unless entry.nil? || entry.source == SOURCE
591
635
  end
592
636
 
593
637
  # 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.
638
+ # +surface+'s toolkit leaves out but reaches (ToolSurface#index_only;
639
+ # none: no group). Each tool's schema is its registered entry's (a
640
+ # config's execute description included), and it acts as itself.
597
641
  # @return [Array<Group>]
598
- def registry_groups
599
- names = @registry ? @registry.reachable_names : []
600
- return [] if names.empty?
642
+ def registry_groups(surface)
643
+ entries = surface.index_only
644
+ return [] if entries.empty?
601
645
 
602
- tools = names.map do |name|
603
- entry = @registry[name]
646
+ tools = entries.map do |entry|
604
647
  schema = ToolDeclarations.chat_schemas([entry.schema]).first
605
- Tool.new(group: CHI_GROUP, name: name, acts_as: name, description: schema[:description].to_s,
648
+ Tool.new(group: CHI_GROUP, name: entry.name, acts_as: entry.name, description: schema[:description].to_s,
606
649
  schema: JSON.parse(JSON.generate(schema[:parameters])), entry: entry)
607
650
  end
608
651
  [Group.new(name: CHI_GROUP, summary: CHI_SUMMARY, tools: tools)]
@@ -687,9 +730,9 @@ module Samagotchi
687
730
  # miss is the error text: a failed group's error, else the closest
688
731
  # tools.
689
732
  # @return [Tool, String]
690
- def lookup(name, waited: false)
733
+ def lookup(name, surface, waited: false)
691
734
  name = name.strip
692
- all = reached_groups
735
+ all = reached_groups(surface)
693
736
  tool = all.lazy.filter_map do |group|
694
737
  next unless name.start_with?("#{group.name}/")
695
738
 
@@ -703,17 +746,21 @@ module Samagotchi
703
746
  return "Error: #{name} names #{same.map(&:full_name).join(" and ")}; call one as <group>/<tool>" if same.size > 1
704
747
 
705
748
  # 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
749
+ # starting server's tool. A layer's tool (forget_outputs) isn't
750
+ # counted, as before the surface (the Registry was asked without
751
+ # the turn's layers).
752
+ declared = surface.declared_entry(name)
753
+ if declared && declared.source != SOURCE && declared.layer.nil?
754
+ return "Error: #{name} is declared; call it directly"
755
+ end
709
756
 
710
757
  # A name with a group's <group>/ waits for that group only; any
711
758
  # 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 }
759
+ named = reached_groups(surface).select { |group| name.start_with?("#{group.name}/") }
760
+ starting = (named.empty? ? reached_groups(surface) : named).select { |group| group.state == :starting }
714
761
  unless waited || starting.empty?
715
762
  starting.each { |group| source_of(group.name)&.wait(group.name) }
716
- return lookup(name, waited: true)
763
+ return lookup(name, surface, waited: true)
717
764
  end
718
765
 
719
766
  failed = all.find { |group| group.state == :failed && name.start_with?("#{group.name}/") }
@@ -726,8 +773,8 @@ module Samagotchi
726
773
  end
727
774
 
728
775
  # #lookup's tool, without waiting for a start: nil on a miss.
729
- def lookup_quietly(name)
730
- tool = lookup(name, waited: true)
776
+ def lookup_quietly(name, surface)
777
+ tool = lookup(name, surface, waited: true)
731
778
  tool.is_a?(Tool) ? tool : nil
732
779
  end
733
780
 
@@ -750,8 +797,8 @@ module Samagotchi
750
797
  # bare name with a source's prefix as it is (no label). Nil for
751
798
  # anything else.
752
799
  # @return [Array(String, String), Array(String, nil), nil]
753
- def claimed(name)
754
- all = all_groups
800
+ def claimed(name, surface)
801
+ all = all_groups(surface)
755
802
  if (own = all.select { |g| own?(g) }.find { |g| g.tools.any? { |t| t.acts_as == name } })
756
803
  return [name, "#{own.name}: #{name}"]
757
804
  end