samagotchi 0.152.0 → 0.153.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -1
  3. data/docs/bundle-authoring.md +1 -1
  4. data/docs/bundles.md +84 -0
  5. data/docs/cli.md +3 -3
  6. data/docs/configuration.md +19 -14
  7. data/docs/guardrails.md +7 -5
  8. data/docs/hooks.md +4 -3
  9. data/docs/internals/anthropic.md +3 -3
  10. data/docs/internals/attached-context.md +3 -0
  11. data/docs/internals/engine.md +9 -5
  12. data/docs/internals/participants.md +1 -1
  13. data/docs/internals/prompt-caching.md +7 -3
  14. data/docs/internals/web-frontend.md +20 -17
  15. data/docs/plugins.md +29 -15
  16. data/docs/releasing.md +1 -1
  17. data/docs/sessions.md +1 -1
  18. data/docs/testing.md +2 -2
  19. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  20. data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
  21. data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
  22. data/lib/samagotchi/engine.rb +10 -5
  23. data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
  24. data/lib/samagotchi/llm_context_stale.rb +2 -2
  25. data/lib/samagotchi/message_parts.rb +3 -2
  26. data/lib/samagotchi/toolkit.rb +8 -5
  27. data/lib/samagotchi/toolkit_binding.rb +30 -5
  28. data/lib/samagotchi/toolkit_report_command.rb +9 -5
  29. data/lib/samagotchi/toolkit_suggest.rb +12 -6
  30. data/lib/samagotchi/tools/builtin_calls.rb +14 -10
  31. data/lib/samagotchi/tools/deferred_tools.rb +2 -2
  32. data/lib/samagotchi/tools/registry.rb +44 -20
  33. data/lib/samagotchi/tools/tool_index.rb +27 -21
  34. data/lib/samagotchi/version.rb +1 -1
  35. data/lib/samagotchi/web/public/app.js +61 -357
  36. data/lib/samagotchi/web/public/composer_mode.js +110 -0
  37. data/lib/samagotchi/web/public/owner_claim.js +81 -0
  38. data/lib/samagotchi/web/public/owner_verify.js +112 -0
  39. data/lib/samagotchi/web/public/session_fields.js +3 -3
  40. data/lib/samagotchi/web/public/strip_grip.js +3 -3
  41. data/lib/samagotchi/web/public/strip_hidden.js +65 -0
  42. data/lib/samagotchi/web/public/version_notices.js +132 -0
  43. data/lib/samagotchi/worker.rb +6 -0
  44. metadata +8 -1
@@ -241,7 +241,9 @@ module Samagotchi
241
241
  def grade(entry, kit, uses)
242
242
  kits = entry["kits"].is_a?(Hash) ? entry["kits"] : {}
243
243
  rules = kits[kit]
244
- core_tools = Array(entry["core_tools"])
244
+ # The tools a kit's rules declare: the built-ins and the plugin tools
245
+ # (chi.tool) logged when the ask started (an older line has none).
246
+ core_tools = Array(entry["core_tools"]) | Array(entry["plugin_tools"])
245
247
  return ["ungradable", " (no rules logged for #{kit})"] unless rules.is_a?(Hash)
246
248
  return ["ungradable", " (tool calls not readable)"] if uses == :ungradable
247
249
 
@@ -259,8 +261,10 @@ module Samagotchi
259
261
  end
260
262
 
261
263
  # Whether a kit's rules declare +use+ (Toolkit::Resolved#declares?): a
262
- # tool-index group by the kit's groups; a built-in by its core list;
263
- # any other tool (a plugin's) unless the kit removes it.
264
+ # tool-index group by the kit's groups; a logged built-in or plugin
265
+ # tool (+core_tools+) by the kit's list (all of them on full) and its
266
+ # adds; any other (the index's own, a layer's, one that registered
267
+ # after the ask) unless the kit removes it.
264
268
  def covers?(rules, core_tools, use)
265
269
  return group_reached?(rules["groups"], use.group) if use.group && use.group != "chi"
266
270
 
@@ -276,8 +280,8 @@ module Samagotchi
276
280
  (declared & core_tools) - Array(rules["removed"])
277
281
  end
278
282
 
