samagotchi 0.68.0 → 0.69.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/docs/architecture.md +1 -1
- data/docs/cli.md +6 -1
- data/docs/configuration.md +28 -6
- data/docs/guardrails.md +23 -1
- data/docs/hooks.md +4 -0
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +15 -8
- data/docs/internals/prompt-caching.md +9 -2
- data/docs/plugins.md +42 -19
- data/docs/releasing.md +14 -6
- data/docs/sessions.md +3 -3
- data/docs/testing.md +7 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -3
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +0 -19
- data/lib/samagotchi/engine.rb +5 -2
- data/lib/samagotchi/guardrails/targets.rb +9 -2
- data/lib/samagotchi/host_config.rb +1 -29
- data/lib/samagotchi/host_model.rb +0 -7
- data/lib/samagotchi/kernel_loop.rb +24 -7
- data/lib/samagotchi/llm/errors.rb +47 -1
- data/lib/samagotchi/llm/http.rb +20 -2
- data/lib/samagotchi/llm_context_stale.rb +6 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +3 -0
- data/lib/samagotchi/model_price.rb +2 -4
- data/lib/samagotchi/session_metrics.rb +4 -1
- data/lib/samagotchi/system_prompt.rb +19 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +5 -2
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +24 -3
- data/lib/samagotchi/toolkit.rb +27 -14
- data/lib/samagotchi/toolkit_binding.rb +8 -2
- data/lib/samagotchi/toolkit_command.rb +20 -4
- data/lib/samagotchi/tools/builtin_calls.rb +45 -1
- data/lib/samagotchi/tools/registry.rb +95 -8
- data/lib/samagotchi/tools/tool_index.rb +223 -72
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -9
- data/lib/samagotchi/web/message_parts.rb +5 -2
- data/lib/samagotchi/web/public/activity.js +3 -1
- data/lib/samagotchi/web/public/app.js +123 -22
- data/lib/samagotchi/web/public/chains.js +19 -2
- data/lib/samagotchi/web/public/families_view.js +15 -0
- data/lib/samagotchi/web/public/index.html +7 -5
- data/lib/samagotchi/web/public/model_picker.js +21 -6
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +5 -2
- data/lib/samagotchi/web/public/timing.js +3 -1
- data/lib/samagotchi/web/public/toast_place.js +16 -0
- data/lib/samagotchi/web/public/toolkit_chip.js +11 -4
- data/lib/samagotchi/web/public/turn_html.js +4 -2
- metadata +2 -1
data/lib/samagotchi/toolkit.rb
CHANGED
|
@@ -25,17 +25,19 @@ module Samagotchi
|
|
|
25
25
|
# The plain form the snapshot and the web carry.
|
|
26
26
|
def to_h = { name: name, add: add, remove: remove }
|
|
27
27
|
|
|
28
|
-
# This toolkit with +tool+ declared (/toolkit add):
|
|
29
|
-
#
|
|
28
|
+
# This toolkit with +tool+ declared (/toolkit add): out of +remove+
|
|
29
|
+
# and into +add+, whatever its kit says (an add after a remove
|
|
30
|
+
# declares it).
|
|
30
31
|
def adding(tool)
|
|
31
32
|
tool = tool.to_s
|
|
32
|
-
|
|
33
|
+
with(remove: remove - [tool], add: (add + [tool]).uniq)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
# This toolkit with +tool+
|
|
36
|
+
# This toolkit with +tool+ removed (/toolkit remove), the other way: a
|
|
37
|
+
# remove is hard (call_tool can't reach it either), also after an add.
|
|
36
38
|
def removing(tool)
|
|
37
39
|
tool = tool.to_s
|
|
38
|
-
|
|
40
|
+
with(add: add - [tool], remove: (remove + [tool]).uniq)
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
# How the UIs name it: "lean", "lean +web_fetch -memory_write".
|
|
@@ -66,21 +68,31 @@ module Samagotchi
|
|
|
66
68
|
|
|
67
69
|
# A toolkit as a session's next turn would declare it (Engine
|
|
68
70
|
# #toolkit_explained): +toolkit+ as saved, +resolved+, the Registry
|
|
69
|
-
# +declared+ entries and the names it leaves out (+left_out+)
|
|
70
|
-
#
|
|
71
|
+
# +declared+ entries and the names it leaves out (+left_out+): the ones
|
|
72
|
+
# call_tool reaches (+reachable+) and the ones it removes (+removed+);
|
|
73
|
+
# and whether that isn't what the Registry declares now (+pending+: it
|
|
71
74
|
# applies at the next turn's start).
|
|
72
|
-
Explained = Data.define(:toolkit, :resolved, :declared, :left_out, :pending) do
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
Explained = Data.define(:toolkit, :resolved, :declared, :left_out, :reachable, :removed, :pending) do
|
|
76
|
+
def initialize(toolkit:, resolved:, declared:, left_out:, pending:, reachable: [], removed: [])
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# What the prompt carries of it: the declared names, the reachable
|
|
81
|
+
# and removed ones (the not-loaded line, find_tools' chi line) and,
|
|
82
|
+
# when either list has any, the kit name the line gives. Two equal
|
|
75
83
|
# keys build the same prompt (/toolkit's "no change", its cost).
|
|
76
|
-
def prompt_key
|
|
84
|
+
def prompt_key
|
|
85
|
+
[declared.map(&:name), reachable, removed, reachable.empty? && removed.empty? ? nil : resolved.name]
|
|
86
|
+
end
|
|
77
87
|
|
|
78
88
|
# What the UIs show (the snapshot, command_ran): nil when it declares
|
|
79
89
|
# every tool on full (full, full +delegate), which has no chip.
|
|
90
|
+
# +left_out+ is +reachable+ and +removed+ together.
|
|
80
91
|
def summary
|
|
81
92
|
return nil if toolkit.name == Toolkit::FULL && left_out.empty? && resolved.fallback.nil?
|
|
82
93
|
|
|
83
|
-
{ name: toolkit.name, label: toolkit.label, left_out: left_out,
|
|
94
|
+
{ name: toolkit.name, label: toolkit.label, left_out: left_out, reachable: reachable, removed: removed,
|
|
95
|
+
fallback: resolved.fallback }
|
|
84
96
|
end
|
|
85
97
|
end
|
|
86
98
|
|
|
@@ -111,8 +123,9 @@ module Samagotchi
|
|
|
111
123
|
Kit.new(name: FULL, tools: :all),
|
|
112
124
|
Kit.new(name: LEAN, tools: LEAN_TOOLS),
|
|
113
125
|
# A delegate child: it reports to its parent (send_note; the
|
|
114
|
-
# delegated-by line names it) and leaves memory writes to it
|
|
115
|
-
|
|
126
|
+
# delegated-by line names it) and leaves memory writes to it, and
|
|
127
|
+
# starts no children of its own (removed: not even through call_tool).
|
|
128
|
+
Kit.new(name: CHILD, base: LEAN, add: %w[send_note], remove: %w[memory_write delegate delegate_result])
|
|
116
129
|
].to_h { |kit| [kit.name, kit] }.freeze
|
|
117
130
|
|
|
118
131
|
# What a toolkit declares, the Registry's filter: every built-in
|
|
@@ -17,14 +17,17 @@ module Samagotchi
|
|
|
17
17
|
# @param can_stage [#call] → whether a plugin may change the toolkit now
|
|
18
18
|
# (no turn runs, no model reply yet)
|
|
19
19
|
# @param reset_prompt [#call] → build the system prompts again
|
|
20
|
+
# @param refresh_index [#call] → the tool index's search tool described
|
|
21
|
+
# for the Registry's toolkit (Tools::ToolIndex#refresh!)
|
|
20
22
|
# @param emit [#call] (sink, event) → emits to a turn's sink + observers
|
|
21
23
|
# @param notify [#call] (text, level, source, extra:) → a notice
|
|
22
|
-
def initialize(tools:, session:, state_dir:, can_stage:, reset_prompt:, emit:, notify:)
|
|
24
|
+
def initialize(tools:, session:, state_dir:, can_stage:, reset_prompt:, emit:, notify:, refresh_index: -> {})
|
|
23
25
|
@tools = tools
|
|
24
26
|
@session = session
|
|
25
27
|
@state_dir = state_dir
|
|
26
28
|
@can_stage = can_stage
|
|
27
29
|
@reset_prompt = reset_prompt
|
|
30
|
+
@refresh_index = refresh_index
|
|
28
31
|
@emit = emit
|
|
29
32
|
@notify = notify
|
|
30
33
|
# The toolkits plugins declare (chi.toolkit), beside the built-in and
|
|
@@ -63,7 +66,9 @@ module Samagotchi
|
|
|
63
66
|
filter = resolved.everything? ? nil : resolved
|
|
64
67
|
tools = @tools.call
|
|
65
68
|
Toolkit::Explained.new(toolkit: toolkit, resolved: resolved, declared: tools.entries(toolkit: filter),
|
|
66
|
-
left_out: tools.left_out_names(toolkit: filter),
|
|
69
|
+
left_out: tools.left_out_names(toolkit: filter),
|
|
70
|
+
reachable: tools.reachable_names(toolkit: filter),
|
|
71
|
+
removed: tools.removed_names(toolkit: filter), pending: filter != tools.toolkit)
|
|
67
72
|
end
|
|
68
73
|
|
|
69
74
|
# The session's toolkit for the UIs (the snapshot, command_ran): nil on
|
|
@@ -114,6 +119,7 @@ module Samagotchi
|
|
|
114
119
|
return false if resolved == tools.toolkit
|
|
115
120
|
|
|
116
121
|
tools.toolkit = resolved
|
|
122
|
+
@refresh_index.call
|
|
117
123
|
@reset_prompt.call
|
|
118
124
|
true
|
|
119
125
|
end
|
|
@@ -7,7 +7,8 @@ require_relative "toolkit"
|
|
|
7
7
|
module Samagotchi
|
|
8
8
|
# /toolkit, the session's toolkit (Session#toolkit): with no argument,
|
|
9
9
|
# the tools its next turn declares with their estimated tokens, the
|
|
10
|
-
# toolkit and the tools it leaves out
|
|
10
|
+
# toolkit and the tools it leaves out (the ones find_tools reaches, the
|
|
11
|
+
# ones it removes); `/toolkit NAME` puts the session
|
|
11
12
|
# on another toolkit (its own adds and removes go), `/toolkit add TOOL`
|
|
12
13
|
# and `/toolkit remove TOOL` change one tool. A change is saved at once
|
|
13
14
|
# and applies at the next turn's start (Engine#apply_toolkit!). The
|
|
@@ -57,7 +58,9 @@ module Samagotchi
|
|
|
57
58
|
[[listing(after), "From the next turn's start (a running turn keeps its tools)."].join("\n"), true, nil]
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
# The toolkit, its declared tools with tokens, and the ones left out
|
|
61
|
+
# The toolkit, its declared tools with tokens, and the ones left out:
|
|
62
|
+
# reachable with find_tools / call_tool, removed (only /toolkit add
|
|
63
|
+
# brings one back), or, with call_tool not declared, not loaded.
|
|
61
64
|
def listing(explained)
|
|
62
65
|
toolkit = explained.toolkit
|
|
63
66
|
lines = ["toolkit: #{toolkit.label}#{" (applies at the next turn's start)" if explained.pending}"]
|
|
@@ -68,7 +71,13 @@ module Samagotchi
|
|
|
68
71
|
lines << " declared (#{declared.size}, ~#{format_tokens(declared.sum { |entry| tokens(entry) })} tokens): " \
|
|
69
72
|
"#{declared.map { |entry| "#{entry.name} ~#{format_tokens(tokens(entry))}" }.join(", ")}"
|
|
70
73
|
left_out = explained.left_out
|
|
71
|
-
|
|
74
|
+
if explained.reachable.empty?
|
|
75
|
+
lines << " not loaded: #{left_out.empty? ? "none" : left_out.join(", ")}"
|
|
76
|
+
else
|
|
77
|
+
route = explained.removed.include?("find_tools") ? "call_tool" : "find_tools"
|
|
78
|
+
lines << " reachable (#{route}): #{explained.reachable.join(", ")}"
|
|
79
|
+
lines << " removed: #{explained.removed.empty? ? "none" : explained.removed.join(", ")}"
|
|
80
|
+
end
|
|
72
81
|
lines << " toolkits: #{@engine.toolkit_kits.keys.join(", ")}; #{USAGE.delete_prefix("usage: ")} changes it"
|
|
73
82
|
lines.join("\n")
|
|
74
83
|
end
|
|
@@ -101,8 +110,15 @@ module Samagotchi
|
|
|
101
110
|
added = names(after) - names(before)
|
|
102
111
|
removed = names(before) - names(after)
|
|
103
112
|
parts = []
|
|
113
|
+
# Removed (hard: call_tool can't reach it), declared before or not;
|
|
114
|
+
# left out by the kit (soft); reachable now and not before.
|
|
115
|
+
hard = after.removed - before.removed
|
|
116
|
+
left = removed - hard
|
|
117
|
+
soft = after.reachable - before.reachable - removed
|
|
104
118
|
parts << "declares #{added.join(", ")}" unless added.empty?
|
|
105
|
-
parts << "leaves out #{
|
|
119
|
+
parts << "leaves out #{left.join(", ")}" unless left.empty?
|
|
120
|
+
parts << "removes #{hard.join(", ")} (call_tool can't reach #{hard.size == 1 ? "it" : "them"})" unless hard.empty?
|
|
121
|
+
parts << "lets call_tool reach #{soft.join(", ")}" unless soft.empty?
|
|
106
122
|
parts << "declares the same tools under its own name" if parts.empty?
|
|
107
123
|
"Switching to #{after.toolkit.label} #{parts.join("; ")}."
|
|
108
124
|
end
|
|
@@ -63,6 +63,14 @@ module Samagotchi
|
|
|
63
63
|
# called_as: with the model's own spelling.
|
|
64
64
|
ALIASES = { "bash" => "execute" }.freeze
|
|
65
65
|
|
|
66
|
+
# The tool index's call tool, whose call of a built-in runs as that
|
|
67
|
+
# built-in (ToolIndex#unwrap; ToolIndex::CALL_NAME is this), and the
|
|
68
|
+
# group it names them by. The one place the name is spelled.
|
|
69
|
+
VIA = "call_tool"
|
|
70
|
+
VIA_GROUP = "chi/"
|
|
71
|
+
# A saved run's "[name]" lead (ToolResponse::LEAD).
|
|
72
|
+
SAVED_LEAD = /\A\[([^\]\s]+)\]/
|
|
73
|
+
|
|
66
74
|
# One built-in's mapping, resolved from its schema and OVERRIDES.
|
|
67
75
|
Row = Data.define(:name, :content_keys, :path_key, :fields, :aliases, :verbatim, :fallback, :fallback_key,
|
|
68
76
|
:options) do
|
|
@@ -97,7 +105,11 @@ module Samagotchi
|
|
|
97
105
|
unknown = ([path_key].compact + Array(o[:verbatim]) + Array(o[:also]) + o.fetch(:aliases, {}).keys) - properties
|
|
98
106
|
unknown << content_keys.join("/") unless content_keys.empty? || content_keys.intersect?(properties)
|
|
99
107
|
raise ArgumentError, "#{name}: overrides name no schema property: #{unknown.join(", ")}" unless unknown.empty?
|
|
100
|
-
|
|
108
|
+
# A property can't set the call's name, nor the fields only chi sets
|
|
109
|
+
# (an alias's called_as, an unwrapped call's via).
|
|
110
|
+
if (reserved = fields & %w[name called_as via]).any?
|
|
111
|
+
raise ArgumentError, "#{name}: a property would replace the call's #{reserved.first}"
|
|
112
|
+
end
|
|
101
113
|
|
|
102
114
|
Row.new(name: name, content_keys: content_keys, path_key: path_key, fields: fields,
|
|
103
115
|
aliases: o.fetch(:aliases, {}), verbatim: Array(o[:verbatim]), fallback: o[:fallback],
|
|
@@ -135,6 +147,38 @@ module Samagotchi
|
|
|
135
147
|
call
|
|
136
148
|
end
|
|
137
149
|
|
|
150
|
+
# A saved call as it ran (a reload, the stale-read pairing): a call_tool
|
|
151
|
+
# of a built-in ("chi/delegate", or a bare "delegate") is that
|
|
152
|
+
# built-in's call with via: "call_tool" when +output+ (its saved
|
|
153
|
+
# result) shows it ran as that built-in: its "[name]" lead, or, with
|
|
154
|
+
# no lead, an error that names it (refused by its name before the
|
|
155
|
+
# gate); any other call as it is (a call_tool that ran, refused or
|
|
156
|
+
# not, leads with "[call_tool]").
|
|
157
|
+
# @param call [Hash] as #build gives it
|
|
158
|
+
# @param output [String, nil]
|
|
159
|
+
# @return [Hash]
|
|
160
|
+
def unwrap_saved(call, output)
|
|
161
|
+
return call unless call[:name] == VIA && call[:args].is_a?(Hash)
|
|
162
|
+
|
|
163
|
+
name = call[:args]["tool"].to_s.strip.delete_prefix(VIA_GROUP)
|
|
164
|
+
return call unless row(name) && ran_as?(output.to_s, name)
|
|
165
|
+
|
|
166
|
+
build(name, saved_args(call[:args]["args"])).merge(via: VIA)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def ran_as?(output, name)
|
|
170
|
+
lead = output[SAVED_LEAD, 1]
|
|
171
|
+
lead ? lead == name : output.start_with?("Error: #{name} ")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# call_tool's inner args as saved: an object, or its JSON text.
|
|
175
|
+
def saved_args(args)
|
|
176
|
+
args = JSON.parse(args) if args.is_a?(String) && !args.strip.empty?
|
|
177
|
+
args.is_a?(Hash) ? args : {}
|
|
178
|
+
rescue JSON::ParserError
|
|
179
|
+
{}
|
|
180
|
+
end
|
|
181
|
+
|
|
138
182
|
# An execute-shaped call the model spelled +name+: built as its +target+,
|
|
139
183
|
# tagged with the model's spelling for the activity event and the log.
|
|
140
184
|
def aliased(name, target, args, raw)
|
|
@@ -26,18 +26,33 @@ module Samagotchi
|
|
|
26
26
|
# @!attribute layer [Symbol, nil] the LLM context layer the tool
|
|
27
27
|
# belongs to (LLMContextStrategy): offered only in a turn running
|
|
28
28
|
# under it (forget_outputs, :forget); nil: always
|
|
29
|
-
|
|
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
|
|
33
|
+
Entry = Struct.new(:name, :schema, :handler, :label, :preview, :targets, :refs, :source, :layer, :unwrap,
|
|
34
|
+
keyword_init: true) do
|
|
30
35
|
def core? = source == "core"
|
|
31
36
|
|
|
32
37
|
# Offered in a turn under +layers+ (the turn's LLM context layers).
|
|
33
38
|
def offered?(layers) = layer.nil? || Array(layers).include?(layer)
|
|
34
39
|
end
|
|
35
40
|
|
|
41
|
+
# What Tools::ToolIndex#declare registered: the entries of +source+
|
|
42
|
+
# are the index's search and call tools, +call_name+ the call tool's
|
|
43
|
+
# name, and +sourced+ whether a source (an MCP server's bundle)
|
|
44
|
+
# registered when the plugins loaded.
|
|
45
|
+
Index = Data.define(:source, :call_name, :sourced)
|
|
46
|
+
|
|
36
47
|
def initialize
|
|
37
48
|
@entries = {}
|
|
38
49
|
@toolkit = nil
|
|
50
|
+
@index = nil
|
|
39
51
|
end
|
|
40
52
|
|
|
53
|
+
# The tool index's declaration (Index), nil without one.
|
|
54
|
+
attr_accessor :index
|
|
55
|
+
|
|
41
56
|
# The session's resolved toolkit (Toolkit::Resolved), which the lists
|
|
42
57
|
# below and #offered filter by; nil declares every tool (full). The
|
|
43
58
|
# Engine sets it at a turn's start, before the prompt is built, and
|
|
@@ -45,11 +60,12 @@ module Samagotchi
|
|
|
45
60
|
attr_accessor :toolkit
|
|
46
61
|
|
|
47
62
|
# @return [Entry]
|
|
48
|
-
def register(name, schema:, handler:, label: nil, preview: nil, targets: nil, refs: nil, source: "core", layer: nil
|
|
63
|
+
def register(name, schema:, handler:, label: nil, preview: nil, targets: nil, refs: nil, source: "core", layer: nil,
|
|
64
|
+
unwrap: nil)
|
|
49
65
|
raise ArgumentError, "tool #{name} is already registered" if @entries.key?(name)
|
|
50
66
|
|
|
51
67
|
@entries[name] = Entry.new(name: name, schema: schema, handler: handler, label: label, preview: preview,
|
|
52
|
-
targets: targets, refs: refs, source: source, layer: layer)
|
|
68
|
+
targets: targets, refs: refs, source: source, layer: layer, unwrap: unwrap)
|
|
53
69
|
end
|
|
54
70
|
|
|
55
71
|
# Remove a tool (a plugin's tool set changed after load).
|
|
@@ -70,18 +86,48 @@ module Samagotchi
|
|
|
70
86
|
end
|
|
71
87
|
|
|
72
88
|
# The entry +name+ names when the session's toolkit leaves it out (a
|
|
73
|
-
# real tool the model can't call here), nil otherwise.
|
|
89
|
+
# real tool the model can't call here, by its name), nil otherwise.
|
|
74
90
|
# @return [Entry, nil]
|
|
75
91
|
def left_out(name, layers: [])
|
|
76
92
|
entry = self[name]
|
|
77
|
-
entry if entry
|
|
93
|
+
entry if entry && left_out?(entry, layers, @toolkit)
|
|
78
94
|
end
|
|
79
95
|
|
|
80
96
|
# The tools +toolkit+ (the session's by default) leaves out of a turn
|
|
81
|
-
# under +layers+.
|
|
97
|
+
# under +layers+. The tool index's own aren't, unless it removes them:
|
|
98
|
+
# they are declared when there is something to reach.
|
|
82
99
|
# @return [Array<String>] in declaration order
|
|
83
100
|
def left_out_names(layers: [], toolkit: @toolkit)
|
|
84
|
-
ordered(@entries.values.select { |entry|
|
|
101
|
+
ordered(@entries.values.select { |entry| left_out?(entry, layers, toolkit) }).map(&:name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The built-in +name+ names when the session's toolkit leaves it out
|
|
105
|
+
# but the tool index reaches it (#reachable_names).
|
|
106
|
+
# @return [Entry, nil]
|
|
107
|
+
def indexed(name, layers: [], toolkit: @toolkit)
|
|
108
|
+
entry = self[name]
|
|
109
|
+
entry if entry && call_offered?(toolkit) && soft?(entry, layers, toolkit)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The built-ins +toolkit+ leaves out of a turn under +layers+ that the
|
|
113
|
+
# tool index reaches (call_tool): not in its kit's list, but not
|
|
114
|
+
# removed either (remove:, --no-tool, /toolkit remove), with call_tool
|
|
115
|
+
# itself declared.
|
|
116
|
+
# @return [Array<String>] in declaration order
|
|
117
|
+
def reachable_names(layers: [], toolkit: @toolkit)
|
|
118
|
+
return [] unless call_offered?(toolkit)
|
|
119
|
+
|
|
120
|
+
ordered(@entries.values.select { |entry| soft?(entry, layers, toolkit) }).map(&:name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The tools +toolkit+ removes from a turn under +layers+ (remove:,
|
|
124
|
+
# --no-tool, /toolkit remove): only the user brings them back.
|
|
125
|
+
# @return [Array<String>] in declaration order
|
|
126
|
+
def removed_names(layers: [], toolkit: @toolkit)
|
|
127
|
+
return [] if toolkit.nil?
|
|
128
|
+
|
|
129
|
+
ordered(@entries.values.select { |entry| entry.offered?(layers) && toolkit.removed.include?(entry.name) })
|
|
130
|
+
.map(&:name)
|
|
85
131
|
end
|
|
86
132
|
|
|
87
133
|
# Every tool +source+ registered, declared or not (a plugin's tool set
|
|
@@ -112,7 +158,48 @@ module Samagotchi
|
|
|
112
158
|
|
|
113
159
|
private
|
|
114
160
|
|
|
115
|
-
|
|
161
|
+
# Declared by +toolkit+ (nil: every tool, full). The tool index's
|
|
162
|
+
# tools are declared when they reach something, whatever the toolkit:
|
|
163
|
+
# a source registered at load, or a built-in it leaves out but
|
|
164
|
+
# doesn't remove (#index_needed?); unless it removes them.
|
|
165
|
+
def declared?(entry, toolkit = @toolkit)
|
|
166
|
+
if index_entry?(entry)
|
|
167
|
+
!toolkit&.removed&.include?(entry.name) && index_needed?(toolkit)
|
|
168
|
+
else
|
|
169
|
+
toolkit.nil? || toolkit.declares?(entry)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def index_entry?(entry) = !@index.nil? && entry.source == @index.source
|
|
174
|
+
|
|
175
|
+
# From what the plugins registered at load and the toolkit alone, so
|
|
176
|
+
# the declared tools stay the same for a session's life. A built-in
|
|
177
|
+
# left out counts only while call_tool can reach it (not removed).
|
|
178
|
+
def index_needed?(toolkit)
|
|
179
|
+
return true if @index.sourced
|
|
180
|
+
return false if toolkit&.removed&.include?(@index.call_name)
|
|
181
|
+
|
|
182
|
+
@entries.each_value.any? { |entry| soft?(entry, [], toolkit) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def left_out?(entry, layers, toolkit)
|
|
186
|
+
return false unless entry.offered?(layers) && !declared?(entry, toolkit)
|
|
187
|
+
|
|
188
|
+
!index_entry?(entry) || toolkit&.removed&.include?(entry.name) == true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# A built-in +toolkit+ leaves out of a turn under +layers+ without
|
|
192
|
+
# removing it.
|
|
193
|
+
def soft?(entry, layers, toolkit)
|
|
194
|
+
!toolkit.nil? && entry.core? && entry.offered?(layers) && !toolkit.removed.include?(entry.name) &&
|
|
195
|
+
!toolkit.declares?(entry)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Whether +toolkit+ declares the index's call tool.
|
|
199
|
+
def call_offered?(toolkit)
|
|
200
|
+
entry = @index && self[@index.call_name]
|
|
201
|
+
!entry.nil? && declared?(entry, toolkit)
|
|
202
|
+
end
|
|
116
203
|
|
|
117
204
|
def ordered(entries)
|
|
118
205
|
core, added = entries.partition(&:core?)
|