samagotchi 0.23.0 → 0.25.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 +93 -1
- data/docs/architecture.md +16 -9
- data/docs/cli.md +10 -4
- data/docs/configuration.md +30 -13
- data/docs/hooks.md +21 -7
- data/docs/internals/background-tasks.md +1 -0
- data/docs/internals/prompt-caching.md +94 -0
- data/docs/plugins.md +17 -3
- data/docs/sub-agent.md +4 -3
- data/docs/testing.md +12 -0
- data/lib/samagotchi/bridge/card_store.rb +4 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +4 -1
- data/lib/samagotchi/bridge/turn_notice.rb +1 -0
- data/lib/samagotchi/bridge.rb +7 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +2 -1
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +43 -7
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/command_steps.rb +233 -0
- data/lib/samagotchi/config.rb +9 -2
- data/lib/samagotchi/empty_answer_retry.rb +2 -2
- data/lib/samagotchi/engine.rb +103 -5
- data/lib/samagotchi/generation_phase.rb +83 -0
- data/lib/samagotchi/guardrails/shell_lex.rb +158 -6
- data/lib/samagotchi/guardrails/shell_paths.rb +1 -1
- data/lib/samagotchi/idle_client.rb +14 -2
- data/lib/samagotchi/idle_recap.rb +4 -2
- data/lib/samagotchi/kernel_loop.rb +40 -12
- data/lib/samagotchi/llm/chat_loop.rb +37 -13
- data/lib/samagotchi/llm/errors.rb +26 -1
- data/lib/samagotchi/llm/openai_chat.rb +8 -4
- data/lib/samagotchi/llm/prompt_cache.rb +34 -9
- data/lib/samagotchi/llm/usage.rb +18 -3
- data/lib/samagotchi/log_subscriber.rb +5 -3
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/prompt.rb +4 -1
- data/lib/samagotchi/session_metrics.rb +38 -4
- data/lib/samagotchi/steer.rb +103 -8
- data/lib/samagotchi/system_prompt.rb +30 -9
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -3
- data/lib/samagotchi/terminal_ui/attached_view.rb +6 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +17 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +40 -9
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +1 -0
- data/lib/samagotchi/terminal_ui.rb +5 -0
- data/lib/samagotchi/thinking_tails.rb +103 -0
- data/lib/samagotchi/thought_stream_splitter.rb +9 -3
- data/lib/samagotchi/token_usage.rb +18 -6
- data/lib/samagotchi/tool_activity.rb +37 -3
- data/lib/samagotchi/tool_declarations.rb +14 -0
- data/lib/samagotchi/tool_runner.rb +7 -0
- data/lib/samagotchi/tool_view.rb +76 -0
- data/lib/samagotchi/tools/builtins.rb +4 -1
- data/lib/samagotchi/tools/execute.rb +2 -1
- data/lib/samagotchi/tools/registry.rb +11 -4
- data/lib/samagotchi/tools/shell.rb +32 -0
- data/lib/samagotchi/tools/task_runtime.rb +3 -3
- data/lib/samagotchi/turn_tally.rb +17 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/waiting_steer.rb +64 -0
- data/lib/samagotchi/web/message_parts.rb +5 -0
- data/lib/samagotchi/web/public/activity.js +5 -0
- data/lib/samagotchi/web/public/app.js +2 -1
- data/lib/samagotchi/web/public/command_view.js +96 -0
- data/lib/samagotchi/web/public/copy.js +7 -0
- data/lib/samagotchi/web/public/format.js +11 -1
- data/lib/samagotchi/web/public/index.html +33 -0
- data/lib/samagotchi/web/public/stage_model.js +16 -5
- data/lib/samagotchi/web/public/stage_view.js +5 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -1
- data/lib/samagotchi/web/public/turn_view.js +15 -6
- data/lib/samagotchi/worker.rb +4 -1
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6abdccc8a805a545ad1e27898b3caf43516d16ef1b7832444bac7b6e0a4b5e63
|
|
4
|
+
data.tar.gz: 8c0e0f354030f8ad954afdfd9dff96ef54d0b1fdd79d8df190cf494a8a32b18d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36530955a235e3d73adaa4496545c12f6abea21daf1ff0a40eaba9477c22e1e7007dced1a500912878164c0a9f1a9ec76b45d4d8a34709473d8c496b1d53ce69
|
|
7
|
+
data.tar.gz: 125d6ab7dd048082ff6296d663803da9d6a8073c7ead520f2264ec4cc27c28d29bdb772addd47f2be39b9aa497308ef09236bd5e3e1bc43854341970fc8e6a6e
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,96 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.25.0] - 2026-10-04
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- The web shows an `execute` command as the steps a person reads: `cd X &&` becomes an "in X" tag, `| head -20` a
|
|
16
|
+
chip, `echo "=== x ==="` a label, a heredoc a collapsed "EOF · N lines" chip; the collapsed row's title is the
|
|
17
|
+
first step `+N`. A `raw` toggle shows the whole command; anything the parser can't read shows as before.
|
|
18
|
+
- `execute` takes an optional `description` ("what the command does, in a few words"), written by the model; it
|
|
19
|
+
becomes the tool row's title in the web and replaces the cut command in the terminal's tool line. Turn it off
|
|
20
|
+
with `execute.description: false`.
|
|
21
|
+
- Every generation logs its prompt-cache counts (`prompt=`, `cached=`, `cache_write=`), and `/stats` shows cache
|
|
22
|
+
writes next to cached tokens.
|
|
23
|
+
- A generation cut mid-thinking (loop-guard, a plugin's stop, a steer) or ended at the provider's output cap keeps
|
|
24
|
+
the last 20k chars of its thinking in the session's folder, `thinking_tails.jsonl`, so an archived session holds
|
|
25
|
+
the loop it was archived for. The model never sees it.
|
|
26
|
+
- Turn records count `cuts` (generations a plugin cut) and `capped` (generations that hit the output cap), and
|
|
27
|
+
`/stats` shows them as "thinking cuts" and "output cap hits": `retries` counts network retries only.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- On macOS, `execute`, `task_create` and `!cmd` run commands in `/bin/zsh` emulating sh (with bash's `{1..3}`,
|
|
32
|
+
`[[ =~ ]]` groups and echo escapes kept) instead of `/bin/sh`, which is bash 3.2 there and failed on an apostrophe
|
|
33
|
+
in a heredoc inside `"$( )"`: `git commit -m "$(cat <<'EOF' … it's … EOF )"` now works. Linux keeps
|
|
34
|
+
`/bin/sh`; see [background tasks](docs/internals/background-tasks.md).
|
|
35
|
+
- loop-guard's cut notice (bundle 0.3.3) quotes the looping sentence: `thinking repeats itself ("Let me write the
|
|
36
|
+
tool call.", one sentence ×8, 33k chars, 63 s): cut`.
|
|
37
|
+
- The system prompt puts what every session shares first and the per-session lines (model, working directory,
|
|
38
|
+
session) last, and plugin tools come in a fixed order, so a new session reuses the server's prompt cache: on a
|
|
39
|
+
local llama.cpp or Splash server its first answer starts in about 0.5 s instead of 4–6 s, and on Claude via
|
|
40
|
+
OpenRouter a new session reads the shared part from the cache (about 7× cheaper to start).
|
|
41
|
+
- Chat hosts (`api: openai`) are no longer asked for `temperature: 0.0`: chi sends no temperature unless one is
|
|
42
|
+
configured, so each model runs at its provider's default (as native llama.cpp hosts already did). Greedy decoding
|
|
43
|
+
made DeepSeek v4.1-flash loop in its thinking. The idle recap and `/btw` follow suit. Set `sampling: {temperature:
|
|
44
|
+
…}` on a `hosts:` or `models:` entry to pin one; the empty-answer retry still runs at 0.6 unless one is set.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- loop-guard (bundle 0.3.3) cuts thinking that loops in a cycle of 7 or 8 short sentences, or with a longer
|
|
49
|
+
sentence ("I'll write the spec file now.") in the cycle: 48 sentences in a row with 12 or fewer different ones are
|
|
50
|
+
a loop. New settings `thinking.window_sentences` (48) and `thinking.window_distinct` (12).
|
|
51
|
+
- A steer row names chi's senders in words: the web's row and trail flash and the terminal's nudged line say
|
|
52
|
+
`parent agent`, `chi send` and `plugin` instead of the raw `parent_agent` / `chi_send` / `plugin_send`.
|
|
53
|
+
- Thinking off on Gemma 4 no longer warns that off wasn't honoured: its empty thought (whitespace alone) is not
|
|
54
|
+
counted as thinking.
|
|
55
|
+
- An OpenRouter 402 whose `metadata.reason` is `weight_exceeds_budget` (the request alone is larger than the
|
|
56
|
+
key's credit budget) fails at once as `request too large for the credit budget on host <name>: …; lower
|
|
57
|
+
max_tokens (default.max_tokens) or raise the key's credit limit`, instead of being retried as credit held by
|
|
58
|
+
in-flight requests. Other 402s are unchanged.
|
|
59
|
+
|
|
60
|
+
Update with `chi update`: loop-guard moves to 0.3.3 (the window rule and the quoted cut notice). Restart `chi web`
|
|
61
|
+
and running sessions afterwards (`chi sessions restart ID`).
|
|
62
|
+
|
|
63
|
+
## [0.24.0] - 2026-10-04
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- `steer.cut_after` (default 20 s, `0` = never): a message for a running turn from you, `chi send` or a parent
|
|
68
|
+
agent cuts a generation that has streamed only thinking for that long (counted from its first thinking token;
|
|
69
|
+
a message that came earlier cuts once the thinking gets there).
|
|
70
|
+
The model starts the step again with the message, and a `↪ cut in for your message` row marks it. Plugins'
|
|
71
|
+
steers never cut. Only the cut thinking is lost (llama.cpp re-reads the few tokens after its cached prompt).
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- The web shows an `execute` / `task_create` call's full command: an expanded tool row has it as a block above the
|
|
76
|
+
output (whitespace kept, a copy button, `in <cwd>` when the call gave one), and the row's and the stage's hovers
|
|
77
|
+
show the whole command instead of the 80-character `command="…"` line. The terminal is unchanged.
|
|
78
|
+
- A message that joins a running turn (a line typed in the terminal or the web, `chi send -m`, a delegate's
|
|
79
|
+
follow-up, `chi answer --option Continue --text`, a plugin's `ctx.steer`) reaches the model with a one-line
|
|
80
|
+
header naming its sender and asking it to follow the message, or carry on if it asks for nothing, so the model
|
|
81
|
+
can tell who is steering it. Sessions keep the raw text.
|
|
82
|
+
- The `generation_stopped` log line names who cut the generation as `by=` (was `bundle=`), as a message can cut
|
|
83
|
+
it now too.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- Guardrails read heredocs (`cat <<'EOF' … EOF`): a heredoc's body no longer asks needlessly (an `rm -rf` or
|
|
88
|
+
`git push` written into a file by `cat`/`tee`), and an apostrophe in a body no longer hides the commands after it
|
|
89
|
+
from the strict-mode check for git outside the repo. A body fed to `bash`, `sh` and the like still counts.
|
|
90
|
+
- A `chi send -m` or delegate follow-up merged into a running turn is saved with its sender (`source: chi_send` /
|
|
91
|
+
`parent_agent`) instead of reading as the user's own words.
|
|
92
|
+
- The idle recap keeps the text of a `chi answer --option Continue --text` (or a continue card answered with
|
|
93
|
+
text); it was dropped as a plugin's prod.
|
|
94
|
+
- check-in no longer says a nudge was not sent when a user line was merged after it (bundle check-in 0.2.3).
|
|
95
|
+
- A plugin's cut of a generation (loop-guard) with a message or a plugin's steer waiting now delivers it even after
|
|
96
|
+
`retry.empty_answer`'s budget is spent; the turn used to end cancelled and drop a waiting plugin steer.
|
|
97
|
+
|
|
98
|
+
Update with `chi update`: check-in moves to 0.2.3 (the nudge-not-sent fix). Restart `chi web` and running sessions
|
|
99
|
+
afterwards (`chi sessions restart ID`).
|
|
100
|
+
|
|
11
101
|
## [0.23.0] - 2026-10-04
|
|
12
102
|
|
|
13
103
|
### Changed
|
|
@@ -1304,7 +1394,9 @@ and long-lived sessions.
|
|
|
1304
1394
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
1305
1395
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
1306
1396
|
|
|
1307
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
1397
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.25.0...HEAD
|
|
1398
|
+
[0.25.0]: https://github.com/dm1try/samagotchi/compare/v0.24.0...v0.25.0
|
|
1399
|
+
[0.24.0]: https://github.com/dm1try/samagotchi/compare/v0.23.0...v0.24.0
|
|
1308
1400
|
[0.23.0]: https://github.com/dm1try/samagotchi/compare/v0.22.0...v0.23.0
|
|
1309
1401
|
[0.22.0]: https://github.com/dm1try/samagotchi/compare/v0.21.0...v0.22.0
|
|
1310
1402
|
[0.21.0]: https://github.com/dm1try/samagotchi/compare/v0.20.0...v0.21.0
|
data/docs/architecture.md
CHANGED
|
@@ -184,12 +184,18 @@ engine.system_prompt # fully built system prompt string
|
|
|
184
184
|
engine.session # current session (Engine owns create/resume)
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
The system prompt (`SystemPrompt#build`) is the base prompt, then rg guidance,
|
|
188
|
-
AGENT.md, the
|
|
189
|
-
<ref> (host …; model key …)` and a line telling
|
|
190
|
-
are you" from it, not from training), the
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
The system prompt (`SystemPrompt#build`) is the base prompt, then rg guidance, the
|
|
188
|
+
identity and the preloaded memories, AGENT.md, the memory indexes, and last **the
|
|
189
|
+
model** (`Model: this session runs on <ref> (host …; model key …)` and a line telling
|
|
190
|
+
the model to answer "which model are you" from it, not from training), the working
|
|
191
|
+
directory and the session id and log (Gemma's native tool declarations still follow).
|
|
192
|
+
The order is for the servers' prompt caches, which reuse only an exact token prefix:
|
|
193
|
+
what every session shares first, what changes least before what changes more, the
|
|
194
|
+
per-session lines last, so a new session reuses everything above them. It is built
|
|
195
|
+
once per loop and rebuilt only by a model switch (or changed tools), so the model
|
|
196
|
+
line costs no KV churn.
|
|
197
|
+
The full request layout, what breaks the cache and the rules for changes:
|
|
198
|
+
[internals/prompt-caching.md](internals/prompt-caching.md).
|
|
193
199
|
|
|
194
200
|
#### Subscribing to the live stream (and the bridge)
|
|
195
201
|
|
|
@@ -275,11 +281,12 @@ The metrics in `/state`, `/snapshot`, `/stats` and the SSE `snapshot`/`reset` fr
|
|
|
275
281
|
newest turn the worker finished and any it hasn't saved yet (at most 20 turns), with their tool
|
|
276
282
|
calls and the running turn's finished ones. The full history is the session's `analytics.json`,
|
|
277
283
|
which `SessionMetrics#persist` rewrites after each turn; the web server merges the two by id.
|
|
278
|
-
The totals' `tokens` block adds the cached and reasoning sums, the cost, the decode time and tokens
|
|
284
|
+
The totals' `tokens` block adds the cached, cache-write and reasoning sums, the cost, the decode time and tokens
|
|
279
285
|
behind the average speed (`avg_decode_tps`) and the last speeds (`last_decode_tps`,
|
|
280
286
|
`last_prefill_tps`, `tps_source`: `server` or `estimate`). Each `generation_completed` event
|
|
281
|
-
carries the generation's `speed` (`{decode_tps, source}`, null without one)
|
|
282
|
-
`
|
|
287
|
+
carries the generation's `speed` (`{decode_tps, source}`, null without one), its own prompt-cache
|
|
288
|
+
counts (`prompt_tokens`, `cached_tokens`, `cache_write_tokens`: null when the server reported no writes) and
|
|
289
|
+
those running `tokens`: the Engine closes the generation in `SessionMetrics#finish_generation` before relaying
|
|
283
290
|
it, so the web's info bar updates per generation without parsing chunk payloads.
|
|
284
291
|
|
|
285
292
|
The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
data/docs/cli.md
CHANGED
|
@@ -274,7 +274,12 @@ Plain `chi` runs the session in a background worker and attaches the terminal
|
|
|
274
274
|
to it (`session.shared`, default `true`). A worker's session can have any number
|
|
275
275
|
of UIs at once: the Web UI and attached terminals (`chi`, `--resume`,
|
|
276
276
|
`--attach`). They all see the same turns as they happen, and any of them can send
|
|
277
|
-
a prompt, also while a turn runs (it merges into that turn as steering
|
|
277
|
+
a prompt, also while a turn runs (it merges into that turn as steering: the model
|
|
278
|
+
is told it is a steer and who sent it). Steering waits for the model's next step,
|
|
279
|
+
unless the model has streamed only thinking for `steer.cut_after` seconds (default
|
|
280
|
+
20), now or while your message waits: then that generation is cut and the step starts again with your message, marked
|
|
281
|
+
`↪ cut in for your message` (see
|
|
282
|
+
[configuration.md](configuration.md#llama-network-retry-behavior)). The
|
|
278
283
|
first answer to an `ask_user_question` wins; the other UIs close their widget.
|
|
279
284
|
An empty answer dismisses the question in every UI. The model is then told
|
|
280
285
|
not to go ahead with what it asked about, or change anything else, and to wait.
|
|
@@ -415,9 +420,10 @@ file and index line stay as they are; only this session doesn't see it.
|
|
|
415
420
|
`/help` lists the commands this session takes, the installed bundles' too, each
|
|
416
421
|
with a line on what it does. `/stats` shows the session's numbers: turns, tool
|
|
417
422
|
calls (by tool, with errors), iterations, tokens in/out summed over every
|
|
418
|
-
request (with the cached and reasoning tokens when the server reports them),
|
|
419
|
-
the decode speed, generation latency, cancellations, retries
|
|
420
|
-
|
|
423
|
+
request (with the cached tokens, the cache writes and the reasoning tokens when the server reports them),
|
|
424
|
+
the decode speed, generation latency, cancellations, retries (the network's),
|
|
425
|
+
thinking cuts (generations a plugin such as loop-guard cut) and output cap hits
|
|
426
|
+
(generations that ended at the provider's cap), context used and window, the prompt profile and the model the server says it ran, and the
|
|
421
427
|
cost when the provider reports one (OpenRouter's `usage.cost`; no prices are
|
|
422
428
|
guessed). The speed line is the last generation's and the session's average
|
|
423
429
|
(weighted by tokens): llama.cpp reports it exactly (`87 tok/s out, 1.9k tok/s
|
data/docs/configuration.md
CHANGED
|
@@ -369,9 +369,9 @@ after an idle exit) gets that process's environment, so put a lasting choice in
|
|
|
369
369
|
|
|
370
370
|
## Sampling
|
|
371
371
|
|
|
372
|
-
chi
|
|
373
|
-
|
|
374
|
-
entry sets request fields for the model's turns:
|
|
372
|
+
chi sends no sampling fields by default, to chat hosts (`api: openai`) or native ones, so each model runs at its
|
|
373
|
+
provider's defaults (llama.cpp: temperature 0.8; vLLM takes them from the model's `generation_config.json`).
|
|
374
|
+
`sampling:` on a `hosts:` entry or a `models:` entry sets request fields for the model's turns:
|
|
375
375
|
|
|
376
376
|
```yaml
|
|
377
377
|
hosts:
|
|
@@ -394,15 +394,16 @@ models:
|
|
|
394
394
|
- A `models:` entry's fields win over its host's, field by field (the host can set a penalty and the model move only
|
|
395
395
|
the temperature). The entry is found the way `profile:` is (the name as typed, alias-resolved or without its host
|
|
396
396
|
prefix).
|
|
397
|
-
- `temperature: null` (or `~`)
|
|
398
|
-
|
|
397
|
+
- `temperature: null` (or `~`) in a model's entry takes back a host's temperature, so the provider's default applies;
|
|
398
|
+
it also keeps the empty-answer retry (below) at that default.
|
|
399
399
|
- Fields chi sets itself are refused with a warning: `model`, `messages`, `prompt`, `stream`, `stream_options`,
|
|
400
400
|
`tools`, `tool_choice`, `stop`, `n_predict`, `max_tokens`, `n`, `parallel_tool_calls`, `response_format`,
|
|
401
401
|
`cache_prompt`. A `sampling:` that isn't a map warns and is skipped.
|
|
402
|
-
- Greedy decoding can make a
|
|
403
|
-
for minutes) or end with an empty answer. Qwen's own advice for its
|
|
404
|
-
top_p: 0.95
|
|
405
|
-
|
|
402
|
+
- Greedy decoding (`temperature: 0`) can make a thinking model loop in its reasoning ("Let me write the reply…"
|
|
403
|
+
for minutes) or end with an empty answer; set it only for a model you have watched. Qwen's own advice for its
|
|
404
|
+
thinking models is `temperature: 0.6, top_p: 0.95`, with `presence_penalty` between 0 and 2 against endless
|
|
405
|
+
repetition.
|
|
406
|
+
- The idle recap and side questions don't use `sampling:`; they send their own short output cap and no temperature.
|
|
406
407
|
|
|
407
408
|
`/model` shows what applies (`sampling: temperature=0.6 presence_penalty=1.5 (hosts.work)`), and each request's
|
|
408
409
|
`stream` line in the debug log carries a `sampling=` field with what was sent. The fields are read every turn, so a
|
|
@@ -514,10 +515,14 @@ gateway that spreads requests over several providers can key on it to keep one c
|
|
|
514
515
|
prompt caches hit and every turn is answered by the same model. Servers that don't know the header ignore it.
|
|
515
516
|
|
|
516
517
|
Chat requests for a Claude model (an id containing `claude`, such as `anthropic/claude-sonnet-5.5`) on a remote
|
|
517
|
-
OpenAI-compatible host get two prompt-cache breakpoints (`cache_control` on the system
|
|
518
|
-
message), so each step reads the earlier prompt from Anthropic's cache instead of paying for it again
|
|
518
|
+
OpenAI-compatible host get two prompt-cache breakpoints (`cache_control` on the system prompt and on the last
|
|
519
|
+
message), so each step reads the earlier prompt from Anthropic's cache instead of paying for it again. The system
|
|
520
|
+
prompt goes as two text parts, the breakpoint after the first: everything sessions share, then the model, working
|
|
521
|
+
directory and session lines, so a new session reads the shared part from the cache as well; the debug
|
|
519
522
|
log's line shows `cache=on`. Nothing to configure. A provider behind a gateway may still not cache: check the
|
|
520
|
-
cached tokens in `/stats`.
|
|
523
|
+
cached tokens in `/stats`. Each request's own counts are in the debug log's `generation_completed` line:
|
|
524
|
+
`prompt=` (its prompt tokens), `cached=` (read from the server's cache) and `cache_write=` (written to it, when the
|
|
525
|
+
server reports writes); recap and side requests log theirs as `recap request_usage`.
|
|
521
526
|
|
|
522
527
|
## Llama Model Routing
|
|
523
528
|
|
|
@@ -616,6 +621,8 @@ Transient network failures are retried automatically with exponential backoff.
|
|
|
616
621
|
- An HTTP 402 about credit held by in-flight requests (OpenRouter reserves credit for each running request, so
|
|
617
622
|
one session's long request can make another's fail) is retried like a 429, 20s apart unless `Retry-After`
|
|
618
623
|
says otherwise: with the default `retry.max` that is about 100s. With `retry.max: 0` it fails at once.
|
|
624
|
+
Not when its `metadata.reason` is `weight_exceeds_budget`: that request alone is larger than the key's credit
|
|
625
|
+
budget, so it fails at once (lower `default.max_tokens` or raise the key's limit).
|
|
619
626
|
- A refused connection (nothing listening) is not retried: the turn fails at once with `can't reach host <name> at
|
|
620
627
|
<address> (connection refused) — is the server running?`.
|
|
621
628
|
- A stream that has already produced output is never retried (the retry would repeat it); it fails the turn.
|
|
@@ -646,6 +653,14 @@ no made-up answer is saved.
|
|
|
646
653
|
A generation a plugin cuts while it streams (loop-guard's thinking watch, or any `stop_generation`,
|
|
647
654
|
[hooks.md](hooks.md#watching-the-stream)) uses the same budget: `↻ cut by loop-guard, asking again (1/1)`, and with
|
|
648
655
|
none left the turn ends as cancelled (hook).
|
|
656
|
+
A message for a running turn waits for the next step, unless the model's generation has streamed only thinking for
|
|
657
|
+
`steer.cut_after` seconds (default `20`, env `SAMAGOTCHI_STEER_CUT_AFTER`, `0` = never; counted from its first
|
|
658
|
+
thinking token, so the wait for a busy server doesn't count) and still does: then a message from you (the terminal,
|
|
659
|
+
the web), `chi send` or a parent agent cuts that generation, and the model starts the step again with the message
|
|
660
|
+
(`↪ cut in for your message`). A message that comes earlier cuts once the thinking passes `steer.cut_after`, unless
|
|
661
|
+
the step ends first and the message goes in there. Such a cut spends no `retry.empty_answer` attempt and sends no
|
|
662
|
+
hidden note; only the cut thinking is lost. A generation that has streamed visible text or a tool call is never cut,
|
|
663
|
+
and a plugin's steer (`ctx.steer`, `ctx.sessions.send`) never cuts.
|
|
649
664
|
|
|
650
665
|
Assist-mode UX:
|
|
651
666
|
|
|
@@ -663,7 +678,7 @@ message (before, a failed llama.cpp `/completion` ended the turn as
|
|
|
663
678
|
| connection | reset, timed out, dropped mid-stream; refused | yes (network retry), not mid-stream; refused: no |
|
|
664
679
|
| rate limited | HTTP 429 | yes, honouring `Retry-After` |
|
|
665
680
|
| credits held | HTTP 402 "… in-flight requests" | yes, after 20s (or `Retry-After`) |
|
|
666
|
-
| credits | any other HTTP 402: out of credits | no |
|
|
681
|
+
| credits | any other HTTP 402: out of credits; one whose `metadata.reason` is `weight_exceeds_budget` (the request alone is over the key's budget: lower `default.max_tokens`) | no |
|
|
667
682
|
| server | HTTP 5xx, llama.cpp's mid-stream `error:` event | 500/502/503/504/529 only |
|
|
668
683
|
| auth | HTTP 401/403 | no |
|
|
669
684
|
| bad request | other 4xx; a prompt larger than the context window, whatever the status | no |
|
|
@@ -887,6 +902,7 @@ described in their own sections.
|
|
|
887
902
|
| `retry.base_delay` | `0.5` | yes | |
|
|
888
903
|
| `retry.max_delay` | `8.0` | yes | |
|
|
889
904
|
| `retry.empty_answer` | `1` | | Times a turn asks again after an empty answer (at most 3, `0` = off). See "Llama Network Retry Behavior". |
|
|
905
|
+
| `steer.cut_after` | `20` | | Seconds a generation must have streamed only thinking before a message for the running turn (yours, `chi send`'s, a parent agent's) cuts it; `0` = never. See "Llama Network Retry Behavior". |
|
|
890
906
|
| `turn.max_iterations` | `100` | | 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). |
|
|
891
907
|
| `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). |
|
|
892
908
|
| `update.gem` | `true` | | `false`: `chi update` never installs a newer gem (`--no-gem` for one run). See [CLI: Updating](cli.md#updating). |
|
|
@@ -903,6 +919,7 @@ described in their own sections.
|
|
|
903
919
|
| `execute.preview_bytes` | `12288` | yes | |
|
|
904
920
|
| `execute.telemetry_threshold_pct` | `80` | yes | |
|
|
905
921
|
| `execute.timeout_sec` | `120` | yes | Seconds one `execute` command may run before it is stopped. |
|
|
922
|
+
| `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. |
|
|
906
923
|
| `web.port` | `4567` | `--port` | `chi web`'s port. See [CLI](cli.md). |
|
|
907
924
|
| `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). |
|
|
908
925
|
| `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
|
data/docs/hooks.md
CHANGED
|
@@ -115,10 +115,16 @@ known-names)` for a bundle hook, `audit.rb (config)` for a config hook,
|
|
|
115
115
|
`turn hook` for one registered at runtime.
|
|
116
116
|
|
|
117
117
|
A steer is saved in the session as `{role: "user", kind: "steer", source:
|
|
118
|
-
"<bundle>", content: "…"}
|
|
118
|
+
"<bundle>", content: "…"}`, its text raw. The model reads it as a user turn
|
|
119
|
+
led by one line naming the sender: `[Steer from the <bundle> plugin,
|
|
120
|
+
mid-task. Follow it; if it asks for nothing, carry on with the task.]`.
|
|
119
121
|
Its `source` is the hook's bundle (a config or turn hook's label otherwise).
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
Lines typed into a running turn are saved as `{role: "user", kind: "input",
|
|
123
|
+
content: "…"}`: part of that turn, not a turn of their own. They carry a
|
|
124
|
+
`source` when they are not the user's own: `chi_send` (`chi send -m`),
|
|
125
|
+
`parent_agent` (a delegate's follow-up) or `plugin_send` (`ctx.sessions.send`).
|
|
126
|
+
The model reads them with the same kind of header (`[Steer from the user, …]`,
|
|
127
|
+
`[Steer sent with chi send, …]`, and so on).
|
|
122
128
|
|
|
123
129
|
Timing: a notice from `:after_turn` or `:session_end` shows after the turn's
|
|
124
130
|
end line. A question from `:before_tool_call` shows **before** the tool
|
|
@@ -172,19 +178,27 @@ What a cut is, in detail:
|
|
|
172
178
|
|
|
173
179
|
- It is an empty answer made early: it uses the `retry.empty_answer` budget
|
|
174
180
|
([configuration.md](configuration.md#llama-network-retry-behavior)). With
|
|
175
|
-
no retry left (`retry.empty_answer: 0`, or already used)
|
|
176
|
-
cancelled (hook): "✕ turn canceled (hook)".
|
|
181
|
+
no retry left (`retry.empty_answer: 0`, or already used) and nothing
|
|
182
|
+
queued, the turn ends cancelled (hook): "✕ turn canceled (hook)".
|
|
177
183
|
- It shows nothing by itself: post your own notice (`event[:notify]`) to say
|
|
178
184
|
why. The bundle and the reason go into the note the model reads and the
|
|
179
185
|
log (`generation_stopped`).
|
|
180
186
|
- Queued input (the user's line, or a hook's `steer`) goes in place of the
|
|
181
|
-
hidden note,
|
|
187
|
+
hidden note, also once the retry budget is spent, and spends no retry;
|
|
188
|
+
then there is no `↻` line.
|
|
182
189
|
- The cut generation is not kept: its thinking and any visible text it had
|
|
183
190
|
streamed go with it (the retry answers anew). `:after_generation` doesn't
|
|
184
191
|
fire for it, and its token usage is lost (the stream never sent its last
|
|
185
192
|
chunk). A client that joins the running turn still sees its thinking in
|
|
186
193
|
the turn so far until the turn ends.
|
|
187
194
|
- A Stop from the user right after a cut is a plain cancel.
|
|
195
|
+
- A cut for a message (a user's, `chi send`'s or a parent agent's line into
|
|
196
|
+
a generation that has streamed only thinking for `steer.cut_after` seconds)
|
|
197
|
+
is not a plugin's cut: it sends no hidden note and spends no retry; the
|
|
198
|
+
message goes in and the UIs print `↪ cut in for your message`. The
|
|
199
|
+
stream's `generation_completed` says `stopped_by: "steer"`, and
|
|
200
|
+
`stop_generation` returns false for a generation a message already cut.
|
|
201
|
+
A plugin's `steer` never cuts.
|
|
188
202
|
|
|
189
203
|
## Presenting the answer (display only)
|
|
190
204
|
|
|
@@ -385,7 +399,7 @@ stripped of surrounding whitespace, except file text, an edit's
|
|
|
385
399
|
|
|
386
400
|
| Tool | `content:` from | `path:` from | Own fields |
|
|
387
401
|
|------|-----------------|--------------|------------|
|
|
388
|
-
| `execute` | `command` | — | `cwd` |
|
|
402
|
+
| `execute` | `command` | — | `description`, `cwd` |
|
|
389
403
|
| `read` | `path` | — | `start_line`, `end_line` |
|
|
390
404
|
| `write` | — | `path` | `content` |
|
|
391
405
|
| `edit` | — | `path` | `old_text`, `new_text`, `start_line`, `end_line` |
|
|
@@ -17,6 +17,7 @@ Recommended workflow:
|
|
|
17
17
|
|
|
18
18
|
Behavior:
|
|
19
19
|
|
|
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.
|
|
20
21
|
- Task metadata and output are persisted under `tmp/tasks/`.
|
|
21
22
|
- Task listing is workspace-scoped (current project only).
|
|
22
23
|
- `task_get` returns metadata and `output_path`; use `read` for output contents.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Prompt caching: how a request is laid out, and how to keep it cache-friendly
|
|
2
|
+
|
|
3
|
+
A model server reuses work only for the **exact token prefix** it has already seen. Change one token and
|
|
4
|
+
everything after it is computed again (prefill). The local models we run (hybrid Qwen 3.6 / Ornith on llama.cpp
|
|
5
|
+
and Splash) can only resume from a **restore point near the end** of an earlier prompt, not from any shared
|
|
6
|
+
prefix. Anthropic (Claude via OpenRouter) caches only up to an explicit `cache_control` breakpoint. So the order
|
|
7
|
+
of what chi sends decides what a new turn, or a new session, has to pay for.
|
|
8
|
+
|
|
9
|
+
Measured with prompt-cache slice 1 (2026-10-04, fresh sessions, ~7.7k-token prompt):
|
|
10
|
+
|
|
11
|
+
| Server | Before | After |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Ornith on llama.cpp | 0 tokens reused, 6.4 s prefill per session | 94% reused, 0.5 s |
|
|
14
|
+
| Splash Qwen 3.6 | 3.8 s | 97% reused from the 3rd session, 0.27 s |
|
|
15
|
+
| Sonnet via OpenRouter | full cache write, $0.024 per session | reads the stable part, $0.0035 |
|
|
16
|
+
|
|
17
|
+
## The layers of a request
|
|
18
|
+
|
|
19
|
+
From the first token to the last. Every layer is a prefix of the next request's layers, unless something in the
|
|
20
|
+
right-hand column happens.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
shared by every session ────────────────────────────────────────────────── breaks it
|
|
24
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
25
|
+
│ tools built-ins, then plugin tools sorted by (bundle, name) │ a tool added/changed,
|
|
26
|
+
│ (chat: the `tools` field; native: the model's template) │ /model, a profile change
|
|
27
|
+
├──────────────────────────────────────────────────────────────────────────┤
|
|
28
|
+
│ SYSTEM, stable part SystemPrompt#system_prompt_with_index
|
|
29
|
+
│ [native thinking token] base prompt │
|
|
30
|
+
│ rg guidance │ /thinking level,
|
|
31
|
+
│ system identity (+ model overlay) │ /model, editing a
|
|
32
|
+
│ explicit (preloaded) memories │ memory / AGENT.md
|
|
33
|
+
│ AGENT.md │ (picked up by a new
|
|
34
|
+
│ project + system memory indexes │ process or a rebuild)
|
|
35
|
+
│ ◄── remote Claude: cache_control breakpoint #1 │
|
|
36
|
+
├──────────────────────────────────────────────────────────────────────────┤
|
|
37
|
+
│ SYSTEM, volatile tail (per session, least → most changing) │
|
|
38
|
+
│ Model: this session runs on … │ another session
|
|
39
|
+
│ working directory (location) │ (expected: only
|
|
40
|
+
│ session id + log │ this tail differs)
|
|
41
|
+
│ [Gemma 4 native only: tool declarations after all of it] │
|
|
42
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
43
|
+
shared by every turn of this session ─────────────────────────────────────
|
|
44
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
45
|
+
│ history user / assistant / tool messages, APPEND-ONLY │ !rollback, a failed or
|
|
46
|
+
│ (images: the newest N, ImagePlan) │ cancelled turn's tail,
|
|
47
|
+
│ ◄── remote Claude: breakpoint #2 on the last │ the image window
|
|
48
|
+
│ non-system message │ sliding, history
|
|
49
|
+
├──────────────────────────────────────────────────────────────────────────┤ sanitizing (thinking
|
|
50
|
+
│ tail notes reminders, turn notes, steers (system/user messages) │ stripped per spec)
|
|
51
|
+
│ new prompt this turn's user message │
|
|
52
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- The system prompt is built **once per loop** and rebuilt only on `@prompt_builder.reset!`: a model switch
|
|
56
|
+
(`Engine#switch_model!`), a plugin's `tools_changed!`, or a profile change. A per-turn rebuild would
|
|
57
|
+
break everything below it.
|
|
58
|
+
- On remote Claude, `LLM::PromptCache.mark` sends the system message as two text parts. The stable part carries
|
|
59
|
+
breakpoint #1, so a **new session** reads it from the cache. The last non-system message carries breakpoint
|
|
60
|
+
#2, so the **next request in the session** reads everything up to it. Mid and tail system messages never take
|
|
61
|
+
a breakpoint. The split point comes from `SystemPrompt#stable_length` via the message's `cache_split:`, which
|
|
62
|
+
never goes on the wire.
|
|
63
|
+
- Side requests (idle recap, `/btw`) go through `IdleClient` with their own short system prompt. On a
|
|
64
|
+
single-slot local server they can push the session's prefix out of the cache. Fixing that is slice 2 of the
|
|
65
|
+
plan (R4 `id_slot`, R2 turn-end warm-up).
|
|
66
|
+
|
|
67
|
+
## Rules for changes
|
|
68
|
+
|
|
69
|
+
1. **Stable first, volatile last.** Anything that differs between sessions or turns (ids, paths, times, the
|
|
70
|
+
model line, counters) goes at the very end of the system prompt, ordered by how often it changes. Never put
|
|
71
|
+
a per-session value in the stable part. A guard spec checks that model, location and session are the last
|
|
72
|
+
sections (`spec/system_prompt_model_spec.rb`).
|
|
73
|
+
2. **History is append-only.** Never rewrite an earlier message. Add a note at the tail instead. If a model's
|
|
74
|
+
spec forces a rewrite (e.g. stripping previous-turn thinking), accept the re-prefill and note it.
|
|
75
|
+
3. **Deterministic order.** Tools, memory lists and anything else that's rendered from a hash or a set needs a
|
|
76
|
+
fixed sort. Plugin tools are sorted by (bundle, name) whatever order their plugins finished starting in
|
|
77
|
+
(R5a).
|
|
78
|
+
4. **Don't rebuild the system prompt per turn.** Rebuild only when its content really changed (`reset!`).
|
|
79
|
+
5. **Measure.** Every generation logs `prompt=`, `cached=` and, where the provider reports it, `cache_write=`
|
|
80
|
+
on its `generation_completed` log line. `/stats` shows "cached N (P%)" and "cache writes". A change that
|
|
81
|
+
drops `cached=` on the second fresh session is a regression. Update the prompt snapshot fixtures with
|
|
82
|
+
`UPDATE_PROMPTS=1` and read the diff: a cache-friendly change moves sections, it doesn't reorder per run.
|
|
83
|
+
|
|
84
|
+
## Server-side settings worth knowing
|
|
85
|
+
|
|
86
|
+
- **Splash:** `--persistent-cache` (with `--max-cache-disk`) keeps restore points across restarts. Splash learns
|
|
87
|
+
a new branch point on the 2nd session, so the 3rd one gets the full reuse.
|
|
88
|
+
- **llama.cpp:** a single slot is shared by everything sent to it. `--parallel N` and `--cache-ram` give side
|
|
89
|
+
requests room without evicting the session. `--ctx-checkpoints` controls how many restore points a hybrid
|
|
90
|
+
model keeps.
|
|
91
|
+
- **OpenRouter / Anthropic:** the default cache TTL is 5 minutes. Provider routing (which upstream serves a
|
|
92
|
+
request) also decides whether a cache is there to hit.
|
|
93
|
+
|
|
94
|
+
Still to come: side requests on their own slot, a turn-end warm-up, image-window batches and longer remote TTLs.
|
data/docs/plugins.md
CHANGED
|
@@ -490,7 +490,7 @@ full label, `plugin.rb (bundle my-bundle)`.
|
|
|
490
490
|
| `ctx.card(title:, body: "", actions: [], level: :info, id: nil)` | a card in every UI, returning its id: see [Cards](#cards) |
|
|
491
491
|
| `ctx.ask_user(question:, options:, header: nil, allow_freeform: false)` | a single-select question through the question flow: `{selected:, freeform:, selected_indices:}`, or nil (no one to ask, cancelled, bad options) |
|
|
492
492
|
| `ctx.cancelled?` | whether the running turn was cancelled (a long tool should stop) |
|
|
493
|
-
| `ctx.steer(text)` | put text into the running turn: its own user message at the loop's next boundary, shown as `my-bundle> nudged: …`. Returns true when queued, false with no turn running (it never starts one; that is `ctx.sessions`' send). Dropped (logged) if the model answers or the turn ends first. Safe from any thread: an anytime command, a hook (false from `:after_turn`), your own |
|
|
493
|
+
| `ctx.steer(text)` | put text into the running turn: its own user message at the loop's next boundary, shown as `my-bundle> nudged: …`. The model reads it with a header naming your bundle (`[Steer from the my-bundle plugin, mid-task. …]`); the session keeps the raw text. Returns true when queued, false with no turn running (it never starts one; that is `ctx.sessions`' send). Dropped (logged) if the model answers or the turn ends first. Safe from any thread: an anytime command, a hook (false from `:after_turn`), your own |
|
|
494
494
|
| `ctx.stop_turn(reason)` | stop the running turn after a warn notice with the reason; true when it stopped one now. In a `:before_tool_call` block it also denies that call |
|
|
495
495
|
| `ctx.stop_generation(reason)` | cut the generation that is streaming: the turn goes on and the model is asked again ([hooks.md](hooks.md#watching-the-stream)). Shows nothing: post your own notice. True when it cut one now |
|
|
496
496
|
| `ctx.ask_model(messages:, prompt:, …)` | a side answer from the session's model: see [Side answers](#side-answers-ctxask_model) |
|
|
@@ -847,14 +847,20 @@ stream](hooks.md#watching-the-stream)) and cuts it:
|
|
|
847
847
|
thinking has short sentences too ("Hmm.", "Fine."), but spread out or all
|
|
848
848
|
different (code lines); a longer sentence ends the run, and lines inside a
|
|
849
849
|
```` ``` ```` code block don't count.
|
|
850
|
+
- Any `window_sentences` sentences in a row, short or long, holding at most
|
|
851
|
+
`window_distinct` different ones are a loop too: a cycle of 7 or 8 short
|
|
852
|
+
sentences, or one where a longer sentence ("I'll write the spec file
|
|
853
|
+
now.") keeps ending the short run. Real thinking holds 40 or more
|
|
854
|
+
different sentences in any 48; DeepSeek's write loops hold 4 to 9.
|
|
850
855
|
- Nothing triggers before `min_chars` of thinking. The watch sees a loop
|
|
851
856
|
within one batch (2000 chars, or a second) of its third cycle.
|
|
852
857
|
|
|
853
858
|
What happens (`action: retry`, the default):
|
|
854
859
|
|
|
855
860
|
1. The first loop in a turn: the generation is cut and the model asked
|
|
856
|
-
again, with a hidden note that it was cut off. The user sees
|
|
857
|
-
|
|
861
|
+
again, with a hidden note that it was cut off. The user sees the loop's
|
|
862
|
+
first sentence quoted,
|
|
863
|
+
`loop-guard> thinking repeats itself ("Wait, the count of the letter r…", 3 sentences ×3, 4k chars, 8 s): cut`,
|
|
858
864
|
and `↻ cut by loop-guard, asking again (1/1)`.
|
|
859
865
|
2. If the retry loops too, the turn is stopped ("stopped the turn: …", "✕
|
|
860
866
|
turn stopped by loop-guard") with a card that quotes the repeated
|
|
@@ -869,6 +875,12 @@ the first loop ends the turn as stopped by loop-guard, with the notice and
|
|
|
869
875
|
no card. `action: stop` stops the turn at the first loop; `action: notify` only
|
|
870
876
|
warns, once per generation.
|
|
871
877
|
|
|
878
|
+
The cut thinking never goes back to the model, but its last 20k chars are kept
|
|
879
|
+
in the session's folder, `thinking_tails.jsonl` (with a generation that ran to
|
|
880
|
+
the provider's output cap, or that a stop_turn ended): an archived session
|
|
881
|
+
keeps them. A turn's record counts its cuts and cap hits (`cuts`, `capped` in
|
|
882
|
+
`analytics.json`; `/stats` shows "thinking cuts" and "output cap hits").
|
|
883
|
+
|
|
872
884
|
```yaml
|
|
873
885
|
bundles:
|
|
874
886
|
loop-guard:
|
|
@@ -885,6 +897,8 @@ bundles:
|
|
|
885
897
|
min_words: 5 # shorter sentences only count toward a short run
|
|
886
898
|
short_run: 24 # this many short sentences in a row…
|
|
887
899
|
short_distinct: 6 # …with at most this many different ones is a loop
|
|
900
|
+
window_sentences: 48 # any this many sentences in a row…
|
|
901
|
+
window_distinct: 12 # …with at most this many different ones is a loop
|
|
888
902
|
forget_after: 10 # good steps in a row that forget a loop; 0: never
|
|
889
903
|
```
|
|
890
904
|
|
data/docs/sub-agent.md
CHANGED
|
@@ -60,9 +60,10 @@ answers waits as a question of kind `continue` (`"limit": 100`, options
|
|
|
60
60
|
`Continue` and `Stop`). `answer_with` continues it:
|
|
61
61
|
`chi answer ID --question QID --option Continue`, and the continued turn's
|
|
62
62
|
reply (or its next question) comes back on that same wait. With
|
|
63
|
-
`--text "…"` the text joins the continued turn as a steer
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
`--text "…"` the text joins the continued turn as a steer, labelled for the
|
|
64
|
+
model as coming from the parent agent that started the session, so one answer
|
|
65
|
+
continues and redirects it. (A `chi send -m` into a running turn joins it too,
|
|
66
|
+
labelled as sent with chi send.) To stop it:
|
|
66
67
|
`--option Stop`, with `--text "why"` for the model;
|
|
67
68
|
that wait ends with `status: not_continued`, exit 0, and the turn's work so far
|
|
68
69
|
stays in the session. A message instead (`chi send ID -m "…"`) drops the
|
data/docs/testing.md
CHANGED
|
@@ -15,6 +15,18 @@ bug) or a broken spec: fix it, or disable that line with the reason (`# rubocop:
|
|
|
15
15
|
No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
|
|
16
16
|
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
|
|
17
17
|
|
|
18
|
+
## Worktrees
|
|
19
|
+
|
|
20
|
+
Each fix or feature gets its own worktree next to the main checkout:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
script/worktree NAME [-b BRANCH] # ../samagotchi-NAME off main, branch NAME unless -b; prints the path
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
It runs `git worktree add ../samagotchi-NAME -b BRANCH main`, then `npm ci --prefer-offline` in it, so
|
|
27
|
+
`npm test` and `npm run e2e` work there right away (Ruby gems and Playwright's browsers are shared: nothing else
|
|
28
|
+
to install). It refuses when the folder or the branch already exists.
|
|
29
|
+
|
|
18
30
|
## Integration specs
|
|
19
31
|
|
|
20
32
|
Examples tagged `:integration` (`spec/integration/`, the "with LLM access" part of `spec/hooks/integration_spec.rb`)
|
|
@@ -16,7 +16,7 @@ module Samagotchi
|
|
|
16
16
|
# iteration started before it (a before_tool_call hook's notice comes
|
|
17
17
|
# before its call's row, so the web puts it above row calls + 1). The
|
|
18
18
|
# loop's "asking again" row (:empty_answer_retry, after an empty or cut
|
|
19
|
-
# answer)
|
|
19
|
+
# answer) and a steer's cut row (:steer_cut) are kept the same way, and so is a question the turn asked
|
|
20
20
|
# (:question_requested), with how it was answered or cancelled, so a
|
|
21
21
|
# reload draws the resolved card where it was.
|
|
22
22
|
#
|
|
@@ -46,11 +46,11 @@ module Samagotchi
|
|
|
46
46
|
# onto a new worker doesn't print those again).
|
|
47
47
|
class CardStore
|
|
48
48
|
CAPACITY = 20
|
|
49
|
-
NOTICE_TYPES = %i[hook_notice guardrail_warning empty_answer_retry].freeze
|
|
49
|
+
NOTICE_TYPES = %i[hook_notice guardrail_warning empty_answer_retry steer_cut].freeze
|
|
50
50
|
FILE = "cards.json"
|
|
51
51
|
# The events after which the file is written again: the ones that
|
|
52
52
|
# change an entry or the turn count.
|
|
53
|
-
SAVED_ON = (%i[card hook_notice empty_answer_retry question_requested question_answered question_cancelled
|
|
53
|
+
SAVED_ON = (%i[card hook_notice empty_answer_retry steer_cut question_requested question_answered question_cancelled
|
|
54
54
|
question_relay turn_failed] + Events::TURN_KEPT).freeze
|
|
55
55
|
|
|
56
56
|
# What a session's saved file lists, as a store started from it lists
|
|
@@ -167,6 +167,7 @@ module Samagotchi
|
|
|
167
167
|
when :guardrail_warning then push({ type: :guardrail_warning, message: event[:message], label: event[:label] }
|
|
168
168
|
.compact.merge(in_turn: false, turns: @turns_done))
|
|
169
169
|
when :empty_answer_retry then add_turn_row(event.slice(:type, :attempt, :of, :stopped_by)) if @running
|
|
170
|
+
when :steer_cut then add_turn_row(event.slice(:type, :source)) if @running
|
|
170
171
|
when :question_requested
|
|
171
172
|
add_turn_row({ type: :question, pending_question: Marshal.load(Marshal.dump(event[:pending_question])) }) if @running
|
|
172
173
|
when :question_answered then resolve_question(event[:id], answer: event[:answer])
|
|
@@ -158,7 +158,8 @@ module Samagotchi
|
|
|
158
158
|
call = { iteration: part[:iteration], call_index: part[:call_index], tool: part[:tool] }
|
|
159
159
|
call[:label] = part[:label] if part[:label]
|
|
160
160
|
title = part[:title] ? { title: part[:title] } : {}
|
|
161
|
-
|
|
161
|
+
view = part[:view] ? { view: part[:view] } : {}
|
|
162
|
+
events = [{ type: :tool_call_started, **call, params: part[:params], **title, **view }]
|
|
162
163
|
if part[:status] == "running"
|
|
163
164
|
events.first[:elapsed_ms] = part[:elapsed_ms] unless part[:elapsed_ms].nil?
|
|
164
165
|
return events
|
|
@@ -170,6 +171,7 @@ module Samagotchi
|
|
|
170
171
|
activity: activity }
|
|
171
172
|
completed[:duration_ms] = part[:duration_ms] unless part[:duration_ms].nil?
|
|
172
173
|
completed[:diff] = part[:diff] if part[:diff]
|
|
174
|
+
completed.merge!(view)
|
|
173
175
|
events << with_images(completed, part[:images])
|
|
174
176
|
end
|
|
175
177
|
private_class_method :tool_replay_events
|
|
@@ -253,6 +255,7 @@ module Samagotchi
|
|
|
253
255
|
tool: event[:tool], params: event[:params], status: "running" }
|
|
254
256
|
part[:label] = event[:label] if event[:label]
|
|
255
257
|
part[:title] = event[:title] if event[:title]
|
|
258
|
+
part[:view] = event[:view] if event[:view]
|
|
256
259
|
parts << part
|
|
257
260
|
@tool_started_at[[event[:iteration], event[:call_index]]] = @clock.call
|
|
258
261
|
when :tool_call_completed
|
|
@@ -12,6 +12,7 @@ module Samagotchi
|
|
|
12
12
|
FIELDS = {
|
|
13
13
|
hook_notice: %i[hook text level],
|
|
14
14
|
empty_answer_retry: %i[iteration attempt of stopped_by],
|
|
15
|
+
steer_cut: %i[iteration source],
|
|
15
16
|
question_requested: %i[pending_question],
|
|
16
17
|
question_answered: %i[id answer],
|
|
17
18
|
question_cancelled: %i[id reason]
|