samagotchi 0.63.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -1
- data/docs/architecture.md +32 -5
- data/docs/cli.md +41 -9
- data/docs/configuration.md +143 -11
- data/docs/internals/engine.md +25 -6
- data/docs/plugins.md +7 -0
- data/docs/sessions.md +14 -10
- data/docs/sub-agent.md +21 -4
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +5 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +1 -0
- data/lib/samagotchi/bridge.rb +29 -5
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -3
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_reports.rb +7 -2
- data/lib/samagotchi/cli/parent_wait.rb +9 -2
- data/lib/samagotchi/client.rb +4 -2
- data/lib/samagotchi/composer_route.rb +90 -0
- data/lib/samagotchi/config.rb +11 -7
- data/lib/samagotchi/continue_offer.rb +17 -3
- data/lib/samagotchi/delivery.rb +13 -8
- data/lib/samagotchi/engine.rb +41 -7
- data/lib/samagotchi/generation_phase.rb +17 -0
- data/lib/samagotchi/host_config.rb +33 -12
- data/lib/samagotchi/host_model.rb +2 -2
- data/lib/samagotchi/host_registry.rb +34 -11
- data/lib/samagotchi/idle_client.rb +62 -25
- data/lib/samagotchi/idle_recap.rb +5 -2
- data/lib/samagotchi/idle_target.rb +30 -7
- data/lib/samagotchi/interrupted_turn.rb +7 -3
- data/lib/samagotchi/last_turn.rb +5 -2
- data/lib/samagotchi/llm/adapters.rb +41 -0
- data/lib/samagotchi/llm/anthropic_assembly.rb +210 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +416 -0
- data/lib/samagotchi/llm/anthropic_wire.rb +238 -0
- data/lib/samagotchi/llm/api_key.rb +26 -9
- data/lib/samagotchi/llm/chat_loop.rb +113 -14
- data/lib/samagotchi/llm/dialect.rb +57 -0
- data/lib/samagotchi/llm/errors.rb +81 -12
- data/lib/samagotchi/llm/model_listing.rb +111 -0
- data/lib/samagotchi/llm/model_result.rb +9 -2
- data/lib/samagotchi/llm/openai_chat.rb +46 -93
- data/lib/samagotchi/model_profile.rb +1 -1
- data/lib/samagotchi/pending_input_queue.rb +10 -3
- data/lib/samagotchi/plugin/sessions.rb +27 -5
- data/lib/samagotchi/reasoning_history.rb +14 -4
- data/lib/samagotchi/recap_setup.rb +2 -1
- data/lib/samagotchi/reply_wait.rb +171 -6
- data/lib/samagotchi/self_report.rb +18 -28
- data/lib/samagotchi/send_command.rb +63 -19
- data/lib/samagotchi/session.rb +3 -2
- data/lib/samagotchi/session_commands.rb +1 -1
- data/lib/samagotchi/session_inbox.rb +63 -7
- data/lib/samagotchi/session_manager.rb +49 -20
- data/lib/samagotchi/session_metrics.rb +6 -11
- data/lib/samagotchi/steer_cut.rb +4 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +55 -28
- data/lib/samagotchi/terminal_ui/event_renderer.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -22
- data/lib/samagotchi/terminal_ui/status_row.rb +24 -3
- data/lib/samagotchi/terminal_ui.rb +72 -57
- data/lib/samagotchi/tool_activity.rb +5 -5
- data/lib/samagotchi/tools/delegate.rb +28 -1
- data/lib/samagotchi/tools/delegate_cursor.rb +9 -4
- data/lib/samagotchi/tools/delegate_wait.rb +17 -5
- data/lib/samagotchi/tools/execute.rb +11 -4
- data/lib/samagotchi/turn_checkpoint.rb +9 -3
- data/lib/samagotchi/turn_note.rb +22 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -1
- data/lib/samagotchi/web/public/activity.js +3 -0
- data/lib/samagotchi/web/public/app.js +96 -13
- data/lib/samagotchi/web/public/commands.js +8 -8
- data/lib/samagotchi/web/public/composer.js +3 -3
- data/lib/samagotchi/web/public/delivery_line.js +228 -0
- data/lib/samagotchi/web/public/index.html +29 -0
- data/lib/samagotchi/web/public/live_turn.js +8 -0
- data/lib/samagotchi/web/public/stage_model.js +27 -7
- data/lib/samagotchi/web/public/stage_view.js +32 -10
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +2 -1
- data/lib/samagotchi/web/public/turn_events.js +22 -8
- data/lib/samagotchi/web/public/turn_html.js +20 -4
- data/lib/samagotchi/web/public/turn_model.js +29 -12
- data/lib/samagotchi/web/public/turn_view.js +6 -4
- data/lib/samagotchi/worker.rb +7 -5
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 43045b7872d6b50ba24cc2ef1678ac6708346aac9236a5764593660e4e81fc6f
|
|
4
|
+
data.tar.gz: dad96a9922329ea3294d7bf04159b3c829796bdfda03d42eef724bbee4a3e4e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a35a44e9905f8c3411a656383c8a7f2493318732b959d526cdcbdc5dba90e71e874fe678057d3b060878dc5e4560398683790dade299258bd43cf541e0b62cf
|
|
7
|
+
data.tar.gz: 9dc56d9cd85b5c5f323315f99bc7c2f4e7580927966f41723a833db561dd5d6c0b3a8720c92ea32e1fafbb3b75e9eb0e93e57e9c4784e1a9983f95e7ac6f17c6
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,89 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.65.0] - 2026-10-13
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Anthropic Messages hosts** (`api: anthropic`): streamed text and thinking, native tool use, images, usage with
|
|
16
|
+
cache reads and writes, thinking blocks replayed verbatim (also after `--resume`; `reasoning_history` defaults to
|
|
17
|
+
`all` there). A new host key `auth: bearer|x-api-key` (any host) picks the header the key goes in. Thinking levels
|
|
18
|
+
map to Messages' thinking (`off` disabled, `low`/`medium`/`high` adaptive at that effort, `default` nothing), with
|
|
19
|
+
fallbacks when a host refuses a field. Recaps, `ctx.ask_model` and broadcast triage work there too. See
|
|
20
|
+
docs/configuration.md, "Anthropic Messages hosts". **Known limit until the next release:** Claude 5.x models
|
|
21
|
+
(Opus, Sonnet and Haiku 5.5, Fable 5.1) may stop a turn with a 400 "… bound to a different conversation" after the
|
|
22
|
+
history changed under their thinking blocks; the next release adds the recovery.
|
|
23
|
+
- `dialect: fireworks` on an `api: anthropic` host, for Fireworks directly or behind a gateway: Bearer auth, no
|
|
24
|
+
thinking `display`, no `cache_control`, and `x-session-affinity` for cache routing. `chi self` names it.
|
|
25
|
+
- A provider's refusal (Messages `stop_reason: refusal`, or an empty `content_filter` stop) ends the turn with
|
|
26
|
+
`no answer: refused by the provider (category)` in every UI and isn't retried. A `content_filter` stop after some
|
|
27
|
+
text keeps the text, marked `[cut off by the provider's content filter]`.
|
|
28
|
+
- Web composer: a row above the text field shows the keys while idle and, while a turn runs, where a message sent now
|
|
29
|
+
goes in (after this thinking step, tool call or answer, with its clock), with `cut in now` and `queue for after the
|
|
30
|
+
turn` links. Alt+Enter (Option+Enter) queues. See docs/cli.md, "Typing during a turn".
|
|
31
|
+
- Terminal: while a turn runs, the status row ends in `· typing goes in at the next step (/cut, /queue)`.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Web: a message queued for after the running turn sits below the running turn's card, not among its rows, also
|
|
36
|
+
after a reload.
|
|
37
|
+
- A chat host that refuses a sampling field (`temperature`, `top_p`, `top_k`) gets the step again without it instead
|
|
38
|
+
of the turn failing; on `api: openai` hosts such a 400 no longer turns reasoning off.
|
|
39
|
+
- A Splash or Fireworks model's vision comes from its model list instead of reading "unknown".
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- A server error without a numeric code is classified by its type: an invalid request fails at once instead of being
|
|
44
|
+
retried, an unknown model (404, `NOT_FOUND`) is no longer retried, a `timeout_error` is a 504.
|
|
45
|
+
- Anthropic's context-overflow 400s ("prompt is too long", "input length and `max_tokens` exceed context limit")
|
|
46
|
+
roll the turn back and report it as too long for the context window, also through a gateway on the OpenAI path.
|
|
47
|
+
|
|
48
|
+
Update with `chi update` (the system bundle moves with the gem).
|
|
49
|
+
|
|
50
|
+
## [0.64.0] - 2026-10-13
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- `/queue TEXT` during a running turn now works in an attached terminal and the web composer too, as it did in the
|
|
55
|
+
plain REPL: TEXT runs as a turn of its own after the running one, instead of merging into it (`(queued: runs after
|
|
56
|
+
this turn)`). A session whose worker is older than this says to restart it rather than steering the turn. See
|
|
57
|
+
docs/cli.md, "Typing during a turn".
|
|
58
|
+
- `chi send --queue`: the message runs as a turn of its own after the running turn instead of merging into it, and
|
|
59
|
+
`chi send --queue --wait` waits for that turn's own answer (a running turn no longer refuses it; the running turn's
|
|
60
|
+
reply, end and questions are not the answer). Each turn's reply file now carries its message's id. See
|
|
61
|
+
docs/sessions.md, "Sending a message", and docs/sub-agent.md.
|
|
62
|
+
- Plugins: `ctx.sessions.send(id, text, delivery: :queue)` runs the message as a turn of its own after the session's
|
|
63
|
+
running turn; `delivery: :cut` raises (plugins never cut). See docs/plugins.md.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- Plain REPL (`--no-shared`): the lines a turn left over (a steer it didn't merge, `/queue` lines, image lines, commands
|
|
68
|
+
that wait) run in the order you sent them; before, an unmerged steer ran ahead of a `/queue` line typed earlier.
|
|
69
|
+
- A message with images sent during a running turn no longer holds back the text lines sent after it: they go in at
|
|
70
|
+
the turn's next step as usual, and the image message still runs as a turn of its own after it.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- Stopping a session mid-turn (`chi sessions stop`, the web's stop, a parent stopping its delegate) cancels the turn
|
|
75
|
+
with its own reason, `session_stopped`, instead of the user's Stop: the turn's line reads "canceled (session
|
|
76
|
+
stopped)", a command it was running "stopped as the session was stopped" (slow-commands 0.2.2 counts it as
|
|
77
|
+
stopped, not slow), and `chi send --wait --format json` reports `cancel_reason: session_stopped`.
|
|
78
|
+
- A delegate (and any worker a session's own commands start, such as `chi send --new`) reads `hosts:` from config.yml
|
|
79
|
+
as it starts, so it sees a host added or changed after its parent's worker started; before, it got the parent's
|
|
80
|
+
start-time hosts through `SAMAGOTCHI_HOSTS_JSON`, which workers no longer inherit (one set in your shell
|
|
81
|
+
applies to that chi process only; put hosts in config.yml).
|
|
82
|
+
- Web: a step that only called plugin tools with a verb label is titled by the label ("echoing"), not "working
|
|
83
|
+
with echoing"; among other tools such a call is named by its tool ("working with read, echo").
|
|
84
|
+
- Web: a send from a stale tab into a session deleted meanwhile goes back to the start page with a "was not
|
|
85
|
+
found" notice, the line kept in the composer, instead of an alert.
|
|
86
|
+
- `chi send`, the web composer and a plugin's `ctx.sessions.send` now report a message the session's worker refuses
|
|
87
|
+
(a 4xx answer such as a delivery it doesn't run) instead of writing it as an input file the worker would merge
|
|
88
|
+
anyway; a body over the worker's size limit still goes in as a file, as before.
|
|
89
|
+
- `chi answer`, `chi send --wait ID` (no message) and `delegate_result` wait for the reply of the turn they are about
|
|
90
|
+
(the answered turn, the running turn, the follow-up's turn), not a newer one queued behind it; a Continue of the
|
|
91
|
+
step-limit question gets the continued turn's reply, and a Stop ends the wait as `not_continued`. Workers older
|
|
92
|
+
than this keep the old behaviour.
|
|
93
|
+
|
|
11
94
|
## [0.63.0] - 2026-10-13
|
|
12
95
|
|
|
13
96
|
### Added
|
|
@@ -2750,7 +2833,9 @@ and long-lived sessions.
|
|
|
2750
2833
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2751
2834
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2752
2835
|
|
|
2753
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2836
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.65.0...HEAD
|
|
2837
|
+
[0.65.0]: https://github.com/dm1try/samagotchi/compare/v0.64.0...v0.65.0
|
|
2838
|
+
[0.64.0]: https://github.com/dm1try/samagotchi/compare/v0.63.0...v0.64.0
|
|
2754
2839
|
[0.63.0]: https://github.com/dm1try/samagotchi/compare/v0.62.0...v0.63.0
|
|
2755
2840
|
[0.62.0]: https://github.com/dm1try/samagotchi/compare/v0.61.0...v0.62.0
|
|
2756
2841
|
[0.61.0]: https://github.com/dm1try/samagotchi/compare/v0.60.0...v0.61.0
|
data/docs/architecture.md
CHANGED
|
@@ -73,6 +73,12 @@ TerminalUI ── delegates ──▶ Engine
|
|
|
73
73
|
delegates all core work to an `Engine`.
|
|
74
74
|
- The `on_event:` seam on `run_turn` exposes raw `KernelLoop` events plus the
|
|
75
75
|
higher-level turn events, so any new UI can render without terminal coupling.
|
|
76
|
+
- **`ComposerRoute`** (`lib/samagotchi/composer_route.rb`) decides where a line typed at
|
|
77
|
+
either terminal's prompt goes (the REPL's `steer_line` / `run_input_line`, the attached
|
|
78
|
+
`AttachedLoop#submit`): `/cut TEXT` / `/queue TEXT`, a terminal's own word, a session
|
|
79
|
+
command, a typo, a prompt. Each UI keeps what it does with the route. The web's
|
|
80
|
+
`composer.js#routeComposerLine` decides the same; `spec/fixtures/composer_routes.json`
|
|
81
|
+
holds the cases both answer (see [testing.md](testing.md)).
|
|
76
82
|
|
|
77
83
|
## Request / turn flow
|
|
78
84
|
|
|
@@ -94,7 +100,7 @@ terminal renders the events it gets over the Bridge.
|
|
|
94
100
|
| Core | `Samagotchi::Engine` | System prompt, memory injection, tool declarations, session lifecycle, model↔tool loop (`run_turn`). No terminal coupling. |
|
|
95
101
|
| UI | `Samagotchi::TerminalUI` | REPL (Reline), rendering, REPL commands. Delegates all core work to an `Engine`. |
|
|
96
102
|
| Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt or OpenAI chat API, chosen per host (see below). |
|
|
97
|
-
| Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, and the HTTP
|
|
103
|
+
| Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, Anthropic's Messages API, and the HTTP they share. |
|
|
98
104
|
| Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, forget_outputs, plus runtime/output-guardrails. |
|
|
99
105
|
| Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
|
|
100
106
|
| Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. The spawned process (own process group) forks the worker, which `setsid`s and is reparented to init once its starter exits (it waits until the worker holds `owner.lock`, so the spawner's `Process.detach` waiter tells a starting worker): nothing that walks the spawner's process tree reaches it (`SAMAGOTCHI_WORKER_DETACH=1`, `detach_from_spawner`). The lock's pid is the worker's; `spawn`'s `child_pid` is its starter's. |
|
|
@@ -369,7 +375,11 @@ session every 10 s, so a `kill -9` shows within a second. The summary (`Web::Ses
|
|
|
369
375
|
shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
|
|
370
376
|
`bridge_up` (the sidecar is there *and* the lock is held: the page attaches its stream on it), and
|
|
371
377
|
from that sidecar `worker_version` (the chi the worker runs) and `worker_features`
|
|
372
|
-
(`Bridge::FEATURES`, what a client may ask of it beyond the routes
|
|
378
|
+
(`Bridge::FEATURES`, what a client may ask of it beyond the routes: `restart`, `task_stop`, `reparent`,
|
|
379
|
+
`delivery`, a worker that queues a message for after the turn; `SessionManager.deliver_turn` and
|
|
380
|
+
the attached TUI refuse a queue to a worker without it, which would steer; `turn_ids`, a worker whose running
|
|
381
|
+
turn's mark, continue turn and Stop carry the turn's `enqueued_id`, which `chi answer`, `chi send --wait` with no
|
|
382
|
+
message and `delegate_result` rely on through `ReplyWait.own_turn`). A worker's Bridge snapshot
|
|
373
383
|
names its `chi_version` too.
|
|
374
384
|
`POST/DELETE /api/sessions` and `/stop` rescan the session before answering (`SessionHub#touch`).
|
|
375
385
|
`GET /api/sessions` is the hub's projection too (its sort, paging and total). `Server` always builds
|
|
@@ -388,6 +398,7 @@ Engine picks the loop from the effective model's host (`HostRegistry#resolve`):
|
|
|
388
398
|
|---|---|---|---|
|
|
389
399
|
| Raw prompt | `KernelLoop`, wrapped by `LLM::NativeBackend` | no `api:`, or `llama_cpp`/`mlx`/`omlx` | `Client` (`/completion` or `/v1/completions`), chi's own Gemma/Qwen prompt and tool-call parsing |
|
|
390
400
|
| Chat | `LLM::ChatLoop` | `api: openai` | `LLM::OpenAIChat` (`/v1/chat/completions`, streamed, native tool calls) |
|
|
401
|
+
| Chat | `LLM::ChatLoop` | `api: anthropic` | `LLM::AnthropicMessages` (`/v1/messages`, streamed SSE, native `tool_use`) |
|
|
391
402
|
|
|
392
403
|
Both return an `LLM::ModelResult` and emit the same stream events; tool calls in
|
|
393
404
|
both go through `ToolRunner` (events, hooks, veto, output cap) and
|
|
@@ -428,12 +439,28 @@ threshold puts the model's own `[CONTEXT: …]` line on the tail as a system
|
|
|
428
439
|
message (docs/internals/context-telemetry.md).
|
|
429
440
|
|
|
430
441
|
**Adapters.** `Client` (raw-prompt servers) and `LLM::OpenAIChat` (one per host,
|
|
431
|
-
`HostRegistry#adapter_for
|
|
432
|
-
reader for streamed bodies, the retry loop (`retry.*`; network errors, 429 and
|
|
442
|
+
`HostRegistry#adapter_for`, built by `LLM::Adapters` from the host's api) share
|
|
443
|
+
`LLM::HTTP`: timeouts, TLS for https, a line reader for streamed bodies, the retry loop (`retry.*`; network errors, 429 and
|
|
433
444
|
500/502/503/504/529, honouring `Retry-After`; never after a stream has produced
|
|
434
445
|
output) and cancel. Cancel closes the in-flight socket from the
|
|
435
446
|
`CancellationController` listener, so it works on any thread.
|
|
436
447
|
|
|
448
|
+
`LLM::AnthropicMessages` talks Anthropic's Messages API (`POST {root}/v1/messages`) for the same chat loop:
|
|
449
|
+
ChatLoop builds the same OpenAI-shaped wire messages for every chat host, and the adapter translates them
|
|
450
|
+
(`LLM::AnthropicWire`: the head system message becomes the top-level `system` blocks, tool calls `tool_use` blocks,
|
|
451
|
+
tool messages `tool_result` blocks in one user message, image parts `image` blocks, later system messages user
|
|
452
|
+
text; empty text blocks are never sent). Its answer (`LLM::AnthropicAssembly`) is a `ChatResponse` whose
|
|
453
|
+
`provider_content` holds the content blocks as received, which ChatLoop saves on the model entry as
|
|
454
|
+
`anthropic_content` (string keys, so a resumed session sends the same bytes) and sends back verbatim when the
|
|
455
|
+
adapter `replays_content?` (only for entries whose calls are all paired, never for an interrupted one; earlier
|
|
456
|
+
entries' thinking blocks left out under `reasoning_history: turn`) (thinking blocks
|
|
457
|
+
need their signature and their order among text and `tool_use`). Usage is normalized to the OpenAI shape the rest
|
|
458
|
+
of chi reads (the prompt is input + cache reads + cache writes), stop reasons to the loop's finish reasons
|
|
459
|
+
(`max_tokens` → `length`, `tool_use` → `tool_calls`, `end_turn` → `stop`). A stream is read by content-block index
|
|
460
|
+
from the `data:` lines alone (`event:` lines are not needed); `message_start` / `message_delta` events reach
|
|
461
|
+
`on_delta` with that normalized `usage`, pings not at all, and a block's start or delta counts as shown for the
|
|
462
|
+
first-token limit. Both adapters share their model-list cache (`LLM::ModelListing`).
|
|
463
|
+
|
|
437
464
|
**Errors.** A failed request raises an `LLM::ProviderError` of one kind:
|
|
438
465
|
`ConnectionError` (`RetryExhausted`), `RateLimited` (`CreditsHeld`: a 402 about
|
|
439
466
|
credit held by in-flight requests, retried after 20 s), `OutOfCredits` (any other
|
|
@@ -448,7 +475,7 @@ stream events (server counts, else a chars/4 estimate). Model lists are `LLM::Mo
|
|
|
448
475
|
server (`/props`), then the host's model list, then config.
|
|
449
476
|
|
|
450
477
|
**Keys.** A host's API key comes only from the environment variable its
|
|
451
|
-
`api_key_env:` names; it never reaches config.yml, `
|
|
478
|
+
`api_key_env:` names; it never reaches config.yml, `SAMAGOTCHI_HOSTS_JSON`, `chi self`,
|
|
452
479
|
logs or events.
|
|
453
480
|
|
|
454
481
|
## Other components
|
data/docs/cli.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
- `chi sessions list|stop|restart|archive|unarchive|delete|prune|clean|stats` — manage persisted sessions; `list` shows this git project's, `list --scope=all` every one, a delegated session with `↳ <parent>`, `list --archived` the archived ones too (see [Sessions](sessions.md)). `stats ID` prints a session's cost, tokens and progress (a live worker's `GET stats`, else its saved `analytics.json`; `--format json` one object) without running a model turn. A usage error (an unknown subcommand or flag, a flag missing its value, `stop`, `restart` or `delete` with no ids, a bad `list --format` or `--scope`) exits 2; an unknown or refused session exits 1
|
|
19
19
|
- `chi note [--source NAME] [-m TEXT] (ID|PREFIX)... | --all` — add a context note (TEXT or stdin) to sessions: background the model sees on its next turn; it starts no turn (see [Sessions: Context notes](sessions.md#context-notes))
|
|
20
20
|
- `chi context <add|push|ls|show|refresh|rm|mute|unmute> …` — attach live external text to sessions (a command chi runs every so often, or text pushed in); the model gets a short note when it changes and reads it with `context_read`; `chi context add <URL>` attaches a GitHub PR with the github-pr bundle (see [Attached context](context.md))
|
|
21
|
-
- `chi send [-m TEXT] [--image PATH]... [--cut] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; `--format json` prints one JSON object instead; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
|
|
21
|
+
- `chi send [-m TEXT] [--image PATH]... [--cut | --queue] (ID|PREFIX)...` — send a message to sessions as if typed there: a turn starts (or a running one takes it at its next step; `--cut` asks it to cut long thinking for it; `--queue` runs it as a turn of its own after the running one, and `--queue --wait` waits for that turn's own answer); piped stdin goes above `-m` as quoted context, and `--image` attaches images (see [Sessions: Sending a message](sessions.md#sending-a-message)); `--new` starts a session with it instead (`--new --continues ID|last:ID` the next link of that session's chain, see [Session chains](sessions.md#session-chains)), and `--wait` prints the answer (`--wait ID` with no message waits for the next reply without sending; `--format json` prints one JSON object instead; exit 3 a question waits, its options on stderr; exit 4 `--timeout` passed with the turn still running; see [Starting a session](sessions.md#starting-a-session))
|
|
22
22
|
- `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). An approval can be denied, not allowed, unless `guardrails.parent_approvals: once` (a convention, not a security boundary; see [Guardrails](guardrails.md#approvals-from-a-parent-agent))
|
|
23
23
|
- `chi desktop install|upgrade|uninstall|status` — the macOS "Send to chi" helper: a Service and a ⌃⌥⌘N hotkey that send text or images to a session (live, stopped or new) as a message (⏎, a turn runs) or a context note (⌘⏎) (see [Desktop helper](desktop.md))
|
|
24
24
|
- `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
|
|
@@ -298,8 +298,8 @@ and cuts nothing. `/cut TEXT` in a terminal or the web composer (or `chi send --
|
|
|
298
298
|
once the model has streamed only thinking for `steer.cut_after` seconds (per host/model: `steer_cut_after`; default
|
|
299
299
|
20), now or while your message waits, that generation is cut and the step starts again with your message, marked
|
|
300
300
|
`↪ cut in for your message` (see
|
|
301
|
-
[configuration.md](configuration.md#messages-during-a-running-turn-steer-and-cut)).
|
|
302
|
-
composer
|
|
301
|
+
[configuration.md](configuration.md#messages-during-a-running-turn-steer-and-cut)). `/queue TEXT`
|
|
302
|
+
(terminal or web composer, or `chi send --queue`) keeps TEXT for after the turn: it runs as a turn of its own once the running one ends. The
|
|
303
303
|
first answer to an `ask_user_question` wins; the other UIs close their widget.
|
|
304
304
|
An empty answer dismisses the question in every UI. The model is then told
|
|
305
305
|
not to go ahead with what it asked about, or change anything else, and to wait.
|
|
@@ -541,12 +541,40 @@ REPL alike:
|
|
|
541
541
|
20 s)` (it waits for the thinking; the step may end first) or `(cutting is
|
|
542
542
|
off; goes in at the next step)` (`steer.cut_after` is 0, per host/model:
|
|
543
543
|
`steer_cut_after`, or no generation to cut).
|
|
544
|
-
- `/queue TEXT`
|
|
545
|
-
|
|
546
|
-
`(
|
|
547
|
-
|
|
544
|
+
- `/queue TEXT` runs TEXT as a turn of its own after this one, in an attached
|
|
545
|
+
terminal, the plain REPL and the web composer alike: `(queued: runs after this
|
|
546
|
+
turn)` (the web draws it as a queued bubble until its turn starts). It never
|
|
547
|
+
merges into the running turn, and a plain line sent after it still does.
|
|
548
|
+
Several queued lines run one turn each, in the order sent; they still run
|
|
549
|
+
after a Ctrl-C or a failed turn, and a queued line drops a continue offer as
|
|
550
|
+
any prompt does. Other terminals show someone else's queued line with the same
|
|
551
|
+
note. A session whose worker is older than queueing would steer it instead, so
|
|
552
|
+
it is not sent: `(/queue needs a newer worker: restart it with chi sessions
|
|
553
|
+
restart, then send again)` (the web shows the refusal under the line). A line
|
|
554
|
+
with images works the same way: it waits for the turn's end, and the lines
|
|
555
|
+
sent after it still go in at the next step.
|
|
548
556
|
At the open prompt (no turn running) `/cut TEXT` and `/queue TEXT` run TEXT
|
|
549
557
|
as a normal prompt.
|
|
558
|
+
- **The web composer** has a row above the text field in a session (always
|
|
559
|
+
there, so the composer never changes height): the keys while idle, and while
|
|
560
|
+
the turn runs the delivery line, where a message sent now goes in (`↳ goes in after this
|
|
561
|
+
thinking step · 0:42 · 3k chars`, `… after this tool call (execute · 0:12)`,
|
|
562
|
+
`… after this answer; the turn may go on`, `… once the card above is
|
|
563
|
+
answered`), then two links. **Enter** (or Send) is the next step;
|
|
564
|
+
**Alt+Enter** (Option+Enter) or `queue for after the turn` queues it, like
|
|
565
|
+
`/queue TEXT`, and the bubble is dashed (`after this turn`), below the
|
|
566
|
+
running turn's card with the commands queued for its end, until its turn
|
|
567
|
+
starts; `cut in now` cuts, like `/cut TEXT` (by click only: it throws the
|
|
568
|
+
thinking away). Cut is enabled only while the step has streamed nothing but
|
|
569
|
+
thinking and cutting is on for the model (the line says `cuts at about 0:25
|
|
570
|
+
if you choose cut in now` until the thinking passes `steer_cut_after`, and
|
|
571
|
+
`cutting is off for <model>` with no cut link when it is 0); the queue link
|
|
572
|
+
is gone for a worker too old to queue (Alt+Enter then sends nothing, keeps the
|
|
573
|
+
text and says to restart the worker). A link clicked with an empty composer
|
|
574
|
+
arms that choice (`cut in now ✓ · Enter sends`); a second click or Esc lets
|
|
575
|
+
go of it. The echo says what it waits for: `waits for the next step` or
|
|
576
|
+
`cutting in…`, then `steered`. A phone shows the short form (`↳ after this
|
|
577
|
+
thinking · 0:42`) with `cut in` and `queue` chips.
|
|
550
578
|
- Commands answer, wait or are refused, by what they do:
|
|
551
579
|
- At once, beside the turn: `/stats`, `/recap`, `/help`, `/context`, a
|
|
552
580
|
plugin's anytime command (`/btw`), and the commands that only show
|
|
@@ -700,7 +728,9 @@ approval, a hook's question and the step limit don't): two muted lines
|
|
|
700
728
|
under its `question → answer` line, the whole text when you open the card,
|
|
701
729
|
in both views and after a reload. The chip at the stage's bottom edge, `N steps · M tool
|
|
702
730
|
calls` with one tick per call, opens the turn view's block right above it (newest step first
|
|
703
|
-
while it runs; a tick opens its step).
|
|
731
|
+
while it runs; a tick opens its step). Below the chip, past a dashed rule, wait the
|
|
732
|
+
messages queued for after the turn and the commands queued for its end; they go into
|
|
733
|
+
the history after the turn's answer. `▾` folds the stage to its status
|
|
704
734
|
row, remembered in this browser. The stage keeps its height while a turn
|
|
705
735
|
runs: it grows, with a short ease, when a card or the answer needs room and gives the room back
|
|
706
736
|
only when the turn moves into the history. When the turn ends the answer shows in
|
|
@@ -836,7 +866,7 @@ Notes:
|
|
|
836
866
|
- Without `--default` the command is session-scoped and does not rewrite config files.
|
|
837
867
|
- With `--default` the new default is written to `~/.config/samagotchi/config.yml` (honoring `XDG_CONFIG_HOME`) and takes effect for all new sessions; the current session's effective model is also updated immediately. Bare aliases and `host:model` are both valid.
|
|
838
868
|
- `--default` and `--alias` change only their one key's line in `config.yml` (`default.model`, or the alias under `model_aliases:`): comments and the rest of the file stay as written, a symlinked file is written through and keeps its mode. When the edited text wouldn't read back as the expected settings (a flow-style section, anchors), chi writes the whole file out from the parsed data instead, which drops its comments.
|
|
839
|
-
-
|
|
869
|
+
- A worker reads `hosts:` from config.yml when it starts (not from the chi that started it).
|
|
840
870
|
- The idle recap uses the session's current model (a switch counts from the next recap), unless `recap: {host_ref, model}` pins one.
|
|
841
871
|
|
|
842
872
|
## Session recap
|
|
@@ -945,6 +975,8 @@ status> model=Qwen3.6-35B | ⑂ 2 running · 1 waiting | ctx=8.1% (under20) | me
|
|
|
945
975
|
report none, `api: openai`, at the turn's end).
|
|
946
976
|
- `mem:`: the memories the session read, with its `--memory` list; `muted:` its `--mute` list.
|
|
947
977
|
Up to 8 names each, then `+N`.
|
|
978
|
+
- While a turn runs it ends in `· typing goes in at the next step (/cut, /queue)`; the hint is the first thing dropped
|
|
979
|
+
on a narrow terminal.
|
|
948
980
|
- The row is cut to the terminal's width. Without a live region (output or input not a terminal,
|
|
949
981
|
`TERM=dumb`) it prints as a line when it changes.
|
|
950
982
|
- `SAMAGOTCHI_STATUS_LINE` / `status.line` (default `on`): `off`, `false` or `0` hides it.
|
data/docs/configuration.md
CHANGED
|
@@ -106,7 +106,10 @@ Behavior:
|
|
|
106
106
|
at its start until it is stopped; the web's model picker lists a `hosts:`
|
|
107
107
|
edit at once). A worker gets the CLI flags of the chi that started it
|
|
108
108
|
through its environment.
|
|
109
|
-
-
|
|
109
|
+
- A worker reads `hosts:` from the file as it starts, as every other setting, so a delegate (or any session
|
|
110
|
+
started later) sees a host added after its parent started. `SAMAGOTCHI_HOSTS_JSON` (a JSON object in the shape
|
|
111
|
+
of `hosts:`) replaces the file's `hosts:` for the process it is set on, the way specs use it; no worker inherits
|
|
112
|
+
it (`SessionManager.spawn_options` unsets it).
|
|
110
113
|
|
|
111
114
|
This lets you run `chi` without repeating common defaults such as model
|
|
112
115
|
and llama host/port on every invocation.
|
|
@@ -228,7 +231,8 @@ hosts:
|
|
|
228
231
|
|
|
229
232
|
`chi self` shows the variable and whether it is set (`api key FIREWORKS_API_KEY (set)`).
|
|
230
233
|
|
|
231
|
-
Every host with `api_key_env:` sends `Authorization: Bearer <key>` on each request
|
|
234
|
+
Every host with `api_key_env:` sends `Authorization: Bearer <key>` on each request (`api: anthropic`:
|
|
235
|
+
`x-api-key: <key>`, see [Anthropic Messages hosts](#anthropic-messages-hosts)),
|
|
232
236
|
chat or raw-prompt alike, so a llama.cpp started with `--api-key` works as a native
|
|
233
237
|
host too (`chi bootstrap --key-env VAR` writes such an entry). An unset variable
|
|
234
238
|
fails the turn before any request (`set VAR`); a 401/403 names the variable to
|
|
@@ -289,6 +293,90 @@ workers, and resumed sessions all use the loop of the model's host. Hosts withou
|
|
|
289
293
|
`api: openai` keep using the native `/completion`, `/v1/completions`, or oMLX
|
|
290
294
|
transport path.
|
|
291
295
|
|
|
296
|
+
### Anthropic Messages hosts
|
|
297
|
+
|
|
298
|
+
`api: anthropic` makes a host talk Anthropic's Messages API (`POST {url}/v1/messages`) through the same chat loop:
|
|
299
|
+
Anthropic's own API, a gateway that serves `/v1/messages`, or a local server that does (Splash). The url is the API
|
|
300
|
+
root **without** `/v1`, as `ANTHROPIC_BASE_URL` takes it (a url that ends in `/v1` is accepted: no second `/v1` is
|
|
301
|
+
added); an entry with neither `url:` nor `host:` talks to `https://api.anthropic.com`. The key goes in the
|
|
302
|
+
`x-api-key` header; `auth: bearer` sends it as `Authorization: Bearer` instead (a gateway or provider that wants
|
|
303
|
+
that), `auth: x-api-key` is the default spelled out. Exactly one of the two headers is sent.
|
|
304
|
+
|
|
305
|
+
```yaml
|
|
306
|
+
hosts:
|
|
307
|
+
claude:
|
|
308
|
+
api: anthropic
|
|
309
|
+
api_key_env: ANTHROPIC_API_KEY
|
|
310
|
+
gateway:
|
|
311
|
+
url: https://gateway.example.com
|
|
312
|
+
api: anthropic
|
|
313
|
+
api_key_env: GATEWAY_KEY
|
|
314
|
+
auth: bearer
|
|
315
|
+
splash-msg:
|
|
316
|
+
url: http://192.0.2.10:8000
|
|
317
|
+
api: anthropic
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`auth:` works on any host (`bearer` is the default everywhere but `api: anthropic`).
|
|
321
|
+
|
|
322
|
+
**Known limit (until the next release):** Claude 5.x models (Opus, Sonnet and Haiku 5.5, Fable 5.1) bind each
|
|
323
|
+
thinking block to the request it came from. When the history before a block changes (the system prompt rebuilt after a
|
|
324
|
+
worker restart or a `/model` switch, a changed tool set, the forget layer's edits), such a model may answer HTTP 400
|
|
325
|
+
"… bound to a different conversation" and the turn fails with that error; `/reasoning-history turn` makes it rarer.
|
|
326
|
+
The next release sends Anthropic's binding controls and recovers from that 400. Fireworks, Splash and other
|
|
327
|
+
non-Claude models are not affected.
|
|
328
|
+
|
|
329
|
+
The loop, tools, events, hooks and UIs are the chat loop's, as for `api: openai`; only the wire differs: the
|
|
330
|
+
system prompt goes as the request's `system` (later system notes as user text), tool calls as `tool_use` blocks,
|
|
331
|
+
results as `tool_result` blocks, images as `image` blocks; thinking streams into the thinking lane; usage counts
|
|
332
|
+
cache reads and writes into the prompt (so `/stats` shows them). Without `default.max_tokens`, `max_tokens` is the
|
|
333
|
+
model list's `max_tokens` capped at 64000, else 64000 (Messages requires one). chi's OpenAI-only request fields are
|
|
334
|
+
never sent there; `sampling:` passes as written.
|
|
335
|
+
|
|
336
|
+
Its model list is `GET {url}/v1/models` (every page): Anthropic's shape (the window from `max_input_tokens`, images
|
|
337
|
+
from `capabilities.image_input`) or the OpenAI shape a gateway, Splash or Fireworks serves there (images from
|
|
338
|
+
`input_modalities`, `vision` or `supports_image_input`). A gateway whose list lacks an
|
|
339
|
+
id (a high-availability alias) takes it from the host's declared `models:`, as on any host.
|
|
340
|
+
|
|
341
|
+
**Dialects.** A provider whose `/v1/messages` departs from Anthropic's gets `dialect:` on its host (only on
|
|
342
|
+
`api: anthropic`; an unknown value, or a dialect on another api, warns once and is ignored). `chi self` names it on
|
|
343
|
+
its `loop` line. `dialect: standard` is the default written out. There is one other dialect today, `fireworks` (Fireworks' Anthropic-compatible API, directly or behind a gateway):
|
|
344
|
+
|
|
345
|
+
- the key goes as `Authorization: Bearer` (an explicit `auth:` still wins);
|
|
346
|
+
- thinking at `low`/`medium`/`high` is `{type: "adaptive"}` with `output_config.effort`, without `display` (Fireworks'
|
|
347
|
+
schema has none and refuses unknown fields; it returns thinking by default); a model switched to the budget form gets
|
|
348
|
+
none either;
|
|
349
|
+
- no `cache_control` breakpoints (Fireworks caches prompt prefixes by itself);
|
|
350
|
+
- `x-session-affinity: <session id>` beside `Session-Id`, so a session stays on one replica, where its cache is.
|
|
351
|
+
|
|
352
|
+
A host without `dialect:` is standard Anthropic (Claude, Splash). Give each upstream kind its own host entry: when a
|
|
353
|
+
gateway serves both Fireworks models and Claude ids on one url, the Claude ids go on a second entry with the same url
|
|
354
|
+
and no `dialect:` (Claude wants `display: summarized` and the breakpoints).
|
|
355
|
+
|
|
356
|
+
```yaml
|
|
357
|
+
hosts:
|
|
358
|
+
work-msg: # the work gateway's Messages route, Fireworks behind it
|
|
359
|
+
url: https://gateway.example.com # the API root, no /v1 (chi adds /v1/messages)
|
|
360
|
+
api: anthropic
|
|
361
|
+
dialect: fireworks # Bearer auth, no thinking display, no cache_control, x-session-affinity
|
|
362
|
+
api_key_env: WORK_API_KEY
|
|
363
|
+
# auth: x-api-key # only if the gateway itself wants that header
|
|
364
|
+
thinking: medium # low|medium|high → output_config.effort; off → thinking disabled
|
|
365
|
+
models:
|
|
366
|
+
accounts/fireworks/models/kimi-k2p5:
|
|
367
|
+
price: {input: 0.6, cache_read: 0.1, output: 2.5} # example rates: put the gateway's (no cache writes on Fireworks)
|
|
368
|
+
work-claude: # the same gateway's Claude ids: standard Messages
|
|
369
|
+
url: https://gateway.example.com
|
|
370
|
+
api: anthropic
|
|
371
|
+
api_key_env: WORK_API_KEY
|
|
372
|
+
auth: bearer # if the gateway wants Bearer for Claude ids too
|
|
373
|
+
fireworks: # Fireworks directly
|
|
374
|
+
url: https://api.fireworks.ai/inference
|
|
375
|
+
api: anthropic
|
|
376
|
+
dialect: fireworks
|
|
377
|
+
api_key_env: FIREWORKS_API_KEY
|
|
378
|
+
```
|
|
379
|
+
|
|
292
380
|
To manually verify a live chat-loop tool round trip, run the gated integration
|
|
293
381
|
spec. It requires the model to call `execute` and return the current UTC date:
|
|
294
382
|
|
|
@@ -372,7 +460,7 @@ system prompt stays the same; a server that swaps models between turns goes unno
|
|
|
372
460
|
`server (chat_template)`, `name`, `default`); `chi self` shows what config says without asking the server. A chat
|
|
373
461
|
host (`api: openai`) formats nothing itself: its profile comes from the name and only strips thought tags.
|
|
374
462
|
|
|
375
|
-
Workers
|
|
463
|
+
Workers read `hosts:` (with `profile:`) and `models:` from the config file.
|
|
376
464
|
`--profile` reaches the worker a chi starts, but a worker that another process wakes later (`chi web`, `--attach`
|
|
377
465
|
after an idle exit) gets that process's environment, so put a lasting choice in config.
|
|
378
466
|
|
|
@@ -416,8 +504,8 @@ models:
|
|
|
416
504
|
|
|
417
505
|
`/model` shows what applies (`sampling: temperature=0.6 presence_penalty=1.5 (hosts.work)`), and each request's
|
|
418
506
|
`stream` line in the debug log carries a `sampling=` field with what was sent. The fields are read every turn, so a
|
|
419
|
-
`/model` switch takes the new model's. A worker
|
|
420
|
-
|
|
507
|
+
`/model` switch takes the new model's. A worker reads `hosts:` (with `sampling:`) from the config file when it
|
|
508
|
+
starts, and `models:` each turn: after changing a host's `sampling:`,
|
|
421
509
|
stop the session's worker (`chi sessions stop`) for it to take effect.
|
|
422
510
|
|
|
423
511
|
## Prices
|
|
@@ -509,7 +597,13 @@ models:
|
|
|
509
597
|
- `/model` shows the level and where it came from (`thinking: off (models: qwen3.6-35b-a3b)`, `thinking: low
|
|
510
598
|
(session)`), `/stats` on its `thinking:` line, `chi self` too (a session's own level first when run from its
|
|
511
599
|
commands).
|
|
512
|
-
- The idle recap
|
|
600
|
+
- The idle recap, plugins' side questions and a broadcast's triage always ask with thinking off, whatever the
|
|
601
|
+
level. A model that refuses that gets no thinking fields on an `api: openai` host (its default); on an
|
|
602
|
+
`api: anthropic` host, where such a model always thinks, it gets adaptive thinking at `low` effort and a limit of
|
|
603
|
+
at least 4096 tokens, so the thinking leaves room for the answer; a model that refuses that too (one that takes no
|
|
604
|
+
thinking fields at all) gets none, at the normal limit. What a host refused is kept for the next recap, the next
|
|
605
|
+
`ctx.ask_model` on the same model, and the rest of a broadcast. A recap and `ctx.ask_model` send the session's id
|
|
606
|
+
as the turns do (`Session-Id`, and `x-session-affinity` under `dialect: fireworks`); a triage sends none.
|
|
513
607
|
|
|
514
608
|
What each backend gets:
|
|
515
609
|
|
|
@@ -518,6 +612,20 @@ What each backend gets:
|
|
|
518
612
|
| native (`/completion`), `qwen36` | an empty thought after the assistant cue, and no turn preamble | no knob: thinking stays as the model has it, one notice |
|
|
519
613
|
| native, `gemma4` | no `<\|think\|>` token at the start of the system prompt | no knob, one notice |
|
|
520
614
|
| chat host (`api: openai`) | `chat_template_kwargs: {enable_thinking: false}` and `reasoning_effort: "none"` | `reasoning_effort: <level>` |
|
|
615
|
+
| Messages host (`api: anthropic`) | `thinking: {type: "disabled"}` | `thinking: {type: "adaptive", display: "summarized"}` and `output_config: {effort: <level>}` |
|
|
616
|
+
| Messages host, `dialect: fireworks` | `thinking: {type: "disabled"}` | `thinking: {type: "adaptive"}` and `output_config: {effort: <level>}` (no `display`) |
|
|
617
|
+
|
|
618
|
+
On a Messages host `default` sends nothing: the provider's own default, which differs by model (some Claude models
|
|
619
|
+
think adaptively but show no thinking text, their blocks still going back; others, and Splash, don't think at all). A model that refuses `disabled` (current
|
|
620
|
+
Claude models) gets the request again without it, as on any chat host. A model that refuses adaptive thinking
|
|
621
|
+
(older Claude: Haiku 4.5) is switched, once and for the worker's life, to the budget form: `thinking: {type:
|
|
622
|
+
"enabled", budget_tokens: B}` with B 2048 / 8192 / 24576 for low / medium / high and no effort; `max_tokens` always
|
|
623
|
+
stays above the budget (the budget is lowered to leave 1024 for the answer, or `max_tokens` raised to 2048). A
|
|
624
|
+
host that refuses the thinking `display` field gets thinking without it from then on (the level still applies); one
|
|
625
|
+
that refuses `cache_control` gets no cache breakpoints from then on, and a `max_tokens` over the model's output
|
|
626
|
+
limit is sent again with the limit the host names. On any chat host, a model that refuses a sampling field
|
|
627
|
+
(`temperature`, `top_p`, `top_k`; Claude refuses the empty-answer retry's temperature while thinking) gets the
|
|
628
|
+
request again without them, and none of them after.
|
|
521
629
|
|
|
522
630
|
On chat hosts: llama.cpp honours both off switches but ignores the effort (when its `/props` says
|
|
523
631
|
`chat_template_caps.supports_reasoning_effort: false`, chi says so once per session and host, from the `/props`
|
|
@@ -590,10 +698,17 @@ models:
|
|
|
590
698
|
turns included, and answers HTTP 400 without it: under `turn` every turn after one that thought fails. Set
|
|
591
699
|
`reasoning_history: all` there (the example above). DeepSeek through OpenRouter takes `turn`.
|
|
592
700
|
- `drop` sends none (what chi did before this setting).
|
|
701
|
+
- **On an `api: anthropic` host** the reasoning goes back as the answer's own content blocks, verbatim (thinking
|
|
702
|
+
blocks with their signatures, in their order among text and `tool_use`), not as `reasoning_content`. The default
|
|
703
|
+
there is `all`: Claude keeps earlier turns' thinking in context, and leaving it out would rewrite the cached prefix
|
|
704
|
+
each turn (a Qwen template on Splash drops earlier turns' reasoning itself, so `all` costs nothing there). `turn`
|
|
705
|
+
leaves earlier turns' thinking blocks out (their text and calls stay), and `drop` counts as `turn`: the running
|
|
706
|
+
turn's thinking blocks always go back (a tool loop with thinking needs the last reply's). An interrupted reply, or
|
|
707
|
+
one whose tool calls never got their results, goes back as plain text without its blocks.
|
|
593
708
|
- `all` sends every reply's reasoning, earlier turns too. It sends only the field: a local template that leaves out
|
|
594
709
|
earlier turns' reasoning (Qwen's) needs `preserve_thinking` as well (above).
|
|
595
710
|
- Order, first set wins: the session's own level, `SAMAGOTCHI_REASONING_HISTORY` (the whole process; for A/B runs),
|
|
596
|
-
the `models:` entry (found the way `profile:` is), the `hosts:` entry, then `turn
|
|
711
|
+
the `models:` entry (found the way `profile:` is), the `hosts:` entry, then `turn` (`all` on `api: anthropic`). Anything else than a level warns
|
|
597
712
|
once and counts as unset. A host's level reaches a worker when it starts, as its `sampling:` does.
|
|
598
713
|
- **A session's own level** (`drop`, `turn` or `all`; an explicit `turn` counts as its own) is saved in the session
|
|
599
714
|
(its file's `"reasoning_history"`), so a `--resume`, a respawned worker, a continue and `/model` keep it; a plugin's
|
|
@@ -888,6 +1003,12 @@ many times per turn; `0` ends the turn at the empty answer as before. An answer
|
|
|
888
1003
|
(90 % or more) is not retried. When the retries run out the turn ends with no answer: every UI shows one muted line,
|
|
889
1004
|
`no answer: the model returned nothing (after 1 retry)`, and the model is told on its next turn with a hidden note;
|
|
890
1005
|
no made-up answer is saved.
|
|
1006
|
+
A provider that refuses the answer (a Messages host's `stop_reason: refusal`, or an OpenAI-compatible host's
|
|
1007
|
+
`finish_reason: content_filter` with no text) is not asked again: the turn ends with no answer (text streamed before
|
|
1008
|
+
a Messages refusal is not kept), every UI shows `no answer: refused by the provider (cyber)` (the category from
|
|
1009
|
+
`stop_details`, when there is one), and the hidden note says the answer was refused. A `content_filter` stop after
|
|
1010
|
+
some text keeps that text as the answer, followed by `[cut off by the provider's content filter]`, and is not asked
|
|
1011
|
+
again either. Tool calls that come with either are not run.
|
|
891
1012
|
A generation a plugin cuts while it streams (loop-guard's thinking watch, or any `stop_generation`,
|
|
892
1013
|
[hooks.md](hooks.md#watching-the-stream)) uses the same budget: `↻ cut by loop-guard, asking again (1/1)`, and with
|
|
893
1014
|
none left the turn ends as cancelled (hook).
|
|
@@ -907,7 +1028,8 @@ that model's cut wait (`steer.cut_after` seconds, default `20`, `0` = never; cou
|
|
|
907
1028
|
so the wait for a busy server doesn't count) and still does, and the model starts the step again with the message
|
|
908
1029
|
(`↪ cut in for your message`). One that comes earlier cuts once the thinking passes it, unless the step ends first
|
|
909
1030
|
and the message goes in there. A plain message (from the web composer, `chi send` without `--cut`, a parent agent)
|
|
910
|
-
never asks for a cut
|
|
1031
|
+
never asks for a cut, nor does a queued one (`/queue TEXT`, `chi send --queue`, `ctx.sessions.send(…, delivery:
|
|
1032
|
+
:queue)`), which runs as a turn of its own after the running one. Such a cut spends no `retry.empty_answer` attempt and sends no hidden note; only the cut
|
|
911
1033
|
thinking is lost. A generation that has streamed visible text or a tool call is never cut, and a plugin's steer
|
|
912
1034
|
(`ctx.steer`, `ctx.sessions.send`) never cuts.
|
|
913
1035
|
|
|
@@ -931,9 +1053,12 @@ models:
|
|
|
931
1053
|
|
|
932
1054
|
`/model` shows the resolved wait (`; steer cut: after 60 s (models: qwen)`, `; steer cut: never (hosts.splash)`).
|
|
933
1055
|
|
|
934
|
-
`/queue TEXT` in the
|
|
935
|
-
|
|
936
|
-
`
|
|
1056
|
+
`/queue TEXT` (in a terminal or the web composer) runs TEXT as a turn of its own after the running one: the request
|
|
1057
|
+
carries `delivery: "queue"`, the input file keeps it, and the worker's drain leaves the file for after the turn. The
|
|
1058
|
+
Bridge acks `delivery: "queue"` while a turn runs (to an idle session the message is simply the next turn:
|
|
1059
|
+
`next_step`). A worker whose sidecar doesn't name the `delivery` feature (older than queueing) would steer it, so the
|
|
1060
|
+
sender refuses it (`409 delivery_unavailable`, "restart it"), and so does a worker that answers the request with a
|
|
1061
|
+
400 `delivery_unavailable`.
|
|
937
1062
|
|
|
938
1063
|
## Server errors
|
|
939
1064
|
|
|
@@ -952,6 +1077,13 @@ message (before, a failed llama.cpp `/completion` ended the turn as
|
|
|
952
1077
|
| bad request | other 4xx; a prompt larger than the context window, whatever the status | no |
|
|
953
1078
|
| protocol | a body the API doesn't promise | no |
|
|
954
1079
|
|
|
1080
|
+
An error event (or an error body inside a 200) has no HTTP status of its own: its numeric `error.code` stands for
|
|
1081
|
+
one, or a Google-style code name (Fireworks: `INVALID_ARGUMENT` 400, `UNAUTHENTICATED` 401, `PERMISSION_DENIED` 403,
|
|
1082
|
+
`NOT_FOUND` 404, `RESOURCE_EXHAUSTED` 429), else its `error.type` (Anthropic's names; some OpenAI-compatible servers,
|
|
1083
|
+
OpenAI and Ollama, use some of them: `invalid_request_error` 400, `authentication_error` 401, `billing_error` 402,
|
|
1084
|
+
`permission_error` 403, `not_found_error` 404, `request_too_large` 413, `rate_limit_error` 429, `api_error` 500,
|
|
1085
|
+
`timeout_error` 504, `overloaded_error` 529); with neither it counts as a 500.
|
|
1086
|
+
|
|
955
1087
|
The turn's prompt and its completed tool calls stay in the session.
|
|
956
1088
|
|
|
957
1089
|
## Debug Log File
|
data/docs/internals/engine.md
CHANGED
|
@@ -54,8 +54,10 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
54
54
|
│
|
|
55
55
|
├── complete_turn(turn, result)
|
|
56
56
|
│ # end_turn(turn, "completed" | "canceled"): under the event lock the session gets the kept
|
|
57
|
-
│ # messages, goes IDLE, records last_turn
|
|
58
|
-
│ #
|
|
57
|
+
│ # messages, goes IDLE, records last_turn (enqueued_id: the turn's input, from its origin;
|
|
58
|
+
│ # a continue turn's origin carries its offer turn's, ContinueOffer#run_continue_turn),
|
|
59
|
+
│ # and :turn_completed {result, turn_summary, display_pending} or :turn_canceled
|
|
60
|
+
│ # {cancellation_reason, cancelled_by, duration_ms} is emitted
|
|
59
61
|
│ # then the :after_turn hook (status, messages, present: the AnswerDisplay presenter,
|
|
60
62
|
│ # refs: the AnswerRefs adder)
|
|
61
63
|
│ # store_answer_display (one replace of the answer: `display` and/or `display_refs`;
|
|
@@ -127,8 +129,8 @@ The Engine does not talk to models directly. `Engine#backend` picks the loop the
|
|
|
127
129
|
|
|
128
130
|
```
|
|
129
131
|
Engine#backend → backend_for(@host_registry.resolve(@effective_model_name))
|
|
130
|
-
→ host entry chat? (api: openai): LLM::ChatLoop (one per Engine, built on first use)
|
|
131
|
-
→ else:
|
|
132
|
+
→ host entry chat? (an api in LLM::Adapters::BY_API: openai, anthropic): LLM::ChatLoop (one per Engine, built on first use)
|
|
133
|
+
→ else: LLM::NativeBackend wrapping the Engine's KernelLoop
|
|
132
134
|
```
|
|
133
135
|
|
|
134
136
|
Both implement the `LLM::ModelBackend` contract (`#complete(messages:, max_iterations:, on_stream_event:,
|
|
@@ -208,7 +210,10 @@ The differences from the native loop:
|
|
|
208
210
|
- A 400 about reasoning (`BadRequest#reasoning_refused?`) for a request that sent reasoning back asks the step again
|
|
209
211
|
without it; when that goes through, `#reasoning_history_refused!` marks the model (worker lifetime) and
|
|
210
212
|
`:reasoning_history_refused` goes out. When it is refused too, the thinking-fields fallback (`#thinking_refused!`,
|
|
211
|
-
`:thinking_refused`) runs on it, still without reasoning. Up to three requests per step.
|
|
213
|
+
`:thinking_refused`) runs on it, still without reasoning. Up to three requests per step. A 400 about a thinking
|
|
214
|
+
block's signature (`BadRequest#thinking_signature?`) or a refused binding control
|
|
215
|
+
(`#thinking_binding_unsupported?`) is not one: `ProviderErrors` leaves `reasoning_refused?` false for them, so one
|
|
216
|
+
bad block never turns reasoning off for the model.
|
|
212
217
|
|
|
213
218
|
### Shared: ToolResponse (per batch) and ToolRunner (per call)
|
|
214
219
|
|
|
@@ -341,6 +346,10 @@ What the running generation streams, fed by the Engine's stream handler: whether
|
|
|
341
346
|
Bridge acks it as `cut`.
|
|
342
347
|
A generation is cuttable while it has streamed only thinking, is still thinking, and has thought for at least
|
|
343
348
|
the model's cut wait (`SteerCutAfter.resolve`: `steer_cut_after` per models:/hosts: entry, else `steer.cut_after`) seconds (`cuttable?(min_age)`). It has `started!`, `chunk!`, `retrying!` and `finished!`.
|
|
349
|
+
`snapshot` is the running generation as `GenerationPhase::Snapshot` `{thinking_only, age}` (nil with none running;
|
|
350
|
+
`age`: its thinking's seconds, nil before any): `SteerCut#generation_snapshot` → `Engine#generation_snapshot`, which
|
|
351
|
+
`Bridge#snapshot` merges into `current_turn.generation`, so a web page joining mid-generation can clock its delivery
|
|
352
|
+
line and tell whether "cut in now" can land.
|
|
344
353
|
|
|
345
354
|
### WaitingSteer
|
|
346
355
|
|
|
@@ -518,10 +527,20 @@ Both loops:
|
|
|
518
527
|
The retry request runs at temperature 0.6 when the configured sampling sets no temperature; a configured one
|
|
519
528
|
is kept. A length stop with the context at least 90% full is not retried. When the retries run out, the
|
|
520
529
|
Engine keeps a `TurnNote.empty` note as the turn's end.
|
|
530
|
+
- **A refusal** (chat loop only): a finish reason `refusal` (Messages), or `content_filter` (OpenAI) with no text, is
|
|
531
|
+
an empty answer never retried; text streamed before a refusal is not kept, and tool calls with it are not run. A
|
|
532
|
+
`content_filter` with text keeps it, marked `ChatLoop::FILTERED_MARK`, also never retried. `ModelResult#refusal` is `{category:}` (from the response's
|
|
533
|
+
`stop_details`) or `{}`, and goes on the `TurnNote.empty` marker and `turn_summary.empty_answer` as `refusal`, so
|
|
534
|
+
every UI words the notice `no answer: refused by the provider (cyber)`.
|
|
521
535
|
- **A cut generation** (`after_cut`): a generation a plugin cut (`stop_generation`) spends the same retry budget
|
|
522
536
|
with its own nudge; when it is used up, the turn ends cancelled (`:hook`). A steer's cut asks again with the
|
|
523
537
|
steer and spends nothing.
|
|
524
|
-
- **Steering**: queued input goes in at iteration boundaries (`Steer.inject!`, `:pending_input_merged`).
|
|
538
|
+
- **Steering**: queued input goes in at iteration boundaries (`Steer.inject!`, `:pending_input_merged`). A
|
|
539
|
+
worker's drain (`Worker#pending_input_drain`) takes every input file in name order except the ones
|
|
540
|
+
`SessionInbox.waits_for_turn_end?` leaves for a turn of their own (`delivery: queue`, or images): those are
|
|
541
|
+
skipped, not a barrier, so a line sent after them still merges. Only the files it claims reach
|
|
542
|
+
`:input_merged`, the failure hand-back (`@merged_this_turn`) and the checkpoint; the skipped ones run after the
|
|
543
|
+
turn, one turn each, in name order (`Worker#run_input_files`).
|
|
525
544
|
- **Context status**: an estimate of the window's fill before each request,
|
|
526
545
|
emitted on every request (`:context_status`) so the UI always has a current
|
|
527
546
|
number, with a guidance line on the tail only when it rises far enough.
|