samagotchi 0.68.0 → 0.70.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 +76 -1
- data/docs/architecture.md +1 -1
- data/docs/cli.md +6 -1
- data/docs/configuration.md +31 -8
- data/docs/context.md +15 -6
- data/docs/guardrails.md +23 -1
- data/docs/hooks.md +5 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +25 -13
- data/docs/internals/prompt-caching.md +9 -2
- data/docs/internals/tool-guardrails.md +61 -1
- data/docs/plugins.md +66 -38
- data/docs/releasing.md +14 -6
- data/docs/sessions.md +5 -5
- data/docs/sub-agent.md +2 -1
- data/docs/testing.md +8 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +10 -8
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +4 -4
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +6 -6
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +3 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +6 -5
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +10 -8
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +4 -4
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +6 -6
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +3 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +6 -5
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +6 -21
- data/lib/samagotchi/context_absorber.rb +35 -21
- data/lib/samagotchi/engine.rb +7 -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 +34 -7
- data/lib/samagotchi/llm/errors.rb +47 -1
- data/lib/samagotchi/llm/http.rb +20 -2
- data/lib/samagotchi/llm_context_stale.rb +8 -2
- data/lib/samagotchi/log_subscriber.rb +1 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +3 -0
- data/lib/samagotchi/model_price.rb +2 -4
- data/lib/samagotchi/peer_sources.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/peer.rb +35 -16
- 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_output_cut.rb +312 -0
- data/lib/samagotchi/tool_output_files.rb +73 -0
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +70 -15
- 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/delegate.rb +4 -1
- data/lib/samagotchi/tools/delegate_wait.rb +12 -1
- data/lib/samagotchi/tools/ref_text.rb +27 -0
- data/lib/samagotchi/tools/registry.rb +95 -8
- data/lib/samagotchi/tools/task_wait.rb +4 -1
- 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 +9 -4
- 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
- data/lib/samagotchi/worker.rb +2 -2
- data/lib/samagotchi/worker_inbound.rb +3 -0
- metadata +5 -1
|
@@ -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)
|
|
@@ -66,7 +66,10 @@ module Samagotchi
|
|
|
66
66
|
return child_id if child_id.start_with?("Error:")
|
|
67
67
|
|
|
68
68
|
note = warning ? "Warning: #{warning}\n" : ""
|
|
69
|
-
|
|
69
|
+
if wait
|
|
70
|
+
result = DelegateWait.call(child_id, peers: peers, timeout: timeout)
|
|
71
|
+
return RefText.wrap(note + result, result.respond_to?(:ref_path) ? result.ref_path : nil)
|
|
72
|
+
end
|
|
70
73
|
|
|
71
74
|
started = session.to_s.strip.empty? ? "Started a delegate session" : "Sent the follow-up to delegate #{child_id[0, 8]}"
|
|
72
75
|
"#{note}session: #{child_id}\nstatus: running\n#{started}; #{running_hint(peers)} " \
|
|
@@ -7,6 +7,7 @@ require_relative "output_guardrails"
|
|
|
7
7
|
require_relative "peers"
|
|
8
8
|
require_relative "delegate_relay"
|
|
9
9
|
require_relative "delegate_cursor"
|
|
10
|
+
require_relative "ref_text"
|
|
10
11
|
|
|
11
12
|
module Samagotchi
|
|
12
13
|
# Loaded on first use: child_reports requires this file (a circular
|
|
@@ -78,7 +79,8 @@ module Samagotchi
|
|
|
78
79
|
unless wait.status == :waiting_for_answer && DelegateRelay.relayable?(relay, wait.question)
|
|
79
80
|
outcomes.concat(relayed_outcomes.delete(key) || [])
|
|
80
81
|
advance(peers.session_id, child_id, wait, state_dir: sd)
|
|
81
|
-
|
|
82
|
+
text = with_outcomes(finish(wait, child_id, timeout: timeout, reports: reports), outcomes)
|
|
83
|
+
return RefText.wrap(text, reply_path(child_id, wait, state_dir: sd))
|
|
82
84
|
end
|
|
83
85
|
|
|
84
86
|
outcome = DelegateRelay.call(child_id, wait.question, relay: relay, state_dir: sd, more: others&.count.to_i)
|
|
@@ -104,6 +106,15 @@ module Samagotchi
|
|
|
104
106
|
})
|
|
105
107
|
end
|
|
106
108
|
|
|
109
|
+
# The child's reply file a :done wait read (its output/ file), which
|
|
110
|
+
# ToolRunner's cut of a long reply names (ToolOutputCut's
|
|
111
|
+
# :head_tail_ref); nil for any other outcome.
|
|
112
|
+
def reply_path(child_id, wait, state_dir:)
|
|
113
|
+
return nil unless wait.status == :done && wait.file
|
|
114
|
+
|
|
115
|
+
File.join(ReplyWait.reply_dir(child_id, state_dir: state_dir), wait.file)
|
|
116
|
+
end
|
|
117
|
+
|
|
107
118
|
# The relayed approvals' outcome lines, after the status line.
|
|
108
119
|
def with_outcomes(text, outcomes)
|
|
109
120
|
return text if outcomes.empty?
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module Tools
|
|
5
|
+
# A tool's text that shows part of a file kept elsewhere in whole: a
|
|
6
|
+
# task's log (task_wait's output tail), a child's reply file (delegate,
|
|
7
|
+
# delegate_result). It is the text itself (a String), like
|
|
8
|
+
# Plugin::ToolResult; #ref_path is the file, which ToolRunner's cut
|
|
9
|
+
# names when the text is longer than max_tool_output_chars
|
|
10
|
+
# (ToolOutputCut's :head_tail_ref), instead of saving a copy;
|
|
11
|
+
# #stderr_path, a handed-over execute's stderr log, the cut names too.
|
|
12
|
+
class RefText < String
|
|
13
|
+
attr_reader :ref_path, :stderr_path
|
|
14
|
+
|
|
15
|
+
def initialize(text, ref_path, stderr_path: nil)
|
|
16
|
+
@ref_path = ref_path
|
|
17
|
+
@stderr_path = stderr_path.to_s.empty? ? nil : stderr_path
|
|
18
|
+
super(text.to_s)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# +text+ as a RefText of +ref_path+; +text+ as it is with no path.
|
|
22
|
+
def self.wrap(text, ref_path, stderr_path: nil)
|
|
23
|
+
ref_path.to_s.empty? ? text : new(text, ref_path, stderr_path: stderr_path)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -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?)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "task_runtime"
|
|
4
|
+
require_relative "ref_text"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
6
7
|
module Tools
|
|
@@ -121,7 +122,9 @@ module Samagotchi
|
|
|
121
122
|
lines << output_tail.rstrip unless output_tail.empty?
|
|
122
123
|
stderr_tail = stderr_tail(record, tail_lines)
|
|
123
124
|
lines.concat(["stderr_tail:", stderr_tail.rstrip]) unless stderr_tail.empty?
|
|
124
|
-
|
|
125
|
+
# The whole log is at output_path (stderr at stderr_path): a cut of
|
|
126
|
+
# this text names them.
|
|
127
|
+
RefText.wrap(lines.join("\n"), record.output_path, stderr_path: record.stderr_path)
|
|
125
128
|
end
|
|
126
129
|
private_class_method :format_response
|
|
127
130
|
|