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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -1
  3. data/docs/architecture.md +50 -28
  4. data/docs/cli.md +3 -1
  5. data/docs/configuration.md +50 -9
  6. data/docs/context.md +35 -1
  7. data/docs/desktop.md +90 -22
  8. data/docs/guardrails.md +8 -5
  9. data/docs/hooks.md +2 -2
  10. data/docs/internals/anthropic.md +156 -0
  11. data/docs/internals/background-tasks.md +1 -1
  12. data/docs/internals/bundles.md +2 -0
  13. data/docs/internals/engine.md +71 -29
  14. data/docs/internals/gemma4-contract.md +1 -1
  15. data/docs/internals/llm-context-bench.md +5 -4
  16. data/docs/internals/prompt-caching.md +26 -11
  17. data/docs/plugins.md +126 -23
  18. data/docs/sessions.md +6 -6
  19. data/docs/sub-agent.md +4 -1
  20. data/docs/testing.md +17 -3
  21. data/lib/samagotchi/bootstrap/probe.rb +4 -3
  22. data/lib/samagotchi/bootstrap_command.rb +13 -3
  23. data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
  24. data/lib/samagotchi/bridge.rb +6 -3
  25. data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
  26. data/lib/samagotchi/bundles/claude-peer/plugin.rb +34 -313
  27. data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
  28. data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
  29. data/lib/samagotchi/bundles/codex-peer/manifest.yml +7 -6
  30. data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
  31. data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
  32. data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
  33. data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
  34. data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
  35. data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
  36. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  37. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  38. data/lib/samagotchi/config.rb +15 -7
  39. data/lib/samagotchi/context_absorber.rb +66 -13
  40. data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
  41. data/lib/samagotchi/desktop/macos/App.swift +3 -3
  42. data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
  43. data/lib/samagotchi/desktop/macos/Images.swift +2 -2
  44. data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
  45. data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
  46. data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
  47. data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
  48. data/lib/samagotchi/desktop/macos.rb +84 -13
  49. data/lib/samagotchi/desktop_command.rb +14 -3
  50. data/lib/samagotchi/engine.rb +96 -191
  51. data/lib/samagotchi/engine_peers.rb +3 -0
  52. data/lib/samagotchi/guardrail_wiring.rb +1 -1
  53. data/lib/samagotchi/guardrails/approval.rb +1 -1
  54. data/lib/samagotchi/guardrails/approvals.rb +1 -1
  55. data/lib/samagotchi/guardrails/rules.rb +1 -1
  56. data/lib/samagotchi/guardrails/targets.rb +1 -1
  57. data/lib/samagotchi/hook_routes.rb +151 -0
  58. data/lib/samagotchi/hooks/registry.rb +2 -2
  59. data/lib/samagotchi/host_keys.rb +147 -0
  60. data/lib/samagotchi/llm/adapter_common.rb +138 -0
  61. data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
  62. data/lib/samagotchi/llm/api_key.rb +23 -7
  63. data/lib/samagotchi/llm/chat_loop.rb +10 -6
  64. data/lib/samagotchi/llm/openai_chat.rb +9 -92
  65. data/lib/samagotchi/llm/refusal_memo.rb +29 -0
  66. data/lib/samagotchi/llm_context_override.rb +1 -0
  67. data/lib/samagotchi/peer_sources.rb +58 -0
  68. data/lib/samagotchi/plugin/api.rb +64 -155
  69. data/lib/samagotchi/plugin/loader.rb +4 -2
  70. data/lib/samagotchi/plugin/peer.rb +394 -0
  71. data/lib/samagotchi/plugin/tool_spec.rb +166 -0
  72. data/lib/samagotchi/plugin_tasks.rb +2 -2
  73. data/lib/samagotchi/row_refs.rb +1 -1
  74. data/lib/samagotchi/self_report.rb +5 -3
  75. data/lib/samagotchi/send_command.rb +21 -11
  76. data/lib/samagotchi/session_continue.rb +58 -15
  77. data/lib/samagotchi/session_inbox.rb +0 -13
  78. data/lib/samagotchi/tool_call_parser.rb +1 -1
  79. data/lib/samagotchi/toolkit_binding.rb +138 -0
  80. data/lib/samagotchi/tools/context_read.rb +14 -0
  81. data/lib/samagotchi/tools/execute.rb +12 -12
  82. data/lib/samagotchi/tools/registry.rb +1 -1
  83. data/lib/samagotchi/tools/task_create.rb +6 -6
  84. data/lib/samagotchi/tools/task_get.rb +13 -13
  85. data/lib/samagotchi/tools/task_list.rb +6 -6
  86. data/lib/samagotchi/tools/task_record.rb +52 -0
  87. data/lib/samagotchi/tools/task_runtime.rb +55 -91
  88. data/lib/samagotchi/tools/task_stop.rb +6 -6
  89. data/lib/samagotchi/tools/task_wait.rb +12 -13
  90. data/lib/samagotchi/tools/tool_index.rb +454 -0
  91. data/lib/samagotchi/update_command.rb +9 -6
  92. data/lib/samagotchi/version.rb +1 -1
  93. data/lib/samagotchi/web/app.rb +16 -9
  94. data/lib/samagotchi/web/capabilities.rb +1 -1
  95. data/lib/samagotchi/web/public/activity.js +2 -2
  96. data/lib/samagotchi/web/public/annotate_actions.js +1 -1
  97. data/lib/samagotchi/web/public/app.js +31 -36
  98. data/lib/samagotchi/web/public/chains.js +1 -1
  99. data/lib/samagotchi/web/public/commands.js +2 -2
  100. data/lib/samagotchi/web/public/composer_size.js +2 -2
  101. data/lib/samagotchi/web/public/copy.js +1 -1
  102. data/lib/samagotchi/web/public/ctx.js +1 -1
  103. data/lib/samagotchi/web/public/delivery_line.js +4 -4
  104. data/lib/samagotchi/web/public/families_view.js +1 -1
  105. data/lib/samagotchi/web/public/format.js +11 -7
  106. data/lib/samagotchi/web/public/images.js +25 -2
  107. data/lib/samagotchi/web/public/llm_context_chip.js +2 -2
  108. data/lib/samagotchi/web/public/notify.js +6 -4
  109. data/lib/samagotchi/web/public/question_card.js +1 -1
  110. data/lib/samagotchi/web/public/ref_scan.js +1 -1
  111. data/lib/samagotchi/web/public/session_state.js +14 -0
  112. data/lib/samagotchi/web/public/session_title.js +2 -3
  113. data/lib/samagotchi/web/public/sessions_list.js +0 -8
  114. data/lib/samagotchi/web/public/stage_model.js +2 -2
  115. data/lib/samagotchi/web/public/strip.js +3 -3
  116. data/lib/samagotchi/web/public/task_stop.js +3 -2
  117. data/lib/samagotchi/web/public/turn_events.js +1 -1
  118. data/lib/samagotchi/web/public/turn_html.js +2 -2
  119. data/lib/samagotchi/web/public/turn_model.js +2 -2
  120. data/lib/samagotchi/web/public/turn_view.js +1 -1
  121. data/lib/samagotchi/web/server.rb +0 -4
  122. data/lib/samagotchi/worker.rb +11 -8
  123. data/lib/samagotchi/worker_inbound.rb +36 -4
  124. data/lib/samagotchi/worker_wakes.rb +67 -7
  125. metadata +14 -1