279
- # +other+ declares no more built-ins and reaches no more groups than
280
- # +rules+, and less of one of them.
283
+ # +other+ declares no more logged tools (built-ins and plugin tools)
284
+ # and reaches no more groups than +rules+, and less of one of them.
281
285
  def narrower?(other, rules, core_tools)
282
286
  mine = core_set(rules, core_tools)
283
287
  theirs = core_set(other, core_tools)
@@ -31,8 +31,13 @@ module Samagotchi
31
31
  # A kit as the ask saw it: its name, classifier description and #rules.
32
32
  Candidate = Data.define(:name, :description, :rules)
33
33
  # +default+ is a Candidate (no description needed); +core_tools+ the
34
- # built-ins registered when the ask started.
35
- Snapshot = Data.define(:session_id, :turn_number, :default, :candidates, :core_tools, :state)
34
+ # built-ins registered when the ask started, +plugin_tools+ the plugin
35
+ # tools (chi.tool) registered then: a kit declares both by its rules.
36
+ Snapshot = Data.define(:session_id, :turn_number, :default, :candidates, :core_tools, :plugin_tools, :state) do
37
+ def initialize(session_id:, turn_number:, default:, candidates:, core_tools:, state:, plugin_tools: [])
38
+ super
39
+ end
40
+ end
36
41
  Suggestion = Data.define(:kit, :raw, :confidence, :decided)
37
42
  # One ask's outcome: its Suggestion, what on mode would do with it
38
43
  # (suggested, default, defer, below_threshold, invalid, no_judgment)
@@ -164,6 +169,7 @@ module Samagotchi
164
169
  decided: !!decided,
165
170
  would: would,
166
171
  core_tools: snapshot.core_tools,
172
+ plugin_tools: snapshot.plugin_tools,
167
173
  kits: [snapshot.default, *snapshot.candidates].to_h { |kit| [kit.name, kit.rules] }
168
174
  }
