samagotchi 0.60.0 → 0.62.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 +99 -1
- data/docs/architecture.md +10 -4
- data/docs/cli.md +22 -8
- data/docs/configuration.md +32 -19
- data/docs/context.md +8 -0
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +80 -1
- data/docs/internals/engine.md +57 -15
- data/docs/internals/prompt-caching.md +8 -1
- data/docs/plugins.md +49 -6
- data/docs/sessions.md +7 -3
- data/docs/testing.md +4 -1
- data/lib/samagotchi/answer_display.rb +4 -3
- data/lib/samagotchi/answer_refs.rb +210 -0
- data/lib/samagotchi/bridge/card_store.rb +6 -1
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +27 -3
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +70 -14
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -1
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +58 -3
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -4
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +47 -9
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +3 -1
- data/lib/samagotchi/interrupted_turn.rb +39 -0
- data/lib/samagotchi/kernel_loop.rb +13 -20
- data/lib/samagotchi/llm/chat_loop.rb +28 -17
- data/lib/samagotchi/log_subscriber.rb +6 -3
- data/lib/samagotchi/plugin/api.rb +36 -18
- data/lib/samagotchi/reasoning_history.rb +22 -4
- data/lib/samagotchi/ref.rb +25 -8
- data/lib/samagotchi/reply_wait.rb +6 -2
- data/lib/samagotchi/row_refs.rb +126 -0
- data/lib/samagotchi/session.rb +40 -4
- data/lib/samagotchi/session_manager.rb +64 -6
- data/lib/samagotchi/session_metrics.rb +24 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +30 -3
- data/lib/samagotchi/tool_activity.rb +8 -4
- data/lib/samagotchi/tool_response.rb +5 -4
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +16 -3
- data/lib/samagotchi/tools/builtins.rb +9 -2
- data/lib/samagotchi/tools/execute.rb +15 -3
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/registry.rb +5 -3
- data/lib/samagotchi/turn_checkpoint.rb +128 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +19 -1
- data/lib/samagotchi/web/message_parts.rb +17 -9
- data/lib/samagotchi/web/public/activity.js +4 -1
- data/lib/samagotchi/web/public/answer_refs.js +217 -0
- data/lib/samagotchi/web/public/app.js +74 -33
- data/lib/samagotchi/web/public/chains.js +18 -20
- data/lib/samagotchi/web/public/context_chips.js +92 -64
- data/lib/samagotchi/web/public/families_view.js +51 -39
- data/lib/samagotchi/web/public/index.html +66 -13
- data/lib/samagotchi/web/public/keep_focus.js +24 -0
- data/lib/samagotchi/web/public/lists_view.js +13 -13
- data/lib/samagotchi/web/public/llm_context_chip.js +55 -34
- data/lib/samagotchi/web/public/question_card.js +11 -0
- data/lib/samagotchi/web/public/question_cards.js +60 -5
- data/lib/samagotchi/web/public/ref_menu.js +24 -3
- data/lib/samagotchi/web/public/ref_scan.js +5 -3
- data/lib/samagotchi/web/public/refs.js +140 -20
- data/lib/samagotchi/web/public/scroll.js +42 -0
- data/lib/samagotchi/web/public/stage_model.js +22 -3
- data/lib/samagotchi/web/public/stage_view.js +299 -70
- data/lib/samagotchi/web/public/stream_handlers.js +5 -3
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +5 -4
- data/lib/samagotchi/web/public/turn_model.js +49 -0
- data/lib/samagotchi/web/public/turn_view.js +121 -17
- data/lib/samagotchi/worker.rb +169 -21
- data/lib/samagotchi/worker_inbound.rb +5 -3
- data/lib/samagotchi/worker_signals.rb +81 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68aeb876a7c4c04d7f2f5de4290a1c6089df3acd181bce7380ccb6bde9ef9de8
|
|
4
|
+
data.tar.gz: ed308c360b3942d9f3d08233762396493a018585103e50661dd17ea4bb0d123c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a3ecca6a15d1c2d0e4acf5531491116139b3423ca0e6fe2b4ad558f5c99b332b50672a700437f9d37c1397d9ed92e527b843e6577b11198906ff75ab16b4ecd
|
|
7
|
+
data.tar.gz: 949538fdd97549e09bd215353cbdf9dccbe42a21806b965ff828db5b708521791e939abd2d2cb93892c23660abbfabd3106bb492412eb505f52cc0cd742d6d99
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,102 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.62.0] - 2026-10-13
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Plugins: `chi.tool(…, refs: ->(args) { [...] })` gives a tool call's web row its refs (a ticket, a PR, a file
|
|
16
|
+
under the session's folder) with their URL actions: the row's title opens the first, its `▾` menu offers the
|
|
17
|
+
rest, on a phone too. Saved with the result, so a reload shows them. See docs/plugins.md, "Refs on a tool's
|
|
18
|
+
row".
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Web: the stage view no longer jumps: it keeps its height while a turn runs, its chip stays put when the answer
|
|
23
|
+
arrives, and the finished turn slides away instead of collapsing. The stage floats above the composer (its rising at
|
|
24
|
+
a turn's start moves nothing, and the history behind it glides instead of jumping); content changes inside it (the
|
|
25
|
+
first words of a reply, an answered card, a short answer), so a card you answer stays where it is; it grows with a
|
|
26
|
+
short ease when a card or the answer needs room; its chip is its footer (the steps open right above it); and when
|
|
27
|
+
the turn moves into the history the stage slides away under the composer, then the turn appears there.
|
|
28
|
+
- Web: on a phone the top bar no longer gains a row mid-turn when the hidden strip's pill grows ("· 1 waiting"), which
|
|
29
|
+
pushed the whole session down; the pill drops its session count while it says live or waiting.
|
|
30
|
+
- Web: while a question or approval waits in the stage view, the stage shows the whole text of the reply that asked
|
|
31
|
+
it, not only its last sentence.
|
|
32
|
+
- Web: the model's question or its own approval, once answered, keeps the reply that asked it (a delegate's relayed
|
|
33
|
+
approval, a hook's question and the step limit don't): two muted lines under its `question → answer` line, the
|
|
34
|
+
whole text when the card is open, live and after a reload.
|
|
35
|
+
- Web: a finished turn's steps block shows its outline when its steps said something: open, one line per step that
|
|
36
|
+
wrote text (a mid-turn note reads without a click), and each run of tool-only steps folded into one `· N steps`
|
|
37
|
+
line that shows them. Live (both views) and after a reload; a turn whose steps said nothing collapses as before.
|
|
38
|
+
- Web: question and approval cards answer from the keyboard. Enter in the response box or on an option submits
|
|
39
|
+
(never Deny or Dismiss), and a new card takes the focus when the message box is empty.
|
|
40
|
+
- github-pr 0.5.0: a `path:line` it links in an answer is also a file ref, so the web's ref menu offers the file in
|
|
41
|
+
this checkout (editor, show in turn, copy) and a "PR diff" (or "File at head") action. Needs chi 0.61.0; run
|
|
42
|
+
`chi update`.
|
|
43
|
+
- source-links 0.5.0: each ref it links in an answer gets an "Open in <source>" action in the web's ref menu; a
|
|
44
|
+
source's new `kind:` key names its refs' kind (default `issue`). Needs chi 0.61.0; run `chi update`.
|
|
45
|
+
- Chat hosts (`api: openai`) keep the running turn's reasoning across a mid-turn steer under `reasoning_history:
|
|
46
|
+
turn`, as native hosts do: a person's line, a plugin's nudge (check-in, agents-md) or a step-limit Continue's text no
|
|
47
|
+
longer drops the reasoning of the replies before it, so the request after it extends the one before and the
|
|
48
|
+
server's prompt cache holds. A `!cmd` run while the step-limit question stands still starts past itself. On a stock
|
|
49
|
+
Qwen template (Splash) the benefit needs `chat_template_kwargs.preserve_thinking: true`; see
|
|
50
|
+
[docs/configuration.md](docs/configuration.md#reasoning-history).
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- A turn run in the REPL (`--no-shared`) keeps the answer's display and refs (source-links' and github-pr's links,
|
|
55
|
+
a hook's `present`), so the web shows them when it opens the session; before, the save dropped them.
|
|
56
|
+
- A command a signal to chi stopped (a worker's TERM, HUP or INT mid-`execute`) says "stopped by a signal to chi",
|
|
57
|
+
not "stopped by the user"; one it kept from starting says "not run, a signal to chi stopped the turn".
|
|
58
|
+
slow-commands 0.2.1 doesn't count either as slow.
|
|
59
|
+
- A turn whose worker was killed (SIGKILL, a crash) keeps its tokens, speed and timings up to its last checkpoint:
|
|
60
|
+
the session's stats count it as a canceled turn (`worker_stopped`); before, they lost it.
|
|
61
|
+
- Web: hovering a ref with a ▾ at the very end of a line no longer pushes it onto the next line.
|
|
62
|
+
- Web: a failed `+ URL` attach keeps the "why" you typed (it was emptied), and a context chip with the keyboard
|
|
63
|
+
focus keeps it when the chips refresh.
|
|
64
|
+
|
|
65
|
+
Update with `chi update`: the system bundle 0.62.0, github-pr 0.5.0, source-links 0.5.0 and slow-commands 0.2.1.
|
|
66
|
+
Then restart `chi web`.
|
|
67
|
+
|
|
68
|
+
## [0.61.0] - 2026-10-12
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- Hooks and plugins can attach refs to the answer: `event[:refs].call(match:, kind:, target:, label:, actions:)` on
|
|
73
|
+
`:after_turn`, stored as `display_refs` on the answer (display only: the model never sees them). The web pairs them
|
|
74
|
+
with the answer's links and text: their actions join the ref's menu, live and after a reload. Caps: 200 refs and
|
|
75
|
+
256 KiB per answer; a refused ref is logged as `refs_rejected`. See
|
|
76
|
+
[docs/hooks.md](docs/hooks.md#refs-on-the-answer-display-only).
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- A ref's values are capped (`Ref`, `RefAction`): a label at most 200 characters, at most 8 actions, a target id
|
|
81
|
+
200 and a path 4,096 characters; an action's id 64 and label 100. An action URL with userinfo
|
|
82
|
+
(`https://user:pass@host/`) is refused, and the web page refuses one too.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- A worker that gets TERM, HUP or INT (`chi sessions stop`, a closed terminal, a process-tree teardown) stops its
|
|
87
|
+
running turn as a Stop with reason `signal` and saves it, then exits; before, the turn and its prompt were lost.
|
|
88
|
+
A stopped session's worker now saves the turn it kept too.
|
|
89
|
+
- A worker killed mid-turn (SIGKILL, a crash) no longer loses the whole turn: the worker writes the turn so far to
|
|
90
|
+
the session file with an `interrupted_turn` mark (the prompt as the turn starts, then its iteration boundaries,
|
|
91
|
+
at most every 10 s), so it loses only what came after its last written boundary. The next worker (or a
|
|
92
|
+
`--no-shared` REPL resuming the session) ends that turn as canceled with a note the model reads
|
|
93
|
+
(`cancelled (worker-stopped)`), so the session is idle again.
|
|
94
|
+
- Workers outlive whoever spawned them: a worker leaves the spawner's process tree (forked again, `setsid`,
|
|
95
|
+
reparented to init), so a tool tearing down a background `chi send --wait`, or a closing terminal, no longer
|
|
96
|
+
kills it mid-turn (run #43 lost a 169-step turn that way).
|
|
97
|
+
- A card shown during a turn by a worker that has since exited comes back read-only: no buttons. A stale check-in
|
|
98
|
+
card's Nudge steered the next worker's turn on every click. Cards shown between turns (github-pr's offer, btw,
|
|
99
|
+
coordinator) keep their buttons.
|
|
100
|
+
- check-in nudges once per check-in: repeated Nudge clicks (or `/checkin nudge`) on the same check-in answer
|
|
101
|
+
"check-in: already nudged at N tool calls" instead of steering the turn again. Run #43 got 12 identical nudges.
|
|
102
|
+
- Web: a ref's `▾` sits in room the ref makes on hover, never over the next words.
|
|
103
|
+
|
|
104
|
+
Update with `chi update`: the system bundle 0.61.0 and check-in 0.2.4 (nudges once per check-in). Then restart
|
|
105
|
+
`chi web`.
|
|
106
|
+
|
|
11
107
|
## [0.60.0] - 2026-10-11
|
|
12
108
|
|
|
13
109
|
### Added
|
|
@@ -2623,7 +2719,9 @@ and long-lived sessions.
|
|
|
2623
2719
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2624
2720
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2625
2721
|
|
|
2626
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2722
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.62.0...HEAD
|
|
2723
|
+
[0.62.0]: https://github.com/dm1try/samagotchi/compare/v0.61.0...v0.62.0
|
|
2724
|
+
[0.61.0]: https://github.com/dm1try/samagotchi/compare/v0.60.0...v0.61.0
|
|
2627
2725
|
[0.60.0]: https://github.com/dm1try/samagotchi/compare/v0.59.0...v0.60.0
|
|
2628
2726
|
[0.59.0]: https://github.com/dm1try/samagotchi/compare/v0.58.0...v0.59.0
|
|
2629
2727
|
[0.58.0]: https://github.com/dm1try/samagotchi/compare/v0.57.0...v0.58.0
|
data/docs/architecture.md
CHANGED
|
@@ -97,7 +97,7 @@ terminal renders the events it gets over the Bridge.
|
|
|
97
97
|
| Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, and the HTTP both share. |
|
|
98
98
|
| Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, plus runtime/output-guardrails. |
|
|
99
99
|
| Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
|
|
100
|
-
| Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. |
|
|
100
|
+
| Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. The spawned process (own process group) forks the worker, which `setsid`s and is reparented to init once its starter exits (it waits until the worker holds `owner.lock`, so the spawner's `Process.detach` waiter tells a starting worker): nothing that walks the spawner's process tree reaches it (`SAMAGOTCHI_WORKER_DETACH=1`, `detach_from_spawner`). The lock's pid is the worker's; `spawn`'s `child_pid` is its starter's. |
|
|
101
101
|
| Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick single-port `127.0.0.1:4567` (index.html + `/api/*` + SSE). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
|
|
102
102
|
| Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`; retention 14d/500, `updated_at desc`, lazy sweep. |
|
|
103
103
|
|
|
@@ -297,7 +297,8 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
297
297
|
(`GET /api/sessions/:id`, `?parts=1`) and the plugin's `list_sessions`; every message is copied.
|
|
298
298
|
- `GET /session/:id/tail` — the page's light re-read (`Bridge#tail_frame`): `session_state_snapshot`,
|
|
299
299
|
`answer` (the last message a UI shows as an answer, `AnswerTail`: the one message, copied alone),
|
|
300
|
-
`cards` (`CardStore#list
|
|
300
|
+
`cards` (`CardStore#list`; a card an earlier worker showed during a turn comes back read-only, with no
|
|
301
|
+
actions: its turn no longer runs), `event_seq`, `event_id`, all taken in one event-log hold; no message
|
|
301
302
|
list, so it costs the same however long the session is. `?turn_id=` answers that turn's answer
|
|
302
303
|
(an unknown or missing id: the newest); the only route that reads its query.
|
|
303
304
|
- `OPTIONS *` — CORS preflight (`Access-Control-Allow-Origin: *`).
|
|
@@ -318,11 +319,16 @@ it, so the web's info bar updates per generation without parsing chunk payloads.
|
|
|
318
319
|
|
|
319
320
|
The page's frequent reads of `GET /api/sessions/:id` stay light too:
|
|
320
321
|
- `?tail=1&recent=1` (each turn's end, a cancel, an answer display): `{tail, session: {id, status,
|
|
321
|
-
used_memory_names}, messages: [the answer], markdown_warning, timing}`, from the worker's `/tail`
|
|
322
|
+
used_memory_names}, messages: [the answer, with its display and display_refs (re-read on an `answer_display` with a display or refs)], markdown_warning, timing}`, from the worker's `/tail`
|
|
322
323
|
alone (no session file parsed, no `analytics.json`); `timing` carries the worker's recent records
|
|
323
324
|
and `turn_count` (finished turns). The page merges the records by id (`timing.js mergeTiming`) and
|
|
324
325
|
finishes the ended turn's own line by its `turn_id` (sent as `&turn_id=`, which also picks that
|
|
325
326
|
turn's answer). A plain `?tail=1` (a tab opened before this) gets the whole timing.
|
|
327
|
+
The page binds an answer's `display_refs` to its bubble (`answer_refs.js`: on a reload's render and on this
|
|
328
|
+
read's re-render) and pairs them with the bubble's links and text before the `path:line` scan.
|
|
329
|
+
A plugin tool's row refs (`refs`, `RowRefs`) ride its tool events and, on a reload, its part (saved as
|
|
330
|
+
`tool_refs`); `turn_view.js` binds them to the row's title (`refs.js bindRowRefs`: the first on the title,
|
|
331
|
+
the others in its menu).
|
|
326
332
|
- `?timing=1`: the whole timing (`analytics.json` merged with the worker's `/state`) and
|
|
327
333
|
`turn_count`, when the page's merge came up short.
|
|
328
334
|
- `?cards=1`: the cards, from `/tail`; the session file is only stat'ed for the 404.
|
|
@@ -451,7 +457,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
451
457
|
|
|
452
458
|
| Component | Main file(s) | What it is |
|
|
453
459
|
|---|---|---|
|
|
454
|
-
| Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. |
|
|
460
|
+
| Worker | `worker.rb` (`worker_idle_exit.rb`, `worker_inbound.rb`, `worker_signals.rb`) | A session's background process: builds the `Engine` and `Bridge`, runs queued turns, exits when idle. A spawned one traps TERM, HUP and INT (`WorkerSignals`, installed by `run_session_loop` when `SAMAGOTCHI_WORKER_PROCESS=1`): the running turn stops as a Stop with reason `signal`, is saved, and the worker leaves (`:signal`, exit 0; a new worker only for input already queued). A running turn is checkpointed at its iteration boundaries (`TurnCheckpoint`, the `interrupted_turn` mark), and the next worker settles a turn its predecessor died in (`TurnCheckpoint.settle`). |
|
|
455
461
|
| Config | `config.rb` | Every setting as an `Entry`: env, config.yml and CLI layers ([configuration.md](configuration.md)). |
|
|
456
462
|
| MemoryBundle | `memory_bundle.rb`, `memory_bundle/{installer,builder,installed_bundle,plugin_ref,bundle_hook}.rb` (`Installer`, `Builder`, `InstalledBundle`, `PluginRef`, `BundleHook`) | Installing, building and reading bundles of memories, hooks and plugins. |
|
|
457
463
|
| Plugin API | `plugin/api.rb` (`Plugin::Api`), `plugin/loader.rb` | What a bundle's plugin registers: tools, commands, hooks, services ([plugins.md](plugins.md)). |
|
data/docs/cli.md
CHANGED
|
@@ -689,14 +689,23 @@ the step's reasoning), the running tool with what it does, and the last
|
|
|
689
689
|
three calls. A plugin's or hook's notice and a plugin's nudge ("check-in
|
|
690
690
|
nudged the model") show ahead of those calls for about 4 s, as their rows
|
|
691
691
|
wait in the closed block. A card that needs you (a question, an approval, check-in)
|
|
692
|
-
sits in the stage too.
|
|
693
|
-
|
|
692
|
+
sits in the stage too. While a question or approval waits, the stage shows
|
|
693
|
+
the whole text of the reply that asked it (in place of its newest
|
|
694
|
+
sentence) above the card, until you answer. Once answered, the model's
|
|
695
|
+
question or its own approval keeps that reply (a delegate's relayed
|
|
696
|
+
approval, a hook's question and the step limit don't): two muted lines
|
|
697
|
+
under its `question → answer` line, the whole text when you open the card,
|
|
698
|
+
in both views and after a reload. The chip at the stage's bottom edge, `N steps · M tool
|
|
699
|
+
calls` with one tick per call, opens the turn view's block right above it (newest step first
|
|
694
700
|
while it runs; a tick opens its step). `▾` folds the stage to its status
|
|
695
|
-
row, remembered in this browser.
|
|
701
|
+
row, remembered in this browser. The stage keeps its height while a turn
|
|
702
|
+
runs: it grows, with a short ease, when a card or the answer needs room and gives the room back
|
|
703
|
+
only when the turn moves into the history. When the turn ends the answer shows in
|
|
696
704
|
the stage, and the whole turn moves up into the history once you are not
|
|
697
705
|
using the stage (the pointer over it, a touch or scroll in the last 4 s,
|
|
698
|
-
keyboard focus or a selection keep it) for 1.5 s
|
|
699
|
-
|
|
706
|
+
keyboard focus or a selection keep it) for 1.5 s: the stage slides away
|
|
707
|
+
under the composer, then the turn appears in the history. Sending the next
|
|
708
|
+
message moves it at once. The history is never scrolled while a turn runs, and only
|
|
700
709
|
follows the hand-off if you were at its end. The `/` command list opens
|
|
701
710
|
over the stage's lower edge.
|
|
702
711
|
|
|
@@ -714,9 +723,14 @@ again). When a step ends its thinking closes to a plain `thinking` line
|
|
|
714
723
|
(unless you opened it); a step that only thought shows that line alone. The
|
|
715
724
|
live narration is a box of about three lines that fills sentence by
|
|
716
725
|
sentence (a sentence shows once it is complete) and scrolls to the newest
|
|
717
|
-
when full. When the turn ends the block
|
|
718
|
-
|
|
719
|
-
|
|
726
|
+
when full. When the turn ends the block shows its outline when any of its steps said
|
|
727
|
+
something: it stays open on one line per step that wrote text (that text's
|
|
728
|
+
first line, so a mid-turn note reads without a click), and each run of steps
|
|
729
|
+
that only called tools or thought sits behind one `· N steps` line that
|
|
730
|
+
shows them; old turns open the same way after a reload. A turn whose steps
|
|
731
|
+
said nothing collapses the block to its summary (`3 steps
|
|
732
|
+
· 8 tool calls · execute ×7 · read ×1`). Either way the answer expands from
|
|
733
|
+
the box into a normal bubble under it (Markdown, annotate). A plain answer
|
|
720
734
|
without tools ends exactly as it does today. Rows the code collapses stay
|
|
721
735
|
as you toggled them. A reloaded session shows the same block
|
|
722
736
|
from the saved messages (each step's thinking, narration, tool parameters
|
data/docs/configuration.md
CHANGED
|
@@ -521,9 +521,10 @@ What each backend gets:
|
|
|
521
521
|
|
|
522
522
|
On chat hosts: llama.cpp honours both off switches but ignores the effort (when its `/props` says
|
|
523
523
|
`chat_template_caps.supports_reasoning_effort: false`, chi says so once per session and host, from the `/props`
|
|
524
|
-
answer the turn already fetched for the window); Splash
|
|
525
|
-
|
|
526
|
-
|
|
524
|
+
answer the turn already fetched for the window); Splash honours both off switches and passes the effort to the
|
|
525
|
+
model's template, which decides: Qwen3.6's ignores it (`low`, `medium` and `high` think the same), and the server's
|
|
526
|
+
`--default-reasoning-effort` decides what `default` means there; OpenRouter translates `reasoning_effort` per model
|
|
527
|
+
(some can't turn thinking off: Qwen3-30B-A3B thinks anyway, gpt-oss refuses).
|
|
527
528
|
|
|
528
529
|
When the model thinks although the level is `off`, chi says so once per session and host
|
|
529
530
|
(`thinking> warning: off wasn't honoured by … (N chars of thinking)`) and logs `thinking_not_honoured` each time.
|
|
@@ -558,26 +559,39 @@ hosts:
|
|
|
558
559
|
url: https://api.deepseek.com/v1
|
|
559
560
|
api: openai
|
|
560
561
|
api_key_env: DEEPSEEK_API_KEY
|
|
561
|
-
reasoning_history: all # its API wants every earlier reply's reasoning
|
|
562
|
+
reasoning_history: all # with tools its API wants every earlier reply's reasoning: under turn, turn 2 fails
|
|
562
563
|
models:
|
|
563
564
|
some-model:
|
|
564
565
|
reasoning_history: drop
|
|
565
566
|
```
|
|
566
567
|
|
|
567
|
-
- `turn` (the default) sends the running turn's reasoning: each reply since the
|
|
568
|
-
|
|
569
|
-
(
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
568
|
+
- `turn` (the default) sends the running turn's reasoning: each reply since the turn's start carries its reasoning
|
|
569
|
+
as `reasoning_content` (OpenRouter takes it for DeepSeek, Qwen and other text-reasoning models). A steer, a plugin's
|
|
570
|
+
nudge (check-in, agents-md) or a Continue's text merged into the turn doesn't reset it, as on a native host: the
|
|
571
|
+
request after it is the one before plus the steer, so the server's prompt cache holds. A turn that goes on by itself
|
|
572
|
+
(a reminder, a context wake, a delegate report's wake) sends nothing of the one before it, an interrupted reply's
|
|
573
|
+
partial thought included. A **step-limit Continue** is the same turn going on: it keeps the reasoning of the run it
|
|
574
|
+
continues (back to the first run of a chain of Continues), so its first request matches the last one before the
|
|
575
|
+
limit and the server's prompt cache stays warm; Continue's text keeps it too. A `!cmd` run while the step-limit
|
|
576
|
+
question stands is a user message added after the run: the reasoning before it goes, and that Continue reads the
|
|
577
|
+
conversation again once (a re-prefill on a local server). After a `/model` change while the question stands the
|
|
578
|
+
Continue starts afresh (no other model's reasoning is sent).
|
|
579
|
+
- **A stock Qwen template (Splash, vLLM, SGLang; llama.cpp with Qwen's own template)** renders an earlier reply's
|
|
580
|
+
reasoning only after the last user message: a steer hides the reasoning before it whatever chi sends, and the
|
|
581
|
+
prompt changes there. `sampling: {chat_template_kwargs: {preserve_thinking: true}}` on the host renders every
|
|
582
|
+
reasoning chi sends (a reply sent without any renders an empty think block; adding it restarts the prompt cache
|
|
583
|
+
once). What it costs by level, with it on: under `turn` a steer no longer re-reads the turn, but each new turn still
|
|
584
|
+
reads the conversation again from the previous turn's first reply (that turn's reasoning leaves the request); under
|
|
585
|
+
`all` every request extends the one before (append-only), at the cost of a prompt that keeps the reasoning of every
|
|
586
|
+
turn. Without it (the template's default) both re-read from the reply before each user message. A strict server
|
|
587
|
+
that rejects unknown fields ("Unrecognized request argument supplied: chat_template_kwargs": OpenAI proper, some
|
|
588
|
+
gateways) fails every request with it, so set it only on a host you know takes it.
|
|
589
|
+
- A **direct DeepSeek API** host (`api.deepseek.com`) with tools wants the reasoning of every earlier reply, earlier
|
|
590
|
+
turns included, and answers HTTP 400 without it: under `turn` every turn after one that thought fails. Set
|
|
591
|
+
`reasoning_history: all` there (the example above). DeepSeek through OpenRouter takes `turn`.
|
|
578
592
|
- `drop` sends none (what chi did before this setting).
|
|
579
593
|
- `all` sends every reply's reasoning, earlier turns too. It sends only the field: a local template that leaves out
|
|
580
|
-
earlier turns' reasoning (Qwen's) needs `
|
|
594
|
+
earlier turns' reasoning (Qwen's) needs `preserve_thinking` as well (above).
|
|
581
595
|
- Order, first set wins: the session's own level, `SAMAGOTCHI_REASONING_HISTORY` (the whole process; for A/B runs),
|
|
582
596
|
the `models:` entry (found the way `profile:` is), the `hosts:` entry, then `turn`. Anything else than a level warns
|
|
583
597
|
once and counts as unset. A host's level reaches a worker when it starts, as its `sampling:` does.
|
|
@@ -597,9 +611,8 @@ models:
|
|
|
597
611
|
| 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 |
|
|
598
612
|
| nothing thought yet, the same level, or a native host | none |
|
|
599
613
|
|
|
600
|
-
- The native loop (`/completion`) ignores it: it always keeps the running turn's reasoning, a mid-turn steer included
|
|
601
|
-
|
|
602
|
-
a mid-turn steer's earlier ones included.
|
|
614
|
+
- The native loop (`/completion`) ignores it: it always keeps the running turn's reasoning, a mid-turn steer included,
|
|
615
|
+
and a step-limit Continue keeps the thoughts of the run it continues, a mid-turn steer's earlier ones included.
|
|
603
616
|
- When a host answers a request that sent reasoning back with an HTTP 400 about reasoning, chi asks again without it,
|
|
604
617
|
sends none to that model from then on (until the worker restarts) and says so once. If the request without it is
|
|
605
618
|
refused too, the thinking fields get the same fallback ("Thinking") and the reasoning goes back on the next step.
|
data/docs/context.md
CHANGED
|
@@ -346,6 +346,14 @@ show the answer as it is.
|
|
|
346
346
|
at the PR's head commit (a removed one: at its base), which has every
|
|
347
347
|
line.
|
|
348
348
|
- Code blocks, markdown links and URLs are left alone.
|
|
349
|
+
- **The ref menu**: each linked reference is also a
|
|
350
|
+
[file ref](hooks.md#refs-on-the-answer-display-only) on the answer: the
|
|
351
|
+
file in this checkout (the repository's root + the PR's path), so its
|
|
352
|
+
menu has the editor, show-in-turn and copy targets, plus **PR diff** (or
|
|
353
|
+
**File at head**, **File at base** for a removed file), the link's URL.
|
|
354
|
+
Only for a PR of this checkout's repository (one of its git remotes
|
|
355
|
+
names it on github.com): another project's PR keeps its links only.
|
|
356
|
+
Bundle 0.5.0, chi 0.61.0.
|
|
349
357
|
- The PR's files are read with `gh api` in the background when a turn
|
|
350
358
|
starts (and when a PR is attached while it runs), at most once a minute,
|
|
351
359
|
and read again only when the PR's head moved; the answer is linked from
|
data/docs/guardrails.md
CHANGED
|
@@ -35,7 +35,8 @@ Approve tool call?
|
|
|
35
35
|
|
|
36
36
|
In a terminal, answer with a number, the exact label, `y` (Allow once) or `n`
|
|
37
37
|
(Deny); add `; reason` to tell the model why (`n; open a PR instead`). An empty
|
|
38
|
-
answer, Ctrl-C, the web's Deny button or a cancelled turn deny it
|
|
38
|
+
answer, Ctrl-C, the web's Deny button or a cancelled turn deny it (on the web card, Enter in the reason box or
|
|
39
|
+
on an option submits, never denies). The prompt stays open during turns: when the question comes up it turns
|
|
39
40
|
into a yellow `? `, with the call and the options listed under it; only a line
|
|
40
41
|
submitted there answers it (never one typed before), and what you had typed comes back
|
|
41
42
|
once it closes. On a short terminal the list shrinks (the hint row, then the `in`/`why`
|
data/docs/hooks.md
CHANGED
|
@@ -51,7 +51,7 @@ The plugin class must respond to `#call(event)` — duck-typed, no base class re
|
|
|
51
51
|
|-------|--------------|---------------|
|
|
52
52
|
| `:session_start` | The first turn this chi process runs for the session (so again after `--resume`, a worker that idle-exited and woke, or a restart) | `{ type: :session_start, session_id: "..." }` |
|
|
53
53
|
| `:before_turn` | Before each turn starts | `{ type: :before_turn, session_id: "...", prompt: "..." (nil on a continue), resumed: true \| false (true on a step-limit Continue, which resumes the run before it; false on every other turn, a reminder's or a wake's too), messages: [...] (the history before this turn) }` |
|
|
54
|
-
| `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model)) }` |
|
|
54
|
+
| `:after_turn` | After a turn completed or was stopped once the model was asked (not after one that failed, was cut short by an interrupt signal (SIGINT), or was stopped before its `:before_turn` hooks) | `{ type: :after_turn, status: "completed" \| "canceled", present: (see [Presenting the answer](#presenting-the-answer-display-only)), refs: (see [Refs on the answer](#refs-on-the-answer-display-only)), messages: [...] (the conversation the turn stored; a cancelled or empty turn ends it with a `kind: turn_note` system message, and a context line is `kind: context`, see [sessions.md](sessions.md#notes-a-turn-leaves-for-the-model)) }` |
|
|
55
55
|
| `:before_generation` | Before each LLM API call (both loops) | `{ type: :before_generation, iteration: N }` |
|
|
56
56
|
| `:after_generation` | After LLM returns (both loops); not after a generation a hook cut (`stop_generation`) | `{ type: :after_generation, iteration: N, response: "...", messages: [...] (the conversation as sent) }` |
|
|
57
57
|
| `:generation_progress` | While the response streams, in batches (see [Watching the stream](#watching-the-stream)) | `{ type: :generation_progress, iteration: N, thinking: "..." (new since the last fire), text: "..." (new visible text), thinking_chars: N, text_chars: N (this generation so far), elapsed_ms: N, restarted: true (only on the first fire after a dropped stream was asked again) }` |
|
|
@@ -275,6 +275,77 @@ end
|
|
|
275
275
|
|
|
276
276
|
A plugin gets the same from `chi.on(:after_turn) { |event, ctx| event[:present].call { … } }`.
|
|
277
277
|
|
|
278
|
+
## Refs on the answer (display only)
|
|
279
|
+
|
|
280
|
+
`:after_turn` also carries `refs:`, a callable that attaches a **ref** to the
|
|
281
|
+
turn's answer: a thing the answer mentions (an issue, a file, a commit) as
|
|
282
|
+
data, with links to open it. Like `present`, it is display only.
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
class JiraRefs
|
|
286
|
+
def call(event)
|
|
287
|
+
return unless event[:type] == :after_turn
|
|
288
|
+
|
|
289
|
+
answer = event[:messages].last
|
|
290
|
+
return unless answer && answer[:role] == "model"
|
|
291
|
+
|
|
292
|
+
answer[:content].to_s.scan(/\b[A-Z][A-Z0-9]+-\d+\b/).uniq.each do |key|
|
|
293
|
+
event[:refs].call(match: key, kind: "issue", target: { id: key }, label: key,
|
|
294
|
+
actions: [{ id: "open", label: "Open in Jira", url: "https://jira.example.com/browse/#{key}" }])
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
- The call: `event[:refs].call(match:, kind:, target:, label: nil, actions: [])`,
|
|
301
|
+
keywords only (String or Symbol keys inside `target` and `actions`).
|
|
302
|
+
- `match`: the text in the answer the ref stands for (at most 500 characters).
|
|
303
|
+
- `kind`: a short lowercase word. `"file"` takes `target: {path:, line:, end_line:}`
|
|
304
|
+
(the lines optional); a relative path is resolved against the session's
|
|
305
|
+
folder, an absolute one is kept as given. Any other kind (`"issue"`,
|
|
306
|
+
`"commit"`, …) takes `target: {id:}`.
|
|
307
|
+
- `label`: what a UI shows for it (optional, at most 200 characters).
|
|
308
|
+
- `actions`: links a UI offers on top of the kind's own, each
|
|
309
|
+
`{id:, label:, url:}`; http(s) URLs only, without userinfo (`user:pass@`),
|
|
310
|
+
at most 8, opened in a new tab.
|
|
311
|
+
- It returns `true` when the ref is kept (or was there already: a duplicate
|
|
312
|
+
is kept once), `false` when it is refused, and `nil` when the turn has no
|
|
313
|
+
answer (as `present`: after a cancelled, failed or empty turn) or the call
|
|
314
|
+
comes after the hooks ran (a plugin that kept `event[:refs]`: the refs are
|
|
315
|
+
stored by then).
|
|
316
|
+
- Caps: 200 refs and 256 KiB (the list's JSON) per answer; an action's id
|
|
317
|
+
at most 64 characters and its label 100; a target id 200, a path 4,096.
|
|
318
|
+
- A refused ref is logged as `refs_rejected` with the hook's label and the
|
|
319
|
+
reason, each reason once per hook per answer (the same again is logged at
|
|
320
|
+
debug with its count). The call never raises into the hook.
|
|
321
|
+
- Refs are independent of `present`: a rejected `present` keeps them, and a
|
|
322
|
+
ref whose `match` isn't in the shown text is still stored (a UI drops it).
|
|
323
|
+
- They are kept as `display_refs` on the answer's model message in the
|
|
324
|
+
session file, next to `display`, and the `answer_display` event says how
|
|
325
|
+
many (`refs: N`). The model never sees them (as `display`), and a fork
|
|
326
|
+
(`chi.sessions.fork(messages: ctx.messages)`) starts without them.
|
|
327
|
+
- **The web** pairs each ref with the shown answer (its rendered markdown,
|
|
328
|
+
or the `present`ed display), live (with the turn-end read when the refs
|
|
329
|
+
are stored by then, else on the `answer_display` event) and on a reload. It looks, over all the answer's refs, for:
|
|
330
|
+
1. a link whose href is one of the ref's action URLs;
|
|
331
|
+
2. else a link whose text is `match`;
|
|
332
|
+
3. else `match` as a whole word inside one run of plain text (not in a
|
|
333
|
+
code block or another link), or an inline code that is `match` alone.
|
|
334
|
+
The word's edges are checked within that run only: text in the next
|
|
335
|
+
element doesn't count (`JIRA-1<em>2</em>` pairs `JIRA-1`).
|
|
336
|
+
|
|
337
|
+
The ref binds every occurrence its first finding step finds (a ref paired
|
|
338
|
+
with its link leaves a plain mention of the same text alone); a ref
|
|
339
|
+
nothing pairs is dropped. Markdown changes text: `__init__.py` renders
|
|
340
|
+
bold, escapes and entities differ, a match across emphasis or code isn't
|
|
341
|
+
one run, so keep a markdown link to the ref's URL, or match the rendered
|
|
342
|
+
text. The ref's actions join its menu (a link's own target is labelled by
|
|
343
|
+
the action with its URL); a `file` ref keeps the editor, show-in-turn and
|
|
344
|
+
copy targets; `label` is its hover title when it differs from the text.
|
|
345
|
+
**Terminals** ignore them.
|
|
346
|
+
|
|
347
|
+
A plugin gets the same from `chi.on(:after_turn) { |event, ctx| event[:refs].call(…) }`.
|
|
348
|
+
|
|
278
349
|
## Settings
|
|
279
350
|
|
|
280
351
|
A hook class whose `initialize` takes an argument gets its settings: **one
|
|
@@ -565,6 +636,7 @@ bundles:
|
|
|
565
636
|
pattern: '\bGH-(\d+)\b' # full form: a regex
|
|
566
637
|
url: 'https://github.com/org/repo/issues/{match}'
|
|
567
638
|
case_insensitive: false # optional, default false
|
|
639
|
+
kind: issue # optional: the refs' kind, default issue
|
|
568
640
|
max: 10 # optional: refs per line, default 10
|
|
569
641
|
note: false # optional: no sources line, default true
|
|
570
642
|
```
|
|
@@ -583,6 +655,13 @@ text ([`fallback_for: :display`](#what-a-hook-can-do-the-runtime)); it shows
|
|
|
583
655
|
when it names one the answer doesn't link (a ref only in code, two sources on
|
|
584
656
|
one ref) and whenever markdown is off. Bundle 0.4.0 needs chi 0.35.0 for that.
|
|
585
657
|
|
|
658
|
+
Each ref the answer links is also a [ref on the
|
|
659
|
+
answer](#refs-on-the-answer-display-only): kind `issue` (or the source's
|
|
660
|
+
`kind:`, a short lowercase word other than `file`), target `{id: "JIRA-123"}`,
|
|
661
|
+
label `JIRA JIRA-123`, and one action, **Open in JIRA** (the source's name),
|
|
662
|
+
its URL. The web's ref menu on the link shows it. A ref whose URL isn't
|
|
663
|
+
http(s) stays a markdown link only. Bundle 0.5.0 needs chi 0.61.0 for that.
|
|
664
|
+
|
|
586
665
|
The `prefix:` form compiles to `\b<prefix>-(\d+)\b` and the URL is
|
|
587
666
|
`base_url` + the full ref text (`JIRA-123`). The `pattern:` form takes a
|
|
588
667
|
regex and a `url:` template with placeholders (below). `case_insensitive:
|
data/docs/internals/engine.md
CHANGED
|
@@ -56,9 +56,11 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
56
56
|
│ # end_turn(turn, "completed" | "canceled"): under the event lock the session gets the kept
|
|
57
57
|
│ # messages, goes IDLE, records last_turn, and :turn_completed {result, turn_summary,
|
|
58
58
|
│ # display_pending} or :turn_canceled {cancellation_reason, cancelled_by, duration_ms} is emitted
|
|
59
|
-
│ # then the :after_turn hook (status, messages, present: the AnswerDisplay presenter
|
|
60
|
-
│ #
|
|
61
|
-
│ #
|
|
59
|
+
│ # then the :after_turn hook (status, messages, present: the AnswerDisplay presenter,
|
|
60
|
+
│ # refs: the AnswerRefs adder)
|
|
61
|
+
│ # store_answer_display (one replace of the answer: `display` and/or `display_refs`;
|
|
62
|
+
│ # :answer_display {display, refs: N} to the observers when a hook changed the answer or
|
|
63
|
+
│ # attached refs, or display: nil when the web was told to wait for it)
|
|
62
64
|
│ # then the :session_end hook
|
|
63
65
|
│
|
|
64
66
|
├── warm_up_next_turn(turn, result)
|
|
@@ -86,7 +88,7 @@ StandardError in run_turn:
|
|
|
86
88
|
# error_kind, retryable, host and summary.
|
|
87
89
|
→ re-raised
|
|
88
90
|
|
|
89
|
-
Interrupt (Ctrl-C):
|
|
91
|
+
Interrupt (Ctrl-C; the REPL only: a worker traps INT, see the signal row below):
|
|
90
92
|
→ if turn.ended: re-raised only
|
|
91
93
|
→ turn.controller.cancel!(:ctrl_c)
|
|
92
94
|
→ end_turn(turn, "canceled"): ctrl_c_messages keeps turn.messages with TurnNote.cancelled(:ctrl_c)
|
|
@@ -102,6 +104,17 @@ A Stop (Engine#cancel_current_turn!, e.g. the Bridge's POST cancel) or a hook's
|
|
|
102
104
|
→ complete_turn keeps the loop's conversation (tool iterations included) with a
|
|
103
105
|
TurnNote.cancelled note; a text streamed before the cancel stays, marked [interrupted]
|
|
104
106
|
|
|
107
|
+
A worker's TERM, HUP or INT (WorkerSignals, trapped by a spawned worker only):
|
|
108
|
+
→ a Stop with reason :signal (Worker#signalled, off the trap, retried until the loop leaves)
|
|
109
|
+
→ :turn_canceled {cancellation_reason: :signal}, saved by the Worker as after any turn;
|
|
110
|
+
Worker#run returns :signal (:stopped with the stop marker), takes no new work
|
|
111
|
+
→ not ended within Worker::SIGNAL_GRACE (a turn that ended and runs its after_turn hooks gets
|
|
112
|
+
SIGNAL_HARD_EXIT more, uncanceled): the session saved as it is, SignalException raised in the
|
|
113
|
+
loop's thread, only while the loop runs (never into start, serve or the teardown, which runs whole)
|
|
114
|
+
→ the process ends SIGNAL_HARD_EXIT (10 s) after that at the latest (exit!); a signal after the
|
|
115
|
+
loop left brings only that hard exit, and run_session_loop restores the default handlers
|
|
116
|
+
→ run_session_loop: exit 0, no discard; input queued behind the stopped turn wakes a new worker
|
|
117
|
+
|
|
105
118
|
Stopped before the model was asked (prepare_turn returned false):
|
|
106
119
|
→ stopped_before_model(turn) → end_turn(turn, "canceled")
|
|
107
120
|
→ nothing kept, :turn_canceled {cancellation_reason, cancelled_by, duration_ms}
|
|
@@ -183,14 +196,15 @@ The differences from the native loop:
|
|
|
183
196
|
- A model turn's saved reasoning (`thinking:`) goes back as `reasoning_content` per the turn's
|
|
184
197
|
`TurnSettings#reasoning_history` (`ReasoningHistory.resolve`, the session's own level first:
|
|
185
198
|
`Session#reasoning_history`, `/reasoning-history`): `all` on every model message, `turn` past
|
|
186
|
-
`ChatLoop#reasoning_boundary`: the
|
|
187
|
-
the
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
`
|
|
193
|
-
|
|
199
|
+
`ChatLoop#reasoning_boundary`: the Run's `@reasoning_start`, fixed once per Run by `ReasoningHistory.run_start`
|
|
200
|
+
(the rule `KernelLoop#start_turn` uses too) from the Run's start (the conversation length at `#complete`), so a
|
|
201
|
+
continue turn (reminder, context wake, delegate report wake) replays nothing of the run before. Entries the Run
|
|
202
|
+
merges (a person's line, a plugin's steer, a Continue's text) don't move it, as natively (nothing re-sanitizes
|
|
203
|
+
within a run): the request after a steer extends the one before. A step-limit Continue's Run starts where the run
|
|
204
|
+
it resumes did: `#complete`'s `reasoning_since:` (the Engine's `resume_from`, checked against the messages' length
|
|
205
|
+
in `#resumed_run_start`), past a user entry added after that run's last reply (a `!cmd`'s output). Without
|
|
206
|
+
`since:` (`#reasoning_chars` from outside a Run) the boundary is the last query (a `role: "user"` or `turn_start`
|
|
207
|
+
entry). `Run#prompt_chars` counts it (`#reasoning_chars`).
|
|
194
208
|
- A 400 about reasoning (`BadRequest#reasoning_refused?`) for a request that sent reasoning back asks the step again
|
|
195
209
|
without it; when that goes through, `#reasoning_history_refused!` marks the model (worker lifetime) and
|
|
196
210
|
`:reasoning_history_refused` goes out. When it is refused too, the thinking-fields fallback (`#thinking_refused!`,
|
|
@@ -227,13 +241,22 @@ One iteration with tool calls:
|
|
|
227
241
|
- Runs the gate first, so `:tool_call_started` shows the call that will run (a before_tool_call hook may
|
|
228
242
|
replace it; known-names corrects a misspelled name this way, and the model gets one line saying what ran).
|
|
229
243
|
- Emits `:tool_call_started` {iteration, call_count, call_index, tool, call, params, label (plugin tools),
|
|
230
|
-
title, called_as, view, ref}. The row fields (title, called_as, view, ref) come from `ToolRowFields.for`, which the
|
|
244
|
+
title, called_as, view, ref, refs}. The row fields (title, called_as, view, ref) come from `ToolRowFields.for`, which the
|
|
231
245
|
reload (`Web::MessageParts#tool_part`) calls too. `ToolRowFields::ACTIVITY_KEYS` (title, called_as) travel
|
|
232
|
-
inside `activity` on the completed event; `EVENT_KEYS` (view, ref) go top-level on both events.
|
|
246
|
+
inside `activity` on the completed event; `EVENT_KEYS` (view, ref, refs) go top-level on both events.
|
|
233
247
|
`Bridge::TurnAccumulator` (snapshot and replay) and the web (`activity.js` `ACTIVITY_FIELDS` /
|
|
234
248
|
`EVENT_FIELDS`) copy them by these lists, so a new row field is added there. `ref` is `FileRef.for` (a
|
|
235
249
|
read/write/edit's file against the cwd, `{kind: "file", path, line?, end_line?}`); the web opens it in the editor
|
|
236
250
|
that `Web::Editor` (web.editor) names, for a local viewer only (`App#viewer_capabilities`).
|
|
251
|
+
`refs` is a plugin tool's row refs: its registry entry's `refs` (chi.tool's `refs:` lambda, `Api.per_call`), run
|
|
252
|
+
once per call by `RowRefs.for` (never raises: a raise or a non-list gives none, `tool_refs_failed`; each entry a
|
|
253
|
+
`Ref`, a bad one dropped, `tool_refs_rejected`; a relative file resolved against the session's folder
|
|
254
|
+
(`KernelLoop#session_cwd`, else the process's), one outside it (its spelling or its real path) dropped, a kept one
|
|
255
|
+
stored under the real path; at most `RowRefs::MAX_REFS` and `MAX_BYTES`). `ToolRowFields.for` can't build it (no plugins on a reload), so the run carries it
|
|
256
|
+
as `refs:`, saved with the result as `tool_refs` (`ToolResponse::SAVED_KEYS`, never sent), and
|
|
257
|
+
`MessageParts#tool_part` reads it back through `RowRefs.for_display` (checked again). The page binds them to the
|
|
258
|
+
row's title (`refs.js bindRowRefs`, from `turn_view.js` live and on a reload): the first ref on it, the rest in its
|
|
259
|
+
menu.
|
|
237
260
|
- Settles an ask verdict after `:tool_call_started`, so a UI shows the tool line and then the approval.
|
|
238
261
|
- Dispatches through `KernelLoop#dispatch_tool_call`, or writes the denial text. Unknown tools are answered in
|
|
239
262
|
`KernelLoop#dispatch` ("Error: no such tool ...", never repeating the wrong name).
|
|
@@ -299,6 +322,17 @@ end
|
|
|
299
322
|
turn through the Bridge (`POST cancel` → `Engine#cancel_current_turn!`).
|
|
300
323
|
- `on_event` is the turn's sink.
|
|
301
324
|
- `ended` is set by `end_turn`.
|
|
325
|
+
- The live conversation: both loops register the array they append to on the kernel
|
|
326
|
+
(`KernelLoop#llm_context_turn`, `LLMContextForget::Turn#conversation`; `KernelLoop#start_turn`,
|
|
327
|
+
`ChatLoop::Run#initialize`). `Engine#live_conversation` returns it while a turn runs (nil otherwise: the kernel
|
|
328
|
+
never clears it). A worker's drain (`Worker#pending_input_drain`, on the turn's thread at every iteration
|
|
329
|
+
boundary) copies it, with the lines that boundary merges, into `Session#turn_checkpoint` (`TurnCheckpoint`):
|
|
330
|
+
every save while the turn runs writes that copy with an `interrupted_turn` mark instead of the session's
|
|
331
|
+
messages, written at the first boundary and then at most every 10 s, so a killed worker loses at most that much
|
|
332
|
+
of its turn. `session.messages` itself is untouched until the turn ends. When the checkpoint writes, the worker
|
|
333
|
+
also calls `Engine#checkpoint_metrics` (`SessionMetrics#persist(running_turn: true)`): `analytics.json` gets the
|
|
334
|
+
running turn's record so far as a killed turn's (canceled, `worker_stopped`), which the next worker loads; the
|
|
335
|
+
turn's end writes over it.
|
|
302
336
|
|
|
303
337
|
### GenerationPhase
|
|
304
338
|
|
|
@@ -352,7 +386,7 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
352
386
|
| `:turn_completed` | normal end | `{result, turn_summary, display_pending}` |
|
|
353
387
|
| `:turn_canceled` | Stop, hook stop, Ctrl-C | `{cancellation_reason, cancelled_by?, duration_ms}` |
|
|
354
388
|
| `:turn_failed` | an error | `{error_class, message, duration_ms, error_kind?, retryable?, host?, summary?}` |
|
|
355
|
-
| `:answer_display` | after the after_turn hooks (observers only, not the sink) | `{display}` (a string, or nil) |
|
|
389
|
+
| `:answer_display` | after the after_turn hooks (observers only, not the sink) | `{display, refs?}` (display a string, or nil; refs the count of the answer's refs, only when a hook attached some) |
|
|
356
390
|
|
|
357
391
|
The boundary events (`:turn_started`, `:turn_completed`, `:turn_canceled`, `:turn_failed`) also carry `origin`
|
|
358
392
|
when the turn was queued with one.
|
|
@@ -410,6 +444,14 @@ puts the hook runtime on the event: `event[:hook]` (the hook's label) and five h
|
|
|
410
444
|
|
|
411
445
|
From `:after_turn` and `:session_end` there is no turn left: `stop_turn` and `steer` return false.
|
|
412
446
|
|
|
447
|
+
`:after_turn` adds two of its own, built by `complete_turn` on the stored conversation's last message when it is the
|
|
448
|
+
model's: `event[:present]` (`AnswerDisplay`, the answer's `display` text) and `event[:refs]` (`AnswerRefs`, refs kept
|
|
449
|
+
as `display_refs`: caps, validation, relative file paths resolved against the session's folder, `refs_rejected`
|
|
450
|
+
logged). Both are display only: `AnswerDisplay::DISPLAY_ONLY` lists `display`, `empty_answer` and `display_refs`, which
|
|
451
|
+
`strip`/`strip_all` drop from every model-facing copy (hooks' `messages:`, the recap, `ctx.messages`,
|
|
452
|
+
after_generation), while `KernelLoop#sanitize_history` and `ChatLoop#plain` copy them so the stored conversation keeps
|
|
453
|
+
them across turns. `Web::App#messages_for_display` sends `display_refs` re-validated (`AnswerRefs.for_display`).
|
|
454
|
+
|
|
413
455
|
### Hook lifecycle
|
|
414
456
|
|
|
415
457
|
- **Bundle hooks** (`Registry#register_bundle`) and **config hooks** (`register_persistent`, from config.yml)
|
|
@@ -127,7 +127,14 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
127
127
|
a per-session value in the stable part. A guard spec checks that model, location and session are the last
|
|
128
128
|
sections (`spec/system_prompt_model_spec.rb`).
|
|
129
129
|
2. **History is append-only.** Never rewrite an earlier message. Add a note at the tail instead. If a model's
|
|
130
|
-
spec forces a rewrite (e.g. stripping previous-turn thinking), accept the re-prefill and note it.
|
|
130
|
+
spec forces a rewrite (e.g. stripping previous-turn thinking), accept the re-prefill and note it. Within a
|
|
131
|
+
turn both loops keep the turn's replies as sent across a mid-turn steer, a plugin's nudge and a Continue's text
|
|
132
|
+
(`ReasoningHistory.run_start` fixes where the run's reasoning starts), so the request after a steer is the one
|
|
133
|
+
before plus the steer; a new turn drops the previous turn's reasoning under `turn` (one re-prefill from its
|
|
134
|
+
first reply, on a server that renders it; a stock Qwen template without `preserve_thinking` re-renders from the
|
|
135
|
+
reply before every user message whatever chi sends). A worker's
|
|
136
|
+
mid-turn checkpoint (`TurnCheckpoint`) is append-only too (the history plus what the loop appended) and
|
|
137
|
+
never changes the request bytes.
|
|
131
138
|
3. **Deterministic order.** Tools, memory lists and anything else that's rendered from a hash or a set needs a
|
|
132
139
|
fixed sort. Plugin tools are sorted by (bundle, name) whatever order their plugins finished starting in
|
|
133
140
|
(R5a).
|