@@ -72,7 +72,7 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
72
72
  │
73
73
  └── ensure: release_turn(probe_cancel_before)
74
74
  # TurnState#finish! (turn flag off, controller, sink and steers cleared)
75
- # GenerationPhase#finished!, WaitingSteer#clear!
75
+ # SteerCut#finished! (its GenerationPhase finished, its WaitingSteer cleared)
76
76
  # probe cancel swapped back
77
77
  # the context window cache dropped, so the next turn asks the server again
78
78
  # steers left logged as dropped, activity recorded, turn-scoped hooks cleared
@@ -83,7 +83,7 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
83
83
  ```
84
84
  StandardError in run_turn:
85
85
  → if turn.ended (it came from the after_turn/session_end hooks): logged as post_turn_error, re-raised
86
- → else: end_turn(turn, "failed", save: true)
86
+ → else: end_turn(turn, "failed", save: true) (a save that fails there is logged, turn_end_save_failed)
87
87
  # Both loops call LLM::FailedTurn.attach(e, conversation) before re-raising, so
88
88
  # failed_messages keeps the loop's partial conversation (tool iterations included),
89
89
  # else turn.messages, with TurnNote.failed(summary, continued: turn.continue) at the tail.
@@ -135,7 +135,8 @@ Engine#backend → backend_for(@host_registry.resolve(@effective_model_name))
135
135
  ```
136
136
 
137
137
  Both implement the `LLM::ModelBackend` contract (`#complete(messages:, max_iterations:, on_stream_event:,
138
- cancel_controller:, model_name:, max_tool_output_chars:, pending_input:)`). `NativeBackend#complete` only calls
138
+ cancel_controller:, model_name:, max_tool_output_chars:, pending_input:, reasoning_since:)`; `reasoning_since:` is
139
+ where the run a step-limit Continue resumes started, the Engine's `resume_from`). `NativeBackend#complete` only calls
139
140
  `KernelLoop#run` with the same arguments. `ChatLoop` runs its own loop but is built with the same KernelLoop,
140
141
  whose tools, hooks, gate and dispatch it uses.
141
142
 
@@ -172,17 +173,21 @@ KernelLoop#run(messages, max_iterations:, on_stream_event:, cancel_controller:,
172
173
  `turn.conversation` is the loop's own copy, not the Engine's `turn.messages`. A failure attaches it to the error
173
174
  (`LLM::FailedTurn.attach`), so `Engine#failed_messages` can keep it; a Ctrl-C does not (see Error paths).
174
175
 
175
- ### ChatLoop (the OpenAI chat API)
176
+ ### ChatLoop (the chat APIs)
176
177
 
177
- Runs hosts with `api: openai`: remote providers such as OpenRouter (Claude and others) and any OpenAI-compatible
178
- chat server. The model gets the tools as JSON schemas and returns structured tool calls.
178
+ Runs hosts whose api has a chat adapter (`LLM::Adapters::BY_API`): `api: openai` on `LLM::OpenAIChat` (remote
179
+ providers such as OpenRouter, Claude among others, and any OpenAI-compatible chat server) and `api: anthropic` on
180
+ `LLM::AnthropicMessages` (Anthropic's Messages API; [anthropic.md](anthropic.md)). The Engine sets the adapter per
181
+ turn for the effective model's host (`HostRegistry#adapter_for`). The model gets the tools as JSON schemas and returns
182
+ structured tool calls.
179
183
 
180
184
  ```
181
185
  ChatLoop#complete(messages:, max_iterations:, on_stream_event:, cancel_controller:,
182
- model_name:, max_tool_output_chars:, pending_input:)
186
+ model_name:, max_tool_output_chars:, pending_input:, reasoning_since:)
183
187
  │
184
188
  │ conversation = Array(messages).map(&:dup) # a shallow copy of each message
185
- │ Run.new(self, conversation, on_stream_event, cancel_controller, model_name, pending_input)
189
+ │ Run.new(self, conversation, on_stream_event, cancel_controller, model_name, pending_input,
190
+ │ start: reasoning_since)
186
191
  │ .call(max_iterations: max_iterations || 1, cap: KernelLoop.resolve_output_char_cap(max_tool_output_chars))
187
192
  │
188
193
  └──→ LLM::ModelResult(text, conversation, exhausted, canceled, tool_activity, ...)
@@ -191,10 +196,13 @@ ChatLoop#complete(messages:, max_iterations:, on_stream_event:, cancel_controlle
191
196
  The differences from the native loop:
192
197
 
193
198
  - The tools go in each request's `tools` field (`tool_definitions`), not in the system prompt.
194
- - The conversation is converted to the OpenAI wire format (`wire_messages`).
199
+ - The conversation is converted to the OpenAI wire format (`wire_messages`), for every chat host;
200
+ `AnthropicMessages` translates that again into Messages' `system` and content blocks (`LLM::AnthropicWire`).
195
201
  - The first system message carries `cache_split:` (from `SystemPrompt#stable_length`, which the Engine passes in
196
- as `stable_length:`), where the prompt's stable part ends. For remote Claude, `LLM::PromptCache` puts the
197
- first cache breakpoint there; see prompt-caching.md.
202
+ as `stable_length:`), where the prompt's stable part ends. The adapter puts the first cache breakpoint there:
203
+ `OpenAIChat` for a Claude model on a remote host (`LLM::PromptCache.mark`), `AnthropicMessages` on every chat
204
+ request unless its dialect has no breakpoints or the host refused `cache_control` (`AnthropicWire.translate`,
205
+ `Dialect#cache_breakpoints`); see prompt-caching.md and anthropic.md.
198
206
  - Each call's result is its own tool message, paired by `tool_call_id`.
199
207
  - A model turn's saved reasoning (`thinking:`) goes back as `reasoning_content` per the turn's
200
208
  `TurnSettings#reasoning_history` (`ReasoningHistory.resolve`, the session's own level first:
@@ -208,8 +216,15 @@ The differences from the native loop:
208
216
  in `#resumed_run_start`), past a user entry added after that run's last reply (a `!cmd`'s output). Without
209
217
  `since:` (`#reasoning_chars` from outside a Run) the boundary is the last query (a `role: "user"` or `turn_start`
210
218
  entry). `Run#prompt_chars` counts it (`#reasoning_chars`).