169
175
  Judged.new(suggestion: Suggestion.new(kit: selected, raw: raw, confidence: confidence, decided: !!decided),
@@ -319,10 +325,10 @@ module Samagotchi
319
325
  end
320
326
 
321
327
  # A resolved kit (Toolkit::Resolved) as frozen, JSON-ready rules, not
322
- # a tool list: plugin and MCP tools may still be loading when the ask
323
- # starts, and every kit declares a non-core tool anyway
324
- # (Resolved#declares?), so the report expands these against the tools
325
- # a turn really called (ToolkitReportCommand).
328
+ # a tool list: a plugin's replaced tools and MCP tools may still be
329
+ # loading when the ask starts, so the report expands these against
330
+ # the tools a turn really called (ToolkitReportCommand), the logged
331
+ # core_tools and plugin_tools by the same rule (Resolved#declares?).
326
332
  # @return [Hash{String => Object}]
327
333
  def self.rules(resolved)
328
334
  {
@@ -149,20 +149,23 @@ module Samagotchi
149
149
  end
150
150
 
151
151
  # A saved call as it ran (a reload, the stale-read pairing): a call_tool
152
- # of a built-in ("chi/delegate", or a bare "delegate") is that
153
- # built-in's call with via: "call_tool" when +output+ (its saved
154
- # result) shows it ran as that built-in: its "[name]" lead, or, with
155
- # no lead, an error that names it (refused by its name before the
156
- # gate); any other call as it is (a call_tool that ran, refused or
157
- # not, leads with "[call_tool]").
152
+ # of a chi tool, a built-in or a plugin's ("chi/delegate",
153
+ # "chi/web_search", or a bare "delegate") is that tool's call with
154
+ # via: "call_tool" when +output+ (its saved result) shows it ran as
155
+ # that tool: its "[name]" lead, or, with no lead, an error that names
156
+ # it (refused by its name before the gate; a plugin tool's only by
157
+ # its chi/ name, the one no MCP tool has); any other call as it is (a
158
+ # call_tool that ran, refused or not, leads with "[call_tool]"). No
159
+ # registry is asked: a reload without the plugin reads the same.
158
160
  # @param call [Hash] as #build gives it
159
161
  # @param output [String, nil]
160
162
  # @return [Hash]
161
163
  def unwrap_saved(call, output)
162
164
  return call unless call[:name] == VIA && call[:args].is_a?(Hash)
163
165
 
164
- name = call[:args]["tool"].to_s.strip.delete_prefix(VIA_GROUP)
165
- return call unless row(name) && ran_as?(output.to_s, name)
166
+ named = call[:args]["tool"].to_s.strip
167
+ name = named.delete_prefix(VIA_GROUP)
168
+ return call if name.empty? || name == VIA || !ran_as?(output.to_s, name, error: row(name) || named != name)
166
169
 
167
170
  build(name, saved_args(call[:args]["args"])).merge(via: VIA)
168
171
  end
@@ -183,9 +186,10 @@ module Samagotchi
183
186
  build(VIA, { "tool" => name, "args" => call[:args] })
184
187
  end
185
188
 
186
- def ran_as?(output, name)
189
+ # +error+: whether a lead-less error that names the tool counts.
190
+ def ran_as?(output, name, error: true)
187
191
  lead = output[SAVED_LEAD, 1]
188
- lead ? lead == name : output.start_with?("Error: #{name} ")
192
+ lead ? lead == name : error && output.start_with?("Error: #{name} ")
189
193
  end
190
194
 
191
195
  # call_tool's inner args as saved: an object, or its JSON text.
@@ -9,8 +9,8 @@ module Samagotchi
9
9
  # that loads tools (LLM::AnthropicMessages#loads_tools?): every tool the
10
10
  # session reaches but doesn't declare, so a find_tools hit can load it
11
11
  # and the model calls it by its own name. In order:
12
- # 1. the built-ins the toolkit leaves out but doesn't remove
13
- # (Registry#reachable_names), in registry order;
12
+ # 1. the chi tools (built-ins and plugin tools) the toolkit leaves out
13
+ # but doesn't remove (Registry#reachable_names), in registry order;
14
14
  # 2. the tool index's source tools (an MCP server's, by acts_as), only
15
15
  # while call_tool is declared (ToolIndex#deferrable: sorted by name);
16
16
  # 3. every other name the conversation loaded (a find result's
@@ -41,14 +41,16 @@ module Samagotchi
41
41
  # What Tools::ToolIndex#declare registered: the entries of +source+
42
42
  # are the index's search and call tools, +call_name+ the call tool's
43
43
  # name, and +groups+ the source groups registered when the plugins
44
- # loaded. +deferrable+ (→ the sources'
44
+ # loaded; +loaded+ the entries registered then (nil: whatever is
45
+ # registered now), which alone decide whether the index's tools are
46
+ # needed (Registry#index_needed?). +deferrable+ (→ the sources'
45
47
  # tools a host that loads tools may declare deferred) and +wrap+
46
48
  # (call → the call_tool call a direct call of one of them runs as, or
47
49
  # nil) are nil when nothing reaches them.
48
- Index = Data.define(:source, :call_name, :groups, :deferrable, :wrap, :unreached) do
49
- def initialize(source:, call_name:, groups:, deferrable: nil, wrap: nil, unreached: nil)
50
+ Index = Data.define(:source, :call_name, :groups, :deferrable, :wrap, :unreached, :loaded) do
51
+ def initialize(source:, call_name:, groups:, deferrable: nil, wrap: nil, unreached: nil, loaded: nil)
50
52
  super(source: source, call_name: call_name, groups: Array(groups).map(&:to_s).uniq.freeze,
51
- deferrable: deferrable, wrap: wrap, unreached: unreached)
53
+ deferrable: deferrable, wrap: wrap, unreached: unreached, loaded: loaded&.dup&.freeze)
52
54
  end
53
55
 
54
56
  def unreached_tool(name) = unreached&.call(name)
@@ -116,18 +118,18 @@ module Samagotchi
116
118
  ordered(@entries.values.select { |entry| left_out?(entry, layers, toolkit) }).map(&:name)
117
119
  end
118
120
 
119
- # The built-in +name+ names when the session's toolkit leaves it out
120
- # but the tool index reaches it (#reachable_names).
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).
121
123
  # @return [Entry, nil]
122
124
  def indexed(name, layers: [], toolkit: @toolkit)
123
125
  entry = self[name]
124
126
  entry if entry && call_offered?(toolkit) && soft?(entry, layers, toolkit)
125
127
  end
126
128
 
