samagotchi 0.58.0 → 0.60.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 +86 -1
- data/README.md +3 -2
- data/bin/chi +23 -2
- data/docs/architecture.md +6 -4
- data/docs/cli.md +14 -7
- data/docs/configuration.md +96 -31
- data/docs/hooks.md +12 -6
- data/docs/internals/background-tasks.md +7 -0
- data/docs/internals/engine.md +20 -12
- data/docs/internals/gemma4-contract.md +1 -0
- data/docs/internals/prompt-caching.md +4 -3
- data/docs/internals/tool-guardrails.md +2 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +75 -6
- data/docs/sessions.md +2 -2
- data/docs/sub-agent.md +2 -2
- data/docs/testing.md +4 -2
- data/lib/samagotchi/bootstrap_command.rb +2 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -0
- data/lib/samagotchi/bridge.rb +6 -2
- data/lib/samagotchi/bundles/agents-md/manifest.yml +11 -0
- data/lib/samagotchi/bundles/agents-md/plugin.rb +198 -0
- data/lib/samagotchi/bundles/core/manifest.yml +2 -1
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +5 -4
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +169 -16
- 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 +44 -10
- data/lib/samagotchi/continue_offer.rb +5 -2
- data/lib/samagotchi/engine.rb +89 -15
- data/lib/samagotchi/host_config.rb +3 -1
- data/lib/samagotchi/host_registry.rb +2 -1
- data/lib/samagotchi/iteration_limit.rb +3 -2
- data/lib/samagotchi/kernel_loop.rb +35 -9
- data/lib/samagotchi/llm/backend.rb +6 -1
- data/lib/samagotchi/llm/chat_loop.rb +11 -6
- data/lib/samagotchi/llm/model_result.rb +11 -2
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/model_settings.rb +4 -2
- data/lib/samagotchi/plugin/context.rb +16 -3
- data/lib/samagotchi/process_group.rb +76 -3
- data/lib/samagotchi/prompt_files.rb +127 -0
- data/lib/samagotchi/ref.rb +124 -0
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session.rb +15 -3
- data/lib/samagotchi/session_commands.rb +15 -3
- data/lib/samagotchi/session_metrics.rb +8 -2
- data/lib/samagotchi/session_setup.rb +11 -5
- data/lib/samagotchi/steer_cut.rb +12 -8
- data/lib/samagotchi/steer_cut_after.rb +69 -0
- data/lib/samagotchi/system_prompt.rb +74 -18
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +18 -9
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +23 -5
- data/lib/samagotchi/terminal_ui.rb +18 -6
- data/lib/samagotchi/tool_activity.rb +18 -1
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tools/execute.rb +318 -70
- data/lib/samagotchi/tools/task_get.rb +2 -0
- data/lib/samagotchi/tools/task_runtime.rb +96 -13
- data/lib/samagotchi/tools/task_wait.rb +23 -5
- data/lib/samagotchi/turn_flow.rb +22 -3
- data/lib/samagotchi/utf8_default.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/favicon.svg +1 -0
- data/lib/samagotchi/web/public/index.html +1 -0
- data/lib/samagotchi/web/public/read_view.js +3 -1
- data/lib/samagotchi/web/public/refs.js +107 -15
- data/lib/samagotchi/web/public/task_stop.js +25 -7
- data/lib/samagotchi/worker.rb +1 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee2aa2b12b6482c02486b2382cc1c287c394d0a323e3bbf851948e49d0ebb2b9
|
|
4
|
+
data.tar.gz: 7bddba2fddfbcc05dfccba0777a6163963f8e238a26d14b6fbfb49b3d0bc0827
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fb47d9df8beac57e413f22b01efdf8bbec31c7c9a5be400976b818d8e6dbe6931081b223568cbb3f345dee39c7f1ffc7a6b39def9e01b4033bdcf7ccda017cdd
|
|
7
|
+
data.tar.gz: 95b1879ed0ee9d093140d2dddaa9cc21c30031d45f7114b1c3311134d2f1082ad1f81480421a8da7f9cd61eb76d4d4aae65b8b29c08c0dd377dcbdab50c98f6d
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,89 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.60.0] - 2026-10-11
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- chi web has a favicon: χ on a blue-to-violet rounded square. Safari showed a "1" monogram before.
|
|
16
|
+
- `chi --append-system-prompt FILE` (repeatable) and a config.yml `append_system_prompt:` list: a file's text goes
|
|
17
|
+
into the system prompt after AGENTS.md, in the cached part. A relative config entry is looked up where chi runs
|
|
18
|
+
(`append_system_prompt: [CLAUDE.md]`); the flag's files are saved in the session and kept by forks and continues.
|
|
19
|
+
`/stats` names the prompt's files on a `prompt files:` line.
|
|
20
|
+
- An analytics `resumed` mark on the turn record of a step-limit Continue (`resumed: true`; every other turn has
|
|
21
|
+
`resumed: false`). `continue` also marks a reminder's and a wake's turn, so `script/model_notes_report` now counts
|
|
22
|
+
Continues from `resumed`, falling back to `continue` record by record for older files. `:turn_started` and the
|
|
23
|
+
`before_turn` hook payload carry `resumed` too.
|
|
24
|
+
- A hook's or plugin's notice can be marked `fallback_for: :editor`: a line naming a file the viewer could open from
|
|
25
|
+
a ref. The web leaves it out for a viewer on this machine with `web.editor` set; a phone and the terminals show it.
|
|
26
|
+
- `ctx.prompt_files` for plugins: the files whose text is in the session's system prompt now — the AGENTS.md chain
|
|
27
|
+
and the appended files — as absolute paths in prompt order.
|
|
28
|
+
- The `agents-md` bundle, in the `core` profile (core 0.3.0: `chi update` installs it for existing core users):
|
|
29
|
+
a nested `AGENTS.md` of the session's checkout — one in a directory the session doesn't run in, which the
|
|
30
|
+
system prompt never sees — is steered into the conversation the first time a tool (`read`, `write` or
|
|
31
|
+
`edit`) touches a file under its directory, outermost first, once per session, and never re-injected after a
|
|
32
|
+
resume or a woken worker. It never stops generation or thinking, and leaves the chain's and appended
|
|
33
|
+
`AGENTS.md` files alone (`ctx.prompt_files`). Files in another repository, a nested repository or a
|
|
34
|
+
submodule are ignored. Opt out with `skip_agents_md: true`, as with the chain; `bundles: agents-md:`
|
|
35
|
+
settings: `tools`, `max_chars`. See "The agents-md bundle" in docs/plugins.md.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- The default step limit (`turn.max_iterations`) is 300, up from 100: most long build turns now finish in one turn
|
|
40
|
+
instead of stopping at the step-limit question. Loops are caught by the loop-guard bundle, and check-in's card
|
|
41
|
+
(at 50 tool calls, then every 50) still says a turn is long without stopping it. `--no-interrupt` stays 1000.
|
|
42
|
+
- Started in a subdirectory, chi loads every `AGENTS.md` from the top of the git work tree down to the current
|
|
43
|
+
directory, outermost first, instead of the subdirectory's replacing the root's. A further file is headed with its
|
|
44
|
+
directory ("Project specific description for pkg/:"); a session with one `AGENTS.md` gets the same prompt as before.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- A step-limit Continue no longer drops the turn's reasoning: on a chat host (`reasoning_history: turn`) the
|
|
49
|
+
continued turn sends the earlier replies' reasoning back as before the limit, and on a native host
|
|
50
|
+
(`/completion`) the prompt keeps the run's thought blocks, those before a mid-turn steer too. The model keeps its
|
|
51
|
+
thinking, and the first request after the Continue matches the last one before the limit (before the fix, a local
|
|
52
|
+
27B read most of a 17k prompt again: 24.9 s to the first token instead of 4.0 s). Continue's text is a steer, and on a chat host still starts
|
|
53
|
+
the reasoning afresh from it. After a `/model` change while the question stands, the Continue starts afresh.
|
|
54
|
+
|
|
55
|
+
Update with `chi update`: the system bundle 0.60.0, the core profile 0.3.0 (now with agents-md) and the new
|
|
56
|
+
agents-md 0.1.0 bundle.
|
|
57
|
+
|
|
58
|
+
## [0.59.0] - 2026-10-11
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- `hosts.<name>.steer_cut_after` and `models.<key>.steer_cut_after`: how long a message sent as a cut waits, per host
|
|
63
|
+
or model (`0` = never, e.g. a slow local thinker); `/model` shows the resolved one.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- At its limit (`execute.timeout_sec`, 120 s), `execute` hands a command still running over as a background task
|
|
68
|
+
instead of killing it: the result names its `task_id` and the output so far, and `task_wait` waits for the rest.
|
|
69
|
+
The execute tool's description says so (the prompt changes once: a cached prompt prefix is rebuilt after the
|
|
70
|
+
upgrade). `execute.on_timeout: kill` keeps the old kill. The `!cmd` bang still kills at the limit. Command output
|
|
71
|
+
now goes through log files under the state dir, and the command's stdin is `/dev/null`. On Linux, a command that
|
|
72
|
+
writes through `/dev/stdout` or `/dev/stderr` loses its earlier output (known gap).
|
|
73
|
+
- slow-commands bundle 0.2.0 (needs chi 0.59.0): a command `execute` handed over as a task at its limit counts as
|
|
74
|
+
slow, and the "a finished task took …" advice also reads handed-over tasks. A command is now remembered without
|
|
75
|
+
the `time` / `timeout N` wrappers and `env` a model adds (`timeout 300 rspec` and `rspec` are one entry), its
|
|
76
|
+
`X=…` assignments sorted and `NO_COLOR`, `FORCE_COLOR` and `TERM` left out; `/slow-commands` shows each as last
|
|
77
|
+
typed. With `execute.on_timeout: kill`, a `timeout N` past the execute limit is denied up front.
|
|
78
|
+
- chi reads a project's AGENTS.md (the name other agent tools read) instead of AGENT.md: rename yours. AGENT.md is no
|
|
79
|
+
longer read. The setting that skips it is now `skip_agents_md` (env `SAMAGOTCHI_SKIP_AGENTS_MD`).
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- A `timeout N cmd` (GNU coreutils) that moved to a process group of its own is now stopped with the command: by the
|
|
84
|
+
user's Stop, at the execute limit and by `task_stop`. It used to run on, orphaned.
|
|
85
|
+
- A `task_create` the guardrails corrected (its result starts with a `ran as:` line) now counts as the session's own
|
|
86
|
+
task: the turn-end and cancel notes list it, a worker doesn't idle-exit under it, and the web's stop-task button
|
|
87
|
+
works for it.
|
|
88
|
+
- Web: a blank line in a read's numbered file lines is empty, not a space, so copying the lines keeps the file
|
|
89
|
+
as it is.
|
|
90
|
+
|
|
91
|
+
Update with `chi update`: it updates slow-commands (0.2.0: hand-overs and command wrappers). Then restart `chi web`.
|
|
92
|
+
If your project has an AGENT.md, rename it to AGENTS.md.
|
|
93
|
+
|
|
11
94
|
## [0.58.0] - 2026-10-11
|
|
12
95
|
|
|
13
96
|
### Added
|
|
@@ -2540,7 +2623,9 @@ and long-lived sessions.
|
|
|
2540
2623
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2541
2624
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2542
2625
|
|
|
2543
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2626
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.60.0...HEAD
|
|
2627
|
+
[0.60.0]: https://github.com/dm1try/samagotchi/compare/v0.59.0...v0.60.0
|
|
2628
|
+
[0.59.0]: https://github.com/dm1try/samagotchi/compare/v0.58.0...v0.59.0
|
|
2544
2629
|
[0.58.0]: https://github.com/dm1try/samagotchi/compare/v0.57.0...v0.58.0
|
|
2545
2630
|
[0.57.0]: https://github.com/dm1try/samagotchi/compare/v0.56.0...v0.57.0
|
|
2546
2631
|
[0.56.0]: https://github.com/dm1try/samagotchi/compare/v0.55.0...v0.56.0
|
data/README.md
CHANGED
|
@@ -76,8 +76,9 @@ It finds out whether the server is llama.cpp or OpenAI-compatible, picks the
|
|
|
76
76
|
model (or asks, when there are several), sends one test request and writes
|
|
77
77
|
the config. With a config already there, it adds a `hosts:` entry and keeps
|
|
78
78
|
the rest of the file. Then it installs the `core` bundles (loop-guard,
|
|
79
|
-
check-in, guardrails, slow-commands: the safety set for a small
|
|
80
|
-
terminal, offers `dev` (known-names, mcp, btw, skills,
|
|
79
|
+
check-in, guardrails, slow-commands, agents-md: the safety set for a small
|
|
80
|
+
local model) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
|
|
81
|
+
source-links). Edit the
|
|
81
82
|
file later as in [Configure](#configure); `chi bootstrap --help` has the
|
|
82
83
|
options.
|
|
83
84
|
|
data/bin/chi
CHANGED
|
@@ -82,7 +82,7 @@ ARGV.each do |arg|
|
|
|
82
82
|
base = arg.split("=", 2).first
|
|
83
83
|
# If dashed version is a known flag (registry or hardcoded), reject underscore variant
|
|
84
84
|
dashed = base.tr("_", "-")
|
|
85
|
-
known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --memory --mute --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
|
|
85
|
+
known = %w[--prompt --resume --attach --shared --no-shared --model --thinking --memory --mute --append-system-prompt --llm-context --llm-context-apply --llm-context-budget --no-interrupt --non-interactive --verbose --no-default-input --port --open --new-token] + Samagotchi::Config.cli_entries.map(&:cli_flag)
|
|
86
86
|
if known.include?(dashed) || known.include?(base)
|
|
87
87
|
warn "Unknown option: #{arg} (did you mean #{dashed}?)"
|
|
88
88
|
exit 1
|
|
@@ -120,6 +120,8 @@ parser = OptionParser.new do |opts|
|
|
|
120
120
|
"before the model's; default unsets it; /thinking changes it). With chi web: the default for the sessions " \
|
|
121
121
|
"its workers run, below a session's own [env SAMAGOTCHI_THINKING_LEVEL]") { |l| options[:thinking] = l }
|
|
122
122
|
opts.on("--memory NAME", "Preload a memory entry into the system prompt (repeatable; a comma list too)") { |m| (options[:memories] ||= []) << m }
|
|
123
|
+
opts.on("--append-system-prompt FILE", "Put FILE's text into the system prompt, after AGENTS.md (repeatable; saved in the session; " \
|
|
124
|
+
"config append_system_prompt: is the baseline)") { |f| (options[:append_system_prompt] ||= []) << f }
|
|
123
125
|
opts.on("--mute NAME", "Hide a memory from this session: not in the prompt, refused by memory_read (repeatable; a comma list too)") { |m| (options[:muted] ||= []) << m }
|
|
124
126
|
opts.on("--llm-context LAYERS", "This session's own LLM context strategy: none, or layers such as stale,forget (saved in the session, " \
|
|
125
127
|
"before the model's; /llm-context changes it)") { |v| (options[:llm_context] ||= {})[:strategy] = v }
|
|
@@ -308,6 +310,23 @@ if options[:llm_context]
|
|
|
308
310
|
end
|
|
309
311
|
end
|
|
310
312
|
|
|
313
|
+
# --append-system-prompt: each file is checked here, before anything starts
|
|
314
|
+
# (the user named it, so a missing one is an error), and made absolute
|
|
315
|
+
# against this shell's cwd.
|
|
316
|
+
if options[:append_system_prompt]
|
|
317
|
+
require "samagotchi/prompt_files"
|
|
318
|
+
options[:appended_prompt_files] = options[:append_system_prompt].map do |value|
|
|
319
|
+
path, reason = Samagotchi::PromptFiles.check_cli_file(value)
|
|
320
|
+
if reason
|
|
321
|
+
warn "Error: --append-system-prompt #{value}: #{reason}"
|
|
322
|
+
exit 1
|
|
323
|
+
end
|
|
324
|
+
chars = Samagotchi::PromptFiles.large_size(path)
|
|
325
|
+
warn "Warning: --append-system-prompt #{value} is #{chars} chars; it is in every request" if chars
|
|
326
|
+
path
|
|
327
|
+
end.uniq
|
|
328
|
+
end
|
|
329
|
+
|
|
311
330
|
chi_update_hint.call
|
|
312
331
|
require "samagotchi/launch_mode"
|
|
313
332
|
launch, launch_note = if scratch
|
|
@@ -341,7 +360,8 @@ if launch == :attached
|
|
|
341
360
|
memories: options[:memories] || [],
|
|
342
361
|
muted_memories: options[:muted] || [],
|
|
343
362
|
llm_context: options[:llm_context] || {},
|
|
344
|
-
thinking: options[:thinking]
|
|
363
|
+
thinking: options[:thinking],
|
|
364
|
+
appended_prompt_files: options[:appended_prompt_files] || [])
|
|
345
365
|
rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
|
|
346
366
|
warn "Error: #{e.message}"
|
|
347
367
|
exit 1
|
|
@@ -360,6 +380,7 @@ begin
|
|
|
360
380
|
muted_memories: options[:muted] || [],
|
|
361
381
|
llm_context: options[:llm_context] || {},
|
|
362
382
|
thinking: options[:thinking],
|
|
383
|
+
appended_prompt_files: options[:appended_prompt_files] || [],
|
|
363
384
|
non_interactive: options[:non_interactive],
|
|
364
385
|
model_name: options[:model],
|
|
365
386
|
scratch: scratch
|
data/docs/architecture.md
CHANGED
|
@@ -176,8 +176,9 @@ engine.run_turn(session, "hello", on_event: nil) # => LLM::ModelResult (`.outp
|
|
|
176
176
|
forwards the raw `KernelLoop` events unchanged (the low-level contract) and adds a few
|
|
177
177
|
higher-level events so UIs get clean turn boundaries without inferring them:
|
|
178
178
|
|
|
179
|
-
- `:turn_started` — `{ session_id:, prompt:, turn_id: }` (
|
|
180
|
-
turn, `
|
|
179
|
+
- `:turn_started` — `{ session_id:, prompt:, turn_id:, cut_after: }` (`cut_after`: the seconds a cut waits for this
|
|
180
|
+
model's thinking, `0` = never; plus `continue: true` for a continue turn, `resumed: true` for a step-limit Continue,
|
|
181
|
+
`images:` with the prompt's image refs)
|
|
181
182
|
- `:turn_completed` — `{ result:, turn_summary:, display_pending: }` (`result`: the final
|
|
182
183
|
`LLM::ModelResult`; `display_pending`: `after_turn` hooks may still present the answer)
|
|
183
184
|
- `:turn_canceled` — `{ cancellation_reason:, cancelled_by:, duration_ms: }` (`cancelled_by`: the bundle or hook label that
|
|
@@ -198,7 +199,7 @@ engine.session # current session (Engine owns create/resume)
|
|
|
198
199
|
```
|
|
199
200
|
|
|
200
201
|
The system prompt (`SystemPrompt#build`) is the base prompt, then rg guidance, the
|
|
201
|
-
identity and the preloaded memories,
|
|
202
|
+
identity and the preloaded memories, AGENTS.md, the memory indexes, and last **the
|
|
202
203
|
model** (`Model: this session runs on <ref> (host …; model key …)` and a line telling
|
|
203
204
|
the model to answer "which model are you" from it, not from training), the working
|
|
204
205
|
directory and the session id and log (Gemma's native tool declarations still follow).
|
|
@@ -394,7 +395,8 @@ stream with a fresh `ThoughtStreamSplitter` (Qwen's `<think>` and Gemma's
|
|
|
394
395
|
`<|channel>thought … <channel|>` are thinking, tool-call bodies are dropped
|
|
395
396
|
from `text:`). The chat loop's model turns keep their `tool_calls` and tool results their `tool_call_id`,
|
|
396
397
|
so later requests and resumed sessions pair them, and their reasoning (`thinking:`), which goes back as
|
|
397
|
-
`reasoning_content` on the running turn's replies (`reasoning_history
|
|
398
|
+
`reasoning_content` on the running turn's replies (`reasoning_history`; a step-limit Continue's
|
|
399
|
+
turn goes on from the run it resumes, `reasoning_since:`). It has its own system prompt
|
|
398
400
|
(no raw-prompt tool declarations; the tools go as JSON schemas).
|
|
399
401
|
|
|
400
402
|
`Engine#build_stream_event_handler` is the one seam both loops' streams pass
|
data/docs/cli.md
CHANGED
|
@@ -189,6 +189,7 @@ controls exit behavior (`--non-interactive`); `--resume` composes with both.
|
|
|
189
189
|
| `--thinking LEVEL` | This session's own thinking level: `off`, `low`, `medium` or `high`, saved in the session and first in the order (`default` unsets it). On `--resume` or `--attach` it is set at once, through a `/thinking` on an attached worker. With `chi web` it is instead the default for the sessions its workers run, below a session's own (as env `SAMAGOTCHI_THINKING_LEVEL` is). See "Thinking" in configuration.md. |
|
|
190
190
|
| `--profile NAME` | Prompt profile (`qwen36` or `gemma4`) for every model in this run, over config and the server's template (same as `--model-profile`, env `SAMAGOTCHI_MODEL_PROFILE`). See "Prompt profile" in configuration.md. |
|
|
191
191
|
| `--memory NAME` | Preload a memory entry into the system prompt (repeatable; a comma list too). Merged under the config.yml `memories:` baseline. Works attached: the list is stored on the session, so its worker builds the same prompt on every respawn. |
|
|
192
|
+
| `--append-system-prompt FILE` | Put `FILE`'s text into the system prompt, after AGENTS.md and before the memory indexes (repeatable; relative to the shell's directory). Added after the config.yml `append_system_prompt:` list. A missing or unreadable file is an error, exit 1. Saved in the session, so a respawned worker, a fork and a continue keep it; ignored with `--attach`/`--resume` (one line, as `--memory`). See [Appended prompt files](configuration.md#appended-prompt-files). |
|
|
192
193
|
| `--mute NAME` | Hide a memory from this session (repeatable; a comma list too): its index line is not in the prompt, `memory_read` refuses it, the identity auto-load skips it, and it is dropped from the preloads (config baseline or `--memory`). A name matches in both scopes (`gh-helper`, `project/gh-helper` and `gh-helper.md` all hide `gh-helper`). Nothing on disk changes. See [Muting a memory](#muting-a-memory). |
|
|
193
194
|
| `--llm-context LAYERS` | This session's own LLM context strategy: `none`, or layers such as `stale` or `stale,forget` (comma, space or `\|` separated). Saved in the session, before the model's `llm_context_strategy`. With `--resume` it is set on the resumed session; on an attached or resumed worker it runs as `/llm-context` before the first prompt (after `--model`'s `/model`). A value that isn't one is an error before anything starts. See "LLM context: a session's own strategy" in configuration.md. |
|
|
194
195
|
| `--llm-context-apply RULE` | This session's own apply rule: `payoff`, `next_request` or `turn_end`. |
|
|
@@ -291,7 +292,7 @@ of UIs at once: the Web UI and attached terminals (`chi`, `--resume`,
|
|
|
291
292
|
a prompt, also while a turn runs (it merges into that turn as steering: the model
|
|
292
293
|
is told it is a steer and who sent it). Steering waits for the model's next step
|
|
293
294
|
and cuts nothing. `/cut TEXT` in a terminal or the web composer (or `chi send --cut`) asks for a cut:
|
|
294
|
-
once the model has streamed only thinking for `steer.cut_after` seconds (default
|
|
295
|
+
once the model has streamed only thinking for `steer.cut_after` seconds (per host/model: `steer_cut_after`; default
|
|
295
296
|
20), now or while your message waits, that generation is cut and the step starts again with your message, marked
|
|
296
297
|
`↪ cut in for your message` (see
|
|
297
298
|
[configuration.md](configuration.md#messages-during-a-running-turn-steer-and-cut)). The web
|
|
@@ -504,7 +505,8 @@ back from the next turn on, and where that came from (`session`,
|
|
|
504
505
|
comes first; `/reasoning-history default` unsets it. It is saved and carried as
|
|
505
506
|
`/thinking`'s is, and a native host ignores it (the reply says so). A change to
|
|
506
507
|
or from `all` once an earlier reply has thinking rewrites the earlier replies;
|
|
507
|
-
`drop` ↔ `turn` costs nothing.
|
|
508
|
+
`drop` ↔ `turn` costs nothing. Under `turn` a step-limit Continue keeps the
|
|
509
|
+
reasoning of the turn it continues. `/stats` shows it on its `reasoning history:`
|
|
508
510
|
line, the web as the `reasoning` chip. See "Reasoning history" in
|
|
509
511
|
[configuration.md](configuration.md).
|
|
510
512
|
|
|
@@ -530,11 +532,12 @@ REPL alike:
|
|
|
530
532
|
does one sent after Ctrl-C: it doesn't merge into the turn being cancelled.
|
|
531
533
|
Reminder turns take merged lines too.
|
|
532
534
|
- `/cut TEXT` asks for a cut: a generation that has streamed only thinking for
|
|
533
|
-
`steer.cut_after` seconds is cut and the
|
|
535
|
+
`steer.cut_after` seconds (per host/model: `steer_cut_after`) is cut and the
|
|
536
|
+
step starts again with TEXT. The dim
|
|
534
537
|
line says what happened: `(cut in now)`, `(cuts in once the thinking passes
|
|
535
538
|
20 s)` (it waits for the thinking; the step may end first) or `(cutting is
|
|
536
|
-
off; goes in at the next step)` (`steer.cut_after
|
|
537
|
-
cut).
|
|
539
|
+
off; goes in at the next step)` (`steer.cut_after` is 0, per host/model:
|
|
540
|
+
`steer_cut_after`, or no generation to cut).
|
|
538
541
|
- `/queue TEXT` (plain REPL only) runs TEXT as a turn of its own after this
|
|
539
542
|
one: `(queued: runs after this turn)`. An attached terminal says
|
|
540
543
|
`(/queue isn't available in an attached session yet)` and sends nothing (the web
|
|
@@ -996,7 +999,7 @@ Behavior notes:
|
|
|
996
999
|
## Iteration Limit Behavior
|
|
997
1000
|
|
|
998
1001
|
- A turn's step limit (model ↔ tool rounds) is `turn.max_iterations` in config.yml (env
|
|
999
|
-
`SAMAGOTCHI_TURN_MAX_ITERATIONS`, default `
|
|
1002
|
+
`SAMAGOTCHI_TURN_MAX_ITERATIONS`, default `300`, an integer of 1 or more). `--no-interrupt` and
|
|
1000
1003
|
`--non-interactive` turns get the larger of 1000 and it. Reminder turns follow it too.
|
|
1001
1004
|
- Tool side effects that already ran before the limit are not rolled back.
|
|
1002
1005
|
- `Samagotchi::KernelLoop#run` returns a resumable result object with the visible output plus the accumulated
|
|
@@ -1015,9 +1018,13 @@ Behavior notes:
|
|
|
1015
1018
|
- In the plain REPL (`--no-shared`) the CLI asks at the `? ` prompt, with the choices listed under it: `yes` (Enter
|
|
1016
1019
|
alone, or `/continue`) resumes, `no` stops, and `no, <explanation>` stops while keeping the reason in conversation
|
|
1017
1020
|
context. Once answered, one line stays: `? The turn ran out of iterations. Continue it? → no, too slow`.
|
|
1021
|
+
- Continue goes on with the same turn: the model keeps the reasoning (thinking) of the run it continues, on chat and
|
|
1022
|
+
native hosts, so a local server's prompt cache stays warm. Continue's text is a steer: on a chat host the reasoning
|
|
1023
|
+
starts again from it. See "Reasoning history" in [configuration.md](configuration.md).
|
|
1018
1024
|
- Stop keeps the interrupted turn's work in the conversation, as a Ctrl-C or a new prompt does, with a note for the
|
|
1019
1025
|
model that it wasn't continued (or your reason); `!rollback` still erases it.
|
|
1020
1026
|
- `--non-interactive` (`chi -p`) has nobody to ask: the turn ends there. A `chi send --wait` on a session nobody can
|
|
1021
1027
|
answer (an older worker) reports `limit` (exit 1) with how to continue it: `chi send ID -m '/continue yes'`.
|
|
1022
1028
|
- The check-in bundle's card closes as the turn ends, before the step-limit question opens, so the two never stand
|
|
1023
|
-
together; with check-in's default `after: 50` and the limit of
|
|
1029
|
+
together; with check-in's default `after: 50` and the limit of 300 a long turn shows check-in cards (at 50, 100, …)
|
|
1030
|
+
well before it.
|
data/docs/configuration.md
CHANGED
|
@@ -81,6 +81,13 @@ session:
|
|
|
81
81
|
memories:
|
|
82
82
|
# - system/user_preferences
|
|
83
83
|
# - project/feature-env-template
|
|
84
|
+
|
|
85
|
+
# Files whose text goes into the system prompt after AGENTS.md (same as
|
|
86
|
+
# --append-system-prompt, which adds to these). A relative entry is looked up
|
|
87
|
+
# in the directory chi runs in; a missing one is skipped.
|
|
88
|
+
append_system_prompt:
|
|
89
|
+
# - CLAUDE.md
|
|
90
|
+
# - ~/notes/house-rules.md
|
|
84
91
|
```
|
|
85
92
|
|
|
86
93
|
Behavior:
|
|
@@ -108,8 +115,8 @@ Besides the settings sections, the file holds maps that are read by their
|
|
|
108
115
|
own subsystems: `hosts:` (below), `models:` (see "Prompt profile" and
|
|
109
116
|
"Images"), `model_aliases:`, `hooks:` (see [Hooks](hooks.md)),
|
|
110
117
|
`guardrails:` (see [Guardrails](guardrails.md)), `bundles:` (a bundle's
|
|
111
|
-
settings for its hooks, see [Hooks: Settings](hooks.md#settings))
|
|
112
|
-
`memories
|
|
118
|
+
settings for its hooks, see [Hooks: Settings](hooks.md#settings)),
|
|
119
|
+
`memories:` and `append_system_prompt:` (see "Appended prompt files"). Their entry names (host names, model ids, aliases) are yours to
|
|
113
120
|
choose. The `memories:` list is the persistent baseline for preloaded memory entries —
|
|
114
121
|
the same name shape as `--memory` (bare name or `scope/name`), merged under
|
|
115
122
|
any per-run `--memory` values (config baseline first, deduped). A per-run
|
|
@@ -561,8 +568,13 @@ models:
|
|
|
561
568
|
reasoning as `reasoning_content`, which is what Qwen's, Gemma 4's and DeepSeek's chat templates and the hosted APIs
|
|
562
569
|
(OpenRouter takes it for DeepSeek, Qwen and other text-reasoning models) expect. The running turn starts at the
|
|
563
570
|
last message you sent (a prompt, or a steer: as the chat template counts it) and never before the turn's own start,
|
|
564
|
-
so a turn that goes on by itself (a reminder, a context wake, a
|
|
565
|
-
before it, an interrupted reply's partial thought included.
|
|
571
|
+
so a turn that goes on by itself (a reminder, a context wake, a delegate report's wake) sends nothing of the one
|
|
572
|
+
before it, an interrupted reply's partial thought included. A **step-limit Continue** is the same turn going on: it
|
|
573
|
+
keeps the reasoning of the run it continues (back to the first run of a chain of Continues), so its first request
|
|
574
|
+
matches the last one before the limit and the server's prompt cache stays warm. Continue's text, or a `!cmd` run
|
|
575
|
+
while the step-limit question stands, is a user message: the reasoning before it goes, as for any steer, and that
|
|
576
|
+
Continue reads the conversation again once (a re-prefill on a local server). After a `/model` change while the
|
|
577
|
+
question stands the Continue starts afresh (no other model's reasoning is sent).
|
|
566
578
|
- `drop` sends none (what chi did before this setting).
|
|
567
579
|
- `all` sends every reply's reasoning, earlier turns too. It sends only the field: a local template that leaves out
|
|
568
580
|
earlier turns' reasoning (Qwen's) needs `sampling: {chat_template_kwargs: {preserve_thinking: true}}` as well.
|
|
@@ -586,7 +598,8 @@ models:
|
|
|
586
598
|
| nothing thought yet, the same level, or a native host | none |
|
|
587
599
|
|
|
588
600
|
- The native loop (`/completion`) ignores it: it always keeps the running turn's reasoning, a mid-turn steer included
|
|
589
|
-
(the chat loop starts again after a steer)
|
|
601
|
+
(the chat loop starts again after a steer), and a step-limit Continue keeps the thoughts of the run it continues,
|
|
602
|
+
a mid-turn steer's earlier ones included.
|
|
590
603
|
- When a host answers a request that sent reasoning back with an HTTP 400 about reasoning, chi asks again without it,
|
|
591
604
|
sends none to that model from then on (until the worker restarts) and says so once. If the request without it is
|
|
592
605
|
refused too, the thinking fields get the same fallback ("Thinking") and the reasoning goes back on the next step.
|
|
@@ -876,26 +889,39 @@ Assist-mode UX:
|
|
|
876
889
|
## Messages during a running turn: steer and cut
|
|
877
890
|
|
|
878
891
|
A message for a running turn goes in at its next step and cuts nothing. A message sent as a cut (`/cut TEXT` in a
|
|
879
|
-
terminal or the web composer, `chi send --cut`) cuts the model's generation once it has streamed only thinking for
|
|
880
|
-
seconds
|
|
881
|
-
wait for a busy server doesn't count) and still does, and the model starts the step again with the message
|
|
882
|
-
(`↪ cut in for your message`). One that comes earlier cuts once the thinking passes
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
892
|
+
terminal or the web composer, `chi send --cut`) cuts the model's generation once it has streamed only thinking for
|
|
893
|
+
that model's cut wait (`steer.cut_after` seconds, default `20`, `0` = never; counted from its first thinking token,
|
|
894
|
+
so the wait for a busy server doesn't count) and still does, and the model starts the step again with the message
|
|
895
|
+
(`↪ cut in for your message`). One that comes earlier cuts once the thinking passes it, unless the step ends first
|
|
896
|
+
and the message goes in there. A plain message (from the web composer, `chi send` without `--cut`, a parent agent)
|
|
897
|
+
never asks for a cut. Such a cut spends no `retry.empty_answer` attempt and sends no hidden note; only the cut
|
|
898
|
+
thinking is lost. A generation that has streamed visible text or a tool call is never cut, and a plugin's steer
|
|
886
899
|
(`ctx.steer`, `ctx.sessions.send`) never cuts.
|
|
887
900
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
`steer.cut_after` (default `20`, env `SAMAGOTCHI_STEER_CUT_AFTER`):
|
|
901
|
+
The wait resolves per model, in this order: env `SAMAGOTCHI_STEER_CUT_AFTER`, then
|
|
902
|
+
`models.<key>.steer_cut_after`, `hosts.<name>.steer_cut_after`, `steer.cut_after` in config.yml, then 20. `0` means
|
|
903
|
+
never cut (a slow local thinker is left alone); a bad value warns and is ignored, like the other keys here.
|
|
893
904
|
|
|
894
905
|
```yaml
|
|
895
906
|
steer:
|
|
896
907
|
cut_after: 20 # seconds of thinking-only output before a cut may land; 0 = never
|
|
908
|
+
|
|
909
|
+
hosts:
|
|
910
|
+
splash:
|
|
911
|
+
host: splash.lan
|
|
912
|
+
steer_cut_after: 0 # never cut: a slow local thinker
|
|
913
|
+
|
|
914
|
+
models:
|
|
915
|
+
qwen:
|
|
916
|
+
steer_cut_after: 60 # a big model thinks a while before it answers
|
|
897
917
|
```
|
|
898
918
|
|
|
919
|
+
`/model` shows the resolved wait (`; steer cut: after 60 s (models: qwen)`, `; steer cut: never (hosts.splash)`).
|
|
920
|
+
|
|
921
|
+
`/queue TEXT` in the plain REPL runs TEXT as a turn of its own after the running one; the attached terminal and the web
|
|
922
|
+
don't queue yet, and a request that asks the Bridge or the web for `delivery: "queue"` is refused (400
|
|
923
|
+
`delivery_unavailable`).
|
|
924
|
+
|
|
899
925
|
## Server errors
|
|
900
926
|
|
|
901
927
|
An error status or a server's error event fails the turn with the server's
|
|
@@ -1058,15 +1084,52 @@ The helper reads these from its launch file: after editing them run
|
|
|
1058
1084
|
|
|
1059
1085
|
## Project specific description
|
|
1060
1086
|
|
|
1061
|
-
If an
|
|
1087
|
+
If an AGENTS.md file is present in the project root, samagotchi injects its
|
|
1062
1088
|
contents into the system prompt under a "Project specific description:" section.
|
|
1063
1089
|
The project root is the top of the git work tree chi runs in (a linked
|
|
1064
|
-
worktree's own checkout), or the current directory outside a repository.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1090
|
+
worktree's own checkout), or the current directory outside a repository.
|
|
1091
|
+
|
|
1092
|
+
Started in a subdirectory, chi loads the chain: every AGENTS.md from the work
|
|
1093
|
+
tree top down to the current directory, outermost first. The first keeps the
|
|
1094
|
+
plain header; each further one is headed with its directory, e.g. "Project
|
|
1095
|
+
specific description for pkg/:". Outside a repository only the current
|
|
1096
|
+
directory's AGENTS.md is read. Edits are picked up by a new process or a
|
|
1097
|
+
prompt rebuild (`/model`).
|
|
1098
|
+
|
|
1099
|
+
To skip loading AGENTS.md (all of the chain), set `skip_agents_md: true` at the top level of
|
|
1100
|
+
`config.yml` (env `SAMAGOTCHI_SKIP_AGENTS_MD=true`).
|
|
1101
|
+
|
|
1102
|
+
Nested `AGENTS.md` files (in directories the session doesn't run in) are
|
|
1103
|
+
steered into the conversation on first touch by the `agents-md` bundle
|
|
1104
|
+
([The agents-md bundle](plugins.md#the-agents-md-bundle)); `skip_agents_md`
|
|
1105
|
+
disables that steering too.
|
|
1106
|
+
|
|
1107
|
+
## Appended prompt files
|
|
1108
|
+
|
|
1109
|
+
`append_system_prompt:` (a top-level list) and `chi --append-system-prompt FILE`
|
|
1110
|
+
(repeatable) put a file's text into the system prompt right after the AGENTS.md
|
|
1111
|
+
chain and before the memory indexes, in the stable part a prompt cache keeps.
|
|
1112
|
+
Each file is one section, `Appended from <path>:`, the path relative to the
|
|
1113
|
+
work tree top when inside it, else with `~`.
|
|
1114
|
+
|
|
1115
|
+
- The config list comes first, then the flag's files; a repeat, or a file the
|
|
1116
|
+
AGENTS.md chain already holds, is left out, and so is an empty file.
|
|
1117
|
+
- A config entry: `~` is expanded, and a relative one is looked up in the
|
|
1118
|
+
directory chi runs in, so `append_system_prompt: [CLAUDE.md]` loads a
|
|
1119
|
+
project's `CLAUDE.md` wherever there is one. A missing entry is skipped
|
|
1120
|
+
without a warning. The list is read when the session's engine starts (a new
|
|
1121
|
+
process picks up an edit).
|
|
1122
|
+
- `--append-system-prompt FILE`: relative to the shell's directory. A missing,
|
|
1123
|
+
unreadable or non-file `FILE` stops chi (`Error: --append-system-prompt FILE:
|
|
1124
|
+
no such file`); above 32,000 chars it warns once (`… is N chars; it is in
|
|
1125
|
+
every request`). The files are saved in the session (`appended_prompt_files`),
|
|
1126
|
+
so a respawned worker builds the same prompt, and a fork or a continue keeps
|
|
1127
|
+
them; a delegate child doesn't. With `--attach` or `--resume` the flag is
|
|
1128
|
+
ignored with one line, as `--memory` is; a REPL `--resume` (`--no-shared`)
|
|
1129
|
+
adds them for that run only.
|
|
1130
|
+
- The files' text is read at each prompt build, as AGENTS.md's is: an edit is
|
|
1131
|
+
picked up by a new process or a rebuild (`/model`). `/stats` lists the files
|
|
1132
|
+
in the prompt on its `prompt files:` line.
|
|
1070
1133
|
|
|
1071
1134
|
## LLM context: the forget layer
|
|
1072
1135
|
|
|
@@ -1206,8 +1269,8 @@ Every setting below takes the three forms described in
|
|
|
1206
1269
|
`config.yml`, `SAMAGOTCHI_<DOTTED_NAME>` in the environment (not for one
|
|
1207
1270
|
marked config.yml only) and, where the CLI
|
|
1208
1271
|
column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
1209
|
-
`model_aliases:`, `hooks:`, `guardrails:` rules, `bundles:`, `memories
|
|
1210
|
-
described in their own sections.
|
|
1272
|
+
`model_aliases:`, `hooks:`, `guardrails:` rules, `bundles:`, `memories:`,
|
|
1273
|
+
`append_system_prompt:`) are described in their own sections.
|
|
1211
1274
|
|
|
1212
1275
|
| Setting | Default | CLI | What it does |
|
|
1213
1276
|
|---|---|---|---|
|
|
@@ -1291,8 +1354,9 @@ described in their own sections.
|
|
|
1291
1354
|
| `retry.base_delay` | `0.5` | yes | |
|
|
1292
1355
|
| `retry.max_delay` | `8.0` | yes | |
|
|
1293
1356
|
| `retry.empty_answer` | `1` | | Times a turn asks again after an empty answer (at most 3, `0` = off). See "Llama Network Retry Behavior". |
|
|
1294
|
-
| `steer.cut_after` | `20` | | Seconds a generation must have streamed only thinking before a message sent as a cut (`/cut TEXT`, `chi send --cut`) cuts it; other messages go in at the next step; `0` = never.
|
|
1295
|
-
| `
|
|
1357
|
+
| `steer.cut_after` | `20` | | Seconds a generation must have streamed only thinking before a message sent as a cut (`/cut TEXT`, `chi send --cut`) cuts it; other messages go in at the next step; `0` = never. `models.<key>.steer_cut_after` and `hosts.<name>.steer_cut_after` come first; see [Messages during a running turn](#messages-during-a-running-turn-steer-and-cut). |
|
|
1358
|
+
| `models.<key>.steer_cut_after`, `hosts.<name>.steer_cut_after` | none | | A model's or host's seconds a cut waits for its thinking: whole seconds, `0` = never (e.g. a slow local thinker). `/model` shows the resolved one. See [Messages during a running turn](#messages-during-a-running-turn-steer-and-cut). |
|
|
1359
|
+
| `turn.max_iterations` | `300` | | A turn's step limit: model ↔ tool rounds before it stops and asks to continue (an integer ≥ 1). `--no-interrupt` turns get the larger of 1000 and this. See [CLI: Iteration Limit Behavior](cli.md#iteration-limit-behavior). |
|
|
1296
1360
|
| `turn.parent_continue` | `true` | | Whether a parent agent (`chi answer`) may answer Continue to a session's step-limit question; `false`: Stop only. config.yml only: no environment variable. See [Sub-agent](sub-agent.md) and [Guardrails](guardrails.md#approvals-from-a-parent-agent). |
|
|
1297
1361
|
| `update.gem` | `true` | | `false`: `chi update` never installs a newer gem (`--no-gem` for one run). See [CLI: Updating](cli.md#updating). |
|
|
1298
1362
|
| `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
|
|
@@ -1307,7 +1371,8 @@ described in their own sections.
|
|
|
1307
1371
|
| `execute.truncate_at_bytes` | `65536` | yes | The same for `execute` output. |
|
|
1308
1372
|
| `execute.preview_bytes` | `12288` | yes | |
|
|
1309
1373
|
| `execute.telemetry_threshold_pct` | `80` | yes | |
|
|
1310
|
-
| `execute.timeout_sec` | `120` | yes | Seconds one `execute` command may run before
|
|
1374
|
+
| `execute.timeout_sec` | `120` | yes | Seconds one `execute` command may run before `execute.on_timeout` applies. |
|
|
1375
|
+
| `execute.on_timeout` | `task` | yes | What happens to a command still running at that limit: `task` keeps it running as a background task (the result names its `task_id`; `task_wait` waits for it, `task_stop` stops it), `kill` stops it (`Error: command timed out after …s`). The `!cmd` bang always kills. See [Background tasks](internals/background-tasks.md). |
|
|
1311
1376
|
| `execute.description` | `true` | | `execute` offers the model an optional `description` (a few words on what the command does), shown as the tool row's title in `chi web` and in place of the command in the TUI's tool line. `false`: the parameter isn't declared, and rows show the command's first step. |
|
|
1312
1377
|
| `web.port` | `4567` | `--port` | `chi web`'s port. See [CLI](cli.md). |
|
|
1313
1378
|
| `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token (`chi web --new-token` replaces it). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
|
|
@@ -1318,8 +1383,8 @@ described in their own sections.
|
|
|
1318
1383
|
| `history.file` | state dir | | Prompt history path, shared by the TUI and the web composer. |
|
|
1319
1384
|
| `no_interrupt` | `false` | `--no-interrupt` | Raise the step limit of a turn to 1000 (or `turn.max_iterations`, when larger); a top-level key. |
|
|
1320
1385
|
| `no_default_input` | `false` | `--no-default-input` | Don't pre-fill `default.input`; a top-level key. |
|
|
1321
|
-
| `
|
|
1386
|
+
| `skip_agents_md` | `false` | | Don't load AGENTS.md; a top-level key (see below). |
|
|
1322
1387
|
|
|
1323
|
-
`max_tool_output_chars`, `
|
|
1388
|
+
`max_tool_output_chars`, `skip_agents_md`, `no_interrupt` and
|
|
1324
1389
|
`no_default_input` have no section: in `config.yml` they are top-level keys as
|
|
1325
1390
|
written (`max_tool_output_chars: 20000`).
|
data/docs/hooks.md
CHANGED
|
@@ -50,13 +50,13 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
50
50
|
| Event | When it fires | Event payload |
|
|
51
51
|
|-------|--------------|---------------|
|
|
52
52
|
| `:session_start` | The first turn this chi process runs for the session (so again after `--resume`, a worker that idle-exited and woke, or a restart) | `{ type: :session_start, session_id: "..." }` |
|
|
53
|
-
| `:before_turn` | Before each turn starts | `{ type: :before_turn, session_id: "...", prompt: "..." (nil on a continue), messages: [...] (the history before this turn) }` |
|
|
53
|
+
| `:before_turn` | Before each turn starts | `{ type: :before_turn, session_id: "...", prompt: "..." (nil on a continue), resumed: true \| false (true on a step-limit Continue, which resumes the run before it; false on every other turn, a reminder's or a wake's too), messages: [...] (the history before this turn) }` |
|
|
54
54
|
| `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model)) }` |
|
|
55
55
|
| `:before_generation` | Before each LLM API call (both loops) | `{ type: :before_generation, iteration: N }` |
|
|
56
56
|
| `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
|
|
57
57
|
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N, restarted: true (only on the first fire after a dropped stream was asked again) }` |
|
|
58
58
|
| `:before_tool_call` | Before tool dispatch (and before `tool_call_started`) | `{ type: :before_tool_call, iteration: N, call: {...}, params: "...", guardrail: Verdict, context: {...}, targets: {...}, blocked: false, block_reason: nil }` |
|
|
59
|
-
| `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars, a cut one ending in a [cut: N of M chars …] line), status: "ok" \| "error" \| "blocked" \| "stopped", call: {...} (the call that ran, as the gate left it), elapsed_ms: N (the tool's own run, without an approval wait; nil for a denied call), result: "..." (the tool's own text: no "[read]" lead, no "ran as:" line, never cut) }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped. Match on `result:`, not `output:`: it is what the tool said (`Error: command timed out after 120s` for an execute killed at its limit, `Samagotchi::Tools::Execute::TIMEOUT_PREFIX`) |
|
|
59
|
+
| `:after_tool_call` | After tool execution | `{ type: :after_tool_call, iteration: N, tool: "read", output: "..." (the text the model gets: capped at max_tool_output_chars, a cut one ending in a [cut: N of M chars …] line), status: "ok" \| "error" \| "blocked" \| "stopped", call: {...} (the call that ran, as the gate left it), elapsed_ms: N (the tool's own run, without an approval wait; nil for a denied call), result: "..." (the tool's own text: no "[read]" lead, no "ran as:" line, never cut) }`; `stopped` is a `task_wait` the turn's Stop ended (the task runs on) or whose task was stopped. Match on `result:`, not `output:`: it is what the tool said (`Error: command timed out after 120s` for an execute killed at its limit in kill mode, `Samagotchi::Tools::Execute::TIMEOUT_PREFIX`; `Still running at the 120s limit: …` for one handed over as a task, `Samagotchi::Tools::Execute::HANDED_OVER`) |
|
|
60
60
|
| `:session_end` | After each turn `:after_turn` fires for, after it (turn-level lifecycle) | `{ type: :session_end, session_id: "..." }` |
|
|
61
61
|
|
|
62
62
|
Every event also carries the hook runtime (next section): `hook:` (the label
|
|
@@ -117,7 +117,7 @@ known-names)` for a bundle hook, `audit.rb (config)` for a config hook,
|
|
|
117
117
|
`turn hook` for one registered at runtime.
|
|
118
118
|
|
|
119
119
|
`event[:notify]` also takes `fallback_for:`, what the line stands in for, so
|
|
120
|
-
that a UI that already shows it can leave the line out. The
|
|
120
|
+
that a UI that already shows it can leave the line out. The main value is
|
|
121
121
|
`:display`: the line repeats what the hook's
|
|
122
122
|
[`event[:present]`](#presenting-the-answer-display-only) display shows (its
|
|
123
123
|
links), for a UI that doesn't render that display. The hook never names a UI;
|
|
@@ -125,7 +125,13 @@ each decides by what it renders. The web with markdown on (`web.markdown` and
|
|
|
125
125
|
the commonmarker gem) leaves such a line out, live and after a reload; with
|
|
126
126
|
markdown off it shows, as it does in the REPL, the attached TUI and
|
|
127
127
|
`chi -p`, which print the answer as text. The log keeps the line with
|
|
128
|
-
`fallback_for=display`.
|
|
128
|
+
`fallback_for=display`. `:editor` marks a line that names a file the viewer
|
|
129
|
+
could open from a ref in its editor: the web leaves it out for a viewer on
|
|
130
|
+
this machine with `web.editor` set, and every other UI and viewer (a phone,
|
|
131
|
+
the terminals) shows it. Name only a file the page shows as a ref (one a
|
|
132
|
+
tool row touched): a loopback viewer with `web.editor` set never sees the
|
|
133
|
+
line, so a file it can't open from a ref would be lost to it. Another value,
|
|
134
|
+
or none, shows the line everywhere.
|
|
129
135
|
Mark a line only when the display really shows all of it (source-links marks
|
|
130
136
|
its note only when the answer links every URL the note names). It needs chi
|
|
131
137
|
0.35.0: an older chi's `event[:notify]` raises `ArgumentError` on the keyword,
|
|
@@ -222,7 +228,7 @@ What a cut is, in detail:
|
|
|
222
228
|
- A Stop from the user right after a cut is a plain cancel.
|
|
223
229
|
- A cut for a message (one sent as a cut, `/cut TEXT` or `chi send --cut`,
|
|
224
230
|
into a generation that has streamed only thinking for `steer.cut_after`
|
|
225
|
-
seconds
|
|
231
|
+
seconds, per host/model: `steer_cut_after`; other messages go in at the next step and never cut)
|
|
226
232
|
is not a plugin's cut: it sends no hidden note and spends no retry; the
|
|
227
233
|
message goes in and the UIs print `↪ cut in for your message`. The
|
|
228
234
|
stream's `generation_completed` says `stopped_by: "steer"`, and
|
|
@@ -507,7 +513,7 @@ Notes:
|
|
|
507
513
|
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
|
|
508
514
|
- Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
509
515
|
- A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
|
|
510
|
-
- Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
|
|
516
|
+
- Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)) and `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)).
|
|
511
517
|
- Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
|
|
512
518
|
|
|
513
519
|
Lifecycle:
|
|
@@ -20,7 +20,14 @@ Behavior:
|
|
|
20
20
|
- `execute`, `task_create` and the `!cmd` shell bang run the command in a non-login shell: `/bin/sh -c` on Linux; on macOS, when `/bin/zsh` is there, `/bin/zsh --emulate sh +o ignore_braces +o sh_glob -o bash_rematch +o bsd_echo -c` (macOS's `/bin/sh` is bash 3.2, which can't parse an apostrophe in a heredoc inside `"$( )"`, the usual commit message shape). That mode reads no rc files and keeps POSIX word splitting, unmatched globs left as is, `{1..3}`, `[[ … =~ (…) ]]` with `BASH_REMATCH`, arrays, `$'…'`, `&>` and echo expanding `\t`. Bash-only builtins and variables (`shopt`, `BASH_VERSION`, `${!prefix*}`, `read -p`) are not there.
|
|
21
21
|
- Task metadata and output are persisted under `tmp/tasks/`.
|
|
22
22
|
- A task record keeps its process's pid and start time (`pid_started`). Before `task_stop` signals, the pid must still lead its own process group and must not have started at another time; otherwise the pid was reused by another process, nothing is signalled and the task ends as failed. A start time that can't be read at that moment (a failed `ps`) falls back to the group check, as does a record without `pid_started` (from an older chi). `task_get`, `task_list`, `task_wait`'s polls and the running-task lists (a cancelled turn's note, a worker's idle exit) make the same check, so a task whose pid was reused reads as exited (failed) at once, not as running until a `task_stop`; on macOS that is one `ps` per running task per check (~5 ms).
|
|
23
|
+
- A stop (`task_stop`, the user's Stop of an `execute`, the execute limit in kill mode) TERMs the task's process group, then KILLs it after a grace. The groups its processes started are stopped too: a GNU `timeout N cmd` (without `--foreground`) moves itself and its command to a group of its own. `ProcessGroup.stop` finds them with one `ps -A -o pid=,ppid=,pgid=` (~40 ms on macOS) before the TERM and again before the KILL, walking down from the group's members. A process whose parent already exited (reparented to init, e.g. a setsid'd `cmd &`, or a backgrounded `timeout N cmd &` after its shell exited) is not found and runs on.
|
|
23
24
|
- Task listing is workspace-scoped (current project only).
|
|
25
|
+
- `execute` runs its command with stdout and stderr in two logs under `$XDG_STATE_HOME/samagotchi/execute/<id>/` (outside the workspace: `git status` and `rm -rf tmp` never see them), stdin `/dev/null` and the same `exit_code` wrapper as a task. Its own copies of the logs hold a shared `flock` across the spawn: while a fresh exclusive `flock` fails, some process the command started still has them open. When the shell exits, a background child still holding them (`server &`) gets 1.5 s, then its group is stopped; one holding none (`nohup server >log 2>&1 &`) is left alone. A holder that left the group (setsid) is out of the stop's reach: it runs on, the result ends with the "left running" hint, and its dir stays for it to write to (the sweep removes it once no one holds the logs). Otherwise a finished command's dir is removed. Known gap on Linux: a command that writes through `/dev/stdout` or `/dev/stderr` (`echo b >/dev/stdout`) reopens the log file itself with `O_TRUNC`, so the output before it is lost (macOS dups the descriptor; the fix would be a relay pipe). The check needs the state dir on a local file system: over NFS, `flock` is emulated with `fcntl` locks, which children don't inherit.
|
|
26
|
+
- The exit code: the command runs after an EXIT trap that writes the shell's status to the `exit_code` file (`TaskRuntime.wrapped_shell_command`, shared with `task_create`), so `exit N`, a `set -e` failure and a trailing `\` or `|` in the command all record what the shell exited with. A command that sets its own EXIT trap replaces it: `execute` still reports the shell's status, and a handed-over one gets its code from chi's waiter while that chi runs; a `task_create` task ends failed with no exit code.
|
|
27
|
+
- A stop of an `execute` (the user's Stop, the limit in kill mode) is done once the shell is reaped and nothing holds the logs: a child that ignores TERM after its shell died gets the KILL.
|
|
28
|
+
- A command still running at `execute.timeout_sec` is handed over as a task (`execute.on_timeout: task`, the default; `kill` stops it instead, and the `!cmd` bang always kills): the result starts `Still running at the 120s limit: moved to a background task, not killed.`, then `task_id: …` and the tails of both logs so far. A timeout during the 1.5 s background grace is a kill (the shell is gone, nothing to hand over). If the record can't be written, the command is killed. `TaskRuntime.adopt` writes a record for the process execute already started, with `handed_over_by: "execute"`. Its logs stay where execute wrote them, so the record's paths are absolute: `output_path` (stdout), `stderr_path` (stderr, a log of its own) and `exit_code_path`. `started_at` (and `created_at`) is the spawn time, not the hand-over's; `pid_started` is read at the hand-over, while execute's detach waiter is still running. From there it is an ordinary task: `task_wait`, `task_get`, `task_list` and `task_stop` treat it as any other. Its dir gets a `task` back-pointer file holding the record's absolute path. Once per process, the first `execute` sweeps `…/execute/`: a dir whose logs no process holds is removed when it has no back-pointer and is a day old (chi died mid-command), when its record is gone, or when it is 14 days old. The records live in each project's `tmp/tasks`, which the state dir can't list: hence the back-pointer.
|
|
29
|
+
- `task_wait` and `task_get` show `stderr_path` when the record has one; where `task_wait` returns an output tail, a `stderr_tail:` of at most 10 lines follows it when that log isn't empty. A `task_create` task has one log (stdout and stderr together) and no `stderr_path`.
|
|
30
|
+
- Which tasks are a session's own (a cancelled turn's note, the turn-end note, a worker's idle exit, the web's stop-task button): the ids its `task_create` results and its handed-over executes name (`TaskRuntime.created_ids_in`, `created_id`). An execute's id is read only after its first line (`Execute::HANDED_OVER`, through `TaskRuntime.handed_over_id`; the web has a JS twin in `task_stop.js`), not from a `task_id:` line inside a run, which the command can print. (A command that prints the `---` separator chi joins a turn's results with, then a hand-over line, still fakes one: a known gap.) Either result may start with the `ran as: …` line of a gate-corrected call.
|
|
24
31
|
- `task_get` returns metadata and `output_path`; use `read` for output contents.
|
|
25
32
|
- `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
|
|
26
33
|
- `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
|