219
+ - On an adapter that sends its blocks back (`#replays_content?`: `AnthropicMessages`), a model turn's saved blocks
220
+ (`anthropic_content`, the response's `provider_content`, saved by `Run#with_thinking`) go back verbatim, thinking
221
+ blocks with their signatures (`#replay_blocks`): not for an interrupted entry or one with an unpaired call, and
222
+ without the thinking blocks before the reasoning boundary (the last model entry with tool calls keeps them).
223
+ `#reasoning_history` floors `drop` at `turn` there, since Messages needs the running run's thinking blocks.
211
224
  - A 400 about reasoning (`BadRequest#reasoning_refused?`) for a request that sent reasoning back asks the step again
212
- without it; when that goes through, `#reasoning_history_refused!` marks the model (worker lifetime) and
225
+ without it (not on a replaying adapter, where the body would be the same); when that goes through,
226
+ `#reasoning_history_refused!` marks the model (worker lifetime; the loop's `LLM::RefusalMemo`, as for the thinking
227
+ and sampling refusals) and
213
228
  `:reasoning_history_refused` goes out. When it is refused too, the thinking-fields fallback (`#thinking_refused!`,
214
229
  `:thinking_refused`) runs on it, still without reasoning. Up to three requests per step. A 400 about a thinking
215
230
  block's signature (`BadRequest#thinking_signature?`) or a refused binding control
@@ -256,7 +271,7 @@ One iteration with tool calls:
256
271
  `EVENT_FIELDS`) copy them by these lists, so a new row field is added there. `ref` is `FileRef.for` (a
257
272
  read/write/edit's file against the cwd, `{kind: "file", path, line?, end_line?}`); the web opens it in the editor
258
273
  that `Web::Editor` (web.editor) names, for a local viewer only (`App#viewer_capabilities`).
259
- `refs` is a plugin tool's row refs: its registry entry's `refs` (chi.tool's `refs:` lambda, `Api.per_call`), run
274
+ `refs` is a plugin tool's row refs: its registry entry's `refs` (chi.tool's `refs:` lambda, `ToolSpec#per_call`), run
260
275
  once per call by `RowRefs.for` (never raises: a raise or a non-list gives none, `tool_refs_failed`; each entry a
261
276
  `Ref`, a bad one dropped, `tool_refs_rejected`; a relative file resolved against the session's folder
262
277
  (`KernelLoop#session_cwd`, else the process's), one outside it (its spelling or its real path) dropped, a kept one
@@ -317,7 +332,8 @@ The turn as the Engine sees it (the KernelLoop has its own `Turn` struct):
317
332
 
318
333
  ```ruby
319
334
  Turn = Struct.new(:session, :prompt, :continue, :on_event, :controller, :origin, :started_at, :messages, :ended,
320
- :settings, :id, :limit) do
335
+ :settings, :id, :limit, :resumed, :resume_from) do
336
+ def resumed? = resumed ? true : false # a step-limit Continue
321
337
  def tag(event) = origin ? event.merge(origin: origin) : event # boundary events carry the origin
322
338
  def elapsed = ... # seconds since started_at
323
339
  end
@@ -342,10 +358,14 @@ end
342
358
  running turn's record so far as a killed turn's (canceled, `worker_stopped`), which the next worker loads; the
343
359
  turn's end writes over it.
344
360
 
345
- ### GenerationPhase
361
+ ### SteerCut, GenerationPhase and WaitingSteer
346
362
 
347
- What the running generation streams, fed by the Engine's stream handler: whether a steer sent as a cut (delivery
348
- `cut`: `/cut TEXT`, `chi send --cut`) may cut it. `SteerCut#cut_for_steer` answers `:now`, `:waits` or `:off`; the
363
+ The Engine holds one `SteerCut` (`@steer_cut`), which owns a `GenerationPhase` and a `WaitingSteer`: the Bridge and
364
+ the TUI ask it (`Engine#cut_for_steer`, `#input_epoch`, `#generation_snapshot`), the Engine's stream handler feeds it
365
+ (`SteerCut#observe`), a drain that took input calls `#delivered!`, and `release_turn` calls `#finished!`.
366
+
367
+ `GenerationPhase` is what the running generation streams, fed through `SteerCut#observe`: whether a steer sent as a
368
+ cut (delivery `cut`: `/cut TEXT`, `chi send --cut`) may cut it. `SteerCut#cut_for_steer` answers `:now`, `:waits` or `:off`; the
349
369
  Bridge acks it as `cut`.
350
370
  A generation is cuttable while it has streamed only thinking, is still thinking, and has thought for at least
351
371
  the model's cut wait (`SteerCutAfter.resolve`: `steer_cut_after` per models:/hosts: entry, else `steer.cut_after`) seconds (`cuttable?(min_age)`). It has `started!`, `chunk!`, `retrying!` and `finished!`.
@@ -357,8 +377,8 @@ line and tell whether "cut in now" can land.
357
377
  ### WaitingSteer
358
378
 
359
379
  A message that may cut the generation but came too early (the generation hadn't been thinking long enough). It
360
- waits until the thinking passes the model's cut wait (the Engine checks on each thinking chunk), until a drain hands
361
- the message to the model at a boundary, or until the turn ends (`clear!` in `release_turn`).
380
+ waits until the thinking passes the model's cut wait (`SteerCut#observe` rechecks it on each thinking chunk), until a
381
+ drain hands the message to the model at a boundary, or until the turn ends (`SteerCut#finished!` clears it).
362
382
 
363
383
  ## Events
364
384
 
@@ -414,15 +434,20 @@ hook registry (`@hooks.fire`), not on the sink or the observers. The other hook
414
434
 
415
435
  ```
416
436
  Engine initialization (in this order, among other things):
417
- @turn_state, @generation_phase, @waiting_steer
437
+ @turn_state, @steer_cut (SteerCut: its GenerationPhase and WaitingSteer)
418
438
  @host_registry, @effective_model_name; @client = @host_registry.resolve(...).client
419
439
  @given_profile = profile or nil (resolved on first need, so building an Engine makes no network call)
440
+ @hook_routes (HookRoutes: notices, hook questions, stop_turn/stop_generation, steer attribution)
420
441
  @guardrail_wiring, @question_desk
442
+ @toolkit_binding (ToolkitBinding: the kits, the plugins' among them, and the turn-start apply)
421
443
  @extension_load = ExtensionLoad.new; @hooks = @extension_load.hooks (config.yml's, then the bundles')
422
- @tools = Tools::Builtins.registry; the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins)
444
+ @tools = Tools::Builtins.registry; the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins),
445
+ then @tool_index.declare(@tools): find_tools + call_tool (source "index", not core) when a plugin
446
+ registered a chi.tool_source (Tools::ToolIndex: the index's groups, search, call checks; the source runs
447
+ the call with its own plugin Context)
423
448
  wire_kernel: @kernel = KernelLoop.new(client:, profile:, hooks:, reminder_store:, tools:)
424
449
  @kernel.guardrail_gate = @guardrail_wiring.gate; @kernel.warmup = PromptWarmup.new
425
- @hooks.runtime = hook_runtime
450
+ @hooks.runtime = @hook_routes.runtime (HookRoutes, built first: what a hook or plugin does reaches the UIs)
426
451
  @native_backend = LLM::NativeBackend.new(kernel: @kernel)
427
452
  @prompt_builder = SystemPrompt.new(...)
428
453
  @session_observer with the metrics, the log subscriber and the thinking tails subscribed (subscribe_session_observers)
@@ -444,7 +469,7 @@ Model probes (per turn):
444
469
 
445
470
  Plugins and hook files register hooks on hook points (see "Hook points" above, and docs/hooks.md). Every fire
446
471
  puts the hook runtime on the event: `event[:hook]` (the hook's label) and five helpers, which call the Engine's
447
- `Hooks::Runtime`. A plugin's `ctx` has the same ones (`ctx.notify`, `ctx.steer`, ...).
472
+ `Hooks::Runtime` (built by `HookRoutes`, `hook_routes.rb`, which routes each to the UIs). A plugin's `ctx` has the same ones (`ctx.notify`, `ctx.steer`, ...).
448
473
 
449
474
  | Helper | What it does | Used by |
450
475
  |---|---|---|
@@ -551,8 +576,10 @@ Both loops:
551
576
  - **The same events**: generation events, tool dispatch and tool call events, and the before/after generation
552
577
  and tool call hooks.
553
578
 
554
- Native loop only: the Qwen open-block recovery, and the thinking prefill (Qwen with thinking off gets an empty
555
- thought after the cue, kept in the model message so the next prompt starts with what the server has cached).
579
+ Native loop only: the Qwen open-block recovery, and the thinking prefill (`Thinking.native`: with thinking off, Qwen
580
+ gets an empty thought after the cue, `QWEN_EMPTY_THOUGHT`, and Gemma 4 its empty thought channel,
581
+ `GEMMA_EMPTY_THOUGHT`, with no `<|think|>` token before the system prompt; kept in the model message so the next
582
+ prompt starts with what the server has cached).
556
583
 
557
584
  ## Key design principles
558
585
 
@@ -566,8 +593,9 @@ thought after the cue, kept in the model message so the next prompt starts with
566
593
  persistent.
567
594
  5. **A stable system prompt for the cache.** `SystemPrompt#build` caches the prompt per Engine, keyed by
568
595
  `[chat, thinking, layers]` (chat/native, the thinking level, the turn's LLM context layers), and is reset
569
- only on a model switch, a tools change or a profile change. The stable part (base prompt, identity,
570
- memories, AGENTS.md, memory indexes) comes before the volatile tail (model, working directory, session).
596
+ only on a model switch, a tools change, a profile change or a toolkit change (`Engine#apply_toolkit!`). The
597
+ stable part (base prompt, the toolkit's not-loaded line, rg guidance, identity, model notes, preloaded memories,
598
+ AGENTS.md, appended files, memory indexes) comes before the volatile tail (model, working directory, session).
571
599
  prompt-caching.md covers this in detail.
572
600
 
573
601
  ## Key files
@@ -576,12 +604,22 @@ Paths are under `lib/samagotchi/`.
576
604
 
577
605
  | File | Role |
578
606
  |---|---|
579
- | `engine.rb` | Engine: turn lifecycle, event emission, hooks runtime, plugins, model switching |
607
+ | `engine.rb` | Engine: turn lifecycle, event emission, plugins, model switching |
580
608
  | `turn_state.rb` | Cross-thread turn state (running, controller, sink, steers, activity clock) |
609
+ | `steer_cut.rb` | `SteerCut`: the Engine's seam for a message cutting the generation |
581
610
  | `generation_phase.rb`, `waiting_steer.rb` | Whether a steer may cut the running generation, and the steer that waits to |
611
+ | `steer_cut_after.rb`, `cut_policy.rb` | The model's cut wait (`SteerCutAfter.resolve`); the loops' cut path (`CutPolicy`) |
582
612
  | `steer.rb` | Queued input and steers at iteration boundaries |
583
613
  | `kernel_loop.rb` | Native loop: `#run` (format, generate, parse, dispatch, iterate) |
584
- | `llm/chat_loop.rb` | Chat loop: `#complete` (tool schemas per request, wire conversion) |
614
+ | `llm/chat_loop.rb` | Chat loop: `#complete` (tool schemas per request, wire conversion, block replay) |
615
+ | `llm/adapters.rb` | `LLM::Adapters::BY_API`: which chat adapter a host's api gets |
616
+ | `llm/openai_chat.rb` | `LLM::OpenAIChat`, the `/v1/chat/completions` adapter |
617
+ | `llm/adapter_common.rb` | `LLM::AdapterCommon`: what both chat adapters share (the HTTP layer and its timeouts, the paged model list, the SSE line loop, JSON errors) |
618
+ | `llm/anthropic_messages.rb`, `llm/anthropic_wire.rb`, `llm/anthropic_assembly.rb`, `llm/dialect.rb` | The `/v1/messages` adapter, its wire translation, its response, its dialects ([anthropic.md](anthropic.md)) |
619
+ | `llm/prompt_cache.rb` | Cache breakpoints on the OpenAI path, and the `cache_control` both adapters send |
620
+ | `llm/turn_settings.rb` | `LLM::TurnSettings`, what a turn runs under (vision, sampling, thinking, reasoning history, …) |
621
+ | `llm/native_tool_normalizer.rb`, `llm/stream_drops.rb` | A chat adapter's tool calls as chi's call hashes (`Tools::BuiltinCalls`); asking again for a step whose stream dropped (both loops) |
622
+ | `reasoning_history.rb` | `ReasoningHistory`: the levels, `.resolve`, `.run_start` |
585
623
  | `llm/backend.rb` | The `LLM::ModelBackend` contract (`#complete(...)`) |
586
624
  | `llm/native_backend.rb` | Wraps KernelLoop as a backend (`#complete` calls `#run`) |
587
625
  | `llm/model_result.rb` | `LLM::ModelResult`, what a backend returns |
@@ -591,7 +629,11 @@ Paths are under `lib/samagotchi/`.
591
629
  | `prompt_warmup.rb` | The turn-end warm-up |
592
630
  | `tools/registry.rb` | The tools a session offers: schemas, handlers, order |
593
631
  | `system_prompt.rb` | System prompt construction (stable part, volatile tail) |
632
+ | `prompt_files.rb`, `model_notes.rb` | The AGENTS.md chain and appended files; the model notes the prompt loads |
633
+ | `toolkit.rb`, `toolkit_command.rb` | A session's toolkit (`Toolkit#resolve`, what `Registry` declares); `/toolkit` |
634
+ | `toolkit_binding.rb` | `ToolkitBinding`: the Engine's toolkit seam (the kits, `#explained`, `ctx.use_toolkit`, the turn-start `#apply!` behind `Engine#apply_toolkit!`) |
594
635
  | `hooks/registry.rb` | Hook registry, fire order, `Hooks::Runtime` |
636
+ | `hook_routes.rb` | `HookRoutes`: the Engine's routes for a hook's or plugin's notice (`:hook_notice`, `fallback_for:`), question, stop and steer, and the guardrails' after-load line |
595
637
  | `extension_load.rb` | What an Engine loads at start: config.yml and bundle hooks, plugins, `bundles:` settings; events held while plugins load |
596
638
  | `guardrail_wiring.rb`, `guardrails/gate.rb` | The gate's context and approvals; the verdict per call |
597
639
  | `events.rb` | Event type sets shared by the Worker, the Bridge, the TUI and the web |
@@ -14,7 +14,7 @@ XML tool tags and declaration-echo parsing are intentionally not supported.
14
14
 
15
15
  Thought handling follows the Gemma guidance:
16
16
 
17
- - Include `<|think|>` in the system instruction to activate thinking mode.
17
+ - Include `<|think|>` in the system instruction to activate thinking mode. With thinking off (`/thinking off`) chi leaves the token out and prefills Gemma's empty thought channel after the model cue (`Thinking::GEMMA_EMPTY_THOUGHT`, `Thinking.native`, `Prompt.prefill_for`), as Gemma's chat template does with `enable_thinking` false.
18
18
  - When thinking mode is active, the model may emit internal reasoning as `<|channel>thought ... <channel|>`.
19
19
  - Standard multi-turn: prior model thoughts are stripped from conversation history before the next turn.
20
20
  - Function/tool-calling exception: during a single turn that includes tool calls, thoughts are not stripped between those tool-call rounds.
@@ -75,9 +75,9 @@ The proxies are the context-edit spike's (lexical and lenient: a basename mentio
75
75
  stale's stubs, every output led by its `[#tN]` id (`LLMContextView`), the `[CONTEXT: …]` line chi's `ContextStatus`
76
76
  adds at a turn's start under `--budget N` (default 64000; no line below its guided buckets, so a small case may get
77
77
  none), and the chat messages and tools of chi's `ChatLoop`, `forget_outputs` with its real description. The model
78
- (`--forget-model MODEL`, a chi model ref on an `api: openai` host) answers once; a `forget_outputs` call among its
79
- calls runs through chi's `LLMContextForget` (refusals, `protect_steps`, `keep`), and the row scores stale's stubs
80
- plus the forgets. Each answer is saved in `--out DIR` (`<case>.forget_offer.json`) and read back, so a rerun asks
78
+ (`--forget-model MODEL`, a chi model ref on a chat host: `api: openai` or `api: anthropic`) answers once; a
79
+ `forget_outputs` call among its calls runs through chi's `LLMContextForget` (refusals, `protect_steps`, `keep`),
80
+ and the row scores stale's stubs plus the forgets. Each answer is saved in `--out DIR` (`<case>.forget_offer.json`) and read back, so a rerun asks
81
81
  only for the rest (an error isn't saved); `--out` alone scores saved answers; `--max-cost USD` stops the asking
82
82
  once the costs the server reports reach it (exit 1, the answers so far kept). The row's notes give the call rate
83
83
  (`called`, `no_call`, `error`, `unasked`), the ids chi refused and the notes' mean length. Ids in this row cost
@@ -96,7 +96,8 @@ ruby script/llm_context_bench.rb "$SESSIONS" --picks deepseek=picks/deepseek --p
96
96
  ## Live picks (the D7 tool-name A/B)
97
97
 
98
98
  The one part that calls a model, and only with `--live MODEL`: at each case it asks the model which of its tool
99
- outputs to forget, through chi's own chat client (the model must be on an `api: openai` host), and saves each
99
+ outputs to forget, through chi's own chat client (the model must be on a chat host, `api: openai` or `api: anthropic`:
100
+ `LivePick.chat_adapter` takes any `HostEntry#chat?` host), and saves each
100
101
  answer under `--out DIR` for `--picks` to score. A saved pick is never asked for again. The request is the
101
102
  session's conversation up to the turn's end (thinking dropped, every tool result led by its id `[#tN]`), chi's
102
103
  built-in chat tools plus the forget tool, and the policy's tail line; a model that doesn't call the tool is asked
@@ -3,8 +3,9 @@
3
3
  A model server reuses work only for the **exact token prefix** it has already seen. Change one token and
4
4
  everything after it is computed again (prefill). The local models we run (hybrid Qwen 3.6 / Ornith on llama.cpp
5
5
  and Splash) can only resume from a **restore point near the end** of an earlier prompt, not from any shared
6
- prefix. Anthropic (Claude via OpenRouter) caches only up to an explicit `cache_control` breakpoint. So the order
7
- of what chi sends decides what a new turn, or a new session, has to pay for.
6
+ prefix. Anthropic (Claude via OpenRouter, or over the Messages API, `api: anthropic`) caches only up to an explicit
7
+ `cache_control` breakpoint. So the order of what chi sends decides what a new turn, or a new session, has to pay
8
+ for.
8
9
 
9
10
  Measured with prompt-cache slice 1 (2026-10-04, fresh sessions, ~7.7k-token prompt):
10
11
 
@@ -23,6 +24,8 @@ right-hand column happens.
23
24
  shared by every session ────────────────────────────────────────────────── breaks it
24
25
  ┌──────────────────────────────────────────────────────────────────────────┐
25
26
  │ tools built-ins, then plugin tools sorted by (bundle, name), │ a tool added/changed,
27
+ │ the tool index's find_tools/call_tool as bundle "index" │
28
+ │ (declared once at load; publishes change only results) │
26
29
  │ those the session's toolkit declares (per toolkit) │ /model, a profile change,
27
30
  │ (chat: the `tools` field; native: the model's template) │ a toolkit change
28
31
  ├──────────────────────────────────────────────────────────────────────────┤
@@ -36,7 +39,7 @@ right-hand column happens.
36
39
  │ AGENTS.md (root → cwd) │ (picked up by a new
37
40
  │ appended files (append_system_prompt:, --append-system-prompt) │ process or a rebuild)
38
41
  │ project + system memory indexes │
39
- │ ◄── remote Claude: cache_control breakpoint #1 │
42
+ │ ◄── remote Claude / Messages: cache_control breakpoint #1 │
40
43
  ├──────────────────────────────────────────────────────────────────────────┤
41
44
  │ SYSTEM, volatile tail (per session, least → most changing) │
42
45
  │ Model: this session runs on … │ another session
@@ -48,17 +51,19 @@ right-hand column happens.
48
51
  ┌──────────────────────────────────────────────────────────────────────────┐
49
52
  │ history user / assistant / tool messages, APPEND-ONLY │ !rollback, a failed or
50
53
  │ (images: up to the newest N, cut in batches; ImagePlan) │ cancelled turn's tail,
51
- │ ◄── remote Claude: breakpoint #2 on the last │ the image cut moving
52
- │ non-system message │ a batch, history
54
+ │ ◄── remote Claude / Messages: breakpoint #2 on the last │ the image cut moving
55
+ │ non-system message (Messages: its last user block) │ a batch, history
53
56
  ├──────────────────────────────────────────────────────────────────────────┤ sanitizing (thinking
54
57
  │ tail notes reminders, turn notes, steers (system/user messages) │ stripped per spec)
55
58
  │ new prompt this turn's user message │
56
59
  └──────────────────────────────────────────────────────────────────────────┘
57
60
  ```
58
61
 
59
- - The system prompt is built **once per loop** and rebuilt only on `@prompt_builder.reset!`: a model switch
60
- (`Engine#switch_model!`), a plugin's `tools_changed!`, or a profile change. A per-turn rebuild would
61
- break everything below it.
62
+ - The system prompt is built **once per key** and kept (`SystemPrompt#build`, keyed by `[chat, thinking, layers]`:
63
+ the loop, the thinking level, the turn's LLM context layers; a `/thinking` change builds under another key). All
64
+ of them are dropped only on `@prompt_builder.reset!`: a model switch (`Engine#switch_model!`), a plugin's
65
+ `tools_changed!`, a profile change, or a toolkit change (`Engine#apply_toolkit!`). A per-turn rebuild would break
66
+ everything below it.
62
67
  - The tools layer is shared **per toolkit** (configuration.md, "Toolkits"): sessions on the same toolkit share it,
63
68
  and a chain's links keep theirs. A session's toolkit is saved when it is created and never resolved again from
64
69
  config. A change (`/toolkit`, `ctx.use_toolkit`) is applied only at a turn's start (`Engine#apply_toolkit!`,
@@ -69,7 +74,14 @@ right-hand column happens.
69
74
  breakpoint #1, so a **new session** reads it from the cache. The last non-system message carries breakpoint
70
75
  #2, so the **next request in the session** reads everything up to it. Mid and tail system messages never take
71
76
  a breakpoint. The split point comes from `SystemPrompt#stable_length` via the message's `cache_split:`, which
72
- never goes on the wire.
77
+ never goes on the wire. `PromptCache.mark` keys on the model id (`claude`) and runs only for a remote host's chat
78
+ requests (`OpenAIChat#cache_breakpoints`).
79
+ - On a Messages host (`api: anthropic`), `AnthropicWire.translate` sets the same two breakpoints on the translated
80
+ request: the first `system` block (the stable part, when `cache_split:` splits it) and the last user block that
81
+ can carry one (never a thinking block, never one from a mid or tail system message). It doesn't look at the model
82
+ id: every chat request carries them, unless the host's dialect has none (`Dialect#cache_breakpoints`: `fireworks`,
83
+ an automatic prefix cache) or the host refused `cache_control` with a 400 (no breakpoints for that worker's life).
84
+ [anthropic.md](anthropic.md) has the details.
73
85
  - Side requests (idle recap, `/btw`) go through `IdleClient` with their own short system prompt. On a current
74
86
  llama.cpp they don't evict the session (see the server settings below); chi never pins them to a slot.
75
87
 
@@ -115,14 +127,17 @@ is prefilled once. The native and the chat loop use the same plan.
115
127
  one provider. `cache.key: session` also sends the session id as `prompt_cache_key` in the body, the field
116
128
  OpenAI's API routes and caches by (OpenRouter honours it too); the log line shows `cache_by=session`. Only
117
129
  `api.openai.com` and OpenRouter get it, and only the session's own chat requests (not recap or `/btw`): a local
118
- llama.cpp, Splash or another gateway may reject an unknown field. Off by default.
130
+ llama.cpp, Splash or another gateway may reject an unknown field. Off by default. A Messages host never gets
131
+ `prompt_cache_key` (`AnthropicMessages::DROPPED_OPTIONS`); its dialect may add a header of its own beside
132
+ `Session-Id` (`Dialect#session_header`: `x-session-affinity` under `dialect: fireworks`, which keeps a session on
133
+ the replica that has its cache).
119
134
 
120
135
  ## Reading the numbers
121
136
 
122
137
  - The debug log's `generation_completed` line: `prompt=` (the request's prompt tokens), `cached=` (read from the
123
138
  server's cache), `cache_write=` (written to it, where the provider reports writes) and `reprefill=` (of the last
124
139
  request's prompt on the same model, what was prefilled again). The `http` line of a
125
- remote Claude request shows `cache=on` (or `cache=1h`) when it carried breakpoints.
140
+ remote Claude or Messages request shows `cache=on` (or `cache=1h`) when it carried breakpoints.
126
141
  - The warm-up logs `model warmup` with `cached=`, `prefilled=` and `prefill_ms=`.
127
142
  - `/stats`: "cached N (P%)", "cache writes" and "re-prefilled" for the session.
128
143
  - A healthy session: from the second request on `cached=` is close to `prompt=`; a second fresh session in the
data/docs/plugins.md CHANGED
@@ -291,7 +291,7 @@ is what `targets:` says:
291
291
  - `command:`: a shell command the call runs; `command:` rules match it.
292
292
  - `cwd:`: where it runs, for the repo root and relative paths.
293
293
  - `acts_as:`: the tool a call stands for, when the tool runs other tools
294
- (the mcp bundle's `mcp_call`). Rules keyed by that tool's name match the
294
+ (chi's `call_tool`, which calls a tool a [source](#chitool_sourcecall-acts_as-label-wait-prefix) published). Rules keyed by that tool's name match the
295
295
  call too, as well as rules on the tool's own name. It can't be one of
296
296
  chi's own tools or another bundle's tool: that is dropped (and logged).
297
297
  - `args:`: the arguments it acts with (a Hash), when they aren't the call's
@@ -308,7 +308,7 @@ targets: ->(args) { { acts_as: "mcp_#{args["server"]}_#{args["tool"]}", args: ar
308
308
  ```
309
309
 
310
310
  An approval of an `acts_as:` call keys on the tool's own name as well
311
- (`mcp_call>mcp_github_x:…`), so it never stands in for an approval of the
311
+ (`call_tool>mcp_github_x:…`), so it never stands in for an approval of the
312
312
  tool it acts as, nor that one for it. Hooks see the call's own name, and so
313
313
  does loop-guard's `ignore_tools`.
314
314
 
@@ -548,7 +548,7 @@ end
548
548
  - **`provides_tools: true`**: the task provides what the plugin's tools
549
549
  need: tools of its own (with `chi.replace_tools`), or what fixed tools
550
550
  read (the mcp bundle's index of a first-run server's tools, which
551
- `find_mcp_tools` searches). A turn sent while it runs starts at once (the user's
551
+ `find_tools` searches). A turn sent while it runs starts at once (the user's
552
552
  message shows), then waits for it **before its first model request**, so
553
553
  the model sees the tools; the UIs keep showing the task meanwhile. The
554
554
  wait lasts at most `timeout` seconds from the task's start (default 60).
@@ -579,6 +579,89 @@ tool (full) until it comes back, with a warning at its first turn. See
579
579
  chi.toolkit "review", base: "lean", add: %w[delegate delegate_result]
580
580
  ```
581
581
 
582
+ ### `chi.tool_source(call:, acts_as:, label:, wait:, prefix:)`
583
+
584
+ Tools the model reaches through a search instead of declaring them one by
585
+ one: the plugin publishes them into chi's **tool index**, and chi declares
586
+ two tools in all, whatever the number of sources: `find_tools` (a keyword
587
+ search that answers each match's `inputSchema`) and `call_tool` (calls one as
588
+ `<group>/<tool>`; its row label is the first source's `label`). They
589
+ are declared once, after the plugins load, and only when a plugin registered
590
+ a source: a session without one declares neither. Nothing a source publishes
591
+ later changes the request's tools, only what a search answers, so the prompt
592
+ cache keeps (the search tool's description lists the groups as they were at
593
+ load). The mcp bundle is the only source so far (a group per server), and the
594
+ texts speak of MCP servers.
595
+
596
+ ```ruby
597
+ @source = chi.tool_source(label: "mcp", prefix: "mcp_",
598
+ call: ->(tool, args, ctx) { run(tool, args, ctx) }, # the result, as a tool block's
599
+ acts_as: ->(group, tool) { "mcp_#{group}_#{tool}" }, # what rules match
600
+ wait: ->(group) { start_of(group).value }) # a :starting group's start
601
+ index = Samagotchi::Tools::ToolIndex
602
+ @source.publish([index::Group.new(name: "fs", summary: "files in ~/scratch", tools: [
603
+ index::Tool.new(group: "fs", name: "read", description: "Read a file.", schema: read_schema,
604
+ acts_as: "mcp_fs_read")
605
+ ]),
606
+ index::Group.new(name: "chrome", state: :starting)])
607
+ ```
608
+
609
+ - **`publish(groups)`** swaps in the plugin's whole list, from any thread, at
610
+ any time (a server that started, `list_changed`): a search sees the old
611
+ list or the new one, never half. A `Group` has a `summary` (its line in
612
+ `find_tools`' description), a `state` (`:ready`; `:starting`, its tools may
613
+ still come: a call to it waits on `wait`, a search says "starting, try
614
+ again"; `:failed` with its `error`) and its tools. A group exists before its
615
+ tools do. A `Tool`'s `refusal` says why `call_tool` refuses it (nil: it can
616
+ be called), its `mark` what a search shows beside its name: the plugin
617
+ decides both (the mcp bundle: a bad schema, a name clash) and publishes
618
+ them again when they change.
619
+ - **`call`** gets a call chi looked up and checked: the `Tool`, its `args`
620
+ typed by its `inputSchema`, and the plugin's `ctx`. It returns what a tool's
621
+ block returns (a String, a `ToolResult` with images).
622
+ - **Guardrails** see a `call_tool` as the tool it calls: its `acts_as`, its
623
+ arguments and `"<group>: <tool>"` as the question's label
624
+ ([acts_as:](#guardrails)). A call made before its tool is published is
625
+ named by `acts_as` from the names alone; a bare name with `prefix` is
626
+ taken as an `acts_as` as given. The `acts_as:` hook must give exactly the
627
+ `acts_as` the source publishes for that tool: a call whose tool isn't the
628
+ one guardrails were given is refused ("the MCP tools changed while the call
629
+ waited"), so a hook that disagrees refuses every call.
630
+ - **The row** shows `label` (default: the bundle's name) and
631
+ `<group>/<tool> args…`. `@source.declared_names` and `declared_tokens` say
632
+ what the two tools take in every request (none when other tools had both
633
+ names). A `Group` in a state other than the three, holding another group's
634
+ tool, or a `Tool` with a blank `acts_as` raises `ArgumentError`.
635
+ - `find_tools` and `call_tool` aren't chi's own tools: every toolkit declares
636
+ them (unless its `remove:` names them), as it does plugin tools. A bundle
637
+ that declares tools of those names itself (an mcp bundle before 0.7.0 has
638
+ `find_mcp_tools` and `mcp_call`) keeps them, and with no source chi
639
+ declares neither.
640
+
641
+ ### `chi.peer_source(name, label:)`
642
+
643
+ Declares a context source as a peer agent's answers: the peer bundles'
644
+ `claude-review` and `codex-review`. Such a source, when it is a pushed source
645
+ of the session's own, wakes the session by the peer loop's rules instead of
646
+ the 10-minute window: up to `context.peer_turns` wake turns in a row, no wake
647
+ for an answer whose verdict is `VERDICT OK`, and a wake line that names the
648
+ peer and keeps the turn within the user's task (see [Waking: peer
649
+ sources](context.md#peer-sources-the-peer-loop)).
650
+
651
+ ```ruby
652
+ chi.peer_source "codex-review", label: "Codex"
653
+ ```
654
+
655
+ - `name`: a context source name (as `chi context add` takes it). Another
656
+ bundle's is a load error.
657
+ - `label:`: the peer's name, said in chi's own lines ("your peer agent
658
+ Codex"): a capital letter, then up to 23 letters and spaces.
659
+
660
+ The declaration lives in the session's process only, like a tool: a source
661
+ file can't make itself a peer's, and uninstalling the bundle ends it.
662
+ Subclasses of `Samagotchi::Plugin::Peer` (below) declare their `REVIEW`
663
+ source. Needs chi 0.67.0 (guard it with `chi.respond_to?(:peer_source)`).
664
+
582
665
  ### `chi.ctx`
583
666
 
584
667
  The plugin's context (the `ctx` its handlers get), for `register` itself:
@@ -907,7 +990,9 @@ only against this API (`lib/samagotchi/bundles/btw/plugin.rb`).
907
990
  only against this API (`lib/samagotchi/bundles/mcp/plugin.rb`), and gives
908
991
  the model the tools of [MCP](https://modelcontextprotocol.io) servers. It
909
992
  has no memory file, and the servers' tools aren't declared to the model one
910
- by one: it has two tools, `find_mcp_tools` and `mcp_call` (~250 tokens in
993
+ by one: the bundle publishes them into chi's tool index
994
+ ([chi.tool_source](#chitool_sourcecall-acts_as-label-wait-prefix), a group
995
+ per server), and the model has its two tools, `find_tools` and `call_tool` (~250 tokens in
911
996
  every request plus a line per server, whatever the servers have; `/mcp`
912
997
  says how many). Stdio servers only, for now.
913
998
 
@@ -925,7 +1010,7 @@ bundles:
925
1010
  env: {NODE_OPTIONS: "--no-warnings"} # added to chi's environment
926
1011
  cwd: ~/scratch # default: where chi runs
927
1012
  tools: [read_*, list_directory] # optional: only these (globs)
928
- description: files in ~/scratch # optional: its line in find_mcp_tools
1013
+ description: files in ~/scratch # optional: its line in find_tools
929
1014
  timeout: 120 # optional: this server's per-call timeout
930
1015
  chrome:
931
1016
  command: [npx, -y, "chrome-devtools-mcp@latest", --slim, --headless]
@@ -944,7 +1029,7 @@ bundles:
944
1029
  one is replaced, and so is what a search finds. The model's tools never
945
1030
  change, so the prompt cache keeps. The saved file also keeps the first
946
1031
  sentence of the server's `instructions` (from `initialize`), for its line
947
- in `find_mcp_tools`.
1032
+ in `find_tools`.
948
1033
  - **The first run** (no saved list, or the config changed) starts the
949
1034
  server in an [init task](#chiinitlabel-provides_tools-false-quiet-false-timeout-nil--ctx--):
950
1035
  every UI shows `Starting MCP server x (first run, saving its tools)`, and
@@ -977,9 +1062,9 @@ bundles:
977
1062
  command: [chrome-devtools-mcp, --slim, --headless] # after npm i -g chrome-devtools-mcp
978
1063
  ```
979
1064
 
980
- - **Tools: search, then call.** The servers' tools are kept in an index;
1065
+ - **Tools: search, then call.** The servers' tools are kept in chi's tool index;
981
1066
  the model has two fixed tools:
982
- - `find_mcp_tools(query, server?)` searches it by keywords over the
1067
+ - `find_tools(query, server?)` searches it by keywords over the
983
1068
  server's name, the tool's name and its description, and answers up to 5
984
1069
  tools, each with its name `<server>/<tool>`, its description and its
985
1070
  whole `inputSchema`. A word counts more the fewer tools have it (a word
@@ -995,7 +1080,7 @@ bundles:
995
1080
  `instructions`, and how many tools it has (`- chrome (26 tools)` with
996
1081
  neither: tool names there get called blind, without a search; a first
997
1082
  run's server is just its name until the next session).
998
- - `mcp_call(tool, args)` calls one: `tool` is `<server>/<tool>`, and
1083
+ - `call_tool(tool, args)` calls one: `tool` is `<server>/<tool>`, and
999
1084
  `mcp_<server>_<tool>` works too (looked up, not parsed). Two tools whose
1000
1085
  `mcp_<server>_<tool>` comes out the same (servers `git-hub` and
1001
1086
  `git_hub`, names alike up to the 48-character cut) are both refused,
@@ -1039,18 +1124,18 @@ bundles:
1039
1124
  closed, then TERM and KILL go to the server's process group.
1040
1125
  - **`/mcp`** (anytime) shows a card with the servers, their state (cached
1041
1126
  (not started), running with its pid, failed with its count, stopped) and
1042
- their tools as `<server>/<tool>`, the name `mcp_call` takes.
1127
+ their tools as `<server>/<tool>`, the name `call_tool` takes.
1043
1128
  A cached or running server's line says roughly how many tokens its tool
1044
1129
  definitions take (`~4,232 tokens`), which only a search's answer carries
1045
- now; the last line says what every request carries (`find_mcp_tools` and
1046
- `mcp_call`) and totals the servers'. It is an estimate:
1130
+ now; the last line says what every request carries (`find_tools` and
1131
+ `call_tool`) and totals the servers'. It is an estimate:
1047
1132
  the definitions' JSON as the chat API gets it (`api: openai`), divided by
1048
1133
  4. The native prompts (Gemma, Qwen) render a flatter schema, so there they
1049
1134
  take less. The log records each server's estimate when it changes
1050
1135
  (`mcp_tools_estimated`).
1051
1136
  - The server's stderr goes to the debug log (`plugins` records, bundle=mcp).
1052
1137
  - **Guardrails.** A rule's `tool:` can be a glob, so one rule covers every
1053
- MCP tool (a search is `find_mcp_tools`, outside `mcp_*`, so it isn't
1138
+ MCP tool (a search is `find_tools`, outside `mcp_*`, so it isn't
1054
1139
  asked about):
1055
1140
 
1056
1141
  ```yaml
@@ -1062,20 +1147,21 @@ bundles:
1062
1147
  reason: an MCP server's tool
1063
1148
  ```
1064
1149
 
1065
- `mcp_call`'s `targets:` act as the tool it calls, by its
1150
+ `call_tool`'s `targets:` act as the tool it calls, by its
1066
1151
  `mcp_<server>_<tool>` name ([acts_as:](#guardrails)), so `tool:
1067
- "mcp_github_*"` or `tool: mcp_github_merge_pull_request` match an
1068
- `mcp_call` to it, as they matched the tool when it was declared by that
1152
+ "mcp_github_*"` or `tool: mcp_github_merge_pull_request` match a
1153
+ `call_tool` to it, as they matched the tool when it was declared by that
1069
1154
  name. A call made before the server's tools are known (a first run
1070
1155
  still starting) is matched by the name the model gave, so the rule fires
1071
1156
  before the call waits; if the tool it then finds isn't that one, the
1072
1157
  call is refused. The question shows the inner arguments under `<server>: <tool>`
1073
1158
  (`everything: get_sum: a=20 b=22`), and "Allow this call for the
1074
1159
  session" (or in this repo) allows that tool with those arguments only.
1075
- (Approvals stored before 0.6.0, under the old name, don't carry over.)
1076
- Two things see `mcp_call`, not the tool: hooks (a `before_tool_call`
1077
- event's `tool:` is `"mcp_call"`, the tool in its args), and loop-guard's
1078
- `ignore_tools` (`mcp_call` ignores every MCP call; calls to different
1160
+ (Approvals stored before 0.6.0, under the old name, don't carry over, nor
1161
+ ones stored before 0.7.0 under `mcp_call`: each MCP tool is asked about once more.)
1162
+ Two things see `call_tool`, not the tool: hooks (a `before_tool_call`
1163
+ event's `tool:` is `"call_tool"`, the tool in its args), and loop-guard's
1164
+ `ignore_tools` (`call_tool` ignores every MCP call; calls to different
1079
1165
  tools, or with different args, stay distinct, since the tool is in the
1080
1166
  args).
1081
1167
 
@@ -1653,10 +1739,20 @@ Claude pairs; without one the bundle costs one tool declaration.
1653
1739
  - `/claude last` sends the model's last completed answer as context. Mid-turn
1654
1740
  it is the answer before the running turn, never the turn's partial text;
1655
1741
  a cancelled or failed turn has no answer, so the one before it goes.
1742
+ - **The peer loop.** The bundle declares `claude-review` a peer's
1743
+ ([`chi.peer_source`](#chipeer_sourcename-label)), so Claude's answers
1744
+ wake chi by the loop's rules ([Waking: peer
1745
+ sources](context.md#peer-sources-the-peer-loop)): chi fixes what Claude
1746
+ flagged or disagrees and sends again, up to `context.peer_turns` (3) wake
1747
+ turns in a row, and a `VERDICT OK` answer arrives as a note and ends the
1748
+ round. Only a message you type in the web or a TUI starts the count again.
1656
1749
  - **The skill.** `skill_claude_peer` (system scope) tells the model to send
1657
- only when you ask, to pick the part rather than the session, not to wait
1658
- or poll, and how to read the `claude-review` answer (a validate ends with
1659
- `VERDICT: OK`, `CHANGES` or `UNSURE`).
1750
+ only when you ask or to reply in a peer turn, to pick the part rather than
1751
+ the session, not to wait or poll, and how to read the `claude-review`
1752
+ answer (a validate ends with `VERDICT: OK`, `CHANGES` or `UNSURE`): act on
1753
+ it only within your task, and stop and tell you when Claude agrees or after
1754
+ the last turn. Claude's skill ends each answer's summary with its verdict
1755
+ and never answers with `chi send` (which counts as you typing).
1660
1756
  - **Asking first.** The tool has no `targets:`, so guardrails match it by name
1661
1757
  only: to be asked before each send, add a rule ([Rules in
1662
1758
  config.yml](guardrails.md#rules-in-configyml)):
@@ -1680,6 +1776,10 @@ Claude pairs; without one the bundle costs one tool declaration.
1680
1776
  bypasses permissions holds it for approval (in `-p` it expires after 5
1681
1777
  minutes). It is local only (a Unix socket), but it does hand session
1682
1778
  content to another agent: only what the model or you picked.
1779
+ - **Needs chi 0.67.0** for the loop (`chi update`): the plugin is built on
1780
+ `Samagotchi::Plugin::Peer` (lib/samagotchi/plugin/peer.rb), the base both
1781
+ peer bundles share (the tool, `/claude`, the cards, payloads), and each
1782
+ bundle sets its strings and transport.
1683
1783
 
1684
1784
  ## The codex-peer bundle
1685
1785
 
@@ -1758,6 +1858,9 @@ skill install card) works the same.
1758
1858
  Codex's names, and on a missing answer points to Codex being mid-turn or
1759
1859
  its sandbox (the `writable_roots` line). To be asked before each send, a
1760
1860
  guardrails rule with `tool: codex_send` (see the claude-peer section).
1861
+ - **The peer loop** works as claude-peer's: `codex-review` is declared a
1862
+ peer's, Codex's answers wake chi up to `context.peer_turns` turns in a row,
1863
+ and `VERDICT OK` ends the round. Needs chi 0.67.0.
1761
1864
 
1762
1865
  ## Shutdown
1763
1866