127
- # The built-ins +toolkit+ leaves out of a turn under +layers+ that the
128
- # tool index reaches (call_tool): not in its kit's list, but not
129
- # removed either (remove:, --no-tool, /toolkit remove), with call_tool
130
- # itself declared.
129
+ # The tools +toolkit+ leaves out of a turn under +layers+ that the tool
130
+ # index reaches (call_tool), built-ins and plugin tools alike: not in
131
+ # its kit's list, but not removed either (remove:, --no-tool, /toolkit
132
+ # remove), with call_tool itself declared. Never the index's own.
131
133
  # @return [Array<String>] in declaration order
132
134
  def reachable_names(layers: [], toolkit: @toolkit)
133
135
  return [] unless call_offered?(toolkit)
@@ -145,6 +147,25 @@ module Samagotchi
145
147
  .map(&:name)
146
148
  end
147
149
 
150
+ # The tools +toolkit+ leaves out of a turn under +layers+ that a
151
+ # plugin registered after the index was declared (chi.replace_tools)
152
+ # and that nothing reaches: call_tool isn't declared (the index's
153
+ # need is the load-time one, so the declared tools don't grow
154
+ # mid-session), nor removed by the user. /toolkit add declares one.
155
+ # @return [Array<String>] in declaration order
156
+ def stranded_names(layers: [], toolkit: @toolkit)
157
+ return [] if @index&.loaded.nil? || toolkit.nil? || toolkit.removed.include?(@index.call_name)
158
+ return [] if call_offered?(toolkit)
159
+
160
+ loaded = @index.loaded.map(&:name)
161
+ ordered(@entries.values.select { |entry| !loaded.include?(entry.name) && soft?(entry, layers, toolkit) })
162
+ .map(&:name)
163
+ end
164
+
165
+ # Every entry registered now, declared or not, in registration order.
166
+ # @return [Array<Entry>]
167
+ def registered = @entries.values
168
+
148
169
  # Every tool +source+ registered, declared or not (a plugin's tool set
149
170
  # changing: Plugin::ToolSpec.apply).
150
171
  # @return [Array<Entry>]
@@ -175,7 +196,7 @@ module Samagotchi
175
196
 
176
197
  # Declared by +toolkit+ (nil: every tool, full). The tool index's
177
198
  # tools are declared when they reach something, whatever the toolkit:
178
- # a source registered at load, or a built-in it leaves out but
199
+ # a source registered at load, or a tool it leaves out but
179
200
  # doesn't remove (#index_needed?); unless it removes them.
180
201
  def declared?(entry, toolkit = @toolkit)
181
202
  if index_entry?(entry)
@@ -187,8 +208,10 @@ module Samagotchi
187
208
 
188
209
  def index_entry?(entry) = !@index.nil? && entry.source == @index.source
189
210
 
190
- # From what the plugins registered at load and the toolkit alone, so
191
- # the declared tools stay the same for a session's life. A built-in
211
+ # From what the plugins registered at load (Index#loaded) and the
212
+ # toolkit alone, so the declared tools stay the same for a session's
213
+ # life: a plugin tool registered later on a kit that leaves it out
214
+ # doesn't bring find_tools and call_tool in (#stranded_names). A tool
192
215
  # left out counts only while call_tool can reach it (not removed).
193
216
  def index_needed?(toolkit)
194
217
  return true if reached_groups?(toolkit)
@@ -198,10 +221,10 @@ module Samagotchi
198
221
  end
199
222
 
200
223
  # Whether the index's tools would reach something under +toolkit+,
201
- # removed or not: a source registered at load, or a built-in the
202
- # toolkit leaves out without removing it.
224
+ # removed or not: a source registered at load, or a tool (a built-in
225
+ # or a plugin's) the toolkit leaves out without removing it.
203
226
  def index_reaches?(toolkit)
204
- reached_groups?(toolkit) || @entries.values.any? { |entry| soft?(entry, [], toolkit) }
227
+ reached_groups?(toolkit) || (@index.loaded || @entries.values).any? { |entry| soft?(entry, [], toolkit) }
205
228
  end
206
229
 
207
230
  def reached_groups?(toolkit)
@@ -218,11 +241,12 @@ module Samagotchi
218
241
  toolkit&.removed&.include?(entry.name) == true && index_reaches?(toolkit)
219
242
  end
220
243
 
