samagotchi 0.39.0 → 0.40.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 +31 -1
- data/docs/configuration.md +51 -3
- data/docs/hooks.md +1 -0
- data/docs/internals/context-telemetry.md +16 -0
- data/docs/internals/llm-context-bench.md +18 -4
- data/docs/internals/llm-context-forget.md +45 -0
- data/docs/plugins.md +1 -1
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +4 -0
- data/lib/samagotchi/config.rb +18 -5
- data/lib/samagotchi/context_status.rb +92 -9
- data/lib/samagotchi/engine.rb +17 -4
- data/lib/samagotchi/guardrails/targets.rb +1 -1
- data/lib/samagotchi/host_registry.rb +5 -3
- data/lib/samagotchi/kernel_loop.rb +33 -3
- data/lib/samagotchi/llm/chat_loop.rb +18 -4
- data/lib/samagotchi/llm_context_apply.rb +17 -5
- data/lib/samagotchi/llm_context_edit.rb +35 -4
- data/lib/samagotchi/llm_context_forget.rb +258 -0
- data/lib/samagotchi/llm_context_stale.rb +23 -7
- data/lib/samagotchi/llm_context_strategy.rb +54 -10
- data/lib/samagotchi/llm_context_view.rb +88 -11
- data/lib/samagotchi/system_prompt.rb +20 -15
- data/lib/samagotchi/tool_activity.rb +18 -0
- data/lib/samagotchi/tool_declarations.rb +60 -4
- data/lib/samagotchi/tools/builtin_calls.rb +3 -2
- data/lib/samagotchi/tools/builtins.rb +15 -5
- data/lib/samagotchi/tools/forget_outputs.rb +62 -0
- data/lib/samagotchi/tools/registry.rb +25 -8
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c41a6466e51c4da290f55cca9398a3221def773f8ee3235102e97322030c882
|
|
4
|
+
data.tar.gz: 86bc79ce21478e0420e82fc47307fdf9934bee4a2642cda3c8bbf1c26a248c7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52bf3393d2a7ff228aba277aa7fea67b4f82619578ba25d28c061e2256c8ef9a36698a250ca928c1916a3ba7a19ffef56fd8eeda7c232d37661fcd1188d63895
|
|
7
|
+
data.tar.gz: 40d4c0006a4ac0a9d07072f09d691169d7c8d765eed4be45ff2c0ff5acdb04d5dc2ff20e009429abbdd54822d4a91e212f8efbbca75b43f0671382a1fcf16768
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,35 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.40.0] - 2026-10-08
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Experimental: the `forget` layer of `llm_context.strategy` (`[stale, forget]`, per model or host as ever). That
|
|
16
|
+
model gets a tool, `forget_outputs`, to forget its own tool outputs: each output shows an id (`[#t41]`), and a
|
|
17
|
+
forget replaces the outputs named with a stub holding the model's note (required; the session keeps the outputs).
|
|
18
|
+
`keep` keeps lines of an output, `restore` brings a non-read one back; outputs of the last `protect_steps` steps,
|
|
19
|
+
and reads of files edited in them unless lines are kept, are refused. The stubs reach the prompt under
|
|
20
|
+
`llm_context.apply`. `llm_context.policy` is the sentence the tool's description carries. Off, the tool list and the
|
|
21
|
+
prompt are what they were. See [docs/configuration.md](docs/configuration.md) ("LLM context: the forget layer").
|
|
22
|
+
- `llm_context.budget_tokens` (and per model or host `llm_context_budget_tokens`), off by default: a soft context
|
|
23
|
+
budget the `[CONTEXT: …]` lines count against instead of the window. Under `forget` those lines show
|
|
24
|
+
`~N/M tokens in use` and offer the tool in tiers (a readout, then "tidy once", then "compact settled outputs now"),
|
|
25
|
+
at a turn's start, mid-turn only in the top tier.
|
|
26
|
+
- The replay bench's `forget_outputs` strategy asks a model at chi's forget offer and scores its picks, call rate and
|
|
27
|
+
notes (`--forget-model`, `--out`, `--budget`, `--max-cost`).
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Web: session cards no longer show the "mem N" chip, leaving room for the delegate chip; the card's tooltip and the
|
|
32
|
+
open session's info bar still list the memories used.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Web: a session card's hover tooltip (id, update time, status, memories) was empty since 0.35.0.
|
|
37
|
+
|
|
38
|
+
Update with `chi update`: it updates loop-guard (0.3.6: ignores `forget_outputs`).
|
|
39
|
+
|
|
11
40
|
## [0.39.0] - 2026-10-07
|
|
12
41
|
|
|
13
42
|
### Added
|
|
@@ -1874,7 +1903,8 @@ and long-lived sessions.
|
|
|
1874
1903
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
1875
1904
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
1876
1905
|
|
|
1877
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
1906
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.40.0...HEAD
|
|
1907
|
+
[0.40.0]: https://github.com/dm1try/samagotchi/compare/v0.39.0...v0.40.0
|
|
1878
1908
|
[0.39.0]: https://github.com/dm1try/samagotchi/compare/v0.38.0...v0.39.0
|
|
1879
1909
|
[0.38.0]: https://github.com/dm1try/samagotchi/compare/v0.37.0...v0.38.0
|
|
1880
1910
|
[0.37.0]: https://github.com/dm1try/samagotchi/compare/v0.36.0...v0.37.0
|
data/docs/configuration.md
CHANGED
|
@@ -855,6 +855,51 @@ file is read.
|
|
|
855
855
|
To skip loading AGENT.md, set `skip_agent_md: true` at the top level of
|
|
856
856
|
`config.yml` (env `SAMAGOTCHI_SKIP_AGENT_MD=true`).
|
|
857
857
|
|
|
858
|
+
## LLM context: the forget layer
|
|
859
|
+
|
|
860
|
+
Experimental, off by default. With `forget` in a model's `llm_context.strategy` (next to `stale`), chi offers that
|
|
861
|
+
model a tool, `forget_outputs`, to free context by forgetting its own tool outputs. Nothing changes for a model
|
|
862
|
+
without it: no ids, no tool, the same prompt.
|
|
863
|
+
|
|
864
|
+
```yaml
|
|
865
|
+
llm_context:
|
|
866
|
+
budget_tokens: 64000 # optional: the [CONTEXT: …] lines count against 64k, not the window
|
|
867
|
+
models:
|
|
868
|
+
deepseek-v4.1-flash:
|
|
869
|
+
llm_context_strategy: [stale, forget]
|
|
870
|
+
llm_context_apply: next_request
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
- **Ids.** Every tool output the model sees starts with its id after the tool's name, `[read]` then `[#t41] …`.
|
|
874
|
+
Outputs a session saved before chi gave them ids show none and can't be forgotten.
|
|
875
|
+
- **Forgetting.** `forget_outputs(ids: ["t41", "t42"], note: "…")` replaces each output with a stub holding the note
|
|
876
|
+
(`[read] [#t41] (forgotten) …`); the call stays, and the session keeps the outputs. The note is required. Outputs
|
|
877
|
+
one call forgot that follow one another carry the note once: the rest say `(forgotten with t41: see its note)`.
|
|
878
|
+
`keep: ["t42:12-40"]` forgets t42 except those lines, which follow its stub: a read's lines are the file's line
|
|
879
|
+
numbers (a read from `start_line` 120 starts at 120), other outputs count from 1.
|
|
880
|
+
- **Refused, per id, with the reason in the result:** an output from the last `llm_context.protect_steps` steps
|
|
881
|
+
before the forget's own (1: the output the model just got); a read of a file edited or written in them, unless the
|
|
882
|
+
forget keeps lines of it; a keep outside the output's lines, of a big file's head/tail preview, or holding all of
|
|
883
|
+
it; an output too small for a stub to free anything; one already stubbed or forgotten; an unknown id, or one whose
|
|
884
|
+
output chi can't pair with its call for sure (it shows no id). The system prompt, user messages, steers and context
|
|
885
|
+
notes have no ids.
|
|
886
|
+
- **When stubs reach the prompt** follows `llm_context.apply`, as `stale`'s do: the result says
|
|
887
|
+
`applied, the next request sends the stubs (frees …)` or `staged until the turn ends`.
|
|
888
|
+
- **Restore.** `forget_outputs(restore: ["t45"])` brings a forgotten output back with the next request, whatever
|
|
889
|
+
`llm_context.apply` says (the model wants the text now; the result says what the server reads again); a non-read
|
|
890
|
+
stub says `[restore: t45]`. A read whose stub was sent isn't restored (read the file again: it may have changed); one
|
|
891
|
+
still staged is.
|
|
892
|
+
- **The description** carries the note contract (facts carried forward, quoted, marked VERIFIED or UNVERIFIED; what
|
|
893
|
+
was ruled out, with the reason and "do not retry"; the commands tried; a NEXT line), the cost of an edit (the
|
|
894
|
+
server reads everything after it again: forget in one batch, older outputs first) and `llm_context.policy`.
|
|
895
|
+
- **Offers.** Under `forget` the `[CONTEXT: …]` line becomes a readout, `~52k/64k tokens in use`, and offers the tool
|
|
896
|
+
in tiers, never "forget now": the readout alone in the lower guided buckets; "finish the unit of work in flight,
|
|
897
|
+
then tidy once" in the bucket under the top; "compact settled outputs now: keep what you'll still edit against;
|
|
898
|
+
don't wipe" in the top bucket or over `llm_context.budget_tokens`. They come at a turn's first request (the turn
|
|
899
|
+
before it answered); mid-turn only the top tier offers. Over the budget the compact offer comes every turn.
|
|
900
|
+
- `/stats` counts the re-prefilled tokens each applied batch costs; `script/llm_context_bench.rb --strategy
|
|
901
|
+
forget_outputs` replays the offer on stored sessions (docs/internals/llm-context-bench.md).
|
|
902
|
+
|
|
858
903
|
## All settings
|
|
859
904
|
|
|
860
905
|
Every setting below takes the three forms described in
|
|
@@ -917,12 +962,15 @@ described in their own sections.
|
|
|
917
962
|
| `context.status` | `true` | yes | Context-usage telemetry for the model. See [context telemetry](internals/context-telemetry.md). |
|
|
918
963
|
| `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". |
|
|
919
964
|
| `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`
|
|
921
|
-
| `models.<key>.llm_context_strategy`, `hosts.<name>.llm_context_strategy` | none | | A model's or host's `llm_context.strategy
|
|
965
|
+
| `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` (experimental) offers the model the `forget_outputs` tool; see "LLM context: the forget layer". An unknown name warns and is `none`. `models.<key>.llm_context_strategy` and `hosts.<name>.llm_context_strategy` come first. |
|
|
966
|
+
| `models.<key>.llm_context_strategy`, `hosts.<name>.llm_context_strategy` | none | | A model's or host's `llm_context.strategy`. |
|
|
922
967
|
| `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
968
|
| `models.<key>.llm_context_apply`, `hosts.<name>.llm_context_apply` | none | | A model's or host's `llm_context.apply`. |
|
|
924
969
|
| `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. |
|
|
970
|
+
| `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. Under `forget`, `forget_outputs` refuses an output from the last N steps, and a read of a file edited or written in them unless the forget keeps some of its lines. |
|
|
971
|
+
| `llm_context.budget_tokens` | none | | A soft context budget in tokens (e.g. `64000`); off when unset or `0`. When set, the context status buckets (`context.status_thresholds`) count against it instead of the window (the smaller of the two), so the `[CONTEXT: …]` lines, `payoff`'s top bucket and, under `forget`, the `forget_outputs` offers come under it. `models.<key>.llm_context_budget_tokens` and `hosts.<name>.llm_context_budget_tokens` come first. |
|
|
972
|
+
| `models.<key>.llm_context_budget_tokens`, `hosts.<name>.llm_context_budget_tokens` | none | | A model's or host's `llm_context.budget_tokens`. |
|
|
973
|
+
| `llm_context.policy` | the subtask-boundaries line | | The sentence `forget_outputs`' description carries (the `forget` layer): "Tidy at subtask boundaries: once a subtask is done, forget its tool outputs and note what it established; keep anything you'll still edit against." Blank: none. Read when a session starts. |
|
|
926
974
|
| `context.chars_per_token` | `4.0` | yes | Estimate ratio when the server reports no usage. |
|
|
927
975
|
| `context.status_thresholds` | `20,40,60,80` | yes | Percentages that trigger a status. |
|
|
928
976
|
| `context.status_cadence` | `0` | yes | Also every N rounds; `0` = thresholds only. |
|
data/docs/hooks.md
CHANGED
|
@@ -440,6 +440,7 @@ stripped of surrounding whitespace, except file text, an edit's
|
|
|
440
440
|
| `delegate` | `task` | — | `model`, `session`, `cwd`, `wait`, `timeout` |
|
|
441
441
|
| `delegate_result` | — | — | `session`, `timeout` |
|
|
442
442
|
| `ask_user_question` | `question` | — | `question`, `options`, `header`, `multi_select`, `allow_freeform` |
|
|
443
|
+
| `forget_outputs` | `note` | — | `ids`, `note`, `keep`, `restore` |
|
|
443
444
|
|
|
444
445
|
A `write`'s `content:` is the file text; an `edit` carries `old_text:` and
|
|
445
446
|
`new_text:` (its `content:` is `""`). A plugin or MCP tool's call has its
|
|
@@ -31,6 +31,22 @@ Emission behavior:
|
|
|
31
31
|
`context.status_thresholds` the top bucket reads critical, the one below it
|
|
32
32
|
elevated, the next moderate, and the first bucket (and the rest below) healthy.
|
|
33
33
|
|
|
34
|
+
Under an LLM context strategy (`ContextStatus.new(llm_context:)`, the turn's `LLMContextStrategy::Resolved`, given by
|
|
35
|
+
both loops):
|
|
36
|
+
|
|
37
|
+
- `llm_context.budget_tokens` (or a model's or host's `llm_context_budget_tokens`): the buckets count against the
|
|
38
|
+
budget instead of the window, the smaller of the two. The event's `window_tokens` and the status line's percentage
|
|
39
|
+
are of it too, and so is `payoff`'s top bucket.
|
|
40
|
+
- The forget layer replaces the guidance with tiered offers of `forget_outputs`, each line a readout,
|
|
41
|
+
`[CONTEXT: ~52k/64k tokens in use (bucket=60plus). …]`: the readout alone in the guided buckets below the top two
|
|
42
|
+
(CLM: how-to at low pressure makes a model wipe everything); "Finish the unit of work in flight, then tidy once
|
|
43
|
+
with forget_outputs: …" in the one under the top; "Compact settled outputs now with forget_outputs: keep what
|
|
44
|
+
you'll still edit against; don't wipe." in the top bucket, or over the budget (`observe`'s absolute check). They
|
|
45
|
+
come at a turn's first request (`iteration_index` 0: the turn before answered, its outputs are settled) on a rise,
|
|
46
|
+
again when the conversation's last line was the readout alone, and every turn while over the budget; mid-turn a
|
|
47
|
+
rise offers only in the top tier, a lower one gets the readout alone (the layout check: pushed mid-task, models
|
|
48
|
+
forget at the base rate). See internals/llm-context-forget.md.
|
|
49
|
+
|
|
34
50
|
Configuration:
|
|
35
51
|
|
|
36
52
|
- `SAMAGOTCHI_CONTEXT_STATUS` (`true` by default): set to `false` or `0` to disable telemetry.
|
|
@@ -54,7 +54,9 @@ The proxies are the context-edit spike's (lexical and lenient: a basename mentio
|
|
|
54
54
|
|
|
55
55
|
- `none`: nothing changes. The base.
|
|
56
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.
|
|
57
|
+
what a model told "free context now" does. Its stubs, and the picks', go through the view as chi sends forgets: a
|
|
58
|
+
call's note on the first of its outputs in a row and a pointer to it on the rest, and `[#tN]` ids on every output
|
|
59
|
+
with a stored id (P4).
|
|
58
60
|
- `stale`: chi's own stale layer (`Samagotchi::LLMContextStale`) as chi runs it: each read a later read covering its
|
|
59
61
|
lines superseded is stubbed from the request after that read (chi applies it at the next request), so a case's
|
|
60
62
|
numbers count every stub since the session began. An edit or write of the file supersedes nothing, nor does a read
|
|
@@ -67,13 +69,25 @@ The proxies are the context-edit spike's (lexical and lenient: a basename mentio
|
|
|
67
69
|
modelled (the bench has no window), so `payoff` applies at a request only when the freed tokens are at least the
|
|
68
70
|
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
71
|
prefill it while the user reads.
|
|
70
|
-
- `forget_outputs` (P4):
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
- `forget_outputs` (P4): chi's forget layer as chi offers it, at each case: the next turn's first request, after a
|
|
73
|
+
turn the model answered. The request is what chi sends there under `[stale, forget]`, built by chi's own code: the
|
|
74
|
+
conversation up to and with the next user message (outputs of a legacy entry get ids as chi gives new ones),
|
|
75
|
+
stale's stubs, every output led by its `[#tN]` id (`LLMContextView`), the `[CONTEXT: …]` line chi's `ContextStatus`
|
|
76
|
+
adds at a turn's start under `--budget N` (default 64000; no line below its guided buckets, so a small case may get
|
|
77
|
+
none), and the chat messages and tools of chi's `ChatLoop`, `forget_outputs` with its real description. The model
|
|
78
|
+
(`--forget-model MODEL`, a chi model ref on an `api: openai` host) answers once; a `forget_outputs` call among its
|
|
79
|
+
calls runs through chi's `LLMContextForget` (refusals, `protect_steps`, `keep`), and the row scores stale's stubs
|
|
80
|
+
plus the forgets. Each answer is saved in `--out DIR` (`<case>.forget_offer.json`) and read back, so a rerun asks
|
|
81
|
+
only for the rest (an error isn't saved); `--out` alone scores saved answers; `--max-cost USD` stops the asking
|
|
82
|
+
once the costs the server reports reach it (exit 1, the answers so far kept). The row's notes give the call rate
|
|
83
|
+
(`called`, `no_call`, `error`, `unasked`), the ids chi refused and the notes' mean length. Ids in this row cost
|
|
84
|
+
tokens too: its freed column is net of them.
|
|
73
85
|
- `--picks LABEL=DIR`: model picks recorded as responses, one file per pick, `<case>.<variant>.json` (the spike's
|
|
74
86
|
`out/<model>/` files, or what `--live` saves). The variant is the row's policy, the label its model.
|
|
75
87
|
|
|
76
88
|
```sh
|
|
89
|
+
ruby script/llm_context_bench.rb "$SESSIONS" --strategy none,stale,forget_outputs --forget-model splash:MODEL \
|
|
90
|
+
--out answers/splash --budget 48000 --cases cases.txt --per-case
|
|
77
91
|
ruby script/llm_context_bench.rb "$SESSIONS" --strategy none,forget_all,stale --per-case
|
|
78
92
|
ruby script/llm_context_bench.rb "$SESSIONS" --strategy none,stale,stale_edits_turn_end,stale_edits_payoff --ends any
|
|
79
93
|
ruby script/llm_context_bench.rb "$SESSIONS" --picks deepseek=picks/deepseek --picks splash=picks/splash --json
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# The forget layer: forget_outputs
|
|
2
|
+
|
|
3
|
+
`llm_context.strategy` with `forget` (experimental; user docs: configuration.md, "LLM context: the forget layer")
|
|
4
|
+
lets the model forget its own tool outputs. The pieces:
|
|
5
|
+
|
|
6
|
+
- **The tool list is per turn.** `Tools::Registry` entries may name an LLM context layer (`layer: :forget`);
|
|
7
|
+
`#entries/#names/#schemas(layers:)` and `#offered(name, layers:)` leave a gated tool out unless the turn runs under
|
|
8
|
+
its layer. The turn's layers are `LLMContextStrategy::Resolved#active_layers` (`[]` under none): the native system
|
|
9
|
+
prompt declares them (`SystemPrompt#build(layers:)`, `Engine#system_prompt` resolving the target's strategy), the
|
|
10
|
+
chat loop sends them (`ChatLoop#tool_definitions`), the kernel dispatches and lists them (`KernelLoop#dispatch`).
|
|
11
|
+
Under none and `[stale]` the tool list, the system prompt and the wire requests are what they were. A model switch
|
|
12
|
+
that changes the layers changes the tool list; it breaks the prompt cache anyway.
|
|
13
|
+
- **Ids** (`LLMContextView`, plan D2): with `:forget` among the view's layers every output with a stored id
|
|
14
|
+
(`ToolIds`, `tool_ids` on the entry) is sent as `[name]` lead, then `[#tN] `, then its text; derived ids of legacy
|
|
15
|
+
entries are never shown or forgettable.
|
|
16
|
+
- **The schema** is `ToolDeclarations.forget_outputs_schema(policy:)` (not in `TOOL_SCHEMAS`, which every turn
|
|
17
|
+
declares; `BUILTIN_SCHEMAS` adds it for `BuiltinCalls`). Its description (about 330 tokens by chars/4, 515 with the
|
|
18
|
+
parameters) carries the spike's refined text, `llm_context.policy` (read when the Engine builds its registry), the
|
|
19
|
+
note contract and the cost lesson. Parameters: `ids`, `note`, `keep` (`"t42:12-40"`: forget t42 except
|
|
20
|
+
those lines; a read's are file lines, kept with the edit's `keep_offset`, start_line - 1), `restore`, none required
|
|
21
|
+
(a restore needs no note); `Tools::ForgetOutputs.parse` reads ids and ranges from lists, JSON strings or words.
|
|
22
|
+
- **The call** (`KernelLoop#forget_outputs`, through the handler's kctx as ask_user_question does) runs
|
|
23
|
+
`LLMContextForget.call` on the running turn (`KernelLoop#llm_context_turn`, a `LLMContextForget::Turn` of the
|
|
24
|
+
turn's conversation and ContextStatus that both loops set as a turn starts). It names only outputs that pair with
|
|
25
|
+
their calls for sure (`LLMContextStale.paired_runs`: by tool_call_id, or a joined entry's runs by their calls'
|
|
26
|
+
`[name]` leads); the view shows ids only on those. The conversation's last step is the forget's own (both loops
|
|
27
|
+
add the model's entry before its calls run). Per id it refuses (with the reason in the result): an output of the
|
|
28
|
+
`protect_steps` steps before that one; a read `LLMContextStale.protected_ids` names (a file edited or written in
|
|
29
|
+
those steps, counted with the forget's own: `protect_steps + 1`) unless the forget keeps lines of it; a keep
|
|
30
|
+
outside the output's lines, of a preview, or holding all of it; an output of `SMALL` (80) chars or less; an output
|
|
31
|
+
with an edit already; an unknown id. What passes is saved as a staged `LLMContextEdit` of kind `:forget` (`by:
|
|
32
|
+
"model"`, the note, `keep`), and the apply rule runs at once as at a request (`KernelLoop#apply_llm_context!`), so
|
|
33
|
+
the result can say whether the batch goes with the next request or waits for the turn's end. `LLMContextApply`'s
|
|
34
|
+
protect_steps hold covers stale's stubs only: a forget was checked when it was made.
|
|
35
|
+
- **Restore** takes the forget off the entry (`LLMContextEdit.remove`, a new edits Hash); the next request sends the
|
|
36
|
+
output again, outside the apply rule (the result says the tail the server reads again, and `ContextStatus#edited!`
|
|
37
|
+
restarts the estimate). A read whose stub was sent isn't restored; a staged one is.
|
|
38
|
+
- **Stubs** (`LLMContextView`): `(forgotten) <note>` on the first output of a call's forgets in a row, `(forgotten
|
|
39
|
+
with tN: see its note)` on the rest (a run of the same note, author and stamp, no other output between them),
|
|
40
|
+
` [restore: tN]` on a non-read with a stored id, then each kept range under `lines A-B kept:`. The entry keeps its
|
|
41
|
+
role, tool_call_id and images bookkeeping as for stale.
|
|
42
|
+
- **Offers**: `ContextStatus` (internals/context-telemetry.md).
|
|
43
|
+
- **Persistence**: the edits ride on the entries (plan D3), so `--resume`, rollbacks and forks keep the stubs and the
|
|
44
|
+
restores; `/stats` counts each applied batch's re-prefill.
|
|
45
|
+
- **Measuring**: the bench's `forget_outputs` strategy (internals/llm-context-bench.md).
|
data/docs/plugins.md
CHANGED
|
@@ -978,7 +978,7 @@ bundles:
|
|
|
978
978
|
loop-guard:
|
|
979
979
|
deny_after: 2 # same call, same result this many times: deny the next one
|
|
980
980
|
stop_after: 4 # stop the turn at this many denies
|
|
981
|
-
ignore_tools: [task_wait, task_get, delegate_result, list_sessions, list_reminders, context_read]
|
|
981
|
+
ignore_tools: [task_wait, task_get, delegate_result, list_sessions, list_reminders, context_read, forget_outputs]
|
|
982
982
|
mode: deny # deny | notify: notify only warns, once per call per turn
|
|
983
983
|
```
|
|
984
984
|
|
data/docs/testing.md
CHANGED
|
@@ -67,6 +67,7 @@ a model that follows tool instructions: a small model may fail some of them.
|
|
|
67
67
|
## The llm_context replay benchmark
|
|
68
68
|
|
|
69
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
|
|
70
|
+
strategies (freed tokens, wrongly forgotten outputs, one-step re-reads, re-prefilled tokens; for `forget_outputs` a
|
|
71
|
+
model's answers to chi's offer, with its call rate and notes). It is opt-in: the suite
|
|
71
72
|
runs its code only on small synthetic sessions, never on yours. See
|
|
72
73
|
[internals/llm-context-bench.md](internals/llm-context-bench.md).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loop-guard
|
|
3
|
-
version: 0.3.
|
|
3
|
+
version: 0.3.6
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Breaks loops: the same tool call with the same result again and again in one turn is denied with advice (after a few denies the turn stops with a card), and thinking that repeats itself while it streams is cut and the model asked again"
|
|
6
6
|
trust_level: reviewed
|
|
7
7
|
plugin:
|
|
8
8
|
file: plugin.rb
|
|
9
|
-
sha256: sha256:
|
|
9
|
+
sha256: sha256:ab71b6f2aee6ad15bbe494cf0e8943046ee94dbf5d56149bd507725e8a9790fe
|
|
10
10
|
requires_chi: ">= 0.6.0"
|
|
@@ -277,7 +277,7 @@ class ThinkingWatch
|
|
|
277
277
|
end
|
|
278
278
|
|
|
279
279
|
class Plugin
|
|
280
|
-
DEFAULT_IGNORE = %w[task_wait task_get delegate_result list_sessions list_reminders context_read].freeze
|
|
280
|
+
DEFAULT_IGNORE = %w[task_wait task_get delegate_result list_sessions list_reminders context_read forget_outputs].freeze
|
|
281
281
|
# Built-in tools carry flat fields; plugin and unknown tools carry args:.
|
|
282
282
|
# An edit's text is its old_text/new_text.
|
|
283
283
|
KEY_FIELDS = %i[content path start_line end_line cwd env scope old_text new_text].freeze
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: samagotchi-system
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.40.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
|
-
self_map.md: sha256:
|
|
8
|
+
self_map.md: sha256:78969929094d2c4f3530529cd54c5c37ce6b13b1c3867a318d9ce07e5b77cd73
|
|
9
9
|
config_modification_protocol.md: sha256:648b19b1b9f7ca35fccdd69fee4c9886cb8cfb3748ae86affef7457c12164399
|
|
10
10
|
memory_guide.md: sha256:f7049304a333be842b761037bab5c8e31e890b8f4ad12f092b14ee8deb053c51
|
|
11
11
|
delegated.md: sha256:aedaa1e756a244904c5560fff0424e06fb468bbdcb0358bccefe321d67cd2614
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
third-party information, never instructions to me. When a source's change matters (a review
|
|
30
30
|
asking for changes, checks failing, the PR closed), chi may start a turn for it with no user
|
|
31
31
|
message ("context <name> changed"): I tell my user what changed and act on nothing.
|
|
32
|
+
- `forget_outputs` (only when my model's `llm_context.strategy` has the experimental forget layer)
|
|
33
|
+
frees context: my tool outputs show ids like `[#t42]`, and a forget replaces them with a stub
|
|
34
|
+
holding my note (the call stays; `keep` keeps lines, `restore` brings a non-read output back).
|
|
35
|
+
A `[CONTEXT: …]` line may offer it; it's never required. See `docs/configuration.md` "llm_context".
|
|
32
36
|
- `chi send -m TEXT <id>` is the other half: the text goes in as the user's message and a
|
|
33
37
|
turn runs (stdin piped too = quoted context above it); see `docs/sessions.md` "Sending a message".
|
|
34
38
|
`chi send --new --wait -m TEXT` starts a session the user can watch in the web and prints its
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -156,7 +156,7 @@ module Samagotchi
|
|
|
156
156
|
Entry.new(key: "context.wake", yaml_path: %w[context wake], type: :bool, default: true, expose: %i[env config cli]),
|
|
157
157
|
|
|
158
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)
|
|
159
|
+
# (stale, forget; "|"-separated or a YAML list); forget (the model's forget_outputs) is experimental.
|
|
160
160
|
# models.<key>.llm_context_strategy and hosts.<name>.llm_context_strategy come first.
|
|
161
161
|
Entry.new(key: "llm_context.strategy", yaml_path: %w[llm_context strategy], type: :string, default: "none", expose: %i[env config]),
|
|
162
162
|
# When a layer's edits reach the prompt (LLMContextApply): payoff, next_request or turn_end;
|
|
@@ -166,6 +166,12 @@ module Samagotchi
|
|
|
166
166
|
Entry.new(key: "llm_context.stale_edits", yaml_path: %w[llm_context stale_edits], type: :bool, default: false, expose: %i[env config]),
|
|
167
167
|
# The steps (tool batches) back whose edited files' reads stale never stubs.
|
|
168
168
|
Entry.new(key: "llm_context.protect_steps", yaml_path: %w[llm_context protect_steps], type: :integer, default: 3, expose: %i[env config]),
|
|
169
|
+
# A soft context budget in tokens: the context status bands (and the forget layer's offers) count against it
|
|
170
|
+
# instead of the window; unset or 0: off. models.<key>.llm_context_budget_tokens and
|
|
171
|
+
# hosts.<name>.llm_context_budget_tokens come first.
|
|
172
|
+
Entry.new(key: "llm_context.budget_tokens", yaml_path: %w[llm_context budget_tokens], type: :integer, default: nil, expose: %i[env config]),
|
|
173
|
+
# The policy sentence forget_outputs' description carries (the forget layer); blank: none.
|
|
174
|
+
Entry.new(key: "llm_context.policy", yaml_path: %w[llm_context policy], type: :string, default: LLMContextStrategy::DEFAULT_POLICY, expose: %i[env config]),
|
|
169
175
|
|
|
170
176
|
Entry.new(key: "thinking.turn_preamble", yaml_path: %w[thinking turn_preamble], type: :bool, default: true, expose: %i[env config cli]),
|
|
171
177
|
# How much models think: off|low|medium|high|default, parsed by Thinking
|
|
@@ -254,8 +260,9 @@ module Samagotchi
|
|
|
254
260
|
# (ConfigFile.hosts_config, ConfigFile.model_settings).
|
|
255
261
|
MAP_ENTRY_KEYS = {
|
|
256
262
|
"hosts" => %w[host port url transport api api_key_env profile first_token_timeout vision sampling thinking enabled
|
|
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
|
|
263
|
+
remote window_tokens llm_context_strategy llm_context_apply llm_context_budget_tokens].freeze,
|
|
264
|
+
"models" => %w[profile vision sampling thinking window_tokens llm_context_strategy llm_context_apply
|
|
265
|
+
llm_context_budget_tokens].freeze
|
|
259
266
|
}.freeze
|
|
260
267
|
# Section keys beyond the registry's: guardrails' YAML rules (Engine#guardrail_rules).
|
|
261
268
|
SECTION_EXTRA_KEYS = { "guardrails" => %w[rules disable].freeze }.freeze
|
|
@@ -843,6 +850,8 @@ module Samagotchi
|
|
|
843
850
|
"hosts entry '#{name}'")
|
|
844
851
|
llm_context_apply = LLMContextStrategy.parse_apply(raw_cfg.key?(LLMContextStrategy::APPLY_KEY) ? raw_cfg[LLMContextStrategy::APPLY_KEY] : raw_cfg[:llm_context_apply],
|
|
845
852
|
"hosts entry '#{name}'")
|
|
853
|
+
llm_context_budget = LLMContextStrategy.parse_budget(raw_cfg.key?(LLMContextStrategy::BUDGET_KEY) ? raw_cfg[LLMContextStrategy::BUDGET_KEY] : raw_cfg[:llm_context_budget_tokens],
|
|
854
|
+
"hosts entry '#{name}'")
|
|
846
855
|
unless first_token_timeout.nil? || (first_token_timeout.is_a?(Numeric) && !first_token_timeout.negative?)
|
|
847
856
|
warn_once "Warning: hosts entry '#{name}': first_token_timeout must be seconds (0 = off); using the default"
|
|
848
857
|
first_token_timeout = nil
|
|
@@ -908,7 +917,7 @@ module Samagotchi
|
|
|
908
917
|
profile: profile.empty? ? nil : profile, first_token_timeout: first_token_timeout,
|
|
909
918
|
vision: vision, sampling: sampling, thinking: thinking, remote: remote,
|
|
910
919
|
window_tokens: window, llm_context_strategy: llm_context,
|
|
911
|
-
llm_context_apply: llm_context_apply }
|
|
920
|
+
llm_context_apply: llm_context_apply, llm_context_budget_tokens: llm_context_budget }
|
|
912
921
|
end
|
|
913
922
|
end
|
|
914
923
|
|
|
@@ -1057,7 +1066,8 @@ module Samagotchi
|
|
|
1057
1066
|
"vision" => v[:vision], "sampling" => v[:sampling], "thinking" => v[:thinking]&.to_s,
|
|
1058
1067
|
"remote" => v[:remote], "window_tokens" => v[:window_tokens],
|
|
1059
1068
|
"llm_context_strategy" => v[:llm_context_strategy]&.map(&:to_s),
|
|
1060
|
-
"llm_context_apply" => v[:llm_context_apply]&.to_s
|
|
1069
|
+
"llm_context_apply" => v[:llm_context_apply]&.to_s,
|
|
1070
|
+
"llm_context_budget_tokens" => v[:llm_context_budget_tokens]).compact
|
|
1061
1071
|
end
|
|
1062
1072
|
# Disabled hosts travel as just that, so a worker refuses "box:x"
|
|
1063
1073
|
# the way its parent does instead of sending it to the default host.
|
|
@@ -1155,6 +1165,9 @@ module Samagotchi
|
|
|
1155
1165
|
apply = LLMContextStrategy.parse_apply(v.key?(LLMContextStrategy::APPLY_KEY) ? v[LLMContextStrategy::APPLY_KEY] : v[:llm_context_apply],
|
|
1156
1166
|
"models: #{key}")
|
|
1157
1167
|
result[key][:llm_context_apply] = apply if apply
|
|
1168
|
+
budget = LLMContextStrategy.parse_budget(v.key?(LLMContextStrategy::BUDGET_KEY) ? v[LLMContextStrategy::BUDGET_KEY] : v[:llm_context_budget_tokens],
|
|
1169
|
+
"models: #{key}")
|
|
1170
|
+
result[key][:llm_context_budget_tokens] = budget if budget
|
|
1158
1171
|
end
|
|
1159
1172
|
rescue StandardError
|
|
1160
1173
|
{}
|
|
@@ -19,6 +19,26 @@ module Samagotchi
|
|
|
19
19
|
#
|
|
20
20
|
# The context.* settings are read once, when the tracker is built (one per
|
|
21
21
|
# run, so a changed setting counts from the next turn).
|
|
22
|
+
#
|
|
23
|
+
# The turn's LLM context strategy (LLMContextStrategy::Resolved) may set
|
|
24
|
+
# a soft budget (llm_context.budget_tokens): the buckets then count
|
|
25
|
+
# against it instead of the window (the smaller of the two).
|
|
26
|
+
#
|
|
27
|
+
# Under the forget layer the model's lines offer forget_outputs in
|
|
28
|
+
# tiers, never "forget now" (the spike: told to free context, models
|
|
29
|
+
# forget nearly everything), each with a "~N/M tokens" readout:
|
|
30
|
+
# - the first bucket with guidance, below the top two: the readout alone
|
|
31
|
+
# (CLM: how-to at low pressure triggers wholesale deletion);
|
|
32
|
+
# - the one under the top: finish the unit of work in flight, then tidy
|
|
33
|
+
# once;
|
|
34
|
+
# - the top, or over the budget: compact settled outputs now, keep what
|
|
35
|
+
# will still be edited against, don't wipe.
|
|
36
|
+
# When: at a turn's first request (the turn before it answered, its
|
|
37
|
+
# outputs are settled), on a rise as ever, and again when the last line
|
|
38
|
+
# was the readout alone (a mid-turn rise) or the context is over the
|
|
39
|
+
# budget (every turn then). Mid-turn only the top tier offers; a lower
|
|
40
|
+
# rise gets the readout alone (the layout check: pushed mid-task, models
|
|
41
|
+
# forget at the base rate).
|
|
22
42
|
class ContextStatus
|
|
23
43
|
STATUS_PREFIX = "CONTEXT_STATUS"
|
|
24
44
|
# The model's own line about its context (a tail system message, kind
|
|
@@ -62,19 +82,35 @@ module Samagotchi
|
|
|
62
82
|
# The bucket of the last status line +conversation+ holds: the model's
|
|
63
83
|
# own line, or a legacy session's injected telemetry.
|
|
64
84
|
def self.last_bucket(conversation)
|
|
85
|
+
content = last_line(conversation)
|
|
86
|
+
match = content&.match(/\bbucket=([a-z0-9_]+)/)
|
|
87
|
+
match && match[1]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The text of the last status line +conversation+ holds, nil for none.
|
|
91
|
+
def self.last_line(conversation)
|
|
65
92
|
message = conversation.reverse.find do |entry|
|
|
66
93
|
content = entry[:content].to_s
|
|
67
94
|
entry[:role] == "system" && content.start_with?(STATUS_PREFIX, LINE_PREFIX)
|
|
68
95
|
end
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
match = message[:content].match(/\bbucket=([a-z0-9_]+)/)
|
|
72
|
-
match && match[1]
|
|
96
|
+
message && message[:content].to_s
|
|
73
97
|
end
|
|
74
98
|
|
|
99
|
+
# What a forget-layer line names when it offers the tool.
|
|
100
|
+
OFFER_TOOL = "forget_outputs"
|
|
101
|
+
# The forget layer's tiers' words (#forget_line).
|
|
102
|
+
TIDY = "Finish the unit of work in flight, then tidy once with #{OFFER_TOOL}: settled outputs, with a note " \
|
|
103
|
+
"that carries what they established."
|
|
104
|
+
COMPACT = "Compact settled outputs now with #{OFFER_TOOL}: keep what you'll still edit against; don't wipe."
|
|
105
|
+
|
|
75
106
|
# @param conversation [Array<Hash>] the turn's conversation so far (its
|
|
76
107
|
# last status line's bucket is where rises count from)
|
|
77
|
-
|
|
108
|
+
# @param llm_context [LLMContextStrategy::Resolved, nil] the turn's
|
|
109
|
+
# strategy (its budget)
|
|
110
|
+
def initialize(conversation: [], llm_context: nil)
|
|
111
|
+
@budget = llm_context&.budget_tokens
|
|
112
|
+
@forget = llm_context&.forget? || false
|
|
113
|
+
@offered = self.class.last_line(conversation).to_s.include?(OFFER_TOOL)
|
|
78
114
|
@enabled = self.class.enabled?
|
|
79
115
|
chars_per_token = Config.get("context.chars_per_token").to_f
|
|
80
116
|
@chars_per_token = chars_per_token.positive? ? chars_per_token : DEFAULT_CHARS_PER_TOKEN
|
|
@@ -121,9 +157,14 @@ module Samagotchi
|
|
|
121
157
|
emit = emit?(bucket: bucket, iteration_index: iteration_index)
|
|
122
158
|
previous_bucket = @last_bucket
|
|
123
159
|
@last_bucket = bucket
|
|
160
|
+
line = if @forget
|
|
161
|
+
forget_line(usage, bucket, previous_bucket, iteration_index)
|
|
162
|
+
elsif guidance_due?(previous_bucket, bucket)
|
|
163
|
+
guidance_message(usage: usage, bucket: bucket)
|
|
164
|
+
end
|
|
165
|
+
@guidance = line if line
|
|
124
166
|
return nil unless emit
|
|
125
167
|
|
|
126
|
-
@guidance = guidance_message(usage: usage, bucket: bucket) if guidance_due?(previous_bucket, bucket)
|
|
127
168
|
{ status: status_message(usage: usage, bucket: bucket), usage: usage, bucket: bucket, source: usage[:source] }
|
|
128
169
|
end
|
|
129
170
|
|
|
@@ -171,7 +212,7 @@ module Samagotchi
|
|
|
171
212
|
return nil unless @enabled
|
|
172
213
|
return nil unless ContextWindow.positive_integer?(used_tokens) && ContextWindow.positive_integer?(window_tokens)
|
|
173
214
|
|
|
174
|
-
pct = (used_tokens.to_f / window_tokens) * 100.0
|
|
215
|
+
pct = (used_tokens.to_f / counted_against(window_tokens)) * 100.0
|
|
175
216
|
{ est_pct: pct, bucket: bucket_for(pct) }
|
|
176
217
|
end
|
|
177
218
|
|
|
@@ -187,11 +228,11 @@ module Samagotchi
|
|
|
187
228
|
window_source = :server if server_usage && server_usage[:context_window_tokens]
|
|
188
229
|
|
|
189
230
|
if server_usage && server_usage[:prompt_tokens]
|
|
190
|
-
window_tokens = server_usage[:context_window_tokens] || window.tokens
|
|
231
|
+
window_tokens = counted_against(server_usage[:context_window_tokens] || window.tokens)
|
|
191
232
|
used_tokens = [server_usage[:prompt_tokens] + appended_tokens(prompt_chars, image_tokens, counted), 0].max
|
|
192
233
|
source = "server"
|
|
193
234
|
else
|
|
194
|
-
window_tokens = window.tokens
|
|
235
|
+
window_tokens = counted_against(window.tokens)
|
|
195
236
|
used_tokens = (prompt_chars / @chars_per_token).ceil + image_tokens
|
|
196
237
|
source = "estimate"
|
|
197
238
|
end
|
|
@@ -208,6 +249,12 @@ module Samagotchi
|
|
|
208
249
|
|
|
209
250
|
private
|
|
210
251
|
|
|
252
|
+
# What the buckets count against: the window, or the budget when one is
|
|
253
|
+
# set and smaller.
|
|
254
|
+
def counted_against(window_tokens)
|
|
255
|
+
@budget ? [@budget, window_tokens].min : window_tokens
|
|
256
|
+
end
|
|
257
|
+
|
|
211
258
|
# The estimate for what the prompt added since the +counted+ one;
|
|
212
259
|
# after an edit (#edited!) what it lost too, as a negative.
|
|
213
260
|
def appended_tokens(prompt_chars, image_tokens, counted)
|
|
@@ -248,6 +295,42 @@ module Samagotchi
|
|
|
248
295
|
bucket_changed || cadence_due
|
|
249
296
|
end
|
|
250
297
|
|
|
298
|
+
# The forget layer's line for this request, or nil (the tiers and when
|
|
299
|
+
# they come: the class comment).
|
|
300
|
+
def forget_line(usage, bucket, previous, iteration_index)
|
|
301
|
+
over = over_budget?(usage)
|
|
302
|
+
tier = over ? :compact : tier_for(bucket)
|
|
303
|
+
return nil unless tier
|
|
304
|
+
|
|
305
|
+
rise = guidance_due?(previous, bucket)
|
|
306
|
+
if iteration_index.zero?
|
|
307
|
+
return nil unless rise || over || (tier != :info && !@offered)
|
|
308
|
+
else
|
|
309
|
+
return nil unless rise
|
|
310
|
+
|
|
311
|
+
tier = :info unless tier == :compact
|
|
312
|
+
end
|
|
313
|
+
@offered = tier != :info
|
|
314
|
+
readout = "~#{k(usage[:estimated_used_tokens])}/#{k(usage[:window_tokens])} tokens in use"
|
|
315
|
+
readout += ", over the budget" if over
|
|
316
|
+
words = { info: nil, tidy: TIDY, compact: COMPACT }.fetch(tier)
|
|
317
|
+
{ role: "system", kind: LINE_KIND, content: "#{LINE_PREFIX}#{readout} (bucket=#{bucket}).#{" #{words}" if words}]" }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# :info, :tidy or :compact for a bucket with guidance (the top one
|
|
321
|
+
# compacts, the one under it tidies, lower ones inform); nil below.
|
|
322
|
+
def tier_for(bucket)
|
|
323
|
+
rank = bucket_rank(bucket)
|
|
324
|
+
return nil if rank < GUIDANCE_FROM_RANK
|
|
325
|
+
return :compact if rank == @thresholds.size
|
|
326
|
+
|
|
327
|
+
rank == @thresholds.size - 1 ? :tidy : :info
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def over_budget?(usage) = !@budget.nil? && usage[:estimated_used_tokens] >= @budget
|
|
331
|
+
|
|
332
|
+
def k(tokens) = tokens >= 1000 ? "#{(tokens / 1000.0).round}k" : tokens.to_s
|
|
333
|
+
|
|
251
334
|
def guidance_message(usage:, bucket:)
|
|
252
335
|
how = usage[:source].to_s == "server" ? "as the server reports" : "estimated"
|
|
253
336
|
{ role: "system", kind: LINE_KIND,
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -176,7 +176,8 @@ module Samagotchi
|
|
|
176
176
|
# The tools this session offers (the prompts' declarations and the
|
|
177
177
|
# kernel's dispatch): the built-ins, per Engine. execute.description
|
|
178
178
|
# off: execute offers no description parameter.
|
|
179
|
-
@tools = Tools::Builtins.registry(command_description: Config.get("execute.description") != false
|
|
179
|
+
@tools = Tools::Builtins.registry(command_description: Config.get("execute.description") != false,
|
|
180
|
+
policy: Config.get(LLMContextStrategy::POLICY_SETTING))
|
|
180
181
|
if @scratch
|
|
181
182
|
# A scratch session's children would outlive it, and so would a
|
|
182
183
|
# note it left in a peer; its memories would too (write and edit
|
|
@@ -930,11 +931,13 @@ module Samagotchi
|
|
|
930
931
|
|
|
931
932
|
# The status line's ctx from the saved context, for a worker that has run
|
|
932
933
|
# no turn yet (it woke after an idle exit): nil when either count is
|
|
933
|
-
# unknown or context.status is off.
|
|
934
|
+
# unknown or context.status is off. Counted as the turns count it:
|
|
935
|
+
# against the effective model's llm_context budget when it has one.
|
|
934
936
|
def saved_context_status(context)
|
|
935
937
|
return nil unless context
|
|
936
938
|
|
|
937
|
-
ContextStatus.new
|
|
939
|
+
ContextStatus.new(llm_context: turn_llm_context)
|
|
940
|
+
.display_for(used_tokens: context[:used_tokens], window_tokens: context[:window_tokens])
|
|
938
941
|
end
|
|
939
942
|
|
|
940
943
|
# The effective model is on a chat host (api: openai), whose loop uses
|
|
@@ -1410,9 +1413,19 @@ module Samagotchi
|
|
|
1410
1413
|
target ||= @host_registry.resolve(@effective_model_name)
|
|
1411
1414
|
chat = target.entry.chat?
|
|
1412
1415
|
level = chat ? nil : thinking_level(target)
|
|
1413
|
-
@prompt_builder.build(chat: chat, thinking: level)
|
|
1416
|
+
@prompt_builder.build(chat: chat, thinking: level, layers: llm_context_layers(target))
|
|
1414
1417
|
end
|
|
1415
1418
|
|
|
1419
|
+
# The LLM context layers +target+'s turns run under: the native prompt
|
|
1420
|
+
# declares the tools they add (forget_outputs under forget), so a
|
|
1421
|
+
# model switch may change the tool list. [] under none.
|
|
1422
|
+
def llm_context_layers(target)
|
|
1423
|
+
LLMContextStrategy.resolve(target, names: model_lookup_names(target)).active_layers
|
|
1424
|
+
rescue StandardError
|
|
1425
|
+
[]
|
|
1426
|
+
end
|
|
1427
|
+
private :llm_context_layers
|
|
1428
|
+
|
|
1416
1429
|
# The base prompt (specs, plugins' declarations).
|
|
1417
1430
|
def assist_system_prompt(chat: false, thinking: nil)
|
|
1418
1431
|
@prompt_builder.base(chat: chat, thinking: thinking)
|
|
@@ -23,7 +23,7 @@ module Samagotchi
|
|
|
23
23
|
PATH_TOOLS = %w[write edit read memory_write].freeze
|
|
24
24
|
|
|
25
25
|
# chi's own tools, by name, whether this session has them or not.
|
|
26
|
-
CORE_TOOLS = ToolDeclarations::
|
|
26
|
+
CORE_TOOLS = ToolDeclarations::BUILTIN_SCHEMAS.map { |schema| schema[:name] }.freeze
|
|
27
27
|
|
|
28
28
|
attr_reader :tool, :command, :paths, :cwd, :repo_root
|
|
29
29
|
|