samagotchi 0.54.0 → 0.56.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 +50 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +6 -2
- data/docs/cli.md +29 -7
- data/docs/configuration.md +113 -8
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/context-telemetry.md +23 -5
- data/docs/internals/engine.md +54 -18
- data/docs/internals/prompt-caching.md +8 -0
- data/docs/sessions.md +2 -2
- data/docs/testing.md +23 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +6 -6
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cache_confirm.rb +51 -0
- data/lib/samagotchi/client_id.rb +12 -0
- data/lib/samagotchi/config.rb +9 -3
- data/lib/samagotchi/engine.rb +102 -8
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/file_ref.rb +59 -0
- data/lib/samagotchi/host_config.rb +6 -3
- data/lib/samagotchi/host_registry.rb +4 -3
- data/lib/samagotchi/kernel_loop.rb +99 -16
- data/lib/samagotchi/llm/chat_loop.rb +122 -71
- data/lib/samagotchi/llm/openai_chat.rb +2 -1
- data/lib/samagotchi/llm/stream_drops.rb +63 -0
- data/lib/samagotchi/llm/turn_settings.rb +9 -5
- data/lib/samagotchi/model_settings.rb +5 -2
- data/lib/samagotchi/reasoning_history.rb +132 -0
- data/lib/samagotchi/reasoning_history_command.rb +107 -0
- data/lib/samagotchi/self_report.rb +21 -0
- data/lib/samagotchi/session.rb +23 -2
- data/lib/samagotchi/session_commands.rb +55 -15
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_setup.rb +7 -5
- data/lib/samagotchi/setting_explained.rb +20 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +2 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +11 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
- data/lib/samagotchi/terminal_ui.rb +13 -6
- data/lib/samagotchi/thinking.rb +20 -25
- data/lib/samagotchi/thinking_command.rb +33 -11
- data/lib/samagotchi/tool_row_fields.rb +46 -0
- data/lib/samagotchi/tool_runner.rb +7 -11
- data/lib/samagotchi/tools/builtins.rb +6 -2
- data/lib/samagotchi/tools/peers.rb +4 -2
- data/lib/samagotchi/tools/task_runtime.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +48 -4
- data/lib/samagotchi/web/capabilities.rb +13 -9
- data/lib/samagotchi/web/editor.rb +38 -0
- data/lib/samagotchi/web/message_parts.rb +5 -10
- data/lib/samagotchi/web/public/activity.js +33 -18
- data/lib/samagotchi/web/public/app.js +256 -1133
- data/lib/samagotchi/web/public/commands.js +269 -0
- data/lib/samagotchi/web/public/composer.js +105 -0
- data/lib/samagotchi/web/public/diff_view.js +30 -4
- data/lib/samagotchi/web/public/index.html +11 -3
- data/lib/samagotchi/web/public/info_bar.js +80 -0
- data/lib/samagotchi/web/public/level_chip.js +174 -0
- data/lib/samagotchi/web/public/lists_view.js +725 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
- data/lib/samagotchi/web/public/model_pick.js +1 -1
- data/lib/samagotchi/web/public/question_cards.js +2 -1
- data/lib/samagotchi/web/public/reasoning_history_chip.js +49 -0
- data/lib/samagotchi/web/public/refs.js +134 -0
- data/lib/samagotchi/web/public/restore.js +81 -0
- data/lib/samagotchi/web/public/session_state.js +57 -0
- data/lib/samagotchi/web/public/stream_handlers.js +332 -0
- data/lib/samagotchi/web/public/thinking_chip.js +38 -144
- data/lib/samagotchi/web/public/turn_events.js +17 -258
- data/lib/samagotchi/web/public/turn_html.js +5 -1
- data/lib/samagotchi/web/server.rb +4 -4
- data/lib/samagotchi/worker.rb +17 -5
- metadata +18 -2
- data/lib/samagotchi/web/public/stable_html.js +0 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 242e670188e9ae58b573c067f949fa0de43e15f6f5f10bcc11ea66a087b16bb8
|
|
4
|
+
data.tar.gz: e6bddbb6238764a871c1bcce826b92472ea7eb6e814ecdf399eb171ec9260c67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf35534c163ba8eb189e85b72ed03b687f203c5f3423909f46fbc358fda45ef1bde4716dd93c368c8a0eceb13fb5044a12250c23fa89eb8f6ee7ebf72857b614
|
|
7
|
+
data.tar.gz: f81d67ea98da8dbe42a2553a02a957cc2ecd70129c474fe2295f64881e980792cbc9ea446f11b338c255213f3b52976f1d380e5e3f5835f10ea475fbaa541e4d
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,53 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.56.0] - 2026-10-11
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `reasoning_history` (`drop`, `turn`, `all`) on a `models:` or `hosts:` entry, and `SAMAGOTCHI_REASONING_HISTORY`
|
|
16
|
+
for a whole process: which of its own reasoning a chat host's model gets back. `/model` names a configured level
|
|
17
|
+
and `chi self` shows it.
|
|
18
|
+
- `/reasoning-history drop|turn|all|default` sets a session's own level (saved; continue and fork copy it); `/model`,
|
|
19
|
+
`/stats` and the web's session bar (a `reasoning` chip, on chat hosts) show it with its source.
|
|
20
|
+
- `/thinking` and `/reasoning-history` ask before a change that makes the server read the conversation again (Gemma,
|
|
21
|
+
hosted APIs, a change to or from `all`); `--yes` applies at once. The terminal puts the `--yes` line in the next
|
|
22
|
+
input, the web's reply has an Apply button; `chi send`, `chi -p … --non-interactive` and piped lines apply directly.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- A chat host (`api: openai`) now gets the running turn's reasoning back: each of the turn's earlier replies carries its
|
|
27
|
+
reasoning as `reasoning_content`, as Qwen's, Gemma 4's and DeepSeek's templates and the hosted APIs expect
|
|
28
|
+
(`reasoning_history: turn`, the default). A local Qwen thought ~8x less per request with it. `drop` sends none (as
|
|
29
|
+
before), `all` sends every reply's, earlier turns too. A host that refuses it (a 400 about reasoning) gets the step
|
|
30
|
+
again without it, and that model sends none from then on (a notice says so once). See "Reasoning history" in
|
|
31
|
+
docs/configuration.md.
|
|
32
|
+
- `/thinking`'s cache note now tells a local chat server (only the tail; the whole prompt for a Gemma model) from a
|
|
33
|
+
hosted one (the whole conversation). The web's thinking chip sends its level with `--yes`.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- A local llama.cpp turn whose connection dropped mid-answer now restarts that step (up to twice) instead of failing.
|
|
38
|
+
|
|
39
|
+
## [0.55.0] - 2026-10-10
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- Web: refs. A file tool's row (read, edit, write) and the lines of its diff open the file in your editor: they look
|
|
44
|
+
like text until hovered (a dotted underline and `↗`, a diff line's `↗ N`), and a click opens the current file at the
|
|
45
|
+
line read or edited, the diff line's line or the hunk's start. Only a viewer on this machine gets them (a loopback
|
|
46
|
+
request, or this machine on its LAN address); the phone sees plain text. `web.editor` picks the editor: `vscode`
|
|
47
|
+
(the default), `vscode-insiders`, `cursor`, `zed`, `none`, or a URL template with `{path}` and `{line}`. The
|
|
48
|
+
browser and VS Code each ask once to allow it.
|
|
49
|
+
- For development: `script/affected-specs`, the spec files to run for a change (changed specs, name matches,
|
|
50
|
+
docs-reading specs, specs naming the changed class), ranked and capped. It isn't part of the gem. See
|
|
51
|
+
[docs/testing.md](docs/testing.md).
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Web: a page that joins mid-turn (or reloads during one) keeps a grep that found nothing as a muted "no match" row,
|
|
56
|
+
as it showed live; it used to lose the mark.
|
|
57
|
+
|
|
11
58
|
## [0.54.0] - 2026-10-10
|
|
12
59
|
|
|
13
60
|
### Changed
|
|
@@ -2442,7 +2489,9 @@ and long-lived sessions.
|
|
|
2442
2489
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2443
2490
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2444
2491
|
|
|
2445
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2492
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.56.0...HEAD
|
|
2493
|
+
[0.56.0]: https://github.com/dm1try/samagotchi/compare/v0.55.0...v0.56.0
|
|
2494
|
+
[0.55.0]: https://github.com/dm1try/samagotchi/compare/v0.54.0...v0.55.0
|
|
2446
2495
|
[0.54.0]: https://github.com/dm1try/samagotchi/compare/v0.53.0...v0.54.0
|
|
2447
2496
|
[0.53.0]: https://github.com/dm1try/samagotchi/compare/v0.52.0...v0.53.0
|
|
2448
2497
|
[0.52.0]: https://github.com/dm1try/samagotchi/compare/v0.51.0...v0.52.0
|
data/bin/chi
CHANGED
|
@@ -137,7 +137,7 @@ parser = OptionParser.new do |opts|
|
|
|
137
137
|
"every session outside a repo) or all") { |v| options[:scope] = v }
|
|
138
138
|
# Accepted values of string entries that the registry doesn't type as enums.
|
|
139
139
|
value_hints = { "status.line" => "on|off", "web.host" => "127.0.0.1|::1|localhost|lan|IP", "recap.sentences" => "N|N-M",
|
|
140
|
-
"web.annotate_presets" => "text|text" }
|
|
140
|
+
"web.annotate_presets" => "text|text", "web.editor" => "vscode|vscode-insiders|cursor|zed|none|URL" }
|
|
141
141
|
# Universal config flags (implicit convention: ENV SAMAGOTCHI_* ↔ YAML dotted ↔ CLI --kebab)
|
|
142
142
|
# Generated from Samagotchi::Config registry — Option A: snake leaf in YAML, kebab in CLI.
|
|
143
143
|
Samagotchi::Config.cli_entries.each do |entry|
|
|
@@ -235,7 +235,8 @@ if options[:web]
|
|
|
235
235
|
view = Samagotchi::Config.get("web.view")
|
|
236
236
|
exit Samagotchi::Web::Server.launch(port: port, scope: options[:scope] || "project", open_browser: options[:web_open],
|
|
237
237
|
markdown: markdown, view: view, new_token: options[:new_token],
|
|
238
|
-
annotate_presets: Samagotchi::Config.get("web.annotate_presets")
|
|
238
|
+
annotate_presets: Samagotchi::Config.get("web.annotate_presets"),
|
|
239
|
+
editor: Samagotchi::Config.get("web.editor"))
|
|
239
240
|
end
|
|
240
241
|
|
|
241
242
|
# --resume and --attach take a unique id prefix too, like git.
|
data/docs/architecture.md
CHANGED
|
@@ -280,7 +280,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
280
280
|
`404` for an unknown id. A POST with a body because routes match exact paths.
|
|
281
281
|
- `POST /session/:id/command` — a session command (`/model`, `/models`, `!rollback`, `!cmd`,
|
|
282
282
|
`/continue`) for the worker loop; `202` with a `command_id` its `:command_ran` names, `400` when
|
|
283
|
-
the line isn't one.
|
|
283
|
+
the line isn't one. A `:command_ran` carries `thinking`, `reasoning_history` and `llm_context`
|
|
284
|
+
(the summaries the next turn runs under) when the command changed them or the model, and
|
|
285
|
+
`confirm: {line, text}` when a `/thinking` or `/reasoning-history` change from a `web:` or `tui:`
|
|
286
|
+
client asks first (`CacheConfirm`; nothing changed, `line` applies it).
|
|
284
287
|
- `POST /session/:id/exit` — ask the worker to exit now (`{client_id:}`). The worker checks with
|
|
285
288
|
the event log held (`WorkerIdleExit#hold_for_request`): `200 {status: "exiting"}` and it leaves
|
|
286
289
|
like an idle exit, or `409 {status: "held", reason:}` with `turn_running`, `input_queued`,
|
|
@@ -390,7 +393,8 @@ server's `reasoning_content`; the raw-prompt loop splits each generation's
|
|
|
390
393
|
stream with a fresh `ThoughtStreamSplitter` (Qwen's `<think>` and Gemma's
|
|
391
394
|
`<|channel>thought … <channel|>` are thinking, tool-call bodies are dropped
|
|
392
395
|
from `text:`). The chat loop's model turns keep their `tool_calls` and tool results their `tool_call_id`,
|
|
393
|
-
so later requests and resumed sessions pair them
|
|
396
|
+
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`). It has its own system prompt
|
|
394
398
|
(no raw-prompt tool declarations; the tools go as JSON schemas).
|
|
395
399
|
|
|
396
400
|
`Engine#build_stream_event_handler` is the one seam both loops' streams pass
|
data/docs/cli.md
CHANGED
|
@@ -489,14 +489,34 @@ from (`session`, `--thinking` / `SAMAGOTCHI_THINKING_LEVEL`, `models: <key>`,
|
|
|
489
489
|
session's own level, which comes first; `/thinking default` unsets it. It is
|
|
490
490
|
saved with the session (a `--resume`, a continue and `/model` keep it) and
|
|
491
491
|
applies from the next turn's start; the reply says what the change costs the
|
|
492
|
-
prompt cache: the whole prompt on
|
|
493
|
-
prompt), only the tail on
|
|
494
|
-
(a local llama.cpp or Splash server
|
|
495
|
-
|
|
492
|
+
prompt cache: the whole prompt on Gemma 4 (native, or a local chat server whose
|
|
493
|
+
model name says gemma: `<|think|>` starts the prompt), only the tail on Qwen
|
|
494
|
+
(native, or a local llama.cpp or Splash server), and the whole conversation on a
|
|
495
|
+
remote chat host (OpenRouter, a hosted API). `/thinking` alone answers
|
|
496
496
|
mid-turn too; a change sent while a turn runs waits for the turn's end, and
|
|
497
497
|
skips that turn's warm-up. `/stats` shows the level on its `thinking:` line.
|
|
498
498
|
See "Thinking" in [configuration.md](configuration.md).
|
|
499
499
|
|
|
500
|
+
`/reasoning-history` shows which of its own reasoning a chat host's model gets
|
|
501
|
+
back from the next turn on, and where that came from (`session`,
|
|
502
|
+
`SAMAGOTCHI_REASONING_HISTORY`, `models: <key>`, `hosts.<name>`, the default
|
|
503
|
+
`turn`). `/reasoning-history drop|turn|all` sets the session's own level, which
|
|
504
|
+
comes first; `/reasoning-history default` unsets it. It is saved and carried as
|
|
505
|
+
`/thinking`'s is, and a native host ignores it (the reply says so). A change to
|
|
506
|
+
or from `all` once an earlier reply has thinking rewrites the earlier replies;
|
|
507
|
+
`drop` ↔ `turn` costs nothing. `/stats` shows it on its `reasoning history:`
|
|
508
|
+
line, the web as the `reasoning` chip. See "Reasoning history" in
|
|
509
|
+
[configuration.md](configuration.md).
|
|
510
|
+
|
|
511
|
+
A `/thinking` or `/reasoning-history` change that restarts the prompt cache
|
|
512
|
+
asks first: the reply says what it costs, changes nothing, and names the line
|
|
513
|
+
that applies it (`/thinking low --yes`). `--yes` (or `-y`) applies it at once.
|
|
514
|
+
The terminal puts that line in the next input (Enter applies it); the web's
|
|
515
|
+
reply has an Apply button. `chi send ID "/thinking low"`,
|
|
516
|
+
`chi -p "/thinking low" --non-interactive` and lines piped into the REPL apply
|
|
517
|
+
without asking (a plain `chi -p` opens the REPL, which asks). See "Changes that
|
|
518
|
+
restart the prompt cache" in [configuration.md](configuration.md).
|
|
519
|
+
|
|
500
520
|
### Typing during a turn
|
|
501
521
|
|
|
502
522
|
The prompt stays open while a turn runs, in an attached terminal and in the plain
|
|
@@ -525,9 +545,10 @@ REPL alike:
|
|
|
525
545
|
- At once, beside the turn: `/stats`, `/recap`, `/help`, `/context`, a
|
|
526
546
|
plugin's anytime command (`/btw`), and the commands that only show
|
|
527
547
|
something: `/models` (with a filter too), and `/model`, `/llm-context`,
|
|
528
|
-
`/thinking` and `/guardrails` alone.
|
|
548
|
+
`/thinking`, `/reasoning-history` and `/guardrails` alone.
|
|
529
549
|
- After the turn: the ones that change something (`/model X`, `/model clear`,
|
|
530
|
-
`/llm-context strategy …`, `/thinking LEVEL`, `/guardrails revoke N`) and
|
|
550
|
+
`/llm-context strategy …`, `/thinking LEVEL`, `/reasoning-history LEVEL`, `/guardrails revoke N`) and
|
|
551
|
+
`!cmd`, whose output
|
|
531
552
|
joins the conversation. `(queued: runs after this turn)` says so at once (the
|
|
532
553
|
web draws the command's bubble dashed, with the same words, until it ran);
|
|
533
554
|
when the turn ends they run in the order sent, a prompt sent before one
|
|
@@ -698,7 +719,8 @@ as you toggled them. A reloaded session shows the same block
|
|
|
698
719
|
from the saved messages (each step's thinking, narration, tool parameters
|
|
699
720
|
and output, the output capped at 2000 characters) and the timing records
|
|
700
721
|
(status and duration per row). On an `api: openai` host the model's
|
|
701
|
-
reasoning is saved with each step for this (
|
|
722
|
+
reasoning is saved with each step for this (and sent back to the model
|
|
723
|
+
within the turn: see "Reasoning history" in [Configuration](configuration.md));
|
|
702
724
|
steps saved before that have none, so they show no thinking. An `edit` or
|
|
703
725
|
`write` row has a closed `diff +3 −1` under it that opens to the change it
|
|
704
726
|
made (up to 120 lines or 8 KB), live and after a reload (see
|
data/docs/configuration.md
CHANGED
|
@@ -487,15 +487,15 @@ models:
|
|
|
487
487
|
(`chi web`'s, say) wakes it later: use `chi send --new --thinking low`.
|
|
488
488
|
- A level is fixed for a turn: a change takes effect at the next turn's start. `/thinking LEVEL` sent while a turn
|
|
489
489
|
runs waits for its end (as `/model X` does), and holds off that turn's warm-up. What a change costs the prompt cache,
|
|
490
|
-
which `/thinking` says:
|
|
490
|
+
which `/thinking` says (and asks before the costly ones: "Changes that restart the prompt cache"):
|
|
491
491
|
|
|
492
|
-
| Backend | A level change |
|
|
493
|
-
|
|
494
|
-
| native Qwen (`qwen36`) | only the prompt's tail (the empty thought after the cue) |
|
|
495
|
-
| native Gemma 4 (`gemma4`) | the whole prompt is read again: `<\|think\|>` starts the system prompt |
|
|
496
|
-
| chat host, local llama.cpp
|
|
497
|
-
| chat host,
|
|
498
|
-
| chat host, OpenRouter
|
|
492
|
+
| Backend | A level change | Asks |
|
|
493
|
+
|---|---|---|
|
|
494
|
+
| native Qwen (`qwen36`) | only the prompt's tail (the empty thought after the cue) | no |
|
|
495
|
+
| native Gemma 4 (`gemma4`) | the whole prompt is read again: `<\|think\|>` starts the system prompt | yes |
|
|
496
|
+
| chat host, local (llama.cpp, Splash), a model name with `gemma` | the whole prompt: the template writes the same token | yes |
|
|
497
|
+
| chat host, local, any other model | other request fields; the server keeps its cache, only the tail changes | no |
|
|
498
|
+
| chat host, remote (OpenRouter, a hosted API) | Claude's thinking params are part of the cached prefix, an OpenAI effort change rewrites hidden instructions: the conversation is read again | yes |
|
|
499
499
|
|
|
500
500
|
- `models:` levels and `thinking.level` in the file are read every turn; a host's `thinking:` reaches a worker when
|
|
501
501
|
it starts, as its `sampling:` does (`chi sessions stop` to change it).
|
|
@@ -539,6 +539,81 @@ hosts:
|
|
|
539
539
|
A different level changes a native model's system prompt (Gemma's token, Qwen's turn preamble), so the next turn
|
|
540
540
|
reads the whole context again once; on a chat host only the end of the prompt changes.
|
|
541
541
|
|
|
542
|
+
## Reasoning history
|
|
543
|
+
|
|
544
|
+
Which of its own reasoning a chat host's model (`api: openai`) gets back. chi saves each step's reasoning; with it
|
|
545
|
+
sent back, the model goes on from its own plan between tool calls instead of working it out again (in one test a local
|
|
546
|
+
Qwen thought about 8x less per request and finished a small task about 40% sooner). One level, set per model or per host:
|
|
547
|
+
|
|
548
|
+
```yaml
|
|
549
|
+
hosts:
|
|
550
|
+
deepseek-api:
|
|
551
|
+
url: https://api.deepseek.com/v1
|
|
552
|
+
api: openai
|
|
553
|
+
api_key_env: DEEPSEEK_API_KEY
|
|
554
|
+
reasoning_history: all # its API wants every earlier reply's reasoning when tools are sent
|
|
555
|
+
models:
|
|
556
|
+
some-model:
|
|
557
|
+
reasoning_history: drop
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- `turn` (the default) sends the running turn's reasoning: each reply since the last user message carries its
|
|
561
|
+
reasoning as `reasoning_content`, which is what Qwen's, Gemma 4's and DeepSeek's chat templates and the hosted APIs
|
|
562
|
+
(OpenRouter takes it for DeepSeek, Qwen and other text-reasoning models) expect. The running turn starts at the
|
|
563
|
+
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 continue after a stop) sends nothing of the one
|
|
565
|
+
before it, an interrupted reply's partial thought included.
|
|
566
|
+
- `drop` sends none (what chi did before this setting).
|
|
567
|
+
- `all` sends every reply's reasoning, earlier turns too. It sends only the field: a local template that leaves out
|
|
568
|
+
earlier turns' reasoning (Qwen's) needs `sampling: {chat_template_kwargs: {preserve_thinking: true}}` as well.
|
|
569
|
+
- Order, first set wins: the session's own level, `SAMAGOTCHI_REASONING_HISTORY` (the whole process; for A/B runs),
|
|
570
|
+
the `models:` entry (found the way `profile:` is), the `hosts:` entry, then `turn`. Anything else than a level warns
|
|
571
|
+
once and counts as unset. A host's level reaches a worker when it starts, as its `sampling:` does.
|
|
572
|
+
- **A session's own level** (`drop`, `turn` or `all`; an explicit `turn` counts as its own) is saved in the session
|
|
573
|
+
(its file's `"reasoning_history"`), so a `--resume`, a respawned worker, a continue and `/model` keep it; a plugin's
|
|
574
|
+
fork copies it, a delegate child starts without it. Set it with `/reasoning-history LEVEL` in the session
|
|
575
|
+
(`/reasoning-history default` unsets it, `/reasoning-history` alone shows the level and where it came from) or the
|
|
576
|
+
web's `reasoning` chip in the session bar (shown on chat hosts only). There is no start flag: a fresh session runs
|
|
577
|
+
the command before its first turn at no cost. A change takes effect at the next turn's start; sent while a turn runs
|
|
578
|
+
it waits for its end.
|
|
579
|
+
- What a change costs the prompt cache (`/reasoning-history` says so, and asks first when it costs: "Changes that
|
|
580
|
+
restart the prompt cache"):
|
|
581
|
+
|
|
582
|
+
| Change | Cost |
|
|
583
|
+
|---|---|
|
|
584
|
+
| `drop` ↔ `turn` | none: a turn's first request carries no reasoning under either |
|
|
585
|
+
| to or from `all`, once an earlier reply has thinking | every earlier reply gains or loses its reasoning: a server that renders it (hosted APIs, llama.cpp's default template) reads the conversation again from the first earlier thought |
|
|
586
|
+
| nothing thought yet, the same level, or a native host | none |
|
|
587
|
+
|
|
588
|
+
- 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).
|
|
590
|
+
- When a host answers a request that sent reasoning back with an HTTP 400 about reasoning, chi asks again without it,
|
|
591
|
+
sends none to that model from then on (until the worker restarts) and says so once. If the request without it is
|
|
592
|
+
refused too, the thinking fields get the same fallback ("Thinking") and the reasoning goes back on the next step.
|
|
593
|
+
- `/model` names a level that isn't the default (`reasoning history: drop (models: some-model)`, `all (session)`),
|
|
594
|
+
`/stats` shows it on its `reasoning history:` line (chat hosts only), and `chi self` on its `reasoning history` row
|
|
595
|
+
(a session's own level first when run from its commands).
|
|
596
|
+
- Claude via OpenRouter signs its reasoning (`reasoning_details`), which chi doesn't keep: it gets the plain text,
|
|
597
|
+
and the fallback above covers a refusal.
|
|
598
|
+
|
|
599
|
+
## Changes that restart the prompt cache
|
|
600
|
+
|
|
601
|
+
`/thinking` and `/reasoning-history` ask before a change that makes the server read the conversation again: Gemma's
|
|
602
|
+
thinking switch, a remote chat host's thinking fields ("Thinking"), and a change to or from `all` once an earlier reply
|
|
603
|
+
has thinking ("Reasoning history"). They ask only when the level the next turn runs under really changes and the
|
|
604
|
+
session has a model reply.
|
|
605
|
+
|
|
606
|
+
- The reply says what it costs and changes nothing yet: `Nothing changed yet: send /thinking low --yes to apply.`
|
|
607
|
+
- `--yes` (or `-y`, anywhere after the command) applies it at once.
|
|
608
|
+
- The terminal (the REPL and an attached TUI) puts the `--yes` line in the next input: Enter applies it.
|
|
609
|
+
- The web's reply bubble has an **Apply (restarts the cache)** button. The session bar's chips are already an explicit
|
|
610
|
+
choice: their Set sends the `--yes` line, and their form says what a change costs. Command bubbles are page-only, so
|
|
611
|
+
the button is gone after a reload.
|
|
612
|
+
- Only a person at a web tab or a chi TUI is asked. `chi send ID "/thinking low"`,
|
|
613
|
+
`chi -p "/thinking low" --non-interactive`, lines piped into the REPL, a delegate, a plugin and a card's action
|
|
614
|
+
apply directly, since nobody would see the question; so do the start flags (`chi --thinking` on a resumed or
|
|
615
|
+
attached session sends `/thinking LEVEL --yes`). A plain `chi -p` opens the REPL, which asks.
|
|
616
|
+
|
|
542
617
|
## Llama HTTP Timeouts
|
|
543
618
|
|
|
544
619
|
Long-running llama.cpp completions can exceed Ruby's default HTTP read timeout.
|
|
@@ -1094,6 +1169,34 @@ its own). The first target is a long session such as a day-long coordinator, whe
|
|
|
1094
1169
|
turn that runs a layer or whose session set its strategy) and the web's chip. A woken worker's status line counts
|
|
1095
1170
|
its saved context against the session's budget.
|
|
1096
1171
|
|
|
1172
|
+
## Opening files from chi web
|
|
1173
|
+
|
|
1174
|
+
In `chi web`, a file tool's row (read, edit, write) and the lines of its diff are refs to that file. They look like
|
|
1175
|
+
text until hovered (the title gets a dotted underline and `↗`, a diff line an accent bar and `↗ N`); a click opens the
|
|
1176
|
+
file in your editor: a row's title at the lines it read or edited (an edit without a range at its first changed
|
|
1177
|
+
line, a write at line 1), a diff line at its line, a hunk header at the hunk's start. A drag that selects text isn't
|
|
1178
|
+
a click. `web.editor` picks what opens:
|
|
1179
|
+
|
|
1180
|
+
```yaml
|
|
1181
|
+
web:
|
|
1182
|
+
editor: vscode # or vscode-insiders, cursor, zed, none
|
|
1183
|
+
# editor: "idea://open?file={path}&line={line}" # a template of your own
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
- A preset, or a URL template with `{path}` (the absolute path, each segment URL-encoded, the slashes kept) and an
|
|
1187
|
+
optional `{line}` (1 when there is none). A value that is neither a preset nor has `{path}` logs a warning and
|
|
1188
|
+
counts as `none`. A change needs a `chi web` restart.
|
|
1189
|
+
- A template that puts `{path}` in a query (`?file={path}`) gets the path's slashes unencoded; editors accept that.
|
|
1190
|
+
- The first click asks twice, once each: the browser asks to open the editor's links (choose "always allow"; Safari
|
|
1191
|
+
and Chrome ask per origin, so another `chi web` port asks again), and VS Code asks to allow opening local files.
|
|
1192
|
+
- It opens the file as it is now, not the agent's copy: after later edits a diff line's number can point elsewhere,
|
|
1193
|
+
and a file in a worktree that was removed won't open. The row's diff and output are what the agent saw.
|
|
1194
|
+
- Only a viewer on this machine gets refs: a loopback request, or this machine on its own LAN address
|
|
1195
|
+
(`web.host: lan`, the LAN URL or its QR code opened here). A phone sees plain text. An SSH tunnel or a local
|
|
1196
|
+
reverse proxy (`tailscale serve`) looks like loopback, so a remote viewer behind one gets refs that can't open.
|
|
1197
|
+
- `GET /api/sessions/:id` (and its `?tail=1`) carries `capabilities.editor` (true for such a viewer) and
|
|
1198
|
+
`editor_url`, the template (null otherwise).
|
|
1199
|
+
|
|
1097
1200
|
## All settings
|
|
1098
1201
|
|
|
1099
1202
|
Every setting below takes the three forms described in
|
|
@@ -1171,6 +1274,7 @@ described in their own sections.
|
|
|
1171
1274
|
| `context.every_seconds` | `300` | | How often an attached context source's command runs when it has no `--every` (seconds, at least 30). See [Attached context](context.md). |
|
|
1172
1275
|
| `context.wake` | `true` | yes | A source whose update says `wake: true` may start a turn in a live, idle session (one per source per 10 minutes, within `session.max_wakes`); `false`: updates wait as notes for the next turn. See [Waking](context.md#waking). |
|
|
1173
1276
|
| `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
|
|
1277
|
+
| `reasoning_history` | `turn` | | Which of its reasoning a chat host's model gets back: `drop`, `turn`, `all`; environment only (`SAMAGOTCHI_REASONING_HISTORY`), below a session's own level (`/reasoning-history`), above the `models:`/`hosts:` entries. See "Reasoning history". |
|
|
1174
1278
|
| `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't; a session's own level outranks them all, and `--thinking` sets it on a session run (on `chi web` it sets this). See "Thinking". |
|
|
1175
1279
|
| `models.<key>.thinking`, `hosts.<name>.thinking` | none | | A model's or host's level. See "Thinking". |
|
|
1176
1280
|
| `hosts.<name>.models` | none | | The model ids a host serves whatever its `/v1/models` lists: a map of ids, or a plain list. See "Models a host serves but doesn't list". |
|
|
@@ -1208,6 +1312,7 @@ described in their own sections.
|
|
|
1208
1312
|
| `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
|
|
1209
1313
|
| `web.view` | `stage` | yes | How `chi web` draws a turn: `stage` (the running turn pinned above the composer) or `turn` (one block per turn in the history). See [CLI](cli.md#web-views). |
|
|
1210
1314
|
| `web.annotate_presets` | `Agreed\|Could you please elaborate?` | yes | Quick replies next to Annotate in `chi web`, `\|`-separated (a YAML list works too); `""` in the file or on the CLI leaves only Annotate (an empty env value means the default). See [CLI](cli.md#web-annotate-presets). |
|
|
1315
|
+
| `web.editor` | `vscode` | yes | What a file ref in `chi web` (a file tool row's title, a diff line) opens, for a viewer on this machine: a preset (`vscode`, `vscode-insiders`, `cursor`, `zed`) or a URL template with `{path}` and an optional `{line}`; `none` turns refs off. See [Opening files from chi web](#opening-files-from-chi-web). |
|
|
1211
1316
|
| `history.file` | state dir | | Prompt history path, shared by the TUI and the web composer. |
|
|
1212
1317
|
| `no_interrupt` | `false` | `--no-interrupt` | Raise the step limit of a turn to 1000 (or `turn.max_iterations`, when larger); a top-level key. |
|
|
1213
1318
|
| `no_default_input` | `false` | `--no-default-input` | Don't pre-fill `default.input`; a top-level key. |
|
|
@@ -24,6 +24,6 @@ Behavior:
|
|
|
24
24
|
- `task_get` returns metadata and `output_path`; use `read` for output contents.
|
|
25
25
|
- `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
|
|
26
26
|
- `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.
|
|
27
|
-
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown)
|
|
27
|
+
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `TaskRuntime::MARKER_ENV_KEYS` (and `SessionManager::SESSION_MARKER_ENV`, which a spawned worker unsets). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
|
|
28
28
|
|
|
29
29
|
Work that needs a model, not a shell command, goes to a child chi session instead: `delegate` / `delegate_result` have the same shape (start, then wait) and return only the child's final reply; see [Sessions: Delegating](../sessions.md#delegating).
|
|
@@ -22,9 +22,8 @@ Emission behavior:
|
|
|
22
22
|
- The estimate is emitted on every request (both loops, before each request),
|
|
23
23
|
so the web and the attached TUI get a fresh number each time.
|
|
24
24
|
- The model's own `[CONTEXT: …]` line is still gated: it is left only on a rise
|
|
25
|
-
into a guidance bucket (from the second threshold up), never on a fall
|
|
26
|
-
|
|
27
|
-
names.
|
|
25
|
+
into a guidance bucket (from the second threshold up), never on a fall, and
|
|
26
|
+
not again for a bucket a resumed session's line already names.
|
|
28
27
|
- This is warn-only behavior (no automatic history truncation).
|
|
29
28
|
- When the model server reports real `usage` fields in the stream payload, the
|
|
30
29
|
telemetry uses those actual token counts (prefixed `src=server`) instead of the
|
|
@@ -57,6 +56,25 @@ both loops):
|
|
|
57
56
|
Configuration:
|
|
58
57
|
|
|
59
58
|
- `SAMAGOTCHI_CONTEXT_STATUS` (`true` by default): set to `false` or `0` to disable telemetry.
|
|
60
|
-
- `SAMAGOTCHI_CONTEXT_WINDOW_TOKENS` / `context.window_tokens`: context window size for when the server
|
|
59
|
+
- `SAMAGOTCHI_CONTEXT_WINDOW_TOKENS` / `context.window_tokens`: context window size for when neither the server nor the model's or host's config gives one (step 4 of the lookup below).
|
|
60
|
+
- `models.<key>.window_tokens` / `hosts.<name>.window_tokens` (config file only): the window for one model, or for every model on a host (step 3 below).
|
|
61
61
|
- `SAMAGOTCHI_CONTEXT_CHARS_PER_TOKEN` (default `4.0`): heuristic ratio for char-to-token estimation.
|
|
62
|
-
- `SAMAGOTCHI_CONTEXT_STATUS_THRESHOLDS` (default `20,40,60,80`): comma-separated threshold percentages.
|
|
62
|
+
- `SAMAGOTCHI_CONTEXT_STATUS_THRESHOLDS` / `context.status_thresholds` (default `20,40,60,80`): comma-separated threshold percentages. They set the buckets, and the buckets' guidance gates the model's own `[CONTEXT: …]` line. (`context.status_cadence` is gone: the event streams on every request.)
|
|
63
|
+
|
|
64
|
+
The `context.*` settings are read once, when the turn's `ContextStatus` is built, so a change counts from the next turn.
|
|
65
|
+
|
|
66
|
+
Context window lookup (`ContextWindow.resolve`, the first that answers wins; the event's `window_source` / the line's
|
|
67
|
+
`window_src` names it):
|
|
68
|
+
|
|
69
|
+
1. The running server (`Client#context_window`): llama.cpp's `/props`, the per-slot `n_ctx` (`:server`). A remote
|
|
70
|
+
chat host has no `/props`, so the chat loop and the engine skip this step for it.
|
|
71
|
+
2. The host's model list (the chat adapter's `#context_window`, e.g. a provider's `context_length`; `:model_list`).
|
|
72
|
+
3. The config file's `models.<key>.window_tokens` (by the model's lookup names, as sampling and vision look a model
|
|
73
|
+
up; `:model_setting`), else `hosts.<name>.window_tokens` (`:host_setting`). `ContextWindow.setting` reads it per
|
|
74
|
+
turn for the effective model, after the hooks that may switch it.
|
|
75
|
+
4. `context.window_tokens`: CLI, `SAMAGOTCHI_CONTEXT_WINDOW_TOKENS` or the config file (`:config` or `:env`).
|
|
76
|
+
5. `ContextWindow::DEFAULT_TOKENS`, 256000 (`:default`).
|
|
77
|
+
|
|
78
|
+
Settings (3 and 4) only fill in when the server and its list report nothing (mlx, oMLX, a server that is down). A
|
|
79
|
+
window the stream's `usage` reports itself (`n_ctx` / `context_window`) wins over all of these for that request
|
|
80
|
+
(`window_src=server`). `chi self` shows the offline answer (steps 3 to 5) and which setting gave it.
|
data/docs/internals/engine.md
CHANGED
|
@@ -16,14 +16,14 @@ Engine#run_turn → backend.complete(...) → KernelLoop#run (native) or ChatLoo
|
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:, max_tool_output_chars:,
|
|
19
|
-
pending_input:, continue:, origin:, images:)
|
|
19
|
+
pending_input:, continue:, origin:, images:, id:)
|
|
20
20
|
│
|
|
21
|
-
├── begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:)
|
|
21
|
+
├── begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id:)
|
|
22
22
|
│ # @session = session, session.status = RUNNING, used memories absorbed,
|
|
23
23
|
│ # TurnState#begin!(controller:, sink:) (a new CancellationController when none is given),
|
|
24
24
|
│ # a recap in flight invalidated, GuardrailWiring#begin_turn(origin)
|
|
25
25
|
│ # Returns a Turn {session, prompt (nil on a continue), continue, on_event, controller,
|
|
26
|
-
│ # origin, started_at, id}
|
|
26
|
+
│ # origin, started_at, id (the caller's id:, else a new uuid)}
|
|
27
27
|
│
|
|
28
28
|
├── Client.swap_probe_cancel(turn.controller) # a Stop cuts this turn's /props probes
|
|
29
29
|
│
|
|
@@ -39,7 +39,9 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
39
39
|
│ # Returns false when a Stop came during the probes or the init wait, else true
|
|
40
40
|
│
|
|
41
41
|
├── generate(turn, max_iterations:, max_tool_output_chars:, pending_input:)
|
|
42
|
-
│ # sync_kernel_client!, then @kernel.turn_settings = turn.settings with model_name
|
|
42
|
+
│ # sync_kernel_client!, then @kernel.turn_settings = turn.settings with model_name, window_setting,
|
|
43
|
+
│ # llm_context (turn_llm_context: the session's, the model's/host's, else llm_context.strategy)
|
|
44
|
+
│ # and price (turn_price: hosts.<name>.models.<id>.price), all read per turn
|
|
43
45
|
│ # turn.limit = max_iterations or IterationLimit.for
|
|
44
46
|
│ # backend.complete(messages: turn.messages, ..., pending_input: turn_drain(pending_input))
|
|
45
47
|
│ # Returns LLM::ModelResult (text, conversation, canceled?, exhausted?, tool_activity,
|
|
@@ -172,6 +174,19 @@ The differences from the native loop:
|
|
|
172
174
|
as `stable_length:`), where the prompt's stable part ends. For remote Claude, `LLM::PromptCache` puts the
|
|
173
175
|
first cache breakpoint there; see prompt-caching.md.
|
|
174
176
|
- Each call's result is its own tool message, paired by `tool_call_id`.
|
|
177
|
+
- A model turn's saved reasoning (`thinking:`) goes back as `reasoning_content` per the turn's
|
|
178
|
+
`TurnSettings#reasoning_history` (`ReasoningHistory.resolve`, the session's own level first:
|
|
179
|
+
`Session#reasoning_history`, `/reasoning-history`): `all` on every model message, `turn` past
|
|
180
|
+
`ChatLoop#reasoning_boundary`: the later of the last query (a `role: "user"` entry: a prompt, a steer, merged input,
|
|
181
|
+
the chat template's `last_query_index`; or a `turn_start` entry) and the Run's start (`Run`'s `@start`, the
|
|
182
|
+
conversation length at `#complete`), so a continue turn (reminder, context wake, continue after a stop) replays
|
|
183
|
+
nothing of the run before. `Run#prompt_chars` counts it (`#reasoning_chars`). The native loop differs on a steer:
|
|
184
|
+
`KernelLoop#inject_pending_input!` doesn't re-sanitize, so native keeps the turn's reasoning across a mid-turn
|
|
185
|
+
steer, where the chat loop starts again (as the template does).
|
|
186
|
+
- A 400 about reasoning (`BadRequest#reasoning_refused?`) for a request that sent reasoning back asks the step again
|
|
187
|
+
without it; when that goes through, `#reasoning_history_refused!` marks the model (worker lifetime) and
|
|
188
|
+
`:reasoning_history_refused` goes out. When it is refused too, the thinking-fields fallback (`#thinking_refused!`,
|
|
189
|
+
`:thinking_refused`) runs on it, still without reasoning. Up to three requests per step.
|
|
175
190
|
|
|
176
191
|
### Shared: ToolResponse (per batch) and ToolRunner (per call)
|
|
177
192
|
|
|
@@ -181,6 +196,7 @@ Both loops run a batch of calls the same way:
|
|
|
181
196
|
One iteration with tool calls:
|
|
182
197
|
KernelLoop#dispatch_calls / ChatLoop::Run#dispatch
|
|
183
198
|
→ ToolResponse.run_batch(runner, calls, iteration:, emit:, on_stream_event:, cap:)
|
|
199
|
+
# chat passes a block, called with each run as it finishes
|
|
184
200
|
→ :tool_dispatch_started {iteration, call_count}
|
|
185
201
|
→ for each call: ToolRunner#run(call, iteration:, call_index:, call_count:,
|
|
186
202
|
on_stream_event:, max_tool_output_chars:)
|
|
@@ -192,15 +208,24 @@ One iteration with tool calls:
|
|
|
192
208
|
→ returns {output:, capped_output:, truncated:, activity:, images:, shown_params:,
|
|
193
209
|
shown_label:, diff:}
|
|
194
210
|
→ :tool_dispatch_completed {iteration, call_count}
|
|
195
|
-
→ native: ToolResponse.joined(runs), one entry with the capped outputs
|
|
196
|
-
chat: ToolResponse.single(run, tool_call_id:)
|
|
211
|
+
→ native: ToolResponse.joined(runs, ids:), one entry with the capped outputs
|
|
212
|
+
chat: ToolResponse.single(run, tool_call_id:, ids:) as each call finishes (the block), with the
|
|
213
|
+
capped output
|
|
214
|
+
ids: ToolIds.next_ids, chi's own ids for the outputs ("t41"), saved on the entry as tool_ids
|
|
215
|
+
(one per call of the batch for native, one for chat); LLM context edits name outputs by them
|
|
197
216
|
```
|
|
198
217
|
|
|
199
218
|
**ToolRunner** (`ToolRunner#run`):
|
|
200
219
|
- Runs the gate first, so `:tool_call_started` shows the call that will run (a before_tool_call hook may
|
|
201
220
|
replace it; known-names corrects a misspelled name this way, and the model gets one line saying what ran).
|
|
202
221
|
- Emits `:tool_call_started` {iteration, call_count, call_index, tool, call, params, label (plugin tools),
|
|
203
|
-
title, view}.
|
|
222
|
+
title, called_as, view, ref}. The row fields (title, called_as, view, ref) come from `ToolRowFields.for`, which the
|
|
223
|
+
reload (`Web::MessageParts#tool_part`) calls too. `ToolRowFields::ACTIVITY_KEYS` (title, called_as) travel
|
|
224
|
+
inside `activity` on the completed event; `EVENT_KEYS` (view, ref) go top-level on both events.
|
|
225
|
+
`Bridge::TurnAccumulator` (snapshot and replay) and the web (`activity.js` `ACTIVITY_FIELDS` /
|
|
226
|
+
`EVENT_FIELDS`) copy them by these lists, so a new row field is added there. `ref` is `FileRef.for` (a
|
|
227
|
+
read/write/edit's file against the cwd, `{kind: "file", path, line?, end_line?}`); the web opens it in the editor
|
|
228
|
+
that `Web::Editor` (web.editor) names, for a local viewer only (`App#viewer_capabilities`).
|
|
204
229
|
- Settles an ask verdict after `:tool_call_started`, so a UI shows the tool line and then the approval.
|
|
205
230
|
- Dispatches through `KernelLoop#dispatch_tool_call`, or writes the denial text. Unknown tools are answered in
|
|
206
231
|
`KernelLoop#dispatch` ("Error: no such tool ...", never repeating the wrong name).
|
|
@@ -211,7 +236,7 @@ One iteration with tool calls:
|
|
|
211
236
|
with `[cut: N of M chars; read it in parts]` (N kept of M, the whole within the cap unless the cap is shorter
|
|
212
237
|
than that line). The model (both loops), the event and the after_tool_call hook get that text.
|
|
213
238
|
- Emits `:tool_call_completed` {iteration, call_count, call_index, tool, output, output_truncated, activity,
|
|
214
|
-
images, diff, waited_ms, view}, then fires `:after_tool_call` {iteration, tool, output, status}.
|
|
239
|
+
images, diff, waited_ms, view, ref}, then fires `:after_tool_call` {iteration, tool, output, status}.
|
|
215
240
|
- The activity's status (`ToolActivity#tool_activity_status`): `error` for an `Error:` result and for an
|
|
216
241
|
execute that exited non-zero, except a grep-family search that found nothing (`ToolActivity.no_match?`:
|
|
217
242
|
exit 1, no stderr block, the last step is grep/egrep/fgrep/rg/ag/pgrep, no `&&` after a step that can fail
|
|
@@ -299,8 +324,8 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
299
324
|
|---|---|---|
|
|
300
325
|
| `:turn_started` | `prepare_turn` | `{session_id, prompt, turn_id, continue?, images?}` |
|
|
301
326
|
| `:reminder_injected` | due reminders went into the turn | `{reminders}` |
|
|
302
|
-
| `:context_status` | every request, the current estimate before it | `{iteration,
|
|
303
|
-
| `:generation_started` | a request begins | `{iteration, context_window_tokens, context_window_source}` (native adds `profile`, `profile_source`) |
|
|
327
|
+
| `:context_status` | every request (unless `context.status` is off), the current estimate before it | `{iteration, status, usage, bucket, source}` (internals/context-telemetry.md) |
|
|
328
|
+
| `:generation_started` | a request begins | `{iteration, context_window_tokens, context_window_source, price}` (`price`: the turn's `ModelPrice#to_h`, nil without one; native adds `profile`, `profile_source`) |
|
|
304
329
|
| `:generation_chunk` | each streamed chunk | `{iteration, content, text, thinking, payload, tool_call?}` |
|
|
305
330
|
| `:generation_retrying` | the transport retries the request, or the chat loop asks again for a step whose stream dropped mid-answer (`restarted: true`: what the step streamed is void, and the web's live step, the TUI's activity line, the Bridge's TurnAccumulator and the stream hooks' watch start it over) | `{iteration, attempt, max_retries, next_delay, error_class, error_message, status, restarted?}` |
|
|
306
331
|
| `:generation_completed` | a generation ended, or was cut | `{iteration, content_length, thinking_chars, served_model, requested_model, finish_reason, ...}`; a cut adds `stopped_by`, `stop_reason`; the Engine adds `speed`, `tokens` |
|
|
@@ -308,6 +333,7 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
308
333
|
| `:empty_answer_retry` | an empty, cut or malformed answer is asked again | `{iteration, attempt, of, finish_reason, thinking_chars, stopped_by?, malformed?}` |
|
|
309
334
|
| `:steer_cut` | a steer cut the generation | `{iteration, source}` |
|
|
310
335
|
| `:llm_context_edited` | the apply rule applied a batch of LLM context edits (a request, a forget_outputs call, turn end; never the warm-up) | `{moment, why, freed_tokens, tail_tokens, staged, text, groups}` (`LLMContextNotice`) |
|
|
336
|
+
| `:thinking_refused` / `:reasoning_history_refused` | the chat loop asked a step again without the thinking fields / the reasoning sent back; the Engine logs it and makes a once-per-session-and-host `:hook_notice` of it, the event itself never reaches the sink | `{iteration, model, level, detail}` |
|
|
311
337
|
| `:pending_input_merged` | queued input joined the conversation | `{iteration, count, content, steers?, answer}` |
|
|
312
338
|
| `:tool_dispatch_started` / `:tool_dispatch_completed` | around a batch | `{iteration, call_count}` |
|
|
313
339
|
| `:tool_call_started` | before a call runs | see ToolRunner |
|
|
@@ -368,7 +394,7 @@ puts the hook runtime on the event: `event[:hook]` (the hook's label) and five h
|
|
|
368
394
|
|
|
369
395
|
| Helper | What it does | Used by |
|
|
370
396
|
|---|---|---|
|
|
371
|
-
| `event[:notify].call(text, level:)` | One line to the user (`:hook_notice`) | mcp (a server didn't start), loop-guard |
|
|
397
|
+
| `event[:notify].call(text, level:, fallback_for:)` | One line to the user (`:hook_notice`); `fallback_for: :display` marks a line that stands in for the answer's display, which a UI that renders the display's links leaves out | mcp (a server didn't start), loop-guard |
|
|
372
398
|
| `event[:ask_user].call(question:, options:, header:, allow_freeform:)` | A single-select question through the question flow; nil when there is no one to ask | known-names (a near-miss name: correct it?) |
|
|
373
399
|
| `event[:stop_turn].call(reason)` | Cancel the running turn (reason `:hook`), with a warn notice | loop-guard (repeated calls), check-in (`/checkin stop`) |
|
|
374
400
|
| `event[:stop_generation].call(reason)` | Cut the streaming generation; the turn goes on | loop-guard (thinking repeats itself) |
|
|
@@ -394,9 +420,18 @@ From `:after_turn` and `:session_end` there is no turn left: `stop_turn` and `st
|
|
|
394
420
|
`ToolRunner` asks the kernel's `Guardrails::Gate` (set by the Engine from `GuardrailWiring`) before each call;
|
|
395
421
|
the gate is not itself a hook. `Gate#evaluate` fires the `:before_tool_call` hooks (the voters, e.g. the
|
|
396
422
|
known-names bundle or a config hook), which vote through `event[:guardrail]` (`deny!`, `ask!`) or the legacy
|
|
397
|
-
`event[:blocked]` flag. A deny stays a deny for the hooks after it. Then the core checks run on the final call
|
|
398
|
-
|
|
399
|
-
|
|
423
|
+
`event[:blocked]` flag. A deny stays a deny for the hooks after it. Then the core checks run on the final call,
|
|
424
|
+
in `GuardrailWiring#checks` order:
|
|
425
|
+
|
|
426
|
+
1. load failures (`Guardrails::LoadFailures`): a required hook or rule file that failed to load denies every call;
|
|
427
|
+
2. `rules.hook_asks`: with guardrails disabled, a hook's ask is dropped (a deny still applies);
|
|
428
|
+
3. protected paths;
|
|
429
|
+
4. `ScratchWrites` (a `chi scratch` session only): write and edit under the memories folder are denied;
|
|
430
|
+
5. `ChildBoundary` (a delegate child only): a change outside its folder asks;
|
|
431
|
+
6. the rules (config and bundles, e.g. the guardrails bundle's `rules.yml`; a rule can match the tool, its
|
|
432
|
+
targets and the model).
|
|
433
|
+
|
|
434
|
+
A gate that raises denies the call.
|
|
400
435
|
|
|
401
436
|
The verdict is one of:
|
|
402
437
|
|
|
@@ -457,10 +492,11 @@ thought after the cue, kept in the model message so the next prompt starts with
|
|
|
457
492
|
`FailedTurn.attach`; after a Ctrl-C, only `turn.messages`.
|
|
458
493
|
4. **Events are the contract.** UIs read events, not internal state. `on_event` is per turn; observers are
|
|
459
494
|
persistent.
|
|
460
|
-
5. **A stable system prompt for the cache.** `SystemPrompt#build` caches the prompt per Engine
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
495
|
+
5. **A stable system prompt for the cache.** `SystemPrompt#build` caches the prompt per Engine, keyed by
|
|
496
|
+
`[chat, thinking, layers]` (chat/native, the thinking level, the turn's LLM context layers), and is reset
|
|
497
|
+
only on a model switch, a tools change or a profile change. The stable part (base prompt, identity,
|
|
498
|
+
memories, AGENT.md, memory indexes) comes before the volatile tail (model, working directory, session).
|
|
499
|
+
prompt-caching.md covers this in detail.
|
|
464
500
|
|
|
465
501
|
## Key files
|
|
466
502
|
|
|
@@ -135,6 +135,14 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
135
135
|
on its `generation_completed` log line. `/stats` shows "cached N (P%)" and "cache writes". A change that
|
|
136
136
|
drops `cached=` on the second fresh session is a regression. Update the prompt snapshot fixtures with
|
|
137
137
|
`UPDATE_PROMPTS=1` and read the diff: a cache-friendly change moves sections, it doesn't reorder per run.
|
|
138
|
+
6. **Ask before a setting change that restarts the cache.** A session setting whose change makes the server read
|
|
139
|
+
the conversation again asks a person at a web tab or a TUI first (`CacheConfirm`: the reply carries the
|
|
140
|
+
`--yes` line, `SessionCommands::Result#confirm` and the `command_ran` event's `confirm`). Each setting has a
|
|
141
|
+
classifier: `Thinking.cache_cost` (`:full` for Gemma, `:provider` for a remote chat host, `:tail` otherwise) and
|
|
142
|
+
`ReasoningHistory.cache_cost` (`:history` for a change to or from `all` once an earlier reply has thinking).
|
|
143
|
+
`CacheConfirm.needed?` asks only for `:full`, `:provider` and `:history`, when the effective level changes and
|
|
144
|
+
the session has a model reply. A new setting with a cost (a `/model` across templates, `/llm-context` turning
|
|
145
|
+
forget on) adds its classifier and goes through the same step.
|
|
138
146
|
|
|
139
147
|
## Server-side settings worth knowing
|
|
140
148
|
|