samagotchi 0.71.0 → 0.72.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 +45 -1
- data/docs/architecture.md +14 -8
- data/docs/cli.md +3 -2
- data/docs/configuration.md +23 -13
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/engine.md +38 -13
- data/docs/internals/llm-context-live.md +1 -1
- data/docs/internals/prompt-caching.md +6 -0
- data/docs/plugins.md +18 -7
- data/docs/sessions.md +3 -1
- data/docs/testing.md +10 -1
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -3
- data/lib/samagotchi/bridge.rb +5 -2
- data/lib/samagotchi/broadcast/scope_card.rb +2 -2
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -7
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +2 -2
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +2 -2
- data/lib/samagotchi/bundles/coordinator/manifest.yml +8 -6
- data/lib/samagotchi/bundles/coordinator/plugin.rb +10 -0
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/config.rb +7 -25
- data/lib/samagotchi/context_note.rb +2 -6
- data/lib/samagotchi/effective_model.rb +1 -1
- data/lib/samagotchi/engine.rb +91 -213
- data/lib/samagotchi/engine_peers.rb +2 -0
- data/lib/samagotchi/event_routes.rb +83 -0
- data/lib/samagotchi/hook_routes.rb +16 -37
- data/lib/samagotchi/host_config.rb +1 -1
- data/lib/samagotchi/host_keys.rb +0 -2
- data/lib/samagotchi/paths.rb +13 -0
- data/lib/samagotchi/plugin/api.rb +11 -5
- data/lib/samagotchi/plugin/peer.rb +5 -8
- data/lib/samagotchi/prompt_files.rb +2 -4
- data/lib/samagotchi/session.rb +2 -1
- data/lib/samagotchi/session_manager.rb +6 -16
- data/lib/samagotchi/session_model_settings.rb +227 -0
- data/lib/samagotchi/session_setup.rb +2 -1
- data/lib/samagotchi/system_prompt.rb +18 -13
- data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -14
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
- data/lib/samagotchi/tool_activity.rb +1 -0
- data/lib/samagotchi/tool_declarations.rb +4 -0
- data/lib/samagotchi/toolkit.rb +55 -24
- data/lib/samagotchi/toolkit_binding.rb +7 -5
- data/lib/samagotchi/toolkit_command.rb +28 -5
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +32 -6
- data/lib/samagotchi/tools/list_sessions.rb +2 -2
- data/lib/samagotchi/tools/peers.rb +5 -2
- data/lib/samagotchi/tools/register_reminder.rb +1 -0
- data/lib/samagotchi/tools/registry.rb +1 -1
- data/lib/samagotchi/tools/tool_index.rb +15 -4
- data/lib/samagotchi/turn_note.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +13 -10
- data/lib/samagotchi/web/public/app.js +20 -18
- data/lib/samagotchi/web/public/bubbles.js +1 -1
- data/lib/samagotchi/web/public/cards_place.js +6 -10
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/chip_popover.js +3 -1
- data/lib/samagotchi/web/public/composer.js +13 -6
- data/lib/samagotchi/web/public/context_chips.js +25 -12
- data/lib/samagotchi/web/public/data.js +5 -1
- data/lib/samagotchi/web/public/format.js +5 -0
- data/lib/samagotchi/web/public/sessions_list.js +2 -3
- data/lib/samagotchi/web/public/stage_frame.js +1 -1
- data/lib/samagotchi/web/public/stage_view.js +8 -2
- data/lib/samagotchi/web/public/stream_handlers.js +6 -3
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/toolkit_chip.js +3 -1
- data/lib/samagotchi/web/session_hub.rb +8 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6421f740a2683f0dcf3d24b92fba7bba62bed9edb92ddfd532ce69c74e14e0e8
|
|
4
|
+
data.tar.gz: 23841684a58d0a3efedddcd0fb0caef4d1403c61a88268ad79f6b28a9d8f00ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91ccf28b2be3f0bfff968c5d0448b0c1748ef90c505f1195ec655ae93784e48975409e54a56e18d7f78605812770d4ec9eeae336208da1a95ddbab2aa057f3da
|
|
7
|
+
data.tar.gz: c5f8e40dfea68d7d6d706970106cbbdc5caf8e9571b92d771dc2928d6a3358704aafcd6b3a047f96ccfbe285b28ee2bc98de81505f10a342cc8b6efc228b0b9c
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,49 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.72.0] - 2026-10-16
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- A toolkit can carry a prompt text (`toolkit.kits.<name>.prompt:`, `chi.toolkit … prompt:`): the sessions on it
|
|
16
|
+
get it in their system prompt, others don't; `/toolkit`, `/stats` and the web's tools chip show it. `delegate`
|
|
17
|
+
can start a child on a named toolkit (`toolkit:`). The coordinator bundle (0.5.0) declares a `coding` toolkit
|
|
18
|
+
(`child`'s tools plus "Be concise in your responses" and "Show file paths clearly when working with files"),
|
|
19
|
+
and its skill starts build and fix children on it.
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
|
|
23
|
+
- `SAMAGOTCHI_HOSTS_JSON`: chi no longer reads it (workers stopped inheriting it in 0.64.0). Hosts come only from
|
|
24
|
+
config.yml's `hosts:`; one still exported in your shell is ignored.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Attached TUI: a turn of images alone sent from the web names each image once while you watch it start (or join
|
|
29
|
+
it running), not twice.
|
|
30
|
+
- Attached TUI: joining a session shows a call the guardrails denied as `blocked`, as it showed live and as the
|
|
31
|
+
web shows it, instead of `error`.
|
|
32
|
+
- Web: an answer's file ref outside the session's folder is dropped from the end-of-turn re-read of a running
|
|
33
|
+
session too, as a reload drops it, instead of showing until the reload.
|
|
34
|
+
- Plugins: a turn no longer fails with "can't add a new key into hash during iteration" when a plugin's
|
|
35
|
+
`replace_tools` registers a tool while a page reads the session's settings (no MCP server loaded).
|
|
36
|
+
- `call_tool` given the bare name of a tool the session declares (`read`) answers at once that it's declared and
|
|
37
|
+
should be called directly, instead of first waiting for every MCP server still starting.
|
|
38
|
+
- Web: a `/model`, `!command` or chip change still in flight when you open another session no longer acts on
|
|
39
|
+
that one: it no longer marks it as held by a terminal, puts the failure in its history or follows its worker,
|
|
40
|
+
and a card's action button no longer follows it either; an after-turn hook's answer re-read no longer lands in it.
|
|
41
|
+
- Web: a stop the server refuses (the stop button in the session bar) now says why in a toast instead of nothing.
|
|
42
|
+
- Web: a failure the server explains (a refused `/model`, send, archive, attach…) shows its words alone, without an
|
|
43
|
+
HTTP status after them (`a terminal chi holds it`, not `… (409)`); the status shows only when the server gave no words.
|
|
44
|
+
- Web: opening another session right as a finished turn slides into the history no longer shows that turn in the
|
|
45
|
+
new session's history, and its "Loading…" line stays until the session is drawn.
|
|
46
|
+
- Web: opening a chip's popover (llm ctx, thinking, reasoning history, attached context) closes the one already
|
|
47
|
+
open, instead of leaving both open.
|
|
48
|
+
- The empty-answer retry note asks the model to continue the task (its next tool call) or answer if it is done,
|
|
49
|
+
instead of "answer the user's last message now, briefly", which made a mid-task model stop before its last steps.
|
|
50
|
+
|
|
51
|
+
Update with `chi update` (the system bundle 0.72.0, coordinator 0.5.0, claude-peer 0.4.1, codex-peer 0.3.1), then
|
|
52
|
+
restart `chi web`.
|
|
53
|
+
|
|
11
54
|
## [0.71.0] - 2026-10-15
|
|
12
55
|
|
|
13
56
|
### Changed
|
|
@@ -3066,7 +3109,8 @@ and long-lived sessions.
|
|
|
3066
3109
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3067
3110
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3068
3111
|
|
|
3069
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3112
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.72.0...HEAD
|
|
3113
|
+
[0.72.0]: https://github.com/dm1try/samagotchi/compare/v0.71.0...v0.72.0
|
|
3070
3114
|
[0.71.0]: https://github.com/dm1try/samagotchi/compare/v0.70.0...v0.71.0
|
|
3071
3115
|
[0.70.0]: https://github.com/dm1try/samagotchi/compare/v0.69.0...v0.70.0
|
|
3072
3116
|
[0.69.0]: https://github.com/dm1try/samagotchi/compare/v0.68.0...v0.69.0
|
data/docs/architecture.md
CHANGED
|
@@ -44,7 +44,7 @@ A compact visual overview of the current architecture, then the core API in pros
|
|
|
44
44
|
│ execute · read · edit · write · memory_read/write · web_fetch · │
|
|
45
45
|
│ task_create/get/list/stop/wait · delegate · delegate_result · ask_user_question │
|
|
46
46
|
│ register_reminder/cancel_reminder/list_reminders · send_note · list_sessions · │
|
|
47
|
-
│ context_read · forget_outputs (+ bundle plugin tools)
|
|
47
|
+
│ context_read · forget_outputs · find_tools · call_tool (+ bundle plugin tools) │
|
|
48
48
|
│ offered by Tools::Registry (Builtins + bundles), filtered by the toolkit │
|
|
49
49
|
└──────────────────────────────────────────────────────────────────────────────────┘
|
|
50
50
|
│
|
|
@@ -101,7 +101,7 @@ terminal renders the events it gets over the Bridge.
|
|
|
101
101
|
| UI | `Samagotchi::TerminalUI` | REPL (Reline), rendering, REPL commands. Delegates all core work to an `Engine`. |
|
|
102
102
|
| Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt, or a chat API (OpenAI chat, Anthropic Messages), chosen per host (see below). |
|
|
103
103
|
| Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, Anthropic's Messages API, and the HTTP they share. |
|
|
104
|
-
| Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, plus runtime/output-guardrails. |
|
|
104
|
+
| Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, find_tools/call_tool (the tool index), plus runtime/output-guardrails. |
|
|
105
105
|
| Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
|
|
106
106
|
| Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. The spawned process (own process group) forks the worker, which `setsid`s and is reparented to init once its starter exits (it waits until the worker holds `owner.lock`, so the spawner's `Process.detach` waiter tells a starting worker): nothing that walks the spawner's process tree reaches it (`SAMAGOTCHI_WORKER_DETACH=1`, `detach_from_spawner`). The lock's pid is the worker's; `spawn`'s `child_pid` is its starter's. |
|
|
107
107
|
| Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick on one port, `127.0.0.1:4567` (index.html + `/api/*` + SSE); with `web.host: lan` also on this machine's LAN address, behind an access token (`Web::Lan`, `Web::Token`). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
|
|
@@ -208,7 +208,8 @@ engine.session # current session (Engine owns create/resume)
|
|
|
208
208
|
The system prompt (`SystemPrompt#build`, assembled in `#system_prompt_with_index`) is the
|
|
209
209
|
base prompt, then the toolkit's "Not loaded in this session" line (only when the toolkit
|
|
210
210
|
leaves tools out), rg guidance, the identity, the model notes, the preloaded memories,
|
|
211
|
-
AGENTS.md (root → cwd), the appended files (`--append-system-prompt`), the
|
|
211
|
+
AGENTS.md (root → cwd), the appended files (`--append-system-prompt`), the toolkit's
|
|
212
|
+
kit prompt (a kit's `prompt:` text, only when it has one), the memory
|
|
212
213
|
indexes, and last **the model** (`Model: this session runs on <ref> (host …; model key …)` and a line telling
|
|
213
214
|
the model to answer "which model are you" from it, not from training), the working
|
|
214
215
|
directory and the session id and log (Gemma's native tool declarations still follow).
|
|
@@ -307,6 +308,7 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
307
308
|
the messages itself, then streams from its `event_seq`). Read by the web's full show
|
|
308
309
|
(`GET /api/sessions/:id`, `?parts=1`) and the plugin's `list_sessions`; every message is copied.
|
|
309
310
|
- `GET /session/:id/tail` — the page's light re-read (`Bridge#tail_frame`): `session_state_snapshot`,
|
|
311
|
+
`working_directory` (the session's folder, which the web checks the answer's file refs against),
|
|
310
312
|
`answer` (the last message a UI shows as an answer, `AnswerTail`: the one message, copied alone),
|
|
311
313
|
`cards` (`CardStore#list`; a card an earlier worker showed during a turn comes back read-only, with no
|
|
312
314
|
actions: its turn no longer runs), `event_seq`, `event_id`, all taken in one event-log hold; no message
|
|
@@ -339,7 +341,9 @@ The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
|
|
339
341
|
finishes the ended turn's own line by its `turn_id` (sent as `&turn_id=`, which also picks that
|
|
340
342
|
turn's answer). A tail read's timing is always the newest turn's; `recent=1` is accepted and not needed.
|
|
341
343
|
The page binds an answer's `display_refs` to its bubble (`answer_refs.js`: on a reload's render and on this
|
|
342
|
-
read's re-render) and pairs them with the bubble's links and text before the `path:line` scan.
|
|
344
|
+
read's re-render) and pairs them with the bubble's links and text before the `path:line` scan. A file ref outside the
|
|
345
|
+
session's folder is dropped, as a reload drops it: the folder `/tail` names, else (an older worker) the hub's
|
|
346
|
+
session list entry's.
|
|
343
347
|
A plugin tool's row refs (`refs`, `RowRefs`) ride its tool events and, on a reload, its part (saved as
|
|
344
348
|
`tool_refs`); `turn_view.js` binds them to the row's title (`refs.js bindRowRefs`: the first on the title,
|
|
345
349
|
the others in its menu).
|
|
@@ -490,7 +494,7 @@ server (`/props`), then the host's model list, then config.
|
|
|
490
494
|
**Keys.** A host's API key comes from the environment variable its
|
|
491
495
|
`api_key_env:` names, else, on macOS, the login Keychain's item for that name (`HostKeys`, read through
|
|
492
496
|
`/usr/bin/security` with a deadline; hits cached per process; `SAMAGOTCHI_KEYCHAIN=off` skips it); it never
|
|
493
|
-
reaches config.yml, `
|
|
497
|
+
reaches config.yml, `chi self`, logs or events.
|
|
494
498
|
|
|
495
499
|
## Other components
|
|
496
500
|
|
|
@@ -504,7 +508,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
504
508
|
| 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)). |
|
|
505
509
|
| Peer bundles' base | `plugin/peer.rb` (`Plugin::Peer`) | What claude-peer and codex-peer share (the send tool, the command, the cards, payloads); each bundle's subclass sets its strings and transport ([plugins.md](plugins.md#the-claude-peer-bundle)). |
|
|
506
510
|
| Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
|
|
507
|
-
| Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb`, `hook_routes.rb` (`HookRoutes`) | Hook registration and dispatch; what a hook or plugin does beyond reading its event (notice, question, stop, steer) routed to the UIs ([hooks.md](hooks.md)). |
|
|
511
|
+
| Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb`, `hook_routes.rb` (`HookRoutes`), `event_routes.rb` (`EventRoutes`) | Hook registration and dispatch; what a hook or plugin does beyond reading its event (notice, question, stop, steer) routed to the UIs (cards and notices through one set of routes) ([hooks.md](hooks.md)). |
|
|
508
512
|
| Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/rules.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing, YAML rules, approvals ([guardrails.md](guardrails.md)). |
|
|
509
513
|
| Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `relay_desk.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
|
|
510
514
|
| send / broadcast | `send_command.rb` (`SendCommand`), `broadcast_command.rb` (`BroadcastCommand`), `broadcast/` | `chi send` and `chi broadcast` ([broadcast.md](broadcast.md), [sub-agent.md](sub-agent.md)). |
|
|
@@ -514,11 +518,13 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
514
518
|
| Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. |
|
|
515
519
|
| Analytics | `analytics_file.rb` (`AnalyticsFile`) | A session's `analytics.json` (`SessionMetrics#persist`). |
|
|
516
520
|
| Images / vision | `image_store.rb` (`ImageStore`), `vision_support.rb`, `vision_context.rb` | Stored prompt images and whether a model takes them. |
|
|
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)). |
|
|
521
|
+
| 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, a kit's prompt text after the appended files; `chi.toolkit` and `ctx.use_toolkit` for plugins ([configuration.md](configuration.md#toolkits)). |
|
|
522
|
+
| Tool index | `tools/tool_index.rb` (`Tools::ToolIndex`) | `find_tools` and `call_tool`: the tools a session knows but doesn't declare one by one (an mcp bundle's servers through `chi.tool_source`, the built-ins its toolkit leaves out), searched and called by name ([plugins.md](plugins.md)). |
|
|
523
|
+
| Tool output cut | `tool_output_cut.rb` (`ToolOutputCut`), `tool_output_files.rb` (`ToolOutputFiles`) | A tool's output over `max_tool_output_chars`, cut by the tool's policy; an execute's whole output saved in the session's `tool_output/` folder ([internals/tool-guardrails.md](internals/tool-guardrails.md)). |
|
|
518
524
|
| 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)). |
|
|
519
525
|
| Prompt files | `prompt_files.rb` (`PromptFiles`) | The AGENTS.md chain (root → cwd) and the appended files the system prompt carries. |
|
|
520
526
|
| 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. |
|
|
527
|
+
| Session settings | `session_setting.rb` (`SessionSetting`), `session_model_settings.rb` (`SessionModelSettings`), `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. |
|
|
522
528
|
| Dialects | `llm/dialect.rb` (`LLM::Dialect`) | What an `api: anthropic` provider's `/v1/messages` does differently (`dialect: fireworks`) ([internals/anthropic.md](internals/anthropic.md)). |
|
|
523
529
|
| 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)). |
|
|
524
530
|
| Stores | `reminder_store.rb` (`ReminderStore`), `recap_store.rb` (`RecapStore`) | A session's reminders, and its idle recap (`recap.json`). |
|
data/docs/cli.md
CHANGED
|
@@ -459,7 +459,7 @@ request (with the cached tokens, the cache writes, the re-prefilled tokens and t
|
|
|
459
459
|
reports them),
|
|
460
460
|
the decode speed, generation latency, cancellations, retries (the network's),
|
|
461
461
|
thinking cuts (generations a plugin such as loop-guard cut) and output cap hits
|
|
462
|
-
(generations that ended at the provider's cap), context used and window (the used figure is counted against the smaller of the window and the session's LLM context budget, and says so: `context used: 16000 tokens (25.0% of the 64000 budget)`), the memory indexes the session's prompt holds ([Memory](memory.md#the-indexs-size)), the model notes it carried (`model notes: model_notes_deepseek (system, 612 chars)`, none without any; [Memory](memory.md#model-notes)), the prompt profile and the model the server says it ran, and the
|
|
462
|
+
(generations that ended at the provider's cap), context used and window (the used figure is counted against the smaller of the window and the session's LLM context budget, and says so: `context used: 16000 tokens (25.0% of the 64000 budget)`), the memory indexes the session's prompt holds ([Memory](memory.md#the-indexs-size)), the model notes it carried (`model notes: model_notes_deepseek (system, 612 chars)`, none without any; [Memory](memory.md#model-notes)), the toolkit's prompt text (`kit prompt: coding (~20 tokens)`, only when its kit has one; [Toolkits](configuration.md#toolkits)), the prompt profile and the model the server says it ran, and the
|
|
463
463
|
cost when the provider reports one (OpenRouter's `usage.cost`), and the cost
|
|
464
464
|
estimated from a price under `hosts.<name>.models` when it reports none or 0,
|
|
465
465
|
marked `~` and naming the current model's host (`cost: $0.42 reported + ~$0.12 from hosts.gw.models prices (this session only)`;
|
|
@@ -520,7 +520,8 @@ line, the web as the `reasoning` chip. See "Reasoning history" in
|
|
|
520
520
|
tokens), the session's toolkit and the tools it leaves out: `reachable
|
|
521
521
|
(find_tools): …` (built-ins the model reaches with find_tools and call_tool)
|
|
522
522
|
and `removed: …` (only `/toolkit add` brings one back), or `not loaded: …` when
|
|
523
|
-
the toolkit removes call_tool
|
|
523
|
+
the toolkit removes call_tool, and its kit's prompt text when it has one (`prompt: <first line> (+N lines, ~T
|
|
524
|
+
tokens)`). `/toolkit remove TOOL` is hard: a tool the
|
|
524
525
|
toolkit left out reachable becomes unreachable (saved, and asked first once the
|
|
525
526
|
session has a model reply), also after an add. `/toolkit NAME` puts
|
|
526
527
|
the session on another toolkit (dropping its own adds and removes); `/toolkit
|
data/docs/configuration.md
CHANGED
|
@@ -107,9 +107,8 @@ Behavior:
|
|
|
107
107
|
edit at once). A worker gets the CLI flags of the chi that started it
|
|
108
108
|
through its environment.
|
|
109
109
|
- A worker reads `hosts:` from the file as it starts, as every other setting, so a delegate (or any session
|
|
110
|
-
started later) sees a host added after its parent started.
|
|
111
|
-
|
|
112
|
-
it (`SessionManager.spawn_options` unsets it).
|
|
110
|
+
started later) sees a host added after its parent started. Hosts come only from config.yml: no environment
|
|
111
|
+
variable sets them.
|
|
113
112
|
|
|
114
113
|
This lets you run `chi` without repeating common defaults such as model
|
|
115
114
|
and llama host/port on every invocation.
|
|
@@ -1330,6 +1329,7 @@ toolkit:
|
|
|
1330
1329
|
child: child # delegate children (unset: full)
|
|
1331
1330
|
kits: # your own, on a base
|
|
1332
1331
|
review: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
|
|
1332
|
+
terse: { base: lean, prompt: "- Be concise in your responses" }
|
|
1333
1333
|
```
|
|
1334
1334
|
|
|
1335
1335
|
| toolkit | declares |
|
|
@@ -1338,15 +1338,25 @@ toolkit:
|
|
|
1338
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` |
|
|
1339
1339
|
| `child` | `lean` plus `send_note`, without `memory_write`, `delegate`, `delegate_result` |
|
|
1340
1340
|
| `coordinator` | `lean` plus `delegate`, `delegate_result`, `list_sessions` (declared by the coordinator bundle) |
|
|
1341
|
+
| `coding` | `child`, with the prompt text `- Be concise in your responses` / `- Show file paths clearly when working with files` (declared by the coordinator bundle, for build and fix children) |
|
|
1341
1342
|
|
|
1342
1343
|
- **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
|
|
1343
|
-
the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:` and `
|
|
1344
|
-
or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name. A bundle declares its own with `chi.toolkit` ([plugins](plugins.md#chitoolkitname-base-add--remove-)).
|
|
1344
|
+
the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:` and `prompt:` (any other
|
|
1345
|
+
key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name. A bundle declares its own with `chi.toolkit` ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil)).
|
|
1346
|
+
- **A kit's prompt text.** `prompt:` (text, several lines allowed) goes into the system prompt of the sessions on
|
|
1347
|
+
that kit, verbatim, with no header: after AGENTS.md and the appended files, before the memory indexes. A kit
|
|
1348
|
+
without one inherits its base's (`review: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
|
|
1349
|
+
it; a session's own `--tool` / `/toolkit add` never touch it. Sessions on the same kit share it in the prompt
|
|
1350
|
+
cache; `full` and every kit without a prompt send the bytes they sent before. Keep it to a line or two that
|
|
1351
|
+
fits the kit's work (the coordinator bundle's `coding`: be concise): one that isn't text or is over 2000
|
|
1352
|
+
characters is left out with a warning, and long text belongs in `append_system_prompt:`. `/toolkit` shows it
|
|
1353
|
+
(`prompt: …`), `/stats` its kit and size (`kit prompt:`), the web's tools chip in its tooltip.
|
|
1345
1354
|
- **Saved at creation, never re-resolved from config.** A session stores its toolkit as named
|
|
1346
1355
|
(`"toolkit": {"name": "lean", "add": [], "remove": []}`) when it is created: `toolkit.default`, `toolkit.child`
|
|
1347
|
-
for a delegate child, or `chi --toolkit NAME` / `--tool` / `--no-tool` ([cli](cli.md)). Changing `toolkit.default`
|
|
1356
|
+
for a delegate child (or the kit its `delegate` call names), or `chi --toolkit NAME` / `--tool` / `--no-tool` ([cli](cli.md)). Changing `toolkit.default`
|
|
1348
1357
|
doesn't move existing sessions, not even when their worker is woken again; a session from before toolkits is on
|
|
1349
|
-
`full`. A continue and a fork keep the toolkit. `/toolkit` shows and changes it.
|
|
1358
|
+
`full`. A continue and a fork keep the toolkit. `/toolkit` shows and changes it. The prompt text is part of the
|
|
1359
|
+
kit, not of the session file: it follows the name.
|
|
1350
1360
|
- **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
|
|
1351
1361
|
`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
1362
|
the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
|
|
@@ -1368,13 +1378,13 @@ toolkit:
|
|
|
1368
1378
|
loaded in this session (toolkit child) and can't be loaded from here; only the user can (/toolkit add
|
|
1369
1379
|
memory_write).` `/toolkit` lists both kinds, and the web's toolkit chip says them in its tooltip.
|
|
1370
1380
|
- **Applied at a turn's start.** A change (`/toolkit`, a plugin's `ctx.use_toolkit`) is saved at once and the next
|
|
1371
|
-
turn declares it; once the session has a model reply, `/toolkit` asks first
|
|
1372
|
-
cache").
|
|
1381
|
+
turn declares it, with its kit's prompt text; once the session has a model reply, `/toolkit` asks first
|
|
1382
|
+
("Changes that restart the prompt cache"), also for a kit with the same tools and another prompt text.
|
|
1373
1383
|
- **A name that doesn't resolve** (a typo, a bundle that was uninstalled) declares every tool for that process,
|
|
1374
1384
|
with one `toolkit>` warning at the first turn; the saved name stays, so the bundle's return brings it back.
|
|
1375
|
-
- **Versions.** A toolkit's definition can change with chi or a bundle, which
|
|
1376
|
-
|
|
1377
|
-
an edit to `toolkit.kits`: a session's worker reads the kits once when it starts, so a running session keeps its
|
|
1385
|
+
- **Versions.** A toolkit's definition (its tools and its prompt text) can change with chi or a bundle, which
|
|
1386
|
+
changes what saved sessions on it send after an upgrade: the next request reads the prompt again once, as a
|
|
1387
|
+
tool's schema change does. So can an edit to `toolkit.kits`: a session's worker reads the kits once when it starts, so a running session keeps its
|
|
1378
1388
|
tools and the edit reaches it at its next worker start (after an idle exit, `chi sessions restart`).
|
|
1379
1389
|
|
|
1380
1390
|
## LLM context: the forget layer
|
|
@@ -1586,7 +1596,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
1586
1596
|
| `context.wake` | `true` | yes | A source whose update says `wake: true` may start a turn in a live, idle session (one per source per 10 minutes, a peer source up to `context.peer_turns` in a row, within `session.max_wakes`); so may a session's pushed source's first push. `false`: updates wait as notes for the next turn. See [Waking](context.md#waking). |
|
|
1587
1597
|
| `context.peer_turns` | `3` | yes | Wake turns in a row a peer agent's answers may start (a peer bundle's outbox source, such as `codex-outbox`), with no message typed in the web or a TUI in between (`chi send` doesn't count); within `session.max_wakes`. Then answers wait as notes, and the UIs hear it once. An answer whose summary ends its verdicts with `VERDICT OK` wakes once to close the round, counted toward `session.max_wakes` only. `0`: a peer source wakes like any other. See [Waking](context.md#waking). |
|
|
1588
1598
|
| `toolkit.default` | `full` | | The toolkit a new session is stored on: the tools its prompts declare (`full`, `lean`, `child`, one of `toolkit.kits` or a bundle's). `chi --toolkit` picks one per session. See "Toolkits". |
|
|
1589
|
-
| `toolkit.child` | `full` | | A delegate child's toolkit. See "Toolkits". |
|
|
1599
|
+
| `toolkit.child` | `full` | | A delegate child's toolkit, unless its `delegate` call names one (`toolkit:`). See "Toolkits". |
|
|
1590
1600
|
| `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
|
|
1591
1601
|
| `reasoning_history` | `turn` | | Which of its reasoning a chat host's model gets back: `drop`, `turn`, `all`; environment only (`SAMAGOTCHI_REASONING_HISTORY`), below a session's own level (`/reasoning-history`), above the `models:`/`hosts:` entries. See "Reasoning history". |
|
|
1592
1602
|
| `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't; a session's own level outranks them all, and `--thinking` sets it on a session run (on `chi web` it sets this). See "Thinking". |
|
data/docs/hooks.md
CHANGED
|
@@ -526,7 +526,7 @@ stripped of surrounding whitespace, except file text, an edit's
|
|
|
526
526
|
| `list_sessions` | — | — | `cwd` |
|
|
527
527
|
| `send_note` | `text` | — | `session` |
|
|
528
528
|
| `context_read` | `name` | — | `offset`, `limit` |
|
|
529
|
-
| `delegate` | `task` | — | `model`, `session`, `cwd`, `wait`, `timeout` |
|
|
529
|
+
| `delegate` | `task` | — | `model`, `session`, `cwd`, `toolkit`, `wait`, `timeout` |
|
|
530
530
|
| `delegate_result` | — | — | `session`, `timeout` |
|
|
531
531
|
| `ask_user_question` | `question` | — | `question`, `options`, `header`, `multi_select`, `allow_freeform` |
|
|
532
532
|
| `forget_outputs` | `note` | — | `ids`, `note`, `keep`, `restore` |
|
data/docs/internals/anthropic.md
CHANGED
|
@@ -82,7 +82,7 @@ index: `message_start` (served model, usage), `content_block_start` (text, think
|
|
|
82
82
|
`content_block_delta` (`text_delta`, `thinking_delta`, `signature_delta`, `input_json_delta`), `message_delta`
|
|
83
83
|
(stop reason, `stop_details`, usage). `AnthropicMessages#chat_streamed` passes every event but `ping` to `on_delta`,
|
|
84
84
|
`message_start` and `message_delta` with the usage so far attached (`#with_usage`); a block's start or delta counts as shown for the first-token limit
|
|
85
|
-
(`.shows?`). A 200 whose body is a JSON error instead of a stream raises that error (
|
|
85
|
+
(`.shows?`). A 200 whose body is a JSON error instead of a stream raises that error (`LLM::AdapterCommon#raise_plain_error`, shared with the OpenAI adapter).
|
|
86
86
|
|
|
87
87
|
`#response` is a `ChatResponse`:
|
|
88
88
|
|
data/docs/internals/engine.md
CHANGED
|
@@ -122,7 +122,7 @@ A worker's TERM, HUP or INT (WorkerSignals, trapped by a spawned worker only):
|
|
|
122
122
|
→ run_session_loop: exit 0, no discard; input queued behind the stopped turn wakes a new worker
|
|
123
123
|
|
|
124
124
|
The effective model doesn't resolve (a broken host config, HostRegistry#resolve raising):
|
|
125
|
-
→ the rescued readers give their fallback (
|
|
125
|
+
→ the rescued readers give their fallback (SessionModelSettings#model_setting) and log effective_model_unresolved;
|
|
126
126
|
a setting that fails on a resolved model gives its fallback and logs <setting>_unresolved
|
|
127
127
|
(sampling, thinking, reasoning_history, steer_cut_after, llm_context, window, price, served,
|
|
128
128
|
vision_adapter); served_probe_failed when asking the server for its served model fails;
|
|
@@ -226,7 +226,7 @@ The differences from the native loop:
|
|
|
226
226
|
merges (a person's line, a plugin's steer, a Continue's text) don't move it, as natively (nothing re-sanitizes
|
|
227
227
|
within a run): the request after a steer extends the one before. A step-limit Continue's Run starts where the run
|
|
228
228
|
it resumes did: `#complete`'s `reasoning_since:` (the Engine's `resume_from`, checked against the messages' length
|
|
229
|
-
in
|
|
229
|
+
in `Engine#resumed_run_start`), past a user entry added after that run's last reply (a `!cmd`'s output). Without
|
|
230
230
|
`since:` (`#reasoning_chars` from outside a Run) the boundary is the last query (a `role: "user"` or `turn_start`
|
|
231
231
|
entry). `Run#prompt_chars` counts it (`#reasoning_chars`).
|
|
232
232
|
- On an adapter that sends its blocks back (`#replays_content?`: `AnthropicMessages`), a model turn's saved blocks
|
|
@@ -312,10 +312,17 @@ One iteration with tool calls:
|
|
|
312
312
|
[Tool output guardrails](tool-guardrails.md#the-runners-cut-max_tool_output_chars). The model (both loops), the
|
|
313
313
|
event and the after_tool_call hook get that text.
|
|
314
314
|
- Emits `:tool_call_completed` {iteration, call_count, call_index, tool, output, output_truncated, output_file,
|
|
315
|
-
activity, images, diff, waited_ms, view, ref}, then fires `:after_tool_call` {iteration, tool, output,
|
|
315
|
+
activity, images, diff, waited_ms, view, ref, refs}, then fires `:after_tool_call` {iteration, tool, output,
|
|
316
|
+
status, call, elapsed_ms, result}.
|
|
316
317
|
`output_file` (the saved whole output) is also on the run hash; the session doesn't keep it (the cut text names it).
|
|
317
|
-
- The activity's status (`ToolActivity#tool_activity_status
|
|
318
|
-
|
|
318
|
+
- The activity's status (`ToolActivity#tool_activity_status`; a call the gate denied is `blocked`, set by
|
|
319
|
+
`ToolRunner#denied`): `stopped` for a call the turn's Stop (or a signal to chi, or the session's stop) left
|
|
320
|
+
unrun (`Tools::Execute::NOT_RUN_ON_STOP`, `NOT_RUN`) or an execute it killed (`STOPPED_BY_USER`, `STOPPED_BY`),
|
|
321
|
+
and for a `task_wait` whose header (before `output_tail:`) says `wait_result: canceled` or `status: stopped`;
|
|
322
|
+
`ok` for an execute handed over as a task (`#handed_over?`: its output tail may hold any `exit: N`); `error` for
|
|
323
|
+
any other `Error:` result, for a `task_wait` whose header says `status: failed`, and for an execute that exited
|
|
324
|
+
non-zero (a numberless `exit:` line, killed by a signal, counts), except a grep-family search that found nothing
|
|
325
|
+
(`ToolActivity.no_match?`:
|
|
319
326
|
exit 1, no stderr block, the last step is grep/egrep/fgrep/rg/ag/pgrep, no `&&` after a step that can fail
|
|
320
327
|
silently or sends output to /dev/null, no background `&`, no earlier exit/return or `set -e`; when unsure,
|
|
321
328
|
not a no-match). That one stays `ok` with `no_match: true` on the activity, which SessionMetrics keeps on the tool
|
|
@@ -358,7 +365,7 @@ read live, a model listing can move a bare id to another host, and another proce
|
|
|
358
365
|
what a resolve gives, all without a `/model`. Frozen, so any thread may hold one.
|
|
359
366
|
|
|
360
367
|
- The Engine's readers take one (`Engine#effective_model`); those whose fallback always covered the resolve go
|
|
361
|
-
through
|
|
368
|
+
through `SessionModelSettings#model_setting` (`session_model_settings.rb`: the Engine forwards its readers of the session's settings there). A turn takes one in `turn_settings` (before the hooks) and one in `generate` (after
|
|
362
369
|
them); `system_prompt`, `steer_cut_after` (at `:turn_started`) and the turn-end warm-up take their own;
|
|
363
370
|
`stats_snapshot` and `session_state_snapshot` take one each (the latter nil-tolerant).
|
|
364
371
|
- Never resolved (they hold on a broken resolve): `effective_model_ref`, `bare_model_name`,
|
|
@@ -490,17 +497,22 @@ hook registry (`@hooks.fire`), not on the sink or the observers. The other hook
|
|
|
490
497
|
```
|
|
491
498
|
Engine initialization (in this order, among other things):
|
|
492
499
|
@turn_state, @steer_cut (SteerCut: its GenerationPhase and WaitingSteer)
|
|
493
|
-
@host_registry, @model_choice (ModelChoice: the name and the name as typed); @client = effective_model.client
|
|
500
|
+
@host_registry, @model_choice (ModelChoice: the name and the name as typed); @model_settings (SessionModelSettings: the choice and the session read at use); @client = effective_model.client
|
|
494
501
|
@given_profile = profile or nil (resolved on first need, so building an Engine makes no network call)
|
|
495
|
-
|
|
496
|
-
@
|
|
497
|
-
@
|
|
502
|
+
MemoryBundle::SystemBundle.ensure! (the shipped system bundle installed; a failure is logged, not raised)
|
|
503
|
+
@event_routes (EventRoutes: where a card or a notice goes: held while the plugins load, an anytime command's, an init task's, a turn event, else announced or held)
|
|
504
|
+
@hook_routes (HookRoutes: notices (through @event_routes), hook questions, stop_turn/stop_generation, steer attribution)
|
|
505
|
+
@guardrail_wiring, @question_desk, @relay_desk (RelayDesk: a delegate's approval relay)
|
|
498
506
|
@extension_load = ExtensionLoad.new; @hooks = @extension_load.hooks (config.yml's, then the bundles')
|
|
499
|
-
@tools = Tools::Builtins.registry;
|
|
507
|
+
@tools = Tools::Builtins.registry; @command_registry (SessionCommands' built-ins); @plugin_tasks
|
|
508
|
+
@toolkit_binding (ToolkitBinding: the kits, the plugins' among them, and the turn-start apply)
|
|
509
|
+
@thinking_notices (ThinkingNotices); @tool_index = Tools::ToolIndex.new; @peer_sources = PeerSources.new
|
|
510
|
+
the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins),
|
|
500
511
|
then (plugins or not) @tool_index.declare(@tools): find_tools + call_tool (source "index", not core),
|
|
501
512
|
declared per toolkit by Registry#declared? (a source registered at load, or a reachable built-in) and
|
|
502
513
|
sets call_tool's unwrap (Tools::ToolIndex: the chi group from Registry#reachable_names, the sources'
|
|
503
514
|
groups, search, call checks, the unwrap; a source runs the call with its own plugin Context)
|
|
515
|
+
@reminder_store = ReminderStore.new (the kernel gets it), @reminder_queue, @reminders
|
|
504
516
|
wire_kernel: @kernel = KernelLoop.new(client:, profile:, hooks:, reminder_store:, tools:)
|
|
505
517
|
@kernel.guardrail_gate = @guardrail_wiring.gate; @kernel.warmup = PromptWarmup.new
|
|
506
518
|
@hooks.runtime = @hook_routes.runtime (HookRoutes, built first: what a hook or plugin does reaches the UIs)
|
|
@@ -555,7 +567,9 @@ them across turns. `Web::App#messages_for_display` sends `display_refs` re-valid
|
|
|
555
567
|
- Order of a fire: bundle hooks (by priority, bundle, hook name), then config hooks, then turn-scoped hooks.
|
|
556
568
|
- A hook that raises is logged; it never breaks the turn.
|
|
557
569
|
- A notice from a hook goes to the running turn's sink and the observers. Outside a turn it is announced with
|
|
558
|
-
`between_turns: true`; while the plugins load it is held and announced after.
|
|
570
|
+
`between_turns: true`; while the plugins load it is held and announced after. `EventRoutes#route` decides it, the
|
|
571
|
+
same routes as a card's (`Engine#show_card`), with their own marks (`NOTICE_MARKS`, `CARD_MARKS`); a guardrails
|
|
572
|
+
line after load takes only the turn / between-turns routes (`#route_by_turn`).
|
|
559
573
|
- Plugin init tasks (`chi.init`, slow setup such as an MCP server's first start): a turn waits for the ones that
|
|
560
574
|
provide tools before it builds its messages, and a Stop or Ctrl-C ends the wait.
|
|
561
575
|
|
|
@@ -652,7 +666,8 @@ prompt starts with what the server has cached).
|
|
|
652
666
|
`[chat, thinking, layers]` (chat/native, the thinking level, the turn's LLM context layers), and is reset
|
|
653
667
|
only on a model switch, a tools change, a profile change or a toolkit change (`Engine#apply_toolkit!`). The
|
|
654
668
|
stable part (base prompt, the toolkit's not-loaded line, rg guidance, identity, model notes, preloaded memories,
|
|
655
|
-
AGENTS.md, appended files, memory indexes) comes before the volatile tail (model,
|
|
669
|
+
AGENTS.md, appended files, the toolkit's kit prompt, memory indexes) comes before the volatile tail (model,
|
|
670
|
+
working directory, session).
|
|
656
671
|
prompt-caching.md covers this in detail.
|
|
657
672
|
|
|
658
673
|
## Key files
|
|
@@ -667,6 +682,7 @@ Paths are under `lib/samagotchi/`.
|
|
|
667
682
|
| `generation_phase.rb`, `waiting_steer.rb` | Whether a steer may cut the running generation, and the steer that waits to |
|
|
668
683
|
| `effective_model.rb` | `EffectiveModel` (the effective model's settings, a snapshot per read; `.of_session`), `ModelChoice`, the fallbacks' log lines (`Unresolved`) |
|
|
669
684
|
| `session_setting.rb` | `SessionSetting`: the per-session settings' descriptors (own value, chips, the web's fallback) |
|
|
685
|
+
| `session_model_settings.rb` | `SessionModelSettings`: the session's settings on its effective model (its own thinking, reasoning_history and llm_context over the model's, the `/model` and `/stats` summaries, what a turn resolves, `#model_setting`'s fallbacks); the Engine forwards its readers there |
|
|
670
686
|
| `level_setting_command.rb`, `thinking_command.rb`, `reasoning_history_command.rb` | `/thinking` and `/reasoning-history` (`LevelSettingCommand` and its two Specs) |
|
|
671
687
|
| `thinking_notices.rb` | `ThinkingNotices`: the notices a turn's thinking level leads to |
|
|
672
688
|
| `steer_cut_after.rb`, `cut_policy.rb` | The model's cut wait (`SteerCutAfter.resolve`); the loops' cut path (`CutPolicy`) |
|
|
@@ -676,6 +692,7 @@ Paths are under `lib/samagotchi/`.
|
|
|
676
692
|
| `llm/adapters.rb` | `LLM::Adapters::BY_API`: which chat adapter a host's api gets |
|
|
677
693
|
| `llm/openai_chat.rb` | `LLM::OpenAIChat`, the `/v1/chat/completions` adapter |
|
|
678
694
|
| `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) |
|
|
695
|
+
| `llm/refusal_memo.rb` | `LLM::RefusalMemo`: what a host refused (for one model, or the whole host), remembered for its holder's life (an adapter, or the worker's ChatLoop) |
|
|
679
696
|
| `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)) |
|
|
680
697
|
| `llm/prompt_cache.rb` | Cache breakpoints on the OpenAI path, and the `cache_control` both adapters send |
|
|
681
698
|
| `llm/turn_settings.rb` | `LLM::TurnSettings`, what a turn runs under (vision, sampling, thinking, reasoning history, …) |
|
|
@@ -685,19 +702,27 @@ Paths are under `lib/samagotchi/`.
|
|
|
685
702
|
| `llm/native_backend.rb` | Wraps KernelLoop as a backend (`#complete` calls `#run`) |
|
|
686
703
|
| `llm/model_result.rb` | `LLM::ModelResult`, what a backend returns |
|
|
687
704
|
| `tool_runner.rb` | Per-call path (gate, start/complete events, output cap, images, diffs) |
|
|
705
|
+
| `tool_output_cut.rb`, `tool_output_files.rb` | `ToolOutputCut`: how a tool's output longer than `max_tool_output_chars` is cut, by the tool's policy (`POLICIES`; `:head` for a tool with none); `ToolOutputFiles`: the whole output of an execute cut that way, saved in the session's `tool_output/` folder ([tool-guardrails.md](tool-guardrails.md)) |
|
|
706
|
+
| `tool_row_fields.rb`, `row_refs.rb` | `ToolRowFields`: a tool row's fields beyond its params (`ACTIVITY_KEYS`, `EVENT_KEYS`), the same live and on a reload; `RowRefs`: a plugin tool's row refs (`refs:`), checked, capped and saved with the result |
|
|
688
707
|
| `tool_response.rb` | Per-batch dispatch and tool_response entries (joined for native, single for chat) |
|
|
689
708
|
| `empty_answer_retry.rb` | The empty-answer and cut retry budget |
|
|
690
709
|
| `prompt_warmup.rb` | The turn-end warm-up |
|
|
691
710
|
| `tools/registry.rb` | The tools a session offers: schemas, handlers, order |
|
|
711
|
+
| `tools/tool_index.rb` | `Tools::ToolIndex`: `find_tools` and `call_tool`, the tools a session knows but doesn't declare one by one (the sources' groups and the built-ins its toolkit leaves out), `#declare`, `#refresh!`, `#unwrap` |
|
|
712
|
+
| `plugin/tool_spec.rb` | `Plugin::ToolSpec`: a checked tool declaration (`chi.tool`, a tool set, `ToolSpec.build`), what `Registry#register` gets; `#per_call` |
|
|
692
713
|
| `system_prompt.rb` | System prompt construction (stable part, volatile tail) |
|
|
693
714
|
| `prompt_files.rb`, `model_notes.rb` | The AGENTS.md chain and appended files; the model notes the prompt loads |
|
|
694
715
|
| `toolkit.rb`, `toolkit_command.rb` | A session's toolkit (`Toolkit#resolve`, what `Registry` declares); `/toolkit` |
|
|
695
716
|
| `toolkit_binding.rb` | `ToolkitBinding`: the Engine's toolkit seam (the kits, `#explained`, `ctx.use_toolkit`, the turn-start `#apply!` behind `Engine#apply_toolkit!`) |
|
|
696
717
|
| `hooks/registry.rb` | Hook registry, fire order, `Hooks::Runtime` |
|
|
697
718
|
| `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 |
|
|
719
|
+
| `event_routes.rb` | `EventRoutes`: where a card (`Engine#show_card`), a notice (`HookRoutes#notify`) and a guardrails line (`#guardrail_notify`, turn or between turns only) go: held while the plugins load, an anytime command's, an init task's, a running turn's event, else announced or held for `holding_announcements` |
|
|
720
|
+
| `engine_peers.rb`, `peer_sources.rb` | `Engine::PeerView` (what the kernel's peer tools see of the Engine) and `Engine::RelayPeer` (a delegate's approval relay); `PeerSources`: the attached context sources a peer bundle declares as its peer's answers (`chi.peer_source`) |
|
|
698
721
|
| `extension_load.rb` | What an Engine loads at start: config.yml and bundle hooks, plugins, `bundles:` settings; events held while plugins load |
|
|
699
722
|
| `guardrail_wiring.rb`, `guardrails/gate.rb` | The gate's context and approvals; the verdict per call |
|
|
700
723
|
| `events.rb` | Event type sets shared by the Worker, the Bridge, the TUI and the web |
|
|
701
724
|
| `cancellation_controller.rb` | A turn's cancel signal, and a generation's child controller |
|
|
702
725
|
| `session.rb` | Session model (status, messages, persistence) |
|
|
726
|
+
| `turn_checkpoint.rb` | `TurnCheckpoint`: a worker's running turn saved at the loop's iteration boundaries (`Session#turn_checkpoint`), so a killed worker loses at most `INTERVAL` of it |
|
|
727
|
+
| `context_status.rb` | `ContextStatus`: one turn's context tracking (the status line's value, the `:context_status` event, the model's guidance line) ([context-telemetry.md](context-telemetry.md)) |
|
|
703
728
|
| `session_manager.rb` | Background session processes (workers) and their IPC |
|
|
@@ -51,7 +51,7 @@ Each task × arm × sample is a run in `ROOT/runs/<task>-<arm>-s<n>/`:
|
|
|
51
51
|
in `config/` (the model's host copied from `--hosts-config`, default the user's config.yml; `turn.max_iterations:
|
|
52
52
|
150`; `recap: false`; `llm_context.apply: payoff`, no `stale_edits`). State goes in `state/`, and it runs `chi
|
|
53
53
|
bundle install loop-guard`. Every chi and rspec command gets `SAMAGOTCHI_ENV=test`, the run's `XDG_STATE_HOME`
|
|
54
|
-
and `XDG_CONFIG_HOME
|
|
54
|
+
and `XDG_CONFIG_HOME` (the smoke-run isolation).
|
|
55
55
|
2. **run (Driver):** `chi send --new --wait --format json --dir repo --model M --llm-context <arm>
|
|
56
56
|
--llm-context-budget <N|off>` with the first turn, then each later turn into the same session. The arms are
|
|
57
57
|
`none`, `stale` and `stale_forget` (`stale,forget`). The tasks use two turns, because forget is offered at turn
|
|
@@ -39,6 +39,7 @@ right-hand column happens.
|
|
|
39
39
|
│ explicit (preloaded) memories │
|
|
40
40
|
│ AGENTS.md (root → cwd) │ (picked up by a new
|
|
41
41
|
│ appended files (append_system_prompt:, --append-system-prompt) │ process or a rebuild)
|
|
42
|
+
│ kit prompt (per toolkit: its kit's prompt: text, only when it has one) │
|
|
42
43
|
│ project + system memory indexes │
|
|
43
44
|
│ ◄── remote Claude / Messages: cache_control breakpoint #1 │
|
|
44
45
|
├──────────────────────────────────────────────────────────────────────────┤
|
|
@@ -71,6 +72,11 @@ right-hand column happens.
|
|
|
71
72
|
beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request, so a mid-turn change
|
|
72
73
|
would differ from the turn's prompt. Under `full` nothing is filtered and no line is added: the bytes are what
|
|
73
74
|
they were before toolkits.
|
|
75
|
+
- A kit's prompt text (`toolkit.kits.<name>.prompt:`, `chi.toolkit … prompt:`) sits after the appended files and
|
|
76
|
+
before the memory indexes (`SystemPrompt#kit_prompt_section`): one prefix per kit, never per-session text, so
|
|
77
|
+
sessions on the kit share it; a memory write (the indexes after it) doesn't move it. It is part of the
|
|
78
|
+
`Toolkit::Resolved` the Registry holds, so a switch that changes only the text is a toolkit change
|
|
79
|
+
(`apply_toolkit!` → `reset!`). `full` and kits without one send the bytes they sent before.
|
|
74
80
|
- find_tools' description is a function of the toolkit and the source groups **at load**: `ToolIndex#declare`
|
|
75
81
|
snapshots the MCP groups' lines (name, summary, callable count) and `ToolIndex#refresh!` (from
|
|
76
82
|
`ToolkitBinding#apply!`) rebuilds only the `chi` line, the built-ins the toolkit reaches. An MCP server that
|
data/docs/plugins.md
CHANGED
|
@@ -568,19 +568,25 @@ end
|
|
|
568
568
|
- Each task runs once per session start. A `-p … --non-interactive` run
|
|
569
569
|
starts them with its turn and shows nothing but the answer.
|
|
570
570
|
|
|
571
|
-
### `chi.toolkit(name, base:, add: [], remove: [])`
|
|
571
|
+
### `chi.toolkit(name, base:, add: [], remove: [], prompt: nil)`
|
|
572
572
|
|
|
573
573
|
A toolkit sessions can be put on: `base`'s tools (`full`, `lean`, `child`, a
|
|
574
|
-
config or bundle toolkit) with `add` declared and `remove` left out
|
|
574
|
+
config or bundle toolkit) with `add` declared and `remove` left out, and
|
|
575
|
+
`prompt`: text the sessions on it get in their system prompt, verbatim (nil
|
|
576
|
+
inherits the base's, `""` clears it; at most 2000 characters, and one that
|
|
577
|
+
isn't a String or is longer is a load error). Users pick
|
|
575
578
|
it like chi's own (`chi --toolkit NAME`, `toolkit.default`, `/toolkit NAME`),
|
|
576
579
|
and the plugin with `ctx.use_toolkit` (below). Names are `a-z`, `0-9`, `_` and
|
|
577
580
|
`-`; one chi has (`full`, `lean`, `child`) or another bundle declared is a
|
|
578
581
|
load error. A session saved on a toolkit whose bundle is gone declares every
|
|
579
582
|
tool (full) until it comes back, with a warning at its first turn. See
|
|
580
|
-
[Toolkits](configuration.md#toolkits).
|
|
583
|
+
[Toolkits](configuration.md#toolkits). `prompt:` needs chi 0.72.0 (an older
|
|
584
|
+
chi refuses the keyword, a load error: a bundle that passes it sets
|
|
585
|
+
`requires_chi: ">= 0.72.0"`).
|
|
581
586
|
|
|
582
587
|
```ruby
|
|
583
588
|
chi.toolkit "review", base: "lean", add: %w[delegate delegate_result]
|
|
589
|
+
chi.toolkit "coding", base: "child", prompt: "- Be concise in your responses"
|
|
584
590
|
```
|
|
585
591
|
|
|
586
592
|
### `chi.tool_source(call:, acts_as:, label:, wait:, prefix:)`
|
|
@@ -645,9 +651,10 @@ index = Samagotchi::Tools::ToolIndex
|
|
|
645
651
|
([acts_as:](#guardrails)), even when another bundle has a tool of that
|
|
646
652
|
name (only the core-tool drop applies). A call made before its tool is published is
|
|
647
653
|
named by `acts_as` from the names alone; a bare name with `prefix` is
|
|
648
|
-
taken as an `acts_as` as given
|
|
654
|
+
taken as an `acts_as` as given (without `prefix`, a bare name is refused:
|
|
655
|
+
call it as `<group>/<tool>`). The `acts_as:` hook must give exactly the
|
|
649
656
|
`acts_as` the source publishes for that tool: a call whose tool isn't the
|
|
650
|
-
one guardrails were given is refused ("the
|
|
657
|
+
one guardrails were given is refused ("the tool list changed while the call
|
|
651
658
|
waited"), so a hook that disagrees refuses every call.
|
|
652
659
|
- **The row** shows `label` (default: the bundle's name) and
|
|
653
660
|
`<group>/<tool> args…`. `@source.declared_names` and `declared_tokens` say
|
|
@@ -1672,6 +1679,11 @@ by themselves there.
|
|
|
1672
1679
|
or a turn already there) and the session's toolkit has no `delegate`, the
|
|
1673
1680
|
reply says to run `/toolkit coordinator`. A continue of a coordinator link
|
|
1674
1681
|
keeps the toolkit.
|
|
1682
|
+
- **The coding toolkit.** The bundle (0.5.0) also declares `coding`: `child`
|
|
1683
|
+
with the prompt text `- Be concise in your responses` / `- Show file paths
|
|
1684
|
+
clearly when working with files`. The skill starts build and fix children
|
|
1685
|
+
on it (`delegate` with `toolkit: "coding"`); reviews, research and plans
|
|
1686
|
+
stay on `toolkit.child`, as they need the length.
|
|
1675
1687
|
- **`/children [all]`** shows the session's children as a card, newest first,
|
|
1676
1688
|
one line each: `` `ab12cd34` · running · fix/flaky · "fix the flaky spec" ``,
|
|
1677
1689
|
`` `9a8b7c6d` · done · feat/x · reported · "All 12 specs pass" `` (`not
|
|
@@ -1761,8 +1773,7 @@ Claude pairs; without one the bundle costs one tool declaration.
|
|
|
1761
1773
|
- In the web, selecting text offers **Confirm with Claude**
|
|
1762
1774
|
([`chi.annotation`](#chiannotationlabel-command-template-quote-send-false)):
|
|
1763
1775
|
it fills the composer with `/claude validate` and the quote, for you to
|
|
1764
|
-
add what needs confirming.
|
|
1765
|
-
the bundle loads without the pill.
|
|
1776
|
+
add what needs confirming.
|
|
1766
1777
|
- `/claude last` sends the model's last completed answer as context. Mid-turn
|
|
1767
1778
|
it is the answer before the running turn, never the turn's partial text;
|
|
1768
1779
|
a cancelled or failed turn has no answer, so the one before it goes.
|
data/docs/sessions.md
CHANGED
|
@@ -226,7 +226,9 @@ A session file's `toolkit` (`{"name", "add", "remove"}`) is the toolkit it was c
|
|
|
226
226
|
`toolkit.default`, `toolkit.child` for a delegate child, or `chi --toolkit` / `chi send --new --toolkit` (with
|
|
227
227
|
`--tool` / `--no-tool`). It is never resolved again from config: a respawned or woken worker declares the same
|
|
228
228
|
tools. `null` (a session from before toolkits) is `full`. A continue (`continue →`, `chi send --new --continues`)
|
|
229
|
-
and a fork (`ctx.sessions.fork`) keep it, as they keep the thinking level; a delegate child gets
|
|
229
|
+
and a fork (`ctx.sessions.fork`) keep it, as they keep the thinking level; a delegate child gets the kit its
|
|
230
|
+
`delegate` call names (`toolkit:`, any kit the parent knows) or `toolkit.child`; one its worker can't resolve
|
|
231
|
+
declares every tool (full). The file stores only the name: a kit's tools and prompt text come from its definition.
|
|
230
232
|
`/toolkit` changes it, saved at once, from the next turn's start. See
|
|
231
233
|
[Toolkits](configuration.md#toolkits).
|
|
232
234
|
|
data/docs/testing.md
CHANGED
|
@@ -36,7 +36,10 @@ A select bound with `.value` instead reads right in both, but lit sets it before
|
|
|
36
36
|
`npm test` runs every file in one process (`--test-isolation=none`; CI and local runs use Node 24): the files share
|
|
37
37
|
`globalThis`, the shim's one happy-dom window and every module's state: a global a spec sets (stage_view's
|
|
38
38
|
`ResizeObserver` stand-in) is there for the files after it, so set only harmless stand-ins and put anything else
|
|
39
|
-
back (`t.mock` restores itself).
|
|
39
|
+
back (`t.mock` restores itself). A test that sets off a module's timer (a flash, a highlight's end) fakes it
|
|
40
|
+
(`t.mock.timers.enable({ apis: ["setTimeout"] })`, then `tick`): a real one still pending after the last test keeps
|
|
41
|
+
node running until it fires (a 4 s one made the run 6 s for 2 s of work). One file:
|
|
42
|
+
`node --test spec/web/public/NAME.test.js`.
|
|
40
43
|
|
|
41
44
|
Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, both terminals) and in the web
|
|
42
45
|
(`composer.js#routeComposerLine`). `spec/fixtures/composer_routes.json` holds the cases both answer: rspec
|
|
@@ -44,6 +47,12 @@ Where a composer line goes is decided twice, in Ruby (`ComposerRoute`, both term
|
|
|
44
47
|
either router adds its row there; a row where a UI differs on purpose (or a found divergence) carries a `web` or
|
|
45
48
|
`repl` override and a `note`.
|
|
46
49
|
|
|
50
|
+
A spec that waits for something uses `wait_until(timeout:)`, or `wait_until!(what, timeout:)` where no check
|
|
51
|
+
follows (stubs, helper threads: it raises "timed out after N s waiting for <what>") — never a loop with no deadline.
|
|
52
|
+
Every example also runs under a 120 s limit (`spec_helper`'s `around`; `integration`, `macos_build`, `warmup` and
|
|
53
|
+
`recap` examples are exempt), so a hang fails its example instead of the run. `SPEC_EXAMPLE_TIMEOUT=N` changes the
|
|
54
|
+
limit, `0` turns it off (a debugger session).
|
|
55
|
+
|
|
47
56
|
## While you work: affected specs
|
|
48
57
|
|
|
49
58
|
```sh
|
|
@@ -60,12 +60,11 @@ module Samagotchi
|
|
|
60
60
|
attr_reader :path
|
|
61
61
|
|
|
62
62
|
# @param path [String] config.yml (a symlink is written through)
|
|
63
|
-
# @param env [Hash] the environment for the check
|
|
64
|
-
# is dropped: it would replace the file's hosts)
|
|
63
|
+
# @param env [Hash] the environment for the check
|
|
65
64
|
# @param now [Proc] the time for the backup's name and the header
|
|
66
65
|
def initialize(path:, env: ENV, now: -> { Time.now })
|
|
67
66
|
@path = path
|
|
68
|
-
@env = env.to_h
|
|
67
|
+
@env = env.to_h
|
|
69
68
|
@now = now
|
|
70
69
|
end
|
|
71
70
|
|