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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/docs/architecture.md +14 -8
  4. data/docs/cli.md +3 -2
  5. data/docs/configuration.md +23 -13
  6. data/docs/hooks.md +1 -1
  7. data/docs/internals/anthropic.md +1 -1
  8. data/docs/internals/engine.md +38 -13
  9. data/docs/internals/llm-context-live.md +1 -1
  10. data/docs/internals/prompt-caching.md +6 -0
  11. data/docs/plugins.md +18 -7
  12. data/docs/sessions.md +3 -1
  13. data/docs/testing.md +10 -1
  14. data/lib/samagotchi/bootstrap/config_writer.rb +2 -3
  15. data/lib/samagotchi/bridge.rb +5 -2
  16. data/lib/samagotchi/broadcast/scope_card.rb +2 -2
  17. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -7
  18. data/lib/samagotchi/bundles/claude-peer/plugin.rb +2 -2
  19. data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -6
  20. data/lib/samagotchi/bundles/codex-peer/plugin.rb +2 -2
  21. data/lib/samagotchi/bundles/coordinator/manifest.yml +8 -6
  22. data/lib/samagotchi/bundles/coordinator/plugin.rb +10 -0
  23. data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +2 -1
  24. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  25. data/lib/samagotchi/bundles/system/manifest.yml +2 -2
  26. data/lib/samagotchi/config.rb +7 -25
  27. data/lib/samagotchi/context_note.rb +2 -6
  28. data/lib/samagotchi/effective_model.rb +1 -1
  29. data/lib/samagotchi/engine.rb +91 -213
  30. data/lib/samagotchi/engine_peers.rb +2 -0
  31. data/lib/samagotchi/event_routes.rb +83 -0
  32. data/lib/samagotchi/hook_routes.rb +16 -37
  33. data/lib/samagotchi/host_config.rb +1 -1
  34. data/lib/samagotchi/host_keys.rb +0 -2
  35. data/lib/samagotchi/paths.rb +13 -0
  36. data/lib/samagotchi/plugin/api.rb +11 -5
  37. data/lib/samagotchi/plugin/peer.rb +5 -8
  38. data/lib/samagotchi/prompt_files.rb +2 -4
  39. data/lib/samagotchi/session.rb +2 -1
  40. data/lib/samagotchi/session_manager.rb +6 -16
  41. data/lib/samagotchi/session_model_settings.rb +227 -0
  42. data/lib/samagotchi/session_setup.rb +2 -1
  43. data/lib/samagotchi/system_prompt.rb +18 -13
  44. data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -14
  45. data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
  46. data/lib/samagotchi/tool_activity.rb +1 -0
  47. data/lib/samagotchi/tool_declarations.rb +4 -0
  48. data/lib/samagotchi/toolkit.rb +55 -24
  49. data/lib/samagotchi/toolkit_binding.rb +7 -5
  50. data/lib/samagotchi/toolkit_command.rb +28 -5
  51. data/lib/samagotchi/tools/builtins.rb +2 -1
  52. data/lib/samagotchi/tools/delegate.rb +32 -6
  53. data/lib/samagotchi/tools/list_sessions.rb +2 -2
  54. data/lib/samagotchi/tools/peers.rb +5 -2
  55. data/lib/samagotchi/tools/register_reminder.rb +1 -0
  56. data/lib/samagotchi/tools/registry.rb +1 -1
  57. data/lib/samagotchi/tools/tool_index.rb +15 -4
  58. data/lib/samagotchi/turn_note.rb +4 -1
  59. data/lib/samagotchi/version.rb +1 -1
  60. data/lib/samagotchi/web/app.rb +13 -10
  61. data/lib/samagotchi/web/public/app.js +20 -18
  62. data/lib/samagotchi/web/public/bubbles.js +1 -1
  63. data/lib/samagotchi/web/public/cards_place.js +6 -10
  64. data/lib/samagotchi/web/public/chains.js +1 -1
  65. data/lib/samagotchi/web/public/chip_popover.js +3 -1
  66. data/lib/samagotchi/web/public/composer.js +13 -6
  67. data/lib/samagotchi/web/public/context_chips.js +25 -12
  68. data/lib/samagotchi/web/public/data.js +5 -1
  69. data/lib/samagotchi/web/public/format.js +5 -0
  70. data/lib/samagotchi/web/public/sessions_list.js +2 -3
  71. data/lib/samagotchi/web/public/stage_frame.js +1 -1
  72. data/lib/samagotchi/web/public/stage_view.js +8 -2
  73. data/lib/samagotchi/web/public/stream_handlers.js +6 -3
  74. data/lib/samagotchi/web/public/timing.js +1 -1
  75. data/lib/samagotchi/web/public/toolkit_chip.js +3 -1
  76. data/lib/samagotchi/web/session_hub.rb +8 -0
  77. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b92ddae3d906d7ba8192ddb77ac75b8d3f12e5d34ffc146f45ca18971a3df9d7
