samagotchi 0.67.0 → 0.69.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 +62 -1
- data/docs/architecture.md +7 -4
- data/docs/cli.md +6 -1
- data/docs/configuration.md +28 -6
- data/docs/guardrails.md +23 -1
- data/docs/hooks.md +4 -0
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +73 -17
- data/docs/internals/prompt-caching.md +9 -2
- data/docs/plugins.md +42 -19
- data/docs/releasing.md +14 -6
- data/docs/sessions.md +3 -3
- data/docs/testing.md +8 -2
- data/lib/samagotchi/bootstrap/probe.rb +1 -3
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +0 -19
- data/lib/samagotchi/effective_model.rb +283 -0
- data/lib/samagotchi/engine.rb +216 -360
- data/lib/samagotchi/guardrails/targets.rb +9 -2
- data/lib/samagotchi/host_config.rb +1 -29
- data/lib/samagotchi/host_model.rb +0 -7
- data/lib/samagotchi/kernel_loop.rb +24 -7
- data/lib/samagotchi/level_setting_command.rb +99 -0
- data/lib/samagotchi/llm/errors.rb +47 -1
- data/lib/samagotchi/llm/http.rb +20 -2
- data/lib/samagotchi/llm_context_stale.rb +6 -0
- data/lib/samagotchi/llm_context_strategy.rb +5 -10
- data/lib/samagotchi/memory_bundle/manifest.rb +3 -0
- data/lib/samagotchi/model_price.rb +2 -4
- data/lib/samagotchi/reasoning_history_command.rb +29 -90
- data/lib/samagotchi/session.rb +22 -8
- data/lib/samagotchi/session_metrics.rb +4 -1
- data/lib/samagotchi/session_setting.rb +86 -0
- data/lib/samagotchi/system_prompt.rb +19 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +5 -2
- data/lib/samagotchi/thinking_command.rb +24 -76
- data/lib/samagotchi/thinking_notices.rb +169 -0
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +24 -3
- data/lib/samagotchi/toolkit.rb +27 -14
- data/lib/samagotchi/toolkit_binding.rb +8 -2
- data/lib/samagotchi/toolkit_command.rb +20 -4
- data/lib/samagotchi/tools/builtin_calls.rb +45 -1
- data/lib/samagotchi/tools/registry.rb +95 -8
- data/lib/samagotchi/tools/tool_index.rb +223 -72
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +69 -93
- data/lib/samagotchi/web/message_parts.rb +5 -2
- data/lib/samagotchi/web/public/activity.js +3 -1
- data/lib/samagotchi/web/public/app.js +196 -526
- data/lib/samagotchi/web/public/bubbles.js +291 -0
- data/lib/samagotchi/web/public/cards_place.js +184 -0
- data/lib/samagotchi/web/public/chains.js +19 -2
- data/lib/samagotchi/web/public/chip_popover.js +111 -0
- data/lib/samagotchi/web/public/command_list.js +12 -10
- data/lib/samagotchi/web/public/families_view.js +15 -0
- data/lib/samagotchi/web/public/index.html +21 -11
- data/lib/samagotchi/web/public/info_bar.js +3 -3
- data/lib/samagotchi/web/public/info_bar_model.js +110 -0
- data/lib/samagotchi/web/public/init_row.js +21 -10
- data/lib/samagotchi/web/public/level_chip.js +29 -80
- data/lib/samagotchi/web/public/lists_view.js +64 -9
- data/lib/samagotchi/web/public/llm_context_chip.js +36 -105
- data/lib/samagotchi/web/public/model_picker.js +47 -28
- data/lib/samagotchi/web/public/ref_scan.js +1 -7
- data/lib/samagotchi/web/public/refs.js +13 -0
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stage_frame.js +179 -0
- data/lib/samagotchi/web/public/stage_model.js +33 -0
- data/lib/samagotchi/web/public/stage_slots.js +130 -0
- data/lib/samagotchi/web/public/stage_view.js +56 -335
- data/lib/samagotchi/web/public/stage_watch.js +65 -0
- data/lib/samagotchi/web/public/stream_handlers.js +5 -2
- data/lib/samagotchi/web/public/timing.js +3 -1
- data/lib/samagotchi/web/public/toast_place.js +16 -0
- data/lib/samagotchi/web/public/toolkit_chip.js +11 -4
- data/lib/samagotchi/web/public/turn_html.js +4 -2
- data/lib/samagotchi/web/public/vendor/directive-helpers.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
- data/lib/samagotchi/worker.rb +11 -16
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ef7820f894e85b932288a38a22e1f312e01b5f4f1d100996acde11443baab19
|
|
4
|
+
data.tar.gz: 44f15fe12934aa7c56bff657e8b93a1d0165a752bcf0715f71cb04bb17ed7ac6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38d2aebcc7f777a40ceb1f0008c0160fd374541ff97b74b44ba0d61611cf7e2a103a4398571327b26cd7ffa80d564a552166d74ecfca172788483b1b43d166c7
|
|
7
|
+
data.tar.gz: 64a08c7808f2a9d58cbd5135ebadef7f59157876d738c16a3b6793c9b424f8a6dca90454ea961672ce0e14a71f809026ac49ba51d1db0b1ff4389073a06fbd4e
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,65 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.69.0] - 2026-10-14
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Web: a new chain link (continue →) shows the model picker in its composer until its first turn, so the next link can
|
|
16
|
+
start on another model or provider; the continue toast shows the fallback warning when the previous model's host is
|
|
17
|
+
gone.
|
|
18
|
+
- Built-ins a session's toolkit leaves out are reachable: `find_tools` searches them (group `chi`) beside MCP
|
|
19
|
+
servers' tools, and `call_tool` runs them through the usual guardrails as the real call. `remove:` /
|
|
20
|
+
`--no-tool` / `/toolkit remove` stay hard.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- find_tools' description and its `server` parameter (now `group`) changed: sessions with MCP servers read their
|
|
25
|
+
prompt again once. So do sessions on `lean`, `child`, `coordinator` or a kit based on them: they now declare
|
|
26
|
+
`find_tools` and `call_tool`, and their not-loaded line changed.
|
|
27
|
+
- A toolkit based on `lean` or `child` (a `toolkit.kits` entry, a bundle's `chi.toolkit`) no longer keeps the
|
|
28
|
+
built-ins its base leaves out from the model: they're reachable through `find_tools` / `call_tool`, guarded as
|
|
29
|
+
themselves. Name one under `remove:` to keep it out.
|
|
30
|
+
- Guardrail rules on `call_tool` no longer see calls of chi's own tools made through it; they match the tool's own
|
|
31
|
+
name.
|
|
32
|
+
- Shipped bundles: a change that needs a newer chi marks the manifest `requires_chi_next` instead of raising
|
|
33
|
+
`requires_chi`; `rake release:bump` raises it to the released version and `rake release:check` fails on a marker
|
|
34
|
+
left over.
|
|
35
|
+
- `/toolkit remove X` on a tool the toolkit leaves out is saved (it makes X unreachable). `/toolkit add X` then
|
|
36
|
+
`/toolkit remove X` now removes X (it used to go back to the kit's default), and a remove then an add declares X.
|
|
37
|
+
- The `child` toolkit also removes `delegate` and `delegate_result`: a child on it can't start children, even
|
|
38
|
+
through call_tool.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- A TLS handshake blip on a gateway (`SSL_connect … unexpected eof`, a reset mid-handshake) is retried like other
|
|
43
|
+
network errors instead of failing the turn at once; a certificate chi doesn't trust, a certificate for another
|
|
44
|
+
name, or an `https://` base_url on a plain HTTP port fails at once, saying what to check (`SSL_CERT_FILE`, the
|
|
45
|
+
host's base_url).
|
|
46
|
+
- A bundle tool named like an MCP tool (`mcp_fs_read`) no longer takes `call_tool` calls of that MCP tool out of
|
|
47
|
+
`mcp_*` rules.
|
|
48
|
+
- Web: on a phone the start page's model, thinking and LLM context chips wrap instead of being cut short; a toast no
|
|
49
|
+
longer covers a fresh link's open model panel; the family popover's status dots keep pulsing in step.
|
|
50
|
+
|
|
51
|
+
## [0.68.0] - 2026-10-14
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- Web: the settings popovers' selects (thinking level, LLM context strategy and apply) look the same in Safari, Chrome
|
|
56
|
+
and Firefox: chi's own box and caret instead of each browser's native control.
|
|
57
|
+
- Web: the thinking chip comes before the LLM context chip, on the session bar (think, reasoning, llm ctx, tools) and
|
|
58
|
+
beside the start page's model picker.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- `/model` while the web or another terminal read `/stats` no longer leaves the new model on the old model's prompt
|
|
63
|
+
profile (its turns formatted for the wrong family until the next `/model`).
|
|
64
|
+
- Web: the sessions strip no longer blinks when a session updates: a card fades in only when it is new to the list,
|
|
65
|
+
and a running or waiting card's dot keeps its pulse through the redraw. All sessions no longer re-fades on every
|
|
66
|
+
update or search keystroke either.
|
|
67
|
+
|
|
68
|
+
Update with `chi update` (the system bundle 0.68.0), then restart `chi web`.
|
|
69
|
+
|
|
11
70
|
## [0.67.0] - 2026-10-14
|
|
12
71
|
|
|
13
72
|
### Added
|
|
@@ -2937,7 +2996,9 @@ and long-lived sessions.
|
|
|
2937
2996
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2938
2997
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2939
2998
|
|
|
2940
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2999
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.69.0...HEAD
|
|
3000
|
+
[0.69.0]: https://github.com/dm1try/samagotchi/compare/v0.68.0...v0.69.0
|
|
3001
|
+
[0.68.0]: https://github.com/dm1try/samagotchi/compare/v0.67.0...v0.68.0
|
|
2941
3002
|
[0.67.0]: https://github.com/dm1try/samagotchi/compare/v0.66.0...v0.67.0
|
|
2942
3003
|
[0.66.0]: https://github.com/dm1try/samagotchi/compare/v0.65.0...v0.66.0
|
|
2943
3004
|
[0.65.0]: https://github.com/dm1try/samagotchi/compare/v0.64.0...v0.65.0
|
data/docs/architecture.md
CHANGED
|
@@ -400,7 +400,9 @@ by one pure resolver, `ModelRef.parse` (host prefix, alias), given the configure
|
|
|
400
400
|
hosts and aliases; `HostRegistry` routes a name that names no host (its model
|
|
401
401
|
index after `/models`) and builds the `ModelTarget`.
|
|
402
402
|
|
|
403
|
-
Engine picks the loop from the effective model's host (`HostRegistry#resolve`)
|
|
403
|
+
Engine picks the loop from the effective model's host: `EffectiveModel.of` resolves it (`HostRegistry#resolve`)
|
|
404
|
+
on every read, never kept, since config.yml and a model listing can move it without a `/model`
|
|
405
|
+
([internals/engine.md](internals/engine.md), "EffectiveModel"):
|
|
404
406
|
|
|
405
407
|
| Loop | Class | Host | Talks through |
|
|
406
408
|
|---|---|---|---|
|
|
@@ -450,7 +452,7 @@ message (docs/internals/context-telemetry.md).
|
|
|
450
452
|
`HostRegistry#adapter_for`, built by `LLM::Adapters` from the host's api) share
|
|
451
453
|
`LLM::HTTP`: timeouts, TLS for https, a line reader for streamed bodies, the retry loop (`retry.*`; network errors, 429 and
|
|
452
454
|
500/502/503/504/529, honouring `Retry-After`; never after a stream has produced
|
|
453
|
-
output) and cancel. Cancel closes the in-flight socket from the
|
|
455
|
+
output; a refused connection and a TLS handshake that can't work, `LLM::TLSError`, fail at once) and cancel. Cancel closes the in-flight socket from the
|
|
454
456
|
`CancellationController` listener, so it works on any thread.
|
|
455
457
|
|
|
456
458
|
`LLM::AnthropicMessages` talks Anthropic's Messages API (`POST {root}/v1/messages`) for the same chat loop:
|
|
@@ -496,7 +498,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
496
498
|
|
|
497
499
|
| Component | Main file(s) | What it is |
|
|
498
500
|
|---|---|---|
|
|
499
|
-
| Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`, `worker_signals.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. A spawned one traps TERM, HUP and INT (`WorkerSignals`, installed by `run_session_loop` when `SAMAGOTCHI_WORKER_PROCESS=1`): the running turn stops as a Stop with reason `signal`, is saved, and the worker leaves (`:signal`, exit 0; a new worker only for input already queued). A running turn is checkpointed at its iteration boundaries (`TurnCheckpoint`, the `interrupted_turn` mark), and the next worker settles a turn its predecessor died in (`TurnCheckpoint.settle`). |
|
|
501
|
+
| Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`, `worker_signals.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. A spawned one traps TERM, HUP and INT (`WorkerSignals`, installed by `run_session_loop` when `SAMAGOTCHI_WORKER_PROCESS=1`): the running turn stops as a Stop with reason `signal`, is saved, and the worker leaves (`:signal`, exit 0; a new worker only for input already queued). A running turn is checkpointed at its iteration boundaries (`TurnCheckpoint`, the `interrupted_turn` mark), and the next worker settles a turn its predecessor died in (`TurnCheckpoint.settle`). The session is saved from the turn's thread, Bridge threads and a signal's thread; `Session#save` takes a per-session lock around its snapshot and write, so an older snapshot never lands over a newer one. |
|
|
500
502
|
| Config | `config.rb` | Every setting as an `Entry`: env, config.yml and CLI layers ([configuration.md](configuration.md)). |
|
|
501
503
|
| MemoryBundle | `memory_bundle.rb`, `memory_bundle/{installer,builder,installed_bundle,plugin_ref,bundle_hook}.rb` (`Installer`, `Builder`, `InstalledBundle`, `PluginRef`, `BundleHook`) | Installing, building and reading bundles of memories, hooks and plugins. |
|
|
502
504
|
| Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/tool_spec.rb` (`Plugin::ToolSpec`: a checked tool declaration, how it is registered), `plugin/loader.rb` | What a bundle's plugin registers: tools, commands, annotation actions, hooks, services, toolkits, peer sources ([plugins.md](plugins.md)). |
|
|
@@ -515,7 +517,8 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
515
517
|
| Toolkits | `toolkit.rb` (`Toolkit`), `toolkit_binding.rb` (`ToolkitBinding`, the Engine's seam), `toolkit_command.rb` (`/toolkit`), `tools/registry.rb` (`Registry#toolkit`, `#left_out_names`), `web/public/toolkit_chip.js` | Which registered tools a session's prompts declare: saved in the session, resolved at a turn's start (`Engine#apply_toolkit!`), named in the prompt's not-loaded line; `chi.toolkit` and `ctx.use_toolkit` for plugins ([configuration.md](configuration.md#toolkits)). |
|
|
516
518
|
| Steer delivery | `steer.rb` (`Steer`), `delivery.rb` (`Delivery`), `steer_cut.rb` (`SteerCut`), `generation_phase.rb`, `waiting_steer.rb`, `steer_cut_after.rb` (`SteerCutAfter`), `cut_policy.rb` (`CutPolicy`) | A message for a running turn: at the next boundary, cutting a thinking-only generation, or queued for the turn's end ([internals/engine.md](internals/engine.md)). |
|
|
517
519
|
| Prompt files | `prompt_files.rb` (`PromptFiles`) | The AGENTS.md chain (root → cwd) and the appended files the system prompt carries. |
|
|
518
|
-
| Model settings | `model_profile.rb` (`ModelProfile`), `model_settings.rb`, `sampling_settings.rb`, `model_catalog.rb` (`ModelCatalog`), `model_notes.rb` (`ModelNotes`) |
|
|
520
|
+
| Model settings | `effective_model.rb` (`EffectiveModel`, `ModelChoice`), `model_profile.rb` (`ModelProfile`), `model_settings.rb`, `sampling_settings.rb`, `model_catalog.rb` (`ModelCatalog`), `model_notes.rb` (`ModelNotes`) | The effective model's settings as one snapshot per read, a model's prompt format, its `models:` entry, its request parameters, the pickable model rows (`chi models`, `/api/models`), the notes its prompt loads. |
|
|
521
|
+
| Session settings | `session_setting.rb` (`SessionSetting`), `level_setting_command.rb` (`/thinking`, `/reasoning-history`), `llm_context_command.rb`, `toolkit_command.rb`, `thinking_notices.rb` (`ThinkingNotices`) | A session's own llm_context, thinking, reasoning_history and toolkit: their chips (session state, `/stats`, `:command_ran`, the web), the commands that set them, the thinking level's notices. |
|
|
519
522
|
| Dialects | `llm/dialect.rb` (`LLM::Dialect`) | What an `api: anthropic` provider's `/v1/messages` does differently (`dialect: fireworks`) ([internals/anthropic.md](internals/anthropic.md)). |
|
|
520
523
|
| Logging | `log.rb` (`Log`), `log_subscriber.rb` (`LogSubscriber`), `log_path.rb` (`LogPath`), `log_line.rb` | The debug log: tagged records, a session's event trail, where the file goes ([configuration.md](configuration.md#debug-log-file)). |
|
|
521
524
|
| Stores | `reminder_store.rb` (`ReminderStore`), `recap_store.rb` (`RecapStore`) | A session's reminders, and its idle recap (`recap.json`). |
|
data/docs/cli.md
CHANGED
|
@@ -517,7 +517,12 @@ line, the web as the `reasoning` chip. See "Reasoning history" in
|
|
|
517
517
|
[configuration.md](configuration.md).
|
|
518
518
|
|
|
519
519
|
`/toolkit` lists the tools the next turn declares (each with its estimated
|
|
520
|
-
tokens), the session's toolkit and the tools it leaves out
|
|
520
|
+
tokens), the session's toolkit and the tools it leaves out: `reachable
|
|
521
|
+
(find_tools): …` (built-ins the model reaches with find_tools and call_tool)
|
|
522
|
+
and `removed: …` (only `/toolkit add` brings one back), or `not loaded: …` when
|
|
523
|
+
the toolkit removes call_tool. `/toolkit remove TOOL` is hard: a tool the
|
|
524
|
+
toolkit left out reachable becomes unreachable (saved, and asked first once the
|
|
525
|
+
session has a model reply), also after an add. `/toolkit NAME` puts
|
|
521
526
|
the session on another toolkit (dropping its own adds and removes); `/toolkit
|
|
522
527
|
add TOOL` and `/toolkit remove TOOL` change one tool (a tool's name keeps its
|
|
523
528
|
case). The change is saved at once and applies from the next turn's start; a
|
data/docs/configuration.md
CHANGED
|
@@ -988,8 +988,8 @@ Transient network failures are retried automatically with exponential backoff.
|
|
|
988
988
|
|
|
989
989
|
- Default retries: `5` (up to `6` total attempts including the first call).
|
|
990
990
|
- Default backoff: `0.5s`, `1s`, `2s`, `4s`, `8s`.
|
|
991
|
-
- Retry scope: transient network errors (timeouts, reset connections, EOF/socket reachability
|
|
992
|
-
HTTP 429 and HTTP 500/502/503/504/529. A `Retry-After` header replaces the backoff delay; one longer than
|
|
991
|
+
- Retry scope: transient network errors (timeouts, reset connections, broken pipes, EOF/socket reachability
|
|
992
|
+
failures, a TLS handshake that drops: `SSL_connect … unexpected eof while reading`), HTTP 429 and HTTP 500/502/503/504/529. A `Retry-After` header replaces the backoff delay; one longer than
|
|
993
993
|
60s is not waited out and the error is reported instead.
|
|
994
994
|
- An HTTP 402 about credit held by in-flight requests (OpenRouter reserves credit for each running request, so
|
|
995
995
|
one session's long request can make another's fail) is retried like a 429, 20s apart unless `Retry-After`
|
|
@@ -998,6 +998,11 @@ Transient network failures are retried automatically with exponential backoff.
|
|
|
998
998
|
budget, so it fails at once (lower `default.max_tokens` or raise the key's limit).
|
|
999
999
|
- A refused connection (nothing listening) is not retried: the turn fails at once with `can't reach host <name> at
|
|
1000
1000
|
<address> (connection refused) — is the server running?`.
|
|
1001
|
+
- Nor is a TLS handshake that can't work, which fails at once saying what to check: a certificate chi doesn't
|
|
1002
|
+
trust (`certificate verify failed (self-signed certificate)`, `unable to get local issuer certificate`, an expired
|
|
1003
|
+
one: set `SSL_CERT_FILE` to a CA bundle that has the gateway's CA), a certificate for another name (`hostname
|
|
1004
|
+
mismatch`: check the host's `base_url`), or a server that doesn't speak TLS there (`wrong version number`: an
|
|
1005
|
+
`https://` base_url on a plain HTTP port; should it be `http://`?).
|
|
1001
1006
|
- A stream that has already produced output is never retried at the transport (the retry would repeat it). The
|
|
1002
1007
|
chat loop (an OpenAI-compatible host) drops the partial reply and asks again for the whole step, from the same
|
|
1003
1008
|
conversation, at most twice (at most `retry.max` times when that is smaller: `retry.max: 0` turns it off), after
|
|
@@ -1331,7 +1336,7 @@ toolkit:
|
|
|
1331
1336
|
|---|---|
|
|
1332
1337
|
| `full` | every built-in; the default |
|
|
1333
1338
|
| `lean` | `execute`, `read`, `write`, `edit`, `memory_read`, `memory_write`, `task_create`, `task_wait`, `task_get`, `task_list`, `task_stop`, `ask_user_question`, `web_fetch`, `context_read` |
|
|
1334
|
-
| `child` | `lean` plus `send_note`, without `memory_write` |
|
|
1339
|
+
| `child` | `lean` plus `send_note`, without `memory_write`, `delegate`, `delegate_result` |
|
|
1335
1340
|
| `coordinator` | `lean` plus `delegate`, `delegate_result`, `list_sessions` (declared by the coordinator bundle) |
|
|
1336
1341
|
|
|
1337
1342
|
- **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
|
|
@@ -1342,9 +1347,26 @@ toolkit:
|
|
|
1342
1347
|
for a delegate child, or `chi --toolkit NAME` / `--tool` / `--no-tool` ([cli](cli.md)). Changing `toolkit.default`
|
|
1343
1348
|
doesn't move existing sessions, not even when their worker is woken again; a session from before toolkits is on
|
|
1344
1349
|
`full`. A continue and a fork keep the toolkit. `/toolkit` shows and changes it.
|
|
1345
|
-
- **
|
|
1346
|
-
|
|
1347
|
-
the
|
|
1350
|
+
- **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
|
|
1351
|
+
`kits:` entry on `base: lean` or `child` inherits that) is left out softly: it is **reachable**. `find_tools` searches it as group `chi` (`chi/delegate`, with its inputSchema) beside
|
|
1352
|
+
the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
|
|
1353
|
+
usual guardrails, approvals and hooks ([plugins](plugins.md#chitool_sourcecall-acts_as-label-wait-prefix)). A
|
|
1354
|
+
tool in a toolkit's `remove:`, a `--no-tool` or a `/toolkit remove` is **removed**: hard, also after an add, and
|
|
1355
|
+
call_tool can't reach it (`child` removes `memory_write`, `delegate` and `delegate_result`, so a child on it
|
|
1356
|
+
starts no children). To keep a built-in away from the model, name it under `remove:`. `find_tools` and `call_tool`
|
|
1357
|
+
are declared when a toolkit has a reachable built-in (or an MCP server is configured); `full` with no MCP server
|
|
1358
|
+
declares neither.
|
|
1359
|
+
- **The prompt line.** One line after the base prompt names them: `Not declared in this session (toolkit lean): ….
|
|
1360
|
+
Search them with find_tools; call one with call_tool.`, then `Removed (toolkit child): memory_write, …. Only the
|
|
1361
|
+
user can load one (/toolkit add <name>).` With `find_tools` removed, the first sentence ends `Call one with
|
|
1362
|
+
call_tool(tool: "chi/<name>", args: {…}).` instead. With `call_tool` itself removed, nothing is reachable and one
|
|
1363
|
+
line names them all: `Not loaded in this session (toolkit lean): …. Only the user can load one (/toolkit add
|
|
1364
|
+
<name>).`
|
|
1365
|
+
- **Calls by name.** A direct call of a reachable tool is refused before the guardrails (no approval card), with
|
|
1366
|
+
the route: `Error: delegate isn't declared in this session (toolkit lean); call it with call_tool(tool:
|
|
1367
|
+
"chi/delegate", args: {…}) (find_tools gives its inputSchema).` A removed tool's: `Error: memory_write isn't
|
|
1368
|
+
loaded in this session (toolkit child) and can't be loaded from here; only the user can (/toolkit add
|
|
1369
|
+
memory_write).` `/toolkit` lists both kinds, and the web's toolkit chip says them in its tooltip.
|
|
1348
1370
|
- **Applied at a turn's start.** A change (`/toolkit`, a plugin's `ctx.use_toolkit`) is saved at once and the next
|
|
1349
1371
|
turn declares it; once the session has a model reply, `/toolkit` asks first ("Changes that restart the prompt
|
|
1350
1372
|
cache").
|
data/docs/guardrails.md
CHANGED
|
@@ -303,7 +303,7 @@ A plugin tool that runs other tools for the model (a dispatcher such as
|
|
|
303
303
|
and with which arguments (`args:`). A rule's `tool:` then matches the call's
|
|
304
304
|
own name **or** the tool it acts as: `tool: mcp_github_*` fires for a
|
|
305
305
|
`call_tool` that acts as `mcp_github_merge_pull_request`, and `tool: call_tool`
|
|
306
|
-
fires whatever it acts as. The question names the tool it acts as (by the
|
|
306
|
+
fires whatever MCP tool it acts as. The question names the tool it acts as (by the
|
|
307
307
|
`label:` its `targets:` gives, if any: display only) and shows its arguments, and an approval of "this call" is keyed by the real name, the
|
|
308
308
|
alias and those arguments (`/guardrails` lists it as
|
|
309
309
|
`call_tool>mcp_github_merge_pull_request:number=7 …`), so it never covers a
|
|
@@ -313,6 +313,28 @@ one of chi's own tools or another bundle's tool: that `acts_as:` is dropped
|
|
|
313
313
|
name only. Hooks and loop-guard's `ignore_tools` still see the call's own
|
|
314
314
|
name.
|
|
315
315
|
|
|
316
|
+
chi's `call_tool` is exempt from the "another bundle's tool" drop (not the
|
|
317
|
+
core-tool one): its `acts_as` is always a tool its sources published, so a
|
|
318
|
+
bundle tool that happens to share an MCP tool's `mcp_<server>_<tool>` name
|
|
319
|
+
can't take `call_tool` calls of that MCP tool out of your `mcp_*` rules. The
|
|
320
|
+
trade-off, when such a clash exists:
|
|
321
|
+
- an `allow` or `ask` rule by that name matches both the bundle tool and the
|
|
322
|
+
MCP call;
|
|
323
|
+
- a `rule`-scope approval ("always for this rule") covers both, because it
|
|
324
|
+
matches by rule, source and place, not by tool;
|
|
325
|
+
- `session` and `repo` approvals stay distinct (`call_tool>mcp_x:…` vs
|
|
326
|
+
`mcp_x:…`);
|
|
327
|
+
- deny rules cover both, which is the point.
|
|
328
|
+
|
|
329
|
+
A `call_tool` of one of chi's own tools (`chi/delegate`: a built-in the
|
|
330
|
+
session's toolkit leaves out, [toolkits](configuration.md#toolkits)) is
|
|
331
|
+
gated **as that tool**: it is turned into the `delegate` call before the
|
|
332
|
+
guardrails run, so every rule on `delegate` (or `execute`'s `command:`, the
|
|
333
|
+
`rm` check, a delegate child's boundary) gives the same verdict as for a
|
|
334
|
+
direct call, and an approval is keyed `delegate:…` either way. `tool:
|
|
335
|
+
call_tool` rules no longer see such calls, only MCP calls and misses. A
|
|
336
|
+
`chi/` name the toolkit removes is refused before the gate, with no card.
|
|
337
|
+
|
|
316
338
|
```yaml
|
|
317
339
|
- id: mcp-ask
|
|
318
340
|
# every MCP call (call_tool, acting as mcp_<server>_<tool>); not a find_tools search.
|
data/docs/hooks.md
CHANGED
|
@@ -536,6 +536,10 @@ A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
|
|
|
536
536
|
arguments whole on `args:` (string keys) instead, and its `content:` is those
|
|
537
537
|
arguments as JSON. An MCP tool's call is chi's `call_tool` (its tool index's, which the mcp
|
|
538
538
|
bundle runs): `tool: "call_tool"`, with `args: {"tool" => "<server>/<tool>", "args" => {…}}`.
|
|
539
|
+
A built-in reached through `call_tool` (`chi/delegate`, a tool the session's toolkit doesn't
|
|
540
|
+
declare) fires as that built-in, its call built as a direct call's, with `call[:via] == "call_tool"`.
|
|
541
|
+
A hook that drops `via:` or rewrites the call to a tool the toolkit removes gets the call refused
|
|
542
|
+
by its name.
|
|
539
543
|
|
|
540
544
|
## Bundle Hooks (unified workflow bundle)
|
|
541
545
|
|
data/docs/internals/bundles.md
CHANGED
|
@@ -24,7 +24,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
24
24
|
|---|---|
|
|
25
25
|
| `BundleCommand` (`lib/samagotchi/bundle_command.rb`) | `chi bundle install/upgrade/uninstall/status/diff/list/build/trash`; runs the Installer, prints its summary, runs the conflict agent step |
|
|
26
26
|
| `SourceNormalizer` (source.rb) | source → local dir; a git/zip/tar extract is *owned* and removed after the install |
|
|
27
|
-
| `Manifest` | parses/validates `manifest.yml` (files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers) and writes it (`Manifest.write`, for Builder) |
|
|
27
|
+
| `Manifest` | parses/validates `manifest.yml` (files, hooks as `BundleHook`, plugin, scripts, needs, requires_chi, includes, context_providers) and writes it (`Manifest.write`, for Builder). It ignores `requires_chi_next`, a shipped bundle's release-time marker (`script/release/release_tools.rb`, docs/releasing.md) |
|
|
28
28
|
| `Installer` | one install/upgrade/dry run of one bundle (below) |
|
|
29
29
|
| `AssetInstaller` | the parts that live in `.bundles/<name>/`: hooks, guardrail rules, plugin, scripts; returns `AssetInstaller::Installed` (the paths the record keeps) |
|
|
30
30
|
| `Merger` | 3-way classification per file: `install`, `fast_forward`, `keep`, `noop`, `conflict` |
|
data/docs/internals/engine.md
CHANGED
|
@@ -34,20 +34,23 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
34
34
|
│ # resumed: true (only on a step-limit Continue: resumed: given),
|
|
35
35
|
│ # images (only when there are any), cut_after (the model's cut wait, s)}
|
|
36
36
|
│ # refresh_profile! (may ask the server's /props once)
|
|
37
|
-
│ # turn.settings = LLM::TurnSettings (vision, sampling, thinking; model_name is set in generate)
|
|
37
|
+
│ # turn.settings = LLM::TurnSettings (vision, sampling, thinking; model_name is set in generate),
|
|
38
|
+
│ # from one EffectiveModel; ThinkingNotices#turn_started keeps the level and says one the host can't do
|
|
38
39
|
│ # announces guardrail load failures, starts and awaits plugin init tasks, applies staged tools,
|
|
39
|
-
│ # then the session's toolkit (apply_toolkit!: Registry#toolkit,
|
|
40
|
+
│ # then the session's toolkit (apply_toolkit!: Registry#toolkit, ToolIndex#refresh! for find_tools'
|
|
41
|
+
│ # chi line, a prompt rebuild when it changed)
|
|
40
42
|
│ # fires the :session_start hook on the Engine's first turn, then the :before_turn hook
|
|
41
43
|
│ # (a frozen copy of the history, the prompt, and resumed)
|
|
42
44
|
│ # turn_messages: system head + history + due reminders (:reminder_injected) + the user message
|
|
43
45
|
│ # Returns false when a Stop came during the probes or the init wait, else true
|
|
44
46
|
│
|
|
45
47
|
├── generate(turn, max_iterations:, max_tool_output_chars:, pending_input:)
|
|
46
|
-
│ #
|
|
47
|
-
│ #
|
|
48
|
-
│ #
|
|
48
|
+
│ # one EffectiveModel (after the hooks, which may switch the model): sync_kernel_client!, then
|
|
49
|
+
│ # @kernel.turn_settings = turn.settings with model_name, window_setting, llm_context (the
|
|
50
|
+
│ # session's, the model's/host's, else llm_context.strategy) and price
|
|
51
|
+
│ # (hosts.<name>.models.<id>.price), all read per turn
|
|
49
52
|
│ # turn.limit = max_iterations or IterationLimit.for
|
|
50
|
-
│ # backend.complete(messages: turn.messages, ..., pending_input: turn_drain(pending_input))
|
|
53
|
+
│ # backend(model).complete(messages: turn.messages, ..., pending_input: turn_drain(pending_input))
|
|
51
54
|
│ # Returns LLM::ModelResult (text, conversation, canceled?, exhausted?, tool_activity,
|
|
52
55
|
│ # context_status, empty_steps, empty_retries)
|
|
53
56
|
│
|
|
@@ -118,6 +121,16 @@ A worker's TERM, HUP or INT (WorkerSignals, trapped by a spawned worker only):
|
|
|
118
121
|
loop left brings only that hard exit, and run_session_loop restores the default handlers
|
|
119
122
|
→ run_session_loop: exit 0, no discard; input queued behind the stopped turn wakes a new worker
|
|
120
123
|
|
|
124
|
+
The effective model doesn't resolve (a broken host config, HostRegistry#resolve raising):
|
|
125
|
+
→ the rescued readers give their fallback (Engine#model_setting) and log effective_model_unresolved;
|
|
126
|
+
a setting that fails on a resolved model gives its fallback and logs <setting>_unresolved
|
|
127
|
+
(sampling, thinking, reasoning_history, steer_cut_after, llm_context, window, price, served,
|
|
128
|
+
vision_adapter); served_probe_failed when asking the server for its served model fails;
|
|
129
|
+
alias_note_failed, thinking_notice_failed likewise
|
|
130
|
+
→ WARN once per process for an (event, model, error class), DEBUG after (EffectiveModel::Unresolved)
|
|
131
|
+
→ session_state_snapshot never raises (its model fields are nil); a turn, stats_snapshot,
|
|
132
|
+
chat_model? and system_prompt raise, as before
|
|
133
|
+
|
|
121
134
|
Stopped before the model was asked (prepare_turn returned false):
|
|
122
135
|
→ stopped_before_model(turn) → end_turn(turn, "canceled")
|
|
123
136
|
→ nothing kept, :turn_canceled {cancellation_reason, cancelled_by, duration_ms}
|
|
@@ -129,7 +142,7 @@ Stopped before the model was asked (prepare_turn returned false):
|
|
|
129
142
|
The Engine does not talk to models directly. `Engine#backend` picks the loop the effective model's host speaks:
|
|
130
143
|
|
|
131
144
|
```
|
|
132
|
-
Engine#backend → backend_for(
|
|
145
|
+
Engine#backend → backend_for(effective_model) # EffectiveModel.of(@host_registry, @model_choice)
|
|
133
146
|
→ host entry chat? (an api in LLM::Adapters::BY_API: openai, anthropic): LLM::ChatLoop (one per Engine, built on first use)
|
|
134
147
|
→ else: LLM::NativeBackend wrapping the Engine's KernelLoop
|
|
135
148
|
```
|
|
@@ -243,8 +256,11 @@ One iteration with tool calls:
|
|
|
243
256
|
→ :tool_dispatch_started {iteration, call_count}
|
|
244
257
|
→ for each call: ToolRunner#run(call, iteration:, call_index:, call_count:,
|
|
245
258
|
on_stream_event:, max_tool_output_chars:)
|
|
246
|
-
→
|
|
247
|
-
|
|
259
|
+
→ first, the call's entry unwraps it (Registry::Entry#unwrap: a call_tool of chi/<built-in> becomes
|
|
260
|
+
that built-in's call with via: "call_tool", ToolIndex#unwrap); the rest sees the real call
|
|
261
|
+
→ a tool the session's toolkit leaves out (Registry#left_out; with via:, one Registry#indexed doesn't
|
|
262
|
+
reach) skips the gate: no hooks, no card; KernelLoop#dispatch answers it (the call_tool route for a
|
|
263
|
+
reachable tool, the not-loaded error for a removed one)
|
|
248
264
|
→ Guardrails::Gate#evaluate (the before_tool_call hooks, then the core checks)
|
|
249
265
|
→ :tool_call_started
|
|
250
266
|
→ an ask verdict: Gate#settle_ask (asks the user, records waited_ms)
|
|
@@ -261,11 +277,13 @@ One iteration with tool calls:
|
|
|
261
277
|
```
|
|
262
278
|
|
|
263
279
|
**ToolRunner** (`ToolRunner#run`):
|
|
280
|
+
- Unwraps a call_tool of a built-in first (`Registry::Entry#unwrap`), so `original`, the params and the label
|
|
281
|
+
are the built-in's and no "ran as:" line follows.
|
|
264
282
|
- Runs the gate first, so `:tool_call_started` shows the call that will run (a before_tool_call hook may
|
|
265
283
|
replace it; known-names corrects a misspelled name this way, and the model gets one line saying what ran).
|
|
266
284
|
- Emits `:tool_call_started` {iteration, call_count, call_index, tool, call, params, label (plugin tools),
|
|
267
|
-
title, called_as, view, ref, refs}. The row fields (title, called_as, view, ref) come from `ToolRowFields.for`, which the
|
|
268
|
-
reload (`Web::MessageParts#tool_part`) calls too. `ToolRowFields::ACTIVITY_KEYS` (title, called_as) travel
|
|
285
|
+
title, called_as, via, view, ref, refs}. The row fields (title, called_as, via, view, ref) come from `ToolRowFields.for`, which the
|
|
286
|
+
reload (`Web::MessageParts#tool_part`, after `BuiltinCalls.unwrap_saved`) calls too. `ToolRowFields::ACTIVITY_KEYS` (title, called_as, via) travel
|
|
269
287
|
inside `activity` on the completed event; `EVENT_KEYS` (view, ref, refs) go top-level on both events.
|
|
270
288
|
`Bridge::TurnAccumulator` (snapshot and replay) and the web (`activity.js` `ACTIVITY_FIELDS` /
|
|
271
289
|
`EVENT_FIELDS`) copy them by these lists, so a new row field is added there. `ref` is `FileRef.for` (a
|
|
@@ -326,6 +344,38 @@ TurnState
|
|
|
326
344
|
|
|
327
345
|
The lock is a leaf: nothing is called out while it is held.
|
|
328
346
|
|
|
347
|
+
### EffectiveModel (a snapshot per read)
|
|
348
|
+
|
|
349
|
+
`EffectiveModel.of(registry, choice)` resolves the model once (`HostRegistry#resolve`: host, client, the id
|
|
350
|
+
sent) with its lookup names, and reads each setting from them (sampling, thinking, reasoning history, steer cut,
|
|
351
|
+
LLM context, window, price, served model, vision, profile). It is built per read and never kept: config.yml is
|
|
352
|
+
read live, a model listing can move a bare id to another host, and another process's saved model lists change
|
|
353
|
+
what a resolve gives, all without a `/model`. Frozen, so any thread may hold one.
|
|
354
|
+
|
|
355
|
+
- The Engine's readers take one (`Engine#effective_model`); those whose fallback always covered the resolve go
|
|
356
|
+
through `#model_setting`. A turn takes one in `turn_settings` (before the hooks) and one in `generate` (after
|
|
357
|
+
them); `system_prompt`, `steer_cut_after` (at `:turn_started`) and the turn-end warm-up take their own;
|
|
358
|
+
`stats_snapshot` and `session_state_snapshot` take one each (the latter nil-tolerant).
|
|
359
|
+
- Never resolved (they hold on a broken resolve): `effective_model_ref`, `bare_model_name`,
|
|
360
|
+
`guardrail_model_name`, `sync_model_key!`, `served_model(probe: false)` (`HostRegistry#model_ref`/`#bare_name`).
|
|
361
|
+
- Readers on other threads: the Bridge (`/state`, `/stats`, the SSE reset, a steer's cut wait), the worker's
|
|
362
|
+
command thread, the recap thread and plugin threads (`session_model_recap_target`), the REPL.
|
|
363
|
+
- `ModelChoice` (`@model_choice`) holds the name and the typed name as one value, so a reader on another thread
|
|
364
|
+
never pairs one model's name with another's typed name.
|
|
365
|
+
- `EffectiveModel.of_session` builds a saved session's (no worker): `Web::App`, `LLMContextStrategy.for_session`.
|
|
366
|
+
|
|
367
|
+
`SessionSetting` names the four per-session settings (llm_context, thinking, reasoning_history, toolkit): the
|
|
368
|
+
session's own value, the commands that move each chip, its chip from an EffectiveModel or from the Engine's
|
|
369
|
+
readers, a saved session's chip, and how the web takes a worker snapshot's. The session state, `/stats` (no
|
|
370
|
+
toolkit), a `:command_ran` and the web's session JSON list them from it.
|
|
371
|
+
|
|
372
|
+
### ThinkingNotices
|
|
373
|
+
|
|
374
|
+
What a turn's thinking level leads to, said once per session and host (`thinking_notices.rb`): an effort the
|
|
375
|
+
host can't do, off the model didn't honour, a host refusing the thinking fields or the reasoning sent back. It
|
|
376
|
+
keeps the turn's `[level, ModelTarget]` and whether the generation thought; `turn_settings` and the stream handler
|
|
377
|
+
call it. Turn thread only.
|
|
378
|
+
|
|
329
379
|
### Turn (Engine::Turn)
|
|
330
380
|
|
|
331
381
|
The turn as the Engine sees it (the KernelLoop has its own `Turn` struct):
|
|
@@ -435,16 +485,17 @@ hook registry (`@hooks.fire`), not on the sink or the observers. The other hook
|
|
|
435
485
|
```
|
|
436
486
|
Engine initialization (in this order, among other things):
|
|
437
487
|
@turn_state, @steer_cut (SteerCut: its GenerationPhase and WaitingSteer)
|
|
438
|
-
@host_registry, @
|
|
488
|
+
@host_registry, @model_choice (ModelChoice: the name and the name as typed); @client = effective_model.client
|
|
439
489
|
@given_profile = profile or nil (resolved on first need, so building an Engine makes no network call)
|
|
440
490
|
@hook_routes (HookRoutes: notices, hook questions, stop_turn/stop_generation, steer attribution)
|
|
441
491
|
@guardrail_wiring, @question_desk
|
|
442
492
|
@toolkit_binding (ToolkitBinding: the kits, the plugins' among them, and the turn-start apply)
|
|
443
493
|
@extension_load = ExtensionLoad.new; @hooks = @extension_load.hooks (config.yml's, then the bundles')
|
|
444
494
|
@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)
|
|
446
|
-
|
|
447
|
-
the
|
|
495
|
+
then (plugins or not) @tool_index.declare(@tools): find_tools + call_tool (source "index", not core),
|
|
496
|
+
declared per toolkit by Registry#declared? (a source registered at load, or a reachable built-in) and
|
|
497
|
+
sets call_tool's unwrap (Tools::ToolIndex: the chi group from Registry#reachable_names, the sources'
|
|
498
|
+
groups, search, call checks, the unwrap; a source runs the call with its own plugin Context)
|
|
448
499
|
wire_kernel: @kernel = KernelLoop.new(client:, profile:, hooks:, reminder_store:, tools:)
|
|
449
500
|
@kernel.guardrail_gate = @guardrail_wiring.gate; @kernel.warmup = PromptWarmup.new
|
|
450
501
|
@hooks.runtime = @hook_routes.runtime (HookRoutes, built first: what a hook or plugin does reaches the UIs)
|
|
@@ -453,8 +504,9 @@ Engine initialization (in this order, among other things):
|
|
|
453
504
|
@session_observer with the metrics, the log subscriber and the thinking tails subscribed (subscribe_session_observers)
|
|
454
505
|
|
|
455
506
|
Model switching, Engine#switch_model!(model_name, persist_default:, typed:):
|
|
456
|
-
→ checks the host (ModelProfile.check_host!), sets @
|
|
457
|
-
→ drops the given profile and the profile resolution (resolved again on first need
|
|
507
|
+
→ checks the host (ModelProfile.check_host!), sets @model_choice (name and typed name in one assignment)
|
|
508
|
+
→ drops the given profile and the profile resolution (resolved again on first need; one that another
|
|
509
|
+
thread finishes for the old model is neither kept nor applied: #store_profile)
|
|
458
510
|
→ sync_model_key!, @prompt_builder.reset! (the system prompt is built again)
|
|
459
511
|
→ sync_kernel_client! (the kernel talks to the new host), the context window cache dropped
|
|
460
512
|
→ the kernel is never rebuilt; the next turn's backend follows the new host's api
|
|
@@ -608,6 +660,10 @@ Paths are under `lib/samagotchi/`.
|
|
|
608
660
|
| `turn_state.rb` | Cross-thread turn state (running, controller, sink, steers, activity clock) |
|
|
609
661
|
| `steer_cut.rb` | `SteerCut`: the Engine's seam for a message cutting the generation |
|
|
610
662
|
| `generation_phase.rb`, `waiting_steer.rb` | Whether a steer may cut the running generation, and the steer that waits to |
|
|
663
|
+
| `effective_model.rb` | `EffectiveModel` (the effective model's settings, a snapshot per read; `.of_session`), `ModelChoice`, the fallbacks' log lines (`Unresolved`) |
|
|
664
|
+
| `session_setting.rb` | `SessionSetting`: the per-session settings' descriptors (own value, chips, the web's fallback) |
|
|
665
|
+
| `level_setting_command.rb`, `thinking_command.rb`, `reasoning_history_command.rb` | `/thinking` and `/reasoning-history` (`LevelSettingCommand` and its two Specs) |
|
|
666
|
+
| `thinking_notices.rb` | `ThinkingNotices`: the notices a turn's thinking level leads to |
|
|
611
667
|
| `steer_cut_after.rb`, `cut_policy.rb` | The model's cut wait (`SteerCutAfter.resolve`); the loops' cut path (`CutPolicy`) |
|
|
612
668
|
| `steer.rb` | Queued input and steers at iteration boundaries |
|
|
613
669
|
| `kernel_loop.rb` | Native loop: `#run` (format, generate, parse, dispatch, iterate) |
|
|
@@ -25,13 +25,14 @@ right-hand column happens.
|
|
|
25
25
|
┌──────────────────────────────────────────────────────────────────────────┐
|
|
26
26
|
│ tools built-ins, then plugin tools sorted by (bundle, name), │ a tool added/changed,
|
|
27
27
|
│ the tool index's find_tools/call_tool as bundle "index" │
|
|
28
|
-
│ (
|
|
28
|
+
│ (per toolkit; MCP lines from load, the chi line per toolkit)│
|
|
29
29
|
│ those the session's toolkit declares (per toolkit) │ /model, a profile change,
|
|
30
30
|
│ (chat: the `tools` field; native: the model's template) │ a toolkit change
|
|
31
31
|
├──────────────────────────────────────────────────────────────────────────┤
|
|
32
32
|
│ SYSTEM, stable part SystemPrompt#system_prompt_with_index
|
|
33
33
|
│ [native thinking token] base prompt │
|
|
34
|
-
│ not-loaded line (only when the toolkit leaves tools out
|
|
34
|
+
│ not-loaded line (only when the toolkit leaves tools out: │
|
|
35
|
+
│ not declared / removed) │
|
|
35
36
|
│ rg guidance │ /thinking level,
|
|
36
37
|
│ system identity (+ model overlay) │ /model, editing a
|
|
37
38
|
│ model notes (model_notes_* matching the model) │ memory / AGENTS.md
|
|
@@ -70,6 +71,12 @@ right-hand column happens.
|
|
|
70
71
|
beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request, so a mid-turn change
|
|
71
72
|
would differ from the turn's prompt. Under `full` nothing is filtered and no line is added: the bytes are what
|
|
72
73
|
they were before toolkits.
|
|
74
|
+
- find_tools' description is a function of the toolkit and the source groups **at load**: `ToolIndex#declare`
|
|
75
|
+
snapshots the MCP groups' lines (name, summary, callable count) and `ToolIndex#refresh!` (from
|
|
76
|
+
`ToolkitBinding#apply!`) rebuilds only the `chi` line, the built-ins the toolkit reaches. An MCP server that
|
|
77
|
+
finishes booting between load and the first turn changes search answers only, so two sessions on the same
|
|
78
|
+
toolkit with the same MCP cache send the same bytes. Whether find_tools/call_tool are declared at all reads the
|
|
79
|
+
same two things (`Registry#index_needed?`), never live groups.
|
|
73
80
|
- On remote Claude, `LLM::PromptCache.mark` sends the system message as two text parts. The stable part carries
|
|
74
81
|
breakpoint #1, so a **new session** reads it from the cache. The last non-system message carries breakpoint
|
|
75
82
|
#2, so the **next request in the session** reads everything up to it. Mid and tail system messages never take
|
data/docs/plugins.md
CHANGED
|
@@ -25,6 +25,8 @@ plugin:
|
|
|
25
25
|
file: plugin.rb
|
|
26
26
|
sha256: sha256:6a1a7022… # shasum -a 256 plugin.rb
|
|
27
27
|
requires_chi: ">= 0.1.28" # optional: a gem-style requirement (">= 0.1.28, < 0.2"); the bundle's hooks/ check it too
|
|
28
|
+
# requires_chi_next: true # shipped bundles only, ignored by chi: rake release:bump raises requires_chi
|
|
29
|
+
# to the chi being released (docs/releasing.md)
|
|
28
30
|
needs: [gh] # optional: outside commands it relies on (see memory.md#bundles-that-need-outside-commands)
|
|
29
31
|
```
|
|
30
32
|
|
|
@@ -294,6 +296,8 @@ is what `targets:` says:
|
|
|
294
296
|
(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
297
|
call too, as well as rules on the tool's own name. It can't be one of
|
|
296
298
|
chi's own tools or another bundle's tool: that is dropped (and logged).
|
|
299
|
+
The "another bundle's tool" drop doesn't apply to chi's `call_tool`: its
|
|
300
|
+
`acts_as` is always a tool its sources published.
|
|
297
301
|
- `args:`: the arguments it acts with (a Hash), when they aren't the call's
|
|
298
302
|
own (a dispatcher's inner arguments). A call with no `command:` or
|
|
299
303
|
`paths:` is asked about with them, and "allow this call" is keyed by them.
|
|
@@ -583,15 +587,32 @@ chi.toolkit "review", base: "lean", add: %w[delegate delegate_result]
|
|
|
583
587
|
|
|
584
588
|
Tools the model reaches through a search instead of declaring them one by
|
|
585
589
|
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
|
|
587
|
-
search that answers each match's `inputSchema`) and
|
|
588
|
-
`<group>/<tool>`; its row label is the
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
590
|
+
two tools in all, whatever the number of sources: `find_tools(query,
|
|
591
|
+
group?)` (a keyword search that answers each match's `inputSchema`) and
|
|
592
|
+
`call_tool(tool, args)` (calls one as `<group>/<tool>`; its row label is the
|
|
593
|
+
first source's `label`). The index has one group of its own, **`chi`**: the
|
|
594
|
+
built-ins the session's toolkit leaves out but doesn't remove
|
|
595
|
+
([toolkits](configuration.md#toolkits)), each with its registered schema.
|
|
596
|
+
The two tools are registered once, after the plugins load, and declared per
|
|
597
|
+
toolkit: when a plugin registered a source then, or when the toolkit has a
|
|
598
|
+
reachable built-in (`full` with no source declares neither). Nothing a
|
|
599
|
+
source publishes later changes the request's tools, only what a search
|
|
600
|
+
answers, so the prompt cache keeps: the search tool's description lists the
|
|
601
|
+
source groups as they were at load, and a `chi` line first, per toolkit.
|
|
602
|
+
The mcp bundle is the only source so far (a group per server). A source's
|
|
603
|
+
group named `chi`, or named like an earlier group, is left out of search,
|
|
604
|
+
lookup and the description, with one warning per name.
|
|
605
|
+
|
|
606
|
+
**A built-in through `call_tool` runs as that built-in.** `call_tool(tool:
|
|
607
|
+
"chi/delegate", args: {…})` (or a bare `delegate` while it's reachable) is
|
|
608
|
+
turned into the `delegate` call itself before the guardrails, so rules on
|
|
609
|
+
`delegate`, approvals (keyed `delegate:…`, the same as a direct call's),
|
|
610
|
+
`before_tool_call`/`after_tool_call` hooks and the row see `delegate`, with
|
|
611
|
+
`call[:via] == "call_tool"`; the row reads "delegate (via call_tool)", live
|
|
612
|
+
and after a reload. `tool: call_tool` rules see MCP calls and misses only. A
|
|
613
|
+
`chi/` name the toolkit removes or declares, or any built-in once
|
|
614
|
+
`call_tool` itself is removed, is refused by its name before the guardrails
|
|
615
|
+
(no hook, no card).
|
|
595
616
|
|
|
596
617
|
```ruby
|
|
597
618
|
@source = chi.tool_source(label: "mcp", prefix: "mcp_",
|
|
@@ -621,7 +642,8 @@ index = Samagotchi::Tools::ToolIndex
|
|
|
621
642
|
block returns (a String, a `ToolResult` with images).
|
|
622
643
|
- **Guardrails** see a `call_tool` as the tool it calls: its `acts_as`, its
|
|
623
644
|
arguments and `"<group>: <tool>"` as the question's label
|
|
624
|
-
([acts_as:](#guardrails))
|
|
645
|
+
([acts_as:](#guardrails)), even when another bundle has a tool of that
|
|
646
|
+
name (only the core-tool drop applies). A call made before its tool is published is
|
|
625
647
|
named by `acts_as` from the names alone; a bare name with `prefix` is
|
|
626
648
|
taken as an `acts_as` as given. The `acts_as:` hook must give exactly the
|
|
627
649
|
`acts_as` the source publishes for that tool: a call whose tool isn't the
|
|
@@ -632,11 +654,11 @@ index = Samagotchi::Tools::ToolIndex
|
|
|
632
654
|
what the two tools take in every request (none when other tools had both
|
|
633
655
|
names). A `Group` in a state other than the three, holding another group's
|
|
634
656
|
tool, or a `Tool` with a blank `acts_as` raises `ArgumentError`.
|
|
635
|
-
- `find_tools` and `call_tool` aren't chi's own tools:
|
|
636
|
-
them (unless its `remove:` names
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
declares neither.
|
|
657
|
+
- `find_tools` and `call_tool` aren't chi's own tools: a toolkit that needs
|
|
658
|
+
them declares them (unless its `remove:` names one; `call_tool` stays
|
|
659
|
+
usable with `find_tools` removed). A bundle that declares tools of other
|
|
660
|
+
names itself (an mcp bundle before 0.7.0 has `find_mcp_tools` and
|
|
661
|
+
`mcp_call`) keeps them; with no source, `full` declares neither of chi's.
|
|
640
662
|
|
|
641
663
|
### `chi.peer_source(name, label:)`
|
|
642
664
|
|
|
@@ -1064,7 +1086,7 @@ bundles:
|
|
|
1064
1086
|
|
|
1065
1087
|
- **Tools: search, then call.** The servers' tools are kept in chi's tool index;
|
|
1066
1088
|
the model has two fixed tools:
|
|
1067
|
-
- `find_tools(query,
|
|
1089
|
+
- `find_tools(query, group?)` searches it by keywords over the
|
|
1068
1090
|
server's name, the tool's name and its description, and answers up to 5
|
|
1069
1091
|
tools, each with its name `<server>/<tool>`, its description and its
|
|
1070
1092
|
whole `inputSchema`. A word counts more the fewer tools have it (a word
|
|
@@ -1089,7 +1111,7 @@ bundles:
|
|
|
1089
1111
|
`args` are typed by the tool's
|
|
1090
1112
|
`inputSchema` (numbers and booleans given as text, JSON text for an
|
|
1091
1113
|
object). A name it doesn't know answers the closest ones (`Error: no
|
|
1092
|
-
|
|
1114
|
+
tool chrome/open_url. Closest: chrome/new_page, …`). Its row shows
|
|
1093
1115
|
`mcp chrome/take_screenshot fullPage=true`.
|
|
1094
1116
|
- A tool turn takes a few more steps than when every tool was declared
|
|
1095
1117
|
(the P1 spike: about 1.5 more on a 30-tool server), and each request is
|
|
@@ -1159,8 +1181,9 @@ bundles:
|
|
|
1159
1181
|
session" (or in this repo) allows that tool with those arguments only.
|
|
1160
1182
|
(Approvals stored before 0.6.0, under the old name, don't carry over, nor
|
|
1161
1183
|
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
|
|
1184
|
+
Two things see `call_tool`, not the MCP tool: hooks (a `before_tool_call`
|
|
1185
|
+
event's `tool:` is `"call_tool"`, the tool in its args; a built-in reached
|
|
1186
|
+
through `call_tool` is the exception, it fires as that built-in), and loop-guard's
|
|
1164
1187
|
`ignore_tools` (`call_tool` ignores every MCP call; calls to different
|
|
1165
1188
|
tools, or with different args, stay distinct, since the tool is in the
|
|
1166
1189
|
args).
|