samagotchi 0.38.0 → 0.39.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 +34 -1
- data/docs/cli.md +2 -1
- data/docs/configuration.md +10 -2
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/llm-context-bench.md +119 -0
- data/docs/internals/prompt-caching.md +3 -2
- data/docs/plugins.md +3 -2
- data/docs/testing.md +7 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +10 -10
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +30 -4
- data/lib/samagotchi/context_note.rb +7 -3
- data/lib/samagotchi/context_status.rb +20 -2
- data/lib/samagotchi/engine.rb +19 -5
- data/lib/samagotchi/guardrails/targets.rb +14 -2
- data/lib/samagotchi/host_registry.rb +6 -3
- data/lib/samagotchi/kernel_loop.rb +109 -11
- data/lib/samagotchi/llm/chat_loop.rb +39 -14
- data/lib/samagotchi/llm/turn_settings.rb +5 -4
- data/lib/samagotchi/llm_context_apply.rb +153 -0
- data/lib/samagotchi/llm_context_edit.rb +71 -0
- data/lib/samagotchi/llm_context_stale.rb +255 -0
- data/lib/samagotchi/llm_context_strategy.rb +133 -0
- data/lib/samagotchi/llm_context_view.rb +124 -0
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/prompt.rb +4 -11
- data/lib/samagotchi/session_manager.rb +9 -1
- data/lib/samagotchi/session_metrics.rb +18 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -2
- data/lib/samagotchi/token_usage.rb +3 -0
- data/lib/samagotchi/tool_ids.rb +84 -0
- data/lib/samagotchi/tool_response.rb +67 -6
- data/lib/samagotchi/tools/context_read.rb +27 -10
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/worker.rb +4 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15442c0fe342d1193ff595b31c554f886076fc4da7dac938fe319ec784e053c6
|
|
4
|
+
data.tar.gz: 37e73e8590db29ba273346191954ba96b72de468a46663bc2f8820807d95278d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7d252dbf3ef0eb6ea34aa61bd47e5da389bf17b5f0f4784c9c0e2bf69d22732e83db29b8652bce40ef7ac4a0593e48bc481eb63b9b86a2485ff4027dac1de5f
|
|
7
|
+
data.tar.gz: 50d569dc04ca76c690c9f4f91ffcec90d85b54445b07f63d2bab1fc6aed5a47d214d9b400faa9b3915783a10e9952613b1a0b0db6361accb514eb4aab7ad3e67
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,38 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.39.0] - 2026-10-07
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `llm_context.strategy` (default `none`), and per model or host `llm_context_strategy`. `stale` sends a file read
|
|
16
|
+
that a later read of the same lines superseded as a one-line stub; the session keeps the output. With
|
|
17
|
+
`llm_context.stale_edits: true` (opt-in, experimental) a read a later edit or write of the file superseded is
|
|
18
|
+
stubbed too; on the replay bench these cost about 5x the tokens they free in re-prefill, and 26% were needed again
|
|
19
|
+
(against a 6% base rate). No read of a file edited in the last `llm_context.protect_steps` (3) steps is stubbed.
|
|
20
|
+
`llm_context.apply` (and per model or host `llm_context_apply`) says when stubs reach the prompt: `payoff` (the
|
|
21
|
+
default) sends them at a request when they free at least the tail the server reads again, else at the end of a
|
|
22
|
+
turn the model answered, which in practice is usually where they go; `next_request` and `turn_end` are the
|
|
23
|
+
others. See [docs/configuration.md](docs/configuration.md).
|
|
24
|
+
- `/stats` shows the re-prefilled tokens for every session (what the server prefilled again of the previous
|
|
25
|
+
request's prompt, 128 tokens or more), and the log's `generation_completed` line has them as `reprefill=`.
|
|
26
|
+
- For development: `script/llm_context_bench.rb`, a replay benchmark for the LLM context strategies over a folder of
|
|
27
|
+
stored sessions (offline; `--live` asks a model, for picks). It isn't part of the gem. See
|
|
28
|
+
[docs/internals/llm-context-bench.md](docs/internals/llm-context-bench.md).
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Answering a step-limit question with Continue and then sending a prompt runs the continue turn first, even when
|
|
33
|
+
both arrive while the worker is between turns (the prompt could run first and drop the offer). A session started
|
|
34
|
+
from another session's `execute` (`chi send`) no longer inherits that session's `SAMAGOTCHI_PARENT_SESSION` and
|
|
35
|
+
`SAMAGOTCHI_SESSION_MODEL`, so your `!chi context add` there attaches to it, not to the other one. `context_read`
|
|
36
|
+
pages within `max_tool_output_chars` whatever its header holds, so a page is never cut a second time behind its
|
|
37
|
+
"Pass offset". The mcp bundle (0.6.1): `mcp_call` with a mistyped `<server>/<tool>` waits only for that server's
|
|
38
|
+
start, and `find_mcp_tools` names a server with no `description:` or instructions by its tool count, not its first
|
|
39
|
+
tool names (a model called those without searching).
|
|
40
|
+
|
|
41
|
+
Update with `chi update` (mcp 0.6.1).
|
|
42
|
+
|
|
11
43
|
## [0.38.0] - 2026-10-07
|
|
12
44
|
|
|
13
45
|
### Added
|
|
@@ -1842,7 +1874,8 @@ and long-lived sessions.
|
|
|
1842
1874
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
1843
1875
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
1844
1876
|
|
|
1845
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
1877
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.39.0...HEAD
|
|
1878
|
+
[0.39.0]: https://github.com/dm1try/samagotchi/compare/v0.38.0...v0.39.0
|
|
1846
1879
|
[0.38.0]: https://github.com/dm1try/samagotchi/compare/v0.37.0...v0.38.0
|
|
1847
1880
|
[0.37.0]: https://github.com/dm1try/samagotchi/compare/v0.36.0...v0.37.0
|
|
1848
1881
|
[0.36.0]: https://github.com/dm1try/samagotchi/compare/v0.35.0...v0.36.0
|
data/docs/cli.md
CHANGED
|
@@ -421,7 +421,8 @@ file and index line stay as they are; only this session doesn't see it.
|
|
|
421
421
|
`/help` lists the commands this session takes, the installed bundles' too, each
|
|
422
422
|
with a line on what it does. `/stats` shows the session's numbers: turns, tool
|
|
423
423
|
calls (by tool, with errors), iterations, tokens in/out summed over every
|
|
424
|
-
request (with the cached tokens, the cache writes and the reasoning tokens when the server
|
|
424
|
+
request (with the cached tokens, the cache writes, the re-prefilled tokens and the reasoning tokens when the server
|
|
425
|
+
reports them),
|
|
425
426
|
the decode speed, generation latency, cancellations, retries (the network's),
|
|
426
427
|
thinking cuts (generations a plugin such as loop-guard cut) and output cap hits
|
|
427
428
|
(generations that ended at the provider's cap), context used and window, the prompt profile and the model the server says it ran, and the
|
data/docs/configuration.md
CHANGED
|
@@ -535,8 +535,10 @@ cache:
|
|
|
535
535
|
key: session # off (default) or session
|
|
536
536
|
```
|
|
537
537
|
Each request's own counts are in the debug log's `generation_completed` line:
|
|
538
|
-
`prompt=` (its prompt tokens), `cached=` (read from the server's cache)
|
|
539
|
-
server reports writes)
|
|
538
|
+
`prompt=` (its prompt tokens), `cached=` (read from the server's cache), `cache_write=` (written to it, when the
|
|
539
|
+
server reports writes) and `reprefill=` (of the previous request's prompt on the same model, what the server
|
|
540
|
+
prefilled again: an `llm_context` edit's cache break, or anything else that rewrote the prompt's middle; only when the
|
|
541
|
+
server reports a cached count, and not on the first request after a warm-up); recap and side requests log theirs as `recap request_usage`.
|
|
540
542
|
|
|
541
543
|
## Llama Model Routing
|
|
542
544
|
|
|
@@ -915,6 +917,12 @@ described in their own sections.
|
|
|
915
917
|
| `context.status` | `true` | yes | Context-usage telemetry for the model. See [context telemetry](internals/context-telemetry.md). |
|
|
916
918
|
| `context.window_tokens` | server's, else 256000 | yes | Context window when the server doesn't report one. `models.<key>.window_tokens` and `hosts.<name>.window_tokens` come first; see "The context window". |
|
|
917
919
|
| `models.<key>.window_tokens`, `hosts.<name>.window_tokens` | none | | A model's or host's context window when the server and its model list report none. |
|
|
920
|
+
| `llm_context.strategy` | `none` | | What the model is sent of the conversation: `none` (everything, unchanged), or a list of layers (`stale`, `forget`; `"\|"`-separated or a YAML list). `stale`: a file read that a later read covering its lines superseded (a read that came back whole: not the head/tail preview of a big file, nor cut at `max_tool_output_chars`) is sent as a stub (`[read] lib/x.rb lines 1-200: superseded by a later read`). With `llm_context.stale_edits: true` (opt-in, experimental) so is a read a later successful edit or write of the file superseded (`superseded by a later edit`); on the replay bench these stubs cost about 5x the tokens they free in re-prefill, and 26% of them were needed again (against a 6% base rate). No read of a file edited or written in the last `llm_context.protect_steps` steps is stubbed. When stubs reach the prompt is `llm_context.apply`: `next_request` sends a read a later read superseded from the next request, and an edit-driven stub at the end of a turn the model answered; `turn_end` sends them all at the end of a turn the model answered (a turn that ran out of steps keeps them staged; on the native loop against a local llama.cpp the turn-end warm-up prefills the edited prompt, elsewhere the next turn's first request carries the cache break); `payoff` sends the batch at a request when the tokens it frees are at least the tail after its earliest stub (what the server reads again), or the context is in the top `context.status_thresholds` bucket (which needs `context.status` on), else at the end of a turn the model answered. A re-read's tail holds the later read, so in practice payoff usually waits for the turn's end. Each batch breaks the prompt cache from its earliest stub; `/stats` counts the re-prefilled tokens. The session keeps the outputs and the stubs it sent, so a `--resume` sends the same. `forget` isn't built yet: a list with it warns and is `none`; an unknown name warns and is `none`. `models.<key>.llm_context_strategy` and `hosts.<name>.llm_context_strategy` come first. |
|
|
921
|
+
| `models.<key>.llm_context_strategy`, `hosts.<name>.llm_context_strategy` | none | | A model's or host's `llm_context.strategy` (`none` or `stale` for now). |
|
|
922
|
+
| `llm_context.apply` | `payoff` | | When a layer's edits reach the prompt (each batch breaks the prompt cache from its earliest edit): `payoff`, `next_request` or `turn_end`. An unknown value warns and is `payoff`. `models.<key>.llm_context_apply` and `hosts.<name>.llm_context_apply` come first. |
|
|
923
|
+
| `models.<key>.llm_context_apply`, `hosts.<name>.llm_context_apply` | none | | A model's or host's `llm_context.apply`. |
|
|
924
|
+
| `llm_context.stale_edits` | `false` | | Opt-in, experimental: `stale` also stubs a read that a later successful edit or write of the file superseded. |
|
|
925
|
+
| `llm_context.protect_steps` | `3` | | `stale` never stubs a read of a file edited or written in the last N steps (tool batches; counted across the whole conversation, not only the turn). `0` turns the protection off. |
|
|
918
926
|
| `context.chars_per_token` | `4.0` | yes | Estimate ratio when the server reports no usage. |
|
|
919
927
|
| `context.status_thresholds` | `20,40,60,80` | yes | Percentages that trigger a status. |
|
|
920
928
|
| `context.status_cadence` | `0` | yes | Also every N rounds; `0` = thresholds only. |
|
|
@@ -23,6 +23,6 @@ Behavior:
|
|
|
23
23
|
- `task_get` returns metadata and `output_path`; use `read` for output contents.
|
|
24
24
|
- `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
|
|
25
25
|
- `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.
|
|
26
|
-
- `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) and `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown). `chi self` reads the latter for its `model` row.
|
|
26
|
+
- `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) and `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown). `chi self` reads the latter for its `model` row. 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.
|
|
27
27
|
|
|
28
28
|
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).
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# The llm_context replay benchmark
|
|
2
|
+
|
|
3
|
+
`script/llm_context_bench.rb` replays stored chi sessions offline and reports what an LLM context strategy
|
|
4
|
+
(`llm_context.strategy`) would have changed in them. Every strategy or policy change gets a number from it before it
|
|
5
|
+
becomes a default. It is a development tool: it lives under `script/`, isn't shipped with the gem, and no spec runs
|
|
6
|
+
it on real sessions (the specs use small synthetic ones).
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
ruby script/llm_context_bench.rb [SESSIONS_DIR] [options] # --help lists the options
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`SESSIONS_DIR` is a folder of `<session id>.json` files: an argument, else `$LLM_CONTEXT_BENCH_SESSIONS`, else chi's
|
|
13
|
+
own (`~/.local/state/samagotchi/sessions`). The benchmark only reads it. Sessions with fewer than two turns are
|
|
14
|
+
skipped; `--top N` keeps the N heaviest, `--session PREFIX` picks some.
|
|
15
|
+
|
|
16
|
+
It measures what chi would send: sessions load through `Session.from_h`, a native batch's joined result splits into
|
|
17
|
+
its runs as `LLMContextView` splits it (calls read by chi's `ToolCallParser`), every output is named by its
|
|
18
|
+
`ToolIds` id, and a strategy's edits are saved as `LLMContextEdit` records on copies of the entries and rendered
|
|
19
|
+
through `LLMContextView`. Tokens are chars/4 throughout, which undercounts code-heavy prompts by about 1.25x.
|
|
20
|
+
|
|
21
|
+
## What it reports
|
|
22
|
+
|
|
23
|
+
**The profile (none, every session):** the resent tokens by category (tool outputs, call arguments, the system
|
|
24
|
+
prompt, prose, user messages, context notes, a native session's inline thinking; stored thinking apart, since chi
|
|
25
|
+
never resends it), tool outputs by tool and size, turns, how much of a turn's output a later turn refers to, and
|
|
26
|
+
repeat reads.
|
|
27
|
+
|
|
28
|
+
**A row per strategy × model × policy**, summed over the cases. A case is the end of a turn that has a next turn,
|
|
29
|
+
scored at the next turn's first request. Cases are named `<first 8 of the session id>_t<turn>`; they come from a
|
|
30
|
+
`--cases FILE` (one name per line), else from the picks given, else every turn with at least `--min-turn-tool`
|
|
31
|
+
(3000) tokens of its own tool output.
|
|
32
|
+
|
|
33
|
+
Each case says how its turn ends: `answer` (the model's final prose, no calls), `tool_result` (it stopped mid-task on
|
|
34
|
+
a tool's output), `turn_note` (chi's note on a turn that ended without an answer), `tool_call` (calls never
|
|
35
|
+
answered) or `empty`. The report's second line tallies them, `--per-case` has an `ends` column, and the JSON has
|
|
36
|
+
`case_ends` (name => ending) and `ends` on each case. `--ends answer|any` picks which turns make cases: by default the
|
|
37
|
+
turn-size rule keeps only `answer` ends, the point where a forget-at-turn-end offer would come (the layout check
|
|
38
|
+
found a model behaves differently mid-task), and named cases (`--cases`, picks) are kept as named. `--ends answer`
|
|
39
|
+
drops named cases that end otherwise and says how many; `--ends any` takes every turn.
|
|
40
|
+
|
|
41
|
+
| Column | Meaning |
|
|
42
|
+
|---|---|
|
|
43
|
+
| forgot/outputs | outputs the strategy edits / outputs in context at the case |
|
|
44
|
+
| freed/tool | what the prompt loses, stubs counted / the tool output tokens in context |
|
|
45
|
+
| wrong strict (base) | edited outputs a later step needs: from the step the edit reaches through the end of the next turn, a call re-reads the file or re-runs the command. In brackets the base rate: the share of all outputs in context that are needed, what a random pick scores |
|
|
46
|
+
| wrong loose (base) | the same, counting a mention of the output's path or of an identifier it brought in |
|
|
47
|
+
| 1-step | edited outputs the step the edit reaches re-reads (a read of the file, not an edit) or re-runs. Offline that step is the session's own, made without the stub, so it is a floor, not the stub's effect |
|
|
48
|
+
| re-prefilled | what the server prefills again because an edit broke its prompt cache: from the earliest changed entry to the end of what the previous request left cached. A case counts the breaks of its own window only (after the previous turn's case point), so a strategy that edits as the session goes isn't counted twice |
|
|
49
|
+
| breaks | the requests of the case's window that an edit first reaches: one prompt-cache break each, a batch's edits together |
|
|
50
|
+
|
|
51
|
+
The proxies are the context-edit spike's (lexical and lenient: a basename mention counts).
|
|
52
|
+
|
|
53
|
+
## Strategies
|
|
54
|
+
|
|
55
|
+
- `none`: nothing changes. The base.
|
|
56
|
+
- `forget_all`: at the turn's end, forget every output in context with an empty note. The spike's base rate; about
|
|
57
|
+
what a model told "free context now" does.
|
|
58
|
+
- `stale`: chi's own stale layer (`Samagotchi::LLMContextStale`) as chi runs it: each read a later read covering its
|
|
59
|
+
lines superseded is stubbed from the request after that read (chi applies it at the next request), so a case's
|
|
60
|
+
numbers count every stub since the session began. An edit or write of the file supersedes nothing, nor does a read
|
|
61
|
+
that came back as a preview or cut. A relative
|
|
62
|
+
path is taken against the session's working directory.
|
|
63
|
+
- `stale_edits_next_request`, `stale_edits_turn_end`, `stale_edits_payoff`: the stale layer with edit-driven stubs
|
|
64
|
+
under an `llm_context.apply` rule, as chi runs it (`Samagotchi::LLMContextApply`, `llm_context.protect_steps`'
|
|
65
|
+
default), over each session from its start: at each request, and at the end of each turn that ends with the
|
|
66
|
+
model's answer (those stubs reach the next turn's first request, the case point). The top context bucket isn't
|
|
67
|
+
modelled (the bench has no window), so `payoff` applies at a request only when the freed tokens are at least the
|
|
68
|
+
tail. A turn-end batch's re-prefill is counted at the next turn's first request even where chi's warm-up would
|
|
69
|
+
prefill it while the user reads.
|
|
70
|
+
- `forget_outputs` (P4): a slot. It prints "not built yet" until its phase adds it to
|
|
71
|
+
`LLMContextBench::Strategies`; a strategy answers `#plans(kase)` with `Plan`s of `PlannedEdit`s (the output id, the
|
|
72
|
+
edit kind, the note, the request it first reaches), and the scorer and the report take it as is.
|
|
73
|
+
- `--picks LABEL=DIR`: model picks recorded as responses, one file per pick, `<case>.<variant>.json` (the spike's
|
|
74
|
+
`out/<model>/` files, or what `--live` saves). The variant is the row's policy, the label its model.
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
ruby script/llm_context_bench.rb "$SESSIONS" --strategy none,forget_all,stale --per-case
|
|
78
|
+
ruby script/llm_context_bench.rb "$SESSIONS" --strategy none,stale,stale_edits_turn_end,stale_edits_payoff --ends any
|
|
79
|
+
ruby script/llm_context_bench.rb "$SESSIONS" --picks deepseek=picks/deepseek --picks splash=picks/splash --json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Live picks (the D7 tool-name A/B)
|
|
83
|
+
|
|
84
|
+
The one part that calls a model, and only with `--live MODEL`: at each case it asks the model which of its tool
|
|
85
|
+
outputs to forget, through chi's own chat client (the model must be on an `api: openai` host), and saves each
|
|
86
|
+
answer under `--out DIR` for `--picks` to score. A saved pick is never asked for again. The request is the
|
|
87
|
+
session's conversation up to the turn's end (thinking dropped, every tool result led by its id `[#tN]`), chi's
|
|
88
|
+
built-in chat tools plus the forget tool, and the policy's tail line; a model that doesn't call the tool is asked
|
|
89
|
+
once more with it forced (`--no-force` asks once).
|
|
90
|
+
|
|
91
|
+
A pick record holds the answer (`unforced`), the forced one when it took that (`forced`) and why it was forced
|
|
92
|
+
(`forced_because`: `no_call`, or `length` when the unforced reply was cut off at `max_tokens`, finish reason
|
|
93
|
+
`length`, before it could call anything), how it was asked (`bench`), and `cost`: what the server reported for the
|
|
94
|
+
pick's requests, summed (OpenRouter's `usage.cost`, asked for with `usage.include`; absent when the server sends
|
|
95
|
+
none). The run's last line counts the picks written, the forced ones, those forced after a cut, the errors, and sums
|
|
96
|
+
the cost.
|
|
97
|
+
|
|
98
|
+
A request that fails is saved as an error record and the run goes on to the next case, except a payment error: a
|
|
99
|
+
402, or an error that mentions credits or balance, stops the run at once (exit 1), saves nothing for that case, and
|
|
100
|
+
says how many picks it wrote before it. A rerun with the same `--out` asks only for the rest.
|
|
101
|
+
|
|
102
|
+
- `--tool-name forget_outputs|forget_llm_context`: the name under test; the description stays the same.
|
|
103
|
+
- `--policy subtask|soft|now`: `subtask` offers the tool and puts the plan's policy line in its description;
|
|
104
|
+
`soft` and `now` are the spike's selective and "free context now" lines.
|
|
105
|
+
- `--layout tail_system|tail_user|boundary`: where the tail line goes. `tail_system` (the default) is a system
|
|
106
|
+
message after the case's last entry; `tail_user` the same line as a user message; `boundary` a system message
|
|
107
|
+
worded as a stopping point. A layout other than the default names its picks `<policy>_<layout>`.
|
|
108
|
+
- `--samples N`, and `--dry-run` to count the requests and prompt tokens (and the cases' endings) without calling
|
|
109
|
+
anything.
|
|
110
|
+
|
|
111
|
+
Case selection matters here: at a turn that ends with the model's answer the forget offer reads as a real turn end,
|
|
112
|
+
while mid-task (a `tool_result` or `turn_note` end) a model mostly carries on with the task, or, worded as a
|
|
113
|
+
stopping point, forgets at the base rate. A forget-at-turn-end test should run on `answer` cases, the default for
|
|
114
|
+
the turn-size rule; give `--ends answer` with a `--cases` file to hold one to that too.
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
ruby script/llm_context_bench.rb "$SESSIONS" --cases cases.txt --live openrouter:deepseek/deepseek-v4.1-flash \
|
|
118
|
+
--tool-name forget_llm_context --policy subtask --out picks/deepseek --dry-run
|
|
119
|
+
```
|
|
@@ -110,10 +110,11 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
110
110
|
## Reading the numbers
|
|
111
111
|
|
|
112
112
|
- The debug log's `generation_completed` line: `prompt=` (the request's prompt tokens), `cached=` (read from the
|
|
113
|
-
server's cache)
|
|
113
|
+
server's cache), `cache_write=` (written to it, where the provider reports writes) and `reprefill=` (of the last
|
|
114
|
+
request's prompt on the same model, what was prefilled again). The `http` line of a
|
|
114
115
|
remote Claude request shows `cache=on` (or `cache=1h`) when it carried breakpoints.
|
|
115
116
|
- The warm-up logs `model warmup` with `cached=`, `prefilled=` and `prefill_ms=`.
|
|
116
|
-
- `/stats`: "cached N (P%)"
|
|
117
|
+
- `/stats`: "cached N (P%)", "cache writes" and "re-prefilled" for the session.
|
|
117
118
|
- A healthy session: from the second request on `cached=` is close to `prompt=`; a second fresh session in the
|
|
118
119
|
same project reads everything up to the volatile tail.
|
|
119
120
|
|
data/docs/plugins.md
CHANGED
|
@@ -848,8 +848,9 @@ bundles:
|
|
|
848
848
|
`(failed: …)`, a first-run server still starting `(starting, try
|
|
849
849
|
again)`. Its description names each server on one line, never its
|
|
850
850
|
tools: the server's `description:`, else the first sentence of its
|
|
851
|
-
`instructions`,
|
|
852
|
-
|
|
851
|
+
`instructions`, and how many tools it has (`- chrome (26 tools)` with
|
|
852
|
+
neither: tool names there get called blind, without a search; a first
|
|
853
|
+
run's server is just its name until the next session).
|
|
853
854
|
- `mcp_call(tool, args)` calls one: `tool` is `<server>/<tool>`, and
|
|
854
855
|
`mcp_<server>_<tool>` works too (looked up, not parsed). Two tools whose
|
|
855
856
|
`mcp_<server>_<tool>` comes out the same (servers `git-hub` and
|
data/docs/testing.md
CHANGED
|
@@ -63,3 +63,10 @@ a model that follows tool instructions: a small model may fail some of them.
|
|
|
63
63
|
`spec/integration/omlx_spec.rb` tests oMLX's model-id resolution and runs only with
|
|
64
64
|
`SAMAGOTCHI_INTEGRATION_TRANSPORT=omlx`, against an oMLX server (a short model selector in
|
|
65
65
|
`SAMAGOTCHI_INTEGRATION_MODEL`).
|
|
66
|
+
|
|
67
|
+
## The llm_context replay benchmark
|
|
68
|
+
|
|
69
|
+
`ruby script/llm_context_bench.rb SESSIONS_DIR` replays stored sessions offline and scores the LLM context
|
|
70
|
+
strategies (freed tokens, wrongly forgotten outputs, one-step re-reads, re-prefilled tokens). It is opt-in: the suite
|
|
71
|
+
runs its code only on small synthetic sessions, never on yours. See
|
|
72
|
+
[internals/llm-context-bench.md](internals/llm-context-bench.md).
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp
|
|
3
|
-
version: 0.6.
|
|
3
|
+
version: 0.6.1
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session; the model searches their tools with find_mcp_tools and calls one with mcp_call (<server>/<tool>); /mcp lists them"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
files: {}
|
|
8
8
|
plugin:
|
|
9
9
|
file: plugin.rb
|
|
10
|
-
sha256: sha256:
|
|
10
|
+
sha256: sha256:7778b1a8f82a043c06c0289a191bc2847c844665de0c961edb493f787f435ad5
|
|
11
11
|
# 0.37.0 (published 2026-10-07) is the release whose targets: take acts_as:, args: and label:.
|
|
12
12
|
requires_chi: ">= 0.37.0"
|
|
@@ -59,8 +59,6 @@ class Plugin
|
|
|
59
59
|
# A query word in a tool's name (or its server's) counts this many times
|
|
60
60
|
# one in its description.
|
|
61
61
|
NAME_WEIGHT = 3
|
|
62
|
-
# Tool names that stand for a server's summary when it has none.
|
|
63
|
-
SAMPLE_NAMES = 6
|
|
64
62
|
# A cached tool list older than this is refreshed in the background.
|
|
65
63
|
CACHE_TTL = 24 * 60 * 60
|
|
66
64
|
# Restarts of a server that exited, per session.
|
|
@@ -689,16 +687,14 @@ class Plugin
|
|
|
689
687
|
end
|
|
690
688
|
|
|
691
689
|
# The server's line: its description: from config, else the first
|
|
692
|
-
# sentence of its instructions, else its
|
|
690
|
+
# sentence of its instructions, else nothing (its line has the tool
|
|
691
|
+
# count). Never its tool names: a model that knows them calls them
|
|
692
|
+
# blind, without a search (a smoke run's Splash did).
|
|
693
693
|
def server_summary(server)
|
|
694
694
|
given = server.config["description"].to_s.strip
|
|
695
695
|
return given unless given.empty?
|
|
696
|
-
return server.info if server.info
|
|
697
696
|
|
|
698
|
-
|
|
699
|
-
return nil if names.empty?
|
|
700
|
-
|
|
701
|
-
"tools #{names.take(SAMPLE_NAMES).join(", ")}#{", …" if names.size > SAMPLE_NAMES}"
|
|
697
|
+
server.info
|
|
702
698
|
end
|
|
703
699
|
|
|
704
700
|
# ── Search ────────────────────────────────────────────────────────────
|
|
@@ -808,7 +804,8 @@ class Plugin
|
|
|
808
804
|
# The McpTool +name+ names: <server>/<tool>, or mcp_<server>_<tool>
|
|
809
805
|
# looked up by chi_name (never parsed: server names may hold _, and long
|
|
810
806
|
# names are cut). A miss while a server is starting waits for it (its
|
|
811
|
-
# tools may be the one
|
|
807
|
+
# tools may be the one; for a <server>/ name, that server's start only);
|
|
808
|
+
# then a miss is the error text: a failed
|
|
812
809
|
# server's error, else the closest tools.
|
|
813
810
|
# @return [McpTool, String]
|
|
814
811
|
def lookup(name, waited: false)
|
|
@@ -828,7 +825,10 @@ class Plugin
|
|
|
828
825
|
"call one as <server>/<tool>"
|
|
829
826
|
end
|
|
830
827
|
|
|
831
|
-
|
|
828
|
+
# A name with a configured server's <server>/ waits for that server
|
|
829
|
+
# only; any other name (mcp_<server>_<tool>) for every one starting.
|
|
830
|
+
named = @servers.select { |server| name.start_with?("#{server.name}/") }
|
|
831
|
+
starting = (named.empty? ? @servers : named).select { |server| server.state == :starting }
|
|
832
832
|
unless waited || starting.empty?
|
|
833
833
|
starting.each { |server| wait_started(server) }
|
|
834
834
|
return lookup(name, waited: true)
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "config_text_edit"
|
|
|
9
9
|
require_relative "log"
|
|
10
10
|
require_relative "model_ref"
|
|
11
11
|
require_relative "paths"
|
|
12
|
+
require_relative "llm_context_strategy"
|
|
12
13
|
|
|
13
14
|
module Samagotchi
|
|
14
15
|
# Parses the thinking: levels of host and model entries (it needs Config).
|
|
@@ -154,6 +155,18 @@ module Samagotchi
|
|
|
154
155
|
# 10 minutes, within session.max_wakes); false: it waits as a note for the next turn.
|
|
155
156
|
Entry.new(key: "context.wake", yaml_path: %w[context wake], type: :bool, default: true, expose: %i[env config cli]),
|
|
156
157
|
|
|
158
|
+
# What the model is sent of the conversation (LLMContextStrategy, LLMContextView): none, or a list of layers
|
|
159
|
+
# (stale, forget; "|"-separated or a YAML list). none and stale are built: a list with forget warns and is none.
|
|
160
|
+
# models.<key>.llm_context_strategy and hosts.<name>.llm_context_strategy come first.
|
|
161
|
+
Entry.new(key: "llm_context.strategy", yaml_path: %w[llm_context strategy], type: :string, default: "none", expose: %i[env config]),
|
|
162
|
+
# When a layer's edits reach the prompt (LLMContextApply): payoff, next_request or turn_end;
|
|
163
|
+
# models.<key>.llm_context_apply and hosts.<name>.llm_context_apply come first.
|
|
164
|
+
Entry.new(key: "llm_context.apply", yaml_path: %w[llm_context apply], type: :string, default: "payoff", expose: %i[env config]),
|
|
165
|
+
# Whether an edit or write of a file makes its earlier reads stale too (opt-in, experimental).
|
|
166
|
+
Entry.new(key: "llm_context.stale_edits", yaml_path: %w[llm_context stale_edits], type: :bool, default: false, expose: %i[env config]),
|
|
167
|
+
# The steps (tool batches) back whose edited files' reads stale never stubs.
|
|
168
|
+
Entry.new(key: "llm_context.protect_steps", yaml_path: %w[llm_context protect_steps], type: :integer, default: 3, expose: %i[env config]),
|
|
169
|
+
|
|
157
170
|
Entry.new(key: "thinking.turn_preamble", yaml_path: %w[thinking turn_preamble], type: :bool, default: true, expose: %i[env config cli]),
|
|
158
171
|
# How much models think: off|low|medium|high|default, parsed by Thinking
|
|
159
172
|
# (a :string, since YAML reads an unquoted off as false). The CLI flag
|
|
@@ -241,8 +254,8 @@ module Samagotchi
|
|
|
241
254
|
# (ConfigFile.hosts_config, ConfigFile.model_settings).
|
|
242
255
|
MAP_ENTRY_KEYS = {
|
|
243
256
|
"hosts" => %w[host port url transport api api_key_env profile first_token_timeout vision sampling thinking enabled
|
|
244
|
-
remote window_tokens].freeze,
|
|
245
|
-
"models" => %w[profile vision sampling thinking window_tokens].freeze
|
|
257
|
+
remote window_tokens llm_context_strategy llm_context_apply].freeze,
|
|
258
|
+
"models" => %w[profile vision sampling thinking window_tokens llm_context_strategy llm_context_apply].freeze
|
|
246
259
|
}.freeze
|
|
247
260
|
# Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
|
|
248
261
|
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze }.freeze
|
|
@@ -826,6 +839,10 @@ module Samagotchi
|
|
|
826
839
|
"hosts entry '#{name}'")
|
|
827
840
|
sampling = ConfigFile.sampling_map(raw_cfg.key?("sampling") ? raw_cfg["sampling"] : raw_cfg[:sampling], "hosts entry '#{name}'")
|
|
828
841
|
thinking = Thinking.level(raw_cfg.key?("thinking") ? raw_cfg["thinking"] : raw_cfg[:thinking], "hosts entry '#{name}'")
|
|
842
|
+
llm_context = LLMContextStrategy.parse(raw_cfg.key?(LLMContextStrategy::KEY) ? raw_cfg[LLMContextStrategy::KEY] : raw_cfg[:llm_context_strategy],
|
|
843
|
+
"hosts entry '#{name}'")
|
|
844
|
+
llm_context_apply = LLMContextStrategy.parse_apply(raw_cfg.key?(LLMContextStrategy::APPLY_KEY) ? raw_cfg[LLMContextStrategy::APPLY_KEY] : raw_cfg[:llm_context_apply],
|
|
845
|
+
"hosts entry '#{name}'")
|
|
829
846
|
unless first_token_timeout.nil? || (first_token_timeout.is_a?(Numeric) && !first_token_timeout.negative?)
|
|
830
847
|
warn_once "Warning: hosts entry '#{name}': first_token_timeout must be seconds (0 = off); using the default"
|
|
831
848
|
first_token_timeout = nil
|
|
@@ -890,7 +907,8 @@ module Samagotchi
|
|
|
890
907
|
url: url.empty? ? nil : url, api_key_env: api_key_env.empty? ? nil : api_key_env,
|
|
891
908
|
profile: profile.empty? ? nil : profile, first_token_timeout: first_token_timeout,
|
|
892
909
|
vision: vision, sampling: sampling, thinking: thinking, remote: remote,
|
|
893
|
-
window_tokens: window
|
|
910
|
+
window_tokens: window, llm_context_strategy: llm_context,
|
|
911
|
+
llm_context_apply: llm_context_apply }
|
|
894
912
|
end
|
|
895
913
|
end
|
|
896
914
|
|
|
@@ -1037,7 +1055,9 @@ module Samagotchi
|
|
|
1037
1055
|
location.merge("transport" => v[:transport]&.to_s, "api" => v[:api]&.to_s, "api_key_env" => v[:api_key_env],
|
|
1038
1056
|
"profile" => v[:profile], "first_token_timeout" => v[:first_token_timeout],
|
|
1039
1057
|
"vision" => v[:vision], "sampling" => v[:sampling], "thinking" => v[:thinking]&.to_s,
|
|
1040
|
-
"remote" => v[:remote], "window_tokens" => v[:window_tokens]
|
|
1058
|
+
"remote" => v[:remote], "window_tokens" => v[:window_tokens],
|
|
1059
|
+
"llm_context_strategy" => v[:llm_context_strategy]&.map(&:to_s),
|
|
1060
|
+
"llm_context_apply" => v[:llm_context_apply]&.to_s).compact
|
|
1041
1061
|
end
|
|
1042
1062
|
# Disabled hosts travel as just that, so a worker refuses "box:x"
|
|
1043
1063
|
# the way its parent does instead of sending it to the default host.
|
|
@@ -1129,6 +1149,12 @@ module Samagotchi
|
|
|
1129
1149
|
result[key][:thinking] = thinking if thinking
|
|
1130
1150
|
window = window_tokens(v.key?("window_tokens") ? v["window_tokens"] : v[:window_tokens], "models: #{key}")
|
|
1131
1151
|
result[key][:window_tokens] = window if window
|
|
1152
|
+
layers = LLMContextStrategy.parse(v.key?(LLMContextStrategy::KEY) ? v[LLMContextStrategy::KEY] : v[:llm_context_strategy],
|
|
1153
|
+
"models: #{key}")
|
|
1154
|
+
result[key][:llm_context_strategy] = layers if layers
|
|
1155
|
+
apply = LLMContextStrategy.parse_apply(v.key?(LLMContextStrategy::APPLY_KEY) ? v[LLMContextStrategy::APPLY_KEY] : v[:llm_context_apply],
|
|
1156
|
+
"models: #{key}")
|
|
1157
|
+
result[key][:llm_context_apply] = apply if apply
|
|
1132
1158
|
end
|
|
1133
1159
|
rescue StandardError
|
|
1134
1160
|
{}
|
|
@@ -64,9 +64,13 @@ module Samagotchi
|
|
|
64
64
|
# (a note that arrived before the first turn, a turn note left by a
|
|
65
65
|
# failed first turn) stays and the prompt goes before it.
|
|
66
66
|
def with_system_head(messages, system_message)
|
|
67
|
-
first
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
[system_message] + (system_head?(messages.first) ? messages.drop(1) : messages)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A system prompt at a conversation's head: a system message of no kind
|
|
71
|
+
# (#with_system_head replaces it).
|
|
72
|
+
def system_head?(message)
|
|
73
|
+
message.is_a?(Hash) && fetch(message, :role).to_s == "system" && fetch(message, :kind).to_s.empty?
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
def clock(created_at)
|
|
@@ -85,6 +85,7 @@ module Samagotchi
|
|
|
85
85
|
@last_bucket = self.class.last_bucket(conversation)
|
|
86
86
|
@server_usage = nil
|
|
87
87
|
@counted = nil
|
|
88
|
+
@edited = false
|
|
88
89
|
@display = nil
|
|
89
90
|
@guidance = nil
|
|
90
91
|
end
|
|
@@ -95,6 +96,12 @@ module Samagotchi
|
|
|
95
96
|
|
|
96
97
|
def enabled? = @enabled
|
|
97
98
|
|
|
99
|
+
# The status line's value is in the top configured bucket (the apply
|
|
100
|
+
# rule's payoff applies staged edits there at once).
|
|
101
|
+
def top_bucket?
|
|
102
|
+
!@display.nil? && bucket_rank(@display[:bucket]) == @thresholds.size
|
|
103
|
+
end
|
|
104
|
+
|
|
98
105
|
# Before a request: estimate the usage of a prompt of +prompt_chars+
|
|
99
106
|
# against +window+ (this request's ContextWindow::Resolved) and update
|
|
100
107
|
# the status line's value. A rise into a bucket that asks the model for
|
|
@@ -120,6 +127,14 @@ module Samagotchi
|
|
|
120
127
|
{ status: status_message(usage: usage, bucket: bucket), usage: usage, bucket: bucket, source: usage[:source] }
|
|
121
128
|
end
|
|
122
129
|
|
|
130
|
+
# An LLM context edit was applied (LLMContextApply): the prompt is
|
|
131
|
+
# shorter than the one the server's count was for. Until the next
|
|
132
|
+
# count the estimate takes what the prompt lost off that count too,
|
|
133
|
+
# instead of holding it (a shorter prompt counts as trimmed otherwise).
|
|
134
|
+
def edited!
|
|
135
|
+
@edited = true
|
|
136
|
+
end
|
|
137
|
+
|
|
123
138
|
# @return [Hash, nil] the model's line from the last #observe (once)
|
|
124
139
|
def take_guidance
|
|
125
140
|
line = @guidance
|
|
@@ -145,6 +160,7 @@ module Samagotchi
|
|
|
145
160
|
return unless usage
|
|
146
161
|
|
|
147
162
|
@counted = { chars: prompt_chars, image_tokens: image_tokens }
|
|
163
|
+
@edited = false
|
|
148
164
|
value = display_for(used_tokens: usage[:total_tokens], window_tokens: usage[:context_window_tokens] || window&.tokens)
|
|
149
165
|
@display = value if value
|
|
150
166
|
end
|
|
@@ -172,7 +188,7 @@ module Samagotchi
|
|
|
172
188
|
|
|
173
189
|
if server_usage && server_usage[:prompt_tokens]
|
|
174
190
|
window_tokens = server_usage[:context_window_tokens] || window.tokens
|
|
175
|
-
used_tokens = server_usage[:prompt_tokens] + appended_tokens(prompt_chars, image_tokens, counted)
|
|
191
|
+
used_tokens = [server_usage[:prompt_tokens] + appended_tokens(prompt_chars, image_tokens, counted), 0].max
|
|
176
192
|
source = "server"
|
|
177
193
|
else
|
|
178
194
|
window_tokens = window.tokens
|
|
@@ -192,11 +208,13 @@ module Samagotchi
|
|
|
192
208
|
|
|
193
209
|
private
|
|
194
210
|
|
|
195
|
-
# The estimate for what the prompt added since the +counted+ one
|
|
211
|
+
# The estimate for what the prompt added since the +counted+ one;
|
|
212
|
+
# after an edit (#edited!) what it lost too, as a negative.
|
|
196
213
|
def appended_tokens(prompt_chars, image_tokens, counted)
|
|
197
214
|
return 0 unless counted
|
|
198
215
|
|
|
199
216
|
chars = prompt_chars - counted[:chars]
|
|
217
|
+
return -(-chars / @chars_per_token).ceil if @edited && chars.negative?
|
|
200
218
|
return 0 unless chars.positive?
|
|
201
219
|
|
|
202
220
|
(chars / @chars_per_token).ceil + [image_tokens - counted[:image_tokens].to_i, 0].max
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -1727,7 +1727,7 @@ module Samagotchi
|
|
|
1727
1727
|
@turn_thinking = [thinking_level(thinking_target), thinking_target]
|
|
1728
1728
|
announce_thinking_level(*@turn_thinking)
|
|
1729
1729
|
LLM::TurnSettings.new(vision: vision, sampling: sampling, thinking: @turn_thinking.first, model_name: nil,
|
|
1730
|
-
window_setting: nil)
|
|
1730
|
+
window_setting: nil, llm_context: nil)
|
|
1731
1731
|
end
|
|
1732
1732
|
private :turn_settings
|
|
1733
1733
|
|
|
@@ -1754,11 +1754,14 @@ module Samagotchi
|
|
|
1754
1754
|
return if reminders_due? || @turn_state.carried_steers? || @next_turn_waiting&.call
|
|
1755
1755
|
|
|
1756
1756
|
messages = ContextNote.with_system_head(turn.session.messages.dup, { role: "system", content: system_prompt(target) })
|
|
1757
|
-
|
|
1757
|
+
# Under the next turn's strategy (a /model switch may have changed it);
|
|
1758
|
+
# the last turn's when it can't be resolved.
|
|
1759
|
+
prompt, images = @kernel.warmup_prompt(messages, llm_context: turn_llm_context || @kernel.turn_settings&.llm_context)
|
|
1758
1760
|
return unless prompt
|
|
1759
1761
|
|
|
1760
|
-
@prompt_warmup.start(client: client, prompt: prompt, model: @kernel.turn_settings.model_name,
|
|
1761
|
-
|
|
1762
|
+
started = @prompt_warmup.start(client: client, prompt: prompt, model: @kernel.turn_settings.model_name,
|
|
1763
|
+
slot: @kernel.last_slot, images: images)
|
|
1764
|
+
@kernel.warmed_up! if started
|
|
1762
1765
|
rescue StandardError => e
|
|
1763
1766
|
Log.warn(:model, "warmup_skipped", error: e.class.name, msg: e.message.to_s[0, 200])
|
|
1764
1767
|
end
|
|
@@ -1816,7 +1819,8 @@ module Samagotchi
|
|
|
1816
1819
|
# The turn's settings, set once here. The chat loop dispatches tools
|
|
1817
1820
|
# through the kernel without its #run: tag those dumps with this
|
|
1818
1821
|
# turn's model, not the last native one.
|
|
1819
|
-
@kernel.turn_settings = turn.settings.with(model_name: bare_for_backend, window_setting: turn_window_setting
|
|
1822
|
+
@kernel.turn_settings = turn.settings.with(model_name: bare_for_backend, window_setting: turn_window_setting,
|
|
1823
|
+
llm_context: turn_llm_context)
|
|
1820
1824
|
|
|
1821
1825
|
turn.limit = @no_interrupt ? IterationLimit.for(no_interrupt: true) : max_iterations || IterationLimit.for
|
|
1822
1826
|
backend.complete(
|
|
@@ -2512,6 +2516,16 @@ module Samagotchi
|
|
|
2512
2516
|
nil
|
|
2513
2517
|
end
|
|
2514
2518
|
|
|
2519
|
+
# The effective model's LLM context strategy (models:/hosts:
|
|
2520
|
+
# llm_context_strategy, else llm_context.strategy), read per turn as
|
|
2521
|
+
# the window is.
|
|
2522
|
+
def turn_llm_context
|
|
2523
|
+
target = @host_registry.resolve(@effective_model_name)
|
|
2524
|
+
LLMContextStrategy.resolve(target, names: model_lookup_names(target))
|
|
2525
|
+
rescue StandardError
|
|
2526
|
+
nil
|
|
2527
|
+
end
|
|
2528
|
+
|
|
2515
2529
|
# See #served_model. A report for another name (before a /model switch)
|
|
2516
2530
|
# doesn't count. Without a target, no probe. A remote chat host has no
|
|
2517
2531
|
# /props: nil until a turn.
|
|
@@ -149,11 +149,23 @@ module Samagotchi
|
|
|
149
149
|
end
|
|
150
150
|
return name unless whose
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
if first_drop?(registry, entry.name, name)
|
|
153
|
+
Log.warn(:plugins, "plugin_acts_as_dropped", tool: entry.name, acts_as: name,
|
|
154
|
+
msg: "#{entry.name} targets: acts_as #{name} dropped: it is #{whose}")
|
|
155
|
+
end
|
|
154
156
|
nil
|
|
155
157
|
end
|
|
156
158
|
|
|
159
|
+
# The drops logged, per registry (a session's): each tool's acts_as
|
|
160
|
+
# once, not at every call (targets run several times a call).
|
|
161
|
+
DROPS_LOGGED = ObjectSpace::WeakKeyMap.new
|
|
162
|
+
DROPS_LOCK = Mutex.new
|
|
163
|
+
|
|
164
|
+
def self.first_drop?(registry, tool, acts_as)
|
|
165
|
+
DROPS_LOCK.synchronize { !(DROPS_LOGGED[registry] ||= Set.new).add?([tool, acts_as]).nil? }
|
|
166
|
+
end
|
|
167
|
+
private_class_method :first_drop?
|
|
168
|
+
|
|
157
169
|
# The longest label: the question takes from targets:.
|
|
158
170
|
LABEL_CHARS = 80
|
|
159
171
|
|
|
@@ -36,10 +36,12 @@ module Samagotchi
|
|
|
36
36
|
# sampling: the configured request parameters (SamplingSettings), nil when unset;
|
|
37
37
|
# thinking: the configured level (Thinking), nil when unset;
|
|
38
38
|
# remote: the configured true/false (#remote?), nil when unset;
|
|
39
|
-
# window_tokens: the configured context window (ContextWindow.setting), nil when unset
|
|
39
|
+
# window_tokens: the configured context window (ContextWindow.setting), nil when unset;
|
|
40
|
+
# llm_context_strategy: the configured layers (LLMContextStrategy), nil when unset;
|
|
41
|
+
# llm_context_apply: the configured apply rule (LLMContextStrategy), nil when unset.
|
|
40
42
|
HostEntry = Struct.new(:name, :host, :port, :transport, :client, :api, :scheme, :url, :api_key_env, :profile,
|
|
41
43
|
:first_token_timeout, :vision, :sampling, :thinking, :remote, :window_tokens,
|
|
42
|
-
keyword_init: true) do
|
|
44
|
+
:llm_context_strategy, :llm_context_apply, keyword_init: true) do
|
|
43
45
|
# Talks the OpenAI chat API (the chat loop); nil and raw apis use the
|
|
44
46
|
# raw-prompt loop.
|
|
45
47
|
def chat? = api == :openai
|
|
@@ -99,7 +101,8 @@ module Samagotchi
|
|
|
99
101
|
api: cfg[:api]&.to_sym, scheme: cfg[:scheme], url: cfg[:url], api_key_env: cfg[:api_key_env],
|
|
100
102
|
profile: cfg[:profile], first_token_timeout: cfg[:first_token_timeout],
|
|
101
103
|
vision: cfg[:vision], sampling: cfg[:sampling], thinking: cfg[:thinking],
|
|
102
|
-
remote: cfg[:remote], window_tokens: cfg[:window_tokens]
|
|
104
|
+
remote: cfg[:remote], window_tokens: cfg[:window_tokens],
|
|
105
|
+
llm_context_strategy: cfg[:llm_context_strategy], llm_context_apply: cfg[:llm_context_apply])
|
|
103
106
|
entry.client = Client.new(host: cfg[:host], port: cfg[:port], transport: transport, scheme: cfg[:scheme],
|
|
104
107
|
first_token_timeout: entry.first_token_limit, name: entry.name,
|
|
105
108
|
api_key_env: entry.api_key_env, env: env)
|