221
- # A built-in +toolkit+ leaves out of a turn under +layers+ without
222
- # removing it.
244
+ # A tool +toolkit+ leaves out of a turn under +layers+ without removing
245
+ # it: a built-in or a plugin's, never one of the index's own (they are
246
+ # declared when they reach something) or a layer's (always declared).
223
247
  def soft?(entry, layers, toolkit)
224
- !toolkit.nil? && entry.core? && entry.offered?(layers) && !toolkit.removed.include?(entry.name) &&
225
- !toolkit.declares?(entry)
248
+ !toolkit.nil? && !index_entry?(entry) && entry.layer.nil? && entry.offered?(layers) &&
249
+ !toolkit.removed.include?(entry.name) && !toolkit.declares?(entry)
226
250
  end
227
251
 
228
252
  # Whether +toolkit+ declares the index's call tool.
@@ -17,14 +17,15 @@ module Samagotchi
17
17
  # inputSchema, and call_tool, which calls one as <group>/<tool>.
18
18
  # A source (the mcp bundle, chi.tool_source) publishes its groups (an
19
19
  # MCP server each) and runs the calls; the index's own group, chi, is
20
- # the built-ins the session's toolkit leaves out but doesn't remove
21
- # (Registry#reachable_names), and a call_tool of one runs as that
22
- # built-in (#unwrap, before the guardrail gate). The index searches the
20
+ # 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),
22
+ # and a call_tool of one runs as that tool (#unwrap, before the
23
+ # guardrail gate). The index searches the
23
24
  # groups, looks a call's tool up and checks it.
24
25
  #
25
26
  # The two tools are registered once, after the plugins load, and
26
27
  # declared per toolkit (Registry#declared?): when a source registered
27
- # then, or the toolkit leaves a built-in reachable. The search tool's
28
+ # then, or the toolkit leaves a tool reachable. The search tool's
28
29
  # description has the source groups' lines as they were at load and a
29
30
  # chi line per toolkit (#refresh!): nothing a source publishes later
30
31
  # changes the request's tools, so the prompt cache keeps.
@@ -44,9 +45,10 @@ module Samagotchi
44
45
  # times one in its description.
45
46
  NAME_WEIGHT = 3
46
47
  PREVIEW_CHARS = 60
47
- # The index's own group: chi's built-ins a toolkit leaves out.
48
+ # The index's own group: chi's tools (built-ins and bundles' chi.tool)
49
+ # a toolkit leaves out.
48
50
  CHI_GROUP = "chi"
49
- CHI_SUMMARY = "chi's own tools not declared in this session"
51
+ CHI_SUMMARY = "chi's and its bundles' tools not declared in this session"
50
52
  # Group names a source can't publish: the index's own groups'.
51
53
  RESERVED_GROUPS = [CHI_GROUP].freeze
52
54
 
@@ -276,7 +278,7 @@ module Samagotchi
276
278
  end
277
279
 
278
280
  # Register the search and call tools in +registry+, once, after the
279
- # plugins load (or without them: the built-ins a toolkit leaves out
281
+ # plugins load (or without them: the tools a toolkit leaves out
280
282
  # are reachable either way); +registry+ declares them per toolkit
281
283
  # (Registry::Index). A name another tool has is left out (a line in
282
284
  # +failures+).
@@ -285,6 +287,7 @@ module Samagotchi
285
287
  @registry = registry
286
288
  @described = description_lines(all_groups.reject { |group| own?(group) }).freeze
287
289
  @declared_group_names = @described.keys.freeze
290
+ loaded = registry.registered
288
291
  specs = [find_spec, call_spec]
289
292
  declared = specs.filter_map do |spec|
290
293
  if (taken = registry[spec.name])
@@ -299,7 +302,7 @@ module Samagotchi
299
302
  end
300
303
  registry.index = Registry::Index.new(source: SOURCE, call_name: CALL_NAME, groups: @declared_group_names,
301
304
  deferrable: -> { deferrable }, wrap: ->(call) { wrap(call) },
302
- unreached: ->(name) { unreached_tool(name) })
305
+ unreached: ->(name) { unreached_tool(name) }, loaded: loaded)
303
306
  declared!(declared)
304
307
  declared.map(&:name)
305
308
  end
