samagotchi 0.66.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 +60 -1
- data/docs/architecture.md +50 -28
- data/docs/cli.md +3 -1
- data/docs/configuration.md +50 -9
- data/docs/context.md +35 -1
- 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 +71 -29
- data/docs/internals/gemma4-contract.md +1 -1
- data/docs/internals/llm-context-bench.md +5 -4
- data/docs/internals/prompt-caching.md +26 -11
- data/docs/plugins.md +126 -23
- data/docs/sessions.md +6 -6
- data/docs/sub-agent.md +4 -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 +34 -313
- 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 +7 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
- 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/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +15 -7
- 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 +96 -191
- 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/llm/adapter_common.rb +138 -0
- 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 +64 -155
- data/lib/samagotchi/plugin/loader.rb +4 -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 +21 -11
- data/lib/samagotchi/session_continue.rb +58 -15
- data/lib/samagotchi/session_inbox.rb +0 -13
- data/lib/samagotchi/tool_call_parser.rb +1 -1
- data/lib/samagotchi/toolkit_binding.rb +138 -0
- data/lib/samagotchi/tools/context_read.rb +14 -0
- data/lib/samagotchi/tools/execute.rb +12 -12
- data/lib/samagotchi/tools/registry.rb +1 -1
- 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 +16 -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 +1 -1
- data/lib/samagotchi/web/public/app.js +31 -36
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/commands.js +2 -2
- 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/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 +14 -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/strip.js +3 -3
- data/lib/samagotchi/web/public/task_stop.js +3 -2
- 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 +11 -8
- data/lib/samagotchi/worker_inbound.rb +36 -4
- data/lib/samagotchi/worker_wakes.rb +67 -7
- metadata +14 -1
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# The mcp bundle (docs/plugins.md, The mcp bundle): tools from MCP servers.
|
|
4
4
|
# Each server in config.yml runs as a child process (stdio only in v1) for
|
|
5
|
-
# the session's life. Its tools aren't declared one by one:
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# the
|
|
5
|
+
# the session's life. Its tools aren't declared one by one: the bundle
|
|
6
|
+
# publishes them into chi's tool index (chi.tool_source, a group per
|
|
7
|
+
# server), and the model has the index's two fixed tools, find_tools (a
|
|
8
|
+
# keyword search that answers each match's inputSchema) and call_tool (calls
|
|
9
|
+
# one as <server>/<tool>, which the index hands back to #call here). The
|
|
10
|
+
# request's tools never change with the servers', so the prompt cache keeps.
|
|
10
11
|
#
|
|
11
12
|
# bundles:
|
|
12
13
|
# mcp:
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
# env: {DEBUG: "0"} # added to chi's environment
|
|
19
20
|
# cwd: ~/scratch # default: where chi runs
|
|
20
21
|
# tools: [echo, add] # optional: only these (globs work)
|
|
21
|
-
# description: adds and echoes # optional: its line in
|
|
22
|
+
# description: adds and echoes # optional: its line in find_tools
|
|
22
23
|
# timeout: 120 # optional: this server's per-call timeout
|
|
23
24
|
# attach_image_paths: true # default: an image path in the text, in the
|
|
24
25
|
# # temp dir or cwd, attaches it
|
|
@@ -40,7 +41,7 @@ require "json"
|
|
|
40
41
|
require "open3"
|
|
41
42
|
require "samagotchi/process_group"
|
|
42
43
|
require "samagotchi/token_usage"
|
|
43
|
-
require "samagotchi/tools/
|
|
44
|
+
require "samagotchi/tools/tool_index"
|
|
44
45
|
require "samagotchi/tool_declarations"
|
|
45
46
|
require "time"
|
|
46
47
|
require "shellwords"
|
|
@@ -51,19 +52,12 @@ class Plugin
|
|
|
51
52
|
CALL_TIMEOUT = 60
|
|
52
53
|
STARTUP_TIMEOUT = 10
|
|
53
54
|
DESCRIPTION_CHARS = 1024
|
|
54
|
-
PREVIEW_CHARS = 60
|
|
55
55
|
NAME_CHARS = 48
|
|
56
|
-
# Matches a search answers, and names a miss suggests.
|
|
57
|
-
FIND_RESULTS = 5
|
|
58
|
-
CLOSEST = 3
|
|
59
|
-
# A query word in a tool's name (or its server's) counts this many times
|
|
60
|
-
# one in its description.
|
|
61
|
-
NAME_WEIGHT = 3
|
|
62
56
|
# A cached tool list older than this is refreshed in the background.
|
|
63
57
|
CACHE_TTL = 24 * 60 * 60
|
|
64
58
|
# Restarts of a server that exited, per session.
|
|
65
59
|
MAX_RESTARTS = 3
|
|
66
|
-
# The states whose server's tools
|
|
60
|
+
# The states whose server's tools call_tool can reach (a cached or exited
|
|
67
61
|
# server starts on the call).
|
|
68
62
|
OFFERED = %i[running cached exited].freeze
|
|
69
63
|
IMAGE_EXT = { "image/png" => "png", "image/jpeg" => "jpg", "image/gif" => "gif", "image/webp" => "webp" }.freeze
|
|
@@ -263,7 +257,7 @@ class Plugin
|
|
|
263
257
|
|
|
264
258
|
# One tool of a server in the index (the tools: filter applied): +name+
|
|
265
259
|
# is the server's, +chi_name+ mcp_<server>_<tool> (what guardrail rules
|
|
266
|
-
# match, and a name
|
|
260
|
+
# match, and a name call_tool takes too), +schema+ its inputSchema (an
|
|
267
261
|
# empty object when it has none). +error+ is why it can't be called (a
|
|
268
262
|
# bad schema), nil when it can.
|
|
269
263
|
McpTool = Data.define(:server, :name, :chi_name, :description, :schema, :error)
|
|
@@ -297,6 +291,7 @@ class Plugin
|
|
|
297
291
|
# index.
|
|
298
292
|
@left_out = []
|
|
299
293
|
@left_out_lock = Mutex.new
|
|
294
|
+
@publish = Mutex.new
|
|
300
295
|
end
|
|
301
296
|
|
|
302
297
|
# The servers' names, never @settings (the raw servers config, env values
|
|
@@ -312,13 +307,20 @@ class Plugin
|
|
|
312
307
|
# in the background. Without a cache (the first run, a changed config)
|
|
313
308
|
# the server starts in an init task (chi.init) that every UI shows; a
|
|
314
309
|
# turn sent meanwhile waits for its tools. start: eager starts it with
|
|
315
|
-
# every session.
|
|
316
|
-
#
|
|
310
|
+
# every session. With a server configured the bundle is a tool source:
|
|
311
|
+
# chi declares find_tools and call_tool once, after the plugins load,
|
|
312
|
+
# from what #publish gave by then; nothing the servers do later changes
|
|
313
|
+
# them, only what a search answers.
|
|
317
314
|
def register(chi)
|
|
318
315
|
@chi = chi
|
|
319
316
|
ctx = chi.ctx
|
|
320
317
|
configs = @settings["servers"]
|
|
321
318
|
configs = {} unless configs.is_a?(Hash)
|
|
319
|
+
unless configs.empty?
|
|
320
|
+
@source = chi.tool_source(label: "mcp", prefix: "mcp_", call: ->(tool, args, call_ctx) { run(tool, args, call_ctx) },
|
|
321
|
+
acts_as: ->(server, tool) { tool_name(server, tool) },
|
|
322
|
+
wait: ->(server) { wait_started(@servers.find { |s| s.name == server }) })
|
|
323
|
+
end
|
|
322
324
|
@servers = configs.map do |name, config|
|
|
323
325
|
server = Server.new(name: name.to_s, config: config.is_a?(Hash) ? config : {}, state: :starting, tools: [],
|
|
324
326
|
restarts: 0, tokens: 0)
|
|
@@ -351,7 +353,7 @@ class Plugin
|
|
|
351
353
|
end
|
|
352
354
|
end
|
|
353
355
|
end
|
|
354
|
-
|
|
356
|
+
publish
|
|
355
357
|
chi.command "/mcp", "list the MCP servers, their state and their tools", anytime: true do |_args, command_ctx|
|
|
356
358
|
command_ctx.card(title: "MCP servers", body: listing, id: "mcp-servers")
|
|
357
359
|
nil
|
|
@@ -429,12 +431,14 @@ class Plugin
|
|
|
429
431
|
def boot(server, ctx)
|
|
430
432
|
client = server.service.value
|
|
431
433
|
server.state = :running
|
|
434
|
+
publish
|
|
432
435
|
ctx.log.info("mcp_server_started", server: server.name, pid: client.pid, tools: server.listed.size)
|
|
433
436
|
count = server.listed.size
|
|
434
437
|
"#{server.name} ready, #{count} tool#{"s" unless count == 1}"
|
|
435
438
|
rescue StandardError => e
|
|
436
439
|
server.state = :failed
|
|
437
440
|
server.error = e.message
|
|
441
|
+
publish
|
|
438
442
|
ctx.log.warn("mcp_server_failed", server: server.name, error: e.class.name, msg: e.message)
|
|
439
443
|
raise Client::Error, "MCP server #{server.name} didn't start: #{e.message}; its calls fail until chi restarts"
|
|
440
444
|
end
|
|
@@ -586,6 +590,7 @@ class Plugin
|
|
|
586
590
|
end
|
|
587
591
|
server.tools = tools.freeze
|
|
588
592
|
say_clashes(ctx)
|
|
593
|
+
publish
|
|
589
594
|
return if tokens == server.tokens
|
|
590
595
|
|
|
591
596
|
server.tokens = tokens
|
|
@@ -620,7 +625,7 @@ class Plugin
|
|
|
620
625
|
@servers.flat_map(&:tools).select { |t| t.chi_name == tool.chi_name && [t.server, t.name] != [tool.server, tool.name] }
|
|
621
626
|
end
|
|
622
627
|
|
|
623
|
-
# Why
|
|
628
|
+
# Why call_tool refuses +tool+, nil when it can call it.
|
|
624
629
|
def refusal(tool)
|
|
625
630
|
return tool.error if tool.error
|
|
626
631
|
|
|
@@ -628,10 +633,10 @@ class Plugin
|
|
|
628
633
|
"name clash with #{others.map { |t| "#{t.server}/#{t.name}" }.join(", ")} (#{tool.chi_name})" unless others.empty?
|
|
629
634
|
end
|
|
630
635
|
|
|
631
|
-
# The server's tools
|
|
636
|
+
# The server's tools call_tool can call (no bad schema, no name clash).
|
|
632
637
|
def offered(server) = server.tools.reject { |tool| refusal(tool) }
|
|
633
638
|
|
|
634
|
-
# The marks a search and /mcp give a tool
|
|
639
|
+
# The marks a search and /mcp give a tool call_tool refuses.
|
|
635
640
|
def mark(tool)
|
|
636
641
|
return "(bad schema)" if tool.error
|
|
637
642
|
|
|
@@ -645,62 +650,33 @@ class Plugin
|
|
|
645
650
|
# flatter schema, so it is an upper bound there.
|
|
646
651
|
# @raise [ArgumentError] a bad schema
|
|
647
652
|
def definition_tokens(name, description, schema)
|
|
648
|
-
spec_tokens(Samagotchi::Plugin::Api.tool_spec(name, description, schema: schema) { nil })
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
#
|
|
652
|
-
#
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
def declare_tools(chi)
|
|
672
|
-
description = find_description
|
|
673
|
-
chi.tool("find_mcp_tools", description, params: FIND_PARAMS,
|
|
674
|
-
preview: ->(args) { [args["query"].to_s.strip, args["server"] && "in #{args["server"]}"].compact.join(" ") }) do |args, _ctx|
|
|
675
|
-
find(args["query"].to_s, args["server"])
|
|
676
|
-
end
|
|
677
|
-
chi.tool("mcp_call", CALL_DESCRIPTION, params: CALL_PARAMS, label: "mcp", preview: ->(args) { call_preview(args) },
|
|
678
|
-
targets: ->(args) { call_targets(args) }) do |args, ctx|
|
|
679
|
-
mcp_call(args["tool"].to_s, args["args"], ctx)
|
|
680
|
-
end
|
|
681
|
-
@fixed_tokens = spec_tokens(Samagotchi::Plugin::Api.tool_spec("find_mcp_tools", description, params: FIND_PARAMS) { nil }) +
|
|
682
|
-
spec_tokens(Samagotchi::Plugin::Api.tool_spec("mcp_call", CALL_DESCRIPTION, params: CALL_PARAMS) { nil })
|
|
683
|
-
end
|
|
684
|
-
|
|
685
|
-
# find_mcp_tools' description: how to use it, and one line per server
|
|
686
|
-
# from what is known at load (a first run's server: its name, and its
|
|
687
|
-
# description: if set). Never every tool: knowing the names, a model
|
|
688
|
-
# searches less and guesses arguments.
|
|
689
|
-
def find_description
|
|
690
|
-
lines = @servers.map do |server|
|
|
691
|
-
count = offered(server).size
|
|
692
|
-
summary = server_summary(server)
|
|
693
|
-
line = "- #{server.name}"
|
|
694
|
-
line += ": #{summary}" if summary
|
|
695
|
-
line += " (#{count} tool#{"s" unless count == 1})" unless server.tools.empty?
|
|
696
|
-
line
|
|
653
|
+
Samagotchi::Tools::ToolIndex.spec_tokens(Samagotchi::Plugin::Api.tool_spec(name, description, schema: schema) { nil })
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# Every server's tools into chi's index, a group each: its state (what a
|
|
657
|
+
# search's notes say), its summary line, and its tools with the marks
|
|
658
|
+
# and refusals of now (a clash with another server's tool too, so all of
|
|
659
|
+
# them each time). After each index and each state change a note reads;
|
|
660
|
+
# one at a time, so the last one published is the newest.
|
|
661
|
+
def publish
|
|
662
|
+
return unless @source
|
|
663
|
+
|
|
664
|
+
@publish.synchronize do
|
|
665
|
+
groups = @servers.map do |server|
|
|
666
|
+
state = { starting: :starting, failed: :failed }.fetch(server.state, :ready)
|
|
667
|
+
tools = Array(server.tools).map do |tool|
|
|
668
|
+
Samagotchi::Tools::ToolIndex::Tool.new(group: tool.server, name: tool.name, description: tool.description,
|
|
669
|
+
schema: tool.schema, acts_as: tool.chi_name, refusal: refusal(tool),
|
|
670
|
+
mark: mark(tool))
|
|
671
|
+
end
|
|
672
|
+
Samagotchi::Tools::ToolIndex::Group.new(name: server.name, summary: server_summary(server), state: state,
|
|
673
|
+
error: state == :failed ? server.error : nil, tools: tools)
|
|
674
|
+
end
|
|
675
|
+
@source.publish(groups)
|
|
697
676
|
end
|
|
698
|
-
"Search the tools of the MCP servers below by keywords. It answers up to #{FIND_RESULTS} tools, each with " \
|
|
699
|
-
"its name (<server>/<tool>) and inputSchema; call one with mcp_call. An empty query lists every tool's " \
|
|
700
|
-
"name.\nServers:\n#{lines.join("\n")}"
|
|
701
677
|
end
|
|
702
678
|
|
|
703
|
-
# The server's line: its description: from config, else the first
|
|
679
|
+
# The server's line in find_tools: its description: from config, else the first
|
|
704
680
|
# sentence of its instructions, else nothing (its line has the tool
|
|
705
681
|
# count). Never its tool names: a model that knows them calls them
|
|
706
682
|
# blind, without a search (a smoke run's Splash did).
|
|
@@ -711,222 +687,25 @@ class Plugin
|
|
|
711
687
|
server.info
|
|
712
688
|
end
|
|
713
689
|
|
|
714
|
-
# ──
|
|
715
|
-
|
|
716
|
-
# find_mcp_tools: the best matches with their schemas, or with an empty
|
|
717
|
-
# query every server's tool names.
|
|
718
|
-
def find(query, only)
|
|
719
|
-
servers = @servers
|
|
720
|
-
if only && !only.strip.empty?
|
|
721
|
-
servers = @servers.select { |server| server.name == only.strip }
|
|
722
|
-
return "Error: no MCP server #{only.strip}. Servers: #{@servers.map(&:name).join(", ")}" if servers.empty?
|
|
723
|
-
end
|
|
724
|
-
return names_text(servers) if words(query).empty?
|
|
725
|
-
|
|
726
|
-
matches = ranked(query, servers.flat_map(&:tools)).take(FIND_RESULTS)
|
|
727
|
-
return "No MCP tool matches \"#{query.strip}\".\n\n#{names_text(servers)}" if matches.empty?
|
|
728
|
-
|
|
729
|
-
blocks = matches.map { |tool| match_text(tool) }
|
|
730
|
-
unsearched = servers.filter_map { |server| "#{server.name} #{note(server)}" if server.tools.empty? && note(server) }
|
|
731
|
-
blocks << "Not searched: #{unsearched.join("; ")}." unless unsearched.empty?
|
|
732
|
-
"Call one with mcp_call(tool: \"<server>/<tool>\", args: {…}).\n\n#{blocks.join("\n\n")}"
|
|
733
|
-
end
|
|
690
|
+
# ── Calls ─────────────────────────────────────────────────────────────
|
|
734
691
|
|
|
735
|
-
#
|
|
736
|
-
#
|
|
737
|
-
def
|
|
738
|
-
server = @servers.find { |s| s.name == tool.
|
|
739
|
-
|
|
740
|
-
refused = refusal(tool)
|
|
741
|
-
return "#{head}: #{tool.description}\nIt can't be called: #{refused}" if refused
|
|
692
|
+
# A call the index looked up and checked (call_tool): +tool+ a
|
|
693
|
+
# ToolIndex::Tool, +args+ typed by its schema.
|
|
694
|
+
def run(tool, args, ctx)
|
|
695
|
+
server = @servers.find { |s| s.name == tool.group }
|
|
696
|
+
return "Error: no MCP server #{tool.group}" unless server
|
|
742
697
|
|
|
743
|
-
|
|
744
|
-
end
|
|
745
|
-
|
|
746
|
-
# What a search says about a server whose tools can't be called now.
|
|
747
|
-
def note(server)
|
|
748
|
-
case server.state
|
|
749
|
-
when :failed then "(failed: #{server.error})"
|
|
750
|
-
when :starting then "(starting, try again)"
|
|
751
|
-
end
|
|
752
|
-
end
|
|
753
|
-
|
|
754
|
-
# Each server's tool names, one line each.
|
|
755
|
-
def names_text(servers)
|
|
756
|
-
lines = servers.map do |server|
|
|
757
|
-
names = server.tools.map { |tool| [tool.name, mark(tool)].compact.join(" ") }
|
|
758
|
-
head = [server.name, note(server)].compact.join(" ")
|
|
759
|
-
names.empty? ? head : "#{head}: #{names.join(", ")}"
|
|
760
|
-
end
|
|
761
|
-
"MCP tools by server (call one as <server>/<tool>; search for its inputSchema):\n#{lines.join("\n")}"
|
|
762
|
-
end
|
|
763
|
-
|
|
764
|
-
# +tools+ that share a word with +query+, best first: a word scores its
|
|
765
|
-
# IDF over +tools+ (a word most of them have counts little), times
|
|
766
|
-
# NAME_WEIGHT in the tool's or its server's name. Ties keep the index's
|
|
767
|
-
# order.
|
|
768
|
-
def ranked(query, tools)
|
|
769
|
-
wanted = words(query).uniq
|
|
770
|
-
docs = tools.map { |tool| [words("#{tool.server} #{tool.name}"), words(tool.description)] }
|
|
771
|
-
df = Hash.new(0)
|
|
772
|
-
docs.each { |name, text| (name | text).each { |word| df[word] += 1 } }
|
|
773
|
-
count = tools.size.to_f
|
|
774
|
-
scored = tools.each_with_index.filter_map do |tool, i|
|
|
775
|
-
name, text = docs[i]
|
|
776
|
-
score = wanted.sum do |word|
|
|
777
|
-
idf = Math.log(((count - df[word] + 0.5) / (df[word] + 0.5)) + 1)
|
|
778
|
-
if name.include?(word) then NAME_WEIGHT * idf
|
|
779
|
-
elsif text.include?(word) then idf
|
|
780
|
-
else 0
|
|
781
|
-
end
|
|
782
|
-
end
|
|
783
|
-
[tool, score, i] if score.positive?
|
|
784
|
-
end
|
|
785
|
-
scored.sort_by { |_, score, i| [-score, i] }.map(&:first)
|
|
786
|
-
end
|
|
787
|
-
|
|
788
|
-
# Lower-case words, split at camelCase too, a plural's s dropped.
|
|
789
|
-
def words(text)
|
|
790
|
-
text.to_s.gsub(/([a-z0-9])([A-Z])/, '\1 \2').downcase.scan(/[a-z0-9]+/).map do |word|
|
|
791
|
-
word.length > 3 && word.end_with?("s") && !word.end_with?("ss") ? word.chomp("s") : word
|
|
792
|
-
end
|
|
793
|
-
end
|
|
794
|
-
|
|
795
|
-
# ── mcp_call ──────────────────────────────────────────────────────────
|
|
796
|
-
|
|
797
|
-
# A call of the tool +name+ names, with +args+ typed by its schema. A
|
|
798
|
-
# failure the arguments may cause answers its inputSchema too. It fails
|
|
799
|
-
# closed: a tool that isn't the one guardrails were given (#claimed) is
|
|
800
|
-
# refused.
|
|
801
|
-
def mcp_call(name, args, ctx)
|
|
802
|
-
tool = lookup(name)
|
|
803
|
-
return tool if tool.is_a?(String)
|
|
804
|
-
unless claimed(name.strip)&.first == tool.chi_name
|
|
805
|
-
return "Error: the MCP tools changed while the call waited for its server; call #{tool.server}/#{tool.name} again"
|
|
806
|
-
end
|
|
807
|
-
|
|
808
|
-
server = @servers.find { |s| s.name == tool.server }
|
|
809
|
-
refused = refusal(tool)
|
|
810
|
-
return "Error: #{tool.server}/#{tool.name} can't be called: #{refused}" if refused
|
|
811
|
-
|
|
812
|
-
inner = inner_args(args, tool)
|
|
813
|
-
return "Error: args must be an object.#{schema_note(tool)}" unless inner
|
|
814
|
-
|
|
815
|
-
call(server, tool, inner, ctx)
|
|
816
|
-
end
|
|
817
|
-
|
|
818
|
-
# The McpTool +name+ names: <server>/<tool>, or mcp_<server>_<tool>
|
|
819
|
-
# looked up by chi_name (never parsed: server names may hold _, and long
|
|
820
|
-
# names are cut). A miss while a server is starting waits for it (its
|
|
821
|
-
# tools may be the one; for a <server>/ name, that server's start only);
|
|
822
|
-
# then a miss is the error text: a failed
|
|
823
|
-
# server's error, else the closest tools.
|
|
824
|
-
# @return [McpTool, String]
|
|
825
|
-
def lookup(name, waited: false)
|
|
826
|
-
name = name.strip
|
|
827
|
-
tool = @servers.lazy.filter_map do |server|
|
|
828
|
-
next unless name.start_with?("#{server.name}/")
|
|
829
|
-
|
|
830
|
-
rest = name.delete_prefix("#{server.name}/")
|
|
831
|
-
server.tools.find { |t| t.name == rest }
|
|
832
|
-
end.first
|
|
833
|
-
return tool if tool
|
|
834
|
-
|
|
835
|
-
same = @servers.flat_map(&:tools).select { |t| t.chi_name == name }
|
|
836
|
-
return same.first if same.size == 1
|
|
837
|
-
if same.size > 1
|
|
838
|
-
return "Error: #{name} names #{same.map { |t| "#{t.server}/#{t.name}" }.join(" and ")}; " \
|
|
839
|
-
"call one as <server>/<tool>"
|
|
840
|
-
end
|
|
841
|
-
|
|
842
|
-
# A name with a configured server's <server>/ waits for that server
|
|
843
|
-
# only; any other name (mcp_<server>_<tool>) for every one starting.
|
|
844
|
-
named = @servers.select { |server| name.start_with?("#{server.name}/") }
|
|
845
|
-
starting = (named.empty? ? @servers : named).select { |server| server.state == :starting }
|
|
846
|
-
unless waited || starting.empty?
|
|
847
|
-
starting.each { |server| wait_started(server) }
|
|
848
|
-
return lookup(name, waited: true)
|
|
849
|
-
end
|
|
850
|
-
|
|
851
|
-
failed = @servers.find { |server| server.state == :failed && name.start_with?("#{server.name}/") }
|
|
852
|
-
return "Error: MCP server #{failed.name} didn't start: #{failed.error}" if failed
|
|
853
|
-
|
|
854
|
-
closest = ranked(name, @servers.flat_map(&:tools)).take(CLOSEST)
|
|
855
|
-
return "Error: no MCP tool #{name}. Search with find_mcp_tools." if closest.empty?
|
|
856
|
-
|
|
857
|
-
"Error: no MCP tool #{name}. Closest: #{closest.map { |t| "#{t.server}/#{t.name}" }.join(", ")}"
|
|
698
|
+
call(server, tool, args, ctx)
|
|
858
699
|
end
|
|
859
700
|
|
|
860
701
|
# Wait for a first-run server's start (its init task's); a failure is
|
|
861
702
|
# its init card's.
|
|
862
703
|
def wait_started(server)
|
|
863
|
-
server
|
|
704
|
+
server&.service&.value
|
|
864
705
|
rescue StandardError
|
|
865
706
|
nil
|
|
866
707
|
end
|
|
867
708
|
|
|
868
|
-
# The inner arguments as a Hash typed by +tool+'s schema (a JSON text
|
|
869
|
-
# parsed; untyped without a tool), nil when they aren't an object; none
|
|
870
|
-
# is {}.
|
|
871
|
-
def inner_args(args, tool)
|
|
872
|
-
args = {} if args.nil? || (args.is_a?(String) && args.strip.empty?)
|
|
873
|
-
args = Samagotchi::Tools::Args.coerce({ "args" => args }, { "properties" => { "args" => { "type" => "object" } } })["args"]
|
|
874
|
-
return nil unless args.is_a?(Hash)
|
|
875
|
-
|
|
876
|
-
Samagotchi::Tools::Args.coerce(args, tool&.schema)
|
|
877
|
-
end
|
|
878
|
-
|
|
879
|
-
# Appended to a failure the arguments may cause: the tool's inputSchema,
|
|
880
|
-
# so the next call can fix them (a call guessed without a search).
|
|
881
|
-
def schema_note(tool)
|
|
882
|
-
"\n\n#{tool.server}/#{tool.name}'s inputSchema: #{JSON.generate(tool.schema)}"
|
|
883
|
-
end
|
|
884
|
-
|
|
885
|
-
# The activity row: <server>/<tool> and its arguments.
|
|
886
|
-
def call_preview(args)
|
|
887
|
-
tool = lookup_quietly(args["tool"].to_s)
|
|
888
|
-
name = tool ? "#{tool.server}/#{tool.name}" : args["tool"].to_s.strip
|
|
889
|
-
inner = tool && inner_args(args["args"], tool)
|
|
890
|
-
inner ? [name, preview(inner)].reject(&:empty?).join(" ") : name
|
|
891
|
-
end
|
|
892
|
-
|
|
893
|
-
# What guardrail rules match an mcp_call: the tool it calls, by today's
|
|
894
|
-
# mcp_<server>_<tool> (rules written for it still fire), its arguments,
|
|
895
|
-
# and the question's label. A tool not indexed yet (its server still
|
|
896
|
-
# starting) is named from what the model gave (#claimed), so a rule on
|
|
897
|
-
# it fires before the call waits for the server. Nothing for a name
|
|
898
|
-
# that can't be a tool (mcp_call refuses it).
|
|
899
|
-
def call_targets(args)
|
|
900
|
-
name = args["tool"].to_s.strip
|
|
901
|
-
tool = lookup_quietly(name)
|
|
902
|
-
acts_as, label = tool ? [tool.chi_name, "#{tool.server}: #{tool.name}"] : claimed(name)
|
|
903
|
-
return {} unless acts_as
|
|
904
|
-
|
|
905
|
-
{ acts_as: acts_as, args: inner_args(args["args"], tool) || {}, label: label }.compact
|
|
906
|
-
end
|
|
907
|
-
|
|
908
|
-
# The mcp_<server>_<tool> and label +name+ stands for, from the name
|
|
909
|
-
# alone: <server>/<tool> of a configured server (the one whose index has
|
|
910
|
-
# the tool, else the first whose name it starts with), or an
|
|
911
|
-
# mcp_<server>_<tool> as it is (no label). Nil for anything else.
|
|
912
|
-
# @return [Array(String, String), Array(String, nil), nil]
|
|
913
|
-
def claimed(name)
|
|
914
|
-
servers = @servers.select { |server| name.start_with?("#{server.name}/") }
|
|
915
|
-
server = servers.find { |s| s.tools.any? { |t| t.name == name.delete_prefix("#{s.name}/") } } || servers.first
|
|
916
|
-
if server
|
|
917
|
-
rest = name.delete_prefix("#{server.name}/")
|
|
918
|
-
[tool_name(server.name, rest), "#{server.name}: #{rest}"]
|
|
919
|
-
elsif name.start_with?("mcp_")
|
|
920
|
-
[name, nil]
|
|
921
|
-
end
|
|
922
|
-
end
|
|
923
|
-
|
|
924
|
-
# #lookup's tool, without waiting for a start: nil on a miss.
|
|
925
|
-
def lookup_quietly(name)
|
|
926
|
-
tool = lookup(name, waited: true)
|
|
927
|
-
tool.is_a?(McpTool) ? tool : nil
|
|
928
|
-
end
|
|
929
|
-
|
|
930
709
|
# mcp_<server>_<tool> in the tool name rule: a-z, 0-9 and _, at most 48.
|
|
931
710
|
def tool_name(server, tool)
|
|
932
711
|
"mcp_#{server}_#{tool}".downcase.gsub(/[^a-z0-9_]+/, "_").squeeze("_")[0, NAME_CHARS].sub(/_+\z/, "")
|
|
@@ -938,15 +717,9 @@ class Plugin
|
|
|
938
717
|
text.length > DESCRIPTION_CHARS ? "#{text[0, DESCRIPTION_CHARS - 1]}…" : text
|
|
939
718
|
end
|
|
940
719
|
|
|
941
|
-
|
|
942
|
-
line = args.map { |key, value| "#{key}=#{value.is_a?(String) ? value : JSON.generate(value)}" }.join(" ")
|
|
943
|
-
line = line.gsub(/\s+/, " ")
|
|
944
|
-
line.length > PREVIEW_CHARS ? "#{line[0, PREVIEW_CHARS - 1]}…" : line
|
|
945
|
-
end
|
|
946
|
-
|
|
947
|
-
# A tools/call of +tool+ (McpTool), as the model's tool result. A cached
|
|
720
|
+
# A tools/call of +tool+ (ToolIndex::Tool), as the model's tool result. A cached
|
|
948
721
|
# server starts here, on its first call. The server's isError and a
|
|
949
|
-
# JSON-RPC error answer the tool's inputSchema too (
|
|
722
|
+
# JSON-RPC error answer the tool's inputSchema too (ToolIndex.schema_note).
|
|
950
723
|
def call(server, tool, args, ctx)
|
|
951
724
|
return "Error: MCP server #{server.name} didn't start: #{server.error}" if server.state == :failed
|
|
952
725
|
|
|
@@ -962,7 +735,7 @@ class Plugin
|
|
|
962
735
|
result = client.request("tools/call", { name: tool.name, arguments: args }, timeout: server.timeout,
|
|
963
736
|
cancelled: -> { ctx.cancelled? })
|
|
964
737
|
text, images = content(result, server, tool.name)
|
|
965
|
-
return "Error: #{text.empty? ? "the tool failed" : text}#{schema_note(tool)}" if result["isError"]
|
|
738
|
+
return "Error: #{text.empty? ? "the tool failed" : text}#{Samagotchi::Tools::ToolIndex.schema_note(tool)}" if result["isError"]
|
|
966
739
|
|
|
967
740
|
images.empty? ? text : Samagotchi::Plugin::ToolResult.new(text, images: images)
|
|
968
741
|
rescue Client::Dead => e
|
|
@@ -970,7 +743,7 @@ class Plugin
|
|
|
970
743
|
rescue Client::Timeout, Client::Cancelled, Samagotchi::Plugin::Service::Stopped => e
|
|
971
744
|
"Error: #{e.message}"
|
|
972
745
|
rescue Client::Error => e
|
|
973
|
-
"Error: #{e.message}#{schema_note(tool)}"
|
|
746
|
+
"Error: #{e.message}#{Samagotchi::Tools::ToolIndex.schema_note(tool)}"
|
|
974
747
|
end
|
|
975
748
|
|
|
976
749
|
# Start a cached server for a call (#spawn indexes the live list). A
|
|
@@ -981,6 +754,7 @@ class Plugin
|
|
|
981
754
|
client = server.service.value
|
|
982
755
|
started = server.state == :cached
|
|
983
756
|
server.state = :running
|
|
757
|
+
publish
|
|
984
758
|
ctx.log.info("mcp_server_started", server: server.name, pid: client.pid, tools: server.listed.size, lazy: true) if started
|
|
985
759
|
client
|
|
986
760
|
rescue Client::Cancelled => e
|
|
@@ -992,6 +766,7 @@ class Plugin
|
|
|
992
766
|
|
|
993
767
|
server.state = :failed
|
|
994
768
|
server.error = e.message
|
|
769
|
+
publish
|
|
995
770
|
ctx.log.warn("mcp_server_failed", server: server.name, error: e.class.name, msg: e.message, lazy: true)
|
|
996
771
|
ctx.notify("MCP server #{server.name} didn't start: #{e.message}; its calls fail until chi restarts", level: :warn)
|
|
997
772
|
"Error: MCP server #{server.name} didn't start: #{e.message}"
|
|
@@ -1020,6 +795,7 @@ class Plugin
|
|
|
1020
795
|
return "Error: MCP server #{server.name} didn't restart: #{e.message}"
|
|
1021
796
|
end
|
|
1022
797
|
server.state = :running
|
|
798
|
+
publish
|
|
1023
799
|
ctx.log.info("mcp_server_restarted", server: server.name, pid: client.pid, restarts: server.restarts)
|
|
1024
800
|
client
|
|
1025
801
|
end
|
|
@@ -1122,6 +898,7 @@ class Plugin
|
|
|
1122
898
|
|
|
1123
899
|
server.state = :exited
|
|
1124
900
|
server.error = reason
|
|
901
|
+
publish
|
|
1125
902
|
ctx.log.warn("mcp_server_exited", server: server.name, msg: reason, restarts: server.restarts)
|
|
1126
903
|
after = if server.restarts < MAX_RESTARTS
|
|
1127
904
|
"it restarts on its next call"
|
|
@@ -1140,11 +917,22 @@ class Plugin
|
|
|
1140
917
|
tools = server.tools.sort_by(&:name).map { |t| ["- `#{t.server}/#{t.name}`", mark(t)].compact.join(" ") }
|
|
1141
918
|
tools.empty? ? head : "#{head}\n#{tools.join("\n")}"
|
|
1142
919
|
end
|
|
920
|
+
[*servers, totals_text].join("\n\n")
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# /mcp's last line: what every request carries, and the servers' tools'
|
|
924
|
+
# tokens. When another tool took both names (chi's tool index left its
|
|
925
|
+
# tools out: a load warning), that the model can't reach them.
|
|
926
|
+
def totals_text
|
|
927
|
+
if @source.declared_names.empty?
|
|
928
|
+
return "The search and call tools aren't declared in this session (other tools have their names; see the " \
|
|
929
|
+
"load warnings), so the model can't reach the tools above."
|
|
930
|
+
end
|
|
931
|
+
|
|
1143
932
|
total = @servers.sum { |server| OFFERED.include?(server.state) ? server.tokens : 0 }
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
.join("\n\n")
|
|
933
|
+
"Every request carries #{@source.declared_names.join(" and ")}: ~#{thousands(@source.declared_tokens)} tokens. " \
|
|
934
|
+
"The tools above, ~#{thousands(total)} tokens, reach the model only in a search's answer " \
|
|
935
|
+
"(estimated: their JSON as the chat API gets it, ÷ #{format("%g", Samagotchi::TokenUsage::CHARS_PER_TOKEN)})."
|
|
1148
936
|
end
|
|
1149
937
|
|
|
1150
938
|
def state_text(server)
|
|
@@ -99,7 +99,7 @@ bundles:
|
|
|
99
99
|
after: 50 # tool calls in one turn with no answer before the first check
|
|
100
100
|
every: 50 # then again every N more
|
|
101
101
|
mode: ask # ask (a card) | nudge (nudge the model by itself) | notify; message:, ignore_tools: [...]
|
|
102
|
-
mcp: # the model searches the tools (
|
|
102
|
+
mcp: # the model searches the tools (find_tools) and calls one (call_tool, <server>/<tool>); /mcp lists them
|
|
103
103
|
timeout: 60 # per call, seconds; startup_timeout: 10
|
|
104
104
|
servers:
|
|
105
105
|
files: # server name
|
|
@@ -107,15 +107,19 @@ bundles:
|
|
|
107
107
|
env: {NODE_OPTIONS: "--no-warnings"} # optional, added to chi's env
|
|
108
108
|
cwd: ~/scratch # optional; default the session's cwd
|
|
109
109
|
tools: [read_*, list_directory] # optional filter (globs)
|
|
110
|
-
description: files in ~/scratch # optional: the server's line in
|
|
110
|
+
description: files in ~/scratch # optional: the server's line in find_tools (default: its instructions' first sentence)
|
|
111
111
|
attach_image_paths: true # default: an answer that is only an image's path (temp dir/cwd) is attached as a picture
|
|
112
112
|
start: lazy # default: tools from the saved tools/list, the server starts on the first call; eager: with every session
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cover every MCP tool:
|
|
115
|
+
A guardrail rule's `tool:` may be a glob (`tool: "mcp_*"`, verdict `ask`) to cover every MCP tool: a `call_tool` acts as `mcp_<server>_<tool>`, so `tool: "mcp_github_*"` covers one server's (a `find_tools` search is never asked about); see `docs/guardrails.md`. A server's optional `description:` is its line in `find_tools`' description. The mcp bundle saves each server's tool list in `$XDG_STATE_HOME/samagotchi/plugins/mcp/tools-<server>.json` (keyed by a digest of command/env/cwd): a changed server config is picked up by the next session start, which shows "Starting MCP server x (config changed, …)".
|
|
116
116
|
|
|
117
117
|
**Preservation rule**: `ConfigFile.write_default_model!` and `ConfigFile.write_model_alias!` (`/model --default`, `/model --alias`) change one key on its own line of the text (`ConfigTextEdit`: the user's comments and layout stay; a new key goes at the end of its section, a missing section at the end of the file), check that the result parses to the old data plus that key (else they dump the whole file from it), and write through `AtomicFile` (a unique temp file renamed over config.yml, through a symlink, keeping its mode). Never overwrite the file with only scalar keys — that would clobber `hooks:` / `model_aliases:` / `hosts:` / `recap:` / `guardrails:` / `bundles:`.
|
|
118
118
|
|
|
119
|
+
## Host API keys
|
|
120
|
+
|
|
121
|
+
A key never goes into config.yml: `api_key_env: VAR` names it. chi reads `VAR` from the environment, else (macOS) the login Keychain item with service `chi`, account `VAR` (`HostKeys`; `SAMAGOTCHI_KEYCHAIN=off` skips it). Don't store or read a key yourself: tell the user to run `security add-generic-password -s chi -a VAR -w` (it prompts for the key). `chi self` shows `VAR (set|keychain|unset)`; `chi desktop status` which keys the desktop helper gets.
|
|
122
|
+
|
|
119
123
|
## Workflow for any config edit
|
|
120
124
|
|
|
121
125
|
1. **Read** the current file via `read` tool (or `ConfigFile.global_path`). If `File.file?` false, start from `{}`.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.67.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
|
-
self_map.md: sha256:
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
8
|
+
self_map.md: sha256:9528cc4c7d512a3fdfb103bf966a0359acad11c218666512d628a8d00378aaab
|
|
9
|
+
config_modification_protocol.md: sha256:c7decf27af999066195f83e71222a6ae294f13174ced9664a21cd1dabf8b854a
|
|
10
10
|
memory_guide.md: sha256:c48eea744db69e4f3e9bb415e9a03a8c78bbcaa1205ad3b1fb157a079f92d22e
|
|
11
11
|
delegated.md: sha256:a0d6ddcd4a8be06526d964898611775fd251cfd9d145a9d16afaccaa503597bd
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
commands (`anytime:` ones run mid-turn), model tools (they can return images I see), hooks,
|
|
51
51
|
cards, side answers (`ask_model`),
|
|
52
52
|
child sessions and services (e.g. MCP server processes). Shipped: `btw` (`/btw` side question),
|
|
53
|
-
`mcp` (MCP server tools, searched with
|
|
53
|
+
`mcp` (MCP server tools, searched with find_tools and called with call_tool; a screenshot comes as a picture), `guardrails` (rules), `known-names` (typo guard), `source-links`
|
|
54
54
|
(turns source refs like JIRA-123 in an answer into links in the web, and a one-line note), `loop-guard`
|
|
55
55
|
(denies a repeated tool call with the same result, stops the turn after a few, and cuts thinking that
|
|
56
56
|
repeats itself, asking me again), `check-in` (after N tool
|