samagotchi 0.154.1 → 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 +16 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- 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/engine.md +220 -99
- 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 +9 -5
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +2 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/manifest.yml +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/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/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +190 -213
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +63 -41
- 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/system_prompt.rb +37 -30
- 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 +91 -100
- 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_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/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -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/composer_fold.js +3 -0
- 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/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
- metadata +17 -1
|
@@ -19,9 +19,11 @@ module Samagotchi
|
|
|
19
19
|
# in +messages+ (Engine#run_turn resume_from:); the replies from there on
|
|
20
20
|
# keep the model's reasoning (thought blocks), as within one run. nil:
|
|
21
21
|
# the run starts here, as every other turn's does.
|
|
22
|
+
# +context+: the run's turn (TurnContext, Engine#generate's), handed down
|
|
23
|
+
# the tool path explicitly; nil: the backend's own (a bare kernel's).
|
|
22
24
|
class ModelBackend
|
|
23
25
|
def complete(messages:, max_iterations: IterationLimit::DEFAULT, on_stream_event: nil, cancel_controller: nil,
|
|
24
|
-
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil)
|
|
26
|
+
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil, context: nil)
|
|
25
27
|
raise NotImplementedError, "#{self.class}#complete must be implemented"
|
|
26
28
|
end
|
|
27
29
|
end
|
|
@@ -24,7 +24,7 @@ require_relative "../tool_runner"
|
|
|
24
24
|
require_relative "../tool_response"
|
|
25
25
|
require_relative "../tool_ids"
|
|
26
26
|
require_relative "../tool_declarations"
|
|
27
|
-
require_relative "../tools/
|
|
27
|
+
require_relative "../tools/tool_surface"
|
|
28
28
|
require_relative "../tool_loads"
|
|
29
29
|
require_relative "../vision_context"
|
|
30
30
|
require_relative "../log"
|
|
@@ -79,8 +79,11 @@ module Samagotchi
|
|
|
79
79
|
# prompt's part every session shares (SystemPrompt#stable_length), or nil
|
|
80
80
|
# @param sleeper [#call, nil] waits the given seconds before a dropped
|
|
81
81
|
# stream's step is asked again (specs pass one that doesn't sleep)
|
|
82
|
-
|
|
82
|
+
# @param context [TurnContext, nil] the turn its helpers read before any
|
|
83
|
+
# #complete (a bench building wire messages); a run's replaces it
|
|
84
|
+
def initialize(kernel:, adapter: nil, stable_length: nil, sleeper: nil, context: nil)
|
|
83
85
|
@kernel = kernel
|
|
86
|
+
@turn_context = context
|
|
84
87
|
@adapter = adapter
|
|
85
88
|
@stable_length = stable_length
|
|
86
89
|
@sleeper = sleeper || ->(seconds) { sleep(seconds) }
|
|
@@ -93,10 +96,14 @@ module Samagotchi
|
|
|
93
96
|
|
|
94
97
|
# +reasoning_since+ (ModelBackend#complete): the resumed run's start,
|
|
95
98
|
# the Run's own start then (Run#initialize, #reasoning_boundary).
|
|
99
|
+
# +context+: the run's turn (TurnContext, Engine#generate's): its
|
|
100
|
+
# settings are what the requests send, its tool calls run in it; a
|
|
101
|
+
# bare loop's (#turn_context) when none is given.
|
|
96
102
|
def complete(messages:, max_iterations: IterationLimit::DEFAULT, on_stream_event: nil, cancel_controller: nil,
|
|
97
|
-
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil)
|
|
103
|
+
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil, context: nil)
|
|
98
104
|
raise ArgumentError, "ChatLoop has no adapter" unless @adapter
|
|
99
105
|
|
|
106
|
+
@turn_context = context if context
|
|
100
107
|
conversation = Array(messages).map(&:dup)
|
|
101
108
|
Run.new(self, conversation, on_stream_event, cancel_controller, model_name, pending_input, start: reasoning_since)
|
|
102
109
|
.call(max_iterations: max_iterations || 1, cap: KernelLoop.resolve_output_char_cap(max_tool_output_chars))
|
|
@@ -108,14 +115,14 @@ module Samagotchi
|
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
# Tool definitions for the request: the schemas the native prompts are
|
|
111
|
-
# rendered from (the
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
# follows, deferred (
|
|
118
|
+
# rendered from (the turn's ToolSurface: what it declares), with the
|
|
119
|
+
# chat-only enums and closed parameters. On an adapter that loads
|
|
120
|
+
# tools for +model+ (#loads_tools?, asked per request: a refusal turns
|
|
121
|
+
# it off mid-turn), every tool the session reaches but doesn't declare
|
|
122
|
+
# follows, deferred (ToolSurface#deferred_for: +conversation+, the
|
|
116
123
|
# request's, names the ones it loaded); never without a declared one.
|
|
117
124
|
def tool_definitions(conversation = nil, model: nil)
|
|
118
|
-
declared = ToolDeclarations.chat_schemas(
|
|
125
|
+
declared = ToolDeclarations.chat_schemas(surface.schemas).map do |schema|
|
|
119
126
|
{ type: "function", function: schema.slice(:name, :description, :parameters) }
|
|
120
127
|
end
|
|
121
128
|
return declared if declared.empty? || !loads_tools?(model)
|
|
@@ -127,10 +134,13 @@ module Samagotchi
|
|
|
127
134
|
end
|
|
128
135
|
|
|
129
136
|
# The tools a request on a host that loads tools declares deferred
|
|
130
|
-
# (
|
|
131
|
-
def deferred_tools(conversation)
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
# (ToolSurface#deferred_for).
|
|
138
|
+
def deferred_tools(conversation) = surface.deferred_for(conversation)
|
|
139
|
+
|
|
140
|
+
# The turn's ToolSurface (#turn_context's), else one computed now from
|
|
141
|
+
# the kernel's tools (a bare loop).
|
|
142
|
+
# @return [Tools::ToolSurface]
|
|
143
|
+
def surface = Tools::ToolSurface.of(turn_context, tools)
|
|
134
144
|
|
|
135
145
|
# Whether the adapter takes tools it doesn't declare up front for
|
|
136
146
|
# +model+ (AnthropicMessages#loads_tools?); OpenAIChat doesn't.
|
|
@@ -147,22 +157,33 @@ module Samagotchi
|
|
|
147
157
|
# The kernel's tools.
|
|
148
158
|
def tools = @kernel.tools
|
|
149
159
|
|
|
150
|
-
# The
|
|
151
|
-
|
|
160
|
+
# The TurnContext of the run in progress, else of the last run
|
|
161
|
+
# (#complete's context:); before any, the one it was built with, else a
|
|
162
|
+
# context of the kernel's own settings (a bare loop). Read on the turn's thread only.
|
|
163
|
+
# @return [TurnContext]
|
|
164
|
+
def turn_context = @turn_context || TurnContext.none(settings: @kernel.turn_settings)
|
|
165
|
+
|
|
166
|
+
# The turn's LLM::TurnSettings (#turn_context's).
|
|
167
|
+
def settings = turn_context.settings
|
|
168
|
+
|
|
169
|
+
# The turn's LLM context layers (TurnContext#llm_context_layers).
|
|
170
|
+
def llm_context_layers = turn_context.llm_context_layers
|
|
152
171
|
|
|
153
172
|
# The turn's LLM context strategy (LLMContextStrategy::Resolved), nil
|
|
154
173
|
# when none was resolved.
|
|
155
|
-
def llm_context =
|
|
174
|
+
def llm_context = settings&.llm_context
|
|
156
175
|
|
|
157
176
|
# KernelLoop#reprefilled_tokens, the kernel's request history.
|
|
158
177
|
def reprefilled_tokens(model, **) = @kernel.reprefilled_tokens(model, **)
|
|
159
178
|
|
|
160
|
-
# The kernel's LLMContextView
|
|
161
|
-
def llm_context_view = @kernel.llm_context_view
|
|
179
|
+
# The kernel's LLMContextView under the turn's strategy.
|
|
180
|
+
def llm_context_view = @kernel.llm_context_view(llm_context)
|
|
162
181
|
|
|
163
182
|
# The strategy's own edits before a request and at turn end
|
|
164
|
-
# (KernelLoop#apply_llm_context!).
|
|
165
|
-
def apply_llm_context!(conversation, **)
|
|
183
|
+
# (KernelLoop#apply_llm_context!), under the turn's strategy.
|
|
184
|
+
def apply_llm_context!(conversation, **)
|
|
185
|
+
@kernel.apply_llm_context!(conversation, llm_context, model: settings&.model_name, **)
|
|
186
|
+
end
|
|
166
187
|
|
|
167
188
|
# The running turn, for forget_outputs (KernelLoop#llm_context_turn).
|
|
168
189
|
def llm_context_turn=(turn)
|
|
@@ -267,7 +288,7 @@ module Samagotchi
|
|
|
267
288
|
TOOL_IMAGES_TEXT = "[images from tool results]"
|
|
268
289
|
|
|
269
290
|
# Whether this turn's requests carry tool changes (the turn's model).
|
|
270
|
-
def tool_change_wire? = tool_changes?(
|
|
291
|
+
def tool_change_wire? = tool_changes?(settings&.model_name)
|
|
271
292
|
|
|
272
293
|
# The first index whose model entries carry their reasoning (nil:
|
|
273
294
|
# none do) under +level+. Under turn: +since+ when given, where a
|
|
@@ -298,14 +319,14 @@ module Samagotchi
|
|
|
298
319
|
conversation.drop(boundary).sum { |entry| entry[:role].to_s == "model" && thought?(entry) ? entry[:thinking].length : 0 }
|
|
299
320
|
end
|
|
300
321
|
|
|
301
|
-
# The turn's ReasoningHistory level (the
|
|
322
|
+
# The turn's ReasoningHistory level (the turn's settings); drop
|
|
302
323
|
# without settings, and for a +model+ whose host refused it
|
|
303
324
|
# (#reasoning_history_refused!). An adapter that sends its blocks back
|
|
304
325
|
# (#replays_content?) is floored at turn: the running run's thinking
|
|
305
326
|
# blocks always go back (a tool loop with thinking needs the last
|
|
306
327
|
# assistant turn's), whatever the level says.
|
|
307
328
|
def reasoning_history(model = nil)
|
|
308
|
-
level = model && @refusals.refused?(:reasoning_history, model) ? :drop :
|
|
329
|
+
level = model && @refusals.refused?(:reasoning_history, model) ? :drop : settings&.reasoning_history
|
|
309
330
|
level ||= :drop
|
|
310
331
|
level == :drop && replays_content? ? :turn : level
|
|
311
332
|
end
|
|
@@ -320,18 +341,17 @@ module Samagotchi
|
|
|
320
341
|
@refusals.remember(:reasoning_history, model)
|
|
321
342
|
end
|
|
322
343
|
|
|
323
|
-
# The turn's VisionContext (
|
|
324
|
-
|
|
325
|
-
def vision = @kernel.turn_settings.vision
|
|
344
|
+
# The turn's VisionContext (its settings'), or nil.
|
|
345
|
+
def vision = settings.vision
|
|
326
346
|
|
|
327
|
-
# The turn's configured price (
|
|
328
|
-
def price =
|
|
347
|
+
# The turn's configured price (its settings'), or nil.
|
|
348
|
+
def price = settings&.price
|
|
329
349
|
|
|
330
|
-
# The turn's request parameters (
|
|
331
|
-
def sampling =
|
|
350
|
+
# The turn's request parameters (its settings').
|
|
351
|
+
def sampling = settings.sampling || {}
|
|
332
352
|
|
|
333
|
-
# The turn's thinking level (
|
|
334
|
-
def thinking =
|
|
353
|
+
# The turn's thinking level (its settings').
|
|
354
|
+
def thinking = settings.thinking || Thinking::DEFAULT
|
|
335
355
|
|
|
336
356
|
# The request fields the thinking level adds, as the adapter's wire
|
|
337
357
|
# takes them (OpenAIChat#thinking_fields: Thinking.chat_fields); none
|
|
@@ -375,24 +395,24 @@ module Samagotchi
|
|
|
375
395
|
def strip_model_thought(text) = @kernel.strip_model_thought(text)
|
|
376
396
|
|
|
377
397
|
def tool_runner
|
|
378
|
-
@tool_runner ||= ToolRunner.new(@kernel, window: -> { context_window(
|
|
398
|
+
@tool_runner ||= ToolRunner.new(@kernel, window: ->(run_settings) { context_window(run_settings&.model_name, run_settings) })
|
|
379
399
|
end
|
|
380
400
|
|
|
381
401
|
# The window for +model+: the kernel's client probes the server it
|
|
382
402
|
# points at (Engine keeps it on the effective model's host; a local
|
|
383
403
|
# llama.cpp answers /props), then the host's model list, the model's
|
|
384
404
|
# or host's window_tokens, config, env and the default. A remote
|
|
385
|
-
# provider has no /props to probe.
|
|
386
|
-
|
|
405
|
+
# provider has no /props to probe. +run_settings+: the LLM::TurnSettings
|
|
406
|
+
# whose window_setting counts (the turn's by default).
|
|
407
|
+
def context_window(model, run_settings = settings)
|
|
387
408
|
client = @adapter.remote? ? nil : @kernel.client
|
|
388
|
-
ContextWindow.resolve(client: client, model: model, adapter: @adapter,
|
|
389
|
-
setting: (@kernel.turn_settings&.window_setting if @kernel.respond_to?(:turn_settings)))
|
|
409
|
+
ContextWindow.resolve(client: client, model: model, adapter: @adapter, setting: run_settings&.window_setting)
|
|
390
410
|
rescue StandardError
|
|
391
411
|
nil
|
|
392
412
|
end
|
|
393
413
|
|
|
394
|
-
# The kernel's generation guard (KernelLoop#guard_generation).
|
|
395
|
-
def guard_generation = @kernel.respond_to?(:guard_generation) ? @kernel.guard_generation : nil
|
|
414
|
+
# The kernel's generation guard for the turn (KernelLoop#guard_generation).
|
|
415
|
+
def guard_generation = @kernel.respond_to?(:guard_generation) ? @kernel.guard_generation(turn_context) : nil
|
|
396
416
|
|
|
397
417
|
# A hook failing must not break the turn (as in ToolRunner).
|
|
398
418
|
def fire_hook(name, event)
|
|
@@ -609,8 +629,10 @@ module Samagotchi
|
|
|
609
629
|
@deferred = []
|
|
610
630
|
@context = ContextStatus.new(conversation: conversation, llm_context: loop.llm_context,
|
|
611
631
|
counted_chars_per_token: loop.counted_chars_per_token)
|
|
632
|
+
# The run's turn: its tool calls run in it.
|
|
633
|
+
@turn_context = loop.turn_context
|
|
612
634
|
loop.llm_context_turn = LLMContextForget::Turn.new(conversation: conversation, context: @context,
|
|
613
|
-
emit: method(:emit))
|
|
635
|
+
emit: method(:emit), turn_id: @turn_context.id)
|
|
614
636
|
end
|
|
615
637
|
|
|
616
638
|
def call(max_iterations:, cap:)
|
|
@@ -1094,7 +1116,7 @@ module Samagotchi
|
|
|
1094
1116
|
def dispatch(tool_calls, iteration, cap)
|
|
1095
1117
|
log_direct_deferred(tool_calls)
|
|
1096
1118
|
calls = tool_calls.map { |tool_call| NativeToolNormalizer.normalize(tool_call) }
|
|
1097
|
-
runs = ToolResponse.run_batch(@loop.tool_runner, calls, iteration: iteration, emit: method(:emit),
|
|
1119
|
+
runs = ToolResponse.run_batch(@loop.tool_runner, calls, context: @turn_context, iteration: iteration, emit: method(:emit),
|
|
1098
1120
|
on_stream_event: @on_stream_event, cap: cap) do |run, index|
|
|
1099
1121
|
@conversation << ToolResponse.single(run, tool_call_id: tool_calls[index].id,
|
|
1100
1122
|
ids: ToolIds.next_ids(@conversation, 1))
|
|
@@ -17,7 +17,7 @@ module Samagotchi
|
|
|
17
17
|
def provider = :native
|
|
18
18
|
|
|
19
19
|
def complete(messages:, max_iterations: IterationLimit::DEFAULT, on_stream_event: nil, cancel_controller: nil,
|
|
20
|
-
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil)
|
|
20
|
+
model_name: nil, max_tool_output_chars: nil, pending_input: nil, reasoning_since: nil, context: nil)
|
|
21
21
|
@kernel.run(
|
|
22
22
|
messages,
|
|
23
23
|
max_iterations: max_iterations,
|
|
@@ -26,7 +26,8 @@ module Samagotchi
|
|
|
26
26
|
model_name: model_name,
|
|
27
27
|
max_tool_output_chars: max_tool_output_chars,
|
|
28
28
|
pending_input: pending_input,
|
|
29
|
-
reasoning_since: reasoning_since
|
|
29
|
+
reasoning_since: reasoning_since,
|
|
30
|
+
context: context || @kernel.context
|
|
30
31
|
)
|
|
31
32
|
end
|
|
32
33
|
end
|
|
@@ -52,9 +52,11 @@ module Samagotchi
|
|
|
52
52
|
# The turn a forget_outputs call runs in: its conversation (the
|
|
53
53
|
# entries the edits are saved on), its ContextStatus (nil: none) and
|
|
54
54
|
# its stream (+emit+, nil: none), which gets an applied batch's ✂ row
|
|
55
|
-
# (LLMContextNotice)
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# (LLMContextNotice); +turn_id+: the Engine turn the run is for
|
|
56
|
+
# (TurnContext#id; nil: none), so a reader tells the running turn's
|
|
57
|
+
# conversation from the last one's (Engine#live_conversation).
|
|
58
|
+
Turn = Data.define(:conversation, :context, :emit, :turn_id) do
|
|
59
|
+
def initialize(emit: nil, turn_id: nil, **fields) = super
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
# One output a call may name: its entry, its ToolIds ref, its tool,
|
|
@@ -8,7 +8,7 @@ module Samagotchi
|
|
|
8
8
|
# An Engine's model choice (ModelChoice: the model as given and as typed)
|
|
9
9
|
# and the prompt profile resolved for it, under one mutex; the counterpart
|
|
10
10
|
# of ToolkitBinding. The memory overlay keys follow the choice
|
|
11
|
-
# (#sync_model_key!).
|
|
11
|
+
# (#sync_model_key!), which each turn's TurnContext takes.
|
|
12
12
|
#
|
|
13
13
|
# The choice changes in one assignment under the mutex (#switch!), so a
|
|
14
14
|
# reader on another thread sees the old pair or the new one. A resolution
|
|
@@ -126,13 +126,13 @@ module Samagotchi
|
|
|
126
126
|
# The memory overlay key follows the model sent (ModelOverlay.key_for
|
|
127
127
|
# its id). A model typed as an alias used to key overlays by the alias
|
|
128
128
|
# (before aliases were applied there): that key is read when the
|
|
129
|
-
# target's overlay is missing
|
|
129
|
+
# target's overlay is missing. A turn takes both in its TurnContext
|
|
130
|
+
# (Engine#generate): memory_read and the gate read them there.
|
|
130
131
|
def sync_model_key!
|
|
131
132
|
choice = @choice
|
|
132
133
|
@model_key = ModelOverlay.key_for(@bare_name.call(choice.name))
|
|
133
134
|
typed_key = ModelOverlay.key_for(@registry.parse_qualified_model(choice.typed).last)
|
|
134
135
|
@fallback_model_key = typed_key == @model_key ? nil : typed_key
|
|
135
|
-
@kernel.call.sync_model_key!(@model_key, fallback: @fallback_model_key)
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
end
|
|
@@ -63,6 +63,13 @@ module Samagotchi
|
|
|
63
63
|
@mutex.synchronize { @sections.any? { |section| section.bundle == bundle.to_s && section.name == name.to_s } }
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# The names +bundle+ registered, in prompt order (what it registered, for
|
|
67
|
+
# Testing::Install and chi bundle check).
|
|
68
|
+
# @return [Array<String>]
|
|
69
|
+
def names(bundle)
|
|
70
|
+
@mutex.synchronize { @sections.select { |section| section.bundle == bundle.to_s }.map(&:name) }
|
|
71
|
+
end
|
|
72
|
+
|
|
66
73
|
# Run every block for +session_id+ and keep what they gave.
|
|
67
74
|
# @param session_id [String, nil] whose last good text a failure keeps
|
|
68
75
|
# @return [Array<Built>] in prompt order, the sections with text
|
|
@@ -118,7 +118,7 @@ module Samagotchi
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Ask the model's question (ask_user_question): the kernel's
|
|
121
|
-
# question_handler, called on the turn thread with the payload
|
|
121
|
+
# Services#question_handler, called on the turn thread with the payload
|
|
122
122
|
# Tools::AskUserQuestion.validate made. Opens it (#open_question) and
|
|
123
123
|
# returns the answer as JSON for the tool result.
|
|
124
124
|
# @param payload [Hash] {question:, options:, header:, multi_select:, allow_freeform:},
|
|
@@ -5,7 +5,7 @@ require_relative "verify_anchor"
|
|
|
5
5
|
module Samagotchi
|
|
6
6
|
# The earlier turn a Revise turn rewrites the answer of, carried only by
|
|
7
7
|
# a Revise turn (TurnAnchors). Its stamp on the turn's opening message
|
|
8
|
-
# is display metadata, never in the prompt (
|
|
8
|
+
# is display metadata, never in the prompt (TurnStages::VerifyAnchors#revision_target).
|
|
9
9
|
ReviseAnchor = Data.define(:turn_id) do
|
|
10
10
|
def self.from(value)
|
|
11
11
|
return value if value.is_a?(self)
|
|
@@ -8,6 +8,7 @@ require_relative "memory_paths"
|
|
|
8
8
|
require_relative "paths"
|
|
9
9
|
require_relative "tool_declarations"
|
|
10
10
|
require_relative "tools/memory"
|
|
11
|
+
require_relative "tools/tool_surface"
|
|
11
12
|
require_relative "muted_memories"
|
|
12
13
|
require_relative "model_notes"
|
|
13
14
|
require_relative "bundle_needs"
|
|
@@ -46,7 +47,8 @@ module Samagotchi
|
|
|
46
47
|
attr_reader :requested_memories
|
|
47
48
|
|
|
48
49
|
# @param profile [#call] → ModelProfile
|
|
49
|
-
# @param tools [#call] → Tools::Registry
|
|
50
|
+
# @param tools [#call] → Tools::Registry, nil (no tools): each build
|
|
51
|
+
# reads it as a Tools::ToolSurface under the build's layers
|
|
50
52
|
# @param session [#call] → Session, nil
|
|
51
53
|
# @param thinking [#call] → Symbol, the effective model's level (Thinking)
|
|
52
54
|
# @param model [#call] → ModelIdentity, nil (the section is left out)
|
|
@@ -91,8 +93,10 @@ module Samagotchi
|
|
|
91
93
|
|
|
92
94
|
# A prompt #build made, with what it read while it did: the memory
|
|
93
95
|
# index sizes (#memory_index), the model notes, the files and the
|
|
94
|
-
# plugins' sections it holds,
|
|
95
|
-
|
|
96
|
+
# plugins' sections it holds, its stable part's length (chat) and the
|
|
97
|
+
# Tools::ToolSurface it declares (the tools, the not-loaded line, the
|
|
98
|
+
# kit's prompt text).
|
|
99
|
+
Built = Data.define(:prompt, :memory_index, :notes, :files, :sections, :stable_length, :surface)
|
|
96
100
|
|
|
97
101
|
# The full prompt: #base wrapped with the index and the rest. Built once
|
|
98
102
|
# per loop and level (until #reset!) so the prompt prefix, and the
|
|
@@ -110,7 +114,7 @@ module Samagotchi
|
|
|
110
114
|
# @param chat [Boolean] for the chat loop
|
|
111
115
|
# @param thinking [Symbol, nil] the level (Thinking); nil: the effective model's
|
|
112
116
|
# @param layers [Array<Symbol>] the turn's LLM context layers: the
|
|
113
|
-
# native prompt declares the tools they add (Tools::
|
|
117
|
+
# native prompt declares the tools they add (Tools::ToolSurface#declared)
|
|
114
118
|
def build(chat: false, thinking: nil, layers: [])
|
|
115
119
|
key = [chat, thinking, layers]
|
|
116
120
|
cache = @cache
|
|
@@ -172,7 +176,8 @@ module Samagotchi
|
|
|
172
176
|
# The base prompt (specs, plugins' declarations), with Gemma's tool
|
|
173
177
|
# declarations at its end as #build has them.
|
|
174
178
|
def base(chat: false, layers: [])
|
|
175
|
-
|
|
179
|
+
surface = tool_surface(layers)
|
|
180
|
+
with_tail_declarations(assist_system_prompt(chat: chat, surface: surface), chat: chat, surface: surface)
|
|
176
181
|
end
|
|
177
182
|
|
|
178
183
|
# Names activated via preloaded --memory entries during prompt
|
|
@@ -192,12 +197,20 @@ module Samagotchi
|
|
|
192
197
|
@section_files = []
|
|
193
198
|
@section_plugins = []
|
|
194
199
|
@measured_stable_length = nil
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
surface = tool_surface(layers)
|
|
201
|
+
prompt = system_prompt_with_index(assist_system_prompt(chat: chat, surface: surface),
|
|
202
|
+
chat: chat, thinking: thinking, surface: surface)
|
|
197
203
|
Built.new(prompt: prompt, memory_index: @measured_index, notes: @section_notes || [],
|
|
198
|
-
files: @section_files.freeze, sections: @section_plugins.freeze, stable_length: @measured_stable_length
|
|
204
|
+
files: @section_files.freeze, sections: @section_plugins.freeze, stable_length: @measured_stable_length,
|
|
205
|
+
surface: surface)
|
|
199
206
|
end
|
|
200
207
|
|
|
208
|
+
# The session's tools as a build under +layers+ declares them, read
|
|
209
|
+
# from the Registry now (one per build: the declarations, the
|
|
210
|
+
# not-loaded line and the kit's text agree).
|
|
211
|
+
# @return [Tools::ToolSurface]
|
|
212
|
+
def tool_surface(layers) = Tools::ToolSurface.for(@tools_lookup.call, layers: layers)
|
|
213
|
+
|
|
201
214
|
def profile = @profile_lookup.call
|
|
202
215
|
|
|
203
216
|
def turn_thinking = @thinking_lookup.call
|
|
@@ -208,8 +221,8 @@ module Samagotchi
|
|
|
208
221
|
|
|
209
222
|
# ── Tool declarations ──────────────────────────────────────────────────────
|
|
210
223
|
|
|
211
|
-
def tool_declarations(
|
|
212
|
-
schemas = ToolDeclarations.native_schemas(
|
|
224
|
+
def tool_declarations(surface)
|
|
225
|
+
schemas = ToolDeclarations.native_schemas(surface)
|
|
213
226
|
case profile.name
|
|
214
227
|
when "qwen36"
|
|
215
228
|
ToolDeclarations.qwen_declarations(schemas)
|
|
@@ -232,7 +245,7 @@ module Samagotchi
|
|
|
232
245
|
|
|
233
246
|
# @param chat [Boolean] for the chat loop: no tool declarations or call
|
|
234
247
|
# syntax (its tools go as schemas with each request)
|
|
235
|
-
def assist_system_prompt(chat: false
|
|
248
|
+
def assist_system_prompt(surface:, chat: false)
|
|
236
249
|
return chat_system_prompt if chat
|
|
237
250
|
|
|
238
251
|
hint = tool_call_hint
|
|
@@ -241,7 +254,7 @@ module Samagotchi
|
|
|
241
254
|
tools = if gemma_tail_declarations?
|
|
242
255
|
"Your tools are declared at the end of this system prompt."
|
|
243
256
|
else
|
|
244
|
-
"You have access to the following tools:\n\n#{tool_declarations(
|
|
257
|
+
"You have access to the following tools:\n\n#{tool_declarations(surface)}\n"
|
|
245
258
|
end
|
|
246
259
|
|
|
247
260
|
<<~SYS
|
|
@@ -332,7 +345,7 @@ module Samagotchi
|
|
|
332
345
|
# memory write changes them) after all of them; the model, the location
|
|
333
346
|
# and the session close the prompt (Gemma's tool declarations still
|
|
334
347
|
# follow them).
|
|
335
|
-
def system_prompt_with_index(base, chat: false, thinking: nil,
|
|
348
|
+
def system_prompt_with_index(base, chat: false, thinking: nil, surface: tool_surface([]))
|
|
336
349
|
thinking_token = chat ? "" : Thinking.native(thinking || turn_thinking, profile).system_token
|
|
337
350
|
indexes = { project: read_memory_index("project"), system: read_memory_index("system") }
|
|
338
351
|
@measured_index = measure_indexes(indexes)
|
|
@@ -340,15 +353,15 @@ module Samagotchi
|
|
|
340
353
|
"Project memories:\n#{indexes[:project]}",
|
|
341
354
|
"System memories:\n#{indexes[:system]}"
|
|
342
355
|
].join("\n\n")
|
|
343
|
-
stable = [thinking_token + base, not_loaded_line(
|
|
344
|
-
project_specific_description, appended_section, kit_prompt_section, plugin_prompt_section, memory_sections]
|
|
356
|
+
stable = [thinking_token + base, not_loaded_line(surface), rg_guidance, system_identity_section, model_notes_section, explicit_memory_section,
|
|
357
|
+
project_specific_description, appended_section, kit_prompt_section(surface), plugin_prompt_section, memory_sections]
|
|
345
358
|
stable_text = stable.compact.join("\n")
|
|
346
359
|
volatile = [participants_section, current_model, project_location, current_session].compact
|
|
347
|
-
return with_tail_declarations(stable_text, chat: chat,
|
|
360
|
+
return with_tail_declarations(stable_text, chat: chat, surface: surface) if volatile.empty?
|
|
348
361
|
|
|
349
362
|
prompt = "#{stable_text}\n#{volatile.join("\n")}"
|
|
350
363
|
@measured_stable_length = stable_text.length + 1 if chat
|
|
351
|
-
with_tail_declarations(prompt, chat: chat,
|
|
364
|
+
with_tail_declarations(prompt, chat: chat, surface: surface)
|
|
352
365
|
end
|
|
353
366
|
|
|
354
367
|
# The tools the session's toolkit leaves out, named once right after the
|
|
@@ -357,15 +370,12 @@ module Samagotchi
|
|
|
357
370
|
# the prompt is what it was before toolkits. The ones call_tool reaches
|
|
358
371
|
# and the ones the toolkit removes (only the user brings those back)
|
|
359
372
|
# get a sentence each; with call_tool not declared, one list.
|
|
360
|
-
def not_loaded_line(
|
|
361
|
-
|
|
362
|
-
return nil unless registry.respond_to?(:left_out_names)
|
|
363
|
-
|
|
364
|
-
names = registry.left_out_names(layers: layers)
|
|
373
|
+
def not_loaded_line(surface)
|
|
374
|
+
names = surface.left_out.keys
|
|
365
375
|
return nil if names.empty?
|
|
366
376
|
|
|
367
|
-
toolkit = "(toolkit #{
|
|
368
|
-
reachable =
|
|
377
|
+
toolkit = "(toolkit #{surface.toolkit&.name})"
|
|
378
|
+
reachable = surface.index_only.map(&:name)
|
|
369
379
|
if reachable.empty?
|
|
370
380
|
return "Not loaded in this session #{toolkit}: #{names.join(", ")}. Only the user can load one (/toolkit add <name>)."
|
|
371
381
|
end
|
|
@@ -387,10 +397,7 @@ module Samagotchi
|
|
|
387
397
|
# sessions on that kit share it, and a memory write doesn't move it.
|
|
388
398
|
# nil on a kit without one and on full: the prompt is what it was
|
|
389
399
|
# before kit prompts.
|
|
390
|
-
def kit_prompt_section
|
|
391
|
-
registry = @tools_lookup.call
|
|
392
|
-
registry.respond_to?(:toolkit) ? registry.toolkit&.prompt : nil
|
|
393
|
-
end
|
|
400
|
+
def kit_prompt_section(surface) = surface.toolkit&.prompt
|
|
394
401
|
|
|
395
402
|
# The plugins' prompt sections (chi.prompt_section), right after the
|
|
396
403
|
# kit prompt and before the memory indexes: a plugin's text depends on
|
|
@@ -406,8 +413,8 @@ module Samagotchi
|
|
|
406
413
|
end
|
|
407
414
|
end
|
|
408
415
|
|
|
409
|
-
def with_tail_declarations(prompt, chat:,
|
|
410
|
-
chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations(
|
|
416
|
+
def with_tail_declarations(prompt, chat:, surface:)
|
|
417
|
+
chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations(surface)
|
|
411
418
|
end
|
|
412
419
|
|
|
413
420
|
def gemma_tail_declarations? = profile.name == "gemma4"
|
|
@@ -94,6 +94,13 @@ module Samagotchi
|
|
|
94
94
|
"Samagotchi::AnswerRefs.new" => "spec: after_turn's refs: helper, as chi's fire site builds it",
|
|
95
95
|
"Samagotchi::Log" => "spec: stubs Log to check what ctx.log recorded: a kit log recorder could replace it"
|
|
96
96
|
},
|
|
97
|
+
"inline-widgets" => {
|
|
98
|
+
'require "samagotchi/inline_widgets"' => "spec: for InlineWidgets",
|
|
99
|
+
"Samagotchi::InlineWidgets.parse" => "spec: each example the sections teach parses strictly, as chi web " \
|
|
100
|
+
"parses a block (the drawing is core's, so is the parse)",
|
|
101
|
+
"Samagotchi::Plugin::PromptSections::MAX" => "spec: the prompt-section limit a section over is dropped at: " \
|
|
102
|
+
"could be a stable constant"
|
|
103
|
+
},
|
|
97
104
|
"loop-guard" => {
|
|
98
105
|
'require "samagotchi/llm/native_tool_normalizer"' => "spec: for NativeToolNormalizer",
|
|
99
106
|
"Samagotchi::LLM::NativeToolNormalizer.normalize" => "spec: replays recorded calls in the shape chi's parser gives",
|
|
@@ -11,6 +11,7 @@ require_relative "../tools/tool_index"
|
|
|
11
11
|
require_relative "../peer_sources"
|
|
12
12
|
require_relative "../toolkit"
|
|
13
13
|
require_relative "../guardrails/gate"
|
|
14
|
+
require_relative "../turn_context"
|
|
14
15
|
require_relative "../memory_bundle/manifest"
|
|
15
16
|
require_relative "../memory_bundle/source"
|
|
16
17
|
require_relative "../session_setup"
|
|
@@ -239,8 +240,9 @@ module Samagotchi
|
|
|
239
240
|
dispatched = nil
|
|
240
241
|
capture = Hooks::Registry.new
|
|
241
242
|
capture.register(:before_tool_call) { |event| dispatched = event.dup }
|
|
242
|
-
gate = Guardrails::Gate.new(
|
|
243
|
-
gate.evaluate(call, iteration: iteration, params: params,
|
|
243
|
+
gate = Guardrails::Gate.new(hooks: capture, tools: tools, context_for: ->(_turn) { context })
|
|
244
|
+
gate.evaluate(call, context: TurnContext.none, iteration: iteration, params: params,
|
|
245
|
+
verdict: verdict || Verdict.new(call: call))
|
|
244
246
|
raise ArgumentError, "guardrails did not dispatch a before_tool_call event for #{call[:name].inspect}" unless dispatched
|
|
245
247
|
|
|
246
248
|
dispatched.slice(:type, :iteration, :call, :params, :blocked, :block_reason, :guardrail, :context, :targets)
|
|
@@ -256,11 +258,11 @@ module Samagotchi
|
|
|
256
258
|
def tool_call(call, params: nil, cwd: nil, context: nil, verdict: nil, iteration: 1)
|
|
257
259
|
context ||= Guardrails::Context.new(cwd: cwd || @recorder[:cwd], session_id: @recorder[:session_id],
|
|
258
260
|
interface: @recorder[:frontend])
|
|
259
|
-
gate = Guardrails::Gate.new(
|
|
260
|
-
|
|
261
|
+
gate = Guardrails::Gate.new(hooks: hooks, tools: tools, context_for: ->(_turn) { context },
|
|
262
|
+
checks_for: ->(_turn) { [] })
|
|
261
263
|
verdict ||= Verdict.new(call: call)
|
|
262
264
|
params ||= "#{call[:name]} #{call[:content] || call[:path]}".strip
|
|
263
|
-
failing { gate.evaluate(call, iteration: iteration, params: params, verdict: verdict) }
|
|
265
|
+
failing { gate.evaluate(call, context: TurnContext.none, iteration: iteration, params: params, verdict: verdict) }
|
|
264
266
|
end
|
|
265
267
|
|
|
266
268
|
# Run a command line ("/name args") as chi does: the first matching
|
|
@@ -31,7 +31,7 @@ module Samagotchi
|
|
|
31
31
|
# What a bundle registered (#registered): commands, tools (names),
|
|
32
32
|
# hooks per event, inits (labels), toolkits, tool sources and peer
|
|
33
33
|
# sources (names).
|
|
34
|
-
Registered = Data.define(:commands, :tools, :hooks, :inits, :toolkits, :tool_sources, :peer_sources)
|
|
34
|
+
Registered = Data.define(:commands, :tools, :hooks, :inits, :toolkits, :tool_sources, :peer_sources, :prompt_sections)
|
|
35
35
|
# A command: its mid_turn policy and its selection-bar labels (chi.annotation).
|
|
36
36
|
Command = Data.define(:name, :mid_turn, :labels)
|
|
37
37
|
# A hook on an event: the bundle, the hook (its file, or the plugin's) and its priority.
|
|
@@ -98,7 +98,8 @@ module Samagotchi
|
|
|
98
98
|
inits: inits.map { |init| init[:label] },
|
|
99
99
|
toolkits: Array(registries.toolkits).map(&:name),
|
|
100
100
|
tool_sources: registries.tool_index.sources.map(&:label),
|
|
101
|
-
peer_sources: registries.peer_sources.names
|
|
101
|
+
peer_sources: registries.peer_sources.names,
|
|
102
|
+
prompt_sections: registries.prompt_sections.names(name)
|
|
102
103
|
)
|
|
103
104
|
end
|
|
104
105
|
|
|
@@ -58,11 +58,11 @@ module Samagotchi
|
|
|
58
58
|
# @param registry [Tools::Registry, nil] the session's tools
|
|
59
59
|
# @param cwd [String, nil] what a file tool's title is relative to (the
|
|
60
60
|
# worker runs in its session's working directory)
|
|
61
|
-
def tool_activity_event(tool_name, call, result, registry: nil, cwd: Dir.pwd)
|
|
61
|
+
def tool_activity_event(tool_name, call, result, registry: nil, cwd: Dir.pwd, surface: nil)
|
|
62
62
|
event = {
|
|
63
63
|
action: tool_activity_action(tool_name, registry: registry, call: call),
|
|
64
64
|
tool: tool_name,
|
|
65
|
-
params: tool_activity_params(tool_name, call, registry: registry),
|
|
65
|
+
params: tool_activity_params(tool_name, call, registry: registry, surface: surface),
|
|
66
66
|
status: tool_activity_status(result, tool_name, command: call.is_a?(Hash) ? call[:content] : nil)
|
|
67
67
|
}
|
|
68
68
|
# A grep that found nothing: ok, and the web row says "no match".
|
|
@@ -359,7 +359,9 @@ module Samagotchi
|
|
|
359
359
|
name == "git" && args.first == "diff" && args.intersect?(%w[--quiet --exit-code])
|
|
360
360
|
end
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
# +surface+: the turn's Tools::ToolSurface, for a preview that reads it
|
|
363
|
+
# (call_tool's); nil outside a turn.
|
|
364
|
+
def tool_activity_params(tool_name, call, registry: nil, surface: nil)
|
|
363
365
|
case tool_name
|
|
364
366
|
when Tools::Execute::NAME
|
|
365
367
|
"command=#{preview_tool_param(call[:content])}"
|
|
@@ -453,7 +455,7 @@ module Samagotchi
|
|
|
453
455
|
parts << "options=#{preview_tool_param(Array(opts).join(","))}" if opts && !Array(opts).empty?
|
|
454
456
|
parts.join(" ")
|
|
455
457
|
else
|
|
456
|
-
registry_params(registry_entry(registry, tool_name), call)
|
|
458
|
+
registry_params(registry_entry(registry, tool_name), call, surface)
|
|
457
459
|
end
|
|
458
460
|
end
|
|
459
461
|
|
|
@@ -472,13 +474,13 @@ module Samagotchi
|
|
|
472
474
|
# the web's reload also has); nil for a tool the registry doesn't know
|
|
473
475
|
# that has no args:, and for a built-in without its own words above
|
|
474
476
|
# (the reminder tools).
|
|
475
|
-
def registry_params(entry, call)
|
|
477
|
+
def registry_params(entry, call, surface = nil)
|
|
476
478
|
return nil if entry&.core?
|
|
477
479
|
return nil if entry.nil? && !call[:args].is_a?(Hash)
|
|
478
480
|
|
|
479
481
|
if entry&.preview
|
|
480
482
|
begin
|
|
481
|
-
return entry.
|
|
483
|
+
return entry.preview_of(call, surface)
|
|
482
484
|
rescue StandardError => e
|
|
483
485
|
Log.warn(:plugins, "plugin_preview_failed", tool: entry.name, error: e.class.name,
|
|
484
486
|
msg: "#{entry.name} preview failed: #{e.message}")
|
|
@@ -635,10 +635,10 @@ module Samagotchi
|
|
|
635
635
|
|
|
636
636
|
# The schemas the native (Gemma, Qwen) prompts declare: the built-ins
|
|
637
637
|
# as they are, and each plugin tool's flattened (.flat_schema).
|
|
638
|
-
# @param
|
|
639
|
-
#
|
|
640
|
-
def native_schemas(
|
|
641
|
-
|
|
638
|
+
# @param surface [Tools::ToolSurface] the tools a turn (or a build
|
|
639
|
+
# under its layers) declares (ToolSurface#declared)
|
|
640
|
+
def native_schemas(surface)
|
|
641
|
+
surface.declared.map { |entry| entry.core? ? entry.schema : flat_schema(entry.schema) }
|
|
642
642
|
end
|
|
643
643
|
|
|
644
644
|
# A plugin tool's schema in the shape the built-ins have, which is all
|