samagotchi 0.65.0 → 0.67.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 +107 -1
- data/README.md +2 -1
- data/bin/chi +51 -2
- data/docs/architecture.md +50 -28
- data/docs/cli.md +14 -2
- data/docs/configuration.md +101 -16
- data/docs/context.md +39 -2
- data/docs/desktop.md +90 -22
- data/docs/guardrails.md +8 -5
- data/docs/hooks.md +2 -2
- data/docs/internals/anthropic.md +156 -0
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +2 -0
- data/docs/internals/engine.md +75 -30
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +36 -13
- data/docs/memory.md +1 -1
- data/docs/plugins.md +289 -25
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +16 -6
- data/docs/sub-agent.md +6 -1
- data/docs/testing.md +17 -3
- data/lib/samagotchi/bootstrap/probe.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +13 -3
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge.rb +6 -3
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +40 -302
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +22 -0
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +189 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +136 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +92 -0
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +76 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +5 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +24 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
- data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
- data/lib/samagotchi/bundles/system/delegated.md +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +5 -5
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/cache_confirm.rb +9 -6
- data/lib/samagotchi/commands/registry.rb +25 -8
- data/lib/samagotchi/config.rb +23 -8
- data/lib/samagotchi/context_absorber.rb +66 -13
- data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
- data/lib/samagotchi/desktop/macos/App.swift +3 -3
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
- data/lib/samagotchi/desktop/macos/Images.swift +2 -2
- data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
- data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
- data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
- data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
- data/lib/samagotchi/desktop/macos.rb +84 -13
- data/lib/samagotchi/desktop_command.rb +14 -3
- data/lib/samagotchi/engine.rb +120 -107
- data/lib/samagotchi/engine_peers.rb +3 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +1 -1
- data/lib/samagotchi/guardrails/approvals.rb +1 -1
- data/lib/samagotchi/guardrails/rules.rb +1 -1
- data/lib/samagotchi/guardrails/targets.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +151 -0
- data/lib/samagotchi/hooks/registry.rb +2 -2
- data/lib/samagotchi/host_keys.rb +147 -0
- data/lib/samagotchi/kernel_loop.rb +9 -1
- data/lib/samagotchi/llm/adapter_common.rb +138 -0
- data/lib/samagotchi/llm/anthropic_assembly.rb +4 -2
- data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
- data/lib/samagotchi/llm/api_key.rb +23 -7
- data/lib/samagotchi/llm/chat_loop.rb +10 -6
- data/lib/samagotchi/llm/openai_chat.rb +9 -92
- data/lib/samagotchi/llm/refusal_memo.rb +29 -0
- data/lib/samagotchi/llm_context_override.rb +1 -0
- data/lib/samagotchi/peer_sources.rb +58 -0
- data/lib/samagotchi/plugin/api.rb +138 -156
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/loader.rb +5 -2
- data/lib/samagotchi/plugin/peer.rb +394 -0
- data/lib/samagotchi/plugin/tool_spec.rb +166 -0
- data/lib/samagotchi/plugin_tasks.rb +2 -2
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/self_report.rb +5 -3
- data/lib/samagotchi/send_command.rb +41 -15
- data/lib/samagotchi/session.rb +18 -1
- data/lib/samagotchi/session_commands.rb +16 -1
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/session_manager.rb +4 -1
- data/lib/samagotchi/session_setup.rb +14 -7
- data/lib/samagotchi/system_prompt.rb +16 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +19 -9
- data/lib/samagotchi/terminal_ui.rb +18 -2
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +13 -1
- data/lib/samagotchi/toolkit.rb +246 -0
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/toolkit_command.rb +117 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +43 -6
- data/lib/samagotchi/tools/task_create.rb +6 -6
- data/lib/samagotchi/tools/task_get.rb +13 -13
- data/lib/samagotchi/tools/task_list.rb +6 -6
- data/lib/samagotchi/tools/task_record.rb +52 -0
- data/lib/samagotchi/tools/task_runtime.rb +55 -91
- data/lib/samagotchi/tools/task_stop.rb +6 -6
- data/lib/samagotchi/tools/task_wait.rb +12 -13
- data/lib/samagotchi/tools/tool_index.rb +454 -0
- data/lib/samagotchi/update_command.rb +9 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +28 -9
- data/lib/samagotchi/web/capabilities.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -2
- data/lib/samagotchi/web/public/annotate_actions.js +109 -0
- data/lib/samagotchi/web/public/annotate_bar.js +84 -2
- data/lib/samagotchi/web/public/app.js +52 -43
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/commands.js +6 -4
- data/lib/samagotchi/web/public/composer_size.js +2 -2
- data/lib/samagotchi/web/public/copy.js +1 -1
- data/lib/samagotchi/web/public/ctx.js +1 -1
- data/lib/samagotchi/web/public/delivery_line.js +4 -4
- data/lib/samagotchi/web/public/families_view.js +1 -1
- data/lib/samagotchi/web/public/format.js +11 -7
- data/lib/samagotchi/web/public/images.js +25 -2
- data/lib/samagotchi/web/public/index.html +9 -0
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/llm_context_chip.js +2 -2
- data/lib/samagotchi/web/public/notify.js +6 -4
- data/lib/samagotchi/web/public/question_card.js +1 -1
- data/lib/samagotchi/web/public/ref_scan.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +17 -0
- data/lib/samagotchi/web/public/session_title.js +2 -3
- data/lib/samagotchi/web/public/sessions_list.js +0 -8
- data/lib/samagotchi/web/public/stage_model.js +2 -2
- data/lib/samagotchi/web/public/stream_handlers.js +13 -0
- data/lib/samagotchi/web/public/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- data/lib/samagotchi/web/public/toolkit_chip.js +29 -0
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +2 -2
- data/lib/samagotchi/web/public/turn_model.js +2 -2
- data/lib/samagotchi/web/public/turn_view.js +1 -1
- data/lib/samagotchi/web/server.rb +0 -4
- data/lib/samagotchi/worker.rb +13 -8
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +23 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "../toolkit"
|
|
4
4
|
require_relative "../log"
|
|
5
5
|
require_relative "../hooks/registry"
|
|
6
|
-
require_relative "../
|
|
6
|
+
require_relative "../commands/registry"
|
|
7
7
|
require_relative "service"
|
|
8
8
|
require_relative "tool_result"
|
|
9
|
+
require_relative "tool_spec"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
module Plugin
|
|
@@ -14,8 +15,8 @@ module Samagotchi
|
|
|
14
15
|
# #register returns, so a plugin that raises halfway adds nothing.
|
|
15
16
|
class Api
|
|
16
17
|
COMMAND_NAME = %r{\A/[a-z][a-z0-9_-]{0,31}\z}
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
ANNOTATION_LABEL_MAX = 40
|
|
19
|
+
ANNOTATIONS_MAX = 5
|
|
19
20
|
|
|
20
21
|
# @param bundle [String]
|
|
21
22
|
# @param label [String] "<file> (bundle <name>)": what hooks and
|
|
@@ -29,9 +30,13 @@ module Samagotchi
|
|
|
29
30
|
@context = context
|
|
30
31
|
@hooks = []
|
|
31
32
|
@commands = []
|
|
33
|
+
@annotations = []
|
|
32
34
|
@tools = []
|
|
33
35
|
@services = []
|
|
34
36
|
@inits = []
|
|
37
|
+
@toolkits = []
|
|
38
|
+
@peer_sources = []
|
|
39
|
+
@tool_source = nil
|
|
35
40
|
@committed = false
|
|
36
41
|
end
|
|
37
42
|
|
|
@@ -63,6 +68,35 @@ module Samagotchi
|
|
|
63
68
|
nil
|
|
64
69
|
end
|
|
65
70
|
|
|
71
|
+
# A pill in the web's selection bar: it prepares +command+ (a command
|
|
72
|
+
# line) with +template+ filled from the selected text on the lines
|
|
73
|
+
# under it ({quote}: the labelled quote block Annotate makes, {text}:
|
|
74
|
+
# the selection alone, {source}: answer, thinking, step, tool or
|
|
75
|
+
# user), and fills the composer with it, or sends it at once with
|
|
76
|
+
# +send+ (docs/plugins.md). Web only.
|
|
77
|
+
# @param label [String] the pill's text, 1–40 characters
|
|
78
|
+
# @param command [String] "/name" or "/name words": a command this
|
|
79
|
+
# plugin registers (checked once #register returns, so the order
|
|
80
|
+
# doesn't matter; another's is a load error)
|
|
81
|
+
# @param template [String] what goes under the command line
|
|
82
|
+
# @param send [Boolean] send at once instead of filling the composer
|
|
83
|
+
def annotation(label, command:, template: "{quote}", send: false)
|
|
84
|
+
label = label.to_s.strip
|
|
85
|
+
unless label.length.between?(1, ANNOTATION_LABEL_MAX)
|
|
86
|
+
raise ArgumentError, "annotation label #{label.inspect} must be 1 to #{ANNOTATION_LABEL_MAX} characters"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
line = command.to_s.strip
|
|
90
|
+
name = line.split(/\s/, 2).first.to_s
|
|
91
|
+
raise ArgumentError, "annotation #{label}: command #{line.inspect} must start with a /name" unless name.match?(COMMAND_NAME)
|
|
92
|
+
raise ArgumentError, "annotation #{label}: template must be a non-blank String" unless template.is_a?(String) && !template.strip.empty?
|
|
93
|
+
raise ArgumentError, "at most #{ANNOTATIONS_MAX} annotations per plugin" if @annotations.size >= ANNOTATIONS_MAX
|
|
94
|
+
|
|
95
|
+
@annotations << { name: name, annotation: Commands::Registry::Annotation.new(label: label, line: line, template: template,
|
|
96
|
+
sends: send ? true : false) }
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
66
100
|
# A tool the model can call: the block gets the call's arguments (a
|
|
67
101
|
# frozen Hash, string keys, typed by the schema: see Tools::Args) and
|
|
68
102
|
# the Context, and returns the result text ("Error: …" marks a
|
|
@@ -83,11 +117,11 @@ module Samagotchi
|
|
|
83
117
|
# a list of {kind:, target:, label:, actions:} (RowRefs, Ref). Run
|
|
84
118
|
# once per call on the tool path, so it must be cheap (no I/O)
|
|
85
119
|
def tool(name, description, **, &)
|
|
86
|
-
spec =
|
|
87
|
-
if (taken = @registries.tools[spec
|
|
88
|
-
raise ArgumentError, "tool #{spec
|
|
120
|
+
spec = ToolSpec.build(name, description, **, &)
|
|
121
|
+
if (taken = @registries.tools[spec.name])
|
|
122
|
+
raise ArgumentError, "tool #{spec.name} is already registered (#{taken.source})"
|
|
89
123
|
end
|
|
90
|
-
raise ArgumentError, "tool #{spec
|
|
124
|
+
raise ArgumentError, "tool #{spec.name} is registered twice" if @tools.any? { |tool| tool.name == spec.name }
|
|
91
125
|
|
|
92
126
|
@tools << spec
|
|
93
127
|
nil
|
|
@@ -111,7 +145,7 @@ module Samagotchi
|
|
|
111
145
|
yield set
|
|
112
146
|
if (stage = @registries.stage_tools)
|
|
113
147
|
stage.call(@bundle, set.specs, @context)
|
|
114
|
-
elsif
|
|
148
|
+
elsif ToolSpec.apply(@registries.tools, @bundle, set.specs, @context)[:changed]
|
|
115
149
|
tools_changed!
|
|
116
150
|
end
|
|
117
151
|
nil
|
|
@@ -119,7 +153,7 @@ module Samagotchi
|
|
|
119
153
|
|
|
120
154
|
# What #replace_tools' block declares tools on.
|
|
121
155
|
class ToolSet
|
|
122
|
-
# @return [Array<
|
|
156
|
+
# @return [Array<ToolSpec>]
|
|
123
157
|
attr_reader :specs
|
|
124
158
|
|
|
125
159
|
def initialize
|
|
@@ -128,8 +162,8 @@ module Samagotchi
|
|
|
128
162
|
|
|
129
163
|
# As Api#tool.
|
|
130
164
|
def tool(name, description, **, &)
|
|
131
|
-
spec =
|
|
132
|
-
raise ArgumentError, "tool #{spec
|
|
165
|
+
spec = ToolSpec.build(name, description, **, &)
|
|
166
|
+
raise ArgumentError, "tool #{spec.name} is registered twice" if @specs.any? { |s| s.name == spec.name }
|
|
133
167
|
|
|
134
168
|
@specs << spec
|
|
135
169
|
nil
|
|
@@ -215,16 +249,88 @@ module Samagotchi
|
|
|
215
249
|
nil
|
|
216
250
|
end
|
|
217
251
|
|
|
218
|
-
#
|
|
252
|
+
# Publish tools into the session's tool index instead of declaring
|
|
253
|
+
# them one by one (docs/plugins.md, "Publishing tools to the index"):
|
|
254
|
+
# the model searches them with find_tools and calls one with
|
|
255
|
+
# call_tool (<group>/<tool>), which chi declares once, after the
|
|
256
|
+
# plugins load, when a source registered. Returns the source: its
|
|
257
|
+
# #publish(groups) (ToolIndex::Group values, each with its
|
|
258
|
+
# ToolIndex::Tool values) swaps in the plugin's whole list, from any
|
|
259
|
+
# thread, at any time, and changes only what searches answer.
|
|
260
|
+
# @param label [String] the call tool's row label (default: the bundle)
|
|
261
|
+
# @param call [#call] (tool, args, ctx) → the call's result, as a
|
|
262
|
+
# tool's block returns it; +tool+ a ToolIndex::Tool the index looked
|
|
263
|
+
# up and checked, +args+ typed by its schema
|
|
264
|
+
# @param acts_as [#call] (group, tool) → the name guardrail rules
|
|
265
|
+
# match for a tool from its names alone (one not published yet)
|
|
266
|
+
# @param wait [#call, nil] (group) waits for a :starting group's start
|
|
267
|
+
# @param prefix [String, nil] a bare name with it is taken as an
|
|
268
|
+
# acts_as as given
|
|
269
|
+
# @return [Tools::ToolIndex::Source]
|
|
270
|
+
def tool_source(call:, acts_as:, label: @bundle, wait: nil, prefix: nil)
|
|
271
|
+
raise ArgumentError, "this chi has no tool index (plugins: false)" unless @registries.tool_index
|
|
272
|
+
raise ArgumentError, "tool_source is declared twice" if @tool_source
|
|
273
|
+
|
|
274
|
+
@tool_source = @registries.tool_index.source(label: label, call: call, acts_as: acts_as, wait: wait,
|
|
275
|
+
prefix: prefix, context: @context)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
TOOLKIT_NAME = /\A[a-z][a-z0-9_-]{0,31}\z/
|
|
279
|
+
|
|
280
|
+
# A named toolkit sessions can be put on (/toolkit NAME, chi --toolkit,
|
|
281
|
+
# toolkit.default, ctx.use_toolkit): +base+'s tools (a toolkit chi or
|
|
282
|
+
# a bundle has) with +add+ declared and +remove+ left out. A name chi
|
|
283
|
+
# (full, lean, child) or another bundle has is a load error; a base
|
|
284
|
+
# that doesn't resolve leaves the sessions on it with every tool
|
|
285
|
+
# (Toolkit#resolve).
|
|
286
|
+
def toolkit(name, base:, add: [], remove: [])
|
|
287
|
+
name = name.to_s
|
|
288
|
+
raise ArgumentError, "toolkit name #{name.inspect} must be a-z, 0-9, _ and -" unless name.match?(TOOLKIT_NAME)
|
|
289
|
+
raise ArgumentError, "this chi has no toolkits (plugins: false)" unless @registries.toolkits
|
|
290
|
+
if Toolkit::BUILTIN_KITS.key?(name)
|
|
291
|
+
raise ArgumentError, "toolkit #{name} is chi's own"
|
|
292
|
+
elsif (taken = @registries.toolkits.find { |kit| kit.name == name })
|
|
293
|
+
raise ArgumentError, "toolkit #{name} is already declared (#{taken.source})"
|
|
294
|
+
end
|
|
295
|
+
raise ArgumentError, "toolkit #{name} is declared twice" if @toolkits.any? { |kit| kit.name == name }
|
|
296
|
+
|
|
297
|
+
@toolkits << Toolkit::Kit.new(name: name, base: base, add: add, remove: remove, source: @bundle)
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# A context source that holds a peer agent's answers (the peer
|
|
302
|
+
# bundles' claude-review, codex-review): +label+ is the peer's name
|
|
303
|
+
# ("Codex", PeerSources::LABEL), said in chi's own lines. Such a
|
|
304
|
+
# source, when it is a pushed one of the session's own, wakes on
|
|
305
|
+
# context.peer_turns rather than the 10-minute window, with the peer
|
|
306
|
+
# line (docs/context.md, Waking). Declared here, in-process, never by
|
|
307
|
+
# a source file; gone with the bundle.
|
|
308
|
+
def peer_source(name, label:)
|
|
309
|
+
raise ArgumentError, "this chi has no peer sources (plugins: false)" unless @registries.peer_sources
|
|
310
|
+
|
|
311
|
+
name = @registries.peer_sources.check!(name, label: label, bundle: @bundle)
|
|
312
|
+
raise ArgumentError, "peer source #{name} is declared twice" if @peer_sources.any? { |item| item[:name] == name }
|
|
313
|
+
|
|
314
|
+
@peer_sources << { name: name, label: label.to_s }
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Register what was staged. Called by Loader after #register. What
|
|
319
|
+
# can only be checked now (an annotation's command) is checked before
|
|
320
|
+
# anything is registered: a raise here leaves the registries as they were.
|
|
219
321
|
def commit!
|
|
322
|
+
check_annotations!
|
|
323
|
+
@peer_sources.each { |item| @registries.peer_sources.check!(item[:name], label: item[:label], bundle: @bundle) }
|
|
220
324
|
context = @context
|
|
221
325
|
@commands.each do |cmd|
|
|
222
326
|
block = cmd[:block]
|
|
223
|
-
@
|
|
327
|
+
annotations = @annotations.select { |item| item[:name] == cmd[:name] }.map { |item| item[:annotation] }
|
|
328
|
+
@registries.commands.register(cmd[:name], cmd[:description], anytime: cmd[:anytime], source: @bundle,
|
|
329
|
+
annotations: annotations) do |args|
|
|
224
330
|
block.call(args, context)
|
|
225
331
|
end
|
|
226
332
|
end
|
|
227
|
-
@tools.each { |tool| @registries.tools.register(tool
|
|
333
|
+
@tools.each { |tool| @registries.tools.register(tool.name, source: @bundle, **tool.entry_fields(context)) }
|
|
228
334
|
@hooks.each do |hook|
|
|
229
335
|
block = hook[:block]
|
|
230
336
|
# Logged only (echo: false): a turn's live region is on screen.
|
|
@@ -243,156 +349,32 @@ module Samagotchi
|
|
|
243
349
|
block.call(context)
|
|
244
350
|
end
|
|
245
351
|
end
|
|
352
|
+
@registries.toolkits&.concat(@toolkits)
|
|
353
|
+
@registries.tool_index.add(@tool_source) if @tool_source
|
|
354
|
+
@peer_sources.each { |item| @registries.peer_sources.declare(item[:name], label: item[:label], bundle: @bundle) }
|
|
246
355
|
@committed = true
|
|
247
356
|
end
|
|
248
357
|
|
|
249
358
|
# @return [Hash] how many of each it registered (for the log)
|
|
250
|
-
def counts = { commands: @commands.size,
|
|
251
|
-
inits: @inits.size
|
|
252
|
-
|
|
253
|
-
# A checked tool declaration (#tool's arguments: Api#tool and
|
|
254
|
-
# ToolSet#tool pass theirs on, so a new keyword is added here).
|
|
255
|
-
# @return [Hash] {name:, schema:, label:, preview:, targets:, refs:, block:}
|
|
256
|
-
def self.tool_spec(name, description, params: {}, schema: nil, label: nil, preview: nil, targets: nil, refs: nil,
|
|
257
|
-
&block)
|
|
258
|
-
raise ArgumentError, "tool #{name.inspect} needs a block" unless block
|
|
259
|
-
|
|
260
|
-
name = name.to_s
|
|
261
|
-
raise ArgumentError, "tool name #{name.inspect} must be a-z, 0-9 and _ (at most 48)" unless name.match?(TOOL_NAME)
|
|
262
|
-
raise ArgumentError, "tool #{name}: preview must respond to #call" if preview && !preview.respond_to?(:call)
|
|
263
|
-
raise ArgumentError, "tool #{name}: targets must respond to #call" if targets && !targets.respond_to?(:call)
|
|
264
|
-
raise ArgumentError, "tool #{name}: refs must respond to #call" if refs && !refs.respond_to?(:call)
|
|
265
|
-
|
|
266
|
-
parameters = schema ? schema_parameters(name, schema) : params_schema(name, params)
|
|
267
|
-
{ name: name, schema: { name: name, description: description.to_s, parameters: parameters },
|
|
268
|
-
label: label&.to_s, preview: preview, targets: targets, refs: refs, block: block }
|
|
269
|
-
end
|
|
359
|
+
def counts = { commands: @commands.size, annotations: @annotations.size, tools: @tools.size, hooks: @hooks.size,
|
|
360
|
+
services: @services.size, inits: @inits.size, toolkits: @toolkits.size,
|
|
361
|
+
tool_sources: @tool_source ? 1 : 0, peer_sources: @peer_sources.size }
|
|
270
362
|
|
|
271
|
-
#
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
# in place: none of them reaches the prompt, so not a change (that
|
|
277
|
-
# would build the prompts again). A name
|
|
278
|
-
# another source has is left out. Called on the turn thread
|
|
279
|
-
# (Engine#apply_staged_tools!).
|
|
280
|
-
# @return [Hash] {changed: Boolean, skipped: [String] (why)}
|
|
281
|
-
def self.apply_tools(registry, bundle, specs, context)
|
|
282
|
-
wanted = specs.to_h { |spec| [spec[:name], spec] }
|
|
283
|
-
changed = false
|
|
284
|
-
registry.entries.each do |entry|
|
|
285
|
-
next unless entry.source == bundle
|
|
286
|
-
|
|
287
|
-
if (spec = wanted[entry.name]) && spec[:schema] == entry.schema && spec[:label] == entry.label
|
|
288
|
-
entry_fields(spec, context).slice(:handler, :preview, :targets, :refs).each { |key, value| entry[key] = value }
|
|
289
|
-
next
|
|
290
|
-
end
|
|
363
|
+
# Each annotation's command is one #command staged.
|
|
364
|
+
def check_annotations!
|
|
365
|
+
names = @commands.map { |cmd| cmd[:name] }
|
|
366
|
+
@annotations.each do |item|
|
|
367
|
+
next if names.include?(item[:name])
|
|
291
368
|
|
|
292
|
-
|
|
293
|
-
changed = true
|
|
369
|
+
raise ArgumentError, "annotation #{item[:annotation].label}: #{item[:name]} is not a command this plugin registers"
|
|
294
370
|
end
|
|
295
|
-
skipped = []
|
|
296
|
-
specs.each do |spec|
|
|
297
|
-
if (taken = registry[spec[:name]])
|
|
298
|
-
skipped << "tool #{spec[:name]} is already registered (#{taken.source})" unless taken.source == bundle
|
|
299
|
-
next
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
registry.register(spec[:name], source: bundle, **entry_fields(spec, context))
|
|
303
|
-
changed = true
|
|
304
|
-
end
|
|
305
|
-
{ changed: changed, skipped: skipped }
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
# Registry#register's keywords for a tool declaration: its block
|
|
309
|
-
# wrapped as a handler (typed args, the Context), preview, targets, refs.
|
|
310
|
-
def self.entry_fields(spec, context)
|
|
311
|
-
block = spec[:block]
|
|
312
|
-
parameters = spec[:schema][:parameters]
|
|
313
|
-
preview = per_call(spec[:preview], parameters)
|
|
314
|
-
{
|
|
315
|
-
schema: spec[:schema], label: spec[:label],
|
|
316
|
-
handler: lambda { |call, _kctx|
|
|
317
|
-
result = block.call(Api.args_of(call, parameters), context)
|
|
318
|
-
Api.result_text(result)
|
|
319
|
-
},
|
|
320
|
-
preview: preview && ->(call) { preview.call(call)&.to_s },
|
|
321
|
-
targets: per_call(spec[:targets], parameters),
|
|
322
|
-
refs: per_call(spec[:refs], parameters)
|
|
323
|
-
}
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
# A plugin's args → value lambda (+fn+) as call → value: it gets the
|
|
327
|
-
# call's typed args (Api.args_of), as the tool's block does. nil
|
|
328
|
-
# without one.
|
|
329
|
-
def self.per_call(fn, parameters)
|
|
330
|
-
fn && ->(call) { fn.call(Api.args_of(call, parameters)) }
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
# A tool's result as the model reads it: a String (a ToolResult too,
|
|
334
|
-
# with its images) as it is, a Hash or Array as JSON (its #to_s is
|
|
335
|
-
# Hash#inspect, which changes with the Ruby version), else #to_s.
|
|
336
|
-
def self.result_text(result)
|
|
337
|
-
case result
|
|
338
|
-
when String then result
|
|
339
|
-
when Hash, Array then JSON.generate(result)
|
|
340
|
-
else result.to_s
|
|
341
|
-
end
|
|
342
|
-
rescue JSON::GeneratorError
|
|
343
|
-
result.to_s
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
# A tool call's arguments as a plugin sees them: the parsers' args:
|
|
347
|
-
# (a call built without one, in a spec say: the call without its
|
|
348
|
-
# name), string keys, typed by +parameters+, frozen.
|
|
349
|
-
def self.args_of(call, parameters = nil)
|
|
350
|
-
given = call[:args].is_a?(Hash) ? call[:args] : call.reject { |key, _| %i[name args].include?(key) }
|
|
351
|
-
Tools::Args.coerce(given, parameters).freeze
|
|
352
371
|
end
|
|
372
|
+
private :check_annotations!
|
|
353
373
|
|
|
354
|
-
# A
|
|
355
|
-
#
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
when Hash then value.to_h { |key, item| [key.to_sym, symbolize(item)] }
|
|
359
|
-
when Array then value.map { |item| symbolize(item) }
|
|
360
|
-
else value
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
# The parameters object from params: (name => property spec).
|
|
365
|
-
def self.params_schema(name, params)
|
|
366
|
-
raise ArgumentError, "tool #{name}: params must be a Hash" unless params.is_a?(Hash)
|
|
367
|
-
|
|
368
|
-
required = []
|
|
369
|
-
properties = params.each_with_object({}) do |(param, spec), acc|
|
|
370
|
-
param = param.to_s
|
|
371
|
-
raise ArgumentError, "tool #{name}: parameter name #{param.inspect} is not a plain word" unless param.match?(PARAM_NAME)
|
|
372
|
-
raise ArgumentError, "tool #{name}: parameter #{param} must be a Hash {type:, description:}" unless spec.is_a?(Hash)
|
|
373
|
-
|
|
374
|
-
spec = Api.symbolize(spec)
|
|
375
|
-
required << param if spec.delete(:required) == true
|
|
376
|
-
acc[param.to_sym] = { type: (spec.delete(:type) || "string").to_s, description: spec.delete(:description).to_s }.merge(spec)
|
|
377
|
-
end
|
|
378
|
-
{ type: "object", properties: properties, required: required }
|
|
379
|
-
end
|
|
380
|
-
private_class_method :params_schema
|
|
381
|
-
|
|
382
|
-
# The parameters object from schema: (a JSON Schema object).
|
|
383
|
-
def self.schema_parameters(name, schema)
|
|
384
|
-
raise ArgumentError, "tool #{name}: schema must be a Hash" unless schema.is_a?(Hash)
|
|
385
|
-
|
|
386
|
-
schema = Api.symbolize(schema)
|
|
387
|
-
raise ArgumentError, "tool #{name}: schema must be {type: \"object\", properties: {…}}" unless schema.fetch(:type, "object").to_s == "object"
|
|
388
|
-
|
|
389
|
-
properties = schema[:properties] || {}
|
|
390
|
-
raise ArgumentError, "tool #{name}: schema properties must be a Hash" unless properties.is_a?(Hash)
|
|
391
|
-
|
|
392
|
-
{ type: "object", properties: properties, required: Array(schema[:required]).map(&:to_s) }
|
|
393
|
-
.merge(schema.except(:type, :properties, :required))
|
|
394
|
-
end
|
|
395
|
-
private_class_method :schema_parameters
|
|
374
|
+
# A checked tool declaration (ToolSpec.build), for bundles: the mcp
|
|
375
|
+
# bundle's token estimate calls it, installed ones included.
|
|
376
|
+
# @return [ToolSpec]
|
|
377
|
+
def self.tool_spec(...) = ToolSpec.build(...)
|
|
396
378
|
end
|
|
397
379
|
end
|
|
398
380
|
end
|
|
@@ -29,9 +29,13 @@ module Samagotchi
|
|
|
29
29
|
# (reason, label), each true when it acted on a running turn.
|
|
30
30
|
# +frontend+ returns the Engine's interface (:repl, :worker,
|
|
31
31
|
# :non_interactive), which ctx.frontend names for plugins.
|
|
32
|
+
# +toolkit+ returns the session's toolkit name, +declared_tools+ the
|
|
33
|
+
# tool names its next turn declares, +use_toolkit+ takes a name and
|
|
34
|
+
# returns whether it staged it (Engine#use_toolkit).
|
|
32
35
|
Host = Struct.new(:session_id, :cwd, :messages, :messages_partial, :notify, :ask_user, :cancelled, :card,
|
|
33
36
|
:ask_model, :model_name, :model_key, :state_dir, :scratch, :setup, :prompt_files,
|
|
34
|
-
:steer, :stop_turn, :stop_generation, :frontend,
|
|
37
|
+
:steer, :stop_turn, :stop_generation, :frontend, :toolkit, :declared_tools, :use_toolkit,
|
|
38
|
+
keyword_init: true)
|
|
35
39
|
|
|
36
40
|
# ctx.ask_model failed: the model couldn't be reached, timed out, or
|
|
37
41
|
# sent nothing usable. The message says why, for the user.
|
|
@@ -109,6 +113,23 @@ module Samagotchi
|
|
|
109
113
|
# current_model_only names it
|
|
110
114
|
def model_key = @host.model_key&.call
|
|
111
115
|
|
|
116
|
+
# @return [String, nil] the name of the session's toolkit ("full" for
|
|
117
|
+
# one from before toolkits; nil with no session)
|
|
118
|
+
def toolkit = @host.toolkit&.call
|
|
119
|
+
|
|
120
|
+
# @return [Array<String>] the tools the session's next turn declares
|
|
121
|
+
# (its toolkit resolved: a kit name says less than this)
|
|
122
|
+
def declared_tools = Array(@host.declared_tools&.call)
|
|
123
|
+
|
|
124
|
+
# Put the session on toolkit +name+ (a plugin's chi.toolkit, or
|
|
125
|
+
# chi's): only before the session's first model reply and never while
|
|
126
|
+
# a turn runs, since the declared tools start every request; it then
|
|
127
|
+
# applies at the next turn's start, and the user is told. Otherwise
|
|
128
|
+
# nothing changes and the plugin can say to run /toolkit NAME.
|
|
129
|
+
# @return [Boolean] whether it was staged
|
|
130
|
+
# @raise [ArgumentError] a toolkit no one declared
|
|
131
|
+
def use_toolkit(name) = !!@host.use_toolkit&.call(name.to_s)
|
|
132
|
+
|
|
112
133
|
# Whether the session is a `chi scratch` one (deleted when it ends).
|
|
113
134
|
def scratch? = !!@host.scratch&.call
|
|
114
135
|
|
|
@@ -16,9 +16,12 @@ module Samagotchi
|
|
|
16
16
|
# (the Engine's system prompts); +services+ (Plugin::Services) is what
|
|
17
17
|
# Engine#shutdown stops; +stage_tools+ is (bundle, specs, context),
|
|
18
18
|
# chi.replace_tools' set, which the Engine applies on its turn thread;
|
|
19
|
-
# +init+ adds a chi.init task (Engine#add_init_task)
|
|
19
|
+
# +init+ adds a chi.init task (Engine#add_init_task); +toolkits+ is the
|
|
20
|
+
# Engine's list of the plugins' toolkits (chi.toolkit: Toolkit::Kit);
|
|
21
|
+
# +tool_index+ the Engine's Tools::ToolIndex (chi.tool_source);
|
|
22
|
+
# +peer_sources+ the Engine's PeerSources (chi.peer_source).
|
|
20
23
|
Registries = Struct.new(:commands, :tools, :hooks, :context_for, :tools_changed, :services, :stage_tools, :init,
|
|
21
|
-
keyword_init: true)
|
|
24
|
+
:toolkits, :tool_index, :peer_sources, keyword_init: true)
|
|
22
25
|
|
|
23
26
|
# Loads installed bundles' plugins (manifest plugin: {file:, sha256:})
|
|
24
27
|
# into an Engine's registries (docs/plugins.md).
|