4
- data.tar.gz: 668e0a999ef5cf276e8b40953a968301c84930399fb2e2ace30e34e18332641e
3
+ metadata.gz: 6421f740a2683f0dcf3d24b92fba7bba62bed9edb92ddfd532ce69c74e14e0e8
4
+ data.tar.gz: 23841684a58d0a3efedddcd0fb0caef4d1403c61a88268ad79f6b28a9d8f00ab
5
5
  SHA512:
6
- metadata.gz: 9ebddc5482d48c2e8b8d637b967ea87c939c267a79db1839dbfd7286459ccb6a303b75e2b2307a6456bc5aafc74228f1a49b912e18f4b5e31eacb1368589cf2c
7
- data.tar.gz: ef0521a3cfa0af34d6469f8b5a77ed0fd8a34bd5f300e08a8a6ef5b1f0878dfc643db2e56b08b0ee0e9f414ed5ed5c5dc875ca1a1410176e2c0e8205dd31b256
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.71.0...HEAD
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 memory
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, `SAMAGOTCHI_HOSTS_JSON`, `chi self`, logs or events.
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. `/toolkit remove TOOL` is hard: a tool the
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
@@ -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. `SAMAGOTCHI_HOSTS_JSON` (a JSON object in the shape
111
- of `hosts:`) replaces the file's `hosts:` for the process it is set on, the way specs use it; no worker inherits
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 `remove:` (any other key, a typo
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 ("Changes that restart the prompt
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 changes what saved sessions on it
1376
- declare after an upgrade: the next request reads the prompt again once, as a tool's schema change does. So can
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` |
@@ -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 (`#raise_plain_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
 
@@ -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 (Engine#model_setting) and log effective_model_unresolved;
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 `#resumed_run_start`), past a user entry added after that run's last reply (a `!cmd`'s output). Without
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, status}.
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`): `error` for an `Error:` result and for an
318
- execute that exited non-zero, except a grep-family search that found nothing (`ToolActivity.no_match?`:
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 `#model_setting`. A turn takes one in `turn_settings` (before the hooks) and one in `generate` (after
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
- @hook_routes (HookRoutes: notices, hook questions, stop_turn/stop_generation, steer attribution)
496
- @guardrail_wiring, @question_desk
497
- @toolkit_binding (ToolkitBinding: the kits, the plugins' among them, and the turn-start apply)
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; the bundles' plugins load (load_plugins → ExtensionLoad#load_plugins),
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, working directory, session).
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`, and no inherited `SAMAGOTCHI_HOSTS_JSON` (the smoke-run isolation).
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. Users pick
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. The `acts_as:` hook must give exactly the
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 MCP tools changed while the call
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. On a chi without `chi.annotation` (0.62–0.65)
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 `toolkit.child`.
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). One file: `node --test spec/web/public/NAME.test.js`.
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 (SAMAGOTCHI_HOSTS_JSON
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.except("SAMAGOTCHI_HOSTS_JSON")
67
+ @env = env.to_h
69
68
  @now = now
70
69
  end
71
70