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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +10 -8
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +4 -4
- data/docs/internals/attached-context.md +8 -6
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/bridge.md +18 -5
- data/docs/internals/engine.md +221 -100
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +12 -6
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +16 -1
- data/lib/samagotchi/answer_text.rb +1 -2
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bridge/card_places.rb +56 -7
- data/lib/samagotchi/bridge/card_store.rb +24 -9
- data/lib/samagotchi/bridge.rb +9 -0
- data/lib/samagotchi/bridge_client.rb +9 -5
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
- data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
- data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
- data/lib/samagotchi/engine/context_judge.rb +19 -125
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/objectives.rb +22 -90
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +5 -2
- data/lib/samagotchi/engine/toolkit.rb +33 -190
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +162 -99
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/engine_snapshots.rb +9 -8
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +6 -4
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/idle_recap.rb +3 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +194 -216
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +66 -42
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/session_manager.rb +8 -13
- data/lib/samagotchi/session_retention.rb +24 -4
- data/lib/samagotchi/steer.rb +12 -1
- data/lib/samagotchi/stream_event.rb +22 -0
- data/lib/samagotchi/system_prompt.rb +84 -61
- data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
- data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
- data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
- data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
- data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/tool_activity.rb +8 -6
- data/lib/samagotchi/tool_declarations.rb +4 -4
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +93 -101
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/registry.rb +43 -40
- data/lib/samagotchi/tools/tool_index.rb +135 -88
- data/lib/samagotchi/tools/tool_surface.rb +173 -0
- data/lib/samagotchi/turn_context.rb +67 -0
- data/lib/samagotchi/turn_flow.rb +7 -2
- data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
- data/lib/samagotchi/turn_stages/objectives.rb +146 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
- data/lib/samagotchi/turn_stages/warmup.rb +83 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -1
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +8 -1
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/card_stamps.js +27 -0
- data/lib/samagotchi/web/public/cards_place.js +5 -3
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/composer_mode.js +1 -1
- data/lib/samagotchi/web/public/format.js +3 -2
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +8 -4
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +8 -0
- data/lib/samagotchi/web/session_show.rb +4 -0
- data/lib/samagotchi/worker.rb +8 -5
- data/lib/samagotchi.rb +0 -1
- metadata +19 -2
- 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.
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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,
|
|
30
|
-
# for, run in its place before
|
|
31
|
-
# of chi's own tools:
|
|
32
|
-
# it is; nil on every
|
|
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
|
|
47
|
-
# tools a host that loads tools may declare deferred)
|
|
48
|
-
# (call → the call_tool call a direct call of one
|
|
49
|
-
# nil)
|
|
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
|
|
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 (
|
|
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 +
|
|
74
|
-
# reaches its group when
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
252
|
-
# sources'. A source group with a reserved name,
|
|
253
|
-
# group has, is left out of search, lookup and the
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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,
|
|
300
|
-
registry.register(spec.name, source: SOURCE, unwrap: unwrap,
|
|
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: ->
|
|
305
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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
|
|
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,
|
|
406
|
-
return nil unless
|
|
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
|
-
|
|
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
|
|
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? ||
|
|
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
|
-
|
|
487
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
619
|
+
def described_groups(surface)
|
|
620
|
+
toolkit = surface.toolkit
|
|
621
|
+
return @described unless toolkit && toolkit.groups != :all
|
|
578
622
|
|
|
579
|
-
@described.select { |name, _line|
|
|
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 =
|
|
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
|
-
#
|
|
595
|
-
#
|
|
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
|
-
|
|
600
|
-
return [] if
|
|
642
|
+
def registry_groups(surface)
|
|
643
|
+
entries = surface.index_only
|
|
644
|
+
return [] if entries.empty?
|
|
601
645
|
|
|
602
|
-
tools =
|
|
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
|
-
|
|
708
|
-
|
|
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
|