samagotchi 0.55.0 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -1
- data/docs/architecture.md +6 -2
- data/docs/cli.md +29 -7
- data/docs/configuration.md +84 -8
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/engine.md +14 -0
- data/docs/internals/prompt-caching.md +8 -0
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cache_confirm.rb +51 -0
- data/lib/samagotchi/client_id.rb +12 -0
- data/lib/samagotchi/config.rb +7 -3
- data/lib/samagotchi/engine.rb +102 -8
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/host_config.rb +6 -3
- data/lib/samagotchi/host_registry.rb +4 -3
- data/lib/samagotchi/kernel_loop.rb +99 -16
- data/lib/samagotchi/llm/chat_loop.rb +122 -71
- data/lib/samagotchi/llm/openai_chat.rb +2 -1
- data/lib/samagotchi/llm/stream_drops.rb +63 -0
- data/lib/samagotchi/llm/turn_settings.rb +9 -5
- data/lib/samagotchi/model_settings.rb +5 -2
- data/lib/samagotchi/reasoning_history.rb +132 -0
- data/lib/samagotchi/reasoning_history_command.rb +107 -0
- data/lib/samagotchi/self_report.rb +21 -0
- data/lib/samagotchi/session.rb +23 -2
- data/lib/samagotchi/session_commands.rb +55 -15
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_setup.rb +7 -5
- data/lib/samagotchi/setting_explained.rb +20 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +2 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +11 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
- data/lib/samagotchi/terminal_ui.rb +13 -6
- data/lib/samagotchi/thinking.rb +20 -25
- data/lib/samagotchi/thinking_command.rb +33 -11
- data/lib/samagotchi/tools/builtins.rb +6 -2
- data/lib/samagotchi/tools/peers.rb +4 -2
- data/lib/samagotchi/tools/task_runtime.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +23 -2
- data/lib/samagotchi/web/public/app.js +20 -1
- data/lib/samagotchi/web/public/commands.js +36 -3
- data/lib/samagotchi/web/public/composer.js +5 -1
- data/lib/samagotchi/web/public/index.html +6 -3
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/level_chip.js +174 -0
- data/lib/samagotchi/web/public/model_pick.js +1 -1
- data/lib/samagotchi/web/public/reasoning_history_chip.js +49 -0
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +10 -2
- data/lib/samagotchi/web/public/thinking_chip.js +33 -138
- data/lib/samagotchi/worker.rb +17 -5
- 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: 242e670188e9ae58b573c067f949fa0de43e15f6f5f10bcc11ea66a087b16bb8
|
|
4
|
+
data.tar.gz: e6bddbb6238764a871c1bcce826b92472ea7eb6e814ecdf399eb171ec9260c67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf35534c163ba8eb189e85b72ed03b687f203c5f3423909f46fbc358fda45ef1bde4716dd93c368c8a0eceb13fb5044a12250c23fa89eb8f6ee7ebf72857b614
|
|
7
|
+
data.tar.gz: f81d67ea98da8dbe42a2553a02a957cc2ecd70129c474fe2295f64881e980792cbc9ea446f11b338c255213f3b52976f1d380e5e3f5835f10ea475fbaa541e4d
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,34 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.56.0] - 2026-10-11
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `reasoning_history` (`drop`, `turn`, `all`) on a `models:` or `hosts:` entry, and `SAMAGOTCHI_REASONING_HISTORY`
|
|
16
|
+
for a whole process: which of its own reasoning a chat host's model gets back. `/model` names a configured level
|
|
17
|
+
and `chi self` shows it.
|
|
18
|
+
- `/reasoning-history drop|turn|all|default` sets a session's own level (saved; continue and fork copy it); `/model`,
|
|
19
|
+
`/stats` and the web's session bar (a `reasoning` chip, on chat hosts) show it with its source.
|
|
20
|
+
- `/thinking` and `/reasoning-history` ask before a change that makes the server read the conversation again (Gemma,
|
|
21
|
+
hosted APIs, a change to or from `all`); `--yes` applies at once. The terminal puts the `--yes` line in the next
|
|
22
|
+
input, the web's reply has an Apply button; `chi send`, `chi -p … --non-interactive` and piped lines apply directly.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- A chat host (`api: openai`) now gets the running turn's reasoning back: each of the turn's earlier replies carries its
|
|
27
|
+
reasoning as `reasoning_content`, as Qwen's, Gemma 4's and DeepSeek's templates and the hosted APIs expect
|
|
28
|
+
(`reasoning_history: turn`, the default). A local Qwen thought ~8x less per request with it. `drop` sends none (as
|
|
29
|
+
before), `all` sends every reply's, earlier turns too. A host that refuses it (a 400 about reasoning) gets the step
|
|
30
|
+
again without it, and that model sends none from then on (a notice says so once). See "Reasoning history" in
|
|
31
|
+
docs/configuration.md.
|
|
32
|
+
- `/thinking`'s cache note now tells a local chat server (only the tail; the whole prompt for a Gemma model) from a
|
|
33
|
+
hosted one (the whole conversation). The web's thinking chip sends its level with `--yes`.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- A local llama.cpp turn whose connection dropped mid-answer now restarts that step (up to twice) instead of failing.
|
|
38
|
+
|
|
11
39
|
## [0.55.0] - 2026-10-10
|
|
12
40
|
|
|
13
41
|
### Added
|
|
@@ -2461,7 +2489,8 @@ and long-lived sessions.
|
|
|
2461
2489
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
2462
2490
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
2463
2491
|
|
|
2464
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
2492
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.56.0...HEAD
|
|
2493
|
+
[0.56.0]: https://github.com/dm1try/samagotchi/compare/v0.55.0...v0.56.0
|
|
2465
2494
|
[0.55.0]: https://github.com/dm1try/samagotchi/compare/v0.54.0...v0.55.0
|
|
2466
2495
|
[0.54.0]: https://github.com/dm1try/samagotchi/compare/v0.53.0...v0.54.0
|
|
2467
2496
|
[0.53.0]: https://github.com/dm1try/samagotchi/compare/v0.52.0...v0.53.0
|
data/docs/architecture.md
CHANGED
|
@@ -280,7 +280,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
280
280
|
`404` for an unknown id. A POST with a body because routes match exact paths.
|
|
281
281
|
- `POST /session/:id/command` — a session command (`/model`, `/models`, `!rollback`, `!cmd`,
|
|
282
282
|
`/continue`) for the worker loop; `202` with a `command_id` its `:command_ran` names, `400` when
|
|
283
|
-
the line isn't one.
|
|
283
|
+
the line isn't one. A `:command_ran` carries `thinking`, `reasoning_history` and `llm_context`
|
|
284
|
+
(the summaries the next turn runs under) when the command changed them or the model, and
|
|
285
|
+
`confirm: {line, text}` when a `/thinking` or `/reasoning-history` change from a `web:` or `tui:`
|
|
286
|
+
client asks first (`CacheConfirm`; nothing changed, `line` applies it).
|
|
284
287
|
- `POST /session/:id/exit` — ask the worker to exit now (`{client_id:}`). The worker checks with
|
|
285
288
|
the event log held (`WorkerIdleExit#hold_for_request`): `200 {status: "exiting"}` and it leaves
|
|
286
289
|
like an idle exit, or `409 {status: "held", reason:}` with `turn_running`, `input_queued`,
|
|
@@ -390,7 +393,8 @@ server's `reasoning_content`; the raw-prompt loop splits each generation's
|
|
|
390
393
|
stream with a fresh `ThoughtStreamSplitter` (Qwen's `<think>` and Gemma's
|
|
391
394
|
`<|channel>thought … <channel|>` are thinking, tool-call bodies are dropped
|
|
392
395
|
from `text:`). The chat loop's model turns keep their `tool_calls` and tool results their `tool_call_id`,
|
|
393
|
-
so later requests and resumed sessions pair them
|
|
396
|
+
so later requests and resumed sessions pair them, and their reasoning (`thinking:`), which goes back as
|
|
397
|
+
`reasoning_content` on the running turn's replies (`reasoning_history`). It has its own system prompt
|
|
394
398
|
(no raw-prompt tool declarations; the tools go as JSON schemas).
|
|
395
399
|
|
|
396
400
|
`Engine#build_stream_event_handler` is the one seam both loops' streams pass
|
data/docs/cli.md
CHANGED
|
@@ -489,14 +489,34 @@ from (`session`, `--thinking` / `SAMAGOTCHI_THINKING_LEVEL`, `models: <key>`,
|
|
|
489
489
|
session's own level, which comes first; `/thinking default` unsets it. It is
|
|
490
490
|
saved with the session (a `--resume`, a continue and `/model` keep it) and
|
|
491
491
|
applies from the next turn's start; the reply says what the change costs the
|
|
492
|
-
prompt cache: the whole prompt on
|
|
493
|
-
prompt), only the tail on
|
|
494
|
-
(a local llama.cpp or Splash server
|
|
495
|
-
|
|
492
|
+
prompt cache: the whole prompt on Gemma 4 (native, or a local chat server whose
|
|
493
|
+
model name says gemma: `<|think|>` starts the prompt), only the tail on Qwen
|
|
494
|
+
(native, or a local llama.cpp or Splash server), and the whole conversation on a
|
|
495
|
+
remote chat host (OpenRouter, a hosted API). `/thinking` alone answers
|
|
496
496
|
mid-turn too; a change sent while a turn runs waits for the turn's end, and
|
|
497
497
|
skips that turn's warm-up. `/stats` shows the level on its `thinking:` line.
|
|
498
498
|
See "Thinking" in [configuration.md](configuration.md).
|
|
499
499
|
|
|
500
|
+
`/reasoning-history` shows which of its own reasoning a chat host's model gets
|
|
501
|
+
back from the next turn on, and where that came from (`session`,
|
|
502
|
+
`SAMAGOTCHI_REASONING_HISTORY`, `models: <key>`, `hosts.<name>`, the default
|
|
503
|
+
`turn`). `/reasoning-history drop|turn|all` sets the session's own level, which
|
|
504
|
+
comes first; `/reasoning-history default` unsets it. It is saved and carried as
|
|
505
|
+
`/thinking`'s is, and a native host ignores it (the reply says so). A change to
|
|
506
|
+
or from `all` once an earlier reply has thinking rewrites the earlier replies;
|
|
507
|
+
`drop` ↔ `turn` costs nothing. `/stats` shows it on its `reasoning history:`
|
|
508
|
+
line, the web as the `reasoning` chip. See "Reasoning history" in
|
|
509
|
+
[configuration.md](configuration.md).
|
|
510
|
+
|
|
511
|
+
A `/thinking` or `/reasoning-history` change that restarts the prompt cache
|
|
512
|
+
asks first: the reply says what it costs, changes nothing, and names the line
|
|
513
|
+
that applies it (`/thinking low --yes`). `--yes` (or `-y`) applies it at once.
|
|
514
|
+
The terminal puts that line in the next input (Enter applies it); the web's
|
|
515
|
+
reply has an Apply button. `chi send ID "/thinking low"`,
|
|
516
|
+
`chi -p "/thinking low" --non-interactive` and lines piped into the REPL apply
|
|
517
|
+
without asking (a plain `chi -p` opens the REPL, which asks). See "Changes that
|
|
518
|
+
restart the prompt cache" in [configuration.md](configuration.md).
|
|
519
|
+
|
|
500
520
|
### Typing during a turn
|
|
501
521
|
|
|
502
522
|
The prompt stays open while a turn runs, in an attached terminal and in the plain
|
|
@@ -525,9 +545,10 @@ REPL alike:
|
|
|
525
545
|
- At once, beside the turn: `/stats`, `/recap`, `/help`, `/context`, a
|
|
526
546
|
plugin's anytime command (`/btw`), and the commands that only show
|
|
527
547
|
something: `/models` (with a filter too), and `/model`, `/llm-context`,
|
|
528
|
-
`/thinking` and `/guardrails` alone.
|
|
548
|
+
`/thinking`, `/reasoning-history` and `/guardrails` alone.
|
|
529
549
|
- After the turn: the ones that change something (`/model X`, `/model clear`,
|
|
530
|
-
`/llm-context strategy …`, `/thinking LEVEL`, `/guardrails revoke N`) and
|
|
550
|
+
`/llm-context strategy …`, `/thinking LEVEL`, `/reasoning-history LEVEL`, `/guardrails revoke N`) and
|
|
551
|
+
`!cmd`, whose output
|
|
531
552
|
joins the conversation. `(queued: runs after this turn)` says so at once (the
|
|
532
553
|
web draws the command's bubble dashed, with the same words, until it ran);
|
|
533
554
|
when the turn ends they run in the order sent, a prompt sent before one
|
|
@@ -698,7 +719,8 @@ as you toggled them. A reloaded session shows the same block
|
|
|
698
719
|
from the saved messages (each step's thinking, narration, tool parameters
|
|
699
720
|
and output, the output capped at 2000 characters) and the timing records
|
|
700
721
|
(status and duration per row). On an `api: openai` host the model's
|
|
701
|
-
reasoning is saved with each step for this (
|
|
722
|
+
reasoning is saved with each step for this (and sent back to the model
|
|
723
|
+
within the turn: see "Reasoning history" in [Configuration](configuration.md));
|
|
702
724
|
steps saved before that have none, so they show no thinking. An `edit` or
|
|
703
725
|
`write` row has a closed `diff +3 −1` under it that opens to the change it
|
|
704
726
|
made (up to 120 lines or 8 KB), live and after a reload (see
|
data/docs/configuration.md
CHANGED
|
@@ -487,15 +487,15 @@ models:
|
|
|
487
487
|
(`chi web`'s, say) wakes it later: use `chi send --new --thinking low`.
|
|
488
488
|
- A level is fixed for a turn: a change takes effect at the next turn's start. `/thinking LEVEL` sent while a turn
|
|
489
489
|
runs waits for its end (as `/model X` does), and holds off that turn's warm-up. What a change costs the prompt cache,
|
|
490
|
-
which `/thinking` says:
|
|
490
|
+
which `/thinking` says (and asks before the costly ones: "Changes that restart the prompt cache"):
|
|
491
491
|
|
|
492
|
-
| Backend | A level change |
|
|
493
|
-
|
|
494
|
-
| native Qwen (`qwen36`) | only the prompt's tail (the empty thought after the cue) |
|
|
495
|
-
| native Gemma 4 (`gemma4`) | the whole prompt is read again: `<\|think\|>` starts the system prompt |
|
|
496
|
-
| chat host, local llama.cpp
|
|
497
|
-
| chat host,
|
|
498
|
-
| chat host, OpenRouter
|
|
492
|
+
| Backend | A level change | Asks |
|
|
493
|
+
|---|---|---|
|
|
494
|
+
| native Qwen (`qwen36`) | only the prompt's tail (the empty thought after the cue) | no |
|
|
495
|
+
| native Gemma 4 (`gemma4`) | the whole prompt is read again: `<\|think\|>` starts the system prompt | yes |
|
|
496
|
+
| chat host, local (llama.cpp, Splash), a model name with `gemma` | the whole prompt: the template writes the same token | yes |
|
|
497
|
+
| chat host, local, any other model | other request fields; the server keeps its cache, only the tail changes | no |
|
|
498
|
+
| chat host, remote (OpenRouter, a hosted API) | Claude's thinking params are part of the cached prefix, an OpenAI effort change rewrites hidden instructions: the conversation is read again | yes |
|
|
499
499
|
|
|
500
500
|
- `models:` levels and `thinking.level` in the file are read every turn; a host's `thinking:` reaches a worker when
|
|
501
501
|
it starts, as its `sampling:` does (`chi sessions stop` to change it).
|
|
@@ -539,6 +539,81 @@ hosts:
|
|
|
539
539
|
A different level changes a native model's system prompt (Gemma's token, Qwen's turn preamble), so the next turn
|
|
540
540
|
reads the whole context again once; on a chat host only the end of the prompt changes.
|
|
541
541
|
|
|
542
|
+
## Reasoning history
|
|
543
|
+
|
|
544
|
+
Which of its own reasoning a chat host's model (`api: openai`) gets back. chi saves each step's reasoning; with it
|
|
545
|
+
sent back, the model goes on from its own plan between tool calls instead of working it out again (in one test a local
|
|
546
|
+
Qwen thought about 8x less per request and finished a small task about 40% sooner). One level, set per model or per host:
|
|
547
|
+
|
|
548
|
+
```yaml
|
|
549
|
+
hosts:
|
|
550
|
+
deepseek-api:
|
|
551
|
+
url: https://api.deepseek.com/v1
|
|
552
|
+
api: openai
|
|
553
|
+
api_key_env: DEEPSEEK_API_KEY
|
|
554
|
+
reasoning_history: all # its API wants every earlier reply's reasoning when tools are sent
|
|
555
|
+
models:
|
|
556
|
+
some-model:
|
|
557
|
+
reasoning_history: drop
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- `turn` (the default) sends the running turn's reasoning: each reply since the last user message carries its
|
|
561
|
+
reasoning as `reasoning_content`, which is what Qwen's, Gemma 4's and DeepSeek's chat templates and the hosted APIs
|
|
562
|
+
(OpenRouter takes it for DeepSeek, Qwen and other text-reasoning models) expect. The running turn starts at the
|
|
563
|
+
last message you sent (a prompt, or a steer: as the chat template counts it) and never before the turn's own start,
|
|
564
|
+
so a turn that goes on by itself (a reminder, a context wake, a continue after a stop) sends nothing of the one
|
|
565
|
+
before it, an interrupted reply's partial thought included.
|
|
566
|
+
- `drop` sends none (what chi did before this setting).
|
|
567
|
+
- `all` sends every reply's reasoning, earlier turns too. It sends only the field: a local template that leaves out
|
|
568
|
+
earlier turns' reasoning (Qwen's) needs `sampling: {chat_template_kwargs: {preserve_thinking: true}}` as well.
|
|
569
|
+
- Order, first set wins: the session's own level, `SAMAGOTCHI_REASONING_HISTORY` (the whole process; for A/B runs),
|
|
570
|
+
the `models:` entry (found the way `profile:` is), the `hosts:` entry, then `turn`. Anything else than a level warns
|
|
571
|
+
once and counts as unset. A host's level reaches a worker when it starts, as its `sampling:` does.
|
|
572
|
+
- **A session's own level** (`drop`, `turn` or `all`; an explicit `turn` counts as its own) is saved in the session
|
|
573
|
+
(its file's `"reasoning_history"`), so a `--resume`, a respawned worker, a continue and `/model` keep it; a plugin's
|
|
574
|
+
fork copies it, a delegate child starts without it. Set it with `/reasoning-history LEVEL` in the session
|
|
575
|
+
(`/reasoning-history default` unsets it, `/reasoning-history` alone shows the level and where it came from) or the
|
|
576
|
+
web's `reasoning` chip in the session bar (shown on chat hosts only). There is no start flag: a fresh session runs
|
|
577
|
+
the command before its first turn at no cost. A change takes effect at the next turn's start; sent while a turn runs
|
|
578
|
+
it waits for its end.
|
|
579
|
+
- What a change costs the prompt cache (`/reasoning-history` says so, and asks first when it costs: "Changes that
|
|
580
|
+
restart the prompt cache"):
|
|
581
|
+
|
|
582
|
+
| Change | Cost |
|
|
583
|
+
|---|---|
|
|
584
|
+
| `drop` ↔ `turn` | none: a turn's first request carries no reasoning under either |
|
|
585
|
+
| to or from `all`, once an earlier reply has thinking | every earlier reply gains or loses its reasoning: a server that renders it (hosted APIs, llama.cpp's default template) reads the conversation again from the first earlier thought |
|
|
586
|
+
| nothing thought yet, the same level, or a native host | none |
|
|
587
|
+
|
|
588
|
+
- The native loop (`/completion`) ignores it: it always keeps the running turn's reasoning, a mid-turn steer included
|
|
589
|
+
(the chat loop starts again after a steer).
|
|
590
|
+
- When a host answers a request that sent reasoning back with an HTTP 400 about reasoning, chi asks again without it,
|
|
591
|
+
sends none to that model from then on (until the worker restarts) and says so once. If the request without it is
|
|
592
|
+
refused too, the thinking fields get the same fallback ("Thinking") and the reasoning goes back on the next step.
|
|
593
|
+
- `/model` names a level that isn't the default (`reasoning history: drop (models: some-model)`, `all (session)`),
|
|
594
|
+
`/stats` shows it on its `reasoning history:` line (chat hosts only), and `chi self` on its `reasoning history` row
|
|
595
|
+
(a session's own level first when run from its commands).
|
|
596
|
+
- Claude via OpenRouter signs its reasoning (`reasoning_details`), which chi doesn't keep: it gets the plain text,
|
|
597
|
+
and the fallback above covers a refusal.
|
|
598
|
+
|
|
599
|
+
## Changes that restart the prompt cache
|
|
600
|
+
|
|
601
|
+
`/thinking` and `/reasoning-history` ask before a change that makes the server read the conversation again: Gemma's
|
|
602
|
+
thinking switch, a remote chat host's thinking fields ("Thinking"), and a change to or from `all` once an earlier reply
|
|
603
|
+
has thinking ("Reasoning history"). They ask only when the level the next turn runs under really changes and the
|
|
604
|
+
session has a model reply.
|
|
605
|
+
|
|
606
|
+
- The reply says what it costs and changes nothing yet: `Nothing changed yet: send /thinking low --yes to apply.`
|
|
607
|
+
- `--yes` (or `-y`, anywhere after the command) applies it at once.
|
|
608
|
+
- The terminal (the REPL and an attached TUI) puts the `--yes` line in the next input: Enter applies it.
|
|
609
|
+
- The web's reply bubble has an **Apply (restarts the cache)** button. The session bar's chips are already an explicit
|
|
610
|
+
choice: their Set sends the `--yes` line, and their form says what a change costs. Command bubbles are page-only, so
|
|
611
|
+
the button is gone after a reload.
|
|
612
|
+
- Only a person at a web tab or a chi TUI is asked. `chi send ID "/thinking low"`,
|
|
613
|
+
`chi -p "/thinking low" --non-interactive`, lines piped into the REPL, a delegate, a plugin and a card's action
|
|
614
|
+
apply directly, since nobody would see the question; so do the start flags (`chi --thinking` on a resumed or
|
|
615
|
+
attached session sends `/thinking LEVEL --yes`). A plain `chi -p` opens the REPL, which asks.
|
|
616
|
+
|
|
542
617
|
## Llama HTTP Timeouts
|
|
543
618
|
|
|
544
619
|
Long-running llama.cpp completions can exceed Ruby's default HTTP read timeout.
|
|
@@ -1199,6 +1274,7 @@ described in their own sections.
|
|
|
1199
1274
|
| `context.every_seconds` | `300` | | How often an attached context source's command runs when it has no `--every` (seconds, at least 30). See [Attached context](context.md). |
|
|
1200
1275
|
| `context.wake` | `true` | yes | A source whose update says `wake: true` may start a turn in a live, idle session (one per source per 10 minutes, within `session.max_wakes`); `false`: updates wait as notes for the next turn. See [Waking](context.md#waking). |
|
|
1201
1276
|
| `thinking.turn_preamble` | `true` | yes | Ask a `qwen36` model to open its thinking with a short `TURN:` line (the step label). |
|
|
1277
|
+
| `reasoning_history` | `turn` | | Which of its reasoning a chat host's model gets back: `drop`, `turn`, `all`; environment only (`SAMAGOTCHI_REASONING_HISTORY`), below a session's own level (`/reasoning-history`), above the `models:`/`hosts:` entries. See "Reasoning history". |
|
|
1202
1278
|
| `thinking.level` | `default` | `--thinking` | `off`, `low`, `medium`, `high` or `default` for every model; the flag and env outrank the `models:`/`hosts:` entries, the file's value doesn't; a session's own level outranks them all, and `--thinking` sets it on a session run (on `chi web` it sets this). See "Thinking". |
|
|
1203
1279
|
| `models.<key>.thinking`, `hosts.<name>.thinking` | none | | A model's or host's level. See "Thinking". |
|
|
1204
1280
|
| `hosts.<name>.models` | none | | The model ids a host serves whatever its `/v1/models` lists: a map of ids, or a plain list. See "Models a host serves but doesn't list". |
|
|
@@ -24,6 +24,6 @@ Behavior:
|
|
|
24
24
|
- `task_get` returns metadata and `output_path`; use `read` for output contents.
|
|
25
25
|
- `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
|
|
26
26
|
- `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
|
|
27
|
-
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown)
|
|
27
|
+
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `TaskRuntime::MARKER_ENV_KEYS` (and `SessionManager::SESSION_MARKER_ENV`, which a spawned worker unsets). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
|
|
28
28
|
|
|
29
29
|
Work that needs a model, not a shell command, goes to a child chi session instead: `delegate` / `delegate_result` have the same shape (start, then wait) and return only the child's final reply; see [Sessions: Delegating](../sessions.md#delegating).
|
data/docs/internals/engine.md
CHANGED
|
@@ -174,6 +174,19 @@ The differences from the native loop:
|
|
|
174
174
|
as `stable_length:`), where the prompt's stable part ends. For remote Claude, `LLM::PromptCache` puts the
|
|
175
175
|
first cache breakpoint there; see prompt-caching.md.
|
|
176
176
|
- Each call's result is its own tool message, paired by `tool_call_id`.
|
|
177
|
+
- A model turn's saved reasoning (`thinking:`) goes back as `reasoning_content` per the turn's
|
|
178
|
+
`TurnSettings#reasoning_history` (`ReasoningHistory.resolve`, the session's own level first:
|
|
179
|
+
`Session#reasoning_history`, `/reasoning-history`): `all` on every model message, `turn` past
|
|
180
|
+
`ChatLoop#reasoning_boundary`: the later of the last query (a `role: "user"` entry: a prompt, a steer, merged input,
|
|
181
|
+
the chat template's `last_query_index`; or a `turn_start` entry) and the Run's start (`Run`'s `@start`, the
|
|
182
|
+
conversation length at `#complete`), so a continue turn (reminder, context wake, continue after a stop) replays
|
|
183
|
+
nothing of the run before. `Run#prompt_chars` counts it (`#reasoning_chars`). The native loop differs on a steer:
|
|
184
|
+
`KernelLoop#inject_pending_input!` doesn't re-sanitize, so native keeps the turn's reasoning across a mid-turn
|
|
185
|
+
steer, where the chat loop starts again (as the template does).
|
|
186
|
+
- A 400 about reasoning (`BadRequest#reasoning_refused?`) for a request that sent reasoning back asks the step again
|
|
187
|
+
without it; when that goes through, `#reasoning_history_refused!` marks the model (worker lifetime) and
|
|
188
|
+
`:reasoning_history_refused` goes out. When it is refused too, the thinking-fields fallback (`#thinking_refused!`,
|
|
189
|
+
`:thinking_refused`) runs on it, still without reasoning. Up to three requests per step.
|
|
177
190
|
|
|
178
191
|
### Shared: ToolResponse (per batch) and ToolRunner (per call)
|
|
179
192
|
|
|
@@ -320,6 +333,7 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
320
333
|
| `:empty_answer_retry` | an empty, cut or malformed answer is asked again | `{iteration, attempt, of, finish_reason, thinking_chars, stopped_by?, malformed?}` |
|
|
321
334
|
| `:steer_cut` | a steer cut the generation | `{iteration, source}` |
|
|
322
335
|
| `:llm_context_edited` | the apply rule applied a batch of LLM context edits (a request, a forget_outputs call, turn end; never the warm-up) | `{moment, why, freed_tokens, tail_tokens, staged, text, groups}` (`LLMContextNotice`) |
|
|
336
|
+
| `:thinking_refused` / `:reasoning_history_refused` | the chat loop asked a step again without the thinking fields / the reasoning sent back; the Engine logs it and makes a once-per-session-and-host `:hook_notice` of it, the event itself never reaches the sink | `{iteration, model, level, detail}` |
|
|
323
337
|
| `:pending_input_merged` | queued input joined the conversation | `{iteration, count, content, steers?, answer}` |
|
|
324
338
|
| `:tool_dispatch_started` / `:tool_dispatch_completed` | around a batch | `{iteration, call_count}` |
|
|
325
339
|
| `:tool_call_started` | before a call runs | see ToolRunner |
|
|
@@ -135,6 +135,14 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
135
135
|
on its `generation_completed` log line. `/stats` shows "cached N (P%)" and "cache writes". A change that
|
|
136
136
|
drops `cached=` on the second fresh session is a regression. Update the prompt snapshot fixtures with
|
|
137
137
|
`UPDATE_PROMPTS=1` and read the diff: a cache-friendly change moves sections, it doesn't reorder per run.
|
|
138
|
+
6. **Ask before a setting change that restarts the cache.** A session setting whose change makes the server read
|
|
139
|
+
the conversation again asks a person at a web tab or a TUI first (`CacheConfirm`: the reply carries the
|
|
140
|
+
`--yes` line, `SessionCommands::Result#confirm` and the `command_ran` event's `confirm`). Each setting has a
|
|
141
|
+
classifier: `Thinking.cache_cost` (`:full` for Gemma, `:provider` for a remote chat host, `:tail` otherwise) and
|
|
142
|
+
`ReasoningHistory.cache_cost` (`:history` for a change to or from `all` once an earlier reply has thinking).
|
|
143
|
+
`CacheConfirm.needed?` asks only for `:full`, `:provider` and `:history`, when the effective level changes and
|
|
144
|
+
the session has a model reply. A new setting with a cost (a `/model` across templates, `/llm-context` turning
|
|
145
|
+
forget on) adds its classifier and goes through the same step.
|
|
138
146
|
|
|
139
147
|
## Server-side settings worth knowing
|
|
140
148
|
|
data/docs/sessions.md
CHANGED
|
@@ -68,7 +68,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
68
68
|
**Ordering:**
|
|
69
69
|
|
|
70
70
|
- `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
|
|
71
|
-
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
71
|
+
- Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them until the window is tall again). A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new chat. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`), and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new chat starts on; it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new chat only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
|
|
72
72
|
- Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
|
|
73
73
|
- The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `day N` chip and rows, the info bar's ← → links, a refused continue), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `format.js` (pure formatters such as `previewOf`), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, the actions per kind, one delegated click). Unit-tested via `npm test` (`node --test spec/web/public/*.test.js`).
|
|
74
74
|
- Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
|
|
@@ -58,7 +58,7 @@ Only the nested form is read. An env name used as a top-level key (`SAMAGOTCHI_D
|
|
|
58
58
|
|
|
59
59
|
- `model_aliases:` map of alias → model id (`ModelRef.parse`, through `HostRegistry#resolve`). Keys lowercased on write (`ConfigFile.write_model_alias!`). Values may be bare `model` or qualified `host:model` (hybrid; only `:` names a host, `org/model` is an id). Aliases apply once: never point an alias at another alias.
|
|
60
60
|
- `hosts:` map of `name → {host, port | url, transport, api, api_key_env, profile, first_token_timeout, vision, sampling, thinking, enabled, models}` (`ConfigFile.hosts_config`, `host_registry.rb` `HostEntry`). Names lowercased; `url:` (http/https, optional path) replaces host/port, never both; `api_key_env:` names the env var holding the API key (never write a key into config.yml); `transport` overrides `server.transport`; `first_token_timeout` (seconds, `0` = off; a negative or non-number warns and is ignored) overrides `server.first_token_timeout` for that host; `models:` (a map of model ids, or a plain list) declares ids the host serves whatever its `/v1/models` lists: no "doesn't list" warning, and a bare id routes there as if listed (`HostModel`); an entry may hold `price: {input, output, cache_read?, cache_write?}` in USD per 1M tokens (`ModelPrice`; an unset cache rate is the input rate), which estimates a cost the provider doesn't report or reports as 0 (`cost_estimate`, shown as `~$`), and `served: [ids] | any`, the models a gateway may answer that id with (no served-model warning for them); model settings stay in the root `models:`; workers inherit via `SAMAGOTCHI_HOSTS_JSON` (`hosts_json_for_env`, `session_manager.rb`).
|
|
61
|
-
- `models:` map of model id or alias → `{profile, vision, sampling, thinking}` (`ConfigFile.model_settings`). `thinking:` (here, on a host, or `thinking.level` for every model) is `off|low|medium|high|default` (`Thinking`; unquoted `off` works, `on` is not a level; `default` = send nothing). Precedence: the session's own level (`/thinking LEVEL`, saved in the session; `chi --thinking` on a session run, `chi send --new --thinking`) > `chi web --thinking`/`SAMAGOTCHI_THINKING_LEVEL` > `models:` > `hosts.<name>.thinking` > `thinking.level` in config.yml > `default`. A `sampling:` key wins over the fields a level sends (`null` drops one); `docs/configuration.md` "Thinking". `sampling:` (here or on a host) is a map of request fields passed to the provider as written (`temperature`, `top_p`, `presence_penalty`, `repeat_penalty`, …; a model's fields win over its host's per field; `null` = don't send; chi's own fields like `max_tokens`/`stream` are refused with a warning; `docs/configuration.md` "Sampling"). Keys match case-insensitively. `profile` (here or on a host) is `qwen36|gemma4`: the raw prompt format for native hosts. Precedence: `--profile`/`SAMAGOTCHI_MODEL_PROFILE` > `models:` > `hosts.<name>.profile` > the llama.cpp server's chat template > the name (`qwen`/`gemma`) > `qwen36` (`ModelProfile.resolve`). Set one when a model's name hides its family (e.g. a Qwen fine-tune under another name on mlx, which has no template to read).
|
|
61
|
+
- `models:` map of model id or alias → `{profile, vision, sampling, thinking, reasoning_history}` (`ConfigFile.model_settings`). `thinking:` (here, on a host, or `thinking.level` for every model) is `off|low|medium|high|default` (`Thinking`; unquoted `off` works, `on` is not a level; `default` = send nothing). Precedence: the session's own level (`/thinking LEVEL`, saved in the session; `chi --thinking` on a session run, `chi send --new --thinking`) > `chi web --thinking`/`SAMAGOTCHI_THINKING_LEVEL` > `models:` > `hosts.<name>.thinking` > `thinking.level` in config.yml > `default`. A `sampling:` key wins over the fields a level sends (`null` drops one); `docs/configuration.md` "Thinking". `reasoning_history:` (here or on a host) is `drop|turn|all`, which of its reasoning a chat host's model gets back. Precedence: the session's own level (`/reasoning-history LEVEL`, saved in the session) > `SAMAGOTCHI_REASONING_HISTORY` > `models:` > `hosts.<name>.reasoning_history` > `turn`; `docs/configuration.md` "Reasoning history". `/thinking` and `/reasoning-history` ask before a change that restarts the prompt cache; `--yes` applies it (`chi send` commands apply directly). `sampling:` (here or on a host) is a map of request fields passed to the provider as written (`temperature`, `top_p`, `presence_penalty`, `repeat_penalty`, …; a model's fields win over its host's per field; `null` = don't send; chi's own fields like `max_tokens`/`stream` are refused with a warning; `docs/configuration.md` "Sampling"). Keys match case-insensitively. `profile` (here or on a host) is `qwen36|gemma4`: the raw prompt format for native hosts. Precedence: `--profile`/`SAMAGOTCHI_MODEL_PROFILE` > `models:` > `hosts.<name>.profile` > the llama.cpp server's chat template > the name (`qwen`/`gemma`) > `qwen36` (`ModelProfile.resolve`). Set one when a model's name hides its family (e.g. a Qwen fine-tune under another name on mlx, which has no template to read).
|
|
62
62
|
- `hooks:` map of `hooks_dir` + per-event lists `{path, on_error}` (`Hooks::Loader.load`). `hooks_dir` may start with `~`.
|
|
63
63
|
- `guardrails:` tool-call rules (`docs/guardrails.md`; read in `Engine#guardrail_rules`, parsed by `Guardrails::Rules.parse`): `enabled` (bool, default true; `false` drops rules and hooks' asks, a deny still applies; config.yml only, like every `guardrails` setting: no env variable), `parent_approvals` (`off`, the default: a parent agent's `chi answer` can only deny an approval; `once`: it may give "Allow once" too; a convention for an honest parent, not a security boundary, and the user's choice: set it only when the user asks), `rules:` (list), `disable:` (a rule id or a list of them, `id` or `bundle:id`, switching off a bundle's or config rule without editing it), `small_models` (`auto`, the default: small = 32B or less read from the name; a glob or list of globs; `[]` for none). A rule takes only `id`, `tool`, `command`, `path`, `git`, `models`, `verdict`, `reason`, `scopes`: `id` required; at least one of `tool` (a name, `shell` = execute + task_create, a `File.fnmatch` glob like `"mcp_*"`, or a list), `command` (a Ruby regex on the shell command), `path` (a glob, or `outside_repo`: outside the session's repo, tmp folders and memory files excepted), `git` (only `outside_repo`: a shell call runs commit/add/reset/… in another checkout); `models` (optional: `small`, a model-name glob, or a list; without it the rule is for every model); `verdict` `ask|deny`; `scopes` (for `ask`) a subset of `once, session, repo, rule`. **Any parse error (an unknown key, a bad regex, no verdict) makes chi deny every tool call** until fixed, so validate with `YAML.safe_load` and keep the list shape. Rules are read again on the next tool call after config.yml or a bundle's rules change: no restart needed. Installed bundles' rule files (`chi bundle install guardrails`) add to them; `/guardrails` lists what loaded.
|
|
64
64
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.56.0
|
|
4
4
|
scope: system
|
|
5
5
|
description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
|
|
6
6
|
files:
|
|
7
7
|
identity.md: sha256:29c2f7df9f6c5acbc5ec17000a4490741e55e426af15ca0d81c8706edd9bd027
|
|
8
8
|
self_map.md: sha256:dfc19266259a9af1e0d769b7e0f67996b740b0c9921fdf3dadad973f6e5c144a
|
|
9
|
-
config_modification_protocol.md: sha256:
|
|
9
|
+
config_modification_protocol.md: sha256:d908921b5988244fb2a03fe04e693d0e7a1dfadf8cd77704e9232826db7eb2d7
|
|
10
10
|
memory_guide.md: sha256:15cd3b24eda853cc458371c6c75809a2d4b5352311d075aaa4384926b5221c27
|
|
11
11
|
delegated.md: sha256:aedaa1e756a244904c5560fff0424e06fb468bbdcb0358bccefe321d67cd2614
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# The confirm step before a session setting change that restarts the
|
|
5
|
+
# prompt cache (/thinking, /reasoning-history): such a change, sent
|
|
6
|
+
# without --yes from a person at a web tab or a chi TUI, changes nothing
|
|
7
|
+
# and replies with what it costs and the line that applies it. Two steps,
|
|
8
|
+
# no stored state: the reply carries a Confirm (SessionCommands::Result,
|
|
9
|
+
# the worker's command_ran), the TUI prefills its line, the web draws an
|
|
10
|
+
# Apply button that sends it.
|
|
11
|
+
module CacheConfirm
|
|
12
|
+
YES_FLAGS = %w[--yes -y].freeze
|
|
13
|
+
# The costs worth asking about (Thinking.cache_cost,
|
|
14
|
+
# ReasoningHistory.cache_cost); :tail and nil are not.
|
|
15
|
+
COSTLY = %i[full provider history].freeze
|
|
16
|
+
|
|
17
|
+
# The line that applies a change, and the question the reply ends with.
|
|
18
|
+
Confirm = Data.define(:line, :text) do
|
|
19
|
+
# The plain form the worker's command_ran carries.
|
|
20
|
+
# @return [Hash] symbol keys
|
|
21
|
+
def to_h = { line: line, text: text }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# A command's argument words with --yes / -y (anywhere) taken out.
|
|
27
|
+
# @param args [String] what follows the command name
|
|
28
|
+
# @return [Array(String, Boolean)] the rest, stripped and downcased, and
|
|
29
|
+
# whether --yes was there
|
|
30
|
+
def split(args)
|
|
31
|
+
words = args.to_s.strip.downcase.split
|
|
32
|
+
yes = words.any? { |word| YES_FLAGS.include?(word) }
|
|
33
|
+
[(words - YES_FLAGS).join(" "), yes]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Whether a change must ask first: the effective level changes, the
|
|
37
|
+
# cost is one of COSTLY, and the session has a model reply (a cache to
|
|
38
|
+
# restart at all).
|
|
39
|
+
def needed?(cost:, history:, changed:)
|
|
40
|
+
changed && history && COSTLY.include?(cost)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The Confirm for +name+ set to +word+: the line with --yes, and the
|
|
44
|
+
# question naming it.
|
|
45
|
+
# @return [Confirm]
|
|
46
|
+
def confirm(name, word)
|
|
47
|
+
line = "#{name} #{word} --yes"
|
|
48
|
+
Confirm.new(line: line, text: "Nothing changed yet: send `#{line}` to apply.")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/samagotchi/client_id.rb
CHANGED
|
@@ -39,8 +39,20 @@ module Samagotchi
|
|
|
39
39
|
HUMAN_PREFIXES = [WEB_PREFIX, TUI_PREFIX].freeze
|
|
40
40
|
HUMAN_IDS = [CLI_SEND].freeze
|
|
41
41
|
|
|
42
|
+
# A person at a screen that shows a command's reply and can answer it
|
|
43
|
+
# (#interactive?): a web tab, an attached chi TUI.
|
|
44
|
+
INTERACTIVE_PREFIXES = [WEB_PREFIX, TUI_PREFIX].freeze
|
|
45
|
+
|
|
42
46
|
module_function
|
|
43
47
|
|
|
48
|
+
# Whether +client_id+ is a web tab or a chi TUI, which see a command's
|
|
49
|
+
# reply and can send the line it asks for (CacheConfirm); chi send, a
|
|
50
|
+
# delegate, a plugin, chi itself and nil are not.
|
|
51
|
+
def interactive?(client_id)
|
|
52
|
+
id = client_id.to_s
|
|
53
|
+
INTERACTIVE_PREFIXES.any? { |prefix| id.start_with?(prefix) }
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
# Whether +client_id+ (a turn's or a line's origin) is a human's input.
|
|
45
57
|
def human?(client_id)
|
|
46
58
|
return true if client_id.nil?
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -187,6 +187,10 @@ module Samagotchi
|
|
|
187
187
|
# is bin/chi's own --thinking; models: and hosts: entries rank below
|
|
188
188
|
# the flag and the env, above config.yml's value.
|
|
189
189
|
Entry.new(key: "thinking.level", yaml_path: %w[thinking level], type: :string, default: "default", expose: %i[env config]),
|
|
190
|
+
# Which of its reasoning a chat host's model gets back (ReasoningHistory):
|
|
191
|
+
# the environment only, for A/B runs; models: and hosts: entries are
|
|
192
|
+
# the config surface.
|
|
193
|
+
Entry.new(key: "reasoning_history", yaml_path: %w[reasoning_history], type: :string, default: nil, expose: %i[env]),
|
|
190
194
|
|
|
191
195
|
Entry.new(key: "default.max_tokens", yaml_path: %w[default max_tokens], type: :integer, default: nil, expose: %i[env config cli]),
|
|
192
196
|
Entry.new(key: "max_tool_output_chars", yaml_path: %w[max_tool_output_chars], type: :integer, default: 10_000, expose: %i[env config cli]),
|
|
@@ -269,9 +273,9 @@ module Samagotchi
|
|
|
269
273
|
# Maps of named entries and the keys an entry may hold
|
|
270
274
|
# (ConfigFile.hosts_config, ConfigFile.model_settings).
|
|
271
275
|
MAP_ENTRY_KEYS = {
|
|
272
|
-
"hosts" => %w[host port url transport api api_key_env profile first_token_timeout vision sampling thinking
|
|
273
|
-
remote window_tokens llm_context_strategy llm_context_apply llm_context_budget_tokens models].freeze,
|
|
274
|
-
"models" => %w[profile vision sampling thinking window_tokens llm_context_strategy llm_context_apply
|
|
276
|
+
"hosts" => %w[host port url transport api api_key_env profile first_token_timeout vision sampling thinking
|
|
277
|
+
reasoning_history enabled remote window_tokens llm_context_strategy llm_context_apply llm_context_budget_tokens models].freeze,
|
|
278
|
+
"models" => %w[profile vision sampling thinking reasoning_history window_tokens llm_context_strategy llm_context_apply
|
|
275
279
|
llm_context_budget_tokens].freeze
|
|
276
280
|
}.freeze
|
|
277
281
|
# The keys a hosts.<name>.models entry may hold (HostModel.parse_map).
|