@@ -362,7 +365,7 @@ module Samagotchi
362
365
  # A call of the tool +name+ names, with +args+ typed by its schema,
363
366
  # run by its source. It fails closed: a tool that isn't the one
364
367
  # guardrails were given (#claimed) is refused, and so is one of the
365
- # index's own (a built-in runs as itself, #unwrap, or not at all).
368
+ # index's own (a chi tool runs as itself, #unwrap, or not at all).
366
369
  def call(name, args)
367
370
  tool = lookup(name)
368
371
  return tool if tool.is_a?(String)
@@ -388,12 +391,12 @@ module Samagotchi
388
391
  source.run(tool, inner)
389
392
  end
390
393
 
391
- # The built-in a call_tool +call+ names, as the call that runs in its
394
+ # The chi tool a call_tool +call+ names, as the call that runs in its
392
395
  # place (ToolRunner#run's first step, before the guardrail gate):
393
396
  # built from its arguments as a direct call would be
394
397
  # (BuiltinCalls.build), with via: "call_tool". The name is a chi tool
395
- # (a bare one only while the toolkit reaches it), or chi/<built-in>
396
- # for any registered built-in: one the toolkit removes or declares
398
+ # (a bare one only while the toolkit reaches it), or chi/<name> for
399
+ # any registered built-in or plugin tool: one the toolkit removes or declares
397
400
  # is then refused by its name before the gate (ToolRunner#left_out?),
398
401
  # with no hook or card. Nil, so the call runs as call_tool, when the
399
402
  # turn under +layers+ doesn't offer call_tool, for an MCP tool, a miss
@@ -429,8 +432,8 @@ module Samagotchi
429
432
  end
430
433
 
431
434
  # The name a request declares +name+'s tool by when it goes out
432
- # deferred (Tools::DeferredTools): a reachable built-in's (by its
433
- # name or chi/<name>), a deferrable source tool's acts_as (by it or
435
+ # deferred (Tools::DeferredTools): a reachable chi tool's, built-in or
436
+ # plugin (by its name or chi/<name>), a deferrable source tool's acts_as (by it or
434
437
  # <group>/<tool>). Nil for any other: declared, removed, unknown, or a
435
438
  # source tool that can't be deferred. Never waits for a start.
436
439
  # @return [String, nil]
@@ -549,7 +552,7 @@ module Samagotchi
549
552
  end
550
553
 
551
554
  # The wire names of +matches+ a request can declare deferred: a
552
- # built-in of the index's own groups by its name, a source's tool by
555
+ # chi tool of the index's own groups by its name, a source's tool by
553
556
  # its acts_as when it is #deferrable.
554
557
  def loadable(matches)
555
558
  sourced = nil
@@ -578,16 +581,17 @@ module Samagotchi
578
581
 
579
582
  def own?(group) = RESERVED_GROUPS.include?(group.name)
580
583
 
581
- # The registered built-in chi/<name> names, reachable or not.
584
+ # The registered chi tool chi/<name> names (a built-in or a plugin's,
585
+ # never the index's own), reachable or not.
582
586
  def builtin_named(name)
583
587
  return nil unless name.start_with?("#{CHI_GROUP}/")
584
588
 
585
589
  entry = @registry[name.delete_prefix("#{CHI_GROUP}/")]
586
- entry if entry&.core?
590
+ entry unless entry.nil? || entry.source == SOURCE
587
591
  end
588
592
 
589
- # The index's own groups: chi, the built-ins the Registry's toolkit
590
- # leaves out but reaches (none: no group). Each tool's schema is its
593
+ # 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
591
595
  # registered entry's (a config's execute description included), and
592
596
  # it acts as itself.
593
597
  # @return [Array<Group>]
@@ -698,8 +702,10 @@ module Samagotchi
698
702
  return same.first if same.size == 1
699
703
  return "Error: #{name} names #{same.map(&:full_name).join(" and ")}; call one as <group>/<tool>" if same.size > 1
700
704
 
701
- # A built-in the session declares is no starting server's tool.
702
- return "Error: #{name} is declared; call it directly" if @registry&.offered(name)&.core?
705
+ # 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
703
709
 
704
710
  # A name with a group's <group>/ waits for that group only; any
705
711
  # other name (an acts_as) for every one starting.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.152.0"
4
+ VERSION = "0.153.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end